1/** 2 * Daily full-page membership interstitial. 3 * 4 * Page one arms a first-party cookie. The next eligible page is covered in the 5 * head, before body content can paint, and the offer is then capped for 24h. 6 */ 7(function () { 8 "use strict"; 9 10 const config = window.ffsScoutInterstitial || {}; 11 const query = new URLSearchParams(window.location.search); 12 const previewValue = query.get("scout_interstitial"); 13 const testerParam = query.get("scout_interstitial_test"); 14 const testerCookieName = "ffs_scout_interstitial_test"; 15 const testerOptIn = testerParam === "1"; 16 const testerOptOut = testerParam === "0"; 17 18 if (testerOptIn) { 19 setTesterCookie(true); 20 } else if (testerOptOut) { 21 setTesterCookie(false); 22 } 23 24 const testerMode = 25 !testerOptOut && 26 (testerOptIn || safeCookieGet(testerCookieName) === "1"); 27 const previewMode = 28 testerMode && 29 (testerOptIn || 30 previewValue === "1" || 31 previewValue === "annual" || 32 previewValue === "monthly" || 33 query.get("forced_popup") === "1" || 34 query.get("force_popup") === "1"); 35 const dailyStateCookieName = "ffs_scout_interstitial_daily_state"; 36 const dailyStateSeconds = 24 * 60 * 60; 37 const dailyStateMilliseconds = dailyStateSeconds * 1000; 38 const dailyFallbackStorageKey = "ffsScoutInterstitialDailyState"; 39 const sessionKey = "ffsScoutInterstitialState"; 40 const sessionExpiryKey = "ffsScoutInterstitialStateExpiresAt"; 41 const oldSessionKey = "ffsForcedPopupShown"; 42 const memberStorageKeys = [ 43 "ffsScoutInterstitialWasMemberAt", 44 "ffsForcedPopupWasMemberAt", 45 "wasMemberAt", 46 ]; 47 const memberCookieName = "ffs_scout_interstitial_was_member_at"; 48 const memberCookieSeconds = 400 * 24 * 60 * 60; 49 const interstitialId = "ffs-scout-interstitial"; 50 const flashClickStorageKey = "ffsFlashSaleClickId"; 51 const flashClickCookieName = "ffs_flash_sale_click"; 52 const excludedPaths = [ 53 "/register", 54 "/upgrade", 55 "/benefits", 56 "/account", 57 "/app-account", 58 "/app-login", 59 "/my-account", 60 "/login", 61 "/sign-in", 62 ]; 63 let failOpenTimer = null; 64 let viewportResizeHandler = null; 65 let viewportScrollHandler = null; 66 let bodyObserver = null; 67 let titleResizeObserver = null; 68 69 if (config.isLoggedIn && config.isMember) { 70 rememberPaidMember(); 71 setDailyState("shown"); 72 return; 73 } 74 75 // A remembered paid member stays excluded after logout. Only clear that 76 // browser marker once the site can confirm they have logged in as a 77 // non-paying user; this avoids showing the acquisition offer between 78 // sessions to somebody whose paid account is still active. 79 if (config.isLoggedIn) { 80 forgetPaidMember(); 81 } 82 83 if (!config.enabled && !testerMode) { 84 return; 85 } 86 87 // An explicit tester preview must still be usable in a browser that once 88 // belonged to a paid member. Normal visits retain the remembered-member 89 // suppression below. 90 if (!previewMode && wasPaidMemberBeforeLogout()) { 91 setDailyState("shown"); 92 return; 93 } 94 95 if (!previewMode && safeSessionGet(oldSessionKey)) { 96 setDailyState("shown"); 97 return; 98 } 99 100 const dailyState = getDailyState(); 101 102 if (!previewMode && dailyState === "shown") { 103 return; 104 } 105 106 // The first page view only arms the daily state. This deliberately happens 107 // even on an excluded route so the first later eligible page can show it. 108 if (!previewMode && dailyState !== "armed") { 109 setDailyState("armed"); 110 return; 111 } 112 113 if (!previewMode && isExcludedRoute(window.location.pathname)) { 114 return; 115 } 116 117 // This runs while the browser is still parsing <head>, preventing a flash of 118 // the destination page. A fail-open timer ensures content can never remain 119 // hidden if another script or malformed page stops the component rendering. 120 document.documentElement.classList.add("ffs-scout-interstitial-pending"); 121 if (config.theme === "ucl") { 122 document.documentElement.classList.add("ffs-ucl-interstitial"); 123 } 124 failOpenTimer = window.setTimeout(failOpen, 4000); 125 whenBodyExists(showInterstitial); 126 127 function getDailyState() { 128 const cookieState = safeCookieGet(dailyStateCookieName); 129 const fallbackState = getDailyFallbackState(); 130 let legacySessionState = safeSessionGet(sessionKey); 131 const sessionExpiry = parseInt(safeSessionGet(sessionExpiryKey), 10); 132 133 if ( 134 legacySessionState && 135 Number.isFinite(sessionExpiry) && 136 sessionExpiry <= Date.now() 137 ) { 138 safeSessionRemove(sessionKey); 139 safeSessionRemove(sessionExpiryKey); 140 legacySessionState = null; 141 } 142 143 const storedState = 144 cookieState === "armed" || cookieState === "shown" 145 ? cookieState 146 : fallbackState; 147 148 // Smooth deployment across pages that may briefly use different cached 149 // script versions. An old script can mark the session as shown after the 150 // new script armed the cookie; promote that result to the daily cap. 151 if (legacySessionState === "shown" && storedState !== "shown") { 152 setDailyState("shown"); 153 return "shown"; 154 } 155 156 if (storedState === "armed" || storedState === "shown") { 157 return storedState; 158 } 159 160 // Migrate an existing session state once so deploying this change cannot 161 // immediately show a second interstitial to somebody who already saw it. 162 if (legacySessionState === "armed" || legacySessionState === "shown") { 163 setDailyState(legacySessionState); 164 return legacySessionState; 165 } 166 167 return null; 168 } 169 170 function setDailyState(state) { 171 const secure = window.location.protocol === "https:" ? "; Secure" : ""; 172 const expiresAt = Date.now() + dailyStateMilliseconds; 173 document.cookie = 174 encodeURIComponent(dailyStateCookieName) + 175 "=" + 176 encodeURIComponent(state) + 177 "; Max-Age=" + 178 dailyStateSeconds + 179 "; Path=/; SameSite=Lax" + 180 secure; 181 182 safeLocalSet( 183 dailyFallbackStorageKey, 184 JSON.stringify({ state: state, expiresAt: expiresAt }), 185 ); 186 187 // Retain a time-bounded same-session marker for pages temporarily running 188 // the previous cached script revision. The explicit expiry prevents a tab 189 // left open for several days from suppressing every subsequent daily view. 190 safeSessionSet(sessionKey, state); 191 safeSessionSet(sessionExpiryKey, expiresAt.toString()); 192 } 193 194 function getDailyFallbackState() { 195 const value = safeLocalGet(dailyFallbackStorageKey); 196 if (!value) { 197 return null; 198 } 199 200 try { 201 const stored = JSON.parse(value); 202 if ( 203 (stored.state === "armed" || stored.state === "shown") && 204 Number.isFinite(stored.expiresAt) && 205 stored.expiresAt > Date.now() 206 ) { 207 return stored.state; 208 } 209 } catch (error) { 210 // Invalid legacy/browser-managed data is treated as expired. 211 } 212 213 safeLocalRemove(dailyFallbackStorageKey); 214 return null; 215 } 216 217 function safeSessionGet(key) { 218 try { 219 return window.sessionStorage.getItem(key); 220 } catch (error) { 221 return safeCookieGet(key); 222 } 223 } 224 225 function safeCookieGet(key) { 226 const cookieName = encodeURIComponent(key) + "="; 227 const cookies = document.cookie ? document.cookie.split(";") : []; 228 229 for (let index = 0; index < cookies.length; index += 1) { 230 const cookie = cookies[index].trim(); 231 if (cookie.indexOf(cookieName) === 0) { 232 return decodeURIComponent(cookie.substring(cookieName.length)); 233 } 234 } 235 236 return null; 237 } 238 239 function setTesterCookie(enabled) { 240 const secure = window.location.protocol === "https:" ? "; Secure" : ""; 241 document.cookie = enabled 242 ? `${encodeURIComponent(testerCookieName)}=1; Max-Age=604800; Path=/; SameSite=Lax${secure}` 243 : `${encodeURIComponent(testerCookieName)}=; Max-Age=0; Path=/; SameSite=Lax${secure}`; 244 } 245 246 function safeSessionSet(key, value) { 247 try { 248 window.sessionStorage.setItem(key, value); 249 return; 250 } catch (error) { 251 document.cookie = 252 encodeURIComponent(key) + 253 "=" + 254 encodeURIComponent(value) + 255 "; path=/; SameSite=Lax"; 256 } 257 } 258 259 function safeSessionRemove(key) { 260 try { 261 window.sessionStorage.removeItem(key); 262 } catch (error) { 263 document.cookie = 264 encodeURIComponent(key) + 265 "=; Max-Age=0; Path=/; SameSite=Lax"; 266 } 267 } 268 269 function safeLocalGet(key) { 270 try { 271 return window.localStorage.getItem(key); 272 } catch (error) { 273 return null; 274 } 275 } 276 277 function safeLocalSet(key, value) { 278 try { 279 window.localStorage.setItem(key, value); 280 } catch (error) { 281 // Privacy modes can block localStorage. The companion cookie below 282 // still preserves former-member suppression where cookies are allowed. 283 } 284 } 285 286 function safeLocalRemove(key) { 287 try { 288 window.localStorage.removeItem(key); 289 } catch (error) { 290 // Nothing else to clear when localStorage is unavailable. 291 } 292 } 293 294 function setPaidMemberCookie(timestamp) { 295 const secure = window.location.protocol === "https:" ? "; Secure" : ""; 296 document.cookie = 297 encodeURIComponent(memberCookieName) + 298 "=" + 299 encodeURIComponent(timestamp) + 300 "; Max-Age=" + 301 memberCookieSeconds + 302 "; Path=/; SameSite=Lax" + 303 secure; 304 } 305 306 function clearPaidMemberCookie() { 307 const secure = window.location.protocol === "https:" ? "; Secure" : ""; 308 document.cookie = 309 encodeURIComponent(memberCookieName) + 310 "=; Max-Age=0; Path=/; SameSite=Lax" + 311 secure; 312 } 313 314 function rememberPaidMember() { 315 const timestamp = Date.now().toString(); 316 memberStorageKeys.forEach(function (key) { 317 safeLocalSet(key, timestamp); 318 }); 319 setPaidMemberCookie(timestamp); 320 } 321 322 function forgetPaidMember() { 323 memberStorageKeys.forEach(function (key) { 324 safeLocalRemove(key); 325 }); 326 clearPaidMemberCookie(); 327 } 328 329 function wasPaidMemberBeforeLogout() { 330 return ( 331 memberStorageKeys.some(function (key) { 332 return safeLocalGet(key) !== null; 333 }) || safeCookieGet(memberCookieName) !== null 334 ); 335 } 336 337 function isExcludedRoute(pathname) { 338 return excludedPaths.some(function (path) { 339 return pathname.indexOf(path) === 0; 340 }); 341 } 342 343 function whenBodyExists(callback) { 344 if (document.body) { 345 callback(); 346 return; 347 } 348 349 bodyObserver = new MutationObserver(function () { 350 if (!document.body) { 351 return; 352 } 353 354 bodyObserver.disconnect(); 355 bodyObserver = null; 356 callback(); 357 }); 358 bodyObserver.observe(document.documentElement, { 359 childList: true, 360 subtree: true, 361 }); 362 363 document.addEventListener( 364 "DOMContentLoaded", 365 function () { 366 if (document.body && !document.getElementById(interstitialId)) { 367 if (bodyObserver) { 368 bodyObserver.disconnect(); 369 bodyObserver = null; 370 } 371 callback(); 372 } 373 }, 374 { once: true }, 375 ); 376 } 377 378 function showInterstitial() { 379 if (!document.body || document.getElementById(interstitialId)) { 380 failOpen(); 381 return; 382 } 383 384 const audience = getAudience(); 385 const offer = getOffer(audience); 386 const clickId = getOrCreateFlashClickId(); 387 const targetUrl = campaignUrl( 388 config.isLoggedIn ? config.upgradeUrl : config.registerUrl, 389 audience, 390 clickId, 391 ); 392 393 document.body.insertAdjacentHTML( 394 "afterbegin", 395 interstitialMarkup(audience, offer, targetUrl), 396 ); 397 398 const interstitial = document.getElementById(interstitialId); 399 if (!interstitial) { 400 failOpen(); 401 return; 402 } 403 404 setDailyState("shown"); 405 document.documentElement.classList.remove( 406 "ffs-scout-interstitial-pending", 407 ); 408 document.documentElement.classList.add("ffs-scout-interstitial-open"); 409 window.clearTimeout(failOpenTimer); 410 failOpenTimer = null; 411 412 const dialog = interstitial.querySelector("[data-interstitial-dialog]"); 413 const closeButtons = interstitial.querySelectorAll( 414 "[data-interstitial-dismiss]", 415 ); 416 const cta = interstitial.querySelector("[data-interstitial-cta]"); 417 418 closeButtons.forEach(function (button) { 419 button.addEventListener("click", function () { 420 trackEvent("dismiss", audience); 421 closeInterstitial(); 422 }); 423 }); 424 cta.addEventListener("click", function () { 425 trackEvent("cta", audience); 426 recordFlashSaleClick(clickId, audience); 427 }); 428 document.addEventListener("keydown", handleKeydown); 429 startViewportTracking(); 430 startTitleFitting(); 431 trackEvent("impression", audience); 432 433 if (dialog) { 434 window.requestAnimationFrame(function () { 435 try { 436 dialog.focus({ preventScroll: true }); 437 } catch (error) { 438 dialog.focus(); 439 } 440 }); 441 } 442 } 443 444 function getAudience() { 445 return config.isLoggedIn ? "free-member" : "guest"; 446 } 447 448 function getOffer() { 449 const configuredOffers = config.offers || {}; 450 return Object.assign(
451 { currentPrice: "£5", originalPrice: "£10", saving: "£5 off your first month" }, 452 configuredOffers.flash || {}, 453 ); 454 } 455 456 function campaignUrl(url, audience, clickId) { 457 const fallback = config.isLoggedIn 458 ? "/upgrade?sub=monthly&bundle=none&offer=flash-sale-ffs5" 459 : "/register?sub=monthly&bundle=none&offer=flash-sale-ffs5"; 460 461 try { 462 const target = new URL(url || fallback, window.location.origin); 463 target.searchParams.set("sub", "monthly"); 464 target.searchParams.set("bundle", "none"); 465 target.searchParams.set("offer", "flash-sale-ffs5"); 466 target.searchParams.set("flash_click", clickId); 467 target.searchParams.set("utm_source", "scout_interstitial"); 468 target.searchParams.set("utm_medium", "onsite"); 469 target.searchParams.set("utm_campaign", config.campaign || "new-season-flash-sale-ffs5"); 470 target.searchParams.set("utm_content", audience); 471 return target.toString(); 472 } catch (error) { 473 return fallback; 474 } 475 } 476 477 function interstitialMarkup(audience, offer, targetUrl) { 478 // wp_localize_script serializes top-level booleans as "1" / "". 479 const isTrial = config.freeTrial === true || config.freeTrial === "1"; 480 const isUclCampaign = !isTrial && config.theme === "ucl"; 481 const creative = isUclCampaign 482 ? { 483 eyebrow: "New Season Sale · UCL Toolkit Included", 484 title: "Win at UCL Fantasy & FPL with Chief Scout", 485 intro: 486 "Our brand-new UCL Fantasy Toolkit is now part of Chief Scout for the 2026/27 season, bringing you projected points, domestic data and smarter squad tools.", 487 benefitsTitle: "Everything you need to Win at UCL Fantasy & FPL", 488 benefitsIntro: 489 "Your UCL toolkit sits alongside the complete Chief Scout FPL offering in one membership.", 490 features: [ 491 [ 492 "Find Hidden Gems with Domestic Data", 493 "Compare domestic form across every UCL club.", 494 ], 495 [ 496 "Plan Ahead with the Fixture Ticker", 497 "Spot the best fixtures across all eight matchdays.", 498 ], 499 [ 500 "Build Smarter with the Draft Tool", 501 "Create and optimise your 15-player UCL squad.", 502 ], 503 [ 504 "Improve Your Squad with our Team Rater", 505 "Get an instant rating and recommended improvements.", 506 ], 507 ], 508 } 509 : { 510 eyebrow: "New season flash sale · £5 off", 511 title: "Kick Off the Season with Chief Scout for £5", 512 intro: 513 "Unlock the complete Scout toolkit, sharper projections and premium Opta data for half price in your first month.", 514 benefitsTitle: "Everything you need to Win at FPL", 515 benefitsIntro: 516 "Build a stronger squad, make smarter decisions and score more points with the Scout tools trusted by over 360,000 FPL managers, including FPL Harry, FPL Focal and FPL General.", 517 features: [ 518 [ 519 "Industry-Leading RMT Projections", 520 "See how many points your players are expected to score and use industry leading projections to make smarter team selections.", 521 ], 522 [ 523 "Unlimited RMT Draft Ratings & Squad Analysis", 524 "Rate unlimited drafts, identify your squadâs strengths and weaknesses, and make smarter changes before every deadline.", 525 ], 526 [ 527 "Personalised Transfer Recommendations", 528 "Get personalised transfer suggestions based on your squad to quickly identify the moves most likely to improve your team.", 529 ], 530 [ 531 "The Ultimate FPL Pre-Season Guide", 532 "Prepare for Gameweek 1 with expert player picks, fixture analysis, team reveals and chip strategies all in one place.", 533 ], 534 ], 535 }; 536 if (isTrial) { 537 creative.eyebrow = "Chief Scout FREE Trial Until GW6"; 538 creative.title = "Save Your FPL Season. Try Chief Scout FREE.";
539 creative.intro = "Beat the international break boredom and plan your FPL comeback with FREE Chief Scout access until the GW6 deadline."; 540 } 541 const priceCopy = isTrial 542 ? "Try Chief Scout FREE until the GW6 deadline, then pay just £5 for your first month." 543 : "your first month<br>then £10/month"; 544 const cta = isTrial ? "Start My Free Trial" : "Get Chief Scout for £5"; 545 const terms = isTrial 546 ? "Code FFS5 is applied automatically. First paid month £5 after your free trial, then £10/month. Cancel anytime." 547 : "Code FFS5 is applied automatically. First month £5, then £10/month. Cancel anytime."; 548 const themeClass = isUclCampaign 549 ? "ffs-interstitial--ucl" 550 : "ffs-interstitial--scout"; 551 const ariaLabel = isUclCampaign 552 ? "Chief Scout UCL Fantasy launch offer" 553 : "Fantasy Football Scout flash sale"; 554 555 return ` 556 <section id="${interstitialId}" class="ffs-interstitial--monthly ffs-interstitial--flash ${themeClass}" aria-label="${ariaLabel}"> 557 <div class="ffs-interstitial__shell"> 558 <header class="ffs-interstitial__topbar"> 559 <div class="ffs-interstitial__brands"> 560 <img class="ffs-interstitial__logo" src="${escapeHtml( 561 (isUclCampaign ? config.uclLogoUrl : config.logoUrl) || 562 config.logoUrl || 563 "", 564 )}" alt="Fantasy Football Scout"> 565 ${ 566 isUclCampaign && config.uclCompetitionLogoUrl 567 ? `<span class="ffs-interstitial__brand-divider" aria-hidden="true"></span> 568 <img class="ffs-interstitial__ucl-logo" src="${escapeHtml(config.uclCompetitionLogoUrl)}" alt="UEFA Champions League">` 569 : "" 570 } 571 </div> 572 <button type="button" class="ffs-interstitial__skip" data-interstitial-dismiss aria-label="Dismiss offer and continue to the site"> 573 <span class="ffs-interstitial__skip-label">Continue to site</span> 574 ${closeIcon()} 575 </button> 576 </header> 577 578 <div class="ffs-interstitial__panel" role="dialog" aria-modal="true" aria-labelledby="ffs-interstitial-title" data-interstitial-dialog tabindex="-1"> 579 <div class="ffs-interstitial__offer"> 580 <p class="ffs-interstitial__eyebrow">${escapeHtml(creative.eyebrow)}</p> 581 <h1 class="ffs-interstitial__title" id="ffs-interstitial-title">${escapeHtml(creative.title)}</h1> 582 <p class="ffs-interstitial__intro">${escapeHtml(creative.intro)}</p> 583 584 <div class="ffs-interstitial__price" aria-label="${escapeHtml( 585 isTrial ? "Chief Scout free trial until GW6" : offer.saving, 586 )}"> 587 <div class="ffs-interstitial__price-main"> 588 <strong class="ffs-interstitial__price-now">${escapeHtml( 589 isTrial ? "FREE" : offer.currentPrice, 590 )}</strong> 591 <span class="ffs-interstitial__price-was">${escapeHtml( 592 offer.originalPrice, 593 )}</span> 594 </div> 595 <p class="ffs-interstitial__price-copy">${priceCopy}</p> 596 </div> 597 598 <div class="ffs-interstitial__actions"> 599 <a class="ffs-interstitial__cta" href="${escapeHtml( 600 targetUrl, 601 )}" data-interstitial-cta>${escapeHtml(cta)}</a> 602 <button type="button" class="ffs-interstitial__dismiss" data-interstitial-dismiss>Not now, continue to site</button> 603 </div> 604 <p class="ffs-interstitial__terms">${escapeHtml(terms)}</p> 605 </div> 606 607 <aside class="ffs-interstitial__benefits" aria-label="Chief Scout features"> 608 <h2 class="ffs-interstitial__benefits-title">${escapeHtml(creative.benefitsTitle)}</h2> 609 <p class="ffs-interstitial__benefits-intro">${escapeHtml(creative.benefitsIntro)}</p> 610 <ul class="ffs-interstitial__features"> 611 ${creative.features 612 .map(function (feature) { 613 return featureMarkup(feature[0], feature[1]); 614 }) 615 .join("")} 616 </ul> 617 </aside> 618 </div> 619 </div> 620 </section>`; 621 } 622 623 function featureMarkup(title, description) { 624 return ` 625 <li class="ffs-interstitial__feature">
626 <span class="ffs-interstitial__feature-icon" aria-hidden="true">${checkIcon()}</span> 627 <strong>${escapeHtml(title)}</strong> 628 <span class="ffs-interstitial__feature-copy">${escapeHtml(description)}</span> 629 </li>`; 630 } 631 632 function checkIcon() { 633 return '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m4.5 10.25 3.35 3.35L15.7 5.8" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>'; 634 } 635 636 function closeIcon() { 637 return '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m5 5 10 10M15 5 5 15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>'; 638 } 639 640 function handleKeydown(event) { 641 if (event.key === "Escape") { 642 trackEvent("dismiss_escape", getAudience()); 643 closeInterstitial(); 644 return; 645 } 646 647 if (event.key !== "Tab") { 648 return; 649 } 650 651 const interstitial = document.getElementById(interstitialId); 652 if (!interstitial) { 653 return; 654 } 655 656 const focusable = interstitial.querySelectorAll( 657 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])', 658 ); 659 if (!focusable.length) { 660 event.preventDefault(); 661 return; 662 } 663 664 const first = focusable[0]; 665 const last = focusable[focusable.length - 1]; 666 if (event.shiftKey && document.activeElement === first) { 667 event.preventDefault(); 668 last.focus(); 669 } else if (!event.shiftKey && document.activeElement === last) { 670 event.preventDefault(); 671 first.focus(); 672 } 673 } 674 675 function closeInterstitial() { 676 const interstitial = document.getElementById(interstitialId); 677 document.removeEventListener("keydown", handleKeydown); 678 stopViewportTracking(); 679 document.documentElement.classList.remove( 680 "ffs-scout-interstitial-pending", 681 "ffs-scout-interstitial-open", 682 "ffs-ucl-interstitial", 683 ); 684 685 if (interstitial) { 686 interstitial.remove(); 687 } 688 } 689 690 function failOpen() { 691 if (bodyObserver) { 692 bodyObserver.disconnect(); 693 bodyObserver = null; 694 } 695 if (failOpenTimer) { 696 window.clearTimeout(failOpenTimer); 697 failOpenTimer = null; 698 } 699 document.documentElement.classList.remove( 700 "ffs-scout-interstitial-pending", 701 "ffs-scout-interstitial-open", 702 "ffs-ucl-interstitial", 703 ); 704 } 705 706 // Keep the full headline within two lines at the largest fitting size. 707 function fitTitle() { 708 const title = document.getElementById("ffs-interstitial-title"); 709 if (!title || !title.clientWidth) return; 710 711 title.style.removeProperty("font-size"); 712 let upper = parseFloat(window.getComputedStyle(title).fontSize); 713 let lower = 1; 714 function fits() { 715 const lineHeight = parseFloat(window.getComputedStyle(title).lineHeight); 716 return title.getBoundingClientRect().height <= lineHeight * 2 + 0.5 && 717 title.scrollWidth <= title.clientWidth + 1; 718 } 719 if (fits()) return; 720 721 while (upper - lower > 0.25) { 722 const size = (lower + upper) / 2; 723 title.style.fontSize = size + "px"; 724 if (fits()) lower = size; 725 else upper = size; 726 } 727 title.style.fontSize = lower + "px"; 728 } 729 730 function startTitleFitting() { 731 fitTitle(); 732 if (document.fonts) document.fonts.ready.then(fitTitle); 733 if (window.ResizeObserver) { 734 let previousWidth = 0; 735 titleResizeObserver = new ResizeObserver(function (entries) { 736 const width = entries[0].contentRect.width; 737 if (width !== previousWidth) { 738 previousWidth = width; 739 fitTitle(); 740 } 741 }); 742 titleResizeObserver.observe(document.getElementById("ffs-interstitial-title")); 743 } 744 window.addEventListener("resize", fitTitle); 745 } 746 747 function startViewportTracking() { 748 syncViewport(); 749 750 if (window.visualViewport) { 751 viewportResizeHandler = syncViewport; 752 viewportScrollHandler = syncViewport; 753 window.visualViewport.addEventListener("resize", viewportResizeHandler); 754 window.visualViewport.addEventListener("scroll", viewportScrollHandler); 755 } else { 756 viewportResizeHandler = syncViewport; 757 window.addEventListener("resize", viewportResizeHandler); 758 } 759 } 760 761 function stopViewportTracking() { 762 if (titleResizeObserver) { 763 titleResizeObserver.disconnect(); 764 titleResizeObserver = null; 765 } 766 window.removeEventListener("resize", fitTitle);
767 if (window.visualViewport) { 768 if (viewportResizeHandler) { 769 window.visualViewport.removeEventListener( 770 "resize", 771 viewportResizeHandler, 772 ); 773 } 774 if (viewportScrollHandler) { 775 window.visualViewport.removeEventListener( 776 "scroll", 777 viewportScrollHandler, 778 ); 779 } 780 } else if (viewportResizeHandler) { 781 window.removeEventListener("resize", viewportResizeHandler); 782 } 783 784 viewportResizeHandler = null; 785 viewportScrollHandler = null; 786 document.documentElement.style.removeProperty("--ffs-interstitial-vh"); 787 document.documentElement.style.removeProperty("--ffs-interstitial-vw"); 788 document.documentElement.style.removeProperty("--ffs-interstitial-top"); 789 document.documentElement.style.removeProperty("--ffs-interstitial-left"); 790 } 791 792 function syncViewport() { 793 const viewport = window.visualViewport; 794 const height = viewport ? viewport.height : window.innerHeight; 795 const width = viewport ? viewport.width : window.innerWidth; 796 const top = viewport ? viewport.offsetTop : 0; 797 const left = viewport ? viewport.offsetLeft : 0; 798 799 document.documentElement.style.setProperty( 800 "--ffs-interstitial-vh", 801 Math.max(1, height) + "px", 802 ); 803 document.documentElement.style.setProperty( 804 "--ffs-interstitial-vw", 805 Math.max(1, width) + "px", 806 ); 807 document.documentElement.style.setProperty( 808 "--ffs-interstitial-top", 809 Math.max(0, top) + "px", 810 ); 811 document.documentElement.style.setProperty( 812 "--ffs-interstitial-left", 813 Math.max(0, left) + "px", 814 ); 815 } 816 817 function getOrCreateFlashClickId() {
818 let clickId = safeLocalGet(flashClickStorageKey); 819 if (!clickId) clickId = safeCookieGet(flashClickCookieName); 820 if (!/^[a-z0-9][a-z0-9_-]{7,79}$/i.test(clickId || "")) { 821 clickId = 822 window.crypto && typeof window.crypto.randomUUID === "function" 823 ? window.crypto.randomUUID().toLowerCase() 824 : (Date.now().toString(36) + Math.random().toString(36).slice(2, 14)).toLowerCase(); 825 } 826 safeLocalSet(flashClickStorageKey, clickId); 827 const secure = window.location.protocol === "https:" ? "; Secure" : ""; 828 document.cookie = 829 encodeURIComponent(flashClickCookieName) + 830 "=" + 831 encodeURIComponent(clickId) + 832 "; Max-Age=604800; Path=/; SameSite=Lax" + 833 secure; 834 return clickId; 835 } 836 837 function recordFlashSaleClick(clickId, audience) { 838 if (!config.eventUrl || !config.eventNonce || !clickId) return; 839 const payload = new FormData(); 840 payload.append("action", "ffs_flash_sale_click"); 841 payload.append("nonce", config.eventNonce); 842 payload.append("click_id", clickId); 843 payload.append("audience", audience); 844 if (navigator.sendBeacon) { 845 navigator.sendBeacon(config.eventUrl, payload); 846 return; 847 } 848 window.fetch(config.eventUrl, { 849 method: "POST", 850 body: payload, 851 credentials: "same-origin", 852 keepalive: true, 853 }).catch(function () {}); 854 } 855 856 function trackEvent(action, audience) { 857 window.dataLayer = window.dataLayer || []; 858 window.dataLayer.push({ 859 event: "scout_interstitial_" + action, 860 scout_interstitial_audience: audience, 861 scout_interstitial_campaign: config.campaign || "new-season-flash-sale-ffs5", 862 }); 863 } 864 865 function escapeHtml(value) { 866 return String(value == null ? "" : value) 867 .replace(/&/g, "&") 868 .replace(/</g, "<") 869 .replace(/>/g, ">") 870 .replace(/"/g, """) 871 .replace(/'/g, "'"); 872 } 873})();
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.