1/* ========================================================================== 2 CirclesCo Popups â Slider engine 3 4 Shared by the live popup (popup.js) and the admin preview (admin.js), so the 5 thing you approve in the preview is the same code the visitor gets. 6 7 Exposes window.CCPSlider: 8 init(root) â build an instance for one .ccp-slider element (idempotent) 9 boot(scope) â init every .ccp-slider inside scope (default: document) 10 Each instance is parked on the element as `el.__ccpSlider` with start()/stop(). 11 ========================================================================== */ 12(function () { 13 'use strict'; 14 15 var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 16 17 function init(root) { 18 if (!root || root.__ccpSlider) return root ? root.__ccpSlider : null; 19 20 var track = root.querySelector('.ccp-slider__track'); 21 var slides = root.querySelectorAll('.ccp-slider__slide'); 22 if (!track || slides.length === 0) return null; 23 24 var dots = root.querySelectorAll('.ccp-slider__dot'); 25 var prevBtn = root.querySelector('.ccp-slider__arrow--prev'); 26 var nextBtn = root.querySelector('.ccp-slider__arrow--next'); 27 var autoplay = root.getAttribute('data-autoplay') === '1'; 28 var interval = parseInt(root.getAttribute('data-interval'), 10) || 5000; 29 var count = slides.length; 30 31 var index = 0; 32 var timer = null; 33 var running = false; 34 35 /* --- Crop and zoom to match slide one --- 36 Every slide's media box takes the FIRST slide's aspect ratio and the 37 images fill it with object-fit:cover, so a set of mismatched images still 38 reads as one deck instead of the popup resizing on every advance. PHP 39 sets --ccp-slide-ar when it knows the dimensions; this is the fallback 40 for an image whose size we never recorded (an external URL, or a slide 41 added before the media library told us). */ 42 function ensureAspectRatio() { 43 if (root.style.getPropertyValue('--ccp-slide-ar')) return; 44 var first = slides[0].querySelector('img'); 45 if (!first) return; 46 var apply = function () {
47 if (first.naturalWidth && first.naturalHeight) { 48 root.style.setProperty('--ccp-slide-ar', first.naturalWidth + ' / ' + first.naturalHeight); 49 } 50 }; 51 if (first.complete) apply(); 52 else first.addEventListener('load', apply, { once: true }); 53 } 54 55 function paint() { 56 for (var i = 0; i < count; i++) { 57 var on = i === index; 58 slides[i].classList.toggle('is-active', on); 59 // Off-screen slides are not reachable by tab or a screen reader. 60 slides[i].setAttribute('aria-hidden', on ? 'false' : 'true'); 61 slides[i].inert = !on; 62 } 63 for (var d = 0; d < dots.length; d++) { 64 dots[d].classList.toggle('is-active', d === index); 65 dots[d].setAttribute('aria-selected', d === index ? 'true' : 'false'); 66 dots[d].setAttribute('tabindex', d === index ? '0' : '-1'); 67 } 68 } 69 70 /* --- Seamless wrap --- 71 A translateX track runs out of slides at both ends. Cloning the last 72 slide before the first and the first after the last gives it somewhere to 73 keep going, so stepping off either end travels ONE slide in the direction 74 you were already moving â the same motion as every other advance â and 75 then silently re-seats on the real slide while nothing is animating. 76 Fade needs none of this: it never travels. */ 77 var isFade = root.classList.contains('ccp-slider--fade'); 78 var cloned = false; 79 var offset = 0; // real slide i sits at track position i + offset 80 var settle = null; 81 82 if (!isFade && count > 1) { 83 var head = slides[count - 1].cloneNode(true); 84 var tail = slides[0].cloneNode(true); 85 [head, tail].forEach(function (c) { 86 c.classList.add('ccp-slider__slide--clone'); 87 c.classList.remove('is-active'); 88 c.setAttribute('aria-hidden', 'true'); 89 c.inert = true; 90 }); 91 track.insertBefore(head, slides[0]); 92 track.appendChild(tail); 93 cloned = true; 94 offset = 1; 95 } 96 97 function position(pos, animate) { 98 track.style.transition = animate ? '' : 'none'; 99 track.style.transform = 'translateX(' + (-100 * pos) + '%)'; 100 if (!animate) void track.offsetWidth; // commit the jump before anything re-enables motion 101 } 102 103 // Re-seat on the real slide once the travel into a clone has finished. 104 // transitionend is the signal; the timer is there for the case it never 105 // fires (an interrupted or suppressed transition) so the deck can't stick. 106 function afterTravel(fn) { 107 clearSettle(); 108 var done = function () { 109 clearSettle(); 110 fn(); 111 }; 112 var timer = setTimeout(done, 700); 113 var onEnd = function (e) { 114 if (e.target !== track) return; 115 done(); 116 }; 117 track.addEventListener('transitionend', onEnd); 118 settle = function () { 119 clearTimeout(timer); 120 track.removeEventListener('transitionend', onEnd); 121 settle = null; 122 }; 123 } 124 125 function clearSettle() { if (settle) settle(); } 126 127 function goTo(next, viaUser, stepped) { 128 if (count < 2) return; 129 var from = index; 130 index = (next % count + count) % count; // wrap both directions 131 paint(); 132 133 if (!isFade) { 134 // A half-finished wrap must be settled before the next move, or the 135 // re-seat fires mid-travel and the deck jumps. 136 clearSettle(); 137 138 if (reduceMotion) { 139 position(index + offset, false); 140 } else if (cloned && stepped && from === count - 1 && index === 0) { 141 // Off the end: keep going right, into the clone of slide one. 142 position(count + offset, true); 143 afterTravel(function () { position(offset, false); }); 144 } else if (cloned && stepped && from === 0 && index === count - 1) { 145 // Off the front: keep going left, into the clone of the last slide. 146 position(0, true); 147 afterTravel(function () { position(count - 1 + offset, false); }); 148 } else { 149 position(index + offset, true); 150 } 151 } 152 153 if (viaUser && running) restart(); // don't advance again immediately 154 } 155 156 function next(viaUser) { goTo(index + 1, viaUser, true); } 157 function prev(viaUser) { goTo(index - 1, viaUser, true); } 158 159 function stop() { 160 running = false;
161 if (timer) { clearInterval(timer); timer = null; } 162 } 163 164 165 function start() { 166 // Autoplaying one slide is just a slide. Reduced motion means the visitor 167 // asked not to be moved, so they drive with the arrows instead. 168 if (!autoplay || count < 2 || reduceMotion) return; 169 stop(); 170 running = true; 171 timer = setInterval(function () { goTo(index + 1, false, true); }, interval); 172 } 173 174 function restart() { if (autoplay && !reduceMotion) start(); } 175 176 /* --- Controls --- */ 177 if (prevBtn) prevBtn.addEventListener('click', function (e) { e.preventDefault(); prev(true); }); 178 if (nextBtn) nextBtn.addEventListener('click', function (e) { e.preventDefault(); next(true); }); 179 180 for (var d = 0; d < dots.length; d++) { 181 (function (i) { 182 dots[i].addEventListener('click', function (e) { e.preventDefault(); goTo(i, true, false); }); 183 })(d); 184 } 185 186 /* --- Pause while the visitor is looking at or using it (WCAG 2.2.2) --- */ 187 root.addEventListener('mouseenter', stop); 188 root.addEventListener('mouseleave', restart); 189 root.addEventListener('focusin', stop); 190 root.addEventListener('focusout', function (e) { 191 if (!root.contains(e.relatedTarget)) restart(); 192 }); 193 194 /* --- Arrow keys, only while this slider is on screen --- */ 195 root.addEventListener('keydown', function (e) { 196 if (e.key === 'ArrowLeft') { e.preventDefault(); prev(true); } 197 if (e.key === 'ArrowRight') { e.preventDefault(); next(true); } 198 }); 199 200 /* --- Swipe --- */ 201 var touchX = null; 202 root.addEventListener('touchstart', function (e) { 203 touchX = e.changedTouches[0].clientX; 204 }, { passive: true }); 205 root.addEventListener('touchend', function (e) { 206 if (touchX === null) return; 207 var dx = e.changedTouches[0].clientX - touchX; 208 touchX = null; 209 if (Math.abs(dx) < 40) return; 210 if (dx < 0) next(true); else prev(true); 211 }, { passive: true }); 212 213 if (reduceMotion) root.classList.add('ccp-slider--no-motion'); 214 215 ensureAspectRatio();
216 paint(); 217 if (!isFade) position(offset, false); 218 219 var api = { start: start, stop: stop, goTo: goTo, next: next, prev: prev, 220 get index() { return index; }, count: count }; 221 root.__ccpSlider = api; 222 return api; 223 } 224 225 function boot(scope) { 226 var els = (scope || document).querySelectorAll('.ccp-slider'); 227 for (var i = 0; i < els.length; i++) init(els[i]); 228 } 229 230 window.CCPSlider = { init: init, boot: boot }; 231 232 if (document.readyState === 'loading') { 233 document.addEventListener('DOMContentLoaded', function () { boot(); }); 234 } else { 235 boot(); 236 } 237})();
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.