PageSourceSearch

https://circlesconference.com/wp-content/plugins/circlesco-popups/assets/js/slider.js?ver=3.8.0

js circlesconference.com collected 2026-10-02 04:11:20 UTC 9,159 bytes, 237 lines download raw bytes

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.