PageSourceSearch

https://www.bancomundial.org/ext/scripts/scripts.js

js bancomundial.org collected 2026-09-24 08:27:11 UTC 36,915 bytes, 1,096 lines download raw bytes

1import {
2  sampleRUM,
3  loadHeader,
4  loadFooter,
5  decorateButtons,
6  decorateIcons,
7  decorateSections,
8  decorateGrid,
9  decorateBlocks,
10  decorateTemplateAndTheme,
11  waitForLCP,
12  loadBlocks,
13  loadCSS,
14  fetchPlaceholders,
15  loadBlock,
16  getMetadata,
17  waitForFirstImage,
18  loadSection,
19  loadScript
20} from './aem.js';
21//import { insertSchema } from './schema.js';
22//import assetsInit from './aem-assets-plugin-support.js';
23
24import {
25  getLanguage,
26  createSource,
27  setPageLanguage,
28  PATH_PREFIX,
29  isInternalPage,
30  scriptEnabled,
31  isHomePage,
32  loadAdobeAnalytics,
33} from './utils-core.js';
34
35
36//const LCP_BLOCKS = ['bio-detail']; // add your LCP blocks to the list
37export const CLASS_MAIN_HEADING = 'main-heading';
38export const LANGUAGE_ROOT = `/ext/${getLanguage()}`;
39export const CORPORATE_LANGUAGES = ['en', 'es', 'fr', 'ar', 'zh', 'ru'];
40
41/**
42 * Moves all the attributes from a given elmenet to another given element.
43 * @param {Element} from the element to copy attributes from
44 * @param {Element} to the element to copy attributes to
45 */
46export function moveAttributes(from, to, attributes) {
47  if (!attributes) {
48    // eslint-disable-next-line no-param-reassign
49    attributes = [...from.attributes].map(({ nodeName }) => nodeName);
50  }
51  attributes.forEach((attr) => {
52    const value = from.getAttribute(attr);
53    if (value) {
54      to.setAttribute(attr, value);
55      from.removeAttribute(attr);
56    }
57  });
58}
59
60/**
61 * Move instrumentation attributes from a given element to another given element.
62 * @param {Element} from the element to copy attributes from
63 * @param {Element} to the element to copy attributes to
64 */
65export function moveInstrumentation(from, to) {
66  moveAttributes(
67    from,
68    to,
69    [...from.attributes]
70      .map(({ nodeName }) => nodeName)
71      .filter((attr) => attr.startsWith('data-aue-') || attr.startsWith('data-richtext-')),
72  );
73}
74
75/**
76 * load fonts.css and set a session storage flag
77 */
78async function loadFonts() {
79  await loadCSS(`${window.hlx.codeBasePath}/styles/fonts.css`);
80  try {
81    if (!window.location.hostname.includes('localhost')) sessionStorage.setItem('fonts-loaded', 'true');
82  } catch (e) {
83    // do nothing
84  }
85}
86
87/**
88 * Decorates Dynamic Media images by modifying their URLs to include specific parameters
89 * and creating a <picture> element with different sources for different image formats and sizes.
90 *
91 * @param {HTMLElement} main - The main container element that includes the links to be processed.
92 */
93export async function decorateDMImages(main) {
94  const { picture, img } = await import('./dom-helpers.js');
95  main.querySelectorAll('a[href^="https://delivery-p"]').forEach((a) => {
96    const url = new URL(a.href);
97    const isVideo = /\.(mp4|webm|ogg)$/i.test(url.pathname);
98    if (isVideo) return;
99    if (url.hostname.endsWith('.adobeaemcloud.com')) {
100      const pictureEl = picture(
101        /*source({ srcset: `${url.href}?width=1400&quality=85&preferwebp=true`, type: 'image/webp', media: '(min-width: 992px)' }),
102        source({ srcset: `${url.href}?width=1320&quality=85&preferwebp=true`, type: 'image/webp', media: '(min-width: 768px)' }),
103        source({ srcset: `${url.href}?width=780&quality=85&preferwebp=true`, type: 'image/webp', media: '(min-width: 320px)' }),
104        source({ srcset: `${url.href}?width=1400&quality=85`, media: '(min-width: 992px)' }),
105        source({ srcset: `${url.href}?width=1320&quality=85`, media: '(min-width: 768px)' }),
106        source({ srcset: `${url.href}?width=780&quality=85`, media: '(min-width: 320px)' }),*/
107        img({ src: `${url.href}`}),
108      );
109      a.replaceWith(pictureEl); 
110    }
111  });
112}
113
114/**
115 * remove the adujusts the auto images
116 * @param {Element} main The container element
117 */
118function adjustAutoImages(main) {
119  const pictureElement = main.querySelector('div > p > picture');
120  if (pictureElement) {
121    const pElement = pictureElement.parentElement;
122    pElement.className = 'auto-image-container';
123  }
124}
125
126/**
127 * Return the placeholder file specific to language
128 * @returns
129 */
130export async function fetchLanguagePlaceholders() {
131  const langCode = getLanguage();
132  try {
133    // Try fetching placeholders with the specified language
134    return await fetchPlaceholders(`${PATH_PREFIX}/${langCode}${!['en', 'es', 'fr', 'ar', 'zh', 'ru'].includes(langCode) ? '/data' : ''}`);
135  } catch (error) {
136    // eslint-disable-next-line no-console
137    console.error(`Error fetching placeholders for lang: ${langCode}. Will try to get en placeholders`, error);
138    // Retry without specifying a language (using the default language)
139    try {
140      return await fetchPlaceholders(`${PATH_PREFIX}/en`);
141    } catch (err) {
142      // eslint-disable-next-line no-console
143      console.error('Error fetching placeholders:', err);
144    }
145  }
146  return {}; // default to empty object
147}
148
149/**
150 * Builds all synthetic blocks in a container element.
151 * @param {Element} main The container element
152 */
153// eslint-disable-next-line no-unused-vars
154function buildAutoBlocks(main) {
155  try {
156    adjustAutoImages(main);
157  } catch (error) {
158    // eslint-disable-next-line no-console
159    console.error('Auto Blocking failed', error);
160  }
161}
162
163/**
164 * Decorates the main element.
165 * @param {Element} main The main element
166 */
167// eslint-disable-next-line import/prefer-default-export
168export function decorateMain(main) {
169  //if (window.hlx?.aemassets?.decorateExternalImages) {
170   // window.hlx.aemassets.decorateExternalImages(main);
171  //}
172  // hopefully forward compatible button decoration
173  decorateButtons(main);
174  decorateIcons(main);
175  buildAutoBlocks(main);
176  decorateSections(main);
177  decorateBlocks(main);
178
179  // Run tabs and wbgcolumns BEFORE decorateGrid so grid's appended section-rows
180  // is not captured as tab content by the last tab's sibling collection
181  main.querySelectorAll('.section').forEach((section) => {
182    decorateWbgTabs(section);
183    decoratePopups(section);
184    // decorateGrid owns the column layout for grid-section-container sections
185    if (!section.classList.contains('grid-section-container')) {
186      decorateWbgColumns(section);
187    }
188  });
189  
190  decorateGrid(main);
191 // decorateDMImages(main);
192}
193
194/**
195 * Loads everything needed to get to LCP.
196 * @param {Element} doc The container element
197 */
198
199async function createSkipToMainNavigationBtn() {
200  const placeholder = await fetchLanguagePlaceholders();
201  const main = document.querySelector('main');
202  main.id = 'main';
203
204  const anchor = document.createElement('a');
205  anchor.id = 'skip-to-main-content';
206  anchor.className = 'visually-hidden focusable';
207  anchor.href = '#main';
208  anchor.textContent = placeholder.skipToMainContent || 'Skip to Main Content';
209  document.body.insertBefore(anchor, document.body.firstChild);
210}
211
212/**
213 * Translate 404 page
214 * @returns
215 */
216export async function load404() {
217  const main = document.querySelector('main');
218  const placeholders = await fetchLanguagePlaceholders();
219  const homelink = main.querySelector('p a');
220  const searchForm = main.querySelector('form');
221  main.querySelector('h1').innerText = placeholders.notFoundHeading || 'Page Not Found';
222  main.querySelector('h2').innerText = placeholders.notFoundSubHeading || '404 Error';
223  main.querySelector('h2 + p').innerText = placeholders.notFoundText || 'The page you requested could not be found. Try using the search box below or click on the homepage button to go there.';
224  homelink.innerText = placeholders.notFoundBtnLabel || 'Go to homepage';
225  homelink.title = placeholders.notFoundBtnLabel || 'Go to homepage';
226  homelink.href = placeholders.logoUrl || '/';
227  searchForm.action = placeholders.searchRedirectUrl || 'https://www.worldbank.org/en/search';
228  searchForm.querySelector('input').placeholder = placeholders.searchVariable || 'Search worldbank.org';
229  main.classList.remove('loading');
230  return null;
231}
232function getCookie(name) {
233  const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
234  return match ? decodeURIComponent(match[1]) : null;
235}
236
237function shouldLoadTarget() {
238  
239  const consent = getCookie('OptanonConsent');
240  // Cookie not present → no consent decision yet → load at.js
241  if (!consent) return true;
242
243  const groups = new URLSearchParams(consent).get('groups');
244
245  // Cookie exists but no groups info → treat as no decision → load
246  if (!groups) return true;
247
248  // Cookie exists with groups → load only if C0004 is 1
249  return groups.split(',').some((g) => {
250    const [id, val] = g.split(':');
251    return id.trim() === 'C0004' && val === '1';
252  });
253}
254
255async function loadEager(doc) {
256  setPageLanguage();
257  decorateTemplateAndTheme();
258  addClassesToBody();
259  const main = doc.querySelector('main');
260  if (main) {
261    decorateMain(main);
262    document.body.classList.add('appear');
263    await loadSection(main.querySelector('.section'));
264  }
265  try {
266    /* if desktop (proxy for fast connection) or fonts already loaded, load fonts.css */
267    if (window.innerWidth >= 900 || sessionStorage.getItem('fonts-loaded')) {
268      loadFonts();
269    }
270  } catch (e) {
271    // do nothing
272  }
273  // Only load Adobe Target if targeting cookies (C0004) are consented
274  if (shouldLoadTarget() && isHomePage()) {
275   /* await loadScript('/ext/scripts/third-party/VisitorAPI.js');
276    var visitor = Visitor.getInstance("1E7B833554B8360D0A4C98A5@AdobeOrg", {
277      trackingServer: "metrics.worldbank.org", // same as s.trackingServer
278      trackingServerSecure: "smetrics.worldbank.org", // same as s.trackingServerSecure
279
280      // To enable CNAME support, add the following configuration variables
281      // If you are not using CNAME, DO NOT include these variables
282      marketingCloudServer: "metrics.worldbank.org",
283      marketingCloudServerSecure: "smetrics.worldbank.org" // same as s.trackingServerSecure
284    });
285
286    await loadScript('/ext/scripts/third-party/at.js');*/
287    //await loadScript('/ext/scripts/third-party/AppMeasurement.js');
288    await loadScript('/ext/scripts/third-party/target-hero-sequence.js');
289  }
290  if (isHomePage()) {
291    loadAdobeAnalytics();
292  }
293}
294
295/**
296 * Return the json for any placeholder file specific to language using filename as argument
297 * @returns
298 */
299export const fetchLangDatabyFileName = async (fileName) => {
300  const langCode = getLanguage();
301  try {
302    const response = await fetch(`${PATH_PREFIX}/${langCode}/${fileName}.json`);
303    if (!response.ok) {
304      throw new Error('Failed to load data');
305    }
306    const json = await response.json();
307    return json.data || [];
308  } catch (error) {
309    return [];
310  }
311};
312
313/**
314 * Create section background image
315 *
316 * @param {*} doc
317 */
318function decorateSectionImages(doc) {
319  const sectionImgContainers = doc.querySelectorAll('main .section[data-image]');
320  sectionImgContainers.forEach((sectionImgContainer) => {
321    const sectionImg = sectionImgContainer.dataset.image;
322    const sectionTabImg = sectionImgContainer.dataset.tabImage;
323    const sectionMobImg = sectionImgContainer.dataset.mobImage;
324    let defaultImgUrl = null;
325
326    const newPic = document.createElement('picture');
327    if (sectionImg) {
328      newPic.appendChild(createSource(sectionImg, 1920, '(min-width: 1024px)'));
329      defaultImgUrl = sectionImg;
330    }
331
332    if (sectionTabImg) {
333      newPic.appendChild(createSource(sectionTabImg, 1024, '(min-width: 768px)'));
334      defaultImgUrl = sectionTabImg;
335    }
336
337    if (sectionMobImg) {
338      newPic.appendChild(createSource(sectionTabImg, 600, '(max-width: 767px)'));
339      defaultImgUrl = sectionMobImg;
340    }
341
342    const newImg = document.createElement('img');
343    newImg.src = defaultImgUrl;
344    newImg.alt = '';
345    newImg.className = 'sec-img';
346    newImg.loading = 'lazy';
347    newImg.width = '768';
348    newImg.height = '100%';
349
350    if (defaultImgUrl) {
351      newPic.appendChild(newImg);
352      sectionImgContainer.prepend(newPic);
353    }
354  });
355}
356
357/**
358 * Loads everything that doesn't need to be delayed.
359 * @param {Element} doc The container element
360 */
361async function loadLazy(doc) {
362  const main = doc.querySelector('main');
363  //lazy load style css for blocks
364  loadCSS(`${window.hlx.codeBasePath}/styles/styles.css`);
365  decorateDMImages(main);
366  await loadBlocks(main);
367  const { initSwiper, initAllDownloadFiles,initBGImage, buildTwitterQuotes } = await import('./utils.js');
368  const { initAllCarousels } = await import('./carousel-init.js');
369   initAllCarousels();
370   initAllDownloadFiles();
371   initSwiper();
372   initBGImage();
373   buildTwitterQuotes();
374
375  const { hash } = window.location;
376  const element = hash ? doc.getElementById(hash.substring(1)) : false;
377  if (hash && element) element.scrollIntoView();
378  await createSkipToMainNavigationBtn();
379  //cookiePopUp();
380  //showCookieConsent();
381  loadHeader(doc.querySelector('header'));
382  const footerElements = doc.querySelectorAll('footer');
383  if (footerElements.length > 1) {
384    loadFooter(footerElements[footerElements.length - 1]);
385  } else {
386    loadFooter(doc.querySelector('footer'));
387  }
388    //load livenow block lazy
389  const liveNow = document.createElement('div');
390  liveNow.className = 'block live-now';
391  liveNow.setAttribute('data-block-name', 'live-now');
392  document.body.appendChild(liveNow);
393  loadBlock(liveNow); // This function loads the block content dynamically
394
395  
396  // loadCSS(`${window.hlx.codeBasePath}/styles/lazy-styles.css`);
397  loadFonts();
398
399  decorateSectionImages(doc);
400
401  addMobileCloneButtons();
402  if (!isInternalPage()) {
403    import('./analytics.js').then(({ renderWBDataLayer }) => renderWBDataLayer());
404    //loadAdobeAnalytics();
405    //insertSchema();
406  }
407
408  sampleRUM('lazy');
409  sampleRUM.observe(main.querySelectorAll('div[data-block-name]'));
410  sampleRUM.observe(main.querySelectorAll('picture > img'));
411}
412
413/**
414 * Loads everything that happens a lot later,
415 * without impacting the user experience.
416 */
417function loadDelayed() {
418  // eslint-disable-next-line import/no-cycle
419  window.setTimeout(() => import('./delayed.js'), 3000);
420  // load anything that can be postponed to the latest here
421  import('./sidekick.js').then(({ initSidekick }) => initSidekick());
422}
423function addMobileCloneButtons() {
424  document.querySelectorAll(".section.clone-button-bottom").forEach(section => {
425    let button = section.querySelector(".lp-heading .button");
426
427    if(!button){
428      button = section.querySelector(".heading .button");
429    }
430
431    if (button && !section.querySelector(".cloned-button")) {
432      // Add class to hide the original button on mobile & tablet
433      button.classList.add("hide-on-mobile-tablet");
434
435      // Clone the button
436      const clonedBtn = button.cloneNode(true);
437      clonedBtn.classList.add("cloned-button");
438      clonedBtn.classList.remove("hide-on-mobile-tablet");
439
440      // Append at end of section
441      if(section.classList.contains("grid")){
442        const sectionRows = section.querySelector(".section-rows");
443        if (sectionRows) sectionRows.appendChild(clonedBtn);
444      }else{
445        section.appendChild(clonedBtn);
446      }
447    }    
448  });
449}
450
451/**
452 * Fetch filtered search results
453 * @returns List of search results
454 */
455export async function fetchSearch() {
456  window.searchData = window.searchData || {};
457  if (Object.keys(window.searchData).length === 0) {
458    const path = `${LANGUAGE_ROOT === '/ext/ja' ? '/ext/en' : LANGUAGE_ROOT}/query-index.json?limit=3500&offset=0`;
459
460    const resp = await fetch(path);
461    window.searchData = JSON.parse(await resp.text()).data;
462  }
463  return window.searchData;
464}
465
466async function loadPage() {
467  const meta = document.querySelector('meta[name="transparent-header"]');
468  const isTransparentHeader = meta?.content === "yes";
469  if (isTransparentHeader) {
470    const header = document.querySelector("header");
471    if (header) {
472      header.classList.add("no-header-style");
473    }
474  }
475  const targetDiv = document.createElement('div');
476  targetDiv.id = 'wbg-aem-target';
477  targetDiv.className = 'wbg-aem-target';
478  window.wbgData ||= {};
479  await loadEager(document);
480  await loadLazy(document);
481  loadDelayed();
482  document.body.insertAdjacentElement('afterbegin', targetDiv);
483}
484//await assetsInit();
485loadPage();
486
487export async function fetchJsonData(type) {
488  const langCode = getLanguage();
489  let filePath;
490  if(type == 'i18n')
491  {
492    filePath = 'siteconfig/i18n';
493  }
494  const jsonFilePath = `${PATH_PREFIX}/${langCode}/${filePath}`;
495  window.jsonData = window.jsonData || {};
496
497  if (!window.jsonData[jsonFilePath]) {
498    window.jsonData[jsonFilePath] = fetch(`${jsonFilePath}.json`)
499      .then((resp) => (resp.ok ? resp.json() : {}))
500      .then((json) => {
501        const jsonData = {};
502        if (json?.data) {
503          json.data
504            .filter((jdata) => jdata.Key)
505            .forEach((jdata) => {
506                const normalizedKey = jdata.Key.toLowerCase().replace(/\s+/g, '-');
507                jsonData[normalizedKey] = (jdata.Value || jdata.Text || '').replace(/\s*\(.*?\)\s*/g, ''); 
508            });
509        }
510        window.jsonData[jsonFilePath] = jsonData;
511        return jsonData;
512      })
513      .catch(() => {
514        window.jsonData[jsonFilePath] = {};
515        return {};
516      });
517  }
518
519  if (!CORPORATE_LANGUAGES.includes(langCode) && !window.jsonData[`${PATH_PREFIX}/en/${filePath}`]) {
520    window.jsonData[`${PATH_PREFIX}/en/${filePath}`] = fetch(`${PATH_PREFIX}/en/${filePath}.json`)
521      .then((resp) => (resp.ok ? resp.json() : {}))
522      .then((json) => {
523        const jsonData = {};
524        if (json?.data) {
525          json.data
526            .filter((jdata) => jdata.Key)
527            .forEach((jdata) => {
528                const normalizedKey = jdata.Key.toLowerCase().replace(/\s+/g, '-');
529                jsonData[normalizedKey] = (jdata.Value || jdata.Text || '').replace(/\s*\(.*?\)\s*/g, ''); 
530            });
531        }
532        window.jsonData[`${PATH_PREFIX}/en/${filePath}`] = jsonData;
533        return jsonData;
534      })
535      .catch(() => {
536        window.jsonData[`${PATH_PREFIX}/en/${filePath}`] = {};
537        return {};
538      });
539  }
540
541  return window.jsonData[jsonFilePath];
542}
543
544export async function getTranslatedValue(tagValueArr) {
545  const jsonData = await fetchJsonData('i18n');
546  if (!tagValueArr) return []; // always return an array
547  let tagValue;
548  if (tagValueArr instanceof Element) {
549    const items = Array.from(tagValueArr.querySelectorAll('li'))
550      .map(li => li.textContent.trim());
551
552    tagValue = items.length > 1 ? items.join(', ') : (items[0] || '');
553  } else {
554    tagValue = tagValueArr;
555  }
556
557  const result = tagValue
558    .split(',')
559    .map(v => {
560      const originalText = v.trim(); 
561      let key = originalText.toLowerCase();
562
563      // Handle "world-bank:" prefix
564      if (key.startsWith('world-bank:')) {
565        const parts = key.split('/');
566        key = parts.pop().trim();
567      }
568
569      key = key.replace(/\s+/g, '-');
570      // Return the translation, or fallback to the exact original text
571      return jsonData?.[key] || originalText; 
572    });
573    
574  return result;
575}
576function wbgColumnClasses(wbgColumn, colIndex) {
577  const colClasses = [];
578  colClasses.push(`col-ctrl-${colIndex + 1}`);
579  
580  // read all 4 rows → col-xs-*, col-sm-*, col-md-*, col-lg-*
581  const authorElements = wbgColumn.querySelectorAll('.wbgcolumn-author-size');
582  if (authorElements.length > 0) {
583    authorElements.forEach((el) => {
584      const clone = el.cloneNode(true);
585      clone.querySelector('strong').remove();
586      const text = clone.textContent.trim();
587      if (text) colClasses.push(text);
588    });
589  } else {
590    const elements = wbgColumn.querySelectorAll('.wbgcolumn div > div');
591    elements.forEach(el => {
592      const liItems = el.querySelectorAll('li');
593      if (liItems.length > 0) {
594        liItems.forEach((li) => {
595          const text = li.textContent.trim();
596          if (text) colClasses.push(text);
597        });
598      } else {
599        const text = el.textContent.trim();
600        if (text) colClasses.push(text);
601      }
602    });
603  }
604
605  if (colClasses.length === 1) colClasses.push('col-xs-12');
606
607  const columnClass = [...(wbgColumn?.querySelector('.wbgcolumn')?.classList || [])]
608    .filter(cls => cls !== 'wbgcolumn' && cls !== 'block');
609  columnClass.forEach(cls => colClasses.push(cls));
610
611  return colClasses;
612}
613
614export function decorateWbgColumns(container) {
615  // skip if already decorated
616  if (container.querySelector('.section-rows')) return;
617
618  const wbgcolumns = [...container.querySelectorAll('.wbgcolumn-wrapper')];
619  if (wbgcolumns.length === 0) return;
620
621  const firstWrapper = wbgcolumns[0];
622  const parent = firstWrapper.parentNode;
623  const referenceNode = firstWrapper;
624
625  const sectionRows = document.createElement('div');
626  sectionRows.classList.add('section-rows');
627
628  wbgcolumns.forEach((wbgcolumnWrapper, colIndex) => {
629    const colDiv = document.createElement('div');
630
631  
632    const colClasses = wbgColumnClasses(wbgcolumnWrapper, colIndex);
633    colClasses.forEach((cls) => {
634      // Split the string by spaces and spread the resulting array into add()
635      if (cls) colDiv.classList.add(...cls.split(' ').filter(Boolean));
636    });
637
638    const nextWbgcolumn = wbgcolumns[colIndex + 1];
639    let sibling = wbgcolumnWrapper.nextElementSibling;
640
641    while (sibling && sibling !== nextWbgcolumn) {
642      const next = sibling.nextElementSibling;
643      colDiv.appendChild(sibling);
644      sibling = next;
645    }
646
647    sectionRows.appendChild(colDiv);
648
649    if (window.parent.location.href.includes('/canvas')) {
650      wbgcolumnWrapper.style.display = 'none';
651      colDiv.prepend(wbgcolumnWrapper);
652    } else {
653      wbgcolumnWrapper.remove();
654    }
655  });
656
657  if (referenceNode.parentNode === parent) {
658    parent.insertBefore(sectionRows, referenceNode);
659  } else {
660    parent.appendChild(sectionRows); // fallback safety
661  }
662}
663
664function decorateWbgTabsInAuthor(section) {
665  const children = Array.from(section.children);
666  let currentColGroup = null;
667  let currentSectionRow = null;
668  let colIndex = 0;
669
670  children.forEach((child) => {
671    const isWbgColumn = child.classList.contains('wbgcolumn-wrapper');
672    const isWbgTab = child.classList.contains('wbgtab-wrapper');
673    const isDivider = child.classList.contains('divider-wrapper');
674
675    if (isWbgColumn) {
676      child.style.display = 'none';
677
678      if (!currentSectionRow) {
679        currentSectionRow = document.createElement('div');
680        currentSectionRow.classList.add('section-rows');
681        
682        child.before(currentSectionRow);
683      }
684      
685      currentColGroup = document.createElement('div');
686
687      const colClasses = wbgColumnClasses(child, colIndex);
688      colClasses.forEach(cls => currentColGroup.classList.add(cls));
689
690      currentSectionRow.appendChild(currentColGroup);
691      currentColGroup.appendChild(child);
692
693      colIndex++;
694    } else if (isWbgTab || isDivider) {
695      currentColGroup = null;
696      currentSectionRow = null;
697
698    } else if (currentColGroup) {
699      currentColGroup.appendChild(child);
700    }
701  });
702}
703
704function getScrollLeftRTL(element) {
705  const isRTL = document.documentElement.dir === 'rtl';
706
707  if (!isRTL) return Math.ceil(element.scrollLeft);
708
709  const { scrollLeft } = element;
710  const maxScroll = element.scrollWidth - element.clientWidth;
711
712  return Math.abs(scrollLeft) === maxScroll ? 0 : maxScroll + scrollLeft;
713}
714
715// ---- STEP 2: tab decorator (calls column decorator inside each panel) ----
716function decorateWbgTabs(section) {
717  // If the section doesn't contain any tab wrappers, exit early.
718  const hasTabs = section.querySelector(':scope > .wbgtab-wrapper');
719  if (!hasTabs) return;
720
721  if (window.parent.location.href.includes('/canvas')) {
722    decorateWbgTabsInAuthor(section);
723    return;
724  }
725
726  const children = Array.from(section.children);
727  let currentTabsWrapper = null;
728  let currentTabsList = null;
729  let currentTabsContent = null;
730  let currentPanel = null;
731  let tabIndex = 0;
732  let isVerticalGroup = false;
733
734  // Helper to sanitize Tab IDs
735  const sanitizeId = (str) => str
736    .toLowerCase()
737    .replace(/[^a-z0-9]/g, '-') // Replace non-alphanumeric with -
738    .replace(/-+/g, '-')        // Replace multiple - with single -
739    .replace(/^-|-$/g, '');     // Trim - from start/end
740
741  const finalizeTabGroup = () => {
742    if (currentPanel) {
743      decorateWbgColumns(currentPanel);
744    }
745    // Ensure the wrapper has the correct class before moving to the next group
746    if (currentTabsWrapper) {
747      currentTabsWrapper.classList.toggle('vertical', isVerticalGroup);
748      currentTabsWrapper.classList.toggle('horizontal', !isVerticalGroup);
749    }
750
751    // Reset state for the next potential group (after a divider)
752    currentTabsWrapper = null;
753    currentTabsList = null;
754    currentTabsContent = null;
755    currentPanel = null;
756    tabIndex = 0;
757    isVerticalGroup = false;
758
759    const dir = document.documentElement.dir || 'ltr';
760    const offset = 150;
761    const tabsWrappers = section.querySelectorAll('.wbgtabs-wrapper');
762    tabsWrappers.forEach(wrapper => {
763      const tabList = wrapper.querySelector('.wbgtabs-list');
764      const rightButton = wrapper.querySelector('.right-btn');
765      const leftButton = wrapper.querySelector('.left-btn');
766
767      const iconVisibility = () => {
768      const scrollLeftValue = getScrollLeftRTL(tabList);
769      const scrollableWidth = tabList.scrollWidth - tabList.clientWidth;
770
771        if (dir === 'rtl') {
772          leftButton.style.display = scrollableWidth > 0 && scrollLeftValue < scrollableWidth ? 'block' : 'none';
773          rightButton.style.display = scrollLeftValue > 1 ? 'block' : 'none';
774        } else {
775          leftButton.style.display = scrollLeftValue > 0 ? 'block' : 'none';
776          rightButton.style.display = scrollableWidth > 0 && scrollLeftValue < scrollableWidth ? 'block' : 'none';
777        }
778      };
779
780      rightButton.addEventListener('click', () => {
781        tabList.scrollLeft += dir === 'rtl' ? -offset : offset;
782        iconVisibility();
783      });
784
785      leftButton.addEventListener('click', () => {
786        tabList.scrollLeft += dir === 'rtl' ? offset : -offset;
787        iconVisibility();
788      });
789
790      setTimeout(iconVisibility, 0);
791      const resizeObserver = new ResizeObserver(() => {
792        iconVisibility();
793      });
794      resizeObserver.observe(tabList);
795    });
796  };
797
798  children.forEach((child) => {
799    const isWbgTabsBlock = child.classList.contains('wbgtab-wrapper');
800    const isDivider = child.classList.contains('divider-wrapper');
801
802    if (isDivider) {
803      finalizeTabGroup();
804      return; // Stop processing this group and move to next elements
805    }
806
807    if (isWbgTabsBlock) {
808      // 1. Check for the vertical keyword in THIS block
809      const verticalText = child.querySelector('.wbgtab > div:nth-child(5)')?.textContent.trim().toLowerCase();
810      if (verticalText === 'vertical') {
811        isVerticalGroup = true;
812      }
813
814      // 2. Initialize the Tab Container if this is the start of a group
815      if (!currentTabsWrapper) {
816        const rightButton = document.createElement('button');
817        rightButton.className = 'right-btn';
818        rightButton.setAttribute('aria-label', 'Scroll tabs right');
819        rightButton.type = 'button';
820        rightButton.tabIndex = 0;
821        const iRight = document.createElement('i');
822        iRight.className = 'lp lp-chevron-right';
823        rightButton.appendChild(iRight);
824
825        const leftButton = document.createElement('button');
826        leftButton.className = 'left-btn';
827        leftButton.setAttribute('aria-label', 'Scroll tabs left');
828        leftButton.type = 'button';
829        leftButton.tabIndex = 0;
830        const iLeft = document.createElement('i');
831        iLeft.className = 'lp lp-chevron-left';
832        leftButton.appendChild(iLeft);
833
834        currentTabsWrapper = document.createElement('div');
835        currentTabsWrapper.className = 'wbgtabs-wrapper';
836
837        const tabsNav = document.createElement('div');
838        tabsNav.className = 'wbgtabs-nav';
839
840        currentTabsList = document.createElement('div');
841        currentTabsList.className = 'wbgtabs-list';
842        currentTabsList.setAttribute('role', 'tablist');
843
844        currentTabsContent = document.createElement('div');
845        currentTabsContent.className = 'wbgtabs-content';
846
847        tabsNav.append(leftButton, currentTabsList, rightButton);
848        currentTabsWrapper.append(tabsNav, currentTabsContent);
849
850        // Insert it into the DOM where the first tab was
851        child.before(currentTabsWrapper);
852      }
853
854      // 3. Update the wrapper class immediately (in case this is the 'vertical' trigger)
855      currentTabsWrapper.classList.toggle('vertical', isVerticalGroup);
856      currentTabsWrapper.classList.toggle('horizontal', !isVerticalGroup);
857
858      if (currentPanel) decorateWbgColumns(currentPanel);
859
860      // 4. Extract data and build the button
861      const elements = child.querySelectorAll('.wbgtab div > div');
862      const tabLabel = elements[0]?.textContent.trim() || `Tab ${tabIndex + 1}`;
863      const rawId = elements[2]?.textContent.trim() || `tab-${tabIndex + 1}`;
864      const sanitizedId = sanitizeId(rawId);
865      const tabId = `tab-${sanitizedId ? sanitizedId : tabIndex + 1}`;
866
867      const hideTab = elements[5]?.textContent.trim() === 'true';
868      if (hideTab && !window.parent.location.href.includes('/canvas')) {
869        currentPanel = document.createElement('div'); // detached — absorbs following content
870        child.remove();
871        return;
872      }
873
874      const tabBtn = document.createElement('button');
875      tabBtn.className = `wbgtab-btn ${tabIndex === 0 ? 'active' : ''}`;
876      tabBtn.textContent = tabLabel;
877      tabBtn.id = tabId;
878      tabBtn.setAttribute('role', 'tab');
879      tabBtn.setAttribute('aria-selected', tabIndex === 0);
880
881      tabBtn.addEventListener('click', () => {
882        const wrap = tabBtn.closest('.wbgtabs-wrapper');
883        wrap.querySelectorAll('.wbgtab-btn').forEach(b => {
884          const active = b === tabBtn;
885          b.classList.toggle('active', active);
886          b.setAttribute('aria-selected', active);
887        });
888        wrap.querySelectorAll('.wbgtab-panel').forEach(p => {
889          const isMatch = p.getAttribute('aria-labelledby') === tabBtn.id;
890          p.hidden = !isMatch;
891          p.setAttribute('aria-hidden', !isMatch);
892          p.classList.toggle('active', isMatch);
893        });
894
895        // Keep selected tab in the URL for deep-linking/shareability.
896        const nextHash = `#${tabId}`;
897        if (window.location.hash !== nextHash) {
898          window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}${nextHash}`);
899        }
900      });
901
902      currentTabsList.appendChild(tabBtn);
903
904      // 5. Create the Panel
905      const tabPanel = document.createElement('div');
906      tabPanel.className = 'wbgtab-panel';
907      if (tabIndex === 0) {
908        tabPanel.classList.add('active');
909      }
910      tabPanel.id = `wbgtab-panel-${tabId}`;
911      tabPanel.setAttribute('role', 'tabpanel');
912      tabPanel.setAttribute('aria-labelledby', tabBtn.id);
913      // Expose the label so editor-support's updateLabels can annotate the
914      // editor content tree and re-apply the tab text after editor re-renders.
915      tabPanel.dataset.tabLabel = tabLabel;
916
917      if (tabIndex !== 0) {
918        tabPanel.hidden = true;
919        tabPanel.setAttribute('aria-hidden', 'true');
920      }
921
922      currentTabsContent.appendChild(tabPanel);
923      currentPanel = tabPanel;
924      tabIndex++;
925      child.remove();
926
927    } else if (currentPanel) {
928      // If we aren't a tab block or a divider, but a panel is open, move the content inside
929      currentPanel.appendChild(child);
930    }
931  });
932
933  // Final check: Catch the last group if it wasn't ended by a divider
934  finalizeTabGroup();
935
936  // On initial load, if URL contains a hash like #tab-*, activate that tab
937  const initialHash = window.location.hash;
938  if (initialHash && initialHash.startsWith('#tab-')) {
939    const safeHash = initialHash.replace(/[^#a-z0-9-]/gi, '');
940    const targetBtn = section.querySelector(safeHash);
941    if (targetBtn && targetBtn.classList.contains('wbgtab-btn')) {
942      targetBtn.click();
943      targetBtn.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
944    }
945  }
946}
947
948function decoratePopupContent(section, containerClasses, stopperClasses) {
949  let showPopContent = false;
950  let hasStarted = false;
951
952  const children = Array.from(section.children);
953  children.forEach((child) => {
954    const isContainer = containerClasses.some(cls =>
955      child.classList.contains(cls)
956    );
957
958    const isStopper = stopperClasses.some(cls =>
959      child.classList.contains(cls)
960    );
961
962    if (isContainer) {
963      const first = child.firstElementChild;
964      if (!first) return;
965
966      const itemIndex = child.classList.contains('popup-card-wrapper') ? 6 : 1;
967      showPopContent = first.children[itemIndex]?.textContent?.trim().toLowerCase() === 'yes';
968
969      hasStarted = true;
970      return;
971    }
972
973    if (!hasStarted) return;
974
975    if (isStopper) {
976      showPopContent = false;
977      hasStarted = false;
978      return;
979    }
980
981    if (!showPopContent) {
982      child.style.display = 'none';
983    }
984  });
985}
986
987function decoratePopups(section) {
988  // Define which blocks act as "containers" and which act as "stoppers"
989  const containerClasses = ['popup-card-wrapper', 'text-banner-wrapper'];
990  const stopperClasses = ['divider-wrapper', 'wbgcolumn-wrapper'];
991
992  const hasPopUp = containerClasses.some(cls =>
993    section.querySelector(`:scope > .${cls}`)
994  );
995
996  if (!hasPopUp) return;
997
998  if (window.parent.location.href.includes('/canvas')) {
999    decoratePopupContent(section, containerClasses, stopperClasses);
1000    return;
1001  }
1002
1003  let currentContentWrapper = null;
1004  const fragment = document.createDocumentFragment();
1005
1006  const finalizeContainer = () => {
1007    if (currentContentWrapper) {
1008      // Move all collected siblings from the fragment into the wrapper at once
1009      currentContentWrapper.appendChild(fragment);
1010      
1011      if (typeof decorateWbgColumns === 'function') {
1012        decorateWbgColumns(currentContentWrapper);
1013      }
1014    }
1015    currentContentWrapper = null;
1016  };
1017
1018  const children = Array.from(section.children);
1019  children.forEach((child) => {
1020    const isContainer = containerClasses.some(cls => child.classList.contains(cls));
1021    const isStopper = stopperClasses.some(cls => child.classList.contains(cls));
1022
1023    if (isContainer) {
1024      // If we were already building a card/banner, finish it first
1025      finalizeContainer();
1026
1027      // Create the internal content holder
1028      currentContentWrapper = document.createElement('div');
1029      currentContentWrapper.className = 'popup-content-wrapper';
1030      currentContentWrapper.hidden = true;
1031      currentContentWrapper.setAttribute('aria-hidden', 'true');
1032
1033      // Add wrapper to the trigger block (the card or banner)
1034      child.appendChild(currentContentWrapper);
1035    } else if (isStopper) {
1036      // If we hit a column or divider, close the current container so the stopper stays outside
1037      finalizeContainer();
1038    } else if (currentContentWrapper) {
1039      // It's a sibling that belongs inside the container
1040      // Use fragment to avoid multiple DOM reflows
1041      fragment.appendChild(child);
1042    }
1043  });
1044
1045  // Final wrap-up for the last block in the section
1046  finalizeContainer();
1047}
1048
1049function buildTabs(main) {
1050  function getTabLabel(section) {
1051    const metadataBlock = section.querySelector('.section-metadata');
1052    const metadata = metadataBlock ? readBlockConfig(metadataBlock) : {};
1053    return metadata['tab-label'];
1054  }
1055
1056  for (let i = 0; i < main.children.length; i += 1) {
1057    const section = main.children[i];
1058    const tabLabel = getTabLabel(section);
1059    const previousSection = i > 0 ? main.children[i - 1] : null;
1060    const previousTabLabel = previousSection ? getTabLabel(previousSection) : null;
1061
1062    if (tabLabel && !previousTabLabel) {
1063      // found first tab panel of a list of consecutive tab panels
1064      // create a tab list block if non exists as last child
1065      let previousBlock = previousSection?.lastElementChild;
1066      if (previousBlock?.matches('.section-metadata')) previousBlock = previousBlock.previousElementSibling;
1067      if (!previousBlock?.matches('.tab-list')) {
1068        const tabListBlock = document.createElement('div');
1069        tabListBlock.className = 'tab-list block';
1070        const newSection = document.createElement('div');
1071        newSection.className = 'section';
1072        newSection.appendChild(tabListBlock);
1073        section.before(newSection);
1074      }
1075    }
1076  }
1077}
1078
1079function handleSelection(event) {
1080  const { detail, target } = event;
1081  if (!detail.selected) return;
1082
1083  const tabPanel = target.closest('[role="tabpanel"]');
1084  if (tabPanel) {
1085    // select the tab item that controls this tab panel
1086    const tabItem = document.querySelector(`[aria-controls="${tabPanel.id}"]`);
1087    if (tabItem) tabItem.click();
1088  }
1089}
1090
1091function addClassesToBody() {
1092  const pageStyle = getMetadata('page-style');
1093  if (!pageStyle || pageStyle.length < 1) return;
1094  
1095  document.body.classList.add(pageStyle);
1096}

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.