tabbrowser.js (341534B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 { 7 // start private scope for Tabbrowser 8 /** 9 * A set of known icons to use for internal pages. These are hardcoded so we can 10 * start loading them faster than FaviconLoader would normally find them. 11 */ 12 const FAVICON_DEFAULTS = { 13 "about:newtab": "chrome://branding/content/icon32.png", 14 "about:home": "chrome://branding/content/icon32.png", 15 "about:welcome": "chrome://branding/content/icon32.png", 16 "about:privatebrowsing": 17 "chrome://browser/skin/privatebrowsing/favicon.svg", 18 }; 19 20 const { 21 LOAD_FLAGS_NONE, 22 LOAD_FLAGS_FROM_EXTERNAL, 23 LOAD_FLAGS_FIRST_LOAD, 24 LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL, 25 LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, 26 LOAD_FLAGS_FIXUP_SCHEME_TYPOS, 27 LOAD_FLAGS_FORCE_ALLOW_DATA_URI, 28 LOAD_FLAGS_DISABLE_TRR, 29 } = Ci.nsIWebNavigation; 30 31 const DIRECTION_FORWARD = 1; 32 const DIRECTION_BACKWARD = -1; 33 34 /** 35 * Updates the User Context UI indicators if the browser is in a non-default context 36 */ 37 function updateUserContextUIIndicator() { 38 function replaceContainerClass(classType, element, value) { 39 let prefix = "identity-" + classType + "-"; 40 if (value && element.classList.contains(prefix + value)) { 41 return; 42 } 43 for (let className of element.classList) { 44 if (className.startsWith(prefix)) { 45 element.classList.remove(className); 46 } 47 } 48 if (value) { 49 element.classList.add(prefix + value); 50 } 51 } 52 53 let hbox = document.getElementById("userContext-icons"); 54 55 let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid"); 56 if (!userContextId) { 57 replaceContainerClass("color", hbox, ""); 58 hbox.hidden = true; 59 return; 60 } 61 62 let identity = 63 ContextualIdentityService.getPublicIdentityFromId(userContextId); 64 if (!identity) { 65 replaceContainerClass("color", hbox, ""); 66 hbox.hidden = true; 67 return; 68 } 69 70 replaceContainerClass("color", hbox, identity.color); 71 72 let label = ContextualIdentityService.getUserContextLabel(userContextId); 73 document.getElementById("userContext-label").textContent = label; 74 // Also set the container label as the tooltip so we can only show the icon 75 // in small windows. 76 hbox.setAttribute("tooltiptext", label); 77 78 let indicator = document.getElementById("userContext-indicator"); 79 replaceContainerClass("icon", indicator, identity.icon); 80 81 hbox.hidden = false; 82 } 83 84 async function getTotalMemoryUsage() { 85 const procInfo = await ChromeUtils.requestProcInfo(); 86 let totalMemoryUsage = procInfo.memory; 87 for (const child of procInfo.children) { 88 totalMemoryUsage += child.memory; 89 } 90 return totalMemoryUsage; 91 } 92 93 window.Tabbrowser = class { 94 init() { 95 this.tabContainer = document.getElementById("tabbrowser-tabs"); 96 this.tabGroupMenu = document.getElementById("tab-group-editor"); 97 this.tabNoteMenu = document.getElementById("tab-note-menu"); 98 this.tabbox = document.getElementById("tabbrowser-tabbox"); 99 this.tabpanels = document.getElementById("tabbrowser-tabpanels"); 100 this.pinnedTabsContainer = document.getElementById( 101 "pinned-tabs-container" 102 ); 103 this.splitViewCommandSet = document.getElementById("splitViewCommands"); 104 105 ChromeUtils.defineESModuleGetters(this, { 106 AsyncTabSwitcher: 107 "moz-src:///browser/components/tabbrowser/AsyncTabSwitcher.sys.mjs", 108 PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs", 109 // SmartTabGrouping.sys.mjs is missing. tor-browser#44045. 110 // Unused in this context. See mozilla bug 1981785. 111 TabMetrics: 112 "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs", 113 TabStateFlusher: 114 "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", 115 TaskbarTabsUtils: 116 "resource:///modules/taskbartabs/TaskbarTabsUtils.sys.mjs", 117 TaskbarTabs: "resource:///modules/taskbartabs/TaskbarTabs.sys.mjs", 118 UrlbarProviderOpenTabs: 119 "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs", 120 FaviconUtils: "moz-src:///browser/modules/FaviconUtils.sys.mjs", 121 }); 122 ChromeUtils.defineLazyGetter(this, "tabLocalization", () => { 123 return new Localization( 124 [ 125 "browser/tabbrowser.ftl", 126 "browser/taskbartabs.ftl", 127 "branding/brand.ftl", 128 ], 129 true 130 ); 131 }); 132 XPCOMUtils.defineLazyPreferenceGetter( 133 this, 134 "_shouldExposeContentTitle", 135 "privacy.exposeContentTitleInWindow", 136 true 137 ); 138 XPCOMUtils.defineLazyPreferenceGetter( 139 this, 140 "_shouldExposeContentTitlePbm", 141 "privacy.exposeContentTitleInWindow.pbm", 142 true 143 ); 144 XPCOMUtils.defineLazyPreferenceGetter( 145 this, 146 "_showTabCardPreview", 147 "browser.tabs.hoverPreview.enabled", 148 true 149 ); 150 XPCOMUtils.defineLazyPreferenceGetter( 151 this, 152 "_allowTransparentBrowser", 153 "browser.tabs.allow_transparent_browser", 154 false 155 ); 156 XPCOMUtils.defineLazyPreferenceGetter( 157 this, 158 "_tabGroupsEnabled", 159 "browser.tabs.groups.enabled", 160 false 161 ); 162 XPCOMUtils.defineLazyPreferenceGetter( 163 this, 164 "_tabNotesEnabled", 165 "browser.tabs.notes.enabled", 166 false 167 ); 168 XPCOMUtils.defineLazyPreferenceGetter( 169 this, 170 "showPidAndActiveness", 171 "browser.tabs.tooltipsShowPidAndActiveness", 172 false 173 ); 174 XPCOMUtils.defineLazyPreferenceGetter( 175 this, 176 "_unloadTabInContextMenu", 177 "browser.tabs.unloadTabInContextMenu", 178 false 179 ); 180 XPCOMUtils.defineLazyPreferenceGetter( 181 this, 182 "_notificationEnableDelay", 183 "security.notification_enable_delay", 184 500 185 ); 186 XPCOMUtils.defineLazyPreferenceGetter( 187 this, 188 "_remoteSVGIconDecoding", 189 "browser.tabs.remoteSVGIconDecoding", 190 false 191 ); 192 193 if (AppConstants.MOZ_CRASHREPORTER) { 194 ChromeUtils.defineESModuleGetters(this, { 195 TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs", 196 }); 197 } 198 199 Services.obs.addObserver(this, "contextual-identity-updated"); 200 Services.obs.addObserver(this, "intl:app-locales-changed"); 201 202 document.addEventListener("keydown", this, { mozSystemGroup: true }); 203 document.addEventListener("keypress", this, { mozSystemGroup: true }); 204 document.addEventListener("visibilitychange", this); 205 window.addEventListener("framefocusrequested", this); 206 window.addEventListener("activate", this); 207 window.addEventListener("deactivate", this); 208 window.addEventListener("TabGroupCollapse", this); 209 window.addEventListener("TabGroupCreateByUser", this); 210 window.addEventListener("TabGrouped", this); 211 window.addEventListener("TabUngrouped", this); 212 window.addEventListener("TabSplitViewActivate", this); 213 window.addEventListener("TabSplitViewDeactivate", this); 214 215 this.tabContainer.init(); 216 this._setupInitialBrowserAndTab(); 217 218 if ( 219 Services.prefs.getIntPref("browser.display.document_color_use") == 2 220 ) { 221 this.tabpanels.style.backgroundColor = Services.prefs.getCharPref( 222 "browser.display.background_color" 223 ); 224 } 225 226 this._setFindbarData(); 227 228 // We take over setting the document title, so remove the l10n id to 229 // avoid it being re-translated and overwriting document content if 230 // we ever switch languages at runtime. 231 document.querySelector("title").removeAttribute("data-l10n-id"); 232 233 this._setupEventListeners(); 234 this._initialized = true; 235 } 236 237 ownerGlobal = window; 238 239 ownerDocument = document; 240 241 closingTabsEnum = { 242 ALL: 0, 243 OTHER: 1, 244 TO_START: 2, 245 TO_END: 3, 246 MULTI_SELECTED: 4, 247 DUPLICATES: 6, 248 ALL_DUPLICATES: 7, 249 }; 250 251 _lastRelatedTabMap = new WeakMap(); 252 253 mProgressListeners = []; 254 255 mTabsProgressListeners = []; 256 257 _tabListeners = new Map(); 258 259 _tabFilters = new Map(); 260 261 _isBusy = false; 262 263 _awaitingToggleCaretBrowsingPrompt = false; 264 265 _previewMode = false; 266 267 _lastFindValue = ""; 268 269 _contentWaitingCount = 0; 270 271 _tabLayerCache = []; 272 273 tabAnimationsInProgress = 0; 274 275 /** 276 * Binding from browser to tab 277 */ 278 _tabForBrowser = new WeakMap(); 279 280 /** 281 * `_createLazyBrowser` will define properties on the unbound lazy browser 282 * which correspond to properties defined in MozBrowser which will be bound to 283 * the browser when it is inserted into the document. If any of these 284 * properties are accessed by consumers, `_insertBrowser` is called and 285 * the browser is inserted to ensure that things don't break. This list 286 * provides the names of properties that may be called while the browser 287 * is in its unbound (lazy) state. 288 */ 289 _browserBindingProperties = [ 290 "canGoBack", 291 "canGoForward", 292 "goBack", 293 "goForward", 294 "permitUnload", 295 "reload", 296 "reloadWithFlags", 297 "stop", 298 "loadURI", 299 "fixupAndLoadURIString", 300 "gotoIndex", 301 "currentURI", 302 "documentURI", 303 "remoteType", 304 "preferences", 305 "imageDocument", 306 "isRemoteBrowser", 307 "messageManager", 308 "getTabBrowser", 309 "finder", 310 "fastFind", 311 "sessionHistory", 312 "contentTitle", 313 "characterSet", 314 "fullZoom", 315 "textZoom", 316 "tabHasCustomZoom", 317 "webProgress", 318 "addProgressListener", 319 "removeProgressListener", 320 "audioPlaybackStarted", 321 "audioPlaybackStopped", 322 "resumeMedia", 323 "mute", 324 "unmute", 325 "blockedPopups", 326 "lastURI", 327 "purgeSessionHistory", 328 "stopScroll", 329 "startScroll", 330 "userTypedValue", 331 "userTypedClear", 332 "didStartLoadSinceLastUserTyping", 333 "audioMuted", 334 ]; 335 336 _removingTabs = new Set(); 337 338 _multiSelectedTabsSet = new WeakSet(); 339 340 _lastMultiSelectedTabRef = null; 341 342 _clearMultiSelectionLocked = false; 343 344 _clearMultiSelectionLockedOnce = false; 345 346 _multiSelectChangeStarted = false; 347 348 _multiSelectChangeAdditions = new Set(); 349 350 _multiSelectChangeRemovals = new Set(); 351 352 _multiSelectChangeSelected = false; 353 354 /** 355 * Tab close requests are ignored if the window is closing anyway, 356 * e.g. when holding Ctrl+W. 357 */ 358 _windowIsClosing = false; 359 360 preloadedBrowser = null; 361 362 /** 363 * This defines a proxy which allows us to access browsers by 364 * index without actually creating a full array of browsers. 365 */ 366 browsers = new Proxy([], { 367 has: (target, name) => { 368 if (typeof name == "string" && Number.isInteger(parseInt(name))) { 369 return name in gBrowser.tabs; 370 } 371 return false; 372 }, 373 get: (target, name) => { 374 if (name == "length") { 375 return gBrowser.tabs.length; 376 } 377 if (typeof name == "string" && Number.isInteger(parseInt(name))) { 378 if (!(name in gBrowser.tabs)) { 379 return undefined; 380 } 381 return gBrowser.tabs[name].linkedBrowser; 382 } 383 return target[name]; 384 }, 385 }); 386 387 /** 388 * List of browsers whose docshells must be active in order for print preview 389 * to work. 390 */ 391 _printPreviewBrowsers = new Set(); 392 393 /** @type {MozTabSplitViewWrapper} */ 394 #activeSplitView = null; 395 396 get activeSplitView() { 397 return this.#activeSplitView; 398 } 399 400 /** 401 * List of browsers which are currently in an active Split View. 402 * 403 * @type {MozBrowser[]} 404 */ 405 get splitViewBrowsers() { 406 const browsers = []; 407 if (this.#activeSplitView) { 408 for (const tab of this.#activeSplitView.tabs) { 409 browsers.push(tab.linkedBrowser); 410 } 411 } 412 return browsers; 413 } 414 415 _switcher = null; 416 417 _soundPlayingAttrRemovalTimer = 0; 418 419 _hoverTabTimer = null; 420 421 get tabs() { 422 return this.tabContainer.allTabs; 423 } 424 425 get tabGroups() { 426 return this.tabContainer.allGroups; 427 } 428 429 get tabsInCollapsedTabGroups() { 430 return this.tabGroups 431 .filter(tabGroup => tabGroup.collapsed) 432 .flatMap(tabGroup => tabGroup.tabs) 433 .filter(tab => !tab.hidden && !tab.closing); 434 } 435 436 addEventListener(...args) { 437 this.tabpanels.addEventListener(...args); 438 } 439 440 removeEventListener(...args) { 441 this.tabpanels.removeEventListener(...args); 442 } 443 444 dispatchEvent(...args) { 445 return this.tabpanels.dispatchEvent(...args); 446 } 447 448 /** 449 * Returns all tabs in the current window, including hidden tabs and tabs 450 * in collapsed groups, but excluding closing tabs and the Firefox View tab. 451 */ 452 get openTabs() { 453 return this.tabContainer.openTabs; 454 } 455 456 /** 457 * Same as `openTabs` but excluding hidden tabs. 458 */ 459 get nonHiddenTabs() { 460 return this.tabContainer.nonHiddenTabs; 461 } 462 463 /** 464 * Same as `openTabs` but excluding hidden tabs and tabs in collapsed groups. 465 */ 466 get visibleTabs() { 467 return this.tabContainer.visibleTabs; 468 } 469 470 get pinnedTabCount() { 471 for (var i = 0; i < this.tabs.length; i++) { 472 if (!this.tabs[i].pinned) { 473 break; 474 } 475 } 476 return i; 477 } 478 479 set selectedTab(val) { 480 if ( 481 gSharedTabWarning.willShowSharedTabWarning(val) || 482 document.documentElement.hasAttribute("window-modal-open") || 483 (gNavToolbox.collapsed && !this._allowTabChange) 484 ) { 485 return; 486 } 487 // Update the tab 488 this.tabbox.selectedTab = val; 489 } 490 491 get selectedTab() { 492 return this._selectedTab; 493 } 494 495 get selectedBrowser() { 496 return this._selectedBrowser; 497 } 498 499 get selectedBrowsers() { 500 const splitViewBrowsers = this.splitViewBrowsers; 501 return splitViewBrowsers.length 502 ? splitViewBrowsers 503 : [this._selectedBrowser]; 504 } 505 506 _setupInitialBrowserAndTab() { 507 // See browser.js for the meaning of window.arguments. 508 // Bug 1485961 covers making this more sane. 509 let userContextId = window.arguments && window.arguments[5]; 510 511 let openWindowInfo = window.docShell.treeOwner 512 .QueryInterface(Ci.nsIInterfaceRequestor) 513 .getInterface(Ci.nsIAppWindow).initialOpenWindowInfo; 514 515 if (!openWindowInfo && window.arguments && window.arguments[11]) { 516 openWindowInfo = window.arguments[11]; 517 } 518 519 let extraOptions; 520 if (window.arguments?.[1] instanceof Ci.nsIPropertyBag2) { 521 extraOptions = window.arguments[1]; 522 } 523 524 // If our opener provided a remoteType which was responsible for creating 525 // this pop-up window, we'll fall back to using that remote type when no 526 // other remote type is available. 527 let triggeringRemoteType; 528 if (extraOptions?.hasKey("triggeringRemoteType")) { 529 triggeringRemoteType = extraOptions.getPropertyAsACString( 530 "triggeringRemoteType" 531 ); 532 } 533 534 let tabArgument = gBrowserInit.getTabToAdopt(); 535 536 // If we have a tab argument with browser, we use its remoteType. Otherwise, 537 // if e10s is disabled or there's a parent process opener (e.g. parent 538 // process about: page) for the content tab, we use a parent 539 // process remoteType. Otherwise, we check the URI to determine 540 // what to do - if there isn't one, we default to the default remote type. 541 // 542 // When adopting a tab, we'll also use that tab's browsingContextGroupId, 543 // if available, to ensure we don't spawn a new process. 544 let remoteType; 545 let initialBrowsingContextGroupId; 546 547 if (tabArgument && tabArgument.hasAttribute("usercontextid")) { 548 // The window's first argument is a tab if and only if we are swapping tabs. 549 // We must set the browser's usercontextid so that the newly created remote 550 // tab child has the correct usercontextid. 551 userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10); 552 } 553 554 if (tabArgument && tabArgument.linkedBrowser) { 555 remoteType = tabArgument.linkedBrowser.remoteType; 556 initialBrowsingContextGroupId = 557 tabArgument.linkedBrowser.browsingContext?.group.id; 558 } else if (openWindowInfo) { 559 userContextId = openWindowInfo.originAttributes.userContextId; 560 if (openWindowInfo.isRemote) { 561 remoteType = triggeringRemoteType ?? E10SUtils.DEFAULT_REMOTE_TYPE; 562 } else { 563 remoteType = E10SUtils.NOT_REMOTE; 564 } 565 } else { 566 let uriToLoad = gBrowserInit.uriToLoadPromise; 567 if (uriToLoad && Array.isArray(uriToLoad)) { 568 uriToLoad = uriToLoad[0]; // we only care about the first item 569 } 570 571 if (uriToLoad && typeof uriToLoad == "string") { 572 let oa = E10SUtils.predictOriginAttributes({ 573 window, 574 userContextId, 575 }); 576 remoteType = E10SUtils.getRemoteTypeForURI( 577 uriToLoad, 578 gMultiProcessBrowser, 579 gFissionBrowser, 580 triggeringRemoteType ?? E10SUtils.DEFAULT_REMOTE_TYPE, 581 null, 582 oa 583 ); 584 } else { 585 // If we reach here, we don't have the url to load. This means that 586 // `uriToLoad` is most likely a promise which is waiting on SessionStore 587 // initialization. We can't delay setting up the browser here, as that 588 // would mean that `gBrowser.selectedBrowser` might not always exist, 589 // which is the current assumption. 590 591 if (Cu.isInAutomation) { 592 ChromeUtils.releaseAssert( 593 !triggeringRemoteType, 594 "Unexpected triggeringRemoteType with no uriToLoad" 595 ); 596 } 597 598 // In this case we default to the privileged about process as that's 599 // the best guess we can make, and we'll likely need it eventually. 600 remoteType = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE; 601 } 602 } 603 604 let createOptions = { 605 uriIsAboutBlank: false, 606 userContextId, 607 initialBrowsingContextGroupId, 608 remoteType, 609 openWindowInfo, 610 }; 611 let browser = this.createBrowser(createOptions); 612 browser.setAttribute("primary", "true"); 613 if (gBrowserAllowScriptsToCloseInitialTabs) { 614 browser.setAttribute("allowscriptstoclose", "true"); 615 } 616 browser.droppedLinkHandler = handleDroppedLink; 617 browser.loadURI = URILoadingWrapper.loadURI.bind( 618 URILoadingWrapper, 619 browser 620 ); 621 browser.fixupAndLoadURIString = 622 URILoadingWrapper.fixupAndLoadURIString.bind( 623 URILoadingWrapper, 624 browser 625 ); 626 627 if (AIWindow.isAIWindowActive(window)) { 628 let uriToLoad = gBrowserInit.uriToLoadPromise; 629 let firstURI = Array.isArray(uriToLoad) ? uriToLoad[0] : uriToLoad; 630 631 if (!this._allowTransparentBrowser) { 632 browser.toggleAttribute( 633 "transparent", 634 !firstURI || 635 AIWindow.isAIWindowContentPage(Services.io.newURI(firstURI)) 636 ); 637 } 638 } 639 640 let uniqueId = this._generateUniquePanelID(); 641 let panel = this.getPanel(browser); 642 panel.id = uniqueId; 643 this.tabpanels.appendChild(panel); 644 645 let tab = this.tabs[0]; 646 tab.linkedPanel = uniqueId; 647 this._selectedTab = tab; 648 this._selectedBrowser = browser; 649 tab.permanentKey = browser.permanentKey; 650 tab._tPos = 0; 651 tab._fullyOpen = true; 652 tab.linkedBrowser = browser; 653 654 if (userContextId) { 655 tab.setAttribute("usercontextid", userContextId); 656 ContextualIdentityService.setTabStyle(tab); 657 } 658 659 this._tabForBrowser.set(browser, tab); 660 661 this.appendStatusPanel(); 662 663 // This is the initial browser, so it's usually active; the default is false 664 // so we have to update it: 665 browser.docShellIsActive = this.shouldActivateDocShell(browser); 666 667 // Hook the browser up with a progress listener. 668 let tabListener = new TabProgressListener(tab, browser, true, false); 669 let filter = Cc[ 670 "@mozilla.org/appshell/component/browser-status-filter;1" 671 ].createInstance(Ci.nsIWebProgress); 672 filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); 673 this._tabListeners.set(tab, tabListener); 674 this._tabFilters.set(tab, filter); 675 browser.webProgress.addProgressListener( 676 filter, 677 Ci.nsIWebProgress.NOTIFY_ALL 678 ); 679 } 680 681 /** 682 * BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT 683 * MAKE SURE TO ADD IT HERE AS WELL. 684 */ 685 get canGoBack() { 686 return this.selectedBrowser.canGoBack; 687 } 688 689 get canGoBackIgnoringUserInteraction() { 690 return this.selectedBrowser.canGoBackIgnoringUserInteraction; 691 } 692 693 get canGoForward() { 694 return this.selectedBrowser.canGoForward; 695 } 696 697 goBack(requireUserInteraction) { 698 return this.selectedBrowser.goBack(requireUserInteraction); 699 } 700 701 goForward(requireUserInteraction) { 702 return this.selectedBrowser.goForward(requireUserInteraction); 703 } 704 705 reload() { 706 return this.selectedBrowser.reload(); 707 } 708 709 reloadWithFlags(aFlags) { 710 return this.selectedBrowser.reloadWithFlags(aFlags); 711 } 712 713 stop() { 714 return this.selectedBrowser.stop(); 715 } 716 717 /** 718 * throws exception for unknown schemes 719 */ 720 loadURI(uri, params) { 721 return this.selectedBrowser.loadURI(uri, params); 722 } 723 /** 724 * throws exception for unknown schemes 725 */ 726 fixupAndLoadURIString(uriString, params) { 727 return this.selectedBrowser.fixupAndLoadURIString(uriString, params); 728 } 729 730 gotoIndex(aIndex) { 731 return this.selectedBrowser.gotoIndex(aIndex); 732 } 733 734 get currentURI() { 735 return this.selectedBrowser.currentURI; 736 } 737 738 get finder() { 739 return this.selectedBrowser.finder; 740 } 741 742 get docShell() { 743 return this.selectedBrowser.docShell; 744 } 745 746 get webNavigation() { 747 return this.selectedBrowser.webNavigation; 748 } 749 750 get webProgress() { 751 return this.selectedBrowser.webProgress; 752 } 753 754 get contentWindow() { 755 return this.selectedBrowser.contentWindow; 756 } 757 758 get sessionHistory() { 759 return this.selectedBrowser.sessionHistory; 760 } 761 762 get contentDocument() { 763 return this.selectedBrowser.contentDocument; 764 } 765 766 get contentTitle() { 767 return this.selectedBrowser.contentTitle; 768 } 769 770 get contentPrincipal() { 771 return this.selectedBrowser.contentPrincipal; 772 } 773 774 get securityUI() { 775 return this.selectedBrowser.securityUI; 776 } 777 778 set fullZoom(val) { 779 this.selectedBrowser.fullZoom = val; 780 } 781 782 get fullZoom() { 783 return this.selectedBrowser.fullZoom; 784 } 785 786 set textZoom(val) { 787 this.selectedBrowser.textZoom = val; 788 } 789 790 get textZoom() { 791 return this.selectedBrowser.textZoom; 792 } 793 794 get isSyntheticDocument() { 795 return this.selectedBrowser.isSyntheticDocument; 796 } 797 798 set userTypedValue(val) { 799 this.selectedBrowser.userTypedValue = val; 800 } 801 802 get userTypedValue() { 803 return this.selectedBrowser.userTypedValue; 804 } 805 806 _setFindbarData() { 807 // Ensure we know what the find bar key is in the content process: 808 let { sharedData } = Services.ppmm; 809 if (!sharedData.has("Findbar:Shortcut")) { 810 let keyEl = document.getElementById("key_find"); 811 let mods = keyEl 812 .getAttribute("modifiers") 813 .replace( 814 /accel/i, 815 AppConstants.platform == "macosx" ? "meta" : "control" 816 ); 817 sharedData.set("Findbar:Shortcut", { 818 key: keyEl.getAttribute("key"), 819 shiftKey: mods.includes("shift"), 820 ctrlKey: mods.includes("control"), 821 altKey: mods.includes("alt"), 822 metaKey: mods.includes("meta"), 823 }); 824 } 825 } 826 827 isFindBarInitialized(aTab) { 828 return (aTab || this.selectedTab)._findBar != undefined; 829 } 830 831 /** 832 * Get the already constructed findbar 833 */ 834 getCachedFindBar(aTab = this.selectedTab) { 835 return aTab._findBar; 836 } 837 838 /** 839 * Get the findbar, and create it if it doesn't exist. 840 * 841 * @return the find bar (or null if the window or tab is closed/closing in the interim). 842 */ 843 async getFindBar(aTab = this.selectedTab) { 844 let findBar = this.getCachedFindBar(aTab); 845 if (findBar) { 846 return findBar; 847 } 848 849 // Avoid re-entrancy by caching the promise we're about to return. 850 if (!aTab._pendingFindBar) { 851 aTab._pendingFindBar = this._createFindBar(aTab); 852 } 853 return aTab._pendingFindBar; 854 } 855 856 /** 857 * Create a findbar instance. 858 * 859 * @param aTab the tab to create the find bar for. 860 * @return the created findbar, or null if the window or tab is closed/closing. 861 */ 862 async _createFindBar(aTab) { 863 let findBar = document.createXULElement("findbar"); 864 let browser = this.getBrowserForTab(aTab); 865 866 browser.parentNode.insertAdjacentElement("afterend", findBar); 867 868 await new Promise(r => requestAnimationFrame(r)); 869 delete aTab._pendingFindBar; 870 if (window.closed || aTab.closing) { 871 return null; 872 } 873 874 findBar.browser = browser; 875 findBar._findField.value = this._lastFindValue; 876 877 aTab._findBar = findBar; 878 879 let event = document.createEvent("Events"); 880 event.initEvent("TabFindInitialized", true, false); 881 aTab.dispatchEvent(event); 882 883 return findBar; 884 } 885 886 appendStatusPanel(browser = this.selectedBrowser) { 887 browser.insertAdjacentElement("afterend", StatusPanel.panel); 888 } 889 890 _updateTabBarForPinnedTabs() { 891 this.tabContainer._unlockTabSizing(); 892 this.tabContainer._handleTabSelect(true); 893 this.tabContainer._updateCloseButtons(); 894 } 895 896 #notifyPinnedStatus( 897 aTab, 898 { telemetrySource = this.TabMetrics.METRIC_SOURCE.UNKNOWN } = {} 899 ) { 900 // browsingContext is expected to not be defined on discarded tabs. 901 if (aTab.linkedBrowser.browsingContext) { 902 aTab.linkedBrowser.browsingContext.isAppTab = aTab.pinned; 903 } 904 905 let event = new CustomEvent(aTab.pinned ? "TabPinned" : "TabUnpinned", { 906 bubbles: true, 907 cancelable: false, 908 detail: { telemetrySource }, 909 }); 910 aTab.dispatchEvent(event); 911 } 912 913 /** 914 * Pin a tab. 915 * 916 * @param {MozTabbrowserTab} aTab 917 * The tab to pin. 918 * @param {object} [options] 919 * @property {string} [options.telemetrySource="unknown"] 920 * The means by which the tab was pinned. 921 * @see TabMetrics.METRIC_SOURCE for possible values. 922 * Defaults to "unknown". 923 */ 924 pinTab( 925 aTab, 926 { telemetrySource = this.TabMetrics.METRIC_SOURCE.UNKNOWN } = {} 927 ) { 928 if (aTab.pinned || aTab == FirefoxViewHandler.tab) { 929 return; 930 } 931 932 this.showTab(aTab); 933 this.#handleTabMove(aTab, () => { 934 let periphery = document.getElementById( 935 "pinned-tabs-container-periphery" 936 ); 937 // If periphery is null, append to end 938 this.pinnedTabsContainer.insertBefore(aTab, periphery); 939 }); 940 941 aTab.setAttribute("pinned", "true"); 942 this._updateTabBarForPinnedTabs(); 943 this.#notifyPinnedStatus(aTab, { telemetrySource }); 944 } 945 946 unpinTab(aTab) { 947 if (!aTab.pinned) { 948 return; 949 } 950 951 this.#handleTabMove(aTab, () => { 952 // we remove this attribute first, so that allTabs represents 953 // the moving of a tab from the pinned tabs container 954 // and back into arrowscrollbox. 955 aTab.removeAttribute("pinned"); 956 this.tabContainer.arrowScrollbox.prepend(aTab); 957 }); 958 959 aTab.style.marginInlineStart = ""; 960 aTab._pinnedUnscrollable = false; 961 this._updateTabBarForPinnedTabs(); 962 this.#notifyPinnedStatus(aTab); 963 } 964 965 previewTab(aTab, aCallback) { 966 let currentTab = this.selectedTab; 967 try { 968 // Suppress focus, ownership and selected tab changes 969 this._previewMode = true; 970 this.selectedTab = aTab; 971 aCallback(); 972 } finally { 973 this.selectedTab = currentTab; 974 this._previewMode = false; 975 } 976 } 977 978 getBrowserAtIndex(aIndex) { 979 return this.browsers[aIndex]; 980 } 981 982 getBrowserForOuterWindowID(aID) { 983 for (let b of this.browsers) { 984 if (b.outerWindowID == aID) { 985 return b; 986 } 987 } 988 989 return null; 990 } 991 992 getTabForBrowser(aBrowser) { 993 return this._tabForBrowser.get(aBrowser); 994 } 995 996 getPanel(aBrowser) { 997 return this.getBrowserContainer(aBrowser).parentNode; 998 } 999 1000 getBrowserContainer(aBrowser) { 1001 return (aBrowser || this.selectedBrowser).parentNode.parentNode; 1002 } 1003 1004 getTabNotificationDeck() { 1005 if (!this._tabNotificationDeck) { 1006 let template = document.getElementById( 1007 "tab-notification-deck-template" 1008 ); 1009 template.replaceWith(template.content); 1010 this._tabNotificationDeck = document.getElementById( 1011 "tab-notification-deck" 1012 ); 1013 } 1014 return this._tabNotificationDeck; 1015 } 1016 1017 _nextNotificationBoxId = 0; 1018 getNotificationBox(aBrowser) { 1019 let browser = aBrowser || this.selectedBrowser; 1020 if (!browser._notificationBox) { 1021 browser._notificationBox = new MozElements.NotificationBox(element => { 1022 element.setAttribute("notificationside", "top"); 1023 element.setAttribute( 1024 "name", 1025 `tab-notification-box-${this._nextNotificationBoxId++}` 1026 ); 1027 this.getTabNotificationDeck().append(element); 1028 if (browser == this.selectedBrowser) { 1029 this._updateVisibleNotificationBox(browser); 1030 } 1031 }, this._notificationEnableDelay); 1032 } 1033 return browser._notificationBox; 1034 } 1035 1036 readNotificationBox(aBrowser) { 1037 let browser = aBrowser || this.selectedBrowser; 1038 return browser._notificationBox || null; 1039 } 1040 1041 _updateVisibleNotificationBox(aBrowser) { 1042 if (!this._tabNotificationDeck) { 1043 // If the deck hasn't been created we don't need to create it here. 1044 return; 1045 } 1046 let notificationBox = this.readNotificationBox(aBrowser); 1047 this.getTabNotificationDeck().selectedViewName = notificationBox 1048 ? notificationBox.stack.getAttribute("name") 1049 : ""; 1050 } 1051 1052 getTabDialogBox(aBrowser) { 1053 if (!aBrowser) { 1054 throw new Error("aBrowser is required"); 1055 } 1056 if (!aBrowser.tabDialogBox) { 1057 aBrowser.tabDialogBox = new TabDialogBox(aBrowser); 1058 } 1059 return aBrowser.tabDialogBox; 1060 } 1061 1062 getTabFromAudioEvent(aEvent) { 1063 if (!aEvent.isTrusted) { 1064 return null; 1065 } 1066 1067 var browser = aEvent.originalTarget; 1068 var tab = this.getTabForBrowser(browser); 1069 return tab; 1070 } 1071 1072 _callProgressListeners( 1073 aBrowser, 1074 aMethod, 1075 aArguments, 1076 aCallGlobalListeners = true, 1077 aCallTabsListeners = true 1078 ) { 1079 var rv = true; 1080 1081 function callListeners(listeners, args) { 1082 for (let p of listeners) { 1083 if (aMethod in p) { 1084 try { 1085 if (!p[aMethod].apply(p, args)) { 1086 rv = false; 1087 } 1088 } catch (e) { 1089 // don't inhibit other listeners 1090 console.error(e); 1091 } 1092 } 1093 } 1094 } 1095 1096 aBrowser = aBrowser || this.selectedBrowser; 1097 1098 if (aCallGlobalListeners && aBrowser == this.selectedBrowser) { 1099 callListeners(this.mProgressListeners, aArguments); 1100 } 1101 1102 if (aCallTabsListeners) { 1103 aArguments.unshift(aBrowser); 1104 1105 callListeners(this.mTabsProgressListeners, aArguments); 1106 } 1107 1108 return rv; 1109 } 1110 1111 /** 1112 * Sets an icon for the tab if the URI is defined in FAVICON_DEFAULTS. 1113 */ 1114 setDefaultIcon(aTab, aURI) { 1115 if (aURI && aURI.spec in FAVICON_DEFAULTS) { 1116 this.setIcon(aTab, FAVICON_DEFAULTS[aURI.spec]); 1117 } 1118 } 1119 1120 setIcon( 1121 aTab, 1122 aIconURL = "", 1123 aOriginalURL = aIconURL, 1124 aClearImageFirst = false 1125 ) { 1126 let makeString = url => (url instanceof Ci.nsIURI ? url.spec : url); 1127 1128 aIconURL = makeString(aIconURL); 1129 aOriginalURL = makeString(aOriginalURL); 1130 1131 let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"]; 1132 1133 if ( 1134 aIconURL && 1135 !LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol)) 1136 ) { 1137 console.error( 1138 `Attempt to set a remote URL ${aIconURL} as a tab icon without a loading principal.` 1139 ); 1140 return; 1141 } 1142 1143 let browser = this.getBrowserForTab(aTab); 1144 browser.mIconURL = aIconURL; 1145 1146 if (aIconURL != aTab.getAttribute("image")) { 1147 if (aClearImageFirst) { 1148 aTab.removeAttribute("image"); 1149 } 1150 if (aIconURL) { 1151 let url = aIconURL; 1152 if ( 1153 this._remoteSVGIconDecoding && 1154 url.startsWith(this.FaviconUtils.SVG_DATA_URI_PREFIX) 1155 ) { 1156 // 16px is hardcoded for .tab-icon-image in tabs.css 1157 let size = Math.floor(16 * window.devicePixelRatio); 1158 url = this.FaviconUtils.getMozRemoteImageURL(url, size); 1159 } 1160 aTab.setAttribute("image", url); 1161 } else { 1162 aTab.removeAttribute("image"); 1163 } 1164 this._tabAttrModified(aTab, ["image"]); 1165 } 1166 1167 // The aOriginalURL argument is currently only used by tests. 1168 this._callProgressListeners(browser, "onLinkIconAvailable", [ 1169 aIconURL, 1170 aOriginalURL, 1171 ]); 1172 } 1173 1174 getIcon(aTab) { 1175 let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser; 1176 return browser.mIconURL; 1177 } 1178 1179 setPageInfo(tab, aURL, aDescription, aPreviewImage) { 1180 if (aURL) { 1181 let pageInfo = { 1182 url: aURL, 1183 description: aDescription, 1184 previewImageURL: aPreviewImage, 1185 }; 1186 PlacesUtils.history.update(pageInfo).catch(console.error); 1187 } 1188 if (tab) { 1189 tab.description = aDescription; 1190 } 1191 } 1192 1193 #cachedTitleInfo = null; 1194 #populateTitleCache() { 1195 this.#cachedTitleInfo = {}; 1196 for (let id of [ 1197 "mainWindowTitle", 1198 "privateWindowTitle", 1199 "privateWindowSuffixForContent", 1200 ]) { 1201 this.#cachedTitleInfo[id] = 1202 document.getElementById(id)?.textContent || ""; 1203 } 1204 } 1205 1206 /** 1207 * The current Taskbar Tab associated with this window. This cannot 1208 * change after it is first set. 1209 * 1210 * @type {TaskbarTab|null} 1211 */ 1212 #taskbarTab = null; 1213 1214 /** 1215 * The last title associated with this window, avoiding re-lookup 1216 * of the container name and localizations. 1217 * 1218 * @type {string|null} 1219 */ 1220 #taskbarTabTitle = null; 1221 1222 /** 1223 * The last profile used when determining the Taskbar Tab title. (This 1224 * can change, for example if the first profile is made after opening 1225 * the Taskbar Tab.) 1226 * 1227 * @type {string|null} 1228 */ 1229 #taskbarTabTitleLastProfile = null; 1230 1231 /** 1232 * Determines the content of the window title that relates to the Taskbar 1233 * Tab. This includes the name of the Taskbar Tab, of the container, and 1234 * of the profile. 1235 * 1236 * If no Taskbar Tab is in use, the profile is added by 1237 * getWindowTitleForBrowser and this returns null. 1238 * 1239 * @returns {string|null} The part of the title that was determined from 1240 * the Taskbar Tab, or null if nothing is needed. 1241 */ 1242 #determineTaskbarTabTitle(aProfile) { 1243 if (!this._shouldExposeContentTitle) { 1244 // The Taskbar Tab and container info expose what site the user's on. 1245 return null; 1246 } 1247 1248 if ( 1249 this.#taskbarTabTitle && 1250 this.#taskbarTabTitleLastProfile == aProfile 1251 ) { 1252 return this.#taskbarTabTitle; 1253 } 1254 1255 let id = this.TaskbarTabsUtils.getTaskbarTabIdFromWindow(window); 1256 if (!id) { 1257 return null; 1258 } 1259 1260 if (!this.#taskbarTab) { 1261 this.TaskbarTabs.getTaskbarTab(id) 1262 .then(tt => { 1263 this.#taskbarTab = tt; 1264 this.updateTitlebar(); 1265 }) 1266 .catch(() => { 1267 // The taskbar tab doesn't exist; leave it as-is. 1268 }); 1269 return null; 1270 } 1271 1272 let containerLabel = this.#taskbarTab.userContextId 1273 ? ContextualIdentityService.getUserContextLabel( 1274 this.#taskbarTab.userContextId 1275 ) 1276 : ""; 1277 1278 let stringName = "taskbar-tab-title-default"; 1279 if (containerLabel && aProfile) { 1280 stringName = "taskbar-tab-title-container-profile"; 1281 } else if (containerLabel && !aProfile) { 1282 stringName = "taskbar-tab-title-container"; 1283 } else if (!containerLabel && aProfile) { 1284 stringName = "taskbar-tab-title-profile"; 1285 } 1286 1287 this.#taskbarTabTitle = this.tabLocalization.formatValueSync(stringName, { 1288 name: this.#taskbarTab.name, 1289 container: containerLabel, 1290 profile: aProfile, 1291 }); 1292 return this.#taskbarTabTitle; 1293 } 1294 1295 #determineContentTitle(browser) { 1296 let title = ""; 1297 if ( 1298 !this._shouldExposeContentTitle || 1299 (PrivateBrowsingUtils.isWindowPrivate(window) && 1300 !this._shouldExposeContentTitlePbm) 1301 ) { 1302 return title; 1303 } 1304 1305 let docElement = document.documentElement; 1306 // If location bar is hidden and the URL type supports a host, 1307 // add the scheme and host to the title to prevent spoofing. 1308 // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239 1309 try { 1310 if (docElement.getAttribute("chromehidden").includes("location")) { 1311 const uri = Services.io.createExposableURI(browser.currentURI); 1312 let prefix = uri.prePath; 1313 if (uri.scheme == "about") { 1314 prefix = uri.spec; 1315 } else if (uri.scheme == "moz-extension") { 1316 const ext = WebExtensionPolicy.getByHostname(uri.host); 1317 if (ext && ext.name) { 1318 let extensionLabel = document.getElementById( 1319 "urlbar-label-extension" 1320 ); 1321 prefix = `${extensionLabel.value} (${ext.name})`; 1322 } 1323 } 1324 title = prefix + " - "; 1325 } 1326 } catch (e) { 1327 // ignored 1328 } 1329 1330 if (docElement.hasAttribute("titlepreface")) { 1331 title += docElement.getAttribute("titlepreface"); 1332 } 1333 1334 let tab = this.getTabForBrowser(browser); 1335 if (tab._labelIsContentTitle) { 1336 // Strip out any null bytes in the content title, since the 1337 // underlying widget implementations of nsWindow::SetTitle pass 1338 // null-terminated strings to system APIs. 1339 title += tab.getAttribute("label").replace(/\0/g, ""); 1340 } 1341 return title; 1342 } 1343 1344 getWindowTitleForBrowser(browser) { 1345 if (!this.#cachedTitleInfo) { 1346 this.#populateTitleCache(); 1347 } 1348 let contentTitle = this.#determineContentTitle(browser); 1349 let docElement = document.documentElement; 1350 let isTemporaryPrivateWindow = 1351 docElement.getAttribute("privatebrowsingmode") == "temporary"; 1352 1353 let profileIdentifier = 1354 SelectableProfileService?.isEnabled && 1355 SelectableProfileService.currentProfile?.name.replace(/\0/g, ""); 1356 // Note that empty/falsy bits get filtered below. 1357 1358 let taskbarTabTitle = this.#determineTaskbarTabTitle(profileIdentifier); 1359 let parts = [contentTitle, taskbarTabTitle ?? profileIdentifier]; 1360 1361 // On macOS PB windows, add the private window suffix if we have a content 1362 // title. We'll add the brand name and private window suffix for all other 1363 // platforms below. 1364 if ( 1365 AppConstants.platform == "macosx" && 1366 contentTitle && 1367 isTemporaryPrivateWindow 1368 ) { 1369 parts.push(this.#cachedTitleInfo.privateWindowSuffixForContent); 1370 } 1371 1372 // Show the brand name if we aren't a Taskbar Tab, since that is done in 1373 // #determineTaskbarTabTitle. On macOS we only do this if we don't have a 1374 // content title; elsewhere, the brand becomes a suffix in the title bar. 1375 if ( 1376 !taskbarTabTitle && 1377 (!contentTitle || AppConstants.platform != "macosx") 1378 ) { 1379 parts.push( 1380 this.#cachedTitleInfo[ 1381 isTemporaryPrivateWindow ? "privateWindowTitle" : "mainWindowTitle" 1382 ] 1383 ); 1384 } 1385 1386 return parts.filter(p => !!p).join(" — "); 1387 } 1388 1389 updateTitlebar() { 1390 document.title = this.getWindowTitleForBrowser(this.selectedBrowser); 1391 } 1392 1393 updateCurrentBrowser(aForceUpdate) { 1394 let newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex); 1395 if (this.selectedBrowser == newBrowser && !aForceUpdate) { 1396 return; 1397 } 1398 1399 let oldBrowser = this.selectedBrowser; 1400 // Once the async switcher starts, it's unpredictable when it will touch 1401 // the address bar, thus we store its state immediately. 1402 gURLBar?.saveSelectionStateForBrowser(oldBrowser); 1403 1404 let newTab = this.getTabForBrowser(newBrowser); 1405 1406 let timerId; 1407 if (!aForceUpdate) { 1408 timerId = Glean.browserTabswitch.update.start(); 1409 1410 if (gMultiProcessBrowser) { 1411 this._asyncTabSwitching = true; 1412 this._getSwitcher().requestTab(newTab); 1413 this._asyncTabSwitching = false; 1414 } 1415 1416 document.commandDispatcher.lock(); 1417 } 1418 1419 let oldTab = this.selectedTab; 1420 1421 // Preview mode should not reset the owner 1422 if (!this._previewMode && !oldTab.selected) { 1423 oldTab.owner = null; 1424 } 1425 1426 let lastRelatedTab = this._lastRelatedTabMap.get(oldTab); 1427 if (lastRelatedTab) { 1428 if (!lastRelatedTab.selected) { 1429 lastRelatedTab.owner = null; 1430 } 1431 } 1432 this._lastRelatedTabMap = new WeakMap(); 1433 1434 if (!gMultiProcessBrowser) { 1435 oldBrowser.removeAttribute("primary"); 1436 oldBrowser.docShellIsActive = false; 1437 newBrowser.setAttribute("primary", "true"); 1438 newBrowser.docShellIsActive = !document.hidden; 1439 } 1440 1441 this._selectedBrowser = newBrowser; 1442 this._selectedTab = newTab; 1443 this.showTab(newTab); 1444 1445 this.appendStatusPanel(); 1446 1447 this._updateVisibleNotificationBox(newBrowser); 1448 1449 let oldBrowserPopupsBlocked = 1450 oldBrowser.popupAndRedirectBlocker.getBlockedPopupCount(); 1451 let newBrowserPopupsBlocked = 1452 newBrowser.popupAndRedirectBlocker.getBlockedPopupCount(); 1453 if (oldBrowserPopupsBlocked != newBrowserPopupsBlocked) { 1454 newBrowser.popupAndRedirectBlocker.sendObserverUpdateBlockedPopupsEvent(); 1455 } 1456 1457 let oldBrowserRedirectBlocked = 1458 oldBrowser.popupAndRedirectBlocker.isRedirectBlocked(); 1459 let newBrowserRedirectBlocked = 1460 newBrowser.popupAndRedirectBlocker.isRedirectBlocked(); 1461 if (oldBrowserRedirectBlocked != newBrowserRedirectBlocked) { 1462 newBrowser.popupAndRedirectBlocker.sendObserverUpdateBlockedRedirectEvent(); 1463 } 1464 1465 // Update the URL bar. 1466 let webProgress = newBrowser.webProgress; 1467 this._callProgressListeners( 1468 null, 1469 "onLocationChange", 1470 [webProgress, null, newBrowser.currentURI, 0, true], 1471 true, 1472 false 1473 ); 1474 1475 let securityUI = newBrowser.securityUI; 1476 if (securityUI) { 1477 this._callProgressListeners( 1478 null, 1479 "onSecurityChange", 1480 [webProgress, null, securityUI.state], 1481 true, 1482 false 1483 ); 1484 // Include the true final argument to indicate that this event is 1485 // simulated (instead of being observed by the webProgressListener). 1486 this._callProgressListeners( 1487 null, 1488 "onContentBlockingEvent", 1489 [webProgress, null, newBrowser.getContentBlockingEvents(), true], 1490 true, 1491 false 1492 ); 1493 } 1494 1495 let listener = this._tabListeners.get(newTab); 1496 if (listener && listener.mStateFlags) { 1497 this._callProgressListeners( 1498 null, 1499 "onUpdateCurrentBrowser", 1500 [ 1501 listener.mStateFlags, 1502 listener.mStatus, 1503 listener.mMessage, 1504 listener.mTotalProgress, 1505 ], 1506 true, 1507 false 1508 ); 1509 } 1510 1511 if (!this._previewMode) { 1512 newTab.recordTimeFromUnloadToReload(); 1513 newTab.updateLastAccessed(); 1514 oldTab.updateLastAccessed(); 1515 // if this is the foreground window, update the last-seen timestamps. 1516 if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { 1517 newTab.updateLastSeenActive(); 1518 oldTab.updateLastSeenActive(); 1519 } 1520 1521 let oldFindBar = oldTab._findBar; 1522 if ( 1523 oldFindBar && 1524 oldFindBar.findMode == oldFindBar.FIND_NORMAL && 1525 !oldFindBar.hidden 1526 ) { 1527 this._lastFindValue = oldFindBar._findField.value; 1528 } 1529 1530 this.updateTitlebar(); 1531 1532 newTab.removeAttribute("titlechanged"); 1533 newTab.attention = false; 1534 1535 // The tab has been selected, it's not unselected anymore. 1536 // Call the current browser's unselectedTabHover() with false 1537 // to dispatch an event. 1538 newBrowser.unselectedTabHover(false); 1539 } 1540 1541 // If the new tab is busy, and our current state is not busy, then 1542 // we need to fire a start to all progress listeners. 1543 if (newTab.hasAttribute("busy") && !this._isBusy) { 1544 this._isBusy = true; 1545 this._callProgressListeners( 1546 null, 1547 "onStateChange", 1548 [ 1549 webProgress, 1550 null, 1551 Ci.nsIWebProgressListener.STATE_START | 1552 Ci.nsIWebProgressListener.STATE_IS_NETWORK, 1553 0, 1554 ], 1555 true, 1556 false 1557 ); 1558 } 1559 1560 // If the new tab is not busy, and our current state is busy, then 1561 // we need to fire a stop to all progress listeners. 1562 if (!newTab.hasAttribute("busy") && this._isBusy) { 1563 this._isBusy = false; 1564 this._callProgressListeners( 1565 null, 1566 "onStateChange", 1567 [ 1568 webProgress, 1569 null, 1570 Ci.nsIWebProgressListener.STATE_STOP | 1571 Ci.nsIWebProgressListener.STATE_IS_NETWORK, 1572 0, 1573 ], 1574 true, 1575 false 1576 ); 1577 } 1578 1579 // TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code 1580 // that might rely upon the other changes suppressed. 1581 // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window 1582 if (!this._previewMode) { 1583 // We've selected the new tab, so go ahead and notify listeners. 1584 let event = new CustomEvent("TabSelect", { 1585 bubbles: true, 1586 cancelable: false, 1587 detail: { 1588 previousTab: oldTab, 1589 }, 1590 }); 1591 newTab.dispatchEvent(event); 1592 1593 this._tabAttrModified(oldTab, ["selected"]); 1594 this._tabAttrModified(newTab, ["selected"]); 1595 1596 this._startMultiSelectChange(); 1597 this._multiSelectChangeSelected = true; 1598 this.clearMultiSelectedTabs(); 1599 if (this._multiSelectChangeAdditions.size) { 1600 // Some tab has been multiselected just before switching tabs. 1601 // The tab that was selected at that point should also be multiselected. 1602 this.addToMultiSelectedTabs(oldTab); 1603 } 1604 1605 if (!gMultiProcessBrowser) { 1606 this._adjustFocusBeforeTabSwitch(oldTab, newTab); 1607 this._adjustFocusAfterTabSwitch(newTab); 1608 } 1609 1610 // Bug 1781806 - A forced update can indicate the tab was already 1611 // selected. To ensure the internal state of the Urlbar is kept in 1612 // sync, notify it as if focus changed. Alternatively, if there is no 1613 // force update but the load context is not using remote tabs, there 1614 // can be a focus change due to the _adjustFocus above. 1615 if (aForceUpdate || !gMultiProcessBrowser) { 1616 gURLBar.afterTabSwitchFocusChange(); 1617 } 1618 } 1619 1620 updateUserContextUIIndicator(); 1621 gPermissionPanel.updateSharingIndicator(); 1622 1623 // Enable touch events to start a native dragging 1624 // session to allow the user to easily drag the selected tab. 1625 // This is currently only supported on Windows. 1626 oldTab.removeAttribute("touchdownstartsdrag"); 1627 newTab.setAttribute("touchdownstartsdrag", "true"); 1628 1629 if (!gMultiProcessBrowser) { 1630 document.commandDispatcher.unlock(); 1631 1632 let event = new CustomEvent("TabSwitchDone", { 1633 bubbles: true, 1634 cancelable: true, 1635 }); 1636 this.dispatchEvent(event); 1637 } 1638 1639 if (!aForceUpdate) { 1640 Glean.browserTabswitch.update.stopAndAccumulate(timerId); 1641 } 1642 } 1643 1644 _adjustFocusBeforeTabSwitch(oldTab, newTab) { 1645 if (this._previewMode) { 1646 return; 1647 } 1648 1649 let oldBrowser = oldTab.linkedBrowser; 1650 let newBrowser = newTab.linkedBrowser; 1651 1652 gURLBar.getBrowserState(oldBrowser).urlbarFocused = gURLBar.focused; 1653 1654 if (this._asyncTabSwitching) { 1655 newBrowser._userTypedValueAtBeforeTabSwitch = newBrowser.userTypedValue; 1656 } 1657 1658 if (this.isFindBarInitialized(oldTab)) { 1659 let findBar = this.getCachedFindBar(oldTab); 1660 oldTab._findBarFocused = 1661 !findBar.hidden && 1662 findBar._findField.getAttribute("focused") == "true"; 1663 } 1664 1665 let activeEl = document.activeElement; 1666 // If focus is on the old tab, move it to the new tab. 1667 if (activeEl == oldTab) { 1668 newTab.focus(); 1669 } else if ( 1670 gMultiProcessBrowser && 1671 activeEl != newBrowser && 1672 activeEl != newTab 1673 ) { 1674 // In e10s, if focus isn't already in the tabstrip or on the new browser, 1675 // and the new browser's previous focus wasn't in the url bar but focus is 1676 // there now, we need to adjust focus further. 1677 let keepFocusOnUrlBar = 1678 newBrowser && 1679 gURLBar.getBrowserState(newBrowser).urlbarFocused && 1680 gURLBar.focused; 1681 if (!keepFocusOnUrlBar) { 1682 // Clear focus so that _adjustFocusAfterTabSwitch can detect if 1683 // some element has been focused and respect that. 1684 document.activeElement.blur(); 1685 } 1686 } 1687 } 1688 1689 _adjustFocusAfterTabSwitch(newTab) { 1690 // Don't steal focus from the tab bar. 1691 if (document.activeElement == newTab) { 1692 return; 1693 } 1694 1695 let newBrowser = this.getBrowserForTab(newTab); 1696 1697 if (newBrowser.hasAttribute("tabDialogShowing")) { 1698 newBrowser.tabDialogBox.focus(); 1699 return; 1700 } 1701 // Focus the location bar if it was previously focused for that tab. 1702 // In full screen mode, only bother making the location bar visible 1703 // if the tab is a blank one. 1704 if (gURLBar.getBrowserState(newBrowser).urlbarFocused) { 1705 let selectURL = () => { 1706 if (this._asyncTabSwitching) { 1707 // Set _awaitingSetURI flag to suppress popup notification 1708 // explicitly while tab switching asynchronously. 1709 newBrowser._awaitingSetURI = true; 1710 1711 // The onLocationChange event called in updateCurrentBrowser() will 1712 // be captured in browser.js, then it calls gURLBar.setURI(). In case 1713 // of that doing processing of here before doing above processing, 1714 // the selection status that gURLBar.select() does will be releasing 1715 // by gURLBar.setURI(). To resolve it, we call gURLBar.select() after 1716 // finishing gURLBar.setURI(). 1717 const currentActiveElement = document.activeElement; 1718 gURLBar.inputField.addEventListener( 1719 "SetURI", 1720 () => { 1721 delete newBrowser._awaitingSetURI; 1722 1723 // If the user happened to type into the URL bar for this browser 1724 // by the time we got here, focusing will cause the text to be 1725 // selected which could cause them to overwrite what they've 1726 // already typed in. 1727 let userTypedValueAtBeforeTabSwitch = 1728 newBrowser._userTypedValueAtBeforeTabSwitch; 1729 delete newBrowser._userTypedValueAtBeforeTabSwitch; 1730 if ( 1731 newBrowser.userTypedValue && 1732 newBrowser.userTypedValue != userTypedValueAtBeforeTabSwitch 1733 ) { 1734 return; 1735 } 1736 1737 if (currentActiveElement != document.activeElement) { 1738 return; 1739 } 1740 gURLBar.restoreSelectionStateForBrowser(newBrowser); 1741 }, 1742 { once: true } 1743 ); 1744 } else { 1745 gURLBar.restoreSelectionStateForBrowser(newBrowser); 1746 } 1747 }; 1748 1749 // This inDOMFullscreen attribute indicates that the page has something 1750 // such as a video in fullscreen mode. Opening a new tab will cancel 1751 // fullscreen mode, so we need to wait for that to happen and then 1752 // select the url field. 1753 if (window.document.documentElement.hasAttribute("inDOMFullscreen")) { 1754 window.addEventListener("MozDOMFullscreen:Exited", selectURL, { 1755 once: true, 1756 wantsUntrusted: false, 1757 }); 1758 return; 1759 } 1760 1761 if (!window.fullScreen || newTab.isEmpty) { 1762 selectURL(); 1763 return; 1764 } 1765 } 1766 1767 // Focus the find bar if it was previously focused for that tab. 1768 if ( 1769 gFindBarInitialized && 1770 !gFindBar.hidden && 1771 this.selectedTab._findBarFocused 1772 ) { 1773 gFindBar._findField.focus(); 1774 return; 1775 } 1776 1777 // Don't focus the content area if something has been focused after the 1778 // tab switch was initiated. 1779 if (gMultiProcessBrowser && document.activeElement != document.body) { 1780 return; 1781 } 1782 1783 // We're now committed to focusing the content area. 1784 let fm = Services.focus; 1785 let focusFlags = fm.FLAG_NOSCROLL; 1786 1787 if (!gMultiProcessBrowser) { 1788 let newFocusedElement = fm.getFocusedElementForWindow( 1789 window.content, 1790 true, 1791 {} 1792 ); 1793 1794 // for anchors, use FLAG_SHOWRING so that it is clear what link was 1795 // last clicked when switching back to that tab 1796 if ( 1797 newFocusedElement && 1798 (HTMLAnchorElement.isInstance(newFocusedElement) || 1799 newFocusedElement.getAttributeNS( 1800 "http://www.w3.org/1999/xlink", 1801 "type" 1802 ) == "simple") 1803 ) { 1804 focusFlags |= fm.FLAG_SHOWRING; 1805 } 1806 } 1807 1808 fm.setFocus(newBrowser, focusFlags); 1809 } 1810 1811 _tabAttrModified(aTab, aChanged) { 1812 if (aTab.closing) { 1813 return; 1814 } 1815 1816 let event = new CustomEvent("TabAttrModified", { 1817 bubbles: true, 1818 cancelable: false, 1819 detail: { 1820 changed: aChanged, 1821 }, 1822 }); 1823 aTab.dispatchEvent(event); 1824 } 1825 1826 resetBrowserSharing(aBrowser) { 1827 let tab = this.getTabForBrowser(aBrowser); 1828 if (!tab) { 1829 return; 1830 } 1831 // If WebRTC was used, leave object to enable tracking of grace periods. 1832 tab._sharingState = tab._sharingState?.webRTC ? { webRTC: {} } : {}; 1833 tab.removeAttribute("sharing"); 1834 this._tabAttrModified(tab, ["sharing"]); 1835 if (aBrowser == this.selectedBrowser) { 1836 gPermissionPanel.updateSharingIndicator(); 1837 } 1838 } 1839 1840 updateBrowserSharing(aBrowser, aState) { 1841 let tab = this.getTabForBrowser(aBrowser); 1842 if (!tab) { 1843 return; 1844 } 1845 if (tab._sharingState == null) { 1846 tab._sharingState = {}; 1847 } 1848 tab._sharingState = Object.assign(tab._sharingState, aState); 1849 1850 if ("webRTC" in aState) { 1851 if (tab._sharingState.webRTC?.sharing) { 1852 if (tab._sharingState.webRTC.paused) { 1853 tab.removeAttribute("sharing"); 1854 } else { 1855 tab.setAttribute("sharing", aState.webRTC.sharing); 1856 } 1857 } else { 1858 tab.removeAttribute("sharing"); 1859 } 1860 this._tabAttrModified(tab, ["sharing"]); 1861 } 1862 1863 if (aBrowser == this.selectedBrowser) { 1864 gPermissionPanel.updateSharingIndicator(); 1865 } 1866 } 1867 1868 getTabSharingState(aTab) { 1869 // Normalize the state object for consumers (ie.extensions). 1870 let state = Object.assign( 1871 {}, 1872 aTab._sharingState && aTab._sharingState.webRTC 1873 ); 1874 return { 1875 camera: !!state.camera, 1876 microphone: !!state.microphone, 1877 screen: state.screen && state.screen.replace("Paused", ""), 1878 }; 1879 } 1880 1881 setInitialTabTitle(aTab, aTitle, aOptions = {}) { 1882 // Convert some non-content title (actually a url) to human readable title 1883 if (!aOptions.isContentTitle && isBlankPageURL(aTitle)) { 1884 aTitle = this.tabContainer.emptyTabTitle; 1885 } 1886 1887 if (aTitle) { 1888 if (!aTab.getAttribute("label")) { 1889 aTab._labelIsInitialTitle = true; 1890 } 1891 1892 this._setTabLabel(aTab, aTitle, aOptions); 1893 } 1894 } 1895 1896 _dataURLRegEx = /^data:[^,]+;base64,/i; 1897 1898 // Regex to test if a string (potential tab label) consists of only non- 1899 // printable characters. We consider Unicode categories Separator 1900 // (spaces & line-breaks) and Other (control chars, private use, non- 1901 // character codepoints) to be unprintable, along with a few specific 1902 // characters whose expected rendering is blank: 1903 // U+2800 BRAILLE PATTERN BLANK (category So) 1904 // U+115F HANGUL CHOSEONG FILLER (category Lo) 1905 // U+1160 HANGUL JUNGSEONG FILLER (category Lo) 1906 // U+3164 HANGUL FILLER (category Lo) 1907 // U+FFA0 HALFWIDTH HANGUL FILLER (category Lo) 1908 // We also ignore combining marks, as in the absence of a printable base 1909 // character they are unlikely to be usefully rendered, and may well be 1910 // clipped away entirely. 1911 _nonPrintingRegEx = 1912 /^[\p{Z}\p{C}\p{M}\u{115f}\u{1160}\u{2800}\u{3164}\u{ffa0}]*$/u; 1913 1914 setTabTitle(aTab) { 1915 var browser = this.getBrowserForTab(aTab); 1916 var title = browser.contentTitle; 1917 1918 if (aTab.hasAttribute("customizemode")) { 1919 title = this.tabLocalization.formatValueSync( 1920 "tabbrowser-customizemode-tab-title" 1921 ); 1922 } 1923 1924 // Don't replace an initially set label with the URL while the tab 1925 // is loading. 1926 if (aTab._labelIsInitialTitle) { 1927 if (!title) { 1928 return false; 1929 } 1930 delete aTab._labelIsInitialTitle; 1931 } 1932 1933 let isURL = false; 1934 1935 // Trim leading and trailing whitespace from the title. 1936 title = title.trim(); 1937 1938 // If the title contains only non-printing characters (or only combining 1939 // marks, but no base character for them), we won't use it. 1940 if (this._nonPrintingRegEx.test(title)) { 1941 title = ""; 1942 } 1943 1944 let isContentTitle = !!title; 1945 if (!title) { 1946 // See if we can use the URI as the title. 1947 if (browser.currentURI.displaySpec) { 1948 try { 1949 title = Services.io.createExposableURI( 1950 browser.currentURI 1951 ).displaySpec; 1952 } catch (ex) { 1953 title = browser.currentURI.displaySpec; 1954 } 1955 } 1956 1957 if (title && !isBlankPageURL(title)) { 1958 isURL = true; 1959 if (title.length <= 500 || !this._dataURLRegEx.test(title)) { 1960 // Try to unescape not-ASCII URIs using the current character set. 1961 try { 1962 let characterSet = browser.characterSet; 1963 title = Services.textToSubURI.unEscapeNonAsciiURI( 1964 characterSet, 1965 title 1966 ); 1967 } catch (ex) { 1968 /* Do nothing. */ 1969 } 1970 } 1971 } else { 1972 // No suitable URI? Fall back to our untitled string. 1973 title = this.tabContainer.emptyTabTitle; 1974 } 1975 } 1976 1977 return this._setTabLabel(aTab, title, { isContentTitle, isURL }); 1978 } 1979 1980 // While an auth prompt from a base domain different than the current sites is open, we do not want to show the tab title of the current site, 1981 // but of the origin that is requesting authentication. 1982 // This is to prevent possible auth spoofing scenarios. 1983 // See bug 791594 for reference. 1984 setTabLabelForAuthPrompts(aTab, aLabel) { 1985 return this._setTabLabel(aTab, aLabel); 1986 } 1987 1988 _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { 1989 if (!aLabel || aLabel.includes("about:reader?")) { 1990 return false; 1991 } 1992 1993 // If it's a long data: URI that uses base64 encoding, truncate to a 1994 // reasonable length rather than trying to display the entire thing, 1995 // which can hang or crash the browser. 1996 // We can't shorten arbitrary URIs like this, as bidi etc might mean 1997 // we need the trailing characters for display. But a base64-encoded 1998 // data-URI is plain ASCII, so this is OK for tab-title display. 1999 // (See bug 1408854.) 2000 if (isURL && aLabel.length > 500 && this._dataURLRegEx.test(aLabel)) { 2001 aLabel = aLabel.substring(0, 500) + "\u2026"; 2002 } 2003 2004 aTab._fullLabel = aLabel; 2005 2006 if (!isContentTitle) { 2007 // Remove protocol and "www." 2008 if (!("_regex_shortenURLForTabLabel" in this)) { 2009 this._regex_shortenURLForTabLabel = /^[^:]+:\/\/(?:www\.)?/; 2010 } 2011 aLabel = aLabel.replace(this._regex_shortenURLForTabLabel, ""); 2012 } 2013 2014 aTab._labelIsContentTitle = isContentTitle; 2015 2016 if (aTab.getAttribute("label") == aLabel) { 2017 return false; 2018 } 2019 2020 let dwu = window.windowUtils; 2021 let isRTL = 2022 dwu.getDirectionFromText(aLabel) == Ci.nsIDOMWindowUtils.DIRECTION_RTL; 2023 2024 aTab.setAttribute("label", aLabel); 2025 aTab.setAttribute("labeldirection", isRTL ? "rtl" : "ltr"); 2026 aTab.toggleAttribute("labelendaligned", isRTL != (document.dir == "rtl")); 2027 2028 // Dispatch TabAttrModified event unless we're setting the label 2029 // before the TabOpen event was dispatched. 2030 if (!beforeTabOpen) { 2031 this._tabAttrModified(aTab, ["label"]); 2032 } 2033 2034 if (aTab.selected) { 2035 this.updateTitlebar(); 2036 } 2037 2038 return true; 2039 } 2040 2041 loadTabs( 2042 aURIs, 2043 { 2044 allowInheritPrincipal, 2045 allowThirdPartyFixup, 2046 inBackground, 2047 newIndex, 2048 elementIndex, 2049 postDatas, 2050 replace, 2051 tabGroup, 2052 targetTab, 2053 triggeringPrincipal, 2054 policyContainer, 2055 userContextId, 2056 fromExternal, 2057 } = {} 2058 ) { 2059 if (!aURIs.length) { 2060 return; 2061 } 2062 2063 // The tab selected after this new tab is closed (i.e. the new tab's 2064 // "owner") is the next adjacent tab (i.e. not the previously viewed tab) 2065 // when several urls are opened here (i.e. closing the first should select 2066 // the next of many URLs opened) or if the pref to have UI links opened in 2067 // the background is set (i.e. the link is not being opened modally) 2068 // 2069 // i.e. 2070 // Number of URLs Load UI Links in BG Focus Last Viewed? 2071 // == 1 false YES 2072 // == 1 true NO 2073 // > 1 false/true NO 2074 var multiple = aURIs.length > 1; 2075 var owner = multiple || inBackground ? null : this.selectedTab; 2076 var firstTabAdded = null; 2077 var targetTabIndex = -1; 2078 2079 if (typeof elementIndex == "number") { 2080 newIndex = this.#elementIndexToTabIndex(elementIndex); 2081 } 2082 if (typeof newIndex != "number") { 2083 newIndex = -1; 2084 } 2085 2086 // When bulk opening tabs, such as from a bookmark folder, we want to insertAfterCurrent 2087 // if necessary, but we also will set the bulkOrderedOpen flag so that the bookmarks 2088 // open in the same order they are in the folder. 2089 if ( 2090 multiple && 2091 newIndex < 0 && 2092 Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent") 2093 ) { 2094 newIndex = this.selectedTab._tPos + 1; 2095 } 2096 2097 if (replace) { 2098 if (this.isTabGroupLabel(targetTab)) { 2099 throw new Error( 2100 "Replacing a tab group label with a tab is not supported" 2101 ); 2102 } 2103 let browser; 2104 if (targetTab) { 2105 browser = this.getBrowserForTab(targetTab); 2106 targetTabIndex = targetTab._tPos; 2107 } else { 2108 browser = this.selectedBrowser; 2109 targetTabIndex = this.tabContainer.selectedIndex; 2110 } 2111 let loadFlags = LOAD_FLAGS_NONE; 2112 if (allowThirdPartyFixup) { 2113 loadFlags |= 2114 LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | LOAD_FLAGS_FIXUP_SCHEME_TYPOS; 2115 } 2116 if (!allowInheritPrincipal) { 2117 loadFlags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; 2118 } 2119 if (fromExternal) { 2120 loadFlags |= LOAD_FLAGS_FROM_EXTERNAL; 2121 } 2122 try { 2123 browser.fixupAndLoadURIString(aURIs[0], { 2124 loadFlags, 2125 postData: postDatas && postDatas[0], 2126 triggeringPrincipal, 2127 policyContainer, 2128 }); 2129 } catch (e) { 2130 // Ignore failure in case a URI is wrong, so we can continue 2131 // opening the next ones. 2132 } 2133 } else { 2134 let params = { 2135 allowInheritPrincipal, 2136 ownerTab: owner, 2137 skipAnimation: multiple, 2138 allowThirdPartyFixup, 2139 postData: postDatas && postDatas[0], 2140 userContextId, 2141 triggeringPrincipal, 2142 bulkOrderedOpen: multiple, 2143 policyContainer, 2144 fromExternal, 2145 tabGroup, 2146 }; 2147 if (newIndex > -1) { 2148 params.tabIndex = newIndex; 2149 } 2150 firstTabAdded = this.addTab(aURIs[0], params); 2151 if (newIndex > -1) { 2152 targetTabIndex = firstTabAdded._tPos; 2153 } 2154 } 2155 2156 let tabNum = targetTabIndex; 2157 for (let i = 1; i < aURIs.length; ++i) { 2158 let params = { 2159 allowInheritPrincipal, 2160 skipAnimation: true, 2161 allowThirdPartyFixup, 2162 postData: postDatas && postDatas[i], 2163 userContextId, 2164 triggeringPrincipal, 2165 bulkOrderedOpen: true, 2166 policyContainer, 2167 fromExternal, 2168 tabGroup, 2169 }; 2170 if (targetTabIndex > -1) { 2171 params.tabIndex = ++tabNum; 2172 } 2173 this.addTab(aURIs[i], params); 2174 } 2175 2176 if (firstTabAdded && !inBackground) { 2177 this.selectedTab = firstTabAdded; 2178 } 2179 } 2180 2181 updateBrowserRemoteness(aBrowser, { newFrameloader, remoteType } = {}) { 2182 let isRemote = aBrowser.getAttribute("remote") == "true"; 2183 2184 // We have to be careful with this here, as the "no remote type" is null, 2185 // not a string. Make sure to check only for undefined, since null is 2186 // allowed. 2187 if (remoteType === undefined) { 2188 throw new Error("Remote type must be set!"); 2189 } 2190 2191 let shouldBeRemote = remoteType !== E10SUtils.NOT_REMOTE; 2192 2193 if (!gMultiProcessBrowser && shouldBeRemote) { 2194 throw new Error( 2195 "Cannot switch to remote browser in a window " + 2196 "without the remote tabs load context." 2197 ); 2198 } 2199 2200 // Abort if we're not going to change anything 2201 let oldRemoteType = aBrowser.remoteType; 2202 if ( 2203 isRemote == shouldBeRemote && 2204 !newFrameloader && 2205 (!isRemote || oldRemoteType == remoteType) 2206 ) { 2207 return false; 2208 } 2209 2210 let tab = this.getTabForBrowser(aBrowser); 2211 // aBrowser needs to be inserted now if it hasn't been already. 2212 this._insertBrowser(tab); 2213 2214 let evt = document.createEvent("Events"); 2215 evt.initEvent("BeforeTabRemotenessChange", true, false); 2216 tab.dispatchEvent(evt); 2217 2218 // Unhook our progress listener. 2219 let filter = this._tabFilters.get(tab); 2220 let listener = this._tabListeners.get(tab); 2221 // We should always have a filter, but if we fail to create a content 2222 // process when creating a new tab, we can end up here trying to switch 2223 // remoteness to load about:tabcrashed, without a filter/listener. 2224 if (filter) { 2225 aBrowser.webProgress.removeProgressListener(filter); 2226 filter.removeProgressListener(listener); 2227 } 2228 2229 // We'll be creating a new listener, so destroy the old one. 2230 listener?.destroy(); 2231 2232 let oldDroppedLinkHandler = aBrowser.droppedLinkHandler; 2233 let oldUserTypedValue = aBrowser.userTypedValue; 2234 let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping(); 2235 2236 // Change the "remote" attribute. 2237 2238 // Make sure the browser is destroyed so it unregisters from observer notifications 2239 aBrowser.destroy(); 2240 2241 if (shouldBeRemote) { 2242 aBrowser.setAttribute("remote", "true"); 2243 aBrowser.setAttribute("remoteType", remoteType); 2244 } else { 2245 aBrowser.setAttribute("remote", "false"); 2246 aBrowser.removeAttribute("remoteType"); 2247 } 2248 2249 // This call actually switches out our frameloaders. Do this as late as 2250 // possible before rebuilding the browser, as we'll need the new browser 2251 // state set up completely first. 2252 aBrowser.changeRemoteness({ 2253 remoteType, 2254 }); 2255 2256 // Once we have new frameloaders, this call sets the browser back up. 2257 aBrowser.construct(); 2258 2259 aBrowser.userTypedValue = oldUserTypedValue; 2260 if (hadStartedLoad) { 2261 aBrowser.urlbarChangeTracker.startedLoad(); 2262 } 2263 2264 aBrowser.droppedLinkHandler = oldDroppedLinkHandler; 2265 2266 // This shouldn't really be necessary, however, this has the side effect 2267 // of sending MozLayerTreeReady / MozLayerTreeCleared events for remote 2268 // frames, which the tab switcher depends on. 2269 // 2270 // eslint-disable-next-line no-self-assign 2271 aBrowser.docShellIsActive = aBrowser.docShellIsActive; 2272 2273 // Create a new tab progress listener for the new browser we just injected, 2274 // since tab progress listeners have logic for handling the initial about:blank 2275 // load 2276 listener = new TabProgressListener(tab, aBrowser, true, false); 2277 this._tabListeners.set(tab, listener); 2278 if (!filter) { 2279 filter = Cc[ 2280 "@mozilla.org/appshell/component/browser-status-filter;1" 2281 ].createInstance(Ci.nsIWebProgress); 2282 this._tabFilters.set(tab, filter); 2283 } 2284 filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL); 2285 2286 // Restore the progress listener. 2287 aBrowser.webProgress.addProgressListener( 2288 filter, 2289 Ci.nsIWebProgress.NOTIFY_ALL 2290 ); 2291 2292 // Restore the securityUI state. 2293 let securityUI = aBrowser.securityUI; 2294 let state = securityUI 2295 ? securityUI.state 2296 : Ci.nsIWebProgressListener.STATE_IS_INSECURE; 2297 this._callProgressListeners( 2298 aBrowser, 2299 "onSecurityChange", 2300 [aBrowser.webProgress, null, state], 2301 true, 2302 false 2303 ); 2304 let event = aBrowser.getContentBlockingEvents(); 2305 // Include the true final argument to indicate that this event is 2306 // simulated (instead of being observed by the webProgressListener). 2307 this._callProgressListeners( 2308 aBrowser, 2309 "onContentBlockingEvent", 2310 [aBrowser.webProgress, null, event, true], 2311 true, 2312 false 2313 ); 2314 2315 if (shouldBeRemote) { 2316 // Switching the browser to be remote will connect to a new child 2317 // process so the browser can no longer be considered to be 2318 // crashed. 2319 tab.removeAttribute("crashed"); 2320 } 2321 2322 // If the findbar has been initialised, reset its browser reference. 2323 if (this.isFindBarInitialized(tab)) { 2324 this.getCachedFindBar(tab).browser = aBrowser; 2325 } 2326 2327 evt = document.createEvent("Events"); 2328 evt.initEvent("TabRemotenessChange", true, false); 2329 tab.dispatchEvent(evt); 2330 2331 return true; 2332 } 2333 2334 updateBrowserRemotenessByURL(aBrowser, aURL, aOptions = {}) { 2335 if (!gMultiProcessBrowser) { 2336 return this.updateBrowserRemoteness(aBrowser, { 2337 remoteType: E10SUtils.NOT_REMOTE, 2338 }); 2339 } 2340 2341 let oldRemoteType = aBrowser.remoteType; 2342 2343 let oa = E10SUtils.predictOriginAttributes({ browser: aBrowser }); 2344 2345 aOptions.remoteType = E10SUtils.getRemoteTypeForURI( 2346 aURL, 2347 gMultiProcessBrowser, 2348 gFissionBrowser, 2349 oldRemoteType, 2350 aBrowser.currentURI, 2351 oa 2352 ); 2353 2354 // If this URL can't load in the current browser then flip it to the 2355 // correct type. 2356 if (oldRemoteType != aOptions.remoteType || aOptions.newFrameloader) { 2357 return this.updateBrowserRemoteness(aBrowser, aOptions); 2358 } 2359 2360 return false; 2361 } 2362 2363 createBrowser({ 2364 isPreloadBrowser, 2365 name, 2366 openWindowInfo, 2367 remoteType, 2368 initialBrowsingContextGroupId, 2369 uriIsAboutBlank, 2370 userContextId, 2371 skipLoad, 2372 } = {}) { 2373 let b = document.createXULElement("browser"); 2374 // Use the JSM global to create the permanentKey, so that if the 2375 // permanentKey is held by something after this window closes, it 2376 // doesn't keep the window alive. 2377 b.permanentKey = new (Cu.getGlobalForObject(Services).Object)(); 2378 2379 const defaultBrowserAttributes = { 2380 contextmenu: "contentAreaContextMenu", 2381 message: "true", 2382 messagemanagergroup: "browsers", 2383 tooltip: "aHTMLTooltip", 2384 type: "content", 2385 manualactiveness: "true", 2386 }; 2387 for (let attribute in defaultBrowserAttributes) { 2388 b.setAttribute(attribute, defaultBrowserAttributes[attribute]); 2389 } 2390 2391 if (gMultiProcessBrowser || remoteType) { 2392 b.setAttribute("maychangeremoteness", "true"); 2393 } 2394 2395 if (userContextId) { 2396 b.setAttribute("usercontextid", userContextId); 2397 } 2398 2399 if (remoteType) { 2400 b.setAttribute("remoteType", remoteType); 2401 b.setAttribute("remote", "true"); 2402 } 2403 2404 if (!isPreloadBrowser) { 2405 b.setAttribute("autocompletepopup", "PopupAutoComplete"); 2406 } 2407 2408 /* 2409 * This attribute is meant to describe if the browser is the 2410 * preloaded browser. When the preloaded browser is created, the 2411 * 'preloadedState' attribute for that browser is set to "preloaded", and 2412 * when a new tab is opened, and it is time to show that preloaded 2413 * browser, the 'preloadedState' attribute for that browser is removed. 2414 * 2415 * See more details on Bug 1420285. 2416 */ 2417 if (isPreloadBrowser) { 2418 b.setAttribute("preloadedState", "preloaded"); 2419 } 2420 2421 // Ensure that the browser will be created in a specific initial 2422 // BrowsingContextGroup. This may change the process selection behaviour 2423 // of the newly created browser, and is often used in combination with 2424 // "remoteType" to ensure that the initial about:blank load occurs 2425 // within the same process as another window. 2426 if (initialBrowsingContextGroupId) { 2427 b.setAttribute( 2428 "initialBrowsingContextGroupId", 2429 initialBrowsingContextGroupId 2430 ); 2431 } 2432 2433 // Propagate information about the opening content window to the browser. 2434 if (openWindowInfo) { 2435 b.openWindowInfo = openWindowInfo; 2436 } 2437 2438 // This will be used by gecko to control the name of the opened 2439 // window. 2440 if (name) { 2441 // XXX: The `name` property is special in HTML and XUL. Should 2442 // we use a different attribute name for this? 2443 b.setAttribute("name", name); 2444 } 2445 2446 if (AIWindow.isAIWindowActive(window) || this._allowTransparentBrowser) { 2447 b.setAttribute("transparent", "true"); 2448 } 2449 2450 let stack = document.createXULElement("stack"); 2451 stack.className = "browserStack"; 2452 stack.appendChild(b); 2453 2454 let decorator = document.createXULElement("hbox"); 2455 decorator.className = "browserDecorator"; 2456 stack.appendChild(decorator); 2457 2458 let browserContainer = document.createXULElement("vbox"); 2459 browserContainer.className = "browserContainer"; 2460 browserContainer.appendChild(stack); 2461 2462 let browserSidebarContainer = document.createXULElement("hbox"); 2463 browserSidebarContainer.className = "browserSidebarContainer"; 2464 browserSidebarContainer.appendChild(browserContainer); 2465 2466 // Prevent the superfluous initial load of a blank document 2467 // if we're going to load something other than about:blank. 2468 if (!uriIsAboutBlank || skipLoad) { 2469 b.setAttribute("nodefaultsrc", "true"); 2470 } 2471 2472 return b; 2473 } 2474 2475 _createLazyBrowser(aTab) { 2476 let browser = aTab.linkedBrowser; 2477 2478 let names = this._browserBindingProperties; 2479 2480 for (let i = 0; i < names.length; i++) { 2481 let name = names[i]; 2482 let getter; 2483 let setter; 2484 switch (name) { 2485 case "audioMuted": 2486 getter = () => aTab.hasAttribute("muted"); 2487 break; 2488 case "contentTitle": 2489 getter = () => SessionStore.getLazyTabValue(aTab, "title"); 2490 break; 2491 case "currentURI": 2492 getter = () => { 2493 // Avoid recreating the same nsIURI object over and over again... 2494 if (browser._cachedCurrentURI) { 2495 return browser._cachedCurrentURI; 2496 } 2497 let url = 2498 SessionStore.getLazyTabValue(aTab, "url") || "about:blank"; 2499 return (browser._cachedCurrentURI = Services.io.newURI(url)); 2500 }; 2501 break; 2502 case "didStartLoadSinceLastUserTyping": 2503 getter = () => () => false; 2504 break; 2505 case "fullZoom": 2506 case "textZoom": 2507 getter = () => 1; 2508 break; 2509 case "tabHasCustomZoom": 2510 getter = () => false; 2511 break; 2512 case "getTabBrowser": 2513 getter = () => () => this; 2514 break; 2515 case "isRemoteBrowser": 2516 getter = () => browser.getAttribute("remote") == "true"; 2517 break; 2518 case "permitUnload": 2519 getter = () => () => ({ permitUnload: true }); 2520 break; 2521 case "reload": 2522 case "reloadWithFlags": 2523 getter = () => params => { 2524 // Wait for load handler to be instantiated before 2525 // initializing the reload. 2526 aTab.addEventListener( 2527 "SSTabRestoring", 2528 () => { 2529 browser[name](params); 2530 }, 2531 { once: true } 2532 ); 2533 gBrowser._insertBrowser(aTab); 2534 }; 2535 break; 2536 case "remoteType": 2537 getter = () => { 2538 let url = 2539 SessionStore.getLazyTabValue(aTab, "url") || "about:blank"; 2540 // Avoid recreating the same nsIURI object over and over again... 2541 let uri; 2542 if (browser._cachedCurrentURI) { 2543 uri = browser._cachedCurrentURI; 2544 } else { 2545 uri = browser._cachedCurrentURI = Services.io.newURI(url); 2546 } 2547 let oa = E10SUtils.predictOriginAttributes({ 2548 browser, 2549 userContextId: aTab.getAttribute("usercontextid"), 2550 }); 2551 return E10SUtils.getRemoteTypeForURI( 2552 url, 2553 gMultiProcessBrowser, 2554 gFissionBrowser, 2555 undefined, 2556 uri, 2557 oa 2558 ); 2559 }; 2560 break; 2561 case "userTypedValue": 2562 case "userTypedClear": 2563 getter = () => SessionStore.getLazyTabValue(aTab, name); 2564 break; 2565 default: 2566 getter = () => { 2567 if (AppConstants.NIGHTLY_BUILD) { 2568 let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`; 2569 Services.console.logStringMessage(message + new Error().stack); 2570 } 2571 this._insertBrowser(aTab); 2572 return browser[name]; 2573 }; 2574 setter = value => { 2575 if (AppConstants.NIGHTLY_BUILD) { 2576 let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`; 2577 Services.console.logStringMessage(message + new Error().stack); 2578 } 2579 this._insertBrowser(aTab); 2580 return (browser[name] = value); 2581 }; 2582 } 2583 Object.defineProperty(browser, name, { 2584 get: getter, 2585 set: setter, 2586 configurable: true, 2587 enumerable: true, 2588 }); 2589 } 2590 } 2591 2592 _insertBrowser(aTab, aInsertedOnTabCreation) { 2593 "use strict"; 2594 2595 // If browser is already inserted or window is closed don't do anything. 2596 if (aTab.linkedPanel || window.closed) { 2597 return; 2598 } 2599 2600 let browser = aTab.linkedBrowser; 2601 2602 // If browser is a lazy browser, delete the substitute properties. 2603 if (this._browserBindingProperties[0] in browser) { 2604 for (let name of this._browserBindingProperties) { 2605 delete browser[name]; 2606 } 2607 } 2608 2609 let { uriIsAboutBlank, usingPreloadedContent } = aTab._browserParams; 2610 delete aTab._browserParams; 2611 delete browser._cachedCurrentURI; 2612 2613 let panel = this.getPanel(browser); 2614 let uniqueId = this._generateUniquePanelID(); 2615 panel.id = uniqueId; 2616 aTab.linkedPanel = uniqueId; 2617 2618 // Inject the <browser> into the DOM if necessary. 2619 if (!panel.parentNode) { 2620 // NB: this appendChild call causes us to run constructors for the 2621 // browser element, which fires off a bunch of notifications. Some 2622 // of those notifications can cause code to run that inspects our 2623 // state, so it is important that the tab element is fully 2624 // initialized by this point. 2625 // AppendChild will cause a synchronous about:blank load. 2626 this.tabpanels.appendChild(panel); 2627 } 2628 2629 // wire up a progress listener for the new browser object. 2630 let tabListener = new TabProgressListener( 2631 aTab, 2632 browser, 2633 uriIsAboutBlank, 2634 usingPreloadedContent 2635 ); 2636 const filter = Cc[ 2637 "@mozilla.org/appshell/component/browser-status-filter;1" 2638 ].createInstance(Ci.nsIWebProgress); 2639 filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); 2640 browser.webProgress.addProgressListener( 2641 filter, 2642 Ci.nsIWebProgress.NOTIFY_ALL 2643 ); 2644 this._tabListeners.set(aTab, tabListener); 2645 this._tabFilters.set(aTab, filter); 2646 2647 browser.droppedLinkHandler = handleDroppedLink; 2648 browser.loadURI = URILoadingWrapper.loadURI.bind( 2649 URILoadingWrapper, 2650 browser 2651 ); 2652 browser.fixupAndLoadURIString = 2653 URILoadingWrapper.fixupAndLoadURIString.bind( 2654 URILoadingWrapper, 2655 browser 2656 ); 2657 2658 // Most of the time, we start our browser's docShells out as inactive, 2659 // and then maintain activeness in the tab switcher. Preloaded about:newtab's 2660 // are already created with their docShell's as inactive, but then explicitly 2661 // render their layers to ensure that we can switch to them quickly. We avoid 2662 // setting docShellIsActive to false again in this case, since that'd cause 2663 // the layers for the preloaded tab to be dropped, and we'd see a flash 2664 // of empty content instead. 2665 // 2666 // So for all browsers except for the preloaded case, we set the browser 2667 // docShell to inactive. 2668 if (!usingPreloadedContent) { 2669 browser.docShellIsActive = false; 2670 } 2671 2672 // If we transitioned from one browser to two browsers, we need to set 2673 // hasSiblings=false on both the existing browser and the new browser. 2674 if (this.tabs.length == 2) { 2675 this.tabs[0].linkedBrowser.browsingContext.hasSiblings = true; 2676 this.tabs[1].linkedBrowser.browsingContext.hasSiblings = true; 2677 } else { 2678 aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; 2679 } 2680 2681 if (aTab.userContextId) { 2682 browser.setAttribute("usercontextid", aTab.userContextId); 2683 } 2684 2685 browser.browsingContext.isAppTab = aTab.pinned; 2686 2687 // We don't want to update the container icon and identifier if 2688 // this is not the selected browser. 2689 if (aTab.selected) { 2690 updateUserContextUIIndicator(); 2691 } 2692 2693 // Only fire this event if the tab is already in the DOM 2694 // and will be handled by a listener. 2695 if (aTab.isConnected) { 2696 var evt = new CustomEvent("TabBrowserInserted", { 2697 bubbles: true, 2698 detail: { insertedOnTabCreation: aInsertedOnTabCreation }, 2699 }); 2700 aTab.dispatchEvent(evt); 2701 } 2702 } 2703 2704 _mayDiscardBrowser(aTab, aForceDiscard) { 2705 let browser = aTab.linkedBrowser; 2706 let action = aForceDiscard ? "unload" : "dontUnload"; 2707 2708 if ( 2709 !aTab || 2710 aTab.selected || 2711 aTab.closing || 2712 this._windowIsClosing || 2713 !browser.isConnected || 2714 !browser.isRemoteBrowser || 2715 !browser.permitUnload(action).permitUnload 2716 ) { 2717 return false; 2718 } 2719 2720 // discarding a browser will dismiss any dialogs, so don't 2721 // allow this unless we're forcing it. 2722 if ( 2723 !aForceDiscard && 2724 this.getTabDialogBox(browser)._tabDialogManager._dialogs.length 2725 ) { 2726 return false; 2727 } 2728 2729 return true; 2730 } 2731 2732 async prepareDiscardBrowser(aTab) { 2733 let browser = aTab.linkedBrowser; 2734 // This is similar to the checks in _mayDiscardBrowser, but 2735 // doesn't have to be complete (and we want to be sure not to 2736 // fire the beforeunload event). Calling TabStateFlusher.flush() 2737 // and then not unloading the browser is fine. 2738 if (aTab.closing || this._windowIsClosing || !browser.isRemoteBrowser) { 2739 return; 2740 } 2741 2742 // Flush the tab's state so session restore has the latest data. 2743 await this.TabStateFlusher.flush(browser); 2744 } 2745 2746 discardBrowser(aTab, aForceDiscard) { 2747 "use strict"; 2748 let browser = aTab.linkedBrowser; 2749 2750 if (!this._mayDiscardBrowser(aTab, aForceDiscard)) { 2751 return false; 2752 } 2753 2754 // Reset sharing state. 2755 if (aTab._sharingState) { 2756 this.resetBrowserSharing(browser); 2757 } 2758 webrtcUI.forgetStreamsFromBrowserContext(browser.browsingContext); 2759 2760 // Abort any dialogs since the browser is about to be discarded. 2761 let tabDialogBox = this.getTabDialogBox(browser); 2762 tabDialogBox.abortAllDialogs(); 2763 2764 // Set browser parameters for when browser is restored. Also remove 2765 // listeners and set up lazy restore data in SessionStore. This must 2766 // be done before browser is destroyed and removed from the document. 2767 aTab._browserParams = { 2768 uriIsAboutBlank: browser.currentURI.spec == "about:blank", 2769 remoteType: browser.remoteType, 2770 usingPreloadedContent: false, 2771 }; 2772 2773 SessionStore.resetBrowserToLazyState(aTab); 2774 // Indicate that this tab was explicitly unloaded (i.e. not 2775 // from a session restore) in case we want to style that 2776 // differently. 2777 if (aForceDiscard) { 2778 aTab.toggleAttribute("discarded", true); 2779 } 2780 2781 // Remove the tab's filter and progress listener. 2782 let filter = this._tabFilters.get(aTab); 2783 let listener = this._tabListeners.get(aTab); 2784 browser.webProgress.removeProgressListener(filter); 2785 filter.removeProgressListener(listener); 2786 listener.destroy(); 2787 2788 this._tabListeners.delete(aTab); 2789 this._tabFilters.delete(aTab); 2790 2791 // Reset the findbar and remove it if it is attached to the tab. 2792 if (aTab._findBar) { 2793 aTab._findBar.close(true); 2794 aTab._findBar.remove(); 2795 delete aTab._findBar; 2796 } 2797 2798 // Remove potentially stale attributes. 2799 let attributesToRemove = [ 2800 "activemedia-blocked", 2801 "busy", 2802 "pendingicon", 2803 "progress", 2804 "soundplaying", 2805 ]; 2806 let removedAttributes = []; 2807 for (let attr of attributesToRemove) { 2808 if (aTab.hasAttribute(attr)) { 2809 removedAttributes.push(attr); 2810 aTab.removeAttribute(attr); 2811 } 2812 } 2813 if (removedAttributes.length) { 2814 this._tabAttrModified(aTab, removedAttributes); 2815 } 2816 2817 browser.destroy(); 2818 this.getPanel(browser).remove(); 2819 aTab.removeAttribute("linkedpanel"); 2820 2821 this._createLazyBrowser(aTab); 2822 2823 let evt = new CustomEvent("TabBrowserDiscarded", { bubbles: true }); 2824 aTab.dispatchEvent(evt); 2825 return true; 2826 } 2827 2828 /** 2829 * Loads a tab with a default null principal unless specified 2830 * 2831 * @param {string} aURI 2832 * @param {object} [params] 2833 * Options from `Tabbrowser.addTab`. 2834 */ 2835 addWebTab(aURI, params = {}) { 2836 if (!params.triggeringPrincipal) { 2837 params.triggeringPrincipal = 2838 Services.scriptSecurityManager.createNullPrincipal({ 2839 userContextId: params.userContextId, 2840 }); 2841 } 2842 if (params.triggeringPrincipal.isSystemPrincipal) { 2843 throw new Error( 2844 "System principal should never be passed into addWebTab()" 2845 ); 2846 } 2847 return this.addTab(aURI, params); 2848 } 2849 2850 /** 2851 * @param {MozTabbrowserTab} tab 2852 * @returns {void} 2853 * New tab will be the `Tabbrowser.selectedTab` or the subject of a 2854 * notification on the `browser-open-newtab-start` topic. 2855 */ 2856 addAdjacentNewTab(tab) { 2857 Services.obs.notifyObservers( 2858 { 2859 wrappedJSObject: new Promise(resolve => { 2860 this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { 2861 tabIndex: tab._tPos + 1, 2862 userContextId: tab.userContextId, 2863 tabGroup: tab.group, 2864 focusUrlBar: true, 2865 }); 2866 resolve(this.selectedBrowser); 2867 }), 2868 }, 2869 "browser-open-newtab-start" 2870 ); 2871 } 2872 2873 /** 2874 * Creates a tab directly after `tab`. 2875 * 2876 * @param {MozTabbrowserTab} adjacentTab 2877 * @param {string} uriString 2878 * @param {object} [options] 2879 * Options from `Tabbrowser.addTab`. 2880 */ 2881 addAdjacentTab(adjacentTab, uriString, options = {}) { 2882 // If the caller opted out of tab group membership but `tab` is in a 2883 // tab group, insert the tab after `tab`'s group. Otherwise, insert the 2884 // new tab right after `tab`. 2885 const tabIndex = 2886 !options.tabGroup && adjacentTab.group 2887 ? adjacentTab.group.tabs.at(-1)._tPos + 1 2888 : adjacentTab._tPos + 1; 2889 2890 return this.addTab(uriString, { 2891 ...options, 2892 tabIndex, 2893 }); 2894 } 2895 2896 /** 2897 * Must only be used sparingly for content that came from Chrome context 2898 * If in doubt use addWebTab 2899 * 2900 * @param {string} aURI 2901 * @param {object} [options] 2902 * @see this.addTab options 2903 * @returns {MozTabbrowserTab|null} 2904 */ 2905 addTrustedTab(aURI, options = {}) { 2906 options.triggeringPrincipal = 2907 Services.scriptSecurityManager.getSystemPrincipal(); 2908 return this.addTab(aURI, options); 2909 } 2910 2911 /** 2912 * @param {string} uriString 2913 * @param {object} options 2914 * @param {object} [options.eventDetail] 2915 * Additional information to include in the `CustomEvent.detail` 2916 * of the resulting TabOpen event. 2917 * @param {boolean} [options.fromExternal] 2918 * Whether this tab was opened from a URL supplied to Firefox from an 2919 * external application. 2920 * @param {MozTabbrowserTabGroup} [options.tabGroup] 2921 * A related tab group where this tab should be added, when applicable. 2922 * When present, the tab is expected to reside in this tab group. When 2923 * absent, the tab is expected to be a standalone tab. 2924 * @returns {MozTabbrowserTab|null} 2925 * The new tab. The return value will be null if the tab couldn't be 2926 * created; this shouldn't normally happen, and an error will be logged 2927 * to the console if it does. 2928 */ 2929 addTab( 2930 uriString, 2931 { 2932 allowInheritPrincipal, 2933 allowThirdPartyFixup, 2934 bulkOrderedOpen, 2935 charset, 2936 createLazyBrowser, 2937 eventDetail, 2938 focusUrlBar, 2939 forceNotRemote, 2940 forceAllowDataURI, 2941 fromExternal, 2942 inBackground = true, 2943 isCaptivePortalTab, 2944 elementIndex, 2945 tabIndex, 2946 lazyTabTitle, 2947 name, 2948 noInitialLabel, 2949 openWindowInfo, 2950 openerBrowser, 2951 originPrincipal, 2952 originStoragePrincipal, 2953 ownerTab, 2954 pinned, 2955 postData, 2956 preferredRemoteType, 2957 referrerInfo, 2958 relatedToCurrent, 2959 initialBrowsingContextGroupId, 2960 skipAnimation, 2961 skipBackgroundNotify, 2962 tabGroup, 2963 triggeringPrincipal, 2964 userContextId, 2965 policyContainer, 2966 skipLoad = createLazyBrowser, 2967 insertTab = true, 2968 globalHistoryOptions, 2969 triggeringRemoteType, 2970 schemelessInput, 2971 hasValidUserGestureActivation = false, 2972 textDirectiveUserActivation = false, 2973 } = {} 2974 ) { 2975 // all callers of addTab that pass a params object need to pass 2976 // a valid triggeringPrincipal. 2977 if (!triggeringPrincipal) { 2978 throw new Error( 2979 "Required argument triggeringPrincipal missing within addTab" 2980 ); 2981 } 2982 2983 if (!UserInteraction.running("browser.tabs.opening", window)) { 2984 UserInteraction.start("browser.tabs.opening", "initting", window); 2985 } 2986 2987 // If we're opening a foreground tab, set the owner by default. 2988 ownerTab ??= inBackground ? null : this.selectedTab; 2989 2990 // if we're adding tabs, we're past interrupt mode, ditch the owner 2991 if (this.selectedTab.owner) { 2992 this.selectedTab.owner = null; 2993 } 2994 2995 // Find the tab that opened this one, if any. This is used for 2996 // determining positioning, and inherited attributes such as the 2997 // user context ID. 2998 // 2999 // If we have a browser opener (which is usually the browser 3000 // element from a remote window.open() call), use that. 3001 // 3002 // Otherwise, if the tab is related to the current tab (e.g., 3003 // because it was opened by a link click), use the selected tab as 3004 // the owner. If referrerInfo is set, and we don't have an 3005 // explicit relatedToCurrent arg, we assume that the tab is 3006 // related to the current tab, since referrerURI is null or 3007 // undefined if the tab is opened from an external application or 3008 // bookmark (i.e. somewhere other than an existing tab). 3009 if (relatedToCurrent == null) { 3010 relatedToCurrent = !!(referrerInfo && referrerInfo.originalReferrer); 3011 } 3012 let openerTab = 3013 (openerBrowser && this.getTabForBrowser(openerBrowser)) || 3014 (relatedToCurrent && this.selectedTab) || 3015 null; 3016 3017 // When overflowing, new tabs are scrolled into view smoothly, which 3018 // doesn't go well together with the width transition. So we skip the 3019 // transition in that case. 3020 let animate = 3021 !skipAnimation && 3022 !pinned && 3023 !this.tabContainer.verticalMode && 3024 !this.tabContainer.overflowing && 3025 !gReduceMotion; 3026 3027 let uriInfo = this._determineURIToLoad(uriString, createLazyBrowser); 3028 let { uri, uriIsAboutBlank, lazyBrowserURI } = uriInfo; 3029 // Have to overwrite this if we're lazy-loading. Should go away 3030 // with bug 1818777. 3031 ({ uriString } = uriInfo); 3032 3033 let usingPreloadedContent = false; 3034 let b, t; 3035 3036 try { 3037 t = this._createTab({ 3038 uriString, 3039 animate, 3040 userContextId, 3041 openerTab, 3042 pinned, 3043 noInitialLabel, 3044 skipBackgroundNotify, 3045 }); 3046 if (insertTab) { 3047 // Insert the tab into the tab container in the correct position. 3048 this.#insertTabAtIndex(t, { 3049 elementIndex, 3050 tabIndex, 3051 ownerTab, 3052 openerTab, 3053 pinned, 3054 bulkOrderedOpen, 3055 tabGroup: tabGroup ?? openerTab?.group, 3056 }); 3057 } 3058 3059 ({ browser: b, usingPreloadedContent } = this._createBrowserForTab(t, { 3060 uriString, 3061 uri, 3062 preferredRemoteType, 3063 openerBrowser, 3064 uriIsAboutBlank, 3065 referrerInfo, 3066 forceNotRemote, 3067 name, 3068 initialBrowsingContextGroupId, 3069 openWindowInfo, 3070 skipLoad, 3071 triggeringRemoteType, 3072 })); 3073 3074 if (focusUrlBar) { 3075 gURLBar.getBrowserState(b).urlbarFocused = true; 3076 } 3077 3078 // If the caller opts in, create a lazy browser. 3079 if (createLazyBrowser) { 3080 this._createLazyBrowser(t); 3081 3082 if (lazyBrowserURI) { 3083 // Lazy browser must be explicitly registered so tab will appear as 3084 // a switch-to-tab candidate in autocomplete. 3085 this.UrlbarProviderOpenTabs.registerOpenTab( 3086 lazyBrowserURI.spec, 3087 t.userContextId, 3088 tabGroup?.id, 3089 PrivateBrowsingUtils.isWindowPrivate(window) 3090 ); 3091 b.registeredOpenURI = lazyBrowserURI; 3092 } 3093 // If we're not inserting the tab into the DOM, we can't set the tab 3094 // state meaningfully. Session restore (the only caller who does this) 3095 // will have to do this work itself later, when the tabs have been 3096 // inserted. 3097 if (insertTab) { 3098 SessionStore.setTabState(t, { 3099 entries: [ 3100 { 3101 url: lazyBrowserURI?.spec || "about:blank", 3102 title: lazyTabTitle, 3103 triggeringPrincipal_base64: 3104 E10SUtils.serializePrincipal(triggeringPrincipal), 3105 }, 3106 ], 3107 // Make sure to store the userContextId associated to the lazy tab 3108 // otherwise it would be created as a default tab when recreated on a 3109 // session restore (See Bug 1819794). 3110 userContextId, 3111 }); 3112 } 3113 } else { 3114 this._insertBrowser(t, true); 3115 // If we were called by frontend and don't have openWindowInfo, 3116 // but we were opened from another browser, set the cross group 3117 // opener ID: 3118 if (openerBrowser?.browsingContext && !openWindowInfo) { 3119 b.browsingContext.crossGroupOpener = openerBrowser.browsingContext; 3120 } 3121 } 3122 } catch (e) { 3123 console.error("Failed to create tab"); 3124 console.error(e); 3125 t?.remove(); 3126 if (t?.linkedBrowser) { 3127 this._tabFilters.delete(t); 3128 this._tabListeners.delete(t); 3129 this.getPanel(t.linkedBrowser).remove(); 3130 } 3131 return null; 3132 } 3133 3134 if (insertTab) { 3135 // Fire a TabOpen event 3136 const tabOpenDetail = { 3137 ...eventDetail, 3138 fromExternal, 3139 }; 3140 this._fireTabOpen(t, tabOpenDetail); 3141 3142 this._kickOffBrowserLoad(b, { 3143 uri, 3144 uriString, 3145 usingPreloadedContent, 3146 triggeringPrincipal, 3147 originPrincipal, 3148 originStoragePrincipal, 3149 uriIsAboutBlank, 3150 allowInheritPrincipal, 3151 allowThirdPartyFixup, 3152 fromExternal, 3153 forceAllowDataURI, 3154 isCaptivePortalTab, 3155 skipLoad: skipLoad || uriIsAboutBlank, 3156 referrerInfo, 3157 charset, 3158 postData, 3159 policyContainer, 3160 globalHistoryOptions, 3161 triggeringRemoteType, 3162 schemelessInput, 3163 hasValidUserGestureActivation: 3164 hasValidUserGestureActivation || 3165 !!openWindowInfo?.hasValidUserGestureActivation, 3166 textDirectiveUserActivation: 3167 textDirectiveUserActivation || 3168 !!openWindowInfo?.textDirectiveUserActivation, 3169 }); 3170 } 3171 3172 // This field is updated regardless if we actually animate 3173 // since it's important that we keep this count correct in all cases. 3174 this.tabAnimationsInProgress++; 3175 3176 if (animate) { 3177 // Kick the animation off. 3178 // TODO: we should figure out a better solution here. We use RAF 3179 // to avoid jank of the animation due to synchronous work happening 3180 // on tab open. 3181 // With preloaded content though a single RAF happens too early. and 3182 // both the transition and the transitionend event don't happen. 3183 if (usingPreloadedContent) { 3184 requestAnimationFrame(() => { 3185 requestAnimationFrame(() => { 3186 t.setAttribute("fadein", "true"); 3187 }); 3188 }); 3189 } else { 3190 requestAnimationFrame(() => { 3191 t.setAttribute("fadein", "true"); 3192 }); 3193 } 3194 } 3195 3196 // Additionally send pinned tab events 3197 if (pinned) { 3198 this.#notifyPinnedStatus(t); 3199 } 3200 3201 gSharedTabWarning.tabAdded(t); 3202 3203 if (!inBackground) { 3204 this.selectedTab = t; 3205 } 3206 return t; 3207 } 3208 3209 #elementIndexToTabIndex(elementIndex) { 3210 if (elementIndex < 0) { 3211 return -1; 3212 } 3213 if (elementIndex >= this.tabContainer.dragAndDropElements.length) { 3214 return this.tabs.length; 3215 } 3216 let element = this.tabContainer.dragAndDropElements[elementIndex]; 3217 if (this.isTabGroupLabel(element)) { 3218 element = element.group.tabs[0]; 3219 } 3220 if (this.isSplitViewWrapper(element)) { 3221 element = element.tabs[0]; 3222 } 3223 return element._tPos; 3224 } 3225 3226 /** 3227 * @param {string} id 3228 * @returns {MozTabSplitViewWrapper} 3229 */ 3230 _createTabSplitView(id) { 3231 let splitview = document.createXULElement("tab-split-view-wrapper", { 3232 is: "tab-split-view-wrapper", 3233 }); 3234 splitview.splitViewId = id; 3235 return splitview; 3236 } 3237 3238 /** 3239 * Adds a new tab split view. 3240 * 3241 * @param {object[]} tabs 3242 * The set of tabs to include in the split view. 3243 * @param {object} [options] 3244 * @param {string} [options.id] 3245 * Optionally assign an ID to the split view. Useful when rebuilding an 3246 * existing splitview e.g. when restoring. A pseudorandom string will be 3247 * generated if not set. 3248 * @param {MozTabbrowserTab} [options.insertBefore] 3249 * An optional argument that accepts a single tab, which, if passed, will 3250 * cause the split view to be inserted just before this tab in the tab strip. By 3251 * default, the split view will be created at the end of the tab strip. 3252 */ 3253 addTabSplitView(tabs, { id = null, insertBefore = null } = {}) { 3254 if (!tabs?.length) { 3255 throw new Error("Cannot create split view with zero tabs"); 3256 } 3257 3258 if (!id) { 3259 id = `${Date.now()}-${Math.round(Math.random() * 100)}`; 3260 } 3261 let splitview = this._createTabSplitView(id); 3262 this.tabContainer.insertBefore( 3263 splitview, 3264 insertBefore?.splitview ?? insertBefore 3265 ); 3266 splitview.addTabs(tabs); 3267 3268 // Bail out if the split view is empty at this point. This can happen if all 3269 // provided tabs are pinned and therefore cannot be grouped. 3270 if (!splitview.tabs.length) { 3271 splitview.remove(); 3272 return null; 3273 } 3274 3275 this.tabContainer.dispatchEvent( 3276 new CustomEvent("SplitViewCreated", { 3277 bubbles: true, 3278 }) 3279 ); 3280 return splitview; 3281 } 3282 3283 /** 3284 * Removes all tabs from a split view wrapper. This also removes the split view wrapper component 3285 * 3286 * @param {MozTabSplitViewWrapper} [splitView] 3287 * The split view to remove. 3288 */ 3289 unsplitTabs(splitview) { 3290 if (!splitview) { 3291 return; 3292 } 3293 3294 gBrowser.setIsSplitViewActive(false, splitview.tabs); 3295 3296 for (let i = splitview.tabs.length - 1; i >= 0; i--) { 3297 this.#handleTabMove(splitview.tabs[i], () => 3298 gBrowser.tabContainer.insertBefore( 3299 splitview.tabs[i], 3300 splitview.nextElementSibling 3301 ) 3302 ); 3303 } 3304 3305 splitview.remove(); 3306 } 3307 3308 /** 3309 * Show the list of tabs <browsers> that are part of a split view. 3310 * 3311 * @param {MozTabbrowserTab[]} tabs 3312 */ 3313 showSplitViewPanels(tabs) { 3314 const panels = []; 3315 for (const tab of tabs) { 3316 this._insertBrowser(tab); 3317 this.#insertSplitViewFooter(tab); 3318 tab.linkedBrowser.docShellIsActive = true; 3319 panels.push(tab.linkedPanel); 3320 } 3321 this.tabpanels.splitViewPanels = panels; 3322 } 3323 3324 /** 3325 * Hide the list of tabs <browsers> that are part of a split view. 3326 * 3327 * @param {MozTabbrowserTab[]} tabs 3328 */ 3329 hideSplitViewPanels(tabs) { 3330 for (const tab of tabs) { 3331 this.tabpanels.removePanelFromSplitView(tab.linkedPanel); 3332 } 3333 } 3334 3335 /** 3336 * Toggle split view active attribute 3337 * 3338 * @param {boolean} isActive 3339 * @param {MozTabbrowserTab[]} tabs 3340 */ 3341 setIsSplitViewActive(isActive, tabs) { 3342 for (const tab of tabs) { 3343 this.tabpanels.setSplitViewPanelActive(isActive, tab.linkedPanel); 3344 } 3345 this.tabpanels.isSplitViewActive = gBrowser.selectedTab.splitview; 3346 } 3347 3348 /** 3349 * Ensures the split view footer exists for the given tab. 3350 * 3351 * @param {MozTabbrowserTab} tab 3352 */ 3353 #insertSplitViewFooter(tab) { 3354 const panelEl = document.getElementById(tab.linkedPanel); 3355 if (panelEl?.querySelector("split-view-footer")) { 3356 return; 3357 } 3358 if (panelEl) { 3359 const footer = document.createXULElement("split-view-footer"); 3360 footer.setTab(tab); 3361 panelEl.querySelector(".browserStack").appendChild(footer); 3362 } 3363 } 3364 3365 openSplitViewMenu(anchorElement) { 3366 const menu = document.getElementById("split-view-menu"); 3367 menu.openPopup(anchorElement, "after_start"); 3368 } 3369 3370 /** 3371 * @param {string} id 3372 * @param {string} color 3373 * @param {boolean} collapsed 3374 * @param {string} [label=] 3375 * @param {boolean} [isAdoptingGroup=false] 3376 * @returns {MozTabbrowserTabGroup} 3377 */ 3378 _createTabGroup(id, color, collapsed, label = "", isAdoptingGroup = false) { 3379 let group = document.createXULElement("tab-group", { is: "tab-group" }); 3380 group.id = id; 3381 group.collapsed = collapsed; 3382 group.color = color; 3383 group.label = label; 3384 group.wasCreatedByAdoption = isAdoptingGroup; 3385 return group; 3386 } 3387 3388 /** 3389 * Adds a new tab group. 3390 * 3391 * @param {object[]} tabs 3392 * The set of tabs or split view to include in the group. 3393 * @param {object} [options] 3394 * @param {string} [options.id] 3395 * Optionally assign an ID to the tab group. Useful when rebuilding an 3396 * existing group e.g. when restoring. A pseudorandom string will be 3397 * generated if not set. 3398 * @param {string} [options.color] 3399 * Color for the group label. See tabgroup-menu.js for possible values. 3400 * If no color specified, will attempt to assign an unused group color. 3401 * @param {string} [options.label] 3402 * Label for the group. 3403 * @param {MozTabbrowserTab} [options.insertBefore] 3404 * An optional argument that accepts a single tab, which, if passed, will 3405 * cause the group to be inserted just before this tab in the tab strip. By 3406 * default, the group will be created at the end of the tab strip. 3407 * @param {boolean} [options.isAdoptingGroup] 3408 * Whether the tab group was created because a tab group with the same 3409 * properties is being adopted from a different window. 3410 * @param {boolean} [options.isUserTriggered] 3411 * Should be true if this group is being created in response to an 3412 * explicit request from the user (as opposed to a group being created 3413 * for technical reasons, such as when an already existing group 3414 * switches windows). 3415 * Causes the group create UI to be displayed and telemetry events to be fired. 3416 * @param {string} [options.telemetryUserCreateSource] 3417 * The means by which the tab group was created. 3418 * @see TabMetrics.METRIC_SOURCE for possible values. 3419 * Defaults to "unknown". 3420 */ 3421 addTabGroup( 3422 tabsAndSplitViews, 3423 { 3424 id = null, 3425 color = null, 3426 label = "", 3427 insertBefore = null, 3428 isAdoptingGroup = false, 3429 isUserTriggered = false, 3430 telemetryUserCreateSource = "unknown", 3431 } = {} 3432 ) { 3433 if ( 3434 !tabsAndSplitViews?.length || 3435 tabsAndSplitViews.some( 3436 tabOrSplitView => 3437 !this.isTab(tabOrSplitView) && 3438 !this.isSplitViewWrapper(tabOrSplitView) 3439 ) 3440 ) { 3441 throw new Error( 3442 "Cannot create tab group with zero tabs or split views" 3443 ); 3444 } 3445 3446 if (!color) { 3447 color = this.tabGroupMenu.nextUnusedColor; 3448 } 3449 3450 if (!id) { 3451 // Note: If this changes, make sure to also update the 3452 // getExtTabGroupIdForInternalTabGroupId implementation in 3453 // browser/components/extensions/parent/ext-browser.js. 3454 // See: Bug 1960104 - Improve tab group ID generation in addTabGroup 3455 id = `${Date.now()}-${Math.round(Math.random() * 100)}`; 3456 } 3457 let group = this._createTabGroup( 3458 id, 3459 color, 3460 false, 3461 label, 3462 isAdoptingGroup 3463 ); 3464 this.tabContainer.insertBefore( 3465 group, 3466 insertBefore?.group ?? insertBefore 3467 ); 3468 group.addTabs(tabsAndSplitViews); 3469 3470 // Bail out if the group is empty at this point. This can happen if all 3471 // provided tabs are pinned and therefore cannot be grouped. 3472 if (!group.tabs.length) { 3473 group.remove(); 3474 return null; 3475 } 3476 3477 if (isUserTriggered) { 3478 group.dispatchEvent( 3479 new CustomEvent("TabGroupCreateByUser", { 3480 bubbles: true, 3481 detail: { 3482 telemetryUserCreateSource, 3483 }, 3484 }) 3485 ); 3486 } 3487 3488 // Fixes bug1953801 and bug1954689 3489 // Ensure that the tab state cache is updated immediately after creating 3490 // a group. This is necessary because we consider group creation a 3491 // deliberate user action indicating the tab has importance for the user. 3492 // Without this, it is not possible to save and close a tab group with 3493 // a short lifetime. 3494 group.tabs.forEach(tab => { 3495 this.TabStateFlusher.flush(tab.linkedBrowser); 3496 }); 3497 3498 return group; 3499 } 3500 3501 /** 3502 * Removes the tab group. This has the effect of closing all the tabs 3503 * in the group. 3504 * 3505 * @param {MozTabbrowserTabGroup} [group] 3506 * The tab group to remove. 3507 * @param {object} [options] 3508 * Options to use when removing tabs. @see removeTabs for more info. 3509 * @param {boolean} [options.isUserTriggered=false] 3510 * Should be true if this group is being removed by an explicit 3511 * request from the user (as opposed to a group being removed 3512 * for technical reasons, such as when an already existing group 3513 * switches windows). This causes telemetry events to fire. 3514 * @param {string} [options.telemetrySource="unknown"] 3515 * The means by which the tab group was removed. 3516 * @see TabMetrics.METRIC_SOURCE for possible values. 3517 * Defaults to "unknown". 3518 */ 3519 async removeTabGroup( 3520 group, 3521 options = { 3522 isUserTriggered: false, 3523 telemetrySource: this.TabMetrics.METRIC_SOURCE.UNKNOWN, 3524 } 3525 ) { 3526 if (this.tabGroupMenu.panel.state != "closed") { 3527 this.tabGroupMenu.panel.hidePopup(options.animate); 3528 } 3529 3530 if (!options.skipPermitUnload) { 3531 // Process permit unload handlers and allow user cancel 3532 let cancel = await this.runBeforeUnloadForTabs(group.tabs); 3533 if (cancel) { 3534 if (SessionStore.getSavedTabGroup(group.id)) { 3535 // If this group is currently saved, it's being removed as part of a 3536 // save & close operation. We need to forget the saved group 3537 // if the close is canceled. 3538 SessionStore.forgetSavedTabGroup(group.id); 3539 } 3540 return; 3541 } 3542 options.skipPermitUnload = true; 3543 } 3544 3545 if (group.tabs.length == this.tabs.length) { 3546 // explicit calls to removeTabGroup are not expected to save groups. 3547 // if removing this group closes a window, we need to tell the window 3548 // not to save the group. 3549 group.saveOnWindowClose = false; 3550 } 3551 3552 // This needs to be fired before tabs are removed because session store 3553 // needs to respond to this while tabs are still part of the group 3554 group.dispatchEvent( 3555 new CustomEvent("TabGroupRemoveRequested", { 3556 bubbles: true, 3557 detail: { 3558 skipSessionStore: options.skipSessionStore, 3559 isUserTriggered: options.isUserTriggered, 3560 telemetrySource: options.telemetrySource, 3561 }, 3562 }) 3563 ); 3564 3565 // Skip session store on a per-tab basis since these tabs will get 3566 // recorded as part of a group 3567 options.skipSessionStore = true; 3568 3569 // tell removeTabs not to subprocess groups since we're removing a group. 3570 options.skipGroupCheck = true; 3571 3572 this.removeTabs(group.tabs, options); 3573 } 3574 3575 /** 3576 * Removes a tab from a group. This has no effect on tabs that are not 3577 * already in a group. 3578 * 3579 * @param tab The tab to ungroup 3580 */ 3581 ungroupTab(tab) { 3582 if (!tab.group) { 3583 return; 3584 } 3585 3586 this.#handleTabMove(tab, () => 3587 gBrowser.tabContainer.insertBefore(tab, tab.group.nextElementSibling) 3588 ); 3589 } 3590 3591 ungroupSplitView(splitView) { 3592 if (!this.isSplitViewWrapper(splitView)) { 3593 return; 3594 } 3595 3596 this.#handleTabMove(splitView, () => 3597 gBrowser.tabContainer.insertBefore( 3598 splitView, 3599 splitView.tabs[0].group.nextElementSibling 3600 ) 3601 ); 3602 } 3603 3604 /** 3605 * @param {MozTabbrowserTabGroup} group 3606 * @param {object} [options] 3607 * @param {number} [options.elementIndex] 3608 * @param {number} [options.tabIndex] 3609 * @param {boolean} [options.selectTab] 3610 * @returns {MozTabbrowserTabGroup} 3611 */ 3612 adoptTabGroup(group, { elementIndex, tabIndex, selectTab } = {}) { 3613 if (group.ownerDocument == document) { 3614 return group; 3615 } 3616 group.removedByAdoption = true; 3617 group.saveOnWindowClose = false; 3618 3619 let oldSelectedTab = selectTab && group.ownerGlobal.gBrowser.selectedTab; 3620 let newTabs = []; 3621 let adoptedTab; 3622 let splitview; 3623 3624 // bug1969925 adopting a tab group will cause the window to close if it 3625 // is the only thing on the tab strip 3626 // In this case, the `TabUngrouped` event will not fire, so we have to do it manually 3627 let noOtherTabsInWindow = group.ownerGlobal.gBrowser.nonHiddenTabs.every( 3628 t => t.group == group 3629 ); 3630 3631 // We dispatch this event in a separate for loop because the tab extension API 3632 // expects event.detail to be a tab. 3633 if (noOtherTabsInWindow) { 3634 for (let element of group.tabs) { 3635 group.dispatchEvent( 3636 new CustomEvent("TabUngrouped", { 3637 bubbles: true, 3638 detail: element, 3639 }) 3640 ); 3641 } 3642 } 3643 3644 for (let element of group.tabsAndSplitViews) { 3645 if (element.tagName == "tab-split-view-wrapper") { 3646 splitview = this.adoptSplitView(element, { 3647 elementIndex, 3648 tabIndex, 3649 }); 3650 newTabs.push(splitview); 3651 tabIndex = splitview.tabs[0]._tPos + splitview.tabs.length; 3652 } else { 3653 adoptedTab = this.adoptTab(element, { 3654 elementIndex, 3655 tabIndex, 3656 selectTab: element === oldSelectedTab, 3657 }); 3658 newTabs.push(adoptedTab); 3659 // Put next tab after current one. 3660 elementIndex = undefined; 3661 tabIndex = adoptedTab._tPos + 1; 3662 } 3663 } 3664 3665 return this.addTabGroup(newTabs, { 3666 id: group.id, 3667 label: group.label, 3668 color: group.color, 3669 insertBefore: newTabs[0], 3670 isAdoptingGroup: true, 3671 }); 3672 } 3673 3674 /** 3675 * @param {MozSplitViewWrapper} container 3676 * @param {object} [options] 3677 * @param {number} [options.elementIndex] 3678 * @param {number} [options.tabIndex] 3679 * @param {boolean} [options.selectTab] 3680 * @returns {MozSplitViewWrapper} 3681 */ 3682 adoptSplitView(container, { elementIndex, tabIndex } = {}) { 3683 if (container.ownerDocument == document) { 3684 return container; 3685 } 3686 3687 let newTabs = []; 3688 3689 if (!tabIndex) { 3690 tabIndex = this.#elementIndexToTabIndex(elementIndex); 3691 } 3692 3693 for (let tab of container.tabs) { 3694 let adoptedTab = this.adoptTab(tab, { 3695 tabIndex, 3696 }); 3697 newTabs.push(adoptedTab); 3698 tabIndex = adoptedTab._tPos + 1; 3699 } 3700 3701 return this.addTabSplitView(newTabs, { 3702 id: container.splitViewId, 3703 insertBefore: newTabs[0], 3704 }); 3705 } 3706 3707 /** 3708 * Get all open tab groups from all windows. Does not include saved groups. 3709 * 3710 * @param {object} [options] 3711 * @param {boolean} [options.sortByLastSeenActive] 3712 * Sort groups so that groups that have more recently seen and active 3713 * tabs appear first. Defaults to false. 3714 */ 3715 getAllTabGroups({ sortByLastSeenActive = false } = {}) { 3716 let groups = BrowserWindowTracker.getOrderedWindows({ 3717 private: PrivateBrowsingUtils.isWindowPrivate(window), 3718 }).reduce( 3719 (acc, thisWindow) => acc.concat(thisWindow.gBrowser.tabGroups), 3720 [] 3721 ); 3722 if (sortByLastSeenActive) { 3723 groups.sort( 3724 (group1, group2) => group2.lastSeenActive - group1.lastSeenActive 3725 ); 3726 } 3727 return groups; 3728 } 3729 3730 getTabGroupById(id) { 3731 for (const win of BrowserWindowTracker.getOrderedWindows({ 3732 private: PrivateBrowsingUtils.isWindowPrivate(window), 3733 })) { 3734 for (const group of win.gBrowser.tabGroups) { 3735 if (group.id === id) { 3736 return group; 3737 } 3738 } 3739 } 3740 return null; 3741 } 3742 3743 _determineURIToLoad(uriString, createLazyBrowser) { 3744 uriString = uriString || "about:blank"; 3745 let aURIObject = null; 3746 try { 3747 aURIObject = Services.io.newURI(uriString); 3748 } catch (ex) { 3749 /* we'll try to fix up this URL later */ 3750 } 3751 3752 let lazyBrowserURI; 3753 if (createLazyBrowser && uriString != "about:blank") { 3754 lazyBrowserURI = aURIObject; 3755 uriString = "about:blank"; 3756 } 3757 3758 let uriIsAboutBlank = uriString == "about:blank"; 3759 return { uri: aURIObject, uriIsAboutBlank, lazyBrowserURI, uriString }; 3760 } 3761 3762 /** 3763 * @param {object} options 3764 * @returns {MozTabbrowserTab} 3765 */ 3766 _createTab({ 3767 uriString, 3768 userContextId, 3769 openerTab, 3770 pinned, 3771 noInitialLabel, 3772 skipBackgroundNotify, 3773 animate, 3774 }) { 3775 var t = document.createXULElement("tab", { is: "tabbrowser-tab" }); 3776 // Tag the tab as being created so extension code can ignore events 3777 // prior to TabOpen. 3778 t.initializingTab = true; 3779 t.openerTab = openerTab; 3780 3781 // Related tab inherits current tab's user context unless a different 3782 // usercontextid is specified 3783 if (userContextId == null && openerTab) { 3784 userContextId = openerTab.getAttribute("usercontextid") || 0; 3785 } 3786 3787 if (!noInitialLabel) { 3788 if (isBlankPageURL(uriString)) { 3789 t.setAttribute("label", this.tabContainer.emptyTabTitle); 3790 } else { 3791 // Set URL as label so that the tab isn't empty initially. 3792 this.setInitialTabTitle(t, uriString, { 3793 beforeTabOpen: true, 3794 isURL: true, 3795 }); 3796 } 3797 } 3798 3799 if (userContextId) { 3800 t.setAttribute("usercontextid", userContextId); 3801 ContextualIdentityService.setTabStyle(t); 3802 } 3803 3804 if (skipBackgroundNotify) { 3805 t.setAttribute("skipbackgroundnotify", true); 3806 } 3807 3808 if (pinned) { 3809 t.setAttribute("pinned", "true"); 3810 } 3811 3812 t.classList.add("tabbrowser-tab"); 3813 3814 this.tabContainer._unlockTabSizing(); 3815 3816 if (!animate) { 3817 UserInteraction.update("browser.tabs.opening", "not-animated", window); 3818 t.setAttribute("fadein", "true"); 3819 3820 // Call _handleNewTab asynchronously as it needs to know if the 3821 // new tab is selected. 3822 setTimeout( 3823 function (tabContainer) { 3824 tabContainer._handleNewTab(t); 3825 }, 3826 0, 3827 this.tabContainer 3828 ); 3829 } else { 3830 UserInteraction.update("browser.tabs.opening", "animated", window); 3831 } 3832 3833 return t; 3834 } 3835 3836 /** 3837 * 3838 * @param {object} options 3839 * @param {nsIPrincipal} [options.originPrincipal] 3840 * If uriString is given, uri might inherit principals, and no preloaded browser is used, 3841 * this is the origin principal to be inherited by the initial about:blank. 3842 * @param {nsIPrincipal} [options.originStoragePrincipal] 3843 * If uriString is given, uri might inherit principals, and no preloaded browser is used, 3844 * this is the origin storage principal to be inherited by the initial about:blank. 3845 */ 3846 _createBrowserForTab( 3847 tab, 3848 { 3849 uriString, 3850 uri, 3851 name, 3852 preferredRemoteType, 3853 openerBrowser, 3854 uriIsAboutBlank, 3855 referrerInfo, 3856 forceNotRemote, 3857 initialBrowsingContextGroupId, 3858 openWindowInfo, 3859 skipLoad, 3860 triggeringRemoteType, 3861 } 3862 ) { 3863 // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and 3864 // we have a remote triggering remote type, use that instead. 3865 if (!preferredRemoteType && triggeringRemoteType) { 3866 preferredRemoteType = triggeringRemoteType; 3867 } 3868 3869 // If we don't have a preferred remote type, and we have a remote 3870 // opener, use the opener's remote type. 3871 if (!preferredRemoteType && openerBrowser) { 3872 preferredRemoteType = openerBrowser.remoteType; 3873 } 3874 3875 let { userContextId } = tab; 3876 3877 var oa = E10SUtils.predictOriginAttributes({ window, userContextId }); 3878 3879 // If URI is about:blank and we don't have a preferred remote type, 3880 // then we need to use the referrer, if we have one, to get the 3881 // correct remote type for the new tab. 3882 if ( 3883 uriIsAboutBlank && 3884 !preferredRemoteType && 3885 referrerInfo && 3886 referrerInfo.originalReferrer 3887 ) { 3888 preferredRemoteType = E10SUtils.getRemoteTypeForURI( 3889 referrerInfo.originalReferrer.spec, 3890 gMultiProcessBrowser, 3891 gFissionBrowser, 3892 E10SUtils.DEFAULT_REMOTE_TYPE, 3893 null, 3894 oa 3895 ); 3896 } 3897 3898 let remoteType = forceNotRemote 3899 ? E10SUtils.NOT_REMOTE 3900 : E10SUtils.getRemoteTypeForURI( 3901 uriString, 3902 gMultiProcessBrowser, 3903 gFissionBrowser, 3904 preferredRemoteType, 3905 null, 3906 oa 3907 ); 3908 3909 let b, 3910 usingPreloadedContent = false; 3911 // If we open a new tab with the newtab URL in the default 3912 // userContext, check if there is a preloaded browser ready. 3913 if (uriString == BROWSER_NEW_TAB_URL && !userContextId) { 3914 b = NewTabPagePreloading.getPreloadedBrowser(window); 3915 if (b) { 3916 usingPreloadedContent = true; 3917 } 3918 } 3919 3920 if (!b) { 3921 // No preloaded browser found, create one. 3922 b = this.createBrowser({ 3923 remoteType, 3924 uriIsAboutBlank, 3925 userContextId, 3926 initialBrowsingContextGroupId, 3927 openWindowInfo, 3928 name, 3929 skipLoad, 3930 }); 3931 } 3932 3933 tab.linkedBrowser = b; 3934 3935 this._tabForBrowser.set(b, tab); 3936 tab.permanentKey = b.permanentKey; 3937 tab._browserParams = { 3938 uriIsAboutBlank, 3939 remoteType, 3940 usingPreloadedContent, 3941 }; 3942 3943 // Hack to ensure that the about:newtab, and about:welcome favicon is loaded 3944 // instantaneously, to avoid flickering and improve perceived performance. 3945 this.setDefaultIcon(tab, uri); 3946 3947 return { browser: b, usingPreloadedContent }; 3948 } 3949 3950 _kickOffBrowserLoad( 3951 browser, 3952 { 3953 uri, 3954 uriString, 3955 usingPreloadedContent, 3956 triggeringPrincipal, 3957 originPrincipal, 3958 originStoragePrincipal, 3959 uriIsAboutBlank, 3960 allowInheritPrincipal, 3961 allowThirdPartyFixup, 3962 fromExternal, 3963 forceAllowDataURI, 3964 isCaptivePortalTab, 3965 skipLoad, 3966 referrerInfo, 3967 charset, 3968 postData, 3969 policyContainer, 3970 globalHistoryOptions, 3971 triggeringRemoteType, 3972 schemelessInput, 3973 hasValidUserGestureActivation, 3974 textDirectiveUserActivation, 3975 } 3976 ) { 3977 const shouldInheritSecurityContext = (() => { 3978 if ( 3979 !usingPreloadedContent && 3980 originPrincipal && 3981 originStoragePrincipal && 3982 uriString 3983 ) { 3984 let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler; 3985 // Unless we know for sure we're not inheriting principals, 3986 // force the about:blank viewer to have the right principal: 3987 if (!uri || doGetProtocolFlags(uri) & URI_INHERITS_SECURITY_CONTEXT) { 3988 return true; 3989 } 3990 } 3991 return false; 3992 })(); 3993 3994 if (shouldInheritSecurityContext) { 3995 browser.createAboutBlankDocumentViewer( 3996 originPrincipal, 3997 originStoragePrincipal 3998 ); 3999 } 4000 4001 // If we didn't swap docShells with a preloaded browser 4002 // then let's just continue loading the page normally. 4003 if ( 4004 !usingPreloadedContent && 4005 (!uriIsAboutBlank || !allowInheritPrincipal) && 4006 !skipLoad 4007 ) { 4008 // pretend the user typed this so it'll be available till 4009 // the document successfully loads 4010 if (uriString && !gInitialPages.includes(uriString)) { 4011 browser.userTypedValue = uriString; 4012 } 4013 4014 let loadFlags = LOAD_FLAGS_NONE; 4015 if (allowThirdPartyFixup) { 4016 loadFlags |= 4017 LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | LOAD_FLAGS_FIXUP_SCHEME_TYPOS; 4018 } 4019 if (fromExternal) { 4020 loadFlags |= LOAD_FLAGS_FROM_EXTERNAL; 4021 } else if (!triggeringPrincipal.isSystemPrincipal) { 4022 // XXX this code must be reviewed and changed when bug 1616353 4023 // lands. 4024 // The purpose of LOAD_FLAGS_FIRST_LOAD is to close a new 4025 // tab if it turns out to be a download. 4026 loadFlags |= LOAD_FLAGS_FIRST_LOAD; 4027 } 4028 if (!allowInheritPrincipal) { 4029 loadFlags |= LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; 4030 } 4031 if (isCaptivePortalTab) { 4032 loadFlags |= LOAD_FLAGS_DISABLE_TRR; 4033 } 4034 if (forceAllowDataURI) { 4035 loadFlags |= LOAD_FLAGS_FORCE_ALLOW_DATA_URI; 4036 } 4037 try { 4038 browser.fixupAndLoadURIString(uriString, { 4039 loadFlags, 4040 triggeringPrincipal, 4041 referrerInfo, 4042 charset, 4043 postData, 4044 policyContainer, 4045 globalHistoryOptions, 4046 triggeringRemoteType, 4047 schemelessInput, 4048 hasValidUserGestureActivation, 4049 textDirectiveUserActivation, 4050 isCaptivePortalTab, 4051 }); 4052 } catch (ex) { 4053 console.error(ex); 4054 } 4055 } 4056 } 4057 4058 /** 4059 * @typedef {object} TabGroupWorkingData 4060 * @property {TabGroupStateData} stateData 4061 * @property {MozTabbrowserTabGroup|undefined} node 4062 * @property {DocumentFragment} containingTabsFragment 4063 */ 4064 4065 /** 4066 * @param {boolean} restoreTabsLazily 4067 * @param {number} selectTab see SessionStoreInternal.restoreTabs { aSelectTab } 4068 * @param {TabStateData[]} tabDataList 4069 * @param {TabGroupStateData[]} tabGroupDataList 4070 * @returns {MozTabbrowserTab[]} 4071 */ 4072 createTabsForSessionRestore( 4073 restoreTabsLazily, 4074 selectTab, 4075 tabDataList, 4076 tabGroupDataList 4077 ) { 4078 let tabs = []; 4079 let tabsFragment = document.createDocumentFragment(); 4080 let tabToSelect = null; 4081 let hiddenTabs = new Map(); 4082 /** @type {Map<TabGroupStateData['id'], TabGroupWorkingData>} */ 4083 let tabGroupWorkingData = new Map(); 4084 4085 for (const tabGroupData of tabGroupDataList) { 4086 tabGroupWorkingData.set(tabGroupData.id, { 4087 stateData: tabGroupData, 4088 node: undefined, 4089 containingTabsFragment: document.createDocumentFragment(), 4090 }); 4091 } 4092 4093 // We create each tab and browser, but only insert them 4094 // into a document fragment so that we can insert them all 4095 // together. This prevents synch reflow for each tab 4096 // insertion. 4097 for (var i = 0; i < tabDataList.length; i++) { 4098 let tabData = tabDataList[i]; 4099 4100 let userContextId = tabData.userContextId; 4101 let select = i == selectTab - 1; 4102 let tab; 4103 let tabWasReused = false; 4104 4105 // Re-use existing selected tab if possible to avoid the overhead of 4106 // selecting a new tab. For now, we only do this for horizontal tabs; 4107 // we'll let tabs.js handle pinning for vertical tabs until we unify 4108 // the logic for both horizontal and vertical tabs in bug 1910097. 4109 if ( 4110 select && 4111 this.selectedTab.userContextId == userContextId && 4112 !SessionStore.isTabRestoring(this.selectedTab) && 4113 !this.tabContainer.verticalMode 4114 ) { 4115 tabWasReused = true; 4116 tab = this.selectedTab; 4117 if (!tabData.pinned) { 4118 this.unpinTab(tab); 4119 } else { 4120 this.pinTab(tab); 4121 } 4122 } 4123 4124 // Add a new tab if needed. 4125 if (!tab) { 4126 let createLazyBrowser = 4127 restoreTabsLazily && !select && !tabData.pinned; 4128 4129 let url = "about:blank"; 4130 if (tabData.entries?.length) { 4131 let activeIndex = (tabData.index || tabData.entries.length) - 1; 4132 // Ensure the index is in bounds. 4133 activeIndex = Math.min(activeIndex, tabData.entries.length - 1); 4134 activeIndex = Math.max(activeIndex, 0); 4135 url = tabData.entries[activeIndex].url; 4136 } 4137 4138 let preferredRemoteType = E10SUtils.getRemoteTypeForURI( 4139 url, 4140 gMultiProcessBrowser, 4141 gFissionBrowser, 4142 E10SUtils.DEFAULT_REMOTE_TYPE, 4143 null, 4144 E10SUtils.predictOriginAttributes({ window, userContextId }) 4145 ); 4146 4147 // If we're creating a lazy browser, let tabbrowser know the future 4148 // URI because progress listeners won't get onLocationChange 4149 // notification before the browser is inserted. 4150 // 4151 // Setting noInitialLabel is a perf optimization. Rendering tab labels 4152 // would make resizing the tabs more expensive as we're adding them. 4153 // Each tab will get its initial label set in restoreTab. 4154 tab = this.addTrustedTab(createLazyBrowser ? url : "about:blank", { 4155 createLazyBrowser, 4156 skipAnimation: true, 4157 noInitialLabel: true, 4158 userContextId, 4159 skipBackgroundNotify: true, 4160 bulkOrderedOpen: true, 4161 insertTab: false, 4162 skipLoad: true, 4163 preferredRemoteType, 4164 }); 4165 4166 if (select) { 4167 tabToSelect = tab; 4168 } 4169 } 4170 4171 tabs.push(tab); 4172 4173 if (tabData.pinned) { 4174 this.pinTab(tab); 4175 // Then ensure all the tab open/pinning information is sent. 4176 this._fireTabOpen(tab, {}); 4177 } else if (tabData.groupId) { 4178 let { groupId } = tabData; 4179 const tabGroup = tabGroupWorkingData.get(groupId); 4180 // if a tab refers to a tab group we don't know, skip any group 4181 // processing 4182 if (tabGroup) { 4183 tabGroup.containingTabsFragment.appendChild(tab); 4184 // if this is the first time encountering a tab group, create its 4185 // DOM node once and place it in the tabs bar fragment 4186 if (!tabGroup.node) { 4187 tabGroup.node = this._createTabGroup( 4188 tabGroup.stateData.id, 4189 tabGroup.stateData.color, 4190 tabGroup.stateData.collapsed, 4191 tabGroup.stateData.name 4192 ); 4193 tabsFragment.appendChild(tabGroup.node); 4194 } 4195 } 4196 } else { 4197 if (tab.hidden) { 4198 tab.hidden = true; 4199 hiddenTabs.set(tab, tabData.extData && tabData.extData.hiddenBy); 4200 } 4201 4202 tabsFragment.appendChild(tab); 4203 if (tabWasReused) { 4204 this.tabContainer._invalidateCachedTabs(); 4205 } 4206 } 4207 4208 tab.initialize(); 4209 } 4210 4211 // inject the top-level tab and tab group DOM nodes 4212 this.tabContainer.appendChild(tabsFragment); 4213 4214 // inject tab DOM nodes into the now-connected tab group DOM nodes 4215 for (const tabGroup of tabGroupWorkingData.values()) { 4216 if (tabGroup.node) { 4217 tabGroup.node.appendChild(tabGroup.containingTabsFragment); 4218 } 4219 } 4220 4221 for (let [tab, hiddenBy] of hiddenTabs) { 4222 let event = document.createEvent("Events"); 4223 event.initEvent("TabHide", true, false); 4224 tab.dispatchEvent(event); 4225 if (hiddenBy) { 4226 SessionStore.setCustomTabValue(tab, "hiddenBy", hiddenBy); 4227 } 4228 } 4229 4230 this.tabContainer._invalidateCachedTabs(); 4231 4232 // We need to wait until after all tabs have been appended to the DOM 4233 // to remove the old selected tab. 4234 if (tabToSelect) { 4235 let leftoverTab = this.selectedTab; 4236 this.selectedTab = tabToSelect; 4237 this.removeTab(leftoverTab); 4238 } 4239 4240 if (tabs.length > 1 || !tabs[0].selected) { 4241 this._updateTabsAfterInsert(); 4242 TabBarVisibility.update(); 4243 4244 for (let tab of tabs) { 4245 // If tabToSelect is a tab, we didn't reuse the selected tab. 4246 if (tabToSelect || !tab.selected) { 4247 // Fire a TabOpen event for all unpinned tabs, except reused selected 4248 // tabs. 4249 if (!tab.pinned) { 4250 this._fireTabOpen(tab, {}); 4251 } 4252 4253 // Fire a TabBrowserInserted event on all tabs that have a connected, 4254 // real browser, except for reused selected tabs. 4255 if (tab.linkedPanel) { 4256 var evt = new CustomEvent("TabBrowserInserted", { 4257 bubbles: true, 4258 detail: { insertedOnTabCreation: true }, 4259 }); 4260 tab.dispatchEvent(evt); 4261 } 4262 } 4263 } 4264 } 4265 4266 return tabs; 4267 } 4268 4269 moveTabsToStart(contextTab) { 4270 let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab]; 4271 // Walk the array in reverse order so the tabs are kept in order. 4272 for (let i = tabs.length - 1; i >= 0; i--) { 4273 this.moveTabToStart(tabs[i]); 4274 } 4275 } 4276 4277 moveTabsToEnd(contextTab) { 4278 let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab]; 4279 for (let tab of tabs) { 4280 this.moveTabToEnd(tab); 4281 } 4282 } 4283 4284 warnAboutClosingTabs(tabsToClose, aCloseTabs) { 4285 // We want to warn about closing duplicates even if there was only a 4286 // single duplicate, so we intentionally place this above the check for 4287 // tabsToClose <= 1. 4288 const shownDupeDialogPref = 4289 "browser.tabs.haveShownCloseAllDuplicateTabsWarning"; 4290 var ps = Services.prompt; 4291 if ( 4292 aCloseTabs == this.closingTabsEnum.ALL_DUPLICATES && 4293 !Services.prefs.getBoolPref(shownDupeDialogPref, false) 4294 ) { 4295 // The first time a user closes all duplicate tabs, tell them what will 4296 // happen and give them a chance to back away. 4297 Services.prefs.setBoolPref(shownDupeDialogPref, true); 4298 4299 window.focus(); 4300 const [title, text, button] = this.tabLocalization.formatValuesSync([ 4301 { id: "tabbrowser-confirm-close-all-duplicate-tabs-title" }, 4302 { id: "tabbrowser-confirm-close-all-duplicate-tabs-text" }, 4303 { 4304 id: "tabbrowser-confirm-close-all-duplicate-tabs-button-closetabs", 4305 }, 4306 ]); 4307 4308 const flags = 4309 ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING + 4310 ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL + 4311 ps.BUTTON_POS_0_DEFAULT; 4312 4313 // buttonPressed will be 0 for close tabs, 1 for cancel. 4314 const buttonPressed = ps.confirmEx( 4315 window, 4316 title, 4317 text, 4318 flags, 4319 button, 4320 null, 4321 null, 4322 null, 4323 {} 4324 ); 4325 return buttonPressed == 0; 4326 } 4327 4328 if (tabsToClose <= 1) { 4329 return true; 4330 } 4331 4332 const pref = 4333 aCloseTabs == this.closingTabsEnum.ALL 4334 ? "browser.tabs.warnOnClose" 4335 : "browser.tabs.warnOnCloseOtherTabs"; 4336 var shouldPrompt = Services.prefs.getBoolPref(pref); 4337 if (!shouldPrompt) { 4338 return true; 4339 } 4340 4341 const maxTabsUndo = Services.prefs.getIntPref( 4342 "browser.sessionstore.max_tabs_undo" 4343 ); 4344 if ( 4345 aCloseTabs != this.closingTabsEnum.ALL && 4346 tabsToClose <= maxTabsUndo 4347 ) { 4348 return true; 4349 } 4350 4351 // Our prompt to close this window is most important, so replace others. 4352 gDialogBox.replaceDialogIfOpen(); 4353 4354 // default to true: if it were false, we wouldn't get this far 4355 var warnOnClose = { value: true }; 4356 4357 // focus the window before prompting. 4358 // this will raise any minimized window, which will 4359 // make it obvious which window the prompt is for and will 4360 // solve the problem of windows "obscuring" the prompt. 4361 // see bug #350299 for more details 4362 window.focus(); 4363 const [title, button, checkbox] = this.tabLocalization.formatValuesSync([ 4364 { 4365 id: "tabbrowser-confirm-close-tabs-title", 4366 args: { tabCount: tabsToClose }, 4367 }, 4368 { id: "tabbrowser-confirm-close-tabs-button" }, 4369 { id: "tabbrowser-ask-close-tabs-checkbox" }, 4370 ]); 4371 let flags = 4372 ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0 + 4373 ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1; 4374 let checkboxLabel = 4375 aCloseTabs == this.closingTabsEnum.ALL ? checkbox : null; 4376 var buttonPressed = ps.confirmEx( 4377 window, 4378 title, 4379 null, 4380 flags, 4381 button, 4382 null, 4383 null, 4384 checkboxLabel, 4385 warnOnClose 4386 ); 4387 4388 var reallyClose = buttonPressed == 0; 4389 4390 // don't set the pref unless they press OK and it's false 4391 if ( 4392 aCloseTabs == this.closingTabsEnum.ALL && 4393 reallyClose && 4394 !warnOnClose.value 4395 ) { 4396 Services.prefs.setBoolPref(pref, false); 4397 } 4398 4399 return reallyClose; 4400 } 4401 4402 /** 4403 * This determines where the tab should be inserted within the tabContainer, 4404 * and inserts it. 4405 * 4406 * @param {MozTabbrowserTab} tab 4407 * @param {object} [options] 4408 * @param {number} [options.elementIndex] 4409 * @param {number} [options.tabIndex] 4410 * @param {MozTabbrowserTabGroup} [options.tabGroup] 4411 * A related tab group where this tab should be added, when applicable. 4412 */ 4413 #insertTabAtIndex( 4414 tab, 4415 { 4416 tabIndex, 4417 elementIndex, 4418 ownerTab, 4419 openerTab, 4420 pinned, 4421 bulkOrderedOpen, 4422 tabGroup, 4423 } = {} 4424 ) { 4425 // If this new tab is owned by another, assert that relationship 4426 if (ownerTab) { 4427 tab.owner = ownerTab; 4428 } 4429 4430 // Ensure we have an index if one was not provided. 4431 if (typeof elementIndex != "number" && typeof tabIndex != "number") { 4432 // Move the new tab after another tab if needed, to the end otherwise. 4433 elementIndex = Infinity; 4434 if ( 4435 !bulkOrderedOpen && 4436 ((openerTab && 4437 Services.prefs.getBoolPref( 4438 "browser.tabs.insertRelatedAfterCurrent" 4439 )) || 4440 Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent")) 4441 ) { 4442 let lastRelatedTab = 4443 openerTab && this._lastRelatedTabMap.get(openerTab); 4444 let previousTab = lastRelatedTab || openerTab || this.selectedTab; 4445 if (!tabGroup) { 4446 tabGroup = previousTab.group; 4447 } 4448 if ( 4449 Services.prefs.getBoolPref( 4450 "browser.tabs.insertAfterCurrentExceptPinned" 4451 ) && 4452 previousTab.pinned 4453 ) { 4454 elementIndex = Infinity; 4455 } else if (previousTab.visible && previousTab.splitview) { 4456 elementIndex = 4457 this.tabContainer.dragAndDropElements.indexOf( 4458 previousTab.splitview 4459 ) + 1; 4460 } else if (previousTab.visible) { 4461 elementIndex = previousTab.elementIndex + 1; 4462 } else if (previousTab == FirefoxViewHandler.tab) { 4463 elementIndex = 0; 4464 } 4465 4466 if (lastRelatedTab) { 4467 lastRelatedTab.owner = null; 4468 } else if (openerTab) { 4469 tab.owner = openerTab; 4470 } 4471 // Always set related map if opener exists. 4472 if (openerTab) { 4473 this._lastRelatedTabMap.set(openerTab, tab); 4474 } 4475 } 4476 } 4477 4478 let allItems; 4479 let index; 4480 if (typeof elementIndex == "number") { 4481 allItems = this.tabContainer.dragAndDropElements; 4482 index = elementIndex; 4483 } else { 4484 allItems = this.tabs; 4485 index = tabIndex; 4486 } 4487 // Ensure index is within bounds. 4488 if (tab.pinned) { 4489 index = Math.max(index, 0); 4490 index = Math.min(index, this.pinnedTabCount); 4491 } else { 4492 index = Math.max(index, this.pinnedTabCount); 4493 index = Math.min(index, allItems.length); 4494 } 4495 /** @type {MozTabbrowserTab|undefined} */ 4496 let itemAfter = allItems.at(index); 4497 4498 if (pinned && !itemAfter?.pinned) { 4499 itemAfter = null; 4500 } else if (itemAfter?.splitview) { 4501 itemAfter = itemAfter.splitview?.nextElementSibling || null; 4502 } 4503 // Prevent a flash of unstyled content by setting up the tab content 4504 // and inherited attributes before appending it (see Bug 1592054): 4505 tab.initialize(); 4506 4507 this.tabContainer._invalidateCachedTabs(); 4508 4509 if (tabGroup) { 4510 if ( 4511 (this.isTab(itemAfter) && itemAfter.group == tabGroup) || 4512 this.isSplitViewWrapper(itemAfter) 4513 ) { 4514 // Place at the front of, or between tabs in, the same tab group 4515 this.tabContainer.insertBefore(tab, itemAfter); 4516 } else { 4517 // Place tab at the end of the contextual tab group because one of: 4518 // 1) no `itemAfter` so `tab` should be the last tab in the tab strip 4519 // 2) `itemAfter` is in a different tab group 4520 tabGroup.appendChild(tab); 4521 } 4522 } else if ( 4523 (this.isTab(itemAfter) && itemAfter.group?.tabs[0] == itemAfter) || 4524 this.isTabGroupLabel(itemAfter) 4525 ) { 4526 // If there is ambiguity around whether or not a tab should be inserted 4527 // into a group (i.e. because the new tab is being inserted on the 4528 // edges of the group), prefer not to insert the tab into the group. 4529 // 4530 // We only need to handle the case where the tab is being inserted at 4531 // the starting boundary of a group because `insertBefore` called on 4532 // the tab just after a tab group will not add it to the group by 4533 // default. 4534 this.tabContainer.insertBefore(tab, itemAfter.group); 4535 } else { 4536 // Place ungrouped tab before `itemAfter` by default 4537 const tabContainer = pinned 4538 ? this.tabContainer.pinnedTabsContainer 4539 : this.tabContainer; 4540 tabContainer.insertBefore(tab, itemAfter); 4541 } 4542 4543 if (tab.group?.collapsed) { 4544 // Bug 1997096: automatically expand the group if we are adding a new 4545 // tab to a collapsed group, and that tab does not have automatic focus 4546 // (i.e. if the user right clicks and clicks "Open in New Tab") 4547 tab.group.collapsed = false; 4548 } 4549 4550 this._updateTabsAfterInsert(); 4551 4552 if (pinned) { 4553 this._updateTabBarForPinnedTabs(); 4554 } 4555 4556 TabBarVisibility.update(); 4557 } 4558 4559 /** 4560 * Dispatch a new tab event. This should be called when things are in a 4561 * consistent state, such that listeners of this event can again open 4562 * or close tabs. 4563 */ 4564 _fireTabOpen(tab, eventDetail) { 4565 delete tab.initializingTab; 4566 let evt = new CustomEvent("TabOpen", { 4567 bubbles: true, 4568 detail: eventDetail || {}, 4569 }); 4570 tab.dispatchEvent(evt); 4571 } 4572 4573 /** 4574 * @param {MozTabbrowserTab} aTab 4575 * @returns {MozTabbrowserTab[]} 4576 */ 4577 _getTabsToTheStartFrom(aTab) { 4578 let tabsToStart = []; 4579 if (!aTab.visible) { 4580 return tabsToStart; 4581 } 4582 let tabs = this.openTabs; 4583 for (let i = 0; i < tabs.length; ++i) { 4584 if (tabs[i] == aTab) { 4585 break; 4586 } 4587 // Ignore pinned and hidden tabs. 4588 if (tabs[i].pinned || tabs[i].hidden) { 4589 continue; 4590 } 4591 // In a multi-select context, select all unselected tabs 4592 // starting from the context tab. 4593 if (aTab.multiselected && tabs[i].multiselected) { 4594 continue; 4595 } 4596 tabsToStart.push(tabs[i]); 4597 } 4598 return tabsToStart; 4599 } 4600 4601 /** 4602 * @param {MozTabbrowserTab} aTab 4603 * @returns {MozTabbrowserTab[]} 4604 */ 4605 _getTabsToTheEndFrom(aTab) { 4606 let tabsToEnd = []; 4607 if (!aTab.visible) { 4608 return tabsToEnd; 4609 } 4610 let tabs = this.openTabs; 4611 for (let i = tabs.length - 1; i >= 0; --i) { 4612 if (tabs[i] == aTab) { 4613 break; 4614 } 4615 // Ignore pinned and hidden tabs. 4616 if (tabs[i].pinned || tabs[i].hidden) { 4617 continue; 4618 } 4619 // In a multi-select context, select all unselected tabs 4620 // starting from the context tab. 4621 if (aTab.multiselected && tabs[i].multiselected) { 4622 continue; 4623 } 4624 tabsToEnd.push(tabs[i]); 4625 } 4626 return tabsToEnd; 4627 } 4628 4629 getDuplicateTabsToClose(aTab) { 4630 // One would think that a set is better, but it would need to copy all 4631 // the strings instead of just keeping references to the nsIURI objects, 4632 // and the array is presumed to be small anyways. 4633 let keys = []; 4634 let keyForTab = tab => { 4635 let uri = tab.linkedBrowser?.currentURI; 4636 if (!uri) { 4637 return null; 4638 } 4639 return { 4640 uri, 4641 userContextId: tab.userContextId, 4642 }; 4643 }; 4644 let keyEquals = (a, b) => { 4645 return a.userContextId == b.userContextId && a.uri.equals(b.uri); 4646 }; 4647 if (aTab.multiselected) { 4648 for (let tab of this.selectedTabs) { 4649 let key = keyForTab(tab); 4650 if (key) { 4651 keys.push(key); 4652 } 4653 } 4654 } else { 4655 let key = keyForTab(aTab); 4656 if (key) { 4657 keys.push(key); 4658 } 4659 } 4660 4661 if (!keys.length) { 4662 return []; 4663 } 4664 4665 let duplicateTabs = []; 4666 for (let tab of this.tabs) { 4667 if (tab == aTab || tab.pinned) { 4668 continue; 4669 } 4670 if (aTab.multiselected && tab.multiselected) { 4671 continue; 4672 } 4673 let key = keyForTab(tab); 4674 if (key && keys.some(k => keyEquals(k, key))) { 4675 duplicateTabs.push(tab); 4676 } 4677 } 4678 4679 return duplicateTabs; 4680 } 4681 4682 getAllDuplicateTabsToClose() { 4683 let lastSeenTabs = this.tabs.toSorted( 4684 (a, b) => b.lastSeenActive - a.lastSeenActive 4685 ); 4686 let duplicateTabs = []; 4687 let keys = []; 4688 for (let tab of lastSeenTabs) { 4689 const uri = tab.linkedBrowser?.currentURI; 4690 if (!uri) { 4691 // Can't tell if it's a duplicate without a URI. 4692 // Safest to leave it be. 4693 continue; 4694 } 4695 4696 const key = { 4697 uri, 4698 userContextId: tab.userContextId, 4699 }; 4700 if ( 4701 !tab.pinned && 4702 keys.some( 4703 k => k.userContextId == key.userContextId && k.uri.equals(key.uri) 4704 ) 4705 ) { 4706 duplicateTabs.push(tab); 4707 } 4708 keys.push(key); 4709 } 4710 return duplicateTabs; 4711 } 4712 4713 removeDuplicateTabs(aTab, options) { 4714 this._removeDuplicateTabs( 4715 aTab, 4716 this.getDuplicateTabsToClose(aTab), 4717 this.closingTabsEnum.DUPLICATES, 4718 options 4719 ); 4720 } 4721 4722 _removeDuplicateTabs(aConfirmationAnchor, tabs, aCloseTabs, options) { 4723 if (!tabs.length) { 4724 return; 4725 } 4726 4727 if (!this.warnAboutClosingTabs(tabs.length, aCloseTabs)) { 4728 return; 4729 } 4730 4731 this.removeTabs(tabs, options); 4732 ConfirmationHint.show( 4733 aConfirmationAnchor, 4734 "confirmation-hint-duplicate-tabs-closed", 4735 { l10nArgs: { tabCount: tabs.length } } 4736 ); 4737 } 4738 4739 removeAllDuplicateTabs() { 4740 // I would like to have the caller provide this target, 4741 // but the caller lives in a different document. 4742 let alltabsButton = document.getElementById("alltabs-button"); 4743 this._removeDuplicateTabs( 4744 alltabsButton, 4745 this.getAllDuplicateTabsToClose(), 4746 this.closingTabsEnum.ALL_DUPLICATES 4747 ); 4748 } 4749 4750 /** 4751 * In a multi-select context, the tabs (except pinned tabs) that are located to the 4752 * left of the leftmost selected tab will be removed. 4753 */ 4754 removeTabsToTheStartFrom(aTab, options) { 4755 let tabs = this._getTabsToTheStartFrom(aTab); 4756 if ( 4757 !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_START) 4758 ) { 4759 return; 4760 } 4761 4762 this.removeTabs(tabs, options); 4763 } 4764 4765 /** 4766 * In a multi-select context, the tabs (except pinned tabs) that are located to the 4767 * right of the rightmost selected tab will be removed. 4768 */ 4769 removeTabsToTheEndFrom(aTab, options) { 4770 let tabs = this._getTabsToTheEndFrom(aTab); 4771 if ( 4772 !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END) 4773 ) { 4774 return; 4775 } 4776 4777 this.removeTabs(tabs, options); 4778 } 4779 4780 /** 4781 * Remove all tabs but `aTab`. By default, in a multi-select context, all 4782 * unpinned and unselected tabs are removed. Otherwise all unpinned tabs 4783 * except aTab are removed. This behavior can be changed using the the bool 4784 * flags below. 4785 * 4786 * @param {MozTabbrowserTab} aTab 4787 * The tab we will skip removing 4788 * @param {object} [aParams] 4789 * An optional set of parameters that will be passed to the 4790 * `removeTabs` function. 4791 * @param {boolean} [aParams.skipWarnAboutClosingTabs=false] 4792 * Skip showing the tab close warning prompt. 4793 * @param {boolean} [aParams.skipPinnedOrSelectedTabs=true] 4794 * Skip closing tabs that are selected or pinned. 4795 */ 4796 removeAllTabsBut(aTab, aParams = {}) { 4797 let { 4798 skipWarnAboutClosingTabs = false, 4799 skipPinnedOrSelectedTabs = true, 4800 } = aParams; 4801 4802 /** @type {function(MozTabbrowserTab):boolean} */ 4803 let filterFn; 4804 4805 // If enabled also filter by selected or pinned state. 4806 if (skipPinnedOrSelectedTabs) { 4807 if (aTab?.multiselected) { 4808 filterFn = tab => !tab.multiselected && !tab.pinned && !tab.hidden; 4809 } else { 4810 filterFn = tab => tab != aTab && !tab.pinned && !tab.hidden; 4811 } 4812 } else { 4813 // Exclude just aTab from being removed. 4814 filterFn = tab => tab != aTab; 4815 } 4816 4817 let tabsToRemove = this.openTabs.filter(filterFn); 4818 4819 // If enabled show the tab close warning. 4820 if ( 4821 !skipWarnAboutClosingTabs && 4822 !this.warnAboutClosingTabs( 4823 tabsToRemove.length, 4824 this.closingTabsEnum.OTHER 4825 ) 4826 ) { 4827 return; 4828 } 4829 4830 this.removeTabs(tabsToRemove, aParams); 4831 } 4832 4833 removeMultiSelectedTabs({ isUserTriggered, telemetrySource } = {}) { 4834 let selectedTabs = this.selectedTabs; 4835 if ( 4836 !this.warnAboutClosingTabs( 4837 selectedTabs.length, 4838 this.closingTabsEnum.MULTI_SELECTED 4839 ) 4840 ) { 4841 return; 4842 } 4843 4844 this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource }); 4845 } 4846 4847 /** 4848 * @typedef {object} _startRemoveTabsReturnValue 4849 * @property {Promise<void>} beforeUnloadComplete 4850 * A promise that is resolved once all the beforeunload handlers have been 4851 * called. 4852 * @property {object[]} tabsWithBeforeUnloadPrompt 4853 * An array of tabs with unload prompts that need to be handled. 4854 * @property {object} [lastToClose] 4855 * The last tab to be closed, if appropriate. 4856 */ 4857 4858 /** 4859 * Starts to remove tabs from the UI: checking for beforeunload handlers, 4860 * closing tabs where possible and triggering running of the unload handlers. 4861 * 4862 * @param {object[]} tabs 4863 * The set of tabs to remove. 4864 * @param {object} options 4865 * @param {boolean} options.animate 4866 * Whether or not to animate closing. 4867 * @param {boolean} options.suppressWarnAboutClosingWindow 4868 * This will supress the warning about closing a window with the last tab. 4869 * @param {boolean} options.skipPermitUnload 4870 * Skips the before unload checks for the tabs. Only set this to true when 4871 * using it in tandem with `runBeforeUnloadForTabs`. 4872 * @param {boolean} options.skipRemoves 4873 * Skips actually removing the tabs. The beforeunload handlers still run. 4874 * @param {boolean} options.skipSessionStore 4875 * If true, don't record the closed tabs in SessionStore. 4876 * @returns {_startRemoveTabsReturnValue} 4877 */ 4878 _startRemoveTabs( 4879 tabs, 4880 { 4881 animate, 4882 // See bug 1883051 4883 // eslint-disable-next-line no-unused-vars 4884 suppressWarnAboutClosingWindow, 4885 skipPermitUnload, 4886 skipRemoves, 4887 skipSessionStore, 4888 isUserTriggered, 4889 telemetrySource, 4890 } 4891 ) { 4892 // Note: if you change any of the unload algorithm, consider also 4893 // changing `runBeforeUnloadForTabs` above. 4894 /** @type {MozTabbrowserTab[]} */ 4895 let tabsWithBeforeUnloadPrompt = []; 4896 /** @type {MozTabbrowserTab[]} */ 4897 let tabsWithoutBeforeUnload = []; 4898 /** @type {Promise<void>[]} */ 4899 let beforeUnloadPromises = []; 4900 /** @type {MozTabbrowserTab|undefined} */ 4901 let lastToClose; 4902 4903 for (let tab of tabs) { 4904 if (!skipRemoves) { 4905 tab._closedInMultiselection = true; 4906 } 4907 if (!skipRemoves && tab.selected) { 4908 lastToClose = tab; 4909 let toBlurTo = this._findTabToBlurTo(lastToClose, tabs); 4910 if (toBlurTo) { 4911 this._getSwitcher().warmupTab(toBlurTo); 4912 } 4913 } else if (!skipPermitUnload && this._hasBeforeUnload(tab)) { 4914 let timerId = Glean.browserTabclose.permitUnloadTime.start(); 4915 // We need to block while calling permitUnload() because it 4916 // processes the event queue and may lead to another removeTab() 4917 // call before permitUnload() returns. 4918 tab._pendingPermitUnload = true; 4919 beforeUnloadPromises.push( 4920 // To save time, we first run the beforeunload event listeners in all 4921 // content processes in parallel. Tabs that would have shown a prompt 4922 // will be handled again later. 4923 tab.linkedBrowser.asyncPermitUnload("dontUnload").then( 4924 ({ permitUnload }) => { 4925 tab._pendingPermitUnload = false; 4926 Glean.browserTabclose.permitUnloadTime.stopAndAccumulate( 4927 timerId 4928 ); 4929 if (tab.closing) { 4930 // The tab was closed by the user while we were in permitUnload, don't 4931 // attempt to close it a second time. 4932 } else if (permitUnload) { 4933 if (!skipRemoves) { 4934 // OK to close without prompting, do it immediately. 4935 this.removeTab(tab, { 4936 animate, 4937 prewarmed: true, 4938 skipPermitUnload: true, 4939 skipSessionStore, 4940 }); 4941 } 4942 } else { 4943 // We will need to prompt, queue it so it happens sequentially. 4944 tabsWithBeforeUnloadPrompt.push(tab); 4945 } 4946 }, 4947 err => { 4948 console.error("error while calling asyncPermitUnload", err); 4949 tab._pendingPermitUnload = false; 4950 Glean.browserTabclose.permitUnloadTime.stopAndAccumulate( 4951 timerId 4952 ); 4953 } 4954 ) 4955 ); 4956 } else { 4957 tabsWithoutBeforeUnload.push(tab); 4958 } 4959 } 4960 4961 // Now that all the beforeunload IPCs have been sent to content processes, 4962 // we can queue unload messages for all the tabs without beforeunload listeners. 4963 // Doing this first would cause content process main threads to be busy and delay 4964 // beforeunload responses, which would be user-visible. 4965 if (!skipRemoves) { 4966 for (let tab of tabsWithoutBeforeUnload) { 4967 this.removeTab(tab, { 4968 animate, 4969 prewarmed: true, 4970 skipPermitUnload, 4971 skipSessionStore, 4972 isUserTriggered, 4973 telemetrySource, 4974 }); 4975 } 4976 } 4977 4978 return { 4979 beforeUnloadComplete: Promise.all(beforeUnloadPromises), 4980 tabsWithBeforeUnloadPrompt, 4981 lastToClose, 4982 }; 4983 } 4984 4985 /** 4986 * Runs the before unload handler for the provided tabs, waiting for them 4987 * to complete. 4988 * 4989 * This can be used in tandem with removeTabs to allow any before unload 4990 * prompts to happen before any tab closures. This should only be used 4991 * in the case where any prompts need to happen before other items before 4992 * the actual tabs are closed. 4993 * 4994 * When using this function alongside removeTabs, specify the `skipUnload` 4995 * option to removeTabs. 4996 * 4997 * @param {object[]} tabs 4998 * An array of tabs to remove. 4999 * @returns {Promise<boolean>} 5000 * Returns true if the unload has been blocked by the user. False if tabs 5001 * may be subsequently closed. 5002 */ 5003 async runBeforeUnloadForTabs(tabs) { 5004 try { 5005 let { beforeUnloadComplete, tabsWithBeforeUnloadPrompt } = 5006 this._startRemoveTabs(tabs, { 5007 animate: false, 5008 suppressWarnAboutClosingWindow: false, 5009 skipPermitUnload: false, 5010 skipRemoves: true, 5011 }); 5012 5013 await beforeUnloadComplete; 5014 5015 // Now run again sequentially the beforeunload listeners that will result in a prompt. 5016 for (let tab of tabsWithBeforeUnloadPrompt) { 5017 tab._pendingPermitUnload = true; 5018 let { permitUnload } = this.getBrowserForTab(tab).permitUnload(); 5019 tab._pendingPermitUnload = false; 5020 if (!permitUnload) { 5021 return true; 5022 } 5023 } 5024 } catch (e) { 5025 console.error(e); 5026 } 5027 return false; 5028 } 5029 5030 /** 5031 * Given an array of tabs, returns a tuple [groups, leftoverTabs] such that: 5032 * - groups contains all groups whose tabs are a subset of the initial array 5033 * - leftoverTabs contains the remaining tabs 5034 * 5035 * @param {Array} tabs list of tabs 5036 * @returns {Array} a tuple where the first element is an array of groups 5037 * and the second is an array of tabs 5038 */ 5039 #separateWholeGroups(tabs) { 5040 /** 5041 * Map of tab group to surviving tabs in the group. 5042 * If any of the `tabs` to be removed belong to a tab group, keep track 5043 * of how many tabs in the tab group will be left after removing `tabs`. 5044 * For any tab group with 0 surviving tabs, we can know that that tab 5045 * group will be removed as a consequence of removing these `tabs`. 5046 * 5047 * @type {Map<MozTabbrowserTabGroup, Set<MozTabbrowserTab>>} 5048 */ 5049 let tabGroupSurvivingTabs = new Map(); 5050 let wholeGroups = []; 5051 for (let tab of tabs) { 5052 if (tab.group) { 5053 if (!tabGroupSurvivingTabs.has(tab.group)) { 5054 tabGroupSurvivingTabs.set(tab.group, new Set(tab.group.tabs)); 5055 } 5056 tabGroupSurvivingTabs.get(tab.group).delete(tab); 5057 } 5058 } 5059 5060 for (let [tabGroup, survivingTabs] of tabGroupSurvivingTabs.entries()) { 5061 if (!survivingTabs.size) { 5062 wholeGroups.push(tabGroup); 5063 tabs = tabs.filter(t => !tabGroup.tabs.includes(t)); 5064 } 5065 } 5066 5067 return [wholeGroups, tabs]; 5068 } 5069 5070 /** 5071 * Removes multiple tabs from the tab browser. 5072 * 5073 * @param {MozTabbrowserTab[]} tabs 5074 * The set of tabs to remove. 5075 * @param {object} [options] 5076 * @param {boolean} [options.animate] 5077 * Whether or not to animate closing, defaults to true. 5078 * @param {boolean} [options.suppressWarnAboutClosingWindow] 5079 * This will supress the warning about closing a window with the last tab. 5080 * @param {boolean} [options.skipPermitUnload] 5081 * Skips the before unload checks for the tabs. Only set this to true when 5082 * using it in tandem with `runBeforeUnloadForTabs`. 5083 * @param {boolean} [options.skipSessionStore] 5084 * If true, don't record the closed tabs in SessionStore. 5085 * @param {boolean} [options.skipGroupCheck] 5086 * Skip separate processing of whole tab groups from the set of tabs. 5087 * Used by removeTabGroup. 5088 * @param {boolean} [options.isUserTriggered] 5089 * Whether or not the removal is the direct result of a user action. 5090 * Used for telemetry. 5091 * @param {string} [options.telemetrySource] 5092 * The system, surface, or control the user used to take this action. 5093 * @see TabMetrics.METRIC_SOURCE for possible values. 5094 */ 5095 removeTabs( 5096 tabs, 5097 { 5098 animate = true, 5099 suppressWarnAboutClosingWindow = false, 5100 skipPermitUnload = false, 5101 skipSessionStore = false, 5102 skipGroupCheck = false, 5103 isUserTriggered = false, 5104 telemetrySource, 5105 } = {} 5106 ) { 5107 // When 'closeWindowWithLastTab' pref is enabled, closing all tabs 5108 // can be considered equivalent to closing the window. 5109 if ( 5110 this.tabs.length == tabs.length && 5111 Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab") 5112 ) { 5113 window.closeWindow( 5114 true, 5115 suppressWarnAboutClosingWindow ? null : window.warnAboutClosingWindow, 5116 "close-last-tab" 5117 ); 5118 return; 5119 } 5120 5121 if (!skipSessionStore) { 5122 SessionStore.resetLastClosedTabCount(window); 5123 } 5124 this._clearMultiSelectionLocked = true; 5125 5126 // Guarantee that _clearMultiSelectionLocked lock gets released. 5127 try { 5128 // If selection includes entire groups, we might want to save them 5129 if (!skipGroupCheck) { 5130 let [groups, leftoverTabs] = this.#separateWholeGroups(tabs); 5131 groups.forEach(group => { 5132 if (!skipSessionStore) { 5133 group.save(); 5134 } 5135 this.removeTabGroup(group, { 5136 animate, 5137 skipSessionStore, 5138 skipPermitUnload, 5139 isUserTriggered, 5140 telemetrySource, 5141 }); 5142 }); 5143 tabs = leftoverTabs; 5144 } 5145 5146 let { beforeUnloadComplete, tabsWithBeforeUnloadPrompt, lastToClose } = 5147 this._startRemoveTabs(tabs, { 5148 animate, 5149 suppressWarnAboutClosingWindow, 5150 skipPermitUnload, 5151 skipRemoves: false, 5152 skipSessionStore, 5153 isUserTriggered, 5154 telemetrySource, 5155 }); 5156 5157 // Wait for all the beforeunload events to have been processed by content processes. 5158 // The permitUnload() promise will, alas, not call its resolution 5159 // callbacks after the browser window the promise lives in has closed, 5160 // so we have to check for that case explicitly. 5161 let done = false; 5162 beforeUnloadComplete.then(() => { 5163 done = true; 5164 }); 5165 Services.tm.spinEventLoopUntilOrQuit( 5166 "tabbrowser.js:removeTabs", 5167 () => done || window.closed 5168 ); 5169 if (!done) { 5170 return; 5171 } 5172 5173 let aParams = { 5174 animate, 5175 prewarmed: true, 5176 skipPermitUnload, 5177 skipSessionStore, 5178 isUserTriggered, 5179 telemetrySource, 5180 }; 5181 5182 // Now run again sequentially the beforeunload listeners that will result in a prompt. 5183 for (let tab of tabsWithBeforeUnloadPrompt) { 5184 this.removeTab(tab, aParams); 5185 if (!tab.closing) { 5186 // If we abort the closing of the tab. 5187 tab._closedInMultiselection = false; 5188 } 5189 } 5190 5191 // Avoid changing the selected browser several times by removing it, 5192 // if appropriate, lastly. 5193 if (lastToClose) { 5194 this.removeTab(lastToClose, aParams); 5195 } 5196 } catch (e) { 5197 console.error(e); 5198 } 5199 5200 this._clearMultiSelectionLocked = false; 5201 this._avoidSingleSelectedTab(); 5202 } 5203 5204 removeCurrentTab(aParams) { 5205 this.removeTab(this.selectedTab, aParams); 5206 } 5207 5208 removeTab( 5209 aTab, 5210 { 5211 animate, 5212 triggeringEvent, 5213 skipPermitUnload, 5214 closeWindowWithLastTab, 5215 prewarmed, 5216 skipSessionStore, 5217 isUserTriggered, 5218 telemetrySource, 5219 } = {} 5220 ) { 5221 if (UserInteraction.running("browser.tabs.opening", window)) { 5222 UserInteraction.finish("browser.tabs.opening", window); 5223 } 5224 5225 // Telemetry stopwatches may already be running if removeTab gets 5226 // called again for an already closing tab. 5227 if (!aTab._closeTimeAnimTimerId && !aTab._closeTimeNoAnimTimerId) { 5228 // Speculatevely start both stopwatches now. We'll cancel one of 5229 // the two later depending on whether we're animating. 5230 aTab._closeTimeAnimTimerId = Glean.browserTabclose.timeAnim.start(); 5231 aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start(); 5232 } 5233 5234 // Handle requests for synchronously removing an already 5235 // asynchronously closing tab. 5236 if (!animate && aTab.closing) { 5237 this._endRemoveTab(aTab); 5238 return; 5239 } 5240 5241 let isVisibleTab = aTab.visible; 5242 // We have to sample the tab width now, since _beginRemoveTab might 5243 // end up modifying the DOM in such a way that aTab gets a new 5244 // frame created for it (for example, by updating the visually selected 5245 // state). 5246 let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; 5247 let isLastTab = this.#isLastTabInWindow(aTab); 5248 if ( 5249 !this._beginRemoveTab(aTab, { 5250 closeWindowFastpath: true, 5251 skipPermitUnload, 5252 closeWindowWithLastTab, 5253 prewarmed, 5254 skipSessionStore, 5255 isUserTriggered, 5256 telemetrySource, 5257 }) 5258 ) { 5259 Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); 5260 aTab._closeTimeAnimTimerId = null; 5261 Glean.browserTabclose.timeNoAnim.cancel(aTab._closeTimeNoAnimTimerId); 5262 aTab._closeTimeNoAnimTimerId = null; 5263 return; 5264 } 5265 5266 let lockTabSizing = 5267 !this.tabContainer.verticalMode && 5268 !aTab.pinned && 5269 isVisibleTab && 5270 aTab._fullyOpen && 5271 triggeringEvent?.inputSource == MouseEvent.MOZ_SOURCE_MOUSE && 5272 triggeringEvent?.target.closest(".tabbrowser-tab"); 5273 if (lockTabSizing) { 5274 this.tabContainer._lockTabSizing(aTab, tabWidth); 5275 } else { 5276 this.tabContainer._unlockTabSizing(); 5277 } 5278 5279 if ( 5280 !animate /* the caller didn't opt in */ || 5281 gReduceMotion || 5282 isLastTab || 5283 aTab.pinned || 5284 !isVisibleTab || 5285 this.tabContainer.verticalMode || 5286 this._removingTabs.size > 5287 3 /* don't want lots of concurrent animations */ || 5288 !aTab.hasAttribute( 5289 "fadein" 5290 ) /* fade-in transition hasn't been triggered yet */ || 5291 tabWidth == 0 /* fade-in transition hasn't moved yet */ 5292 ) { 5293 // We're not animating, so we can cancel the animation stopwatch. 5294 Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); 5295 aTab._closeTimeAnimTimerId = null; 5296 this._endRemoveTab(aTab); 5297 return; 5298 } 5299 5300 // We're animating, so we can cancel the non-animation stopwatch. 5301 Glean.browserTabclose.timeNoAnim.cancel(aTab._closeTimeNoAnimTimerId); 5302 aTab._closeTimeNoAnimTimerId = null; 5303 5304 aTab.style.maxWidth = ""; // ensure that fade-out transition happens 5305 aTab.removeAttribute("fadein"); 5306 aTab.removeAttribute("bursting"); 5307 5308 setTimeout( 5309 function (tab, tabbrowser) { 5310 if ( 5311 tab.container && 5312 window.getComputedStyle(tab).maxWidth == "0.1px" 5313 ) { 5314 console.assert( 5315 false, 5316 "Giving up waiting for the tab closing animation to finish (bug 608589)" 5317 ); 5318 tabbrowser._endRemoveTab(tab); 5319 } 5320 }, 5321 3000, 5322 aTab, 5323 this 5324 ); 5325 } 5326 5327 /** 5328 * Returns `true` if `tab` is the last tab in this window. This logic is 5329 * intended for cases like determining if a window should close due to `tab` 5330 * being closed, therefore hidden tabs are not considered in this function. 5331 * 5332 * Note: must be called before `tab` is closed/closing. 5333 * 5334 * @param {MozTabbrowserTab} tab 5335 * @returns {boolean} 5336 */ 5337 #isLastTabInWindow(tab) { 5338 for (const otherTab of this.tabs) { 5339 if (otherTab != tab && otherTab.isOpen && !otherTab.hidden) { 5340 return false; 5341 } 5342 } 5343 return true; 5344 } 5345 5346 _hasBeforeUnload(aTab) { 5347 let browser = aTab.linkedBrowser; 5348 if (browser.isRemoteBrowser && browser.frameLoader) { 5349 return browser.hasBeforeUnload; 5350 } 5351 return false; 5352 } 5353 5354 _beginRemoveTab( 5355 aTab, 5356 { 5357 adoptedByTab, 5358 closeWindowWithLastTab, 5359 closeWindowFastpath, 5360 skipPermitUnload, 5361 prewarmed, 5362 skipSessionStore = false, 5363 isUserTriggered, 5364 telemetrySource, 5365 } = {} 5366 ) { 5367 if (aTab.closing || this._windowIsClosing) { 5368 return false; 5369 } 5370 5371 var browser = this.getBrowserForTab(aTab); 5372 if ( 5373 !skipPermitUnload && 5374 !adoptedByTab && 5375 aTab.linkedPanel && 5376 !aTab._pendingPermitUnload && 5377 (!browser.isRemoteBrowser || this._hasBeforeUnload(aTab)) 5378 ) { 5379 if (!prewarmed) { 5380 let blurTab = this._findTabToBlurTo(aTab); 5381 if (blurTab) { 5382 this.warmupTab(blurTab); 5383 } 5384 } 5385 5386 let timerId = Glean.browserTabclose.permitUnloadTime.start(); 5387 5388 // We need to block while calling permitUnload() because it 5389 // processes the event queue and may lead to another removeTab() 5390 // call before permitUnload() returns. 5391 aTab._pendingPermitUnload = true; 5392 let { permitUnload } = browser.permitUnload(); 5393 aTab._pendingPermitUnload = false; 5394 5395 Glean.browserTabclose.permitUnloadTime.stopAndAccumulate(timerId); 5396 5397 // If we were closed during onbeforeunload, we return false now 5398 // so we don't (try to) close the same tab again. Of course, we 5399 // also stop if the unload was cancelled by the user: 5400 if (aTab.closing || !permitUnload) { 5401 return false; 5402 } 5403 } 5404 5405 this.tabContainer._invalidateCachedVisibleTabs(); 5406 5407 // this._switcher would normally cover removing a tab from this 5408 // cache, but we may not have one at this time. 5409 let tabCacheIndex = this._tabLayerCache.indexOf(aTab); 5410 if (tabCacheIndex != -1) { 5411 this._tabLayerCache.splice(tabCacheIndex, 1); 5412 } 5413 5414 // Delay hiding the the active tab if we're screen sharing. 5415 // See Bug 1642747. 5416 let screenShareInActiveTab = 5417 aTab == this.selectedTab && aTab._sharingState?.webRTC?.screen; 5418 5419 if (!screenShareInActiveTab) { 5420 this._blurTab(aTab); 5421 } 5422 5423 var closeWindow = false; 5424 var newTab = false; 5425 if (this.#isLastTabInWindow(aTab)) { 5426 closeWindow = 5427 closeWindowWithLastTab != null 5428 ? closeWindowWithLastTab 5429 : !window.toolbar.visible || 5430 Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab"); 5431 5432 if (closeWindow) { 5433 // We've already called beforeunload on all the relevant tabs if we get here, 5434 // so avoid calling it again: 5435 window.skipNextCanClose = true; 5436 } 5437 5438 // Closing the tab and replacing it with a blank one is notably slower 5439 // than closing the window right away. If the caller opts in, take 5440 // the fast path. 5441 if (closeWindow && closeWindowFastpath && !this._removingTabs.size) { 5442 // This call actually closes the window, unless the user 5443 // cancels the operation. We are finished here in both cases. 5444 this._windowIsClosing = window.closeWindow( 5445 true, 5446 window.warnAboutClosingWindow, 5447 "close-last-tab" 5448 ); 5449 return false; 5450 } 5451 5452 newTab = true; 5453 } 5454 aTab._endRemoveArgs = [closeWindow, newTab]; 5455 5456 // swapBrowsersAndCloseOther will take care of closing the window without animation. 5457 if (closeWindow && adoptedByTab) { 5458 // Remove the tab's filter and progress listener to avoid leaking. 5459 if (aTab.linkedPanel) { 5460 const filter = this._tabFilters.get(aTab); 5461 browser.webProgress.removeProgressListener(filter); 5462 const listener = this._tabListeners.get(aTab); 5463 filter.removeProgressListener(listener); 5464 listener.destroy(); 5465 this._tabListeners.delete(aTab); 5466 this._tabFilters.delete(aTab); 5467 } 5468 return true; 5469 } 5470 5471 if (!aTab._fullyOpen) { 5472 // If the opening tab animation hasn't finished before we start closing the 5473 // tab, decrement the animation count since _handleNewTab will not get called. 5474 this.tabAnimationsInProgress--; 5475 } 5476 5477 this.tabAnimationsInProgress++; 5478 5479 // Mute audio immediately to improve perceived speed of tab closure. 5480 if (!adoptedByTab && aTab.hasAttribute("soundplaying")) { 5481 // Don't persist the muted state as this wasn't a user action. 5482 // This lets undo-close-tab return it to an unmuted state. 5483 aTab.linkedBrowser.mute(true); 5484 } 5485 5486 aTab.closing = true; 5487 this._removingTabs.add(aTab); 5488 this.tabContainer._invalidateCachedTabs(); 5489 5490 // Invalidate hovered tab state tracking for this closing tab. 5491 aTab._mouseleave(); 5492 5493 if (newTab) { 5494 this.addTrustedTab(BROWSER_NEW_TAB_URL, { 5495 skipAnimation: true, 5496 // In the event that insertAfterCurrent is set and the current tab is 5497 // inside a group that is being closed we want to avoid creating the 5498 // new tab inside that group. 5499 tabIndex: 0, 5500 }); 5501 } else { 5502 TabBarVisibility.update(); 5503 } 5504 5505 // Splice this tab out of any lines of succession before any events are 5506 // dispatched. 5507 this.replaceInSuccession(aTab, aTab.successor); 5508 this.setSuccessor(aTab, null); 5509 5510 // We're committed to closing the tab now. 5511 // Dispatch a notification. 5512 // We dispatch it before any teardown so that event listeners can 5513 // inspect the tab that's about to close. 5514 let evt = new CustomEvent("TabClose", { 5515 bubbles: true, 5516 detail: { 5517 adoptedBy: adoptedByTab, 5518 skipSessionStore, 5519 isUserTriggered, 5520 telemetrySource, 5521 }, 5522 }); 5523 aTab.dispatchEvent(evt); 5524 5525 if (this.tabs.length == 2) { 5526 // We're closing one of our two open tabs, inform the other tab that its 5527 // sibling is going away. 5528 for (let tab of this.tabs) { 5529 let bc = tab.linkedBrowser.browsingContext; 5530 if (bc) { 5531 bc.hasSiblings = false; 5532 } 5533 } 5534 } 5535 5536 let notificationBox = this.readNotificationBox(browser); 5537 notificationBox?._stack?.remove(); 5538 5539 if (aTab.linkedPanel) { 5540 if (!adoptedByTab && !gMultiProcessBrowser) { 5541 // Prevent this tab from showing further dialogs, since we're closing it 5542 browser.contentWindow.windowUtils.disableDialogs(); 5543 } 5544 5545 // Remove the tab's filter and progress listener. 5546 const filter = this._tabFilters.get(aTab); 5547 5548 browser.webProgress.removeProgressListener(filter); 5549 5550 const listener = this._tabListeners.get(aTab); 5551 filter.removeProgressListener(listener); 5552 listener.destroy(); 5553 } 5554 5555 if (browser.registeredOpenURI && !adoptedByTab) { 5556 let userContextId = browser.getAttribute("usercontextid") || 0; 5557 this.UrlbarProviderOpenTabs.unregisterOpenTab( 5558 browser.registeredOpenURI.spec, 5559 userContextId, 5560 aTab.group?.id, 5561 PrivateBrowsingUtils.isWindowPrivate(window) 5562 ); 5563 delete browser.registeredOpenURI; 5564 } 5565 5566 // We are no longer the primary content area. 5567 browser.removeAttribute("primary"); 5568 5569 return true; 5570 } 5571 5572 _endRemoveTab(aTab) { 5573 if (!aTab || !aTab._endRemoveArgs) { 5574 return; 5575 } 5576 5577 var [aCloseWindow, aNewTab] = aTab._endRemoveArgs; 5578 aTab._endRemoveArgs = null; 5579 5580 if (this._windowIsClosing) { 5581 aCloseWindow = false; 5582 aNewTab = false; 5583 } 5584 5585 this.tabAnimationsInProgress--; 5586 5587 this._lastRelatedTabMap = new WeakMap(); 5588 5589 // update the UI early for responsiveness 5590 aTab.collapsed = true; 5591 this._blurTab(aTab); 5592 5593 this._removingTabs.delete(aTab); 5594 5595 if (aCloseWindow) { 5596 this._windowIsClosing = true; 5597 for (let tab of this._removingTabs) { 5598 this._endRemoveTab(tab); 5599 } 5600 } else if (!this._windowIsClosing) { 5601 if (aNewTab) { 5602 gURLBar.select(); 5603 } 5604 } 5605 5606 // We're going to remove the tab and the browser now. 5607 this._tabFilters.delete(aTab); 5608 this._tabListeners.delete(aTab); 5609 5610 var browser = this.getBrowserForTab(aTab); 5611 5612 if (aTab.linkedPanel) { 5613 // Because of the fact that we are setting JS properties on 5614 // the browser elements, and we have code in place 5615 // to preserve the JS objects for any elements that have 5616 // JS properties set on them, the browser element won't be 5617 // destroyed until the document goes away. So we force a 5618 // cleanup ourselves. 5619 // This has to happen before we remove the child since functions 5620 // like `getBrowserContainer` expect the browser to be parented. 5621 browser.destroy(); 5622 } 5623 5624 // Remove the tab ... 5625 aTab.remove(); 5626 this.tabContainer._invalidateCachedTabs(); 5627 5628 // ... and fix up the _tPos properties immediately. 5629 for (let i = aTab._tPos; i < this.tabs.length; i++) { 5630 this.tabs[i]._tPos = i; 5631 } 5632 5633 if (!this._windowIsClosing) { 5634 // update tab close buttons state 5635 this.tabContainer._updateCloseButtons(); 5636 5637 setTimeout( 5638 function (tabs) { 5639 tabs._lastTabClosedByMouse = false; 5640 }, 5641 0, 5642 this.tabContainer 5643 ); 5644 } 5645 5646 // update tab positional properties and attributes 5647 this.selectedTab._selected = true; 5648 5649 // Removing the panel requires fixing up selectedPanel immediately 5650 // (see below), which would be hindered by the potentially expensive 5651 // browser removal. So we remove the browser and the panel in two 5652 // steps. 5653 5654 var panel = this.getPanel(browser); 5655 5656 // In the multi-process case, it's possible an asynchronous tab switch 5657 // is still underway. If so, then it's possible that the last visible 5658 // browser is the one we're in the process of removing. There's the 5659 // risk of displaying preloaded browsers that are at the end of the 5660 // deck if we remove the browser before the switch is complete, so 5661 // we alert the switcher in order to show a spinner instead. 5662 if (this._switcher) { 5663 this._switcher.onTabRemoved(aTab); 5664 } 5665 5666 // This will unload the document. An unload handler could remove 5667 // dependant tabs, so it's important that the tabbrowser is now in 5668 // a consistent state (tab removed, tab positions updated, etc.). 5669 browser.remove(); 5670 5671 // Release the browser in case something is erroneously holding a 5672 // reference to the tab after its removal. 5673 this._tabForBrowser.delete(aTab.linkedBrowser); 5674 aTab.linkedBrowser = null; 5675 5676 panel.remove(); 5677 5678 // closeWindow might wait an arbitrary length of time if we're supposed 5679 // to warn about closing the window, so we'll just stop the tab close 5680 // stopwatches here instead. 5681 if (aTab._closeTimeAnimTimerId) { 5682 Glean.browserTabclose.timeAnim.stopAndAccumulate( 5683 aTab._closeTimeAnimTimerId 5684 ); 5685 aTab._closeTimeAnimTimerId = null; 5686 } 5687 if (aTab._closeTimeNoAnimTimerId) { 5688 Glean.browserTabclose.timeNoAnim.stopAndAccumulate( 5689 aTab._closeTimeNoAnimTimerId 5690 ); 5691 aTab._closeTimeNoAnimTimerId = null; 5692 } 5693 5694 if (aCloseWindow) { 5695 this._windowIsClosing = closeWindow( 5696 true, 5697 window.warnAboutClosingWindow, 5698 "close-last-tab" 5699 ); 5700 } 5701 } 5702 5703 /** 5704 * Closes all tabs matching the list of nsURIs. 5705 * This does not close any tabs that have a beforeUnload prompt. 5706 * 5707 * @param {nsURI[]} urisToClose 5708 * The set of uris to remove. 5709 * @returns {number} The count of successfully closed tabs. 5710 */ 5711 async closeTabsByURI(urisToClose) { 5712 let tabsToRemove = []; 5713 for (let tab of this.tabs) { 5714 let currentURI = tab.linkedBrowser.currentURI; 5715 // Find any URI that matches the current tab's URI 5716 const matchedIndex = urisToClose.findIndex(uriToClose => 5717 uriToClose.equals(currentURI) 5718 ); 5719 5720 if (matchedIndex > -1) { 5721 tabsToRemove.push(tab); 5722 } 5723 } 5724 5725 let closedCount = 0; 5726 5727 if (tabsToRemove.length) { 5728 const { beforeUnloadComplete, lastToClose } = this._startRemoveTabs( 5729 tabsToRemove, 5730 { 5731 animate: false, 5732 suppressWarnAboutClosingWindow: true, 5733 skipPermitUnload: false, 5734 skipRemoves: false, 5735 skipSessionStore: false, 5736 } 5737 ); 5738 5739 // Wait for the beforeUnload handlers to complete. 5740 await beforeUnloadComplete; 5741 5742 closedCount = tabsToRemove.length - (lastToClose ? 1 : 0); 5743 5744 // _startRemoveTabs doesn't close the last tab in the window 5745 // for this use case, we simply close it 5746 if (lastToClose) { 5747 this.removeTab(lastToClose); 5748 closedCount++; 5749 } 5750 } 5751 return closedCount; 5752 } 5753 5754 async explicitUnloadTabs(tabs) { 5755 let unloadBlocked = await this.runBeforeUnloadForTabs(tabs); 5756 if (unloadBlocked) { 5757 return; 5758 } 5759 let unloadSelectedTab = false; 5760 let allTabsUnloaded = false; 5761 if (tabs.some(tab => tab.selected)) { 5762 // Unloading the currently selected tab. 5763 // Need to select a different one before unloading. 5764 // Avoid selecting any tab we're unloading now or 5765 // any tab that is already unloaded. 5766 unloadSelectedTab = true; 5767 const tabsToExclude = tabs.concat( 5768 this.tabContainer.allTabs.filter(tab => !tab.linkedPanel) 5769 ); 5770 let newTab = this._findTabToBlurTo(this.selectedTab, tabsToExclude); 5771 if (newTab) { 5772 this.selectedTab = newTab; 5773 } else { 5774 allTabsUnloaded = true; 5775 // all tabs are unloaded - show Firefox View if it's present, otherwise open a new tab 5776 // Firefox View counts as present if its tab is already open, or if the button 5777 // is visible, so as to not do this in private browsing mode or if the user 5778 // has removed the button from their toolbar (bug 1946432, bug 1989429) 5779 let firefoxViewAvailable = 5780 FirefoxViewHandler.tab && 5781 FirefoxViewHandler.button?.checkVisibility({ 5782 checkVisibilityCSS: true, 5783 visibilityProperty: true, 5784 }); 5785 if (firefoxViewAvailable) { 5786 FirefoxViewHandler.openTab("opentabs"); 5787 } else { 5788 this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, { 5789 skipAnimation: true, 5790 }); 5791 } 5792 } 5793 } 5794 let memoryUsageBeforeUnload = await getTotalMemoryUsage(); 5795 let timeBeforeUnload = performance.now(); 5796 let numberOfTabsUnloaded = 0; 5797 await Promise.all(tabs.map(tab => this.prepareDiscardBrowser(tab))); 5798 5799 for (let tab of tabs) { 5800 numberOfTabsUnloaded += this.discardBrowser(tab, true) ? 1 : 0; 5801 } 5802 let timeElapsed = Math.floor(performance.now() - timeBeforeUnload); 5803 Glean.browserEngagement.tabExplicitUnload.record({ 5804 unload_selected_tab: unloadSelectedTab, 5805 all_tabs_unloaded: allTabsUnloaded, 5806 tabs_unloaded: numberOfTabsUnloaded, 5807 memory_before: memoryUsageBeforeUnload, 5808 memory_after: await getTotalMemoryUsage(), 5809 time_to_unload_in_ms: timeElapsed, 5810 }); 5811 } 5812 5813 /** 5814 * Handles opening a new tab with mouse middleclick. 5815 * 5816 * @param node 5817 * @param event 5818 * The click event 5819 */ 5820 handleNewTabMiddleClick(node, event) { 5821 // We should be using the disabled property here instead of the attribute, 5822 // but some elements that this function is used with don't support it (e.g. 5823 // menuitem). 5824 if (node.hasAttribute("disabled")) { 5825 return; 5826 } // Do nothing 5827 5828 if (event.button == 1) { 5829 BrowserCommands.openTab({ event }); 5830 // Stop the propagation of the click event, to prevent the event from being 5831 // handled more than once. 5832 // E.g. see https://bugzilla.mozilla.org/show_bug.cgi?id=1657992#c4 5833 event.stopPropagation(); 5834 event.preventDefault(); 5835 } 5836 } 5837 5838 /** 5839 * Finds the tab that we will blur to if we blur aTab. 5840 * 5841 * @param {MozTabbrowserTab} aTab 5842 * The tab we would blur 5843 * @param {MozTabbrowserTab[]} [aExcludeTabs=[]] 5844 * Tabs to exclude from our search (i.e., because they are being 5845 * closed along with aTab) 5846 */ 5847 _findTabToBlurTo(aTab, aExcludeTabs = []) { 5848 if (!aTab.selected) { 5849 return null; 5850 } 5851 if (FirefoxViewHandler.tab) { 5852 aExcludeTabs.push(FirefoxViewHandler.tab); 5853 } 5854 5855 let excludeTabs = new Set(aExcludeTabs); 5856 5857 // If this tab has a successor, it should be selectable, since 5858 // hiding or closing a tab removes that tab as a successor. 5859 if (aTab.successor && !excludeTabs.has(aTab.successor)) { 5860 return aTab.successor; 5861 } 5862 5863 if ( 5864 aTab.owner?.visible && 5865 !excludeTabs.has(aTab.owner) && 5866 Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") 5867 ) { 5868 return aTab.owner; 5869 } 5870 5871 // Try to find a remaining tab that comes after the given tab 5872 let remainingTabs = Array.prototype.filter.call( 5873 this.visibleTabs, 5874 tab => !excludeTabs.has(tab) 5875 ); 5876 5877 let tab = this.tabContainer.findNextTab(aTab, { 5878 direction: 1, 5879 filter: _tab => remainingTabs.includes(_tab), 5880 }); 5881 5882 if (!tab) { 5883 tab = this.tabContainer.findNextTab(aTab, { 5884 direction: -1, 5885 filter: _tab => remainingTabs.includes(_tab), 5886 }); 5887 } 5888 5889 if (tab) { 5890 return tab; 5891 } 5892 5893 // If no qualifying visible tab was found, see if there is a tab in 5894 // a collapsed tab group that could be selected. 5895 let eligibleTabs = new Set(this.tabsInCollapsedTabGroups).difference( 5896 excludeTabs 5897 ); 5898 5899 tab = this.tabContainer.findNextTab(aTab, { 5900 direction: 1, 5901 filter: _tab => eligibleTabs.has(_tab), 5902 }); 5903 5904 if (!tab) { 5905 tab = this.tabContainer.findNextTab(aTab, { 5906 direction: -1, 5907 filter: _tab => eligibleTabs.has(_tab), 5908 }); 5909 } 5910 5911 return tab; 5912 } 5913 5914 _blurTab(aTab) { 5915 this.selectedTab = this._findTabToBlurTo(aTab); 5916 } 5917 5918 /** 5919 * @returns {boolean} 5920 * False if swapping isn't permitted, true otherwise. 5921 */ 5922 swapBrowsersAndCloseOther(aOurTab, aOtherTab) { 5923 // Do not allow transfering a private tab to a non-private window 5924 // and vice versa. 5925 if ( 5926 PrivateBrowsingUtils.isWindowPrivate(window) != 5927 PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerGlobal) 5928 ) { 5929 return false; 5930 } 5931 5932 // Do not allow transfering a useRemoteSubframes tab to a 5933 // non-useRemoteSubframes window and vice versa. 5934 if (gFissionBrowser != aOtherTab.ownerGlobal.gFissionBrowser) { 5935 return false; 5936 } 5937 5938 let ourBrowser = this.getBrowserForTab(aOurTab); 5939 let otherBrowser = aOtherTab.linkedBrowser; 5940 5941 // Can't swap between chrome and content processes. 5942 if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser) { 5943 return false; 5944 } 5945 5946 // Keep the userContextId if set on other browser 5947 if (otherBrowser.hasAttribute("usercontextid")) { 5948 ourBrowser.setAttribute( 5949 "usercontextid", 5950 otherBrowser.getAttribute("usercontextid") 5951 ); 5952 } 5953 5954 // That's gBrowser for the other window, not the tab's browser! 5955 var remoteBrowser = aOtherTab.ownerGlobal.gBrowser; 5956 var isPending = aOtherTab.hasAttribute("pending"); 5957 5958 let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab); 5959 let stateFlags = 0; 5960 if (otherTabListener) { 5961 stateFlags = otherTabListener.mStateFlags; 5962 } 5963 5964 // Expedite the removal of the icon if it was already scheduled. 5965 if (aOtherTab._soundPlayingAttrRemovalTimer) { 5966 clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer); 5967 aOtherTab._soundPlayingAttrRemovalTimer = 0; 5968 aOtherTab.removeAttribute("soundplaying"); 5969 remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]); 5970 } 5971 5972 // First, start teardown of the other browser. Make sure to not 5973 // fire the beforeunload event in the process. Close the other 5974 // window if this was its last tab. 5975 if ( 5976 !remoteBrowser._beginRemoveTab(aOtherTab, { 5977 adoptedByTab: aOurTab, 5978 closeWindowWithLastTab: true, 5979 }) 5980 ) { 5981 return false; 5982 } 5983 5984 // If this is the last tab of the window, hide the window 5985 // immediately without animation before the docshell swap, to avoid 5986 // about:blank being painted. 5987 let [closeWindow] = aOtherTab._endRemoveArgs; 5988 if (closeWindow) { 5989 let win = aOtherTab.ownerGlobal; 5990 win.windowUtils.suppressAnimation(true); 5991 // Only suppressing window animations isn't enough to avoid 5992 // an empty content area being painted. 5993 let baseWin = win.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow); 5994 baseWin.visibility = false; 5995 } 5996 5997 let modifiedAttrs = []; 5998 if (aOtherTab.hasAttribute("muted")) { 5999 aOurTab.toggleAttribute("muted", true); 6000 aOurTab.muteReason = aOtherTab.muteReason; 6001 // For non-lazy tabs, mute() must be called. 6002 if (aOurTab.linkedPanel) { 6003 ourBrowser.mute(); 6004 } 6005 modifiedAttrs.push("muted"); 6006 } 6007 if (aOtherTab.hasAttribute("discarded")) { 6008 aOurTab.toggleAttribute("discarded", true); 6009 modifiedAttrs.push("discarded"); 6010 } 6011 if (aOtherTab.hasAttribute("undiscardable")) { 6012 aOurTab.toggleAttribute("undiscardable", true); 6013 modifiedAttrs.push("undiscardable"); 6014 } 6015 if (aOtherTab.hasAttribute("soundplaying")) { 6016 aOurTab.toggleAttribute("soundplaying", true); 6017 modifiedAttrs.push("soundplaying"); 6018 } 6019 if (aOtherTab.hasAttribute("usercontextid")) { 6020 aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid")); 6021 modifiedAttrs.push("usercontextid"); 6022 } 6023 if (aOtherTab.hasAttribute("sharing")) { 6024 aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing")); 6025 modifiedAttrs.push("sharing"); 6026 aOurTab._sharingState = aOtherTab._sharingState; 6027 webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser); 6028 } 6029 if (aOtherTab.hasAttribute("pictureinpicture")) { 6030 aOurTab.toggleAttribute("pictureinpicture", true); 6031 modifiedAttrs.push("pictureinpicture"); 6032 6033 let event = new CustomEvent("TabSwapPictureInPicture", { 6034 detail: aOurTab, 6035 }); 6036 aOtherTab.dispatchEvent(event); 6037 } 6038 6039 // Copy tab note-related properties of the tab. 6040 aOurTab.hasTabNote = aOtherTab.hasTabNote; 6041 aOurTab.canonicalUrl = aOtherTab.canonicalUrl; 6042 6043 if (otherBrowser.isDistinctProductPageVisit) { 6044 ourBrowser.isDistinctProductPageVisit = true; 6045 } 6046 6047 SitePermissions.copyTemporaryPermissions(otherBrowser, ourBrowser); 6048 6049 // Add a reference to the original registeredOpenURI to the closing 6050 // tab so that events operating on the tab before close can reference it. 6051 aOtherTab._originalRegisteredOpenURI = otherBrowser.registeredOpenURI; 6052 6053 // If the other tab is pending (i.e. has not been restored, yet) 6054 // then do not switch docShells but retrieve the other tab's state 6055 // and apply it to our tab. 6056 if (isPending) { 6057 // Tag tab so that the extension framework can ignore tab events that 6058 // are triggered amidst the tab/browser restoration process 6059 // (TabHide, TabPinned, TabUnpinned, "muted" attribute changes, etc.). 6060 aOurTab.initializingTab = true; 6061 delete ourBrowser._cachedCurrentURI; 6062 SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab)); 6063 delete aOurTab.initializingTab; 6064 6065 // Make sure to unregister any open URIs. 6066 this._swapRegisteredOpenURIs(ourBrowser, otherBrowser); 6067 } else { 6068 // Workarounds for bug 458697 6069 // Icon might have been set on DOMLinkAdded, don't override that. 6070 if (!ourBrowser.mIconURL && otherBrowser.mIconURL) { 6071 this.setIcon(aOurTab, otherBrowser.mIconURL); 6072 } 6073 var isBusy = aOtherTab.hasAttribute("busy"); 6074 if (isBusy) { 6075 aOurTab.setAttribute("busy", "true"); 6076 modifiedAttrs.push("busy"); 6077 if (aOurTab.selected) { 6078 this._isBusy = true; 6079 } 6080 } 6081 6082 this._swapBrowserDocShells(aOurTab, otherBrowser, stateFlags); 6083 } 6084 6085 // Unregister the previously opened URI 6086 if (otherBrowser.registeredOpenURI) { 6087 let userContextId = otherBrowser.getAttribute("usercontextid") || 0; 6088 this.UrlbarProviderOpenTabs.unregisterOpenTab( 6089 otherBrowser.registeredOpenURI.spec, 6090 userContextId, 6091 aOtherTab.group?.id, 6092 PrivateBrowsingUtils.isWindowPrivate(window) 6093 ); 6094 delete otherBrowser.registeredOpenURI; 6095 } 6096 6097 // Handle findbar data (if any) 6098 let otherFindBar = aOtherTab._findBar; 6099 if (otherFindBar && otherFindBar.findMode == otherFindBar.FIND_NORMAL) { 6100 let oldValue = otherFindBar._findField.value; 6101 let wasHidden = otherFindBar.hidden; 6102 let ourFindBarPromise = this.getFindBar(aOurTab); 6103 ourFindBarPromise.then(ourFindBar => { 6104 if (!ourFindBar) { 6105 return; 6106 } 6107 ourFindBar._findField.value = oldValue; 6108 if (!wasHidden) { 6109 ourFindBar.onFindCommand(); 6110 } 6111 }); 6112 } 6113 6114 // Finish tearing down the tab that's going away. 6115 if (closeWindow) { 6116 aOtherTab.ownerGlobal.close(); 6117 } else { 6118 remoteBrowser._endRemoveTab(aOtherTab); 6119 } 6120 6121 this.setTabTitle(aOurTab); 6122 6123 // If the tab was already selected (this happens in the scenario 6124 // of replaceTabWithWindow), notify onLocationChange, etc. 6125 if (aOurTab.selected) { 6126 this.updateCurrentBrowser(true); 6127 } 6128 6129 if (modifiedAttrs.length) { 6130 this._tabAttrModified(aOurTab, modifiedAttrs); 6131 } 6132 6133 return true; 6134 } 6135 6136 swapBrowsers(aOurTab, aOtherTab) { 6137 let otherBrowser = aOtherTab.linkedBrowser; 6138 let otherTabBrowser = otherBrowser.getTabBrowser(); 6139 6140 // We aren't closing the other tab so, we also need to swap its tablisteners. 6141 let filter = otherTabBrowser._tabFilters.get(aOtherTab); 6142 let tabListener = otherTabBrowser._tabListeners.get(aOtherTab); 6143 otherBrowser.webProgress.removeProgressListener(filter); 6144 filter.removeProgressListener(tabListener); 6145 6146 // Perform the docshell swap through the common mechanism. 6147 this._swapBrowserDocShells(aOurTab, otherBrowser); 6148 6149 // Restore the listeners for the swapped in tab. 6150 tabListener = new otherTabBrowser.ownerGlobal.TabProgressListener( 6151 aOtherTab, 6152 otherBrowser, 6153 false, 6154 false 6155 ); 6156 otherTabBrowser._tabListeners.set(aOtherTab, tabListener); 6157 6158 const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL; 6159 filter.addProgressListener(tabListener, notifyAll); 6160 otherBrowser.webProgress.addProgressListener(filter, notifyAll); 6161 } 6162 6163 _swapBrowserDocShells(aOurTab, aOtherBrowser, aStateFlags) { 6164 // aOurTab's browser needs to be inserted now if it hasn't already. 6165 this._insertBrowser(aOurTab); 6166 6167 // Unhook our progress listener 6168 const filter = this._tabFilters.get(aOurTab); 6169 let tabListener = this._tabListeners.get(aOurTab); 6170 let ourBrowser = this.getBrowserForTab(aOurTab); 6171 ourBrowser.webProgress.removeProgressListener(filter); 6172 filter.removeProgressListener(tabListener); 6173 6174 // Make sure to unregister any open URIs. 6175 this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser); 6176 6177 let remoteBrowser = aOtherBrowser.ownerGlobal.gBrowser; 6178 6179 // If switcher is active, it will intercept swap events and 6180 // react as needed. 6181 if (!this._switcher) { 6182 aOtherBrowser.docShellIsActive = 6183 this.shouldActivateDocShell(ourBrowser); 6184 } 6185 6186 let ourBrowserContainer = 6187 ourBrowser.ownerDocument.getElementById("browser"); 6188 let otherBrowserContainer = 6189 aOtherBrowser.ownerDocument.getElementById("browser"); 6190 let ourBrowserContainerWasHidden = ourBrowserContainer.hidden; 6191 let otherBrowserContainerWasHidden = otherBrowserContainer.hidden; 6192 6193 // #browser is hidden in Customize Mode; this breaks docshell swapping, 6194 // so we need to toggle 'hidden' to make swapping work in this case. 6195 ourBrowserContainer.hidden = otherBrowserContainer.hidden = false; 6196 6197 // Swap the docshells 6198 ourBrowser.swapDocShells(aOtherBrowser); 6199 6200 ourBrowserContainer.hidden = ourBrowserContainerWasHidden; 6201 otherBrowserContainer.hidden = otherBrowserContainerWasHidden; 6202 6203 // Swap permanentKey properties. 6204 let ourPermanentKey = ourBrowser.permanentKey; 6205 ourBrowser.permanentKey = aOtherBrowser.permanentKey; 6206 aOtherBrowser.permanentKey = ourPermanentKey; 6207 aOurTab.permanentKey = ourBrowser.permanentKey; 6208 if (remoteBrowser) { 6209 let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser); 6210 if (otherTab) { 6211 otherTab.permanentKey = aOtherBrowser.permanentKey; 6212 } 6213 } 6214 6215 // Restore the progress listener 6216 tabListener = new TabProgressListener( 6217 aOurTab, 6218 ourBrowser, 6219 false, 6220 false, 6221 aStateFlags 6222 ); 6223 this._tabListeners.set(aOurTab, tabListener); 6224 6225 const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL; 6226 filter.addProgressListener(tabListener, notifyAll); 6227 ourBrowser.webProgress.addProgressListener(filter, notifyAll); 6228 } 6229 6230 _swapRegisteredOpenURIs(aOurBrowser, aOtherBrowser) { 6231 // Swap the registeredOpenURI properties of the two browsers 6232 let tmp = aOurBrowser.registeredOpenURI; 6233 delete aOurBrowser.registeredOpenURI; 6234 if (aOtherBrowser.registeredOpenURI) { 6235 aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI; 6236 delete aOtherBrowser.registeredOpenURI; 6237 } 6238 if (tmp) { 6239 aOtherBrowser.registeredOpenURI = tmp; 6240 } 6241 } 6242 6243 reloadMultiSelectedTabs() { 6244 this.reloadTabs(this.selectedTabs); 6245 } 6246 6247 reloadTabs(tabs) { 6248 for (let tab of tabs) { 6249 try { 6250 this.getBrowserForTab(tab).reload(); 6251 } catch (e) { 6252 // ignore failure to reload so others will be reloaded 6253 } 6254 } 6255 } 6256 6257 reloadTab(aTab) { 6258 let browser = this.getBrowserForTab(aTab); 6259 // Reset temporary permissions on the current tab. This is done here 6260 // because we only want to reset permissions on user reload. 6261 SitePermissions.clearTemporaryBlockPermissions(browser); 6262 // Also reset DOS mitigations for the basic auth prompt on reload. 6263 delete browser.authPromptAbuseCounter; 6264 gIdentityHandler.hidePopup(); 6265 gPermissionPanel.hidePopup(); 6266 browser.reload(); 6267 } 6268 6269 addProgressListener(aListener) { 6270 if (arguments.length != 1) { 6271 console.error( 6272 "gBrowser.addProgressListener was " + 6273 "called with a second argument, " + 6274 "which is not supported. See bug " + 6275 "608628. Call stack: ", 6276 new Error().stack 6277 ); 6278 } 6279 6280 this.mProgressListeners.push(aListener); 6281 } 6282 6283 removeProgressListener(aListener) { 6284 this.mProgressListeners = this.mProgressListeners.filter( 6285 l => l != aListener 6286 ); 6287 } 6288 6289 addTabsProgressListener(aListener) { 6290 this.mTabsProgressListeners.push(aListener); 6291 } 6292 6293 removeTabsProgressListener(aListener) { 6294 this.mTabsProgressListeners = this.mTabsProgressListeners.filter( 6295 l => l != aListener 6296 ); 6297 } 6298 6299 getBrowserForTab(aTab) { 6300 return aTab.linkedBrowser; 6301 } 6302 6303 showTab(aTab) { 6304 if (!aTab.hidden || aTab == FirefoxViewHandler.tab) { 6305 return; 6306 } 6307 aTab.removeAttribute("hidden"); 6308 this.tabContainer._invalidateCachedVisibleTabs(); 6309 6310 this.tabContainer._updateCloseButtons(); 6311 if (aTab.multiselected) { 6312 this._updateMultiselectedTabCloseButtonTooltip(); 6313 } 6314 6315 let event = document.createEvent("Events"); 6316 event.initEvent("TabShow", true, false); 6317 aTab.dispatchEvent(event); 6318 SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); 6319 } 6320 6321 hideTab(aTab, aSource) { 6322 if ( 6323 aTab.hidden || 6324 aTab.pinned || 6325 aTab.selected || 6326 aTab.closing || 6327 // Tabs that are sharing the screen, microphone or camera cannot be hidden. 6328 aTab._sharingState?.webRTC?.sharing 6329 ) { 6330 return; 6331 } 6332 aTab.setAttribute("hidden", "true"); 6333 this.tabContainer._invalidateCachedVisibleTabs(); 6334 6335 this.tabContainer._updateCloseButtons(); 6336 if (aTab.multiselected) { 6337 this._updateMultiselectedTabCloseButtonTooltip(); 6338 } 6339 6340 // Splice this tab out of any lines of succession before any events are 6341 // dispatched. 6342 this.replaceInSuccession(aTab, aTab.successor); 6343 this.setSuccessor(aTab, null); 6344 6345 let event = document.createEvent("Events"); 6346 event.initEvent("TabHide", true, false); 6347 aTab.dispatchEvent(event); 6348 if (aSource) { 6349 SessionStore.setCustomTabValue(aTab, "hiddenBy", aSource); 6350 } 6351 } 6352 6353 selectTabAtIndex(aIndex, aEvent) { 6354 let tabs = this.visibleTabs; 6355 6356 // count backwards for aIndex < 0 6357 if (aIndex < 0) { 6358 aIndex += tabs.length; 6359 // clamp at index 0 if still negative. 6360 if (aIndex < 0) { 6361 aIndex = 0; 6362 } 6363 } else if (aIndex >= tabs.length) { 6364 // clamp at right-most tab if out of range. 6365 aIndex = tabs.length - 1; 6366 } 6367 6368 this.selectedTab = tabs[aIndex]; 6369 6370 if (aEvent) { 6371 aEvent.preventDefault(); 6372 aEvent.stopPropagation(); 6373 } 6374 } 6375 6376 /** 6377 * Moves a tab to a new browser window, unless it's already the only tab 6378 * in the current window, in which case this will do nothing. 6379 * 6380 * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab 6381 */ 6382 replaceTabWithWindow(aTab, aOptions) { 6383 if (this.tabs.length == 1) { 6384 return null; 6385 } 6386 // TODO bug 1967925: Consider handling the case where aTab is a tab group 6387 // and also the only tab group in its window. 6388 6389 var options = "chrome,dialog=no,all"; 6390 for (var name in aOptions) { 6391 options += "," + name + "=" + aOptions[name]; 6392 } 6393 6394 if (PrivateBrowsingUtils.isWindowPrivate(window)) { 6395 options += ",private=1"; 6396 } 6397 6398 // Play the tab closing animation to give immediate feedback while 6399 // waiting for the new window to appear. 6400 if (!gReduceMotion && this.isTab(aTab)) { 6401 aTab.style.maxWidth = ""; // ensure that fade-out transition happens 6402 aTab.removeAttribute("fadein"); 6403 } 6404 6405 // tell a new window to take the "dropped" tab 6406 return window.openDialog( 6407 AppConstants.BROWSER_CHROME_URL, 6408 "_blank", 6409 options, 6410 aTab 6411 ); 6412 } 6413 6414 /** 6415 * Move contextTab (or selected tabs in a mutli-select context) 6416 * to a new browser window, unless it is (they are) already the only tab(s) 6417 * in the current window, in which case this will do nothing. 6418 */ 6419 replaceTabsWithWindow(contextTab, aOptions = {}) { 6420 if (this.isTabGroupLabel(contextTab)) { 6421 // TODO bug 1967937: Pass contextTab.group instead. 6422 return this.replaceTabWithWindow(contextTab, aOptions); 6423 } 6424 6425 let tabs; 6426 if (contextTab.multiselected) { 6427 tabs = this.selectedTabs; 6428 } else { 6429 tabs = [contextTab]; 6430 } 6431 6432 if (this.tabs.length == tabs.length) { 6433 return null; 6434 } 6435 6436 if (tabs.length == 1) { 6437 return this.replaceTabWithWindow(tabs[0], aOptions); 6438 } 6439 6440 // Play the closing animation for all selected tabs to give 6441 // immediate feedback while waiting for the new window to appear. 6442 if (!gReduceMotion) { 6443 for (let tab of tabs) { 6444 tab.style.maxWidth = ""; // ensure that fade-out transition happens 6445 tab.removeAttribute("fadein"); 6446 } 6447 } 6448 6449 // Create a new window and make it adopt the tabs, preserving their relative order. 6450 // The initial tab of the new window will be selected, so it should adopt the 6451 // selected tab of the original window, if applicable, or else the first moving tab. 6452 // This avoids tab-switches in the new window, preserving tab laziness. 6453 // However, to avoid multiple tab-switches in the original window, the other tabs 6454 // should be adopted before the selected one. 6455 let { selectedTab } = gBrowser; 6456 if (!tabs.includes(selectedTab)) { 6457 selectedTab = tabs[0]; 6458 } 6459 6460 let win = this.replaceTabWithWindow(selectedTab, aOptions); 6461 win.addEventListener( 6462 "before-initial-tab-adopted", 6463 () => { 6464 let tabIndex = 0; 6465 for (let tab of tabs) { 6466 if (tab !== selectedTab) { 6467 const newTab = win.gBrowser.adoptTab(tab, { tabIndex }); 6468 if (!newTab) { 6469 // The adoption failed. Restore "fadein" and don't increase the index. 6470 tab.setAttribute("fadein", "true"); 6471 continue; 6472 } 6473 } 6474 6475 ++tabIndex; 6476 } 6477 // Restore tab selection 6478 let winVisibleTabs = win.gBrowser.visibleTabs; 6479 let winTabLength = winVisibleTabs.length; 6480 win.gBrowser.addRangeToMultiSelectedTabs( 6481 winVisibleTabs[0], 6482 winVisibleTabs[winTabLength - 1] 6483 ); 6484 win.gBrowser.lockClearMultiSelectionOnce(); 6485 }, 6486 { once: true } 6487 ); 6488 return win; 6489 } 6490 6491 /** 6492 * Moves group to a new window. 6493 * 6494 * @param {MozTabbrowserTabGroup} group 6495 * The tab group to move. 6496 */ 6497 replaceGroupWithWindow(group) { 6498 return this.replaceTabWithWindow(group); 6499 } 6500 6501 /** 6502 * @param {Element} element 6503 * @returns {boolean} 6504 * `true` if element is a `<tab>` 6505 */ 6506 isTab(element) { 6507 return !!(element?.tagName == "tab"); 6508 } 6509 6510 /** 6511 * @param {Element} element 6512 * @returns {boolean} 6513 * `true` if element is a `<tab-group>` 6514 */ 6515 isTabGroup(element) { 6516 return !!(element?.tagName == "tab-group"); 6517 } 6518 6519 /** 6520 * @param {Element} element 6521 * @returns {boolean} 6522 * `true` if element is the `<label>` in a `<tab-group>` 6523 */ 6524 isTabGroupLabel(element) { 6525 return !!element?.classList?.contains("tab-group-label"); 6526 } 6527 6528 /** 6529 * @param {Element} element 6530 * @returns {boolean} 6531 * `true` if element is a `<tab-split-view-wrapper>` 6532 */ 6533 isSplitViewWrapper(element) { 6534 return !!(element?.tagName == "tab-split-view-wrapper"); 6535 } 6536 6537 _updateTabsAfterInsert() { 6538 for (let i = 0; i < this.tabs.length; i++) { 6539 this.tabs[i]._tPos = i; 6540 this.tabs[i]._selected = false; 6541 } 6542 6543 // If we're in the midst of an async tab switch while calling 6544 // moveTabTo, we can get into a case where _visuallySelected 6545 // is set to true on two different tabs. 6546 // 6547 // What we want to do in moveTabTo is to remove logical selection 6548 // from all tabs, and then re-add logical selection to selectedTab 6549 // (and visual selection as well if we're not running with e10s, which 6550 // setting _selected will do automatically). 6551 // 6552 // If we're running with e10s, then the visual selection will not 6553 // be changed, which is fine, since if we weren't in the midst of a 6554 // tab switch, the previously visually selected tab should still be 6555 // correct, and if we are in the midst of a tab switch, then the async 6556 // tab switcher will set the visually selected tab once the tab switch 6557 // has completed. 6558 this.selectedTab._selected = true; 6559 } 6560 6561 /** 6562 * @param {MozTabbrowserTab|MozTabbrowserTabGroup} element 6563 * The tab or tab group to move. Also accepts a tab group label as a 6564 * stand-in for its group. 6565 * @param {object} [options] 6566 * @param {number} [options.tabIndex] 6567 * The desired position, expressed as the index within the `tabs` array. 6568 * @param {number} [options.elementIndex] 6569 * The desired position, expressed as the index within the 6570 * `MozTabbrowserTabs::dragAndDropElements` array. 6571 * @param {boolean} [options.forceUngrouped=false] 6572 * Force `element` to move into position as a standalone tab, overriding 6573 * any possibility of entering a tab group. For example, setting `true` 6574 * ensures that a pinned tab will not accidentally be placed inside of 6575 * a tab group, since pinned tabs are presently not allowed in tab groups. 6576 * @property {boolean} [options.isUserTriggered=false] 6577 * Should be true if there was an explicit action/request from the user 6578 * (as opposed to some action being taken internally or for technical 6579 * bookkeeping reasons alone) to move the tab. This causes telemetry 6580 * events to fire. 6581 * @property {string} [options.telemetrySource="unknown"] 6582 * The system, surface, or control the user used to move the tab. 6583 * @see TabMetrics.METRIC_SOURCE for possible values. 6584 * Defaults to "unknown". 6585 */ 6586 moveTabTo( 6587 element, 6588 { 6589 elementIndex, 6590 tabIndex, 6591 forceUngrouped = false, 6592 isUserTriggered = false, 6593 telemetrySource = this.TabMetrics.METRIC_SOURCE.UNKNOWN, 6594 } = {} 6595 ) { 6596 if (typeof elementIndex == "number") { 6597 tabIndex = this.#elementIndexToTabIndex(elementIndex); 6598 } 6599 6600 // Don't allow mixing pinned and unpinned tabs. 6601 if (this.isTab(element) && element.pinned) { 6602 tabIndex = Math.min(tabIndex, this.pinnedTabCount - 1); 6603 } else { 6604 tabIndex = Math.max(tabIndex, this.pinnedTabCount); 6605 } 6606 6607 // Return early if the tab is already in the right spot. 6608 if ( 6609 this.isTab(element) && 6610 element._tPos == tabIndex && 6611 !(element.group && forceUngrouped) 6612 ) { 6613 return; 6614 } 6615 6616 // When asked to move a tab group label, we need to move the whole group 6617 // instead. 6618 if (this.isTabGroupLabel(element)) { 6619 element = element.group; 6620 } 6621 if (this.isTabGroup(element)) { 6622 forceUngrouped = true; 6623 } 6624 6625 this.#handleTabMove( 6626 element, 6627 () => { 6628 let neighbor = this.tabs[tabIndex]; 6629 if (forceUngrouped && neighbor?.group) { 6630 neighbor = neighbor.group; 6631 } 6632 if (neighbor && this.isTab(element) && tabIndex > element._tPos) { 6633 neighbor.after(element); 6634 } else { 6635 this.tabContainer.insertBefore(element, neighbor); 6636 } 6637 }, 6638 { isUserTriggered, telemetrySource } 6639 ); 6640 } 6641 6642 /** 6643 * @param {MozTabbrowserTab|MozTabbrowserTabGroup} element 6644 * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement 6645 * @param {TabMetricsContext} [metricsContext] 6646 */ 6647 moveTabBefore(element, targetElement, metricsContext) { 6648 this.#moveTabNextTo(element, targetElement, true, metricsContext); 6649 } 6650 6651 /** 6652 * @param {MozTabbrowserTab|MozTabbrowserTabGroup[]} elements 6653 * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement 6654 * @param {TabMetricsContext} [metricsContext] 6655 */ 6656 moveTabsBefore(elements, targetElement, metricsContext) { 6657 this.#moveTabsNextTo(elements, targetElement, true, metricsContext); 6658 } 6659 6660 /** 6661 * @param {MozTabbrowserTab|MozTabbrowserTabGroup} element 6662 * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement 6663 * @param {TabMetricsContext} [metricsContext] 6664 */ 6665 moveTabAfter(element, targetElement, metricsContext) { 6666 this.#moveTabNextTo(element, targetElement, false, metricsContext); 6667 } 6668 6669 /** 6670 * @param {MozTabbrowserTab|MozTabbrowserTabGroup[]} elements 6671 * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement 6672 * @param {TabMetricsContext} [metricsContext] 6673 */ 6674 moveTabsAfter(elements, targetElement, metricsContext) { 6675 this.#moveTabsNextTo(elements, targetElement, false, metricsContext); 6676 } 6677 6678 /** 6679 * @param {MozTabbrowserTab|MozTabbrowserTabGroup} element 6680 * The tab or tab group to move. Also accepts a tab group label as a 6681 * stand-in for its group. 6682 * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement 6683 * @param {boolean} [moveBefore=false] 6684 * @param {TabMetricsContext} [metricsContext] 6685 */ 6686 #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { 6687 if (this.isTabGroupLabel(targetElement)) { 6688 targetElement = targetElement.group; 6689 if (!moveBefore && !targetElement.collapsed) { 6690 // Right after the tab group label = before the first tab in the tab group 6691 targetElement = targetElement.tabs[0]; 6692 moveBefore = true; 6693 } 6694 } 6695 if (this.isTabGroupLabel(element)) { 6696 element = element.group; 6697 if (targetElement?.group) { 6698 targetElement = targetElement.group; 6699 } 6700 } 6701 6702 // Don't allow mixing pinned and unpinned tabs. 6703 if (element.pinned && !targetElement?.pinned) { 6704 targetElement = this.tabs[this.pinnedTabCount - 1]; 6705 moveBefore = false; 6706 } else if (!element.pinned && targetElement && targetElement.pinned) { 6707 // If the caller asks to move an unpinned element next to a pinned 6708 // tab, move the unpinned element to be the first unpinned element 6709 // in the tab strip. Potential scenarios: 6710 // 1. Moving an unpinned tab and the first unpinned tab is ungrouped: 6711 // move the unpinned tab right before the first unpinned tab. 6712 // 2. Moving an unpinned tab and the first unpinned tab is grouped: 6713 // move the unpinned tab right before the tab group. 6714 // 3. Moving a tab group and the first unpinned tab is ungrouped: 6715 // move the tab group right before the first unpinned tab. 6716 // 4. Moving a tab group and the first unpinned tab is grouped: 6717 // move the tab group right before the first unpinned tab's tab group. 6718 targetElement = this.tabs[this.pinnedTabCount]; 6719 if (targetElement.group) { 6720 targetElement = targetElement.group; 6721 } 6722 moveBefore = true; 6723 } 6724 6725 let getContainer = () => 6726 element.pinned 6727 ? this.tabContainer.pinnedTabsContainer 6728 : this.tabContainer; 6729 6730 this.#handleTabMove( 6731 element, 6732 () => { 6733 if (moveBefore) { 6734 getContainer().insertBefore(element, targetElement); 6735 } else if (targetElement) { 6736 targetElement.after(element); 6737 } else { 6738 getContainer().appendChild(element); 6739 } 6740 }, 6741 metricsContext 6742 ); 6743 } 6744 6745 /** 6746 * @param {MozTabbrowserTab[]} elements 6747 * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement 6748 * @param {boolean} [moveBefore=false] 6749 * @param {TabMetricsContext} [metricsContext] 6750 */ 6751 #moveTabsNextTo( 6752 elements, 6753 targetElement, 6754 moveBefore = false, 6755 metricsContext 6756 ) { 6757 this.#moveTabNextTo( 6758 elements[0], 6759 targetElement, 6760 moveBefore, 6761 metricsContext 6762 ); 6763 for (let i = 1; i < elements.length; i++) { 6764 this.#moveTabNextTo( 6765 elements[i], 6766 elements[i - 1], 6767 false, 6768 metricsContext 6769 ); 6770 } 6771 } 6772 6773 /** 6774 * 6775 * @param {MozTabbrowserTab} aTab 6776 * @param {MozTabSplitViewWrapper} aSplitViewWrapper 6777 */ 6778 moveTabToSplitView(aTab, aSplitViewWrapper) { 6779 if (!this.isTab(aTab)) { 6780 throw new Error("Can only move a tab into a split view wrapper"); 6781 } 6782 if (aTab.pinned) { 6783 return; 6784 } 6785 if ( 6786 aTab.splitview && 6787 aTab.splitview.splitViewId === aSplitViewWrapper.splitViewId 6788 ) { 6789 return; 6790 } 6791 6792 this.#handleTabMove(aTab, () => aSplitViewWrapper.appendChild(aTab)); 6793 this.removeFromMultiSelectedTabs(aTab); 6794 this.tabContainer._notifyBackgroundTab(aTab); 6795 } 6796 6797 /** 6798 * 6799 * @param {MozTabbrowserTab} aTab 6800 * @param {MozTabbrowserTabGroup} aGroup 6801 * @param {TabMetricsContext} [metricsContext] 6802 */ 6803 moveTabToExistingGroup(aTab, aGroup, metricsContext) { 6804 if (!this.isTab(aTab)) { 6805 throw new Error("Can only move a tab into a tab group"); 6806 } 6807 if (aTab.pinned) { 6808 return; 6809 } 6810 if (aTab.group && aTab.group.id === aGroup.id) { 6811 return; 6812 } 6813 if (aTab.splitview) { 6814 let splitViewTabs = aTab.splitview.tabs; 6815 this.#handleTabMove( 6816 aTab.splitview, 6817 () => aGroup.appendChild(aTab.splitview), 6818 metricsContext 6819 ); 6820 for (const splitViewTab of splitViewTabs) { 6821 this.removeFromMultiSelectedTabs(splitViewTab); 6822 this.tabContainer._notifyBackgroundTab(splitViewTab); 6823 } 6824 } else { 6825 this.#handleTabMove( 6826 aTab, 6827 () => aGroup.appendChild(aTab), 6828 metricsContext 6829 ); 6830 this.removeFromMultiSelectedTabs(aTab); 6831 this.tabContainer._notifyBackgroundTab(aTab); 6832 } 6833 } 6834 6835 /** 6836 * 6837 * @param {MozSplitViewWrapper} aSplitView 6838 * @param {MozTabbrowserTabGroup} aGroup 6839 * @param {TabMetricsContext} [metricsContext] 6840 */ 6841 moveSplitViewToExistingGroup(aSplitView, aGroup, metricsContext = null) { 6842 if (!this.isSplitViewWrapper(aSplitView)) { 6843 throw new Error("Can only move a split view into a tab group"); 6844 } 6845 if (aSplitView.group && aSplitView.group.id === aGroup.id) { 6846 return; 6847 } 6848 6849 let splitViewTabs = aSplitView.tabs; 6850 this.#handleTabMove( 6851 aSplitView, 6852 () => aGroup.appendChild(aSplitView), 6853 metricsContext 6854 ); 6855 for (const splitViewTab of splitViewTabs) { 6856 this.removeFromMultiSelectedTabs(splitViewTab); 6857 this.tabContainer._notifyBackgroundTab(splitViewTab); 6858 } 6859 } 6860 6861 /** 6862 * @typedef {object} TabMoveState 6863 * @property {number} tabIndex 6864 * @property {number} [elementIndex] 6865 * @property {string} [tabGroupId] 6866 */ 6867 6868 /** 6869 * @param {MozTabbrowserTab} tab 6870 * @returns {TabMoveState|undefined} 6871 */ 6872 #getTabMoveState(tab) { 6873 if (!this.isTab(tab)) { 6874 return undefined; 6875 } 6876 6877 let state = { 6878 tabIndex: tab._tPos, 6879 }; 6880 if (tab.visible) { 6881 state.elementIndex = tab.elementIndex; 6882 } 6883 if (tab.group) { 6884 state.tabGroupId = tab.group.id; 6885 } 6886 if (tab.splitview) { 6887 state.splitViewId = tab.splitview.splitViewId; 6888 } 6889 return state; 6890 } 6891 6892 /** 6893 * @param {MozTabbrowserTab} tab 6894 * @param {TabMoveState} [previousTabState] 6895 * @param {TabMoveState} [currentTabState] 6896 * @param {TabMetricsContext} [metricsContext] 6897 */ 6898 #notifyOnTabMove(tab, previousTabState, currentTabState, metricsContext) { 6899 if (!this.isTab(tab) || !previousTabState || !currentTabState) { 6900 return; 6901 } 6902 6903 let changedPosition = 6904 previousTabState.tabIndex != currentTabState.tabIndex; 6905 let changedTabGroup = 6906 previousTabState.tabGroupId != currentTabState.tabGroupId; 6907 6908 if (changedPosition || changedTabGroup) { 6909 tab.dispatchEvent( 6910 new CustomEvent("TabMove", { 6911 bubbles: true, 6912 detail: { 6913 previousTabState, 6914 currentTabState, 6915 isUserTriggered: metricsContext?.isUserTriggered ?? false, 6916 telemetrySource: 6917 metricsContext?.telemetrySource ?? 6918 this.TabMetrics.METRIC_SOURCE.UNKNOWN, 6919 }, 6920 }) 6921 ); 6922 } 6923 } 6924 6925 /** 6926 * @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabSplitViewWrapper} element 6927 * @param {function():void} moveActionCallback 6928 * @param {TabMetricsContext} [metricsContext] 6929 */ 6930 #handleTabMove(element, moveActionCallback, metricsContext) { 6931 let tabs; 6932 if (this.isTab(element)) { 6933 tabs = [element]; 6934 } else if (this.isTabGroup(element) || this.isSplitViewWrapper(element)) { 6935 tabs = element.tabs; 6936 } else { 6937 throw new Error( 6938 "Can only move a tab, tab group, or split view within the tab bar" 6939 ); 6940 } 6941 6942 let wasFocused = document.activeElement == this.selectedTab; 6943 let previousTabStates = tabs.map(tab => this.#getTabMoveState(tab)); 6944 6945 moveActionCallback(); 6946 6947 // Clear tabs cache after moving nodes because the order of tabs may have 6948 // changed. 6949 this.tabContainer._invalidateCachedTabs(); 6950 this._lastRelatedTabMap = new WeakMap(); 6951 this._updateTabsAfterInsert(); 6952 6953 if (wasFocused) { 6954 this.selectedTab.focus(); 6955 } 6956 6957 // When a tab group with multiple tabs is moved forwards, emit TabMove in 6958 // the reverse order, so that the index in previousTabState values are 6959 // still accurate until the event is dispatched. If we were to start with 6960 // the front tab, then logically that tab moves, and all following tabs 6961 // would shift, which would invalidate the index in previousTabState. 6962 let reverseEvents = 6963 tabs.length > 1 && tabs[0]._tPos > previousTabStates[0].tabIndex; 6964 6965 for (let i = 0; i < tabs.length; i++) { 6966 let ii = reverseEvents ? tabs.length - i - 1 : i; 6967 let tab = tabs[ii]; 6968 if (tab.selected) { 6969 this.tabContainer._handleTabSelect(true); 6970 } 6971 6972 let currentTabState = this.#getTabMoveState(tab); 6973 this.#notifyOnTabMove( 6974 tab, 6975 previousTabStates[ii], 6976 currentTabState, 6977 metricsContext 6978 ); 6979 } 6980 6981 let currentFirst = this.#getTabMoveState(tabs[0]); 6982 if ( 6983 this.isTabGroup(element) && 6984 previousTabStates[0].tabIndex != currentFirst.tabIndex 6985 ) { 6986 let event = new CustomEvent("TabGroupMoved", { bubbles: true }); 6987 element.dispatchEvent(event); 6988 } 6989 } 6990 6991 /** 6992 * Adopts a tab from another browser window, and inserts it at the given index. 6993 * 6994 * @returns {object} 6995 * The new tab in the current window, null if the tab couldn't be adopted. 6996 */ 6997 adoptTab(aTab, { elementIndex, tabIndex, selectTab = false } = {}) { 6998 // Swap the dropped tab with a new one we create and then close 6999 // it in the other window (making it seem to have moved between 7000 // windows). We also ensure that the tab we create to swap into has 7001 // the same remote type and process as the one we're swapping in. 7002 // This makes sure we don't get a short-lived process for the new tab. 7003 let linkedBrowser = aTab.linkedBrowser; 7004 let createLazyBrowser = !aTab.linkedPanel; 7005 let index; 7006 let nextElement; 7007 if (typeof elementIndex == "number") { 7008 index = elementIndex; 7009 nextElement = this.tabContainer.dragAndDropElements.at(elementIndex); 7010 } else { 7011 index = tabIndex; 7012 nextElement = this.tabs.at(tabIndex); 7013 } 7014 let tabInGroup = !!aTab.group; 7015 let params = { 7016 eventDetail: { adoptedTab: aTab }, 7017 preferredRemoteType: linkedBrowser.remoteType, 7018 initialBrowsingContextGroupId: linkedBrowser.browsingContext?.group.id, 7019 skipAnimation: true, 7020 elementIndex, 7021 tabIndex, 7022 tabGroup: this.isTab(nextElement) && nextElement.group, 7023 createLazyBrowser, 7024 }; 7025 7026 // We want to explicitly set this param rather than carry it over to 7027 // avoid situations like an unpinned tab being dragged between pinned 7028 // tabs but not getting pinned as expected. 7029 let numPinned = this.pinnedTabCount; 7030 if (index < numPinned || (aTab.pinned && index == numPinned)) { 7031 params.pinned = true; 7032 } 7033 7034 if (aTab.hasAttribute("usercontextid")) { 7035 // new tab must have the same usercontextid as the old one 7036 params.userContextId = aTab.getAttribute("usercontextid"); 7037 } 7038 params.skipLoad = true; 7039 let newTab = this.addWebTab("about:blank", params); 7040 7041 aTab.container.tabDragAndDrop.finishAnimateTabMove(); 7042 7043 if (!this.swapBrowsersAndCloseOther(newTab, aTab)) { 7044 // Swapping wasn't permitted. Bail out. 7045 this.removeTab(newTab); 7046 return null; 7047 } 7048 7049 if (selectTab) { 7050 this.selectedTab = newTab; 7051 } 7052 7053 if (tabInGroup) { 7054 Glean.tabgroup.tabInteractions.remove_other_window.add(); 7055 } 7056 7057 return newTab; 7058 } 7059 7060 moveTabForward() { 7061 let { selectedTab } = this; 7062 let nextTab = this.tabContainer.findNextTab(selectedTab, { 7063 direction: DIRECTION_FORWARD, 7064 filter: tab => !tab.hidden && selectedTab.pinned == tab.pinned, 7065 }); 7066 if (nextTab) { 7067 this.#handleTabMove(selectedTab, () => { 7068 if (!selectedTab.group && nextTab.group) { 7069 if (nextTab.group.collapsed) { 7070 // Skip over collapsed tab group. 7071 nextTab.group.after(selectedTab); 7072 } else { 7073 // Enter first position of tab group. 7074 nextTab.group.insertBefore(selectedTab, nextTab); 7075 } 7076 } else if (selectedTab.group != nextTab.group) { 7077 // Standalone tab after tab group. 7078 selectedTab.group.after(selectedTab); 7079 } else { 7080 nextTab.after(selectedTab); 7081 } 7082 }); 7083 } else if (selectedTab.group) { 7084 // selectedTab is the last tab and is grouped. 7085 // remove it from its group. 7086 selectedTab.group.after(selectedTab); 7087 } 7088 } 7089 7090 moveTabBackward() { 7091 let { selectedTab } = this; 7092 7093 let previousTab = this.tabContainer.findNextTab(selectedTab, { 7094 direction: DIRECTION_BACKWARD, 7095 filter: tab => !tab.hidden && selectedTab.pinned == tab.pinned, 7096 }); 7097 7098 if (previousTab) { 7099 this.#handleTabMove(selectedTab, () => { 7100 if (!selectedTab.group && previousTab.group) { 7101 if (previousTab.group.collapsed) { 7102 // Skip over collapsed tab group. 7103 previousTab.group.before(selectedTab); 7104 } else { 7105 // Enter last position of tab group. 7106 previousTab.group.append(selectedTab); 7107 } 7108 } else if (selectedTab.group != previousTab.group) { 7109 // Standalone tab before tab group. 7110 selectedTab.group.before(selectedTab); 7111 } else { 7112 previousTab.before(selectedTab); 7113 } 7114 }); 7115 } else if (selectedTab.group) { 7116 // selectedTab is the first tab and is grouped. 7117 // remove it from its group. 7118 selectedTab.group.before(selectedTab); 7119 } 7120 } 7121 7122 moveTabToStart(aTab = this.selectedTab) { 7123 this.moveTabTo(aTab, { tabIndex: 0, forceUngrouped: true }); 7124 } 7125 7126 moveTabToEnd(aTab = this.selectedTab) { 7127 this.moveTabTo(aTab, { 7128 tabIndex: this.tabs.length - 1, 7129 forceUngrouped: true, 7130 }); 7131 } 7132 7133 /** 7134 * @param aTab 7135 * Can be from a different window as well 7136 * @param aRestoreTabImmediately 7137 * Can defer loading of the tab contents 7138 * @param aOptions 7139 * The new index of the tab 7140 */ 7141 duplicateTab(aTab, aRestoreTabImmediately, aOptions) { 7142 let newTab = SessionStore.duplicateTab( 7143 window, 7144 aTab, 7145 0, 7146 aRestoreTabImmediately, 7147 aOptions 7148 ); 7149 if (aTab.group) { 7150 Glean.tabgroup.tabInteractions.duplicate.add(); 7151 } 7152 return newTab; 7153 } 7154 7155 /** 7156 * Update accessible names of close buttons in the (multi) selected tabs 7157 * collection with how many tabs they will close 7158 */ 7159 _updateMultiselectedTabCloseButtonTooltip() { 7160 const tabCount = gBrowser.selectedTabs.length; 7161 gBrowser.selectedTabs.forEach(selectedTab => { 7162 document.l10n.setArgs(selectedTab.querySelector(".tab-close-button"), { 7163 tabCount, 7164 }); 7165 }); 7166 } 7167 7168 addToMultiSelectedTabs(aTab) { 7169 if (aTab.multiselected) { 7170 return; 7171 } 7172 7173 aTab.setAttribute("multiselected", "true"); 7174 aTab.setAttribute("aria-selected", "true"); 7175 this._multiSelectedTabsSet.add(aTab); 7176 this._startMultiSelectChange(); 7177 if (this._multiSelectChangeRemovals.has(aTab)) { 7178 this._multiSelectChangeRemovals.delete(aTab); 7179 } else { 7180 this._multiSelectChangeAdditions.add(aTab); 7181 } 7182 7183 this._updateMultiselectedTabCloseButtonTooltip(); 7184 } 7185 7186 /** 7187 * Adds two given tabs and all tabs between them into the (multi) selected tabs collection 7188 */ 7189 addRangeToMultiSelectedTabs(aTab1, aTab2) { 7190 if (aTab1 == aTab2) { 7191 return; 7192 } 7193 7194 const tabs = this.visibleTabs; 7195 const indexOfTab1 = tabs.indexOf(aTab1); 7196 const indexOfTab2 = tabs.indexOf(aTab2); 7197 7198 const [lowerIndex, higherIndex] = 7199 indexOfTab1 < indexOfTab2 7200 ? [Math.max(0, indexOfTab1), indexOfTab2] 7201 : [Math.max(0, indexOfTab2), indexOfTab1]; 7202 7203 for (let i = lowerIndex; i <= higherIndex; i++) { 7204 this.addToMultiSelectedTabs(tabs[i]); 7205 } 7206 7207 this._updateMultiselectedTabCloseButtonTooltip(); 7208 } 7209 7210 removeFromMultiSelectedTabs(aTab) { 7211 if (!aTab.multiselected) { 7212 return; 7213 } 7214 aTab.removeAttribute("multiselected"); 7215 aTab.removeAttribute("aria-selected"); 7216 this._multiSelectedTabsSet.delete(aTab); 7217 this._startMultiSelectChange(); 7218 if (this._multiSelectChangeAdditions.has(aTab)) { 7219 this._multiSelectChangeAdditions.delete(aTab); 7220 } else { 7221 this._multiSelectChangeRemovals.add(aTab); 7222 } 7223 // Update labels for Close buttons of the remaining multiselected tabs: 7224 this._updateMultiselectedTabCloseButtonTooltip(); 7225 // Update the label for the Close button of the tab being removed 7226 // from the multiselection: 7227 document.l10n.setArgs(aTab.querySelector(".tab-close-button"), { 7228 tabCount: 1, 7229 }); 7230 } 7231 7232 clearMultiSelectedTabs() { 7233 if (this._clearMultiSelectionLocked) { 7234 if (this._clearMultiSelectionLockedOnce) { 7235 this._clearMultiSelectionLockedOnce = false; 7236 this._clearMultiSelectionLocked = false; 7237 } 7238 return; 7239 } 7240 7241 if (this.multiSelectedTabsCount < 1) { 7242 return; 7243 } 7244 7245 for (let tab of this.selectedTabs) { 7246 this.removeFromMultiSelectedTabs(tab); 7247 } 7248 this._lastMultiSelectedTabRef = null; 7249 } 7250 7251 selectAllTabs() { 7252 let visibleTabs = this.visibleTabs; 7253 gBrowser.addRangeToMultiSelectedTabs( 7254 visibleTabs[0], 7255 visibleTabs[visibleTabs.length - 1] 7256 ); 7257 } 7258 7259 allTabsSelected() { 7260 return ( 7261 this.visibleTabs.length == 1 || 7262 this.visibleTabs.every(t => t.multiselected) 7263 ); 7264 } 7265 7266 lockClearMultiSelectionOnce() { 7267 this._clearMultiSelectionLockedOnce = true; 7268 this._clearMultiSelectionLocked = true; 7269 } 7270 7271 unlockClearMultiSelection() { 7272 this._clearMultiSelectionLockedOnce = false; 7273 this._clearMultiSelectionLocked = false; 7274 } 7275 7276 /** 7277 * Remove a tab from the multiselection if it's the only one left there. 7278 * 7279 * In fact, some scenario may lead to only one single tab multi-selected, 7280 * this is something to avoid (Chrome does the same) 7281 * Consider 4 tabs A,B,C,D with A having the focus 7282 * 1. select C with Ctrl 7283 * 2. Right-click on B and "Close Tabs to The Right" 7284 * 7285 * Expected result 7286 * C and D closing 7287 * A being the only multi-selected tab, selection should be cleared 7288 * 7289 * 7290 * Single selected tab could even happen with a none-focused tab. 7291 * For exemple with the menu "Close other tabs", it could happen 7292 * with a multi-selected pinned tab. 7293 * For illustration, consider 4 tabs A,B,C,D with B active 7294 * 1. pin A and Ctrl-select it 7295 * 2. Ctrl-select C 7296 * 3. right-click on D and click "Close Other Tabs" 7297 * 7298 * Expected result 7299 * B and C closing 7300 * A[pinned] being the only multi-selected tab, selection should be cleared. 7301 */ 7302 _avoidSingleSelectedTab() { 7303 if (this.multiSelectedTabsCount == 1) { 7304 this.clearMultiSelectedTabs(); 7305 } 7306 } 7307 7308 _switchToNextMultiSelectedTab() { 7309 this._clearMultiSelectionLocked = true; 7310 7311 // Guarantee that _clearMultiSelectionLocked lock gets released. 7312 try { 7313 let lastMultiSelectedTab = this.lastMultiSelectedTab; 7314 if (!lastMultiSelectedTab.selected) { 7315 this.selectedTab = lastMultiSelectedTab; 7316 } else { 7317 let selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys( 7318 this._multiSelectedTabsSet 7319 ).filter(this._mayTabBeMultiselected); 7320 this.selectedTab = selectedTabs.at(-1); 7321 } 7322 } catch (e) { 7323 console.error(e); 7324 } 7325 7326 this._clearMultiSelectionLocked = false; 7327 } 7328 7329 set selectedTabs(tabs) { 7330 this.clearMultiSelectedTabs(); 7331 this.selectedTab = tabs[0]; 7332 if (tabs.length > 1) { 7333 for (let tab of tabs) { 7334 this.addToMultiSelectedTabs(tab); 7335 } 7336 } 7337 } 7338 7339 get selectedTabs() { 7340 let { selectedTab, _multiSelectedTabsSet } = this; 7341 let tabs = ChromeUtils.nondeterministicGetWeakSetKeys( 7342 _multiSelectedTabsSet 7343 ).filter(this._mayTabBeMultiselected); 7344 if ( 7345 (!_multiSelectedTabsSet.has(selectedTab) && 7346 this._mayTabBeMultiselected(selectedTab)) || 7347 !tabs.length 7348 ) { 7349 tabs.push(selectedTab); 7350 } 7351 return tabs.sort((a, b) => a._tPos > b._tPos); 7352 } 7353 7354 get multiSelectedTabsCount() { 7355 return ChromeUtils.nondeterministicGetWeakSetKeys( 7356 this._multiSelectedTabsSet 7357 ).filter(this._mayTabBeMultiselected).length; 7358 } 7359 7360 get lastMultiSelectedTab() { 7361 let tab = this._lastMultiSelectedTabRef 7362 ? this._lastMultiSelectedTabRef.get() 7363 : null; 7364 if (tab && tab.isConnected && this._multiSelectedTabsSet.has(tab)) { 7365 return tab; 7366 } 7367 let selectedTab = this.selectedTab; 7368 this.lastMultiSelectedTab = selectedTab; 7369 return selectedTab; 7370 } 7371 7372 set lastMultiSelectedTab(aTab) { 7373 this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab); 7374 } 7375 7376 _mayTabBeMultiselected(aTab) { 7377 return aTab.visible; 7378 } 7379 7380 _startMultiSelectChange() { 7381 if (!this._multiSelectChangeStarted) { 7382 this._multiSelectChangeStarted = true; 7383 Promise.resolve().then(() => this._endMultiSelectChange()); 7384 } 7385 } 7386 7387 _endMultiSelectChange() { 7388 let noticeable = false; 7389 let { selectedTab } = this; 7390 if (this._multiSelectChangeAdditions.size) { 7391 if (!selectedTab.multiselected) { 7392 this.addToMultiSelectedTabs(selectedTab); 7393 } 7394 noticeable = true; 7395 } 7396 if (this._multiSelectChangeRemovals.size) { 7397 if (this._multiSelectChangeRemovals.has(selectedTab)) { 7398 this._switchToNextMultiSelectedTab(); 7399 } 7400 this._avoidSingleSelectedTab(); 7401 noticeable = true; 7402 } 7403 this._multiSelectChangeStarted = false; 7404 if (noticeable || this._multiSelectChangeSelected) { 7405 this._multiSelectChangeSelected = false; 7406 this._multiSelectChangeAdditions.clear(); 7407 this._multiSelectChangeRemovals.clear(); 7408 this.dispatchEvent( 7409 new CustomEvent("TabMultiSelect", { bubbles: true }) 7410 ); 7411 } 7412 } 7413 7414 toggleMuteAudioOnMultiSelectedTabs(aTab) { 7415 let tabMuted = aTab.linkedBrowser.audioMuted; 7416 let tabsToToggle = this.selectedTabs.filter( 7417 tab => tab.linkedBrowser.audioMuted == tabMuted 7418 ); 7419 for (let tab of tabsToToggle) { 7420 tab.toggleMuteAudio(); 7421 } 7422 } 7423 7424 resumeDelayedMediaOnMultiSelectedTabs() { 7425 for (let tab of this.selectedTabs) { 7426 tab.resumeDelayedMedia(); 7427 } 7428 } 7429 7430 pinMultiSelectedTabs() { 7431 for (let tab of this.selectedTabs) { 7432 this.pinTab(tab); 7433 } 7434 } 7435 7436 unpinMultiSelectedTabs() { 7437 // The selectedTabs getter returns the tabs 7438 // in visual order. We need to unpin in reverse 7439 // order to maintain visual order. 7440 let selectedTabs = this.selectedTabs; 7441 for (let i = selectedTabs.length - 1; i >= 0; i--) { 7442 let tab = selectedTabs[i]; 7443 this.unpinTab(tab); 7444 } 7445 } 7446 7447 activateBrowserForPrintPreview(aBrowser) { 7448 this._printPreviewBrowsers.add(aBrowser); 7449 if (this._switcher) { 7450 this._switcher.activateBrowserForPrintPreview(aBrowser); 7451 } 7452 aBrowser.docShellIsActive = true; 7453 } 7454 7455 deactivatePrintPreviewBrowsers() { 7456 let browsers = this._printPreviewBrowsers; 7457 this._printPreviewBrowsers = new Set(); 7458 for (let browser of browsers) { 7459 browser.docShellIsActive = this.shouldActivateDocShell(browser); 7460 } 7461 } 7462 7463 /** 7464 * Returns true if a given browser's docshell should be active. 7465 */ 7466 shouldActivateDocShell(aBrowser) { 7467 if (this._switcher) { 7468 return this._switcher.shouldActivateDocShell(aBrowser); 7469 } 7470 return ( 7471 (aBrowser == this.selectedBrowser && !document.hidden) || 7472 this._printPreviewBrowsers.has(aBrowser) || 7473 this.PictureInPicture.isOriginatingBrowser(aBrowser) 7474 ); 7475 } 7476 7477 _getSwitcher() { 7478 if (!this._switcher) { 7479 this._switcher = new this.AsyncTabSwitcher(this); 7480 } 7481 return this._switcher; 7482 } 7483 7484 warmupTab(aTab) { 7485 if (gMultiProcessBrowser) { 7486 this._getSwitcher().warmupTab(aTab); 7487 } 7488 } 7489 7490 /** 7491 * _maybeRequestReplyFromRemoteContent may call 7492 * aEvent.requestReplyFromRemoteContent if necessary. 7493 * 7494 * @param aEvent The handling event. 7495 * @return true if the handler should wait a reply event. 7496 * false if the handle can handle the immediately. 7497 */ 7498 _maybeRequestReplyFromRemoteContent(aEvent) { 7499 if (aEvent.defaultPrevented) { 7500 return false; 7501 } 7502 // If the event target is a remote browser, and the event has not been 7503 // handled by the remote content yet, we should wait a reply event 7504 // from the content. 7505 if (aEvent.isWaitingReplyFromRemoteContent) { 7506 return true; // Somebody called requestReplyFromRemoteContent already. 7507 } 7508 if ( 7509 !aEvent.isReplyEventFromRemoteContent && 7510 aEvent.target?.isRemoteBrowser === true 7511 ) { 7512 aEvent.requestReplyFromRemoteContent(); 7513 return true; 7514 } 7515 return false; 7516 } 7517 7518 _handleKeyDownEvent(aEvent) { 7519 if (!aEvent.isTrusted) { 7520 // Don't let untrusted events mess with tabs. 7521 return; 7522 } 7523 7524 // Skip this only if something has explicitly cancelled it. 7525 if (aEvent.defaultCancelled) { 7526 return; 7527 } 7528 7529 // Skip if chrome code has cancelled this: 7530 if (aEvent.defaultPreventedByChrome) { 7531 return; 7532 } 7533 7534 // Don't check if the event was already consumed because tab 7535 // navigation should always work for better user experience. 7536 7537 switch (ShortcutUtils.getSystemActionForEvent(aEvent)) { 7538 case ShortcutUtils.TOGGLE_CARET_BROWSING: 7539 this._maybeRequestReplyFromRemoteContent(aEvent); 7540 return; 7541 case ShortcutUtils.MOVE_TAB_BACKWARD: 7542 this.moveTabBackward(); 7543 aEvent.preventDefault(); 7544 return; 7545 case ShortcutUtils.MOVE_TAB_FORWARD: 7546 this.moveTabForward(); 7547 aEvent.preventDefault(); 7548 return; 7549 case ShortcutUtils.CLOSE_TAB: 7550 if (gBrowser.multiSelectedTabsCount) { 7551 gBrowser.removeMultiSelectedTabs(); 7552 } else if (!this.selectedTab.pinned) { 7553 this.removeCurrentTab({ animate: true }); 7554 } 7555 aEvent.preventDefault(); 7556 } 7557 } 7558 7559 toggleCaretBrowsing() { 7560 const kPrefShortcutEnabled = 7561 "accessibility.browsewithcaret_shortcut.enabled"; 7562 const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret"; 7563 const kPrefCaretBrowsingOn = "accessibility.browsewithcaret"; 7564 7565 var isEnabled = Services.prefs.getBoolPref(kPrefShortcutEnabled); 7566 if (!isEnabled || this._awaitingToggleCaretBrowsingPrompt) { 7567 return; 7568 } 7569 7570 // Toggle browse with caret mode 7571 var browseWithCaretOn = Services.prefs.getBoolPref( 7572 kPrefCaretBrowsingOn, 7573 false 7574 ); 7575 var warn = Services.prefs.getBoolPref(kPrefWarnOnEnable, true); 7576 if (warn && !browseWithCaretOn) { 7577 var checkValue = { value: false }; 7578 var promptService = Services.prompt; 7579 7580 try { 7581 this._awaitingToggleCaretBrowsingPrompt = true; 7582 const [title, message, checkbox] = 7583 this.tabLocalization.formatValuesSync([ 7584 "tabbrowser-confirm-caretbrowsing-title", 7585 "tabbrowser-confirm-caretbrowsing-message", 7586 "tabbrowser-confirm-caretbrowsing-checkbox", 7587 ]); 7588 var buttonPressed = promptService.confirmEx( 7589 window, 7590 title, 7591 message, 7592 // Make "No" the default: 7593 promptService.STD_YES_NO_BUTTONS | 7594 promptService.BUTTON_POS_1_DEFAULT, 7595 null, 7596 null, 7597 null, 7598 checkbox, 7599 checkValue 7600 ); 7601 } catch (ex) { 7602 return; 7603 } finally { 7604 this._awaitingToggleCaretBrowsingPrompt = false; 7605 } 7606 if (buttonPressed != 0) { 7607 if (checkValue.value) { 7608 try { 7609 Services.prefs.setBoolPref(kPrefShortcutEnabled, false); 7610 } catch (ex) {} 7611 } 7612 return; 7613 } 7614 if (checkValue.value) { 7615 try { 7616 Services.prefs.setBoolPref(kPrefWarnOnEnable, false); 7617 } catch (ex) {} 7618 } 7619 } 7620 7621 // Toggle the pref 7622 try { 7623 Services.prefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn); 7624 } catch (ex) {} 7625 } 7626 7627 _handleKeyPressEvent(aEvent) { 7628 if (!aEvent.isTrusted) { 7629 // Don't let untrusted events mess with tabs. 7630 return; 7631 } 7632 7633 // Skip this only if something has explicitly cancelled it. 7634 if (aEvent.defaultCancelled) { 7635 return; 7636 } 7637 7638 // Skip if chrome code has cancelled this: 7639 if (aEvent.defaultPreventedByChrome) { 7640 return; 7641 } 7642 7643 switch (ShortcutUtils.getSystemActionForEvent(aEvent, { rtl: RTL_UI })) { 7644 case ShortcutUtils.TOGGLE_CARET_BROWSING: 7645 if ( 7646 aEvent.defaultPrevented || 7647 this._maybeRequestReplyFromRemoteContent(aEvent) 7648 ) { 7649 break; 7650 } 7651 this.toggleCaretBrowsing(); 7652 break; 7653 7654 case ShortcutUtils.NEXT_TAB: 7655 if (AppConstants.platform == "macosx") { 7656 this.tabContainer.advanceSelectedTab(DIRECTION_FORWARD, true); 7657 aEvent.preventDefault(); 7658 } 7659 break; 7660 case ShortcutUtils.PREVIOUS_TAB: 7661 if (AppConstants.platform == "macosx") { 7662 this.tabContainer.advanceSelectedTab(DIRECTION_BACKWARD, true); 7663 aEvent.preventDefault(); 7664 } 7665 break; 7666 } 7667 } 7668 7669 /** 7670 * 7671 * @param {MozTabbrowserTab} tab 7672 */ 7673 #isFirstOrLastInTabGroup(tab) { 7674 if (tab.group) { 7675 let groupTabs = tab.group.tabs; 7676 if (groupTabs.at(0) == tab || groupTabs.at(-1) == tab) { 7677 return true; 7678 } 7679 } 7680 return false; 7681 } 7682 7683 getTabPids(tab) { 7684 if (!tab?.linkedBrowser) { 7685 return []; 7686 } 7687 7688 // Get the PIDs of the content process and remote subframe processes 7689 let [contentPid, ...framePids] = E10SUtils.getBrowserPids( 7690 tab.linkedBrowser, 7691 gFissionBrowser 7692 ); 7693 let pids = contentPid ? [contentPid] : []; 7694 return pids.concat(framePids.sort()); 7695 } 7696 7697 /** 7698 * @param {MozTabbrowserTab} tab 7699 * @param {boolean} [includeLabel=true] 7700 * Include the tab's title/full label in the tooltip. Defaults to true, 7701 * Can be disabled for contexts where including the title in the tooltip 7702 * string would be duplicative would already available information, 7703 * e.g. accessibility descriptions. 7704 * @returns {string} 7705 */ 7706 getTabTooltip(tab, includeLabel = true) { 7707 let labelArray = []; 7708 if (includeLabel) { 7709 labelArray.push(tab._fullLabel || tab.getAttribute("label")); 7710 } 7711 if (this.showPidAndActiveness) { 7712 const pids = this.getTabPids(tab); 7713 let debugStringArray = []; 7714 if (pids.length) { 7715 let pidLabel = pids.length > 1 ? "pids" : "pid"; 7716 debugStringArray.push(`(${pidLabel} ${pids.join(", ")})`); 7717 } 7718 7719 if (tab.linkedBrowser.docShellIsActive) { 7720 debugStringArray.push("[A]"); 7721 } 7722 7723 if (debugStringArray.length) { 7724 labelArray.push(debugStringArray.join(" ")); 7725 } 7726 } 7727 7728 // Add a line to the tooltip with additional tab context (e.g. container 7729 let containerName = tab.userContextId 7730 ? ContextualIdentityService.getUserContextLabel(tab.userContextId) 7731 : ""; 7732 let tabGroupName = this.#isFirstOrLastInTabGroup(tab) 7733 ? tab.group.name || 7734 this.tabLocalization.formatValueSync("tab-group-name-default") 7735 : ""; 7736 7737 if (containerName || tabGroupName) { 7738 let tabContextString; 7739 if (containerName && tabGroupName) { 7740 tabContextString = this.tabLocalization.formatValueSync( 7741 "tabbrowser-tab-tooltip-tab-group-container", 7742 { 7743 tabGroupName, 7744 containerName, 7745 } 7746 ); 7747 } else if (tabGroupName) { 7748 tabContextString = this.tabLocalization.formatValueSync( 7749 "tabbrowser-tab-tooltip-tab-group", 7750 { 7751 tabGroupName, 7752 } 7753 ); 7754 } else { 7755 tabContextString = this.tabLocalization.formatValueSync( 7756 "tabbrowser-tab-tooltip-container", 7757 { 7758 containerName, 7759 } 7760 ); 7761 } 7762 labelArray.push(tabContextString); 7763 } 7764 7765 if (tab.soundPlaying) { 7766 let audioPlayingString = this.tabLocalization.formatValueSync( 7767 "tabbrowser-tab-audio-playing-description" 7768 ); 7769 labelArray.push(audioPlayingString); 7770 } 7771 return labelArray.join("\n"); 7772 } 7773 7774 createTooltip(event) { 7775 event.stopPropagation(); 7776 let tab = event.target.triggerNode?.closest("tab"); 7777 if (!tab) { 7778 if (event.target.triggerNode?.getRootNode()?.host?.closest("tab")) { 7779 // Check if triggerNode is within shadowRoot of moz-button 7780 tab = event.target.triggerNode?.getRootNode().host.closest("tab"); 7781 } else { 7782 event.preventDefault(); 7783 return; 7784 } 7785 } 7786 7787 const tooltip = event.target; 7788 tooltip.removeAttribute("data-l10n-id"); 7789 7790 const tabCount = this.selectedTabs.includes(tab) 7791 ? this.selectedTabs.length 7792 : 1; 7793 if (tab._overPlayingIcon || tab._overAudioButton) { 7794 let l10nId; 7795 const l10nArgs = { tabCount }; 7796 if (tab.selected) { 7797 l10nId = tab.linkedBrowser.audioMuted 7798 ? "tabbrowser-unmute-tab-audio-tooltip" 7799 : "tabbrowser-mute-tab-audio-tooltip"; 7800 const keyElem = document.getElementById("key_toggleMute"); 7801 l10nArgs.shortcut = ShortcutUtils.prettifyShortcut(keyElem); 7802 } else if (tab.hasAttribute("activemedia-blocked")) { 7803 l10nId = "tabbrowser-unblock-tab-audio-tooltip"; 7804 } else { 7805 l10nId = tab.linkedBrowser.audioMuted 7806 ? "tabbrowser-unmute-tab-audio-background-tooltip" 7807 : "tabbrowser-mute-tab-audio-background-tooltip"; 7808 } 7809 tooltip.label = ""; 7810 document.l10n.setAttributes(tooltip, l10nId, l10nArgs); 7811 } else { 7812 // Prevent the tooltip from appearing if card preview is enabled, but 7813 // only if the user is not hovering over the media play icon or the 7814 // close button 7815 if (this._showTabCardPreview) { 7816 event.preventDefault(); 7817 return; 7818 } 7819 tooltip.label = this.getTabTooltip(tab, true); 7820 } 7821 } 7822 7823 handleEvent(aEvent) { 7824 switch (aEvent.type) { 7825 case "keydown": 7826 this._handleKeyDownEvent(aEvent); 7827 break; 7828 case "keypress": 7829 this._handleKeyPressEvent(aEvent); 7830 break; 7831 case "framefocusrequested": { 7832 let tab = this.getTabForBrowser(aEvent.target); 7833 if (!tab || tab == this.selectedTab) { 7834 // Let the focus manager try to do its thing by not calling 7835 // preventDefault(). It will still raise the window if appropriate. 7836 break; 7837 } 7838 this.selectedTab = tab; 7839 window.focus(); 7840 aEvent.preventDefault(); 7841 break; 7842 } 7843 case "visibilitychange": { 7844 const inactive = document.hidden; 7845 if (!this._switcher) { 7846 for (const browser of this.selectedBrowsers) { 7847 browser.preserveLayers(inactive); 7848 browser.docShellIsActive = !inactive; 7849 } 7850 } 7851 break; 7852 } 7853 case "TabGroupCollapse": 7854 aEvent.target.tabs.forEach(tab => { 7855 this.removeFromMultiSelectedTabs(tab); 7856 }); 7857 break; 7858 case "TabGroupCreateByUser": 7859 this.tabGroupMenu.openCreateModal(aEvent.target); 7860 break; 7861 case "TabGrouped": { 7862 let tab = aEvent.detail; 7863 let uri = 7864 tab.linkedBrowser?.registeredOpenURI || 7865 tab._originalRegisteredOpenURI; 7866 if (uri) { 7867 this.UrlbarProviderOpenTabs.unregisterOpenTab( 7868 uri.spec, 7869 tab.userContextId, 7870 null, 7871 PrivateBrowsingUtils.isWindowPrivate(window) 7872 ); 7873 this.UrlbarProviderOpenTabs.registerOpenTab( 7874 uri.spec, 7875 tab.userContextId, 7876 tab.group?.id, 7877 PrivateBrowsingUtils.isWindowPrivate(window) 7878 ); 7879 } 7880 break; 7881 } 7882 case "TabUngrouped": { 7883 let tab = aEvent.detail; 7884 let uri = 7885 tab.linkedBrowser?.registeredOpenURI || 7886 tab._originalRegisteredOpenURI; 7887 if (uri) { 7888 // By the time the tab makes it to us it is already ungrouped, but 7889 // the original group is preserved in the event target. 7890 let originalGroup = aEvent.target; 7891 this.UrlbarProviderOpenTabs.unregisterOpenTab( 7892 uri.spec, 7893 tab.userContextId, 7894 originalGroup.id, 7895 PrivateBrowsingUtils.isWindowPrivate(window) 7896 ); 7897 this.UrlbarProviderOpenTabs.registerOpenTab( 7898 uri.spec, 7899 tab.userContextId, 7900 null, 7901 PrivateBrowsingUtils.isWindowPrivate(window) 7902 ); 7903 } 7904 break; 7905 } 7906 case "TabSplitViewActivate": 7907 this.#activeSplitView = aEvent.detail.splitview; 7908 break; 7909 case "TabSplitViewDeactivate": 7910 if (this.#activeSplitView === aEvent.detail.splitview) { 7911 this.#activeSplitView = null; 7912 } 7913 break; 7914 case "activate": 7915 // Intentional fallthrough 7916 case "deactivate": 7917 this.selectedTab.updateLastSeenActive(); 7918 break; 7919 } 7920 } 7921 7922 observe(aSubject, aTopic) { 7923 switch (aTopic) { 7924 case "contextual-identity-updated": { 7925 let identity = aSubject.wrappedJSObject; 7926 for (let tab of this.tabs) { 7927 if (tab.getAttribute("usercontextid") == identity.userContextId) { 7928 ContextualIdentityService.setTabStyle(tab); 7929 } 7930 } 7931 break; 7932 } 7933 case "intl:app-locales-changed": { 7934 this.#populateTitleCache(); 7935 this.updateTitlebar(); 7936 break; 7937 } 7938 } 7939 } 7940 7941 refreshBlocked(actor, browser, data) { 7942 // The data object is expected to contain the following properties: 7943 // - URI (string) 7944 // The URI that a page is attempting to refresh or redirect to. 7945 // - delay (int) 7946 // The delay (in milliseconds) before the page was going to 7947 // reload or redirect. 7948 // - sameURI (bool) 7949 // true if we're refreshing the page. false if we're redirecting. 7950 7951 let notificationBox = this.getNotificationBox(browser); 7952 let notification = 7953 notificationBox.getNotificationWithValue("refresh-blocked"); 7954 7955 let l10nId = data.sameURI 7956 ? "refresh-blocked-refresh-label" 7957 : "refresh-blocked-redirect-label"; 7958 if (notification) { 7959 notification.label = { "l10n-id": l10nId }; 7960 } else { 7961 const buttons = [ 7962 { 7963 "l10n-id": "refresh-blocked-allow", 7964 callback() { 7965 actor.sendAsyncMessage("RefreshBlocker:Refresh", data); 7966 }, 7967 }, 7968 ]; 7969 7970 notificationBox.appendNotification( 7971 "refresh-blocked", 7972 { 7973 label: { "l10n-id": l10nId }, 7974 image: "chrome://browser/skin/notification-icons/popup.svg", 7975 priority: notificationBox.PRIORITY_INFO_MEDIUM, 7976 }, 7977 buttons 7978 ); 7979 } 7980 } 7981 7982 _generateUniquePanelID() { 7983 if (!this._uniquePanelIDCounter) { 7984 this._uniquePanelIDCounter = 0; 7985 } 7986 7987 let outerID = window.docShell.outerWindowID; 7988 7989 // We want panel IDs to be globally unique, that's why we include the 7990 // window ID. We switched to a monotonic counter as Date.now() lead 7991 // to random failures because of colliding IDs. 7992 return "panel-" + outerID + "-" + ++this._uniquePanelIDCounter; 7993 } 7994 7995 destroy() { 7996 this.tabContainer.destroy(); 7997 Services.obs.removeObserver(this, "contextual-identity-updated"); 7998 Services.obs.removeObserver(this, "intl:app-locales-changed"); 7999 8000 for (let tab of this.tabs) { 8001 let browser = tab.linkedBrowser; 8002 if (browser.registeredOpenURI) { 8003 let userContextId = browser.getAttribute("usercontextid") || 0; 8004 this.UrlbarProviderOpenTabs.unregisterOpenTab( 8005 browser.registeredOpenURI.spec, 8006 userContextId, 8007 tab.group?.id, 8008 PrivateBrowsingUtils.isWindowPrivate(window) 8009 ); 8010 delete browser.registeredOpenURI; 8011 } 8012 8013 let filter = this._tabFilters.get(tab); 8014 if (filter) { 8015 browser.webProgress.removeProgressListener(filter); 8016 8017 let listener = this._tabListeners.get(tab); 8018 if (listener) { 8019 filter.removeProgressListener(listener); 8020 listener.destroy(); 8021 } 8022 8023 this._tabFilters.delete(tab); 8024 this._tabListeners.delete(tab); 8025 } 8026 } 8027 8028 document.removeEventListener("keydown", this, { mozSystemGroup: true }); 8029 if (AppConstants.platform == "macosx") { 8030 document.removeEventListener("keypress", this, { 8031 mozSystemGroup: true, 8032 }); 8033 } 8034 document.removeEventListener("visibilitychange", this); 8035 window.removeEventListener("framefocusrequested", this); 8036 window.removeEventListener("activate", this); 8037 window.removeEventListener("deactivate", this); 8038 8039 if (gMultiProcessBrowser) { 8040 if (this._switcher) { 8041 this._switcher.destroy(); 8042 } 8043 } 8044 } 8045 8046 _setupEventListeners() { 8047 this.tabpanels.addEventListener("select", event => { 8048 if (event.target == this.tabpanels) { 8049 this.updateCurrentBrowser(); 8050 } 8051 }); 8052 8053 this.addEventListener("DOMWindowClose", event => { 8054 let browser = event.target; 8055 if (!browser.isRemoteBrowser) { 8056 if (!event.isTrusted) { 8057 // If the browser is not remote, then we expect the event to be trusted. 8058 // In the remote case, the DOMWindowClose event is captured in content, 8059 // a message is sent to the parent, and another DOMWindowClose event 8060 // is re-dispatched on the actual browser node. In that case, the event 8061 // won't be marked as trusted, since it's synthesized by JavaScript. 8062 return; 8063 } 8064 // In the parent-process browser case, it's possible that the browser 8065 // that fired DOMWindowClose is actually a child of another browser. We 8066 // want to find the top-most browser to determine whether or not this is 8067 // for a tab or not. The chromeEventHandler will be the top-most browser. 8068 browser = event.target.docShell.chromeEventHandler; 8069 } 8070 8071 if (this.tabs.length == 1) { 8072 // We already did PermitUnload in the content process 8073 // for this tab (the only one in the window). So we don't 8074 // need to do it again for any tabs. 8075 window.skipNextCanClose = true; 8076 // In the parent-process browser case, the nsCloseEvent will actually take 8077 // care of tearing down the window, but we need to do this ourselves in the 8078 // content-process browser case. Doing so in both cases doesn't appear to 8079 // hurt. 8080 window.close(); 8081 return; 8082 } 8083 8084 let tab = this.getTabForBrowser(browser); 8085 if (tab) { 8086 // Skip running PermitUnload since it already happened in 8087 // the content process. 8088 this.removeTab(tab, { skipPermitUnload: true }); 8089 // If we don't preventDefault on the DOMWindowClose event, then 8090 // in the parent-process browser case, we're telling the platform 8091 // to close the entire window. Calling preventDefault is our way of 8092 // saying we took care of this close request by closing the tab. 8093 event.preventDefault(); 8094 } 8095 }); 8096 8097 this.addEventListener("pagetitlechanged", event => { 8098 let browser = event.target; 8099 let tab = this.getTabForBrowser(browser); 8100 if (!tab || tab.hasAttribute("pending")) { 8101 return; 8102 } 8103 8104 // Ignore empty title changes on internal pages. This prevents the title 8105 // from changing while Fluent is populating the (initially-empty) title 8106 // element. 8107 if ( 8108 !browser.contentTitle && 8109 browser.contentPrincipal.isSystemPrincipal 8110 ) { 8111 return; 8112 } 8113 8114 let titleChanged = this.setTabTitle(tab); 8115 if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) { 8116 tab.setAttribute("titlechanged", "true"); 8117 } 8118 }); 8119 8120 this.addEventListener( 8121 "DOMWillOpenModalDialog", 8122 event => { 8123 if (!event.isTrusted) { 8124 return; 8125 } 8126 8127 let targetIsWindow = Window.isInstance(event.target); 8128 8129 // We're about to open a modal dialog, so figure out for which tab: 8130 // If this is a same-process modal dialog, then we're given its DOM 8131 // window as the event's target. For remote dialogs, we're given the 8132 // browser, but that's in the originalTarget and not the target, 8133 // because it's across the tabbrowser's XBL boundary. 8134 let tabForEvent = targetIsWindow 8135 ? this.getTabForBrowser(event.target.docShell.chromeEventHandler) 8136 : this.getTabForBrowser(event.originalTarget); 8137 8138 // Focus window for beforeunload dialog so it is seen but don't 8139 // steal focus from other applications. 8140 if ( 8141 event.detail && 8142 event.detail.tabPrompt && 8143 event.detail.inPermitUnload && 8144 Services.focus.activeWindow 8145 ) { 8146 window.focus(); 8147 } 8148 8149 // Don't need to act if the tab is already selected or if there isn't 8150 // a tab for the event (e.g. for the webextensions options_ui remote 8151 // browsers embedded in the "about:addons" page): 8152 if (!tabForEvent || tabForEvent.selected) { 8153 return; 8154 } 8155 8156 // We always switch tabs for beforeunload tab-modal prompts. 8157 if ( 8158 event.detail && 8159 event.detail.tabPrompt && 8160 !event.detail.inPermitUnload 8161 ) { 8162 let docPrincipal = targetIsWindow 8163 ? event.target.document.nodePrincipal 8164 : null; 8165 // At least one of these should/will be non-null: 8166 let promptPrincipal = 8167 event.detail.promptPrincipal || 8168 docPrincipal || 8169 tabForEvent.linkedBrowser.contentPrincipal; 8170 8171 // For null principals, we bail immediately and don't show the checkbox: 8172 if (!promptPrincipal || promptPrincipal.isNullPrincipal) { 8173 tabForEvent.attention = true; 8174 return; 8175 } 8176 8177 // For non-system/expanded principals without permission, we bail and show the checkbox. 8178 if (promptPrincipal.URI && !promptPrincipal.isSystemPrincipal) { 8179 let permission = Services.perms.testPermissionFromPrincipal( 8180 promptPrincipal, 8181 "focus-tab-by-prompt" 8182 ); 8183 if (permission != Services.perms.ALLOW_ACTION) { 8184 // Tell the prompt box we want to show the user a checkbox: 8185 let tabPrompt = this.getTabDialogBox(tabForEvent.linkedBrowser); 8186 tabPrompt.onNextPromptShowAllowFocusCheckboxFor( 8187 promptPrincipal 8188 ); 8189 tabForEvent.attention = true; 8190 return; 8191 } 8192 } 8193 // ... so system and expanded principals, as well as permitted "normal" 8194 // URI-based principals, always get to steal focus for the tab when prompting. 8195 } 8196 8197 // If permissions/origins dictate so, bring tab to the front. 8198 this.selectedTab = tabForEvent; 8199 }, 8200 true 8201 ); 8202 8203 // When cancelling beforeunload tabmodal dialogs, reset the URL bar to 8204 // avoid spoofing risks. 8205 this.addEventListener( 8206 "DOMModalDialogClosed", 8207 event => { 8208 if ( 8209 event.detail?.promptType != "beforeunload" || 8210 event.detail.areLeaving || 8211 event.target.nodeName != "browser" 8212 ) { 8213 return; 8214 } 8215 event.target.userTypedValue = null; 8216 if (event.target == this.selectedBrowser) { 8217 gURLBar.setURI(); 8218 } 8219 }, 8220 true 8221 ); 8222 8223 let onTabCrashed = event => { 8224 if (!event.isTrusted) { 8225 return; 8226 } 8227 8228 let browser = event.originalTarget; 8229 8230 if (!event.isTopFrame) { 8231 TabCrashHandler.onSubFrameCrash(browser, event.childID); 8232 return; 8233 } 8234 8235 // Preloaded browsers do not actually have any tabs. If one crashes, 8236 // it should be released and removed. 8237 if (browser === this.preloadedBrowser) { 8238 NewTabPagePreloading.removePreloadedBrowser(window); 8239 return; 8240 } 8241 8242 let isRestartRequiredCrash = 8243 event.type == "oop-browser-buildid-mismatch"; 8244 8245 let icon = browser.mIconURL; 8246 let tab = this.getTabForBrowser(browser); 8247 8248 if (this.selectedBrowser == browser) { 8249 TabCrashHandler.onSelectedBrowserCrash( 8250 browser, 8251 isRestartRequiredCrash 8252 ); 8253 } else { 8254 TabCrashHandler.onBackgroundBrowserCrash( 8255 browser, 8256 isRestartRequiredCrash 8257 ); 8258 } 8259 8260 tab.removeAttribute("soundplaying"); 8261 this.setIcon(tab, icon); 8262 }; 8263 8264 this.addEventListener("oop-browser-crashed", onTabCrashed); 8265 this.addEventListener("oop-browser-buildid-mismatch", onTabCrashed); 8266 8267 this.addEventListener("DOMAudioPlaybackStarted", event => { 8268 var tab = this.getTabFromAudioEvent(event); 8269 if (!tab) { 8270 return; 8271 } 8272 8273 clearTimeout(tab._soundPlayingAttrRemovalTimer); 8274 tab._soundPlayingAttrRemovalTimer = 0; 8275 8276 let modifiedAttrs = []; 8277 if (tab.hasAttribute("soundplaying-scheduledremoval")) { 8278 tab.removeAttribute("soundplaying-scheduledremoval"); 8279 modifiedAttrs.push("soundplaying-scheduledremoval"); 8280 } 8281 8282 if (!tab.hasAttribute("soundplaying")) { 8283 tab.toggleAttribute("soundplaying", true); 8284 modifiedAttrs.push("soundplaying"); 8285 } 8286 8287 if (modifiedAttrs.length) { 8288 // Flush style so that the opacity takes effect immediately, in 8289 // case the media is stopped before the style flushes naturally. 8290 getComputedStyle(tab).opacity; 8291 } 8292 8293 this._tabAttrModified(tab, modifiedAttrs); 8294 }); 8295 8296 this.addEventListener("DOMAudioPlaybackStopped", event => { 8297 var tab = this.getTabFromAudioEvent(event); 8298 if (!tab) { 8299 return; 8300 } 8301 8302 if (tab.hasAttribute("soundplaying")) { 8303 let removalDelay = Services.prefs.getIntPref( 8304 "browser.tabs.delayHidingAudioPlayingIconMS" 8305 ); 8306 8307 tab.style.setProperty( 8308 "--soundplaying-removal-delay", 8309 `${removalDelay - 300}ms` 8310 ); 8311 tab.toggleAttribute("soundplaying-scheduledremoval", true); 8312 this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]); 8313 8314 tab._soundPlayingAttrRemovalTimer = setTimeout(() => { 8315 tab.removeAttribute("soundplaying-scheduledremoval"); 8316 tab.removeAttribute("soundplaying"); 8317 this._tabAttrModified(tab, [ 8318 "soundplaying", 8319 "soundplaying-scheduledremoval", 8320 ]); 8321 }, removalDelay); 8322 } 8323 }); 8324 8325 this.addEventListener("DOMAudioPlaybackBlockStarted", event => { 8326 var tab = this.getTabFromAudioEvent(event); 8327 if (!tab) { 8328 return; 8329 } 8330 8331 if (!tab.hasAttribute("activemedia-blocked")) { 8332 tab.setAttribute("activemedia-blocked", true); 8333 this._tabAttrModified(tab, ["activemedia-blocked"]); 8334 } 8335 }); 8336 8337 this.addEventListener("DOMAudioPlaybackBlockStopped", event => { 8338 var tab = this.getTabFromAudioEvent(event); 8339 if (!tab) { 8340 return; 8341 } 8342 8343 if (tab.hasAttribute("activemedia-blocked")) { 8344 tab.removeAttribute("activemedia-blocked"); 8345 this._tabAttrModified(tab, ["activemedia-blocked"]); 8346 } 8347 }); 8348 8349 this.addEventListener("GloballyAutoplayBlocked", event => { 8350 let browser = event.originalTarget; 8351 let tab = this.getTabForBrowser(browser); 8352 if (!tab) { 8353 return; 8354 } 8355 8356 SitePermissions.setForPrincipal( 8357 browser.contentPrincipal, 8358 "autoplay-media", 8359 SitePermissions.BLOCK, 8360 SitePermissions.SCOPE_GLOBAL, 8361 browser 8362 ); 8363 }); 8364 8365 let tabContextFTLInserter = () => { 8366 this.translateTabContextMenu(); 8367 this.tabContainer.removeEventListener( 8368 "contextmenu", 8369 tabContextFTLInserter, 8370 true 8371 ); 8372 this.tabContainer.removeEventListener( 8373 "mouseover", 8374 tabContextFTLInserter 8375 ); 8376 this.tabContainer.removeEventListener( 8377 "focus", 8378 tabContextFTLInserter, 8379 true 8380 ); 8381 }; 8382 this.tabContainer.addEventListener( 8383 "contextmenu", 8384 tabContextFTLInserter, 8385 true 8386 ); 8387 this.tabContainer.addEventListener("mouseover", tabContextFTLInserter); 8388 this.tabContainer.addEventListener("focus", tabContextFTLInserter, true); 8389 8390 // Fired when Gecko has decided a <browser> element will change 8391 // remoteness. This allows persisting some state on this element across 8392 // process switches. 8393 this.addEventListener("WillChangeBrowserRemoteness", event => { 8394 let browser = event.originalTarget; 8395 let tab = this.getTabForBrowser(browser); 8396 if (!tab) { 8397 return; 8398 } 8399 8400 // Dispatch the `BeforeTabRemotenessChange` event, allowing other code 8401 // to react to this tab's process switch. 8402 let evt = document.createEvent("Events"); 8403 evt.initEvent("BeforeTabRemotenessChange", true, false); 8404 tab.dispatchEvent(evt); 8405 8406 // Unhook our progress listener. 8407 let filter = this._tabFilters.get(tab); 8408 let oldListener = this._tabListeners.get(tab); 8409 browser.webProgress.removeProgressListener(filter); 8410 filter.removeProgressListener(oldListener); 8411 let stateFlags = oldListener.mStateFlags; 8412 let requestCount = oldListener.mRequestCount; 8413 8414 // We'll be creating a new listener, so destroy the old one. 8415 oldListener.destroy(); 8416 8417 let oldDroppedLinkHandler = browser.droppedLinkHandler; 8418 let oldUserTypedValue = browser.userTypedValue; 8419 let hadStartedLoad = browser.didStartLoadSinceLastUserTyping(); 8420 8421 let didChange = () => { 8422 browser.userTypedValue = oldUserTypedValue; 8423 if (hadStartedLoad) { 8424 browser.urlbarChangeTracker.startedLoad(); 8425 } 8426 8427 browser.droppedLinkHandler = oldDroppedLinkHandler; 8428 8429 // This shouldn't really be necessary, however, this has the side effect 8430 // of sending MozLayerTreeReady / MozLayerTreeCleared events for remote 8431 // frames, which the tab switcher depends on. 8432 // 8433 // eslint-disable-next-line no-self-assign 8434 browser.docShellIsActive = browser.docShellIsActive; 8435 8436 // Create a new tab progress listener for the new browser we just 8437 // injected, since tab progress listeners have logic for handling the 8438 // initial about:blank load 8439 let listener = new TabProgressListener( 8440 tab, 8441 browser, 8442 false, 8443 false, 8444 stateFlags, 8445 requestCount 8446 ); 8447 this._tabListeners.set(tab, listener); 8448 filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL); 8449 8450 // Restore the progress listener. 8451 browser.webProgress.addProgressListener( 8452 filter, 8453 Ci.nsIWebProgress.NOTIFY_ALL 8454 ); 8455 8456 let cbEvent = browser.getContentBlockingEvents(); 8457 // Include the true final argument to indicate that this event is 8458 // simulated (instead of being observed by the webProgressListener). 8459 this._callProgressListeners( 8460 browser, 8461 "onContentBlockingEvent", 8462 [browser.webProgress, null, cbEvent, true], 8463 true, 8464 false 8465 ); 8466 8467 if (browser.isRemoteBrowser) { 8468 // Switching the browser to be remote will connect to a new child 8469 // process so the browser can no longer be considered to be 8470 // crashed. 8471 tab.removeAttribute("crashed"); 8472 } 8473 8474 if (this.isFindBarInitialized(tab)) { 8475 this.getCachedFindBar(tab).browser = browser; 8476 } 8477 8478 evt = document.createEvent("Events"); 8479 evt.initEvent("TabRemotenessChange", true, false); 8480 tab.dispatchEvent(evt); 8481 }; 8482 browser.addEventListener("DidChangeBrowserRemoteness", didChange, { 8483 once: true, 8484 }); 8485 }); 8486 8487 this.addEventListener("pageinfo", event => { 8488 let browser = event.originalTarget; 8489 let tab = this.getTabForBrowser(browser); 8490 if (!tab) { 8491 return; 8492 } 8493 const { url, description, previewImageURL } = event.detail; 8494 this.setPageInfo(tab, url, description, previewImageURL); 8495 }); 8496 8497 this.splitViewCommandSet.addEventListener("command", event => { 8498 switch (event.target.id) { 8499 case "splitViewCmd_separateTabs": 8500 this.#activeSplitView.unsplitTabs(); 8501 break; 8502 case "splitViewCmd_reverseTabs": 8503 this.#activeSplitView.reverseTabs(); 8504 break; 8505 case "splitViewCmd_closeTabs": 8506 this.#activeSplitView.close(); 8507 break; 8508 } 8509 }); 8510 } 8511 8512 translateTabContextMenu() { 8513 if (this._tabContextMenuTranslated) { 8514 return; 8515 } 8516 MozXULElement.insertFTLIfNeeded("browser/tabContextMenu.ftl"); 8517 // Un-lazify the l10n-ids now that the FTL file has been inserted. 8518 document 8519 .getElementById("tabContextMenu") 8520 .querySelectorAll("[data-lazy-l10n-id]") 8521 .forEach(el => { 8522 el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id")); 8523 el.removeAttribute("data-lazy-l10n-id"); 8524 }); 8525 this._tabContextMenuTranslated = true; 8526 } 8527 8528 setSuccessor(aTab, successorTab) { 8529 if (aTab.ownerGlobal != window) { 8530 throw new Error("Cannot set the successor of another window's tab"); 8531 } 8532 if (successorTab == aTab) { 8533 successorTab = null; 8534 } 8535 if (successorTab && successorTab.ownerGlobal != window) { 8536 throw new Error("Cannot set the successor to another window's tab"); 8537 } 8538 if (aTab.successor) { 8539 aTab.successor.predecessors.delete(aTab); 8540 } 8541 aTab.successor = successorTab; 8542 if (successorTab) { 8543 if (!successorTab.predecessors) { 8544 successorTab.predecessors = new Set(); 8545 } 8546 successorTab.predecessors.add(aTab); 8547 } 8548 } 8549 8550 /** 8551 * For all tabs with aTab as a successor, set the successor to aOtherTab 8552 * instead. 8553 */ 8554 replaceInSuccession(aTab, aOtherTab) { 8555 if (aTab.predecessors) { 8556 for (const predecessor of Array.from(aTab.predecessors)) { 8557 this.setSuccessor(predecessor, aOtherTab); 8558 } 8559 } 8560 } 8561 8562 /** 8563 * Get the triggering principal for the last navigation in the session history. 8564 */ 8565 _getTriggeringPrincipalFromHistory(aBrowser) { 8566 let sessionHistory = aBrowser?.browsingContext?.sessionHistory; 8567 if ( 8568 !sessionHistory || 8569 !sessionHistory.index || 8570 sessionHistory.count == 0 8571 ) { 8572 return undefined; 8573 } 8574 let currentEntry = sessionHistory.getEntryAtIndex(sessionHistory.index); 8575 let triggeringPrincipal = currentEntry?.triggeringPrincipal; 8576 return triggeringPrincipal; 8577 } 8578 8579 clearRelatedTabs() { 8580 this._lastRelatedTabMap = new WeakMap(); 8581 } 8582 }; 8583 8584 /** 8585 * A web progress listener object definition for a given tab. 8586 */ 8587 class TabProgressListener { 8588 constructor( 8589 aTab, 8590 aBrowser, 8591 aStartsBlank, 8592 aWasPreloadedBrowser, 8593 aOrigStateFlags, 8594 aOrigRequestCount 8595 ) { 8596 let stateFlags = aOrigStateFlags || 0; 8597 // Initialize mStateFlags to non-zero e.g. when creating a progress 8598 // listener for preloaded browsers as there was no progress listener 8599 // around when the content started loading. If the content didn't 8600 // quite finish loading yet, mStateFlags will very soon be overridden 8601 // with the correct value and end up at STATE_STOP again. 8602 if (aWasPreloadedBrowser) { 8603 stateFlags = 8604 Ci.nsIWebProgressListener.STATE_STOP | 8605 Ci.nsIWebProgressListener.STATE_IS_REQUEST; 8606 } 8607 8608 this.mTab = aTab; 8609 this.mBrowser = aBrowser; 8610 this.mBlank = aStartsBlank; 8611 8612 // cache flags for correct status UI update after tab switching 8613 this.mStateFlags = stateFlags; 8614 this.mStatus = 0; 8615 this.mMessage = ""; 8616 this.mTotalProgress = 0; 8617 8618 // count of open requests (should always be 0 or 1) 8619 this.mRequestCount = aOrigRequestCount || 0; 8620 } 8621 8622 destroy() { 8623 delete this.mTab; 8624 delete this.mBrowser; 8625 } 8626 8627 _callProgressListeners(...args) { 8628 args.unshift(this.mBrowser); 8629 return gBrowser._callProgressListeners.apply(gBrowser, args); 8630 } 8631 8632 _shouldShowProgress(aRequest) { 8633 if (this.mBlank) { 8634 return false; 8635 } 8636 8637 // Don't show progress indicators in tabs for about: URIs 8638 // pointing to local resources. 8639 if ( 8640 aRequest instanceof Ci.nsIChannel && 8641 aRequest.originalURI.schemeIs("about") 8642 ) { 8643 return false; 8644 } 8645 8646 return true; 8647 } 8648 8649 _isForInitialAboutBlank(aWebProgress, aStateFlags, aLocation) { 8650 if (!this.mBlank || !aWebProgress.isTopLevel) { 8651 return false; 8652 } 8653 8654 // If the state has STATE_STOP, and no requests were in flight, then this 8655 // must be the initial "stop" for the initial about:blank document. 8656 if ( 8657 aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && 8658 this.mRequestCount == 0 && 8659 !aLocation 8660 ) { 8661 return true; 8662 } 8663 8664 let location = aLocation ? aLocation.spec : ""; 8665 return location == "about:blank"; 8666 } 8667 8668 onProgressChange( 8669 aWebProgress, 8670 aRequest, 8671 aCurSelfProgress, 8672 aMaxSelfProgress, 8673 aCurTotalProgress, 8674 aMaxTotalProgress 8675 ) { 8676 this.mTotalProgress = aMaxTotalProgress 8677 ? aCurTotalProgress / aMaxTotalProgress 8678 : 0; 8679 8680 if (!this._shouldShowProgress(aRequest)) { 8681 return; 8682 } 8683 8684 if (this.mTotalProgress && this.mTab.hasAttribute("busy")) { 8685 this.mTab.setAttribute("progress", "true"); 8686 gBrowser._tabAttrModified(this.mTab, ["progress"]); 8687 } 8688 8689 this._callProgressListeners("onProgressChange", [ 8690 aWebProgress, 8691 aRequest, 8692 aCurSelfProgress, 8693 aMaxSelfProgress, 8694 aCurTotalProgress, 8695 aMaxTotalProgress, 8696 ]); 8697 } 8698 8699 onProgressChange64( 8700 aWebProgress, 8701 aRequest, 8702 aCurSelfProgress, 8703 aMaxSelfProgress, 8704 aCurTotalProgress, 8705 aMaxTotalProgress 8706 ) { 8707 return this.onProgressChange( 8708 aWebProgress, 8709 aRequest, 8710 aCurSelfProgress, 8711 aMaxSelfProgress, 8712 aCurTotalProgress, 8713 aMaxTotalProgress 8714 ); 8715 } 8716 8717 /* eslint-disable complexity */ 8718 onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { 8719 if (!aRequest) { 8720 return; 8721 } 8722 8723 let location, originalLocation; 8724 try { 8725 aRequest.QueryInterface(Ci.nsIChannel); 8726 location = aRequest.URI; 8727 originalLocation = aRequest.originalURI; 8728 } catch (ex) {} 8729 8730 let ignoreBlank = this._isForInitialAboutBlank( 8731 aWebProgress, 8732 aStateFlags, 8733 location 8734 ); 8735 8736 const { STATE_START, STATE_STOP, STATE_IS_NETWORK } = 8737 Ci.nsIWebProgressListener; 8738 8739 // If we were ignoring some messages about the initial about:blank, and we 8740 // got the STATE_STOP for it, we'll want to pay attention to those messages 8741 // from here forward. Similarly, if we conclude that this state change 8742 // is one that we shouldn't be ignoring, then stop ignoring. 8743 if ( 8744 (ignoreBlank && 8745 aStateFlags & STATE_STOP && 8746 aStateFlags & STATE_IS_NETWORK) || 8747 (!ignoreBlank && this.mBlank) 8748 ) { 8749 this.mBlank = false; 8750 } 8751 8752 if (aStateFlags & STATE_START && aStateFlags & STATE_IS_NETWORK) { 8753 this.mRequestCount++; 8754 8755 if (aWebProgress.isTopLevel) { 8756 // Need to use originalLocation rather than location because things 8757 // like about:home and about:privatebrowsing arrive with nsIRequest 8758 // pointing to their resolved jar: or file: URIs. 8759 if ( 8760 !( 8761 originalLocation && 8762 gInitialPages.includes(originalLocation.spec) && 8763 originalLocation != "about:blank" && 8764 this.mBrowser.initialPageLoadedFromUserAction != 8765 originalLocation.spec && 8766 this.mBrowser.currentURI && 8767 this.mBrowser.currentURI.spec == "about:blank" 8768 ) 8769 ) { 8770 // Indicating that we started a load will allow the location 8771 // bar to be cleared when the load finishes. 8772 // In order to not overwrite user-typed content, we avoid it 8773 // (see if condition above) in a very specific case: 8774 // If the load is of an 'initial' page (e.g. about:privatebrowsing, 8775 // about:newtab, etc.), was not explicitly typed in the location 8776 // bar by the user, is not about:blank (because about:blank can be 8777 // loaded by websites under their principal), and the current 8778 // page in the browser is about:blank (indicating it is a newly 8779 // created or re-created browser, e.g. because it just switched 8780 // remoteness or is a new tab/window). 8781 this.mBrowser.urlbarChangeTracker.startedLoad(); 8782 8783 // To improve the user experience and perceived performance when 8784 // opening links in new tabs, we show the url and tab title sooner, 8785 // but only if it's safe (from a phishing point of view) to do so, 8786 // thus there's no session history and the load starts from a 8787 // non-web-controlled blank page. 8788 if ( 8789 this.mBrowser.browsingContext.sessionHistory?.count === 0 && 8790 BrowserUIUtils.checkEmptyPageOrigin( 8791 this.mBrowser, 8792 originalLocation 8793 ) 8794 ) { 8795 gBrowser.setInitialTabTitle(this.mTab, originalLocation.spec, { 8796 isURL: true, 8797 }); 8798 8799 this.mBrowser.browsingContext.nonWebControlledBlankURI = 8800 originalLocation; 8801 if (this.mTab.selected && !gBrowser.userTypedValue) { 8802 gURLBar.setURI(); 8803 } 8804 } 8805 } 8806 delete this.mBrowser.initialPageLoadedFromUserAction; 8807 // If the browser is loading it must not be crashed anymore 8808 this.mTab.removeAttribute("crashed"); 8809 } 8810 8811 if (this._shouldShowProgress(aRequest)) { 8812 if ( 8813 !(aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) && 8814 aWebProgress && 8815 aWebProgress.isTopLevel 8816 ) { 8817 this.mTab.setAttribute("busy", "true"); 8818 gBrowser._tabAttrModified(this.mTab, ["busy"]); 8819 this.mTab._notselectedsinceload = !this.mTab.selected; 8820 } 8821 8822 if (this.mTab.selected) { 8823 gBrowser._isBusy = true; 8824 } 8825 } 8826 } else if (aStateFlags & STATE_STOP && aStateFlags & STATE_IS_NETWORK) { 8827 // since we (try to) only handle STATE_STOP of the last request, 8828 // the count of open requests should now be 0 8829 this.mRequestCount = 0; 8830 8831 let modifiedAttrs = []; 8832 if (this.mTab.hasAttribute("busy")) { 8833 this.mTab.removeAttribute("busy"); 8834 modifiedAttrs.push("busy"); 8835 8836 // Only animate the "burst" indicating the page has loaded if 8837 // the top-level page is the one that finished loading. 8838 if ( 8839 aWebProgress.isTopLevel && 8840 !aWebProgress.isLoadingDocument && 8841 Components.isSuccessCode(aStatus) && 8842 !gBrowser.tabAnimationsInProgress && 8843 !gReduceMotion 8844 ) { 8845 if (this.mTab._notselectedsinceload) { 8846 this.mTab.setAttribute("notselectedsinceload", "true"); 8847 } else { 8848 this.mTab.removeAttribute("notselectedsinceload"); 8849 } 8850 8851 this.mTab.setAttribute("bursting", "true"); 8852 } 8853 } 8854 8855 if (this.mTab.hasAttribute("progress")) { 8856 this.mTab.removeAttribute("progress"); 8857 modifiedAttrs.push("progress"); 8858 } 8859 8860 if (aWebProgress.isTopLevel) { 8861 let isSuccessful = Components.isSuccessCode(aStatus); 8862 if (!isSuccessful && !this.mTab.isEmpty) { 8863 // Restore the current document's location in case the 8864 // request was stopped (possibly from a content script) 8865 // before the location changed. 8866 8867 this.mBrowser.userTypedValue = null; 8868 // When SHIP is enabled and a load gets cancelled due to another one 8869 // starting, the error is NS_BINDING_CANCELLED_OLD_LOAD. 8870 // When these prefs are not enabled, the error is different and 8871 // that's why we still want to look at the isNavigating flag. 8872 // We could add a workaround and make sure that in the alternative 8873 // codepaths we would also omit the same error, but considering 8874 // how we will be enabling fission by default soon, we can keep 8875 // using isNavigating for now, and remove it when SHIP is enabled 8876 // by default. 8877 // Bug 1725716 has been filed to consider removing isNavigating 8878 // field alltogether. 8879 let isNavigating = this.mBrowser.isNavigating; 8880 if ( 8881 this.mTab.selected && 8882 aStatus != Cr.NS_BINDING_CANCELLED_OLD_LOAD && 8883 !isNavigating 8884 ) { 8885 gURLBar.setURI(); 8886 } 8887 } else if (isSuccessful) { 8888 this.mBrowser.urlbarChangeTracker.finishedLoad(); 8889 } 8890 } 8891 8892 // If we don't already have an icon for this tab then clear the tab's 8893 // icon. Don't do this on the initial about:blank load to prevent 8894 // flickering. Don't clear the icon if we already set it from one of the 8895 // known defaults. Note we use the original URL since about:newtab 8896 // redirects to a prerendered page. 8897 const shouldRemoveFavicon = 8898 !this.mBrowser.mIconURL && 8899 !ignoreBlank && 8900 !(originalLocation.spec in FAVICON_DEFAULTS); 8901 if (shouldRemoveFavicon && this.mTab.hasAttribute("image")) { 8902 this.mTab.removeAttribute("image"); 8903 modifiedAttrs.push("image"); 8904 } else if (!shouldRemoveFavicon) { 8905 // Bug 1804166: Allow new tabs to set the favicon correctly if the 8906 // new tabs behavior is set to open a blank page 8907 // This is a no-op unless this.mBrowser._documentURI is in 8908 // FAVICON_DEFAULTS. 8909 gBrowser.setDefaultIcon(this.mTab, this.mBrowser._documentURI); 8910 } 8911 8912 // For keyword URIs clear the user typed value since they will be changed into real URIs 8913 if (location.scheme == "keyword") { 8914 this.mBrowser.userTypedValue = null; 8915 } 8916 8917 if (this.mTab.selected) { 8918 gBrowser._isBusy = false; 8919 } 8920 8921 if (modifiedAttrs.length) { 8922 gBrowser._tabAttrModified(this.mTab, modifiedAttrs); 8923 } 8924 } 8925 8926 if (ignoreBlank) { 8927 this._callProgressListeners( 8928 "onUpdateCurrentBrowser", 8929 [aStateFlags, aStatus, "", 0], 8930 true, 8931 false 8932 ); 8933 } else { 8934 this._callProgressListeners( 8935 "onStateChange", 8936 [aWebProgress, aRequest, aStateFlags, aStatus], 8937 true, 8938 false 8939 ); 8940 } 8941 8942 this._callProgressListeners( 8943 "onStateChange", 8944 [aWebProgress, aRequest, aStateFlags, aStatus], 8945 false 8946 ); 8947 8948 if (aStateFlags & (STATE_START | STATE_STOP)) { 8949 // reset cached temporary values at beginning and end 8950 this.mMessage = ""; 8951 this.mTotalProgress = 0; 8952 } 8953 this.mStateFlags = aStateFlags; 8954 this.mStatus = aStatus; 8955 } 8956 /* eslint-enable complexity */ 8957 8958 onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { 8959 // OnLocationChange is called for both the top-level content 8960 // and the subframes. 8961 let topLevel = aWebProgress.isTopLevel; 8962 8963 let isSameDocument = !!( 8964 aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT 8965 ); 8966 if (topLevel) { 8967 let isReload = !!( 8968 aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD 8969 ); 8970 let isErrorPage = !!( 8971 aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE 8972 ); 8973 8974 // We need to clear the typed value 8975 // if the document failed to load, to make sure the urlbar reflects the 8976 // failed URI (particularly for SSL errors). However, don't clear the value 8977 // if the error page's URI is about:blank, because that causes complete 8978 // loss of urlbar contents for invalid URI errors (see bug 867957). 8979 // Another reason to clear the userTypedValue is if this was an anchor 8980 // navigation initiated by the user. 8981 // Finally, we do insert the URL if this is a same-document navigation 8982 // and the user cleared the URL manually. 8983 if ( 8984 this.mBrowser.didStartLoadSinceLastUserTyping() || 8985 (isErrorPage && aLocation.spec != "about:blank") || 8986 (isSameDocument && this.mBrowser.isNavigating) || 8987 (isSameDocument && !this.mBrowser.userTypedValue) 8988 ) { 8989 this.mBrowser.userTypedValue = null; 8990 } 8991 8992 // If the tab has been set to "busy" outside the stateChange 8993 // handler below (e.g. by sessionStore.navigateAndRestore), and 8994 // the load results in an error page, it's possible that there 8995 // isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy 8996 // attribute being removed. In this case we should remove the 8997 // attribute here. 8998 if (isErrorPage && this.mTab.hasAttribute("busy")) { 8999 this.mTab.removeAttribute("busy"); 9000 gBrowser._tabAttrModified(this.mTab, ["busy"]); 9001 } 9002 9003 if (!isSameDocument) { 9004 // If the browser was playing audio, we should remove the playing state. 9005 if (this.mTab.hasAttribute("soundplaying")) { 9006 clearTimeout(this.mTab._soundPlayingAttrRemovalTimer); 9007 this.mTab._soundPlayingAttrRemovalTimer = 0; 9008 this.mTab.removeAttribute("soundplaying"); 9009 gBrowser._tabAttrModified(this.mTab, ["soundplaying"]); 9010 } 9011 9012 // If the browser was previously muted, we should restore the muted state. 9013 if (this.mTab.hasAttribute("muted")) { 9014 this.mTab.linkedBrowser.mute(); 9015 } 9016 9017 if (gBrowser.isFindBarInitialized(this.mTab)) { 9018 let findBar = gBrowser.getCachedFindBar(this.mTab); 9019 9020 // Close the Find toolbar if we're in old-style TAF mode 9021 if (findBar.findMode != findBar.FIND_NORMAL) { 9022 findBar.close(); 9023 } 9024 } 9025 9026 // Note that we're not updating for same-document loads, despite 9027 // the `title` argument to `history.pushState/replaceState`. For 9028 // context, see https://bugzilla.mozilla.org/show_bug.cgi?id=585653 9029 // and https://github.com/whatwg/html/issues/2174 9030 if (!isReload) { 9031 gBrowser.setTabTitle(this.mTab); 9032 } 9033 9034 // Don't clear the favicon if this tab is in the pending 9035 // state, as SessionStore will have set the icon for us even 9036 // though we're pointed at an about:blank. Also don't clear it 9037 // if the tab is in customize mode, to keep the one set by 9038 // gCustomizeMode.setTab (bug 1551239). Also don't clear it 9039 // if onLocationChange was triggered by a pushState or a 9040 // replaceState (bug 550565) or a hash change (bug 408415). 9041 if ( 9042 !this.mTab.hasAttribute("pending") && 9043 !this.mTab.hasAttribute("customizemode") && 9044 aWebProgress.isLoadingDocument 9045 ) { 9046 // Removing the tab's image here causes flickering, wait until the 9047 // load is complete. 9048 this.mBrowser.mIconURL = null; 9049 } 9050 9051 if (!isReload && aWebProgress.isLoadingDocument) { 9052 let triggerer = gBrowser._getTriggeringPrincipalFromHistory( 9053 this.mBrowser 9054 ); 9055 // Typing a url, searching or clicking a bookmark will load a new 9056 // document that is no longer tied to a navigation from the previous 9057 // content and will have a system principal as the triggerer. 9058 if (triggerer && triggerer.isSystemPrincipal) { 9059 // Reset the related tab map so that the next tab opened will be related 9060 // to this new document and not to tabs opened by the previous one. 9061 gBrowser.clearRelatedTabs(); 9062 } 9063 } 9064 9065 if ( 9066 aRequest instanceof Ci.nsIChannel && 9067 !isBlankPageURL(aRequest.originalURI.spec) 9068 ) { 9069 this.mBrowser.originalURI = aRequest.originalURI; 9070 } 9071 9072 if (!this._allowTransparentBrowser) { 9073 this.mBrowser.toggleAttribute( 9074 "transparent", 9075 AIWindow.isAIWindowActive(window) && 9076 AIWindow.isAIWindowContentPage(aLocation) 9077 ); 9078 } 9079 } 9080 9081 let userContextId = this.mBrowser.getAttribute("usercontextid") || 0; 9082 if (this.mBrowser.registeredOpenURI) { 9083 let uri = this.mBrowser.registeredOpenURI; 9084 gBrowser.UrlbarProviderOpenTabs.unregisterOpenTab( 9085 uri.spec, 9086 userContextId, 9087 this.mTab.group?.id, 9088 PrivateBrowsingUtils.isWindowPrivate(window) 9089 ); 9090 delete this.mBrowser.registeredOpenURI; 9091 } 9092 if (!isBlankPageURL(aLocation.spec)) { 9093 gBrowser.UrlbarProviderOpenTabs.registerOpenTab( 9094 aLocation.spec, 9095 userContextId, 9096 this.mTab.group?.id, 9097 PrivateBrowsingUtils.isWindowPrivate(window) 9098 ); 9099 this.mBrowser.registeredOpenURI = aLocation; 9100 } 9101 9102 if (this.mTab != gBrowser.selectedTab) { 9103 let tabCacheIndex = gBrowser._tabLayerCache.indexOf(this.mTab); 9104 if (tabCacheIndex != -1) { 9105 gBrowser._tabLayerCache.splice(tabCacheIndex, 1); 9106 gBrowser._getSwitcher().cleanUpTabAfterEviction(this.mTab); 9107 } 9108 } 9109 } 9110 9111 if (!this.mBlank || this.mBrowser.hasContentOpener) { 9112 this._callProgressListeners("onLocationChange", [ 9113 aWebProgress, 9114 aRequest, 9115 aLocation, 9116 aFlags, 9117 ]); 9118 if (topLevel && !isSameDocument) { 9119 // Include the true final argument to indicate that this event is 9120 // simulated (instead of being observed by the webProgressListener). 9121 this._callProgressListeners("onContentBlockingEvent", [ 9122 aWebProgress, 9123 null, 9124 0, 9125 true, 9126 ]); 9127 } 9128 } 9129 9130 if (topLevel) { 9131 this.mBrowser.lastURI = aLocation; 9132 this.mBrowser.lastLocationChange = Date.now(); 9133 } 9134 } 9135 9136 onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { 9137 if (this.mBlank) { 9138 return; 9139 } 9140 9141 this._callProgressListeners("onStatusChange", [ 9142 aWebProgress, 9143 aRequest, 9144 aStatus, 9145 aMessage, 9146 ]); 9147 9148 this.mMessage = aMessage; 9149 } 9150 9151 onSecurityChange(aWebProgress, aRequest, aState) { 9152 this._callProgressListeners("onSecurityChange", [ 9153 aWebProgress, 9154 aRequest, 9155 aState, 9156 ]); 9157 } 9158 9159 onContentBlockingEvent(aWebProgress, aRequest, aEvent) { 9160 this._callProgressListeners("onContentBlockingEvent", [ 9161 aWebProgress, 9162 aRequest, 9163 aEvent, 9164 ]); 9165 } 9166 9167 onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) { 9168 return this._callProgressListeners("onRefreshAttempted", [ 9169 aWebProgress, 9170 aURI, 9171 aDelay, 9172 aSameURI, 9173 ]); 9174 } 9175 } 9176 TabProgressListener.prototype.QueryInterface = ChromeUtils.generateQI([ 9177 "nsIWebProgressListener", 9178 "nsIWebProgressListener2", 9179 "nsISupportsWeakReference", 9180 ]); 9181 9182 let URILoadingWrapper = { 9183 _normalizeLoadURIOptions(browser, loadURIOptions) { 9184 if (!loadURIOptions.triggeringPrincipal) { 9185 throw new Error("Must load with a triggering Principal"); 9186 } 9187 9188 if ( 9189 loadURIOptions.userContextId && 9190 loadURIOptions.userContextId != browser.getAttribute("usercontextid") 9191 ) { 9192 throw new Error("Cannot load with mismatched userContextId"); 9193 } 9194 9195 loadURIOptions.loadFlags |= loadURIOptions.flags | LOAD_FLAGS_NONE; 9196 delete loadURIOptions.flags; 9197 loadURIOptions.hasValidUserGestureActivation ??= 9198 document.hasValidTransientUserGestureActivation; 9199 }, 9200 9201 _loadFlagsToFixupFlags(browser, loadFlags) { 9202 // Attempt to perform URI fixup to see if we can handle this URI in chrome. 9203 let fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_NONE; 9204 if (loadFlags & LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) { 9205 fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; 9206 } 9207 if (loadFlags & LOAD_FLAGS_FIXUP_SCHEME_TYPOS) { 9208 fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS; 9209 } 9210 if (PrivateBrowsingUtils.isBrowserPrivate(browser)) { 9211 fixupFlags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT; 9212 } 9213 return fixupFlags; 9214 }, 9215 9216 _fixupURIString(browser, uriString, loadURIOptions) { 9217 let fixupFlags = this._loadFlagsToFixupFlags( 9218 browser, 9219 loadURIOptions.loadFlags 9220 ); 9221 9222 // XXXgijs: If we switch to loading the URI we return from this method, 9223 // rather than redoing fixup in docshell (see bug 1815509), we need to 9224 // ensure that the loadURIOptions have the fixup flag removed here for 9225 // loads where `uriString` already parses if just passed immediately 9226 // to `newURI`. 9227 // Right now this happens in nsDocShellLoadState code. 9228 try { 9229 let fixupInfo = Services.uriFixup.getFixupURIInfo( 9230 uriString, 9231 fixupFlags 9232 ); 9233 return fixupInfo.preferredURI; 9234 } catch (e) { 9235 // getFixupURIInfo may throw. Just return null, our caller will deal. 9236 } 9237 return null; 9238 }, 9239 9240 /** 9241 * Handles URIs when we want to deal with them in chrome code rather than pass 9242 * them down to a content browser. This can avoid unnecessary process switching 9243 * for the browser. 9244 * 9245 * @param aBrowser the browser that is attempting to load the URI 9246 * @param aUri the nsIURI that is being loaded 9247 * @returns true if the URI is handled, otherwise false 9248 */ 9249 _handleUriInChrome(aBrowser, aUri) { 9250 if (aUri.scheme == "file") { 9251 try { 9252 let mimeType = Cc["@mozilla.org/mime;1"] 9253 .getService(Ci.nsIMIMEService) 9254 .getTypeFromURI(aUri); 9255 if (mimeType == "application/x-xpinstall") { 9256 let systemPrincipal = 9257 Services.scriptSecurityManager.getSystemPrincipal(); 9258 AddonManager.getInstallForURL(aUri.spec, { 9259 telemetryInfo: { source: "file-url" }, 9260 }).then(install => { 9261 AddonManager.installAddonFromWebpage( 9262 mimeType, 9263 aBrowser, 9264 systemPrincipal, 9265 install 9266 ); 9267 }); 9268 return true; 9269 } 9270 } catch (e) { 9271 return false; 9272 } 9273 } 9274 9275 return false; 9276 }, 9277 9278 _updateTriggerMetadataForLoad( 9279 browser, 9280 uriString, 9281 { loadFlags, globalHistoryOptions } 9282 ) { 9283 if (globalHistoryOptions?.triggeringSponsoredURL) { 9284 try { 9285 // Browser may access URL after fixing it up, then store the URL into DB. 9286 // To match with it, fix the link up explicitly. 9287 const triggeringSponsoredURL = Services.uriFixup.getFixupURIInfo( 9288 globalHistoryOptions.triggeringSponsoredURL, 9289 this._loadFlagsToFixupFlags(browser, loadFlags) 9290 ).fixedURI.spec; 9291 browser.setAttribute( 9292 "triggeringSponsoredURL", 9293 triggeringSponsoredURL 9294 ); 9295 const time = 9296 globalHistoryOptions.triggeringSponsoredURLVisitTimeMS || 9297 Date.now(); 9298 browser.setAttribute("triggeringSponsoredURLVisitTimeMS", time); 9299 browser.setAttribute( 9300 "triggeringSource", 9301 globalHistoryOptions.triggeringSource 9302 ); 9303 } catch (e) {} 9304 } 9305 9306 if (globalHistoryOptions?.triggeringSearchEngine) { 9307 browser.setAttribute( 9308 "triggeringSearchEngine", 9309 globalHistoryOptions.triggeringSearchEngine 9310 ); 9311 browser.setAttribute("triggeringSearchEngineURL", uriString); 9312 } else { 9313 browser.removeAttribute("triggeringSearchEngine"); 9314 browser.removeAttribute("triggeringSearchEngineURL"); 9315 } 9316 }, 9317 9318 // Both of these are used to override functions on browser-custom-element. 9319 fixupAndLoadURIString(browser, uriString, loadURIOptions = {}) { 9320 this._internalMaybeFixupLoadURI(browser, uriString, null, loadURIOptions); 9321 }, 9322 loadURI(browser, uri, loadURIOptions = {}) { 9323 this._internalMaybeFixupLoadURI(browser, "", uri, loadURIOptions); 9324 }, 9325 9326 // A shared function used by both remote and non-remote browsers to 9327 // load a string URI or redirect it to the correct process. 9328 _internalMaybeFixupLoadURI(browser, uriString, uri, loadURIOptions) { 9329 this._normalizeLoadURIOptions(browser, loadURIOptions); 9330 // Some callers pass undefined/null when calling 9331 // loadURI/fixupAndLoadURIString. Just load about:blank instead: 9332 if (!uriString && !uri) { 9333 uri = Services.io.newURI("about:blank"); 9334 } 9335 9336 // We need a URI in frontend code for checking various things. Ideally 9337 // we would then also pass that URI to webnav/browsingcontext code 9338 // for loading, but we historically haven't. Changing this would alter 9339 // fixup scenarios in some non-obvious cases. 9340 let startedWithURI = !!uri; 9341 if (!uri) { 9342 // Note: this may return null if we can't make a URI out of the input. 9343 uri = this._fixupURIString(browser, uriString, loadURIOptions); 9344 } 9345 9346 if (uri && this._handleUriInChrome(browser, uri)) { 9347 // If we've handled the URI in chrome, then just return here. 9348 return; 9349 } 9350 9351 this._updateTriggerMetadataForLoad( 9352 browser, 9353 uriString || uri.spec, 9354 loadURIOptions 9355 ); 9356 9357 if (loadURIOptions.isCaptivePortalTab) { 9358 browser.browsingContext.isCaptivePortalTab = true; 9359 } 9360 9361 // XXX(nika): Is `browser.isNavigating` necessary anymore? 9362 // XXX(gijs): Unsure. But it mirrors docShell.isNavigating, but in the parent process 9363 // (and therefore imperfectly so). 9364 browser.isNavigating = true; 9365 9366 try { 9367 // Should more generally prefer loadURI here - see bug 1815509. 9368 if (startedWithURI) { 9369 browser.webNavigation.loadURI(uri, loadURIOptions); 9370 } else { 9371 browser.webNavigation.fixupAndLoadURIString( 9372 uriString, 9373 loadURIOptions 9374 ); 9375 } 9376 } finally { 9377 browser.isNavigating = false; 9378 } 9379 }, 9380 }; 9381 } // end private scope for gBrowser 9382 9383 var StatusPanel = { 9384 // This is useful for debugging (set to `true` in the interesting state for 9385 // the panel to remain in that state). 9386 _frozen: false, 9387 9388 get panel() { 9389 delete this.panel; 9390 this.panel = document.getElementById("statuspanel"); 9391 this.panel.addEventListener( 9392 "transitionend", 9393 this._onTransitionEnd.bind(this) 9394 ); 9395 this.panel.addEventListener( 9396 "transitioncancel", 9397 this._onTransitionEnd.bind(this) 9398 ); 9399 return this.panel; 9400 }, 9401 9402 get isVisible() { 9403 return !this.panel.hasAttribute("inactive"); 9404 }, 9405 9406 update() { 9407 if (BrowserHandler.kiosk || this._frozen) { 9408 return; 9409 } 9410 let text; 9411 let type; 9412 let types = ["overLink"]; 9413 if (XULBrowserWindow.busyUI) { 9414 types.push("status"); 9415 } 9416 types.push("letterboxingStatus"); 9417 types.push("defaultStatus"); 9418 for (type of types) { 9419 if ((text = XULBrowserWindow[type])) { 9420 break; 9421 } 9422 } 9423 9424 // If it's a long data: URI that uses base64 encoding, truncate to 9425 // a reasonable length rather than trying to display the entire thing. 9426 // We can't shorten arbitrary URIs like this, as bidi etc might mean 9427 // we need the trailing characters for display. But a base64-encoded 9428 // data-URI is plain ASCII, so this is OK for status panel display. 9429 // (See bug 1484071.) 9430 let textCropped = false; 9431 if (text.length > 500 && text.match(/^data:[^,]+;base64,/)) { 9432 text = text.substring(0, 500) + "\u2026"; 9433 textCropped = true; 9434 } 9435 9436 if (this._labelElement.value != text || (text && !this.isVisible)) { 9437 this.panel.setAttribute("previoustype", this.panel.getAttribute("type")); 9438 this.panel.setAttribute("type", type); 9439 9440 this._label = text; 9441 this._labelElement.setAttribute( 9442 "crop", 9443 type == "overLink" && !textCropped ? "center" : "end" 9444 ); 9445 } 9446 }, 9447 9448 get _labelElement() { 9449 delete this._labelElement; 9450 return (this._labelElement = document.getElementById("statuspanel-label")); 9451 }, 9452 9453 set _label(val) { 9454 if (!this.isVisible) { 9455 this.panel.removeAttribute("mirror"); 9456 this.panel.removeAttribute("sizelimit"); 9457 } 9458 9459 if ( 9460 this.panel.getAttribute("type") == "status" && 9461 this.panel.getAttribute("previoustype") == "status" 9462 ) { 9463 // Before updating the label, set the panel's current width as its 9464 // min-width to let the panel grow but not shrink and prevent 9465 // unnecessary flicker while loading pages. We only care about the 9466 // panel's width once it has been painted, so we can do this 9467 // without flushing layout. 9468 this.panel.style.minWidth = 9469 window.windowUtils.getBoundsWithoutFlushing(this.panel).width + "px"; 9470 } else { 9471 this.panel.style.minWidth = ""; 9472 } 9473 9474 if (val) { 9475 this._labelElement.value = val; 9476 if (this.panel.hidden) { 9477 this.panel.hidden = false; 9478 // This ensures that the "inactive" attribute removal triggers a 9479 // transition. 9480 getComputedStyle(this.panel).display; 9481 } 9482 this.panel.removeAttribute("inactive"); 9483 MousePosTracker.addListener(this); 9484 } else { 9485 this.panel.setAttribute("inactive", "true"); 9486 MousePosTracker.removeListener(this); 9487 } 9488 }, 9489 9490 _onTransitionEnd() { 9491 if (!this.isVisible) { 9492 this.panel.hidden = true; 9493 } 9494 }, 9495 9496 getMouseTargetRect() { 9497 let container = this.panel.parentNode; 9498 let panelRect = window.windowUtils.getBoundsWithoutFlushing(this.panel); 9499 let containerRect = window.windowUtils.getBoundsWithoutFlushing(container); 9500 9501 return { 9502 top: panelRect.top, 9503 bottom: panelRect.bottom, 9504 left: RTL_UI ? containerRect.right - panelRect.width : containerRect.left, 9505 right: RTL_UI 9506 ? containerRect.right 9507 : containerRect.left + panelRect.width, 9508 }; 9509 }, 9510 9511 onMouseEnter() { 9512 this._mirror(); 9513 }, 9514 9515 onMouseLeave() { 9516 this._mirror(); 9517 }, 9518 9519 _mirror() { 9520 if (this._frozen) { 9521 return; 9522 } 9523 if (this.panel.hasAttribute("mirror")) { 9524 this.panel.removeAttribute("mirror"); 9525 } else { 9526 this.panel.setAttribute("mirror", "true"); 9527 } 9528 9529 if (!this.panel.hasAttribute("sizelimit")) { 9530 this.panel.setAttribute("sizelimit", "true"); 9531 } 9532 }, 9533 }; 9534 9535 var TabBarVisibility = { 9536 _initialUpdateDone: false, 9537 9538 update(force = false) { 9539 let isPopup = !window.toolbar.visible; 9540 let isTaskbarTab = document.documentElement.hasAttribute("taskbartab"); 9541 let isSingleTabWindow = isPopup || isTaskbarTab; 9542 9543 let hasVerticalTabs = 9544 !isSingleTabWindow && 9545 Services.prefs.getBoolPref("sidebar.verticalTabs", false); 9546 9547 // When `gBrowser` has not been initialized, we're opening a new window and 9548 // assume only a single tab is loading. 9549 let hasSingleTab = !gBrowser || gBrowser.visibleTabs.length == 1; 9550 9551 // To prevent tabs being lost, hiding the tabs toolbar should only work 9552 // when only a single tab is visible or tabs are displayed elsewhere. 9553 let hideTabsToolbar = 9554 (isSingleTabWindow && hasSingleTab) || hasVerticalTabs; 9555 9556 // We only want a non-customized titlebar for popups. It should not be the 9557 // case, but if a popup window contains more than one tab we re-enable 9558 // titlebar customization and display tabs. 9559 CustomTitlebar.allowedBy("non-popup", !(isPopup && hasSingleTab)); 9560 9561 // Update the browser chrome. 9562 9563 let tabsToolbar = document.getElementById("TabsToolbar"); 9564 let navbar = document.getElementById("nav-bar"); 9565 9566 gNavToolbox.toggleAttribute("tabs-hidden", hideTabsToolbar); 9567 // Should the nav-bar look and function like a titlebar? 9568 navbar.classList.toggle( 9569 "browser-titlebar", 9570 CustomTitlebar.enabled && hideTabsToolbar 9571 ); 9572 9573 document 9574 .getElementById("browser") 9575 .classList.toggle( 9576 "browser-toolbox-background", 9577 CustomTitlebar.enabled && hasVerticalTabs 9578 ); 9579 9580 if ( 9581 hideTabsToolbar == tabsToolbar.collapsed && 9582 !force && 9583 this._initialUpdateDone 9584 ) { 9585 // No further updates needed, `TabsToolbar` already matches the expected 9586 // visibilty. 9587 return; 9588 } 9589 this._initialUpdateDone = true; 9590 9591 tabsToolbar.collapsed = hideTabsToolbar; 9592 9593 // Stylize close menu items based on tab visibility. When a window will only 9594 // ever have a single tab, only show the option to close the tab, and 9595 // simplify the text since we don't need to disambiguate from closing the window. 9596 document.getElementById("menu_closeWindow").hidden = hideTabsToolbar; 9597 document.l10n.setAttributes( 9598 document.getElementById("menu_close"), 9599 hideTabsToolbar 9600 ? "tabbrowser-menuitem-close" 9601 : "tabbrowser-menuitem-close-tab" 9602 ); 9603 }, 9604 }; 9605 9606 var TabContextMenu = { 9607 contextTab: null, 9608 _updateToggleMuteMenuItems(aTab, aConditionFn) { 9609 ["muted", "soundplaying"].forEach(attr => { 9610 if (!aConditionFn || aConditionFn(attr)) { 9611 if (aTab.hasAttribute(attr)) { 9612 aTab.toggleMuteMenuItem.setAttribute(attr, "true"); 9613 aTab.toggleMultiSelectMuteMenuItem.setAttribute(attr, "true"); 9614 } else { 9615 aTab.toggleMuteMenuItem.removeAttribute(attr); 9616 aTab.toggleMultiSelectMuteMenuItem.removeAttribute(attr); 9617 } 9618 } 9619 }); 9620 }, 9621 // eslint-disable-next-line complexity 9622 updateContextMenu(aPopupMenu) { 9623 let triggerTab = 9624 aPopupMenu.triggerNode && 9625 (aPopupMenu.triggerNode.tab || aPopupMenu.triggerNode.closest("tab")); 9626 this.contextTab = triggerTab || gBrowser.selectedTab; 9627 this.contextTab.addEventListener("TabAttrModified", this); 9628 aPopupMenu.addEventListener("popuphidden", this); 9629 9630 this.multiselected = this.contextTab.multiselected; 9631 this.contextTabs = this.multiselected 9632 ? gBrowser.selectedTabs 9633 : [this.contextTab]; 9634 9635 let splitViews = new Set(); 9636 // bug1973996: This call is not guaranteed to complete 9637 // before the saved groups menu is populated 9638 for (let tab of this.contextTabs) { 9639 gBrowser.TabStateFlusher.flush(tab.linkedBrowser); 9640 9641 // Add unique split views for count info below 9642 if (tab.splitview) { 9643 splitViews.add(tab.splitview); 9644 } 9645 } 9646 9647 let disabled = gBrowser.tabs.length == 1; 9648 let tabCountInfo = JSON.stringify({ 9649 tabCount: this.contextTabs.length, 9650 }); 9651 let splitViewCountInfo = JSON.stringify({ 9652 splitViewCount: splitViews.size, 9653 }); 9654 9655 var menuItems = aPopupMenu.getElementsByAttribute( 9656 "tbattr", 9657 "tabbrowser-multiple" 9658 ); 9659 for (let menuItem of menuItems) { 9660 menuItem.disabled = disabled; 9661 } 9662 9663 disabled = gBrowser.visibleTabs.length == 1; 9664 menuItems = aPopupMenu.getElementsByAttribute( 9665 "tbattr", 9666 "tabbrowser-multiple-visible" 9667 ); 9668 for (let menuItem of menuItems) { 9669 menuItem.disabled = disabled; 9670 } 9671 9672 let contextNewTabButton = document.getElementById("context_openANewTab"); 9673 // update context menu item strings for vertical tabs 9674 document.l10n.setAttributes( 9675 contextNewTabButton, 9676 gBrowser.tabContainer?.verticalMode 9677 ? "tab-context-new-tab-open-vertical" 9678 : "tab-context-new-tab-open" 9679 ); 9680 9681 // Session store 9682 let closedCount = SessionStore.getLastClosedTabCount(window); 9683 document 9684 .getElementById("History:UndoCloseTab") 9685 .toggleAttribute("disabled", closedCount == 0); 9686 document.l10n.setArgs(document.getElementById("context_undoCloseTab"), { 9687 tabCount: closedCount, 9688 }); 9689 9690 // Show/hide fullscreen context menu items and set the 9691 // autohide item's checked state to mirror the autohide pref. 9692 showFullScreenViewContextMenuItems(aPopupMenu); 9693 9694 // #context_moveTabToNewGroup is a simplified context menu item that only 9695 // appears if there are no existing tab groups available to move the tab to. 9696 let contextMoveTabToNewGroup = document.getElementById( 9697 "context_moveTabToNewGroup" 9698 ); 9699 let contextMoveTabToGroup = document.getElementById( 9700 "context_moveTabToGroup" 9701 ); 9702 let contextUngroupTab = document.getElementById("context_ungroupTab"); 9703 let contextMoveSplitViewToNewGroup = document.getElementById( 9704 "context_moveSplitViewToNewGroup" 9705 ); 9706 let contextUngroupSplitView = document.getElementById( 9707 "context_ungroupSplitView" 9708 ); 9709 let isAllSplitViewTabs = this.contextTabs.every( 9710 contextTab => contextTab.splitview 9711 ); 9712 9713 if (gBrowser._tabGroupsEnabled) { 9714 let selectedGroupCount = new Set( 9715 // The filter removes the "null" group for ungrouped tabs. 9716 this.contextTabs.map(t => t.group).filter(g => g) 9717 ).size; 9718 9719 let openGroupsToMoveTo = gBrowser.getAllTabGroups({ 9720 sortByLastSeenActive: true, 9721 }); 9722 9723 // Determine whether or not the "current" tab group should appear in the 9724 // "move tab to group" context menu. 9725 if (selectedGroupCount == 1) { 9726 let groupToFilter = this.contextTabs[0].group; 9727 if (groupToFilter && this.contextTabs.every(t => t.group)) { 9728 openGroupsToMoveTo = openGroupsToMoveTo.filter( 9729 group => group !== groupToFilter 9730 ); 9731 } 9732 } 9733 9734 // Populate the saved groups context menu 9735 // Only enable in non-private windows, or if at least one of the tabs is 9736 // considered saveable 9737 let savedGroupsToMoveTo = []; 9738 if ( 9739 !PrivateBrowsingUtils.isWindowPrivate(window) && 9740 SessionStore.shouldSaveTabsToGroup(this.contextTabs) 9741 ) { 9742 savedGroupsToMoveTo = SessionStore.getSavedTabGroups(); 9743 } 9744 9745 if (!openGroupsToMoveTo.length && !savedGroupsToMoveTo.length) { 9746 if (isAllSplitViewTabs) { 9747 contextMoveTabToGroup.hidden = true; 9748 contextMoveTabToNewGroup.hidden = true; 9749 contextMoveSplitViewToNewGroup.hidden = false; 9750 contextMoveSplitViewToNewGroup.setAttribute( 9751 "data-l10n-args", 9752 splitViewCountInfo 9753 ); 9754 } else { 9755 contextMoveTabToGroup.hidden = true; 9756 contextMoveSplitViewToNewGroup.hidden = true; 9757 contextMoveTabToNewGroup.hidden = false; 9758 contextMoveTabToNewGroup.setAttribute("data-l10n-args", tabCountInfo); 9759 } 9760 } else { 9761 if (isAllSplitViewTabs) { 9762 contextMoveTabToNewGroup.hidden = true; 9763 contextMoveSplitViewToNewGroup.hidden = true; 9764 contextMoveTabToGroup.hidden = false; 9765 contextMoveTabToGroup.setAttribute( 9766 "data-l10n-id", 9767 "tab-context-move-split-view-to-group" 9768 ); 9769 contextMoveTabToGroup.setAttribute( 9770 "data-l10n-args", 9771 splitViewCountInfo 9772 ); 9773 } else { 9774 contextMoveTabToNewGroup.hidden = true; 9775 contextMoveSplitViewToNewGroup.hidden = true; 9776 contextMoveTabToGroup.hidden = false; 9777 contextMoveTabToGroup.setAttribute( 9778 "data-l10n-id", 9779 "tab-context-move-tab-to-group" 9780 ); 9781 contextMoveTabToGroup.setAttribute("data-l10n-args", tabCountInfo); 9782 } 9783 9784 const openGroupsMenu = contextMoveTabToGroup.querySelector("menupopup"); 9785 openGroupsMenu 9786 .querySelectorAll("[tab-group-id]") 9787 .forEach(el => el.remove()); 9788 const upperSeparator = openGroupsMenu.querySelector( 9789 `#open-tab-groups-separator-upper` 9790 ); 9791 const lowerSeparator = openGroupsMenu.querySelector( 9792 `#open-tab-groups-separator-lower` 9793 ); 9794 9795 lowerSeparator.hidden = !openGroupsToMoveTo.length; 9796 9797 openGroupsToMoveTo.toReversed().forEach(group => { 9798 let item = this._createTabGroupMenuItem(group, false); 9799 upperSeparator.after(item); 9800 }); 9801 9802 const savedGroupsMenu = contextMoveTabToGroup.querySelector( 9803 "#context_moveTabToSavedGroup" 9804 ); 9805 const savedGroupsMenuPopup = savedGroupsMenu.querySelector("menupopup"); 9806 9807 savedGroupsMenuPopup 9808 .querySelectorAll("[tab-group-id]") 9809 .forEach(el => el.remove()); 9810 if (savedGroupsToMoveTo.length) { 9811 savedGroupsMenu.disabled = false; 9812 9813 savedGroupsToMoveTo.forEach(group => { 9814 let item = this._createTabGroupMenuItem(group, true); 9815 savedGroupsMenuPopup.appendChild(item); 9816 }); 9817 } else { 9818 savedGroupsMenu.disabled = true; 9819 } 9820 } 9821 9822 let groupInfo = JSON.stringify({ 9823 groupCount: selectedGroupCount, 9824 }); 9825 if (isAllSplitViewTabs) { 9826 contextUngroupSplitView.hidden = !selectedGroupCount; 9827 contextUngroupTab.hidden = true; 9828 contextUngroupSplitView.setAttribute("data-l10n-args", groupInfo); 9829 } else { 9830 contextUngroupTab.hidden = !selectedGroupCount; 9831 contextUngroupSplitView.hidden = true; 9832 contextUngroupTab.setAttribute("data-l10n-args", groupInfo); 9833 } 9834 } else { 9835 contextMoveTabToNewGroup.hidden = true; 9836 contextMoveTabToGroup.hidden = true; 9837 contextUngroupTab.hidden = true; 9838 contextMoveSplitViewToNewGroup.hidden = true; 9839 contextUngroupSplitView.hidden = true; 9840 } 9841 9842 let contextAddNote = document.getElementById("context_addNote"); 9843 let contextUpdateNote = document.getElementById("context_updateNote"); 9844 if (gBrowser._tabNotesEnabled) { 9845 // Tab notes behaviour is disabled if a user has a selection of tabs that 9846 // contains more than one canonical URL. 9847 let multiselectingDiverseUrls = 9848 this.multiselected && 9849 !this.contextTabs.every( 9850 t => t.canonicalUrl === this.contextTabs[0].canonicalUrl 9851 ); 9852 9853 contextAddNote.disabled = 9854 multiselectingDiverseUrls || !this.TabNotes.isEligible(this.contextTab); 9855 contextUpdateNote.disabled = multiselectingDiverseUrls; 9856 9857 this.TabNotes.has(this.contextTab).then(hasNote => { 9858 contextAddNote.hidden = hasNote; 9859 contextUpdateNote.hidden = !hasNote; 9860 }); 9861 } else { 9862 contextAddNote.hidden = true; 9863 contextUpdateNote.hidden = true; 9864 } 9865 9866 // Split View 9867 let splitViewEnabled = Services.prefs.getBoolPref( 9868 "browser.tabs.splitView.enabled", 9869 false 9870 ); 9871 let contextMoveTabToNewSplitView = document.getElementById( 9872 "context_moveTabToSplitView" 9873 ); 9874 let contextSeparateSplitView = document.getElementById( 9875 "context_separateSplitView" 9876 ); 9877 let hasSplitViewTab = this.contextTabs.some(tab => tab.splitview); 9878 contextMoveTabToNewSplitView.hidden = !splitViewEnabled || hasSplitViewTab; 9879 contextSeparateSplitView.hidden = !splitViewEnabled || !hasSplitViewTab; 9880 if (splitViewEnabled) { 9881 contextMoveTabToNewSplitView.removeAttribute("data-l10n-id"); 9882 contextMoveTabToNewSplitView.setAttribute( 9883 "data-l10n-id", 9884 this.contextTabs.length < 2 9885 ? "tab-context-add-split-view" 9886 : "tab-context-open-in-split-view" 9887 ); 9888 9889 let pinnedTabs = this.contextTabs.filter(t => t.pinned); 9890 contextMoveTabToNewSplitView.disabled = 9891 this.contextTabs.length > 2 || pinnedTabs.length; 9892 } 9893 9894 // Only one of Reload_Tab/Reload_Selected_Tabs should be visible. 9895 document.getElementById("context_reloadTab").hidden = this.multiselected; 9896 document.getElementById("context_reloadSelectedTabs").hidden = 9897 !this.multiselected; 9898 let unloadTabItem = document.getElementById("context_unloadTab"); 9899 if (gBrowser._unloadTabInContextMenu) { 9900 // linkedPanel is false if the tab is already unloaded 9901 // Cannot unload about: pages, etc., so skip browsers that are not remote 9902 let unloadableTabs = this.contextTabs.filter( 9903 t => t.linkedPanel && t.linkedBrowser?.isRemoteBrowser 9904 ); 9905 unloadTabItem.hidden = unloadableTabs.length === 0; 9906 unloadTabItem.setAttribute( 9907 "data-l10n-args", 9908 JSON.stringify({ tabCount: unloadableTabs.length }) 9909 ); 9910 } else { 9911 unloadTabItem.hidden = true; 9912 } 9913 9914 // Show Play Tab menu item if the tab has attribute activemedia-blocked 9915 document.getElementById("context_playTab").hidden = !( 9916 this.contextTab.activeMediaBlocked && !this.multiselected 9917 ); 9918 document.getElementById("context_playSelectedTabs").hidden = !( 9919 this.contextTab.activeMediaBlocked && this.multiselected 9920 ); 9921 9922 // Only one of pin/unpin/multiselect-pin/multiselect-unpin should be visible 9923 let contextPinTab = document.getElementById("context_pinTab"); 9924 contextPinTab.hidden = this.contextTab.pinned || this.multiselected; 9925 let contextUnpinTab = document.getElementById("context_unpinTab"); 9926 contextUnpinTab.hidden = !this.contextTab.pinned || this.multiselected; 9927 let contextPinSelectedTabs = document.getElementById( 9928 "context_pinSelectedTabs" 9929 ); 9930 contextPinSelectedTabs.hidden = 9931 this.contextTab.pinned || !this.multiselected; 9932 let contextUnpinSelectedTabs = document.getElementById( 9933 "context_unpinSelectedTabs" 9934 ); 9935 contextUnpinSelectedTabs.hidden = 9936 !this.contextTab.pinned || !this.multiselected; 9937 9938 // Build Ask Chat items 9939 // GenAI is missing. tor-browser#44045. 9940 9941 // Move Tab items 9942 let contextMoveTabOptions = document.getElementById( 9943 "context_moveTabOptions" 9944 ); 9945 // gBrowser.visibleTabs excludes tabs in collapsed groups, 9946 // which we want to include in calculations for Move Tab items 9947 let visibleOrCollapsedTabs = gBrowser.tabs.filter( 9948 t => t.isOpen && !t.hidden 9949 ); 9950 let allTabsSelected = visibleOrCollapsedTabs.every(t => t.multiselected); 9951 contextMoveTabOptions.setAttribute("data-l10n-args", tabCountInfo); 9952 contextMoveTabOptions.disabled = this.contextTab.hidden || allTabsSelected; 9953 let selectedTabs = gBrowser.selectedTabs; 9954 let contextMoveTabToEnd = document.getElementById("context_moveToEnd"); 9955 let allSelectedTabsAdjacent = selectedTabs.every( 9956 (element, index, array) => { 9957 return array.length > index + 1 9958 ? element._tPos + 1 == array[index + 1]._tPos 9959 : true; 9960 } 9961 ); 9962 9963 let lastVisibleTab = visibleOrCollapsedTabs.at(-1); 9964 let lastTabToMove = this.contextTabs.at(-1); 9965 9966 let isLastPinnedTab = false; 9967 if (lastTabToMove.pinned) { 9968 let sibling = gBrowser.tabContainer.findNextTab(lastTabToMove); 9969 isLastPinnedTab = !sibling || !sibling.pinned; 9970 } 9971 contextMoveTabToEnd.disabled = 9972 (lastTabToMove == lastVisibleTab || isLastPinnedTab) && 9973 !lastTabToMove.group && 9974 allSelectedTabsAdjacent; 9975 let contextMoveTabToStart = document.getElementById("context_moveToStart"); 9976 let isFirstTab = 9977 !this.contextTabs[0].group && 9978 (this.contextTabs[0] == visibleOrCollapsedTabs[0] || 9979 this.contextTabs[0] == visibleOrCollapsedTabs[gBrowser.pinnedTabCount]); 9980 contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent; 9981 9982 document.getElementById("context_openTabInWindow").disabled = 9983 this.contextTab.hasAttribute("customizemode"); 9984 9985 // Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible. 9986 document.getElementById("context_duplicateTab").hidden = this.multiselected; 9987 document.getElementById("context_duplicateTabs").hidden = 9988 !this.multiselected; 9989 9990 let closeTabsToTheStartItem = document.getElementById( 9991 "context_closeTabsToTheStart" 9992 ); 9993 9994 // update context menu item strings for vertical tabs 9995 document.l10n.setAttributes( 9996 closeTabsToTheStartItem, 9997 gBrowser.tabContainer?.verticalMode 9998 ? "close-tabs-to-the-start-vertical" 9999 : "close-tabs-to-the-start" 10000 ); 10001 10002 let closeTabsToTheEndItem = document.getElementById( 10003 "context_closeTabsToTheEnd" 10004 ); 10005 10006 // update context menu item strings for vertical tabs 10007 document.l10n.setAttributes( 10008 closeTabsToTheEndItem, 10009 gBrowser.tabContainer?.verticalMode 10010 ? "close-tabs-to-the-end-vertical" 10011 : "close-tabs-to-the-end" 10012 ); 10013 10014 // Disable "Close Tabs to the Left/Right" if there are no tabs 10015 // preceding/following it. 10016 let noTabsToStart = !gBrowser._getTabsToTheStartFrom(this.contextTab) 10017 .length; 10018 closeTabsToTheStartItem.disabled = noTabsToStart; 10019 10020 let noTabsToEnd = !gBrowser._getTabsToTheEndFrom(this.contextTab).length; 10021 closeTabsToTheEndItem.disabled = noTabsToEnd; 10022 10023 // Disable "Close other Tabs" if there are no unpinned tabs. 10024 let unpinnedTabsToClose = this.multiselected 10025 ? gBrowser.openTabs.filter( 10026 t => !t.multiselected && !t.pinned && !t.hidden 10027 ).length 10028 : gBrowser.openTabs.filter( 10029 t => t != this.contextTab && !t.pinned && !t.hidden 10030 ).length; 10031 let closeOtherTabsItem = document.getElementById("context_closeOtherTabs"); 10032 closeOtherTabsItem.disabled = unpinnedTabsToClose < 1; 10033 10034 // Update the close item with how many tabs will close. 10035 document 10036 .getElementById("context_closeTab") 10037 .setAttribute("data-l10n-args", tabCountInfo); 10038 10039 let closeDuplicateEnabled = Services.prefs.getBoolPref( 10040 "browser.tabs.context.close-duplicate.enabled" 10041 ); 10042 let closeDuplicateTabsItem = document.getElementById( 10043 "context_closeDuplicateTabs" 10044 ); 10045 closeDuplicateTabsItem.hidden = !closeDuplicateEnabled; 10046 closeDuplicateTabsItem.disabled = 10047 !closeDuplicateEnabled || 10048 !gBrowser.getDuplicateTabsToClose(this.contextTab).length; 10049 10050 // Disable "Close Multiple Tabs" if all sub menuitems are disabled 10051 document.getElementById("context_closeTabOptions").disabled = 10052 closeTabsToTheStartItem.disabled && 10053 closeTabsToTheEndItem.disabled && 10054 closeOtherTabsItem.disabled; 10055 10056 // Hide "Bookmark Tab…" for multiselection. 10057 // Update its state if visible. 10058 let bookmarkTab = document.getElementById("context_bookmarkTab"); 10059 bookmarkTab.hidden = this.multiselected; 10060 10061 // Show "Bookmark Selected Tabs" in a multiselect context and hide it otherwise. 10062 let bookmarkMultiSelectedTabs = document.getElementById( 10063 "context_bookmarkSelectedTabs" 10064 ); 10065 bookmarkMultiSelectedTabs.hidden = !this.multiselected; 10066 10067 let toggleMute = document.getElementById("context_toggleMuteTab"); 10068 let toggleMultiSelectMute = document.getElementById( 10069 "context_toggleMuteSelectedTabs" 10070 ); 10071 10072 // Only one of mute_unmute_tab/mute_unmute_selected_tabs should be visible 10073 toggleMute.hidden = this.multiselected; 10074 toggleMultiSelectMute.hidden = !this.multiselected; 10075 10076 const isMuted = this.contextTab.hasAttribute("muted"); 10077 document.l10n.setAttributes( 10078 toggleMute, 10079 isMuted ? "tabbrowser-context-unmute-tab" : "tabbrowser-context-mute-tab" 10080 ); 10081 document.l10n.setAttributes( 10082 toggleMultiSelectMute, 10083 isMuted 10084 ? "tabbrowser-context-unmute-selected-tabs" 10085 : "tabbrowser-context-mute-selected-tabs" 10086 ); 10087 10088 this.contextTab.toggleMuteMenuItem = toggleMute; 10089 this.contextTab.toggleMultiSelectMuteMenuItem = toggleMultiSelectMute; 10090 this._updateToggleMuteMenuItems(this.contextTab); 10091 10092 let selectAllTabs = document.getElementById("context_selectAllTabs"); 10093 selectAllTabs.disabled = gBrowser.allTabsSelected(); 10094 10095 gSync.updateTabContextMenu(aPopupMenu, this.contextTab); 10096 10097 let reopenInContainer = document.getElementById( 10098 "context_reopenInContainer" 10099 ); 10100 reopenInContainer.hidden = 10101 !Services.prefs.getBoolPref("privacy.userContext.enabled", false) || 10102 PrivateBrowsingUtils.isWindowPrivate(window); 10103 reopenInContainer.disabled = this.contextTab.hidden; 10104 10105 SharingUtils.updateShareURLMenuItem( 10106 this.contextTab.linkedBrowser, 10107 document.getElementById("context_moveTabOptions") 10108 ); 10109 }, 10110 10111 _createTabGroupMenuItem(group, isSaved) { 10112 let item = document.createXULElement("menuitem"); 10113 item.setAttribute("tab-group-id", group.id); 10114 10115 // Open groups have labels, and saved groups have names 10116 let label = group.label ?? group.name; 10117 if (label) { 10118 item.setAttribute("label", label); 10119 } else { 10120 document.l10n.setAttributes(item, "tab-context-unnamed-group"); 10121 } 10122 10123 item.classList.add("menuitem-iconic", "tab-group-icon"); 10124 if (isSaved) { 10125 item.classList.add("tab-group-icon-closed"); 10126 } 10127 10128 item.style.setProperty( 10129 "--tab-group-color", 10130 `var(--tab-group-color-${group.color})` 10131 ); 10132 item.style.setProperty( 10133 "--tab-group-color-invert", 10134 `var(--tab-group-color-${group.color}-invert)` 10135 ); 10136 item.style.setProperty( 10137 "--tab-group-color-pale", 10138 `var(--tab-group-color-${group.color}-pale)` 10139 ); 10140 10141 return item; 10142 }, 10143 10144 handleEvent(aEvent) { 10145 switch (aEvent.type) { 10146 case "popuphidden": 10147 if (aEvent.target.id == "tabContextMenu") { 10148 this.contextTab.removeEventListener("TabAttrModified", this); 10149 this.contextTab = null; 10150 this.contextTabs = null; 10151 } 10152 break; 10153 case "TabAttrModified": { 10154 let tab = aEvent.target; 10155 this._updateToggleMuteMenuItems(tab, attr => 10156 aEvent.detail.changed.includes(attr) 10157 ); 10158 break; 10159 } 10160 } 10161 }, 10162 10163 createReopenInContainerMenu(event) { 10164 createUserContextMenu(event, { 10165 isContextMenu: true, 10166 excludeUserContextId: this.contextTab.getAttribute("usercontextid"), 10167 }); 10168 }, 10169 duplicateSelectedTabs() { 10170 let newIndex = this.contextTabs.at(-1)._tPos + 1; 10171 for (let tab of this.contextTabs) { 10172 let newTab = SessionStore.duplicateTab(window, tab); 10173 if (tab.group) { 10174 Glean.tabgroup.tabInteractions.duplicate.add(); 10175 } 10176 gBrowser.moveTabTo(newTab, { tabIndex: newIndex++ }); 10177 } 10178 }, 10179 reopenInContainer(event) { 10180 let userContextId = parseInt( 10181 event.target.getAttribute("data-usercontextid") 10182 ); 10183 10184 for (let tab of this.contextTabs) { 10185 if (tab.getAttribute("usercontextid") == userContextId) { 10186 continue; 10187 } 10188 10189 /* Create a triggering principal that is able to load the new tab 10190 For content principals that are about: chrome: or resource: we need system to load them. 10191 Anything other than system principal needs to have the new userContextId. 10192 */ 10193 let triggeringPrincipal; 10194 10195 if (tab.linkedPanel) { 10196 triggeringPrincipal = tab.linkedBrowser.contentPrincipal; 10197 } else { 10198 // For lazy tab browsers, get the original principal 10199 // from SessionStore 10200 let tabState = JSON.parse(SessionStore.getTabState(tab)); 10201 try { 10202 triggeringPrincipal = E10SUtils.deserializePrincipal( 10203 tabState.triggeringPrincipal_base64 10204 ); 10205 } catch (ex) { 10206 continue; 10207 } 10208 } 10209 10210 if (!triggeringPrincipal || triggeringPrincipal.isNullPrincipal) { 10211 // Ensure that we have a null principal if we couldn't 10212 // deserialize it (for lazy tab browsers) ... 10213 // This won't always work however is safe to use. 10214 triggeringPrincipal = 10215 Services.scriptSecurityManager.createNullPrincipal({ userContextId }); 10216 } else if (triggeringPrincipal.isContentPrincipal) { 10217 triggeringPrincipal = Services.scriptSecurityManager.principalWithOA( 10218 triggeringPrincipal, 10219 { 10220 userContextId, 10221 } 10222 ); 10223 } 10224 10225 let newTab = gBrowser.addTab(tab.linkedBrowser.currentURI.spec, { 10226 userContextId, 10227 pinned: tab.pinned, 10228 tabIndex: tab._tPos + 1, 10229 triggeringPrincipal, 10230 }); 10231 10232 Glean.containers.tabAssignedContainer.record({ 10233 from_container_id: tab.getAttribute("usercontextid"), 10234 to_container_id: userContextId, 10235 }); 10236 10237 if (gBrowser.selectedTab == tab) { 10238 gBrowser.selectedTab = newTab; 10239 } 10240 if (tab.muted && !newTab.muted) { 10241 newTab.toggleMuteAudio(tab.muteReason); 10242 } 10243 } 10244 }, 10245 10246 closeContextTabs() { 10247 if (this.contextTab.multiselected) { 10248 gBrowser.removeMultiSelectedTabs( 10249 gBrowser.TabMetrics.userTriggeredContext( 10250 gBrowser.TabMetrics.METRIC_SOURCE.TAB_STRIP 10251 ) 10252 ); 10253 } else { 10254 gBrowser.removeTab(this.contextTab, { 10255 animate: true, 10256 ...gBrowser.TabMetrics.userTriggeredContext( 10257 gBrowser.TabMetrics.METRIC_SOURCE.TAB_STRIP 10258 ), 10259 }); 10260 } 10261 }, 10262 10263 explicitUnloadTabs() { 10264 gBrowser.explicitUnloadTabs(this.contextTabs); 10265 }, 10266 10267 moveTabsToNewGroup() { 10268 let insertBefore = this.contextTab; 10269 if (insertBefore._tPos < gBrowser.pinnedTabCount) { 10270 insertBefore = gBrowser.tabs[gBrowser.pinnedTabCount]; 10271 } else if (this.contextTab.group) { 10272 insertBefore = this.contextTab.group; 10273 } else if (this.contextTab.splitview) { 10274 insertBefore = this.contextTab.splitview; 10275 } 10276 gBrowser.addTabGroup(this.contextTabs, { 10277 insertBefore, 10278 isUserTriggered: true, 10279 telemetryUserCreateSource: "tab_menu", 10280 }); 10281 gBrowser.selectedTab = this.contextTabs[0]; 10282 10283 // When using the tab context menu to create a group from the all tabs 10284 // panel, make sure we close that panel so that it doesn't obscure the tab 10285 // group creation panel. 10286 gTabsPanel.hideAllTabsPanel(); 10287 }, 10288 10289 moveSplitViewToNewGroup() { 10290 let insertBefore = this.contextTab; 10291 if (insertBefore._tPos < gBrowser.pinnedTabCount) { 10292 insertBefore = gBrowser.tabs[gBrowser.pinnedTabCount]; 10293 } else if (this.contextTab.group) { 10294 insertBefore = this.contextTab.group; 10295 } else if (this.contextTab.splitview) { 10296 insertBefore = this.contextTab.splitview; 10297 } 10298 let tabsAndSplitViews = []; 10299 for (const contextTab of this.contextTabs) { 10300 if (contextTab.splitView) { 10301 if (!tabsAndSplitViews.includes(contextTab.splitView)) { 10302 tabsAndSplitViews.push(contextTab.splitView); 10303 } 10304 } else { 10305 tabsAndSplitViews.push(contextTab); 10306 } 10307 } 10308 gBrowser.addTabGroup(tabsAndSplitViews, { 10309 insertBefore, 10310 isUserTriggered: true, 10311 telemetryUserCreateSource: "tab_menu", 10312 }); 10313 gBrowser.selectedTab = this.contextTabs[0]; 10314 10315 // When using the tab context menu to create a group from the all tabs 10316 // panel, make sure we close that panel so that it doesn't obscure the tab 10317 // group creation panel. 10318 gTabsPanel.hideAllTabsPanel(); 10319 }, 10320 10321 /** 10322 * @param {MozTabbrowserTabGroup} group 10323 */ 10324 moveTabsToGroup(group) { 10325 group.addTabs( 10326 this.contextTabs, 10327 gBrowser.TabMetrics.userTriggeredContext( 10328 gBrowser.TabMetrics.METRIC_SOURCE.TAB_MENU 10329 ) 10330 ); 10331 group.ownerGlobal.focus(); 10332 }, 10333 10334 addTabsToSavedGroup(groupId) { 10335 SessionStore.addTabsToSavedGroup( 10336 groupId, 10337 this.contextTabs, 10338 gBrowser.TabMetrics.userTriggeredContext( 10339 gBrowser.TabMetrics.METRIC_SOURCE.TAB_MENU 10340 ) 10341 ); 10342 this.closeContextTabs(); 10343 }, 10344 10345 ungroupTabs() { 10346 for (let i = this.contextTabs.length - 1; i >= 0; i--) { 10347 gBrowser.ungroupTab(this.contextTabs[i]); 10348 } 10349 }, 10350 10351 ungroupSplitViews() { 10352 let splitViews = new Set(); 10353 for (const tab of this.contextTabs) { 10354 if (!splitViews.has(tab.splitview)) { 10355 splitViews.add(tab.splitview); 10356 gBrowser.ungroupSplitView(tab.splitview); 10357 } 10358 } 10359 }, 10360 10361 moveTabsToSplitView() { 10362 let insertBefore = this.contextTabs.includes(gBrowser.selectedTab) 10363 ? gBrowser.selectedTab 10364 : this.contextTabs[0]; 10365 let tabsToAdd = this.contextTabs; 10366 10367 // Ensure selected tab is always first in split view 10368 const selectedTabIndex = tabsToAdd.indexOf(gBrowser.selectedTab); 10369 if (selectedTabIndex > -1 && selectedTabIndex != 0) { 10370 const [removed] = tabsToAdd.splice(selectedTabIndex, 1); 10371 tabsToAdd.unshift(removed); 10372 } 10373 10374 let newTab = null; 10375 if (this.contextTabs.length < 2) { 10376 // Open new tab to split with context tab 10377 newTab = gBrowser.addTrustedTab("about:opentabs"); 10378 tabsToAdd = [this.contextTabs[0], newTab]; 10379 } 10380 10381 gBrowser.addTabSplitView(tabsToAdd, { 10382 insertBefore, 10383 }); 10384 10385 if (newTab) { 10386 gBrowser.selectedTab = newTab; 10387 } 10388 }, 10389 10390 unsplitTabs() { 10391 const splitviews = new Set( 10392 this.contextTabs.map(tab => tab.splitview).filter(Boolean) 10393 ); 10394 splitviews.forEach(splitview => gBrowser.unsplitTabs(splitview)); 10395 }, 10396 10397 addNewBadge() { 10398 let badgeNewMenuItems = document.querySelectorAll( 10399 "#tabContextMenu menuitem.badge-new" 10400 ); 10401 10402 badgeNewMenuItems.forEach(badgedMenuItem => { 10403 badgedMenuItem.setAttribute( 10404 "badge", 10405 gBrowser.tabLocalization.formatValueSync("tab-context-badge-new") 10406 ); 10407 }); 10408 }, 10409 10410 deleteTabNotes() { 10411 for (let tab of this.contextTabs) { 10412 this.TabNotes.delete(tab, { 10413 telemetrySource: this.TabNotes.TELEMETRY_SOURCE.TAB_CONTEXT_MENU, 10414 }); 10415 } 10416 }, 10417 }; 10418 10419 ChromeUtils.defineESModuleGetters(TabContextMenu, { 10420 // GenAI.sys.mjs is missing. tor-browser#44045. 10421 TabNotes: "moz-src:///browser/components/tabnotes/TabNotes.sys.mjs", 10422 });