PageSourceSearch

https://www.soschildrensvillages.ca/sites/default/files/asset_inje…-6e831a101dd9550c4f19f5839d2fc8b0.js?tlf1xb

js soschildrensvillages.ca collected 2026-09-24 21:05:43 UTC 14,976 bytes, 402 lines download raw bytes

1/* ==========================================================================
2   MAKE Digital — SOS Children's Villages Canada homepage behaviors
3   --------------------------------------------------------------------------
4   What this file does:
5     1. Hero slider — wraps .field--field-image-slider in Glide markup,
6        then instantiates Glide with fade transitions on desktop and slide
7        transitions on mobile. Auto-advances every 6s.
8     2. Emergencies carousel — wraps .field--field-emergencies-cards in
9        Glide markup and instantiates a peek-style carousel with arrows
10        on desktop, swipe on mobile.
11     3. Support cards — adds touch/click toggle for mobile so the reveal
12        works without hover.
13     4. Inserts chevron/arrow SVGs into Glide controls.
14   --------------------------------------------------------------------------
15   Dependencies (already loaded by the Drupal theme):
16     - @glidejs/glide (window.Glide)
17   This file is defensive: if Glide isn't available (e.g. during a cache
18   rebuild), the fallback CSS scroll-snap keeps the carousels usable.
19   ========================================================================== */
20
21(function () {
22  'use strict';
23
24  console.log('[MAKE home] script loaded');
25
26  // -------- Utilities --------
27  function ready(fn) {
28    if (document.readyState !== 'loading') fn();
29    else document.addEventListener('DOMContentLoaded', fn);
30  }
31
32  function svgChevron(direction) {
33    // direction: 'left' or 'right'
34    var d = direction === 'left' ? 'M15 18L9 12l6-6' : 'M9 6l6 6-6 6';
35    return (
36      '<svg viewBox="0 0 24 24" aria-hidden="true">' +
37      '<path d="' + d + '"></path>' +
38      '</svg>'
39    );
40  }
41
42  function isMobile() {
43    return window.matchMedia('(max-width: 767px)').matches;
44  }
45
46  // -------- 1. Hero slider --------
47  function initHeroSlider() {
48    // The Drupal field that contains the hero slides. The layout div
49    // around it does NOT have a MAKE-home-slider class (unlike the other
50    // sections), so we target the field by its type-specific class directly.
51    var fieldWrap = document.querySelector('.field--field-image-slider');
52    if (!fieldWrap) return;
53
54    // Mark the containing layout with a class so the CSS can scope to it
55    // (mirrors what the other sections do via Layout Builder admin classes)
56    var section = fieldWrap.closest('.layout--onecol') || fieldWrap.parentNode;
57    if (section && !section.classList.contains('MAKE-home-slider')) {
58      section.classList.add('MAKE-home-slider');
59    }
60
61    // Within the field is a .field-container with multiple .field-content
62    // children — each containing one .paragraph--type--home-slider. We need
63    // to turn that into Glide's expected structure:
64    //   .glide > .glide__track[data-glide-el=track] > .glide__slides
65    //     > .glide__slide * N
66    //   + .glide__arrows * (prev/next)
67    var slideHost = fieldWrap.querySelector('.field-container');
68    if (!slideHost) return;
69
70    var slideDivs = Array.prototype.slice.call(
71      slideHost.querySelectorAll(':scope > .field-content')
72    );
73    if (slideDivs.length < 2) return; // No need to init if there's only one slide
74
75    // Build Glide DOM around the existing slides
76    fieldWrap.classList.add('glide');
77
78    var track = document.createElement('div');
79    track.className = 'glide__track';
80    track.setAttribute('data-glide-el', 'track');
81
82    var slidesUl = document.createElement('ul');
83    slidesUl.className = 'glide__slides';
84
85    slideDivs.forEach(function (div) {
86      var li = document.createElement('li');
87      li.className = 'glide__slide';
88      li.appendChild(div);
89      slidesUl.appendChild(li);
90    });
91
92    track.appendChild(slidesUl);
93
94    // Clear the original host and append the Glide structure to the fieldWrap
95    slideHost.parentNode.removeChild(slideHost);
96    fieldWrap.appendChild(track);
97
98    // Arrows
99    var arrows = document.createElement('div');
100    arrows.className = 'glide__arrows';
101    arrows.setAttribute('data-glide-el', 'controls');
102    arrows.innerHTML =
103      '<button class="glide__arrow glide__arrow--left" data-glide-dir="<" aria-label="Previous slide">' +
104      svgChevron('left') +
105      '</button>' +
106      '<button class="glide__arrow glide__arrow--right" data-glide-dir=">" aria-label="Next slide">' +
107      svgChevron('right') +
108      '</button>';
109    fieldWrap.appendChild(arrows);
110
111    if (typeof window.Glide !== 'function') return; // Graceful degradation
112
113    // Desktop = fade (via CSS), mobile = native slide. Both use type 'slider'
114    // (not 'carousel') so there are no clones. Loop with rewind:true.
115    var useFade = !isMobile();
116    if (useFade) fieldWrap.classList.add('is-fade-mode');
117
118    var glide = new window.Glide(fieldWrap, {
119      type: 'slider',
120      perView: 1,
121      autoplay: 6000,
122      hoverpause: true,
123      rewind: true,
124      animationDuration: useFade ? 600 : 500,
125      animationTimingFunc: 'cubic-bezier(0.4, 0, 0.2, 1)',
126      gap: 0
127    });
128
129    glide.mount();
130
131    // Re-init mode on resize (fade <-> slide switch between breakpoints)
132    var currentMobile = isMobile();
133    window.addEventListener('resize', debounce(function () {
134      var nowMobile = isMobile();
135      if (nowMobile !== currentMobile) {
136        currentMobile = nowMobile;
137        // Re-initialize to swap between fade and slide behavior
138        glide.destroy();
139        initHeroSlider();
140      }
141    }, 250));
142  }
143
144  // -------- 2. Emergency cards carousel --------
145  function initEmergencyCarousel() {
146    var section = document.querySelector('.MAKE-home-emergencies');
147    if (!section) return;
148
149    var fieldWrap = section.querySelector('.field--field-emergencies-cards');
150    if (!fieldWrap) return;
151
152    var slideHost = fieldWrap.querySelector('.field-container');
153    if (!slideHost) return;
154
155    var slideDivs = Array.prototype.slice.call(
156      slideHost.querySelectorAll(':scope > .field-content')
157    );
158    if (slideDivs.length === 0) return;
159
160    fieldWrap.classList.add('glide');
161
162    var track = document.createElement('div');
163    track.className = 'glide__track';
164    track.setAttribute('data-glide-el', 'track');
165
166    var slidesUl = document.createElement('ul');
167    slidesUl.className = 'glide__slides';
168
169    slideDivs.forEach(function (div) {
170      var li = document.createElement('li');
171      li.className = 'glide__slide';
172      li.appendChild(div);
173      slidesUl.appendChild(li);
174    });
175
176    track.appendChild(slidesUl);
177    slideHost.parentNode.removeChild(slideHost);
178    fieldWrap.appendChild(track);
179
180    // Arrows (desktop only — CSS hides on mobile)
181    var arrows = document.createElement('div');
182    arrows.className = 'glide__arrows';
183    arrows.setAttribute('data-glide-el', 'controls');
184    arrows.innerHTML =
185      '<button class="glide__arrow glide__arrow--left" data-glide-dir="<" aria-label="Previous emergency">' +
186      svgChevron('left') +
187      '</button>' +
188      '<button class="glide__arrow glide__arrow--right" data-glide-dir=">" aria-label="Next emergency">' +
189      svgChevron('right') +
190      '</button>';
191    fieldWrap.appendChild(arrows);
192
193    // Dot indicators (bullets). Glide wires these up via data-glide-el="controls[nav]"
194    // and data-glide-dir="=N" on each bullet button.
195    var bullets = document.createElement('div');
196    bullets.className = 'glide__bullets';
197    bullets.setAttribute('data-glide-el', 'controls[nav]');
198    var bulletsHtml = '';
199    for (var i = 0; i < slideDivs.length; i++) {
200      bulletsHtml +=
201        '<button class="glide__bullet" data-glide-dir="=' + i + '" ' +
202        'aria-label="Go to emergency ' + (i + 1) + '"></button>';
203    }
204    bullets.innerHTML = bulletsHtml;
205    fieldWrap.appendChild(bullets);
206
207    if (typeof window.Glide !== 'function') return;
208
209    // Full-width card carousel with dot indicators (no peek — arrows + dots
210    // convey that there are multiple cards).
211    var mobile = isMobile();
212    var glide = new window.Glide(fieldWrap, {
213      type: 'slider',
214      perView: 1,
215      focusAt: 0,
216      gap: 0,
217      bound: true,
218      rewind: true,
219      peek: { before: 0, after: 0 },
220      animationDuration: 450,
221      animationTimingFunc: 'cubic-bezier(0.4, 0, 0.2, 1)'
222    });
223
224    glide.mount();
225
226    // With rewind:true we can always navigate; keep both arrows enabled.
227    function updateArrowStates() {
228      var prev = fieldWrap.querySelector('.glide__arrow--left');
229      var next = fieldWrap.querySelector('.glide__arrow--right');
230      if (!prev || !next) return;
231      prev.disabled = false;
232      next.disabled = false;
233    }
234    updateArrowStates();
235    glide.on(['run.after', 'build.after'], updateArrowStates);
236
237    // Re-init on breakpoint cross so the peek recalculates
238    var currentMobile = mobile;
239    window.addEventListener('resize', debounce(function () {
240      var nowMobile = isMobile();
241      if (nowMobile !== currentMobile) {
242        currentMobile = nowMobile;
243        glide.update({
244          peek: { before: 0, after: 0 }
245        });
246      }
247    }, 200));
248  }
249
250  // -------- 3. Support cards — touch reveal + section-wide link --------
251  function initSupportCards() {
252    var section = document.querySelector('.MAKE-home-children-families');
253    if (!section) return;
254
255    // All six support cards share ONE destination URL stored in
256    // .field--field-how-we-support-link (single Link field on the section,
257    // not a per-card link). Pull the href once and apply to all cards.
258    var linkField = section.querySelector('.field--field-how-we-support-link a');
259    var destination = linkField ? linkField.getAttribute('href') : null;
260
261    var cards = section.querySelectorAll('.paragraph--type--home-support-card');
262    if (!cards.length) return;
263
264    cards.forEach(function (card) {
265      // Keyboard accessibility
266      card.setAttribute('tabindex', '0');
267      if (destination) {
268        card.setAttribute('role', 'link');
269        card.setAttribute('aria-label', getCardLabel(card) + ' — learn more');
270        card.style.cursor = 'pointer';
271      }
272
273      var isTouch = window.matchMedia('(hover: none)').matches;
274
275      card.addEventListener('click', function (e) {
276        // Don't hijack clicks on real links inside the card
277        if (e.target.closest('a')) return;
278
279        if (isTouch) {
280          // Two-tap pattern on touch: first tap reveals description,
281          // second tap navigates. Prevents accidental nav before reading.
282          if (!card.classList.contains('is-open')) {
283            // Close any other open cards first (only one revealed at a time)
284            cards.forEach(function (other) {
285              if (other !== card) other.classList.remove('is-open');
286            });
287            card.classList.add('is-open');
288            return;
289          }
290          // Already open — navigate
291          if (destination) window.location.href = destination;
292        } else {
293          // Desktop: single click anywhere on card navigates
294          if (destination) window.location.href = destination;
295        }
296      });
297
298      card.addEventListener('keydown', function (e) {
299        if (e.key === 'Enter' || e.key === ' ') {
300          e.preventDefault();
301          if (isTouch && !card.classList.contains('is-open')) {
302            card.classList.add('is-open');
303          } else if (destination) {
304            window.location.href = destination;
305          }
306        }
307      });
308    });
309  }
310
311  // Get the heading text of a card for aria-label
312  function getCardLabel(card) {
313    var heading = card.querySelector('.field-container:nth-child(2) .field-content');
314    return heading ? heading.textContent.trim() : 'Support card';
315  }
316
317  // -------- 4. Newsletter relocation --------
318  // The ActiveCampaign embed script sometimes appends the form directly to
319  // <body> instead of into its target placeholder <div class="_form_8"> — this
320  // happens when the script fires before its target element is parsed, or
321  // when it collides with other DOM mutations. We watch for the injected form
322  // and move it back into the correct place inside the newsletter section.
323  function initNewsletterFix() {
324    var section = document.querySelector('.MAKE-home-newsletter');
325    if (!section) return;
326    // Look inside newsletter for the target — ActiveCampaign looks for ._form_N
327    var targetPlaceholder = section.querySelector('._form_8, [class^="_form_"]');
328
329    function relocate() {
330      // The rendered form appears as a direct child of <body>, usually an
331      // element with id matching _form_N or a wrapper ._form-block-8 etc.
332      var candidates = document.querySelectorAll(
333        'body > ._form_8, body > [id^="_form_"], body > form[id*="form_"]'
334      );
335      if (!candidates.length || !targetPlaceholder) return false;
336      candidates.forEach(function (el) {
337        // Don't move elements already inside our section
338        if (!section.contains(el)) {
339          targetPlaceholder.appendChild(el);
340        }
341      });
342      return true;
343    }
344
345    // Poll for up to ~10s for the form to appear
346    var attempts = 0;
347    var maxAttempts = 50;
348    var id = setInterval(function () {
349      attempts++;
350      if (relocate() || attempts >= maxAttempts) {
351        clearInterval(id);
352      }
353    }, 200);
354
355    // Also observe body mutations so the form is moved the moment it's added
356    if (typeof MutationObserver !== 'undefined') {
357      var mo = new MutationObserver(function () {
358        relocate();
359      });
360      mo.observe(document.body, { childList: true });
361      // Stop observing after 15s to avoid leaks
362      setTimeout(function () { mo.disconnect(); }, 15000);
363    }
364  }
365
366  // -------- Debounce helper --------
367  function debounce(fn, wait) {
368    var t;
369    return function () {
370      var args = arguments;
371      var ctx = this;
372      clearTimeout(t);
373      t = setTimeout(function () {
374        fn.apply(ctx, args);
375      }, wait);
376    };
377  }
378
379  // -------- Boot --------
380  // We wait for BOTH: DOM ready AND Glide available. If Glide hasn't loaded
381  // by the time DOM is ready, we poll briefly (fallback carousels still work
382  // via CSS scroll-snap if Glide never appears).
383  function bootWhenReady() {
384    var attempts = 0;
385    var maxAttempts = 30; // ~3 seconds
386    function tryBoot() {
387      if (typeof window.Glide === 'function' || attempts >= maxAttempts) {
388        console.log('[MAKE home] booting, Glide available:', typeof window.Glide === 'function');
389        try { initHeroSlider(); } catch (e) { console.error('[MAKE home] hero init failed:', e); }
390        try { initEmergencyCarousel(); } catch (e) { console.error('[MAKE home] emergency carousel init failed:', e); }
391        try { initSupportCards(); } catch (e) { console.error('[MAKE home] support cards init failed:', e); }
392        try { initNewsletterFix(); } catch (e) { console.error('[MAKE home] newsletter fix failed:', e); }
393        return;
394      }
395      attempts++;
396      setTimeout(tryBoot, 100);
397    }
398    tryBoot();
399  }
400
401  ready(bootWhenReady);
402})();

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.