UNPKG

@ribajs/bs5

Version:

Bootstrap 5 module for Riba.js

199 lines (179 loc) 5.5 kB
import { EventDispatcher } from "@ribajs/events"; import { themeChoices } from "../constants/index.js"; import { Bs5Service } from "./bs5.service.js"; import type { ThemeChoice, ThemeData, ThemeChangedCallback, } from "../types/index.js"; /** * The theme service is used to change and observe the color scheme from the OS or the user. * Can be dark or light respectively. * @credits https://stackoverflow.com/questions/56393880/how-do-i-detect-dark-mode-using-javascript */ export class ThemeService { protected eventDispatcher = EventDispatcher.getInstance(); protected static instance?: ThemeService; protected bs5 = Bs5Service.getSingleton(); public current: ThemeChoice = "os"; protected constructor() { this.addEventListeners(); const data = this.init(); const selectEl = document.getElementById( "theme", ) as HTMLSelectElement | null; if (selectEl) this.select(data.choice, selectEl); } public static getSingleton() { if (ThemeService.instance) { return ThemeService.instance; } return this.setSingleton(); } public static setSingleton() { if (ThemeService.instance) { throw new Error(`Singleton of ThemeService already defined!`); } ThemeService.instance = new ThemeService(); return ThemeService.instance; } protected addEventListeners() { // To watch for changes window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", () => { this.set(this.current); }); } /** * Sets the last selected theme (which was stored in the localStorage) * @param choices The theme theme-(light, dark or os) * @returns The selected theme or or `null` in case of an error */ public init() { let savedTheme: ThemeChoice = "os"; if (this.bs5.options.allowStoreDataInBrowser) { savedTheme = (localStorage.getItem("bs5-theme") || "os") as ThemeChoice; } return this.set(savedTheme); } /** * Selects the option of a global theme select element * @param selectEl * @param choice */ public select(choice: ThemeChoice, selectEl: HTMLSelectElement) { if (selectEl?.nodeName === "SELECT") { selectEl.value = choice; } } /** * Get the default data for internal use. * The default is the system light theme. * @returns */ protected getDefaultData(): ThemeData { return { supported: !!window.matchMedia, // TODO more checks? isDark: false, isLight: false, systemIsDark: false, systemIsLight: true, bySystem: true, byUser: false, choice: "os", }; } protected triggerChange(e?: MediaQueryListEvent, oldValue?: ThemeData) { if (!oldValue) { oldValue = this.getDefaultData(); } const newValue = this.getScheme(); this.eventDispatcher.trigger("theme-change", { oldValue, newValue, }); return newValue; } /** * Adds an one time event listener triggered on theme change * @param cb * @param thisContext */ onceChange(cb: ThemeChangedCallback, thisContext?: any): void { this.eventDispatcher.once("theme-change", cb, thisContext); } /** * Adds an event listener triggered on theme change * @param cb * @param thisContext */ onChange(cb: ThemeChangedCallback, thisContext?: any): void { this.eventDispatcher.on("theme-change", cb, thisContext); } /** * Removes the theme changed event listener * @param cb * @param thisContext */ offChange(cb?: ThemeChangedCallback, thisContext?: any): void { this.eventDispatcher.off("theme-change", cb, thisContext); } /** * Sets the theme class to the <html>. * - `os` means that the theme will be used by the operating system (which corresponds to light or dark) * - `light` means that the light theme will be used * - `dark` means that the dark theme will be used * @param choices * @returns The selected theme */ set(newColorScheme: ThemeChoice): ThemeData { const oldScheme = this.getScheme(); if (!themeChoices.includes(newColorScheme)) { console.warn( `Unsupported theme "${newColorScheme}", set instead the default "os".`, ); newColorScheme = "os"; } if (this.bs5.options.allowStoreDataInBrowser) { localStorage.setItem("bs5-theme", newColorScheme); } if (newColorScheme === "os") { const newScheme = this.getScheme(newColorScheme); document.documentElement.setAttribute( "data-bs-theme", newScheme.isDark ? "dark" : "light", ); } else { document.documentElement.setAttribute("data-bs-theme", newColorScheme); } this.current = newColorScheme; return this.triggerChange(undefined, oldScheme); } getScheme(choice: ThemeChoice = this.current): ThemeData { const data = this.getDefaultData(); data.systemIsDark = window.matchMedia( "(prefers-color-scheme: dark)", ).matches; data.systemIsLight = !data.systemIsDark; if (choice === "os") { data.bySystem = true; data.byUser = false; data.choice = "os"; data.isDark = data.systemIsDark; data.isLight = data.systemIsLight; } else { data.bySystem = false; data.byUser = true; if (choice === "dark") { data.isDark = true; data.choice = "dark"; } if (choice === "light") { data.isLight = true; data.choice = "light"; } } return data; } }