1/* 2 * Copyright 2025 Adobe. All rights reserved. 3 * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. You may obtain a copy 5 * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 * 7 * Unless required by applicable law or agreed to in writing, software distributed under 8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 * OF ANY KIND, either express or implied. See the License for the specific language 10 * governing permissions and limitations under the License. 11 */ 12 13/* eslint-env browser */ 14function sampleRUM(checkpoint, data) { 15 // eslint-disable-next-line max-len 16 const timeShift = () => (window.performance ? window.performance.now() : Date.now() - window.hlx.rum.firstReadTime); 17 try { 18 window.hlx = window.hlx || {}; 19 if (!window.hlx.rum || !window.hlx.rum.collector) { 20 sampleRUM.enhance = () => { }; 21 const params = new URLSearchParams(window.location.search); 22 const { currentScript } = document; 23 const rate = params.get('rum') 24 || window.SAMPLE_PAGEVIEWS_AT_RATE 25 || params.get('optel') 26 || (currentScript && currentScript.dataset.rate); 27 const rateValue = { 28 on: 1, 29 off: 0, 30 high: 10, 31 low: 1000, 32 }[rate]; 33 const weight = rateValue !== undefined ? rateValue : 100; 34 const id = (window.hlx.rum && window.hlx.rum.id) || crypto.randomUUID().slice(-9); 35 const isSelected = (window.hlx.rum && window.hlx.rum.isSelected) 36 || (weight > 0 && Math.random() * weight < 1); 37 // eslint-disable-next-line object-curly-newline, max-len 38 window.hlx.rum = { 39 weight, 40 id, 41 isSelected, 42 firstReadTime: window.performance ? window.performance.timeOrigin : Date.now(), 43 sampleRUM, 44 queue: [], 45 collector: (...args) => window.hlx.rum.queue.push(args), 46 }; 47 if (isSelected) { 48 const dataFromErrorObj = (error) => { 49 const errData = { source: 'undefined error' }; 50 try { 51 errData.target = error.toString(); 52 if (error.stack) { 53 errData.source = error.stack 54 .split('\n') 55 .filter((line) => line.match(/https?:\/\//)) 56 .shift() 57 .replace(/at ([^ ]+) \((.+)\)/, '$1@$2') 58 .replace(/ at /, '@') 59 .trim(); 60 } 61 } catch (err) { 62 /* error structure was not as expected */ 63 } 64 return errData; 65 }; 66 67 window.addEventListener('error', ({ error }) => { 68 const errData = dataFromErrorObj(error); 69 sampleRUM('error', errData); 70 }); 71 72 window.addEventListener('unhandledrejection', ({ reason }) => { 73 let errData = { 74 source: 'Unhandled Rejection', 75 target: reason || 'Unknown', 76 }; 77 if (reason instanceof Error) { 78 errData = dataFromErrorObj(reason); 79 } 80 sampleRUM('error', errData); 81 }); 82 83 window.addEventListener('securitypolicyviolation', (e) => { 84 if (e.blockedURI.includes('helix-rum-enhancer') && e.disposition === 'enforce') { 85 const errData = { 86 source: 'csp', 87 target: e.blockedURI, 88 }; 89 sampleRUM.sendPing('error', timeShift(), errData); 90 } 91 }); 92 93 sampleRUM.baseURL = sampleRUM.baseURL || new URL(window.RUM_BASE || '/', new URL('https://ot.aem.live')); 94 sampleRUM.collectBaseURL = sampleRUM.collectBaseURL || sampleRUM.baseURL; 95 sampleRUM.sendPing = (ck, time, pingData = {}) => { 96 // eslint-disable-next-line max-len, object-curly-newline 97 const rumData = JSON.stringify({ 98 weight, 99 id, 100 referer: window.location.href, 101 checkpoint: ck, 102 t: time, 103 ...pingData, 104 }); 105 const urlParams = window.RUM_PARAMS 106 ? new URLSearchParams(window.RUM_PARAMS).toString() || '' 107 : ''; 108 const { href: url, origin } = new URL( 109 `.rum/${weight}
109${urlParams ? `?${urlParams}` : ''}`, 110 sampleRUM.collectBaseURL, 111 ); 112 const body = origin === window.location.origin 113 ? new Blob([rumData], { type: 'application/json' }) 114 : rumData; 115 navigator.sendBeacon(url, body); 116 // eslint-disable-next-line no-console 117 console.debug(`ping:${ck}`, pingData); 118 }; 119 sampleRUM.sendPing('top', timeShift()); 120 121 sampleRUM.enhance = () => { 122 // only enhance once 123 if (document.querySelector('script[src*="rum-enhancer"]')) return; 124 const { enhancerVersion, enhancerHash } = sampleRUM.enhancerContext || {}; 125 const script = document.createElement('script'); 126 if (enhancerHash) { 127 script.integrity = enhancerHash; 128 script.setAttribute('crossorigin', 'anonymous'); 129 } 130 script.src = new URL( 131 `.rum/@adobe/helix-rum-enhancer@${enhancerVersion || '^2'}/src/index.js`, 132 sampleRUM.baseURL, 133 ).href; 134 document.head.appendChild(script); 135 }; 136 if (!window.hlx.RUM_MANUAL_ENHANCE) { 137 sampleRUM.enhance(); 138 } 139 } 140 } 141 if (window.hlx.rum && window.hlx.rum.isSelected && checkpoint) { 142 window.hlx.rum.collector(checkpoint, data, timeShift()); 143 } 144 document.dispatchEvent(new CustomEvent('rum', { detail: { checkpoint, data } })); 145 } catch (error) { 146 // something went awry 147 } 148} 149 150/** 151 * Setup block utils. 152 */ 153function setup() { 154 window.hlx = window.hlx || {}; 155 window.hlx.RUM_MASK_URL = 'full'; 156 window.hlx.RUM_MANUAL_ENHANCE = true; 157 window.hlx.codeBasePath = ''; 158 window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse') === 'on'; 159 160 const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]'); 161 if (scriptEl) { 162 try { 163 const scriptURL = new URL(scriptEl.src, window.location); 164 if (scriptURL.host === window.location.host) { 165 [window.hlx.codeBasePath] = scriptURL.pathname.split('/scripts/scripts.js'); 166 } else { 167 [window.hlx.codeBasePath] = scriptURL.href.split('/scripts/scripts.js'); 168 } 169 } catch (error) { 170 // eslint-disable-next-line no-console 171 console.log(error); 172 } 173 } 174} 175 176/** 177 * Auto initialization. 178 */ 179 180function init() { 181 setup(); 182 sampleRUM.collectBaseURL = window.origin; 183 sampleRUM(); 184} 185 186/** 187 * Sanitizes a string for use as class name. 188 * @param {string} name The unsanitized string 189 * @returns {string} The class name 190 */ 191function toClassName(name) { 192 return typeof name === 'string' 193 ? name 194 .toLowerCase() 195 .replace(/[^0-9a-z]/gi, '-') 196 .replace(/-+/g, '-') 197 .replace(/^-|-$/g, '') 198 : ''; 199} 200 201/** 202 * Sanitizes a string for use as a js property name. 203 * @param {string} name The unsanitized string 204 * @returns {string} The camelCased name 205 */ 206function toCamelCase(name) { 207 return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); 208} 209 210/** 211 * Extracts the config from a block. 212 * @param {Element} block The block element 213 * @returns {object} The block config 214 */ 215// eslint-disable-next-line import/prefer-default-export 216function readBlockConfig(block) { 217 const config = {}; 218 block.querySelectorAll(':scope > div').forEach((row) => { 219 if (row.children) { 220 const cols = [...row.children]; 221 if (cols[1]) { 222 const col = cols[1]; 223 const name = toClassName(cols[0].textContent); 224 let value = ''; 225 if (col.querySelector('a')) { 226 const as = [...col.querySelectorAll('a')]; 227 if (as.length === 1) { 228 // For DAM paths, prefer textContent over href â Franklin delivery 229 // normalizes hrefs (lowercase + underscoreâhyphen) which breaks 230 // case-sensitive JCR DAM lookups on AEM Publish. 231 const a = as[0]; 232 const text = a.textContent.trim(); 233 value = (text.startsWith('/content/dam/') && a.href.includes('/content/dam/')) 234 ? text : a.href; 235 } else { 236 value = as.map((a) => { 237 const text = a.textContent.trim(); 238 return (text.startsWith('/content/dam/') && a.href.includes('/content/dam/')) 239 ? text : a.href; 240 }); 241 } 242 } else if (col.querySelector('img')) { 243 const imgs = [...col.querySelectorAll('img')]; 244 if (imgs.length === 1) { 245 value = imgs[0].src; 246 } else { 247 value = imgs.map((img) => img.src); 248 } 249 } else if (col.querySelector('p')) { 250 const ps = [...col.querySelectorAll('p')]; 251 if (ps.length === 1) { 252 value = ps[0].textContent; 253 } else { 254 value = ps.map((p) => p.textContent); 255 } 256 } else value = row.children[1].textContent; 257 config[name] = value; 258 } 259 } 260 }); 261 return config; 262} 263
264/** 265 * Loads a CSS file. 266 * @param {string} href URL to the CSS file 267 */ 268async function loadCSS(href) { 269 return new Promise((resolve, reject) => { 270 if (!document.querySelector(`head > link[href="${href}"]`)) { 271 const link = document.createElement('link'); 272 link.rel = 'stylesheet'; 273 link.href = href; 274 link.onload = resolve; 275 link.onerror = reject; 276 document.head.append(link); 277 } else { 278 resolve(); 279 } 280 }); 281} 282 283/** 284 * Loads a non module JS file. 285 * @param {string} src URL to the JS file 286 * @param {Object} attrs additional optional attributes 287 */ 288async function loadScript(src, attrs) { 289 return new Promise((resolve, reject) => { 290 if (!document.querySelector(`head > script[src="${src}"]`)) { 291 const script = document.createElement('script'); 292 script.src = src; 293 if (attrs) { 294 // eslint-disable-next-line no-restricted-syntax, guard-for-in 295 for (const attr in attrs) { 296 script.setAttribute(attr, attrs[attr]); 297 } 298 } 299 script.onload = resolve; 300 script.onerror = reject; 301 document.head.append(script); 302 } else { 303 resolve(); 304 } 305 }); 306} 307 308/** 309 * Retrieves the content of metadata tags. 310 * @param {string} name The metadata name (or property) 311 * @param {Document} doc Document object to query for metadata. Defaults to the window's document 312 * @returns {string} The metadata value(s) 313 */ 314function getMetadata(name, doc = document) { 315 const attr = name && name.includes(':') ? 'property' : 'name'; 316 const meta = [...doc.head.querySelectorAll(`meta[${attr}="${name}"]`)] 317 .map((m) => m.content) 318 .join(', '); 319 return meta || ''; 320} 321 322/** 323 * Returns a picture element with webp and fallbacks 324 * @param {string} src The image URL 325 * @param {string} [alt] The image alternative text 326 * @param {boolean} [eager] Set loading attribute to eager 327 * @param {Array} [breakpoints] Breakpoints and corresponding params (eg. width) 328 * @returns {Element} The picture element 329 */ 330function createOptimizedPicture( 331 src, 332 alt = '', 333 eager = false, 334 breakpoints = [{ media: '(min-width: 600px)', width: '2000' }, { width: '750' }], 335) { 336 const url = new URL(src, window.location.href); 337 const picture = document.createElement('picture'); 338 const { pathname } = url; 339 const ext = pathname.substring(pathname.lastIndexOf('.') + 1); 340 341 // webp 342 breakpoints.forEach((br) => { 343 const source = document.createElement('source'); 344 if (br.media) source.setAttribute('media', br.media); 345 source.setAttribute('type', 'image/webp'); 346 source.setAttribute('srcset', `${pathname}?width=${br.width}&format=webply&optimize=medium`); 347 picture.appendChild(source); 348 }); 349 350 // fallback 351 breakpoints.forEach((br, i) => { 352 if (i < breakpoints.length - 1) { 353 const source = document.createElement('source'); 354 if (br.media) source.setAttribute('media', br.media); 355 source.setAttribute('srcset', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); 356 picture.appendChild(source); 357 } else { 358 const img = document.createElement('img'); 359 img.setAttribute('loading', eager ? 'eager' : 'lazy'); 360 img.setAttribute('alt', alt); 361 picture.appendChild(img); 362 img.setAttribute('src', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); 363 } 364 }); 365 366 return picture; 367} 368 369/** 370 * Set template (page structure) and theme (page styles). 371 */ 372function decorateTemplateAndTheme() { 373 const addClasses = (element, classes) => { 374 classes.split(',').forEach((c) => { 375 element.classList.add(toClassName(c.trim())); 376 }); 377 }; 378 const template = getMetadata('template'); 379 if (template) addClasses(document.body, template); 380 const theme = getMetadata('theme'); 381 if (theme) addClasses(document.body, theme); 382} 383 384/** 385 * Wrap inline text content of block cells within a <p> tag. 386 * @param {Element} block the block element 387 */ 388function wrapTextNodes(block) { 389 const validWrappers = [ 390 'P', 391 'PRE', 392 'UL', 393 'OL', 394 'PICTURE', 395 'TABLE', 396 'H1', 397 'H2', 398 'H3', 399 'H4', 400 'HR', 401 ]; 402 403 const wrap = (el) => { 404 const wrapper = document.createElement('p'); 405 wrapper.append(...el.childNodes); 406 [...el.attributes] 407 // move the instrumentation from the cell to the new paragraph, also keep the class 408 // in case the content is a buttton and the cell the button-container 409 .filter(({ nodeName }) => nodeName === 'class' 410 || nodeName.startsWith('data-aue') 411 || nodeName.startsWith('data-richtext')) 412 .forEach(({ nodeName, nodeValue }) => {
413 wrapper.setAttribute(nodeName, nodeValue); 414 el.removeAttribute(nodeName); 415 }); 416 el.append(wrapper); 417 }; 418 419 block.querySelectorAll(':scope > div > div').forEach((blockColumn) => { 420 if (blockColumn.hasChildNodes()) { 421 const hasWrapper = !!blockColumn.firstElementChild 422 && validWrappers.some((tagName) => blockColumn.firstElementChild.tagName === tagName); 423 if (!hasWrapper) { 424 wrap(blockColumn); 425 } else if ( 426 blockColumn.firstElementChild.tagName === 'PICTURE' 427 && (blockColumn.children.length > 1 || !!blockColumn.textContent.trim()) 428 ) { 429 wrap(blockColumn); 430 } 431 } 432 }); 433} 434 435/** 436 * Decorates paragraphs containing a single link as buttons. 437 * @param {Element} element container element 438 */ 439function decorateButtons(element) { 440 element.querySelectorAll('a').forEach((a) => { 441 a.title = a.title || a.textContent; 442 if (a.closest('.cta')) return; 443 if (a.href !== a.textContent) { 444 const up = a.parentElement; 445 const twoup = a.parentElement.parentElement; 446 if (!a.querySelector('img')) { 447 if (up.childNodes.length === 1 && (up.tagName === 'P' || up.tagName === 'DIV')) { 448 a.className = 'button'; // default 449 up.classList.add('button-container'); 450 } 451 if ( 452 up.childNodes.length === 1 453 && up.tagName === 'STRONG' 454 && twoup.childNodes.length === 1 455 && twoup.tagName === 'P' 456 ) { 457 a.className = 'button primary'; 458 twoup.classList.add('button-container'); 459 } 460 if ( 461 up.childNodes.length === 1 462 && up.tagName === 'EM' 463 && twoup.childNodes.length === 1 464 && twoup.tagName === 'P' 465 ) { 466 a.className = 'button secondary'; 467 twoup.classList.add('button-container'); 468 } 469 } 470 } 471 }); 472} 473 474/** 475 * Add <img> for icon, prefixed with codeBasePath and optional prefix. 476 * @param {Element} [span] span element with icon classes 477 * @param {string} [prefix] prefix to be added to icon src 478 * @param {string} [alt] alt text to be added to icon 479 */ 480function decorateIcon(span, prefix = '', alt = '') { 481 const iconName = Array.from(span.classList) 482 .find((c) => c.startsWith('icon-')) 483 .substring(5); 484 const img = document.createElement('img'); 485 img.dataset.iconName = iconName; 486 img.src = `${window.hlx.codeBasePath}${prefix}/icons/${iconName}.svg`; 487 img.alt = alt; 488 img.loading = 'lazy'; 489 img.width = 16; 490 img.height = 16; 491 span.append(img); 492} 493 494/** 495 * Add <img> for icons, prefixed with codeBasePath and optional prefix. 496 * @param {Element} [element] Element containing icons 497 * @param {string} [prefix] prefix to be added to icon the src 498 */ 499function decorateIcons(element, prefix = '') { 500 const icons = element.querySelectorAll('span.icon'); 501 icons.forEach((span) => { 502 decorateIcon(span, prefix); 503 }); 504} 505 506/** 507 * Decorates all sections in a container element. 508 * @param {Element} main The container element 509 */ 510function decorateSections(main) { 511 main.querySelectorAll(':scope > div:not([data-section-status])').forEach((section) => { 512 const wrappers = []; 513 let defaultContent = false; 514 [...section.children].forEach((e) => { 515 if ((e.tagName === 'DIV' && e.className) || !defaultContent) { 516 const wrapper = document.createElement('div'); 517 wrappers.push(wrapper); 518 defaultContent = e.tagName !== 'DIV' || !e.className; 519 if (defaultContent) wrapper.classList.add('default-content-wrapper'); 520 } 521 wrappers[wrappers.length - 1].append(e); 522 }); 523 wrappers.forEach((wrapper) => section.append(wrapper)); 524 section.classList.add('section'); 525 section.dataset.sectionStatus = 'initialized'; 526 section.style.display = 'none'; 527 528 // Process section metadata 529 const sectionMeta = section.querySelector('div.section-metadata'); 530 if (sectionMeta) { 531 const meta = readBlockConfig(sectionMeta); 532 Object.keys(meta).forEach((key) => { 533 if (key === 'style') { 534 const styles = meta.style 535 .split(',') 536 .filter((style) => style) 537 .map((style) => toClassName(style.trim()));
538 styles.forEach((style) => section.classList.add(style)); 539 } else { 540 section.dataset[toCamelCase(key)] = meta[key]; 541 } 542 }); 543 sectionMeta.parentNode.remove(); 544 } 545 }); 546} 547 548/** 549 * Builds a block DOM Element from a two dimensional array, string, or object 550 * @param {string} blockName name of the block 551 * @param {*} content two dimensional array or string or object of content 552 */ 553function buildBlock(blockName, content) { 554 const table = Array.isArray(content) ? content : [[content]]; 555 const blockEl = document.createElement('div'); 556 // build image block nested div structure 557 blockEl.classList.add(blockName); 558 table.forEach((row) => { 559 const rowEl = document.createElement('div'); 560 row.forEach((col) => { 561 const colEl = document.createElement('div'); 562 const vals = col.elems ? col.elems : [col]; 563 vals.forEach((val) => { 564 if (val) { 565 if (typeof val === 'string') { 566 colEl.innerHTML += val; 567 } else { 568 colEl.appendChild(val); 569 } 570 } 571 }); 572 rowEl.appendChild(colEl); 573 }); 574 blockEl.appendChild(rowEl); 575 }); 576 return blockEl; 577} 578 579/** 580 * Attempts to load a resource from multiple paths 581 * @param {string[]} paths Array of paths to try 582 * @param {Function} loader Function to load the resource 583 * @returns {Promise<any>} The loaded resource or null 584 */ 585async function loadFromPaths(paths, loader) { 586 const attempted = []; 587 let lastError = null; 588 let failedPath = null; 589 590 for (const path of paths) { 591 const candidates = []; 592 const basePath = window.hlx?.codeBasePath || ''; 593 candidates.push(`${basePath}${path}.js`); 594 candidates.push(`${path}.js`); 595 596 const scriptEls = [ 597 ...document.querySelectorAll('script[src*="/scripts/scripts.js"]'), 598 ]; 599 scriptEls.forEach((scriptEl) => { 600 try { 601 const scriptURL = new URL(scriptEl.src, window.location); 602 const [scriptBase] = scriptURL.href.split('/scripts/scripts.js'); 603 if (scriptBase) { 604 candidates.push(`${scriptBase}${path}.js`); 605 } 606 } catch (error) { 607 // Ignore bad script URL entries and continue with other candidates. 608 } 609 }); 610 611 const uniqueCandidates = [...new Set(candidates.filter(Boolean))]; 612 613 for (let i = 0; i < uniqueCandidates.length; i += 1) { 614 const candidate = uniqueCandidates[i]; 615 attempted.push(candidate);
616 try { 617 const result = await loader(candidate); 618 return { 619 result, 620 attempted, 621 failedPath: null, 622 error: null, 623 }; 624 } catch (error) { 625 lastError = error; 626 } 627 } 628 629 if (lastError) failedPath = path; 630 } 631 632 return { 633 result: null, 634 attempted, 635 failedPath, 636 error: lastError, 637 }; 638} 639 640/** 641 * Loads JS and CSS for a block with site resolution support. 642 * Resolution order: brands/{brand}/blocks â /blocks 643 * @param {Element} block The block element 644 */ 645async function loadBlock(block) { 646 const status = block.dataset.blockStatus; 647 if (status !== 'loading' && status !== 'loaded') { 648 block.dataset.blockStatus = 'loading'; 649 const { blockName } = block.dataset; 650 651 try { 652 // Import site resolver dynamically 653 const { getBlockPaths, getCurrentBrand, hasBrandCssOverride } = await import('./site-resolver.js'); 654 655 // Get JS resolution paths (brand-specific only if declared in overrides.js) 656 const jsPaths = getBlockPaths(blockName); 657 658 // Load CSS in parallel: shared first (guaranteed), brand second (overrides via DOM order) 659 // Brand CSS only requested if declared in overrides â avoids 404s on every block 660 const brand = getCurrentBrand(); 661 const sharedCss = loadCSS( 662 `${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.css`, 663 ).catch(() => { }); 664 const brandCss = (brand && hasBrandCssOverride(blockName)) 665 ? loadCSS( 666 `${window.hlx.codeBasePath}/brands/${brand}/blocks/${blockName}/${blockName}.css`, 667 ).catch(() => { }) 668 : Promise.resolve(); 669 const cssLoaded = Promise.all([sharedCss, brandCss]); 670 671 // Try to load JS from resolution paths 672 const decorationComplete = new Promise((resolve) => { 673 (async () => { 674 try { 675 const { 676 result: mod, attempted, failedPath, error, 677 } = await loadFromPaths( 678 jsPaths, 679 (url) => import(url), 680 ); 681 682 if (mod && mod.default) { 683 await mod.default(block); 684 } else if (error) { 685 // eslint-disable-next-line no-console 686 console.error( 687 `failed to load module for ${blockName} from ${failedPath || 'unknown path'}`, 688 error, 689 ); 690 // eslint-disable-next-line no-console 691 console.warn(`Attempted module URLs for ${blockName}: ${attempted.join(', ')}`); 692 } else { 693 // eslint-disable-next-line no-console 694 console.warn(`No module found for ${blockName} in any resolution path`); 695 // eslint-disable-next-line no-console 696 console.warn(`Attempted module URLs for ${blockName}: ${attempted.join(', ')}`); 697 } 698 } catch (error) { 699 // eslint-disable-next-line no-console 700 console.error(`failed to load module for ${blockName}`, error); 701 } 702 const { decorateExternalLinks } = await import('./link-utils.js'); 703 decorateExternalLinks(block); 704 resolve(); 705 })(); 706 }); 707 708 await Promise.all([cssLoaded, decorationComplete]); 709 } catch (error) { 710 // eslint-disable-next-line no-console 711 console.error(`failed to load block ${blockName}`, error); 712 } 713 block.dataset.blockStatus = 'loaded'; 714 } 715 return block; 716} 717 718/** 719 * Decorates a block. 720 * @param {Element} block The block element 721 */ 722function decorateBlock(block) { 723 const shortBlockName = block.classList[0]; 724 if (shortBlockName && !block.dataset.blockStatus) { 725 block.classList.add('block'); 726 block.dataset.blockName = shortBlockName; 727 block.dataset.blockStatus = 'initialized'; 728 wrapTextNodes(block); 729 const blockWrapper = block.parentElement; 730 blockWrapper.classList.add(`${shortBlockName}-wrapper`); 731 const section = block.closest('.section'); 732 if (section) section.classList.add(`${shortBlockName}-container`); 733 // eslint-disable-next-line no-use-before-define 734 decorateButtons(block); 735 } 736} 737 738/** 739 * Decorates all blocks in a container element. 740 * @param {Element} main The container element 741 */ 742function decorateBlocks(main) { 743 main.querySelectorAll('div.section > div > div').forEach(decorateBlock); 744} 745 746/** 747 * Decorates and loads nested blocks inside a container element (e.g. tabs, columns). 748 * Container blocks call this after their own DOM restructuring to initialize 749 * any child blocks that the standard decorateBlocks() pipeline doesn't reach. 750 * @param {Element} container The container element to search for nested blocks 751 * @returns {Promise<void>} 752 */ 753async function loadNestedBlocks(container) { 754 const nestedBlocks = [...container.querySelectorAll(':scope div[class]:not
754(.block)')] 755 .filter((el) => el.classList[0] && !el.dataset.blockStatus); 756 for (let i = 0; i < nestedBlocks.length; i += 1) { 757 const el = nestedBlocks[i]; 758 // Ensure the element has a wrapper parent (decorateBlock assumes parentElement exists) 759 if (!el.parentElement || el.parentElement === container) { 760 const wrapper = document.createElement('div'); 761 el.parentNode.insertBefore(wrapper, el); 762 wrapper.append(el); 763 } 764 decorateBlock(el); 765 // eslint-disable-next-line no-await-in-loop 766 await loadBlock(el); 767 } 768} 769 770/** 771 * Loads a block named 'header' into header 772 * @param {Element} header header element 773 * @returns {Promise} 774 */ 775async function loadHeader(header) { 776 const headerBlock = buildBlock('header', ''); 777 header.append(headerBlock); 778 decorateBlock(headerBlock); 779 return loadBlock(headerBlock); 780} 781 782/** 783 * Loads a block named 'footer' into footer 784 * @param footer footer element 785 * @returns {Promise} 786 */ 787async function loadFooter(footer) { 788 const footerBlock = buildBlock('footer', ''); 789 footer.append(footerBlock); 790 decorateBlock(footerBlock); 791 return loadBlock(footerBlock); 792} 793 794/** 795 * Wait for Image. 796 * @param {Element} section section element 797 */ 798async function waitForFirstImage(section) { 799 const lcpCandidate = section.querySelector('img'); 800 await new Promise((resolve) => { 801 if (lcpCandidate && !lcpCandidate.complete) { 802 lcpCandidate.setAttribute('loading', 'eager'); 803 lcpCandidate.addEventListener('load', resolve); 804 lcpCandidate.addEventListener('error', resolve); 805 } else { 806 resolve(); 807 } 808 }); 809} 810 811/** 812 * Loads all blocks in a section. 813 * @param {Element} section The section element 814 */ 815 816async function loadSection(section, loadCallback) { 817 const status = section.dataset.sectionStatus; 818 if (!status || status === 'initialized') { 819 section.dataset.sectionStatus = 'loading'; 820 const blocks = [...section.querySelectorAll('div.block')]; 821 for (let i = 0; i < blocks.length; i += 1) { 822 // eslint-disable-next-line no-await-in-loop 823 await loadBlock(blocks[i]); 824 } 825 if (loadCallback) await loadCallback(section); 826 section.dataset.sectionStatus = 'loaded'; 827 section.style.display = null; 828 } 829} 830 831/** 832 * Loads all sections. 833 * @param {Element} element The parent element of sections to load 834 */ 835 836async function loadSections(element) { 837 const sections = [...element.querySelectorAll('div.section')]; 838 for (let i = 0; i < sections.length; i += 1) { 839 // eslint-disable-next-line no-await-in-loop 840 await loadSection(sections[i]); 841 if (i === 0 && sampleRUM.enhance) { 842 sampleRUM.enhance(); 843 } 844 } 845} 846 847function extractAueConfig(block) { 848 const config = {}; 849 const auePropElements = block.querySelectorAll('[data-aue-prop]'); 850 auePropElements.forEach((el) => { 851 const prop = el.getAttribute('data-aue-prop'); 852 const value = el.textContent.trim(); 853 if (prop && value) { 854 config[prop] = value; 855 } 856 }); 857 return config; 858} 859 860init(); 861 862export { 863 buildBlock, 864 createOptimizedPicture, 865 extractAueConfig, 866 decorateBlock, 867 decorateBlocks, 868 decorateButtons, 869 decorateIcons, 870 decorateSections, 871 decorateTemplateAndTheme, 872 getMetadata, 873 loadBlock, 874 loadCSS, 875 loadFooter, 876 loadNestedBlocks, 877 loadHeader, 878 loadScript, 879 loadSection, 880 loadSections, 881 readBlockConfig, 882 sampleRUM, 883 setup, 884 toCamelCase, 885 toClassName, 886 waitForFirstImage, 887 wrapTextNodes, 888};
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.