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 { 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 { 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 = $(``).appendTo($("head")); } meta.attr("content", tracker.css("color")); }; tracker.on("transitionend", applyThemeColor); applyThemeColor(); } } }