diff --git a/src/Core.Assets/src/Design/ThemeStorage.ts b/src/Core.Assets/src/Design/ThemeStorage.ts index f0e67a30fc..023a48103f 100644 --- a/src/Core.Assets/src/Design/ThemeStorage.ts +++ b/src/Core.Assets/src/Design/ThemeStorage.ts @@ -1,5 +1,25 @@ import { DesignTheme } from "../DesignTheme"; +type ThemeStorageValue = { mode: string | null, primaryColor: string | null, neutralColor: string | null }; + +type StorageLike = Pick; + +const memoryStorage = (() => { + const values: Record = {}; + + return { + getItem(key: string): string | null { + return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : null; + }, + setItem(key: string, value: string): void { + values[key] = value; + }, + removeItem(key: string): void { + delete values[key]; + }, + } satisfies StorageLike; +})(); + class ThemeStorage { private _designTheme: DesignTheme @@ -19,13 +39,21 @@ class ThemeStorage { return this._designTheme.storageName; } + private static getStorage(): StorageLike { + try { + const storage = window.localStorage; + if (storage == null) { + return memoryStorage; + } - public updateLocalStorage(mode: string | null, primaryColor: string | null, neutralColor: string | null): void { - - // If LocalStorage is not available, do nothing. - if (localStorage == null) { - return; + return storage; + } catch { + return memoryStorage; } + } + + public updateLocalStorage(mode: string | null, primaryColor: string | null, neutralColor: string | null): void { + const storage = ThemeStorage.getStorage(); // Wait the component to be initialized if (!this._designTheme._isInitialized) { @@ -37,56 +65,60 @@ class ThemeStorage { return; } - // Save to the localstorage - localStorage.setItem(this.storageName, JSON.stringify({ + const theme: ThemeStorageValue = { mode: ThemeStorage.getValueOrNull(mode), primaryColor: ThemeStorage.getValueOrNull(primaryColor), neutralColor: ThemeStorage.getValueOrNull(neutralColor), - })); - } - - public readLocalStorage(): { mode: string | null, primaryColor: string | null, neutralColor: string | null } | null { + }; - // If LocalStorage is not available, do nothing. - if (localStorage == null) { - return null; + try { + storage.setItem(this.storageName, JSON.stringify(theme)); + } catch { + // Ignore storage write failures and continue with in-memory theme state. } + } + + public readLocalStorage(): ThemeStorageValue | null { + const storage = ThemeStorage.getStorage(); // Check if storageName attribute is defined if (this.storageName == null) { return null; } - // Check if localstorage exists - const storageJson = localStorage.getItem(this.storageName); + try { + const storageJson = storage.getItem(this.storageName); + if (storageJson == null) { + return null; + } + + // Read the localstorage + const storageItems = JSON.parse(storageJson); - if (storageJson == null) { + return { + mode: ThemeStorage.getValueOrNull(storageItems?.mode), + primaryColor: ThemeStorage.getValueOrNull(storageItems?.primaryColor), + neutralColor: ThemeStorage.getValueOrNull(storageItems?.neutralColor), + }; + } catch { + this.clearLocalStorage(); return null; } + } - // Read the localstorage - const storageItems = JSON.parse(storageJson); + public clearLocalStorage(): void { + const storage = ThemeStorage.getStorage(); - return { - mode: ThemeStorage.getValueOrNull(storageItems?.mode), - primaryColor: ThemeStorage.getValueOrNull(storageItems?.primaryColor), - neutralColor: ThemeStorage.getValueOrNull(storageItems?.neutralColor), + // Check if storageName attribute is defined + if (this.storageName == null) { + return; } - } - public clearLocalStorage(): void { - // If LocalStorage is not available, do nothing. - if (localStorage == null) { - return; - } - - // Check if storageName attribute is defined - if (this.storageName == null) { - return; - } - - // Clear the localstorage - localStorage.removeItem(this.storageName); + try { + storage.removeItem(this.storageName); + } catch { + // Ignore storage clear failures and continue with in-memory theme state. + } } /** diff --git a/src/Core/wwwroot/js/loading-theme.js b/src/Core/wwwroot/js/loading-theme.js index 9529e56e9d..48075c0a27 100644 --- a/src/Core/wwwroot/js/loading-theme.js +++ b/src/Core/wwwroot/js/loading-theme.js @@ -2,6 +2,55 @@ // Here we'll find the first web component and wait for it to be upgraded. // When it is, we'll remove this invisibility from the body. +const memoryStorage = (() => { + const values = {}; + + return { + getItem(key) { + return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : null; + }, + setItem(key, value) { + values[key] = String(value); + }, + removeItem(key) { + delete values[key]; + } + }; +})(); + +function getThemeStorage() { + try { + const storage = window.localStorage; + if (storage == null) { + return memoryStorage; + } + + return storage; + } catch { + return memoryStorage; + } +} + +function readStoredTheme(storageName) { + if (!storageName) { + return null; + } + + const storage = getThemeStorage(); + + try { + const theme = storage.getItem(storageName); + return theme ? JSON.parse(theme) : null; + } catch { + try { + storage.removeItem(storageName); + } catch { + } + + return null; + } +} + class LoadingTheme extends HTMLElement { className = "hidden-body"; @@ -17,17 +66,12 @@ class LoadingTheme extends HTMLElement { // Custom element added to page. connectedCallback() { - - // If LocalStorage is not available, do nothing. - if (localStorage == null) { - return; - } - // Attributes const storageName = this.getAttribute("storage-name"); const mode = this.getAttribute("mode"); const primaryColor = this.getAttribute("primary-color"); const neutralColor = this.getAttribute("neutral-color"); + const storedTheme = readStoredTheme(storageName); const isDark = (modeSaved, isSystemDark) => { switch (modeSaved) { @@ -41,9 +85,9 @@ class LoadingTheme extends HTMLElement { }; // Compute the saved or the system theme (dark/light). - const modeSaved = mode ?? JSON.parse(localStorage.getItem(storageName))?.mode; - const primaryColorSaved = primaryColor ?? JSON.parse(localStorage.getItem(storageName))?.primaryColor; - const neutralColorSaved = neutralColor ?? JSON.parse(localStorage.getItem(storageName))?.neutralColor; + const modeSaved = mode ?? storedTheme?.mode; + const primaryColorSaved = primaryColor ?? storedTheme?.primaryColor; + const neutralColorSaved = neutralColor ?? storedTheme?.neutralColor; const isSystemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const bgColor = isDark(modeSaved, isSystemDark) ? this.defaultDarkColor : this.defaultLightColor;