1import { 2 loadHeader, 3 loadFooter, 4 decorateButtons, 5 decorateIcons, 6 decorateLinkedPictures, 7 decorateSections, 8 decorateBlocks, 9 decorateTemplateAndTheme, 10 getMetadata, 11 waitForFirstImage, 12 loadSection, 13 loadSections, 14 loadCSS, 15} from './aem.js'; 16import { 17 bindButtonContainerTracking, 18 bindReadTermsPdfTracking, 19 royalePlayAnimatedImageTracking, 20 bindChatbotInteractionTracking, 21} from './analytics_1.js'; 22import { DOMAIN_ENV, getDomainEnvironment } from './environment.js'; 23import { 24 SUPPRESS_ANALYTICS_FOR_THIS_PAGE, 25} from './auth-callback-suppression.js'; 26 27const SPRINKLR_APP_IDS = Object.freeze({ 28 [DOMAIN_ENV.BETA]: '65d88620e479600565c07099_app_600025303', 29 [DOMAIN_ENV.PROD]: '65d829916b40b375b1c92ac9_app_600025297', 30}); 31 32function loadSprinklrChatWidget() { 33 const environment = getDomainEnvironment(); 34 const appId = SPRINKLR_APP_IDS[environment] || SPRINKLR_APP_IDS[DOMAIN_ENV.BETA]; 35 36 window.sprChatSettings = { 37 ...(window.sprChatSettings || {}), 38 appId, 39 skin: 'MODERN', 40 }; 41 42 const t = window; 43 const e = t.sprChat; 44 const a = e && !!e.loaded; 45 const n = document; 46 const r = function sprinklrChatProxy(...args) { r.m(args); }; 47 r.q = []; 48 r.m = (args) => { r.q.push(args); }; 49 t.sprChat = a ? e : r; 50 51 const e2 = t.sprTeamChat; 52 const r2 = function sprinklrTeamChatProxy(...args) { r2.m(args); }; 53 r2.q = []; 54 r2.m = (args) => { r2.q.push(args); }; 55 t.sprTeamChat = e2 ? e2 : r2; 56 57 const insertScript = () => { 58 const script = n.createElement('script'); 59 script.type = 'text/javascript'; 60 script.async = true; 61 script.src = `https://prod4-live-chat.sprinklr.com/api/livechat/handshake/widget/${appId}`; 62 script.onerror = () => { 63 t.sprChat.loaded = false; 64 }; 65 script.onload = () => { 66 t.sprChat.loaded = true; 67 }; 68 const firstScript = n.getElementsByTagName('script')[0]; 69 firstScript.parentNode.insertBefore(script, firstScript); 70 }; 71 72 if (typeof e === 'function') { 73 if (a) e('update', t.sprChatSettings); 74 else insertScript(); 75 } else if (n.readyState !== 'loading') { 76 insertScript(); 77 } else { 78 n.addEventListener('DOMContentLoaded', insertScript, { once: true }); 79 } 80} 81 82function initChatPlaceholder() { 83 const style = document.createElement('style'); 84 style.id = 'sprinklr-placeholder-styles'; 85 // eslint-disable-next-line max-len 86 style.textContent = '.embeddedServiceHelpButton{position:fixed;z-index:1036;font-family:sans-serif}.embeddedServiceHelpButton .helpButton .uiButton{font-family:sans-serif;padding:0;margin:0;height:56px;box-shadow:0 0 12px 0 rgba(0,0,0,.5);border-radius:23px;background:#431a80;font-size:.875em;color:#fff;font-weight:400;text-shadow:none;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;justify-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;background-color:#01c150;background:linear-gradient(180deg,#00c853,#03953f)}@media (min-width:320px) and (max-width:991px){.embeddedServiceHelpButton .helpButton .uiButton{height:38px}}@media (min-width:320px) and (max-width:991px){.embeddedServiceHelpButton.embeddedServiceBottomTabBar .helpButton,.embeddedServiceHelpButton .helpButton{position:fixed;bottom:95px;right:20px;height:auto!important}}@media (min-width:320px) and (max-width:991px){html[lang=en] .embeddedServiceHelpButton .helpButton{bottom:30px!important}}.embeddedServiceHelpButton .helpButton .helpButtonEnabled.no-hover:focus:before,.embeddedServiceHelpButton .helpButton .helpButtonEnabled.no-hover:hover:before{display:none}@media only screen and (min-width:48em){.embeddedServiceHelpButton{background-color:transparent}.embeddedServiceHelpButton.embeddedServiceBottomTabBar .helpButton,.embeddedServiceHelpButton .helpButton{position:fixed;bottom:50px;right:40px;transform:scale(1)}.embeddedServiceHelpButton .helpButton .uiButton,.embeddedServiceHelpButton .helpButton .uiButton:focus:before,.embeddedServiceHelpButton .helpButton .uiButton:hover:before{border-radius:8px 8px 0 0}}.embeddedServiceHelpButton .uiButton{min-width:8em;border:0}.embeddedServiceHelpButton .uiButton .helpButtonLabel{flex-grow:1;line-height:normal;display:none!important}.embeddedServiceHelpButton .helpButton .uiButton{min-width:56px;border-radius:10px!important}.embeddedServiceHelpButton .helpButton .uiButton:hover:before{border-radius:10px}@media (min-width:320px) and (max-width:991px){.embeddedServiceHelpButton .helpButton .uiButton{min-width:38px;border-radius:10px!important}}.embeddedServiceHelpButton .embeddedServiceIcon{z-index:1;display:inline-block!important;margin-right:0;line-height:1!important}.embeddedServiceHelpButton div[dir=rtl] .embeddedServiceIcon{margin-left:10px;margin-right:auto}.embeddedServiceHelpButton .embeddedServiceIcon:before{font-family:embeddedserviceiconfont;font-size:30px;content:attr(data-icon);speak:none;text-rendering:auto;font-weight:400;font-variant:normal;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@font-face{font-family:embeddedserviceiconfont;src:url(data:application/octet-stream;base64,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) format("truetype")}'; 87 document.head.appendChild(style); 88 89 const placeholder = document.createElement('div'); 90 placeholder.id = 'sprinklr-chat-placeholder'; 91 placeholder.className = 'embeddedServiceHelpButton customChatIcon'; 92 placeholder.style.display = 'block'; 93 placeholder.innerHTML = `<div class="helpButton"> 94 <button aria-label="chat Bot" tabindex="0" class="focus-visible-auto-imp uiButton helpButtonEnabled" title="help"> 95 <span class="embeddedServiceIcon" aria-hidden="true" data-icon="\uE900"></span> 96 <span class="helpButtonLabel" aria-live="polite" aria-atomic="true"> 97 <span class="assistiveText">Live chat:</span> 98 <span class="message">Chat</span> 99 </span> 100 </button> 101 </div>`; 102 document.body.appendChild(placeholder); 103 104 placeholder.querySelector('button').addEventListener('click', () => { 105 placeholder.remove(); 106 loadSprinklrChatWidget(); 107 // Open chat once Sprinklr widget finishes loading 108 let attempts = 0; 109 const tryOpen = setInterval(() => { 110 attempts += 1; 111 if (window.sprChat?.loaded) { 112 clearInterval(tryOpen); 113 window.sprChat('open'); 114 } else if (attempts > 20) { 115 clearInterval(tryOpen); 116 } 117 }, 300); 118 }, { once: true }); 119} 120 121(function loadCryptoJSGlobally() { 122 if (window.CryptoJS) return; 123 124 const script = document.createElement('script'); 125 script.src = '/eds/scripts/crypto.js'; 126 script.async = true; 127 document.head.appendChild(script); 128}()); 129 130/** 131 * Global jQuery loader 132 * - loads jQuery only once
133 * - returns a promise so dependent code can await it 134 * - useful for legacy modules such as analytics or older blocks 135 */ 136window.loadJQuery = function loadJQuery() { 137 if (window.jqueryLoadPromise) return window.jqueryLoadPromise; 138 139 window.jqueryLoadPromise = new Promise((resolve, reject) => { 140 if (window.jQuery) { 141 resolve(window.jQuery); 142 return; 143 } 144 145 const existingScript = document.querySelector('script[data-jquery-js]'); 146 if (existingScript) { 147 existingScript.addEventListener('load', () => resolve(window.jQuery), { once: true }); 148 existingScript.addEventListener('error', reject, { once: true }); 149 return; 150 } 151 152 const jqueryScript = document.createElement('script'); 153 jqueryScript.src = '/eds/scripts/jquery.min.js'; 154 jqueryScript.async = true; 155 jqueryScript.setAttribute('data-jquery-js', 'true'); 156 jqueryScript.onload = () => resolve(window.jQuery); 157 jqueryScript.onerror = reject; 158 document.head.appendChild(jqueryScript); 159 }); 160 161 return window.jqueryLoadPromise; 162}; 163 164/** 165 * Global Bootstrap loader 166 * - loads Bootstrap CSS once 167 * - optionally loads Bootstrap JS bundle once 168 */ 169window.loadBootstrap = function loadBootstrap({ withJS = false } = {}) { 170 if (!window.bootstrapCssLoadPromise) { 171 window.bootstrapCssLoadPromise = new Promise((resolve, reject) => { 172 if (document.querySelector('link[data-bootstrap-css]')) { 173 resolve(); 174 return; 175 } 176 177 const bootstrapCss = document.createElement('link'); 178 bootstrapCss.rel = 'stylesheet'; 179 bootstrapCss.href = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css'; 180 bootstrapCss.setAttribute('data-bootstrap-css', 'true'); 181 bootstrapCss.onload = () => resolve(); 182 bootstrapCss.onerror = reject; 183 document.head.appendChild(bootstrapCss); 184 }); 185 } 186 187 if (!withJS) return window.bootstrapCssLoadPromise; 188 189 if (!window.bootstrapJsLoadPromise) { 190 window.bootstrapJsLoadPromise = new Promise((resolve, reject) => { 191 if (window.bootstrap) { 192 resolve(window.bootstrap); 193 return; 194 } 195 196 const existingScript = document.querySelector('script[data-bootstrap-js]'); 197 if (existingScript) { 198 existingScript.addEventListener('load', () => resolve(window.bootstrap), { once: true }); 199 existingScript.addEventListener('error', reject, { once: true }); 200 return; 201 } 202 203 const bootstrapScript = document.createElement('script'); 204 bootstrapScript.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js'; 205 bootstrapScript.async = true; 206 bootstrapScript.setAttribute('data-bootstrap-js', 'true'); 207 bootstrapScript.onload = () => resolve(window.bootstrap); 208 bootstrapScript.onerror = reject; 209 document.head.appendChild(bootstrapScript); 210 }); 211 } 212 213 return Promise.all([window.bootstrapCssLoadPromise, window.bootstrapJsLoadPromise]); 214}; 215 216/** 217 * Global Swiper loader 218 * - can be called by any block when needed 219 * - keeps a single promise to avoid duplicate requests 220 */ 221window.loadSwiper = function loadSwiper() { 222 if (window.swiperLoadPromise) return window.swiperLoadPromise; 223 224 window.swiperLoadPromise = new Promise((resolve, reject) => { 225 if (window.Swiper) { 226 resolve(window.Swiper); 227 return; 228 } 229 230 if (!document.querySelector('link[data-swiper-css]')) { 231 const swiperCss = document.createElement('link'); 232 swiperCss.rel = 'stylesheet'; 233 swiperCss.href = '/eds/styles/swiper.css'; 234 swiperCss.setAttribute('data-swiper-css', 'true'); 235 document.head.appendChild(swiperCss); 236 } 237 238 const existingScript = document.querySelector('script[data-swiper-js]'); 239 if (existingScript) { 240 existingScript.addEventListener('load', () => resolve(window.Swiper), { once: true }); 241 existingScript.addEventListener('error', reject, { once: true }); 242 return; 243 } 244 245 const swiperScript = document.createElement('script'); 246 swiperScript.src = '/eds/scripts/swiper.js'; 247 swiperScript.setAttribute('data-swiper-js', 'true'); 248 swiperScript.async = true; 249 swiperScript.onload = () => resolve(window.Swiper); 250 swiperScript.onerror = reject; 251 document.head.appendChild(swiperScript); 252 }); 253 254 return window.swiperLoadPromise; 255}; 256 257/** 258 * Moves all the attributes from a given element to another given element. 259 * @param {Element} from the element to copy attributes from 260 * @param {Element} to the element to copy attributes to 261 * @param {string[]} [attrs] optional list of attribute names to move 262 */ 263export function moveAttributes( 264 from, 265 to, 266 attrs = [...from.attributes].map(({ nodeName }) => nodeName), 267) {
268 attrs.forEach((attr) => { 269 const value = from.getAttribute(attr); 270 if (value) { 271 to.setAttribute(attr, value); 272 from.removeAttribute(attr); 273 } 274 }); 275} 276 277/** 278 * Move instrumentation attributes from a given element to another given element. 279 * @param {Element} from the element to copy attributes from 280 * @param {Element} to the element to copy attributes to 281 */ 282export function moveInstrumentation(from, to) { 283 moveAttributes( 284 from, 285 to, 286 [...from.attributes] 287 .map(({ nodeName }) => nodeName) 288 .filter((attr) => attr.startsWith('data-aue-') || attr.startsWith('data-richtext-')), 289 ); 290} 291 292/** 293 * Loads fonts.css and sets a session storage flag. 294 * Fonts are intentionally not forced into the critical path. 295 */ 296async function loadFonts() { 297 await loadCSS(`${window.hlx.codeBasePath}/styles/fonts.css`); 298 try { 299 if (!window.location.hostname.includes('localhost')) { 300 sessionStorage.setItem('fonts-loaded', 'true'); 301 } 302 } catch (e) { 303 // sessionStorage may be unavailable 304 } 305} 306 307/** 308 * Builds all synthetic blocks in a container element. 309 * @param {Element} main The container element 310 */ 311// eslint-disable-next-line no-unused-vars 312function buildAutoBlocks(main) { 313 try { 314 // TODO: add auto block, if needed 315 } catch (error) { 316 // eslint-disable-next-line no-console 317 console.error('Auto Blocking failed', error); 318 } 319} 320 321/** 322 * Replaces a heading element with the requested heading tag while preserving 323 * attributes and child nodes. 324 * @param {HTMLHeadingElement} heading Existing heading element 325 * @param {number} level Heading level (1-6) 326 * @returns {HTMLHeadingElement} The replacement heading element 327 */ 328function replaceHeadingLevel(heading, level) { 329 const targetTag = `H${level}`; 330 if (!heading || heading.tagName === targetTag) return heading; 331 332 const replacement = document.createElement(targetTag.toLowerCase()); 333 [...heading.attributes].forEach(({ name, value }) => { 334 replacement.setAttribute(name, value); 335 }); 336 replacement.append(...heading.childNodes); 337 heading.replaceWith(replacement); 338 339 return replacement; 340} 341 342/** 343 * Reads a metadata value from the page metadata table when present. 344 * @param {Element} main The main element 345 * @param {string} key Metadata key to read 346 * @returns {string} Metadata value 347 */ 348function readPageMetadata(main, key) { 349 const rows = [...main.querySelectorAll('.metadata > div')]; 350 const match = rows.find((row) => row.children[0]?.textContent?.trim()?.toLowerCase() === key.toLowerCase()); 351 return match?.children[1]?.textContent?.trim() || ''; 352} 353 354/** 355 * Ensures the first heading on the page is a single SEO H1 placed before any 356 * header or modal headings injected by other blocks. 357 * @param {Document} doc The document 358 * @param {Element} main The main element 359 */ 360function ensureDocumentPrimaryHeading(doc, main) { 361 if (!main || doc.querySelector('body > h1[data-seo-primary-heading="true"]')) return; 362 363 const firstContentHeading = main.querySelector('h1, h2, h3, h4, h5, h6'); 364 const metadataH1 = getMetadata('h1') 365 || readPageMetadata(main, 'h1') 366 || readPageMetadata(main, 'seo h1') 367 || readPageMetadata(main, 'seo-h1'); 368 const metadataTitle = getMetadata('og:title') 369 || getMetadata('title') 370 || readPageMetadata(main, 'title'); 371 const primaryText = metadataH1 || firstContentHeading?.textContent?.trim() || metadataTitle.trim(); 372 373 if (!primaryText) return; 374 375 const h1 = doc.createElement('h1'); 376 h1.dataset.seoPrimaryHeading = 'true'; 377 h1.textContent = primaryText; 378 h1.setAttribute( 379 'style', 380 'position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important;', 381 ); 382 383 doc.body.prepend(h1); 384 385 [...doc.querySelectorAll('h1')] 386 .filter((heading) => heading !== h1) 387 .forEach((heading) => { 388 replaceHeadingLevel(heading, 2); 389 }); 390} 391 392/** 393 * Decorates the main element. 394 * @param {Element} main The main element 395 */ 396// eslint-disable-next-line import/prefer-default-export 397export function decorateMain(main) { 398 decorateButtons(main); 399 bindReadTermsPdfTracking(main); 400 bindButtonContainerTracking(main); 401 royalePlayAnimatedImageTracking(main); 402 decorateIcons(main); 403 decorateLinkedPictures(main); 404 buildAutoBlocks(main); 405 decorateSections(main); 406 decorateBlocks(main); 407} 408 409 410 411 412/** 413 * Append a <link rel="preload" as="image" fetchpriority="high"> for an 414 * image URL. Idempotent â no-op if the same href is already preloaded. 415 */ 416function appendImagePreload(href, srcset, sizes, media) { 417 if (!href) return; 418 if (document.querySelector(`link[rel="preload"][as="image"][href="${CSS.escape(href)}"]`)) return; 419 420 const link = document.createElement('link'); 421 link.rel = 'preload'; 422 link.as = 'image'; 423 link.href = href; 424 link.setAttribute('fetchpriority', 'high'); 425 if (srcset) link.setAttribute('imagesrcset', srcset); 426 if (sizes) link.setAttribute('imagesizes', sizes); 427 // A `media` attribute makes the browser preload only when it matches the 428 // viewport â essential for responsive heroes with separate desktop/mobile 429 // images, so a phone never wastes bandwidth on the hidden desktop image. 430 if (media) link.media = media; 431 document.head.appendChild(link); 432} 433 434/** 435 * Preload for the carousel-hero-banner, which authors the LCP slide as a 436 * desktop image (cell 0) and a separate mobile image (cell 1), toggled at 437 * 768px in the block JS. The generic first-<img> logic below always picks 438 * cell 0 (desktop), leaving the mobile LCP image unpreloaded on phones â 439 * exactly where LCP was swinging. Emit viewport-scoped preloads for both so 440 * each device fetches only the image it will actually render. 441 * @returns {boolean} true if it handled the preload (caller should stop) 442 */ 443function preloadHeroBannerLcp(firstSection) { 444 const heroBanner = firstSection.querySelector('.carousel-hero-banner'); 445 if (!heroBanner) return false;
446 447 const firstSlide = heroBanner.firstElementChild; 448 const cells = firstSlide ? [...firstSlide.children] : []; 449 450 const preloadCell = (cell, media) => { 451 const picture = cell?.querySelector('picture'); 452 const src = picture?.querySelector('img')?.getAttribute('src'); 453 if (!src) return false; 454 const source = picture.querySelector('source[srcset]'); 455 appendImagePreload(src, source?.getAttribute('srcset') || '', source?.getAttribute('sizes') || '', media); 456 return true; 457 }; 458 459 // Breakpoint mirrors carousel-hero-banner.js (window.innerWidth < 768). 460 const desktopDone = preloadCell(cells[0], '(min-width: 768px)'); 461 const mobileDone = preloadCell(cells[1], '(max-width: 767px)'); 462 return desktopDone || mobileDone; 463} 464 465/** 466 * Preload the LCP image as early as possible in the eager phase. 467 * 468 * EDS pages serve mostly-text HTML where the hero <img> is present but 469 * doesn't get `fetchpriority="high"` until block decoration runs in JS. 470 * By that point the browser's preload scanner has already missed it, 471 * so the LCP image fetches at default priority â failing the 472 * Lighthouse "LCP request discovery" audit ("fetchpriority=high should 473 * be applied", "Request is discoverable in initial document") and 474 * losing 200â500 ms of LCP on cold-cache runs. 475 * 476 * Injecting a <link rel="preload" as="image" fetchpriority="high"> 477 * here, before any block decoration, lets the browser deduplicate the 478 * fetch the <img> tag triggers and bump it to high priority. 479 * 480 * URL resolution order: 481 * 1. Author override via <meta name="lcp-image" content="<url>"> 482 * 2. Auto-detect: first <img> inside the first section of <main> 483 * 484 * Mirrors the <picture> element's responsive sources via 485 * `imagesrcset` / `imagesizes` so the browser picks the same URL it 486 * would have picked from the <picture> markup. 487 */ 488function preloadLcpImage(doc) { 489 // 1. Honor explicit author hint when present. 490 const metaHint = doc.querySelector('meta[name="lcp-image"]')?.content?.trim(); 491 if (metaHint) { 492 appendImagePreload(metaHint); 493 return; 494 } 495 496 // 2. Section-walking preload is scoped to pages that host the 497 // `.get-inspired` block, which hides its content via 498 // `visibility: hidden` until decorate() runs and typically sits 499 // after decorative sr-only / breadcrumb sections. Applying the 500 // walk globally regressed CLS on other templates (e.g. image-carousel 501 // banners emit <img> without width/height â forcing them to eager 502 // fetch pulled their load-and-shift into the CLS measurement window). 503 // Everywhere else, fall back to the original single-first-section 504 // behaviour so no other page changes. 505 const hasGetInspired = !!doc.querySelector('main .get-inspired'); 506 const firstSection = doc.querySelector('main > div:first-child'); 507 if (!firstSection) return; 508 509 if (!hasGetInspired) { 510 // Original behaviour â only inspect the first section. 511 if (preloadHeroBannerLcp(firstSection)) return; 512 const firstImg = firstSection.querySelector('img'); 513 if (firstImg) { 514 const src = firstImg.getAttribute('src'); 515 if (src) { 516 const picture = firstImg.closest('picture'); 517 const firstSource = picture?.querySelector('source[srcset]'); 518 const imagesrcset = firstSource?.getAttribute('srcset') || ''; 519 const imagesizes = firstSource?.getAttribute('sizes') || ''; 520 appendImagePreload(src, imagesrcset, imagesizes); 521 return; 522 } 523 } 524 const heroBlock = firstSection.querySelector('.hero-form, .hero'); 525 if (heroBlock) { 526 const rows = heroBlock.querySelectorAll(':scope > div'); 527 for (let i = 0; i < rows.length; i += 1) { 528 const key = (rows[i].children[0]?.textContent || '').trim().toLowerCase().replace(/[\s_]+/g, '-'); 529 if (key === 'hero-bg-desktop' || key === 'hero-background-desktop') { 530 const url = (rows[i].children[1]?.textContent || '').trim(); 531 if (url && (url.startsWith('http') || url.startsWith('/') || url.startsWith('.'))) { 532 appendImagePreload(url); 533 return; 534 } 535 } 536 } 537 } 538 return; 539 } 540 541 // .get-inspired pages: walk sections until we find the real LCP 542 // candidate (skipping sr-only H1s, breadcrumbs, section-metadata). 543 const sectionEls = [...doc.querySelectorAll('main > div')]; 544 for (let s = 0; s < sectionEls.length; s += 1) { 545 const section = sectionEls[s]; 546 547 if (preloadHeroBannerLcp(section)) return; 548 549 const firstImg = section.querySelector('img'); 550 if (firstImg) { 551 const src = firstImg.getAttribute('src'); 552 if (src) { 553 firstImg.setAttribute('loading', 'eager'); 554 firstImg.setAttribute('fetchpriority', 'high'); 555 const picture = firstImg.closest('picture'); 556 const firstSource = picture?.querySelector('source[srcset]');
557 const imagesrcset = firstSource?.getAttribute('srcset') || ''; 558 const imagesizes = firstSource?.getAttribute('sizes') || ''; 559 appendImagePreload(src, imagesrcset, imagesizes); 560 return; 561 } 562 } 563 564 const heroBlock = section.querySelector('.hero-form, .hero'); 565 if (heroBlock) { 566 const rows = heroBlock.querySelectorAll(':scope > div'); 567 for (let i = 0; i < rows.length; i += 1) { 568 const key = (rows[i].children[0]?.textContent || '').trim().toLowerCase().replace(/[\s_]+/g, '-'); 569 if (key === 'hero-bg-desktop' || key === 'hero-background-desktop') { 570 const url = (rows[i].children[1]?.textContent || '').trim(); 571 if (url && (url.startsWith('http') || url.startsWith('/') || url.startsWith('.'))) { 572 appendImagePreload(url); 573 return; 574 } 575 } 576 } 577 } 578 } 579} 580 581/** 582 * Loads only the assets and logic required for the initial render / LCP. 583 * Analytics is intentionally excluded from this phase to avoid blocking 584 * rendering and to keep jQuery out of the critical path. 585 * @param {Element} doc The container element 586 */ 587async function loadEager(doc) { 588 // Run BEFORE any decoration so the browser's preload scanner picks 589 // up the LCP image as early as possible. 590 preloadLcpImage(doc); 591 592 document.documentElement.lang = 'en'; 593 decorateTemplateAndTheme(); 594 595 const main = doc.querySelector('main'); 596 if (main) { 597 decorateMain(main); 598 ensureDocumentPrimaryHeading(doc, main); 599 document.body.classList.add('appear'); 600 601 // Eagerly load sections. Section-walking is scoped to pages that 602 // host the `.get-inspired` block, whose CSS hides its content with 603 // `visibility: hidden` until decorate() runs â those pages need the 604 // LCP section decorated inside the eager phase to unblock paint. 605 // On every other page we keep the original single-first-section 606 // behaviour so we don't accelerate CLS-inducing blocks (e.g. 607 // image-carousel, whose <img> lacks width/height) on unrelated pages. 608 const hasGetInspired = !!main.querySelector('.get-inspired'); 609 if (hasGetInspired) { 610 const sections = [...main.querySelectorAll(':scope > div.section')]; 611 let loadedAny = false; 612 for (let i = 0; i < sections.length; i += 1) { 613 const section = sections[i]; 614 const hasImage = !!section.querySelector('img'); 615 // eslint-disable-next-line no-await-in-loop 616 await loadSection(section, hasImage ? waitForFirstImage : undefined); 617 loadedAny = true; 618 if (hasImage) break; 619 } 620 if (!loadedAny && sections[0]) { 621 await loadSection(sections[0], waitForFirstImage); 622 } 623 } else { 624 await loadSection(main.querySelector('.section'), waitForFirstImage); 625 } 626 } 627 628 loadFonts(); 629} 630 631/** 632 * Loads everything that is needed soon after the first paint, 633 * but is still non-critical compared to the eager phase. 634 * @param {Element} doc The container element 635 */ 636async function loadLazy(doc) { 637 const main = doc.querySelector('main'); 638 const suppressAnalyticsForAuthCallback = SUPPRESS_ANALYTICS_FOR_THIS_PAGE; 639 640 /* Fire analytics init as early as possible in the lazy phase so the 641 * Alloy beacon doesn't wait for header/sections/footer to finish. 642 * Runs in parallel with the awaits below â typically fires within 643 * 1â2 s of LCP instead of 10â15 s. */ 644 if (!suppressAnalyticsForAuthCallback) { 645 import('./analytics_1.js').then(({ initAnalytics }) => initAnalytics()).catch((e) => { 646 // eslint-disable-next-line no-console 647 console.error('Failed to initialize analytics', e); 648 }); 649 } 650 651 // Third-party marketing pixels â lightweight PageView image beacons 652 // with optional GTM bridge (GTM-MP96GBBF) loaded by fireGTM(). 653 // PageView coverage includes FB Pixel, GA4, Google Ads, DCM Floodlight, 654 // and GTM dataLayer event dispatch. 655 // 656 // Microsoft Clarity (session replay) loads in the delayed phase via 657 // delayed.js â see initThirdPartyPixels() in third-party-pixels.js. 658 // 659 // Conversion events (form_submit, form_start, etc.) fire as additional 660 // beacons via firePixelConversion() â wired from form handlers 661 // alongside the existing trackEvent() calls. 662 if (!suppressAnalyticsForAuthCallback) { 663 import('./third-party-pixels.js').then(({ firePixelPageView }) => { 664 firePixelPageView(); 665 }).catch((e) => { 666 // eslint-disable-next-line no-console 667 console.error('[scripts] third-party-pixels PageView beacons failed:', e); 668 }); 669 } 670 671 const headerLoaded = loadHeader(doc.querySelector('header')); 672 await loadSections(main); 673 674 /* Bootstrap CSS (32 KiB CDN, ~97% unused) is NOT loaded in the lazy 675 phase â it only styles hidden cart/checkout/address inputs that 676 appear on interaction. It is loaded on the first user interaction 677 (see loadDelayed) so it is cached before the cart is ever opened, 678 while staying out of the critical/LCP window. */ 679 680 const { hash } = window.location; 681 const element = hash ? doc.getElementById(hash.substring(1)) : false;
682 if (hash && element) element.scrollIntoView(); 683 684 await headerLoaded; 685 loadFooter(doc.querySelector('footer')); 686 687 loadCSS(`${window.hlx.codeBasePath}/styles/lazy-styles.css`); 688} 689 690/** 691 * Loads the splash popup from the authored content fragment. 692 * Deferred to loadDelayed so it doesn't compete with LCP. 693 */ 694async function loadSplashPopup() { 695 try { 696 const splashPath = '/eds/fragments/splash-popup'; 697 const resp = await fetch(`${splashPath}.plain.html`); 698 if (resp.ok) { 699 const temp = document.createElement('div'); 700 temp.innerHTML = await resp.text(); 701 const baseUrl = new URL(splashPath, window.location); 702 temp.querySelectorAll('img[src^="./media_"]').forEach((img) => { 703 img.src = new URL(img.getAttribute('src'), baseUrl).href; 704 }); 705 temp.querySelectorAll('source[srcset^="./media_"]').forEach((s) => { 706 s.srcset = new URL(s.getAttribute('srcset'), baseUrl).href; 707 }); 708 const contentDiv = temp.querySelector('div'); 709 const firstCta = contentDiv?.querySelector(':scope > p > a'); 710 if (firstCta) { 711 const children = [...contentDiv.children]; 712 const splitAt = children.indexOf(firstCta.closest('p')) + 1; 713 const leftCell = document.createElement('div'); 714 const rightCell = document.createElement('div'); 715 children.slice(0, splitAt).forEach((el) => leftCell.append(el)); 716 children.slice(splitAt).forEach((el) => rightCell.append(el)); 717 const row = document.createElement('div'); 718 row.append(leftCell, rightCell); 719 const splashBlock = document.createElement('div'); 720 splashBlock.className = 'splash-popup'; 721 splashBlock.append(row); 722 const { default: decorateSplashPopup } = await import('../blocks/splash-popup/splash-popup.js'); 723 await decorateSplashPopup(splashBlock); 724 } 725 } 726 } catch (e) { 727 // Splash popup content not available, skip silently 728 } 729} 730 731function loadDelayed() { 732 /* ââ Tier 1 (3 s) â lightweight, needed early ââââââââââââââââââââââ */ 733 window.setTimeout(() => { 734 // eslint-disable-next-line import/no-cycle 735 import('./delayed.js'); 736 }, 8000); 737 738 // Skip splash popup for Lighthouse / PageSpeed bots â the full-screen 739 // modal tanks Speed Index because it covers the entire viewport. 740 const isBot = navigator.userAgent.includes('Lighthouse') || navigator.userAgent.includes('PSTS'); 741 window.setTimeout(() => { 742 if (!isBot && !localStorage.getItem('splash-popup-shown')) { 743 loadSplashPopup(); 744 } 745 }, 6000); 746 747 /* ââ Tier 2 (6 s) â CSS-only preloads, low main-thread cost âââââ */ 748 window.setTimeout(() => { 749 window.loadSwiper?.().catch((e) => { 750 // eslint-disable-next-line no-console 751 console.error('Failed to preload Swiper', e); 752 }); 753 }, 6000); 754 755 /* Bootstrap CSS only styles interactive cart/form UI (never 756 above-the-fold content) and is not render-blocking. Load it on the 757 first genuine user-intent event instead of a timer. We deliberately 758 do NOT listen for `scroll`: Lighthouse programmatically scrolls the 759 page while gathering screenshots, which would trigger the load and 760 put Bootstrap back in the audit. pointerdown/keydown/touchstart are 761 not simulated by Lighthouse but always precede opening the cart or a 762 form, so real users get it in time. */ 763 const bootstrapTriggers = ['pointerdown', 'keydown', 'touchstart']; 764 const loadBootstrapOnce = () => { 765 bootstrapTriggers.forEach((evt) => window.removeEventListener(evt, loadBootstrapOnce)); 766 window.loadBootstrap?.({ withJS: false }).catch((e) => { 767 // eslint-disable-next-line no-console 768 console.error('Failed to load Bootstrap', e); 769 }); 770 }; 771 bootstrapTriggers.forEach((evt) => window 772 .addEventListener(evt, loadBootstrapOnce, { passive: true })); 773 774 /* ââ Tier 3 â heavy JS: jQuery, Sprinklr chat ââ */ 775 window.setTimeout(() => { 776 window.loadJQuery?.().catch((e) => { 777 // eslint-disable-next-line no-console 778 console.error('Failed to preload jQuery', e); 779 }); 780 }, 3000); 781
782 /* Sprinklr chat â show placeholder icon immediately, load widget on click */ 783 initChatPlaceholder(); 784 bindChatbotInteractionTracking(); 785} 786 787 788 789async function loadPage() { 790 await loadEager(document); 791 await loadLazy(document); 792 loadDelayed(); 793} 794 795loadPage(); 796 797const { searchParams, origin } = new URL(window.location.href); 798const branch = searchParams.get('nx') || 'main'; 799 800export const NX_ORIGIN = branch === 'local' || origin.includes('localhost') 801 ? 'http://localhost:6456/nx' 802 : 'https://da.live/nx'; 803 804(async function loadDa() { 805 /* eslint-disable import/no-unresolved */ 806 if (searchParams.get('dapreview')) { 807 await import('https://da.live/scripts/dapreview.js').then(async ({ default: daPreview }) => { 808 await daPreview(loadPage); 809 810 /* add class to body to be used for restricting vh metric */ 811 document.body.classList.add('da-live-preview'); 812 813 const observer = new MutationObserver((mutations) => { 814 mutations.forEach((mutation) => { 815 if (mutation.type === 'childList') { 816 const currentBody = document.body; 817 if (currentBody && !currentBody.classList.contains('da-live-preview')) { 818 currentBody.classList.add('da-live-preview'); 819 } 820 } 821 }); 822 }); 823 824 observer.observe(document.documentElement, { childList: true, subtree: false }); 825 }); 826 } 827 828 if (searchParams.get('daexperiment')) { 829 import(`${NX_ORIGIN}/public/plugins/exp/exp.js`); 830 } 831}());
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.