trilium/apps/client/src/widgets/containers/root_container.ts
2026-02-26 15:52:27 +02:00

164 lines
5.6 KiB
TypeScript

import { LOCALES } from "@triliumnext/commons";
import { EventData } from "../../components/app_context.js";
import { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js";
import options from "../../services/options.js";
import utils, { isIOS, isMobile } from "../../services/utils.js";
import { readCssVar } from "../../utils/css-var.js";
import type BasicWidget from "../basic_widget.js";
import FlexContainer from "./flex_container.js";
/**
* The root container is the top-most widget/container, from which the entire layout derives.
*
* For convenience, the root container has a few class selectors that can be used to target some global state:
*
* - `#root-container.virtual-keyboard-opened`, on mobile devices if the virtual keyboard is open.
* - `#root-container.horizontal-layout`, if the current layout is horizontal.
* - `#root-container.vertical-layout`, if the current layout is horizontal.
*/
export default class RootContainer extends FlexContainer<BasicWidget> {
private originalWindowHeight: number;
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "column" : "row");
this.originalWindowHeight = window.innerHeight ?? 0;
this.id("root-widget");
this.css("height", "100dvh");
}
render(): JQuery<HTMLElement> {
if (utils.isMobile()) {
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
}
this.#initTheme();
this.#setDeviceSpecificClasses();
this.#setMaxContentWidth();
this.#setMotion();
this.#setShadows();
this.#setBackdropEffects();
this.#setThemeCapabilities();
this.#setLocaleAndDirection(options.get("locale"));
this.#setExperimentalFeatures();
this.#initPWATopbarColor();
return super.render();
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("motionEnabled")) {
this.#setMotion();
}
if (loadResults.isOptionReloaded("shadowsEnabled")) {
this.#setShadows();
}
if (loadResults.isOptionReloaded("backdropEffectsEnabled")) {
this.#setBackdropEffects();
}
if (loadResults.isOptionReloaded("maxContentWidth")
|| loadResults.isOptionReloaded("centerContent")) {
this.#setMaxContentWidth();
}
}
#initTheme() {
const colorSchemeChangeObserver = matchMedia("(prefers-color-scheme: dark)")
colorSchemeChangeObserver.addEventListener("change", () => this.#updateColorScheme());
this.#updateColorScheme();
}
#updateColorScheme() {
const colorScheme = readCssVar(document.body, "theme-style").asString();
document.body.classList.toggle("light-theme", colorScheme === "light");
document.body.classList.toggle("dark-theme", colorScheme === "dark");
}
#onMobileResize() {
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
const windowHeight = Math.max(window.innerHeight, this.originalWindowHeight); // inner height changes when keyboard is opened, we need to compare with the original height to detect it.
// If viewport is significantly smaller, keyboard is likely open
const isKeyboardOpened = windowHeight - viewportHeight > 150;
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
}
#setMaxContentWidth() {
const width = Math.max(options.getInt("maxContentWidth") || 0, 640);
document.body.style.setProperty("--preferred-max-content-width", `${width}px`);
document.body.classList.toggle("prefers-centered-content", options.is("centerContent"));
}
#setMotion() {
const enabled = options.is("motionEnabled");
document.body.classList.toggle("motion-disabled", !enabled);
jQuery.fx.off = !enabled;
}
#setShadows() {
const enabled = options.is("shadowsEnabled");
document.body.classList.toggle("shadows-disabled", !enabled);
}
#setBackdropEffects() {
const enabled = options.is("backdropEffectsEnabled") && !isMobile();
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
}
#setThemeCapabilities() {
// Supports background effects
const useBgfx = readCssVar(document.documentElement, "allow-background-effects")
.asBoolean(false);
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
}
#setExperimentalFeatures() {
for (const featureId of getEnabledExperimentalFeatureIds()) {
document.body.classList.add(`experimental-feature-${featureId}`);
}
}
#setLocaleAndDirection(locale: string) {
const correspondingLocale = LOCALES.find(l => l.id === locale);
document.body.lang = locale;
document.body.dir = correspondingLocale?.rtl ? "rtl" : "ltr";
}
#setDeviceSpecificClasses() {
if (isIOS()) {
document.body.classList.add("ios");
}
}
#initPWATopbarColor() {
if (!utils.isPWA()) return;
const tracker = $("#background-color-tracker");
if (tracker.length) {
const applyThemeColor = () => {
let meta = $("meta[name='theme-color']");
if (!meta.length) {
meta = $(`<meta name="theme-color">`).appendTo($("head"));
}
meta.attr("content", tracker.css("color"));
};
tracker.on("transitionend", applyThemeColor);
applyThemeColor();
}
}
}