1(function () { 2 'use strict'; 3 4 /* ------------------------------------------------------------- 5 Scroll progress bar 6 ------------------------------------------------------------- */ 7 const progressBar = document.querySelector('.scroll-progress__bar'); 8 const nav = document.querySelector('.nav'); 9 10 function onScroll() { 11 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 12 const docHeight = document.documentElement.scrollHeight - window.innerHeight; 13 const pct = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0; 14 15 if (progressBar) progressBar.style.width = pct + '%'; 16 17 if (nav) { 18 if (scrollTop > 40) nav.classList.add('is-scrolled'); 19 else nav.classList.remove('is-scrolled'); 20 } 21 } 22 window.addEventListener('scroll', onScroll, { passive: true }); 23 onScroll(); 24 25 /* ------------------------------------------------------------- 26 Hero scroll cue â smooth scroll to next section 27 ------------------------------------------------------------- */ 28 const heroScroll = document.querySelector('.hero__scroll'); 29 if (heroScroll) { 30 heroScroll.addEventListener('click', function (event) { 31 const href = heroScroll.getAttribute('href'); 32 if (!href || href.charAt(0) !== '#') return; 33 34 const target = document.querySelector(href); 35 if (!target) return; 36 37 event.preventDefault(); 38 target.scrollIntoView({ behavior: 'smooth', block: 'start' }); 39 40 if (history.replaceState) { 41 history.replaceState(null, '', href); 42 } 43 }); 44 } 45 46 /* ------------------------------------------------------------- 47 Mobile nav â hamburger + sub-menu dropdowns 48 ------------------------------------------------------------- */ 49 const navToggle = document.querySelector('.nav__toggle'); 50 const navMenu = document.querySelector('.nav__menu'); 51 52 if (navToggle && navMenu) { 53 navToggle.addEventListener('click', function () { 54 const isOpen = navMenu.classList.toggle('is-open'); 55 navToggle.setAttribute('aria-expanded', String(isOpen)); 56 navToggle.setAttribute('aria-label', isOpen ? 'Close menu' : 'Open menu'); 57 }); 58 59 navMenu.querySelectorAll('a').forEach(function (link) { 60 link.addEventListener('click', function () { 61 navMenu.classList.remove('is-open'); 62 navToggle.setAttribute('aria-expanded', 'false'); 63 navToggle.setAttribute('aria-label', 'Open menu'); 64 }); 65 }); 66 } 67 68 /* Sub-menu dropdowns (index.html uses its own nav, not layout.js) */ 69 document.querySelectorAll('.nav__sub-toggle').forEach(function (btn) { 70 btn.addEventListener('click', function () { 71 var open = btn.getAttribute('aria-expanded') === 'true'; 72 /* Close any other open sub-menus first */ 73 document.querySelectorAll('.nav__sub-toggle[aria-expanded="true"]').forEach(function (other) { 74 if (other !== btn) { 75 other.setAttribute('aria-expanded', 'false'); 76 other.parentElement.classList.remove('is-open'); 77 } 78 }); 79 btn.setAttribute('aria-expanded', String(!open)); 80 btn.parentElement.classList.toggle('is-open', !open); 81 }); 82 }); 83 84 /* Close sub-menus when clicking outside the nav */ 85 document.addEventListener('click', function (e) { 86 if (!e.target.closest('.nav__inner')) { 87 document.querySelectorAll('.nav__sub-toggle[aria-expanded="true"]').forEach(function (btn) { 88 btn.setAttribute('aria-expanded', 'false'); 89 btn.parentElement.classList.remove('is-open'); 90 }); 91 } 92 }); 93 94 /* ------------------------------------------------------------- 95 SDG accordion â close others on open 96 ------------------------------------------------------------- */ 97 const sdgItems = document.querySelectorAll('.sdg-grid .sdg'); 98 sdgItems.forEach(function (item) { 99 item.addEventListener('toggle', function () { 100 if (!item.open) return; 101 sdgItems.forEach(function (other) { 102 if (other !== item && other.open) other.open = false; 103 }); 104 }); 105 }); 106 107 /* ------------------------------------------------------------- 108 Reveal-on-scroll using IntersectionObserver 109 ------------------------------------------------------------- */ 110 const revealEls = document.querySelectorAll('.reveal'); 111 112 function revealAll() {
113 revealEls.forEach(function (el) { el.classList.add('is-visible'); }); 114 } 115 116 if ('IntersectionObserver' in window && revealEls.length) { 117 const io = new IntersectionObserver(function (entries) { 118 entries.forEach(function (entry) { 119 if (entry.isIntersecting) { 120 entry.target.classList.add('is-visible'); 121 io.unobserve(entry.target); 122 } 123 }); 124 }, { threshold: 0.05, rootMargin: '0px 0px -40px 0px' }); 125 126 revealEls.forEach(function (el) { io.observe(el); }); 127 128 /* Safety net: if anything is still hidden after 1.5 s, reveal it. 129 Guards against observer not firing (e.g. certain file:// environments). */ 130 setTimeout(function () { 131 revealEls.forEach(function (el) { 132 if (!el.classList.contains('is-visible')) { 133 el.classList.add('is-visible'); 134 } 135 }); 136 }, 1500); 137 } else { 138 revealAll(); 139 } 140 141 /* ------------------------------------------------------------- 142 Newsletter signup 143 ------------------------------------------------------------- */ 144 const signupForm = document.querySelector('.signup'); 145 if (signupForm) { 146 signupForm.addEventListener('submit', function (e) { 147 e.preventDefault(); 148 const input = signupForm.querySelector('input[type="email"]'); 149 if (!input || !input.value) return; 150 151 /* Basic email format check before doing anything with the value */ 152 const emailVal = input.value.trim(); 153 if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(emailVal)) { 154 input.setAttribute('aria-invalid', 'true'); 155 input.style.borderColor = '#d73a49'; 156 return; 157 } 158 input.setAttribute('aria-invalid', 'false'); 159 input.style.borderColor = ''; 160 161 /* 162 * TODO: POST to your newsletter endpoint here. 163 * Example using Mailchimp, ConvertKit, or a custom backend: 164 * 165 * fetch('/api/subscribe', { 166 * method: 'POST', 167 * headers: { 'Content-Type': 'application/json' }, 168 * body: JSON.stringify({ email: emailVal }) 169 * }).then(r => r.ok ? showSuccess() : showError()); 170 * 171 * Until a backend endpoint is wired up, we show a friendly message. 172 */ 173 input.value = ''; 174 input.placeholder = 'â Thanks â we will be in touch'; 175 setTimeout(function () { 176 input.placeholder = '[email protected]'; 177 }, 3500); 178 }); 179 } 180 181 /* ------------------------------------------------------------- 182 Home video â portable paths, legacy Instagram save fallback 183 ------------------------------------------------------------- */ 184 const _themeUri = (typeof agewatchData !== 'undefined' && agewatchData.themeUri) 185 ? agewatchData.themeUri.replace(/\/+$/, '') 186 : ''; 187 const HOME_VIDEO_SOURCES = [ 188 _themeUri + '/videos/AQNzCfBVHGy9dUI9cBeWIz_CBgxgwyUKfVIFu4rOQjhCXd_4f_3N1xIRNnKS.mp4', 189 _themeUri + '/videos/home-impact.mp4', 190 _themeUri + '/videos/life-with-agewatch.mp4' 191 ]; 192 const HOME_VIDEO_POSTERS = [ 193 _themeUri + '/images/agewatch-home-video-first-frame.png', 194 _themeUri + '/images/ph_70939_283517.jpg' 195 ]; 196 197 function mediaUrl(relativePath) { 198 return new URL(relativePath, window.location.href).href; 199 } 200 201 function probeVideoSrc(url) { 202 return new Promise(function (resolve) { 203 const probe = document.createElement('video'); 204 let settled = false; 205 206 function finish(ok) { 207 if (settled) return; 208 settled = true; 209 clearTimeout(timer); 210 probe.removeAttribute('src'); 211 probe.load(); 212 resolve(ok); 213 } 214 215 const timer = setTimeout(function () { finish(false); }, 10000); 216 probe.preload = 'metadata'; 217 probe.addEventListener('loadedmetadata', function () { finish(true); }, { once: true }); 218 probe.addEventListener('error', function () { finish(false); }, { once: true }); 219 probe.src = url; 220 }); 221 } 222 223 function probePoster(url) { 224 return new Promise(function (resolve) { 225 const img = new Image(); 226 const timer = setTimeout(function () { resolve(false); }, 5000); 227 img.onload = function () { 228 clearTimeout(timer); 229 resolve(true); 230 }; 231 img.onerror = function () { 232 clearTimeout(timer); 233 resolve(false); 234 }; 235 img.src = url; 236 }); 237 } 238 239 async function attachHomeVideo(video, player) { 240 const notice = player.querySelector('.instagram-player__notice'); 241 242 if (video.readyState >= 1 && !video.error) { 243 if (notice) notice.hidden = true; 244 player.classList.remove('is-unavailable'); 245 return true; 246 } 247 248 video.preload = 'auto'; 249 250 for (let i = 0; i < HOME_VIDEO_SOURCES.length; i += 1) { 251 const url = mediaUrl(HOME_VIDEO_SOURCES[i]); 252 if (!(await probeVideoSrc(url))) continue; 253 254 video.src = url; 255 video.dataset.videoSourceIndex = String(i); 256 video.removeAttribute('data-src'); 257 258 for (let j = 0; j < HOME_VIDEO_POSTERS.length; j += 1) { 259 const posterUrl = mediaUrl(HOME_VIDEO_POSTERS[j]); 260 if (await probePoster(posterUrl)) { 261 video.poster = posterUrl; 262 break; 263 } 264 } 265 266 video.load(); 267 if (notice) notice.hidden = true; 268 player.classList.remove('is-unavailable'); 269 return true; 270 } 271 272 player.classList.add('is-unavailable'); 273 if (notice) notice.hidden = false;
274 return false; 275 } 276 277 async function tryAlternateHomeVideoSource(video, player) { 278 const notice = player.querySelector('.instagram-player__notice'); 279 const currentIndex = Number(video.dataset.videoSourceIndex || 0); 280 281 for (let i = currentIndex + 1; i < HOME_VIDEO_SOURCES.length; i += 1) { 282 const url = mediaUrl(HOME_VIDEO_SOURCES[i]); 283 if (!(await probeVideoSrc(url))) continue; 284 285 video.src = url; 286 video.dataset.videoSourceIndex = String(i); 287 video.load(); 288 video.play().catch(function () { 289 // If autoplay is blocked, the user can still manually start playback. 290 }); 291 player.classList.remove('is-unavailable'); 292 if (notice) notice.hidden = true; 293 return true; 294 } 295 296 player.classList.add('is-unavailable'); 297 if (notice) notice.hidden = false; 298 return false; 299 } 300 301 const instaPlayers = document.querySelectorAll('.instagram-player'); 302 instaPlayers.forEach(function (player) { 303 const video = player.querySelector('video'); 304 if (!video) return; 305 306 attachHomeVideo(video, player); 307 308 video.addEventListener('play', function () { 309 player.classList.add('is-playing'); 310 }); 311 312 video.addEventListener('pause', function () { 313 player.classList.remove('is-playing'); 314 }); 315 316 video.addEventListener('error', function () { 317 tryAlternateHomeVideoSource(video, player); 318 }); 319 }); 320 321 /* ------------------------------------------------------------- 322 Our Services hero â slideshow (carousel) 323 ------------------------------------------------------------- */ 324 (function initServicesHeroSlideshow() { 325 const root = document.querySelector('[data-services-hero-slideshow]'); 326 if (!root) return; 327 328 const track = root.querySelector('.services-hero-slideshow__track'); 329 const slides = root.querySelectorAll('.services-hero-slideshow__slide'); 330 const prevBtn = root.querySelector('[data-slideshow-prev]'); 331 const nextBtn = root.querySelector('[data-slideshow-next]'); 332 const dots = root.querySelectorAll('[data-slideshow-dot]'); 333 const statusEl = root.querySelector('.services-hero-slideshow__status'); 334 335 if (!track || !slides.length || slides.length !== dots.length) return; 336 337 const n = slides.length; 338 let index = 0; 339 let timer = null; 340 const intervalMs = 6400; 341 342 function setTransform() { 343 const pct = (100 / n) * index; 344 track.style.transform = 'translateX(-' + pct + '%)'; 345 } 346 347 function updateUI() { 348 setTransform(); 349 dots.forEach(function (d, k) { 350 const on = k === index; 351 d.classList.toggle('is-active', on); 352 d.setAttribute('aria-selected', String(on)); 353 }); 354 if (statusEl) { 355 statusEl.textContent = 'Slide ' + (index + 1) + ' of ' + n; 356 } 357 } 358 359 function go(i) { 360 index = (i + n) % n; 361 updateUI(); 362 } 363 364 function nextSlide() { 365 go(index + 1); 366 } 367 368 function prevSlide() { 369 go(index - 1); 370 } 371 372 function startTimer() { 373 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 374 stopTimer(); 375 timer = window.setInterval(nextSlide, intervalMs); 376 } 377 378 function stopTimer() { 379 if (timer) { 380 window.clearInterval(timer); 381 timer = null; 382 } 383 } 384 385 if (prevBtn) prevBtn.addEventListener('click', function () { prevSlide(); startTimer(); }); 386 if (nextBtn) nextBtn.addEventListener('click', function () { nextSlide(); startTimer(); }); 387 388 dots.forEach(function (d, k) { 389 d.addEventListener('click', function () { 390 go(k); 391 startTimer(); 392 }); 393 }); 394 395 root.addEventListener('mouseenter', stopTimer); 396 root.addEventListener('mouseleave', startTimer); 397 398 document.addEventListener('visibilitychange', function () { 399 if (document.hidden) stopTimer(); 400 else startTimer(); 401 }); 402 403 root.tabIndex = 0; 404 root.addEventListener('keydown', function (e) { 405 if (e.key === 'ArrowLeft') { 406 e.preventDefault(); 407 prevSlide(); 408 startTimer(); 409 } else if (e.key === 'ArrowRight') { 410 e.preventDefault(); 411 nextSlide(); 412 startTimer(); 413 } 414 }); 415 416 updateUI(); 417 startTimer(); 418 })(); 419 420 /* ------------------------------------------------------------- 421 Contact form â prefill from ?topic= (partner resources) 422 ------------------------------------------------------------- */ 423 (function () { 424 var params = new URLSearchParams(window.location.search); 425 var topic = params.get('topic'); 426 if (!topic) return; 427 428 topic = String(topic).replace(/[^a-zA-Z0-9_-]/g, ''); 429 430 var enquiry = document.getElementById('enquiry'); 431 var subject = document.getElementById('subject'); 432 var message = document.getElementById('message'); 433 if (!enquiry) return; 434 435 var topics = { 436 brief: { 437 subject: 'Request: Organisational brief', 438 message: 'Hello AgeWatch team,\n\nPlease send the latest one-page organisational brief (PDF). I am reviewing AgeWatch for a potential partnership.\n\nOrganisation:\nTimeline:\n\nThank you.' 439 }, 440 pitch: { 441 subject: 'Request: Donor pitch deck', 442 message: 'Hello AgeWatch team,\n\nPlease share your donor pitch deck â including programme reach, team, and stewardship narrative.\n\nI am particularly interested in (ElderCare / Futures / both):\n\nThank you.' 443 }, 444 partnerships: { 445 subject: 'Partnership / CSR enquiry', 446 message: 'Hello AgeWatch partnerships team,\n\nWe are exploring a CSR or sponsorship partnership. Please send your partnership prospectus and tier options.\n\nCompany / foundation:\nPreferred engagement (event sponsor / cohort / grant / in-kind):\n\nThank you.' 447 }, 448 toc: { 449 subject: 'Request: Theory of change', 450 message: 'Hello AgeWatch team,\n\nPlease share your theory-of-change document aligned to our reporting framework.\n\nAgency / foundation:\nProgramme area of interest:\n\nThank you.' 451 }, 452 impact: { 453 subject: 'Request: Annual impact report', 454 message: 'Hello AgeWatch team,\n\nPlease send your latest annual impact report (including programme highlights and financial summary).\n\nThank you.' 455 }, 456 media: { 457 subject: 'Media enquiry â AgeWatch Africa', 458 message: 'Hello AgeWatch press team,\n\nI am working on a story about AgeWatch and would like to request a spokesperson interview and high-resolution photos.\n\nOutlet:\nDeadline:\nAngle:\n\nThank you.' 459 }, 460 care: { 461 subject: 'Elder care enquiry', 462 message: 'Hello AgeWatch team,\n\nI am enquiring about care for a loved one. Please advise on home, day, or residential options.\n\nCounty:\nBrief situation:\n\nThank you.' 463 }, 464 training: { 465 subject: 'Caregiver training enquiry', 466 message: 'Hello AgeWatch team,\n\nI am interested in caregiver training or hosting a Best-Practice Care Interventions cohort.\n\nOrganisation / county:\n\nThank you.' 467 }, 468 'inclusive-care': { 469 subject: 'Inclusive care for a person with disability', 470 message: 'Hello AgeWatch team,\n\nI am enquiring about care for an older person (or family member) living with a disability. Please advise on home, day, or residential options and any assistive support available.\n\nCounty:\nMobility / sensory / cognitive needs (brief):\n\nThank you.' 471 }, 472 'plwd-support': { 473 subject: 'Support AgeWatch inclusive care programmes',
474 message: 'Hello AgeWatch team,\n\nI would like to support your work with elders and community members living with disabilities (donation, partnership, or in-kind).\n\nHow I hope to help:\n\nThank you.' 475 } 476 }; 477 478 var preset = topics[topic]; 479 if (!preset) return; 480 481 function escapeCssAttribute(value) { 482 if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') { 483 return CSS.escape(String(value)); 484 } 485 return String(value).replace(/(["'\\])/g, '\\$1'); 486 } 487 488 var option = enquiry.querySelector('option[value="' + escapeCssAttribute(topic) + '"]'); 489 if (option) enquiry.value = topic; 490 if (subject && !subject.value) subject.value = preset.subject; 491 if (message && !message.value) message.value = preset.message; 492 493 var form = document.getElementById('contact-form'); 494 if (form) form.scrollIntoView({ behavior: 'smooth', block: 'start' }); 495 })(); 496 497 (function () { 498 var form = document.getElementById('contact-form'); 499 if (!form) return; 500 501 function isValidEmail(value) { 502 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || '').trim()); 503 } 504 505 form.addEventListener('submit', function (event) { 506 var nameInput = form.querySelector('#name'); 507 var emailInput = form.querySelector('#email-contact'); 508 var subjectInput = form.querySelector('#subject'); 509 var messageInput = form.querySelector('#message'); 510 var errors = []; 511 512 if (!nameInput || !String(nameInput.value || '').trim()) { 513 errors.push('Please enter your name.'); 514 } 515 if (!emailInput || !isValidEmail(emailInput.value)) { 516 errors.push('Please enter a valid email address.'); 517 } 518 if (!subjectInput || !String(subjectInput.value || '').trim()) { 519 errors.push('Please enter a subject.'); 520 } 521 if (!messageInput || !String(messageInput.value || '').trim()) { 522 errors.push('Please enter a message.'); 523 } 524 525 if (errors.length) { 526 event.preventDefault(); 527 alert(errors.join('\n')); 528 [nameInput, emailInput, subjectInput, messageInput].forEach(function (field) { 529 if (field) { 530 field.setAttribute('aria-invalid', String(!field.value.trim())); 531 } 532 }); 533 if (nameInput && !nameInput.value.trim()) { 534 nameInput.focus(); 535 } 536 } 537 }); 538 })(); 539 540 /* ------------------------------------------------------------- 541 Back-button history support 542 All links with class .gpp-back, .ec-breadcrumb, or .pillar-back 543 use history.back() so users always return to the page they came 544 from, regardless of which page linked them here. 545 The href attribute is kept as a fallback for direct navigation, 546 new-tab opens, and JS-disabled environments. 547 ------------------------------------------------------------- */ 548 document.querySelectorAll('.gpp-back, .ec-breadcrumb, .pillar-back').forEach(function (link) { 549 if (link.tagName !== 'A') return; 550 link.addEventListener('click', function (e) { 551 if (history.length > 1) { 552 e.preventDefault(); 553 history.back(); 554 } 555 }); 556 }); 557 558 /* Partner hub â highlight nav pill for current section */ 559 (function () { 560 var hubNav = document.querySelector('.partner-hub-nav'); 561 if (!hubNav || !('IntersectionObserver' in window)) return; 562 563 var links = hubNav.querySelectorAll('.partner-hub-nav__link'); 564 var sections = []; 565 links.forEach(function (link) { 566 var id = link.getAttribute('href'); 567 if (!id || id.charAt(0) !== '#') return; 568 var el = document.querySelector(id); 569 if (el) sections.push({ link: link, el: el }); 570 }); 571 if (!sections.length) return; 572 573 var io = new IntersectionObserver(function (entries) { 574 entries.forEach(function (entry) { 575 if (!entry.isIntersecting) return; 576 links.forEach(function (l) { l.classList.remove('is-active'); }); 577 var match = sections.find(function (s) { return s.el === entry.target; }); 578 if (match) match.link.classList.add('is-active'); 579 }); 580 }, { rootMargin: '-30% 0px -55% 0px', threshold: 0 }); 581 582 sections.forEach(function (s) { io.observe(s.el); }); 583 })(); 584})();
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.