1/* Landing-page CTA utilities (vanilla JS â no jQuery dependency, safe to 2 * load deferred before jQuery). 3 * 4 * 1. Signup links carry permitted campaign parameters and an opaque consent 5 * handoff. Meta click identifiers stay in the consent-bound server receipt, 6 * rather than being copied into signup links or booking forms. 7 * 2. CTA click events: pushes a first-party `cta_click` dataLayer event with the CTA 8 * type (signup / demo / live_demo / video) and page position (hero, 9 * sticky, header, section id). Google Consent Mode determines whether 10 * Google receives full or cookieless measurement. 11 * 3. Scroll depth: pushes the equivalent first-party event once per threshold 12 * (25 / 50 / 75 / 100%). 13 * 14 * GTM/GA4 wiring for these events is documented in 15 * docs/LANDING_CTA_TRACKING.md. 16 */ 17(function () { 18 'use strict'; 19 20 if (window.__ttfLandingCtaInitialized) return; 21 window.__ttfLandingCtaInitialized = true; 22 23 /* Meta identifiers travel through the server-side consent receipt. Signup 24 * URLs carry only its opaque handoff and the existing campaign parameters. */ 25 var PASS_PARAMS = [ 26 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 27 'gclid', 'gbraid', 'wbraid', 'msclkid', 'ttclid', 'irclickid' 28 ]; 29 var PERSISTED_PARAMS = [ 30 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 31 'gclid', 'gbraid', 'wbraid', 'fbclid', '_fbc' 32 ]; 33 /* The Meta click cookies are refreshed by the consent-gated pixel on every 34 * page load, so they are never the stale value the Google suppression below 35 * exists to keep out, and they must survive a page that carries a Google 36 * click of its own. */ 37 var META_PARAMS = ['fbclid', '_fbc']; 38 var metaClickWithdrawn = false; 39 var capturedMetaFbc = null; 40 /* Every Meta cookie this browser may hold, including the durable browser id 41 * that is never put in a URL. Withdrawing permission erases all of them. */ 42 var META_COOKIES = ['fbclid', '_fbc', '_fbp']; 43 var CONSENT_HANDOFF_PARAM = 'ttf_consent_handoff'; 44 var CONSENT_REFERENCE_KEY = 'ttfAcquisitionAdvertisingConsent'; 45 var consentHandoffToken = null; 46 var consentHandoffReference = null; 47 var consentHandoffPromise = null; 48 var consentHandoffGeneration = 0; 49 var pendingConsentRevocations = []; 50 var consentRevocationsInFlight = {}; 51 var consentRevocationRetryTimer = null; 52 var consentRevocationRetryDelay = 5000; 53 54 function storedConsentReferences() { 55 try { 56 var value = window.localStorage.getItem(CONSENT_REFERENCE_KEY); 57 if (/^[a-f0-9]{64}$/.test(value || '')) return [value]; 58 var parsed = JSON.parse(value || '[]'); 59 return Array.isArray(parsed) ? parsed.filter(function (reference, index) { 60 return /^[a-f0-9]{64}$/.test(reference || '') 61 && parsed.indexOf(reference) === index; 62 }) : []; 63 } catch (e) { 64 return []; 65 } 66 } 67 68 function storeConsentReferences(values) { 69 try { 70 if (values.length) { 71 window.localStorage.setItem(CONSENT_REFERENCE_KEY, JSON.stringify(values)); 72 } else { 73 window.localStorage.removeItem(CONSENT_REFERENCE_KEY); 74 } 75 } catch (e) {} 76 } 77 78 /* Keep the handle that withdraws this grant, and say whether it stuck. 79 * 80 * The write is read back rather than assumed. Where storage is blocked -- 81 * private browsing, a browser set to refuse site data -- the write throws 82 * and is swallowed, and only the in-memory reference is left. That survives 83 * this page and nothing more, so after a reload a later withdrawal has no 84 * handle for a grant the server still honours, and a conversion for a click 85 * the person has withdrawn could still be reported. The caller uses the 86 * answer to give the grant up instead. */ 87 function retainConsentReference(value) { 88 if (!/^[a-f0-9]{64}$/.test(value || '')) return false; 89 var references = storedConsentReferences(); 90 if (references.indexOf(value) === -1) references.push(value); 91 storeConsentReferences(references); 92 93 return storedConsentReferences().indexOf(value) !== -1; 94 } 95 96 function forgetConsentReference(value) { 97 storeConsentReferences(storedConsentReferences().filter(function (reference) { 98 return reference !== value; 99 })); 100 } 101 102 function advertisingAllowed() {
103 return window.__ttfAdvertisingAllowed === true; 104 } 105 106 /* Delete one persisted click cookie. The value may have been written by our 107 * server on the shared registrable domain or by Meta's own pixel at the host 108 * level, so both are cleared: a cookie left behind would let a later grant 109 * mint a fresh permission for a click the person has already withdrawn. */ 110 function eraseCookie(name) { 111 var past = '; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; 112 var host = String(window.location.hostname || ''); 113 var labels = host.split('.'); 114 try { 115 /* Host-only first, then every parent suffix. Guessing the registrable 116 * domain as the last two labels is wrong on ticketfairy.co.uk and 117 * ticketfairy.com.au, where that yields a public suffix the browser 118 * refuses, leaving the cookie in place. Walking every suffix needs no 119 * public-suffix list: the browser silently ignores the writes it will 120 * not accept, and the one that matches how the cookie was written 121 * succeeds. */ 122 document.cookie = name + '=' + past; 123 for (var i = 0; i < labels.length - 1; i++) { 124 var domain = labels.slice(i).join('.'); 125 document.cookie = name + '=' + past + '; domain=' + domain; 126 document.cookie = name + '=' + past + '; domain=.' + domain; 127 } 128 } catch (e) { 129 /* A browser refusing the write leaves the cookie to expire on its own. */ 130 } 131 } 132 133 function readCookie(name) { 134 var prefix = name + '='; 135 var cookies = String(document.cookie || '').split(';'); 136 for (var i = 0; i < cookies.length; i++) { 137 var cookie = cookies[i].trim(); 138 if (cookie.indexOf(prefix) !== 0) continue; 139 try { 140 var value = decodeURIComponent(cookie.slice(prefix.length)); 141 return value 142 && value.length <= 512 143 && !/[\u0000-\u001f\u007f]/.test(value) 144 ? value 145 : null; 146 } catch (e) { 147 return null; 148 } 149 } 150 return null; 151 } 152 153 function revokeConsentIdentifier(identifier) { 154 if (!/^[a-f0-9]{64}$/.test(identifier || '') || !window.fetch) { 155 return Promise.resolve(false); 156 } 157 return window.fetch('/api/v1/acquisition/consent-handoff/revoke', { 158 method: 'POST', 159 credentials: 'same-origin', 160 keepalive: true, 161 headers: { 'Content-Type': 'application/json' }, 162 body: JSON.stringify({ 163 advertisingConsentReference: identifier 164 }) 165 }).then(function (response) { 166 return response.ok; 167 }).catch(function () { 168 return false; 169 }); 170 } 171 172 function scheduleConsentRevocationRetry() { 173 if (consentRevocationRetryTimer || pendingConsentRevocations.length === 0) return; 174 consentRevocationRetryTimer = window.setTimeout(function () { 175 consentRevocationRetryTimer = null; 176 processPendingConsentRevocations(); 177 }, consentRevocationRetryDelay); 178 consentRevocationRetryDelay = Math.min(consentRevocationRetryDelay * 2, 60000); 179 } 180 181 function processPendingConsentRevocations() { 182 pendingConsentRevocations.slice().forEach(function (identifier) { 183 if (consentRevocationsInFlight[identifier]) return; 184 consentRevocationsInFlight[identifier] = true; 185 revokeConsentIdentifier(identifier).then(function (revoked) { 186 delete consentRevocationsInFlight[identifier]; 187 if (!revoked) { 188 scheduleConsentRevocationRetry(); 189 return; 190 } 191 pendingConsentRevocations = pendingConsentRevocations.filter( 192 function (pendingIdentifier) { 193 return pendingIdentifier !== identifier; 194 } 195 ); 196 forgetConsentReference(identifier); 197 if (pendingConsentRevocations.length === 0) { 198 consentRevocationRetryDelay = 5000; 199 } 200 }); 201 }); 202 } 203 204 function revokeAllConsentIdentifiers() { 205 var identifiers = storedConsentReferences(); 206 identifiers.push(consentHandoffReference, consentHandoffToken);
207 identifiers.forEach(function (identifier) { 208 if ( 209 /^[a-f0-9]{64}$/.test(identifier || '') 210 && pendingConsentRevocations.indexOf(identifier) === -1 211 ) { 212 pendingConsentRevocations.push(identifier); 213 } 214 }); 215 processPendingConsentRevocations(); 216 } 217 218 function collectParams() { 219 if (!advertisingAllowed()) return null; 220 221 var out = {}; 222 var found = false; 223 try { 224 var search = new URLSearchParams(window.location.search); 225 var hasCurrentGoogleClick = Boolean( 226 search.get('gclid') || search.get('gbraid') || search.get('wbraid') 227 ); 228 var captureParams = PASS_PARAMS.concat(META_PARAMS); 229 for (var i = 0; i < captureParams.length; i++) { 230 if (metaClickWithdrawn && META_PARAMS.indexOf(captureParams[i]) !== -1) continue; 231 var v = search.get(captureParams[i]); 232 var suppressPersisted = hasCurrentGoogleClick 233 && META_PARAMS.indexOf(captureParams[i]) === -1; 234 if ( 235 !v 236 && !suppressPersisted 237 && PERSISTED_PARAMS.indexOf(captureParams[i]) !== -1 238 ) { 239 v = readCookie(captureParams[i]); 240 } 241 if (v) { 242 out[captureParams[i]] = v; 243 found = true; 244 } 245 } 246 if (out.fbclid && /^[A-Za-z0-9_-]{8,512}$/.test(out.fbclid)) { 247 var fbcParts = String(out._fbc || '').split('.'); 248 if (fbcParts.slice(3).join('.') !== out.fbclid) { 249 if (!capturedMetaFbc || capturedMetaFbc.slice(capturedMetaFbc.lastIndexOf('.') + 1) !== out.fbclid) { 250 capturedMetaFbc = 'fb.1.' + Date.now() + '.' + out.fbclid; 251 } 252 out._fbc = capturedMetaFbc; 253 } 254 } 255 var body = document.body; 256 var landingPageId = body ? body.getAttribute('data-google-ads-landing-page-id') : ''; 257 if (/^[1-9][0-9]{0,10}$/.test(landingPageId || '')) { 258 out.gads_lp_id = landingPageId; 259 found = true; 260 } else if (!hasCurrentGoogleClick) { 261 var persistedLandingPageId = readCookie('gads_lp_id'); 262 if (/^[1-9][0-9]{0,10}$/.test(persistedLandingPageId || '')) { 263 out.gads_lp_id = persistedLandingPageId; 264 found = true; 265 } 266 } 267 } catch (e) { 268 return null; // URLSearchParams unsupported â leave links untouched 269 } 270 return found ? out : null; 271 } 272 273 function decorateSignupLinks() { 274 var params = collectParams(); 275 var links = document.querySelectorAll( 276 'a[href^="https://manage.ticketfairy.com/welcome"], ' + 277 'a[href^="https://manage.ticketfairy.com/registration"]' 278 ); 279 for (var i = 0; i < links.length; i++) { 280 try { 281 var url = new URL(links[i].href); 282 META_PARAMS.forEach(function (name) { url.searchParams.delete(name); }); 283 284 if (!params) { 285 for (var p = 0; p < PASS_PARAMS.length; p++) { 286 url.searchParams.delete(PASS_PARAMS[p]); 287 } 288 url.searchParams.delete('gads_lp_id'); 289 url.searchParams.delete(CONSENT_HANDOFF_PARAM); 290 } else { 291 for (var key in params) { 292 if (Object.prototype.hasOwnProperty.call(params, key) && META_PARAMS.indexOf(key) === -1 && !url.searchParams.has(key)) { 293 url.searchParams.set(key, params[key]); 294 } 295 } 296 if (consentHandoffToken) { 297 url.searchParams.set(CONSENT_HANDOFF_PARAM, consentHandoffToken); 298 META_PARAMS.forEach(function (name) { url.searchParams.delete(name); }); 299 } 300 } 301 links[i].href = url.toString(); 302 } catch (e) { 303 // Malformed href or no URL support â leave this link as-is. 304 } 305 } 306 307 decorateSignupForms(params); 308 } 309 310 /* A specialist landing page's email field posts to our own host, which files the address and 311 * then sends the visitor to signup. The same parameters the links carry 312 * travel as hidden fields, so that hop keeps the attribution a link would 313 * have handed over directly. */ 314 function decorateSignupForms(params) { 315 var forms = document.querySelectorAll('form.js-signup-form'); 316 317 for (var i = 0; i < forms.length; i++) { 318 var carried = {}; 319 320 if (params) { 321 for (var key in params) { 322 if (Object.prototype.hasOwnProperty.call(params, key) && META_PARAMS.indexOf(key) === -1) { 323 carried[key] = params[key]; 324 } 325 } 326 if (consentHandoffToken) { 327 carried[CONSENT_HANDOFF_PARAM] = consentHandoffToken; 328 } 329 } 330 331 setCarriedFields(forms[i], carried); 332 } 333 } 334
335 /* One hidden field per carried parameter, replaced rather than added to, so 336 * a consent choice made after the page loaded cannot leave a stale value 337 * behind. */ 338 function setCarriedFields(form, carried) { 339 var existing = form.querySelectorAll('input[data-cta-carried]'); 340 341 for (var i = 0; i < existing.length; i++) { 342 existing[i].parentNode.removeChild(existing[i]); 343 } 344 345 for (var key in carried) { 346 if (!Object.prototype.hasOwnProperty.call(carried, key)) continue; 347 var field = document.createElement('input'); 348 field.type = 'hidden'; 349 field.name = key; 350 field.value = carried[key]; 351 field.setAttribute('data-cta-carried', ''); 352 form.appendChild(field); 353 } 354 } 355 356 function syncConsentHandoff() { 357 var generation = ++consentHandoffGeneration; 358 359 if (!advertisingAllowed()) { 360 if (window.__ttfAdvertisingChoice !== 'denied') { 361 decorateSignupLinks(); 362 return null; 363 } 364 metaClickWithdrawn = true; 365 revokeAllConsentIdentifiers(); 366 /* Revoking the server reference stops the reporting. Erasing the 367 * click cookies is the other half: without it, this browser still 368 * carries the click, and the next time the person allows 369 * advertising a brand-new permission would be minted for the very 370 * click they withdrew. */ 371 for (var m = 0; m < META_COOKIES.length; m++) { 372 eraseCookie(META_COOKIES[m]); 373 } 374 consentHandoffToken = null; 375 consentHandoffReference = null; 376 consentHandoffPromise = null; 377 window.__ttfAcquisitionConsentHandoffPromise = null; 378 window.__ttfAcquisitionConsentReference = null; 379 window.__ttfAcquisitionTracking = null; 380 decorateSignupLinks(); 381 return null; 382 } 383 384 var tracking = collectParams(); 385 /* A Meta click needs the same durable, revocable permission a Google 386 * click does: the demo booking, the signup and the activation are all 387 * reported from the server days later, and each one rechecks it. */ 388 var hasPaidClick = tracking 389 && (tracking.gclid || tracking.gbraid || tracking.wbraid 390 || tracking.fbclid || tracking._fbc); 391 if (!tracking || !hasPaidClick || !window.fetch) { 392 return null; 393 } 394 window.__ttfAcquisitionTracking = tracking; 395 396 function requestHandoff(bundle) { 397 return window.fetch('/api/v1/acquisition/consent-handoff', { 398 method: 'POST', 399 credentials: 'same-origin', 400 headers: { 'Content-Type': 'application/json' }, 401 body: JSON.stringify(bundle) 402 }).then(function (response) { 403 return response.ok ? response.json() : null; 404 }); 405 } 406 consentHandoffPromise = requestHandoff(tracking).then(function (result) { 407 if (result || !(tracking.gclid || tracking.gbraid || tracking.wbraid) 408 || !(tracking.fbclid || tracking._fbc) 409 || generation !== consentHandoffGeneration || !advertisingAllowed()) return result; 410 /* A failed Meta receipt must not disable the existing Google path. 411 * Retry only that network's exact submitted bundle once. */ 412 var googleTracking = {}; 413 Object.keys(tracking).forEach(function (key) { 414 if (META_PARAMS.indexOf(key) === -1) googleTracking[key] = tracking[key]; 415 }); 416 tracking = googleTracking; 417 window.__ttfAcquisitionTracking = tracking; 418 return requestHandoff(tracking); 419 }).then(function (result) { 420 var token = result && result.token; 421 var consentReference = result && result.consent_reference; 422 if (generation !== consentHandoffGeneration || !advertisingAllowed()) { 423 revokeConsentIdentifier(consentReference || token); 424 return null; 425 } 426 consentHandoffToken = /^[a-f0-9]{64}$/.test(token || '') ? token : null; 427 consentHandoffReference = /^[a-f0-9]{64}$/.test(consentReference || '') 428 ? consentReference 429 : null; 430 if (consentHandoffReference && !retainConsentReference(consentHandoffReference)) { 431 /* A grant we cannot withdraw later is worse than no grant. Hand 432 * it straight back while it is still in memory and carry on 433 * without one: not reporting a conversion is the acceptable 434 * failure here, reporting one after a withdrawal is not. 435 * 436 * Through the pending queue rather than a single call. A one 437 * shot that is refused or aborted would leave the grant alive 438 * on the server with the handle already dropped from memory, 439 * which is the failure this is here to close. The queue holds 440 * its own copy and retries with backoff. */ 441 if (pendingConsentRevocations.indexOf(consentHandoffReference) === -1) { 442 pendingConsentRevocations.push(consentHandoffReference); 443 } 444 processPendingConsentRevocations(); 445 consentHandoffToken = null; 446 consentHandoffReference = null; 447 } 448 window.__ttfAcquisitionConsentReference = consentHandoffReference; 449 decorateSignupLinks(); 450 return consentHandoffToken; 451 }).catch(function () { 452 if (generation === consentHandoffGeneration) { 453 consentHandoffToken = null; 454 consentHandoffReference = null; 455 window.__ttfAcquisitionConsentReference = null; 456 } 457 return null; 458 }); 459 window.__ttfAcquisitionConsentHandoffPromise = consentHandoffPromise; 460 461 return consentHandoffPromise; 462 } 463 464 function preserveHandoffBeforeSignupNavigation() { 465 document.addEventListener('click', function (event) { 466 var target = event.target; 467 var link = target && target.closest ? target.closest( 468 'a[href*="manage.ticketfairy.com/welcome"], ' + 469 'a[href*="manage.ticketfairy.com/registration"]' 470 ) : null; 471 472 if (!link || !advertisingAllowed() || !consentHandoffPromise) return; 473 474 var opensNewContext = link.target === '_blank' 475 || event.metaKey 476 || event.ctrlKey 477 || event.shiftKey; 478 var pendingWindow = null; 479 if (opensNewContext) { 480 try { 481 pendingWindow = window.open('about:blank', '_blank'); 482 if (pendingWindow) pendingWindow.opener = null; 483 } catch (e) { 484 pendingWindow = null; 485 } 486 // Preserve the browser's native navigation when pop-ups are blocked. 487 if (!pendingWindow) return; 488 } 489 490 event.preventDefault(); 491 var navigated = false;
492 var navigate = function () { 493 if (navigated) return; 494 navigated = true; 495 if (pendingWindow) { 496 if (!pendingWindow.closed) pendingWindow.location.href = link.href; 497 return; 498 } 499 window.location.href = link.href; 500 }; 501 consentHandoffPromise.then(navigate, navigate); 502 window.setTimeout(navigate, 1500); 503 }, true); 504 505 preserveHandoffBeforeSignupSubmit(); 506 } 507 508 /* A specialist landing page's email form posts as soon as it is submitted, which can be 509 * before the consent handoff has been minted. Hold the submission for the 510 * same moment the link path waits for, then send it with the token in 511 * place, and never longer than the same 1.5 seconds. */ 512 function preserveHandoffBeforeSignupSubmit() { 513 document.addEventListener('submit', function (event) { 514 var form = event.target; 515 516 if (!form || !form.classList || !form.classList.contains('js-signup-form')) return; 517 if (form.getAttribute('data-cta-handoff-ready')) return; 518 if (!advertisingAllowed() || !consentHandoffPromise || consentHandoffToken) return; 519 520 event.preventDefault(); 521 var submitted = false; 522 var submit = function () { 523 if (submitted) return; 524 submitted = true; 525 decorateSignupForms(collectParams()); 526 form.setAttribute('data-cta-handoff-ready', '1'); 527 form.submit(); 528 }; 529 consentHandoffPromise.then(submit, submit); 530 window.setTimeout(submit, 1500); 531 }, true); 532 } 533 534 function ctaPosition(el) { 535 var explicit = el.getAttribute('data-cta-position'); 536 if (explicit) return explicit; 537 if (!el.closest) return 'page'; 538 var scope = el.closest('.sticky-ctas, header, section[id], section, [id]'); 539 if (!scope) return 'page'; 540 if (scope.className && String(scope.className).indexOf('sticky-ctas') !== -1) return 'sticky'; 541 if (scope.tagName === 'HEADER') return 'header'; 542 return scope.id || 'page'; 543 } 544 545 function trackCtaClicks() { 546 document.addEventListener('click', function (e) { 547 var target = e.target; 548 if (!target || !target.closest) return; 549 // a[href$="#apply"] covers pages whose demo CTAs scroll to an 550 // on-page application form instead of opening the modal (v9). 551 // capital.ticketfairy.com is the funding application, which is the 552 // conversion /capital exists to drive â it is not a signup. 553 var el = target.closest( 554 'a[href*="manage.ticketfairy.com/welcome"], ' + 555 'a[href*="manage.ticketfairy.com/registration"], ' + 556 'a[href*="capital.ticketfairy.com"], ' + 557 '.open-modal, .js-watch-video, .js-open-dashboard-demo, ' + 558 '.js-signup-submit, a[href$="#apply"]' 559 ); 560 if (!el) return; 561 var type = 'signup'; 562 if (el.getAttribute('href') && el.getAttribute('href').indexOf('capital.ticketfairy.com') !== -1) { 563 type = 'capital_application'; 564 } else if (el.classList.contains('open-modal')) { 565 type = 'demo'; 566 } else if (el.classList.contains('js-watch-video')) { 567 type = 'video'; 568 } else if (el.classList.contains('js-open-dashboard-demo')) { 569 type = 'live_demo'; 570 } else if (el.getAttribute('href') && /#apply$/.test(el.getAttribute('href'))) { 571 type = 'demo'; 572 } 573 var position = ctaPosition(el); 574 window.dataLayer = window.dataLayer || []; 575 window.dataLayer.push({ 576 event: 'cta_click', 577 cta_type: type, 578 cta_position: position 579 }); 580 581 // A funding application is a Meta Lead conversion, and campaign 582 // optimisation reads it. It carries the visitor's identifiers, so 583 // it is emitted only while the advertising choice is granted â 584 // unlike the first-party event above, which consent mode keeps. 585 if (type === 'capital_application' && advertisingAllowed() && typeof window.fbq === 'function') { 586 window.fbq('track', 'Lead', { 587 content_name: 'Ticket Fairy Capital application', 588 content_category: position 589 }); 590 } 591 }, true); 592 } 593 594 function trackScrollDepth() { 595 var thresholds = [25, 50, 75, 100]; 596 var fired = 0; 597 var ticking = false;
598 599 function measure() { 600 ticking = false; 601 var doc = document.documentElement; 602 var scrollable = doc.scrollHeight - window.innerHeight; 603 if (scrollable <= 0) return; 604 // The +2px slack absorbs sub-pixel gaps between scrollTop, 605 // innerHeight and scrollHeight at the very bottom of the page, 606 // which otherwise leave pct just under 100 and the final 607 // milestone unfired. 608 var pct = (((window.pageYOffset || doc.scrollTop || 0) + 2) / scrollable) * 100; 609 610 while (fired < thresholds.length && pct >= thresholds[fired]) { 611 window.dataLayer = window.dataLayer || []; 612 window.dataLayer.push({ 613 event: 'scroll_depth', 614 percent_scrolled: thresholds[fired] 615 }); 616 fired++; 617 } 618 if (fired >= thresholds.length) { 619 window.removeEventListener('scroll', onScroll); 620 window.removeEventListener('pageshow', onScroll); 621 } 622 } 623 624 function onScroll() { 625 if (ticking) return; 626 ticking = true; 627 (window.requestAnimationFrame || window.setTimeout)(measure); 628 } 629 630 window.addEventListener('scroll', onScroll, { passive: true }); 631 // Browsers restore the scroll offset on reload / back-forward cache 632 // without emitting a scroll event â measure once at init and again 633 // on pageshow so milestones below a restored position still fire. 634 measure(); 635 window.addEventListener('pageshow', onScroll); 636 } 637 638 function init() { 639 decorateSignupLinks(); 640 syncConsentHandoff(); 641 window.addEventListener('ttf:advertising-choice', function () { 642 decorateSignupLinks(); 643 syncConsentHandoff(); 644 }); 645 preserveHandoffBeforeSignupNavigation(); 646 trackCtaClicks(); 647 trackScrollDepth(); 648 } 649 650 if (document.readyState === 'loading') { 651 document.addEventListener('DOMContentLoaded', init); 652 } else { 653 init(); 654 } 655})();
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.