1(function () { 2 "use strict"; 3 4 if (window.ExtremosAdsManagerStarted) return; 5 window.ExtremosAdsManagerStarted = true; 6 7 const CONFIG_URL = "/painel/publi/config.js"; 8 const AD_ITEM_SELECTOR = "[data-billboard], [data-trip-card], [data-book-ad], [data-book], [data-cta]"; 9 const LEGACY_SLOT_SELECTORS = { 10 billboard: "[data-billboard-fragment]", 11 travel: "[data-trip-fragment]", 12 book: "[data-book-ad-fragment]", 13 books: "[data-store-fragment]", 14 cta: "[data-cta-fragment]" 15 }; 16 const DEFAULT_BODY_SELECTORS = [ 17 ".news-article", 18 ".news-article-body", 19 ".news-content", 20 "#texto-noticia", 21 ".page-content" 22 ]; 23 const claimedLegacySlots = new WeakSet(); 24 const trackedImpressions = new WeakSet(); 25 const registeredFrequencySlots = new WeakSet(); 26 const placementByElement = new WeakMap(); 27 28 function loadManagerConfig() { 29 if (window.ExtremosAdsConfig) return Promise.resolve(window.ExtremosAdsConfig); 30 31 return new Promise((resolve) => { 32 const script = document.createElement("script"); 33 script.src = `${CONFIG_URL}?t=${Date.now()}`; 34 script.async = true; 35 script.onload = () => resolve(window.ExtremosAdsConfig || null); 36 script.onerror = () => resolve(null); 37 document.head.appendChild(script); 38 }); 39 } 40 41 function normalizeToken(value) { 42 return String(value || "") 43 .normalize("NFD") 44 .replace(/[\u0300-\u036f]/g, "") 45 .toLowerCase() 46 .trim() 47 .replace(/[^a-z0-9]+/g, "-") 48 .replace(/^-+|-+$/g, ""); 49 } 50 51 function splitTokens(value) { 52 if (Array.isArray(value)) return value.map(normalizeToken).filter(Boolean); 53 return String(value || "") 54 .split(",") 55 .map(normalizeToken) 56 .filter(Boolean); 57 } 58 59 function getPageCategories(settings) { 60 const categories = new Set(); 61 const selectors = settings.categorySelectors || []; 62 63 selectors.forEach((selector) => { 64 document.querySelectorAll(selector).forEach((element) => { 65 const value = element.tagName === "META" 66 ? element.getAttribute("content") 67 : element.textContent; 68 splitTokens(value).forEach((category) => categories.add(category)); 69 }); 70 }); 71 72 splitTokens(document.body.getAttribute("data-category")).forEach((category) => { 73 categories.add(category); 74 }); 75 76 return Array.from(categories); 77 } 78 79 function matchesCategories(requiredCategories, pageCategories) { 80 const required = splitTokens(requiredCategories); 81 if (!required.length || required.includes("*") || required.includes("all")) return true; 82 return required.some((category) => pageCategories.includes(category)); 83 } 84 85 function getPageChannel() { 86 const path = window.location.pathname.toLowerCase(); 87 if (path === "/" || path === "/index.html") return "home"; 88 if (path.startsWith("/blog/")) return "blog"; 89 if (path.startsWith("/podcast/")) return "podcast"; 90 if (path.startsWith("/online/")) return "online"; 91 if (path.startsWith("/cicloviagem/")) return "cicloviagem"; 92 if (path.startsWith("/expedicao/")) return "expedicao"; 93 if (path.startsWith("/noticia/") && document.body.classList.contains("news-page")) { 94 return "modernNews"; 95 } 96 if (path.startsWith("/noticia/")) return "legacyNews"; 97 return null; 98 } 99 100 function isCoverageOpen(config) { 101 const channel = getPageChannel(); 102 if (!channel) return false; 103 const coverage = config?.coverage?.[channel]; 104 return !coverage || coverage.status === "open"; 105 } 106 107 function isInsideDateRange(start, end, now = Date.now()) { 108 const startTime = start ? Date.parse(start) : Number.NaN; 109 const endTime = end ? Date.parse(end) : Number.NaN; 110 if (!Number.isNaN(startTime) && now < startTime) return false; 111 if (!Number.isNaN(endTime) && now > endTime) return false; 112 return true; 113 } 114 115 function getFrequencyStorage(frequency) { 116 if (!frequency || !Number.isFinite(Number(frequency.limit))) return null; 117 try { 118 return frequency.period === "day" ? window.localStorage : window.sessionStorage; 119 } catch (error) { 120 return null; 121 } 122 } 123 124 function getFrequencyKey(placement) { 125 const frequency = placement.frequency; 126 const day = frequency && frequency.period === "day" 127 ? `:${new Date().toISOString().slice(0, 10)}` 128 : ""; 129 return `extremos-ad-frequency:${placement.id}${day}`; 130 } 131 132 function isFrequencyAllowed(placement) { 133 const frequency = placement.frequency; 134 if (!frequency || Number(frequency.limit) <= 0) return true; 135 const storage = getFrequencyStorage(frequency);
136 if (!storage) return true; 137 const count = Number(storage.getItem(getFrequencyKey(placement))) || 0; 138 return count < Number(frequency.limit); 139 } 140 141 function registerFrequencyImpression(placement) { 142 const frequency = placement.frequency; 143 if (!frequency || Number(frequency.limit) <= 0) return; 144 const storage = getFrequencyStorage(frequency); 145 if (!storage) return; 146 const key = getFrequencyKey(placement); 147 const count = Number(storage.getItem(key)) || 0; 148 storage.setItem(key, String(count + 1)); 149 } 150 151 function findArticleBody(settings) { 152 if (getPageChannel() === "home") { 153 return document.getElementById("home-publicidade"); 154 } 155 const selectors = settings.bodySelectors || DEFAULT_BODY_SELECTORS; 156 for (const selector of selectors) { 157 const body = document.querySelector(selector); 158 if (body) return body; 159 } 160 return document.querySelector("main article, article, main"); 161 } 162 163 function isEditorialParagraph(paragraph, body, settings) { 164 if (!paragraph || !body.contains(paragraph)) return false; 165 if ((paragraph.textContent || "").trim().length < (settings.minimumParagraphCharacters || 40)) { 166 return false; 167 } 168 169 const exclusions = settings.excludedParagraphSelectors || []; 170 if (exclusions.some((selector) => paragraph.matches(selector) || paragraph.closest(selector))) { 171 return false; 172 } 173 174 if (body.classList.contains("page-content")) { 175 return paragraph.classList.contains("texto2") || !paragraph.hasAttribute("class"); 176 } 177 178 return true; 179 } 180 181 function getEditorialParagraphs(body, settings) { 182 if (!body) return []; 183 184 let candidates; 185 if (body.classList.contains("news-article")) { 186 candidates = body.querySelectorAll(settings.modernParagraphSelector || ".news-copy p"); 187 } else if (body.classList.contains("page-content")) { 188 candidates = body.querySelectorAll(settings.legacyParagraphSelector || "p.texto2, p:not([class])"); 189 } else { 190 candidates = body.querySelectorAll("p"); 191 } 192 193 return Array.from(candidates).filter((paragraph) => { 194 return isEditorialParagraph(paragraph, body, settings); 195 }); 196 } 197 198 function getEditorialSections(body, settings) { 199 if (!body || !body.classList.contains("news-article")) return []; 200 const selector = settings.modernSectionSelector || ".news-copy > .news-section"; 201 return Array.from(body.querySelectorAll(selector)).filter((section) => { 202 if (!body.contains(section)) return false; 203 return !section.closest("[data-extremos-ad-slot], .news-comments, [data-extremos-comments]"); 204 }); 205 } 206 207 function findTopAnchor(body, placement) { 208 if (placement.selector) { 209 const configured = document.querySelector(placement.selector); 210 if (configured) return configured; 211 } 212 return document.querySelector(".news-hero, .home-full.main-slide") || body; 213 } 214 215 function insertAfter(anchor, element) { 216 if (!anchor || !anchor.parentNode) return false; 217 anchor.parentNode.insertBefore(element, anchor.nextSibling); 218 return true; 219 } 220 221 function insertBefore(anchor, element) { 222 if (!anchor || !anchor.parentNode) return false; 223 anchor.parentNode.insertBefore(element, anchor); 224 return true; 225 } 226 227 function getParagraphAnchor(paragraph, body, paragraphs, direction) { 228 if (!paragraph || !body.classList.contains("news-article")) return paragraph; 229 const copyBlock = paragraph.closest(".news-copy"); 230 if (!copyBlock) return paragraph; 231 const blockParagraphs = paragraphs.filter((candidate) => copyBlock.contains(candidate)); 232 if (direction === "after" && paragraph === blockParagraphs[blockParagraphs.length - 1]) { 233 return copyBlock; 234 } 235 if (direction === "before" && paragraph === blockParagraphs[0]) return copyBlock; 236 return paragraph; 237 } 238 239 function getSectionAnchor(section, body, direction) { 240 if (!section || !body.classList.contains("news-article")) return section;
241 const copyBlock = section.closest(".news-copy"); 242 if (!copyBlock) return section; 243 if (direction === "after") return copyBlock; 244 return copyBlock; 245 } 246 247 function findCommentsAnchor(body, settings) { 248 const selector = settings.commentsSelector 249 || ".news-comments, [data-extremos-comments], #disqus_thread"; 250 const anchor = body.querySelector(selector); 251 if (!anchor) return null; 252 return anchor.matches("#disqus_thread") 253 ? anchor.closest(".news-comments, [data-extremos-comments], section") || anchor 254 : anchor; 255 } 256 257 function placeAtArticleEnd(slot, body, settings) { 258 const comments = findCommentsAnchor(body, settings); 259 if (comments) return insertBefore(comments, slot); 260 body.appendChild(slot); 261 return true; 262 } 263 264 function placeSlot(slot, placement, context) { 265 if (slot.hasAttribute("data-ad-home-slot")) return true; 266 const { body, paragraphs, sections, settings } = context; 267 const paragraphNumber = Math.max(1, Number.parseInt(placement.paragraph, 10) || 1); 268 const sectionNumber = Math.max(1, Number.parseInt(placement.section, 10) || 1); 269 const requestedParagraph = paragraphs[paragraphNumber - 1]; 270 const finalParagraph = paragraphs[paragraphs.length - 1]; 271 const requestedSection = sections[sectionNumber - 1]; 272 const finalSection = sections[sections.length - 1]; 273 let anchor; 274 275 switch (placement.position) { 276 case "after-intro": 277 anchor = document.querySelector(".news-intro") 278 || document.querySelector(".news-intro__lead"); 279 return anchor ? insertAfter(anchor, slot) : insertBefore(body, slot); 280 281 case "top": 282 anchor = findTopAnchor(body, placement); 283 return anchor === body ? insertBefore(body, slot) : insertAfter(anchor, slot); 284 285 case "middle": 286 anchor = paragraphs[Math.floor((paragraphs.length - 1) / 2)] || body; 287 anchor = getParagraphAnchor(anchor, body, paragraphs, "after"); 288 return anchor === body ? insertBefore(body, slot) : insertAfter(anchor, slot); 289 290 case "end": 291 case "before-comments": 292 return placeAtArticleEnd(slot, body, settings); 293 294 case "after-section": 295 anchor = requestedSection || finalSection; 296 anchor = getSectionAnchor(anchor, body, "after"); 297 return anchor ? insertAfter(anchor, slot) : placeAtArticleEnd(slot, body, settings); 298 299 case "before-section": 300 anchor = requestedSection || finalSection; 301 anchor = getSectionAnchor(anchor, body, "before"); 302 return anchor ? insertBefore(anchor, slot) : insertBefore(body, slot); 303 304 case "after-paragraph": 305 anchor = requestedParagraph || finalParagraph; 306 anchor = getParagraphAnchor(anchor, body, paragraphs, "after"); 307 return anchor ? insertAfter(anchor, slot) : insertAfter(body, slot); 308 309 case "before-paragraph": 310 anchor = requestedParagraph || finalParagraph; 311 anchor = getParagraphAnchor(anchor, body, paragraphs, "before"); 312 return anchor ? insertBefore(anchor, slot) : insertBefore(body, slot); 313 314 case "after-selector": 315 anchor = placement.selector ? document.querySelector(placement.selector) : null; 316 return anchor ? insertAfter(anchor, slot) : (finalParagraph ? insertAfter(finalParagraph, slot) : false); 317 318 case "before-selector": 319 anchor = placement.selector ? document.querySelector(placement.selector) : null; 320 return anchor ? insertBefore(anchor, slot) : (finalParagraph ? insertBefore(finalParagraph, slot) : false); 321 322 case "sidebar-right": 323 slot.classList.add("news-store-rail--right"); 324 return insertBefore(body, slot); 325 326 case "sidebar-left": 327 slot.classList.add("news-store-rail--left"); 328 return insertBefore(body, slot); 329 330 default: 331 return false; 332 } 333 } 334 335 function findLegacySlot(type) { 336 const selector = LEGACY_SLOT_SELECTORS[type]; 337 if (!selector) return null; 338 return Array.from(document.querySelectorAll(selector)).find((slot) => { 339 return !claimedLegacySlots.has(slot); 340 }) || null; 341 } 342 343 function createOrReuseSlot(placement) { 344 let slot = document.querySelector(`[data-extremos-ad-slot="${placement.id}"]`); 345 if (!slot) slot = findLegacySlot(placement.type); 346 if (!slot) slot = document.createElement(placement.type === "cta" ? "div" : "aside"); 347 348 claimedLegacySlots.add(slot); 349 slot.replaceChildren();
350 slot.className = placement.className || ""; 351 slot.setAttribute("data-extremos-ad-slot", placement.id); 352 slot.setAttribute("data-ad-type", placement.type); 353 slot.setAttribute("data-priority", String(placement.priority || 0)); 354 slot.setAttribute("aria-label", placement.ariaLabel || "Publicidade"); 355 slot.setAttribute("aria-busy", "true"); 356 slot.hidden = true; 357 placementByElement.set(slot, placement); 358 return slot; 359 } 360 361 function removeUnusedLegacySlots(activeSlots) { 362 Object.values(LEGACY_SLOT_SELECTORS).forEach((selector) => { 363 document.querySelectorAll(selector).forEach((slot) => { 364 if (!activeSlots.has(slot)) slot.remove(); 365 }); 366 }); 367 } 368 369 function getItemId(item, type) { 370 return item.getAttribute("data-ad-id") 371 || `${type}:${item.getAttribute("data-billboard") 372 || item.getAttribute("data-trip-card") 373 || item.getAttribute("data-book-ad") 374 || item.getAttribute("data-book") 375 || item.getAttribute("data-cta") 376 || "unknown"}`; 377 } 378 379 function getCreativeStatus(item, placement) { 380 const id = item.getAttribute("data-ad-id"); 381 const override = Array.isArray(placement?.creatives) 382 ? placement.creatives.find((creative) => creative.id === id) 383 : null; 384 return override ? override.status : item.getAttribute("data-status"); 385 } 386 387 function isCampaignEligible(item, pageCategories, placement) { 388 if (getCreativeStatus(item, placement) !== "on") return false; 389 if (!matchesCategories(item.getAttribute("data-categories") || ["*"], pageCategories)) return false; 390 return isInsideDateRange( 391 item.getAttribute("data-start"), 392 item.getAttribute("data-end") 393 ); 394 } 395 396 function randomNumber() { 397 if (window.crypto && window.crypto.getRandomValues) { 398 return window.crypto.getRandomValues(new Uint32Array(1))[0] / 4294967296; 399 } 400 return Math.random(); 401 } 402 403 function weightedPick(items) { 404 if (!items.length) return null; 405 const weights = items.map((item) => Math.max(0, Number(item.getAttribute("data-weight")) || 1)); 406 const total = weights.reduce((sum, weight) => sum + weight, 0); 407 if (total <= 0) return items[Math.floor(randomNumber() * items.length)]; 408 409 let cursor = randomNumber() * total; 410 for (let index = 0; index < items.length; index += 1) { 411 cursor -= weights[index]; 412 if (cursor <= 0) return items[index]; 413 } 414 return items[items.length - 1]; 415 } 416 417 function weightedSelection(items, count) { 418 const pool = items.slice(); 419 const selected = []; 420 while (pool.length && selected.length < count) { 421 const item = weightedPick(pool); 422 selected.push(item); 423 pool.splice(pool.indexOf(item), 1); 424 } 425 return selected; 426 } 427 428 function selectFixedItems(items, fixedValue, count, attribute) { 429 const fixedIds = String(fixedValue || "") 430 .split(",") 431 .map((id) => id.trim()) 432 .filter(Boolean); 433 const selected = []; 434 435 fixedIds.forEach((id) => { 436 const item = items.find((candidate) => candidate.getAttribute(attribute) === id); 437 if (item && !selected.includes(item) && selected.length < count) selected.push(item); 438 }); 439 440 items.forEach((item) => { 441 if (!selected.includes(item) && selected.length < count) selected.push(item); 442 }); 443 return selected; 444 } 445 446 function parseFragment(html) { 447 const template = document.createElement("template"); 448 template.innerHTML = html.trim(); 449 return template.content; 450 } 451
452 function renderStoreFragment(fragment, placement, pageCategories) { 453 const config = fragment.querySelector("[data-store-config]"); 454 if (!config) return null; 455 456 const inner = config.querySelector(".news-store-rail__inner"); 457 const list = config.querySelector(".news-store-rail__list"); 458 const items = Array.from(config.querySelectorAll("[data-book]")).filter((item) => { 459 return isCampaignEligible(item, pageCategories, placement); 460 }); 461 const count = Math.max(1, Math.min(Number.parseInt(config.getAttribute("data-count"), 10) || 3, items.length)); 462 if (!inner || !list || !items.length) return null; 463 464 const selected = config.getAttribute("data-mode") === "fixed" 465 ? selectFixedItems(items, config.getAttribute("data-fixed"), count, "data-book") 466 : weightedSelection(items, count); 467 468 const rendered = inner.cloneNode(true); 469 const renderedList = rendered.querySelector(".news-store-rail__list"); 470 renderedList.replaceChildren(...selected.map((item) => item.cloneNode(true))); 471 return rendered; 472 } 473
474 function renderSingleFragment(fragment, placement, pageCategories) { 475 const definitions = { 476 billboard: { 477 config: "[data-billboard-config]", 478 item: "[data-billboard]", 479 attribute: "data-billboard" 480 }, 481 travel: { 482 config: "[data-trip-config]", 483 item: "[data-trip-card]", 484 attribute: "data-trip-card" 485 }, 486 book: { 487 config: "[data-book-ad-config]", 488 item: "[data-book-ad]", 489 attribute: "data-book-ad" 490 }, 491 cta: { 492 config: "[data-cta-config]", 493 item: "[data-cta]", 494 attribute: "data-cta" 495 } 496 }; 497 const definition = definitions[placement.type]; 498 if (!definition) return null; 499 500 const config = fragment.querySelector(definition.config); 501 if (!config) return null; 502 503 let items; 504 if (placement.type === "cta") { 505 const activeItems = Array.from(config.querySelectorAll(definition.item)).filter((item) => { 506 if (getCreativeStatus(item, placement) !== "on") return false; 507 return isInsideDateRange(item.getAttribute("data-start"), item.getAttribute("data-end")); 508 }); 509 const segmentItems = activeItems.filter((item) => { 510 const categories = splitTokens(item.getAttribute("data-categories")); 511 return categories.some((category) => { 512 return category !== "all" && category !== "*" && pageCategories.includes(category); 513 }); 514 }); 515 const allItems = activeItems.filter((item) => { 516 const categories = splitTokens(item.getAttribute("data-categories")); 517 return categories.includes("all") || categories.includes("*") || !categories.length; 518 }); 519 items = segmentItems.length ? segmentItems : allItems; 520 } else { 521 items = Array.from(config.querySelectorAll(definition.item)).filter((item) => { 522 return isCampaignEligible(item, pageCategories, placement); 523 }); 524 } 525 if (!items.length) return null; 526 527 let selected; 528 if (config.getAttribute("data-mode") === "fixed") { 529 selected = items.find((item) => { 530 return item.getAttribute(definition.attribute) === config.getAttribute("data-fixed"); 531 }) || items[0]; 532 } else { 533 selected = weightedPick(items); 534 } 535 return selected.cloneNode(true); 536 } 537 538 async function loadAndRenderPlacement(slot, placement, pageCategories, ga4Settings) { 539 try { 540 const response = await window.fetch(placement.fragment, { 541 credentials: "same-origin", 542 cache: "no-store" 543 }); 544 if (!response.ok) throw new Error(`Falha ao carregar ${placement.id}`); 545 546 const fragment = parseFragment(await response.text()); 547 const content = placement.type === "books" 548 ? renderStoreFragment(fragment, placement, pageCategories) 549 : renderSingleFragment(fragment, placement, pageCategories); 550 551 if (!content) { 552 slot.remove(); 553 return; 554 } 555 556 slot.replaceChildren(content); 557 slot.hidden = false; 558 slot.removeAttribute("aria-busy"); 559 activateAdTracking(slot, placement, pageCategories, ga4Settings); 560 } catch (error) { 561 slot.remove(); 562 } 563 } 564 565 function getAdMetadata(advertisement, placement, pageCategories) { 566 const type = placement?.type || advertisement.closest("[data-ad-type]")?.getAttribute("data-ad-type") || "ad"; 567 const slot = advertisement.closest("[data-extremos-ad-slot]"); 568 const placementId = placement?.id || slot?.getAttribute("data-extremos-ad-slot") || "unknown-placement"; 569 const title = advertisement.querySelector("h3, strong"); 570 const image = advertisement.querySelector("img"); 571 const name = title?.textContent.trim() 572 || advertisement.getAttribute("aria-label") 573 || getItemId(advertisement, type); 574 575 return { 576 id: getItemId(advertisement, type), 577 name, 578 type, 579 placementId, 580 position: placement?.position || "legacy", 581 priority: Number(placement?.priority || 0), 582 variant: advertisement.getAttribute("data-variant") || "default", 583 creative: image?.getAttribute("alt") || name, 584 categories: pageCategories || [] 585 }; 586 } 587 588 function sendGa4Promotion(eventName, advertisement, placement, pageCategories) { 589 if (typeof window.gtag !== "function") return; 590 const metadata = getAdMetadata(advertisement, placement, pageCategories); 591 592 window.gtag("event", eventName, { 593 promotion_id: metadata.id, 594 promotion_name: metadata.name, 595 creative_name: metadata.creative, 596 creative_slot: metadata.placementId, 597 placement_id: metadata.placementId, 598 placement_position: metadata.position, 599 ad_type: metadata.type, 600 ad_variant: metadata.variant, 601 ad_priority: metadata.priority, 602 page_categories: metadata.categories.join(","), 603 page_path: window.location.pathname, 604 items: [{ 605 item_id: metadata.id, 606 item_name: metadata.name, 607 item_category: metadata.type, 608 item_category2: metadata.variant, 609 item_list_id: metadata.placementId, 610 item_list_name: metadata.position 611 }] 612 });
613 } 614 615 function addExternalCampaignParameters(advertisement, placement, pageCategories) { 616 const metadata = getAdMetadata(advertisement, placement, pageCategories); 617 advertisement.querySelectorAll("a[href]").forEach((link) => { 618 let destination; 619 try { 620 destination = new URL(link.href, window.location.href); 621 } catch (error) { 622 return; 623 } 624 625 if (destination.origin === window.location.origin || !/^https?:$/.test(destination.protocol)) return; 626 if (!destination.searchParams.has("utm_source")) destination.searchParams.set("utm_source", "extremos"); 627 if (!destination.searchParams.has("utm_medium")) destination.searchParams.set("utm_medium", "display"); 628 if (!destination.searchParams.has("utm_campaign")) destination.searchParams.set("utm_campaign", metadata.id); 629 if (!destination.searchParams.has("utm_content")) destination.searchParams.set("utm_content", metadata.placementId); 630 link.href = destination.toString(); 631 }); 632 } 633 634 function observeAdImpression(advertisement, placement, pageCategories, ga4Settings) { 635 if (advertisement.getAttribute("data-ga4-ready") === "true") return; 636 advertisement.setAttribute("data-ga4-ready", "true"); 637 addExternalCampaignParameters(advertisement, placement, pageCategories); 638 639 const register = () => { 640 if (trackedImpressions.has(advertisement)) return; 641 trackedImpressions.add(advertisement); 642 const slot = advertisement.closest("[data-extremos-ad-slot]") || advertisement; 643 if (!registeredFrequencySlots.has(slot)) { 644 registeredFrequencySlots.add(slot); 645 registerFrequencyImpression(placement); 646 } 647 if (ga4Settings.enabled !== false) { 648 sendGa4Promotion("view_promotion", advertisement, placement, pageCategories); 649 } 650 }; 651 652 if (!("IntersectionObserver" in window)) { 653 register(); 654 return; 655 } 656 657 const ratio = Number(ga4Settings.visibleRatio) || 0.5; 658 const duration = Number(ga4Settings.visibleMilliseconds) || 1000; 659 let visibleTimer = 0; 660 const observer = new IntersectionObserver((entries) => { 661 entries.forEach((entry) => { 662 if (entry.intersectionRatio >= ratio) { 663 if (visibleTimer) return; 664 visibleTimer = window.setTimeout(() => { 665 visibleTimer = 0; 666 register(); 667 observer.unobserve(advertisement); 668 }, duration); 669 } else if (visibleTimer) { 670 window.clearTimeout(visibleTimer); 671 visibleTimer = 0; 672 } 673 }); 674 }, { threshold: [0, ratio, 1] }); 675 676 observer.observe(advertisement); 677 } 678 679 function activateAdTracking(container, placement, pageCategories, ga4Settings = {}) { 680 const advertisements = []; 681 if (container.matches(AD_ITEM_SELECTOR)) advertisements.push(container); 682 advertisements.push(...container.querySelectorAll(AD_ITEM_SELECTOR)); 683 advertisements.forEach((advertisement) => { 684 placementByElement.set(advertisement, placement); 685 observeAdImpression(advertisement, placement, pageCategories, ga4Settings); 686 }); 687 } 688 689 function initializeAdClickTracking(pageCategories, ga4Settings) { 690 document.addEventListener("click", (event) => { 691 const link = event.target.closest?.("a[href]"); 692 const advertisement = link?.closest?.(AD_ITEM_SELECTOR); 693 if (!advertisement) return; 694 const placement = placementByElement.get(advertisement) 695 || placementByElement.get(advertisement.closest("[data-extremos-ad-slot]")); 696 if (!placement || ga4Settings.enabled === false) return; 697 sendGa4Promotion("select_promotion", advertisement, placement, pageCategories); 698 }); 699 } 700 701 function isPlacementActive(placement, pageCategories, channel) { 702 if (!placement?.id || !placement.fragment || placement.status !== "open") return false; 703 if (Array.isArray(placement.channels) && !placement.channels.includes(channel)) return false; 704 if (!matchesCategories(placement.categories, pageCategories)) return false;
705 if (!isInsideDateRange(placement.start, placement.end)) return false; 706 return isFrequencyAllowed(placement); 707 } 708 709 function resolvePlacementForViewport(placement) { 710 const viewport = window.matchMedia("(max-width: 1279px)").matches ? "mobile" : "desktop"; 711 const responsiveRule = placement && typeof placement[viewport] === "object" 712 ? placement[viewport] 713 : null; 714 return { 715 ...placement, 716 ...(responsiveRule || {}), 717 viewport 718 }; 719 } 720 721 async function initializeAdManager(config) { 722 if (!config || !Array.isArray(config.placements)) { 723 removeUnusedLegacySlots(new Set()); 724 return; 725 } 726 727 if (!isCoverageOpen(config)) { 728 removeUnusedLegacySlots(new Set()); 729 document.dispatchEvent(new CustomEvent("extremos:ads-disabled", { 730 detail: { channel: getPageChannel(), version: config.version } 731 })); 732 return; 733 } 734 735 const settings = config.settings || {}; 736 const body = findArticleBody(settings); 737 if (!body) return; 738 739 const pageCategories = getPageCategories(settings); 740 const ga4Settings = settings.ga4 || {}; 741 const paragraphs = getEditorialParagraphs(body, settings); 742 const sections = getEditorialSections(body, settings); 743 const activeSlots = new Set(); 744 const activePlacements = config.placements 745 .map(resolvePlacementForViewport) 746 .filter((placement) => isPlacementActive(placement, pageCategories, getPageChannel())) 747 .sort((first, second) => Number(first.priority || 0) - Number(second.priority || 0)); 748 749 const jobs = []; 750 activePlacements.forEach((placement) => { 751 const slot = createOrReuseSlot(placement); 752 placementByElement.set(slot, placement); 753 if (!placeSlot(slot, placement, { body, paragraphs, sections, settings })) { 754 slot.remove(); 755 return; 756 } 757 activeSlots.add(slot); 758 jobs.push(loadAndRenderPlacement(slot, placement, pageCategories, ga4Settings)); 759 }); 760 761 removeUnusedLegacySlots(activeSlots); 762 initializeAdClickTracking(pageCategories, ga4Settings); 763 await Promise.all(jobs); 764 765 document.dispatchEvent(new CustomEvent("extremos:ads-ready", { 766 detail: { 767 version: config.version, 768 categories: pageCategories, 769 paragraphs: paragraphs.length, 770 sections: sections.length, 771 placements: activePlacements.map((placement) => placement.id) 772 } 773 })); 774 } 775 776 loadManagerConfig().then(initializeAdManager); 777}());
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.