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 */ 14let inlineStylesProcessorPromise; 15function loadInlineStylesProcessor() { 16 if (!inlineStylesProcessorPromise) { 17 inlineStylesProcessorPromise = import('./inline-styles-processor.js'); 18 } 19 return inlineStylesProcessorPromise; 20} 21 22function sampleRUM(checkpoint, data) { 23 // eslint-disable-next-line max-len 24 const timeShift = () => (window.performance ? window.performance.now() : Date.now() - window.hlx.rum.firstReadTime); 25 try { 26 window.hlx = window.hlx || {}; 27 sampleRUM.enhance = () => { }; 28 if (!window.hlx.rum) { 29 const param = new URLSearchParams(window.location.search).get('rum'); 30 const weight = (window.SAMPLE_PAGEVIEWS_AT_RATE === 'high' && 10) 31 || (window.SAMPLE_PAGEVIEWS_AT_RATE === 'low' && 1000) 32 || (param === 'on' && 1) 33 || 100; 34 const id = Math.random().toString(36).slice(-4); 35 const isSelected = param !== 'off' && Math.random() * weight < 1; 36 // eslint-disable-next-line object-curly-newline, max-len 37 window.hlx.rum = { 38 weight, 39 id, 40 isSelected, 41 firstReadTime: window.performance ? window.performance.timeOrigin : Date.now(), 42 sampleRUM, 43 queue: [], 44 collector: (...args) => window.hlx.rum.queue.push(args), 45 }; 46 if (isSelected) { 47 const dataFromErrorObj = (error) => { 48 const errData = { source: 'undefined error' }; 49 try { 50 errData.target = error.toString(); 51 errData.source = error.stack 52 .split('\n') 53 .filter((line) => line.match(/https?:\/\//)) 54 .shift() 55 .replace(/at ([^ ]+) \((.+)\)/, '$1@$2') 56 .replace(/ at /, '@') 57 .trim(); 58 } catch (err) { 59 /* error structure was not as expected */ 60 } 61 return errData; 62 }; 63 64 window.addEventListener('error', ({ error }) => { 65 const errData = dataFromErrorObj(error); 66 sampleRUM('error', errData); 67 }); 68 69 window.addEventListener('unhandledrejection', ({ reason }) => { 70 let errData = { 71 source: 'Unhandled Rejection', 72 target: reason || 'Unknown', 73 }; 74 if (reason instanceof Error) { 75 errData = dataFromErrorObj(reason); 76 } 77 sampleRUM('error', errData); 78 }); 79 80 sampleRUM.baseURL = sampleRUM.baseURL || new URL(window.RUM_BASE || '/', new URL('https://rum.hlx.page')); 81 sampleRUM.collectBaseURL = sampleRUM.collectBaseURL || sampleRUM.baseURL; 82 sampleRUM.sendPing = (ck, time, pingData = {}) => { 83 // eslint-disable-next-line max-len, object-curly-newline 84 const rumData = JSON.stringify({ 85 weight, 86 id, 87 referer: window.location.href, 88 checkpoint: ck, 89 t: time, 90 ...pingData, 91 }); 92 const urlParams = window.RUM_PARAMS 93 ? `?${new URLSearchParams(window.RUM_PARAMS).toString()}` 94 : ''; 95 const { href: url, origin } = new URL( 96 `.rum/${weight}${urlParams}`, 97 sampleRUM.collectBaseURL, 98 ); 99 const body = origin === window.location.origin 100 ? new Blob([rumData], { type: 'application/json' }) 101 : rumData; 102 navigator.sendBeacon(url, body); 103 // eslint-disable-next-line no-console 104 console.debug(`ping:${ck}`, pingData); 105 }; 106 sampleRUM.sendPing('top', timeShift()); 107 108 sampleRUM.enhance = () => { 109 // only enhance once 110 if (document.querySelector('script[src*="rum-enhancer"]')) return; 111 const { enhancerVersion, enhancerHash } = sampleRUM.enhancerContext || {}; 112 const script = document.createElement('script'); 113 if (enhancerHash) { 114 script.integrity = enhancerHash; 115 script.setAttribute('crossorigin', 'anonymous'); 116 } 117 script.src = new URL( 118 `.rum/@adobe/helix-rum-enhancer@${enhancerVersion || '^2'}/src/index.js`, 119 sampleRUM.baseURL, 120 ).href; 121 document.head.appendChild(script); 122 }; 123 if (!window.hlx.RUM_MANUAL_ENHANCE) { 124 sampleRUM.enhance(); 125 } 126 } 127 } 128 if (window.hlx.rum && window.hlx.rum.isSelected && checkpoint) { 129 window.hlx.rum.collector(checkpoint, data, timeShift()); 130 } 131 document.dispatchEvent(new CustomEvent('rum', { detail: { checkpoint, data } })); 132 } catch (error) { 133 // something went awry 134 } 135} 136 137/** 138 * Setup block utils. 139 */ 140function setup() { 141 window.hlx = window.hlx || {}; 142 window.hlx.RUM_MASK_URL = 'full'; 143 window.hlx.RUM_MANUAL_ENHANCE = true; 144 window.hlx.codeBasePath = ''; 145 window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse')
145=== 'on'; 146 147 const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]'); 148 if (scriptEl) { 149 try { 150 const scriptURL = new URL(scriptEl.src, window.location); 151 if (scriptURL.host === window.location.host) { 152 [window.hlx.codeBasePath] = scriptURL.pathname.split('/scripts/scripts.js'); 153 } else { 154 [window.hlx.codeBasePath] = scriptURL.href.split('/scripts/scripts.js'); 155 } 156 } catch (error) { 157 // eslint-disable-next-line no-console 158 console.log(error); 159 } 160 } 161} 162 163/** 164 * Auto initializiation. 165 */ 166 167function init() { 168 setup(); 169 sampleRUM(); 170} 171 172/** 173 * Sanitizes a string for use as class name. 174 * @param {string} name The unsanitized string 175 * @returns {string} The class name 176 */ 177function toClassName(name) { 178 return typeof name === 'string' 179 ? name 180 .toLowerCase() 181 .replace(/[^0-9a-z]/gi, '-') 182 .replace(/-+/g, '-') 183 .replace(/^-|-$/g, '') 184 : ''; 185} 186 187/** 188 * Sanitizes a string for use as a js property name. 189 * @param {string} name The unsanitized string 190 * @returns {string} The camelCased name 191 */ 192function toCamelCase(name) { 193 return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); 194} 195 196/** 197 * Extracts the config from a block. 198 * @param {Element} block The block element 199 * @returns {object} The block config 200 */ 201// eslint-disable-next-line import/prefer-default-export 202function readBlockConfig(block) { 203 const config = {}; 204 block.querySelectorAll(':scope > div').forEach((row) => { 205 if (row.children) { 206 const cols = [...row.children]; 207 if (cols[1]) { 208 const col = cols[1]; 209 const name = toClassName(cols[0].textContent); 210 let value = ''; 211 if (col.querySelector('a')) { 212 const as = [...col.querySelectorAll('a')]; 213 if (as.length === 1) { 214 value = as[0].href; 215 } else { 216 value = as.map((a) => a.href); 217 } 218 } else if (col.querySelector('img')) { 219 const imgs = [...col.querySelectorAll('img')]; 220 if (imgs.length === 1) { 221 value = imgs[0].src; 222 } else { 223 value = imgs.map((img) => img.src); 224 } 225 } else if (col.querySelector('p')) { 226 const ps = [...col.querySelectorAll('p')]; 227 if (ps.length === 1) { 228 value = ps[0].textContent; 229 } else { 230 value = ps.map((p) => p.textContent); 231 } 232 } else value = row.children[1].textContent; 233 config[name] = value; 234 } 235 } 236 }); 237 return config; 238} 239 240/** 241 * Loads a CSS file. 242 * @param {string} href URL to the CSS file 243 */ 244async function loadCSS(href) { 245 return new Promise((resolve, reject) => { 246 if (!document.querySelector(`head > link[href="${href}"]`)) { 247 const link = document.createElement('link'); 248 link.rel = 'stylesheet'; 249 link.href = href; 250 link.onload = resolve; 251 link.onerror = reject; 252 document.head.append(link); 253 } else { 254 resolve(); 255 } 256 }); 257} 258 259/** 260 * Loads a non module JS file. 261 * @param {string} src URL to the JS file 262 * @param {Object} attrs additional optional attributes 263 */ 264async function loadScript(src, attrs) { 265 return new Promise((resolve, reject) => { 266 if (!document.querySelector(`head > script[src="${src}"]`)) { 267 const script = document.createElement('script'); 268 script.src = src; 269 if (attrs) { 270 // eslint-disable-next-line no-restricted-syntax, guard-for-in 271 for (const attr in attrs) { 272 script.setAttribute(attr, attrs[attr]); 273 } 274 } 275 script.onload = resolve; 276 script.onerror = reject; 277 document.head.append(script); 278 } else { 279 resolve(); 280 } 281 }); 282} 283 284/** 285 * Retrieves the content of metadata tags. 286 * @param {string} name The metadata name (or property) 287 * @param {Document} doc Document object to query for metadata. Defaults to the window's document 288 * @returns {string} The metadata value(s) 289 */ 290function getMetadata(name, doc = document) { 291 const attr = name && name.includes(':') ? 'property' : 'name'; 292 const meta = [...doc.head.querySelectorAll(`meta[${attr}="${name}"]`)] 293 .map((m) => m.content) 294 .join(', '); 295 return meta || ''; 296} 297 298/** 299 * Returns a picture element with webp and fallbacks 300 * @param {string} src The image URL 301 * @param {string} [alt] The image alternative text 302 * @param {boolean} [eager] Set loading attribute to eager 303 * @param {Array}
303 [breakpoints] Breakpoints and corresponding params (eg. width) 304 * @param {boolean} useFullUrl Use full url of media image instead of relative path. Usable for AC media integration 305 * @param {string} fileExtension Override file extension 306 * 307 * @returns {Element} The picture element 308 */ 309function createOptimizedPicture( 310 src, 311 alt = '', 312 eager = false, 313 breakpoints = [{ media: '(min-width: 600px)', width: '2000' }, { width: '750' }], 314 useFullUrl = false, 315 fileExtension = '' 316) { 317 const url = new URL(src, window.location.href); 318 const picture = document.createElement('picture'); 319 const pathname = useFullUrl ? url.href : url.pathname; 320 const ext = fileExtension ?? pathname.substring(pathname.lastIndexOf('.') + 1); 321 322 // webp 323 breakpoints.forEach((br) => { 324 const source = document.createElement('source'); 325 if (br.media) source.setAttribute('media', br.media); 326 source.setAttribute('type', 'image/webp'); 327 source.setAttribute('srcset', `${pathname}?width=${br.width}&format=webply&optimize=medium`); 328 picture.appendChild(source); 329 }); 330 331 // fallback 332 breakpoints.forEach((br, i) => { 333 if (i < breakpoints.length - 1) { 334 const source = document.createElement('source'); 335 if (br.media) source.setAttribute('media', br.media); 336 source.setAttribute('srcset', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); 337 picture.appendChild(source); 338 } else { 339 const img = document.createElement('img'); 340 img.setAttribute('loading', eager ? 'eager' : 'lazy'); 341 img.setAttribute('fetchpriority', eager ? 'high' : 'auto'); 342 img.setAttribute('alt', alt); 343 picture.appendChild(img); 344 img.setAttribute('src', `${pathname}?width=${br.width}&format=${ext}&optimize=medium`); 345 } 346 }); 347 348 return picture; 349} 350 351/** 352 * Set template (page structure) and theme (page styles). 353 */ 354function decorateTemplateAndTheme() { 355 const addClasses = (element, classes) => { 356 classes.split(',').forEach((c) => { 357 element.classList.add(toClassName(c.trim())); 358 }); 359 }; 360 const template = getMetadata('template'); 361 if (template) addClasses(document.body, template); 362 const theme = getMetadata('theme'); 363 if (theme) addClasses(document.body, theme); 364} 365 366/** 367 * Wrap inline text content of block cells within a <p> tag. 368 * @param {Element} block the block element 369 */ 370function wrapTextNodes(block) { 371 const validWrappers = [ 372 'P', 373 'PRE', 374 'UL', 375 'OL', 376 'PICTURE', 377 'TABLE', 378 'H1', 379 'H2', 380 'H3', 381 'H4', 382 'H5', 383 'H6', 384 ]; 385 386 const wrap = (el) => { 387 const wrapper = document.createElement('p'); 388 wrapper.append(...el.childNodes); 389 [...el.attributes] 390 // move the instrumentation from the cell to the new paragraph, also keep the class 391 // in case the content is a buttton and the cell the button-container 392 .filter(({ nodeName }) => nodeName === 'class' 393 || nodeName.startsWith('data-aue') 394 || nodeName.startsWith('data-richtext')) 395 .forEach(({ nodeName, nodeValue }) => { 396 wrapper.setAttribute(nodeName, nodeValue); 397 el.removeAttribute(nodeName); 398 }); 399 el.append(wrapper); 400 }; 401 402 block.querySelectorAll(':scope > div > div').forEach((blockColumn) => { 403 if (blockColumn.hasChildNodes()) { 404 const hasWrapper = !!blockColumn.firstElementChild 405 && validWrappers.some((tagName) => blockColumn.firstElementChild.tagName === tagName); 406 if (!hasWrapper) { 407 wrap(blockColumn); 408 } else if ( 409 blockColumn.firstElementChild.tagName === 'PICTURE' 410 && (blockColumn.children.length > 1 || !!blockColumn.textContent.trim()) 411 ) { 412 wrap(blockColumn); 413 } 414 } 415 }); 416} 417 418/** 419 * Decorates paragraphs containing a single link as buttons. 420 * @param {Element} element container element 421 */ 422function decorateButtons(element) { 423 element.querySelectorAll('a').forEach((a) => { 424 a.title = a.title || a.textContent; 425 if (a.href !== a.textContent) { 426 const up = a.parentElement; 427 const twoup = a.parentElement.parentElement; 428 if (!a.querySelector('img')) { 429 if ((up.tagName === 'P' || up.tagName === 'DIV') && up.textContent.includes('{')) { 430 loadInlineStylesProcessor().then(({ applyInlineStyles }) => applyInlineStyles(up, false, a)); 431 } 432 if (up.childNodes.length === 1 && (up.tagName === 'P' || up.tagName === 'DIV')) { 433 a.className = 'button'; // default 434 up.classList.add('button-container'); 435 } 436 if ( 437 up.childNodes.length === 1 438 && up.tagName === 'STRONG' 439 && twoup.childNodes.length === 1 440 && twoup.tagName === 'P' 441 ) { 442 a.className = 'button primary'; 443 twoup.classList.add('button-container'); 444 } 445 if ( 446 up.childNodes.length === 1 447 && up.tagName === 'EM' 448 && twoup.childNodes.length === 1 449 && twoup.tagName === 'P' 450 ) { 451 a.className = 'button secondary'; 452 twoup.classList.add('button-container'); 453 } 454 } 455 } 456 }); 457} 458 459/** 460 * Add <img> for icon, prefixed with codeBasePath and optional prefix. 461 * @param {Element} [span] span element with icon classes 462 * @param {string} [prefix] prefix to be added to icon src 463 * @param {string} [alt] alt text to be added to icon 464 */ 465function decorateIcon(span, prefix = '', alt = '') { 466 const iconName = Array.from(span.classList) 467 .find((c) => c.startsWith('icon-')) 468 .substring(5); 469 const img = document.createElement('img'); 470 img.dataset.iconName = iconName; 471 img.src = `${window.hlx.codeBasePath}${prefix}/icons/${iconName}.svg`; 472 img.alt = alt; 473 img.loading = 'lazy'; 474 span.append(img); 475} 476 477/** 478 * Add <img> for icons, prefixed with codeBasePath and optional prefix. 479 * @param {Element} [element] Element containing icons 480 * @param {string} [prefix] prefix to be added to icon the src 481 */ 482function decorateIcons(element, prefix = '') { 483 const icons = [...element.querySelectorAll('span.icon')]; 484 icons.forEach((span) => { 485 decorateIcon(span, prefix); 486 }); 487} 488 489/** 490 * Decorates all sections in a container element. 491 * @param {Element} main The container element 492 */ 493function decorateSections(main) { 494 495 main.querySelectorAll(':scope > div:not([data-section-status])').forEach((section) => { 496 const wrappers = []; 497 let defaultContent = false;
498 [...section.children].forEach((e) => { 499 if ((e.tagName === 'DIV' && e.className) || !defaultContent) { 500 const wrapper = document.createElement('div'); 501 wrappers.push(wrapper); 502 defaultContent = e.tagName !== 'DIV' || !e.className; 503 if (defaultContent) wrapper.classList.add('default-content-wrapper'); 504 } 505 wrappers[wrappers.length - 1].append(e); 506 }); 507 wrappers.forEach((wrapper) => section.append(wrapper)); 508 section.classList.add('section'); 509 section.dataset.sectionStatus = 'initialized'; 510 section.style.display = 'none'; 511 512 section.querySelectorAll('p, h1, h2, h3, h4, h5, h6').forEach((el) => { 513 const hasText = el.textContent && el.textContent.trim().length > 0; 514 const hasBrackets = el.textContent.includes('{'); 515 516 if (hasText && hasBrackets) { 517 loadInlineStylesProcessor().then(({ applyInlineStyles }) => applyInlineStyles(el, false)); 518 } 519 }); 520 521 // Process section metadata 522 const sectionMeta = section.querySelector('div.section-metadata'); 523 if (sectionMeta) { 524 const meta = readBlockConfig(sectionMeta); 525 Object.keys(meta).forEach((key) => { 526 if (key === 'style') { 527 const styles = meta.style 528 .split(',') 529 .filter((style) => style) 530 .map((style) => toClassName(style.trim())); 531 styles.forEach((style) => section.classList.add(style)); 532 } else if (key === 'ID' || key === 'id') { 533 // Set the ID as an actual id attribute for anchor links 534 const sectionId = meta[key].trim(); 535 if (sectionId) { 536 section.id = sectionId; 537 } 538 } else { 539 section.dataset[toCamelCase(key)] = meta[key]; 540 } 541 }); 542 543 // Handle background colour 544 if (meta['backgroundcolour']) { 545 // String in format of "https://example.com/#FF0000", 546 // get the hex color value from the url by splitting the string on slashes 547 // section.classList.add('has-background-colour'); 548 const backgroundColourRaw = meta['backgroundcolour']; 549 const backgroundColourString = typeof backgroundColourRaw === 'string' 550 ? backgroundColourRaw 551 : `${backgroundColourRaw}`; 552 const metaBackgroundColour = backgroundColourString.match(/url\(['"]?(.*?)['"]?\)/)?.[1] || backgroundColourString; 553 const hexColor = metaBackgroundColour.split('/').pop(); 554 555 // After extracting the hex color value, clean up any remaining url fragments by splitting on the hash symbol and taking the last part 556 const cleanHexColor = hexColor.split('#').pop(); 557 558 // Apply background colour directly to section 559 section.style.backgroundColor = `#${cleanHexColor}`; 560 } 561 562 // Handle border image top 563 if (meta['borderimagetop']) { 564 section.classList.add('has-border-image-top'); 565 const borderImageTop = document.createElement('span'); 566 borderImageTop.className = 'section-border-image-top'; 567 const img = document.createElement('img'); 568 img.src = meta['borderimagetop']; 569 img.alt = ''; 570 borderImageTop.appendChild(img); 571 section.prepend(borderImageTop); 572 } 573 574 // Handle border image bottom 575 if (meta['borderimagebottom']) { 576 section.classList.add('has-border-image-bottom'); 577 const borderImageBottom = document.createElement('span'); 578 borderImageBottom.className = 'section-border-image-bottom'; 579 const img = document.createElement('img'); 580 img.src = meta['borderimagebottom']; 581 img.alt = ''; 582 borderImageBottom.appendChild(img); 583 section.appendChild(borderImageBottom); 584 } 585 586 sectionMeta.parentNode.remove(); 587 } 588 589 // Decorate section background images 590 const desktopSrc = section.dataset.backgroundimage; 591 const mobileSrc = section.dataset.backgroundimagemobile; 592 if (desktopSrc || mobileSrc) { 593 const picture = document.createElement('picture'); 594 picture.classList.add('section-background-image'); 595 596 // Apply desktop width/height/position settings 597 const bgWidth = section.dataset.backgroundimagewidth; 598 const bgHeightFull = section.dataset.backgroundimageheightfull; 599 const bgPosition = section.dataset.backgroundimageposition; 600 if (bgWidth) picture.classList.add(bgWidth); 601 if (bgHeightFull === 'true') picture.classList.add('bg-img-height-full'); 602 if (bgPosition) picture.classList.add(bgPosition); 603 604 // Apply mobile width/height/position settings 605 const bgMobileWidth = section.dataset.backgroundimagemobilewidth; 606 const bgMobileHeightFull = section.dataset.backgroundimagemobileheightfull; 607 const bgMobilePosition = section.dataset.backgroundimagemobileposition; 608 if (bgMobileWidth) picture.classList.add(bgMobileWidth); 609 if (bgMobileHeightFull === 'true') picture.classList.add('bg-img-mobile-height-full'); 610 if (bgMobilePosition) picture.classList.add(bgMobilePosition); 611 612 if (desktopSrc && mobileSrc) { 613 const mobileSource = document.createElement('source'); 614 mobileSource.setAttribute('media', '(max-width: 640px)'); 615 mobileSource.setAttribute('srcset', mobileSrc); 616 picture.appendChild(mobileSource); 617 618 const img = document.createElement('img'); 619 img.setAttribute('src', desktopSrc); 620 img.setAttribute('loading', 'lazy'); 621 img.setAttribute('alt', ''); 622 picture.appendChild(img); 623 } else { 624 const img = document.createElement('img'); 625 img.setAttribute('src', desktopSrc || mobileSrc); 626 img.setAttribute('loading', 'lazy'); 627 img.setAttribute('alt', ''); 628 picture.appendChild(img); 629 } 630 631 section.prepend(picture); 632 } 633 634 if (section.dataset.enableinlinestyling === 'true') { 635 section.querySelectorAll('p, h1, h2, h3, h4, h5, h6').forEach((el) => { 636 if (el.textContent && el.textContent.includes('{')) { 637 loadInlineStylesProcessor().then(({ applyInlineStyles }) => applyInlineStyles(el)); 638 } 639 }); 640 } 641 }); 642} 643 644/** 645 * Gets placeholders object. 646 * @param {string} [prefix] Location of placeholders 647 * @returns {object} Window placeholders object 648 */ 649// eslint-disable-next-line import/prefer-default-export 650async function fetchPlaceholders() { 651 window.placeholders = window.placeholders || {}; 652 653 if (!window.placeholders.default) { 654 window.placeholders.default = new Promise((resolve) => { 655 const url = getMetadata('placeholders') || '/placeholders.json'; 656 657 fetch(url) 658 .then((resp) => resp.ok ? resp.json() : {}) 659 .then((json) => { 660 const placeholders = {}; 661 json.data?.forEach(({ Key, Text }) => { 662 if (Key) placeholders[toCamelCase(Key)] = Text; 663 }); 664 window.placeholders.default = placeholders; 665 resolve(placeholders); 666 }) 667 .catch(() => { 668 window.placeholders.default = {}; 669 resolve({}); 670 }); 671 }); 672 } 673 674 return window.placeholders.default; 675} 676 677/**
678 * Builds a block DOM Element from a two dimensional array, string, or object 679 * @param {string} blockName name of the block 680 * @param {*} content two dimensional array or string or object of content 681 */ 682function buildBlock(blockName, content) { 683 const table = Array.isArray(content) ? content : [[content]]; 684 const blockEl = document.createElement('div'); 685 // build image block nested div structure 686 blockEl.classList.add(blockName); 687 table.forEach((row) => { 688 const rowEl = document.createElement('div'); 689 row.forEach((col) => { 690 const colEl = document.createElement('div'); 691 const vals = col.elems ? col.elems : [col]; 692 vals.forEach((val) => { 693 if (val) { 694 if (typeof val === 'string') { 695 colEl.innerHTML += val; 696 } else { 697 colEl.appendChild(val); 698 } 699 } 700 }); 701 rowEl.appendChild(colEl); 702 }); 703 blockEl.appendChild(rowEl); 704 }); 705 return blockEl; 706} 707 708/** 709 * Loads JS and CSS for a block. 710 * @param {Element} block The block element 711 */ 712async function loadBlock(block) { 713 const status = block.dataset.blockStatus; 714 if (status !== 'loading' && status !== 'loaded') { 715 block.dataset.blockStatus = 'loading'; 716 const { blockName } = block.dataset; 717 try { 718 const cssLoaded = loadCSS(`${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.css`); 719 const decorationComplete = new Promise((resolve) => { 720 (async () => { 721 try { 722 const mod = await import( 723 `${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.js` 724 ); 725 if (mod.default) { 726 await mod.default(block); 727 } 728 } catch (error) { 729 // eslint-disable-next-line no-console 730 console.log(`failed to load module for ${blockName}`, error); 731 } 732 resolve(); 733 })(); 734 }); 735 await Promise.all([cssLoaded, decorationComplete]); 736 } catch (error) { 737 // eslint-disable-next-line no-console 738 console.log(`failed to load block ${blockName}`, error); 739 } 740 block.dataset.blockStatus = 'loaded'; 741 } 742 return block; 743} 744 745/** 746 * Updates all section status in a container element. 747 * @param {Element} main The container element 748 */ 749export function updateSectionsStatus(main) { 750 const sections = [...main.querySelectorAll(':scope > div.section')]; 751 for (let i = 0; i < sections.length; i += 1) { 752 const section = sections[i]; 753 const status = section.dataset.sectionStatus; 754 if (status !== 'loaded') { 755 const loadingBlock = section.querySelector('.block[data-block-status="initialized"], .block[data-block-status="loading"]'); 756 if (loadingBlock) { 757 section.dataset.sectionStatus = 'loading'; 758 break; 759 } else { 760 section.dataset.sectionStatus = 'loaded'; 761 section.style.display = null; 762 } 763 } 764 } 765} 766 767export async function loadBlocks(main) { 768 updateSectionsStatus(main); 769 const blocks = [...main.querySelectorAll('div.block')]; 770 for (let i = 0; i < blocks.length; i += 1) { 771 // eslint-disable-next-line no-await-in-loop 772 await loadBlock(blocks[i]); 773 updateSectionsStatus(main); 774 } 775} 776 777/** 778 * Decorates a block. 779 * @param {Element} block The block element 780 */ 781function decorateBlock(block) { 782 const shortBlockName = block.classList[0]; 783 if (shortBlockName && !block.dataset.blockStatus) { 784 block.classList.add('block'); 785 786 // [...block.children].forEach((row) => { 787 // const label = row.children[0]; 788 // const body = row.children[1]; 789 790 // if (!body) { 791 // if (row) { 792 // if (label.innerText && row.closest('.block')) { 793 // try { 794 // row.closest('.block').classList.add(label.innerText); 795 // } catch (err) { 796 // return; 797 // } 798 // } 799 // } 800 // row.remove(); 801 // } 802 // }); 803 804 block.dataset.blockName = shortBlockName; 805 block.dataset.blockStatus = 'initialized'; 806 wrapTextNodes(block); 807 const blockWrapper = block.parentElement; 808 blockWrapper.classList.add(`${shortBlockName}-wrapper`); 809 const section = block.closest('.section'); 810 if (section) section.classList.add(`${shortBlockName}-container`); 811 // eslint-disable-next-line no-use-before-define 812 decorateButtons(block); 813 } 814} 815 816/** 817 * Decorates all blocks in a container element. 818 * @param {Element} main The container element 819 */ 820function decorateBlocks(main) { 821 main.querySelectorAll('div.section:not(.no-load) > div > div').forEach(decorateBlock); 822} 823 824/** 825 * Loads a block named 'header' into header 826 * @param {Element} header header element 827 * @returns {Promise} 828 */ 829async function loadHeader(header) { 830 const headerBlock = buildBlock('header', ''); 831 header.append(headerBlock); 832 decorateBlock(headerBlock); 833 return loadBlock(headerBlock); 834} 835 836/** 837 * Loads a block named 'footer' into footer 838 * @param footer footer element 839 * @returns {Promise} 840 */ 841async function loadFooter(footer) { 842 const footerBlock = buildBlock('footer', ''); 843 footer.append(footerBlock); 844 decorateBlock(footerBlock); 845 return loadBlock(footerBlock); 846} 847 848/** 849 * Loads all blocks in a section. 850 * @param {Element} section The section element 851 */ 852 853async function loadSection(section, loadCallback) { 854 const status = section.dataset.sectionStatus; 855 if (!status || status === 'initialized') { 856 section.dataset.sectionStatus = 'loading'; 857 const blocks = [...section.querySelectorAll('div.block')]; 858 for (let i = 0; i < blocks.length; i += 1) { 859 // eslint-disable-next-line no-await-in-loop 860 await loadBlock(blocks[i]); 861 } 862 if (loadCallback) await loadCallback(section); 863 section.dataset.sectionStatus = 'loaded'; 864 section.style.display = null; 865 } 866} 867 868/** 869 * Loads all sections. 870 * @param {Element} element The parent element of sections to load 871 */ 872 873async function loadSections(element) { 874 /** 875 * .no-load is a PDOL customisation. As part of PDP SSR initilisation 876 * we manually build up the section that houses the PDP dropin. This 877 * Change prevents it from loading again. 878 */ 879 const sections = [...element.querySelectorAll('div.section:not(.no-load)')]; 880 for (let i = 0; i < sections.length; i += 1) { 881 // eslint-disable-next-line no-await-in-loop 882 await loadSection(sections[i]); 883 if (i === 0 && sampleRUM.enhance) { 884 sampleRUM.enhance(); 885 } 886 } 887} 888 889init(); 890 891export { 892 buildBlock, 893 createOptimizedPicture, 894 decorateBlock, 895 decorateBlocks, 896 decorateButtons, 897 decorateIcons, 898 decorateSections, 899 decorateTemplateAndTheme, 900 fetchPlaceholders, 901 getMetadata, 902 loadBlock, 903 loadCSS, 904 loadFooter, 905 loadHeader, 906 loadScript, 907 loadSection, 908 loadSections, 909 readBlockConfig, 910 sampleRUM, 911 setup, 912 toCamelCase, 913 toClassName, 914 wrapTextNodes, 915};
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.