PageSourceSearch

https://agewatchafrica.com/wp-content/themes/agewatch-new-theme/main.js?ver=1.8.6

js agewatchafrica.com collected 2026-10-05 21:50:04 UTC 21,278 bytes, 584 lines download raw bytes

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.