import { sampleRUM, loadHeader, loadFooter, decorateButtons, decorateIcons, decorateSections, decorateGrid, decorateBlocks, decorateTemplateAndTheme, waitForLCP, loadBlocks, loadCSS, fetchPlaceholders, loadScript, loadBlock } from './aem.js'; import { renderWBDataLayer } from './analytics.js'; import { picture, source, img } from './dom-helpers.js'; import { insertSchema } from './schema.js'; import { initAllDownloadFiles, SUPPORTED_LANGUAGES } from './utils.js'; import { initSwiper } from './utils.js'; import { getLanguage, createSource, setPageLanguage, cookiePopUp, showCookieConsent, PATH_PREFIX, isInternalPage, scriptEnabled, } from './utils.js'; import { initAllCarousels } from './carousel-init.js'; import decorate from '../blocks/popup-card/popup-card.js'; const LCP_BLOCKS = ['bio-detail','curated-cards']; // add your LCP blocks to the list export const CLASS_MAIN_HEADING = 'main-heading'; export const LANGUAGE_ROOT = `/ext/${getLanguage()}`; document.addEventListener('DOMContentLoaded', () => { // Wrap cards and initialize Swipers initAllCarousels(); initAllDownloadFiles(); initSwiper(); }); /** * Moves all the attributes from a given elmenet to another given element. * @param {Element} from the element to copy attributes from * @param {Element} to the element to copy attributes to */ export function moveAttributes(from, to, attributes) { if (!attributes) { // eslint-disable-next-line no-param-reassign attributes = [...from.attributes].map(({ nodeName }) => nodeName); } attributes.forEach((attr) => { const value = from.getAttribute(attr); if (value) { to.setAttribute(attr, value); from.removeAttribute(attr); } }); } /** * Move instrumentation attributes from a given element to another given element. * @param {Element} from the element to copy attributes from * @param {Element} to the element to copy attributes to */ export function moveInstrumentation(from, to) { moveAttributes( from, to, [...from.attributes] .map(({ nodeName }) => nodeName) .filter((attr) => attr.startsWith('data-aue-') || attr.startsWith('data-richtext-')), ); } /** * load fonts.css and set a session storage flag */ async function loadFonts() { await loadCSS(`${window.hlx.codeBasePath}/styles/fonts.css`); try { if (!window.location.hostname.includes('localhost')) sessionStorage.setItem('fonts-loaded', 'true'); } catch (e) { // do nothing } } /** * Decorates Dynamic Media images by modifying their URLs to include specific parameters * and creating a element with different sources for different image formats and sizes. * * @param {HTMLElement} main - The main container element that includes the links to be processed. */ export function decorateDMImages(main) { main.querySelectorAll('a[href^="https://delivery-p"]').forEach((a) => { const url = new URL(a.href.split('?')[0]); if (url.hostname.endsWith('.adobeaemcloud.com')) { const pictureEl = picture( source({ srcset: `${url.href}?width=1400&quality=85&preferwebp=true`, type: 'image/webp', media: '(min-width: 992px)' }), source({ srcset: `${url.href}?width=1320&quality=85&preferwebp=true`, type: 'image/webp', media: '(min-width: 768px)' }), source({ srcset: `${url.href}?width=780&quality=85&preferwebp=true`, type: 'image/webp', media: '(min-width: 320px)' }), source({ srcset: `${url.href}?width=1400&quality=85`, media: '(min-width: 992px)' }), source({ srcset: `${url.href}?width=1320&quality=85`, media: '(min-width: 768px)' }), source({ srcset: `${url.href}?width=780&quality=85`, media: '(min-width: 320px)' }), img({ src: `${url.href}?width=1400&quality=85`, alt: a.innerText }), ); a.replaceWith(pictureEl); } }); } export async function decorateDMImagesWithRendition(image,imageRatio,modifiers,disableSmartCrop,title,alt,aspectRatioDiv,objectPosDiv,loadingValue) { const getText = (el, defaultValue = '') => { if (el instanceof Element) { const p = el.querySelector('p'); return p ? p.textContent.trim() : defaultValue; } return defaultValue; }; const imageRendition = getText(imageRatio); const quality = getText(modifiers, 'quality=85'); const isDisable = getText(disableSmartCrop, 'false'); const aspectRatio = getText(aspectRatioDiv); const objectPos = getText(objectPosDiv); const altValue = getText(alt); const titleValue = getText(title); if(!loadingValue) { loadingValue = 'lazy'; } const sources = []; const deliveryLink = image.querySelector('a[href^="https://delivery-p"]'); if (deliveryLink) { const url = new URL(deliveryLink.href.split('?')[0]); const originalHref = url.href; const fileName = originalHref.substring(originalHref.lastIndexOf("/") + 1); const altText = fileName.substring(0, fileName.lastIndexOf(".")); const imgEl = img({ loading: loadingValue, alt: altValue != ''? altValue : altText, title: titleValue != ''? titleValue : altText, src: originalHref.replace('/original', '') }); if (url.hostname.endsWith('.adobeaemcloud.com')) { if(imageRendition != '' && isDisable == 'false') { let metaPath = url.pathname.replace('/original', ''); const asIndex = metaPath.indexOf("/as/"); if (asIndex !== -1) { metaPath = metaPath.substring(0, asIndex); } const metadataUrl = `${url.origin}${metaPath}/metadata`; let availableRenditions = {}; try { const resp = await fetch(metadataUrl); if (resp.ok) { const json = await resp.json(); availableRenditions = json?.repositoryMetadata?.smartcrops || {}; } } catch (e) { console.error("Failed to fetch metadata:", e); } const breakpoints = imageRendition.split(",").reduce((acc, item) => { const [resolution, size] = item.split(":"); if (!resolution || !size) return acc; const [width, height] = size.split("x").map(Number); if (width && height) { acc[Number(resolution)] = { width, height }; } return acc; }, {}); const sortedBps = Object.entries(breakpoints).sort((a, b) => Number(b[0]) - Number(a[0])); const [largestBp, largestData] = sortedBps[0]; const largestKey = `${largestData.width}x${largestData.height}`; let largestRenditionHref = originalHref; largestRenditionHref = originalHref.replace('/original', ''); sortedBps.forEach(([bp, { width, height }]) => { const key = `${width}x${height}`; let renditionHref = originalHref; renditionHref = renditionHref.replace('/original', ''); if (availableRenditions[key]) { sources.push( source({ type: "image/webp", media: `(min-width: ${bp}px)`, srcset: `${renditionHref}?${quality}&smartcrop=${key}`, }) ); } else { sources.push( source({ type: "image/webp", media: `(min-width: ${bp}px)`, srcset: `${renditionHref}?${quality}&smartcrop=${key}`, }) ); } }); imgEl.src = `${largestRenditionHref}?${quality}&smartcrop=${largestKey}`; imgEl.style.aspectRatio = "auto"; imgEl.style.objectPosition = "initial"; } else { if(quality != '') { const imgSrc = originalHref.replace('/original', ''); imgEl.src = `${imgSrc}?${quality}`; } if (aspectRatio != '') { imgEl.style.aspectRatio = aspectRatio; } if (objectPos != '') { imgEl.style.objectPosition = objectPos; } } const parent = deliveryLink.parentElement; if (parent && parent.tagName.toLowerCase() === 'p') { parent.replaceWith(picture(...sources, imgEl)); } else { deliveryLink.replaceWith(picture(...sources, imgEl)); } } } else { const pictureTag = image.querySelector('picture'); if (pictureTag) { const pic = pictureTag.querySelector('img'); if(altValue != '') { pic.alt = altValue; } else { pic.alt = 'default alt'; } if (aspectRatio != '') { pic.style.aspectRatio = aspectRatio; } if (objectPos != '') { pic.style.objectPosition = objectPos; } if (pic) { const newImg = pic.cloneNode(true); // clone so we don't lose it pictureTag.innerHTML = ''; // clear all children pictureTag.appendChild(newImg); // put back only } const parent = pictureTag.parentElement; if (parent && parent.tagName.toLowerCase() === 'p') { parent.replaceWith(pictureTag); } } } [imageRatio, alt, modifiers, disableSmartCrop, aspectRatioDiv, objectPosDiv].forEach((el) => { if (el instanceof Element) el.remove(); }); } /** * remove the adujusts the auto images * @param {Element} main The container element */ function adjustAutoImages(main) { const pictureElement = main.querySelector('div > p > picture'); if (pictureElement) { const pElement = pictureElement.parentElement; pElement.className = 'auto-image-container'; } } /** * Return the placeholder file specific to language * @returns */ export async function fetchLanguagePlaceholders() { const langCode = getLanguage(); try { // Try fetching placeholders with the specified language return await fetchPlaceholders(`${PATH_PREFIX}/${langCode}${!SUPPORTED_LANGUAGES.includes(langCode) ? '/data' : ''}`); } catch (error) { // eslint-disable-next-line no-console console.error(`Error fetching placeholders for lang: ${langCode}. Will try to get en placeholders`, error); // Retry without specifying a language (using the default language) try { return await fetchPlaceholders(`${PATH_PREFIX}/en`); } catch (err) { // eslint-disable-next-line no-console console.error('Error fetching placeholders:', err); } } return {}; // default to empty object } /** * Builds all synthetic blocks in a container element. * @param {Element} main The container element */ // eslint-disable-next-line no-unused-vars function buildAutoBlocks(main) { try { adjustAutoImages(main); } catch (error) { // eslint-disable-next-line no-console console.error('Auto Blocking failed', error); } } /** * Decorates the main element. * @param {Element} main The main element */ // eslint-disable-next-line import/prefer-default-export export function decorateMain(main) { // hopefully forward compatible button decoration decorateButtons(main); decorateIcons(main); buildAutoBlocks(main); decorateSections(main); decorateBlocks(main); decorateGrid(main); //decorateDMImages(main); } /** * Loads everything needed to get to LCP. * @param {Element} doc The container element */ async function createSkipToMainNavigationBtn() { const placeholder = await fetchLanguagePlaceholders(); const main = document.querySelector('main'); main.id = 'main'; const anchor = document.createElement('a'); anchor.id = 'skip-to-main-content'; anchor.className = 'visually-hidden focusable'; anchor.href = '#main'; anchor.textContent = placeholder.skipToMainContent || 'Skip to Main Content'; document.body.insertBefore(anchor, document.body.firstChild); } /** * Translate 404 page * @returns */ export async function load404() { const main = document.querySelector('main'); const placeholders = await fetchLanguagePlaceholders(); const homelink = main.querySelector('p a'); const searchForm = main.querySelector('form'); main.querySelector('h1').innerText = placeholders.notFoundHeading || 'Page Not Found'; main.querySelector('h2').innerText = placeholders.notFoundSubHeading || '404 Error'; 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.'; homelink.innerText = placeholders.notFoundBtnLabel || 'Go to homepage'; homelink.title = placeholders.notFoundBtnLabel || 'Go to homepage'; homelink.href = placeholders.logoUrl || '/'; searchForm.action = placeholders.searchRedirectUrl || 'https://www.worldbank.org/en/search'; searchForm.querySelector('input').placeholder = placeholders.searchVariable || 'Search worldbank.org'; main.classList.remove('loading'); return null; } async function loadEager(doc) { setPageLanguage(); decorateTemplateAndTheme(); const main = doc.querySelector('main'); if (main) { decorateMain(main); document.body.classList.add('appear'); await waitForLCP(LCP_BLOCKS); } try { /* if desktop (proxy for fast connection) or fonts already loaded, load fonts.css */ if (window.innerWidth >= 900 || sessionStorage.getItem('fonts-loaded')) { loadFonts(); } } catch (e) { // do nothing } } /** * Return the json for any placeholder file specific to language using filename as argument * @returns */ export const fetchLangDatabyFileName = async (fileName) => { const langCode = getLanguage(); try { const response = await fetch(`${PATH_PREFIX}/${langCode}/${fileName}.json`); if (!response.ok) { throw new Error('Failed to load data'); } const json = await response.json(); return json.data || []; } catch (error) { return []; } }; /** * Create section background image * * @param {*} doc */ function decorateSectionImages(doc) { const sectionImgContainers = doc.querySelectorAll('main .section[data-image]'); sectionImgContainers.forEach((sectionImgContainer) => { const sectionImg = sectionImgContainer.dataset.image; const sectionTabImg = sectionImgContainer.dataset.tabImage; const sectionMobImg = sectionImgContainer.dataset.mobImage; let defaultImgUrl = null; const newPic = document.createElement('picture'); if (sectionImg) { newPic.appendChild(createSource(sectionImg, 1920, '(min-width: 1024px)')); defaultImgUrl = sectionImg; } if (sectionTabImg) { newPic.appendChild(createSource(sectionTabImg, 1024, '(min-width: 768px)')); defaultImgUrl = sectionTabImg; } if (sectionMobImg) { newPic.appendChild(createSource(sectionTabImg, 600, '(max-width: 767px)')); defaultImgUrl = sectionMobImg; } const newImg = document.createElement('img'); newImg.src = defaultImgUrl; newImg.alt = ''; newImg.className = 'sec-img'; newImg.loading = 'lazy'; newImg.width = '768'; newImg.height = '100%'; if (defaultImgUrl) { newPic.appendChild(newImg); sectionImgContainer.prepend(newPic); } }); } /** * Loads everything that doesn't need to be delayed. * @param {Element} doc The container element */ async function loadLazy(doc) { const main = doc.querySelector('main'); await loadBlocks(main); const { hash } = window.location; const element = hash ? doc.getElementById(hash.substring(1)) : false; if (hash && element) element.scrollIntoView(); await createSkipToMainNavigationBtn(); cookiePopUp(); showCookieConsent(); loadHeader(doc.querySelector('header')); loadFooter(doc.querySelector('footer')); //load livenow block lazy const liveNow = document.createElement('div'); liveNow.className = 'block live-now'; liveNow.setAttribute('data-block-name', 'live-now'); document.body.appendChild(liveNow); loadBlock(liveNow); // This function loads the block content dynamically loadCSS(`${window.hlx.codeBasePath}/styles/lazy-styles.css`); loadFonts(); decorateSectionImages(doc); addMobileCloneButtons(); if (!isInternalPage()) { renderWBDataLayer(); loadAdobeAnalytics(); insertSchema(); } sampleRUM('lazy'); sampleRUM.observe(main.querySelectorAll('div[data-block-name]')); sampleRUM.observe(main.querySelectorAll('picture > img')); } /** * Loads everything that happens a lot later, * without impacting the user experience. */ function loadDelayed() { // eslint-disable-next-line import/no-cycle window.setTimeout(() => import('./delayed.js'), 3000); // load anything that can be postponed to the latest here import('./sidekick.js').then(({ initSidekick }) => initSidekick()); } function addMobileCloneButtons() { document.querySelectorAll(".section.clone-button-bottom").forEach(section => { let button = section.querySelector(".lp-heading .button"); if(!button){ button = section.querySelector(".heading .button"); } if (button && !section.querySelector(".cloned-button")) { // Add class to hide the original button on mobile & tablet button.classList.add("hide-on-mobile-tablet"); // Clone the button const clonedBtn = button.cloneNode(true); clonedBtn.classList.add("cloned-button"); clonedBtn.classList.remove("hide-on-mobile-tablet"); // Append at end of section if(section.classList.contains("grid")){ section.querySelector(".section-rows").appendChild(clonedBtn); }else{ section.appendChild(clonedBtn); } } }); } /** * Fetch filtered search results * @returns List of search results */ export async function fetchSearch() { window.searchData = window.searchData || {}; if (Object.keys(window.searchData).length === 0) { const corporateLangs = ['/ext/en', '/ext/fr', '/ext/ar', '/ext/ru', '/ext/zh', '/ext/es']; const targetRoot = corporateLangs.includes(LANGUAGE_ROOT) ? LANGUAGE_ROOT : '/ext/en'; const path = `${targetRoot}/query-index.json?limit=2500&offset=0`; const resp = await fetch(path); window.searchData = JSON.parse(await resp.text()).data; } return window.searchData; } async function loadAdobeAnalytics() { if (!scriptEnabled()) { return; } const config = await fetchPlaceholders(PATH_PREFIX); await loadScript(config["analyticsEndpoint"]); } async function loadPage() { const meta = document.querySelector('meta[name="transparent-header"]'); const isTransparentHeader = meta?.content === "yes"; if (isTransparentHeader) { const header = document.querySelector("header"); if (header) { header.classList.add("no-header-style"); } } const targetDiv = document.createElement('div'); targetDiv.id = 'wbg-aem-target'; targetDiv.className = 'wbg-aem-target'; window.wbgData ||= {}; await loadEager(document); await loadLazy(document); loadDelayed(); document.body.insertAdjacentElement('afterbegin', targetDiv); } loadPage(); export async function fetchJsonData(type) { const langCode = getLanguage(); let filePath; if(type == 'i18n') { filePath = 'siteconfig/i18n'; } const jsonFilePath = `${PATH_PREFIX}/${langCode}/${filePath}`; window.jsonData = window.jsonData || {}; if (!window.jsonData[jsonFilePath]) { window.jsonData[jsonFilePath] = fetch(`${jsonFilePath}.json`) .then((resp) => (resp.ok ? resp.json() : {})) .then((json) => { const jsonData = {}; if (json?.data) { json.data .filter((jdata) => jdata.Key) .forEach((jdata) => { const normalizedKey = jdata.Key.toLowerCase().replace(/\s+/g, '-'); jsonData[normalizedKey] = (jdata.Value || jdata.Text || '').replace(/\s*\(.*?\)\s*/g, ''); }); } window.jsonData[jsonFilePath] = jsonData; return jsonData; }) .catch(() => { window.jsonData[jsonFilePath] = {}; return {}; }); } return window.jsonData[jsonFilePath]; } export async function getTranslatedValue(tagValue) { const jsonData = await fetchJsonData('i18n'); if (!tagValue) return []; // always return an array const result = tagValue .split(',') .map(v => v.trim().toLowerCase()) .map(v => { // Handle "world-bank:" prefix if (v.startsWith('world-bank:')) { const delimiter = v.includes('/') ? '/' : ':'; v = v.split(delimiter).pop().trim(); } v = v.replace(/\s+/g, '-'); return jsonData?.[v] || v; }); return result; }