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.