PageSourceSearch

https://www.sunstar-engineering.com/scripts/scripts.js

js sunstar-engineering.com collected 2026-10-04 05:17:56 UTC 16,803 bytes, 582 lines download raw bytes

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 * &lt; 1 2 3 &gt;
476 * </code></pre>
477 * The numbers are hyperlinks to the repective pages and the &lt; and &gt;
478 * buttons are links to next and previous pages. If this is the first page
479 * then the &lt; link has the style 'disabled' and if this is the lase one
480 * the &gt; 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.