document.getElementById(this.getAttribute("tabcontainer")); this.tabContainer.childNodes; !tab.hidden && !tab.closing); return this._visibleTabs; ]]> ({ ALL: 0, OTHER: 1, TO_END: 2 }); null Components.classes["@mozilla.org/docshell/urifixup;1"] .getService(Components.interfaces.nsIURIFixup); Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"] .getService(Components.interfaces.mozIPlacesAutoComplete); document.getAnonymousElementByAttribute(this, "anonid", "tabbox"); document.getAnonymousElementByAttribute(this, "anonid", "panelcontainer"); null new WeakMap(); null [] [] new Map() new Map() false new Map(); AppConstants == "macosx"; null false "" 0 0 { if (!aTab.parentNode) { return; } const animations = Array.from(aTab.parentNode.getElementsByTagName("tab")) .map(tab => { const throbber = document.getAnonymousElementByAttribute(tab, "anonid", "tab-throbber"); return throbber ? throbber.getAnimations({ subtree: true }) : []; }) .reduce((a, b) => a.concat(b)) .filter(anim => anim instanceof CSSAnimation && (anim.animationName === "tab-throbber-animation" || anim.animationName === "tab-throbber-animation-rtl") && (anim.playState === "running" || anim.playState === "pending")); // Synchronize with the oldest running animation, if any. const firstStartTime = Math.min( ...animations.map(anim => anim.startTime === null ? Infinity : anim.startTime) ); if (firstStartTime === Infinity) { return; } requestAnimationFrame(() => { for (let animation of animations) { // If |animation| has been cancelled since this rAF callback // was scheduled we don't want to set its startTime since // that would restart it. We check for a cancelled animation // by looking for a null currentTime rather than checking // the playState, since reading the playState of // a CSSAnimation object will flush style. if (animation.currentTime !== null) { animation.startTime = firstStartTime; } } }); }); ]]> 0 && aStatus == NS_ERROR_UNKNOWN_HOST) { // to prevent bug 235825: wait for the request handled // by the automatic keyword resolver return; } // since we (try to) only handle STATE_STOP of the last request, // the count of open requests should now be 0 this.mRequestCount = 0; } if (aStateFlags & nsIWebProgressListener.STATE_START && aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { if (aWebProgress.isTopLevel) { // Need to use originalLocation rather than location because things // like about:home and about:privatebrowsing arrive with nsIRequest // pointing to their resolved jar: or file: URIs. if (!(originalLocation && gInitialPages.includes(originalLocation.spec) && originalLocation != "about:blank" && this.mBrowser.initialPageLoadedFromURLBar != originalLocation.spec && this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) { // Indicating that we started a load will allow the location // bar to be cleared when the load finishes. // In order to not overwrite user-typed content, we avoid it // (see if condition above) in a very specific case: // If the load is of an 'initial' page (e.g. about:privatebrowsing, // about:newtab, etc.), was not explicitly typed in the location // bar by the user, is not about:blank (because about:blank can be // loaded by websites under their principal), and the current // page in the browser is about:blank (indicating it is a newly // created or re-created browser, e.g. because it just switched // remoteness or is a new tab/window). this.mBrowser.urlbarChangeTracker.startedLoad(); } delete this.mBrowser.initialPageLoadedFromURLBar; // If the browser is loading it must not be crashed anymore this.mTab.removeAttribute("crashed"); } if (this._shouldShowProgress(aRequest)) { if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING) && aWebProgress && aWebProgress.isTopLevel) { this.mTab.setAttribute("busy", "true"); this.mTab._notselectedsinceload = !this.mTab.selected; SchedulePressure.startMonitoring(window, { highPressureFn() { // Only switch back to the SVG loading indicator after getting // three consecutive low pressure callbacks. Used to prevent // switching quickly between the SVG and APNG loading indicators. gBrowser.tabContainer._schedulePressureCount = gBrowser.schedulePressureDefaultCount; gBrowser.tabContainer.setAttribute("schedulepressure", "true"); }, lowPressureFn() { if (!gBrowser.tabContainer._schedulePressureCount || --gBrowser.tabContainer._schedulePressureCount <= 0) { gBrowser.tabContainer.removeAttribute("schedulepressure"); } // If tabs are closed while they are loading we need to // stop monitoring schedule pressure. We don't stop monitoring // during high pressure times because we want to eventually // return to the SVG tab loading animations. let continueMonitoring = true; if (!document.querySelector(".tabbrowser-tab[busy]")) { SchedulePressure.stopMonitoring(window); continueMonitoring = false; } return {continueMonitoring}; }, }); this.mTabBrowser.syncThrobberAnimations(this.mTab); } if (this.mTab.selected) { this.mTabBrowser.mIsBusy = true; } } } else if (aStateFlags & nsIWebProgressListener.STATE_STOP && aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { if (this.mTab.hasAttribute("busy")) { this.mTab.removeAttribute("busy"); if (!document.querySelector(".tabbrowser-tab[busy]")) { SchedulePressure.stopMonitoring(window); this.mTabBrowser.tabContainer.removeAttribute("schedulepressure"); } // Only animate the "burst" indicating the page has loaded if // the top-level page is the one that finished loading. if (aWebProgress.isTopLevel && !aWebProgress.isLoadingDocument && Components.isSuccessCode(aStatus) && !this.mTabBrowser.tabAnimationsInProgress && Services.prefs.getBoolPref("toolkit.cosmeticAnimations.enabled")) { if (this.mTab._notselectedsinceload) { this.mTab.setAttribute("notselectedsinceload", "true"); } else { this.mTab.removeAttribute("notselectedsinceload"); } this.mTab.setAttribute("bursting", "true"); } this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]); if (!this.mTab.selected) this.mTab.setAttribute("unread", "true"); } this.mTab.removeAttribute("progress"); if (aWebProgress.isTopLevel) { let isSuccessful = Components.isSuccessCode(aStatus); if (!isSuccessful && !isTabEmpty(this.mTab)) { // Restore the current document's location in case the // request was stopped (possibly from a content script) // before the location changed. this.mBrowser.userTypedValue = null; let inLoadURI = this.mBrowser.inLoadURI; if (this.mTab.selected && gURLBar && !inLoadURI) { URLBarSetURI(); } } else if (isSuccessful) { this.mBrowser.urlbarChangeTracker.finishedLoad(); } // Ignore initial about:blank to prevent flickering. if (!this.mBrowser.mIconURL && !ignoreBlank) { // Don't switch to the default icon on about:home or about:newtab, // since these pages get their favicon set in browser code to // improve perceived performance. let isNewTab = originalLocation && (originalLocation.spec == "about:newtab" || originalLocation.spec == "about:privatebrowsing" || originalLocation.spec == "about:home"); if (!isNewTab) { this.mTabBrowser.useDefaultIcon(this.mTab); } } } // For keyword URIs clear the user typed value since they will be changed into real URIs if (location.scheme == "keyword") this.mBrowser.userTypedValue = null; if (this.mTab.selected) this.mTabBrowser.mIsBusy = false; } if (ignoreBlank) { this._callProgressListeners("onUpdateCurrentBrowser", [aStateFlags, aStatus, "", 0], true, false); } else { this._callProgressListeners("onStateChange", [aWebProgress, aRequest, aStateFlags, aStatus], true, false); } this._callProgressListeners("onStateChange", [aWebProgress, aRequest, aStateFlags, aStatus], false); if (aStateFlags & (nsIWebProgressListener.STATE_START | nsIWebProgressListener.STATE_STOP)) { // reset cached temporary values at beginning and end this.mMessage = ""; this.mTotalProgress = 0; } this.mStateFlags = aStateFlags; this.mStatus = aStatus; }, /* eslint-enable complexity */ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { // OnLocationChange is called for both the top-level content // and the subframes. let topLevel = aWebProgress.isTopLevel; if (topLevel) { let isSameDocument = !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); // We need to clear the typed value // if the document failed to load, to make sure the urlbar reflects the // failed URI (particularly for SSL errors). However, don't clear the value // if the error page's URI is about:blank, because that causes complete // loss of urlbar contents for invalid URI errors (see bug 867957). // Another reason to clear the userTypedValue is if this was an anchor // navigation initiated by the user. if (this.mBrowser.didStartLoadSinceLastUserTyping() || ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) && aLocation.spec != "about:blank") || (isSameDocument && this.mBrowser.inLoadURI)) { this.mBrowser.userTypedValue = null; } // If the tab has been set to "busy" outside the stateChange // handler below (e.g. by sessionStore.navigateAndRestore), and // the load results in an error page, it's possible that there // isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy // attribute being removed. In this case we should remove the // attribute here. if ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) && this.mTab.hasAttribute("busy")) { this.mTab.removeAttribute("busy"); this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]); } // If the browser was playing audio, we should remove the playing state. if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) { clearTimeout(this.mTab._soundPlayingAttrRemovalTimer); this.mTab._soundPlayingAttrRemovalTimer = 0; this.mTab.removeAttribute("soundplaying"); this.mTabBrowser._tabAttrModified(this.mTab, ["soundplaying"]); } // If the browser was previously muted, we should restore the muted state. if (this.mTab.hasAttribute("muted")) { this.mTab.linkedBrowser.mute(); } if (this.mTabBrowser.isFindBarInitialized(this.mTab)) { let findBar = this.mTabBrowser.getFindBar(this.mTab); // Close the Find toolbar if we're in old-style TAF mode if (findBar.findMode != findBar.FIND_NORMAL) { findBar.close(); } } this.mTabBrowser.setTabTitle(this.mTab); // Don't clear the favicon if this tab is in the pending // state, as SessionStore will have set the icon for us even // though we're pointed at an about:blank. Also don't clear it // if onLocationChange was triggered by a pushState or a // replaceState (bug 550565) or a hash change (bug 408415). if (!this.mTab.hasAttribute("pending") && aWebProgress.isLoadingDocument && !isSameDocument) { this.mBrowser.mIconURL = null; } let userContextId = this.mBrowser.getAttribute("usercontextid") || 0; if (this.mBrowser.registeredOpenURI) { this.mTabBrowser._unifiedComplete .unregisterOpenPage(this.mBrowser.registeredOpenURI, userContextId); delete this.mBrowser.registeredOpenURI; } // Tabs in private windows aren't registered as "Open" so // that they don't appear as switch-to-tab candidates. if (!isBlankPageURL(aLocation.spec) && (!PrivateBrowsingUtils.isWindowPrivate(window) || PrivateBrowsingUtils.permanentPrivateBrowsing)) { this.mTabBrowser._unifiedComplete .registerOpenPage(aLocation, userContextId); this.mBrowser.registeredOpenURI = aLocation; } } if (!this.mBlank) { this._callProgressListeners("onLocationChange", [aWebProgress, aRequest, aLocation, aFlags]); } if (topLevel) { this.mBrowser.lastURI = aLocation; this.mBrowser.lastLocationChange = Date.now(); } }, onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { if (this.mBlank) return; this._callProgressListeners("onStatusChange", [aWebProgress, aRequest, aStatus, aMessage]); this.mMessage = aMessage; }, onSecurityChange(aWebProgress, aRequest, aState) { this._callProgressListeners("onSecurityChange", [aWebProgress, aRequest, aState]); }, onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) { return this._callProgressListeners("onRefreshAttempted", [aWebProgress, aURI, aDelay, aSameURI]); }, QueryInterface(aIID) { if (aIID.equals(Components.interfaces.nsIWebProgressListener) || aIID.equals(Components.interfaces.nsIWebProgressListener2) || aIID.equals(Components.interfaces.nsISupportsWeakReference) || aIID.equals(Components.interfaces.nsISupports)) return this; throw Components.results.NS_NOINTERFACE; } }); ]]> Cc["@mozilla.org/network/serialization-helper;1"] .getService(Ci.nsISerializationHelper); null { if (this._tabSwitchID === tabSwitchID) { TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_MS"); this._tabSwitchID = null; } window.removeEventListener("MozAfterPaint", onMozAfterPaint); }; window.addEventListener("MozAfterPaint", onMozAfterPaint); } } var oldTab = this.mCurrentTab; // Preview mode should not reset the owner if (!this._previewMode && !oldTab.selected) oldTab.owner = null; let lastRelatedTab = this._lastRelatedTabMap.get(oldTab); if (lastRelatedTab) { if (!lastRelatedTab.selected) lastRelatedTab.owner = null; } this._lastRelatedTabMap = new WeakMap(); var oldBrowser = this.mCurrentBrowser; if (!gMultiProcessBrowser) { oldBrowser.removeAttribute("primary"); oldBrowser.docShellIsActive = false; newBrowser.setAttribute("primary", "true"); newBrowser.docShellIsActive = (window.windowState != window.STATE_MINIMIZED && !window.isFullyOccluded); } var updateBlockedPopups = false; if ((oldBrowser.blockedPopups && !newBrowser.blockedPopups) || (!oldBrowser.blockedPopups && newBrowser.blockedPopups)) updateBlockedPopups = true; this.mCurrentBrowser = newBrowser; this.mCurrentTab = this.tabContainer.selectedItem; this.showTab(this.mCurrentTab); gURLBar.setAttribute("switchingtabs", "true"); window.addEventListener("MozAfterPaint", function() { gURLBar.removeAttribute("switchingtabs"); }, {once: true}); this._appendStatusPanel(); if (updateBlockedPopups) this.mCurrentBrowser.updateBlockedPopups(); // Update the URL bar. var loc = this.mCurrentBrowser.currentURI; var webProgress = this.mCurrentBrowser.webProgress; var securityUI = this.mCurrentBrowser.securityUI; this._callProgressListeners(null, "onLocationChange", [webProgress, null, loc, 0], true, false); if (securityUI) { // Include the true final argument to indicate that this event is // simulated (instead of being observed by the webProgressListener). this._callProgressListeners(null, "onSecurityChange", [webProgress, null, securityUI.state, true], true, false); } var listener = this._tabListeners.get(this.mCurrentTab); if (listener && listener.mStateFlags) { this._callProgressListeners(null, "onUpdateCurrentBrowser", [listener.mStateFlags, listener.mStatus, listener.mMessage, listener.mTotalProgress], true, false); } if (!this._previewMode) { this.mCurrentTab.updateLastAccessed(); this.mCurrentTab.removeAttribute("unread"); oldTab.updateLastAccessed(); let oldFindBar = oldTab._findBar; if (oldFindBar && oldFindBar.findMode == oldFindBar.FIND_NORMAL && !oldFindBar.hidden) this._lastFindValue = oldFindBar._findField.value; this.updateTitlebar(); this.mCurrentTab.removeAttribute("titlechanged"); this.mCurrentTab.removeAttribute("attention"); // The tab has been selected, it's not unselected anymore. // (1) Call the current tab's finishUnselectedTabHoverTimer() // to save a telemetry record. // (2) Call the current browser's unselectedTabHover() with false // to dispatch an event. this.mCurrentTab.finishUnselectedTabHoverTimer(); this.mCurrentBrowser.unselectedTabHover(false); } // If the new tab is busy, and our current state is not busy, then // we need to fire a start to all progress listeners. const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; if (this.mCurrentTab.hasAttribute("busy") && !this.mIsBusy) { this.mIsBusy = true; this._callProgressListeners(null, "onStateChange", [webProgress, null, nsIWebProgressListener.STATE_START | nsIWebProgressListener.STATE_IS_NETWORK, 0], true, false); } // If the new tab is not busy, and our current state is busy, then // we need to fire a stop to all progress listeners. if (!this.mCurrentTab.hasAttribute("busy") && this.mIsBusy) { this.mIsBusy = false; this._callProgressListeners(null, "onStateChange", [webProgress, null, nsIWebProgressListener.STATE_STOP | nsIWebProgressListener.STATE_IS_NETWORK, 0], true, false); } // TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code // that might rely upon the other changes suppressed. // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window if (!this._previewMode) { // We've selected the new tab, so go ahead and notify listeners. let event = new CustomEvent("TabSelect", { bubbles: true, cancelable: false, detail: { previousTab: oldTab } }); this.mCurrentTab.dispatchEvent(event); this._tabAttrModified(oldTab, ["selected"]); this._tabAttrModified(this.mCurrentTab, ["selected"]); if (oldBrowser != newBrowser && oldBrowser.getInPermitUnload) { oldBrowser.getInPermitUnload(inPermitUnload => { if (!inPermitUnload) { return; } // Since the user is switching away from a tab that has // a beforeunload prompt active, we remove the prompt. // This prevents confusing user flows like the following: // 1. User attempts to close Firefox // 2. User switches tabs (ingoring a beforeunload prompt) // 3. User returns to tab, presses "Leave page" let promptBox = this.getTabModalPromptBox(oldBrowser); let prompts = promptBox.listPrompts(); // There might not be any prompts here if the tab was closed // while in an onbeforeunload prompt, which will have // destroyed aforementioned prompt already, so check there's // something to remove, first: if (prompts.length) { // NB: This code assumes that the beforeunload prompt // is the top-most prompt on the tab. prompts[prompts.length - 1].abortPrompt(); } }); } if (!gMultiProcessBrowser) { this._adjustFocusBeforeTabSwitch(oldTab, this.mCurrentTab); this._adjustFocusAfterTabSwitch(this.mCurrentTab); } } updateUserContextUIIndicator(); gIdentityHandler.updateSharingIndicator(); this.tabContainer._setPositionalAttributes(); // Enable touch events to start a native dragging // session to allow the user to easily drag the selected tab. // This is currently only supported on Windows. oldTab.removeAttribute("touchdownstartsdrag"); this.mCurrentTab.setAttribute("touchdownstartsdrag", "true"); if (!gMultiProcessBrowser) { document.commandDispatcher.unlock(); let event = new CustomEvent("TabSwitchDone", { bubbles: true, cancelable: true }); this.dispatchEvent(event); } if (!aForceUpdate) TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS"); ]]> 500 && title.match(/^data:[^,]+;base64,/)) { title = title.substring(0, 500) + "\u2026"; } else { var characterSet = browser.characterSet; title = Services.textToSubURI.unEscapeNonAsciiURI(characterSet, title); } } catch (ex) { /* Do nothing. */ } } else { // Still no title? Fall back to our untitled string. title = gTabBrowserBundle.GetStringFromName("tabs.emptyTabTitle"); } } return this._setTabLabel(aTab, title, { isContentTitle }); ]]> 1 false/true NO var multiple = aURIs.length > 1; var owner = multiple || aLoadInBackground ? null : this.selectedTab; var firstTabAdded = null; var targetTabIndex = -1; if (aReplace) { let browser; if (aTargetTab) { browser = this.getBrowserForTab(aTargetTab); targetTabIndex = aTargetTab._tPos; } else { browser = this.mCurrentBrowser; targetTabIndex = this.tabContainer.selectedIndex; } let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; if (aAllowThirdPartyFixup) { flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; } try { browser.loadURIWithFlags(aURIs[0], { flags, postData: aPostDatas[0], triggeringPrincipal: aTriggeringPrincipal, }); } catch (e) { // Ignore failure in case a URI is wrong, so we can continue // opening the next ones. } } else { firstTabAdded = this.addTab(aURIs[0], { ownerTab: owner, skipAnimation: multiple, allowThirdPartyFixup: aAllowThirdPartyFixup, postData: aPostDatas[0], userContextId: aUserContextId, triggeringPrincipal: aTriggeringPrincipal, }); if (aNewIndex !== -1) { this.moveTabTo(firstTabAdded, aNewIndex); targetTabIndex = firstTabAdded._tPos; } } let tabNum = targetTabIndex; for (let i = 1; i < aURIs.length; ++i) { let tab = this.addTab(aURIs[i], { skipAnimation: true, allowThirdPartyFixup: aAllowThirdPartyFixup, postData: aPostDatas[i], userContextId: aUserContextId, triggeringPrincipal: aTriggeringPrincipal, }); if (targetTabIndex !== -1) this.moveTabTo(tab, ++tabNum); } if (firstTabAdded && !aLoadInBackground) { this.selectedTab = firstTabAdded; } ]]> null [ "canGoBack", "canGoForward", "goBack", "goForward", "permitUnload", "reload", "reloadWithFlags", "stop", "loadURI", "loadURIWithFlags", "goHome", "homePage", "gotoIndex", "currentURI", "documentURI", "preferences", "imageDocument", "isRemoteBrowser", "messageManager", "getTabBrowser", "finder", "fastFind", "sessionHistory", "contentTitle", "characterSet", "fullZoom", "textZoom", "webProgress", "addProgressListener", "removeProgressListener", "audioPlaybackStarted", "audioPlaybackStopped", "pauseMedia", "stopMedia", "resumeMedia", "mute", "unmute", "blockedPopups", "lastURI", "purgeSessionHistory", "stopScroll", "startScroll", "userTypedValue", "userTypedClear", "mediaBlocked", "didStartLoadSinceLastUserTyping" ] false; break; case "contentTitle": getter = () => SessionStore.getLazyTabValue(aTab, "title"); break; case "currentURI": getter = () => { let url = SessionStore.getLazyTabValue(aTab, "url"); return Services.io.newURI(url); }; break; case "didStartLoadSinceLastUserTyping": getter = () => () => false; break; case "fullZoom": case "textZoom": getter = () => 1; break; case "getTabBrowser": getter = () => () => this; break; case "isRemoteBrowser": getter = () => browser.getAttribute("remote") == "true"; break; case "permitUnload": getter = () => () => ({ permitUnload: true, timedOut: false }); break; case "reload": case "reloadWithFlags": getter = () => params => { // Wait for load handler to be instantiated before // initializing the reload. aTab.addEventListener("SSTabRestoring", () => { browser[name](params); }, { once: true }); gBrowser._insertBrowser(aTab); }; break; case "resumeMedia": getter = () => () => { // No need to insert a browser, so we just call the browser's // method. aTab.addEventListener("SSTabRestoring", () => { browser[name](); }, { once: true }); }; break; case "userTypedValue": case "userTypedClear": case "mediaBlocked": getter = () => SessionStore.getLazyTabValue(aTab, name); break; default: getter = () => { if (AppConstants.NIGHTLY_BUILD) { let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`; console.log(message + new Error().stack); } this._insertBrowser(aTab); return browser[name]; }; setter = value => { if (AppConstants.NIGHTLY_BUILD) { let message = `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`; console.log(message + new Error().stack); } this._insertBrowser(aTab); return browser[name] = value; }; } Object.defineProperty(browser, name, { get: getter, set: setter, configurable: true, enumerable: true }); } ]]> into the DOM if necessary. if (!notificationbox.parentNode) { // NB: this appendChild call causes us to run constructors for the // browser element, which fires off a bunch of notifications. Some // of those notifications can cause code to run that inspects our // state, so it is important that the tab element is fully // initialized by this point. this.mPanelContainer.appendChild(notificationbox); } // wire up a progress listener for the new browser object. let tabListener = this.mTabProgressListener(aTab, browser, uriIsAboutBlank, usingPreloadedContent); const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] .createInstance(Ci.nsIWebProgress); filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); browser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL); this._tabListeners.set(aTab, tabListener); this._tabFilters.set(aTab, filter); browser.droppedLinkHandler = handleDroppedLink; // We start our browsers out as inactive, and then maintain // activeness in the tab switcher. browser.docShellIsActive = false; // When addTab() is called with an URL that is not "about:blank" we // set the "nodefaultsrc" attribute that prevents a frameLoader // from being created as soon as the linked is inserted // into the DOM. We thus have to register the new outerWindowID // for non-remote browsers after we have called browser.loadURI(). if (remoteType == E10SUtils.NOT_REMOTE) { this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser); } var evt = new CustomEvent("TabBrowserInserted", { bubbles: true, detail: { insertedOnTabCreation: aInsertedOnTabCreation } }); aTab.dispatchEvent(evt); ]]> = 0; --i) { if (tabs[i] == aTab || tabs[i].pinned) { break; } tabsToEnd.push(tabs[i]); } return tabsToEnd; ]]> { // Avoid changing the selected browser several times. if (tab.selected) this.selectedTab = aTab; this.removeTab(tab, aParams); }; let tabs = this.getTabsToTheEndFrom(aTab); let tabsWithBeforeUnload = []; for (let i = tabs.length - 1; i >= 0; --i) { let tab = tabs[i]; if (this._hasBeforeUnload(tab)) tabsWithBeforeUnload.push(tab); else removeTab(tab); } tabsWithBeforeUnload.forEach(removeTab); ]]> = 0; --i) { let tab = tabs[i]; if (tab != aTab && !tab.pinned) { if (this._hasBeforeUnload(tab)) tabsWithBeforeUnload.push(tab); else this.removeTab(tab, {animate: true}); } } for (let tab of tabsWithBeforeUnload) { this.removeTab(tab, {animate: true}); } ]]> [] 3 /* don't want lots of concurrent animations */ || aTab.getAttribute("fadein") != "true" /* fade-in transition hasn't been triggered yet */ || window.getComputedStyle(aTab).maxWidth == "0.1px" /* fade-in transition hasn't moved yet */ || !this.animationsEnabled) { // We're not animating, so we can cancel the animation stopwatch. TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab); this._endRemoveTab(aTab); return; } // We're animating, so we can cancel the non-animation stopwatch. TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab); aTab.style.maxWidth = ""; // ensure that fade-out transition happens aTab.removeAttribute("fadein"); aTab.removeAttribute("bursting"); setTimeout(function(tab, tabbrowser) { if (tab.parentNode && window.getComputedStyle(tab).maxWidth == "0.1px") { NS_ASSERT(false, "Giving up waiting for the tab closing animation to finish (bug 608589)"); tabbrowser._endRemoveTab(tab); } }, 3000, aTab, this); ]]> false l != aListener); ]]> this.mTabsProgressListeners.push(aListener); l != aListener); ]]> = tabs.length) { // clamp at right-most tab if out of range. aIndex = tabs.length - 1; } this.selectedTab = tabs[aIndex]; if (aEvent) { aEvent.preventDefault(); aEvent.stopPropagation(); } ]]> return this.mCurrentTab; { if (typeof name == "string" && Number.isInteger(parseInt(name))) { return (name in this.tabs); } return false; }, get: (target, name) => { if (name == "length") { return this.tabs.length; } if (typeof name == "string" && Number.isInteger(parseInt(name))) { if (!(name in this.tabs)) { return undefined; } return this.tabs[name].linkedBrowser; } return target[name]; } }); ]]> 0) this.moveTabTo(this.mCurrentTab, 0); ]]> new Set() null { this.tabbrowser._adjustFocusAfterTabSwitch(showTab); }, {once: true}); } else { this.tabbrowser._adjustFocusAfterTabSwitch(showTab); } this.maybeActivateDocShell(this.requestedTab); } } // This doesn't necessarily exist if we're a new window and haven't switched tabs yet if (this.lastVisibleTab) this.lastVisibleTab._visuallySelected = false; this.visibleTab._visuallySelected = true; } this.lastVisibleTab = this.visibleTab; }, assert(cond) { if (!cond) { dump("Assertion failure\n" + Error().stack); // Don't break a user's browser if an assertion fails. if (AppConstants.DEBUG) { throw new Error("Assertion failure"); } } }, // We've decided to try to load requestedTab. loadRequestedTab() { this.assert(!this.loadTimer); this.assert(!this.minimizedOrFullyOccluded); // loadingTab can be non-null here if we timed out loading the current tab. // In that case we just overwrite it with a different tab; it's had its chance. this.loadingTab = this.requestedTab; this.log("Loading tab " + this.tinfo(this.loadingTab)); this.loadTimer = this.setTimer(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT); this.setTabState(this.requestedTab, this.STATE_LOADING); }, maybeActivateDocShell(tab) { // If we've reached the point where the requested tab has entered // the loaded state, but the DocShell is still not yet active, we // should activate it. let browser = tab.linkedBrowser; let state = this.getTabState(tab); let canCheckDocShellState = !browser.mDestroyed && (browser.docShell || browser.frameLoader.tabParent); if (tab == this.requestedTab && canCheckDocShellState && state == this.STATE_LOADED && !browser.docShellIsActive && !this.minimizedOrFullyOccluded) { browser.docShellIsActive = true; this.logState("Set requested tab docshell to active and preserveLayers to false"); // If we minimized the window before the switcher was activated, // we might have set the preserveLayers flag for the current // browser. Let's clear it. browser.preserveLayers(false); } }, // This function runs before every event. It fixes up the state // to account for closed tabs. preActions() { this.assert(this.tabbrowser._switcher); this.assert(this.tabbrowser._switcher === this); for (let [tab, ] of this.tabState) { if (!tab.linkedBrowser) { this.tabState.delete(tab); this.unwarmTab(tab); } } if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) { this.lastVisibleTab = null; } if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) { this.lastPrimaryTab = null; } if (this.blankTab && !this.blankTab.linkedBrowser) { this.blankTab = null; } if (this.spinnerTab && !this.spinnerTab.linkedBrowser) { this.spinnerHidden(); this.spinnerTab = null; } if (this.loadingTab && !this.loadingTab.linkedBrowser) { this.loadingTab = null; this.clearTimer(this.loadTimer); this.loadTimer = null; } }, // This code runs after we've responded to an event or requested a new // tab. It's expected that we've already updated all the principal // state variables. This function takes care of updating any auxilliary // state. postActions() { // Once we finish loading loadingTab, we null it out. So the state should // always be LOADING. this.assert(!this.loadingTab || this.getTabState(this.loadingTab) == this.STATE_LOADING); // We guarantee that loadingTab is non-null iff loadTimer is non-null. So // the timer is set only when we're loading something. this.assert(!this.loadTimer || this.loadingTab); this.assert(!this.loadingTab || this.loadTimer); // If we're switching to a non-remote tab, there's no need to wait // for it to send layers to the compositor, as this will happen // synchronously. Clearing this here means that in the next step, // we can load the non-remote browser immediately. if (!this.requestedTab.linkedBrowser.isRemoteBrowser) { this.loadingTab = null; if (this.loadTimer) { this.clearTimer(this.loadTimer); this.loadTimer = null; } } // If we're not loading anything, try loading the requested tab. let stateOfRequestedTab = this.getTabState(this.requestedTab); if (!this.loadTimer && !this.minimizedOrFullyOccluded && (stateOfRequestedTab == this.STATE_UNLOADED || stateOfRequestedTab == this.STATE_UNLOADING || this.warmingTabs.has(this.requestedTab))) { this.assert(stateOfRequestedTab != this.STATE_LOADED); this.loadRequestedTab(); } // See how many tabs still have work to do. let numPending = 0; let numWarming = 0; for (let [tab, state] of this.tabState) { // Skip print preview browsers since they shouldn't affect tab switching. if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) { continue; } if (state == this.STATE_LOADED && tab !== this.requestedTab) { numPending++; if (tab !== this.visibleTab) { numWarming++; } } if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) { numPending++; } } this.updateDisplay(); // It's possible for updateDisplay to trigger one of our own event // handlers, which might cause finish() to already have been called. // Check for that before calling finish() again. if (!this.tabbrowser._switcher) { return; } this.maybeFinishTabSwitch(); if (numWarming > this.tabbrowser.tabWarmingMax) { this.logState("Hit tabWarmingMax"); if (this.unloadTimer) { this.clearTimer(this.unloadTimer); } this.unloadNonRequiredTabs(); } if (numPending == 0) { this.finish(); } this.logState("done"); }, // Fires when we're ready to unload unused tabs. onUnloadTimeout() { this.logState("onUnloadTimeout"); this.preActions(); this.unloadTimer = null; this.unloadNonRequiredTabs(); this.postActions(); }, // If there are any non-visible and non-requested tabs in // STATE_LOADED, sets them to STATE_UNLOADING. Also queues // up the unloadTimer to run onUnloadTimeout if there are still // tabs in the process of unloading. unloadNonRequiredTabs() { this.warmingTabs = new WeakSet(); let numPending = 0; // Unload any tabs that can be unloaded. for (let [tab, state] of this.tabState) { if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) { continue; } if (state == this.STATE_LOADED && !this.maybeVisibleTabs.has(tab) && tab !== this.lastVisibleTab && tab !== this.loadingTab && tab !== this.requestedTab) { this.setTabState(tab, this.STATE_UNLOADING); } if (state != this.STATE_UNLOADED && tab !== this.requestedTab) { numPending++; } } if (numPending) { // Keep the timer going since there may be more tabs to unload. this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY); } }, // Fires when an ongoing load has taken too long. onLoadTimeout() { this.logState("onLoadTimeout"); this.preActions(); this.loadTimer = null; this.loadingTab = null; this.postActions(); }, // Fires when the layers become available for a tab. onLayersReady(browser) { let tab = this.tabbrowser.getTabForBrowser(browser); if (!tab) { // We probably got a layer update from a tab that got before // the switcher was created, or for browser that's not being // tracked by the async tab switcher (like the preloaded about:newtab). return; } this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`); this.assert(this.getTabState(tab) == this.STATE_LOADING || this.getTabState(tab) == this.STATE_LOADED); this.setTabState(tab, this.STATE_LOADED); this.unwarmTab(tab); if (this.loadingTab === tab) { this.clearTimer(this.loadTimer); this.loadTimer = null; this.loadingTab = null; } }, // Fires when we paint the screen. Any tab switches we initiated // previously are done, so there's no need to keep the old layers // around. onPaint() { this.maybeVisibleTabs.clear(); }, // Called when we're done clearing the layers for a tab. onLayersCleared(browser) { let tab = this.tabbrowser.getTabForBrowser(browser); if (tab) { this.logState(`onLayersCleared(${tab._tPos})`); this.assert(this.getTabState(tab) == this.STATE_UNLOADING || this.getTabState(tab) == this.STATE_UNLOADED); this.setTabState(tab, this.STATE_UNLOADED); } }, // Called when a tab switches from remote to non-remote. In this case // a MozLayerTreeReady notification that we requested may never fire, // so we need to simulate it. onRemotenessChange(tab) { this.logState(`onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`); if (!tab.linkedBrowser.isRemoteBrowser) { if (this.getTabState(tab) == this.STATE_LOADING) { this.onLayersReady(tab.linkedBrowser); } else if (this.getTabState(tab) == this.STATE_UNLOADING) { this.onLayersCleared(tab.linkedBrowser); } } else if (this.getTabState(tab) == this.STATE_LOADED) { // A tab just changed from non-remote to remote, which means // that it's gone back into the STATE_LOADING state until // it sends up a layer tree. this.setTabState(tab, this.STATE_LOADING); } }, // Called when a tab has been removed, and the browser node is // about to be removed from the DOM. onTabRemoved(tab) { if (this.lastVisibleTab == tab) { // The browser that was being presented to the user is // going to be removed during this tick of the event loop. // This will cause us to show a tab spinner instead. this.preActions(); this.lastVisibleTab = null; this.postActions(); } }, onSizeModeOrOcclusionStateChange() { if (this.minimizedOrFullyOccluded) { for (let [tab, state] of this.tabState) { // Skip print preview browsers since they shouldn't affect tab switching. if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) { continue; } if (state == this.STATE_LOADING || state == this.STATE_LOADED) { this.setTabState(tab, this.STATE_UNLOADING); } } if (this.loadTimer) { this.clearTimer(this.loadTimer); this.loadTimer = null; } this.loadingTab = null; } else { // We're no longer minimized or occluded. This means we might want // to activate the current tab's docShell. this.maybeActivateDocShell(gBrowser.selectedTab); } }, onSwapDocShells(ourBrowser, otherBrowser) { // This event fires before the swap. ourBrowser is from // our window. We save the state of otherBrowser since ourBrowser // needs to take on that state at the end of the swap. let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser; let otherState; if (otherTabbrowser && otherTabbrowser._switcher) { let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser); let otherSwitcher = otherTabbrowser._switcher; otherState = otherSwitcher.getTabState(otherTab); } else { otherState = (otherBrowser.docShellIsActive ? this.STATE_LOADED : this.STATE_UNLOADED); } if (!this.swapMap) { this.swapMap = new WeakMap(); } this.swapMap.set(otherBrowser, { state: otherState, }); }, onEndSwapDocShells(ourBrowser, otherBrowser) { // The swap has happened. We reset the loadingTab in // case it has been swapped. We also set ourBrowser's state // to whatever otherBrowser's state was before the swap. if (this.loadTimer) { // Clearing the load timer means that we will // immediately display a spinner if ourBrowser isn't // ready yet. Typically it will already be ready // though. If it's not, we're probably in a new window, // in which case we have no other tabs to display anyway. this.clearTimer(this.loadTimer); this.loadTimer = null; } this.loadingTab = null; let { state: otherState } = this.swapMap.get(otherBrowser); this.swapMap.delete(otherBrowser); let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser); if (ourTab) { this.setTabStateNoAction(ourTab, otherState); } }, shouldActivateDocShell(browser) { let tab = this.tabbrowser.getTabForBrowser(browser); let state = this.getTabState(tab); return state == this.STATE_LOADING || state == this.STATE_LOADED; }, activateBrowserForPrintPreview(browser) { let tab = this.tabbrowser.getTabForBrowser(browser); let state = this.getTabState(tab); if (state != this.STATE_LOADING && state != this.STATE_LOADED) { this.setTabState(tab, this.STATE_LOADING); this.logState("Activated browser " + this.tinfo(tab) + " for print preview"); } }, canWarmTab(tab) { if (!this.tabbrowser.tabWarmingEnabled) { return false; } if (!tab) { return false; } // If the tab is not yet inserted, closing, not remote, // crashed, already visible, or already requested, warming // up the tab makes no sense. if (this.minimizedOrFullyOccluded || !tab.linkedPanel || tab.closing || !tab.linkedBrowser.isRemoteBrowser || !tab.linkedBrowser.frameLoader.tabParent) { return false; } // Similarly, if the tab is already in STATE_LOADING or // STATE_LOADED somehow, there's no point in trying to // warm it up. let state = this.getTabState(tab); if (state === this.STATE_LOADING || state === this.STATE_LOADED) { return false; } return true; }, unwarmTab(tab) { this.warmingTabs.delete(tab); }, warmupTab(tab) { if (!this.canWarmTab(tab)) { return; } this.logState("warmupTab " + this.tinfo(tab)); this.warmingTabs.add(tab); this.setTabState(tab, this.STATE_LOADING); this.suppressDisplayPortAndQueueUnload(tab, this.tabbrowser.tabWarmingUnloadDelay); }, // Called when the user asks to switch to a given tab. requestTab(tab) { if (tab === this.requestedTab) { return; } if (this.tabbrowser.tabWarmingEnabled) { let warmingState = "disqualified"; if (this.warmingTabs.has(tab)) { let tabState = this.getTabState(tab); if (tabState == this.STATE_LOADING) { warmingState = "stillLoading"; } else if (tabState == this.STATE_LOADED) { warmingState = "loaded"; } } else if (this.canWarmTab(tab)) { warmingState = "notWarmed"; } Services.telemetry .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE") .add(warmingState); } this._requestingTab = true; this.logState("requestTab " + this.tinfo(tab)); this.startTabSwitch(); this.requestedTab = tab; tab.linkedBrowser.setAttribute("primary", "true"); if (this.lastPrimaryTab && this.lastPrimaryTab != tab) { this.lastPrimaryTab.linkedBrowser.removeAttribute("primary"); } this.lastPrimaryTab = tab; this.suppressDisplayPortAndQueueUnload(this.requestedTab, this.UNLOAD_DELAY); this._requestingTab = false; }, suppressDisplayPortAndQueueUnload(tab, unloadTimeout) { let browser = tab.linkedBrowser; let fl = browser.frameLoader; if (fl && fl.tabParent && !this.activeSuppressDisplayport.has(fl.tabParent)) { fl.tabParent.suppressDisplayport(true); this.activeSuppressDisplayport.add(fl.tabParent); } this.preActions(); if (this.unloadTimer) { this.clearTimer(this.unloadTimer); } this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), unloadTimeout); this.postActions(); }, handleEvent(event, delayed = false) { if (this._processing) { this.setTimer(() => this.handleEvent(event, true), 0); return; } if (delayed && this.tabbrowser._switcher != this) { // if we delayed processing this event, we might be out of date, in which // case we drop the delayed events return; } this._processing = true; this.preActions(); if (event.type == "MozLayerTreeReady") { this.onLayersReady(event.originalTarget); } if (event.type == "MozAfterPaint") { this.onPaint(); } else if (event.type == "MozLayerTreeCleared") { this.onLayersCleared(event.originalTarget); } else if (event.type == "TabRemotenessChange") { this.onRemotenessChange(event.target); } else if (event.type == "sizemodechange" || event.type == "occlusionstatechange") { this.onSizeModeOrOcclusionStateChange(); } else if (event.type == "SwapDocShells") { this.onSwapDocShells(event.originalTarget, event.detail); } else if (event.type == "EndSwapDocShells") { this.onEndSwapDocShells(event.originalTarget, event.detail); } this.postActions(); this._processing = false; }, /* * Telemetry and Profiler related helpers for recording tab switch * timing. */ startTabSwitch() { TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", window); TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", window); this.addMarker("AsyncTabSwitch:Start"); this.switchInProgress = true; }, /** * Something has occurred that might mean that we've completed * the tab switch (layers are ready, paints are done, spinners * are hidden). This checks to make sure all conditions are * satisfied, and then records the tab switch as finished. */ maybeFinishTabSwitch() { if (this.switchInProgress && this.requestedTab && (this.getTabState(this.requestedTab) == this.STATE_LOADED || this.requestedTab === this.blankTab)) { // After this point the tab has switched from the content thread's point of view. // The changes will be visible after the next refresh driver tick + composite. let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", window); if (time != -1) { TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", window); this.log("DEBUG: tab switch time = " + time); this.addMarker("AsyncTabSwitch:Finish"); } this.switchInProgress = false; } }, spinnerDisplayed() { this.assert(!this.spinnerTab); let browser = this.requestedTab.linkedBrowser; this.assert(browser.isRemoteBrowser); TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window); // We have a second, similar probe for capturing recordings of // when the spinner is displayed for very long periods. TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window); this.addMarker("AsyncTabSwitch:SpinnerShown"); }, spinnerHidden() { this.assert(this.spinnerTab); this.log("DEBUG: spinner time = " + TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window)); TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window); TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window); this.addMarker("AsyncTabSwitch:SpinnerHidden"); // we do not get a onPaint after displaying the spinner }, addMarker(marker) { if (Services.profiler) { Services.profiler.AddMarker(marker); } }, /* * Debug related logging for switcher. */ _useDumpForLogging: false, _logInit: false, logging() { if (this._useDumpForLogging) return true; if (this._logInit) return this._shouldLog; let result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming", false); this._shouldLog = result; this._logInit = true; return this._shouldLog; }, tinfo(tab) { if (tab) { return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")"; } return "null"; }, log(s) { if (!this.logging()) return; if (this._useDumpForLogging) { dump(s + "\n"); } else { Services.console.logStringMessage(s); } }, logState(prefix) { if (!this.logging()) return; let accum = prefix + " "; for (let i = 0; i < this.tabbrowser.tabs.length; i++) { let tab = this.tabbrowser.tabs[i]; let state = this.getTabState(tab); let isWarming = this.warmingTabs.has(tab); accum += i + ":"; if (tab === this.lastVisibleTab) accum += "V"; if (tab === this.loadingTab) accum += "L"; if (tab === this.requestedTab) accum += "R"; if (tab === this.blankTab) accum += "B"; if (isWarming) accum += "(W)"; if (state == this.STATE_LOADED) accum += "(+)"; if (state == this.STATE_LOADING) accum += "(+?)"; if (state == this.STATE_UNLOADED) accum += "(-)"; if (state == this.STATE_UNLOADING) accum += "(-?)"; accum += " "; } if (this._useDumpForLogging) { dump(accum + "\n"); } else { Services.console.logStringMessage(accum); } }, }; this._switcher = switcher; switcher.init(); return switcher; ]]> { let keyElem = document.getElementById(keyElemId); let shortcut = ShortcutUtils.prettifyShortcut(keyElem); return gTabBrowserBundle.formatStringFromName(stringId, [shortcut], 1); }; var label; if (tab.mOverCloseButton) { label = tab.selected ? stringWithShortcut("tabs.closeSelectedTab.tooltip", "key_close") : gTabBrowserBundle.GetStringFromName("tabs.closeTab.tooltip"); } else if (tab._overPlayingIcon) { let stringID; if (tab.selected) { stringID = tab.linkedBrowser.audioMuted ? "tabs.unmuteAudio.tooltip" : "tabs.muteAudio.tooltip"; label = stringWithShortcut(stringID, "key_toggleMute"); } else { if (tab.hasAttribute("activemedia-blocked")) { stringID = "tabs.unblockAudio.tooltip"; } else { stringID = tab.linkedBrowser.audioMuted ? "tabs.unmuteAudio.background.tooltip" : "tabs.muteAudio.background.tooltip"; } label = gTabBrowserBundle.GetStringFromName(stringID); } } else { label = tab._fullLabel || tab.getAttribute("label"); if (AppConstants.NIGHTLY_BUILD && tab.linkedBrowser && tab.linkedBrowser.isRemoteBrowser && tab.linkedBrowser.frameLoader) { label += " (pid " + tab.linkedBrowser.frameLoader.tabParent.osPid + ")"; } if (tab.userContextId) { label = gTabBrowserBundle.formatStringFromName("tabs.containers.tooltip", [label, ContextualIdentityService.getUserContextLabel(tab.userContextId)], 2); } } event.target.setAttribute("label", label); ]]> n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n; let unwrap = n => n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n; let sib = this.tabContainer; do { sib = unwrap(wrap(sib).nextElementSibling); } while (sib && sib.hidden); const kAttr = "hasadjacentnewtabbutton"; if (sib && sib.id == "new-tab-button") { this.tabContainer.setAttribute(kAttr, "true"); } else { this.tabContainer.removeAttribute(kAttr); } ]]> 50 this.tabMinWidth = newValue, newValue => Math.max(newValue, this._tabMinWidthLimit), ); this.tabMinWidth = this.tabMinWidthPref; ]]> 0 null { tab.removeAttribute("soundplaying-scheduledremoval"); tab.removeAttribute("soundplaying"); this._tabAttrModified(tab, ["soundplaying", "soundplaying-scheduledremoval"]); }, removalDelay); } ]]>