/** * ROI Theme - Anchor & Vignette Ads JavaScript * * Logica para: * - Mostrar/ocultar/colapsar anchors * - Triggers de vignette (pageview, scroll, exit_intent, time_delay) * - Persistencia con localStorage * - Tiempo de reaparicion configurable * * NO usa onclick inline - usa addEventListener */ (function() { 'use strict'; // ===================================================== // UTILIDADES // ===================================================== /** * Obtiene configuracion desde script type="application/json" */ function getConfig(id) { var el = document.getElementById(id); if (!el) return null; try { return JSON.parse(el.textContent); } catch (e) { return null; } } /** * Calcula duracion en milisegundos */ function getDurationMs(duration) { switch (duration) { case 'session': return 0; // sessionStorage case '1hour': return 60 * 60 * 1000; case '1day': return 24 * 60 * 60 * 1000; case '1week': return 7 * 24 * 60 * 60 * 1000; default: return 0; } } /** * Guarda estado en storage */ function saveState(key, value, duration) { var data = { value: value, expires: duration === 'session' ? 0 : Date.now() + getDurationMs(duration) }; if (duration === 'session') { sessionStorage.setItem(key, JSON.stringify(data)); } else { localStorage.setItem(key, JSON.stringify(data)); } } /** * Recupera estado desde storage */ function getState(key, duration) { var storage = duration === 'session' ? sessionStorage : localStorage; var raw = storage.getItem(key); if (!raw) return null; try { var data = JSON.parse(raw); // Verificar expiracion if (data.expires && data.expires < Date.now()) { storage.removeItem(key); return null; } return data.value; } catch (e) { return null; } } // ===================================================== // ANCHOR ADS // ===================================================== /** * Observa los slots de AdSense y MUESTRA el contenedor solo cuando se llena * Los anchors estan OCULTOS por defecto via CSS * AdSense agrega data-ad-status="filled" cuando hay anuncio */ function watchAdStatus(containers) { containers.forEach(function(container) { var ins = container.querySelector('ins.adsbygoogle'); if (!ins) return; // Funcion para mostrar el anchor cuando el ad esta listo function showIfFilled() { var status = ins.getAttribute('data-ad-status'); if (status === 'filled') { container.classList.add('ad-loaded'); return true; } return false; } // Verificar si ya tiene estado (por si ya cargo) if (showIfFilled()) { return; } // Usar MutationObserver para detectar cuando AdSense llena el slot var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'attributes' && mutation.attributeName === 'data-ad-status') { if (showIfFilled()) { observer.disconnect(); } } }); }); observer.observe(ins, { attributes: true, attributeFilter: ['data-ad-status'] }); // Timeout de seguridad: despues de 5s desconectar observer // Si no se lleno, el anchor permanece oculto (comportamiento deseado) setTimeout(function() { observer.disconnect(); }, 5000); }); } function initAnchorAds() { var config = getConfig('roi-anchor-config'); if (!config) return; var anchors = document.querySelectorAll('.roi-anchor-ad'); if (!anchors.length) return; // Restaurar estados guardados anchors.forEach(function(anchor) { var pos = anchor.dataset.position; var stateKey = 'roi_anchor_' + pos + '_state'; if (config.rememberState) { var savedState = getState(stateKey, config.rememberDuration); if (savedState === 'closed') { anchor.classList.add('hidden'); } else if (savedState === 'collapsed' && config.collapsible) { anchor.classList.add('collapsed'); } } }); // Mostrar anchors solo cuando AdSense llene el slot (ocultos por defecto via CSS) watchAdStatus(anchors); // Event delegation para botones document.addEventListener('click', function(e) { var btn = e.target.closest('[data-action]'); if (!btn) return; var action = btn.dataset.action; var anchor = btn.closest('.roi-anchor-ad'); if (!anchor) return; var pos = anchor.dataset.position; var stateKey = 'roi_anchor_' + pos + '_state'; switch (action) { case 'close': anchor.classList.add('hidden'); if (config.rememberState) { saveState(stateKey, 'closed', config.rememberDuration); } break; case 'collapse': if (config.collapsible) { anchor.classList.add('collapsed'); if (config.rememberState) { saveState(stateKey, 'collapsed', config.rememberDuration); } } break; case 'expand': anchor.classList.remove('collapsed'); if (config.rememberState) { saveState(stateKey, 'expanded', config.rememberDuration); } break; } }); } // ===================================================== // VIGNETTE ADS // ===================================================== function initVignetteAds() { var config = getConfig('roi-vignette-config'); if (!config) return; var overlay = document.getElementById('roi-vignette-overlay'); if (!overlay) return; var STORAGE_KEYS = { lastClosed: 'roi_vignette_last_closed', sessionCount: 'roi_vignette_session_count', pageCount: 'roi_vignette_page_count_' + window.location.pathname }; // Estado local var pageShowCount = 0; var triggered = false; /** * Verifica si se puede mostrar el vignette */ function canShow() { // Verificar max por pagina var maxPage = config.maxPerPage === 'unlimited' ? 999 : parseInt(config.maxPerPage); if (pageShowCount >= maxPage) return false; // Verificar max por sesion var maxSession = config.maxPerSession === 'unlimited' ? 999 : parseInt(config.maxPerSession); var sessionCount = parseInt(sessionStorage.getItem(STORAGE_KEYS.sessionCount) || '0'); if (sessionCount >= maxSession) return false; // Verificar tiempo de reaparicion if (config.reshowEnabled) { var lastClosed = parseInt(localStorage.getItem(STORAGE_KEYS.lastClosed) || '0'); var minWait = config.reshowTime * 60 * 1000; // minutos a ms if (lastClosed && (Date.now() - lastClosed) < minWait) { return false; } } else { // Si no se permite reaparicion, verificar si ya se cerro if (sessionStorage.getItem(STORAGE_KEYS.lastClosed)) { return false; } } return true; } /** * Muestra el vignette */ function showVignette() { if (!canShow() || triggered) return; triggered = true; // Mostrar overlay overlay.classList.add('active'); document.body.style.overflow = 'hidden'; // Incrementar contadores pageShowCount++; var sessionCount = parseInt(sessionStorage.getItem(STORAGE_KEYS.sessionCount) || '0'); sessionStorage.setItem(STORAGE_KEYS.sessionCount, (sessionCount + 1).toString()); // Manejar delay del boton cerrar var closeBtn = overlay.querySelector('.roi-vignette-close'); if (closeBtn && closeBtn.classList.contains('delayed')) { var delay = parseInt(closeBtn.dataset.delay || '0') * 1000; setTimeout(function() { closeBtn.classList.remove('delayed'); }, delay); } } /** * Cierra el vignette */ function closeVignette() { overlay.classList.remove('active'); document.body.style.overflow = ''; // Guardar tiempo de cierre localStorage.setItem(STORAGE_KEYS.lastClosed, Date.now().toString()); if (!config.reshowEnabled) { sessionStorage.setItem(STORAGE_KEYS.lastClosed, '1'); } // Permitir nueva trigger si se permite reaparicion if (config.reshowEnabled) { triggered = false; } } // Event listeners para cerrar document.addEventListener('click', function(e) { var btn = e.target.closest('[data-action="close-vignette"]'); if (btn) { closeVignette(); return; } // Click fuera del modal if (e.target === overlay) { closeVignette(); } }); // Escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && overlay.classList.contains('active')) { closeVignette(); } }); // === TRIGGERS === /** * Trigger: Al cargar pagina */ function setupPageviewTrigger() { setTimeout(function() { showVignette(); }, config.triggerDelay * 1000); } /** * Trigger: Al scrollear X% */ function setupScrollTrigger(percent) { var scrollHandler = function() { if (triggered) return; var scrollHeight = document.documentElement.scrollHeight - window.innerHeight; var scrolled = window.scrollY / scrollHeight; if (scrolled >= percent / 100) { showVignette(); window.removeEventListener('scroll', scrollHandler); } }; window.addEventListener('scroll', scrollHandler, { passive: true }); } /** * Trigger: Exit intent (mouse sale del viewport) */ function setupExitIntentTrigger() { var exitHandler = function(e) { if (triggered) return; // Solo activar si el mouse sale por arriba if (e.clientY <= 0) { showVignette(); document.removeEventListener('mouseout', exitHandler); } }; document.addEventListener('mouseout', exitHandler); } /** * Trigger: Despues de X segundos */ function setupTimeDelayTrigger() { setTimeout(function() { showVignette(); }, config.triggerDelay * 1000); } // Configurar trigger segun config switch (config.trigger) { case 'pageview': setupPageviewTrigger(); break; case 'scroll_50': setupScrollTrigger(50); break; case 'scroll_75': setupScrollTrigger(75); break; case 'exit_intent': setupExitIntentTrigger(); break; case 'time_delay': setupTimeDelayTrigger(); break; } } // ===================================================== // INICIALIZACION // ===================================================== function init() { initAnchorAds(); initVignetteAds(); } // Ejecutar cuando el DOM este listo if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();