1/* 2 * Copyright 2026 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 = () => 17 window.performance ? window.performance.now() : Date.now() - window.hlx.rum.firstReadTime; 18 try { 19 window.hlx = window.hlx || {}; 20 if (!window.hlx.rum || !window.hlx.rum.collector) { 21 sampleRUM.enhance = () => {}; 22 const params = new URLSearchParams(window.location.search); 23 const { currentScript } = document; 24 const rate = 25 params.get('rum') || 26 window.SAMPLE_PAGEVIEWS_AT_RATE || 27 params.get('optel') || 28 (currentScript && currentScript.dataset.rate); 29 const rateValue = { 30 on: 1, 31 off: 0, 32 high: 10, 33 low: 1000, 34 }[rate]; 35 const weight = rateValue !== undefined ? rateValue : 100; 36 const id = (window.hlx.rum && window.hlx.rum.id) || crypto.randomUUID().slice(-9); 37 const isSelected = 38 (window.hlx.rum && window.hlx.rum.isSelected) || (weight > 0 && Math.random() * weight < 1); 39 // eslint-disable-next-line object-curly-newline, max-len 40 window.hlx.rum = { 41 weight, 42 id, 43 isSelected, 44 firstReadTime: window.performance ? window.performance.timeOrigin : Date.now(), 45 sampleRUM, 46 queue: [], 47 collector: (...args) => window.hlx.rum.queue.push(args), 48 }; 49 if (isSelected) { 50 const dataFromErrorObj = (error) => { 51 const errData = { source: 'undefined error' }; 52 try { 53 errData.target = error.toString(); 54 if (error.stack) { 55 errData.source = error.stack 56 .split('\n') 57 .filter((line) => line.match(/https?:\/\//)) 58 .shift() 59 .replace(/at ([^ ]+) \((.+)\)/, '$1@$2') 60 .replace(/ at /, '@') 61 .trim(); 62 } 63 } catch (err) { 64 /* error structure was not as expected */ 65 } 66 return errData; 67 }; 68 69 window.addEventListener('error', ({ error }) => { 70 const errData = dataFromErrorObj(error); 71 sampleRUM('error', errData); 72 }); 73 74 window.addEventListener('unhandledrejection', ({ reason }) => { 75 let errData = { 76 source: 'Unhandled Rejection', 77 target: reason || 'Unknown', 78 }; 79 if (reason instanceof Error) { 80 errData = dataFromErrorObj(reason); 81 } 82 sampleRUM('error', errData); 83 }); 84 85 window.addEventListener('securitypolicyviolation', (e) => { 86 if (e.blockedURI.includes('helix-rum-enhancer') && e.disposition === 'enforce') { 87 const errData = { 88 source: 'csp', 89 target: e.blockedURI, 90 }; 91 sampleRUM.sendPing('error', timeShift(), errData); 92 } 93 }); 94 95 sampleRUM.baseURL = 96 sampleRUM.baseURL || new URL(window.RUM_BASE || '/', new URL('https://ot.aem.live')); 97 sampleRUM.collectBaseURL = sampleRUM.collectBaseURL || sampleRUM.baseURL; 98 sampleRUM.sendPing = (ck, time, pingData = {}) => { 99 // eslint-disable-next-line max-len, object-curly-newline 100 const rumData = JSON.stringify({ 101 weight, 102 id, 103 referer: window.location.href, 104 checkpoint: ck, 105 t: time, 106 ...pingData, 107 }); 108 const urlParams = window.RUM_PARAMS 109 ? new URLSearchParams(window.RUM_PARAMS).toString() || '' 110 : ''; 111 const { href: url, origin } = new URL( 112 `.rum/${weight}
112${urlParams ? `?${urlParams}` : ''}`, 113 sampleRUM.collectBaseURL 114 ); 115 const body = 116 origin === window.location.origin 117 ? new Blob([rumData], { type: 'application/json' }) 118 : rumData; 119 navigator.sendBeacon(url, body); 120 // eslint-disable-next-line no-console 121 console.debug(`ping:${ck}`, pingData); 122 }; 123 sampleRUM.sendPing('top', timeShift()); 124 125 sampleRUM.enhance = () => { 126 // only enhance once 127 if (document.querySelector('script[src*="rum-enhancer"]')) return; 128 const { enhancerVersion, enhancerHash } = sampleRUM.enhancerContext || {}; 129 const script = document.createElement('script'); 130 if (enhancerHash) { 131 script.integrity = enhancerHash; 132 script.setAttribute('crossorigin', 'anonymous'); 133 } 134 script.src = new URL( 135 `.rum/@adobe/helix-rum-enhancer@${enhancerVersion || '^2'}/src/index.js`, 136 sampleRUM.baseURL 137 ).href; 138 document.head.appendChild(script); 139 }; 140 if (!window.hlx.RUM_MANUAL_ENHANCE) { 141 sampleRUM.enhance(); 142 } 143 } 144 } 145 if (window.hlx.rum && window.hlx.rum.isSelected && checkpoint) { 146 window.hlx.rum.collector(checkpoint, data, timeShift()); 147 } 148 document.dispatchEvent(new CustomEvent('rum', { detail: { checkpoint, data } })); 149 } catch (error) { 150 // something went awry 151 } 152} 153 154/** 155 * Setup block utils. 156 */ 157function setup() { 158 window.hlx = window.hlx || {}; 159 window.hlx.RUM_MASK_URL = 'full'; 160 window.hlx.RUM_MANUAL_ENHANCE = true; 161 window.hlx.codeBasePath = ''; 162 window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse') === 'on'; 163 164 const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]'); 165 if (scriptEl) { 166 try { 167 const scriptURL = new URL(scriptEl.src, window.location); 168 if (scriptURL.host === window.location.host) { 169 [window.hlx.codeBasePath] = scriptURL.pathname.split('/scripts/scripts.js'); 170 } else { 171 [window.hlx.codeBasePath] = scriptURL.href.split('/scripts/scripts.js'); 172 } 173 } catch (error) { 174 // eslint-disable-next-line no-console 175 console.log(error); 176 } 177 } 178} 179 180/** 181 * Auto initialization. 182 */ 183 184function init() { 185 setup(); 186 sampleRUM.collectBaseURL = window.origin; 187 sampleRUM(); 188} 189 190/** 191 * Sanitizes a string for use as class name. 192 * @param {string} name The unsanitized string 193 * @returns {string} The class name 194 */ 195function toClassName(name) { 196 return typeof name === 'string' 197 ? name 198 .toLowerCase() 199 .replace(/[^0-9a-z]/gi, '-') 200 .replace(/-+/g, '-') 201 .replace(/^-|-$/g, '') 202 : ''; 203} 204 205/** 206 * Sanitizes a string for use as a js property name. 207 * @param {string} name The unsanitized string 208 * @returns {string} The camelCased name 209 */ 210function toCamelCase(name) { 211 return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); 212} 213 214/** 215 * Extracts the config from a block. 216 * @param {Element} block The block element 217 * @returns {object} The block config 218 */ 219// eslint-disable-next-line import-x/prefer-default-export 220function readBlockConfig(block) { 221 const config = {}; 222 block.querySelectorAll(':scope > div').forEach((row) => { 223 if (row.children) { 224 const cols = [...row.children]; 225 if (cols[1]) { 226 const col = cols[1]; 227 const name = toClassName(cols[0].textContent); 228 let value = ''; 229 if (col.querySelector('a')) { 230 const as = [...col.querySelectorAll('a')]; 231 if (as.length === 1) { 232 value = as[0].href; 233 } else { 234 value = as.map((a) => a.href); 235 } 236 } else if (col.querySelector('img')) { 237 const imgs = [...col.querySelectorAll('img')]; 238 if (imgs.length === 1) { 239 value = imgs[0].src; 240 } else { 241 value = imgs.map((img) => img.src); 242 } 243 } else if (col.querySelector('p')) { 244 const ps = [...col.querySelectorAll('p')]; 245 if (ps.length === 1) { 246 value = ps[0].textContent; 247 } else { 248 value = ps.map((p) => p.textContent); 249 } 250 } else value = row.children[1].textContent; 251 config[name] = value; 252 } 253 } 254 }); 255 return config; 256} 257
258/** 259 * Loads a CSS file. 260 * @param {string} href URL to the CSS file 261 */ 262async function loadCSS(href) { 263 return new Promise((resolve, reject) => { 264 if (!document.querySelector(`head > link[href="${href}"]`)) { 265 const link = document.createElement('link'); 266 link.rel = 'stylesheet'; 267 link.href = href; 268 link.onload = resolve; 269 link.onerror = reject; 270 document.head.append(link); 271 } else { 272 resolve(); 273 } 274 }); 275} 276 277/** 278 * Loads a non module JS file. 279 * @param {string} src URL to the JS file 280 * @param {Object} attrs additional optional attributes 281 */ 282async function loadScript(src, attrs) { 283 return new Promise((resolve, reject) => { 284 if (!document.querySelector(`head > script[src="${src}"]`)) { 285 const script = document.createElement('script'); 286 script.src = src; 287 if (attrs) { 288 // eslint-disable-next-line no-restricted-syntax, guard-for-in 289 for (const attr in attrs) { 290 script.setAttribute(attr, attrs[attr]); 291 } 292 } 293 script.onload = resolve; 294 script.onerror = reject; 295 document.head.append(script); 296 } else { 297 resolve(); 298 } 299 }); 300} 301 302/** 303 * Retrieves the content of metadata tags. 304 * @param {string} name The metadata name (or property) 305 * @param {Document} doc Document object to query for metadata. Defaults to the window's document 306 * @returns {string} The metadata value(s) 307 */ 308function getMetadata(name, doc = document) { 309 const attr = name && name.includes(':') ? 'property' : 'name'; 310 const meta = [...doc.head.querySelectorAll(`meta[${attr}="${name}"]`)] 311 .map((m) => m.content) 312 .join(', '); 313 return meta || ''; 314} 315 316/** 317 * Returns a picture element with webp and fallbacks 318 * @param {string} src The image URL 319 * @param {string} [alt] The image alternative text 320 * @param {boolean} [eager] Set loading attribute to eager 321 * @param {Array} [breakpoints] Breakpoints and corresponding params (eg. width) 322 * @returns {Element} The picture element 323 */ 324function createOptimizedPicture( 325 src, 326 alt = '', 327 eager = false, 328 breakpoints = [{ media: '(min-width: 600px)', width: '2000' }, { width: '750' }] 329) { 330 const url = new URL(src, window.location.href); 331 const picture = document.createElement('picture'); 332 const { pathname } = url; 333 const ext = pathname.substring(pathname.lastIndexOf('.') + 1); 334 335 // webp 336 breakpoints.forEach((br) => { 337 const source = document.createElement('source'); 338 if (br.media) source.setAttribute('media', br.media); 339 source.setAttribute('type', 'image/webp'); 340 source.setAttribute('srcset', `${pathname}?width=${br.width}&format=webply&optimize=medium`); 341 picture.appendChild(source); 342 }); 343 344 // fallback 345 breakpoints.forEach((br, i) => { 346 if (i < breakpoints.length - 1) { 347 const source = document.createElement('source'); 348 if (br.media) source.setAttribute('media', br.media); 349 source.setAttribute('srcset', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); 350 picture.appendChild(source); 351 } else { 352 const img = document.createElement('img'); 353 img.setAttribute('loading', eager ? 'eager' : 'lazy'); 354 img.setAttribute('alt', alt); 355 picture.appendChild(img); 356 img.setAttribute('src', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); 357 } 358 }); 359 360 return picture; 361} 362 363/** 364 * Set template (page structure) and theme (page styles). 365 */ 366function decorateTemplateAndTheme() { 367 const addClasses = (element, classes) => { 368 classes.split(',').forEach((c) => { 369 element.classList.add(toClassName(c.trim())); 370 }); 371 }; 372 const template = getMetadata('template'); 373 if (template) addClasses(document.body, template); 374 const theme = getMetadata('theme'); 375 if (theme) addClasses(document.body, theme); 376} 377 378/** 379 * Wrap inline text content of block cells within a <p> tag. 380 * @param {Element} block the block element 381 */ 382function wrapTextNodes(block) { 383 const validWrappers = [ 384 'P', 385 'PRE', 386 'UL', 387 'OL', 388 'PICTURE', 389 'TABLE', 390 'H1', 391 'H2', 392 'H3', 393 'H4', 394 'H5', 395 'H6', 396 'HR', 397 ]; 398 399 const wrap = (el) => { 400 const wrapper = document.createElement('p'); 401 wrapper.append(...el.childNodes); 402 [...el.attributes] 403 // move the instrumentation from the cell to the new paragraph, also keep the class 404 // in case the content is a buttton and the cell the button-container 405 .filter( 406 ({ nodeName }) => 407 nodeName === 'class' || 408 nodeName.startsWith('data-aue') || 409 nodeName.startsWith('data-richtext') 410 ) 411 .forEach(({ nodeName, nodeValue }) => {
412 wrapper.setAttribute(nodeName, nodeValue); 413 el.removeAttribute(nodeName); 414 }); 415 el.append(wrapper); 416 }; 417 418 block.querySelectorAll(':scope > div > div').forEach((blockColumn) => { 419 if (blockColumn.hasChildNodes()) { 420 const hasWrapper = 421 !!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.href !== a.textContent) { 443 const up = a.parentElement; 444 const twoup = a.parentElement.parentElement; 445 if (!a.querySelector('img')) { 446 if (up.childNodes.length === 1 && (up.tagName === 'P' || up.tagName === 'DIV')) { 447 a.className = 'button'; // default 448 up.classList.add('button-container'); 449 } 450 if ( 451 up.childNodes.length === 1 && 452 up.tagName === 'STRONG' && 453 twoup.childNodes.length === 1 && 454 twoup.tagName === 'P' 455 ) { 456 a.className = 'button primary'; 457 twoup.classList.add('button-container'); 458 } 459 if ( 460 up.childNodes.length === 1 && 461 up.tagName === 'EM' && 462 twoup.childNodes.length === 1 && 463 twoup.tagName === 'P' 464 ) { 465 a.className = 'button secondary'; 466 twoup.classList.add('button-container'); 467 } 468 } 469 } 470 }); 471} 472 473/** 474 * Add <img> for icon, prefixed with codeBasePath and optional prefix. 475 * @param {Element} [span] span element with icon classes 476 * @param {string} [prefix] prefix to be added to icon src 477 * @param {string} [alt] alt text to be added to icon 478 */ 479function decorateIcon(span, prefix = '', alt = '') { 480 const iconName = Array.from(span.classList) 481 .find((c) => c.startsWith('icon-')) 482 .substring(5); 483 const img = document.createElement('img'); 484 img.dataset.iconName = iconName; 485 img.src = `${window.hlx.codeBasePath}${prefix}/icons/${iconName}.svg`; 486 img.alt = alt; 487 img.loading = 'lazy'; 488 img.width = 16; 489 img.height = 16; 490 span.append(img); 491} 492 493/** 494 * Add <img> for icons, prefixed with codeBasePath and optional prefix. 495 * @param {Element} [element] Element containing icons 496 * @param {string} [prefix] prefix to be added to icon the src 497 */ 498function decorateIcons(element, prefix = '') { 499 const icons = element.querySelectorAll('span.icon'); 500 icons.forEach((span) => { 501 decorateIcon(span, prefix); 502 }); 503} 504 505/** 506 * Decorates all sections in a container element. 507 * @param {Element} main The container element 508 */ 509function decorateSections(main) { 510 main.querySelectorAll(':scope > div:not([data-section-status])').forEach((section) => { 511 const wrappers = []; 512 let defaultContent = false; 513 [...section.children].forEach((e) => { 514 if ((e.tagName === 'DIV' && e.className) || !defaultContent) { 515 const wrapper = document.createElement('div'); 516 wrappers.push(wrapper); 517 defaultContent = e.tagName !== 'DIV' || !e.className; 518 if (defaultContent) wrapper.classList.add('default-content-wrapper'); 519 } 520 wrappers[wrappers.length - 1].append(e); 521 }); 522 wrappers.forEach((wrapper) => section.append(wrapper)); 523 section.classList.add('section'); 524 section.dataset.sectionStatus = 'initialized'; 525 section.style.display = 'none'; 526 527 // Process section metadata 528 const sectionMeta = section.querySelector('div.section-metadata'); 529 if (sectionMeta) { 530 const meta = readBlockConfig(sectionMeta); 531 Object.keys(meta).forEach((key) => { 532 if (key === 'style') { 533 const styles = meta.style 534 .split(',') 535 .filter((style) => style) 536 .map((style) => toClassName(style.trim()));
537 styles.forEach((style) => section.classList.add(style)); 538 } else { 539 section.dataset[toCamelCase(key)] = meta[key]; 540 } 541 }); 542 sectionMeta.parentNode.remove(); 543 } 544 }); 545} 546 547/** 548 * Builds a block DOM Element from a two dimensional array, string, or object 549 * @param {string} blockName name of the block 550 * @param {*} content two dimensional array or string or object of content 551 */ 552function buildBlock(blockName, content) { 553 const table = Array.isArray(content) ? content : [[content]]; 554 const blockEl = document.createElement('div'); 555 // build image block nested div structure 556 blockEl.classList.add(blockName); 557 table.forEach((row) => { 558 const rowEl = document.createElement('div'); 559 row.forEach((col) => { 560 const colEl = document.createElement('div'); 561 const vals = col.elems ? col.elems : [col]; 562 vals.forEach((val) => { 563 if (val) { 564 if (typeof val === 'string') { 565 colEl.innerHTML += val; 566 } else { 567 colEl.appendChild(val); 568 } 569 } 570 }); 571 rowEl.appendChild(colEl); 572 }); 573 blockEl.appendChild(rowEl); 574 }); 575 return blockEl; 576} 577 578/** 579 * Loads JS and CSS for a block. 580 * @param {Element} block The block element 581 */ 582async function loadBlock(block) { 583 const status = block.dataset.blockStatus; 584 if (status !== 'loading' && status !== 'loaded') { 585 block.dataset.blockStatus = 'loading'; 586 const { blockName } = block.dataset; 587 try { 588 const cssLoaded = loadCSS(`${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.css`); 589 const decorationComplete = new Promise((resolve) => { 590 (async () => { 591 try { 592 const mod = await import( 593 `${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.js` 594 ); 595 if (mod.default) { 596 await mod.default(block); 597 } 598 } catch (error) { 599 // eslint-disable-next-line no-console 600 console.error(`failed to load module for ${blockName}`, error); 601 } 602 resolve(); 603 })(); 604 }); 605 await Promise.all([cssLoaded, decorationComplete]); 606 } catch (error) { 607 // eslint-disable-next-line no-console 608 console.error(`failed to load block ${blockName}`, error); 609 } 610 block.dataset.blockStatus = 'loaded'; 611 } 612 return block; 613} 614 615/** 616 * Decorates a block. 617 * @param {Element} block The block element 618 */ 619function decorateBlock(block) { 620 const shortBlockName = block.classList[0]; 621 if (shortBlockName && !block.dataset.blockStatus) { 622 block.classList.add('block'); 623 block.dataset.blockName = shortBlockName; 624 block.dataset.blockStatus = 'initialized'; 625 wrapTextNodes(block); 626 const blockWrapper = block.parentElement; 627 blockWrapper.classList.add(`${shortBlockName}-wrapper`); 628 const section = block.closest('.section'); 629 if (section) section.classList.add(`${shortBlockName}-container`); 630 // eslint-disable-next-line no-use-before-define 631 decorateButtons(block); 632 } 633} 634 635/** 636 * Decorates all blocks in a container element. 637 * @param {Element} main The container element 638 */ 639function decorateBlocks(main) { 640 main.querySelectorAll('div.section > div > div').forEach(decorateBlock); 641} 642 643/** 644 * Loads a block named 'header' into header 645 * @param {Element} header header element 646 * @returns {Promise} 647 */ 648async function loadHeader(header) { 649 const headerBlock = buildBlock('header', ''); 650 header.append(headerBlock); 651 decorateBlock(headerBlock); 652 return loadBlock(headerBlock); 653} 654 655/** 656 * Loads a block named 'footer' into footer 657 * @param footer footer element 658 * @returns {Promise} 659 */ 660async function loadFooter(footer) { 661 const footerBlock = buildBlock('footer', ''); 662 footer.append(footerBlock); 663 decorateBlock(footerBlock); 664 return loadBlock(footerBlock); 665} 666 667/** 668 * Wait for Image. 669 * @param {Element} section section element 670 */ 671async function waitForFirstImage(section) { 672 const lcpCandidate = section.querySelector('img'); 673 await new Promise((resolve) => { 674 if (lcpCandidate && !lcpCandidate.complete) { 675 lcpCandidate.setAttribute('loading', 'eager'); 676 lcpCandidate.addEventListener('load', resolve); 677 lcpCandidate.addEventListener('error', resolve); 678 } else { 679 resolve(); 680 } 681 }); 682} 683 684/** 685 * Loads all blocks in a section. 686 * @param {Element} section The section element 687 */ 688 689async function loadSection(section, loadCallback) { 690 const status = section.dataset.sectionStatus; 691 if (!status || status === 'initialized') { 692 section.dataset.sectionStatus = 'loading'; 693 const blocks = [...section.querySelectorAll('div.block')]; 694 for (let i = 0; i < blocks.length; i += 1) { 695 // eslint-disable-next-line no-await-in-loop 696 await loadBlock(blocks[i]); 697 } 698 if (loadCallback) await loadCallback(section); 699 section.dataset.sectionStatus = 'loaded'; 700 section.style.display = null; 701 } 702} 703 704/** 705 * Loads all sections. 706 * @param {Element} element The parent element of sections to load 707 */ 708 709async function loadSections(element) { 710 const sections = [...element.querySelectorAll('div.section')]; 711 for (let i = 0; i < sections.length; i += 1) { 712 // eslint-disable-next-line no-await-in-loop 713 await loadSection(sections[i]); 714 if (i === 0 && sampleRUM.enhance) { 715 sampleRUM.enhance(); 716 } 717 } 718} 719 720init(); 721 722export { 723 buildBlock, 724 createOptimizedPicture, 725 decorateBlock, 726 decorateBlocks, 727 decorateButtons, 728 decorateIcons, 729 decorateSections, 730 decorateTemplateAndTheme, 731 getMetadata, 732 loadBlock, 733 loadCSS, 734 loadFooter, 735 loadHeader, 736 loadScript, 737 loadSection, 738 loadSections, 739 readBlockConfig, 740 sampleRUM, 741 setup, 742 toCamelCase, 743 toClassName, 744 waitForFirstImage, 745 wrapTextNodes, 746};
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.