PageSourceSearch

https://sxm-talks.com/js/ad-banner.js

js sxm-talks.com collected 2026-09-24 09:03:40 UTC 9,998 bytes, 351 lines download raw bytes

1(function () {
2  function enhanceAdAccessibility(container) {
3    var links = container.querySelectorAll("a");
4    links.forEach(function (link) {
5      if (!link.getAttribute("target")) {
6        link.setAttribute("target", "_blank");
7        link.setAttribute("rel", "noopener noreferrer");
8      }
9
10      if (link.getAttribute("aria-label") || link.textContent.trim()) return;
11
12      var img = link.querySelector("img");
13      if (img) {
14        var alt = (img.getAttribute("alt") || "").trim();
15        if (alt) {
16          link.setAttribute("aria-label", alt);
17        } else {
18          link.setAttribute("aria-label", "Advertisement");
19        }
20        img.setAttribute("alt", "");
21      } else {
22        link.setAttribute("aria-label", "Advertisement");
23      }
24    });
25  }
26
27  function waitForImages(container, callback) {
28    var imgs = container.querySelectorAll("img");
29    if (!imgs.length) {
30      callback();
31      return;
32    }
33
34    var remaining = imgs.length;
35    function done() {
36      remaining--;
37      if (remaining <= 0) callback();
38    }
39
40    for (var i = 0; i < imgs.length; i++) {
41      var img = imgs[i];
42      if (img.complete) done();
43      else {
44        img.addEventListener("load", done);
45        img.addEventListener("error", done);
46      }
47    }
48  }
49
50  function stabilizeSlideHeight(container, slides) {
51    var maxHeight = 0;
52    for (var i = 0; i < slides.length; i++) {
53      slides[i].classList.add("is-measuring");
54      maxHeight = Math.max(maxHeight, slides[i].offsetHeight);
55      slides[i].classList.remove("is-measuring");
56    }
57    if (maxHeight > 0) {
58      container.style.minHeight = maxHeight + "px";
59    }
60    container.classList.add("is-ready");
61  }
62
63  function clearAdInterval(el) {
64    var id = el.dataset.adIntervalId;
65    if (id) {
66      clearInterval(parseInt(id, 10));
67      delete el.dataset.adIntervalId;
68    }
69  }
70
71  function recordImpression(url, adId) {
72    if (!adId) return;
73    var baseUrl = url.split("/adrotate/serve")[0];
74    fetch(baseUrl + "/adrotate/serve", {
75      method: "POST",
76      headers: { "Content-Type": "application/json" },
77      body: JSON.stringify({
78        action: "impression",
79        adId: parseInt(adId, 10),
80      }),
81    }).catch(function () {});
82  }
83
84  function showSlide(slides, index) {
85    for (var i = 0; i < slides.length; i++) {
86      slides[i].classList.toggle("is-active", i === index);
87    }
88  }
89
90  function preloadSlideImage(slide, callback) {
91    var img = slide.querySelector("img");
92    if (!img) {
93      callback();
94      return;
95    }
96
97    img.setAttribute("loading", "eager");
98
99    if (img.complete && img.naturalWidth > 0) {
100      callback();
101      return;
102    }
103
104    function onDone() {
105      img.removeEventListener("load", onDone);
106      img.removeEventListener("error", onDone);
107      callback();
108    }
109
110    img.addEventListener("load", onDone);
111    img.addEventListener("error", onDone);
112  }
113
114  function startRotation(el, slides, url, speed) {
115    clearAdInterval(el);
116
117    var currentIndex = 0;
118    var isTransitioning = false;
119
120    var intervalId = setInterval(function () {
121      if (!document.body.contains(el)) {
122        clearInterval(intervalId);
123        return;
124      }
125      if (isTransitioning) return;
126
127      isTransitioning = true;
128      var nextIndex = (currentIndex + 1) % slides.length;
129      var nextSlide = slides[nextIndex];
130
131      preloadSlideImage(nextSlide, function () {
132        showSlide(slides, nextIndex);
133        recordImpression(url, nextSlide.getAttribute("data-ad-id"));
134        currentIndex = nextIndex;
135
136        window.setTimeout(function () {
137          isTransitioning = false;
138        }, 500);
139      });
140    }, speed);
141
142    el.dataset.adIntervalId = String(intervalId);
143  }
144
145  function parseRatio(ratio) {
146    if (!ratio) return null;
147    var parts = ratio.split("/");
148    if (parts.length === 2) {
149      var w = parseFloat(parts[0]);
150      var h = parseFloat(parts[1]);
151      if (w && h) {
152        return {
153          width: Math.round(w * 100),
154          height: Math.round(h * 100),
155        };
156      }
157    }
158    return null;
159  }
160
161  function getDefaultImageDimensions(el) {
162    var ratioAttr = el.getAttribute("data-ad-ratio");
163    var parsed = parseRatio(ratioAttr);
164    if (parsed) return parsed;
165
166    var minHeight = el.getAttribute("data-ad-min-height") || "250px";
167    var heightNum = parseInt(minHeight, 10);
168    if (!heightNum) heightNum = 250;
169    return { width: Math.round(heightNum * 1.2), height: heightNum };
170  }
171
172  function setImageDimensions(el) {
173    var dims = getDefaultImageDimensions(el);
174    var imgs = el.querySelectorAll(".ad-slide img, .ad-block-item img");
175    imgs.forEach(function (img) {
176      if (!img.hasAttribute("width")) {
177        img.setAttribute("width", String(dims.width));
178      }
179      if (!img.hasAttribute("height")) {
180        img.setAttribute("height", String(dims.height));
181      }
182    });
183  }
184
185  /**
186   * Avoid Mixed Content: HTTPS pages must not fetch http://… ad endpoints.
187   * Prefer same-origin relative /adrotate/serve when an absolute http URL was baked in.
188   */
189  function normalizeAdApiUrl(url) {
190    if (!url) return url;
191    try {
192      if (url.charAt(0) === "/") return url;
193      if (typeof location !== "undefined" && location.protocol === "https:") {
194        var parsed = new URL(url, location.href);
195        if (parsed.protocol === "http:" && parsed.pathname.indexOf("/adrotate/") === 0) {
196          return parsed.pathname + parsed.search;
197        }
198      }
199    } catch (e) {}
200    return url;
201  }
202
203  function initEl(el) {
204    if (el.dataset.adInitialized === "true") return;
205    el.dataset.adInitialized = "true";
206
207    var url = normalizeAdApiUrl(el.getAttribute("data-ad-api"));
208    if (!url) return;
209
210    fetch(url, { cache: "no-store" })
211      .then(function (r) {
212        if (!r.ok) return null;
213        return r.json();
214      })
215      .then(function (data) {
216        if (!data || !data.ads || !data.ads.length) {
217          el.innerHTML = "";
218          return;
219        }
220
221        var ads = data.ads;
222        var modus = data.groupModus || "dynamic";
223        var speed =
224          data.adSpeed ||
225          parseInt(el.getAttribute("data-ad-rotate") || "7000", 10);
226
227        // Optional: pin this banner to a single ad by 0-based index
228        // (e.g. first/second/third in-article slot → ad 0/1/2).
229        var adIndexAttr = el.getAttribute("data-ad-index");
230        var pinnedIndex =
231          adIndexAttr !== null && adIndexAttr !== ""
232            ? parseInt(adIndexAttr, 10)
233            : NaN;
234        if (!isNaN(pinnedIndex) && pinnedIndex >= 0) {
235          if (pinnedIndex >= ads.length) {
236            el.innerHTML = "";
237            return;
238          }
239          ads = [ads[pinnedIndex]];
240          // Single ad: never rotate or render as multi-ad block
241          modus = "dynamic";
242        }
243
244        if (modus === "block") {
245          var html =
246            '<div class="ad-block-grid flex flex-wrap gap-4 justify-center w-full">';
247          for (var i = 0; i < ads.length; i++) {
248            html += '<div class="ad-block-item">' + ads[i].html + "</div>";
249          }
250          html += "</div>";
251          el.innerHTML = html;
252          enhanceAdAccessibility(el);
253          setImageDimensions(el);
254        } else {
255          var html = '<div class="ad-slides-container">';
256          for (var j = 0; j < ads.length; j++) {
257            var activeClass = j === 0 ? " is-active" : "";
258            html +=
259              '<div class="ad-slide' +
260              activeClass +
261              '" data-ad-id="' +
262              ads[j].id +
263              '">' +
264              ads[j].html +
265              "</div>";
266          }
267          html += "</div>";
268          el.innerHTML = html;
269          enhanceAdAccessibility(el);
270          setImageDimensions(el);
271
272          var adImages = el.querySelectorAll(".ad-slide img");
273          adImages.forEach(function (img, index) {
274            if (!img.hasAttribute("decoding")) {
275              img.setAttribute("decoding", "async");
276            }
277            // Only the first/active slide needs to load eagerly.
278            // Hidden slides can lazy-load to reduce initial payload.
279            if (index === 0) {
280              img.setAttribute("loading", "eager");
281              img.setAttribute("fetchpriority", "low");
282            } else {
283              img.setAttribute("loading", "lazy");
284            }
285          });
286
287          var container = el.querySelector(".ad-slides-container");
288          var slides = el.querySelectorAll(".ad-slide");
289
290          waitForImages(el, function () {
291            stabilizeSlideHeight(container, slides);
292
293            if (ads.length > 1) {
294              startRotation(el, slides, url, speed);
295            }
296          });
297        }
298      })
299      .catch(function () {
300        el.innerHTML = "";
301      });
302  }
303
304  function observeEl(el) {
305    if (!("IntersectionObserver" in window)) {
306      initEl(el);
307      return;
308    }
309
310    var observer = new IntersectionObserver(
311      function (entries) {
312        for (var i = 0; i < entries.length; i++) {
313          if (entries[i].isIntersecting) {
314            initEl(el);
315            observer.disconnect();
316            break;
317          }
318        }
319      },
320      // Keep ads out of the LCP critical path (was 300px — fetched too early on mobile)
321      { rootMargin: "80px 0px", threshold: 0.01 }
322    );
323
324    observer.observe(el);
325  }
326
327  function init() {
328    var els = document.querySelectorAll(".ad-banner:not([data-ad-initialized])");
329    els.forEach(observeEl);
330  }
331
332  function startWhenIdle() {
333    var run = function () {
334      init();
335    };
336    if ("requestIdleCallback" in window) {
337      requestIdleCallback(run, { timeout: 2500 });
338    } else {
339      setTimeout(run, 200);
340    }
341  }
342
343  // After full load so LCP image wins bandwidth on slow mobile
344  if (document.readyState === "complete") {
345    startWhenIdle();
346  } else {
347    window.addEventListener("load", startWhenIdle);
348  }
349
350  document.addEventListener("astro:page-load", startWhenIdle);
351})();

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.