PageSourceSearch

https://www.asianpaints.com/eds/scripts/scripts.js

js asianpaints.com collected 2026-09-25 09:20:34 UTC 37,377 bytes, 831 lines download raw bytes

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.