1/** 2 * JPX Ads Manager â Frontend engine 3 * Lazy load, IntersectionObserver, sendBeacon, injeção de scripts de agência, 4 * batch requests e tratamento de slot vazio. 5 */ 6(function() { 7 'use strict'; 8 9 var REST_URL = (window.jpxAdsData && window.jpxAdsData.restUrl) || '/wp-json/jpx-ads/v1/'; 10 11 // Contexto da página (categorias/tags) â calculado no PHP no render, pois 12 // dentro do request REST o contexto da página não está disponÃvel. 13 function contextQuery() { 14 var ctx = (window.jpxAdsData && window.jpxAdsData.context) || {}; 15 var parts = []; 16 if (ctx.cats && ctx.cats.length) parts.push('cats=' + encodeURIComponent(ctx.cats.join(','))); 17 if (ctx.tags && ctx.tags.length) parts.push('tags=' + encodeURIComponent(ctx.tags.join(','))); 18 return parts.join('&'); 19 } 20 21 function init() { 22 var slots = document.querySelectorAll('.jpx-ad-slot'); 23 if (!slots.length) return; 24 25 // Agrupa slots para batch request 26 var batchItems = []; 27 var slotMap = []; 28 29 slots.forEach(function(slot, index) { 30 var grupo = slot.dataset.group; 31 var banner = slot.dataset.banner; 32 if (!grupo && !banner) return; 33 34 if (grupo) { 35 batchItems.push({ type: 'group', id: parseInt(grupo, 10) }); 36 } else { 37 batchItems.push({ type: 'banner', id: parseInt(banner, 10) }); 38 } 39 slotMap.push({ slot: slot, index: index }); 40 }); 41 42 if (batchItems.length > 1) { 43 // Tenta batch primeiro 44 var cq = contextQuery(); 45 fetch(REST_URL + 'serve-batch' + (cq ? '?' + cq : ''), { 46 method: 'POST', 47 headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, 48 body: JSON.stringify(batchItems), 49 cache: 'no-store' 50 }) 51 .then(function(r) { return r.json(); }) 52 .then(function(data) { 53 if (Array.isArray(data) && data.length === batchItems.length) { 54 data.forEach(function(response, i) { 55 handleSlotResponse(slotMap[i].slot, response); 56 }); 57 } else { 58 // Fallback para requisições individuais 59 loadSlotsIndividually(slotMap); 60 } 61 }) 62 .catch(function() { 63 // Fallback para requisições individuais 64 loadSlotsIndividually(slotMap); 65 }); 66 } else if (batchItems.length === 1) { 67 // Apenas 1 slot: requisição individual direta 68 loadSlotIndividual(slotMap[0].slot); 69 } 70 } 71 72 function loadSlotsIndividually(slotMap) { 73 slotMap.forEach(function(item) { 74 loadSlotIndividual(item.slot); 75 }); 76 } 77 78 function loadSlotIndividual(slot) { 79 var grupo = slot.dataset.group; 80 var banner = slot.dataset.banner; 81 if (!grupo && !banner) return; 82 83 var params = grupo ? 'grupo=' + encodeURIComponent(grupo) : 'banner=' + encodeURIComponent(banner); 84 var cq = contextQuery(); 85 if (cq) params += '&' + cq; 86 fetch(REST_URL + 'serve?' + params, { 87 method: 'GET', 88 cache: 'no-store', 89 headers: { 'Accept': 'application/json' } 90 }) 91 .then(function(r) { return r.json(); }) 92 .then(function(data) { 93 handleSlotResponse(slot, data); 94 }) 95 .catch(function() { 96 // REST falhou (rede, WAF, erro 500...): aplica o empty_behavior 97 // do próprio slot em vez de deixar o placeholder cru reservado. 98 handleEmptySlot(slot, {}); 99 }); 100 } 101 102 function handleSlotResponse(slot, data) { 103 if (window.jpxAdsData && window.jpxAdsData.debug) { 104 console.log('JPX Ads slot response:', slot.dataset.group || slot.dataset.banner, data); 105 } 106 if (data.error === 'no_ad') { 107 handleEmptySlot(slot, data); 108 return; 109 } 110 if (data.error) { 111 if (window.jpxAdsData && window.jpxAdsData.debug) { 112 console.warn('JPX Ads error:', data.error); 113 } 114 return; 115 } 116 117 if (Array.isArray(data)) { 118 // Modo dinâmico â array de anúncios. 119 handleDynamic(slot, data); 120 } else { 121 handleSingle(slot, data); 122 } 123 } 124 125 function handleEmptySlot(slot, data) { 126 var behavior = data.empty_behavior || slot.dataset.emptyBehavior || 'reserve'; 127 if (behavior === 'collapse') { 128 slot.style.display = 'none'; 129 collapseThemeWrapper(slot); 130 return; 131 } 132 if (behavior === 'house_ad' && data.house_ad) { 133 injectAd(slot, { tipo: 'codigo', conteudo: data.house_ad, id: 0, grupo_id: 0, titulo: 'House Ad', anunciante: '' }); 134 return; 135 } 136 // reserve: mantém o placeholder como está 137 } 138 139 // Esconde o wrapper do TEMA (ex: .ad-super-banner) que reserva espaço 140 // ao redor do slot, quando configurado no grupo. Permite portar o 141 // colapso pra qualquer tema sem editar CSS/PHP do tema. 142 function collapseThemeWrapper(slot) { 143 var selector = slot.dataset.wrapperSelector; 144 if (!selector) return; 145 try { 146 var wrapper = slot.closest(selector); 147 if (wrapper) wrapper.style.display = 'none'; 148 } catch (e) { 149 // seletor CSS inválido digitado no admin â ignora silenciosamente 150 } 151 } 152 153 function handleSingle(slot, ad) { 154 injectAd(slot, ad); 155 observeViewable(slot, ad); 156 bindClick(slot, ad); 157 } 158 159 function handleDynamic(slot, ads) { 160 if (!ads.length) return; 161 var idx = 0; 162 var interval = parseInt(slot.dataset.interval || '6', 10) * 1000; 163 164 function show(i) { 165 var ad = ads[i % ads.length]; 166 injectAd(slot, ad); 167 observeViewable(slot, ad); 168 bindClick(slot, ad); 169 } 170 171 show(0); 172 if (ads.length > 1 && interval >= 1000) { 173 setInterval(function() { 174 idx++; 175 show(idx); 176 }, interval); 177 } 178 } 179 180 function injectAd(slot, ad) { 181 var label = slot.querySelector('.jpx-ad-label'); 182 slot.innerHTML = ''; 183 if (label) slot.appendChild(label); 184 185 var wrap = document.createElement('div'); 186 wrap.className = 'jpx-ad-content'; 187 188 if (ad.tipo === 'codigo') { 189 injectWithScripts(wrap, ad.conteudo); 190 } else { 191 wrap.innerHTML = ad.conteudo; 192 } 193 194 slot.appendChild(wrap); 195 } 196 197 function injectWithScripts(container, html) { 198 container.innerHTML = html; 199 var scripts = container.querySelectorAll('script');
200 scripts.forEach(function(oldScript) { 201 var newScript = document.createElement('script'); 202 Array.prototype.forEach.call(oldScript.attributes, function(attr) { 203 newScript.setAttribute(attr.name, attr.value); 204 }); 205 if (oldScript.src) { 206 newScript.src = oldScript.src; 207 newScript.async = false; 208 } else { 209 newScript.textContent = oldScript.textContent; 210 } 211 oldScript.parentNode.replaceChild(newScript, oldScript); 212 }); 213 } 214 215 function observeViewable(slot, ad) { 216 if (!window.IntersectionObserver) { 217 trackEvent(ad, 'i'); 218 return; 219 } 220 221 var observed = false; 222 var observer = new IntersectionObserver(function(entries) { 223 entries.forEach(function(entry) { 224 if (entry.isIntersecting && !observed) { 225 observed = true; 226 // Aguarda 1s visÃvel para considerar viewable. 227 setTimeout(function() { 228 trackEvent(ad, 'i'); 229 tryGtag('view_promotion', ad); 230 }, 1000); 231 observer.disconnect(); 232 } 233 }); 234 }, { threshold: 0.5 }); 235 236 observer.observe(slot); 237 } 238 239 function bindClick(slot, ad) { 240 if (ad.tipo === 'codigo') return; // clique da agência 241 slot.addEventListener('click', function(e) { 242 var link = e.target.closest('a.jpx-ad-link'); 243 if (!link) return; 244 trackEvent(ad, 'c'); 245 tryGtag('select_promotion', ad); 246 }); 247 } 248 249 function trackEvent(ad, tipo) { 250 var payload = { 251 ad_id: ad.id, 252 grupo_id: ad.grupo_id || 0, 253 tipo: tipo 254 }; 255 var endpoint = REST_URL + 'event'; 256 var body = JSON.stringify(payload); 257 258 function doFetch() { 259 fetch(endpoint, { 260 method: 'POST', 261 headers: { 'Content-Type': 'application/json' }, 262 body: body, 263 keepalive: true 264 }) 265 .then(function(r) { return r.json(); }) 266 .then(function(data) { 267 if (window.jpxAdsData && window.jpxAdsData.debug) { 268 console.log('JPX Ads trackEvent:', data, payload); 269 } 270 }) 271 .catch(function(err) { 272 if (window.jpxAdsData && window.jpxAdsData.debug) { 273 console.warn('JPX Ads trackEvent error:', err); 274 } 275 // Fallback para sendBeacon se fetch falhar 276 if (navigator.sendBeacon) { 277 navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' })); 278 } 279 }); 280 } 281 282 doFetch(); 283 } 284 285 function tryGtag(eventName, ad) { 286 // Gating de consentimento LGPD 287 if (window.jpxAdsData && window.jpxAdsData.consentMode === 'respect_banner') { 288 if (!window.jpxAdsConsent || !window.jpxAdsConsent.granted) { 289 return; // não envia ao GA4 sem consentimento 290 } 291 } 292 293 var mode = (window.jpxAdsData && window.jpxAdsData.eventMode) || 'gtag'; 294 295 if (mode === 'gtag' && typeof gtag === 'function') { 296 gtag('event', eventName, { 297 creative_name: ad.titulo, 298 creative_slot: 'grupo_' + (ad.grupo_id || 0), 299 promotion_id: 'ad_' + ad.id, 300 promotion_name: ad.anunciante || '' 301 }); 302 } else if (mode === 'dataLayer' && window.dataLayer) { 303 window.dataLayer.push({ 304 event: 'jpx_ads_' + eventName, 305 jpx_creative_name: ad.titulo, 306 jpx_creative_slot: 'grupo_' + (ad.grupo_id || 0),
307 jpx_promotion_id: 'ad_' + ad.id, 308 jpx_promotion_name: ad.anunciante || '' 309 }); 310 } 311 } 312 313 // Objeto global de consentimento â o banner de cookies do portal pode chamar setGranted(true) 314 window.jpxAdsConsent = { 315 granted: false, 316 setGranted: function(v) { this.granted = !!v; } 317 }; 318 319 // Integração automática com Complianz (se ativo) 320 if ( typeof window.cmplz_get_consent === 'function' || typeof window.cmplz_has_consent === 'function' ) { 321 function jpxUpdateComplianzConsent() { 322 var granted = false; 323 if ( typeof cmplz_has_consent === 'function' ) { 324 granted = cmplz_has_consent( 'marketing' ); 325 } else if ( typeof cmplz_get_consent === 'function' ) { 326 var c = cmplz_get_consent(); 327 granted = !!(c && c.marketing); 328 } 329 window.jpxAdsConsent.setGranted( granted ); 330 } 331 document.addEventListener( 'cmplzConsentSaved', jpxUpdateComplianzConsent ); 332 document.addEventListener( 'cmplzBannerStatusChanged', jpxUpdateComplianzConsent ); 333 jpxUpdateComplianzConsent(); 334 } 335 336 // Integração via WP Consent API (padrão universal WordPress) 337 if ( typeof window.wpConsent === 'object' && typeof window.wpConsentApi === 'object' ) { 338 function jpxUpdateWpConsent() { 339 var granted = false; 340 if ( typeof wp_has_consent === 'function' ) { 341 granted = wp_has_consent( 'marketing' ); 342 } else if ( window.wpConsentApi && typeof window.wpConsentApi.hasConsent === 'function' ) { 343 granted = window.wpConsentApi.hasConsent( 'marketing' ); 344 } 345 window.jpxAdsConsent.setGranted( granted ); 346 } 347 document.addEventListener( 'wp_consent_type_defined', jpxUpdateWpConsent ); 348 document.addEventListener( 'wp_listen_for_consent_change', jpxUpdateWpConsent ); 349 jpxUpdateWpConsent(); 350 } 351 352 if (document.readyState === 'loading') { 353 document.addEventListener('DOMContentLoaded', init); 354 } else { 355 init(); 356 } 357})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.