PageSourceSearch

https://www.steamlocomotives.org/js/ads.js?v=1784468293

js steamlocomotives.org collected 2026-10-03 02:01:24 UTC 9,843 bytes, 234 lines download raw bytes

1/* public/js/ads.js — ad lifecycle manager.
2 *
3 * Two responsibilities:
4 *   1) Fire adsbygoogle.push({}) for every <ins class="adsbygoogle"
5 *      data-ad-push-pending="1"> AFTER window.load. This replaces the
6 *      inline push script AdSense normally wants next to every <ins>,
7 *      because that inline push fires mid-parse — at which point any
8 *      <ins> inside a position:fixed sticky wrapper measures
9 *      offsetWidth=0 and AdSense aborts with "No slot size for
10 *      availableWidth=0". Waiting for load guarantees stable layout.
11 *
12 *   2) For sticky/float wrappers: measure the rendered height, set it
13 *      as a CSS custom property (--ad-sticky-bottom-h / --ad-sticky-top-h)
14 *      and add a body class so ads.css can reserve exact page padding.
15 *      This keeps the footer and page content visible — the ad pushes
16 *      content out of its way rather than overlaying it.
17 */
18(function () {
19    'use strict';
20
21    // ------------------------------------------------------------------
22    // 1. Fire the deferred adsbygoogle pushes
23    // ------------------------------------------------------------------
24
25    function pushPending() {
26        var units = document.querySelectorAll(
27            'ins.adsbygoogle[data-ad-push-pending="1"]'
28        );
29        for (var i = 0; i < units.length; i++) {
30            var ins = units[i];
31            ins.removeAttribute('data-ad-push-pending');
32            try {
33                (window.adsbygoogle = window.adsbygoogle || []).push({});
34            } catch (e) {
35                if (window.console) console.warn('ad push failed:', e);
36            }
37            // Start watching for unfilled so we can swap in backfill HTML
38            // if the unit has any. Cheap if the unit has no backfill block.
39            watchForBackfill(ins);
40        }
41    }
42
43    if (document.readyState === 'complete') {
44        setTimeout(pushPending, 0);
45    } else {
46        window.addEventListener('load', pushPending);
47    }
48
49    // ------------------------------------------------------------------
50    // Backfill — show admin-supplied house ad when AdSense doesn't fill.
51    //
52    // *** DO NOT REINTRODUCE display:none ON THE <ins>. ***
53    //
54    // This used to set ins.style.display='none' as soon as it decided to
55    // show the backfill, including on the 3.5s backstop — i.e. while the ad
56    // request was very often STILL IN FLIGHT. AdSense then found a hidden
57    // tag and logged "No ad requested because of display:none on the
58    // adsbygoogle tag", so the unit could never fill, so the house backfill
59    // won permanently. The slot was unfillable by construction.
60    //
61    // The rule: hiding the <ins> before AdSense has resolved the request
62    // cancels the request. So we never hide it at all. The <ins> stays in
63    // the layout for the whole page lifetime, which also means a later
64    // AdSense refresh can still fill it.
65    //
66    // Decision logic:
67    //   - data-ad-status="filled"   → hide the backfill (ad wins)
68    //   - data-ad-status="unfilled" → show the backfill (definitive: the
69    //                                 request completed, nothing to show)
70    //   - still no status at 3.5s   → show the backfill PROVISIONALLY, but
71    //                                 leave the <ins> alone and keep
72    //                                 watching. If the ad lands late, the
73    //                                 backfill is hidden again.
74    //
75    // The backfill is absolutely positioned over the <ins> (see ads.css), so
76    // both states occupy the same reserved space and nothing reflows. The
77    // <ins> only gets pointer-events:none while the backfill is up, so
78    // clicks reach the house ad rather than an empty ad slot.
79    //
80    // Status is watched with a MutationObserver on the attribute plus a
81    // polling fallback, so a late fill is noticed either way.
82    //
83    // Beacons fire once on reveal (impression) and on click (anywhere inside
84    // the backfill block). GDPR-safe: no cookies, no fingerprinting, no PII —
85    // the endpoint just increments two integers on the adsense_units row.
86    // ------------------------------------------------------------------
87
88    function findBackfill(ins) {
89        var wrap = ins.closest('.ad-sticky, .ad-float, .ad-inline');
90        if (!wrap || !wrap.classList.contains('ad-has-backfill')) return null;
91        var node = wrap.querySelector('.ad-backfill');
92        return node || null;
93    }
94
95    function sendBeacon(action, slug) {
96        if (!slug) return;
97        var url = '/ads-track.php?action=' + encodeURIComponent(action)
98                + '&unit=' + encodeURIComponent(slug);
99        try {
100            if (navigator.sendBeacon) {
101                // sendBeacon needs a body for the request to be POST.
102                navigator.sendBeacon(url, new Blob([''], { type: 'application/x-www-form-urlencoded' }));
103            } else {
104                // Fallback for very old browsers; fire-and-forget.
105                fetch(url, { method: 'POST', keepalive: true, credentials: 'omit' }).catch(function () {});
106            }
107        } catch (e) { /* never let analytics break the page */ }
108    }
109
110    function showBackfill(ins, backfill) {
111        if (!backfill.hasAttribute('hidden')) return;   // already visible
112        backfill.removeAttribute('hidden');
113
114        // NOTE: the <ins> deliberately keeps display:block here. Only its
115        // interactivity is suppressed, so clicks land on the house ad while
116        // the slot itself stays alive and refreshable.
117        ins.style.pointerEvents = 'none';
118
119        var slug = backfill.getAttribute('data-ad-backfill-slug') || '';
120
121        // Impression fires once per page view even if the backfill is hidden
122        // and shown again by a late fill / later unfill.
123        if (!backfill.hasAttribute('data-ad-impression-sent')) {
124            backfill.setAttribute('data-ad-impression-sent', '1');
125            sendBeacon('impression', slug);
126        }
127        // Delegate clicks anywhere inside the backfill — fires the click
128        // beacon then lets the original link / handler proceed normally.
129        if (!backfill.hasAttribute('data-ad-click-bound')) {
130            backfill.setAttribute('data-ad-click-bound', '1');
131            backfill.addEventListener('click', function () { sendBeacon('click', slug); }, { passive: true });
132        }
133    }
134
135    function hideBackfill(ins, backfill) {
136        if (backfill.hasAttribute('hidden')) return;    // already hidden
137        backfill.setAttribute('hidden', '');
138        ins.style.pointerEvents = '';
139    }
140
141    function watchForBackfill(ins) {
142        var backfill = findBackfill(ins);
143        if (!backfill) return;   // no backfill configured for this unit
144
145        var t0 = Date.now();
146        var REVEAL_MS  = 3500;    // provisional reveal while still pending
147        var GIVE_UP_MS = 30000;   // stop watching for a late fill
148        var POLL_MS    = 250;
149
150        function evaluate() {
151            var status = ins.getAttribute('data-ad-status');
152            if (status === 'filled')   { hideBackfill(ins, backfill); return; }
153            if (status === 'unfilled') { showBackfill(ins, backfill); return; }
154            // No status yet — the request may still be in flight. Show the
155            // house ad so the space isn't blank, but do NOT touch the <ins>.
156            if ((Date.now() - t0) >= REVEAL_MS) showBackfill(ins, backfill);
157        }
158
159        evaluate();
160
161        var mo = null;
162        if (window.MutationObserver) {
163            mo = new MutationObserver(evaluate);
164            mo.observe(ins, { attributes: true, attributeFilter: ['data-ad-status'] });
165        }
166
167        // Polling fallback — also bounds how long we keep watching.
168        var iv = setInterval(function () {
169            evaluate();
170            if ((Date.now() - t0) >= GIVE_UP_MS) {
171                clearInterval(iv);
172                if (mo) mo.disconnect();
173            }
174        }, POLL_MS);
175    }
176
177    // ------------------------------------------------------------------
178    // 2. Measure sticky wrappers and reserve page padding
179    // ------------------------------------------------------------------
180
181    function measure(wrap, varName, bodyClass) {
182        if (!wrap) return;
183
184        var sync = function () {
185            var cs = window.getComputedStyle(wrap);
186            if (cs.display === 'none' || wrap.hasAttribute('hidden')) {
187                document.body.classList.remove(bodyClass);
188                document.documentElement.style.setProperty(varName, '0px');
189                return;
190            }
191            var h = wrap.offsetHeight;
192            if (!h) {
193                document.body.classList.remove(bodyClass);
194                document.documentElement.style.setProperty(varName, '0px');
195                return;
196            }
197            document.documentElement.style.setProperty(varName, h + 'px');
198            document.body.classList.add(bodyClass);
199        };
200
201        sync();
202
203        if (window.ResizeObserver) {
204            new ResizeObserver(sync).observe(wrap);
205        } else {
206            window.addEventListener('resize', sync);
207        }
208
209        var attempts = 0;
210        var poll = setInterval(function () {
211            sync();
212            if (++attempts > 10) clearInterval(poll);
213        }, 500);
214    }
215
216    function initSticky() {
217        measure(
218            document.querySelector('.ad-sticky--bottom'),
219            '--ad-sticky-bottom-h',
220            'has-ad-sticky-bottom'
221        );
222        measure(
223            document.querySelector('.ad-sticky--top'),
224            '--ad-sticky-top-h',
225            'has-ad-sticky-top'
226        );
227    }
228
229    if (document.readyState === 'loading') {
230        document.addEventListener('DOMContentLoaded', initSticky);
231    } else {
232        initSticky();
233    }
234})();

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.