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