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.