PageSourceSearch

https://www.archipel-des-sciences.org/wp-content/plugins/wp-respon…/js/rts-modern-slider.js?ver=1.1.15

js archipel-des-sciences.org collected 2026-09-26 13:40:06 UTC 13,495 bytes, 391 lines download raw bytes

1/**
2 * Responsive Thumbnail Slider - Modern Engine
3 * Dependency-free replacement for the legacy bxSlider engine.
4 * No jQuery required for slider mechanics.
5 */
6(function () {
7	'use strict';
8
9	function RTSModernSlider(root) {
10		this.root = root;
11		this.viewport = root.querySelector('.rts-modern-viewport');
12		this.track = root.querySelector('.rts-modern-track');
13		this.slides = Array.prototype.slice.call(root.querySelectorAll('.rts-modern-slide'));
14		this.realCount = this.slides.length;
15
16		var settingsRaw = root.getAttribute('data-settings');
17		this.settings = settingsRaw ? JSON.parse(settingsRaw) : {};
18
19		this.visible = Math.max(1, parseInt(this.settings.visible, 10) || 1);
20		this.minVisible = Math.max(1, parseInt(this.settings.minVisible, 10) || 1);
21		this.scroll = Math.max(1, parseInt(this.settings.scroll, 10) || 1);
22		this.margin = parseInt(this.settings.margin, 10) || 0;
23		this.speed = parseInt(this.settings.speed, 10) || 500;
24		this.pauseMs = parseInt(this.settings.pause, 10) || 4000;
25		this.auto = parseInt(this.settings.auto, 10) || 0; // 0 = off, 1 = auto (no arrows), 2 = auto (with arrows)
26		this.pauseOnHover = !!this.settings.pauseOnHover;
27		this.circular = !!this.settings.circular;
28		this.showPager = !!this.settings.showPager;
29		this.fade = this.visible === 1 && !!this.settings.fade;
30
31		this.effectiveVisible = this.visible;
32		this.index = 0; // index into the (possibly cloned) slide list
33		this.timer = null;
34		this.pendingSnap = false;
35		this.snapTimeout = null;
36
37		if (this.realCount === 0) {
38			return;
39		}
40
41		this._buildClones();
42		this._buildControls();
43		this._recalc();
44		this._bindEvents();
45
46		if (this.auto === 1 || this.auto === 2) {
47			this._startAutoplay();
48		}
49	}
50
51	RTSModernSlider.prototype._buildClones = function () {
52		if (!this.circular || this.realCount <= this.visible || this.fade) {
53			this.cloneCount = 0;
54			return;
55		}
56
57		this.cloneCount = this.visible;
58
59		var headClones = this.slides.slice(0, this.cloneCount).map(function (el) {
60			var clone = el.cloneNode(true);
61			clone.setAttribute('aria-hidden', 'true');
62			clone.classList.add('rts-modern-clone');
63			return clone;
64		});
65		var tailClones = this.slides.slice(-this.cloneCount).map(function (el) {
66			var clone = el.cloneNode(true);
67			clone.setAttribute('aria-hidden', 'true');
68			clone.classList.add('rts-modern-clone');
69			return clone;
70		});
71
72		var beforeNode = this.track.firstChild;
73		tailClones.forEach(function (clone) {
74			this.track.insertBefore(clone, beforeNode);
75		}, this);
76		headClones.forEach(function (clone) {
77			this.track.appendChild(clone);
78		}, this);
79
80		this.index = this.cloneCount;
81	};
82
83	RTSModernSlider.prototype._buildControls = function () {
84		var self = this;
85
86		if (this.auto !== 1) {
87			this.prevBtn = document.createElement('button');
88			this.prevBtn.type = 'button';
89			this.prevBtn.className = 'rts-modern-arrow rts-modern-prev';
90			this.prevBtn.setAttribute('aria-label', 'Previous');
91			this.prevBtn.innerHTML = '&#10094;';
92
93			this.nextBtn = document.createElement('button');
94			this.nextBtn.type = 'button';
95			this.nextBtn.className = 'rts-modern-arrow rts-modern-next';
96			this.nextBtn.setAttribute('aria-label', 'Next');
97			this.nextBtn.innerHTML = '&#10095;';
98
99			this.root.appendChild(this.prevBtn);
100			this.root.appendChild(this.nextBtn);
101
102			this.prevBtn.addEventListener('click', function () { self._userNav(-1); });
103			this.nextBtn.addEventListener('click', function () { self._userNav(1); });
104		}
105
106		if (this.showPager && this.realCount > this.visible) {
107			this.pagerWrap = document.createElement('div');
108			this.pagerWrap.className = 'rts-modern-pager';
109			this.root.appendChild(this.pagerWrap);
110
111			var pages = Math.ceil(this.realCount / this.scroll);
112			this.pagerDots = [];
113			for (var p = 0; p < pages; p++) {
114				(function (pageIndex) {
115					var dot = document.createElement('button');
116					dot.type = 'button';
117					dot.className = 'rts-modern-dot';
118					dot.setAttribute('aria-label', 'Go to slide ' + (pageIndex + 1));
119					dot.addEventListener('click', function () {
120						self._goTo(self.cloneCount + pageIndex * self.scroll, true);
121					});
122					self.pagerWrap.appendChild(dot);
123					self.pagerDots.push(dot);
124				})(p);
125			}
126		}
127	};
128
129	RTSModernSlider.prototype._recalc = function () {
130		var availableWidth = (this.root.parentElement ? this.root.parentElement.clientWidth : this.viewport.clientWidth) || this.viewport.clientWidth;
131		var configuredWidth = Math.max(1, parseInt(this.settings.slideWidth, 10) || 120);
132
133		this.track.style.gap = this.margin + 'px';
134
135		if (this.fade) {
136			this.effectiveVisible = 1;
137			this.currentSlideWidthPx = Math.min(configuredWidth, availableWidth);
138		} else {
139			var fit = Math.max(1, Math.floor((availableWidth + this.margin) / (configuredWidth + this.margin)));
140			this.effectiveVisible = Math.max(this.minVisible, Math.min(this.visible, fit));
141			this.currentSlideWidthPx = configuredWidth; // honor the configured thumbnail width instead of stretching to fill
142		}
143
144		var slideList = this.track.querySelectorAll('.rts-modern-slide');
145		for (var i = 0; i < slideList.length; i++) {
146			slideList[i].style.flex = '0 0 ' + this.currentSlideWidthPx + 'px';
147			slideList[i].style.width = this.currentSlideWidthPx + 'px';
148		}
149
150		// Size the widget itself to exactly "effectiveVisible" slides so the browser
151		// can't show a partial extra slide just because the parent column is wider.
152		var desiredWidth = this.effectiveVisible * this.currentSlideWidthPx + Math.max(0, this.effectiveVisible - 1) * this.margin;
153		this.root.style.width = Math.min(desiredWidth, availableWidth) + 'px';
154
155		this._applyPosition(false);
156	};
157
158	RTSModernSlider.prototype._slideStep = function () {
159		return (this.currentSlideWidthPx || 0) + this.margin;
160	};
161
162	RTSModernSlider.prototype._applyPosition = function (animate) {
163		if (this.fade) {
164			var all = this.track.querySelectorAll('.rts-modern-slide');
165			if (animate) {
166				void this.track.offsetHeight;
167			}
168			for (var i = 0; i < all.length; i++) {
169				all[i].style.transition = animate ? 'opacity ' + this.speed + 'ms ease' : 'none';
170				all[i].style.opacity = (i === this.index) ? '1' : '0';
171				all[i].style.position = 'absolute';
172				all[i].style.left = '0';
173				all[i].style.top = '0';
174			}
175			this._updatePager();
176			return;
177		}
178
179		var offset = this.index * this._slideStep();
180
181		if (animate) {
182			// Force a reflow so the browser commits any just-applied instant (non-animated)
183			// state — e.g. the invisible loop-reset jump — before this transition starts.
184			// Without this, two rapid consecutive style writes can get coalesced by the
185			// browser, which never paints the "from" state and the transition silently
186			// doesn't animate (looks like an instant jump instead of a slide).
187			void this.track.offsetHeight;
188			this.track.style.transition = 'transform ' + this.speed + 'ms ease';
189		} else {
190			this.track.style.transition = 'none';
191		}
192
193		this.track.style.transform = 'translateX(-' + offset + 'px)';
194		this._updatePager();
195	};
196
197	RTSModernSlider.prototype._updatePager = function () {
198		if (!this.pagerDots) return;
199		var realIndex = ((this.index - this.cloneCount) % this.realCount + this.realCount) % this.realCount;
200		var page = Math.floor(realIndex / this.scroll);
201		this.pagerDots.forEach(function (dot, i) {
202			dot.classList.toggle('rts-modern-dot-active', i === page);
203		});
204	};
205
206	RTSModernSlider.prototype._goTo = function (index, animate) {
207		// _goTo (pager clicks) is an explicit, authoritative navigation. If a loop-reset
208		// was still pending from a recent arrow/autoplay move, it must not be allowed to
209		// fire afterward and silently override this — cancel it instead.
210		if (this.pendingSnap) {
211			this.pendingSnap = false;
212			if (this.snapTimeout) {
213				window.clearTimeout(this.snapTimeout);
214				this.snapTimeout = null;
215			}
216		}
217
218		this.index = index;
219		this._applyPosition(animate !== false);
220	};
221
222	RTSModernSlider.prototype._userNav = function (dir) {
223		this._restartAutoplay();
224		this._move(dir);
225	};
226
227	RTSModernSlider.prototype._move = function (dir) {
228		if (this.fade) {
229			this.index = ((this.index + dir) % this.realCount + this.realCount) % this.realCount;
230			this._applyPosition(true);
231			return;
232		}
233
234		if (this.pendingSnap) {
235			return; // avoid stacking moves while a loop-reset is in flight
236		}
237
238		this.index += dir * this.scroll;
239
240		if (this.circular && this.cloneCount > 0) {
241			// The clone buffer only extends cloneCount slides past each end of the real
242			// content — a scroll step larger than 1 can jump straight past that buffer in
243			// a single move (especially backward, which has zero cushion below index 0;
244			// there is no DOM content at all before the first clone). Clamp to the buffer's
245			// true extent before ever rendering, so we never attempt to show a position
246			// that doesn't exist regardless of how big the scroll step is.
247			var minIndex = 0;
248			var maxIndex = this.cloneCount + this.realCount + this.cloneCount - this.effectiveVisible;
249			if (this.index < minIndex) this.index = minIndex;
250			if (this.index > maxIndex) this.index = maxIndex;
251		}
252
253		this._applyPosition(true);
254
255		if (!this.circular || this.cloneCount === 0) {
256			var maxNonCircularIndex = Math.max(0, this.realCount - this.effectiveVisible);
257			if (this.index < 0) this.index = 0;
258			if (this.index > maxNonCircularIndex) this.index = maxNonCircularIndex;
259			return;
260		}
261
262		// The clone buffer on each side holds exactly cloneCount slides, at fixed DOM
263		// positions [0, cloneCount-1] (tail clones) and [cloneCount+realCount, cloneCount+realCount+cloneCount-1]
264		// (head clones) — these bounds are NOT scaled by realCount. Using cloneCount-realCount
265		// here previously went deeply negative whenever realCount > cloneCount (the normal
266		// case), letting backward navigation travel into DOM positions where no clone
267		// actually exists, which is why only the "previous" direction could get stuck.
268		var lower = 0;
269		var upper = this.cloneCount + this.realCount;
270
271		if (this.index >= upper || this.index <= lower) {
272			this._scheduleSnap();
273		}
274	};
275
276	RTSModernSlider.prototype._scheduleSnap = function () {
277		var self = this;
278		this.pendingSnap = true;
279
280		if (this.snapTimeout) {
281			window.clearTimeout(this.snapTimeout);
282		}
283
284		// transitionend is the normal trigger, but it can be missed entirely — e.g. a
285		// window resize interrupts the transition right as the loop boundary is crossed.
286		// Without a fallback, a single missed event leaves pendingSnap stuck true forever,
287		// silently blocking all further arrow clicks and autoplay ticks. This guarantees
288		// the snap always completes even if that event never fires.
289		this.snapTimeout = window.setTimeout( function () {
290			self._performSnap();
291		}, this.speed + 150 );
292	};
293
294	RTSModernSlider.prototype._performSnap = function () {
295		if (!this.pendingSnap) return;
296
297		this.pendingSnap = false;
298		if (this.snapTimeout) {
299			window.clearTimeout(this.snapTimeout);
300			this.snapTimeout = null;
301		}
302
303		this.index = this.cloneCount + (((this.index - this.cloneCount) % this.realCount + this.realCount) % this.realCount);
304		this._applyPosition(false);
305	};
306
307	RTSModernSlider.prototype._handleTransitionEnd = function (e) {
308		if (e && e.target !== this.track) return;
309		if (e && e.propertyName && e.propertyName !== 'transform') return;
310		this._performSnap();
311	};
312
313	RTSModernSlider.prototype._startAutoplay = function () {
314		var self = this;
315		this.timer = window.setInterval(function () { self._move(1); }, this.pauseMs);
316	};
317
318	RTSModernSlider.prototype._stopAutoplay = function () {
319		if (this.timer) {
320			window.clearInterval(this.timer);
321			this.timer = null;
322		}
323	};
324
325	RTSModernSlider.prototype._restartAutoplay = function () {
326		if (this.auto === 1 || this.auto === 2) {
327			this._stopAutoplay();
328			this._startAutoplay();
329		}
330	};
331
332	RTSModernSlider.prototype._bindEvents = function () {
333		var self = this;
334
335		this.track.addEventListener('transitionend', function (e) { self._handleTransitionEnd(e); });
336
337		if (this.pauseOnHover && (this.auto === 1 || this.auto === 2)) {
338			this.root.addEventListener('mouseenter', function () { self._stopAutoplay(); });
339			this.root.addEventListener('mouseleave', function () { self._startAutoplay(); });
340		}
341
342		window.addEventListener('resize', debounce(function () { self._recalc(); }, 150));
343
344		// Basic touch/swipe support.
345		var startX = 0, deltaX = 0, dragging = false;
346
347		this.track.addEventListener('touchstart', function (e) {
348			dragging = true;
349			startX = e.touches[0].clientX;
350		}, { passive: true });
351
352		this.track.addEventListener('touchmove', function (e) {
353			if (!dragging) return;
354			deltaX = e.touches[0].clientX - startX;
355		}, { passive: true });
356
357		this.track.addEventListener('touchend', function () {
358			if (!dragging) return;
359			dragging = false;
360			if (Math.abs(deltaX) > 40) {
361				self._userNav(deltaX < 0 ? 1 : -1);
362			}
363			deltaX = 0;
364		});
365	};
366
367	function debounce(fn, wait) {
368		var t;
369		return function () {
370			var args = arguments, ctx = this;
371			window.clearTimeout(t);
372			t = window.setTimeout(function () { fn.apply(ctx, args); }, wait);
373		};
374	}
375
376	function initAll() {
377		var nodes = document.querySelectorAll('.rts-modern-slider:not([data-rts-inited])');
378		for (var i = 0; i < nodes.length; i++) {
379			nodes[i].setAttribute('data-rts-inited', '1');
380			new RTSModernSlider(nodes[i]);
381		}
382	}
383
384	if (document.readyState === 'loading') {
385		document.addEventListener('DOMContentLoaded', initAll);
386	} else {
387		initAll();
388	}
389
390	window.RTSModernSlider = RTSModernSlider;
391})();

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.