/* 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 all browser windows. Wrap in a block to prevent
// leaking to window scope.
{
const DIRECTION_BACKWARD = -
1;
const DIRECTION_FORWARD =
1;
const isTab = element => gBrowser.isTab(element);
const isTabGroup = element => gBrowser.isTabGroup(element);
const isTabGroupLabel = element => gBrowser.isTabGroupLabel(element);
const isSplitViewWrapper = element => gBrowser.isSplitViewWrapper(element);
class MozTabbrowserTabs
extends MozElements.TabsBase {
static observedAttributes = [
"orient"];
#mustUpdateTabMinHeight =
false;
#tabMinHeight =
36;
#animatingGroups =
new Set();
constructor() {
super();
this.addEventListener(
"TabSelect",
this);
this.addEventListener(
"TabClose",
this);
this.addEventListener(
"TabAttrModified",
this);
this.addEventListener(
"TabHide",
this);
this.addEventListener(
"TabShow",
this);
this.addEventListener(
"TabHoverStart",
this);
this.addEventListener(
"TabHoverEnd",
this);
this.addEventListener(
"TabNoteIconHoverStart",
this);
this.addEventListener(
"TabNoteIconHoverEnd",
this);
this.addEventListener(
"TabGroupLabelHoverStart",
this);
this.addEventListener(
"TabGroupLabelHoverEnd",
this);
// Capture collapse/expand early so we mark animating groups before
// overflow/underflow handlers run.
this.addEventListener(
"TabGroupExpand",
this,
true);
this.addEventListener(
"TabGroupCollapse",
this,
true);
this.addEventListener(
"TabGroupAnimationComplete",
this);
this.addEventListener(
"TabGroupCreate",
this);
this.addEventListener(
"TabGroupRemoved",
this);
this.addEventListener(
"SplitViewCreated",
this);
this.addEventListener(
"SplitViewRemoved",
this);
this.addEventListener(
"transitionend",
this);
this.addEventListener(
"dblclick",
this);
this.addEventListener(
"click",
this);
this.addEventListener(
"click",
this,
true);
this.addEventListener(
"keydown",
this, { mozSystemGroup:
true });
this.addEventListener(
"mouseleave",
this);
this.addEventListener(
"focusin",
this);
this.addEventListener(
"focusout",
this);
this.addEventListener(
"contextmenu",
this);
this.addEventListener(
"dragstart",
this);
this.addEventListener(
"dragover",
this);
this.addEventListener(
"drop",
this);
this.addEventListener(
"dragend",
this);
this.addEventListener(
"dragleave",
this);
}
init() {
this.startupTime = Services.startup.getStartupInfo().start.getTime();
this.arrowScrollbox = document.getElementById(
"tabbrowser-arrowscrollbox"
);
this.arrowScrollbox.addEventListener(
"wheel",
this,
true);
this.arrowScrollbox.addEventListener(
"underflow",
this);
this.arrowScrollbox.addEventListener(
"overflow",
this);
this.pinnedTabsContainer = document.getElementById(
"pinned-tabs-container"
);
this.pinnedTabsContainer.setAttribute(
"orient",
this.getAttribute(
"orient")
);
// Override arrowscrollbox.js method, since our scrollbox's children are
// inherited from the scrollbox binding parent (this).
this.arrowScrollbox._getScrollableElements = () => {
return this.ariaFocusableItems.reduce((elements, item) => {
if (
this.arrowScrollbox._canScrollToElement(item)) {
elements.push(item);
if (
isTab(item) &&
item.group &&
item.group.collapsed &&
item.selected
) {
// overflow container is scrollable, but not in focus order
elements.push(item.group.overflowContainer);
}
}
return elements;
}, []);
};
this.arrowScrollbox._canScrollToElement = element => {
if (isTab(element)) {
return !element.pinned;
}
return true;
};
// Override for performance reasons. This is the size of a single element
// that can be scrolled when using mouse wheel scrolling. If we don't do
// this then arrowscrollbox computes this value by calling
// _getScrollableElements and dividing the box size by that number.
// However in the tabstrip case we already know the answer to this as,
// when we're overflowing, it is always the same as the tab min width or
// height. For tab group labels, the number won't exactly match, but
// that shouldn't be a problem in practice since the arrowscrollbox
// stops at element bounds when finishing scrolling.
Object.defineProperty(
this.arrowScrollbox,
"lineScrollAmount", {
get: () =>
this.verticalMode ?
this.#tabMinHeight :
this._tabMinWidthPref,
});
this.baseConnect();
this._blockDblClick =
false;
this._closeButtonsUpdatePending =
false;
this._closingTabsSpacer =
this.querySelector(
".closing-tabs-spacer");
this._tabDefaultMaxWidth = NaN;
this._lastTabClosedByMouse =
false;
this._hasTabTempMaxWidth =
false;
this._scrollButtonWidth =
0;
this._animateElement =
this.arrowScrollbox;
this._tabClipWidth = Services.prefs.getIntPref(
"browser.tabs.tabClipWidth"
);
this._hiddenSoundPlayingTabs =
new Set();
this.previewPanel =
null;
this.allTabs[
0].label =
this.emptyTabTitle;
this.newTabButton.setAttribute(
"aria-label",
DynamicShortcutTooltip.getText(
"tabs-newtab-button")
);
let handleResize = () => {
this._updateCloseButtons();
this._handleTabSelect(
true);
};
window.addEventListener(
"resize", handleResize);
this._fullscreenMutationObserver =
new MutationObserver(handleResize);
this._fullscreenMutationObserver.observe(document.documentElement, {
attributeFilter: [
"inFullscreen",
"inDOMFullscreen"],
});
window.addEventListener(
"uidensitychanged",
this);
this.boundObserve = (...args) =>
this.observe(...args);
Services.prefs.addObserver(
"privacy.userContext",
this.boundObserve);
this.observe(
null,
"nsPref:changed",
"privacy.userContext.enabled");
document
.getElementById(
"vertical-tabs-newtab-button")
.addEventListener(
"keypress",
this);
document
.getElementById(
"tabs-newtab-button")
.addEventListener(
"keypress",
this);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_tabMinWidthPref",
"browser.tabs.tabMinWidth",
null,
(pref, prevValue, newValue) =>
this.#updateTabMinWidth(newValue),
newValue => {
const LIMIT =
50;
return Math.max(newValue, LIMIT);
}
);
this.#updateTabMinWidth(
this._tabMinWidthPref);
this.#updateTabMinHeight();
CustomizableUI.addListener(
this);
this._updateNewTabVisibility();
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_closeTabByDblclick",
"browser.tabs.closeTabByDblclick",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_sidebarVisibility",
"sidebar.visibility",
"always-show"
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_sidebarPositionStart",
"sidebar.position_start",
true
);
if (gMultiProcessBrowser) {
this.tabbox.tabpanels.setAttribute(
"async",
"true");
}
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_showTabHoverPreview",
"browser.tabs.hoverPreview.enabled",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_showTabGroupHoverPreview",
"browser.tabs.groups.hoverPreview.enabled",
false
);
this.tooltip =
"tabbrowser-tab-tooltip";
this.tabDragAndDrop =
new window.TabDragAndDrop(
this);
this.tabDragAndDrop.init();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name ==
"orient") {
// reset this attribute so we don't have incorrect styling for vertical tabs
this.removeAttribute(
"overflow");
this.#updateTabMinWidth();
this.#updateTabMinHeight();
this.pinnedTabsContainer?.setAttribute(
"orient", newValue);
}
super.attributeChangedCallback(name, oldValue, newValue);
}
// Event handlers
handleEvent(aEvent) {
switch (aEvent.type) {
case "mouseout": {
// If the "related target" (the node to which the pointer went) is not
// a child of the current document, the mouse just left the window.
let relatedTarget = aEvent.relatedTarget;
if (relatedTarget && relatedTarget.ownerDocument == document) {
break;
}
}
// fall through
case "mousemove":
if (
document.getElementById(
"tabContextMenu").state !=
"open" &&
!
this.#isMovingTab()
) {
this._unlockTabSizing();
}
break;
case "mouseleave":
this.previewPanel?.deactivate();
break;
default: {
let methodName = `on_${aEvent.type}`;
if (methodName in
this) {
this[methodName](aEvent);
}
else {
throw new Error(`Unexpected event ${aEvent.type}`);
}
}
}
}
/**
* @param {CustomEvent} event
*/
on_TabSelect(event) {
const {
target: newTab,
detail: { previousTab },
} = event;
// In some cases (e.g. by selecting a tab in a collapsed tab group),
// changing the selected tab may cause a tab to appear/disappear.
if (previousTab.group?.collapsed || newTab.group?.collapsed) {
this._invalidateCachedVisibleTabs();
}
this._handleTabSelect();
}
on_TabClose(event) {
this._hiddenSoundPlayingStatusChanged(event.target, { closed:
true });
}
on_TabAttrModified(event) {
if (
event.detail.changed.includes(
"soundplaying") &&
!event.target.visible
) {
this._hiddenSoundPlayingStatusChanged(event.target);
}
if (
event.detail.changed.includes(
"soundplaying") ||
event.detail.changed.includes(
"muted") ||
event.detail.changed.includes(
"activemedia-blocked")
) {
this.updateTabSoundLabel(event.target);
}
}
on_TabHide(event) {
if (event.target.soundPlaying) {
this._hiddenSoundPlayingStatusChanged(event.target);
}
}
on_TabShow(event) {
if (event.target.soundPlaying) {
this._hiddenSoundPlayingStatusChanged(event.target);
}
}
on_TabHoverStart(event) {
if (!
this._showTabHoverPreview) {
return;
}
this.ensureTabPreviewPanelLoaded();
this.previewPanel.activate(event.target);
}
on_TabHoverEnd(event) {
this.previewPanel?.deactivate(event.target);
}
on_TabNoteIconHoverStart(event) {
if (!
this._showTabHoverPreview) {
return;
}
this.ensureTabPreviewPanelLoaded();
this.previewPanel.activateNotePanel(
event.target,
event.detail.noteIconElement
);
}
on_TabNoteIconHoverEnd(event) {
this.previewPanel?.deactivateNotePanel(event.target);
if (event.detail.returningToTab) {
this.previewPanel?.activate(event.target);
}
}
cancelTabGroupPreview() {
this.previewPanel?.panelOpener.clear();
}
showTabGroupPreview(group) {
if (!
this._showTabGroupHoverPreview) {
return;
}
this.ensureTabPreviewPanelLoaded();
this.previewPanel.activate(group);
}
on_TabGroupLabelHoverStart(event) {
this.showTabGroupPreview(event.target.group);
}
on_TabGroupLabelHoverEnd(event) {
this.previewPanel?.deactivate(event.target.group);
}
on_TabGroupExpand(event) {
this._invalidateCachedVisibleTabs();
this.#animatingGroups.add(event.target.id);
}
on_TabGroupCollapse(event) {
this._invalidateCachedVisibleTabs();
this._unlockTabSizing();
this.#animatingGroups.add(event.target.id);
}
on_TabGroupAnimationComplete(event) {
// Delay clearing the animating flag so overflow/underflow handlers
// triggered by the size change can observe it and skip auto-scroll.
window.requestAnimationFrame(() => {
this.#animatingGroups.
delete(event.target.id);
});
}
on_TabGroupCreate() {
this._invalidateCachedTabs();
}
on_TabGroupRemoved() {
this._invalidateCachedTabs();
}
on_SplitViewCreated() {
this._invalidateCachedTabs();
}
on_SplitViewRemoved() {
this._invalidateCachedTabs();
}
/**
* @param {TransitionEvent} event
*/
on_transitionend(event) {
if (event.propertyName !=
"max-width") {
return;
}
let tab = event.target?.closest(
"tab");
if (!tab) {
return;
}
if (tab.hasAttribute(
"fadein")) {
if (tab._fullyOpen) {
this._updateCloseButtons();
}
else {
this._handleNewTab(tab);
}
}
else if (tab.closing) {
gBrowser._endRemoveTab(tab);
}
let evt =
new CustomEvent(
"TabAnimationEnd", { bubbles:
true });
tab.dispatchEvent(evt);
}
on_dblclick(event) {
// When the tabbar has an unified appearance with the titlebar
// and menubar, a double-click in it should have the same behavior
// as double-clicking the titlebar
if (CustomTitlebar.enabled && !
this.verticalMode) {
return;
}
// Make sure it is the primary button, we are hitting our arrowscrollbox,
// and we're not hitting the scroll buttons.
if (
event.button !=
0 ||
event.target !=
this.arrowScrollbox ||
event.composedTarget.localName ==
"toolbarbutton"
) {
return;
}
if (!
this._blockDblClick) {
BrowserCommands.openTab();
}
event.preventDefault();
}
on_click(event) {
if (event.eventPhase == Event.CAPTURING_PHASE && event.button ==
0) {
/* Catches extra clicks meant for the in-tab close button.
* Placed here to avoid leaking (a temporary handler added from the
* in-tab close button binding would close over the tab and leak it
* until the handler itself was removed). (bug 897751)
*
* The only sequence in which a second click event (i.e. dblclik)
* can be dispatched on an in-tab close button is when it is shown
* after the first click (i.e. the first click event was dispatched
* on the tab). This happens when we show the close button only on
* the active tab. (bug 352021)
* The only sequence in which a third click event can be dispatched
* on an in-tab close button is when the tab was opened with a
* double click on the tabbar. (bug 378344)
* In both cases, it is most likely that the close button area has
* been accidentally clicked, therefore we do not close the tab.
*
* We don't want to ignore processing of more than one click event,
* though, since the user might actually be repeatedly clicking to
* close many tabs at once.
*/
let target = event.originalTarget;
if (target.classList.contains(
"tab-close-button")) {
// We preemptively set this to allow the closing-multiple-tabs-
// in-a-row case.
if (
this._blockDblClick) {
target._ignoredCloseButtonClicks =
true;
}
else if (event.detail >
1 && !target._ignoredCloseButtonClicks) {
target._ignoredCloseButtonClicks =
true;
event.stopPropagation();
return;
}
else {
// Reset the "ignored click" flag
target._ignoredCloseButtonClicks =
false;
}
}
/* Protects from close-tab-button errant doubleclick:
* Since we're removing the event target, if the user
* double-clicks the button, the dblclick event will be dispatched
* with the tabbar as its event target (and explicit/originalTarget),
* which treats that as a mouse gesture for opening a new tab.
* In this context, we're manually blocking the dblclick event.
*/
if (
this._blockDblClick) {
if (!(
"_clickedTabBarOnce" in
this)) {
this._clickedTabBarOnce =
true;
return;
}
delete this._clickedTabBarOnce;
this._blockDblClick =
false;
}
}
else if (
event.eventPhase == Event.BUBBLING_PHASE &&
event.button ==
1
) {
let tab = event.target?.closest(
"tab");
if (tab) {
if (tab.multiselected) {
gBrowser.removeMultiSelectedTabs();
}
else {
gBrowser.removeTab(tab, {
animate:
true,
triggeringEvent: event,
});
}
}
else if (isTabGroupLabel(event.target)) {
event.target.group.saveAndClose();
}
else if (
event.originalTarget.closest(
"scrollbox") &&
!Services.prefs.getBoolPref(
"widget.gtk.titlebar-action-middle-click-enabled"
)
) {
// Check whether the click
// was dispatched on the open space of it.
let visibleTabs =
this.visibleTabs;
let lastTab = visibleTabs.at(-
1);
let winUtils = window.windowUtils;
let endOfTab =
winUtils.getBoundsWithoutFlushing(lastTab)[
(
this.verticalMode &&
"bottom") ||
(
this.#rtlMode ?
"left" :
"right")
];
if (
(
this.verticalMode && event.clientY > endOfTab) ||
(!
this.verticalMode &&
(
this.#rtlMode
? event.clientX < endOfTab
: event.clientX > endOfTab))
) {
BrowserCommands.openTab();
}
}
else {
return;
}
event.preventDefault();
event.stopPropagation();
}
}
on_keydown(event) {
let { altKey, shiftKey } = event;
let [accel, nonAccel] =
AppConstants.platform ==
"macosx"
? [event.metaKey, event.ctrlKey]
: [event.ctrlKey, event.metaKey];
let keyComboForFocusedElement =
!accel && !shiftKey && !altKey && !nonAccel;
let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
if (!keyComboForFocusedElement && !keyComboForMove && !keyComboForFocus) {
return;
}
if (keyComboForFocusedElement) {
let ariaFocusedItem =
this.ariaFocusedItem;
if (isTabGroupLabel(ariaFocusedItem)) {
switch (event.keyCode) {
case KeyEvent.DOM_VK_SPACE:
case KeyEvent.DOM_VK_RETURN: {
ariaFocusedItem.click();
event.preventDefault();
}
}
}
}
else if (keyComboForMove) {
switch (event.keyCode) {
case KeyEvent.DOM_VK_UP:
gBrowser.moveTabBackward();
break;
case KeyEvent.DOM_VK_DOWN:
gBrowser.moveTabForward();
break;
case KeyEvent.DOM_VK_RIGHT:
if (RTL_UI) {
gBrowser.moveTabBackward();
}
else {
gBrowser.moveTabForward();
}
break;
case KeyEvent.DOM_VK_LEFT:
if (RTL_UI) {
gBrowser.moveTabForward();
}
else {
gBrowser.moveTabBackward();
}
break;
case KeyEvent.DOM_VK_HOME:
gBrowser.moveTabToStart();
break;
case KeyEvent.DOM_VK_END:
gBrowser.moveTabToEnd();
break;
default:
// Consume the keydown event for the above keyboard
// shortcuts only.
return;
}
event.preventDefault();
}
else if (keyComboForFocus) {
switch (event.keyCode) {
case KeyEvent.DOM_VK_UP:
this.#advanceFocus(DIRECTION_BACKWARD);
break;
case KeyEvent.DOM_VK_DOWN:
this.#advanceFocus(DIRECTION_FORWARD);
break;
case KeyEvent.DOM_VK_RIGHT:
if (RTL_UI) {
this.#advanceFocus(DIRECTION_BACKWARD);
}
else {
this.#advanceFocus(DIRECTION_FORWARD);
}
break;
case KeyEvent.DOM_VK_LEFT:
if (RTL_UI) {
this.#advanceFocus(DIRECTION_FORWARD);
}
else {
this.#advanceFocus(DIRECTION_BACKWARD);
}
break;
case KeyEvent.DOM_VK_HOME:
this.ariaFocusedItem =
this.ariaFocusableItems.at(
0);
break;
case KeyEvent.DOM_VK_END:
this.ariaFocusedItem =
this.ariaFocusableItems.at(-
1);
break;
case KeyEvent.DOM_VK_SPACE: {
let ariaFocusedItem =
this.ariaFocusedItem;
if (isTab(ariaFocusedItem)) {
if (ariaFocusedItem.multiselected) {
gBrowser.removeFromMultiSelectedTabs(ariaFocusedItem);
}
else {
gBrowser.addToMultiSelectedTabs(ariaFocusedItem);
}
}
break;
}
default:
// Consume the keydown event for the above keyboard
// shortcuts only.
return;
}
event.preventDefault();
}
}
/**
* @param {FocusEvent} event
*/
on_focusin(event) {
if (event.target ==
this.selectedItem) {
this.tablistHasFocus =
true;
if (!
this.ariaFocusedItem) {
// If the active tab is receiving focus and there isn't a keyboard
// focus target yet, set the keyboard focus target to the active
// tab. Do not override the keyboard-focused item if the user
// already set a keyboard focus.
this.ariaFocusedItem =
this.selectedItem;
}
}
let focusReturnedFromGroupPanel = event.relatedTarget?.classList.contains(
"group-preview-button"
);
if (
!focusReturnedFromGroupPanel &&
this.tablistHasFocus &&
isTabGroupLabel(
this.ariaFocusedItem)
) {
this.showTabGroupPreview(
this.ariaFocusedItem.group);
}
}
/**
* @param {FocusEvent} event
*/
on_focusout(event) {
this.cancelTabGroupPreview();
if (event.target ==
this.selectedItem) {
this.tablistHasFocus =
false;
}
}
on_keypress(event) {
if (event.defaultPrevented) {
return;
}
if (event.key ==
" " || event.key ==
"Enter") {
event.preventDefault();
event.target.click();
}
}
on_dragstart(event) {
this.tabDragAndDrop.handle_dragstart(event);
}
on_dragover(event) {
this.tabDragAndDrop.handle_dragover(event);
}
on_drop(event) {
this.tabDragAndDrop.handle_drop(event);
}
on_dragend(event) {
this.tabDragAndDrop.handle_dragend(event);
}
on_dragleave(event) {
this.tabDragAndDrop.handle_dragleave(event);
}
on_wheel(event) {
if (
Services.prefs.getBoolPref(
"toolkit.tabbox.switchByScrolling",
false)
) {
event.stopImmediatePropagation();
}
}
on_overflow(event) {
// Ignore overflow events from nested scrollable elements
if (event.target !=
this.arrowScrollbox) {
return;
}
this.toggleAttribute(
"overflow",
true);
this._updateCloseButtons();
if (!
this.#animatingGroups.size) {
this._handleTabSelect(
true);
}
document
.getElementById(
"tab-preview-panel")
?.setAttribute(
"rolluponmousewheel",
true);
}
on_underflow(event) {
// Ignore underflow events:
// - from nested scrollable elements
// - corresponding to an overflow event that we ignored
if (event.target !=
this.arrowScrollbox || !
this.overflowing) {
return;
}
this.removeAttribute(
"overflow");
if (
this._lastTabClosedByMouse) {
this._expandSpacerBy(
this._scrollButtonWidth);
}
for (let tab of gBrowser._removingTabs) {
gBrowser.removeTab(tab);
}
this._updateCloseButtons();
document
.getElementById(
"tab-preview-panel")
?.removeAttribute(
"rolluponmousewheel");
}
on_contextmenu(event) {
// When pressing the context menu key (as opposed to right-clicking)
// while a tab group label has aria focus (as opposed to DOM focus),
// open the tab group context menu as if the label had DOM focus.
// The button property is used to differentiate between key and mouse.
if (event.button ==
0 && isTabGroupLabel(
this.ariaFocusedItem)) {
gBrowser.tabGroupMenu.openEditModal(
this.ariaFocusedItem.group);
event.preventDefault();
}
}
on_uidensitychanged() {
this._updateCloseButtons();
this.#updateTabMinHeight();
this._handleTabSelect(
true);
}
// Utilities
get emptyTabTitle() {
// Normal tab title is used also in the permanent private browsing mode.
const l10nId =
PrivateBrowsingUtils.isWindowPrivate(window) &&
!Services.prefs.getBoolPref(
"browser.privatebrowsing.autostart")
?
"tabbrowser-empty-private-tab-title"
:
"tabbrowser-empty-tab-title";
return gBrowser.tabLocalization.formatValueSync(l10nId);
}
get tabbox() {
return document.getElementById(
"tabbrowser-tabbox");
}
get newTabButton() {
return this.querySelector(
"#tabs-newtab-button");
}
get verticalMode() {
return this.getAttribute(
"orient") ==
"vertical";
}
get expandOnHover() {
return this._sidebarVisibility ==
"expand-on-hover";
}
get #rtlMode() {
return !
this.verticalMode && RTL_UI;
}
get overflowing() {
return this.hasAttribute(
"overflow");
}
#allTabs;
get allTabs() {
if (
this.#allTabs) {
return this.#allTabs;
}
// Remove temporary periphery element added at drag start.
let pinnedChildren = Array.from(
this.pinnedTabsContainer.children);
if (pinnedChildren?.at(-
1)?.id ==
"pinned-tabs-container-periphery") {
pinnedChildren.pop();
}
let unpinnedChildren = Array.from(
this.arrowScrollbox.children);
// remove arrowScrollbox periphery element.
unpinnedChildren.pop();
// explode tab groups and split view wrappers
// Iterate backwards over the array to preserve indices while we modify
// things in place
for (let i = unpinnedChildren.length -
1; i >=
0; i--) {
if (
unpinnedChildren[i].tagName ==
"tab-group" ||
unpinnedChildren[i].tagName ==
"tab-split-view-wrapper"
) {
unpinnedChildren.splice(i,
1, ...unpinnedChildren[i].tabs);
}
}
this.#allTabs = [...pinnedChildren, ...unpinnedChildren];
return this.#allTabs;
}
get allGroups() {
let children = Array.from(
this.arrowScrollbox.children);
return children.filter(node => node.tagName ==
"tab-group");
}
get allSplitViews() {
let children = Array.from(
this.arrowScrollbox.children);
let splitViews = [];
for (let node of children) {
if (node.tagName ==
"tab-split-view-wrapper") {
splitViews.push(node);
}
else if (node.tagName ==
"tab-group") {
splitViews.push(
...Array.from(node.children).filter(
child => child.tagName ==
"tab-split-view-wrapper"
)
);
}
}
return splitViews;
}
/**
* Returns all tabs in the current window, including hidden tabs and tabs
* in collapsed groups, but excluding closing tabs and the Firefox View tab.
*/
get openTabs() {
if (!
this.#openTabs) {
this.#openTabs =
this.allTabs.filter(tab => tab.isOpen);
}
return this.#openTabs;
}
#openTabs;
/**
* Same as `openTabs` but excluding hidden tabs.
*/
get nonHiddenTabs() {
if (!
this.#nonHiddenTabs) {
this.#nonHiddenTabs =
this.openTabs.filter(tab => !tab.hidden);
}
return this.#nonHiddenTabs;
}
#nonHiddenTabs;
/**
* Same as `openTabs` but excluding hidden tabs and tabs in collapsed groups.
*/
get visibleTabs() {
if (!
this.#visibleTabs) {
this.#visibleTabs =
this.openTabs.filter(tab => tab.visible);
}
return this.#visibleTabs;
}
#visibleTabs;
/**
* @returns {boolean} true if the keyboard focus is on the active tab
*/
get tablistHasFocus() {
return this.hasAttribute(
"tablist-has-focus");
}
/**
* @param {boolean} hasFocus true if the keyboard focus is on the active tab
*/
set tablistHasFocus(hasFocus) {
this.toggleAttribute(
"tablist-has-focus", hasFocus);
}
/** @typedef {MozTabbrowserTab|MozTextLabel} FocusableItem */
/** @type {FocusableItem[]} */
#focusableItems;
/** @type {dragAndDropElements[]} */
#dragAndDropElements;
/**
* @returns {FocusableItem[]}
* @override
*/
get ariaFocusableItems() {
if (
this.#focusableItems) {
return this.#focusableItems;
}
let unpinnedChildren = Array.from(
this.arrowScrollbox.children);
let pinnedChildren = Array.from(
this.pinnedTabsContainer.children);
let focusableItems = [];
for (let child of pinnedChildren) {
if (isTab(child)) {
focusableItems.push(child);
}
}
for (let child of unpinnedChildren) {
if (isTab(child) && child.visible) {
focusableItems.push(child);
}
else if (isTabGroup(child)) {
focusableItems.push(child.labelElement);
let visibleTabsInGroup = child.tabs.filter(tab => tab.visible);
focusableItems.push(...visibleTabsInGroup);
}
else if (child.tagName ==
"tab-split-view-wrapper") {
let visibleTabsInSplitView = child.tabs.filter(tab => tab.visible);
focusableItems.push(...visibleTabsInSplitView);
}
}
this.#focusableItems = focusableItems;
return this.#focusableItems;
}
/**
* @returns {dragAndDropElements[]}
* Representation of every drag and drop element including tabs, tab group labels and split view wrapper.
* We keep this separate from ariaFocusableItems because not every element for drag n'drop also needs to be
* focusable (ex, we don't want the splitview container to be focusable, only its children).
*/
get dragAndDropElements() {
if (
this.#dragAndDropElements) {
return this.#dragAndDropElements;
}
let elementIndex =
0;
let dragAndDropElements = [];
let unpinnedChildren = Array.from(
this.arrowScrollbox.children);
let pinnedChildren = Array.from(
this.pinnedTabsContainer.children);
for (let child of [...pinnedChildren, ...unpinnedChildren]) {
if (
!(
(isTab(child) && child.visible) ||
isTabGroup(child) ||
isSplitViewWrapper(child)
)
) {
continue;
}
if (isTabGroup(child)) {
child.labelElement.elementIndex = elementIndex++;
dragAndDropElements.push(child.labelElement);
let tabsAndSplitViews = child.tabsAndSplitViews.filter(
node => node.visible
);
tabsAndSplitViews.forEach(ele => {
ele.elementIndex = elementIndex++;
});
dragAndDropElements.push(...tabsAndSplitViews);
}
else {
child.elementIndex = elementIndex++;
dragAndDropElements.push(child);
}
}
this.#dragAndDropElements = dragAndDropElements;
return this.#dragAndDropElements;
}
/**
* Moves the ARIA focus in the tab strip left or right, as appropriate, to
* the next tab or tab group label.
*
* @param {-1|1} direction
*/
#advanceFocus(direction) {
let currentIndex =
this.ariaFocusableItems.indexOf(
this.ariaFocusedItem);
let newIndex = currentIndex + direction;
// Clamp the index so that the focus stops at the edges of the tab strip
newIndex = Math.min(
this.ariaFocusableItems.length -
1,
Math.max(
0, newIndex)
);
let itemToFocus =
this.ariaFocusableItems[newIndex];
this.ariaFocusedItem = itemToFocus;
// If the newly-focused item is a tab group label and the group is collapsed,
// proactively show the tab group preview
if (isTabGroupLabel(
this.ariaFocusedItem)) {
this.showTabGroupPreview(
this.ariaFocusedItem.group);
}
}
_invalidateCachedTabs() {
this.#allTabs =
null;
this._invalidateCachedVisibleTabs();
}
_invalidateCachedVisibleTabs() {
this.#openTabs =
null;
this.#nonHiddenTabs =
null;
this.#visibleTabs =
null;
// Focusable items must also be visible, but they do not depend on
// this.#visibleTabs, so changes to visible tabs need to also invalidate
// the focusable items and dragAndDropElements cache.
this.#focusableItems =
null;
this.#dragAndDropElements =
null;
}
#isMovingTab() {
return this.hasAttribute(
"movingtab");
}
isContainerVerticalPinnedGrid(tab) {
return (
tab.pinned &&
this.verticalMode &&
this.hasAttribute(
"expanded") &&
!
this.expandOnHover
);
}
/**
* Changes the selected tab or tab group label on the tab strip
* relative to the ARIA-focused tab strip element or the active tab. This
* is intended for traversing the tab strip visually, e.g by using keyboard
* arrows. For cases where keyboard shortcuts or other logic should only
* select tabs (and never tab group labels), see `advanceSelectedTab`.
*
* @override
* @param {-1|1} direction
* @param {boolean} shouldWrap
*/
advanceSelectedItem(aDir, aWrap) {
let groupPanel =
this.previewPanel?.tabGroupPanel;
if (groupPanel && groupPanel.isActive) {
// if the group panel is open, it should receive keyboard focus here
// instead of moving to the next item in the tabstrip.
groupPanel.focusPanel(aDir);
return;
}
// cancel any pending group popup since we expect to deselect the label
this.cancelTabGroupPreview();
let { ariaFocusableItems, ariaFocusedIndex } =
this;
// Advance relative to the ARIA-focused item if set, otherwise advance
// relative to the active tab.
let currentItemIndex =
ariaFocusedIndex >=
0
? ariaFocusedIndex
: ariaFocusableItems.indexOf(
this.selectedItem);
let newItemIndex = currentItemIndex + aDir;
if (aWrap) {
if (newItemIndex >= ariaFocusableItems.length) {
newItemIndex =
0;
}
else if (newItemIndex <
0) {
newItemIndex = ariaFocusableItems.length -
1;
}
}
else {
newItemIndex = Math.min(
ariaFocusableItems.length -
1,
Math.max(
0, newItemIndex)
);
}
if (currentItemIndex == newItemIndex) {
return;
}
// If the next item is a tab, select it. If the next item is a tab group
// label, keep the active tab selected and just set ARIA focus on the tab
// group label.
let newItem = ariaFocusableItems[newItemIndex];
if (isTab(newItem)) {
this._selectNewTab(newItem, aDir, aWrap);
}
this.ariaFocusedItem = newItem;
// If the newly-focused item is a tab group label and the group is collapsed,
// proactively show the tab group preview
if (isTabGroupLabel(
this.ariaFocusedItem)) {
this.showTabGroupPreview(
this.ariaFocusedItem.group);
}
}
ensureTabPreviewPanelLoaded() {
if (!
this.previewPanel) {
const TabHoverPanelSet = ChromeUtils.importESModule(
"chrome://browser/content/tabbrowser/tab-hover-preview.mjs"
).
default;
this.previewPanel =
new TabHoverPanelSet(window);
}
}
appendChild(tab) {
return this.insertBefore(tab,
null);
}
insertBefore(tab, node) {
if (!
this.arrowScrollbox) {
throw new Error(
"Shouldn't call this without arrowscrollbox");
}
if (node ==
null) {
// We have a container for non-tab elements at the end of the scrollbox.
node =
this.arrowScrollbox.lastChild;
}
node.before(tab);
if (
this.#mustUpdateTabMinHeight) {
this.#updateTabMinHeight();
}
}
#updateTabMinWidth(val) {
this.style.setProperty(
"--tab-min-width-pref",
(val ??
this._tabMinWidthPref) +
"px"
);
}
#updateTabMinHeight() {
if (!
this.verticalMode || !window.toolbar.visible) {
this.#mustUpdateTabMinHeight =
false;
return;
}
// Find at least one tab we can scroll to.
let firstScrollableTab =
this.visibleTabs.find(
this.arrowScrollbox._canScrollToElement
);
if (!firstScrollableTab) {
// If not, we're in a pickle. We should never get here except if we
// also don't use the outcome of this work (because there's nothing to
// scroll so we don't care about the scrollbox size).
// So just set a flag so we re-run once we do have a new tab.
this.#mustUpdateTabMinHeight =
true;
return;
}
let { height } =
window.windowUtils.getBoundsWithoutFlushing(firstScrollableTab);
// Use the current known height or a sane default.
this.#tabMinHeight = height ||
36;
// The height we got may be incorrect if a flush is pending so re-check it after
// a flush completes.
window
.promiseDocumentFlushed(() => {})
.then(
() => {
height =
window.windowUtils.getBoundsWithoutFlushing(
firstScrollableTab
).height;
if (height) {
this.#tabMinHeight = height;
}
},
() => {
/* ignore errors */
}
);
}
get _isCustomizing() {
return document.documentElement.hasAttribute(
"customizing");
}
// This overrides the TabsBase _selectNewTab method so that we can
// potentially interrupt keyboard tab switching when sharing the
// window or screen.
_selectNewTab(aNewTab, aFallbackDir, aWrap) {
if (!gSharedTabWarning.willShowSharedTabWarning(aNewTab)) {
super._selectNewTab(aNewTab, aFallbackDir, aWrap);
}
}
observe(aSubject, aTopic) {
switch (aTopic) {
case "nsPref:changed": {
// This is has to deal with changes in
// privacy.userContext.enabled and
// privacy.userContext.newTabContainerOnLeftClick.enabled.
let containersEnabled =
Services.prefs.getBoolPref(
"privacy.userContext.enabled") &&
!PrivateBrowsingUtils.isWindowPrivate(window);
// This pref won't change so often, so just recreate the menu.
const newTabLeftClickOpensContainersMenu = Services.prefs.getBoolPref(
"privacy.userContext.newTabContainerOnLeftClick.enabled"
);
// There are separate "new tab" buttons for horizontal tabs toolbar, vertical tabs and
// for when the tab strip is overflowed (which is shared by vertical and horizontal tabs);
// Attach the long click popup to all of them.
const newTab = document.getElementById(
"new-tab-button");
const newTab2 =
this.newTabButton;
const newTabVertical = document.getElementById(
"vertical-tabs-newtab-button"
);
for (let parent of [newTab, newTab2, newTabVertical]) {
if (!parent) {
continue;
}
parent.removeAttribute(
"type");
if (parent.menupopup) {
parent.menupopup.remove();
}
if (containersEnabled) {
parent.setAttribute(
"context",
"new-tab-button-popup");
let popup = document
.getElementById(
"new-tab-button-popup")
.cloneNode(
true);
popup.removeAttribute(
"id");
popup.className =
"new-tab-popup";
popup.setAttribute(
"position",
"after_end");
popup.addEventListener(
"popupshowing", CreateContainerTabMenu);
parent.prepend(popup);
parent.setAttribute(
"type",
"menu");
// Update tooltip text
DynamicShortcutTooltip.nodeToTooltipMap[parent.id] =
newTabLeftClickOpensContainersMenu
?
"newTabAlwaysContainer.tooltip"
:
"newTabContainer.tooltip";
}
else {
DynamicShortcutTooltip.nodeToTooltipMap[parent.id] =
"newTabButton.tooltip";
parent.removeAttribute(
"context",
"new-tab-button-popup");
}
// evict from tooltip cache
DynamicShortcutTooltip.cache.
delete(parent.id);
// If containers and press-hold container menu are both used,
// add to gClickAndHoldListenersOnElement; otherwise, remove.
if (containersEnabled && !newTabLeftClickOpensContainersMenu) {
gClickAndHoldListenersOnElement.add(parent);
}
else {
gClickAndHoldListenersOnElement.remove(parent);
}
}
break;
}
}
}
_updateCloseButtons() {
if (
this.overflowing) {
// Tabs are at their minimum widths.
this.setAttribute(
"closebuttons",
"activetab");
return;
}
if (
this._closeButtonsUpdatePending) {
return;
}
this._closeButtonsUpdatePending =
true;
// Wait until after the next paint to get current layout data from
// getBoundsWithoutFlushing.
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
this._closeButtonsUpdatePending =
false;
// The scrollbox may have started overflowing since we checked
// overflow earlier, so check again.
if (
this.overflowing) {
this.setAttribute(
"closebuttons",
"activetab");
return;
}
// Check if tab widths are below the threshold where we want to
// remove close buttons from background tabs so that people don't
// accidentally close tabs by selecting them.
let rect = ele => {
return window.windowUtils.getBoundsWithoutFlushing(ele);
};
// See bug 2007766, we need to find the first tab that isn't
// inside a split view, because those can be narrower than the threshold.
let tab =
this.visibleTabs
.slice(gBrowser.pinnedTabCount)
.find(t => !t.splitview);
if (tab && rect(tab).width <=
this._tabClipWidth) {
this.setAttribute(
"closebuttons",
"activetab");
}
else {
this.removeAttribute(
"closebuttons");
}
});
});
}
/**
* @param {boolean} [aInstant]
*/
_handleTabSelect(aInstant) {
let selectedTab =
this.selectedItem;
this.#ensureTabIsVisible(selectedTab, aInstant);
selectedTab._notselectedsinceload =
false;
}
/**
* @param {MozTabbrowserTab} tab
* @param {boolean} [shouldScrollInstantly=false]
*/
#ensureTabIsVisible(tab, shouldScrollInstantly =
false) {
let arrowScrollbox = tab.closest(
"arrowscrollbox");
if (arrowScrollbox?.overflowing) {
arrowScrollbox.ensureElementIsVisible(tab, shouldScrollInstantly);
}
}
/**
* Try to keep the active tab's close button under the mouse cursor
*/
_lockTabSizing(aClosingTab, aTabWidth) {
if (
this.verticalMode) {
return;
}
let tabs =
this.visibleTabs;
let numPinned = gBrowser.pinnedTabCount;
if (tabs.length <= numPinned) {
// There are no unpinned tabs left.
return;
}
let isEndTab = aClosingTab && aClosingTab._tPos > tabs.at(-
1)._tPos;
if (!
this._tabDefaultMaxWidth) {
this._tabDefaultMaxWidth = parseFloat(
window.getComputedStyle(tabs[numPinned]).maxWidth
);
}
this._lastTabClosedByMouse =
true;
this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
this.arrowScrollbox._scrollButtonDown
).width;
if (aTabWidth === undefined) {
aTabWidth = window.windowUtils.getBoundsWithoutFlushing(
tabs[numPinned]
).width;
}
if (
this.overflowing) {
// Don't need to do anything if we're in overflow mode and aren't scrolled
// all the way to the right, or if we're closing the last tab.
if (isEndTab || !
this.arrowScrollbox.hasAttribute(
"scrolledtoend")) {
return;
}
// If the tab has an owner that will become the active tab, the owner will
// be to the left of it, so we actually want the left tab to slide over.
// This can't be done as easily in non-overflow mode, so we don't bother.
if (aClosingTab?.owner) {
return;
}
this._expandSpacerBy(aTabWidth);
}
/* non-overflow mode */ else {
if (isEndTab && !
this._hasTabTempMaxWidth) {
// Locking is neither in effect nor needed, so let tabs expand normally.
return;
}
// Force tabs to stay the same width, unless we're closing the last tab,
// which case we need to let them expand just enough so that the overall
// tabbar width is the same.
if (isEndTab) {
let numNormalTabs = tabs.length - numPinned;
aTabWidth = (aTabWidth * (numNormalTabs +
1)) / numNormalTabs;
if (aTabWidth >
this._tabDefaultMaxWidth) {
aTabWidth =
this._tabDefaultMaxWidth;
}
}
aTabWidth +=
"px";
let tabsToReset = [];
for (let i = numPinned; i < tabs.length; i++) {
let tab = tabs[i];
tab.style.setProperty(
"max-width", aTabWidth,
"important");
if (!isEndTab) {
// keep tabs the same width
tab.animationsEnabled =
false;
tabsToReset.push(tab);
}
}
if (tabsToReset.length) {
window
.promiseDocumentFlushed(() => {})
.then(() => {
window.requestAnimationFrame(() => {
for (let tab of tabsToReset) {
tab.animationsEnabled =
true;
}
});
});
}
this._hasTabTempMaxWidth =
true;
gBrowser.addEventListener(
"mousemove",
this);
window.addEventListener(
"mouseout",
this);
}
}
_expandSpacerBy(pixels) {
let spacer =
this._closingTabsSpacer;
spacer.style.width = parseFloat(spacer.style.width) + pixels +
"px";
this.toggleAttribute(
"using-closing-tabs-spacer",
true);
gBrowser.addEventListener(
"mousemove",
this);
window.addEventListener(
"mouseout",
this);
}
_unlockTabSizing() {
gBrowser.removeEventListener(
"mousemove",
this);
window.removeEventListener(
"mouseout",
this);
if (
this._hasTabTempMaxWidth) {
this._hasTabTempMaxWidth =
false;
// Only visible tabs have their sizes locked, but those visible tabs
// could become invisible before being unlocked (e.g. by being inside
// of a collapsing tab group), so it's better to reset all tabs.
let tabs =
this.allTabs;
for (let i =
0; i < tabs.length; i++) {
tabs[i].style.maxWidth =
"";
}
}
if (
this.hasAttribute(
"using-closing-tabs-spacer")) {
this.removeAttribute(
"using-closing-tabs-spacer");
this._closingTabsSpacer.style.width =
0;
}
}
_notifyBackgroundTab(aTab) {
if (aTab.pinned || !aTab.visible || !
this.overflowing) {
return;
}
this._lastTabToScrollIntoView = aTab;
if (!
this._backgroundTabScrollPromise) {
this._backgroundTabScrollPromise = window
.promiseDocumentFlushed(() => {
let lastTabRect =
this._lastTabToScrollIntoView.getBoundingClientRect();
let selectedTab =
this.selectedItem;
if (selectedTab.pinned) {
selectedTab =
null;
}
else {
selectedTab = selectedTab.getBoundingClientRect();
selectedTab = {
left: selectedTab.left,
right: selectedTab.right,
top: selectedTab.top,
bottom: selectedTab.bottom,
};
}
return [
this._lastTabToScrollIntoView,
this.arrowScrollbox.scrollClientRect,
lastTabRect,
selectedTab,
];
})
.then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
// First off, remove the promise so we can re-enter if necessary.
delete this._backgroundTabScrollPromise;
// Then, if the layout info isn't for the last-scrolled-to-tab, re-run
// the code above to get layout info for *that* tab, and don't do
// anything here, as we really just want to run this for the last-opened tab.
if (
this._lastTabToScrollIntoView != tabToScrollIntoView) {
this._notifyBackgroundTab(
this._lastTabToScrollIntoView);
return;
}
delete this._lastTabToScrollIntoView;
// Is the new tab already completely visible?
if (
this.verticalMode
? scrollRect.top <= tabRect.top &&
tabRect.bottom <= scrollRect.bottom
: scrollRect.left <= tabRect.left &&
tabRect.right <= scrollRect.right
) {
return;
}
if (
this.arrowScrollbox.smoothScroll) {
// Can we make both the new tab and the selected tab completely visible?
if (
!selectedRect ||
(
this.verticalMode
? Math.max(
tabRect.bottom - selectedRect.top,
selectedRect.bottom - tabRect.top
) <= scrollRect.height
: Math.max(
tabRect.right - selectedRect.left,
selectedRect.right - tabRect.left
) <= scrollRect.width)
) {
this.#ensureTabIsVisible(tabToScrollIntoView);
return;
}
let scrollPixels;
if (
this.verticalMode) {
scrollPixels = tabRect.top - selectedRect.top;
}
else if (
this.#rtlMode) {
scrollPixels = selectedRect.right - scrollRect.right;
}
else {
scrollPixels = selectedRect.left - scrollRect.left;
}
this.arrowScrollbox.scrollByPixels(scrollPixels);
}
if (!
this._animateElement.hasAttribute(
"highlight")) {
this._animateElement.toggleAttribute(
"highlight",
true);
setTimeout(
function (ele) {
ele.removeAttribute(
"highlight");
},
150,
this._animateElement
);
}
});
}
}
_handleNewTab(tab) {
if (tab.container !=
this) {
return;
}
tab._fullyOpen =
true;
gBrowser.tabAnimationsInProgress--;
this._updateCloseButtons();
if (tab.hasAttribute(
"selected")) {
this._handleTabSelect();
}
else if (!tab.hasAttribute(
"skipbackgroundnotify")) {
this._notifyBackgroundTab(tab);
}
// If this browser isn't lazy (indicating it's probably created by
// session restore), preload the next about:newtab if we don't
// already have a preloaded browser.
if (tab.linkedPanel) {
NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
}
if (UserInteraction.running(
"browser.tabs.opening", window)) {
UserInteraction.finish(
"browser.tabs.opening", window);
}
}
_canAdvanceToTab(aTab) {
return !aTab.closing;
}
/**
* Returns the panel associated with a tab if it has a connected browser
* and/or it is the selected tab.
* For background lazy browsers, this will return null.
*/
getRelatedElement(aTab) {
if (!aTab) {
return null;
}
// Cannot access gBrowser before it's initialized.
if (!gBrowser._initialized) {
return this.tabbox.tabpanels.firstElementChild;
}
// If the tab's browser is lazy, we need to `_insertBrowser` in order
// to have a linkedPanel. This will also serve to bind the browser
// and make it ready to use. We only do this if the tab is selected
// because otherwise, callers might end up unintentionally binding the
// browser for lazy background tabs.
if (!aTab.linkedPanel) {
if (!aTab.selected) {
return null;
}
gBrowser._insertBrowser(aTab);
}
return document.getElementById(aTab.linkedPanel);
}
_updateNewTabVisibility() {
// Helper functions to help deal with customize mode wrapping some items
let wrap = n =>
n.parentNode.localName ==
"toolbarpaletteitem" ? n.parentNode : n;
let unwrap = n =>
n && n.localName ==
"toolbarpaletteitem" ? n.firstElementChild : n;
// Starting from the tabs element, find the next sibling that:
// - isn't hidden; and
// - isn't the all-tabs button.
// If it's the new tab button, consider the new tab button adjacent to the tabs.
// If the new tab button is marked as adjacent and the tabstrip doesn't
// overflow, we'll display the 'new tab' button inline in the tabstrip.
// In all other cases, the separate new tab button is displayed in its
// customized location.
let sib =
this;
do {
sib = unwrap(wrap(sib).nextElementSibling);
}
while (sib && (sib.hidden || sib.id ==
"alltabs-button"));
this.toggleAttribute(
"hasadjacentnewtabbutton",
sib && sib.id ==
"new-tab-button"
);
}
onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
if (
aContainer.ownerDocument == document &&
aContainer.id ==
"TabsToolbar-customization-target"
) {
this._updateNewTabVisibility();
}
}
onAreaNodeRegistered(aArea, aContainer) {
if (aContainer.ownerDocument == document && aArea ==
"TabsToolbar") {
this._updateNewTabVisibility();
}
}
onAreaReset(aArea, aContainer) {
this.onAreaNodeRegistered(aArea, aContainer);
}
_hiddenSoundPlayingStatusChanged(tab, opts) {
let closed = opts && opts.closed;
if (!closed && tab.soundPlaying && !tab.visible) {
this._hiddenSoundPlayingTabs.add(tab);
this.toggleAttribute(
"hiddensoundplaying",
true);
}
else {
this._hiddenSoundPlayingTabs.
delete(tab);
if (
this._hiddenSoundPlayingTabs.size ==
0) {
this.removeAttribute(
"hiddensoundplaying");
}
}
}
destroy() {
if (
this.boundObserve) {
Services.prefs.removeObserver(
"privacy.userContext",
this.boundObserve);
}
CustomizableUI.removeListener(
this);
}
updateTabSoundLabel(tab) {
// Add aria-label for inline audio button
const [unmute, mute, unblock] =
gBrowser.tabLocalization.formatMessagesSync([
"tabbrowser-unmute-tab-audio-aria-label",
"tabbrowser-mute-tab-audio-aria-label",
"tabbrowser-unblock-tab-audio-aria-label",
]);
if (tab.audioButton) {
if (tab.hasAttribute(
"muted") || tab.hasAttribute(
"soundplaying")) {
let ariaLabel;
tab.linkedBrowser.audioMuted
? (ariaLabel = unmute.attributes[
0].value)
: (ariaLabel = mute.attributes[
0].value);
tab.audioButton.setAttribute(
"aria-label", ariaLabel);
}
else if (tab.hasAttribute(
"activemedia-blocked")) {
tab.audioButton.setAttribute(
"aria-label",
unblock.attributes[
0].value
);
}
}
}
}
customElements.define(
"tabbrowser-tabs", MozTabbrowserTabs, {
extends:
"tabs",
});
}