PageSourceSearch

https://www.visitasilomar.com/scripts/scripts.js

js visitasilomar.com collected 2026-09-24 10:18:57 UTC 20,812 bytes, 604 lines download raw bytes

1import {
2  loadHeader,
3  loadFooter,
4  decorateButtons,
5  decorateSections,
6  decorateBlocks,
7  decorateTemplateAndTheme,
8  waitForFirstImage,
9  loadSection,
10  loadSections,
11  loadCSS,
12  getMetadata,
13} from './aem.js';
14import { enableModalTriggers } from '../blocks/modal/modal.js';
15import { getConfigValue } from './baici/utils/config.js';
16import { injectSvg, resolveIconUrl } from './baici/utils/utils.js';
17import { ANALYTICS_EVENTS } from './constants.js';
18import { decorateExternalLinks } from './link-utils.js';
19import {
20  attachDefaultContentLinkAnalytics,
21  attachProductPageLeaveTracking,
22  pushAnalyticsEvent,
23  pushProductViewAnalytics,
24} from './analytics.js';
25import { configureUePreview } from './ue-preview.js';
26
27/**
28 * Decorates icon spans using font-based rendering for Phosphor icons (ph-*)
29 * and inline SVG injection for all other icons (mb-*, custom).
30 * Replaces the default aem.js decorateIcons (which injects <img> tags)
31 * so icons are styleable via CSS fill/stroke/currentColor.
32 * Fire-and-forget: called without await from decorateMain so icons
33 * fill in progressively without blocking LCP.
34 * @param {Element} element
35 */
36async function decorateIcons(element) {
37  const spans = [...element.querySelectorAll('span.icon')];
38  await Promise.all(spans.map((span) => {
39    const cls = [...span.classList].find((c) => c.startsWith('icon-'));
40    const iconName = cls ? cls.substring(5) : null;
41    if (!iconName) return Promise.resolve();
42    if (iconName === 'hr') {
43      // Brand divider — replace with .brand-divider span
44      const divider = document.createElement('span');
45      divider.className = 'brand-divider';
46      divider.setAttribute('aria-hidden', 'true');
47      span.replaceWith(divider);
48      return Promise.resolve();
49    }
50    const url = resolveIconUrl(iconName);
51    if (!url) {
52      // Phosphor icon — render via font
53      span.classList.add('ph', `ph-${iconName.replace(/^ph-/, '')}`);
54      span.setAttribute('aria-hidden', 'true');
55      return Promise.resolve();
56    }
57    return injectSvg(url, span, { normalizeColors: true });
58  }));
59}
60
61/**
62 * Replaces literal `:hr:` text patterns with `.brand-divider` spans.
63 * Handles UE RTE content where the icon syntax is not pre-processed.
64 * @param {Element} element
65 */
66function decorateDividers(element) {
67  const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
68  const toReplace = [];
69  let node = walker.nextNode();
70  while (node) {
71    if (node.nodeValue.includes(':hr:')) toReplace.push(node);
72    node = walker.nextNode();
73  }
74  toReplace.forEach((textNode) => {
75    const parts = textNode.nodeValue.split(':hr:');
76    const frag = document.createDocumentFragment();
77    parts.forEach((part, i) => {
78      if (part) frag.appendChild(document.createTextNode(part));
79      if (i < parts.length - 1) {
80        const divider = document.createElement('span');
81        divider.className = 'brand-divider';
82        divider.setAttribute('aria-hidden', 'true');
83        frag.appendChild(divider);
84      }
85    });
86    textNode.replaceWith(frag);
87  });
88}
89
90/**
91 * Moves all the attributes from a given elmenet to another given element.
92 * @param {Element} from the element to copy attributes from
93 * @param {Element} to the element to copy attributes to
94 */
95export function moveAttributes(from, to, attributes) {
96  if (!attributes) {
97    // eslint-disable-next-line no-param-reassign
98    attributes = [...from.attributes].map(({ nodeName }) => nodeName);
99  }
100  attributes.forEach((attr) => {
101    const value = from.getAttribute(attr);
102    if (value) {
103      to?.setAttribute(attr, value);
104      from.removeAttribute(attr);
105    }
106  });
107}
108
109/**
110 * Move instrumentation attributes from a given element to another given element.
111 * @param {Element} from the element to copy attributes from
112 * @param {Element} to the element to copy attributes to
113 */
114export function moveInstrumentation(from, to) {
115  moveAttributes(
116    from,
117    to,
118    [...from.attributes]
119      .map(({ nodeName }) => nodeName)
120      .filter((attr) => attr.startsWith('data-aue-') || attr.startsWith('data-richtext-')),
121  );
122}
123
124/**
125 * Reads the first variant class from a block and sets block.dataset.variant.
126 * @param {Element} block
127 * @returns {string|undefined} The variant class, if any
128 */
129export function readVariant(block) {
130  const blockName = block.dataset.blockName || block.classList[0];
131  const variant = [...block.classList].find(
132    (c) => c !== 'block' && c !== blockName,
133  );
134  if (variant) block.dataset.variant = variant;
135  return variant;
136}
137
138/**
139 * load fonts.css and set a session storage flag
140 */
141async function loadFonts() {
142  await loadCSS(`${window.hlx.codeBasePath}/dist/styles/fonts.css`);
143  try {
144    if (!window.location.hostname.includes('localhost')) sessionStorage.setItem('fonts-loaded', 'true');
145  } catch (e) {
146    // do nothing
147  }
148}
149
150function a11yLinks(main) {
151  const links = main.querySelectorAll('a');
152  links.forEach((link) => {
153    let label = link.textContent;
154    if (!label && link.querySelector('span.icon')) {
155      const icon = link.querySelector('span.icon');
156      label = icon ? icon.classList[1]?.split('-')[1] : label;
157    }
158    link.setAttribute('aria-label', label);
159  });
160}
161
162/**
163 * Assembles navigation sections in a nav fragment into a nested hierarchy.
164 *
165 * Authoring: all sections are flat siblings on the nav fragment page, each
166 * identified by a `type` value in section-metadata:
167 *
168 *   navigation                          — L1 top-bar button (Plan, Stay, Compare)
169 *   navigation-group-section            — L2 mega-menu panel (one per L1)
170 *   navigation-group-section-item       — L3 column inside the panel (title + link)
171 *   navigation-group-section-item-group — L4 link group, contains navigation-group block
172 *
173 * Assembly (3 passes, bottom-up, using live HTMLCollection):
174 *   L4 → into preceding L3
175 *   L3 → into preceding L2
176 *   L2 → into preceding L1
177 *
178 * Must run BEFORE decorateBlocks so blocks inside nested sections are still
179 * reachable by the `div.section > div > div` selector.
180 *
181 * Skipped inside Universal Editor — authors see sections stacked normally.
182 * @param {Element} main
183 */
184function buildNavSections(main) {
185  if (main.querySelector(':scope > .section[data-aue-resource]')) return;
186
187  // Nest consecutive child-type siblings under the nearest preceding parent-type sibling.
188  // Uses live main.children so DOM mutations are reflected immediately.
189  const nestConsecutive = (parentType, childType) => {
190    let i = 0;
191    while (i < main.children.length) {
192      const el = main.children[i];
193      if (el.classList.contains('section') && el.dataset.type === parentType) {
194        while (
195          i + 1 < main.children.length
196          && main.children[i + 1].classList.contains('section')
197          && main.children[i + 1].dataset.type === childType
198        ) {
199          el.append(main.children[i + 1]);
200        }
201      }
202      i += 1;
203    }
204  };
205
206  // Bottom-up: nest L4 into L3, then L3 into L2, then L2 into L1
207  nestConsecutive('navigation-group-section-item', 'navigation-group-section-item-group');
208  nestConsecutive('navigation-group-section', 'navigation-group-section-item');
209  nestConsecutive('navigation', 'navigation-group-section');
210}
211
212/**
213 * Groups consecutive Tab sections (sections with data-tab-label set by the
214 * tab-section model) into a tabs block via auto-blocking. If a tabs-wrapper
215 * section (data-tabs-wrapper) immediately precedes the run, the tabs block is
216 * appended inside it so header content and tabs share the same background.
217 * Otherwise a new wrapper section is created.
218 *
219 * Runs after decorateSections (which extracts section metadata into dataset)
220 * and before decorateBlocks (which decorates the newly-built tabs block).
221 * @param {Element} main
222 */
223function buildAutoBlocks(main) {
224  // Skip in Universal Editor: tab-sections must stay as direct children of
225  // main with their data-aue-resource attrs intact so UE's content tree can
226  // find them. applyTabSectionsPreview() handles the visual grouping instead.
227  // Detect UE via data-aue-resource, which AEM sets before any JS runs.
228  if (main.querySelector(':scope > .section[data-aue-resource]')) return;
229
230  const allSections = [...main.querySelectorAll('.section')];
231
232  // Collect runs of consecutive tab-sections
233  const run = [];
234  const flush = () => {
235    if (run.length < 1) return;
236    const tabSections = run.splice(0);
237
238    // Build the tabs block: each row = [title cell | content cell]
239    const tabsBlock = document.createElement('div');
240    tabsBlock.className = 'tabs';
241
242    tabSections.forEach((section) => {
243      const row = document.createElement('div');
244
245      const titleCell = document.createElement('div');
246      titleCell.textContent = section.dataset.tabLabel;
247      row.appendChild(titleCell);
248
249      const contentCell = document.createElement('div');
250      // Move all block wrappers from the section into the content cell
251      [...section.querySelectorAll(':scope > div')].forEach((child) => {
252        contentCell.appendChild(child);
253      });
254      row.appendChild(contentCell);
255
256      tabsBlock.appendChild(row);
257    });
258
259    const blockWrapper = document.createElement('div');
260    blockWrapper.appendChild(tabsBlock);
261
262    // Check if the section immediately before the tab-section run is a
263    // tabs-wrapper (explicit marker) or any non-tab section that can serve
264    // as a wrapper (convention: the preceding section becomes the container
265    // so header content and tabs share the same background).
266    const firstTabSection = tabSections[0];
267    const prevSection = firstTabSection.previousElementSibling;
268    const isWrapper = prevSection?.classList.contains('section')
269      && !prevSection.dataset.tabLabel;
270
271    if (isWrapper) {
272      // Append tabs block into the existing wrapper section
273      prevSection.appendChild(blockWrapper);
274      tabSections.forEach((s) => s.remove());
275    } else {
276      // No wrapper — create a standalone section (legacy / backward compat)
277      const newSection = document.createElement('div');
278      newSection.className = 'section';
279      newSection.dataset.sectionStatus = 'initialized';
280      newSection.style.display = 'none';
281
282      newSection.appendChild(blockWrapper);
283
284      firstTabSection.insertAdjacentElement('beforebegin', newSection);
285      tabSections.forEach((s) => s.remove());
286    }
287  };
288
289  allSections.forEach((section) => {
290    if (section.dataset.tabLabel) {
291      run.push(section);
292    } else {
293      flush();
294    }
295  });
296  flush(); // handle tab sections at the end of the page
297}
298
299/**
300 * Maps column width values to flex-grow integers (12-unit grid).
301 * Applied to individual `.layout-column` sections via the `columnWidth` UE field.
302 * "1/3" + "2/3" produces 4fr + 8fr → correct 1:2 ratio.
303 */
304/**
305 * Assembles authored Layout Columns groups into flex row containers.
306 *
307 * Authoring model (all sections, resource type = section/v1/section):
308 *   .layout-columns — config section (first in group); carries columnCount + shared styling
309 *   .layout-column  — individual column panel; follows the config section
310 *
311 * On delivery each group is wrapped:
312 *   <div class="layout-columns-row layout-columns-row-N-cols">
313 *     <section.layout-columns>…</section>   ← config / first column
314 *     <section.layout-column>…</section>    ← column 2
315 *     …
316 *   </div>
317 *
318 * Skipped inside the Universal Editor — authors see sections stacked normally.
319 * @param {Element} main
320 */
321export function assembleLayoutColumns(main) {
322  // Skip in Universal Editor (sections have data-aue-resource instrumentation)
323  if (main.querySelector(':scope > .section[data-aue-resource]')) return;
324
325  const children = [...main.children];
326  let i = 0;
327  while (i < children.length) {
328    const child = children[i];
329    if (child.classList.contains('section') && child.classList.contains('layout-columns')) {
330      // Collect consecutive layout-column sections following the wrapper
331      const columns = [];
332      let j = i + 1;
333      while (
334        j < children.length
335        && children[j].classList.contains('section')
336        && children[j].classList.contains('layout-column')
337      ) {
338        columns.push(children[j]);
339        j += 1;
340      }
341
342      if (columns.length) {
343        // Create the row wrapper for the columns only
344        const row = document.createElement('div');
345        row.className = `layout-columns-row layout-columns-row-${columns.length}-cols`;
346
347        // Propagate explicit column count from config section if set
348        const { columncount } = child.dataset;
349        if (columncount) row.style.setProperty('--lc-column-count', columncount);
350
351        // Move column sections into the row
352        columns.forEach((section) => {
353          row.append(section);
354        });
355
356        // Append the row to the wrapper section so wrapper content (titles, etc.)
357        // appears above the columns — mirrors the tabs-wrapper / tab-section pattern.
358        child.append(row);
359      }
360
361      i = j;
362    } else {
363      i += 1;
364    }
365  }
366}
367
368/**
369 * Decorates the main element.
370 * @param {Element} main The main element
371 */
372// eslint-disable-next-line import/prefer-default-export
373export function decorateMain(main) {
374  // hopefully forward compatible button decoration
375  decorateButtons(main);
376  decorateDividers(main);
377  decorateIcons(main);
378  decorateExternalLinks(main);
379  decorateSections(main);
380  buildAutoBlocks(main);
381  decorateBlocks(main);
382  attachDefaultContentLinkAnalytics(main);
383  assembleLayoutColumns(main);
384  // Nest nav sections AFTER decorateBlocks so that blocks inside nested
385  // sections are already decorated (div.section > div > div selector won't
386  // reach blocks once sections are nested 3 levels deep).
387  buildNavSections(main);
388  enableModalTriggers(main);
389  a11yLinks(main);
390}
391
392/**
393 * Loads everything needed to get to LCP.
394 * @param {Element} doc The container element
395 */
396async function loadEager(doc) {
397  document.documentElement.lang = 'en';
398  decorateTemplateAndTheme();
399
400  // Add homepage class when at the site root — enables the transparent-over-hero
401  // header (desktop only) defined in header.scss under .homepage.
402  // Falls back to URL detection when page metadata does not carry template:homepage.
403  if (window.location.pathname === '/' && !document.body.classList.contains('homepage')) {
404    document.body.classList.add('homepage');
405  }
406
407  // Kick off the header fragment fetch as early as possible — before any awaits —
408  // so the network request is in-flight while brand tokens and the header JS
409  // module are loading. header.js reuses this Promise instead of starting its own.
410  const headerMeta = getMetadata('header');
411  const headerPath = headerMeta ? new URL(headerMeta, window.location).pathname : '/fragments/header';
412  window.hlx.headerFragmentFetch = fetch(`${headerPath}.plain.html`)
413    .then((r) => (r.ok ? r.text() : null))
414    .catch(() => null);
415
416  // Load brand-specific design tokens and JS override registry before first paint
417  const { getCurrentBrand, setBrandOverrides, setBrandCssOverrides } = await import('./site-resolver.js');
418  const brand = getCurrentBrand();
419  if (brand) {
420    await loadCSS(`${window.hlx.codeBasePath}/brands/${brand}/tokens.css`);
421    try {
422      const { default: overrides } = await import(
423        `${window.hlx.codeBasePath}/brands/${brand}/overrides.js`
424      );
425      // overrides may be an array (legacy) or { js: [], css: [] }
426      if (Array.isArray(overrides)) {
427        setBrandOverrides(overrides);
428        setBrandCssOverrides([]);
429      } else {
430        setBrandOverrides(overrides.js || []);
431        setBrandCssOverrides(overrides.css || []);
432      }
433    } catch {
434      setBrandOverrides([]);
435      setBrandCssOverrides([]);
436    }
437  }
438
439  if (getMetadata('breadcrumbs').toLowerCase() === 'true') {
440    doc.body.dataset.breadcrumbs = true;
441  }
442
443  const main = doc.querySelector('main');
444
445  // Start header load immediately — don't gate on main content
446  const headerLoaded = loadHeader(doc.querySelector('header'));
447
448  if (main) {
449    decorateMain(main);
450
451    // Load section block CSS (sections are not auto-loaded as blocks)
452    loadCSS(`${window.hlx.codeBasePath}/blocks/section/section.css`).catch(() => { });
453    loadCSS(`${window.hlx.codeBasePath}/blocks/layout-columns/layout-columns.css`).catch(() => { });
454
455    // Decorate all sections with section.js (overlays, gradients, themes)
456    const { decorate: decorateSection } = await import('../blocks/section/section.js');
457    main.querySelectorAll('.section').forEach((section) => decorateSection(section));
458
459    document.body.classList.add('appear');
460    await Promise.all([
461      loadSection(main.querySelector('.section'), waitForFirstImage),
462      headerLoaded,
463    ]);
464  } else {
465    await headerLoaded;
466  }
467
468  try {
469    /* if desktop (proxy for fast connection) or fonts already loaded, load fonts.css */
470    if (window.innerWidth >= 900 || sessionStorage.getItem('fonts-loaded')) {
471      loadFonts();
472    }
473  } catch (e) {
474    // do nothing
475  }
476}
477
478/**
479 * Loads everything that doesn't need to be delayed.
480 * @param {Element} doc The container element
481 */
482/**
483 * Loads a banner fragment driven by <meta name="banner" content="/path/to/fragment">.
484 * Inserts a <div id="banner"> before the <header> element.
485 * No-ops if the metadata value is absent.
486 * @param {Document} doc
487 */
488async function loadBanner(doc) {
489  const bannerMeta = getMetadata('banner');
490  if (!bannerMeta) return;
491  const bannerPath = new URL(bannerMeta, window.location).pathname;
492  const html = await fetch(`${bannerPath}.plain.html`)
493    .then((r) => (r.ok ? r.text() : null))
494    .catch(() => null);
495  if (!html) return;
496  const fragMain = document.createElement('main');
497  fragMain.innerHTML = html;
498  decorateMain(fragMain);
499  await loadSections(fragMain);
500  const bannerEl = document.createElement('div');
501  bannerEl.id = 'banner';
502  while (fragMain.firstElementChild) bannerEl.append(fragMain.firstElementChild);
503  doc.querySelector('header').insertAdjacentElement('beforebegin', bannerEl);
504}
505
506async function loadLazy(doc) {
507  const main = doc.querySelector('main');
508  await loadSections(main);
509
510  const { hash } = window.location;
511  const element = hash ? doc.getElementById(hash.substring(1)) : false;
512  if (hash && element) element.scrollIntoView();
513
514  loadBanner(doc);
515  loadFooter(doc.querySelector('footer'));
516
517  loadFonts();
518}
519
520/**
521 * Resolve Adobe Launch script URL from `/config.json` key `launch_url`.
522 * @returns {Promise<string>}
523 */
524async function fetchLaunchScriptUrl() {
525  let launchUrl = '';
526  const configured = await getConfigValue('launch_url');
527  if (configured) {
528    launchUrl = configured;
529  }
530  return launchUrl;
531}
532
533/**
534 * Loads Adobe Launch asynchronously after resolving embed URL (supports API-driven URL later).
535 * `onLoaded` runs after script network load (or immediately if URL resolve/inject fails).
536 * @param {() => void} [onLoaded]
537 * @returns {Promise<void>}
538 */
539async function loadLaunchScript(onLoaded) {
540  let launchSrc;
541  try {
542    launchSrc = await fetchLaunchScriptUrl();
543  } catch {
544    if (typeof onLoaded === 'function') onLoaded();
545    return;
546  }
547  if (!launchSrc) {
548    if (typeof onLoaded === 'function') onLoaded();
549    return;
550  }
551
552  const launchScript = document.createElement('script');
553  launchScript.src = launchSrc;
554  if (typeof onLoaded === 'function') {
555    launchScript.addEventListener('load', onLoaded);
556    launchScript.addEventListener('error', onLoaded);
557  }
558  document.body.appendChild(launchScript);
559}
560
561/**
562 * Loads everything that happens a lot later,
563 * without impacting the user experience.
564 */
565function loadDelayed() {
566  // eslint-disable-next-line import/no-cycle
567  window.setTimeout(() => import('./delayed.js'), 3000);
568}
569
570function pageLoadAnalytics() {
571  loadLaunchScript(() => {
572    pushAnalyticsEvent(ANALYTICS_EVENTS.PAGE_VIEW.name);
573
574    const productName = getMetadata('product_name').trim();
575    if (productName) {
576      pushProductViewAnalytics({
577        productName,
578        productCategory: getMetadata('product_category').trim(),
579      });
580      attachProductPageLeaveTracking(productName);
581    }
582  }).catch(() => { });
583}
584
585function initPageLoadAnalytics() {
586  // head.html enables Speculation Rules prerendering (hover/focus), which loads
587  // the page in a hidden browsing context and fires `load` there too. Defer to
588  // `prerenderingchange`, which only fires once the user actually navigates in.
589  if (document.prerendering) {
590    document.addEventListener('prerenderingchange', () => pageLoadAnalytics(), { once: true });
591  } else {
592    pageLoadAnalytics();
593  }
594}
595
596async function loadPage() {
597  await loadEager(document);
598  await loadLazy(document);
599  loadDelayed();
600  initPageLoadAnalytics();
601}
602
603configureUePreview().catch(() => { });
604loadPage();

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.