diff --git a/Elecciones-Web/frontend/public/bootstrap.js b/Elecciones-Web/frontend/public/bootstrap.js index 9c8c6b1..90c0a7d 100644 --- a/Elecciones-Web/frontend/public/bootstrap.js +++ b/Elecciones-Web/frontend/public/bootstrap.js @@ -4,9 +4,16 @@ // El dominio donde se alojan los widgets const WIDGETS_HOST = 'https://elecciones2025.eldia.com'; - // Función para cargar dinámicamente un script + // Estado interno para evitar recargas y re-fetch innecesarios + const __state = { + assetsLoaded: false, + manifest: null, + }; + + // Función para cargar dinámicamente un script (evita duplicados) function loadScript(src) { return new Promise((resolve, reject) => { + if ([...document.scripts].some(s => s.src === src)) return resolve(); const script = document.createElement('script'); script.type = 'module'; script.src = src; @@ -16,73 +23,116 @@ }); } - // Función para cargar dinámicamente una hoja de estilos + // Función para cargar dinámicamente una hoja de estilos (evita duplicados) function loadCSS(href) { + if ([...document.querySelectorAll('link[rel="stylesheet"]')].some(l => l.href === href)) return; const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = href; document.head.appendChild(link); } - // Función principal + // Carga (una sola vez) JS/CSS definidos por el manifest + async function ensureAssetsFromManifest() { + if (__state.assetsLoaded) return; + + // 1) Obtener el manifest.json (cache: no-store por si hay deploys frecuentes) + if (!__state.manifest) { + const response = await fetch(`${WIDGETS_HOST}/manifest.json`, { cache: 'no-store' }); + if (!response.ok) throw new Error('No se pudo cargar el manifest de los widgets.'); + __state.manifest = await response.json(); + } + + // 2) Encontrar el entry principal (isEntry=true) + const entryKey = Object.keys(__state.manifest).find(key => __state.manifest[key].isEntry); + if (!entryKey) throw new Error('No se encontró el punto de entrada en el manifest.'); + + const entry = __state.manifest[entryKey]; + const jsUrl = `${WIDGETS_HOST}/${entry.file}`; + + // 3) Cargar el CSS si existe (una sola vez) + if (entry.css && entry.css.length > 0) { + entry.css.forEach(cssFile => loadCSS(`${WIDGETS_HOST}/${cssFile}`)); + } + + // 4) Cargar el JS principal (una sola vez) + await loadScript(jsUrl); + + __state.assetsLoaded = true; + } + + // Render: busca contenedores y llama a la API global del widget + function renderWidgetsOnPage() { + if (!(window.EleccionesWidgets && typeof window.EleccionesWidgets.render === 'function')) { + // La librería aún no expuso la API (puede ocurrir en primeros ms tras cargar) + return; + } + + const widgetContainers = document.querySelectorAll('[data-elecciones-widget]'); + if (widgetContainers.length === 0) { + // En algunas rutas no habrá widgets: no es error. + return; + } + + widgetContainers.forEach(container => { + window.EleccionesWidgets.render(container, container.dataset); + }); + } + + // Función principal (re-usable) para inicializar y renderizar async function initWidgets() { try { - // 1. Obtener el manifest.json para saber los nombres de archivo actuales - const response = await fetch(`${WIDGETS_HOST}/manifest.json`); - if (!response.ok) { - throw new Error('No se pudo cargar el manifest de los widgets.'); - } - const manifest = await response.json(); - - // 2. Encontrar el punto de entrada principal (nuestro main.tsx) - const entryKey = Object.keys(manifest).find(key => manifest[key].isEntry); - if (!entryKey) { - throw new Error('No se encontró el punto de entrada en el manifest.'); - } - - const entry = manifest[entryKey]; - const jsUrl = `${WIDGETS_HOST}/${entry.file}`; - - // 3. Cargar el CSS si existe - if (entry.css && entry.css.length > 0) { - entry.css.forEach(cssFile => { - const cssUrl = `${WIDGETS_HOST}/${cssFile}`; - loadCSS(cssUrl); - }); - } - - // 4. Cargar el JS principal y esperar a que esté listo - await loadScript(jsUrl); - - - // 5. Una vez cargado, llamar a la función de renderizado. - if (window.EleccionesWidgets && typeof window.EleccionesWidgets.render === 'function') { - console.log('Bootstrap: La función render existe. Renderizando todos los widgets encontrados...'); - - const widgetContainers = document.querySelectorAll('[data-elecciones-widget]'); - - if (widgetContainers.length === 0) { - console.warn('Bootstrap: No se encontraron contenedores de widget en la página.'); - } - - widgetContainers.forEach(container => { - // 'dataset' es un objeto que contiene todos los atributos data-* - window.EleccionesWidgets.render(container, container.dataset); - }); - } else { - console.error('Bootstrap: ERROR CRÍTICO - La función render() NO SE ENCONTRÓ en window.EleccionesWidgets.'); - console.log('Bootstrap: Contenido de window.EleccionesWidgets:', window.EleccionesWidgets); - } - + await ensureAssetsFromManifest(); + renderWidgetsOnPage(); } catch (error) { console.error('Error al inicializar los widgets de elecciones:', error); } } - if (document.readyState === 'loading') { // Aún cargando + // Exponer para invocación manual (por ejemplo, en hooks del router) + window.__eleccionesInit = initWidgets; + + // Primer render en carga inicial + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initWidgets); - } else { // Ya cargado + } else { initWidgets(); } -})(); \ No newline at end of file + // --- Reinvocar en cada navegación de SPA --- + function dispatchLocationChange() { + window.dispatchEvent(new Event('locationchange')); + } + + ['pushState', 'replaceState'].forEach(method => { + const orig = history[method]; + history[method] = function () { + const ret = orig.apply(this, arguments); + dispatchLocationChange(); + return ret; + }; + }); + window.addEventListener('popstate', dispatchLocationChange); + + let navDebounce = null; + window.addEventListener('locationchange', () => { + clearTimeout(navDebounce); + navDebounce = setTimeout(() => { + initWidgets(); + }, 0); + }); + + // --- (Opcional) Re-render si aparecen contenedores luego del montaje de la vista --- + const mo = new MutationObserver((mutations) => { + for (const m of mutations) { + if (m.type === 'childList') { + const added = [...m.addedNodes].some(n => + n.nodeType === 1 && + (n.matches?.('[data-elecciones-widget]') || n.querySelector?.('[data-elecciones-widget]')) + ); + if (added) { renderWidgetsOnPage(); break; } + } + } + }); + mo.observe(document.body, { childList: true, subtree: true }); +})(); diff --git a/Elecciones-Web/frontend/src/main.tsx b/Elecciones-Web/frontend/src/main.tsx index eba916f..c5e7d7e 100644 --- a/Elecciones-Web/frontend/src/main.tsx +++ b/Elecciones-Web/frontend/src/main.tsx @@ -94,9 +94,6 @@ if (import.meta.env.DEV) { if (widgetName && WIDGET_MAP[widgetName]) { const WidgetComponent = WIDGET_MAP[widgetName]; const root = ReactDOM.createRoot(container); - - // Pasamos todas las props (ej. { eleccionesWidget: '...', focoMunicipio: '...' }) - // al componente que se va a renderizar. root.render(