- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 76 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 75 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 74 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 80 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 81 %
- : 80 %
- : 81 %
- : 92 %
- : 93 %
- : 92 %
- : 92 %
- : 92 %
- : 92 %
- : 92 %
- : 92 %
- : 92 %
- : 92 %
- : 93 %
- : 93 %
- : 92 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 92 %
- : 93 %
- : 93 %
- : 92 %
- : 92 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 92 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 92 %
- : 92 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 92 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 79 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 92 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 63 %
- : 93 %
- : 93 %
- : 92 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 93 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 93 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 60 %
- : 58 %
- : 60 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 77 %
- : 94 %
- : 94 %
- : 77 %
- : 77 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 85 %
- : 85 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
Source code
Revision control
Copy as Markdown
Other Tools
/* 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;
// Hide the secondary text for locales where it is unsupported due to size constraints.
const language = Services.locale.appLocaleAsBCP47;
const unsupportedLocales = Services.prefs.getCharPref(
"browser.tabs.secondaryTextUnsupportedLocales"
);
this.toggleAttribute(
"secondarytext-unsupported",
unsupportedLocales.split(",").includes(language.split("-")[0])
);
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",
});
}