UNPKG

@rws-framework/client

Version:

This package provides the core client-side framework for Realtime Web Suit (RWS), enabling modular, asynchronous web components, state management, and integration with backend services. It is located in `.dev/client`.

188 lines (151 loc) 6.76 kB
import { domEvents } from '../events'; import IndexedDBService, { IndexedDBServiceInstance } from '../services/IndexedDBService'; type CSSInjectMode = 'adopted' | 'legacy' | 'both'; const _DEFAULT_INJECT_CSS_CACHE_LIMIT_DAYS = 1; interface ICSSInjectionOptions { mode?: CSSInjectMode; maxDaysExp?: number; } interface ICSSInjectionComponent { shadowRoot: ShadowRoot | null; indexedDBService: IndexedDBServiceInstance; $emit(eventName: string): void; } export class CSSInjectionManager { private static CACHED_STYLES: Map<string, CSSStyleSheet> = new Map(); private static STYLES_OWNER_COMPONENT: ICSSInjectionComponent | null = null; static getCachedStyles(styleLinks: string[]): CSSStyleSheet[] { return styleLinks .filter(link => CSSInjectionManager.CACHED_STYLES.has(link)) .map(link => CSSInjectionManager.CACHED_STYLES.get(link)!); } static hasCachedStyles(styleLinks: string[]): boolean { return styleLinks.every(link => CSSInjectionManager.CACHED_STYLES.has(link)); } static getStylesOwnerComponent(): ICSSInjectionComponent | null { return CSSInjectionManager.STYLES_OWNER_COMPONENT; } static clearCachedStyles(): void { CSSInjectionManager.CACHED_STYLES.clear(); CSSInjectionManager.STYLES_OWNER_COMPONENT = null; } static async injectStyles( component: ICSSInjectionComponent, styleLinks: string[], options: ICSSInjectionOptions = {} ): Promise<void> { const { mode = 'adopted', maxDaysExp } = options; if (!component.shadowRoot) { throw new Error('Component must have a shadow root for CSS injection'); } // Add initial transition styles to host element const transitionSheet = new CSSStyleSheet(); await transitionSheet.replace(` :host { opacity: 0; transition: opacity 0.3s ease-in-out; } `); component.shadowRoot.adoptedStyleSheets = [ transitionSheet, ...component.shadowRoot.adoptedStyleSheets, ]; let adoptedSheets: CSSStyleSheet[] = []; let doneAdded = false; // Check if we already have cached styles from the owner component const cachedSheets: CSSStyleSheet[] = []; const uncachedLinks: string[] = []; for (const styleLink of styleLinks) { if (CSSInjectionManager.CACHED_STYLES.has(styleLink)) { cachedSheets.push(CSSInjectionManager.CACHED_STYLES.get(styleLink)!); } else { uncachedLinks.push(styleLink); } } // If we have cached styles, use them immediately if (cachedSheets.length > 0) { adoptedSheets.push(...cachedSheets); doneAdded = true; } // Only process uncached styles if (uncachedLinks.length > 0) { // Set this component as the owner if no owner exists yet if (!CSSInjectionManager.STYLES_OWNER_COMPONENT) { CSSInjectionManager.STYLES_OWNER_COMPONENT = component; } const dbName = 'css-cache'; const storeName = 'styles'; const db = await component.indexedDBService.openDB(dbName, storeName); const maxAgeMs = 1000 * 60 * 60 * 24; // 24h const maxDaysAge = maxDaysExp ? maxDaysExp : _DEFAULT_INJECT_CSS_CACHE_LIMIT_DAYS; const maxAgeDays = maxAgeMs * maxDaysAge; for (const styleLink of uncachedLinks) { const loadPromise = new Promise<void>(async (resolve, reject) => { if (mode === 'legacy' || mode === 'both') { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = styleLink; component.shadowRoot!.appendChild(link); link.onload = () => { doneAdded = true; if(mode === 'legacy'){ resolve(); } }; } if (mode === 'adopted' || mode === 'both') { const entry = await component.indexedDBService.getFromDB(db, storeName, styleLink); let cssText: string | null = null; if (entry && typeof entry === 'object' && 'css' in entry && 'timestamp' in entry) { const expired = Date.now() - entry.timestamp > maxAgeDays; if (!expired) { cssText = entry.css; } } if (!cssText) { cssText = await fetch(styleLink).then(res => res.text()); await component.indexedDBService.saveToDB(db, storeName, styleLink, { css: cssText, timestamp: Date.now() }); console.log(`System saved stylesheet: ${styleLink} to IndexedDB`) } const sheet = new CSSStyleSheet(); await sheet.replace(cssText); // Cache the stylesheet for future use CSSInjectionManager.CACHED_STYLES.set(styleLink, sheet); adoptedSheets.push(sheet); if(mode === 'adopted' || mode === 'both'){ resolve(); } } }); await loadPromise; } doneAdded = true; } if (adoptedSheets.length) { component.shadowRoot.adoptedStyleSheets = [ ...adoptedSheets, ...component.shadowRoot.adoptedStyleSheets, ]; doneAdded = true; } if (doneAdded) { // Set opacity to 1 to fade in the component const opacitySheet = new CSSStyleSheet(); await opacitySheet.replace(` :host { opacity: 1 !important; } `); component.shadowRoot.adoptedStyleSheets = [ opacitySheet, ...component.shadowRoot.adoptedStyleSheets, ]; component.$emit(domEvents.loadedLinkedStyles); } } } export default CSSInjectionManager; export { CSSInjectMode, ICSSInjectionOptions, ICSSInjectionComponent };