1/*! 2 * Centralized AdSense loader for onlinecompass.net. 3 * 4 * Does three things, all from one deferred script: 5 * 6 * 1. DEFER THE AD LIBRARIES. The PubGuru header-bidding wrapper and the 7 * AdSense loader are NOT placed in <head>. This script injects them only 8 * AFTER the Cookiebot consent decision, at the next idle moment â so their 9 * main-thread work (including Google's Funding Choices TCF wall) never 10 * competes with first paint or the pre-consent interactions. This is the 11 * main INP/LCP lever for an ad-supported page. 12 * Order is preserved (PubGuru before AdSense) so bids are ready before slots 13 * fill, and revenue/header-bidding are unaffected â ads simply load a moment 14 * later, during idle time. 15 * 16 * 2. FIX THE ADSENSE TAG ERRORS the old per-slot inline push() scripts caused: 17 * - "No slot size for availableWidth=0": push({}) fills the NEXT unfilled 18 * <ins> in document order, so a hidden responsive variant (ad-mobile / 19 * ad-tablet / ad-desktop, display:none on non-matching viewports) was 20 * measured at width 0 and also stole a push meant for a visible slot. 21 * Fixed by removing every hidden <ins> before any push runs. 22 * - "All 'ins' elements ... already have ads in them": push({}) throws 23 * this when no <ins> is left to fill â which happens when PubGuru 24 * header-bidding or page-level auto-ads fill a slot out-of-band, so a 25 * pure count cap can over-push by one. Fixed by also requiring an 26 * unprocessed <ins> (no data-ad-status) before each push, with a 27 * try/catch around push() for the residual async race. 28 * 29 * 3. LAZY-FILL SLOTS. Ad <iframe> creation (triggered by push()) is heavy, so 30 * visible slots are filled via IntersectionObserver only as they near the 31 * viewport. A 4 s safety net hides the skeleton shimmer if a slot never 32 * resolves, so it can't appear to load forever. 33 * 34 * REVERT to eager loading: put the two <script> tags back in <head> of 35 * Master.blade.php / DistancMap.blade.php and delete injectLoaders()/onReady(). 36 */ 37(function () { 38 'use strict'; 39 40 var AD_CLIENT = 'ca-pub-8348016522017879'; 41 var loadersInjected = false; 42 43 /** 44 * Inject the third-party ad scripts (PubGuru then AdSense). Guarded so it 45 * only ever runs once per page, no matter how many triggers fire. 46 */ 47 function injectLoaders() { 48 if (loadersInjected) return; 49 loadersInjected = true; 50 var head = document.head; 51 var pg = document.createElement('script'); 52 pg.async = true; 53 pg.src = '//c.pubguru.net/pghb.onlinecompass_net.tc.js'; 54 var ads = document.createElement('script'); 55 ads.async = true; 56 ads.crossOrigin = 'anonymous'; 57 ads.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=' + AD_CLIENT; 58 head.appendChild(pg); 59 head.appendChild(ads); 60 } 61 62 /** 63 * Run fn at the next idle moment, or on load â whichever comes first. 64 * 65 * NOTE: this previously also fired on the FIRST user interaction 66 * (pointerdown/keydown/touchstart/scroll). That was removed because 67 * injecting the heavy ad libraries *on* the first tap made that very tap 68 * wait for their parse/execute â directly hurting INP (the metric we are 69 * fixing). Visible ad fill is unaffected: it is gated separately by the
70 * IntersectionObserver in init(), so the libraries only need to be present 71 * by the time a slot nears the viewport, which idle (<=2s) + load covers. 72 */ 73 function onReady(fn) { 74 var done = false; 75 function run() { 76 if (done) return; 77 done = true; 78 fn(); 79 } 80 if ('requestIdleCallback' in window) { 81 window.requestIdleCallback(run, { timeout: 2000 }); 82 } else { 83 window.setTimeout(run, 2000); 84 } 85 window.addEventListener('load', run, { once: true }); 86 } 87 88 /** 89 * Drop the <ins> from any slot hidden on this viewport. offsetWidth === 0 90 * means the slot (or an ancestor) is display:none; such an <ins> can't be 91 * measured (width 0) and, because push({}) fills ins in document order, 92 * would steal a push meant for a later visible slot. Returns the fillable 93 * (visible) slots. 94 * 95 * This is safe to call immediately at DOMContentLoaded because the 96 * responsive breakpoints (.ad-mobile/.ad-tablet/.ad-desktop visibility) 97 * are inlined in critical-css.blade.php, so hidden variants already have 98 * offsetWidth === 0 from first paint â no need to wait for app.css. 99 */ 100 function pruneHidden(slots) { 101 // Read all geometry FIRST, then mutate. Interleaving offsetWidth 102 // reads with ins.remove() writes forces a synchronous layout reflow 103 // on every iteration (layout is invalidated by each removal); batching 104 // the reads collapses them into a single layout calculation. 105 var toRemove = []; 106 var visible = []; 107 for (var i = 0; i < slots.length; i++) { 108 var slot = slots[i]; 109 var ins = slot.querySelector('ins.adsbygoogle'); 110 if (!ins) continue; 111 if (slot.offsetWidth === 0) { 112 toRemove.push(ins); 113 } else { 114 visible.push(slot); 115 } 116 } 117 for (var j = 0; j < toRemove.length; j++) { 118 toRemove[j].remove(); 119 } 120 return visible; 121 } 122 123 /** 124 * Issue one push({}). AdSense's push({}) fills the next <ins> it has not 125 * yet processed (document order) and THROWS "All 'ins' elements ... already 126 * have ads in them" when none is left. PubGuru header-bidding and page-level 127 * auto-ads can fill a slot out-of-band, so a pure count cap can desync 128 * (increment on throw â later slots skip their push). Instead, check for a 129 * live unprocessed <ins> before each push and swallow the residual async- 130 * race TagError (AdSense marks an <ins> processed via data-ad-status, 131 * asynchronously, so there is a window where the guard sees an ins that 132 * AdSense has already claimed internally). 133 */ 134 function pushOne() { 135 // push({}) fills the FIRST unprocessed <ins> in document order. If that 136 // <ins> is 0-width (a responsive variant that became hidden after init, 137 // or a layout race after a deploy/cache-purge), AdSense throws 138 // "No slot size for availableWidth=0". Prune it â same as pruneHidden at 139 // init â and skip the push rather than trigger the TagError. 140 var ins = document.querySelector('ins.adsbygoogle:not([data-ad-status])'); 141 if (!ins) return; 142 if (ins.offsetWidth === 0) { ins.remove(); return; } 143 try { 144 (window.adsbygoogle = window.adsbygoogle || []).push({}); 145 } catch (e) { 146 // TagError: raced past the last unfilled <ins> before AdSense 147 // marked it processed (data-ad-status). Benign â next slot retries. 148 } 149 } 150 151 /** Hide the skeleton shimmer if AdSense never sets data-ad-status. */ 152 function watchShimmer(slot) { 153 window.setTimeout(function () { 154 var ins = slot.querySelector('ins.adsbygoogle'); 155 if (ins && !ins.getAttribute('data-ad-status')) { 156 slot.classList.add('is-loaded'); 157 } 158 }, 4000); 159 } 160 161 function fill(slot) { 162 pushOne(); 163 watchShimmer(slot); 164 } 165 166 /** 167 * Run cb once the user has made a Cookiebot consent decision â never before. 168 * 169 * WHY: the ad libraries (PubGuru .tc.js + adsbygoogle.js) pull in Google's 170 * Funding Choices IAB-TCF-v2 consent wall (web_iab_tcf_v2_wall_executable), 171 * a ~0.3 s main-thread long task. Injecting them at idle/load lands that 172 * task in the window where users tap the consent button (0.45 s) or the 173 * compass (0.31 s) â the top INP blockers on the homepage. Holding the ad 174 * libraries until the consent decision keeps the wall out of those 175 * pre-consent interactions entirely (the fc- button doesn't even exist 176 * before consent), and matches what Cookiebot's data-blockingmode="auto" 177 * already does for gtag. This is the GDPR-correct order too: ads.js was 178 * previously able to load AdSense before any consent decision. 179 * 180 * Covers: already-consented returning users (Cookiebot.consent === true), 181 * the live decision (CookiebotOnAccept / CookiebotOnDecline), and a safety 182 * net if Cookiebot is absent/blocked (e.g. a non-EU request with no event). 183 * Slot filling is unaffected: push({}) only queues into the adsbygoogle 184 * array; nothing renders until the library loads here, post-consent. 185 */ 186 function whenConsented(cb) { 187 var fired = false;
188 var timer = null; 189 function fire() { 190 if (fired) return; 191 fired = true; 192 window.removeEventListener('CookiebotOnAccept', fire); 193 window.removeEventListener('CookiebotOnDecline', fire); 194 if (timer) clearTimeout(timer); 195 cb(); 196 } 197 if (window.Cookiebot && window.Cookiebot.consent) { 198 fire(); 199 return; 200 } 201 window.addEventListener('CookiebotOnAccept', fire); 202 window.addEventListener('CookiebotOnDecline', fire); 203 // Safety net: Cookiebot absent / blocked / non-EU without an auto-event. 204 timer = window.setTimeout(fire, 6000); 205 } 206 207 function init() { 208 // Inject ad libraries only AFTER consent, so page-level auto ads still 209 // work on pages with no .ad-slot â but without dropping Google's 210 // Funding Choices wall into the pre-consent interaction window. 211 whenConsented(function () { onReady(injectLoaders); }); 212 213 var slots = Array.prototype.slice.call(document.querySelectorAll('.ad-slot')); 214 if (!slots.length) return; 215 216 // Safe to prune immediately: responsive breakpoints live in critical CSS 217 // (critical-css.blade.php), so hidden variants already have 218 // offsetWidth === 0 from first paint â no need to gate on app.css. 219 var visible = pruneHidden(slots); 220 if (!visible.length) return; 221 222 if ('IntersectionObserver' in window) { 223 var io = new IntersectionObserver(function (entries, obs) { 224 for (var i = 0; i < entries.length; i++) { 225 if (!entries[i].isIntersecting) continue; 226 obs.unobserve(entries[i].target); // each slot fires at most once 227 fill(entries[i].target); 228 } 229 /* rootMargin kept tight (200px, down from 600px) so slots fill 230 only just before they enter the viewport. The wider margin 231 filled ads ~1 viewport early, which meant AdSense's 232 show_ads_impl (~95ms main-thread, a top INP LOAF blocker) 233 was already running by the time users tapped/interacted â 234 directly competing with those interactions. 200px still 235 fills before the slot is visible (no blank ad on scroll). */ 236 }, { rootMargin: '200px 0px', threshold: 0 }); 237 visible.forEach(function (slot) { io.observe(slot); }); 238 } else { 239 visible.forEach(function (slot) { fill(slot); }); 240 } 241 } 242 243 if (document.readyState === 'loading') { 244 document.addEventListener('DOMContentLoaded', init); 245 } else { 246 init(); 247 } 248})();
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.