PageSourceSearch

https://www.banquemondiale.org/etc.clientlibs/worldbankgroup/compo…/service/headerservice/clientlibs/site.js

js banquemondiale.org collected 2026-09-24 08:47:54 UTC 46,582 bytes, 1,375 lines download raw bytes

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.