/** * AdSense Lazy Loader con Intersection Observer * * Carga anuncios AdSense individualmente cuando entran al viewport, * detecta si reciben contenido, y oculta slots vacios. * * @package ROI_Theme * @since 1.5.0 * @version 2.0.0 - Refactorizado con Intersection Observer */ (function() { 'use strict'; // ========================================================================= // CONFIGURACION // ========================================================================= /** * Configuracion por defecto, sobrescrita por window.roiAdsenseConfig * * rootMargin: 600px precarga slots 600px antes de entrar al viewport. * Esto da tiempo suficiente para que AdSense cargue el anuncio antes * de que el usuario llegue al slot, evitando layout shift. * Basado en best practices: https://support.google.com/adsense/answer/10762946 */ var DEFAULT_CONFIG = { lazyEnabled: true, rootMargin: '600px 0px', fillTimeout: 5000, debug: false }; /** * Obtiene configuracion desde wp_localize_script o usa defaults */ function getConfig() { var wpConfig = window.roiAdsenseConfig || {}; return { lazyEnabled: typeof wpConfig.lazyEnabled !== 'undefined' ? wpConfig.lazyEnabled : DEFAULT_CONFIG.lazyEnabled, rootMargin: wpConfig.rootMargin || DEFAULT_CONFIG.rootMargin, fillTimeout: typeof wpConfig.fillTimeout !== 'undefined' ? parseInt(wpConfig.fillTimeout, 10) : DEFAULT_CONFIG.fillTimeout, debug: typeof wpConfig.debug !== 'undefined' ? wpConfig.debug : DEFAULT_CONFIG.debug }; } var CONFIG = getConfig(); // ========================================================================= // ESTADO GLOBAL // ========================================================================= var libraryLoaded = false; var libraryLoading = false; var libraryLoadFailed = false; var loadRetryCount = 0; var MAX_LOAD_RETRIES = 1; var RETRY_DELAY = 2000; /** @type {IntersectionObserver|null} */ var slotObserver = null; /** @type {Map} */ var fillObservers = new Map(); /** @type {Map} */ var fillTimeouts = new Map(); /** @type {Set} */ var activatedSlots = new Set(); /** @type {Array} */ var pendingActivations = []; // ========================================================================= // LOGGING // ========================================================================= /** * Log condicional basado en CONFIG.debug * @param {string} message * @param {string} [level='log'] - 'log', 'warn', 'error' */ function debugLog(message, level) { if (!CONFIG.debug || typeof console === 'undefined') { return; } level = level || 'log'; var prefix = '[AdSense Lazy] '; if (level === 'error') { console.error(prefix + message); } else if (level === 'warn') { console.warn(prefix + message); } else { console.log(prefix + message); } } // ========================================================================= // DETECCION DE SOPORTE // ========================================================================= /** * Verifica si el navegador soporta Intersection Observer */ function hasIntersectionObserverSupport() { return typeof window.IntersectionObserver !== 'undefined'; } /** * Verifica si el navegador soporta MutationObserver */ function hasMutationObserverSupport() { return typeof window.MutationObserver !== 'undefined'; } // ========================================================================= // CARGA DE BIBLIOTECA ADSENSE // ========================================================================= /** * Carga la biblioteca adsbygoogle.js * @param {Function} onSuccess * @param {Function} onError */ function loadAdSenseLibrary(onSuccess, onError) { if (libraryLoaded) { debugLog('Biblioteca ya cargada'); onSuccess(); return; } if (libraryLoading) { debugLog('Biblioteca en proceso de carga, encolando callback'); pendingActivations.push(onSuccess); return; } libraryLoading = true; debugLog('Cargando biblioteca adsbygoogle.js...'); var scriptTags = document.querySelectorAll('script[data-adsense-script]'); if (scriptTags.length === 0) { debugLog('No se encontro script[data-adsense-script]', 'warn'); libraryLoading = false; onError(); return; } var oldScript = scriptTags[0]; var newScript = document.createElement('script'); newScript.src = oldScript.src; newScript.async = true; if (oldScript.getAttribute('crossorigin')) { newScript.crossOrigin = oldScript.getAttribute('crossorigin'); } newScript.onload = function() { debugLog('Biblioteca cargada exitosamente'); libraryLoaded = true; libraryLoading = false; window.adsbygoogle = window.adsbygoogle || []; // Ejecutar callbacks pendientes onSuccess(); while (pendingActivations.length > 0) { var callback = pendingActivations.shift(); callback(); } }; newScript.onerror = function() { debugLog('Error cargando biblioteca (intento ' + (loadRetryCount + 1) + ')', 'error'); libraryLoading = false; if (loadRetryCount < MAX_LOAD_RETRIES) { loadRetryCount++; debugLog('Reintentando en ' + RETRY_DELAY + 'ms...'); setTimeout(function() { loadAdSenseLibrary(onSuccess, onError); }, RETRY_DELAY); } else { debugLog('Maximo de reintentos alcanzado', 'error'); libraryLoadFailed = true; onError(); } }; oldScript.parentNode.replaceChild(newScript, oldScript); } /** * Marca todos los slots como error cuando la biblioteca falla */ function markAllSlotsAsError() { var slots = document.querySelectorAll('.roi-ad-slot[data-ad-lazy="true"]'); slots.forEach(function(slot) { slot.classList.add('roi-ad-error'); cleanupSlot(slot); }); debugLog('Todos los slots marcados como error', 'error'); } // ========================================================================= // ACTIVACION DE SLOTS // ========================================================================= /** * Activa un slot individual ejecutando adsbygoogle.push() * @param {Element} slot */ function activateSlot(slot) { if (activatedSlots.has(slot)) { debugLog('Slot ya activado, omitiendo'); return; } if (libraryLoadFailed) { debugLog('Biblioteca fallida, marcando slot como error'); slot.classList.add('roi-ad-error'); return; } activatedSlots.add(slot); var doActivation = function() { var ins = slot.querySelector('ins.adsbygoogle'); if (!ins) { debugLog('No se encontro en slot', 'warn'); slot.classList.add('roi-ad-empty'); return; } debugLog('Activando slot: ' + (ins.getAttribute('data-ad-slot') || 'unknown')); // Ejecutar push try { window.adsbygoogle = window.adsbygoogle || []; window.adsbygoogle.push({}); } catch (e) { debugLog('Error en push: ' + e.message, 'error'); slot.classList.add('roi-ad-error'); return; } // Iniciar observacion de llenado startFillDetection(slot, ins); }; // Si la biblioteca ya cargo, activar inmediatamente if (libraryLoaded) { doActivation(); } else { // Cargar biblioteca y luego activar loadAdSenseLibrary(doActivation, function() { markAllSlotsAsError(); }); } } // ========================================================================= // DETECCION DE LLENADO // ========================================================================= /** @type {Map} */ var pollIntervals = new Map(); /** Intervalo de polling rapido en ms */ var POLL_INTERVAL = 50; /** Maximo de intentos de polling (50ms * 60 = 3 segundos max) */ var MAX_POLL_ATTEMPTS = 60; /** * Inicia la deteccion de llenado para un slot * @param {Element} slot * @param {Element} ins */ function startFillDetection(slot, ins) { // Verificar inmediatamente si ya tiene contenido if (checkFillStatus(slot, ins)) { return; } // Estrategia: Polling rapido (50ms) para detectar data-ad-status lo antes posible. // AdSense establece data-ad-status muy rapido despues de inyectar el iframe, // pero MutationObserver a veces no lo detecta inmediatamente. var pollCount = 0; var pollId = setInterval(function() { pollCount++; if (checkFillStatus(slot, ins)) { // Estado detectado, limpiar polling clearInterval(pollId); pollIntervals.delete(slot); return; } // Si alcanzamos el maximo de intentos, marcar como vacio if (pollCount >= MAX_POLL_ATTEMPTS) { debugLog('Polling timeout alcanzado (' + (pollCount * POLL_INTERVAL) + 'ms)'); clearInterval(pollId); pollIntervals.delete(slot); markSlotEmpty(slot); } }, POLL_INTERVAL); pollIntervals.set(slot, pollId); } /** * Verifica el estado de llenado de un slot * @param {Element} slot * @param {Element} ins * @returns {boolean} true si el estado fue determinado (filled o empty) */ function checkFillStatus(slot, ins) { // IMPORTANTE: Solo data-ad-status es confiable para determinar el estado final. // AdSense inyecta iframe ANTES de establecer data-ad-status, por lo que // la presencia de iframe NO indica que el anuncio fue llenado. var status = ins.getAttribute('data-ad-status'); // Estado definitivo: filled if (status === 'filled') { debugLog('Slot llenado (data-ad-status=filled)'); markSlotFilled(slot); return true; } // Estado definitivo: unfilled (sin anuncio disponible) if (status === 'unfilled') { debugLog('Slot vacio (data-ad-status=unfilled)'); markSlotEmpty(slot); return true; } // Si no hay data-ad-status, AdSense aun no ha respondido. // NO usar iframe como criterio porque AdSense inyecta iframe incluso para unfilled. // El MutationObserver seguira observando hasta que data-ad-status aparezca o timeout. debugLog('Esperando data-ad-status...'); return false; } /** * Marca un slot como llenado * @param {Element} slot */ function markSlotFilled(slot) { slot.classList.remove('roi-ad-empty', 'roi-ad-error'); slot.classList.add('roi-ad-filled'); cleanupSlot(slot); } /** * Marca un slot como vacio * @param {Element} slot */ function markSlotEmpty(slot) { slot.classList.remove('roi-ad-filled', 'roi-ad-error'); slot.classList.add('roi-ad-empty'); cleanupSlot(slot); } /** * Limpia observadores, timeouts e intervalos de un slot * @param {Element} slot */ function cleanupSlot(slot) { // Limpiar polling interval if (pollIntervals.has(slot)) { clearInterval(pollIntervals.get(slot)); pollIntervals.delete(slot); } // Limpiar timeout (legacy) if (fillTimeouts.has(slot)) { clearTimeout(fillTimeouts.get(slot)); fillTimeouts.delete(slot); } // Limpiar MutationObserver (legacy) if (fillObservers.has(slot)) { fillObservers.get(slot).disconnect(); fillObservers.delete(slot); } // Dejar de observar con IntersectionObserver if (slotObserver) { slotObserver.unobserve(slot); } } // ========================================================================= // INTERSECTION OBSERVER // ========================================================================= /** * Inicializa el Intersection Observer para slots */ function initIntersectionObserver() { if (!hasIntersectionObserverSupport()) { debugLog('Sin soporte Intersection Observer, usando modo legacy', 'warn'); return false; } var options = { root: null, rootMargin: CONFIG.rootMargin, threshold: 0 }; slotObserver = new IntersectionObserver(function(entries) { entries.forEach(function(entry) { if (entry.isIntersecting) { var slot = entry.target; debugLog('Slot entro al viewport'); activateSlot(slot); } }); }, options); debugLog('Intersection Observer inicializado con rootMargin: ' + CONFIG.rootMargin); return true; } /** * Observa todos los slots lazy en la pagina */ function observeAllSlots() { var slots = document.querySelectorAll('.roi-ad-slot[data-ad-lazy="true"]'); debugLog('Encontrados ' + slots.length + ' slots para observar'); slots.forEach(function(slot) { if (!activatedSlots.has(slot)) { slotObserver.observe(slot); } }); } /** * Observa nuevos slots agregados dinamicamente */ function observeNewSlots() { var slots = document.querySelectorAll('.roi-ad-slot[data-ad-lazy="true"]'); var newCount = 0; slots.forEach(function(slot) { if (!activatedSlots.has(slot)) { slotObserver.observe(slot); newCount++; } }); if (newCount > 0) { debugLog('Agregados ' + newCount + ' nuevos slots al observer'); } } // ========================================================================= // MODO LEGACY (FALLBACK) // ========================================================================= /** * Variables para modo legacy */ var legacyLoaded = false; var legacyTimeout = null; /** * Carga todos los ads en modo legacy (sin Intersection Observer) */ function loadAllAdsLegacy() { if (legacyLoaded) { return; } legacyLoaded = true; debugLog('Modo legacy: Cargando todos los ads'); if (legacyTimeout) { clearTimeout(legacyTimeout); } removeLegacyEventListeners(); loadAdSenseLibrary(function() { executeAllPushScripts(); }, function() { debugLog('Error en modo legacy', 'error'); }); } /** * Ejecuta todos los scripts de push en modo legacy */ function executeAllPushScripts() { var pushScripts = document.querySelectorAll('script[data-adsense-push]'); debugLog('Ejecutando ' + pushScripts.length + ' scripts de push'); window.adsbygoogle = window.adsbygoogle || []; pushScripts.forEach(function(oldScript) { var newScript = document.createElement('script'); newScript.innerHTML = oldScript.innerHTML; newScript.type = 'text/javascript'; oldScript.parentNode.replaceChild(newScript, oldScript); }); document.body.classList.add('adsense-loaded'); } /** * Event handler para modo legacy */ function handleLegacyInteraction() { debugLog('Interaccion detectada (modo legacy)'); loadAllAdsLegacy(); } /** * Agrega listeners para modo legacy */ function addLegacyEventListeners() { window.addEventListener('scroll', handleLegacyInteraction, { passive: true, once: true }); window.addEventListener('mousemove', handleLegacyInteraction, { passive: true, once: true }); window.addEventListener('touchstart', handleLegacyInteraction, { passive: true, once: true }); window.addEventListener('click', handleLegacyInteraction, { passive: true, once: true }); window.addEventListener('keydown', handleLegacyInteraction, { passive: true, once: true }); } /** * Remueve listeners de modo legacy */ function removeLegacyEventListeners() { window.removeEventListener('scroll', handleLegacyInteraction, { passive: true }); window.removeEventListener('mousemove', handleLegacyInteraction, { passive: true }); window.removeEventListener('touchstart', handleLegacyInteraction, { passive: true }); window.removeEventListener('click', handleLegacyInteraction, { passive: true }); window.removeEventListener('keydown', handleLegacyInteraction, { passive: true }); } /** * Inicia modo legacy con listeners de interaccion */ function initLegacyMode() { debugLog('Iniciando modo legacy'); addLegacyEventListeners(); legacyTimeout = setTimeout(function() { debugLog('Timeout legacy alcanzado'); loadAllAdsLegacy(); }, CONFIG.fillTimeout); } // ========================================================================= // EVENTO DINAMICO // ========================================================================= /** * Configura listener para ads dinamicos */ function setupDynamicAdsListener() { window.addEventListener('roi-adsense-activate', function() { debugLog('Evento roi-adsense-activate recibido'); if (CONFIG.lazyEnabled && slotObserver) { observeNewSlots(); } else if (!legacyLoaded) { loadAllAdsLegacy(); } else { // Ya cargado en legacy, ejecutar nuevos push activateDynamicSlotsLegacy(); } }); } /** * Activa slots dinamicos en modo legacy */ function activateDynamicSlotsLegacy() { var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]'); if (pendingPushScripts.length === 0) { return; } debugLog('Activando ' + pendingPushScripts.length + ' slots dinamicos (legacy)'); window.adsbygoogle = window.adsbygoogle || []; pendingPushScripts.forEach(function(oldScript) { try { var newScript = document.createElement('script'); newScript.type = 'text/javascript'; newScript.innerHTML = oldScript.innerHTML; oldScript.parentNode.replaceChild(newScript, oldScript); } catch (e) { debugLog('Error activando slot dinamico: ' + e.message, 'error'); } }); } // ========================================================================= // INICIALIZACION // ========================================================================= /** * Inicializa el sistema * * ESTRATEGIA v2.2 (basada en documentacion oficial de Google): * - Los slots NO estan ocultos inicialmente (Google puede no ejecutar requests para slots ocultos) * - Usamos Intersection Observer con rootMargin grande (600px) para precargar * - Google automaticamente oculta slots unfilled via CSS: ins[data-ad-status="unfilled"] * - Nuestro CSS colapsa el contenedor .roi-ad-slot cuando el ins tiene unfilled * - Esto funciona MEJOR que eager loading porque no satura AdSense con requests simultaneos */ function init() { // Siempre configurar listener para ads dinamicos setupDynamicAdsListener(); debugLog('Listener dinamico configurado'); // Verificar si delay esta habilitado globalmente if (!window.roiAdsenseDelayed) { debugLog('Delay global no habilitado'); return; } debugLog('Inicializando AdSense Lazy Loader v2.2 (IO + Google Official CSS)'); debugLog('Config: lazyEnabled=' + CONFIG.lazyEnabled + ', rootMargin=' + CONFIG.rootMargin + ', fillTimeout=' + CONFIG.fillTimeout); // Decidir modo de operacion if (!CONFIG.lazyEnabled) { debugLog('Lazy loading deshabilitado, usando modo legacy'); initLegacyMode(); return; } // Verificar soporte para Intersection Observer if (!hasIntersectionObserverSupport()) { debugLog('Sin soporte IO, usando modo legacy', 'warn'); initLegacyMode(); return; } // Inicializar Intersection Observer if (!initIntersectionObserver()) { debugLog('Fallo inicializando IO, usando modo legacy', 'warn'); initLegacyMode(); return; } // Esperar a que el DOM este listo y observar slots if (document.readyState === 'interactive' || document.readyState === 'complete') { observeAllSlots(); } else { document.addEventListener('DOMContentLoaded', function() { observeAllSlots(); }); } } // Iniciar init(); })();