1/** 2 * 360 Dental Care â Smile Gallery Carousel 3 * @package 360_Dental 4 * 5 * Editorial-smooth carousel for the before/after smile gallery sections. 6 * Picks up every .sg-carousel on the page automatically. 7 * 8 * Behaviour: 9 * - Mobile (â¤768px): 1 card in view + 8% peek 10 * - Desktop (>768px): 2 cards in view + 8% peek 11 * - Arrows: always visible, sit in outer gutters (CSS), disable at ends 12 * - Dots: thin pills underneath, elongate when active 13 * - Cross-fade thumbnails on slide change via .is-changing on the carousel 14 * - Keyboard arrows when the carousel has focus 15 * - prefers-reduced-motion: arrow taps jump directly (no smooth scroll), 16 * scale/opacity transitions removed by CSS 17 * 18 * No external dependencies. Vanilla JS, IE11-incompatible by design. 19 */ 20(function () { 21 'use strict'; 22 23 var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 24 25 function initCarousel(root) { 26 // If already initialised, tear down first so we can rebuild with the 27 // current set of cards (used after load-more appends new cards to the 28 // archive carousels). 29 if (root._sgCarouselTeardown) { 30 root._sgCarouselTeardown(); 31 } 32 33 var track = root.querySelector('.sg-carousel__track'); 34 var prevBtn = root.querySelector('[data-sg-arrow="prev"]'); 35 var nextBtn = root.querySelector('[data-sg-arrow="next"]'); 36 var dotsHost = root.querySelector('[data-sg-dots]'); 37 var cards = track ? Array.prototype.slice.call(track.querySelectorAll('.sg-card')) : []; 38 39 if (!track || cards.length === 0) return; 40 41 // âââââ Build dots âââââ 42 // One dot per card. Aria-label tells screen readers which case they're 43 // jumping to. Dots are decorative wrt the parent (aria-hidden via the 44 // template's data attribute) but each dot is a real button for keyboard. 45 var dots = []; 46 if (dotsHost) { 47 dotsHost.innerHTML = ''; 48 for (var i = 0; i < cards.length; i++) { 49 var dot = document.createElement('button'); 50 dot.type = 'button'; 51 dot.className = 'sg-carousel__dot'; 52 dot.setAttribute('data-sg-dot-index', String(i)); 53 dot.setAttribute('aria-label', 'Go to case ' + (i + 1)); 54 dotsHost.appendChild(dot); 55 dots.push(dot); 56 } 57 // Dots host can be visible to screen readers since each dot is labelled 58 dotsHost.removeAttribute('aria-hidden'); 59 } 60 61 // âââââ Active index tracking âââââ 62 var activeIndex = 0; 63 64 function setActive(index) { 65 if (index < 0) index = 0; 66 if (index >= cards.length) index = cards.length - 1; 67 activeIndex = index; 68 69 cards.forEach(function (card, i) { 70 card.classList.toggle('is-active', i === index); 71 }); 72 dots.forEach(function (dot, i) { 73 dot.classList.toggle('is-active', i === index); 74 }); 75 76 // Update arrow disabled state at the ends 77 if (prevBtn) prevBtn.disabled = (index === 0); 78 if (nextBtn) nextBtn.disabled = (index === cards.length - 1); 79 } 80 81 // âââââ Scroll-to-card with cross-fade âââââ 82 // The cross-fade is implemented by toggling a class on the carousel root 83 // for ~340ms (matching the CSS transition duration on .sg-card img). 84 var changingTimeout = null; 85 86 // Flag set while a programmatic scroll (arrow/dot/keyboard) is in 87 // progress, so the IntersectionObserver below doesn't fight the click 88 // handler and over-set activeIndex while the scroll animation is mid- 89 // flight. Without this, clicking "next" once moves the carousel by 2 90 // because the observer briefly sees card N+1 as the most-intersecting 91 // mid-animation before settling on card N. (v2.16.14 fix.) 92 var programmaticScrollUntil = 0; 93 function scrollToCard(index, smooth) { 94 if (index < 0 || index >= cards.length) return; 95 var card = cards[index]; 96 if (!card) return; 97 98 // Compute the target scrollLeft so the card sits centred in the viewport. 99 // scrollIntoView works but inline:'center' has inconsistent behaviour 100 // across browsers when the parent overflow is the scroll container. 101 var trackRect = track.getBoundingClientRect(); 102 var cardRect = card.getBoundingClientRect(); 103 var currentScrollLeft = track.scrollLeft; 104 var delta = (cardRect.left - trackRect.left) - (trackRect.width / 2) + (cardRect.width / 2); 105 var target = currentScrollLeft + delta; 106 107 // Set the active state IMMEDIATELY so click handlers reading 108 // activeIndex (e.g. successive arrow clicks) see the freshly 109 // chosen index, and so the observer's suppression window below is 110 // measured against the right target.
111 setActive(index); 112 113 // Suppress the observer for ~600ms â long enough for smooth-scroll 114 // to complete (browsers typically animate scrollTo in 200â500ms). 115 programmaticScrollUntil = Date.now() + 600; 116 117 // Trigger the cross-fade 118 root.classList.add('is-changing'); 119 if (changingTimeout) clearTimeout(changingTimeout); 120 changingTimeout = setTimeout(function () { 121 root.classList.remove('is-changing'); 122 }, 340); 123 124 if (smooth && !prefersReducedMotion && typeof track.scrollTo === 'function') { 125 track.scrollTo({ left: target, behavior: 'smooth' }); 126 } else { 127 track.scrollLeft = target; 128 } 129 } 130 131 // âââââ Detect which card is currently centred (via scroll observation) âââââ 132 // We use IntersectionObserver to find the most-visible card. This handles 133 // user-driven swipes; arrow clicks bypass it via programmaticScrollUntil. 134 var io = new IntersectionObserver(function (entries) { 135 // Skip if we're mid programmatic scroll â the click handler already set 136 // the correct active index, and observer noise during the smooth-scroll 137 // animation would otherwise advance past it. 138 if (Date.now() < programmaticScrollUntil) return; 139 140 // Find the entry with the largest intersection ratio 141 var best = null; 142 entries.forEach(function (e) { 143 if (!best || e.intersectionRatio > best.intersectionRatio) best = e; 144 }); 145 if (best && best.intersectionRatio > 0.55) { 146 var index = cards.indexOf(best.target); 147 if (index >= 0 && index !== activeIndex) {
148 setActive(index); 149 } 150 } 151 }, { 152 root: track, 153 // Threshold steps so we update precisely as cards cross the centre 154 threshold: [0.25, 0.55, 0.75, 1] 155 }); 156 157 cards.forEach(function (card) { io.observe(card); }); 158 159 // âââââ Wire up controls âââââ 160 if (prevBtn) { 161 prevBtn.addEventListener('click', function () { 162 scrollToCard(activeIndex - 1, true); 163 }); 164 } 165 if (nextBtn) { 166 nextBtn.addEventListener('click', function () { 167 scrollToCard(activeIndex + 1, true); 168 }); 169 } 170 dots.forEach(function (dot, i) { 171 dot.addEventListener('click', function () { 172 scrollToCard(i, true); 173 }); 174 }); 175 176 // Keyboard: when arrow buttons or dots have focus, left/right arrow keys 177 // navigate. Native scroll handles ArrowDown/Up so we don't trap those. 178 root.addEventListener('keydown', function (e) { 179 if (e.key === 'ArrowLeft') { 180 e.preventDefault(); 181 scrollToCard(activeIndex - 1, true); 182 } else if (e.key === 'ArrowRight') { 183 e.preventDefault(); 184 scrollToCard(activeIndex + 1, true); 185 } 186 }); 187 188 // Initialise: first card active 189 setActive(0); 190 191 // Store teardown so re-init can clean up before rebuilding 192 root._sgCarouselTeardown = function () { 193 io.disconnect(); 194 // Strip dots so init can rebuild them 195 if (dotsHost) dotsHost.innerHTML = ''; 196 // Clear active state from cards 197 cards.forEach(function (c) { c.classList.remove('is-active'); }); 198 }; 199 200 // After fonts/images settle, re-centre on the active card so any layout 201 // shift doesn't leave us off-snap. Two passes catches both font and image. 202 setTimeout(function () { scrollToCard(0, false); }, 50); 203 if (document.fonts && document.fonts.ready) { 204 document.fonts.ready.then(function () { scrollToCard(activeIndex, false); }); 205 } 206 } 207 208 function init() { 209 var carousels = document.querySelectorAll('[data-sg-carousel]'); 210 Array.prototype.forEach.call(carousels, initCarousel); 211 } 212 213 // Re-initialise archive carousels when load-more appends new cards so the 214 // dot count and active-card tracking reflect the new total. 215 document.addEventListener('sg:cards-appended', function () { 216 var archiveCarousels = document.querySelectorAll('[data-sg-carousel^="archive-"]'); 217 Array.prototype.forEach.call(archiveCarousels, initCarousel); 218 }); 219 220 if (document.readyState === 'loading') { 221 document.addEventListener('DOMContentLoaded', init); 222 } else { 223 init(); 224 } 225})();
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.