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 = '❮'; 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 = '❯'; 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.