Files
tubestation/browser/components/tabbrowser/content/tab.js
Greg Stoll 52693f9877 Bug 1926973 - make only explicitly unloaded tabs have a grayscale dimmed favicon r=desktop-theme-reviewers,tabbrowser-reviewers,dao,sessionstore-reviewers
This is what UX has decided makes sense, and will be much less disruptive
than dimming out all favicons on session restore. When we're ready the
default value of "browser.tabs.fadeOutExplicitlyUnloadedTabs" will be
true, and the default value of "browser.tabs.fadeOutUnloadedTabs" will
remain false.

Differential Revision: https://phabricator.services.mozilla.com/D241481
2025-03-15 00:30:28 +00:00

707 lines
21 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// This is loaded into chrome windows with the subscript loader. Wrap in
// a block to prevent accidentally leaking globals onto `window`.
{
class MozTabbrowserTab extends MozElements.MozTab {
static markup = `
<stack class="tab-stack" flex="1">
<vbox class="tab-background">
<hbox class="tab-context-line"/>
<hbox class="tab-loading-burst" flex="1"/>
<hbox class="tab-group-line"/>
</vbox>
<hbox class="tab-content" align="center">
<stack class="tab-icon-stack">
<hbox class="tab-throbber"/>
<hbox class="tab-icon-pending"/>
<html:img class="tab-icon-image" role="presentation" decoding="sync" />
<image class="tab-sharing-icon-overlay" role="presentation"/>
<image class="tab-icon-overlay" role="presentation"/>
</stack>
<html:moz-button type="icon ghost" size="small" class="tab-audio-button" tabindex="-1"></html:moz-button>
<vbox class="tab-label-container"
align="start"
pack="center"
flex="1">
<label class="tab-text tab-label" role="presentation"/>
<hbox class="tab-secondary-label">
<label class="tab-icon-sound-label tab-icon-sound-pip-label" data-l10n-id="browser-tab-audio-pip" role="presentation"/>
</hbox>
</vbox>
<image class="tab-close-button close-icon" role="button" data-l10n-id="tabbrowser-close-tabs-button" data-l10n-args='{"tabCount": 1}' keyNav="false"/>
</hbox>
</stack>
`;
constructor() {
super();
this.addEventListener("mouseover", this);
this.addEventListener("mouseout", this);
this.addEventListener("dragstart", this, true);
this.addEventListener("dragstart", this);
this.addEventListener("mousedown", this);
this.addEventListener("mouseup", this);
this.addEventListener("click", this);
this.addEventListener("dblclick", this, true);
this.addEventListener("animationstart", this);
this.addEventListener("animationend", this);
this.addEventListener("focus", this);
this.addEventListener("AriaFocus", this);
this._hover = false;
this._selectedOnFirstMouseDown = false;
/**
* Describes how the tab ended up in this mute state. May be any of:
*
* - undefined: The tabs mute state has never changed.
* - null: The mute state was last changed through the UI.
* - Any string: The ID was changed through an extension API. The string
* must be the ID of the extension which changed it.
*/
this.muteReason = undefined;
this.mOverCloseButton = false;
this.mCorrespondingMenuitem = null;
this.closing = false;
}
static get inheritedAttributes() {
return {
".tab-background":
"selected=visuallyselected,fadein,multiselected,dragover-createGroup",
".tab-group-line": "selected=visuallyselected,multiselected",
".tab-loading-burst": "pinned,bursting,notselectedsinceload",
".tab-content":
"pinned,selected=visuallyselected,multiselected,titlechanged,attention",
".tab-icon-stack":
"sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked",
".tab-throbber":
"fadein,pinned,busy,progress,selected=visuallyselected",
".tab-icon-pending":
"fadein,pinned,busy,progress,selected=visuallyselected,pendingicon",
".tab-icon-image":
"src=image,triggeringprincipal=iconloadingprincipal,requestcontextid,fadein,pinned,selected=visuallyselected,busy,crashed,sharing,pictureinpicture,pending,discarded",
".tab-sharing-icon-overlay": "sharing,selected=visuallyselected,pinned",
".tab-icon-overlay":
"sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked",
".tab-audio-button":
"crashed,soundplaying,soundplaying-scheduledremoval,pinned,muted,activemedia-blocked",
".tab-label-container":
"pinned,selected=visuallyselected,labeldirection",
".tab-label":
"text=label,accesskey,fadein,pinned,selected=visuallyselected,attention",
".tab-label-container .tab-secondary-label":
"pinned,blocked,selected=visuallyselected,pictureinpicture",
".tab-close-button": "fadein,pinned,selected=visuallyselected",
};
}
connectedCallback() {
this.initialize();
}
initialize() {
if (this._initialized) {
return;
}
this.textContent = "";
this.appendChild(this.constructor.fragment);
this.initializeAttributeInheritance();
this.setAttribute("context", "tabContextMenu");
this._initialized = true;
if (!("_lastAccessed" in this)) {
this.updateLastAccessed();
}
let labelContainer = this.querySelector(".tab-label-container");
labelContainer.addEventListener("overflow", this);
labelContainer.addEventListener("underflow", this);
}
#elementIndex;
get elementIndex() {
// Make sure the index is up to date.
this.container.ariaFocusableItems;
return this.#elementIndex;
}
set elementIndex(index) {
this.#elementIndex = index;
}
#owner;
get owner() {
let owner = this.#owner?.deref();
if (owner && !owner.closing) {
return owner;
}
return null;
}
set owner(owner) {
this.#owner = owner ? new WeakRef(owner) : null;
}
get container() {
return gBrowser.tabContainer;
}
set attention(val) {
if (val == this.hasAttribute("attention")) {
return;
}
this.toggleAttribute("attention", val);
gBrowser._tabAttrModified(this, ["attention"]);
}
set _visuallySelected(val) {
if (val == this.hasAttribute("visuallyselected")) {
return;
}
this.toggleAttribute("visuallyselected", val);
gBrowser._tabAttrModified(this, ["visuallyselected"]);
}
set _selected(val) {
// in e10s we want to only pseudo-select a tab before its rendering is done, so that
// the rest of the system knows that the tab is selected, but we don't want to update its
// visual status to selected until after we receive confirmation that its content has painted.
if (val) {
this.setAttribute("selected", "true");
} else {
this.removeAttribute("selected");
}
// If we're non-e10s we need to update the visual selection at the same
// time, otherwise AsyncTabSwitcher will take care of this.
if (!gMultiProcessBrowser) {
this._visuallySelected = val;
}
}
get pinned() {
return this.hasAttribute("pinned");
}
get isOpen() {
return (
this.isConnected && !this.closing && this != FirefoxViewHandler.tab
);
}
get visible() {
return this.isOpen && !this.hidden && !this.group?.collapsed;
}
get hidden() {
// This getter makes `hidden` read-only
return super.hidden;
}
get muted() {
return this.hasAttribute("muted");
}
get multiselected() {
return this.hasAttribute("multiselected");
}
get userContextId() {
return this.hasAttribute("usercontextid")
? parseInt(this.getAttribute("usercontextid"))
: 0;
}
get soundPlaying() {
return this.hasAttribute("soundplaying");
}
get pictureinpicture() {
return this.hasAttribute("pictureinpicture");
}
get activeMediaBlocked() {
return this.hasAttribute("activemedia-blocked");
}
get undiscardable() {
return this.hasAttribute("undiscardable");
}
set undiscardable(val) {
if (val == this.hasAttribute("undiscardable")) {
return;
}
this.toggleAttribute("undiscardable", val);
gBrowser._tabAttrModified(this, ["undiscardable"]);
}
get isEmpty() {
// Determines if a tab is "empty", usually used in the context of determining
// if it's ok to close the tab.
if (this.hasAttribute("busy")) {
return false;
}
if (this.hasAttribute("customizemode")) {
return false;
}
let browser = this.linkedBrowser;
if (!isBlankPageURL(browser.currentURI.spec)) {
return false;
}
if (!BrowserUIUtils.checkEmptyPageOrigin(browser)) {
return false;
}
if (browser.canGoForward || browser.canGoBack) {
return false;
}
return true;
}
get lastAccessed() {
return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
}
/**
* Returns a timestamp which attempts to represent the last time the user saw this tab.
* If the tab has not been active in this session, any lastAccessed is used. We
* differentiate between selected and explicitly visible; a selected tab in a hidden
* window is last seen when that window and tab were last visible.
* We use the application start time as a fallback value when no other suitable value
* is available.
*/
get lastSeenActive() {
const isForegroundWindow =
this.ownerGlobal ==
BrowserWindowTracker.getTopWindow({ allowPopups: true });
// the timestamp for the selected tab in the active window is always now
if (isForegroundWindow && this.selected) {
return Date.now();
}
if (this._lastSeenActive) {
return this._lastSeenActive;
}
if (
!this._lastAccessed ||
this._lastAccessed >= this.container.startupTime
) {
// When the tab was created this session but hasn't been seen by the user,
// default to the application start time.
return this.container.startupTime;
}
// The tab was restored from a previous session but never seen.
// Use the lastAccessed as the best proxy for when the user might have seen it.
return this._lastAccessed;
}
get _overPlayingIcon() {
return this.overlayIcon?.matches(":hover");
}
get _overAudioButton() {
return this.audioButton?.matches(":hover");
}
get overlayIcon() {
return this.querySelector(".tab-icon-overlay");
}
get audioButton() {
return this.querySelector(".tab-audio-button");
}
get throbber() {
return this.querySelector(".tab-throbber");
}
get iconImage() {
return this.querySelector(".tab-icon-image");
}
get sharingIcon() {
return this.querySelector(".tab-sharing-icon-overlay");
}
get textLabel() {
return this.querySelector(".tab-label");
}
get closeButton() {
return this.querySelector(".tab-close-button");
}
get group() {
if (this.parentElement?.tagName == "tab-group") {
return this.parentElement;
}
return null;
}
updateLastAccessed(aDate) {
this._lastAccessed = this.selected ? Infinity : aDate || Date.now();
}
updateLastSeenActive() {
this._lastSeenActive = Date.now();
}
updateLastUnloadedByTabUnloader() {
this._lastUnloaded = Date.now();
Glean.browserEngagement.tabUnloadCount.add(1);
}
recordTimeFromUnloadToReload() {
if (!this._lastUnloaded) {
return;
}
const diff_in_msec = Date.now() - this._lastUnloaded;
Services.telemetry
.getHistogramById("TAB_UNLOAD_TO_RELOAD")
.add(diff_in_msec / 1000);
Glean.browserEngagement.tabReloadCount.add(1);
delete this._lastUnloaded;
}
on_mouseover(event) {
if (event.target.classList.contains("tab-close-button")) {
this.mOverCloseButton = true;
}
if (!this.visible) {
return;
}
let tabToWarm = this.mOverCloseButton
? gBrowser._findTabToBlurTo(this)
: this;
gBrowser.warmupTab(tabToWarm);
// If the previous target wasn't part of this tab then this is a mouseenter event.
if (!this.contains(event.relatedTarget)) {
this._mouseenter();
}
}
on_mouseout(event) {
if (event.target.classList.contains("tab-close-button")) {
this.mOverCloseButton = false;
}
// If the new target is not part of this tab then this is a mouseleave event.
if (!this.contains(event.relatedTarget)) {
this._mouseleave();
}
}
on_dragstart(event) {
// We use "failed" drag end events that weren't cancelled by the user
// to detach tabs. Ensure that we do not show the drag image returning
// to its point of origin when this happens, as it makes the drag
// finishing feel very slow.
event.dataTransfer.mozShowFailAnimation = false;
if (event.eventPhase == Event.CAPTURING_PHASE) {
this.style.MozUserFocus = "";
} else if (
this.mOverCloseButton ||
gSharedTabWarning.willShowSharedTabWarning(this)
) {
event.stopPropagation();
}
}
on_mousedown(event) {
let eventMaySelectTab = true;
let tabContainer = this.container;
if (
tabContainer._closeTabByDblclick &&
event.button == 0 &&
event.detail == 1
) {
this._selectedOnFirstMouseDown = this.selected;
}
if (this.selected) {
this.style.MozUserFocus = "ignore";
} else if (
event.target.classList.contains("tab-close-button") ||
event.target.classList.contains("tab-icon-overlay") ||
event.target.classList.contains("tab-audio-button")
) {
eventMaySelectTab = false;
}
if (event.button == 1) {
gBrowser.warmupTab(gBrowser._findTabToBlurTo(this));
}
if (event.button == 0) {
let shiftKey = event.shiftKey;
let accelKey = event.getModifierState("Accel");
if (shiftKey) {
eventMaySelectTab = false;
const lastSelectedTab = gBrowser.lastMultiSelectedTab;
if (!accelKey) {
gBrowser.selectedTab = lastSelectedTab;
// Make sure selection is cleared when tab-switch doesn't happen.
gBrowser.clearMultiSelectedTabs();
}
gBrowser.addRangeToMultiSelectedTabs(lastSelectedTab, this);
} else if (accelKey) {
// Ctrl (Cmd for mac) key is pressed
eventMaySelectTab = false;
if (this.multiselected) {
gBrowser.removeFromMultiSelectedTabs(this);
} else if (this != gBrowser.selectedTab) {
gBrowser.addToMultiSelectedTabs(this);
gBrowser.lastMultiSelectedTab = this;
}
} else if (!this.selected && this.multiselected) {
gBrowser.lockClearMultiSelectionOnce();
}
}
if (gSharedTabWarning.willShowSharedTabWarning(this)) {
eventMaySelectTab = false;
}
if (eventMaySelectTab) {
super.on_mousedown(event);
}
}
on_mouseup() {
// Make sure that clear-selection is released.
// Otherwise selection using Shift key may be broken.
gBrowser.unlockClearMultiSelection();
this.style.MozUserFocus = "";
}
on_click(event) {
if (event.button != 0) {
return;
}
if (event.getModifierState("Accel") || event.shiftKey) {
return;
}
if (
gBrowser.multiSelectedTabsCount > 0 &&
!event.target.classList.contains("tab-close-button") &&
!event.target.classList.contains("tab-icon-overlay") &&
!event.target.classList.contains("tab-audio-button")
) {
// Tabs were previously multi-selected and user clicks on a tab
// without holding Ctrl/Cmd Key
gBrowser.clearMultiSelectedTabs();
}
if (
event.target.classList.contains("tab-icon-overlay") ||
event.target.classList.contains("tab-audio-button")
) {
if (this.activeMediaBlocked) {
if (this.multiselected) {
gBrowser.resumeDelayedMediaOnMultiSelectedTabs(this);
} else {
this.resumeDelayedMedia();
}
} else if (this.soundPlaying || this.muted) {
if (this.multiselected) {
gBrowser.toggleMuteAudioOnMultiSelectedTabs(this);
} else {
this.toggleMuteAudio();
}
}
return;
}
if (event.target.classList.contains("tab-close-button")) {
if (this.multiselected) {
gBrowser.removeMultiSelectedTabs();
} else {
gBrowser.removeTab(this, {
animate: true,
triggeringEvent: event,
});
}
// This enables double-click protection for the tab container
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
}
on_dblclick(event) {
if (event.button != 0) {
return;
}
// for the one-close-button case
if (event.target.classList.contains("tab-close-button")) {
event.stopPropagation();
}
let tabContainer = this.container;
if (
tabContainer._closeTabByDblclick &&
this._selectedOnFirstMouseDown &&
this.selected &&
!event.target.classList.contains("tab-icon-overlay")
) {
gBrowser.removeTab(this, {
animate: true,
triggeringEvent: event,
});
}
}
on_animationstart(event) {
if (!event.animationName.startsWith("tab-throbber-animation")) {
return;
}
// The animation is on a pseudo-element so we need to use `subtree: true`
// to get our hands on it.
for (let animation of event.target.getAnimations({ subtree: true })) {
if (animation.animationName === event.animationName) {
// Ensure all tab throbber animations are synchronized by sharing an
// start time.
animation.startTime = 0;
}
}
}
on_animationend(event) {
if (event.target.classList.contains("tab-loading-burst")) {
this.removeAttribute("bursting");
}
}
_mouseenter() {
this._hover = true;
if (this.selected) {
this.container._handleTabSelect();
} else if (this.linkedPanel) {
this.linkedBrowser.unselectedTabHover(true);
}
// Prepare connection to host beforehand.
SessionStore.speculativeConnectOnTabHover(this);
this.dispatchEvent(new CustomEvent("TabHoverStart", { bubbles: true }));
}
_mouseleave() {
if (!this._hover) {
return;
}
this._hover = false;
if (this.linkedPanel && !this.selected) {
this.linkedBrowser.unselectedTabHover(false);
}
this.dispatchEvent(new CustomEvent("TabHoverEnd", { bubbles: true }));
}
resumeDelayedMedia() {
if (this.activeMediaBlocked) {
this.removeAttribute("activemedia-blocked");
this.linkedBrowser.resumeMedia();
gBrowser._tabAttrModified(this, ["activemedia-blocked"]);
}
}
toggleMuteAudio(aMuteReason) {
let browser = this.linkedBrowser;
if (browser.audioMuted) {
if (this.linkedPanel) {
// "Lazy Browser" should not invoke its unmute method
browser.unmute();
}
this.removeAttribute("muted");
} else {
if (this.linkedPanel) {
// "Lazy Browser" should not invoke its mute method
browser.mute();
}
this.toggleAttribute("muted", true);
}
this.muteReason = aMuteReason || null;
gBrowser._tabAttrModified(this, ["muted"]);
}
setUserContextId(aUserContextId) {
if (aUserContextId) {
if (this.linkedBrowser) {
this.linkedBrowser.setAttribute("usercontextid", aUserContextId);
}
this.setAttribute("usercontextid", aUserContextId);
} else {
if (this.linkedBrowser) {
this.linkedBrowser.removeAttribute("usercontextid");
}
this.removeAttribute("usercontextid");
}
ContextualIdentityService.setTabStyle(this);
}
updateA11yDescription() {
let prevDescTab = gBrowser.tabContainer.querySelector(
"tab[aria-describedby]"
);
if (prevDescTab) {
// We can only have a description for the focused tab.
prevDescTab.removeAttribute("aria-describedby");
}
let desc = document.getElementById("tabbrowser-tab-a11y-desc");
desc.textContent = gBrowser.getTabTooltip(this, false);
this.setAttribute("aria-describedby", "tabbrowser-tab-a11y-desc");
}
on_focus() {
this.updateA11yDescription();
}
on_AriaFocus() {
this.updateA11yDescription();
}
on_overflow(event) {
event.currentTarget.toggleAttribute("textoverflow", true);
}
on_underflow(event) {
event.currentTarget.removeAttribute("textoverflow");
}
}
customElements.define("tabbrowser-tab", MozTabbrowserTab, {
extends: "tab",
});
}