1/*if (document.readyState === "loading") { 2 // Loading hasn't finished yet 3 document.addEventListener("DOMContentLoaded",initializeDropdown); 4 document.addEventListener("DOMContentLoaded", initializeNavigation); 5} else { 6 // `DOMContentLoaded` has already fired 7 initializeDropdown(); 8 initializeNavigation(); 9}*/ 10const SUPPORTED_LANGUAGES = ['en', 'zh', 'ru', 'fr', 'es', 'ar']; 11 12// media query match that indicates mobile/tablet width 13const isDesktop = window.matchMedia('(min-width: 1024px)'); 14let lang; 15let lastFocusedElement = null; 16 17 18function populateHeader(options) { 19const langcode = (options && options.languagecode) ? (options.languagecode) : getLanguage(); 20const selector = (options && options.selector) ? (options.selector) : 'header'; 21const serviceEndpoint = (options && options.serviceEndpoint) ? (options.serviceEndpoint) : getHeaderServiceHost(options); 22fetch(serviceEndpoint) 23 .then(resp => { 24 if (!resp.ok) { 25 throw new Error(`HTTP error! Status: ${resp.status}`); 26 } 27 return resp.text(); 28 }) 29 .then(html => { 30 const template = document.querySelector(selector); 31 template.innerHTML = html; 32 initializeHeader(options); 33 }) 34 .catch(error => { 35 console.error("Error setting up the header :", error); 36 }); 37} 38 39function getHeaderServiceHost(options) { 40 const environment = (options) ? options.environment: "prod" 41 if(environment == 'dev') { 42 //return 'extdataportaldev.worldbank.org'; 43 return 'extdataportalqa.worldbank.org/qa'; 44 } else if (environment == 'qa') { 45 return 'extdataportalqa.worldbank.org/qa'; 46 } else if (environment == 'stage') { 47 return 'extdataportalqa.worldbank.org/qa'; 48 } else { 49 return 'extdataportal.worldbank.org'; 50 } 51} 52 53function initializeHeader(options) { 54 const langcode = (options && options.languagecode) ? (options.languagecode) : getLanguage(); 55 options.languagecode = langcode; 56 if(options && options.languageToggler) { 57 populateLanguageDropdown(options) 58 } 59 60 initializeDropdown(); 61 intitializeSearch(); 62 initializeNavigation(); 63 initializeTopMenuMainNavigation(); 64 loadNavCountryElements(); 65 filterNavCountryElements(); 66 initializeDropdowns(); 67 //if ((isDesktop.matches) && (options && options.enabletrendingdata)) populateTrendingData(options); 68 // const countdownServiceEndpoint = window.location.href.includes('author-p136806') 69 //? 'https://webapi.worldbank.org/api/wbg/aem/service/meetingcountdownbanner' 70 //: '/wbg/aem/service/meetingcountdownbanner'; 71 // initializeCountdownBanner({ languagecode: langcode, countdownServiceEndpoint: countdownServiceEndpoint }); 72 73 if(!window.location.href.includes('author-p136806')) { 74 initializeCountdownBanner({ languagecode: langcode, countdownServiceEndpoint: '/wbg/aem/service/meetingcountdownbanner' }); 75 } 76 77} 78 79function makeUppercaseSafe(str) { 80 if (!str) return ''; 81 return str.trim().toUpperCase(); 82} 83 84// Get Language Selector Display Text based on screen size 85const getLanguageDisplayText = (lang, langcode) => (( 86 (window.screen.width >= 768) && lang) 87 ? capitalizeFirstLetter(lang) 88 : makeUppercaseSafe(langcode)); 89 90/** 91 * Creates a custom dropdown with clickable links. 92 * @param {Array<{label: string, href: string}>} optionsArray - Array of link options. 93 * 94 */ 95async function populateLanguageDropdown(options) { 96 const container = document.getElementById('dropdownMenuLang'); 97 if (!container) { 98 console.error(`Container with ID '${containerId}' not found.`); 99 return; 100 } 101 let optionsArray = []; 102 const togglerOpts = options.languageToggler; 103 if((togglerOpts.default && togglerOpts.default == true) || togglerOpts.langServiceEndpoint){ 104 optionsArray = await getLangTogglerData(options); 105 } else { 106 optionsArray = options.languageToggler.langdata; 107 } 108 const langcode = options.languagecode; 109 const dropdownLangText = document.querySelector("#dropdownLangText"); 110 // Clear existing content 111 container.innerHTML = ''; 112 if(optionsArray && optionsArray.length > 1) { 113 optionsArray.forEach((option, index) => { 114 if (option.langcode.toLowerCase() == langcode.toLowerCase()) { 115 dropdownLangText.textContent = getLanguageDisplayText(
115option.lang, langcode); 116 } else { 117 const item = document.createElement('li'); 118 item.setAttribute('role', 'option'); 119 //item.setAttribute('tabindex', '0'); 120 //item.setAttribute('aria-selected', index === 0 ? 'true' : 'false'); 121 122 const link = document.createElement('a'); 123 link.href = option.href; 124 link.textContent = capitalizeFirstLetter(option.lang); 125 link.lang = option.langcode; 126 127 item.appendChild(link); 128 container.appendChild(item); 129 } 130 }); 131 132 const wrapper = container.closest(".dropdownWrapper"); 133 wrapper.hidden = false; 134 /*const dropdownLang = document.querySelector("#dropdownLang"); 135 const langDropdown = document.querySelectorAll("#dropdownMenuLang li"); 136 const wrapper = container.closest(".dropdownWrapper"); 137 //preselct the language 138 langDropdown.forEach((option, index) => { 139 let link = option.querySelector('a'); 140 if(link.lang == langcode) { 141 option.setAttribute("aria-selected", "true"); 142 document.querySelector("#dropdownLangText").textContent = getLanguageDisplayText(option.textContent, langcode); 143 //dropdownLang.textContent = getLanguageDisplayText(option.textContent, langcode); 144 dropdownLang.setAttribute("aria-expanded", "false"); 145 container.hidden = true; 146 wrapper.classList.remove("active"); 147 } 148 149 }); 150 151 wrapper.hidden = false;*/ 152 153 // Show Language code in Mobile when the window is resized 154 const resizeObserver = new ResizeObserver(() => { 155 const filtered = optionsArray.find(opt => opt.langcode === langcode); 156 if(filtered) dropdownLangText.textContent = getLanguageDisplayText(filtered.lang, langcode); 157 }); 158 const headerTag = document.querySelector('header'); 159 if (headerTag) { 160 resizeObserver.observe(headerTag); 161 } else { 162 resizeObserver.observe(document.body); 163 } 164 165 } 166 167} 168 169function initializeTopMenuMainNavigation() { 170 var workBtn = document.getElementById('wbg-ext-main-nav-top'); 171 var topNav = document.getElementById('top-nav'); 172 if(!workBtn || !topNav) return; 173 174 workBtn.addEventListener('click', function(e){ 175 e.preventDefault(); 176 177 // 1. Open nav container 178 topNav.style.display = 'block'; 179 topNav.classList.add('active'); 180 topNav.classList.add('show'); 181 topNav.setAttribute('aria-expanded', 'true'); 182 document.body.classList.add('wbg_ext_header_overlay'); 183 184 // 2. Clear inline display overrides and set panels to hidden 185 var panels = topNav.querySelectorAll('.wbg_ext_navigation_menu'); 186 panels.forEach(function(p){ 187 p.style.display = ''; 188 p.setAttribute('hidden', ''); 189 }); 190 191 // 3. Reset tabs 192 var tabs = topNav.querySelectorAll('.wbg_ext_menu_list');
193 tabs.forEach(function(t){ 194 t.style.display = ''; 195 t.classList.remove('active'); 196 t.setAttribute('aria-selected', 'false'); 197 t.setAttribute('tabindex', '-1'); 198 }); 199 200 // 4. Activate Tab 5 201 var tab5 = document.getElementById('tab5'); 202 if (tab5) { 203 tab5.classList.add('active'); 204 tab5.setAttribute('aria-selected', 'true'); 205 tab5.setAttribute('tabindex', '0'); 206 tab5.focus(); 207 } 208 209 // 5. Remove hidden attribute from submenu5 210 var targetPanel = document.getElementById('submenu5'); 211 if (targetPanel) { 212 targetPanel.removeAttribute('hidden'); 213 } 214 215 // 6. Update button ARIA state 216 workBtn.setAttribute('aria-expanded', 'true'); 217 }); 218 219 220// Initial check on load 221//updateButtonText(mediaQuery); 222 223// Listen for viewport resizes 224//mediaQuery.addEventListener('change', updateButtonText); 225 226 227 228} 229 230 231function initializeDropdown() { 232 const dropdowns = document.querySelectorAll(".dropdownSwitcher"); 233 234 dropdowns.forEach((langSwitcher) => { 235 const wrapper = langSwitcher.closest(".dropdownWrapper"); // parent wrapper 236 const langMenu = wrapper.querySelector(".dropdownMenu"); 237 const options = langMenu.querySelectorAll("li"); 238 239 // Toggle dropdown 240 langSwitcher.addEventListener("click", (e) => { 241 e.stopPropagation(); // prevent immediate close 242 243 lastFocusedElement = document.activeElement; 244 245 const expanded = langSwitcher.getAttribute("aria-expanded") === "true"; 246 const newState = (!expanded).toString(); 247 248 249 langSwitcher.focus(); 250 251 252 253 254 // Close all other dropdowns first (optional, if you want only one open at a time) 255 document.querySelectorAll(".dropdownWrapper.active").forEach((otherWrapper) => { 256 if (otherWrapper !== wrapper) { 257 otherWrapper.classList.remove("active"); 258 const btn = otherWrapper.querySelector(".dropdownSwitcher"); 259 const menu = otherWrapper.querySelector(".dropdownMenu"); 260 btn.setAttribute("aria-expanded", "false"); 261 menu.hidden = true; 262 } 263 }); 264 265 langSwitcher.setAttribute("aria-expanded", newState); 266 langMenu.hidden = expanded; 267 wrapper.classList.toggle("active", !expanded); 268 269 270 }); 271 272 273langSwitcher.addEventListener("keydown", (e) => { 274 if (e.key === "Enter" || e.key === " ") { 275 e.preventDefault(); 276 langSwitcher.click(); 277 } 278 279 if (e.key === "Escape" && wrapper.classList.contains("active")) { 280 langMenu.hidden = true; 281 wrapper.classList.remove("active"); 282 langSwitcher.setAttribute("aria-expanded", "false"); 283 langSwitcher.focus(); 284 285 if (lastFocusedElement) { 286 lastFocusedElement.focus(); 287 lastFocusedElement = null; 288 } 289 } 290}); 291 292 // Close when clicking outside 293 document.addEventListener("click", (e) => { 294 if (!wrapper.contains(e.target)) { 295 langSwitcher.setAttribute("aria-expanded", "false"); 296 langMenu.hidden = true; 297 wrapper.classList.remove("active"); 298 299 } 300 }); 301 302 303 // Keyboard navigation 304 /* options.forEach((option, index) => { 305 option.addEventListener("click", () => { 306 options.forEach((opt) => opt.removeAttribute("aria-selected")); 307 option.setAttribute("aria-selected", "true"); 308 langSwitcher.textContent = option.textContent; 309 langSwitcher.setAttribute("aria-expanded", "false"); 310 langMenu.hidden = true; 311 wrapper.classList.remove("active"); 312 });*/ 313 314 /*option.addEventListener("keydown", (e) => { 315 if (e.key === "ArrowDown") { 316 e.preventDefault(); 317 options[(index + 1) % options.length].focus(); 318 } else if (e.key === "ArrowUp") { 319 e.preventDefault(); 320 options[(index - 1 + options.length) % options.length].focus(); 321 } else if (e.key === "Enter" || e.key === " ") { 322 e.preventDefault(); 323 option.click(); 324 } else if (e.key === "Escape") { 325 langSwitcher.focus(); 326 langSwitcher.setAttribute("aria-expanded", "false"); 327 langMenu.hidden = true; 328 wrapper.classList.remove("active"); 329 } 330 }); 331 });*/ 332 }); 333 334} 335 336function intitializeSearch() { 337 const searchForm = document.querySelector('.wbg_ext_search_section form'); 338 if (searchForm) { 339 searchForm.addEventListener('submit', function(e) { 340 e.preventDefault(); 341 const val = document.getElementById('search-input').value.trim(); 342 if (val) { 343 window.location.href = 'https://www.worldbank.org/en/search?q=' + encodeURIComponent(val); 344 } 345 }); 346 } 347 348 // Toggle search dropdown 349 document.getElementById('nav-search-toggle').addEventListener('click', function() { 350 lastFocusedElement = document.activeElement; 351 //toggleOverflowY(); 352 document.body.classList.toggle("wbg_ext_header_overlay"); 353 354 const isOpen = document.getElementById('nav-search-menu').classList.toggle('show'); 355 356 document.getElementById('nav-search-toggle').classList.toggle('show', isOpen); 357 document.getElementById('nav-search-toggle').setAttribute('aria-expanded', isOpen.toString()); 358 }); 359 360 // Close dropdowns if clicked outside 361 document.addEventListener('click', function(e) { 362 const searchBtn = document.getElementById('nav-search-toggle'); 363 const searchMenu = document.getElementById('nav-search-menu'); 364 365 if (!searchBtn.contains(e.target) && !searchMenu.contains(e.target) && searchMenu.classList.contains('show')) { 366 searchMenu.classList.remove('show'); 367 document.getElementById('nav-search-toggle').setAttribute('aria-expanded', 'false'); 368 document.getElementById('nav-search-toggle').classList.remove('show'); 369 //document.body.style.overflowY = ''; 370 document.body.classList.remove("wbg_ext_header_overlay"); 371 } 372 }); 373 374 document.getElementById('search-btn').addEventListener('click', function() { 375 const searchInputBox = document.getElementById('search-input'); 376 if (searchInputBox.value) { 377 window.location.href = 'https://www.worldbank.org/en/search?q=' + searchInputBox.value; 378 } 379 }); 380 381 const searchInputBoxEle = document.getElementById('search-input'); 382 searchInputBoxEle.addEventListener('keydown', function(event) { 383 if (event.key !== 'Enter') return; 384 if (searchInputBoxEle.value) { 385 window.location.href = 'https://www.worldbank.org/en/search?q=' + searchInputBoxEle.value; 386 } 387 }); 388} 389 390function initializeNavigation() { 391 const navContainer = document.getElementById("top-nav"); 392 const navLinks = document.querySelectorAll(".wbg_ext_global_header__nav-link"); 393 const tabs = Array.from(navContainer.querySelectorAll('.wbg_ext_menu_list[role="toptab"]')); 394 const panels = Array.from(navContainer.querySelectorAll('.wbg_ext_navigation_menu[role="toptabpanel"]')); 395 const hamburger = document.getElementById("hamburger"); 396 const closeBtn = document.getElementById("close-btn"); 397 const backBtn = document.getElementById("back-arrow"); 398 const lpHeaderContainar = document.querySelector(".lp-overlay-global-header-wrapper"); 399 400 let isMobile = window.innerWidth <= 1199; 401 402 // ---------- Helpers ---------- 403 const getActiveTab = () => tabs.find(t => t.getAttribute("aria-selected") === "true") || null; 404 405 const getTabFlowElements = () => { 406 const visibleTabs = tabs.filter(tab => tab.offsetParent !== null); 407 return closeBtn ? [...visibleTabs, closeBtn] : visibleTabs; 408}; 409 410 411const closeMenuAndRestoreFocus = () => { 412 closeMenu(); 413 414 if (lastFocusedElement) { 415 lastFocusedElement.focus(); 416 lastFocusedElement = null; 417 } 418}; 419 420 const activateTopmenuTab = (tab) => {
421 tabs.forEach(t => { 422 const isActive = t === tab; 423 t.classList.toggle("active", isActive); 424 t.setAttribute("aria-selected", isActive); 425 t.tabIndex = isActive ? 0 : -1; 426 focusTab(getActiveTab()); 427 t.classList.toggle("active", isActive); 428 429 const panel = document.getElementById(t.getAttribute("aria-controls")); 430 if (panel) panel.hidden = !isActive; 431 }); 432 }; 433 434 const focusTab = (tab) => { 435 if (!tab) return; 436 tabs.forEach(t => (t.tabIndex = -1)); 437 tab.tabIndex = 0; 438 tab.focus(); 439 }; 440 441 const getFirstFocusable = (panel) => panel.querySelector('a, button, input, [tabindex]:not([tabindex="-1"])'); 442 443 const getNextElement = (elements, index, offset) => { 444 const nextIndex = index + offset; 445 if (nextIndex >= 0 && nextIndex < elements.length) return elements[nextIndex]; 446 return null; 447 }; 448 449 const openMenu = () => { 450 451 lastFocusedElement = document.activeElement; 452 453 navContainer.style.display = "block"; 454 navContainer.classList.add("active"); 455 navContainer.classList.remove("show"); 456 navContainer.classList.remove("slideOut"); 457 navContainer.setAttribute("aria-expanded", "true"); 458 //document.body.style.overflowY = "hidden"; 459 document.body.classList.add("wbg_ext_header_overlay"); 460 //To handle transperent header 461 if(lpHeaderContainar) { 462 lpHeaderContainar.classList.add("wbg-transparent-header"); 463 } 464 465 // Desktop â focus first active tab 466 if (!isMobile) focusTab(getActiveTab()); 467 468 if (isMobile) { 469 panels.forEach(panel => panel.hidden = true);
470 tabs.forEach(tab => { 471 tab.style.display = "block"; 472 }); 473 } 474 475 476 }; 477 478 const closeMenu = (hide = true) => { 479 navContainer.classList.remove("active", "show"); 480 navContainer.classList.add("slideOut"); 481 navContainer.setAttribute("aria-expanded", "false"); 482 if (backBtn) backBtn.style.display = "none"; 483 //document.body.style.overflowY = ""; 484 document.body.classList.remove("wbg_ext_header_overlay"); 485 //To handle transperent header 486 if(lpHeaderContainar) { 487 lpHeaderContainar.classList.remove("wbg-transparent-header"); 488 } 489 490 tabs.forEach(tab => { 491 tab.classList.remove("active") 492 tab.setAttribute("aria-selected", "false"); 493 tab.style.display = "block"; 494 }); 495 tabs.forEach(tab => tab.classList.remove("active")); 496 panels.forEach(panel => panel.hidden = true); 497 navLinks.forEach(link => link.setAttribute("aria-expanded", "false")); 498 499 if (hide) navContainer.style.display = "none"; 500 }; 501 502 // ---------- navLinks ---------- 503 navLinks.forEach(link => { 504 link.addEventListener("click", e => { 505 e.stopPropagation(); 506 if (e.target === hamburger || e.target === closeBtn) return; 507 e.preventDefault(); 508 509 const expanded = link.getAttribute("aria-expanded") === "true"; 510 navLinks.forEach(l => l.setAttribute("aria-expanded", "false")); 511 512 if (!expanded) { 513 link.setAttribute("aria-expanded", "true"); 514 openMenu(); 515 if (!isMobile) { 516 const tabText = link.textContent.trim().toLowerCase(); 517 const matchTab = tabs.find(tab => tab.textContent.trim().toLowerCase() === tabText); 518 if (matchTab) activateTopmenuTab(matchTab); 519 } 520 } else closeMenu(false); 521 }); 522 }); 523 524 525 526 527 528 // ---------- Left tablist click (mobile) ----------
529 tabs.forEach(tab => { 530 531 tab.addEventListener("mouseenter", () => { 532 if (!isMobile) activateTopmenuTab(tab); 533 }); 534 535 536 tab.addEventListener("click", e => { 537 if (!isMobile) return; 538 if (isMobile && e.target.closest(".wbg_ext_menu_list").classList.contains("active")) return; 539 e.preventDefault(); 540 activateTopmenuTab(tab); 541 542 // On mobile: show only the active tab 543 tabs.forEach(t => { 544 t.style.display = t.classList.contains("active") ? "block" : "none"; 545 }); 546 547 navContainer.classList.add("show"); 548 if (backBtn) backBtn.style.display = "block"; 549 }); 550 551 }); 552 553 // ---------- Mobile back button ---------- 554 if (backBtn) { 555 backBtn.addEventListener("click", () => { 556 navContainer.classList.remove("show"); 557 backBtn.style.display = "none"; 558 559 tabs.forEach(tab => { 560 tab.classList.remove("active"); 561 tab.setAttribute("aria-selected", "false"); 562 tab.style.display = "block"; // show all tabs again 563 }); 564 panels.forEach(panel => (panel.hidden = true)); 565 }); 566 } 567 568 // ---------- Hamburger / Close ---------- 569 if (hamburger) hamburger.addEventListener("click", () => openMenu()); 570 if (closeBtn) { 571 closeBtn.addEventListener("click", () => { 572 closeMenuAndRestoreFocus(); 573 }); 574 575 closeBtn.addEventListener("keydown", (e) => { 576 if (e.key === "Enter" || e.key === " ") { 577 e.preventDefault(); 578 closeMenuAndRestoreFocus(); 579 } 580 }); 581} 582 583 584 // ---------- Click outside ---------- 585/*document.addEventListener("click", e => { 586 if ( 587 navContainer.contains(e.target) || 588 e.target.closest("#hamburger") || 589 e.target.closest("#close-btn") || 590 e.target.closest(".wbg_ext_global_header") || 591 e.target.closest("#wbg-top-navigation") 592 ) return; 593 594 closeMenu(); 595});*/ 596 597 598 // ---------- Keyboard Navigation ---------- 599 navContainer.addEventListener("keydown", (event) => { 600 const focused = document.activeElement; 601 const activeTab = getActiveTab(); 602 const isTabFocused = tabs.includes(focused); 603 604 // Escape closes menu 605 if (event.key === "Escape") { 606 event.preventDefault(); 607 closeMenuAndRestoreFocus(); 608 return; 609} 610 611 // ---------- Left tablist ---------- 612 if (isTabFocused) { 613 const index = tabs.indexOf(focused); 614 615 if (event.key === "ArrowDown" || event.key === "Tab") { 616 event.preventDefault(); 617 618 const flow = getTabFlowElements(); 619 const idx = flow.indexOf(focused); 620 const next = flow[idx + 1]; 621 622 if (next) { 623 next.focus(); 624 } 625} 626 627if (event.key === "ArrowUp") { 628 event.preventDefault(); 629 630 const flow = getTabFlowElements(); 631 const idx = flow.indexOf(focused); 632 const prev = flow[idx - 1]; 633 634 if (prev) { 635 prev.focus(); 636 } 637} 638 639 if (event.key === "Enter" || event.key === " ") { 640 event.preventDefault(); 641 activateTopmenuTab(focused); 642 } 643 644 if (event.key === "ArrowRight") { 645 event.preventDefault(); 646 const panel = document.getElementById(focused.getAttribute("aria-controls")); 647 const first = getFirstFocusable(panel); 648 if (first) first.focus(); 649 } 650 } 651 652 // ---------- Right tabpanel ---------- 653 const panel = focused.closest('[role="toptabpanel"]'); 654 if (panel) { 655 const items = Array.from(panel.querySelectorAll('a, button, input, [tabindex]:not([tabindex="-1"])')); 656 const idx = items.indexOf(focused); 657 658 if (event.key === "ArrowDown") { 659 event.preventDefault(); 660 const next = getNextElement(items, idx, 1); 661 if (next) next.focus(); 662 } 663 664 if (event.key === "ArrowUp") { 665 event.preventDefault(); 666 const prev = getNextElement(items, idx, -1); 667 if (prev) prev.focus(); 668 } 669 670 if (event.key === "ArrowLeft") { 671 event.preventDefault(); 672 if (activeTab) activeTab.focus(); 673 } 674 } 675 }); 676 677 function checkMode() { 678 isMobile = window.innerWidth <= 1199; 679 680 if (!isMobile) { 681 // -------- Desktop -------- 682 const activeTab = tabs.find(tab => tab.classList.contains("active")); 683 if (activeTab) activateTopmenuTab(activeTab); 684
685 tabs.forEach(tab => (tab.style.display = "block")); 686 if (backBtn) backBtn.style.display = "none"; 687 navContainer.classList.remove("show"); 688 return; 689 } 690 691 692 693 //Tab click (mobile) 694 tabs.forEach(tab => { 695 tab.onclick = e => { 696 if (isMobile && e.target.closest(".wbg_ext_menu_list").classList.contains("active")) return; 697 e.preventDefault(); 698 tabs.forEach(t => { 699 if (t !== tab) { 700 t.classList.remove("active"); 701 t.setAttribute("aria-selected", "false"); 702 } 703 }); 704 navContainer.classList.add("show"); 705 if (backBtn) backBtn.style.display = "block"; 706 }; 707 }); 708 709 // Back button (mobile) 710 if (backBtn) { 711 backBtn.onclick = () => { 712 tabs.forEach(tab => { 713 tab.style.display = "block"; 714 tab.classList.remove("active"); 715 tab.setAttribute("aria-selected", "false"); 716 }); 717 panels.forEach(panel => (panel.hidden = true)); 718 navContainer.classList.remove("show"); 719 backBtn.style.display = "none"; 720 }; 721 } 722 723 const activeTab = getActiveTab(); 724 725 if (activeTab !== null) { 726 // Only show active tab 727 navContainer.classList.add("show");
728 tabs.forEach(tab => { 729 tab.style.display = tab === activeTab ? "block" : "none"; 730 }); 731 732 // Back button visible only if nav is open 733 if (navContainer.classList.contains("active") && backBtn) { 734 backBtn.style.display = "flex"; 735 } 736 737 738 } else { 739 // No active tab â show all 740 tabs.forEach(tab => (tab.style.display = "block")); 741 navContainer.classList.remove("show"); 742 if (backBtn) backBtn.style.display = "none"; 743 } 744 745 // Panels visibility 746 panels.forEach(panel => { 747 const tabId = panel.getAttribute("id"); 748 const isActive = activeTab && activeTab.getAttribute("aria-controls") === tabId; 749 panel.hidden = !isActive; 750 }); 751 752 753} 754 755 checkMode(); 756 window.addEventListener("resize", checkMode); 757 758 759} 760 761 762 763 764 765/* search inside navigation dropdown */ 766 767function loadNavCountryElements() { 768 var menu = document.querySelector(".wbg-ext-tt-menu"); 769 if (menu) { 770 menu.querySelector("li.tt-suggestion").removeAttribute("style"); 771 } 772 773 document.addEventListener('keyup', function () { 774 const search_country = document.getElementById('nav-search-country'); 775 var b = search_country.value.length; 776 if (b > 0) { 777 if (!document.querySelector(".wbg-ext-tt-menu").classList.contains("wbg-ext-show-tt-menu")) { 778 document.querySelector(".wbg-ext-tt-menu").classList.add("wbg-ext-show-tt-menu") 779 } 780 } else { 781 document.querySelector(".wbg-ext-tt-menu").classList.remove("wbg-ext-show-tt-menu") 782 } 783 return true 784 }); 785 786 var eventList = ["keydown", "keyup", "keypress"]; 787 for (event of eventList) { 788 var searchCountry = document.querySelector("#nav-search-country") 789 if (searchCountry) { 790 searchCountry.addEventListener(event, function (c) { 791 var b = c.keyCode || c.which; 792 if (c.keyCode == 13) { 793 c.preventDefault(); 794 return false 795 } 796 }); 797 } 798 } 799} 800 801 802 803function filterNavCountryElements() { 804 const search_country = document.getElementById('nav-search-country'); 805 //console.log(search_country, 'search_country') 806 if (search_country) { 807 document.querySelector("#nav-search-country").addEventListener('keyup', function (c) { 808 var value = search_country.value.toLowerCase(); 809 document.querySelectorAll(".wbg_ext_navigation_dropdown li .country-name").forEach(function (e) { 810 811 var length = value.length; 812 var substring = e.textContent.toLowerCase().substring(0, length); 813 //console.log(value, 'dups', substring, e.parentElement); 814 if (value == substring) { 815 e.parentElement.style.display = 'inline-flex'; 816 } else { 817 e.parentElement.style.display = 'none'; 818 } 819 }); 820 }); 821 } 822 823 var eventLists = ["keydown", "keyup", "keypress"]; 824 for (event of eventLists) { 825 var searchCountry = document.querySelector("#nav-search-country") 826 if (searchCountry) { 827 searchCountry.addEventListener(event, function (c) { 828 //console.log(c.keyCode, 'tes', c.which); 829 var b = c.keyCode || c.which; 830 if (c.keyCode == 13) { 831 c.preventDefault(); 832 return false 833 } 834 }); 835 } 836 } 837} 838 839 840 841function clearField(field) { 842 if (field.value == field.defaultValue) 843 field.value = ''; 844} 845 846 847 848 849 850 851function toggleOverflowY() { 852 const current = document.body.style.overflowY; 853 document.body.style.overflowY = current === 'hidden' ? '' : 'hidden'; 854} 855 856 857 858function populateTrendingData(options) { 859 let languageCode = options.languagecode; 860 if (!SUPPORTED_LANGUAGES.includes(languageCode)) { 861 languageCode = 'en'; 862 } 863 let topbarSections = document.querySelector(".wbg_ext_global_header__topbar"); 864 if (topbarSections) { 865 //const host = getHeaderServiceHost(options); 866 //TODO: ONly prod is having the trending data 867 const host = 'extdataportal.worldbank.org'; 868 const currentHost = window.location.hostname; 869 const url = `https://${host}/api/ext/${languageCode}/trending.json?host=${encodeURIComponent(currentHost)}`; 870 fetch(url).then(res => res.json()).then(response => { 871 if (response && response.hasOwnProperty("data")) { 872 let trendingData = response["data"]; 873 let trendingPage = trendingData[Math.floor(Math.random() * trendingData.length)]; 874 875 let trendingText = topbarSections.querySelector(".wbg_ext_global_header__trending-text"); 876 if (trendingText) { 877 trendingText.innerHTML = trendingPage.Text; 878 } 879 880 let trendingLink = topbarSections.querySelector("#trending_link"); 881 if (trendingLink) { 882 trendingLink.setAttribute('href', trendingPage.Link); 883 // keep the accessible name in sync with the visible headline 884 //trendingLink.setAttribute('aria-label', 'Trending Data: ' + trendingPage.Text); 885 } 886 } 887 }).catch(err => console.log(err)); 888 } 889} 890 891/** 892 * Process current pathname and return details for use in language switching 893 * Considers pathnames like /en/path/to/content and 894 * /content/world-bank/global/en/path/to/content.html for both EDS and AEM 895 */ 896function getPathDetails() { 897 const { pathname } = window.location; 898 const isContentPath = pathname.startsWith('/content'); 899 const parts = pathname.split('/'); 900 const safeLangGet = (index) => (parts.length > index ? parts[index] : 'en'); 901 /* 5 is the index of the language in the path for AEM content paths like 902 /content/world-bank/corporate/ext/en/path/to/content.html 903 2 is the index of the language in the path for EDS paths like /en/path/to/content 904 */ 905 let langCode = isContentPath ? safeLangGet(5) : safeLangGet(2); 906 // remove suffix from lang if any 907 if (langCode.indexOf('.') > -1) { 908 langCode = langCode.substring(0, langCode.indexOf('.')); 909 } 910 if (!langCode) langCode = 'en'; // default to en 911 // substring before lang 912 const prefix = pathname.substring(0, pathname.indexOf(`/${langCode}`)) || ''; 913 const suffix = pathname.substring(pathname.indexOf(`/${langCode}`) + langCode.length + 1) || ''; 914 return { 915 prefix, 916 suffix, 917 langCode, 918 isContentPath, 919 }; 920} 921 922/** 923 * Fetch and return language of current page. 924 * @returns language of current page 925 */ 926function getLanguage() { 927 if (!lang) { 928 lang = getPathDetails().langCode;
929 if (!SUPPORTED_LANGUAGES.includes(lang)) { 930 lang = 'en'; 931 } 932 } 933 return lang; 934} 935 936async function getLangTogglerData(options) { 937 let isAuthor = false; 938 let host, environment, currentPath, serviceURL; 939 // Get the current path from the browser 940 if(options.languageToggler && options.languageToggler.langServiceEndpoint){ 941 serviceURL = `${options.languageToggler.langServiceEndpoint}` 942 } else { 943 let currentPath = window.location.pathname; // e.g., "/ext/en/home" 944 if (currentPath.startsWith("/content")) { 945 serviceURL = currentPath.replace(".html", ".wbglangtoggler"); 946 isAuthor = true; 947 } else { 948 if (!currentPath.startsWith('/ext')) { 949 currentPath = `/ext${currentPath}`; 950 } 951 const host = getHeaderServiceHost(options); 952 environment = ""; 953 if(options && options.environment) { 954 if(options.environment != 'stage' && options.environment != 'prod') { 955 environment = options.environment; 956 } 957 } 958 // Construct the full service URL 959 // TODO : dont have the toggler in prod 960 const baseURL = `https://${host}/api/ext/langtoggler`; 961 if(environment) { 962 serviceURL = `${baseURL}${currentPath}?env=${environment}`; 963 } else { 964 serviceURL = `${baseURL}${currentPath}`; 965 } 966 } 967 } 968 969 try { 970 const response = await fetch(serviceURL); 971 if (!response.ok) { 972 throw new Error(`HTTP error! Status: ${response.status}`); 973 } 974 const data = await response.json(); 975 let landdataarr = []; 976 if (data && Array.isArray(data.languages) && data.languages.length > 0) { 977 //const langPairs = metaLangContent.split(constants.COMMA_SEPARATOR); 978 data['languages'].forEach((ldata) => { 979 let hlink = ldata.link; 980 if(isAuthor === true && ldata.authorLink) { 981 if(ldata.authorLink.startsWith("/content")) { 982 hlink = `${ldata.authorLink}.html`; 983 } else { 984 hlink = ldata.authorLink; 985 } 986 }else if (ldata.link.includes("/news/")) { 987 // Non-author URL â ensure path starts with /ext after the domain 988 try { 989 const url = new URL(ldata.link); 990 991 if (!url.pathname.startsWith("/ext/") && url.pathname !== "/ext") { 992 url.pathname = `${url.pathname}`; 993 hlink = url.toString(); 994 } 995 } catch (e) { 996 // Relative link (no domain) 997 if (!ldata.link.startsWith("/ext/") && ldata.link !== "/ext") { 998 hlink = `${ldata.link}`; 999 } 1000 } 1001 } 1002 //const [language, url] = pair.split(constants.PIPE_SEPARATOR).map((part) => part.trim()); 1003 let langdata = { 1004 lang: ldata.title, 1005 langcode: ldata.lang, 1006 href: hlink 1007 } 1008 landdataarr.push(langdata); 1009 }); 1010 } 1011 return landdataarr; 1012 } catch (error) { 1013 console.error("Error calling language toggler service:", error); 1014 return null; 1015 } 1016} 1017 1018function initializeDropdowns() { 1019 const searchInput = document.getElementById("nav-search-country"); 1020 const dropdownToggle = document.querySelector(".wbg_ext_dropdown-toggle"); 1021 const dropdownMenu = document.querySelector(".wbg-ext-tt-menu"); 1022 1023 function toggleDropdown(show = null) { 1024 if (show === true) { 1025 dropdownMenu.classList.add("show"); 1026 dropdownToggle.classList.add("active"); 1027 } else if (show === false) { 1028 dropdownMenu.classList.remove("show"); 1029 dropdownToggle.classList.remove("active"); 1030 } else { 1031 dropdownMenu.classList.toggle("show"); 1032 dropdownToggle.classList.toggle("active"); 1033 } 1034 } 1035 1036 // Toggle dropdown on input click 1037 searchInput.addEventListener("click", function (e) { 1038 e.stopPropagation(); 1039 toggleDropdown(); 1040 }); 1041 1042 // Toggle dropdown on icon click 1043 dropdownToggle.addEventListener("click", function (e) { 1044 e.stopPropagation(); 1045 toggleDropdown(); 1046 }); 1047 1048 // Close dropdown if clicked outside 1049 document.addEventListener("click", function (e) { 1050 if ( 1051 !dropdownMenu.contains(e.target) && 1052 !searchInput.contains(e.target) && 1053 !dropdownToggle.contains(e.target) 1054 ) { 1055 toggleDropdown(false); 1056 } 1057 }); 1058 } 1059 1060function capitalizeFirstLetter(str) { 1061 if (!str) return str; 1062 return str.charAt(0).toUpperCase() + str.slice(1); 1063} 1064// AM26 countdown banner: fetches active event data and renders a dismissible banner above the header. 1065 1066const DEFAULT_ENDPOINT = 'https://wwwstg.worldbank.org/wbg/aem/service/meetingcountdownbanner'; 1067const DISMISS_DURATION_MS = 24 * 60 * 60 * 1000; 1068const CONTAINER_ID = 'wbg-ext-countdown-banner'; 1069const MESSAGE_ID = 'wbg-ext-countdown-message';
1070const DISMISS_STORAGE_KEY = 'wbgCountdownDismissed'; 1071 1072let countdownIntervalId = null; 1073 1074function clearCountdownTimer() { 1075 if (countdownIntervalId) { 1076 clearInterval(countdownIntervalId); 1077 countdownIntervalId = null; 1078 } 1079} 1080 1081function readDismissedMap() { 1082 try { 1083 return JSON.parse(localStorage.getItem(DISMISS_STORAGE_KEY)) || {}; 1084 } catch (error) { 1085 return {}; 1086 } 1087} 1088 1089function isEventDismissed(eventName, nowAbsoluteMs) { 1090 const expiresAt = readDismissedMap()[eventName]; 1091 return typeof expiresAt === 'number' && nowAbsoluteMs < expiresAt; 1092} 1093 1094// Persists across page loads/tabs for 24h (or options.dismissDurationMs); falls back to a no-op if storage is unavailable (private mode/quota). 1095function dismissEvent(eventName, nowAbsoluteMs, durationMs) { 1096 try { 1097 const dismissedMap = readDismissedMap(); 1098 dismissedMap[eventName] = nowAbsoluteMs + (durationMs || DISMISS_DURATION_MS); 1099 localStorage.setItem(DISMISS_STORAGE_KEY, JSON.stringify(dismissedMap)); 1100 } catch (error) { 1101 console.warn('Unable to persist countdown banner dismissal:', error); 1102 } 1103} 1104 1105// Substitutes {days}/{hours}/{minutes}/{seconds} tokens in an API-provided template, e.g. "{days}d / {hours}h / {minutes}m / {seconds}s". 1106// hours/minutes/seconds are zero-padded to 2 digits so the pill's width doesn't jump each time a value crosses 9/10. 1107function formatCountdownTemplate(template, remainingMs) { 1108 const totalSeconds = Math.floor(Math.max(remainingMs, 0) / 1000); 1109 const days = Math.floor(totalSeconds / 86400); 1110 const hours = String(Math.floor((totalSeconds % 86400) / 3600)).padStart(2, '0'); 1111 const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0'); 1112 const seconds = String(totalSeconds % 60).padStart(2, '0'); 1113 return template 1114 .replace(/\{days\}/gi, days) 1115 .replace(/\{hours\}/gi, hours) 1116 .replace(/\{minutes\}/gi, minutes) 1117 .replace(/\{seconds\}/gi, seconds); 1118} 1119 1120// Parses tokens like "UTC+07:00" / "+07:00" / "UTC-04:00" into signed offset minutes. 1121function parseUtcOffsetMinutes(tzString) { 1122 if (!tzString) return 0; 1123 const match = /([+-])(\d{2}):?(\d{2})/.exec(tzString); 1124 if (!match) return 0; 1125 const sign = match[1] === '-' ? -1 : 1; 1126 return sign * (Number(match[2]) * 60 + Number(match[3])); 1127} 1128 1129// A naive timestamp (no offset/Z suffix) is wall-clock time already expressed in the event's own zone. 1130function isNaiveTimestamp(isoString) { 1131 return !/(Z|[+-]\d{2}:?\d{2})$/.test(isoString); 1132} 1133 1134// Converts a naive "YYYY-MM-DDTHH:mm:ss" wall-clock string in a given zone to an absolute epoch ms. 1135function naiveToAbsoluteMs(isoString, offsetMinutes) { 1136 if (!isNaiveTimestamp(isoString)) return Date.parse(isoString); 1137 const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/.exec(isoString); 1138 if (!match) return NaN; 1139 const [, y, mo, d, h, mi, s] = match.map(Number); 1140 return Date.UTC(y, mo - 1, d, h, mi, s) - offsetMinutes * 60000; 1141} 1142 1143// serverTime carries an explicit offset, so parsing it gives the correct absolute instant regardless 1144// of the visitor's local clock/timezone; the Date.now() delta only tracks elapsed time since fetch. 1145function getAbsoluteNowMs(serverTimeIso, fetchLocalTs) { 1146 return Date.parse(serverTimeIso) + (Date.now() - fetchLocalTs); 1147} 1148 1149function getNextEventMidnightDelayMs(nowAbsoluteMs, offsetMinutes) { 1150 const wallNowMs = nowAbsoluteMs + offsetMinutes * 60000; 1151 const wallDate = new Date(wallNowMs); 1152 const nextWallMidnightMs = Date.UTC(wallDate.getUTCFullYear(), wallDate.getUTCMonth(), wallDate.getUTCDate() + 1, 0, 0, 0, 0); 1153 const nextAbsoluteMs = nextWallMidnightMs - offsetMinutes * 60000; 1154 return Math.max(nextAbsoluteMs - nowAbsoluteMs, 60000); 1155} 1156 1157function detectLanguageCode(options) { 1158 if (options && options.languagecode) return options.languagecode.toLowerCase(); 1159 const segments = window.location.pathname.split('/').filter(Boolean); 1160 const candidate = segments.find((seg) => /^[a-z]{2}$/i.test(seg)); 1161 return candidate ? candidate.toLowerCase() : 'en'; 1162} 1163 1164function pickLanguageEntry(data, languageCode) { 1165 return (data || []).find((entry) => entry.language === languageCode) || null; 1166} 1167 1168// Lower priority number wins; bannerStart/bannerEnd are re-checked defensively against stale "status". 1169function pickActiveEvent(events, nowAbsoluteMs) { 1170 const candidates = (events || []).filter((event) => { 1171 if (event.status !== 'active') return false;
1172 const offsetMinutes = parseUtcOffsetMinutes(event.timeZone); 1173 const startMs = naiveToAbsoluteMs(event.bannerStart, offsetMinutes); 1174 const endMs = naiveToAbsoluteMs(event.bannerEnd, offsetMinutes); 1175 return nowAbsoluteMs >= startMs && nowAbsoluteMs <= endMs; 1176 }); 1177 candidates.sort((a, b) => a.priority - b.priority); 1178 return candidates[0] || null; 1179} 1180 1181// Entries wrapped like "/pattern/flags" are regex-tested; anything else is a substring match against the current URL. 1182function isUrlExcluded(excludePaths) { 1183 if (!Array.isArray(excludePaths) || excludePaths.length === 0) return false; 1184 const currentUrl = window.location.href; 1185 return excludePaths.some((entry) => { 1186 const regexMatch = /^\/(.*)\/([a-z]*)$/i.exec(entry); 1187 if (regexMatch) { 1188 try { 1189 return new RegExp(regexMatch[1], regexMatch[2]).test(currentUrl); 1190 } catch (error) { 1191 return false; 1192 } 1193 } 1194 return currentUrl.includes(entry); 1195 }); 1196} 1197 1198function buildMessage(event) { 1199 const template = (event.messages && event.messages[event.phase]) || event.message || ''; 1200 return template.replace(/\{days(Remaining)?\}/gi, event.daysRemaining); 1201} 1202 1203function resolveAsset(event, languageEntry, key) { 1204 return (event[key] && String(event[key]).trim()) || (languageEntry[key] && String(languageEntry[key]).trim()) || ''; 1205} 1206 1207function renderBanner(event, languageEntry, languageCode, serverTimeIso, fetchLocalTs, options) { 1208 const desktopImage = resolveAsset(event, languageEntry, 'desktopImage'); 1209 const mobileImage = resolveAsset(event, languageEntry, 'mobileImage'); 1210 const imageAlt = resolveAsset(event, languageEntry, 'imageAlt'); 1211 const offsetMinutes = parseUtcOffsetMinutes(event.timeZone); 1212 const isTickingCountdown = event.phase === 'countdown' && !!event.eventDate; 1213 const targetMs = isTickingCountdown ? naiveToAbsoluteMs(event.eventDate, offsetMinutes) : null; 1214 const countdownTemplate = (event.messages && event.messages.countdown) || '{days}d / {hours}h / {minutes}m / {seconds}s'; 1215 const mobileMediaQuery = window.matchMedia('(max-width: 767px)'); 1216 // MediaQueryList.matches is live, so re-reading it each call keeps this in sync across resizes. 1217 const getEffectiveCountdownTemplate = () => { 1218 const mobileTemplate = event.messages && event.messages.countdownMobile; 1219 return (mobileMediaQuery.matches && mobileTemplate) ? mobileTemplate : countdownTemplate; 1220 }; 1221 const messageText = isTickingCountdown 1222 ? formatCountdownTemplate(getEffectiveCountdownTemplate(), targetMs - getAbsoluteNowMs(serverTimeIso, fetchLocalTs)) 1223 : buildMessage(event); 1224 1225 const container = document.createElement('div'); 1226 container.id = CONTAINER_ID; 1227 container.className = 'wbg_count_banner'; 1228 container.setAttribute('role', 'region'); 1229 container.setAttribute('aria-label', event.eventName || 'Countdown banner'); 1230 container.setAttribute('dir', languageEntry.rtl ? 'rtl' : 'ltr'); 1231 container.setAttribute('lang', languageCode); 1232 1233 let mediaHtml = ''; 1234 if (desktopImage || mobileImage) { 1235 const desktopSrc = desktopImage || mobileImage; 1236 const mobileSrc = mobileImage || desktopImage; 1237 mediaHtml = ` 1238 <div class="wbg_count_logo_section"> 1239 <img class="wbg_banner_desktop" src="${desktopSrc}" alt="${imageAlt}" loading="eager" fetchpriority="high"> 1240 <img class="wbg_banner_mobile" src="${mobileSrc}" alt="${imageAlt}" loading="eager" fetchpriority="high"> 1241 </div> 1242 <div class="wbg_count_divider"></div>`; 1243 } 1244 1245 container.innerHTML = ` 1246 <div class="wbg_background_image" aria-hidden="true"> 1247 <img src="/content/dam/sites/service/header/img/am/background.svg" alt=""> 1248 </div> 1249 <div class="wbg_count_wrapper"> 1250 <div class="wbg_count_content"> 1251 ${mediaHtml} 1252 <div class="wbg_count_section"> 1253 <p class="wbg_count_message" id="${MESSAGE_ID}"${isTickingCountdown ? ' aria-hidden="true"' : ''}>${messageText}</p> 1254 ${event.annualMeetingText ? `<div class="wbg_count_annual_text">${event.annualMeetingText}</div>` : ''} 1255 <a href="${event.ctaUrl}" class="wbg_count_cta">${event.ctaLabel}</a> 1256 </div> 1257 </div> 1258 <button type="button" class="wbg_count_close" aria-label="Close banner"><span class="lp lp-times" aria-hidden="true"></span></button> 1259 </div>`; 1260 1261 container.querySelector('.wbg_count_close').addEventListener('click', () => { 1262 clearCountdownTimer(); 1263 dismissEvent(event.eventName, getAbsoluteNowMs(serverTimeIso, fetchLocalTs), options && options.dismissDurationMs); 1264 container.remove(); 1265 }); 1266 1267 clearCountdownTimer(); 1268 if (isTickingCountdown) { 1269 countdownIntervalId = setInterval(() => { 1270 const messageEl = document.getElementById(MESSAGE_ID); 1271 if (!messageEl) { 1272 clearCountdownTimer(); 1273 return; 1274 } 1275 const remainingMs = targetMs - getAbsoluteNowMs(serverTimeIso, fetchLocalTs); 1276 messageEl.textContent = formatCountdownTemplate(getEffectiveCountdownTemplate(), remainingMs); 1277 if (remainingMs <= 0) clearCountdownTimer(); 1278 }, 1000); 1279 } 1280 1281 return container; 1282} 1283 1284function mountBanner(container, options) { 1285 const existing = document.getElementById(CONTAINER_ID); 1286 if (existing) { 1287 clearCountdownTimer(); 1288 existing.remove(); 1289 } 1290 const headerSelector = (options && options.headerSelector) || 'header'; 1291 const headerEl = document.querySelector(headerSelector); 1292 if (headerEl) { 1293 headerEl.insertBefore(container, headerEl.firstChild); 1294 } else { 1295 document.body.insertBefore(container, document.body.firstChild); 1296 } 1297} 1298 1299function fetchBannerData(options) { 1300 const endpoint = (options && options.countdownServiceEndpoint) || DEFAULT_ENDPOINT; 1301 const fetchLocalTs = Date.now(); 1302 return fetch(endpoint, { cache: 'no-store' }).then((response) => { 1303 if (!response.ok) throw new Error('HTTP error! Status: ' + response.status); 1304 return response.json(); 1305 }).then((payload) => { 1306 return { payload: payload, fetchLocalTs: fetchLocalTs }; 1307 }); 1308} 1309 1310function refresh(options) { 1311 return fetchBannerData(options).then((result) => { 1312 const payload = result.payload; 1313 const fetchLocalTs = result.fetchLocalTs; 1314 const languageCode = detectLanguageCode(options); 1315 let effectiveLanguageCode = languageCode;
1316 let languageEntry = pickLanguageEntry(payload.data, languageCode); 1317 if (!languageEntry && languageCode !== 'en') { 1318 languageEntry = pickLanguageEntry(payload.data, 'en'); 1319 effectiveLanguageCode = 'en'; 1320 } 1321 if (!languageEntry) { 1322 const existingEntry = document.getElementById(CONTAINER_ID); 1323 if (existingEntry) { 1324 clearCountdownTimer(); 1325 existingEntry.remove(); 1326 } 1327 return null; 1328 } 1329 1330 const nowAbsoluteMs = getAbsoluteNowMs(payload.serverTime, fetchLocalTs); 1331 const activeEvent = pickActiveEvent(languageEntry.events, nowAbsoluteMs); 1332 if (!activeEvent) { 1333 const existingEvent = document.getElementById(CONTAINER_ID); 1334 if (existingEvent) { 1335 clearCountdownTimer(); 1336 existingEvent.remove(); 1337 } 1338 return null; 1339 } 1340 1341 if (isEventDismissed(activeEvent.eventName, nowAbsoluteMs)) { 1342 const existingDismissed = document.getElementById(CONTAINER_ID); 1343 if (existingDismissed) { 1344 clearCountdownTimer(); 1345 existingDismissed.remove(); 1346 } 1347 return { nowAbsoluteMs: nowAbsoluteMs, offsetMinutes: parseUtcOffsetMinutes(activeEvent.timeZone) }; 1348 } 1349 1350 if (isUrlExcluded(activeEvent.excludePaths)) { 1351 const existingExcluded = document.getElementById(CONTAINER_ID); 1352 if (existingExcluded) { 1353 clearCountdownTimer(); 1354 existingExcluded.remove(); 1355 } 1356 return { nowAbsoluteMs: nowAbsoluteMs, offsetMinutes: parseUtcOffsetMinutes(activeEvent.timeZone) }; 1357 } 1358 1359 mountBanner(renderBanner(activeEvent, languageEntry, effectiveLanguageCode, payload.serverTime, fetchLocalTs, options), options); 1360 return { nowAbsoluteMs: nowAbsoluteMs, offsetMinutes: parseUtcOffsetMinutes(activeEvent.timeZone) }; 1361 }); 1362} 1363 1364function scheduleDailyRefresh(options, nowAbsoluteMs, offsetMinutes) { 1365 const delay = getNextEventMidnightDelayMs(nowAbsoluteMs, offsetMinutes) + 2000; 1366 setTimeout(() => initializeCountdownBanner(options), delay); 1367} 1368 1369function initializeCountdownBanner(options) { 1370 return refresh(options).then((result) => { 1371 if (result) scheduleDailyRefresh(options, result.nowAbsoluteMs, result.offsetMinutes); 1372 }).catch((error) => { 1373 console.warn('Error initializing countdown banner:', error); 1374 }); 1375}
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.