/** * Lazy Loader - Dynamic script loading with dependency management * Enables code splitting for heavy libraries (Chart.js, Cropper.js, etc.) */ class LazyLoader { constructor() { this._loaded = {}; // url -> Promise this._ready = {}; // name -> boolean } /** * Load a script dynamically * @param {string} url - Script URL * @param {string} globalName - Global variable name to check for existence * @returns {Promise} */ loadScript(url, globalName = null) { if (this._loaded[url]) return this._loaded[url]; this._loaded[url] = new Promise((resolve, reject) => { // Already available globally? if (globalName && window[globalName]) { this._ready[globalName] = true; resolve(window[globalName]); return; } const script = document.createElement('script'); script.src = url; script.async = true; script.onload = () => { if (globalName) this._ready[globalName] = true; resolve(globalName ? window[globalName] : true); }; script.onerror = () => { delete this._loaded[url]; reject(new Error(`Failed to load: ${url}`)); }; document.head.appendChild(script); }); return this._loaded[url]; } /** * Load CSS dynamically */ loadCSS(url) { if (document.querySelector(`link[href="${url}"]`)) return Promise.resolve(); return new Promise((resolve, reject) => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; link.onload = resolve; link.onerror = reject; document.head.appendChild(link); }); } /** * Check if a library is ready */ isReady(name) { return !!this._ready[name]; } /** * Wait for a library to be available (polls window[name]) */ waitFor(name, timeout = 10000) { if (window[name]) return Promise.resolve(window[name]); return new Promise((resolve, reject) => { const start = Date.now(); const check = () => { if (window[name]) { this._ready[name] = true; resolve(window[name]); return; } if (Date.now() - start > timeout) { reject(new Error(`${name} not loaded after ${timeout}ms`)); return; } setTimeout(check, 50); }; check(); }); } } // Singleton const lazyLoader = new LazyLoader(); window.lazyLoader = lazyLoader; // Predefined lazy-load bundles const LAZY_BUNDLES = { charts: () => Promise.all([ lazyLoader.loadScript('https://cdn.jsdelivr.net/npm/chart.js@4', 'Chart'), lazyLoader.loadScript('https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3'), lazyLoader.loadScript('https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3'), ]), cropper: () => lazyLoader.loadCSS('https://cdn.jsdelivr.net/npm/cropperjs@1/dist/cropper.min.css') .then(() => lazyLoader.loadScript('https://cdn.jsdelivr.net/npm/cropperjs@1', 'Cropper')), }; window.LAZY_BUNDLES = LAZY_BUNDLES;