1import { 2 sampleRUM, 3 buildBlock, 4 loadHeader, 5 loadFooter, 6 decorateButtons, 7 decorateIcons, 8 decorateSections, 9 decorateBlocks, 10 decorateTemplateAndTheme, 11 waitForLCP, 12 loadBlocks, 13 loadCSS, 14 getMetadata, 15 isInternalPage, 16} from './lib-franklin.js'; 17 18const LCP_BLOCKS = [ 19 'hero', 20 'hero-banner', 21 'hero-horizontal-tabs', 22 'hero-vertical-tabs', 23 'overlapping-content', 24 'carousel', 25]; // add your LCP blocks to the list 26const SKIP_FROM_LCP = ['breadcrumb']; // add blocks that shouldn't ever be LCP candidates to the list 27// search for at least these many blocks (post-skipping-non-candidates) to find LCP candidates 28const MAX_LCP_CANDIDATE_BLOCKS = 2; 29 30const LANGUAGES = new Set(['en', 'de', 'cn', 'th', 'id', 'it', 'ja']); 31 32const MODAL_FRAGMENTS_PATH_SEGMENT = '/fragments/modals/'; 33export const MODAL_FRAGMENTS_ANCHOR_SELECTOR = `a[href*="${MODAL_FRAGMENTS_PATH_SEGMENT}"]`; 34 35let language; 36 37export function getLanguageFromPath(pathname, resetCache = false) { 38 if (resetCache) { 39 language = undefined; 40 } 41 42 if (language !== undefined) return language; 43 44 const segs = pathname.split('/'); 45 if (segs.length > 1) { 46 const l = segs[1]; 47 if (LANGUAGES.has(l)) { 48 language = l; 49 } 50 } 51 52 if (language === undefined) { 53 language = 'en'; // default to English 54 } 55 56 return language; 57} 58 59export function getLanguage(curPath = window.location.pathname, resetCache = false) { 60 return getLanguageFromPath(curPath, resetCache); 61} 62 63export function getLanguangeSpecificPath(path) { 64 const lang = getLanguage(); 65 if (lang === 'en') return path; 66 return `/${lang}${path}`; 67} 68 69/** 70 * Builds hero block and prepends to main in a new section. 71 * @param {Element} main The container element 72 */ 73function buildHeroBlock(main) { 74 const h1 = main.querySelector('h1'); 75 const picture = main.querySelector('picture'); 76 const hasHeroBlockVariant = main.querySelector('[class^="hero-"]'); 77 // omit to build hero block here for other hero blocks variants like hero-banner, 78 // hero-horizontal-tabs and hero-vertical-tabs 79 if (hasHeroBlockVariant) { 80 return; 81 } 82 // eslint-disable-next-line no-bitwise 83 if (h1 && picture && (h1.compareDocumentPosition(picture) & Node.DOCUMENT_POSITION_PRECEDING)) { 84 const section = document.createElement('div'); 85 section.append(buildBlock('hero', { elems: [picture, h1] })); 86 main.prepend(section); 87 } 88} 89 90function buildModalFragmentBlock(main) { 91 const MODAL_FRAGMENT_BLOCK_NAME = 'modal-fragment'; 92 if (main.querySelector(MODAL_FRAGMENTS_ANCHOR_SELECTOR) 93 && !main.querySelector(MODAL_FRAGMENT_BLOCK_NAME)) { 94 const section = document.createElement('div'); 95 const blockEl = buildBlock(MODAL_FRAGMENT_BLOCK_NAME, { elems: [] }); 96 section.append(blockEl); 97 main.prepend(section); 98 } 99} 100 101/** 102 * Builds all synthetic blocks in a container element. 103 * @param {Element} main The container element 104 */ 105function buildAutoBlocks(main) { 106 try { 107 buildHeroBlock(main); 108 buildModalFragmentBlock(main); 109 } catch (error) { 110 // eslint-disable-next-line no-console 111 console.error('Auto Blocking failed', error); 112 } 113} 114 115/** 116 * decorates anchors with video links 117 * for styling updates via CSS 118 * @param {Element}s anchor elements to decorate 119 * @returns {void} 120 */ 121export function decorateVideoLinks(youTubeAnchors) { 122 // currently only youtube links are supported 123 if (youTubeAnchors.length) { 124 youTubeAnchors.forEach((a) => { 125 a.classList.add('video-link'); 126 a.classList.add('youtube'); 127 }); 128 } 129} 130 131/** 132 * decorates external links to open in new window 133 * for styling updates via CSS 134 * @param {Element}s element The element to decorate 135 * @returns {void} 136 */ 137export function decorateExternalAnchors(externalAnchors) { 138 if (externalAnchors.length) { 139 externalAnchors.forEach((a) => { 140 a.target = '_blank'; 141 }); 142 } 143} 144 145/** 146 * Gets the extension of a URL. 147 * @param {string} url The URL 148 * @returns {string} The extension 149 * @private 150 * @example 151 * get_url_extension('https://example.com/foo.jpg'); 152 * // returns 'jpg' 153 * get_url_extension('https://example.com/foo.jpg?bar=baz'); 154 * // returns 'jpg' 155 * get_url_extension('https://example.com/foo'); 156 * // returns '' 157 * get_url_extension('https://example.com/foo.jpg#qux'); 158 * // returns 'jpg' 159 */ 160function getUrlExtension(url) { 161 return url.split(/[#?]/)[0].split('.').pop().trim(); 162} 163 164/** 165 * decorates anchors 166 * for styling updates via CSS 167 * @param {Element} element The element to decorate 168 * @returns {void} 169 */ 170export function decorateAnchors(element = document) { 171 const anchors = element.getElementsByTagName('a'); 172 decorateVideoLinks(Array.from(anchors).filter( 173 (a) => a.href.includes('youtu'), 174 )); 175 decorateExternalAnchors(Array.from(anchors).filter( 176 (a) => a.href && (!a.href.match(`^http[s]*://${window.location.host}/`) 177 || ['pdf'].includes(getUrlExtension(a.href).toLowerCase())), 178 )); 179} 180 181// Function to get the current window size 182export function getWindowSize() { 183 const windowWidth = window.innerWidth 184 || document.documentElement.clientWidth 185 || document.body.clientWidth; 186 const windowHeight = window.innerHeight 187 || document.documentElement.clientHeight 188 || document.body.clientHeight; 189 return { 190 width: windowWidth, 191 height: windowHeight, 192 }; 193} 194/** 195 * Decorates the main element. 196 * @param {Element} main The main element 197 */ 198// eslint-disable-next-line import/prefer-default-export 199export function decorateMain(main) { 200 // hopefully forward compatible button decoration 201 decorateButtons(main); 202 decorateAnchors(main); 203 decorateIcons(main); 204 buildAutoBlocks(main); 205 decorateSections(main); 206 decorateBlocks(main); 207} 208 209function decoratePageStyles() { 210 const pageStyle = getMetadata('page-style'); 211 if (pageStyle && pageStyle.trim().length > 0) { 212 loadCSS(`${`${window.location.protocol}//${window.location.host}`}/styles/pages/${pageStyle.toLowerCase()}.css`); 213 document.body.classList.add(pageStyle.toLowerCase()); 214 } 215} 216 217/** 218 * load fonts.css and set a session storage flag 219 */ 220async function loadFonts() { 221 await loadCSS(`${window.hlx.codeBasePath}/styles/fonts/fonts.css`); 222 try { 223 if (!window.location.hostname.includes('localhost')) sessionStorage.setItem('fonts-loaded', 'true'); 224 } catch (e) { 225 // do nothing 226 } 227} 228 229/** 230 * Loads everything needed to get to LCP. 231 * @param {Element} doc The container element 232 */ 233async function loadEager(doc) { 234 document.documentElement.lang = 'en'; 235 decorateTemplateAndTheme(); 236 decoratePageStyles(); 237 const main = doc.querySelector('main'); 238 if (main) { 239 decorateMain(main); 240 document.body.classList.add('appear'); 241 await waitForLCP(LCP_BLOCKS, SKIP_FROM_LCP, MAX_LCP_CANDIDATE_BLOCKS); 242 try { 243 /* if desktop (proxy for fast connection) or fonts already loaded, load fonts.css */ 244 if (window.innerWidth >= 900 || sessionStorage.getItem('fonts-loaded')) { 245 loadFonts(); 246 } 247 } catch (e) { 248 // do nothing 249 } 250 } 251} 252 253/** 254 * Adds the favicon. 255 * @param {string} href The favicon URL 256 */ 257export function addFavIcon(href) { 258 const link = document.createElement('link'); 259 link.rel = 'icon'; 260 link.type = 'image/png'; 261 link.href = href; 262 const existingLink = document.querySelector('head link[rel="icon"]'); 263 if (existingLink) { 264 existingLink.replaceWith(link); 265 } else { 266 document.head.append(link); 267 } 268} 269 270/** 271 * Loads everything that doesn't need to be delayed. 272 * @param {Element} doc The container element 273 */ 274async function loadLazy(doc) { 275 const main = doc.querySelector('main'); 276 await loadBlocks(main); 277 278 const { hash } = window.location; 279 const element = hash ? doc.getElementById(decodeURIComponent(hash.substring(1))) : null; 280 if (hash && element) element.scrollIntoView(); 281 if (!isInternalPage()) { 282 loadHeader(doc.querySelector('header')); 283 loadFooter(doc.querySelector('footer')); 284 285 loadCSS(`${window.hlx.codeBasePath}/styles/lazy-styles.css`); 286 loadFonts(); 287 addFavIcon(`${window.hlx.codeBasePath}/icons/favicon.ico`); 288 sampleRUM('lazy'); 289 sampleRUM.observe(main.querySelectorAll('div[data-block-name]')); 290 sampleRUM.observe(main.querySelectorAll('picture > img')); 291 } 292 import('../tools/sidekick/aem-sites-optimizer-preflight.js'); 293 import('../tools/sidekick/aem-genai-variations.js'); 294} 295 296/** 297 * Results returned from {@link fetchIndex}
297 come from a derived Excel sheet that is constructed 298 * with the FILTER function. This FILTER function has the unwanted side effect of returning '0' in 299 * cells that are empty in the original sheet. 300 * 301 * This function replaces those '0' values with empty cells again. 302 * 303 * @see fetchIndex 304 * @param {Object} data - the data returned from the fetchIndex function. 305 */ 306export function fixExcelFilterZeroes(data) { 307 data.forEach((line) => { 308 Object.keys(line).forEach((k) => { 309 line[k] = line[k] === '0' ? '' : line[k]; 310 }); 311 }); 312} 313 314export async function fetchIndex(indexFile, sheet, pageSize = 500) { 315 const idxKey = indexFile.concat(sheet || ''); 316 317 const handleIndex = async (offset) => { 318 const sheetParam = sheet ? `&sheet=${sheet}` : ''; 319 320 const resp = await fetch(`/${indexFile}.json?limit=${pageSize}&offset=${offset}${sheetParam}`); 321 const json = await resp.json(); 322 323 const newIndex = { 324 complete: (json.limit + json.offset) === json.total, 325 offset: json.offset + pageSize, 326 promise: null, 327 data: [...window.index[idxKey].data, ...json.data], 328 }; 329 330 return newIndex; 331 }; 332 333 window.index = window.index || {}; 334 window.index[idxKey] = window.index[idxKey] || { 335 data: [], 336 offset: 0, 337 complete: false, 338 promise: null, 339 }; 340 341 if (window.index[idxKey].complete) { 342 return window.index[idxKey]; 343 } 344 345 if (window.index[idxKey].promise) { 346 return window.index[idxKey].promise; 347 } 348 349 window.index[idxKey].promise = handleIndex(window.index[idxKey].offset); 350 const newIndex = await (window.index[idxKey].promise); 351 window.index[idxKey] = newIndex; 352 353 return newIndex; 354} 355 356/** 357 * Loads everything that happens a lot later, 358 * without impacting the user experience. 359 */ 360function loadDelayed() { 361 // eslint-disable-next-line import/no-cycle 362 window.setTimeout(() => import('./delayed.js'), 3000); 363 // load anything that can be postponed to the latest here 364} 365 366async function loadPage() { 367 await loadEager(document); 368 await loadLazy(document); 369 loadDelayed(); 370} 371 372/** 373 * Convert html in text form to document element 374 * @param {string} html the html to process 375 * @returns A document element 376 */ 377export function htmlToElement(html) { 378 const div = document.createElement('div'); 379 div.innerHTML = html; 380 return div.firstElementChild; 381} 382 383/** 384 * This function finds an element with the given name specified as text in the block 385 * It then returns the sibling element _after_ it, which is the data associated with 386 * the named element in a MD/Document table. 387 * 388 * @param {HTMLElement} block The block to look in 389 * @param {string} name The name (case-insensitive) 390 * @returns The element after the element that contains the name as text 391 */ 392export function getNamedValueFromTable(block, name) { 393 // This XPath finds the div that has the name. It uses the XPath translate function to make 394 // the lookup case-insensitive. 395 return document.evaluate( 396 `//div/text()[translate(.,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz') = '${name.toLowerCase()}']/parent::div/parent::div/div[2]`, 397 block, 398 null, 399 XPathResult.ANY_TYPE, 400 null, 401 ).iterateNext(); 402} 403 404export function getSearchWidget(placeholders, initialVal, searchbox, lang = getLanguage()) { 405 const langPrefix = lang === 'en' ? '' : `/${lang}`; 406 const searchType = searchbox ? 'search' : 'text'; 407 408 const searchWidget = htmlToElement(` 409 <form method="get" class="search"> 410 <div> 411 <input name="s" class="search-text" required="true"> 412 <button class="icon search-icon" aria-label="Search"></button> 413 </div> 414 </form>`); 415 416 searchWidget.action = `${langPrefix}/search`; 417 418 const input = searchWidget.querySelector('input'); 419 input.type = searchType; 420 input.value = initialVal ?? ''; 421 input.placeholder = placeholders.searchtext; 422 input.addEventListener('input', () => { 423 input.setCustomValidity(''); 424 }); 425 input.addEventListener('invalid', () => { 426 input.setCustomValidity(placeholders.emptysearchtext); 427 }); 428 429 return searchWidget; 430} 431 432/* 433 * Returns the environment type based on the hostname. 434*/ 435export function getEnvType(hostname = window.location.hostname) { 436 const fqdnToEnvType = { 437 'sunstar-engineering.com': 'live', 438 'www.sunstar-engineering.com': 'live', 439 'main--sunstar-engineering--sunstar-engineering.aem.page': 'preview', 440 'main--sunstar-engineering--sunstar-engineering.aem.live': 'live', 441 }; 442 return fqdnToEnvType[hostname] || 'dev'; 443} 444 445export async function loadFragment(path) { 446 const resp = await fetch(`${path}.plain.html`); 447 if (resp.ok) { 448 const main = document.createElement('main'); 449 main.innerHTML = await resp.text(); 450 decorateMain(main); 451 await loadBlocks(main); 452 return main; 453 } 454 return null; 455} 456 457export async function loadScript(url, attrs = {}) { 458 const script = document.createElement('script'); 459 script.src = url; 460 // eslint-disable-next-line no-restricted-syntax 461 for (const [name, value] of Object.entries(attrs)) { 462 script.setAttribute(name, value); 463 } 464 const loadingPromise = new Promise((resolve, reject) => { 465 script.onload = resolve; 466 script.onerror = reject; 467 }); 468 document.head.append(script); 469 return loadingPromise; 470} 471 472/** 473 * Add a paging widget to the div. The paging widget looks like this: 474 * <pre><code> 475 * < 1 2 3 > 476 * </code></pre> 477 * The numbers are hyperlinks to the repective pages and the < and > 478 * buttons are links to next and previous pages. If this is the first page 479 * then the < link has the style 'disabled' and if this is the lase one 480 * the > link is disabled. 481 * @param {HTMLElement} div - The div to add the widget to 482 * @param {number} curpage - The current page number (starting at 0) 483 * @param {number} totalPages - The total number of pages 484 * @param {Document} doc - The current Document 485 * @param {Location} curLocation - THe current window.location to use 486 */ 487export function addPagingWidget( 488 div, 489 curpage, 490 totalPages, 491 doc = document, 492 curLocation = window.location, 493) { 494 const queryParams = new URLSearchParams(curLocation.search); 495 const nav = doc.createElement('ul'); 496 nav.classList.add('pagination'); 497 498 if (totalPages > 1) { 499 const lt = doc.createElement('li'); 500 lt.classList.add('page'); 501 lt.classList.add('prev'); 502 const lta = doc.createElement('a'); 503 if (curpage === 0) { 504 lt.classList.add('disabled'); 505 } else { 506 queryParams.set('pg', curpage - 1); 507 lta.href = `${curLocation.pathname}?${queryParams}`; 508 } 509 lt.appendChild(lta); 510 nav.appendChild(lt); 511 512 for (let i = 0; i < totalPages; i += 1) { 513 const numli = doc.createElement('li'); 514 if (i === curpage) { 515 numli.classList.add('active'); 516 } 517 518 const a = doc.createElement('a'); 519 a.innerText = i + 1; 520 521 queryParams.set('pg', i); 522 a.href = `${curLocation.pathname}?${queryParams}`; 523 numli.appendChild(a); 524 525 nav.appendChild(numli); 526 } 527 528 const rt = doc.createElement('li'); 529 rt.classList.add('page'); 530 rt.classList.add('next'); 531 const rta = doc.createElement('a'); 532 if (curpage === totalPages - 1) { 533 rt.classList.add('disabled'); 534 } else { 535 queryParams.set('pg', curpage + 1); 536 rta.href = `${curLocation.pathname}?${queryParams}`; 537 } 538 539 rt.appendChild(rta); 540 nav.appendChild(rt); 541 } 542 543 div.appendChild(nav); 544} 545 546export function wrapImgsInLinks(container) { 547 const pictures = container.querySelectorAll('p picture');
548 pictures.forEach((pic) => { 549 const parent = pic.parentNode; 550 const link = parent.nextElementSibling.querySelector('a'); 551 if (link && link.href) { 552 link.parentElement.remove(); 553 link.innerHTML = pic.outerHTML; 554 parent.replaceWith(link); 555 } 556 }); 557} 558 559/** 560 * Loads the user consent manager and dispatches a `consentmanager` window event when loaded. 561 * Note: that this is currently invoked in `delayed.js` and could be moved there. 562 * @returns {Promise<void>} 563 */ 564export async function loadConsentManager() { 565 const ccmConfig = { 566 id: 'usercentrics-cmp', 567 'data-settings-id': '_2XSaYDrpo', 568 async: 'async', 569 }; 570 571 if (getEnvType() !== 'live') { 572 ccmConfig['data-version'] = 'preview'; 573 } 574 575 await Promise.all([ 576 loadScript('https://app.usercentrics.eu/browser-ui/latest/loader.js', ccmConfig), 577 loadScript('https://privacy-proxy.usercentrics.eu/latest/uc-block.bundle.js'), 578 ]); 579 window.dispatchEvent(new CustomEvent('consentmanager')); 580} 581 582loadPage();
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.