1const navToggle = document.querySelector('[data-nav-toggle]'); 2const mobileNav = document.querySelector('[data-mnav]'); 3const mobileNavSheet = mobileNav?.querySelector('[data-mnav-sheet]') || null; 4const dropdowns = [...document.querySelectorAll('.main-menu--desktop [data-dropdown]')]; 5const topbarMenu = document.querySelector('[data-topbar-menu]'); 6const header = document.querySelector('[data-header]'); 7const desktopHeaderBreakpoint = 1280; 8const desktopHeaderTopThreshold = 16; 9const menuOpenLabel = navToggle?.getAttribute('data-label-open') || (document.documentElement.lang === 'en' ? 'Open menu' : 'ááááá£á¡ ááá®á¡áá'); 10const menuCloseLabel = navToggle?.getAttribute('data-label-close') || (document.documentElement.lang === 'en' ? 'Close menu' : 'ááááá£á¡ ááá®á£á áá'); 11 12const syncSiteHeaderOffset = () => { 13 if (!header) { 14 return; 15 } 16 17 header.style.setProperty('--v16-site-header-offset', `${header.offsetHeight}px`); 18}; 19 20if (topbarMenu) { 21 const topbarTray = topbarMenu.querySelector('[data-topbar-tray]'); 22 const topbarTriggers = [...topbarMenu.querySelectorAll('[data-topbar-trigger]')]; 23 const topbarPanels = [...topbarMenu.querySelectorAll('[data-topbar-panel]')]; 24 const topbarCloseButtons = [...topbarMenu.querySelectorAll('[data-topbar-close]')]; 25 const topbarMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); 26 const topbarCloseDuration = () => (topbarMotionQuery.matches ? 0 : 340); 27 const topbarSwitchDuration = () => (topbarMotionQuery.matches ? 0 : 220); 28 let topbarCloseTimer = null; 29 30 const resetTopbarPanels = () => { 31 topbarPanels.forEach((panel) => { 32 panel.classList.remove('is-active', 'is-switching'); 33 panel.setAttribute('hidden', ''); 34 }); 35 }; 36 37 const setTopbarTriggerState = (activeTrigger = null) => { 38 topbarTriggers.forEach((trigger) => { 39 const isActive = trigger === activeTrigger; 40 trigger.classList.toggle('is-active', isActive); 41 trigger.setAttribute('aria-expanded', String(isActive)); 42 }); 43 }; 44 45 const activateTopbarPanel = (panel) => { 46 panel.classList.remove('is-switching'); 47 panel.classList.add('is-active'); 48 panel.removeAttribute('hidden'); 49 }; 50 51 const finalizeTopbarClose = () => { 52 window.clearTimeout(topbarCloseTimer); 53 topbarCloseTimer = null; 54 topbarMenu.classList.remove('is-tray-open', 'is-tray-closing'); 55 topbarTray?.setAttribute('aria-hidden', 'true'); 56 resetTopbarPanels(); 57 setTopbarTriggerState(); 58 syncSiteHeaderOffset(); 59 }; 60 61 const closeTopbarTray = (immediate = false) => { 62 if (!topbarMenu.classList.contains('is-tray-open')) { 63 return; 64 } 65 66 if (immediate || topbarCloseDuration() === 0) { 67 finalizeTopbarClose(); 68 return; 69 } 70 71 topbarMenu.classList.add('is-tray-closing'); 72 topbarMenu.classList.remove('is-tray-open'); 73 syncSiteHeaderOffset(); 74 75 window.clearTimeout(topbarCloseTimer); 76 topbarCloseTimer = window.setTimeout(finalizeTopbarClose, topbarCloseDuration()); 77 }; 78 79 const switchTopbarPanel = (panel, trigger) => { 80 const activePanel = topbarPanels.find((item) => item.classList.contains('is-active')); 81 82 if (activePanel && activePanel !== panel) { 83 activePanel.classList.add('is-switching'); 84 activePanel.classList.remove('is-active'); 85 86 window.setTimeout(() => { 87 activePanel.classList.remove('is-switching'); 88 activePanel.setAttribute('hidden', ''); 89 activateTopbarPanel(panel); 90 setTopbarTriggerState(trigger); 91 syncSiteHeaderOffset(); 92 }, topbarSwitchDuration()); 93 94 return; 95 } 96 97 activateTopbarPanel(panel); 98 setTopbarTriggerState(trigger); 99 syncSiteHeaderOffset(); 100 }; 101 102 const openTopbarTray = (panelId, trigger) => { 103 const panel = topbarMenu.querySelector(`[data-topbar-panel="${panelId}"]`); 104 if (!panel || !topbarTray) { 105 return; 106 } 107 108 window.clearTimeout(topbarCloseTimer); 109 topbarMenu.classList.remove('is-tray-closing'); 110 topbarTray.setAttribute('aria-hidden', 'false'); 111 112 if (topbarMenu.classList.contains('is-tray-open')) { 113 switchTopbarPanel(panel, trigger); 114 return; 115 } 116 117 resetTopbarPanels(); 118 topbarMenu.classList.add('is-tray-open'); 119 activateTopbarPanel(panel); 120 setTopbarTriggerState(trigger); 121 syncSiteHeaderOffset(); 122 }; 123
124 topbarTriggers.forEach((trigger) => { 125 trigger.addEventListener('click', (event) => { 126 event.stopPropagation(); 127 const panelId = trigger.getAttribute('data-topbar-trigger'); 128 if (!panelId) { 129 return; 130 } 131 132 if (trigger.classList.contains('is-active')) { 133 closeTopbarTray(); 134 trigger.focus(); 135 return; 136 } 137 138 openTopbarTray(panelId, trigger); 139 }); 140 }); 141 142 topbarCloseButtons.forEach((button) => { 143 button.addEventListener('click', (event) => { 144 event.stopPropagation(); 145 const activeTrigger = topbarTriggers.find((trigger) => trigger.classList.contains('is-active')); 146 closeTopbarTray(); 147 activeTrigger?.focus(); 148 }); 149 }); 150 151 topbarMenu.addEventListener('keydown', (event) => { 152 if (event.key === 'Escape') { 153 const activeTrigger = topbarTriggers.find((trigger) => trigger.classList.contains('is-active')); 154 closeTopbarTray(); 155 activeTrigger?.focus(); 156 } 157 }); 158 159 topbarMenu.addEventListener('click', (event) => { 160 if (event.target instanceof HTMLAnchorElement) { 161 closeTopbarTray(); 162 } 163 }); 164 165 document.addEventListener('click', (event) => { 166 if (!(event.target instanceof Node) || topbarMenu.contains(event.target)) { 167 return; 168 } 169 170 closeTopbarTray(); 171 }); 172 173 topbarTray?.addEventListener('transitionend', (event) => { 174 if (event.target !== topbarTray) { 175 return; 176 } 177 178 if (event.propertyName === 'grid-template-rows') { 179 syncSiteHeaderOffset(); 180 } 181 }); 182} 183 184const closeDropdowns = (exceptDropdown = null) => { 185 dropdowns.forEach((dropdown) => { 186 if (dropdown === exceptDropdown) { 187 return; 188 } 189 190 // Keep ancestor menus open when expanding a nested submenu. 191 if (exceptDropdown && dropdown.contains(exceptDropdown)) { 192 return; 193 } 194 195 dropdown.classList.remove('is-open'); 196 dropdown.querySelectorAll('[data-dropdown-toggle]').forEach((toggle) => { 197 toggle.setAttribute('aria-expanded', 'false'); 198 }); 199 }); 200}; 201 202dropdowns.forEach((dropdown) => { 203 const toggle = dropdown.querySelector('[data-dropdown-toggle]'); 204 205 toggle?.addEventListener('click', (event) => { 206 event.preventDefault(); 207 event.stopPropagation(); 208 const isOpen = dropdown.classList.toggle('is-open'); 209 toggle.setAttribute('aria-expanded', String(isOpen)); 210 closeDropdowns(isOpen ? dropdown : null); 211 212 if (!isOpen) { 213 dropdown.querySelectorAll('[data-dropdown].is-open').forEach((nested) => { 214 nested.classList.remove('is-open'); 215 nested.querySelector('[data-dropdown-toggle]')?.setAttribute('aria-expanded', 'false'); 216 }); 217 } 218 }); 219 220 dropdown.addEventListener('keydown', (event) => { 221 if (event.key === 'Escape') { 222 dropdown.classList.remove('is-open'); 223 toggle?.setAttribute('aria-expanded', 'false'); 224 toggle?.focus(); 225 } 226 }); 227 228 dropdown.addEventListener('click', (event) => { 229 if (event.target instanceof HTMLAnchorElement) { 230 // Only close sibling menus when navigating; leave parent chain for page leave. 231 closeDropdowns(); 232 } 233 }); 234}); 235 236document.addEventListener('click', (event) => { 237 const target = event.target; 238 if (!(target instanceof Node)) { 239 return; 240 } 241 242 if (target.closest('.main-menu--desktop [data-dropdown]')) { 243 return; 244 } 245 246 closeDropdowns(); 247}); 248 249const setMobileNavOpen = (isOpen) => { 250 if (!mobileNav || !navToggle) { 251 return; 252 } 253 254 mobileNav.classList.toggle('is-open', isOpen); 255 mobileNav.setAttribute('aria-hidden', String(!isOpen)); 256 navToggle.classList.toggle('is-active', isOpen); 257 navToggle.setAttribute('aria-expanded', String(isOpen)); 258 navToggle.setAttribute('aria-label', isOpen ? menuCloseLabel : menuOpenLabel); 259 document.body.classList.toggle('nav-open', isOpen); 260 261 if (isOpen) { 262 const navScroller = mobileNav.querySelector('.v16-mnav__nav'); 263 if (navScroller) { 264 navScroller.scrollTop = 0; 265 } 266 mobileNavSheet?.querySelector('[data-mnav-close]')?.focus({ preventScroll: true });
267 } 268}; 269 270const closeMobileNav = () => setMobileNavOpen(false); 271 272if (navToggle && mobileNav) { 273 navToggle.addEventListener('click', (event) => { 274 event.stopPropagation(); 275 setMobileNavOpen(!mobileNav.classList.contains('is-open')); 276 }); 277 278 mobileNav.querySelectorAll('[data-mnav-close]').forEach((button) => { 279 button.addEventListener('click', closeMobileNav); 280 }); 281 282 mobileNav.addEventListener('click', (event) => { 283 if (event.target instanceof HTMLAnchorElement) { 284 closeMobileNav(); 285 } 286 }); 287 288 // Keep only one top-level accordion open for faster scanning. 289 mobileNav.querySelectorAll(':scope > .v16-mnav__sheet > .v16-mnav__nav > .v16-mnav__acc').forEach((acc) => { 290 acc.addEventListener('toggle', () => { 291 if (!acc.open) { 292 return; 293 } 294 295 mobileNav.querySelectorAll(':scope > .v16-mnav__sheet > .v16-mnav__nav > .v16-mnav__acc').forEach((other) => { 296 if (other !== acc) { 297 other.open = false; 298 } 299 }); 300 }); 301 }); 302 303 document.addEventListener('keydown', (event) => { 304 if (event.key === 'Escape' && mobileNav.classList.contains('is-open')) { 305 closeMobileNav(); 306 navToggle.focus(); 307 } 308 }); 309 310 window.addEventListener('resize', () => { 311 if (window.innerWidth > desktopHeaderBreakpoint) { 312 closeMobileNav(); 313 closeDropdowns(); 314 } 315 }); 316} 317 318syncSiteHeaderOffset(); 319window.addEventListener('resize', syncSiteHeaderOffset, { passive: true }); 320 321if (header) { 322 const resetHeaderAnimation = () => { 323 header.style.setProperty('--header-progress', '0'); 324 header.classList.remove('is-scrolled'); 325 }; 326 327 let headerScrollRaf = 0; 328 const syncHeaderAnimation = () => { 329 if (window.innerWidth <= desktopHeaderBreakpoint) { 330 resetHeaderAnimation(); 331 return; 332 } 333 334 if (window.scrollY > desktopHeaderTopThreshold) { 335 header.style.setProperty('--header-progress', '1'); 336 header.classList.add('is-scrolled'); 337 } else { 338 header.style.setProperty('--header-progress', '0'); 339 header.classList.remove('is-scrolled'); 340 } 341 }; 342 343 const onHeaderScroll = () => { 344 if (headerScrollRaf) { 345 return; 346 } 347 348 headerScrollRaf = window.requestAnimationFrame(() => { 349 headerScrollRaf = 0; 350 syncHeaderAnimation(); 351 }); 352 }; 353 354 syncHeaderAnimation(); 355 window.addEventListener('scroll', onHeaderScroll, { passive: true }); 356 window.addEventListener('resize', () => { 357 if (window.innerWidth <= desktopHeaderBreakpoint) { 358 resetHeaderAnimation(); 359 return; 360 } 361 362 syncHeaderAnimation(); 363 }); 364} 365 366const searchPanel = document.querySelector('[data-search-panel]'); 367const searchInput = document.querySelector('[data-search-input]'); 368const searchResults = document.querySelector('[data-search-results]'); 369const searchEmpty = document.querySelector('[data-search-empty]'); 370const searchOpenButtons = [...document.querySelectorAll('[data-search-open]')]; 371const searchCloseButtons = [...document.querySelectorAll('[data-search-close]')]; 372const heroSearchForms = [...document.querySelectorAll('[data-hero-search]')]; 373let searchIndex = []; 374let searchApiUrl = ''; 375let searchTimer = null; 376let searchRequestId = 0; 377 378try { 379 searchIndex = JSON.parse(document.getElementById('v16-search-index')?.textContent || document.getElementById('v15-search-index')?.textContent || '[]'); 380} catch (error) { 381 searchIndex = []; 382} 383 384try { 385 const searchConfig = JSON.parse(document.getElementById('v16-search-config')?.textContent || '{}'); 386 searchApiUrl = typeof searchConfig.api === 'string' ? searchConfig.api : ''; 387} catch (error) { 388 searchApiUrl = ''; 389} 390 391const searchLocale = document.documentElement.lang === 'en' ? 'en' : 'ka'; 392 393const normalizeSearchValue = (value) => String(value ?? '') 394 .normalize('NFC') 395 .toLocaleLowerCase(searchLocale) 396 .trim(); 397 398const searchHaystackFor = (item) => normalizeSearchValue( 399 `${item.title || ''} ${item.group || ''} ${item.keywords || ''}` 400); 401 402const matchesSearchQuery = (haystack, query) => { 403 if (query === '') { 404 return true; 405 } 406
407 const tokens = query.split(/\s+/).filter(Boolean); 408 if (tokens.length === 0) { 409 return true; 410 } 411 412 return tokens.every((token) => haystack.includes(token)); 413}; 414 415const paintSearchResults = (matches) => { 416 if (!searchResults) { 417 return; 418 } 419 420 searchResults.replaceChildren(...matches.map((item) => { 421 const link = document.createElement('a'); 422 const group = document.createElement('span'); 423 const title = document.createElement('strong'); 424 425 link.href = item.href || '#'; 426 if (/^https?:\/\//i.test(link.href) && !link.href.includes(window.location.host)) { 427 link.target = '_blank'; 428 link.rel = 'noopener noreferrer'; 429 } else if (/\.pdf(?:$|\?)/i.test(link.href)) { 430 link.target = '_blank'; 431 link.rel = 'noopener noreferrer'; 432 } 433 group.textContent = item.group || ''; 434 title.textContent = item.title || ''; 435 link.append(group, title); 436 437 return link; 438 })); 439 440 if (searchEmpty) { 441 searchEmpty.hidden = matches.length > 0; 442 } 443}; 444 445const mergeSearchResults = (primary, secondary, limit = 16) => { 446 const seen = new Set(); 447 const merged = []; 448 449 [...primary, ...secondary].forEach((item) => { 450 const href = String(item?.href || ''); 451 if (!href || seen.has(href)) { 452 return; 453 } 454 seen.add(href); 455 merged.push(item); 456 }); 457 458 return merged.slice(0, limit); 459}; 460 461const renderSearchResults = (query = '') => { 462 if (!searchResults) { 463 return; 464 } 465 466 const normalizedQuery = normalizeSearchValue(query); 467 const newsItems = Array.from(document.querySelectorAll('[data-newsstage-item]')).map(item => ({ 468 title: item.dataset.title || '', 469 href: item.dataset.url || '#', 470 group: 'News' 471 })); 472 const combinedIndex = searchIndex.concat(newsItems); 473 const staticMatches = combinedIndex 474 .filter((item) => matchesSearchQuery(searchHaystackFor(item), normalizedQuery)) 475 .slice(0, 12); 476 477 paintSearchResults(staticMatches); 478 479 clearTimeout(searchTimer); 480 if (!searchApiUrl || normalizedQuery.length < 2) { 481 return; 482 } 483 484 const requestId = ++searchRequestId; 485 searchTimer = window.setTimeout(async () => { 486 try { 487 const response = await fetch(`${searchApiUrl}?q=${encodeURIComponent(query.trim())}`, { 488 headers: { Accept: 'application/json' }, 489 credentials: 'same-origin', 490 }); 491 if (!response.ok || requestId !== searchRequestId) { 492 return; 493 } 494 495 const payload = await response.json(); 496 if (requestId !== searchRequestId) { 497 return; 498 } 499 500 const apiItems = Array.isArray(payload?.items) ? payload.items : []; 501 // Live content first (news/events/docs), then pages/staff. 502 paintSearchResults(mergeSearchResults(apiItems, staticMatches.slice(0, 8), 18)); 503 } catch (error) { 504 // Keep static matches if the live index is unavailable. 505 } 506 }, 280); 507}; 508 509const closeSearch = () => { 510 if (!searchPanel) { 511 return; 512 } 513 514 clearTimeout(searchTimer); 515 searchRequestId += 1; 516 searchPanel.classList.remove('is-open'); 517 searchPanel.setAttribute('aria-hidden', 'true'); 518 document.body.classList.remove('search-open'); 519}; 520 521const openSearch = () => { 522 if (!searchPanel) { 523 return; 524 } 525 526 closeMobileNav(); 527 closeDropdowns(); 528 renderSearchResults(searchInput?.value || ''); 529 searchPanel.classList.add('is-open'); 530 searchPanel.setAttribute('aria-hidden', 'false'); 531 document.body.classList.add('search-open'); 532 window.setTimeout(() => searchInput?.focus(), 60); 533}; 534 535searchOpenButtons.forEach((button) => button.addEventListener('click', openSearch)); 536searchCloseButtons.forEach((button) => button.addEventListener('click', closeSearch));
537heroSearchForms.forEach((form) => { 538 form.addEventListener('submit', (event) => { 539 event.preventDefault(); 540 const heroInput = form.querySelector('[data-hero-search-input]'); 541 542 if (searchInput && heroInput instanceof HTMLInputElement) { 543 searchInput.value = heroInput.value; 544 } 545 546 openSearch(); 547 }); 548}); 549searchInput?.addEventListener('input', () => renderSearchResults(searchInput.value)); 550searchResults?.addEventListener('click', (event) => { 551 const target = event.target; 552 if (target instanceof Element && target.closest('a')) { 553 closeSearch(); 554 } 555}); 556 557document.addEventListener('keydown', (event) => { 558 if (document.querySelector('[data-locale-picker]')) { 559 return; 560 } 561 562 if (event.key === 'Escape') { 563 closeSearch(); 564 } 565 566 if ((event.ctrlKey || event.metaKey) && event.key.toLocaleLowerCase() === 'k') { 567 event.preventDefault(); 568 openSearch(); 569 } 570}); 571 572const V16_LOCALE_COOKIE = 'v16_locale_pref'; 573const V16_LOCALE_COOKIE_TTL = 31536000; 574 575const setV16LocaleCookie = (locale) => { 576 const secure = window.location.protocol === 'https:' ? '; secure' : ''; 577 document.cookie = `${V16_LOCALE_COOKIE}=${locale}; path=/; max-age=${V16_LOCALE_COOKIE_TTL}; samesite=lax${secure}`; 578}; 579 580document.querySelectorAll('[data-v16-locale-set]').forEach((link) => { 581 link.addEventListener('click', () => { 582 const locale = link.getAttribute('data-v16-locale-set'); 583 if (locale === 'ge' || locale === 'ka' || locale === 'en') { 584 setV16LocaleCookie(locale); 585 } 586 }); 587}); 588 589const localePicker = document.querySelector('[data-locale-picker]'); 590 591if (localePicker) { 592 document.body.classList.add('v16-locale-picker-open'); 593 594 const currentLocale = document.body.dataset.v16UrlLocale === 'en' ? 'en' : 'ge'; 595 596 const closeLocalePicker = () => { 597 localePicker.remove(); 598 document.body.classList.remove('v16-locale-picker-open'); 599 }; 600 601 localePicker.querySelectorAll('[data-locale-pick]').forEach((link) => { 602 link.addEventListener('click', (event) => { 603 const locale = link.getAttribute('data-locale-pick'); 604 if (locale !== 'ge' && locale !== 'en') { 605 return; 606 } 607 608 setV16LocaleCookie(locale); 609 610 if (locale === currentLocale) { 611 event.preventDefault(); 612 closeLocalePicker(); 613 } 614 }); 615 }); 616} 617 618document.querySelectorAll('[data-slider]').forEach((slider) => { 619 const slides = [...slider.querySelectorAll('[data-slide]')]; 620 const dots = [...slider.querySelectorAll('[data-slider-dot]')]; 621 const thumbs = [...slider.querySelectorAll('[data-slider-thumb]')]; 622 const sideItems = [...slider.querySelectorAll('[data-slider-side]')]; 623 const previousButton = slider.querySelector('[data-slider-prev]'); 624 const nextButton = slider.querySelector('[data-slider-next]'); 625 626 if (slides.length === 0) { 627 return; 628 } 629 630 let activeIndex = Math.max(0, slides.findIndex((slide) => slide.classList.contains('is-active'))); 631 let timerId; 632 633 const showSlide = (nextIndex) => { 634 activeIndex = (nextIndex + slides.length) % slides.length; 635 636 slides.forEach((slide, index) => { 637 const isActive = index === activeIndex; 638 slide.classList.toggle('is-active', isActive); 639 slide.setAttribute('aria-hidden', String(!isActive)); 640 }); 641 642 dots.forEach((dot, index) => { 643 dot.classList.toggle('is-active', index === activeIndex); 644 dot.setAttribute('aria-selected', String(index === activeIndex)); 645 }); 646 647 thumbs.forEach((thumb, index) => { 648 const isActive = index === activeIndex; 649 thumb.classList.toggle('is-active', isActive); 650 thumb.setAttribute('aria-pressed', String(isActive)); 651 }); 652 653 sideItems.forEach((item, index) => { 654 const isActive = index === activeIndex; 655 item.classList.toggle('is-active', isActive); 656 item.setAttribute('aria-pressed', String(isActive)); 657 }); 658 }; 659 660 const startSlider = () => { 661 window.clearInterval(timerId); 662 663 if (slides.length > 1) { 664 timerId = window.setInterval(() => showSlide(activeIndex + 1), 6500); 665 } 666 }; 667 668 previousButton?.addEventListener('click', () => { 669 showSlide(activeIndex - 1); 670 startSlider(); 671 });
672 673 nextButton?.addEventListener('click', () => { 674 showSlide(activeIndex + 1); 675 startSlider(); 676 }); 677 678 dots.forEach((dot, index) => { 679 dot.addEventListener('click', () => { 680 showSlide(index); 681 startSlider(); 682 }); 683 }); 684 685 thumbs.forEach((thumb, index) => { 686 thumb.addEventListener('click', () => { 687 showSlide(index); 688 startSlider(); 689 }); 690 }); 691 692 sideItems.forEach((item, index) => { 693 item.addEventListener('click', () => { 694 showSlide(index); 695 startSlider(); 696 }); 697 }); 698 699 slider.addEventListener('mouseenter', () => window.clearInterval(timerId)); 700 slider.addEventListener('mouseleave', startSlider); 701 slider.addEventListener('focusin', () => window.clearInterval(timerId)); 702 slider.addEventListener('focusout', startSlider); 703 704 showSlide(activeIndex); 705 startSlider(); 706}); 707 708document.querySelectorAll('[data-newsstage]').forEach((newsstage) => { 709 const storyList = newsstage.querySelector('.tma-news-timeline'); 710 const featureLink = newsstage.querySelector('[data-newsstage-feature-link]'); 711 const featureMedia = newsstage.querySelector('[data-newsstage-feature-media]'); 712 const featureImage = newsstage.querySelector('[data-newsstage-feature-image]'); 713 const featureMeta = newsstage.querySelector('.tma-news-featured__meta'); 714 const featureDate = newsstage.querySelector('[data-newsstage-feature-date]'); 715 const featureTitle = newsstage.querySelector('[data-newsstage-feature-title]'); 716 const featureExcerpt = newsstage.querySelector('[data-newsstage-feature-excerpt]'); 717 const items = [...newsstage.querySelectorAll('[data-newsstage-item]')]; 718 719 if (!storyList || !featureLink || !featureMedia || !featureImage || !featureDate || !featureTitle || !featureExcerpt || items.length === 0) { 720 return; 721 } 722 723 const INTERVAL = 6200; 724 newsstage.style.setProperty('--v16-newsstage-interval', `${INTERVAL}ms`); 725 726 const storyItems = items.filter((i) => i.closest('.tma-news-timeline')); 727 728 const syncFixedHeight = () => { 729 if (window.innerWidth <= 1220) { 730 newsstage.style.removeProperty('--v16-newsstage-fixed-height'); 731 return; 732 } 733 newsstage.style.setProperty('--v16-newsstage-fixed-height', `${storyList.offsetHeight}px`); 734 }; 735 736 const restartBar = (activeItem) => { 737 const bar = activeItem.querySelector('.tma-news-timeline__progress'); 738 if (!bar) return; 739 bar.classList.remove('is-running'); 740 void bar.offsetWidth; 741 bar.classList.add('is-running'); 742 }; 743 744 const crossfadeImage = (src, alt) => { 745 featureImage.style.transition = 'opacity 220ms ease'; 746 featureImage.style.opacity = '0'; 747 const onLoad = () => { 748 featureImage.style.opacity = '1'; 749 }; 750 featureImage.setAttribute('alt', alt); 751 featureImage.setAttribute('src', src); 752 if (featureImage.complete) { 753 onLoad(); 754 } else { 755 featureImage.addEventListener('load', onLoad, { once: true }); 756 } 757 }; 758 759 const syncFeature = (item) => { 760 const url = item.getAttribute('data-url') || '#'; 761 const title = item.getAttribute('data-title') || ''; 762 const date = item.getAttribute('data-date') || ''; 763 const datetime = item.getAttribute('data-datetime') || ''; 764 const excerpt = item.getAttribute('data-excerpt') || ''; 765 const image = item.getAttribute('data-image') || ''; 766 const imageAlt = item.getAttribute('data-image-alt') || ''; 767 const isFallback = item.getAttribute('data-fallback') === 'true'; 768 769 featureLink.setAttribute('href', url); 770 featureLink.setAttribute('aria-label', title); 771 featureTitle.textContent = title; 772 773 if (date !== '') { 774 featureDate.textContent = date; 775 featureDate.setAttribute('datetime', datetime); 776 featureDate.hidden = false; 777 if (featureMeta) featureMeta.hidden = false; 778 } else { 779 featureDate.textContent = ''; 780 featureDate.setAttribute('datetime', ''); 781 featureDate.hidden = true; 782 if (featureMeta) featureMeta.hidden = true; 783 } 784 785 if (excerpt !== '') { 786 featureExcerpt.textContent = excerpt; 787 featureExcerpt.hidden = false; 788 } else { 789 featureExcerpt.textContent = ''; 790 featureExcerpt.hidden = true; 791 } 792 793 crossfadeImage(image, imageAlt); 794 featureMedia.classList.toggle('is-fallback', isFallback); 795
796 items.forEach((button) => { 797 const isActive = (button.getAttribute('data-url') || '#') === url; 798 button.classList.toggle('is-active', isActive); 799 button.setAttribute('aria-pressed', String(isActive)); 800 if (isActive) restartBar(button); 801 }); 802 803 window.requestAnimationFrame(syncFixedHeight); 804 }; 805 806 // Auto-advance 807 let autoTimer = null; 808 809 const stopAuto = () => { 810 if (autoTimer !== null) { 811 window.clearInterval(autoTimer); 812 autoTimer = null; 813 } 814 }; 815 816 const advance = () => { 817 const active = storyItems.find((i) => i.classList.contains('is-active')); 818 const currentIdx = active ? storyItems.indexOf(active) : 0; 819 syncFeature(storyItems[(currentIdx + 1) % storyItems.length]); 820 }; 821 822 const startAuto = () => { 823 stopAuto(); 824 autoTimer = window.setInterval(advance, INTERVAL); 825 }; 826 827 // Click handlers 828 items.forEach((item) => { 829 item.addEventListener('click', () => { 830 stopAuto(); 831 syncFeature(item); 832 startAuto(); 833 }); 834 }); 835 836 // Pause on hover / focus 837 newsstage.addEventListener('mouseenter', stopAuto); 838 newsstage.addEventListener('mouseleave', startAuto); 839 newsstage.addEventListener('focusin', stopAuto); 840 newsstage.addEventListener('focusout', startAuto); 841 842 // Touch / swipe on feature card 843 let touchStartX = 0; 844 let touchStartY = 0; 845 featureLink.addEventListener('touchstart', (e) => { 846 touchStartX = e.touches[0].clientX; 847 touchStartY = e.touches[0].clientY; 848 }, { passive: true }); 849 featureLink.addEventListener('touchend', (e) => { 850 const dx = e.changedTouches[0].clientX - touchStartX; 851 const dy = e.changedTouches[0].clientY - touchStartY; 852 if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 44) { 853 const active = storyItems.find((i) => i.classList.contains('is-active')); 854 const currentIdx = active ? storyItems.indexOf(active) : 0; 855 const nextIdx = dx < 0 856 ? (currentIdx + 1) % storyItems.length 857 : (currentIdx - 1 + storyItems.length) % storyItems.length; 858 stopAuto(); 859 syncFeature(storyItems[nextIdx]); 860 startAuto(); 861 } 862 }); 863 864 // Keyboard navigation on story list (arrow up/down) 865 storyList.addEventListener('keydown', (e) => { 866 if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return; 867 const focused = document.activeElement; 868 const idx = storyItems.indexOf(focused); 869 if (idx === -1) return; 870 e.preventDefault(); 871 const nextIdx = e.key === 'ArrowDown' 872 ? (idx + 1) % storyItems.length 873 : (idx - 1 + storyItems.length) % storyItems.length; 874 storyItems[nextIdx].focus(); 875 stopAuto(); 876 syncFeature(storyItems[nextIdx]); 877 startAuto(); 878 }); 879 880 if ('ResizeObserver' in window) { 881 const resizeObserver = new ResizeObserver(() => syncFixedHeight()); 882 resizeObserver.observe(storyList); 883 resizeObserver.observe(newsstage); 884 } else { 885 window.addEventListener('resize', syncFixedHeight); 886 } 887 888 if (featureImage.complete) { 889 syncFixedHeight(); 890 } else { 891 featureImage.addEventListener('load', syncFixedHeight, { once: true }); 892 } 893 894 // Initialise 895 syncFixedHeight(); 896 const firstStory = storyItems[0]; 897 if (firstStory) restartBar(firstStory); 898 startAuto(); 899}); 900 901 902const initAcademicDirectory = () => { 903 const academicDirectory = document.querySelector('[data-academic-directory]'); 904 if (!academicDirectory) return; 905 906 const searchInput = academicDirectory.querySelector('[data-academic-search]'); 907 const rankButtons = [...academicDirectory.querySelectorAll('[data-rank-filter]')]; 908 const dirButtons = [...academicDirectory.querySelectorAll('[data-dir-filter]')]; 909 const cards = [...academicDirectory.querySelectorAll('[data-academic-card]')]; 910 const groups = [...academicDirectory.querySelectorAll('[data-academic-group]')]; 911 const visibleCountEl = academicDirectory.querySelector('[data-academic-visible-count]'); 912 const emptyState = academicDirectory.querySelector('[data-academic-empty]'); 913 const modal = academicDirectory.querySelector('[data-academic-modal]'); 914 const modalImage = academicDirectory.querySelector('[data-academic-modal-image]'); 915 const modalName = academicDirectory.querySelector('[data-academic-modal-name]'); 916 const modalRole = academicDirectory.querySelector('[data-academic-modal-role]'); 917 const modalEmail = academicDirectory.querySelector('[data-academic-modal-email]'); 918 const modalBio = academicDirectory.querySelector('[data-academic-modal-bio]'); 919 let activeRank = 'all'; 920 let activeDir = 'all'; 921 const noRankToggle = academicDirectory.hasAttribute('data-rank-filter-no-toggle'); 922 const deptDropdown = academicDirectory.querySelector('[data-admin-dept-dropdown]'); 923 const deptDropdownToggle = deptDropdown?.querySelector('[data-admin-dept-dropdown-toggle]'); 924 const deptDropdownMenu = deptDropdown?.querySelector('[data-admin-dept-dropdown-menu]'); 925 const deptCurrentLabel = deptDropdown?.querySelector('[data-admin-dept-current]'); 926 const deptDefaultLabel = deptDropdown?.dataset.adminDeptDefault ?? ''; 927 928 const setDeptDropdownOpen = (open) => { 929 if (!deptDropdown || !deptDropdownToggle || !deptDropdownMenu) return;
930 deptDropdown.classList.toggle('is-open', open); 931 deptDropdownToggle.setAttribute('aria-expanded', String(open)); 932 deptDropdownMenu.hidden = !open; 933 }; 934 935 const updateDeptDropdownLabel = (rankBtn) => { 936 if (!deptCurrentLabel || !rankBtn) return; 937 const val = rankBtn.dataset.rankFilter ?? 'all'; 938 if (val === 'all') { 939 deptCurrentLabel.textContent = deptDefaultLabel; 940 return; 941 } 942 deptCurrentLabel.textContent = rankBtn.dataset.deptLabel ?? deptDefaultLabel; 943 }; 944 945 const normalize = (value) => value.toLowerCase().trim(); 946 947 const applyFilters = () => { 948 const query = normalize(searchInput?.value ?? ''); 949 let visibleCount = 0; 950 951 cards.forEach((card) => { 952 const rank = card.dataset.rank ?? ''; 953 const direction = card.dataset.direction ?? ''; 954 const haystack = normalize(card.dataset.search ?? ''); 955 const matchesRank = activeRank === 'all' || rank === activeRank; 956 const matchesDir = activeDir === 'all' || direction === activeDir; 957 const matchesQuery = query === '' || haystack.includes(query); 958 const isVisible = matchesRank && matchesDir && matchesQuery; 959 card.classList.toggle('is-hidden', !isVisible); 960 if (isVisible) visibleCount += 1; 961 }); 962 963 groups.forEach((group) => { 964 const visibleInGroup = group.querySelectorAll('[data-academic-card]:not(.is-hidden)').length; 965 const countBadge = group.querySelector('[data-academic-group-count]'); 966 if (countBadge) countBadge.textContent = String(visibleInGroup); 967 group.classList.toggle('is-hidden', visibleInGroup === 0); 968 }); 969 970 if (visibleCountEl) visibleCountEl.textContent = String(visibleCount); 971 if (emptyState) emptyState.hidden = visibleCount !== 0; 972 }; 973 974 // Rank filter button click handling via event delegation 975 academicDirectory.addEventListener('click', (event) => { 976 const rankBtn = event.target.closest('[data-rank-filter]'); 977 if (rankBtn) { 978 event.preventDefault(); 979 event.stopPropagation(); 980 const val = rankBtn.dataset.rankFilter ?? 'all'; 981 activeRank = noRankToggle ? val : (activeRank === val ? 'all' : val); 982 rankButtons.forEach((item) => { 983 const isActive = item.dataset.rankFilter === activeRank; 984 item.classList.toggle('is-active', isActive); 985 item.setAttribute('aria-selected', String(isActive)); 986 }); 987 if (deptDropdown && rankBtn.closest('[data-admin-dept-dropdown]')) { 988 updateDeptDropdownLabel(rankBtn); 989 setDeptDropdownOpen(false); 990 } 991 applyFilters(); 992 return; 993 } 994 995 const dirBtn = event.target.closest('[data-dir-filter]'); 996 if (dirBtn) { 997 event.preventDefault(); 998 event.stopPropagation(); 999 const val = dirBtn.dataset.dirFilter ?? 'all'; 1000 activeDir = val; 1001 dirButtons.forEach((item) => { 1002 const isActive = item.dataset.dirFilter === activeDir; 1003 item.classList.toggle('is-active', isActive); 1004 item.setAttribute('aria-selected', String(isActive)); 1005 }); 1006 applyFilters(); 1007 } 1008 }); 1009 1010 searchInput?.addEventListener('input', applyFilters); 1011 1012 deptDropdownToggle?.addEventListener('click', (event) => { 1013 event.preventDefault(); 1014 event.stopPropagation(); 1015 const isOpen = deptDropdown?.classList.contains('is-open'); 1016 setDeptDropdownOpen(!isOpen); 1017 }); 1018 1019 document.addEventListener('click', (event) => { 1020 if (!deptDropdown?.classList.contains('is-open')) return; 1021 if (event.target.closest('[data-admin-dept-dropdown]')) return; 1022 setDeptDropdownOpen(false); 1023 }); 1024 1025 document.addEventListener('keydown', (event) => { 1026 if (event.key === 'Escape') setDeptDropdownOpen(false); 1027 });
1028 1029 const openModal = (member) => { 1030 if (!modal || !member) return; 1031 if (modalImage) modalImage.src = member.image || ''; 1032 if (modalName) modalName.textContent = member.name || ''; 1033 if (modalRole) modalRole.textContent = member.role || ''; 1034 if (modalEmail) { 1035 if (member.email) { 1036 modalEmail.href = `mailto:${member.email}`; 1037 modalEmail.textContent = member.email; 1038 modalEmail.hidden = false; 1039 } else { 1040 modalEmail.hidden = true; 1041 } 1042 } 1043 if (modalBio && member.bio_html) { 1044 modalBio.innerHTML = member.bio_html; 1045 } else if (modalBio && member.bio) { 1046 modalBio.textContent = member.bio; 1047 } 1048 if (typeof modal.showModal === 'function') modal.showModal(); 1049 }; 1050 1051 const closeModal = () => { 1052 if (modal && typeof modal.close === 'function') modal.close(); 1053 }; 1054 1055 academicDirectory.querySelectorAll('[data-academic-open]').forEach((button) => { 1056 button.addEventListener('click', () => { 1057 try { 1058 const memberJson = button.getAttribute('data-member'); 1059 if (memberJson) { 1060 const member = JSON.parse(memberJson); 1061 openModal(member); 1062 } 1063 } catch (error) { 1064 console.error('[Academic Directory] Error parsing member data:', error); 1065 } 1066 }); 1067 }); 1068 1069 academicDirectory.querySelector('[data-academic-close]')?.addEventListener('click', closeModal); 1070 1071 modal?.addEventListener('cancel', (event) => { 1072 event.preventDefault(); 1073 closeModal(); 1074 }); 1075 1076 const photoLightbox = academicDirectory.querySelector('[data-admin-photo-lightbox]'); 1077 const photoImage = academicDirectory.querySelector('[data-admin-photo-image]'); 1078 const photoName = academicDirectory.querySelector('[data-admin-photo-name]'); 1079 const photoRole = academicDirectory.querySelector('[data-admin-photo-role]'); 1080 const modalUnit = academicDirectory.querySelector('[data-admin-modal-unit]'); 1081 const modalEmailRow = academicDirectory.querySelector('[data-admin-modal-email-row]'); 1082 const modalPhoneRow = academicDirectory.querySelector('[data-admin-modal-phone-row]'); 1083 const modalEmailLink = academicDirectory.querySelector('[data-admin-modal-email]'); 1084 const modalPhoneLink = academicDirectory.querySelector('[data-admin-modal-phone]'); 1085 const modalMailBtn = academicDirectory.querySelector('[data-admin-modal-mail-btn]'); 1086 const modalPhoneBtn = academicDirectory.querySelector('[data-admin-modal-phone-btn]'); 1087 const modalMailLabel = academicDirectory.querySelector('[data-admin-modal-mail-label]'); 1088 const modalPhoneLabel = academicDirectory.querySelector('[data-admin-modal-phone-label]'); 1089 1090 const setLinkField = (row, link, href, text) => { 1091 if (!row || !link) return; 1092 const value = (text ?? '').trim(); 1093 if (value === '') { 1094 row.hidden = true; 1095 link.removeAttribute('href'); 1096 link.textContent = ''; 1097 return; 1098 } 1099 row.hidden = false; 1100 link.href = href; 1101 link.textContent = value; 1102 }; 1103 1104 const setActionButton = (button, labelEl, href, label) => { 1105 if (!button || !labelEl) return; 1106 const value = (label ?? '').trim(); 1107 if (value === '') { 1108 button.hidden = true; 1109 button.removeAttribute('href'); 1110 labelEl.textContent = ''; 1111 return; 1112 } 1113 button.hidden = false; 1114 button.href = href; 1115 labelEl.textContent = value; 1116 labelEl.title = value; 1117 }; 1118 1119 const openMemberModal = (member) => { 1120 if (!photoLightbox || !photoImage || !member) return; 1121 1122 const name = member.name ?? ''; 1123 const role = member.role ?? ''; 1124 const email = member.email ?? ''; 1125 const mobile = member.mobile ?? ''; 1126 const image = member.image ?? ''; 1127 const unit = member.unit ?? ''; 1128 1129 if (photoName) photoName.textContent = name; 1130 if (photoRole) photoRole.textContent = role; 1131 if (modalUnit) modalUnit.textContent = unit; 1132 1133 setLinkField(modalEmailRow, modalEmailLink, `mailto:${email}`, email); 1134 setLinkField(modalPhoneRow, modalPhoneLink, `tel:${mobile.replace(/\s+/g, '')}`, mobile); 1135
1136 setActionButton(modalMailBtn, modalMailLabel, `mailto:${email}`, email); 1137 setActionButton(modalPhoneBtn, modalPhoneLabel, `tel:${mobile.replace(/\s+/g, '')}`, mobile); 1138 1139 if (image === '') { 1140 photoImage.removeAttribute('src'); 1141 photoImage.alt = name; 1142 photoImage.hidden = true; 1143 } else { 1144 photoImage.hidden = false; 1145 photoImage.src = image; 1146 photoImage.alt = name; 1147 } 1148 1149 if (typeof photoLightbox.showModal === 'function') { 1150 photoLightbox.showModal(); 1151 } 1152 document.body.classList.add('v16-admin-photo-open'); 1153 }; 1154 1155 const closePhotoLightbox = () => { 1156 if (photoLightbox && typeof photoLightbox.close === 'function') { 1157 photoLightbox.close(); 1158 } 1159 document.body.classList.remove('v16-admin-photo-open'); 1160 }; 1161 1162 academicDirectory.querySelectorAll('[data-admin-member-open]').forEach((button) => { 1163 button.addEventListener('click', () => { 1164 try { 1165 const memberJson = button.getAttribute('data-member'); 1166 if (!memberJson) return; 1167 openMemberModal(JSON.parse(memberJson)); 1168 } catch (error) { 1169 console.error('[Admin Directory] Error parsing member data:', error); 1170 } 1171 }); 1172 }); 1173 1174 photoLightbox?.querySelector('[data-admin-photo-close]')?.addEventListener('click', closePhotoLightbox); 1175 1176 photoLightbox?.addEventListener('click', (event) => { 1177 if (event.target === photoLightbox) { 1178 closePhotoLightbox(); 1179 } 1180 }); 1181 1182 photoLightbox?.addEventListener('cancel', (event) => { 1183 event.preventDefault(); 1184 closePhotoLightbox(); 1185 }); 1186 1187 const applyStaffDeepLink = () => { 1188 const params = new URLSearchParams(window.location.search); 1189 const staffId = params.get('staff'); 1190 const query = params.get('q'); 1191 1192 if (query && searchInput) { 1193 searchInput.value = query; 1194 applyFilters(); 1195 } 1196 1197 if (!staffId) { 1198 return; 1199 } 1200 1201 const openButtons = academicDirectory.querySelectorAll('[data-academic-open], [data-admin-member-open]'); 1202 for (const button of openButtons) { 1203 try { 1204 const member = JSON.parse(button.getAttribute('data-member') || 'null'); 1205 if (!member || String(member.id) !== staffId) { 1206 continue; 1207 } 1208 1209 const card = button.closest('[data-academic-card]'); 1210 card?.scrollIntoView({ behavior: 'smooth', block: 'center' }); 1211 button.click(); 1212 break; 1213 } catch (error) { 1214 // Ignore malformed member payloads. 1215 } 1216 } 1217 }; 1218 1219 applyFilters(); 1220 applyStaffDeepLink(); 1221}; 1222 1223// Run initialization when DOM is ready 1224if (document.readyState === 'loading') { 1225 document.addEventListener('DOMContentLoaded', initAcademicDirectory); 1226} else { 1227 // DOM is already loaded (script is deferred), so init immediately 1228 initAcademicDirectory(); 1229}
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.