Files
desktop/src/zen/common/modules/ZenUIManager.mjs

1701 lines
53 KiB
JavaScript

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import { nsZenMultiWindowFeature } from "chrome://browser/content/zen-components/ZenCommonUtils.mjs";
import { nsZenMenuBar } from "chrome://browser/content/zen-components/ZenMenubar.mjs";
window.gZenUIManager = {
_popupTrackingElements: [],
_hoverPausedForExpand: false,
_hasLoadedDOM: false,
testingEnabled: Services.prefs.getBoolPref("zen.testing.enabled", false),
profilingEnabled: Services.prefs.getBoolPref(
"zen.testing.profiling.enabled",
false
),
_lastClickPosition: null,
_toastTimeouts: [],
init() {
window.gZenMenubar = new nsZenMenuBar();
document.addEventListener("popupshowing", this.onPopupShowing.bind(this));
document.addEventListener("popuphidden", this.onPopupHidden.bind(this));
document.addEventListener(
"mousedown",
this.handleMouseDown.bind(this),
true
);
ChromeUtils.defineLazyGetter(this, "motion", () => {
Services.scriptloader.loadSubScript(
"chrome://browser/content/zen-vendor/motion.min.mjs",
window
);
const motion = window.Motion;
delete window.Motion;
return motion;
});
ChromeUtils.defineLazyGetter(this, "_toastContainer", () => {
return document.getElementById("zen-toast-container");
});
new ResizeObserver(
gZenCommonActions.throttle(
gZenCompactModeManager.getAndApplySidebarWidth.bind(
gZenCompactModeManager
),
Services.prefs.getIntPref("zen.view.sidebar-height-throttle", 500)
)
).observe(gNavToolbox);
gZenWorkspaces.promiseInitialized.finally(() => {
this._hasLoadedDOM = true;
this.updateTabsToolbar();
});
window.addEventListener("TabClose", this.onTabClose.bind(this));
window.addEventListener(
"Zen:UrlbarSearchModeChanged",
this.onUrlbarSearchModeChanged.bind(this)
);
gZenMediaController.init();
gZenVerticalTabsManager.init();
gZenLiveFoldersUI.init();
this._initCreateNewPopup();
this._debloatContextMenus();
this._addNewCustomizableButtonsIfNeeded();
this._initOmnibox();
this._initBookmarkCollapseListener();
gURLBar._setPlaceholder(null);
document
.getElementById("PersonalToolbar")
.setAttribute("fullscreentoolbar", "true");
},
/**
* Animate an element using Element.animate API.
* This is not using gZenUIManager.motion, because motion library has some issues
* with certain properties and we want to have a simple wrapper for that.
*
* @param {Element} element
* @param {object} rawKeyframes
* @param {...any} args
*/
async elementAnimate(element, rawKeyframes, ...args) {
rawKeyframes = { ...rawKeyframes };
// Convert 'y' property to 'transform' with translateY and 'x' to translateX,
// and 'scale' to 'transform' with scale.
if (
(rawKeyframes.y || rawKeyframes.x || rawKeyframes.scale) &&
!rawKeyframes.transform
) {
const yValues = rawKeyframes.y || [];
const xValues = rawKeyframes.x || [];
const scaleValues = rawKeyframes.scale || [];
delete rawKeyframes.y;
delete rawKeyframes.x;
delete rawKeyframes.scale;
rawKeyframes.transform = [];
if (
yValues.length !== 0 &&
xValues.length !== 0 &&
yValues.length !== xValues.length
) {
console.error("y and x keyframes must have the same length");
}
const keyframeLength = Math.max(
yValues.length,
xValues.length,
scaleValues.length
);
for (let i = 0; i < keyframeLength; i++) {
const y = yValues[i] !== undefined ? `translateY(${yValues[i]}px)` : "";
const x = xValues[i] !== undefined ? `translateX(${xValues[i]}px)` : "";
const scale =
scaleValues[i] !== undefined ? `scale(${scaleValues[i]})` : "";
rawKeyframes.transform.push(`${x} ${y} ${scale}`.trim());
}
}
let keyframes = [];
for (let i = 0; i < Object.values(rawKeyframes)[0].length; i++) {
let frame = {};
for (const [property, values] of Object.entries(rawKeyframes)) {
frame[property] = values[i];
}
keyframes.push(frame);
}
return await new Promise(resolve => {
const animation = element.animate(keyframes, ...args);
animation.onfinish = () => resolve();
});
},
_addNewCustomizableButtonsIfNeeded() {
const kPref = "zen.ui.migration.compact-mode-button-added";
let navbarPlacements = CustomizableUI.getWidgetIdsInArea(
"zen-sidebar-top-buttons"
);
try {
if (
!navbarPlacements.length &&
!Services.prefs.getBoolPref(kPref, false)
) {
CustomizableUI.addWidgetToArea(
"zen-toggle-compact-mode",
"zen-sidebar-top-buttons",
0
);
gZenVerticalTabsManager._topButtonsSeparatorElement.before(
document.getElementById("zen-toggle-compact-mode")
);
}
} catch (e) {
console.error("Error adding compact mode button to sidebar:", e);
}
Services.prefs.setBoolPref(kPref, true);
},
_initBookmarkCollapseListener() {
const bookmarkToolbar = document.getElementById("PersonalToolbar");
if (!bookmarkToolbar.hasAttribute("collapsed")) {
// Set it initially if bookmarks toolbar is visible, customizable UI
// is ran before this function.
document.documentElement.setAttribute("zen-has-bookmarks", "true");
}
bookmarkToolbar.addEventListener("toolbarvisibilitychange", event => {
const visible = event.detail.visible;
if (visible) {
document.documentElement.setAttribute("zen-has-bookmarks", "true");
} else {
document.documentElement.removeAttribute("zen-has-bookmarks");
}
});
},
_initOmnibox() {
const { registerZenUrlbarProviders } = ChromeUtils.importESModule(
"resource:///modules/ZenUBProvider.sys.mjs"
);
const { nsZenSiteDataPanel: ZenSiteDataPanel } = ChromeUtils.importESModule(
"resource:///modules/ZenSiteDataPanel.sys.mjs"
);
registerZenUrlbarProviders();
window.gZenSiteDataPanel = new ZenSiteDataPanel(window);
gURLBar._zenTrimURL = this.urlbarTrim.bind(this);
},
_debloatContextMenus() {
if (!Services.prefs.getBoolPref("zen.view.context-menu.refresh", false)) {
return;
}
const contextMenusToClean = [
// Remove the 'new tab below' context menu.
// reason: It doesn't properly work with zen and it's philosophy of not having
// new tabs. It's also semi-not working as it doesn't create a new tab below
// the current one.
"context_openANewTab",
];
for (const id of contextMenusToClean) {
const menu = document.getElementById(id);
if (!menu) {
continue;
}
menu.setAttribute("hidden", "true");
}
},
_initCreateNewPopup() {
const popup = document.getElementById("zenCreateNewPopup");
popup.addEventListener("popupshowing", () => {
const button = document.getElementById("zen-create-new-button");
if (!button) {
return;
}
const image = button.querySelector("image");
button.setAttribute("open", "true");
gZenUIManager.motion.animate(
image,
{ transform: ["rotate(0deg)", "rotate(45deg)"] },
{ duration: 0.2 }
);
popup.addEventListener(
"popuphidden",
() => {
button.removeAttribute("open");
gZenUIManager.motion.animate(
image,
{ transform: ["rotate(45deg)", "rotate(0deg)"] },
{ duration: 0.2 }
);
},
{ once: true }
);
});
},
handleMouseDown(event) {
this._lastClickPosition = {
clientX: event.clientX,
clientY: event.clientY,
};
},
updateTabsToolbar() {
const kUrlbarHeight = 335;
gURLBar.style.setProperty(
"--zen-urlbar-top",
`${window.innerHeight / 2 - Math.max(kUrlbarHeight, window.windowUtils.getBoundsWithoutFlushing(gURLBar).height) / 2}px`
);
gURLBar.style.setProperty(
"--zen-urlbar-width",
`${Math.min(window.innerWidth / 2, 750)}px`
);
gZenVerticalTabsManager.actualWindowButtons.removeAttribute(
"zen-has-hover"
);
gZenVerticalTabsManager.recalculateURLBarHeight(true);
if (!this._preventToolbarRebuild) {
setTimeout(() => {
gZenWorkspaces.updateTabsContainers();
}, 0);
}
delete this._preventToolbarRebuild;
},
get tabsWrapper() {
if (this._tabsWrapper) {
return this._tabsWrapper;
}
this._tabsWrapper = document.getElementById("zen-tabs-wrapper");
return this._tabsWrapper;
},
onTabClose(event = undefined) {
if (!event?.target?._closedInMultiselection) {
this.updateTabsToolbar();
}
},
onFloatingURLBarOpen() {
requestAnimationFrame(() => {
this.updateTabsToolbar();
});
},
openAndChangeToTab(url, options) {
if (window.ownerGlobal.parent) {
const tab = window.ownerGlobal.parent.gBrowser.addTrustedTab(
url,
options
);
window.ownerGlobal.parent.gBrowser.selectedTab = tab;
return tab;
}
const tab = window.gBrowser.addTrustedTab(url, options);
window.gBrowser.selectedTab = tab;
return tab;
},
generateUuidv4() {
return Services.uuid.generateUUID().toString();
},
createValidXULText(text) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
},
/**
* Adds the 'has-popup-menu' attribute to the element when popup is opened on it.
*
* @param {Element} element element to track
*/
addPopupTrackingAttribute(element) {
this._popupTrackingElements.push(element);
},
removePopupTrackingAttribute(element) {
this._popupTrackingElements.remove(element);
},
// On macOS, the app menu panel is displayed as a native NSPopover which
// silently clips content beyond the screen without informing Firefox's
// layout engine. This makes bottom menu items unreachable by scrolling.
// Setting max-height based on available screen space lets Firefox's layout
// handle the constraint, enabling proper overflow scrolling.
// See gh-12782
_constrainNativePopoverHeight(panel) {
if (panel.id !== "appMenu-popup") {
return;
}
// NSPopover adds 13px of chrome on all sides (26px vertical total),
// measured via Accessibility Inspector on macOS 26 (Tahoe).
// Previous macOS versions have similar or smaller values, so this is a
// conservative upper bound.
const popoverChrome = 26;
panel.style.maxHeight = `${window.screen.availHeight - popoverChrome}px`;
},
onPopupShowing(showEvent) {
if (
AppConstants.platform === "macosx" &&
Services.prefs.getBoolPref("widget.macos.native-context-menus", false)
) {
this._constrainNativePopoverHeight(showEvent.target);
}
for (const el of this._popupTrackingElements) {
// target may be inside a shadow root, not directly under the element
// we also ignore menus inside panels
if (
!el.contains(showEvent.explicitOriginalTarget) ||
(Element.isInstance(showEvent.explicitOriginalTarget) &&
showEvent.explicitOriginalTarget?.closest("panel")) ||
// See bug #7590: Ignore menupopup elements opening.
// Also see #10612 for the exclusion of the zen-appcontent-navbar-wrapper
(showEvent.explicitOriginalTarget.tagName === "menupopup" &&
el.id !== "zen-appcontent-navbar-wrapper")
) {
continue;
}
document.removeEventListener("mousemove", this.__removeHasPopupAttribute);
gZenCompactModeManager._setElementExpandAttribute(
el,
true,
"has-popup-menu"
);
this.__currentPopup = showEvent.target;
this.__currentPopupTrackElement = el;
break;
}
},
onPopupHidden(hideEvent) {
if (!this.__currentPopup || this.__currentPopup !== hideEvent.target) {
return;
}
const element = this.__currentPopupTrackElement;
if (document.getElementById("main-window").matches(":hover")) {
gZenCompactModeManager._setElementExpandAttribute(
element,
false,
"has-popup-menu"
);
} else {
this.__removeHasPopupAttribute = () =>
gZenCompactModeManager._setElementExpandAttribute(
element,
false,
"has-popup-menu"
);
document.addEventListener("mousemove", this.__removeHasPopupAttribute, {
once: true,
});
}
this.__currentPopup = null;
this.__currentPopupTrackElement = null;
},
// Section: URL bar
onUrlbarOpen() {
setTimeout(() => {
const hadValid = gURLBar.getAttribute("pageproxystate") === "valid";
gURLBar.setPageProxyState("invalid", false);
gURLBar.setAttribute("had-proxystate", hadValid);
}, 0);
},
onUrlbarClose() {
if (gURLBar.getAttribute("had-proxystate") == "true") {
gURLBar.setPageProxyState("valid", false);
}
gURLBar.removeAttribute("had-proxystate");
},
onUrlbarSearchModeChanged(event) {
const { searchMode } = event.detail;
const input = gURLBar;
if (gURLBar.hasAttribute("breakout-extend") && !this._animatingSearchMode) {
this._animatingSearchMode = true;
this.motion
.animate(input, { scale: [1, 0.98, 1] }, { duration: 0.25 })
.then(() => {
delete this._animatingSearchMode;
});
if (searchMode) {
gURLBar.setAttribute("animate-searchmode", "true");
this._animatingSearchModeTimeout = setTimeout(() => {
requestAnimationFrame(() => {
gURLBar.removeAttribute("animate-searchmode");
delete this._animatingSearchModeTimeout;
});
}, 1000);
}
}
},
enableCommandsMode(event) {
event.preventDefault();
if (!gURLBar.hasAttribute("breakout-extend") || this._animatingSearchMode) {
return;
}
const currentSearchMode = gURLBar.getSearchMode(gBrowser.selectedBrowser);
let searchMode = null;
if (!currentSearchMode) {
searchMode = {
source: UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS,
isPreview: true,
};
}
gURLBar.removeAttribute("animate-searchmode");
if (this._animatingSearchModeTimeout) {
clearTimeout(this._animatingSearchModeTimeout);
delete this._animatingSearchModeTimeout;
}
gURLBar.searchMode = searchMode;
gURLBar.startQuery({
allowAutofill: false,
event,
});
},
get newtabButtons() {
return document.querySelectorAll("#tabs-newtab-button");
},
_prevUrlbarLabel: null,
_lastSearch: "",
_clearTimeout: null,
_lastTab: null,
// Check if browser elements are in a valid state for tab operations
_validateBrowserState() {
// Check if browser window is still open
if (window.closed) {
return false;
}
// Check if gBrowser is available
if (!gBrowser || !gBrowser.tabContainer) {
return false;
}
// Check if URL bar is available
if (!gURLBar) {
return false;
}
return true;
},
handleNewTab(
werePassedURL,
searchClipboard,
where,
overridePreferance = false
) {
// Validate browser state first
if (!this._validateBrowserState()) {
console.warn("Browser state invalid for new tab operation");
return false;
}
if (this.testingEnabled && !overridePreferance) {
return false;
}
const shouldOpenURLBar =
overridePreferance ||
(gZenVerticalTabsManager._canReplaceNewTab &&
!werePassedURL &&
!searchClipboard &&
where === "tab");
if (!shouldOpenURLBar) {
return false;
}
// Close the new tab popup on cmd/ctrl + t
if (!overridePreferance && gURLBar.hasAttribute("zen-newtab")) {
this.handleUrlbarClose();
return true;
}
// Clear any existing timeout
if (this._clearTimeout) {
clearTimeout(this._clearTimeout);
this._clearTimeout = null;
}
// Store the current tab
this._lastTab = gBrowser.selectedTab;
if (!this._lastTab) {
console.warn("No selected tab found when creating new tab");
return false;
}
// Set visual state with proper validation
if (this._lastTab && !this._lastTab.closing) {
this._lastTab._visuallySelected = false;
}
// Store URL bar state
this._prevUrlbarLabel = gURLBar._untrimmedValue || "";
// Set up URL bar for new tab
gURLBar._zenHandleUrlbarClose = this.handleUrlbarClose.bind(this);
gURLBar.setAttribute("zen-newtab", true);
// Update newtab buttons
for (const button of this.newtabButtons) {
button.setAttribute("in-urlbar", true);
}
// Open location command
try {
gURLBar.search(this._lastSearch || "");
document.getElementById("Browser:OpenLocation").doCommand();
} catch (e) {
console.error("Error opening location in new tab:", e);
this.handleUrlbarClose(false);
return false;
}
return true;
},
clearUrlbarData() {
this._prevUrlbarLabel = null;
this._lastSearch = "";
},
handleUrlbarClose(onSwitch = false, onElementPicked = false) {
// Validate browser state first
if (!this._validateBrowserState()) {
console.warn("Browser state invalid for URL bar close operation");
return;
}
// Reset URL bar state
if (gURLBar._zenHandleUrlbarClose) {
gURLBar._zenHandleUrlbarClose = null;
}
const isFocusedBefore = gURLBar.focused;
setTimeout(() => {
// We use this attribute on Tabbrowser::addTab
gURLBar.removeAttribute("zen-newtab");
// Safely restore tab visual state with proper validation
if (
this._lastTab &&
!this._lastTab.closing &&
this._lastTab.ownerGlobal &&
!this._lastTab.ownerGlobal.closed &&
gBrowser.selectedTab === this._lastTab
) {
this._lastTab._visuallySelected = true;
this._lastTab = null;
}
// Reset newtab buttons
for (const button of this.newtabButtons) {
button.removeAttribute("in-urlbar");
}
// Handle search data
if (onSwitch) {
this.clearUrlbarData();
} else {
this._lastSearch = gURLBar._untrimmedValue || "";
if (this._clearTimeout) {
clearTimeout(this._clearTimeout);
}
this._clearTimeout = setTimeout(() => {
this.clearUrlbarData();
}, this.urlbarWaitToClear);
}
// Safely restore URL bar state with proper validation
if (this._prevUrlbarLabel) {
gURLBar.setURI({
uri: this._prevUrlbarLabel,
dueToTabSwitch: onSwitch,
isSameDocument: !onSwitch,
});
}
gURLBar.handleRevert();
if (isFocusedBefore) {
setTimeout(() => {
window.dispatchEvent(
new CustomEvent("ZenURLBarClosed", {
detail: { onSwitch, onElementPicked },
})
);
gURLBar.view.close({ elementPicked: onElementPicked });
gURLBar.updateTextOverflow();
if (onElementPicked && onSwitch) {
gURLBar.setURI({ dueToTabSwitch: onSwitch });
}
// Ensure tab and browser are valid before updating state
const selectedTab = gBrowser.selectedTab;
if (
selectedTab &&
selectedTab.linkedBrowser &&
!selectedTab.closing &&
onSwitch
) {
const browserState = gURLBar.getBrowserState(
selectedTab.linkedBrowser
);
if (browserState) {
browserState.urlbarFocused = false;
}
}
}, 0);
}
}, 0);
},
urlbarTrim(aURL) {
if (gURLBar.hasAttribute("breakout-extend")) {
return aURL;
}
if (
gZenVerticalTabsManager._hasSetSingleToolbar &&
this.urlbarShowDomainOnly
) {
let url = BrowserUIUtils.removeSingleTrailingSlashFromURL(aURL);
return url.startsWith("https://") ? url.split("/")[2] : url;
}
return BrowserUIUtils.trimURL(aURL);
},
// Section: Notification messages
_createToastElement(messageId, options) {
const createButton = () => {
const button = document.createXULElement("button");
button.id = options.button.id;
button.classList.add("footer-button");
button.classList.add("primary");
button.addEventListener("command", options.button.command);
return button;
};
// Check if this message ID already exists
for (const child of this._toastContainer.children) {
if (child._messageId === messageId) {
child.removeAttribute("button");
if (options.button) {
const button = createButton();
const existingButton = child.querySelector("button");
if (existingButton) {
existingButton.remove();
}
child.appendChild(button);
child.setAttribute("button", true);
}
return [child, true];
}
}
const wrapper = document.createXULElement("hbox");
const element = document.createXULElement("vbox");
const label = document.createXULElement("label");
document.l10n.setAttributes(label, messageId, options.l10nArgs);
element.appendChild(label);
if (options.descriptionId) {
const description = document.createXULElement("label");
description.classList.add("description");
document.l10n.setAttributes(description, options.descriptionId, options);
element.appendChild(description);
}
wrapper.appendChild(element);
if (options.button) {
const button = createButton();
wrapper.appendChild(button);
wrapper.setAttribute("button", true);
}
wrapper.classList.add("zen-toast");
wrapper._messageId = messageId;
return [wrapper, false];
},
async showToast(messageId, options = {}) {
const [toast, reused] = this._createToastElement(messageId, options);
this._toastContainer.removeAttribute("hidden");
this._toastContainer.appendChild(toast);
const timeoutFunction = () => {
if (Services.prefs.getBoolPref("ui.popup.disable_autohide")) {
return;
}
this.motion
.animate(
toast,
{ opacity: [1, 0], scale: [1, 0.5] },
{ duration: 0.2, bounce: 0 }
)
.then(() => {
toast.remove();
if (this._toastContainer.children.length === 0) {
this._toastContainer.setAttribute("hidden", true);
}
});
};
if (reused) {
await this.motion.animate(
toast,
{ scale: 0.2 },
{ duration: 0.1, bounce: 0 }
);
} else {
toast.addEventListener("mouseover", () => {
if (this._toastTimeouts[messageId]) {
clearTimeout(this._toastTimeouts[messageId]);
}
});
toast.addEventListener("mouseout", () => {
if (this._toastTimeouts[messageId]) {
clearTimeout(this._toastTimeouts[messageId]);
}
this._toastTimeouts[messageId] = setTimeout(
timeoutFunction,
options.timeout || 2000
);
});
}
if (!toast.style.transform) {
toast.style.transform = "scale(0)";
}
await this.motion.animate(
toast,
{ scale: 1 },
{ type: "spring", bounce: 0.2, duration: 0.5 }
);
if (this._toastTimeouts[messageId]) {
clearTimeout(this._toastTimeouts[messageId]);
}
this._toastTimeouts[messageId] = setTimeout(
timeoutFunction,
options.timeout || 2000
);
},
panelUIPosition(panel, anchor) {
void panel;
// The alignment position of the panel is determined during the "popuppositioned" event
// when the panel opens. The alignment positions help us determine in which orientation
// the panel is anchored to the screen space.
//
// * "after_start": The panel is anchored at the top-left corner in LTR locales, top-right in RTL locales.
// * "after_end": The panel is anchored at the top-right corner in LTR locales, top-left in RTL locales.
// * "before_start": The panel is anchored at the bottom-left corner in LTR locales, bottom-right in RTL locales.
// * "before_end": The panel is anchored at the bottom-right corner in LTR locales, bottom-left in RTL locales.
//
// ┌─Anchor(LTR) ┌─Anchor(RTL)
// │ Anchor(RTL)─┐ │ Anchor(LTR)─┐
// │ │ │ │
// x───────────────────x x───────────────────x
// │ │ │ │
// │ Panel │ │ Panel │
// │ "after_start" │ │ "after_end" │
// │ │ │ │
// └───────────────────┘ └───────────────────┘
//
// ┌───────────────────┐ ┌───────────────────┐
// │ │ │ │
// │ Panel │ │ Panel │
// │ "before_start" │ │ "before_end" │
// │ │ │ │
// x───────────────────x x───────────────────x
// │ │ │ │
// │ Anchor(RTL)─┘ │ Anchor(LTR)─┘
// └─Anchor(LTR) └─Anchor(RTL)
//
// The default choice for the panel is "after_start", to match the content context menu's alignment. However, it is
// possible to end up with any of the four combinations. Before the panel is opened, the XUL popup manager needs to
// make a determination about the size of the panel and whether or not it will fit within the visible screen area with
// the intended alignment. The manager may change the panel's alignment before opening to ensure the panel is fully visible.
//
// For example, if the panel is opened such that the bottom edge would be rendered off screen, then the XUL popup manager
// will change the alignment from "after_start" to "before_start", anchoring the panel's bottom corner to the target screen
// location instead of its top corner. This transformation ensures that the whole of the panel is visible on the screen.
//
// When the panel is anchored by one of its bottom corners (the "before_..." options), then it causes unintentionally odd
// behavior where dragging the text-area resizer downward with the mouse actually grows the panel's top edge upward, since
// the bottom of the panel is anchored in place. We want to disable the resizer if the panel was positioned to be anchored
// from one of its bottom corners.
let block = "bottomleft";
let inline = "topleft";
if (anchor?.closest("#zen-sidebar-top-buttons")) {
block = "topleft";
}
if (
(gZenVerticalTabsManager._hasSetSingleToolbar &&
gZenVerticalTabsManager._prefsRightSide) ||
(panel?.id === "zen-unified-site-data-panel" &&
!gZenVerticalTabsManager._hasSetSingleToolbar) ||
(panel?.id === "unified-extensions-panel" &&
gZenVerticalTabsManager._hasSetSingleToolbar)
) {
block = "bottomright";
inline = "topright";
}
return `${block} ${inline}`;
},
urlStringsDomainMatch(url1, url2) {
if (!url1.startsWith("http") || !url2?.startsWith("http")) {
return false;
}
return Services.io.newURI(url1).host === Services.io.newURI(url2).host;
},
getOpenUILinkWhere(url, browser, openUILinkWhere) {
try {
let tab = gBrowser.getTabForBrowser(browser);
if (
openUILinkWhere === "current" &&
!this.urlStringsDomainMatch(url, browser.currentURI.spec) &&
tab.pinned &&
Services.prefs.getBoolPref("zen.tabs.open-pinned-in-new-tab")
) {
return "tab";
}
} catch (e) {
console.error("Error in getOpenUILinkWhere:", e);
}
return openUILinkWhere;
},
};
XPCOMUtils.defineLazyPreferenceGetter(
gZenUIManager,
"contentElementSeparation",
"zen.theme.content-element-separation",
0
);
XPCOMUtils.defineLazyPreferenceGetter(
gZenUIManager,
"urlbarWaitToClear",
"zen.urlbar.wait-to-clear",
0
);
XPCOMUtils.defineLazyPreferenceGetter(
gZenUIManager,
"urlbarShowDomainOnly",
"zen.urlbar.show-domain-only-in-sidebar",
true
);
window.gZenVerticalTabsManager = {
init() {
this._multiWindowFeature = new nsZenMultiWindowFeature();
this._initWaitPromise();
ChromeUtils.defineLazyGetter(this, "isWindowsStyledButtons", () => {
return !(
window.AppConstants.platform === "macosx" ||
window.matchMedia("(-moz-gtk-csd-reversed-placement)").matches ||
Services.prefs.getBoolPref(
"zen.view.experimental-force-window-controls-left"
)
);
});
ChromeUtils.defineLazyGetter(this, "hidesTabsToolbar", () => {
return (
document.documentElement
.getAttribute("chromehidden")
?.includes("toolbar") ||
document.documentElement
.getAttribute("chromehidden")
?.includes("menubar")
);
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_canReplaceNewTab",
"zen.urlbar.replace-newtab",
true
);
var updateEvent = this._updateEvent.bind(this);
var onPrefChange = this._onPrefChange.bind(this);
this.initializePreferences(onPrefChange);
this._toolbarOriginalParent =
document.getElementById("nav-bar").parentElement;
gZenCompactModeManager.addEventListener(updateEvent);
this.initRightSideOrderContextMenu();
window.addEventListener(
"customizationstarting",
this._preCustomize.bind(this)
);
window.addEventListener(
"aftercustomization",
this._postCustomize.bind(this)
);
this._updateEvent();
if (!this.isWindowsStyledButtons) {
document.documentElement.setAttribute(
"zen-window-buttons-reversed",
true
);
}
this._renameTabHalt = this.renameTabHalt.bind(this);
gBrowser.tabContainer.addEventListener(
"dblclick",
this.renameTabStart.bind(this)
);
},
toggleExpand() {
const newVal = !Services.prefs.getBoolPref("zen.view.sidebar-expanded");
Services.prefs.setBoolPref("zen.view.sidebar-expanded", newVal);
Services.prefs.setBoolPref("zen.view.use-single-toolbar", false);
},
get navigatorToolbox() {
return gNavToolbox;
},
initRightSideOrderContextMenu() {
const kConfigKey = "zen.tabs.vertical.right-side";
const fragment = window.MozXULElement.parseXULToFragment(`
<menuitem id="zen-toolbar-context-tabs-right"
type="checkbox"
${Services.prefs.getBoolPref(kConfigKey) ? 'checked="true"' : ""}
data-lazy-l10n-id="zen-toolbar-context-tabs-right"
command="cmd_zenToggleTabsOnRight"
/>
`);
document.getElementById("viewToolbarsMenuSeparator").before(fragment);
},
get _topButtonsSeparatorElement() {
if (this.__topButtonsSeparatorElement) {
return this.__topButtonsSeparatorElement;
}
this.__topButtonsSeparatorElement = document.getElementById(
"zen-sidebar-top-buttons-separator"
);
return this.__topButtonsSeparatorElement;
},
animateItemOpen(aItem) {
if (
!gZenUIManager.motion ||
!aItem ||
!gZenUIManager._hasLoadedDOM ||
!aItem.isConnected ||
// We do want to do some animations during testing with profiling enabled
// so we can capture and improve them.
(gZenUIManager.testingEnabled && !gZenUIManager.profilingEnabled) ||
!gZenStartup.isReady ||
aItem.group?.hasAttribute("split-view-group")
) {
return;
}
// get next visible tab
const isLastItem = () => {
const visibleItems = gBrowser.tabContainer.ariaFocusableItems;
return visibleItems[visibleItems.length - 1] === aItem;
};
try {
const itemSize = aItem.getBoundingClientRect().height;
const transform = `-${itemSize}px`;
gZenUIManager.motion
.animate(
aItem,
{
opacity: [0, 1],
transform: ["scale(0.95)", "scale(1)"],
marginBottom: isLastItem() ? ["0px", "0px"] : [transform, "0px"],
},
{
duration: 0.1,
easing: "easeOut",
}
)
.then(() => {})
.catch(err => {
console.error(err);
})
.finally(() => {
aItem.style.removeProperty("margin-bottom");
aItem.style.removeProperty("transform");
aItem.style.removeProperty("opacity");
});
const itemLabel =
aItem.querySelector(".tab-group-label-container") ||
aItem.querySelector(".tab-content");
gZenUIManager.motion
.animate(
itemLabel,
{
filter: ["blur(1px)", "blur(0px)"],
},
{
duration: 0.075,
easing: "easeOut",
}
)
.then(() => {})
.catch(err => {
console.error(err);
})
.finally(() => {
itemLabel.style.removeProperty("filter");
});
} catch (e) {
console.error(e);
}
},
animateItemClose(aItem) {
if (
aItem.hasAttribute("zen-essential") ||
aItem.group?.hasAttribute("split-view-group") ||
!gZenUIManager.motion ||
gReduceMotion
) {
return Promise.resolve();
}
const height = aItem.getBoundingClientRect().height;
const visibleItems = gBrowser.tabContainer.ariaFocusableItems;
const isLastItem = visibleItems[visibleItems.length - 1] === aItem;
return gZenUIManager.motion.animate(
aItem,
{
opacity: [1, 0],
transform: ["scale(1)", "scale(0.95)"],
...(isLastItem
? {}
: {
marginBottom: [`0px`, `-${height}px`],
}),
},
{
duration: 0.075,
easing: "easeOut",
}
);
},
get actualWindowButtons() {
// we have multiple ".titlebar-buttonbox-container" in the DOM, because of the titlebar
if (!this.__actualWindowButtons) {
this.__actualWindowButtons = !this.isWindowsStyledButtons
? document.querySelector(".titlebar-buttonbox-container") // TODO: test if it works 100% of the time
: document.querySelector("#nav-bar .titlebar-buttonbox-container");
this.__actualWindowButtons.setAttribute("overflows", "false");
}
return this.__actualWindowButtons;
},
async _preCustomize() {
await this._multiWindowFeature.foreachWindowAsActive(async browser => {
browser.gZenVerticalTabsManager._updateEvent({
forCustomizableMode: true,
dontRebuildAreas: true,
});
});
this.rebuildAreas();
this.navigatorToolbox.setAttribute("zen-sidebar-expanded", "true");
document.documentElement.setAttribute("zen-sidebar-expanded", "true"); // force expanded sidebar
},
_postCustomize() {
// No need to use `await` here, because the customization is already done
this._multiWindowFeature.foreachWindowAsActive(async browser => {
browser.gZenVerticalTabsManager._updateEvent({ dontRebuildAreas: true });
});
},
initializePreferences(updateEvent) {
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_prefsVerticalTabs",
"zen.tabs.vertical",
true,
updateEvent
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_prefsRightSide",
"zen.tabs.vertical.right-side",
false,
updateEvent
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_prefsUseSingleToolbar",
"zen.view.use-single-toolbar",
false,
updateEvent
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_prefsSidebarExpanded",
"zen.view.sidebar-expanded",
false,
updateEvent
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_prefsSidebarExpandedMaxWidth",
"zen.view.sidebar-expanded.max-width",
300,
updateEvent
);
},
_initWaitPromise() {
this._waitPromise = new Promise(resolve => {
this._resolveWaitPromise = resolve;
});
},
async _onPrefChange() {
this._resolveWaitPromise();
// only run if we are in the active window
await this._multiWindowFeature.foreachWindowAsActive(async browser => {
if (
browser.gZenVerticalTabsManager._multiWindowFeature.windowIsActive(
browser
)
) {
return;
}
await browser.gZenVerticalTabsManager._waitPromise;
browser.gZenVerticalTabsManager._updateEvent({ dontRebuildAreas: true });
browser.gZenVerticalTabsManager._initWaitPromise();
});
if (nsZenMultiWindowFeature.isActiveWindow) {
this._updateEvent();
this._initWaitPromise();
}
},
recalculateURLBarHeight(updateFormat = false) {
if (gZenWorkspaces._processingResize) {
return;
}
requestAnimationFrame(() => {
requestAnimationFrame(() => {
gURLBar.removeAttribute("--urlbar-height");
let height;
if (!this._hasSetSingleToolbar) {
height = AppConstants.platform == "macosx" ? 34 : 32;
} else if (gURLBar.getAttribute("breakout-extend") !== "true") {
height = 38;
}
if (typeof height !== "undefined") {
gURLBar.style.setProperty("--urlbar-height", `${height}px`);
}
if (updateFormat) {
gURLBar.zenFormatURLValue();
}
});
});
},
// eslint-disable-next-line complexity
_updateEvent({ forCustomizableMode = false, dontRebuildAreas = false } = {}) {
if (this._isUpdating) {
return;
}
this._isUpdating = true;
try {
this._updateMaxWidth();
if (window.docShell) {
window.docShell.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIAppWindow)
.rollupAllPopups();
}
const topButtons = document.getElementById("zen-sidebar-top-buttons");
const isCompactMode =
gZenCompactModeManager.preference && !forCustomizableMode;
const isVerticalTabs = this._prefsVerticalTabs || forCustomizableMode;
const isSidebarExpanded = this._prefsSidebarExpanded || !isVerticalTabs;
const isRightSide = this._prefsRightSide && isVerticalTabs;
const isSingleToolbar =
((this._prefsUseSingleToolbar && isVerticalTabs && isSidebarExpanded) ||
!isVerticalTabs) &&
!forCustomizableMode &&
!this.hidesTabsToolbar;
const titlebar = document.getElementById("titlebar");
gBrowser.tabContainer.setAttribute(
"orient",
isVerticalTabs ? "vertical" : "horizontal"
);
gBrowser.tabContainer.arrowScrollbox.setAttribute(
"orient",
isVerticalTabs ? "vertical" : "horizontal"
);
// on purpose, we set the orient to horizontal, because the arrowScrollbox is vertical
gBrowser.tabContainer.arrowScrollbox.scrollbox.setAttribute(
"orient",
isVerticalTabs ? "vertical" : "horizontal"
);
const buttonsTarget = document.getElementById(
"zen-sidebar-top-buttons-customization-target"
);
if (isRightSide) {
this.navigatorToolbox.setAttribute("zen-right-side", "true");
document.documentElement.setAttribute("zen-right-side", "true");
} else {
this.navigatorToolbox.removeAttribute("zen-right-side");
document.documentElement.removeAttribute("zen-right-side");
}
delete this._hadSidebarCollapse;
if (isSidebarExpanded) {
this._hadSidebarCollapse = !document.documentElement.hasAttribute(
"zen-sidebar-expanded"
);
this.navigatorToolbox.setAttribute("zen-sidebar-expanded", "true");
document.documentElement.setAttribute("zen-sidebar-expanded", "true");
gBrowser.tabContainer.setAttribute("expanded", "true");
} else {
this.navigatorToolbox.removeAttribute("zen-sidebar-expanded");
document.documentElement.removeAttribute("zen-sidebar-expanded");
gBrowser.tabContainer.removeAttribute("expanded");
}
const appContentNavbarContaienr = document.getElementById(
"zen-appcontent-navbar-container"
);
const appContentNavbarWrapper = document.getElementById(
"zen-appcontent-navbar-wrapper"
);
appContentNavbarWrapper.style.transition = "none";
let shouldHide = false;
if (
((!isRightSide && this.isWindowsStyledButtons) ||
(isRightSide && !this.isWindowsStyledButtons) ||
(isCompactMode && isSingleToolbar && this.isWindowsStyledButtons)) &&
isSingleToolbar
) {
appContentNavbarWrapper.setAttribute("should-hide", true);
shouldHide = true;
} else {
appContentNavbarWrapper.removeAttribute("should-hide");
}
// Check if the sidebar is in hover mode
if (
!this.navigatorToolbox.hasAttribute("zen-right-side") &&
!isCompactMode
) {
this.navigatorToolbox.prepend(topButtons);
}
let windowButtons = this.actualWindowButtons;
let doNotChangeWindowButtons =
!isCompactMode && isRightSide && this.isWindowsStyledButtons;
const navBar = document.getElementById("nav-bar");
if (isSingleToolbar) {
this._navbarParent = navBar.parentElement;
let elements = document.querySelectorAll(
'#nav-bar-customization-target > :is([cui-areatype="toolbar"], .chromeclass-toolbar-additional):not(#urlbar-container):not(toolbarspring)'
);
elements = Array.from(elements).reverse();
// Add separator if it doesn't exist
if (!this._hasSetSingleToolbar) {
buttonsTarget.append(this._topButtonsSeparatorElement);
}
for (const button of elements) {
this._topButtonsSeparatorElement.after(button);
}
buttonsTarget.prepend(
document.getElementById("unified-extensions-button")
);
const panelUIButton = document.getElementById("PanelUI-button");
buttonsTarget.prepend(panelUIButton);
panelUIButton.setAttribute("overflows", "false");
buttonsTarget.parentElement.append(
document.getElementById("nav-bar-overflow-button")
);
if (this.isWindowsStyledButtons && !doNotChangeWindowButtons) {
appContentNavbarContaienr.append(windowButtons);
}
if (isCompactMode) {
titlebar.moveBefore(navBar, titlebar.firstChild);
titlebar.moveBefore(topButtons, titlebar.firstChild);
} else {
titlebar.parentNode.moveBefore(topButtons, titlebar);
titlebar.parentNode.moveBefore(navBar, titlebar);
}
document.documentElement.setAttribute("zen-single-toolbar", true);
this._hasSetSingleToolbar = true;
} else if (this._hasSetSingleToolbar) {
this._hasSetSingleToolbar = false;
// Do the opposite
this._navbarParent.prepend(navBar);
const elements = document.querySelectorAll(
'#zen-sidebar-top-buttons-customization-target > :is([cui-areatype="toolbar"], .chromeclass-toolbar-additional)'
);
for (const button of elements) {
document
.getElementById("nav-bar-customization-target")
.append(button);
}
this._topButtonsSeparatorElement.remove();
document.documentElement.removeAttribute("zen-single-toolbar");
const panelUIButton = document.getElementById("PanelUI-button");
navBar.appendChild(panelUIButton);
panelUIButton.removeAttribute("overflows");
navBar.appendChild(document.getElementById("nav-bar-overflow-button"));
this._toolbarOriginalParent.prepend(navBar);
if (!dontRebuildAreas) {
this.rebuildAreas();
}
}
if (isCompactMode) {
titlebar.prepend(topButtons);
} else if (isSidebarExpanded) {
titlebar.before(topButtons);
} else {
titlebar.prepend(topButtons);
}
// Case: single toolbar, not compact mode, not right side and macos styled buttons
if (
!doNotChangeWindowButtons &&
isSingleToolbar &&
!isCompactMode &&
!isRightSide &&
!this.isWindowsStyledButtons
) {
topButtons.prepend(windowButtons);
}
const canHideTabBarPref = Services.prefs.getBoolPref(
"zen.view.compact.hide-tabbar"
);
const captionsShouldStayOnSidebar =
!canHideTabBarPref &&
((!this.isWindowsStyledButtons && !isRightSide) ||
(this.isWindowsStyledButtons && isRightSide));
if (
(!isSingleToolbar && isCompactMode && !captionsShouldStayOnSidebar) ||
!isSidebarExpanded
) {
navBar.prepend(topButtons);
}
// Case: single toolbar, compact mode, right side and windows styled buttons
if (
isSingleToolbar &&
isCompactMode &&
isRightSide &&
this.isWindowsStyledButtons
) {
topButtons.prepend(windowButtons);
}
if (doNotChangeWindowButtons) {
if (isRightSide && !isSidebarExpanded) {
navBar.appendChild(windowButtons);
} else {
topButtons.appendChild(windowButtons);
}
} else if (!isSingleToolbar && !isCompactMode) {
if (this.isWindowsStyledButtons) {
if (isRightSide) {
appContentNavbarContaienr.append(windowButtons);
} else {
navBar.append(windowButtons);
}
} else {
// not windows styled buttons
// eslint-disable-next-line no-lonely-if
if (isRightSide || !isSidebarExpanded) {
navBar.prepend(windowButtons);
} else {
topButtons.prepend(windowButtons);
}
}
} else if (!isSingleToolbar && isCompactMode) {
if (captionsShouldStayOnSidebar) {
topButtons.prepend(windowButtons);
} else {
navBar.appendChild(windowButtons);
}
} else if (isSingleToolbar && isCompactMode) {
if (!isRightSide && !this.isWindowsStyledButtons) {
topButtons.prepend(windowButtons);
}
}
if (shouldHide) {
appContentNavbarContaienr.append(windowButtons);
}
if (
this._hasSetSingleToolbar &&
Services.prefs.getBoolPref("zen.view.overflow-webext-toolbar", true)
) {
topButtons.setAttribute(
"addon-webext-overflowtarget",
"zen-overflow-extensions-list"
);
} else {
topButtons.setAttribute(
"addon-webext-overflowtarget",
"overflowed-extensions-list"
);
}
gZenCompactModeManager.updateCompactModeContext(isSingleToolbar);
// Always move the splitter next to the sidebar
const splitter = document.getElementById("zen-sidebar-splitter");
splitter.addEventListener("dragover", gBrowser.tabContainer);
this.navigatorToolbox.after(splitter);
window.dispatchEvent(new Event("resize"));
if (!isCompactMode) {
gZenCompactModeManager.getAndApplySidebarWidth();
}
gZenUIManager.updateTabsToolbar();
this.rebuildURLBarMenus();
appContentNavbarWrapper.style.transition = "";
} catch (e) {
console.error(e);
}
this._isUpdating = false;
},
rebuildURLBarMenus() {
if (document.getElementById("paste-and-go")) {
return;
}
gURLBar._initCopyCutController();
gURLBar._initPasteAndGo();
gURLBar._initStripOnShare();
gURLBar._updatePlaceholderFromDefaultEngine();
},
rebuildAreas() {
CustomizableUI.zenInternalCU._rebuildRegisteredAreas(
/* zenDontRebuildCollapsed */ true
);
},
_updateMaxWidth() {
const maxWidth = Services.prefs.getIntPref(
"zen.view.sidebar-expanded.max-width"
);
const toolbox = gNavToolbox;
if (!this._prefsCompactMode) {
toolbox.style.maxWidth = `${maxWidth}px`;
} else {
toolbox.style.removeProperty("maxWidth");
}
},
get expandButton() {
if (this._expandButton) {
return this._expandButton;
}
this._expandButton = document.getElementById("zen-expand-sidebar-button");
return this._expandButton;
},
toggleTabsOnRight() {
const newVal = !Services.prefs.getBoolPref("zen.tabs.vertical.right-side");
Services.prefs.setBoolPref("zen.tabs.vertical.right-side", newVal);
},
appendCustomizableItem(target, child, placements) {
if (
target.id === "zen-sidebar-top-buttons-customization-target" &&
this._hasSetSingleToolbar &&
placements.includes(child.id)
) {
this._topButtonsSeparatorElement.before(child);
return;
}
target.appendChild(child);
},
async renameTabKeydown(event) {
event.stopPropagation();
if (event.key === "Enter") {
const isTab = !!event.target.closest(".tabbrowser-tab");
let label = isTab
? this._tabEdited.querySelector(".tab-label-container-editing")
: this._tabEdited;
let input = document.getElementById("tab-label-input");
let newName = input.value.replace(/\s+/g, " ").trim();
const hasChanged = input.value !== input._originalValue && newName;
document.documentElement.removeAttribute("zen-renaming-tab");
input.remove();
if (!isTab) {
await this._tabEdited.onRenameFinished(newName);
} else {
// Check if name is blank, reset if so
// Always remove, so we can always rename and if it's empty,
// it will reset to the original name anyway
if (hasChanged || (this._tabEdited.zenStaticLabel && newName)) {
this._tabEdited.zenStaticLabel = newName;
gBrowser._setTabLabel(this._tabEdited, newName);
gZenUIManager.showToast("zen-tabs-renamed");
} else {
delete this._tabEdited.zenStaticLabel;
gBrowser.setTabTitle(this._tabEdited);
}
gZenUIManager.motion.animate(
this._tabEdited,
{
scale: [1, 0.98, 1],
},
{
duration: 0.25,
}
);
}
const editorContainer = this._tabEdited.querySelector(
".tab-editor-container"
);
if (editorContainer) {
editorContainer.remove();
}
label.classList.remove("tab-label-container-editing");
this._tabEdited = null;
} else if (event.key === "Escape") {
event.target.blur();
}
},
renameTabStart(event) {
let target = event.target;
if (event.target.id === "context_zen-edit-tab-title") {
target = TabContextMenu.contextTab;
}
const isTab = !!target.closest(".tabbrowser-tab");
if (
this._tabEdited ||
((!Services.prefs.getBoolPref("zen.tabs.rename-tabs") ||
(Services.prefs.getBoolPref("browser.tabs.closeTabByDblclick") &&
event.type === "dblclick")) &&
isTab) ||
!gZenVerticalTabsManager._prefsSidebarExpanded
) {
return;
}
if (
isTab &&
!target.closest(".tab-label-container") &&
event.type === "dblclick"
) {
return;
}
this._tabEdited =
target.closest(".tabbrowser-tab") ||
target.closest(".zen-current-workspace-indicator-name") ||
(event.explicit && target.closest(".tab-group-label"));
if (
!this._tabEdited ||
(this._tabEdited.hasAttribute("zen-essential") && isTab)
) {
this._tabEdited = null;
return;
}
gZenFolders.cancelPopupTimer();
event.stopPropagation?.();
document.documentElement.setAttribute("zen-renaming-tab", "true");
const label = isTab
? this._tabEdited.querySelector(".tab-label-container")
: this._tabEdited;
label.classList.add("tab-label-container-editing");
if (isTab) {
const container = window.MozXULElement.parseXULToFragment(`
<vbox class="tab-label-container tab-editor-container" flex="1" align="start" pack="center"></vbox>
`);
label.after(container);
}
const input = document.createElement("input");
const content = isTab ? this._tabEdited.label : this._tabEdited.textContent;
input.id = "tab-label-input";
input._originalValue = content;
input.value = content;
input.addEventListener("keydown", this.renameTabKeydown.bind(this));
if (isTab) {
const containerHtml = this._tabEdited.querySelector(
".tab-editor-container"
);
containerHtml.appendChild(input);
} else {
this._tabEdited.after(input);
}
input.focus();
input.select();
input.addEventListener("blur", this._renameTabHalt);
},
renameTabHalt(event) {
if (document.activeElement === event.target || !this._tabEdited) {
return;
}
document.documentElement.removeAttribute("zen-renaming-tab");
const editorContainer = this._tabEdited.querySelector(
".tab-editor-container"
);
let input = document.getElementById("tab-label-input");
input.remove();
if (editorContainer) {
editorContainer.remove();
}
const isTab = !!this._tabEdited.closest(".tabbrowser-tab");
const label = isTab
? this._tabEdited.querySelector(".tab-label-container-editing")
: this._tabEdited;
label.classList.remove("tab-label-container-editing");
this._tabEdited = null;
},
};