1import { 2 loadHeader, 3 loadFooter, 4 decorateButtons, 5 decorateSections, 6 decorateBlocks, 7 decorateTemplateAndTheme, 8 waitForFirstImage, 9 loadSection, 10 loadSections, 11 loadCSS, 12 getMetadata, 13} from './aem.js'; 14import { enableModalTriggers } from '../blocks/modal/modal.js'; 15import { getConfigValue } from './baici/utils/config.js'; 16import { injectSvg, resolveIconUrl } from './baici/utils/utils.js'; 17import { ANALYTICS_EVENTS } from './constants.js'; 18import { decorateExternalLinks } from './link-utils.js'; 19import { 20 attachDefaultContentLinkAnalytics, 21 attachProductPageLeaveTracking, 22 pushAnalyticsEvent, 23 pushProductViewAnalytics, 24} from './analytics.js'; 25import { configureUePreview } from './ue-preview.js'; 26 27/** 28 * Decorates icon spans using font-based rendering for Phosphor icons (ph-*) 29 * and inline SVG injection for all other icons (mb-*, custom). 30 * Replaces the default aem.js decorateIcons (which injects <img> tags) 31 * so icons are styleable via CSS fill/stroke/currentColor. 32 * Fire-and-forget: called without await from decorateMain so icons 33 * fill in progressively without blocking LCP. 34 * @param {Element} element 35 */ 36async function decorateIcons(element) { 37 const spans = [...element.querySelectorAll('span.icon')]; 38 await Promise.all(spans.map((span) => { 39 const cls = [...span.classList].find((c) => c.startsWith('icon-')); 40 const iconName = cls ? cls.substring(5) : null; 41 if (!iconName) return Promise.resolve(); 42 if (iconName === 'hr') { 43 // Brand divider â replace with .brand-divider span 44 const divider = document.createElement('span'); 45 divider.className = 'brand-divider'; 46 divider.setAttribute('aria-hidden', 'true'); 47 span.replaceWith(divider); 48 return Promise.resolve(); 49 } 50 const url = resolveIconUrl(iconName); 51 if (!url) { 52 // Phosphor icon â render via font 53 span.classList.add('ph', `ph-${iconName.replace(/^ph-/, '')}`); 54 span.setAttribute('aria-hidden', 'true'); 55 return Promise.resolve(); 56 } 57 return injectSvg(url, span, { normalizeColors: true }); 58 })); 59} 60 61/** 62 * Replaces literal `:hr:` text patterns with `.brand-divider` spans. 63 * Handles UE RTE content where the icon syntax is not pre-processed. 64 * @param {Element} element 65 */ 66function decorateDividers(element) { 67 const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); 68 const toReplace = []; 69 let node = walker.nextNode(); 70 while (node) { 71 if (node.nodeValue.includes(':hr:')) toReplace.push(node); 72 node = walker.nextNode(); 73 } 74 toReplace.forEach((textNode) => { 75 const parts = textNode.nodeValue.split(':hr:'); 76 const frag = document.createDocumentFragment(); 77 parts.forEach((part, i) => { 78 if (part) frag.appendChild(document.createTextNode(part)); 79 if (i < parts.length - 1) { 80 const divider = document.createElement('span'); 81 divider.className = 'brand-divider'; 82 divider.setAttribute('aria-hidden', 'true'); 83 frag.appendChild(divider); 84 } 85 }); 86 textNode.replaceWith(frag); 87 }); 88} 89 90/** 91 * Moves all the attributes from a given elmenet to another given element. 92 * @param {Element} from the element to copy attributes from 93 * @param {Element} to the element to copy attributes to 94 */ 95export function moveAttributes(from, to, attributes) { 96 if (!attributes) { 97 // eslint-disable-next-line no-param-reassign 98 attributes = [...from.attributes].map(({ nodeName }) => nodeName); 99 } 100 attributes.forEach((attr) => { 101 const value = from.getAttribute(attr); 102 if (value) { 103 to?.setAttribute(attr, value); 104 from.removeAttribute(attr); 105 } 106 }); 107} 108 109/** 110 * Move instrumentation attributes from a given element to another given element. 111 * @param {Element} from the element to copy attributes from 112 * @param {Element} to the element to copy attributes to 113 */ 114export function moveInstrumentation(from, to) { 115 moveAttributes( 116 from, 117 to, 118 [...from.attributes] 119 .map(({ nodeName }) => nodeName) 120 .filter((attr) => attr.startsWith('data-aue-') || attr.startsWith('data-richtext-')), 121 ); 122} 123 124/** 125 * Reads the first variant class from a block and sets block.dataset.variant. 126 * @param {Element} block 127 * @returns {string|undefined} The variant class, if any 128 */ 129export function readVariant(block) { 130 const blockName = block.dataset.blockName || block.classList[0]; 131 const variant = [...block.classList].find( 132 (c) => c !== 'block' && c !== blockName, 133 ); 134 if (variant) block.dataset.variant = variant; 135 return variant; 136} 137 138/** 139 * load fonts.css and set a session storage flag 140 */ 141async function loadFonts() { 142 await loadCSS(`${window.hlx.codeBasePath}/dist/styles/fonts.css`); 143 try { 144 if (!window.location.hostname.includes('localhost')) sessionStorage.setItem('fonts-loaded', 'true'); 145 } catch (e) { 146 // do nothing 147 } 148} 149 150function a11yLinks(main) { 151 const links = main.querySelectorAll('a'); 152 links.forEach((link) => { 153 let label = link.textContent; 154 if (!label && link.querySelector('span.icon')) { 155 const icon = link.querySelector('span.icon'); 156 label = icon ? icon.classList[1]?.split('-')[1] : label; 157 } 158 link.setAttribute('aria-label', label); 159 }); 160} 161 162/**
163 * Assembles navigation sections in a nav fragment into a nested hierarchy. 164 * 165 * Authoring: all sections are flat siblings on the nav fragment page, each 166 * identified by a `type` value in section-metadata: 167 * 168 * navigation â L1 top-bar button (Plan, Stay, Compare) 169 * navigation-group-section â L2 mega-menu panel (one per L1) 170 * navigation-group-section-item â L3 column inside the panel (title + link) 171 * navigation-group-section-item-group â L4 link group, contains navigation-group block 172 * 173 * Assembly (3 passes, bottom-up, using live HTMLCollection): 174 * L4 â into preceding L3 175 * L3 â into preceding L2 176 * L2 â into preceding L1 177 * 178 * Must run BEFORE decorateBlocks so blocks inside nested sections are still 179 * reachable by the `div.section > div > div` selector. 180 * 181 * Skipped inside Universal Editor â authors see sections stacked normally. 182 * @param {Element} main 183 */ 184function buildNavSections(main) { 185 if (main.querySelector(':scope > .section[data-aue-resource]')) return; 186 187 // Nest consecutive child-type siblings under the nearest preceding parent-type sibling. 188 // Uses live main.children so DOM mutations are reflected immediately. 189 const nestConsecutive = (parentType, childType) => { 190 let i = 0; 191 while (i < main.children.length) { 192 const el = main.children[i]; 193 if (el.classList.contains('section') && el.dataset.type === parentType) { 194 while ( 195 i + 1 < main.children.length 196 && main.children[i + 1].classList.contains('section') 197 && main.children[i + 1].dataset.type === childType 198 ) { 199 el.append(main.children[i + 1]); 200 } 201 } 202 i += 1; 203 } 204 }; 205 206 // Bottom-up: nest L4 into L3, then L3 into L2, then L2 into L1 207 nestConsecutive('navigation-group-section-item', 'navigation-group-section-item-group'); 208 nestConsecutive('navigation-group-section', 'navigation-group-section-item'); 209 nestConsecutive('navigation', 'navigation-group-section'); 210} 211 212/** 213 * Groups consecutive Tab sections (sections with data-tab-label set by the 214 * tab-section model) into a tabs block via auto-blocking. If a tabs-wrapper 215 * section (data-tabs-wrapper) immediately precedes the run, the tabs block is 216 * appended inside it so header content and tabs share the same background. 217 * Otherwise a new wrapper section is created. 218 * 219 * Runs after decorateSections (which extracts section metadata into dataset) 220 * and before decorateBlocks (which decorates the newly-built tabs block). 221 * @param {Element} main 222 */ 223function buildAutoBlocks(main) { 224 // Skip in Universal Editor: tab-sections must stay as direct children of 225 // main with their data-aue-resource attrs intact so UE's content tree can 226 // find them. applyTabSectionsPreview() handles the visual grouping instead. 227 // Detect UE via data-aue-resource, which AEM sets before any JS runs. 228 if (main.querySelector(':scope > .section[data-aue-resource]')) return; 229 230 const allSections = [...main.querySelectorAll('.section')]; 231 232 // Collect runs of consecutive tab-sections 233 const run = []; 234 const flush = () => { 235 if (run.length < 1) return; 236 const tabSections = run.splice(0); 237 238 // Build the tabs block: each row = [title cell | content cell] 239 const tabsBlock = document.createElement('div'); 240 tabsBlock.className = 'tabs'; 241 242 tabSections.forEach((section) => { 243 const row = document.createElement('div'); 244 245 const titleCell = document.createElement('div'); 246 titleCell.textContent = section.dataset.tabLabel; 247 row.appendChild(titleCell); 248 249 const contentCell = document.createElement('div'); 250 // Move all block wrappers from the section into the content cell 251 [...section.querySelectorAll(':scope > div')].forEach((child) => { 252 contentCell.appendChild(child); 253 }); 254 row.appendChild(contentCell); 255 256 tabsBlock.appendChild(row); 257 }); 258 259 const blockWrapper = document.createElement('div'); 260 blockWrapper.appendChild(tabsBlock); 261 262 // Check if the section immediately before the tab-section run is a 263 // tabs-wrapper (explicit marker) or any non-tab section that can serve 264 // as a wrapper (convention: the preceding section becomes the container 265 // so header content and tabs share the same background). 266 const firstTabSection = tabSections[0]; 267 const prevSection = firstTabSection.previousElementSibling; 268 const isWrapper = prevSection?.classList.contains('section') 269 && !prevSection.dataset.tabLabel; 270 271 if (isWrapper) {
272 // Append tabs block into the existing wrapper section 273 prevSection.appendChild(blockWrapper); 274 tabSections.forEach((s) => s.remove()); 275 } else { 276 // No wrapper â create a standalone section (legacy / backward compat) 277 const newSection = document.createElement('div'); 278 newSection.className = 'section'; 279 newSection.dataset.sectionStatus = 'initialized'; 280 newSection.style.display = 'none'; 281 282 newSection.appendChild(blockWrapper); 283 284 firstTabSection.insertAdjacentElement('beforebegin', newSection); 285 tabSections.forEach((s) => s.remove()); 286 } 287 }; 288 289 allSections.forEach((section) => { 290 if (section.dataset.tabLabel) { 291 run.push(section); 292 } else { 293 flush(); 294 } 295 }); 296 flush(); // handle tab sections at the end of the page 297} 298 299/** 300 * Maps column width values to flex-grow integers (12-unit grid). 301 * Applied to individual `.layout-column` sections via the `columnWidth` UE field. 302 * "1/3" + "2/3" produces 4fr + 8fr â correct 1:2 ratio. 303 */ 304/** 305 * Assembles authored Layout Columns groups into flex row containers. 306 * 307 * Authoring model (all sections, resource type = section/v1/section): 308 * .layout-columns â config section (first in group); carries columnCount + shared styling 309 * .layout-column â individual column panel; follows the config section 310 * 311 * On delivery each group is wrapped: 312 * <div class="layout-columns-row layout-columns-row-N-cols"> 313 * <section.layout-columns>â¦</section> â config / first column 314 * <section.layout-column>â¦</section> â column 2 315 * ⦠316 * </div> 317 * 318 * Skipped inside the Universal Editor â authors see sections stacked normally. 319 * @param {Element} main 320 */ 321export function assembleLayoutColumns(main) { 322 // Skip in Universal Editor (sections have data-aue-resource instrumentation) 323 if (main.querySelector(':scope > .section[data-aue-resource]')) return; 324 325 const children = [...main.children]; 326 let i = 0; 327 while (i < children.length) { 328 const child = children[i]; 329 if (child.classList.contains('section') && child.classList.contains('layout-columns')) { 330 // Collect consecutive layout-column sections following the wrapper 331 const columns = []; 332 let j = i + 1; 333 while ( 334 j < children.length 335 && children[j].classList.contains('section') 336 && children[j].classList.contains('layout-column') 337 ) { 338 columns.push(children[j]); 339 j += 1; 340 } 341 342 if (columns.length) { 343 // Create the row wrapper for the columns only 344 const row = document.createElement('div'); 345 row.className = `layout-columns-row layout-columns-row-${columns.length}-cols`; 346 347 // Propagate explicit column count from config section if set 348 const { columncount } = child.dataset; 349 if (columncount) row.style.setProperty('--lc-column-count', columncount); 350 351 // Move column sections into the row 352 columns.forEach((section) => { 353 row.append(section); 354 }); 355 356 // Append the row to the wrapper section so wrapper content (titles, etc.) 357 // appears above the columns â mirrors the tabs-wrapper / tab-section pattern. 358 child.append(row); 359 } 360 361 i = j; 362 } else { 363 i += 1; 364 } 365 } 366} 367 368/** 369 * Decorates the main element. 370 * @param {Element} main The main element 371 */ 372// eslint-disable-next-line import/prefer-default-export 373export function decorateMain(main) { 374 // hopefully forward compatible button decoration 375 decorateButtons(main); 376 decorateDividers(main); 377 decorateIcons(main); 378 decorateExternalLinks(main); 379 decorateSections(main); 380 buildAutoBlocks(main); 381 decorateBlocks(main); 382 attachDefaultContentLinkAnalytics(main); 383 assembleLayoutColumns(main); 384 // Nest nav sections AFTER decorateBlocks so that blocks inside nested 385 // sections are already decorated (div.section > div > div selector won't 386 // reach blocks once sections are nested 3 levels deep). 387 buildNavSections(main); 388 enableModalTriggers(main); 389 a11yLinks(main); 390} 391 392/** 393 * Loads everything needed to get to LCP. 394 * @param {Element} doc The container element 395 */ 396async function loadEager(doc) { 397 document.documentElement.lang = 'en'; 398 decorateTemplateAndTheme(); 399 400 // Add homepage class when at the site root â enables the transparent-over-hero 401 // header (desktop only) defined in header.scss under .homepage. 402 // Falls back to URL detection when page metadata does not carry template:homepage. 403 if (window.location.pathname === '/' && !document.body.classList.contains('homepage')) { 404 document.body.classList.add('homepage'); 405 } 406 407 // Kick off the header fragment fetch as early as possible â before any awaits â 408 // so the network request is in-flight while brand tokens and the header JS 409 // module are loading. header.js reuses this Promise instead of starting its own. 410 const headerMeta = getMetadata('header'); 411 const headerPath = headerMeta ? new URL(headerMeta, window.location).pathname : '/fragments/header'; 412 window.hlx.headerFragmentFetch = fetch(`${headerPath}.plain.html`) 413 .then((r) => (r.ok ? r.text() : null)) 414 .catch(() => null); 415 416 // Load brand-specific design tokens and JS override registry before first paint 417 const { getCurrentBrand, setBrandOverrides, setBrandCssOverrides } = await import('./site-resolver.js'); 418 const brand = getCurrentBrand(); 419 if (brand) { 420 await loadCSS(`${window.hlx.codeBasePath}/brands/${brand}/tokens.css`); 421 try { 422 const { default: overrides } = await import( 423 `${window.hlx.codeBasePath}/brands/${brand}/overrides.js` 424 ); 425 // overrides may be an array (legacy) or { js: [], css: [] } 426 if (Array.isArray(overrides)) { 427 setBrandOverrides(overrides); 428 setBrandCssOverrides([]); 429 } else { 430 setBrandOverrides(overrides.js || []); 431 setBrandCssOverrides(overrides.css || []); 432 } 433 } catch { 434 setBrandOverrides([]); 435 setBrandCssOverrides([]); 436 } 437 } 438 439 if (getMetadata('breadcrumbs').toLowerCase() === 'true') { 440 doc.body.dataset.breadcrumbs = true; 441 } 442 443 const main = doc.querySelector('main'); 444 445 // Start header load immediately â don't gate on main content
446 const headerLoaded = loadHeader(doc.querySelector('header')); 447 448 if (main) { 449 decorateMain(main); 450 451 // Load section block CSS (sections are not auto-loaded as blocks) 452 loadCSS(`${window.hlx.codeBasePath}/blocks/section/section.css`).catch(() => { }); 453 loadCSS(`${window.hlx.codeBasePath}/blocks/layout-columns/layout-columns.css`).catch(() => { }); 454 455 // Decorate all sections with section.js (overlays, gradients, themes) 456 const { decorate: decorateSection } = await import('../blocks/section/section.js'); 457 main.querySelectorAll('.section').forEach((section) => decorateSection(section)); 458 459 document.body.classList.add('appear'); 460 await Promise.all([ 461 loadSection(main.querySelector('.section'), waitForFirstImage), 462 headerLoaded, 463 ]); 464 } else { 465 await headerLoaded; 466 } 467 468 try { 469 /* if desktop (proxy for fast connection) or fonts already loaded, load fonts.css */ 470 if (window.innerWidth >= 900 || sessionStorage.getItem('fonts-loaded')) { 471 loadFonts(); 472 } 473 } catch (e) { 474 // do nothing 475 } 476} 477 478/** 479 * Loads everything that doesn't need to be delayed. 480 * @param {Element} doc The container element 481 */ 482/** 483 * Loads a banner fragment driven by <meta name="banner" content="/path/to/fragment">. 484 * Inserts a <div id="banner"> before the <header> element. 485 * No-ops if the metadata value is absent. 486 * @param {Document} doc 487 */ 488async function loadBanner(doc) { 489 const bannerMeta = getMetadata('banner'); 490 if (!bannerMeta) return; 491 const bannerPath = new URL(bannerMeta, window.location).pathname; 492 const html = await fetch(`${bannerPath}.plain.html`) 493 .then((r) => (r.ok ? r.text() : null)) 494 .catch(() => null); 495 if (!html) return; 496 const fragMain = document.createElement('main'); 497 fragMain.innerHTML = html; 498 decorateMain(fragMain); 499 await loadSections(fragMain); 500 const bannerEl = document.createElement('div'); 501 bannerEl.id = 'banner'; 502 while (fragMain.firstElementChild) bannerEl.append(fragMain.firstElementChild); 503 doc.querySelector('header').insertAdjacentElement('beforebegin', bannerEl); 504} 505 506async function loadLazy(doc) { 507 const main = doc.querySelector('main'); 508 await loadSections(main); 509 510 const { hash } = window.location; 511 const element = hash ? doc.getElementById(hash.substring(1)) : false; 512 if (hash && element) element.scrollIntoView(); 513 514 loadBanner(doc); 515 loadFooter(doc.querySelector('footer')); 516 517 loadFonts(); 518} 519 520/** 521 * Resolve Adobe Launch script URL from `/config.json` key `launch_url`. 522 * @returns {Promise<string>} 523 */ 524async function fetchLaunchScriptUrl() { 525 let launchUrl = ''; 526 const configured = await getConfigValue('launch_url'); 527 if (configured) { 528 launchUrl = configured; 529 } 530 return launchUrl; 531} 532 533/** 534 * Loads Adobe Launch asynchronously after resolving embed URL (supports API-driven URL later). 535 * `onLoaded` runs after script network load (or immediately if URL resolve/inject fails). 536 * @param {() => void} [onLoaded] 537 * @returns {Promise<void>} 538 */ 539async function loadLaunchScript(onLoaded) { 540 let launchSrc; 541 try { 542 launchSrc = await fetchLaunchScriptUrl(); 543 } catch { 544 if (typeof onLoaded === 'function') onLoaded(); 545 return; 546 } 547 if (!launchSrc) { 548 if (typeof onLoaded === 'function') onLoaded(); 549 return; 550 } 551 552 const launchScript = document.createElement('script'); 553 launchScript.src = launchSrc; 554 if (typeof onLoaded === 'function') { 555 launchScript.addEventListener('load', onLoaded); 556 launchScript.addEventListener('error', onLoaded); 557 } 558 document.body.appendChild(launchScript); 559} 560 561/** 562 * Loads everything that happens a lot later, 563 * without impacting the user experience. 564 */ 565function loadDelayed() { 566 // eslint-disable-next-line import/no-cycle 567 window.setTimeout(() => import('./delayed.js'), 3000); 568} 569 570function pageLoadAnalytics() { 571 loadLaunchScript(() => { 572 pushAnalyticsEvent(ANALYTICS_EVENTS.PAGE_VIEW.name); 573 574 const productName = getMetadata('product_name').trim(); 575 if (productName) { 576 pushProductViewAnalytics({ 577 productName, 578 productCategory: getMetadata('product_category').trim(), 579 }); 580 attachProductPageLeaveTracking(productName); 581 } 582 }).catch(() => { }); 583} 584 585function initPageLoadAnalytics() {
586 // head.html enables Speculation Rules prerendering (hover/focus), which loads 587 // the page in a hidden browsing context and fires `load` there too. Defer to 588 // `prerenderingchange`, which only fires once the user actually navigates in. 589 if (document.prerendering) { 590 document.addEventListener('prerenderingchange', () => pageLoadAnalytics(), { once: true }); 591 } else { 592 pageLoadAnalytics(); 593 } 594} 595 596async function loadPage() { 597 await loadEager(document); 598 await loadLazy(document); 599 loadDelayed(); 600 initPageLoadAnalytics(); 601} 602 603configureUePreview().catch(() => { }); 604loadPage();
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.