Files
tubestation/browser/base/content/tabbrowser-tab.js
Mihai Alexandru Michis e10f79168a Backed out 12 changesets (bug 1685801) for causing bc failures in browser_ctrlTab.js
CLOSED TREE

Backed out changeset 021924b62f13 (bug 1685801)
Backed out changeset 38cc10101c1f (bug 1685801)
Backed out changeset 9ab9574ac72a (bug 1685801)
Backed out changeset 1a7f259cc2ec (bug 1685801)
Backed out changeset b267b19a7f6e (bug 1685801)
Backed out changeset 7dfcf0257487 (bug 1685801)
Backed out changeset ee0d0169b079 (bug 1685801)
Backed out changeset 0c358ee51951 (bug 1685801)
Backed out changeset 338ab91af557 (bug 1685801)
Backed out changeset a49415007aaf (bug 1685801)
Backed out changeset b91098299143 (bug 1685801)
Backed out changeset edf6209861a8 (bug 1685801)
2021-01-28 22:55:11 +02:00

674 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 get markup() {
return `
<stack class="tab-stack" flex="1">
<vbox class="tab-background">
<hbox class="tab-line"/>
<spacer flex="1" class="tab-background-inner"/>
<hbox class="tab-bottom-line"/>
</vbox>
<hbox class="tab-loading-burst"/>
<hbox class="tab-content" align="center">
<hbox class="tab-throbber" layer="true"/>
<hbox class="tab-icon-pending"/>
<image class="tab-icon-image" validate="never" role="presentation"/>
<image class="tab-sharing-icon-overlay" role="presentation"/>
<image class="tab-icon-overlay" role="presentation"/>
<hbox class="tab-label-container"
onoverflow="this.setAttribute('textoverflow', 'true');"
onunderflow="this.removeAttribute('textoverflow');"
flex="1">
<label class="tab-text tab-label" role="presentation"/>
</hbox>
<image class="tab-icon-sound" role="presentation"/>
<vbox class="tab-label-container proton"
onoverflow="this.setAttribute('textoverflow', 'true');"
onunderflow="this.removeAttribute('textoverflow');"
align="start"
flex="1">
<label class="tab-text tab-label" role="presentation"/>
<hbox class="tab-icon-sound">
<image class="tab-icon-sound-image" role="presentation"/>
<label class="tab-icon-sound-playing-label" data-l10n-id="browser-tab-audio-playing" role="presentation"/>
<label class="tab-icon-sound-muted-label" data-l10n-id="browser-tab-audio-muted" role="presentation"/>
</hbox>
</vbox>
<image class="tab-close-button close-icon" role="presentation"/>
</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("animationend", this);
this.addEventListener("focus", this);
this.addEventListener("AriaFocus", this);
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",
".tab-line":
"selected=visuallyselected,multiselected,before-multiselected",
".tab-loading-burst": "pinned,bursting,notselectedsinceload",
".tab-content":
"pinned,selected=visuallyselected,titlechanged,attention",
".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",
".tab-sharing-icon-overlay": "sharing,selected=visuallyselected,pinned",
".tab-icon-overlay":
"pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked",
".tab-label-container":
"pinned,selected=visuallyselected,labeldirection",
".tab-label-container.proton":
"pinned,selected=visuallyselected,labeldirection",
".tab-label":
"text=label,accesskey,fadein,pinned,selected=visuallyselected,attention",
".tab-label-container.proton .tab-label":
"text=label,accesskey,fadein,pinned,selected=visuallyselected,attention",
".tab-icon-sound":
"soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,pictureinpicture",
".tab-label-container.proton .tab-icon-sound":
"soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,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();
}
}
get container() {
return gBrowser.tabContainer;
}
set _visuallySelected(val) {
if (val == (this.getAttribute("visuallyselected") == "true")) {
return;
}
if (val) {
this.setAttribute("visuallyselected", "true");
} else {
this.removeAttribute("visuallyselected");
}
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 should update the visual selection as well at the same time,
// *or* if we're e10s and the visually selected tab isn't changing, in which case the
// tab switcher code won't run and update anything else (like the before- and after-
// selected attributes).
if (
!gMultiProcessBrowser ||
(val && this.hasAttribute("visuallyselected"))
) {
this._visuallySelected = val;
}
}
get pinned() {
return this.getAttribute("pinned") == "true";
}
get hidden() {
return this.getAttribute("hidden") == "true";
}
get muted() {
return this.getAttribute("muted") == "true";
}
get multiselected() {
return this.getAttribute("multiselected") == "true";
}
get beforeMultiselected() {
return this.getAttribute("before-multiselected") == "true";
}
get userContextId() {
return this.hasAttribute("usercontextid")
? parseInt(this.getAttribute("usercontextid"))
: 0;
}
get soundPlaying() {
return this.getAttribute("soundplaying") == "true";
}
get pictureinpicture() {
return this.getAttribute("pictureinpicture") == "true";
}
get activeMediaBlocked() {
return this.getAttribute("activemedia-blocked") == "true";
}
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 (!BrowserUtils.checkEmptyPageOrigin(browser)) {
return false;
}
if (browser.canGoForward || browser.canGoBack) {
return false;
}
return true;
}
get lastAccessed() {
return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
}
get _overPlayingIcon() {
let iconVisible =
this.soundPlaying || this.muted || this.activeMediaBlocked;
let soundPlayingIcon = this.soundPlayingIcon;
let overlayIcon = this.overlayIcon;
return (
(soundPlayingIcon && soundPlayingIcon.matches(":hover")) ||
(overlayIcon && overlayIcon.matches(":hover") && iconVisible)
);
}
get soundPlayingIcon() {
return gProtonTabs
? this.querySelector(".tab-label-container.proton > .tab-icon-sound")
: this.querySelector(".tab-icon-sound");
}
get overlayIcon() {
return this.querySelector(".tab-icon-overlay");
}
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");
}
_isEventForTabSoundIcon(event) {
return gProtonTabs
? event.target.closest(".tab-icon-sound")
: event.target.classList.contains("tab-icon-sound");
}
updateLastAccessed(aDate) {
this._lastAccessed = this.selected ? Infinity : aDate || Date.now();
}
on_mouseover(event) {
if (event.target.classList.contains("tab-close-button")) {
this.mOverCloseButton = true;
}
this._mouseenter();
}
on_mouseout(event) {
if (event.target.classList.contains("tab-close-button")) {
this.mOverCloseButton = false;
}
this._mouseleave();
}
on_dragstart(event) {
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") ||
this._isEventForTabSoundIcon(event) ||
event.target.classList.contains("tab-icon-overlay")
) {
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(event) {
// 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") &&
!this._isEventForTabSoundIcon(event) &&
!event.target.classList.contains("tab-icon-overlay")
) {
// Tabs were previously multi-selected and user clicks on a tab
// without holding Ctrl/Cmd Key
gBrowser.clearMultiSelectedTabs();
}
if (
this._isEventForTabSoundIcon(event) ||
(event.target.classList.contains("tab-icon-overlay") &&
(this.soundPlaying || this.muted || this.activeMediaBlocked))
) {
if (this.multiselected) {
gBrowser.toggleMuteAudioOnMultiSelectedTabs(this);
} else {
if (this._isEventForTabSoundIcon(event) && this.pictureinpicture) {
// When Picture-in-Picture is open, we repurpose '.tab-icon-sound' as
// an inert Picture-in-Picture indicator, and expose the '.tab-icon-overlay'
// as the mechanism for muting the tab, so we don't need to handle clicks on
// '.tab-icon-sound' in this case.
return;
}
this.toggleMuteAudio();
}
return;
}
if (event.target.classList.contains("tab-close-button")) {
if (this.multiselected) {
gBrowser.removeMultiSelectedTabs();
} else {
gBrowser.removeTab(this, {
animate: true,
byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
});
}
// 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 &&
!(
this._isEventForTabSoundIcon(event) ||
event.target.classList.contains("tab-icon-overlay")
)
) {
gBrowser.removeTab(this, {
animate: true,
byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
});
}
}
on_animationend(event) {
if (event.target.classList.contains("tab-loading-burst")) {
this.removeAttribute("bursting");
}
}
_mouseenter() {
if (this.hidden || this.closing) {
return;
}
let tabContainer = this.container;
let visibleTabs = tabContainer._getVisibleTabs();
let tabIndex = visibleTabs.indexOf(this);
if (this.selected) {
tabContainer._handleTabSelect();
}
if (tabIndex == 0) {
tabContainer._beforeHoveredTab = null;
} else {
let candidate = visibleTabs[tabIndex - 1];
let separatedByScrollButton =
tabContainer.getAttribute("overflow") == "true" &&
candidate.pinned &&
!this.pinned;
if (!candidate.selected && !separatedByScrollButton) {
tabContainer._beforeHoveredTab = candidate;
candidate.setAttribute("beforehovered", "true");
}
}
if (tabIndex == visibleTabs.length - 1) {
tabContainer._afterHoveredTab = null;
} else {
let candidate = visibleTabs[tabIndex + 1];
if (!candidate.selected) {
tabContainer._afterHoveredTab = candidate;
candidate.setAttribute("afterhovered", "true");
}
}
tabContainer._hoveredTab = this;
if (this.linkedPanel && !this.selected) {
this.linkedBrowser.unselectedTabHover(true);
this.startUnselectedTabHoverTimer();
}
// Prepare connection to host beforehand.
SessionStore.speculativeConnectOnTabHover(this);
let tabToWarm = this;
if (this.mOverCloseButton) {
tabToWarm = gBrowser._findTabToBlurTo(this);
}
gBrowser.warmupTab(tabToWarm);
}
_mouseleave() {
let tabContainer = this.container;
if (tabContainer._beforeHoveredTab) {
tabContainer._beforeHoveredTab.removeAttribute("beforehovered");
tabContainer._beforeHoveredTab = null;
}
if (tabContainer._afterHoveredTab) {
tabContainer._afterHoveredTab.removeAttribute("afterhovered");
tabContainer._afterHoveredTab = null;
}
tabContainer._hoveredTab = null;
if (this.linkedPanel && !this.selected) {
this.linkedBrowser.unselectedTabHover(false);
this.cancelUnselectedTabHoverTimer();
}
}
startUnselectedTabHoverTimer() {
// Only record data when we need to.
if (!this.linkedBrowser.shouldHandleUnselectedTabHover) {
return;
}
if (
!TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)
) {
TelemetryStopwatch.start("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
}
if (this._hoverTabTimer) {
clearTimeout(this._hoverTabTimer);
this._hoverTabTimer = null;
}
}
cancelUnselectedTabHoverTimer() {
// Since we're listening "mouseout" event, instead of "mouseleave".
// Every time the cursor is moving from the tab to its child node (icon),
// it would dispatch "mouseout"(for tab) first and then dispatch
// "mouseover" (for icon, eg: close button, speaker icon) soon.
// It causes we would cancel present TelemetryStopwatch immediately
// when cursor is moving on the icon, and then start a new one.
// In order to avoid this situation, we could delay cancellation and
// remove it if we get "mouseover" within very short period.
this._hoverTabTimer = setTimeout(() => {
if (
TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)
) {
TelemetryStopwatch.cancel("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
}
}, 100);
}
finishUnselectedTabHoverTimer() {
// Stop timer when the tab is opened.
if (
TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)
) {
TelemetryStopwatch.finish("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
}
}
toggleMuteAudio(aMuteReason) {
let browser = this.linkedBrowser;
let modifiedAttrs = [];
let hist = Services.telemetry.getHistogramById(
"TAB_AUDIO_INDICATOR_USED"
);
if (this.activeMediaBlocked) {
this.removeAttribute("activemedia-blocked");
modifiedAttrs.push("activemedia-blocked");
browser.resumeMedia();
hist.add(3 /* unblockByClickingIcon */);
} else {
if (browser.audioMuted) {
if (this.linkedPanel) {
// "Lazy Browser" should not invoke its unmute method
browser.unmute();
}
this.removeAttribute("muted");
hist.add(1 /* unmute */);
} else {
if (this.linkedPanel) {
// "Lazy Browser" should not invoke its mute method
browser.mute();
}
this.setAttribute("muted", "true");
hist.add(0 /* mute */);
}
this.muteReason = aMuteReason || null;
modifiedAttrs.push("muted");
}
gBrowser._tabAttrModified(this, modifiedAttrs);
}
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(event) {
this.updateA11yDescription();
}
on_AriaFocus(event) {
this.updateA11yDescription();
}
}
customElements.define("tabbrowser-tab", MozTabbrowserTab, {
extends: "tab",
});
}