@mozert-studio/mozert-slider 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1739 @@
1
+ /*!
2
+ * Mozert 1.1.0
3
+ * A dependency-free, responsive, touch-friendly carousel / slider.
4
+ * MIT License
5
+ */
6
+ (function (root, factory) {
7
+ if (typeof define === 'function' && define.amd) define([], factory);
8
+ else if (typeof module === 'object' && module.exports) { module.exports = factory(); module.exports.default = module.exports; }
9
+ else root.Mozert = factory();
10
+ }(typeof self !== 'undefined' ? self : this, function () {
11
+ 'use strict';
12
+ //#region core
13
+ const VERSION = '1.1.0';
14
+
15
+ const DEFAULTS = {
16
+ accessibility: true,
17
+ adaptiveHeight: false,
18
+ appendArrows: null,
19
+ appendDots: null,
20
+ arrows: true,
21
+ asNavFor: null,
22
+ autoplay: false,
23
+ disableOnInteraction: false,
24
+ autoplayReverse: false,
25
+ autoplaySpeed: 3000,
26
+ centerMode: false,
27
+ centerPadding: '50px',
28
+ cssEase: 'ease',
29
+ customPaging: null,
30
+ dots: false,
31
+ dotsClass: 'mozert-dots',
32
+ dragThreshold: 6,
33
+ draggable: true,
34
+ easing: 'linear',
35
+ edgeFriction: 0.15,
36
+ edgePadding: 0,
37
+ effect: 'slide',
38
+ fade: false,
39
+ focusOnSelect: false,
40
+ freeMode: false,
41
+ infinite: true,
42
+ initialSlide: 0,
43
+ labels: null,
44
+ lazyLoad: 'ondemand',
45
+ mobileFirst: false,
46
+ nextArrow: null,
47
+ pauseOnDotsHover: false,
48
+ pauseOnFocus: true,
49
+ pauseOnHover: true,
50
+ prevArrow: null,
51
+ respectReducedMotion: true,
52
+ respondTo: 'window',
53
+ responsive: null,
54
+ rows: 1,
55
+ rtl: false,
56
+ slide: '',
57
+ slideWidth: 0,
58
+ slidesPerRow: 1,
59
+ slidesToScroll: 1,
60
+ slidesToShow: 1,
61
+ spaceBetween: 0,
62
+ speed: 300,
63
+ stopOnLastSlide: false,
64
+ swipe: true,
65
+ swipeToSlide: false,
66
+ touchMove: true,
67
+ touchRatio: 1,
68
+ touchThreshold: 5,
69
+ useCSS: true,
70
+ useTransform: true,
71
+ variableWidth: false,
72
+ vertical: false,
73
+ verticalSwiping: false,
74
+ waitForAnimate: true,
75
+ zIndex: 1000,
76
+ on: null,
77
+ };
78
+
79
+ const LABELS = { slide: 'Slide {n} of {total}', prev: 'Previous slide', next: 'Next slide', goTo: 'Go to slide {n}' };
80
+ const DEFAULT_PREV = '<button type="button" class="mozert-prev" aria-label="Previous slide">Previous</button>';
81
+ const DEFAULT_NEXT = '<button type="button" class="mozert-next" aria-label="Next slide">Next</button>';
82
+ const FOCUSABLE = 'a[href],button,input,select,textarea,[tabindex],[contenteditable="true"]';
83
+ const STASH_ATTRS = ['class', 'style', 'role', 'aria-hidden', 'aria-label', 'aria-roledescription', 'data-mozert-index'];
84
+
85
+ const STACKED = ['fade', 'flip', 'cube', 'cards', 'creative'];
86
+ const EASINGS = {
87
+ linear: (t) => t,
88
+ swing: (t) => 0.5 - Math.cos(t * Math.PI) / 2,
89
+ easeInQuad: (t) => t * t,
90
+ easeOutQuad: (t) => t * (2 - t),
91
+ easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
92
+ easeInCubic: (t) => t * t * t,
93
+ easeOutCubic: (t) => --t * t * t + 1,
94
+ easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1),
95
+ easeInOutQuart: (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t),
96
+ };
97
+
98
+ const registry = {
99
+ get: (e) => e._mozert || undefined,
100
+ set: (e, v) => { e._mozert = v; },
101
+ delete: (e) => { delete e._mozert; },
102
+ };
103
+ const isEl = (v) => typeof Element !== 'undefined' && v instanceof Element;
104
+ const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
105
+
106
+ function htmlToEl(html) {
107
+ const t = document.createElement('template');
108
+ t.innerHTML = String(html).trim();
109
+ return t.content.firstElementChild;
110
+ }
111
+
112
+ function resolveNode(v) {
113
+ if (!v) return null;
114
+ if (isEl(v)) return v;
115
+ if (typeof v === 'string') {
116
+ const t = v.trim();
117
+ return t.charAt(0) === '<' ? htmlToEl(t) : document.querySelector(t);
118
+ }
119
+ if (v[0] && isEl(v[0])) return v[0];
120
+ return null;
121
+ }
122
+
123
+ function parseData(el) {
124
+ const raw = el.getAttribute('data-mozert');
125
+ if (!raw) return {};
126
+ try {
127
+ return JSON.parse(raw);
128
+ } catch (e) {
129
+ console.warn('Mozert: could not parse data-mozert JSON', e);
130
+ return {};
131
+ }
132
+ }
133
+
134
+ class Mozert {
135
+ constructor(el, options = {}) {
136
+ if (typeof el === 'string') el = document.querySelector(el);
137
+ if (!isEl(el)) throw new TypeError('Mozert: a valid element or selector is required');
138
+ const existing = registry.get(el);
139
+ if (existing) return existing;
140
+
141
+ this.el = el;
142
+ const _o = Object.assign({}, options, parseData(el));
143
+ Mozert._boot.forEach((f) => f(el, _o));
144
+ this._base = Object.assign({}, DEFAULTS, options, parseData(el));
145
+ this.options = this._base;
146
+ this._dormant = false;
147
+ this.active = false;
148
+ this.canNav = false;
149
+ this.current = 0;
150
+ this.animating = false;
151
+ this.n = 0;
152
+
153
+ this._handlers = {};
154
+ this._sources = [];
155
+ this._filter = null;
156
+ this._stash = new WeakMap();
157
+ this._logical = 0;
158
+ this._t = 0;
159
+ this._cur = 0;
160
+ this._bp = null;
161
+ this._off = [];
162
+ this._life = [];
163
+ this._pause = new Set();
164
+ this._fxHooks = [];
165
+ this._mi = [];
166
+ this._release = null;
167
+ this._lockDrag = false;
168
+ this._autoOn = false;
169
+ this._dir = 1;
170
+ this._built = false;
171
+ this._initDone = false;
172
+ this._origDir = el.getAttribute('dir');
173
+ this._lastW = el.clientWidth;
174
+
175
+ registry.set(el, this);
176
+ this._collect();
177
+ this._bindLife();
178
+ this._apply(true);
179
+ }
180
+
181
+ /* ------------------------------------------------------------------ */
182
+ /* Public API */
183
+ /* ------------------------------------------------------------------ */
184
+
185
+ /** Read-only snapshot of the slider's current state. */
186
+ getState() {
187
+ return {
188
+ active: this.active,
189
+ current: this.current,
190
+ count: this.n,
191
+ total: this.trackLen || 0,
192
+ inner: this.Lc || 0,
193
+ clones: this.cloneCount || 0,
194
+ offset: this._cur || 0,
195
+ animating: this.animating,
196
+ canNavigate: this.canNav,
197
+ playing: this.isPlaying(),
198
+ breakpoint: this._bp,
199
+ };
200
+ }
201
+
202
+ slideCount() {
203
+ return this.n;
204
+ }
205
+
206
+ currentSlide() {
207
+ return this.current;
208
+ }
209
+
210
+ isPlaying() {
211
+ return !!(this.active && this._autoOn && !this._pause.has('user'));
212
+ }
213
+
214
+ get _c() {
215
+ return this.cloneCount;
216
+ }
217
+
218
+ next() {
219
+ if (!this.active || !this.canNav) return this;
220
+ this._slideTo(this.current + this.s.slidesToScroll);
221
+ return this;
222
+ }
223
+
224
+ prev() {
225
+ if (!this.active || !this.canNav) return this;
226
+ this._slideTo(this.current - this.s.slidesToScroll);
227
+ return this;
228
+ }
229
+
230
+ goTo(index, dontAnimate = false) {
231
+ if (!this.active || !this.canNav) return this;
232
+ index = parseInt(index, 10);
233
+ if (isNaN(index)) return this;
234
+ this._slideTo(this.s.infinite ? this._norm(index) : clamp(index, 0, Math.max(0, this.n - 1)), { animate: !dontAnimate });
235
+ return this;
236
+ }
237
+
238
+ play() {
239
+ const was = this.isPlaying();
240
+ this._autoOn = true;
241
+ this._pause.delete('user');
242
+ this._schedule();
243
+ this._live();
244
+ if (!was) this.emit('autoplayStart');
245
+ return this;
246
+ }
247
+
248
+ pause() {
249
+ const was = this.isPlaying();
250
+ this._autoOn = false;
251
+ this._pause.add('user');
252
+ clearTimeout(this._apTimer);
253
+ cancelAnimationFrame(this._apRaf);
254
+ this._live();
255
+ if (was) this.emit('autoplayStop');
256
+ return this;
257
+ }
258
+
259
+ getSlides() {
260
+ return this._units ? this._units.slice() : this._activeItems();
261
+ }
262
+
263
+ getOption(name) {
264
+ return this.options[name];
265
+ }
266
+
267
+ setOption(name, value, refresh) {
268
+ if (name && typeof name === 'object') {
269
+ refresh = value;
270
+ Object.assign(this._base, name);
271
+ } else {
272
+ this._base[name] = value;
273
+ }
274
+ if (refresh === false) {
275
+ // Only re-resolve settings that are read live (autoplay, pauseOn*, ...).
276
+ this._applyLive();
277
+ } else {
278
+ this._apply(false);
279
+ }
280
+ return this;
281
+ }
282
+
283
+ _applyLive() {
284
+ const r = this._resolve();
285
+ this.options = r.s;
286
+ if (this.active) {
287
+ this.s = this._prepare(r.s);
288
+ this._autoOn = !!this.s.autoplay && this.canNav;
289
+ if (this._autoOn) this._pause.delete('user');
290
+ this._live();
291
+ this._schedule();
292
+ }
293
+ }
294
+
295
+ add(markup, index, addBefore) {
296
+ const node = resolveNode(typeof markup === 'string' && markup.trim().charAt(0) !== '<' ? `<div>${markup}</div>` : markup);
297
+ if (!node) return false;
298
+ if (typeof index === 'boolean') {
299
+ addBefore = index;
300
+ index = null;
301
+ }
302
+ const items = this._activeItems();
303
+ if (index != null && (index < 0 || index >= items.length)) return false;
304
+ if (index == null || !items.length) {
305
+ if (addBefore) this._sources.unshift(node);
306
+ else this._sources.push(node);
307
+ } else {
308
+ const at = this._sources.indexOf(items[index]);
309
+ this._sources.splice(at + (addBefore ? 0 : 1), 0, node);
310
+ }
311
+ this._refreshKeep();
312
+ return this;
313
+ }
314
+
315
+ remove(index, removeBefore) {
316
+ const items = this._activeItems();
317
+ if (typeof index === 'boolean') {
318
+ removeBefore = index;
319
+ index = removeBefore ? 0 : items.length - 1;
320
+ } else {
321
+ index = removeBefore ? index - 1 : index;
322
+ }
323
+ if (index == null || index < 0 || index >= items.length) return false;
324
+ const node = items[index];
325
+ this._sources.splice(this._sources.indexOf(node), 1);
326
+ this._restoreItem(node);
327
+ if (node.parentNode) node.parentNode.removeChild(node);
328
+ if (index < this.current) this.current -= 1;
329
+ this._refreshKeep();
330
+ return this;
331
+ }
332
+
333
+ removeAll() {
334
+ this._activeItems().forEach((node) => {
335
+ this._sources.splice(this._sources.indexOf(node), 1);
336
+ this._restoreItem(node);
337
+ if (node.parentNode) node.parentNode.removeChild(node);
338
+ });
339
+ this.current = 0;
340
+ this._refreshKeep();
341
+ return this;
342
+ }
343
+
344
+ filter(selectorOrFn) {
345
+ this._filter = selectorOrFn || null;
346
+ this.current = 0;
347
+ this._refreshKeep();
348
+ return this;
349
+ }
350
+
351
+ unfilter() {
352
+ return this.filter(null);
353
+ }
354
+
355
+ /** Re-read options and rebuild everything, keeping the current slide. */
356
+ refresh() {
357
+ this._refreshKeep();
358
+ this.emit('reInit');
359
+ return this;
360
+ }
361
+
362
+ /** Re-measure and reposition, e.g. after the container was resized by script. */
363
+ setPosition() {
364
+ if (!this.active) return this;
365
+ this._layout();
366
+ this._paintCurrent(false);
367
+ this.emit('setPosition');
368
+ return this;
369
+ }
370
+
371
+ destroy() {
372
+ this.emit('destroy');
373
+ if (this.active) this._teardown();
374
+ this._sources.forEach((n) => this.el.appendChild(n));
375
+ this.active = false;
376
+ this._life.forEach((fn) => fn());
377
+ this._life = [];
378
+ if (this._ro) this._ro.disconnect();
379
+ registry.delete(this.el);
380
+ this._dormant = false;
381
+ this._handlers = {};
382
+ return this;
383
+ }
384
+
385
+ on(name, fn) {
386
+ (this._handlers[name] = this._handlers[name] || []).push(fn);
387
+ return this;
388
+ }
389
+
390
+ once(name, fn) {
391
+ const wrap = (...a) => {
392
+ this.off(name, wrap);
393
+ fn.apply(this, a);
394
+ };
395
+ return this.on(name, wrap);
396
+ }
397
+
398
+ one(name, fn) {
399
+ return this.once(name, fn);
400
+ }
401
+
402
+ off(name, fn) {
403
+ if (!name) this._handlers = {};
404
+ else if (!fn) delete this._handlers[name];
405
+ else this._handlers[name] = (this._handlers[name] || []).filter((f) => f !== fn);
406
+ return this;
407
+ }
408
+
409
+ emit(name, ...args) {
410
+ (this._handlers[name] || []).slice().forEach((fn) => fn.call(this, this, ...args));
411
+ const cb = this._base.on && this._base.on[name];
412
+ if (typeof cb === 'function') cb.call(this, this, ...args);
413
+ this.el.dispatchEvent(new CustomEvent('mozert:' + name, { bubbles: true, detail: { slider: this, args } }));
414
+ if (Mozert._bridge) Mozert._bridge(this, name, args);
415
+ }
416
+
417
+ static get(el) {
418
+ if (typeof el === 'string') el = document.querySelector(el);
419
+ return (isEl(el) && registry.get(el)) || null;
420
+ }
421
+
422
+ static initAll(root, options) {
423
+ const scope = root && root.querySelectorAll ? root : document;
424
+ const found = Array.from(scope.querySelectorAll('[data-mozert]'));
425
+ if (scope !== document && scope.matches && scope.matches('[data-mozert]')) found.unshift(scope);
426
+ return found.map((n) => Mozert.get(n) || new Mozert(n, options));
427
+ }
428
+
429
+ /* ------------------------------------------------------------------ */
430
+ /* Settings and responsive handling */
431
+ /* ------------------------------------------------------------------ */
432
+
433
+ _respondWidth() {
434
+ const mode = this._base.respondTo;
435
+ const w = window.innerWidth;
436
+ if (mode === 'slider') return this.el.clientWidth;
437
+ if (mode === 'min') return Math.min(w, this.el.clientWidth);
438
+ return w;
439
+ }
440
+
441
+ _resolve() {
442
+ const base = this._base;
443
+ const list = Array.isArray(base.responsive) ? base.responsive.filter((r) => r && typeof r.breakpoint === 'number') : [];
444
+ const w = this._respondWidth();
445
+ let hit = null;
446
+ if (base.mobileFirst) {
447
+ list.sort((a, b) => a.breakpoint - b.breakpoint).forEach((r) => {
448
+ if (w >= r.breakpoint) hit = r;
449
+ });
450
+ } else {
451
+ list.sort((a, b) => b.breakpoint - a.breakpoint).forEach((r) => {
452
+ if (w <= r.breakpoint) hit = r;
453
+ });
454
+ }
455
+ if (hit && hit.settings === 'destroy') return { bp: hit.breakpoint, dormant: true, s: Object.assign({}, base) };
456
+ return { bp: hit ? hit.breakpoint : null, dormant: false, s: Object.assign({}, base, hit ? hit.settings : {}) };
457
+ }
458
+
459
+ _prepare(raw) {
460
+ const s = Object.assign({}, raw);
461
+ s.slidesToShow = Math.max(1, parseFloat(s.slidesToShow) || 1);
462
+ s.slidesToScroll = Math.max(1, parseInt(s.slidesToScroll, 10) || 1);
463
+ s.rows = Math.max(1, parseInt(s.rows, 10) || 1);
464
+ s.slidesPerRow = Math.max(1, parseInt(s.slidesPerRow, 10) || 1);
465
+ s.effect = s.fade ? 'fade' : s.effect || 'slide';
466
+ s.fade = s.effect === 'fade';
467
+ s.stacked = STACKED.indexOf(s.effect) > -1;
468
+ if (s.stacked) {
469
+ s.slidesToShow = 1;
470
+ s.slidesToScroll = 1;
471
+ }
472
+ if (s.effect === 'coverflow') s.centerMode = true;
473
+ if (s.freeMode || s.virtual || s.bleed) s.infinite = false;
474
+ if (s.marquee) {
475
+ s.infinite = true;
476
+ s.arrows = false;
477
+ s.dots = false;
478
+ s.autoplay = false;
479
+ }
480
+ s.spaceBetween = parseFloat(s.spaceBetween) || 0;
481
+ if (s.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
482
+ s.speed = 0;
483
+ s.autoplay = false;
484
+ }
485
+ return s;
486
+ }
487
+
488
+ _apply(first) {
489
+ const r = this._resolve();
490
+ const changed = r.bp !== this._bp;
491
+ this._bp = r.bp;
492
+ this._raw = r.s;
493
+ this.options = r.s;
494
+ this._dormant = !!r.dormant;
495
+ if (r.dormant) {
496
+ if (this.active) {
497
+ this._teardown();
498
+ this.active = false;
499
+ this.emit('destroy');
500
+ }
501
+ if (changed && !first) this.emit('breakpoint', r.bp);
502
+ return;
503
+ }
504
+ this.s = this._prepare(r.s);
505
+ if (this.active) this._teardown();
506
+ this._build(!first);
507
+ if (changed && !first) this.emit('breakpoint', r.bp);
508
+ }
509
+
510
+ _refreshKeep() {
511
+ if (!this.active) {
512
+ this._apply(false);
513
+ return;
514
+ }
515
+ this._teardown();
516
+ this._build(true);
517
+ }
518
+
519
+ _bindLife() {
520
+ let timer;
521
+ const onResize = () => {
522
+ clearTimeout(timer);
523
+ timer = setTimeout(() => this._onResize(), 40);
524
+ };
525
+ window.addEventListener('resize', onResize);
526
+ window.addEventListener('orientationchange', onResize);
527
+ const onVis = () => {
528
+ if (document.hidden) this._holdOn('hidden');
529
+ else this._holdOff('hidden');
530
+ };
531
+ document.addEventListener('visibilitychange', onVis);
532
+ this._life.push(() => {
533
+ clearTimeout(timer);
534
+ window.removeEventListener('resize', onResize);
535
+ window.removeEventListener('orientationchange', onResize);
536
+ document.removeEventListener('visibilitychange', onVis);
537
+ });
538
+ if (typeof ResizeObserver !== 'undefined') {
539
+ this._ro = new ResizeObserver(() => {
540
+ const w = this.el.clientWidth;
541
+ if (w !== this._lastW) {
542
+ this._lastW = w;
543
+ onResize();
544
+ }
545
+ });
546
+ this._ro.observe(this.el);
547
+ }
548
+ }
549
+
550
+ _onResize() {
551
+ const r = this._resolve();
552
+ const must = r.bp !== this._bp || (r.dormant && this.active) || (!r.dormant && !this.active);
553
+ if (must) {
554
+ this._apply(false);
555
+ return;
556
+ }
557
+ if (!this.active) return;
558
+ this._layout();
559
+ if (this._computeCan() !== this.canNav) {
560
+ this._refreshKeep();
561
+ return;
562
+ }
563
+ this._paintCurrent(false);
564
+ this.emit('setPosition');
565
+ }
566
+
567
+ /* ------------------------------------------------------------------ */
568
+ /* DOM build / teardown */
569
+ /* ------------------------------------------------------------------ */
570
+
571
+ _collect() {
572
+ const kids = Array.from(this.el.children);
573
+ const sel = this._base.slide;
574
+ this._sources = sel ? kids.filter((k) => k.matches(sel)) : kids;
575
+ }
576
+
577
+ _activeItems() {
578
+ const f = this._filter;
579
+ if (!f) return this._sources.slice();
580
+ return this._sources.filter((el, i) => (typeof f === 'function' ? f.call(el, el, i) : el.matches(f)));
581
+ }
582
+
583
+ _remember(el) {
584
+ if (this._stash.has(el)) return;
585
+ const o = {};
586
+ STASH_ATTRS.forEach((a) => (o[a] = el.getAttribute(a)));
587
+ this._stash.set(el, o);
588
+ }
589
+
590
+ _restoreItem(el) {
591
+ const o = this._stash.get(el);
592
+ if (!o) return;
593
+ STASH_ATTRS.forEach((a) => (o[a] === null ? el.removeAttribute(a) : el.setAttribute(a, o[a])));
594
+ el.querySelectorAll('[data-mozert-tabindex]').forEach((f) => {
595
+ const v = f.getAttribute('data-mozert-tabindex');
596
+ if (v === '') f.removeAttribute('tabindex');
597
+ else f.setAttribute('tabindex', v);
598
+ f.removeAttribute('data-mozert-tabindex');
599
+ });
600
+ el.querySelectorAll('[data-mozert-bg]').forEach((b) => b.removeAttribute('data-mozert-bg'));
601
+ this._stash.delete(el);
602
+ }
603
+
604
+ _label(key, vars) {
605
+ const l = Object.assign({}, LABELS, this.s && this.s.labels);
606
+ return String(l[key]).replace(/\{(\w+)\}/g, (m, k) => (vars && vars[k] !== undefined ? vars[k] : m));
607
+ }
608
+
609
+ _px(v) {
610
+ const p = String(v || '0');
611
+ if (p.indexOf('%') > -1) return (parseFloat(p) / 100) * this.el.clientWidth;
612
+ return parseFloat(p) || 0;
613
+ }
614
+
615
+ _padPx() {
616
+ return this._px(this.s.centerPadding);
617
+ }
618
+
619
+ _edgePad() {
620
+ const s = this.s;
621
+ return s.centerMode ? this._padPx() : this._px(s.edgePadding);
622
+ }
623
+
624
+ /** Works out how many slides fit (can be fractional) before the first layout. */
625
+ _calcShow() {
626
+ const s = this.s;
627
+ let show = s.slidesToShow;
628
+ if (s.slideWidth && !s.vertical && !s.variableWidth) {
629
+ const inner = Math.max(1, (this.list ? this.list.clientWidth : this.el.clientWidth) - 2 * this._edgePad());
630
+ show = Math.max(1, (inner + s.spaceBetween) / (parseFloat(s.slideWidth) + s.spaceBetween));
631
+ }
632
+ this._show = show;
633
+ this._showC = Math.ceil(show - 1e-6);
634
+ }
635
+
636
+ _build(keep) {
637
+ const s = this.s;
638
+ const el = this.el;
639
+ const items = this._activeItems();
640
+ // Slides hidden by filter() leave the DOM until unfilter() (or destroy) puts them back.
641
+ this._sources.forEach((n) => {
642
+ if (items.indexOf(n) === -1 && n.parentNode === el) el.removeChild(n);
643
+ });
644
+
645
+ el.classList.add('mozert-slider', 'mozert-initialized');
646
+ el.classList.toggle('mozert-vertical', !!s.vertical);
647
+ el.classList.toggle('mozert-rtl', !!s.rtl);
648
+ el.classList.toggle('mozert-fade', !!s.fade);
649
+ el.classList.toggle('mozert-fx', s.effect !== 'slide' && !s.fade);
650
+ if (s.effect !== 'slide') el.classList.add('mozert-effect-' + s.effect);
651
+ el.classList.toggle('mozert-center', !!s.centerMode);
652
+ el.classList.toggle('mozert-adaptive', !!s.adaptiveHeight);
653
+ el.classList.toggle('mozert-draggable', !!(s.draggable || s.swipe));
654
+ if (s.rtl) el.setAttribute('dir', 'rtl');
655
+ el.setAttribute('role', 'region');
656
+ el.setAttribute('aria-roledescription', 'carousel');
657
+
658
+ this.list = document.createElement('div');
659
+ this.list.className = 'mozert-list';
660
+ this.track = document.createElement('div');
661
+ this.track.className = 'mozert-track';
662
+ this.list.appendChild(this.track);
663
+ el.appendChild(this.list);
664
+
665
+ // Group into pages when grid mode is on.
666
+ let units = items;
667
+ if (s.rows > 1 || s.slidesPerRow > 1) {
668
+ units = [];
669
+ const per = s.rows * s.slidesPerRow;
670
+ for (let i = 0; i < items.length; i += per) {
671
+ const page = document.createElement('div');
672
+ page.className = 'mozert-page';
673
+ for (let r = 0; r < s.rows; r++) {
674
+ const row = document.createElement('div');
675
+ row.className = 'mozert-row';
676
+ for (let c = 0; c < s.slidesPerRow; c++) {
677
+ const it = items[i + r * s.slidesPerRow + c];
678
+ if (!it) break;
679
+ this._remember(it);
680
+ it.classList.add('mozert-cell');
681
+ it.style.width = 100 / s.slidesPerRow + '%';
682
+ row.appendChild(it);
683
+ }
684
+ if (row.children.length) page.appendChild(row);
685
+ }
686
+ units.push(page);
687
+ }
688
+ }
689
+ this._items = items;
690
+ this._units = units;
691
+ this.n = units.length;
692
+ this._calcShow();
693
+
694
+ units.forEach((u, i) => {
695
+ this._remember(u);
696
+ u.classList.add('mozert-slide');
697
+ u.setAttribute('data-mozert-index', i);
698
+ u.setAttribute('role', 'group');
699
+ u.setAttribute('aria-roledescription', 'slide');
700
+ u.setAttribute('aria-label', this._label('slide', { n: i + 1, total: units.length }));
701
+ u.querySelectorAll('[data-lazy-bg]').forEach((b, k) => b.setAttribute('data-mozert-bg', String(k)));
702
+ this.track.appendChild(u);
703
+ });
704
+ this.dom = units.slice();
705
+ this.cloneCount = 0;
706
+
707
+ // First pass (only for variable widths) so we know whether navigation is needed.
708
+ if (s.variableWidth) this._layout();
709
+ this.canNav = this._computeCan();
710
+
711
+ if (s.infinite && this.canNav && !s.stacked) this._addClones();
712
+
713
+ this._layout();
714
+ this.maxIndex = this._maxIndex();
715
+ this.current = clamp(keep ? this.current : parseInt(s.initialSlide, 10) || 0, 0, s.infinite ? Math.max(0, this.n - 1) : this.maxIndex);
716
+ if (!this.canNav) this.current = 0;
717
+ this._logical = this.current;
718
+
719
+ if (!this.canNav) el.classList.add('mozert-static');
720
+ else el.classList.remove('mozert-static');
721
+
722
+ this._buildArrows();
723
+ this._buildDots();
724
+ this._bindEvents();
725
+ this.active = true;
726
+ this._built = true;
727
+ this._paintCurrent(false);
728
+ if (s.fade) {
729
+ // Show the first slide immediately, then switch the cross-fade on.
730
+ this.dom.forEach((e) => (e.style.transition = 'none'));
731
+ void this.track.offsetWidth;
732
+ this.dom.forEach((e) => (e.style.transition = s.speed > 0 ? 'opacity ' + s.speed + 'ms ' + s.cssEase : 'none'));
733
+ }
734
+ this._updateNav();
735
+ this._lazy();
736
+ this._mi = Mozert._mods.map((m) => (m.create ? m.create(this) : null)).filter(Boolean);
737
+
738
+ this._autoOn = !!s.autoplay && this.canNav;
739
+ if (!this._initDone) {
740
+ this._initDone = true;
741
+ this._autoOn = !!s.autoplay && this.canNav;
742
+ this.emit('init');
743
+ } else if (keep) {
744
+ this.emit('reInit');
745
+ }
746
+ this._live();
747
+ this._schedule();
748
+ this.emit('setPosition');
749
+ }
750
+
751
+ _addClones() {
752
+ const s = this.s;
753
+ const n = this.n;
754
+ const count = s.variableWidth ? n * 2 : this._showC + s.slidesToScroll + (s.centerMode ? 1 : 0);
755
+ this.cloneCount = count;
756
+ const mk = (idx) => {
757
+ const base = this._units[((idx % n) + n) % n];
758
+ const c = base.cloneNode(true);
759
+ c.removeAttribute('id');
760
+ c.querySelectorAll('[id]').forEach((x) => x.removeAttribute('id'));
761
+ c.classList.remove('mozert-active', 'mozert-center');
762
+ c.classList.add('mozert-cloned');
763
+ c.setAttribute('aria-hidden', 'true');
764
+ c.removeAttribute('role');
765
+ c.removeAttribute('aria-label');
766
+ c.removeAttribute('aria-roledescription');
767
+ c.querySelectorAll(FOCUSABLE).forEach((f) => {
768
+ f.setAttribute('data-mozert-tabindex', f.getAttribute('tabindex') || '');
769
+ f.setAttribute('tabindex', '-1');
770
+ });
771
+ return c;
772
+ };
773
+ const before = [];
774
+ const after = [];
775
+ for (let i = -count; i < 0; i++) before.push(mk(i));
776
+ for (let i = 0; i < count; i++) after.push(mk(n + i));
777
+ const first = this._units[0];
778
+ before.forEach((c) => this.track.insertBefore(c, first));
779
+ after.forEach((c) => this.track.appendChild(c));
780
+ this.dom = before.concat(this._units, after);
781
+ }
782
+
783
+ _teardown() {
784
+ this._mi.forEach((m) => m.destroy && m.destroy());
785
+ this._mi = [];
786
+ this._fxHooks = [];
787
+ this._release = null;
788
+ this._lockDrag = false;
789
+ this._off.forEach((fn) => fn());
790
+ this._off = [];
791
+ clearTimeout(this._animTimer);
792
+ clearTimeout(this._apTimer);
793
+ if (this._raf) cancelAnimationFrame(this._raf);
794
+ this._raf = 0;
795
+ this.animating = false;
796
+ const el = this.el;
797
+
798
+ (this._items || []).forEach((it) => {
799
+ this._restoreItem(it);
800
+ // Slides removed through remove()/removeAll() must not come back.
801
+ if (this._sources.indexOf(it) > -1) el.appendChild(it);
802
+ });
803
+ (this._units || []).forEach((u) => {
804
+ if (this._items.indexOf(u) === -1) this._stash.delete(u);
805
+ });
806
+ if (this.list && this.list.parentNode) this.list.parentNode.removeChild(this.list);
807
+ (this._navNodes || []).forEach((fn) => fn());
808
+ this._navNodes = [];
809
+
810
+ Array.from(el.classList)
811
+ .filter((c) => c.indexOf('mozert') === 0)
812
+ .forEach((c) => el.classList.remove(c));
813
+ el.removeAttribute('role');
814
+ el.removeAttribute('aria-roledescription');
815
+ if (this._origDir === null) el.removeAttribute('dir');
816
+ else el.setAttribute('dir', this._origDir);
817
+ this.dom = [];
818
+ this.active = false;
819
+ }
820
+
821
+ /* ------------------------------------------------------------------ */
822
+ /* Measuring */
823
+ /* ------------------------------------------------------------------ */
824
+
825
+ _layout() {
826
+ const s = this.s;
827
+ const v = !!s.vertical;
828
+ const list = this.list;
829
+ const track = this.track;
830
+ const dom = this.dom;
831
+ this._calcShow();
832
+ const show = this._show;
833
+ const gap = s.spaceBetween;
834
+ const pad = this._edgePad();
835
+ const mProp = v ? 'marginBottom' : s.rtl ? 'marginLeft' : 'marginRight';
836
+ list.style.padding = v ? pad + 'px 0' : '0 ' + pad + 'px';
837
+
838
+ if (v) {
839
+ dom.forEach((e) => (e.style.height = ''));
840
+ track.style.height = '';
841
+ let h = 0;
842
+ this._units.forEach((u) => (h = Math.max(h, u.offsetHeight)));
843
+ this.slideSize = h;
844
+ dom.forEach((e) => {
845
+ e.style.height = h + 'px';
846
+ e.style[mProp] = gap ? gap + 'px' : '';
847
+ });
848
+ this.Lc = h * show + gap * (show - 1);
849
+ list.style.height = this.Lc + 2 * pad + 'px';
850
+ this.trackLen = (h + gap) * dom.length - gap;
851
+ track.style.height = this.trackLen + gap + 'px';
852
+ } else {
853
+ this.Lc = Math.max(0, list.clientWidth - 2 * pad);
854
+ if (!s.variableWidth) {
855
+ const w = s.slideWidth ? parseFloat(s.slideWidth) : (this.Lc - gap * (show - 1)) / show;
856
+ this.slideSize = w;
857
+ dom.forEach((e) => {
858
+ e.style.width = w + 'px';
859
+ e.style[mProp] = gap ? gap + 'px' : '';
860
+ });
861
+ this.trackLen = (w + gap) * dom.length - gap;
862
+ track.style.width = this.trackLen + gap + 'px';
863
+ } else {
864
+ dom.forEach((e) => (e.style[mProp] = gap ? gap + 'px' : ''));
865
+ track.style.width = '100000px';
866
+ }
867
+ }
868
+
869
+ const tr = track.getBoundingClientRect();
870
+ this._mags = [];
871
+ this._sizes = [];
872
+ dom.forEach((e) => {
873
+ const r = e.getBoundingClientRect();
874
+ if (v) {
875
+ this._mags.push(r.top - tr.top);
876
+ this._sizes.push(r.height);
877
+ } else if (s.rtl) {
878
+ this._mags.push(tr.right - r.right);
879
+ this._sizes.push(r.width);
880
+ } else {
881
+ this._mags.push(r.left - tr.left);
882
+ this._sizes.push(r.width);
883
+ }
884
+ });
885
+ if (!v && s.variableWidth) {
886
+ let end = 0;
887
+ this._mags.forEach((m, i) => (end = Math.max(end, m + this._sizes[i])));
888
+ this.trackLen = Math.ceil(end);
889
+ track.style.width = this.trackLen + 'px';
890
+ }
891
+ const cc = this.cloneCount;
892
+ const last = cc + this.n - 1;
893
+ this._realLen = this.n ? this._mags[last] + this._sizes[last] - this._mags[cc] : 0;
894
+
895
+ if (s.stacked) {
896
+ const sz = this.slideSize;
897
+ dom.forEach((e, i) => {
898
+ e.style.position = 'relative';
899
+ if (v) e.style.top = -i * sz + 'px';
900
+ else e.style.left = (s.rtl ? i * sz : -i * sz) + 'px';
901
+ });
902
+ }
903
+ this._adaptHeight();
904
+ this._arrowY();
905
+ this.maxIndex = this._maxIndex();
906
+ }
907
+
908
+ _computeCan() {
909
+ const s = this.s;
910
+ if (s.variableWidth) return this.n > 1 && this._realLen > this.Lc + 1;
911
+ return this.n > this._show + 1e-6;
912
+ }
913
+
914
+ _maxIndex() {
915
+ const s = this.s;
916
+ if (s.infinite || s.stacked) return Math.max(0, this.n - 1);
917
+ if (s.centerMode) return Math.max(0, this.n - 1);
918
+ if (s.variableWidth) {
919
+ const need = this._realLen - this.Lc - 0.5;
920
+ if (need <= 0) return 0;
921
+ for (let i = 0; i < this.n; i++) if (this._mags[this.cloneCount + i] - this._mags[this.cloneCount] >= need) return i;
922
+ return this.n - 1;
923
+ }
924
+ return Math.max(0, Math.ceil(this.n - this._show - 1e-6));
925
+ }
926
+
927
+ _adaptHeight() {
928
+ const s = this.s;
929
+ if (!s.adaptiveHeight || s.vertical || this._show !== 1) return;
930
+ const e = this.dom[this.cloneCount + this._logical] || this.dom[0];
931
+ if (!e) return;
932
+ this.list.style.transition = s.speed > 0 ? 'height ' + s.speed + 'ms ' + s.cssEase : 'none';
933
+ this.list.style.height = e.offsetHeight + 'px';
934
+ this._arrowY(e.offsetHeight);
935
+ }
936
+
937
+ /** Keeps arrows centred on the slides, not on the whole slider (dots/pagination sit below the list). */
938
+ _arrowY(h) {
939
+ const l = this.list;
940
+ if (!l || this.s.vertical) return;
941
+ const hh = h === undefined ? l.offsetHeight : h;
942
+ this.el.style.setProperty('--mozert-arrow-y', l.offsetTop + hh / 2 + 'px');
943
+ }
944
+
945
+ _norm(i) {
946
+ const n = this.n;
947
+ if (this.s.infinite || this.s.stacked) return ((i % n) + n) % n;
948
+ return clamp(i, 0, this.maxIndex);
949
+ }
950
+
951
+ _pos(L) {
952
+ const s = this.s;
953
+ const k = this.cloneCount + L;
954
+ if (this._mags[k] === undefined) return this._t;
955
+ let mag = this._mags[k];
956
+ let c = 0;
957
+ if (s.centerMode) c = (this.Lc - this._sizes[k]) / 2;
958
+ else if (!s.infinite) mag = Math.min(mag, Math.max(0, this.trackLen - this.Lc));
959
+ const dir = s.rtl && !s.vertical ? -1 : 1;
960
+ return dir * (c - mag);
961
+ }
962
+
963
+ /* ------------------------------------------------------------------ */
964
+ /* Movement */
965
+ /* ------------------------------------------------------------------ */
966
+
967
+ _paint(val, animate) {
968
+ const s = this.s;
969
+ const track = this.track;
970
+ this._cur = val;
971
+ if (s.useTransform) track.style.transform = s.vertical ? 'translate3d(0,' + val + 'px,0)' : 'translate3d(' + val + 'px,0,0)';
972
+ else track.style[s.vertical ? 'top' : 'left'] = val + 'px';
973
+ if (this._fxHooks.length) this._fx(this._pvFromT(val), !!animate);
974
+ }
975
+
976
+ /** Visual position in slide units (0 = first slide at the anchor), used by effects, scrollbar and parallax. */
977
+ _pvFromT(t) {
978
+ const s = this.s;
979
+ if (s.variableWidth || !this._mags || !this._mags.length) return this._logical;
980
+ const dir = s.rtl && !s.vertical ? -1 : 1;
981
+ const c = s.centerMode ? (this.Lc - this.slideSize) / 2 : 0;
982
+ const pitch = this.slideSize + s.spaceBetween;
983
+ return (c - dir * t - this._mags[this.cloneCount]) / (pitch || 1);
984
+ }
985
+
986
+ _fx(pv, animate) {
987
+ for (let i = 0; i < this._fxHooks.length; i++) this._fxHooks[i](pv, animate, this.s.speed);
988
+ }
989
+
990
+ /** For modules: run fn(pv, animate, speed) whenever the visual position changes. */
991
+ _onFx(fn) {
992
+ this._fxHooks.push(fn);
993
+ }
994
+
995
+ /** For modules: subscribe to a slider event for the life of this build. */
996
+ _sub(name, fn) {
997
+ this.on(name, fn);
998
+ this._off.push(() => this.off(name, fn));
999
+ }
1000
+
1001
+ _user() {
1002
+ if (this.s.disableOnInteraction && this._autoOn) this.pause();
1003
+ }
1004
+
1005
+ _setPos(t, animate, opts) {
1006
+ const s = this.s;
1007
+ const track = this.track;
1008
+ if (this._raf) cancelAnimationFrame(this._raf);
1009
+ this._raf = 0;
1010
+ if (animate && s.speed > 0) {
1011
+ const dur = opts && opts.duration ? opts.duration : s.speed;
1012
+ if (s.useCSS) {
1013
+ track.style.transition = (s.useTransform ? 'transform' : s.vertical ? 'top' : 'left') + ' ' + dur + 'ms ' + (opts && opts.ease ? opts.ease : s.cssEase);
1014
+ this._paint(t, true);
1015
+ } else {
1016
+ track.style.transition = 'none';
1017
+ const from = this._cur;
1018
+ const ease = typeof s.easing === 'function' ? s.easing : EASINGS[s.easing] || EASINGS.linear;
1019
+ const t0 = performance.now();
1020
+ const step = (now) => {
1021
+ const p = Math.min(1, (now - t0) / s.speed);
1022
+ this._paint(from + (t - from) * ease(p));
1023
+ this._raf = p < 1 ? requestAnimationFrame(step) : 0;
1024
+ };
1025
+ this._raf = requestAnimationFrame(step);
1026
+ }
1027
+ } else {
1028
+ track.style.transition = 'none';
1029
+ this._paint(t);
1030
+ void track.offsetWidth;
1031
+ }
1032
+ this._t = t;
1033
+ }
1034
+
1035
+ _paintCurrent(animate) {
1036
+ if (this.s.stacked) this._fx(this._logical, false);
1037
+ else this._setPos(this._pos(this._logical), animate);
1038
+ this._updateSlideClasses(this._logical);
1039
+ this._adaptHeight();
1040
+ }
1041
+
1042
+ _snapBack() {
1043
+ if (this.s.stacked) this._fx(this._logical, true);
1044
+ else this._setPos(this._pos(this._logical), true);
1045
+ }
1046
+
1047
+ _slideTo(target, opts = {}) {
1048
+ const s = this.s;
1049
+ if (!this.active || !this.canNav) return false;
1050
+ if (this.animating && !opts.fromSync && s.waitForAnimate) return false;
1051
+ let next = target;
1052
+ if (!s.infinite || s.stacked) next = s.stacked ? this._norm(next) : clamp(next, 0, this.maxIndex);
1053
+ const real = this._norm(next);
1054
+ // A synced partner follows the requested slide even when this slider cannot scroll that far
1055
+ // (a thumbnail strip stops at its last full page, but the main slider should still go to the clicked one).
1056
+ const want = s.infinite || s.stacked ? real : clamp(target, 0, this.n - 1);
1057
+ if (next === this.current && !this.animating && !opts.force) {
1058
+ if (!opts.fromSync && want !== next) {
1059
+ this._sel = want;
1060
+ this._updateSlideClasses(this._logical);
1061
+ this._syncNav(want);
1062
+ }
1063
+ return false;
1064
+ }
1065
+
1066
+ const old = this.current;
1067
+ this.emit('beforeChange', old, real);
1068
+ this._sel = opts.fromSync ? (opts.sel !== undefined && opts.sel !== next ? opts.sel : null) : want !== next ? want : null;
1069
+ if (!opts.fromSync) this._syncNav(want);
1070
+ clearTimeout(this._animTimer);
1071
+ this.current = real;
1072
+ this._logical = next;
1073
+ this._updateNav();
1074
+ this._updateSlideClasses(next);
1075
+ this._lazy();
1076
+
1077
+ const animate = opts.animate !== false;
1078
+ if (s.stacked) this._fx(next, animate && s.speed > 0);
1079
+ else this._setPos(this._pos(next), animate);
1080
+ if (!animate || s.speed <= 0) {
1081
+ this._finish();
1082
+ } else {
1083
+ this.animating = true;
1084
+ this._animTimer = setTimeout(() => this._finish(), s.speed);
1085
+ }
1086
+ return true;
1087
+ }
1088
+
1089
+ _finish() {
1090
+ this.animating = false;
1091
+ if (this._logical !== this.current) {
1092
+ this._logical = this.current;
1093
+ if (!this.s.stacked) this._setPos(this._pos(this.current), false);
1094
+ this._updateSlideClasses(this._logical);
1095
+ }
1096
+ this._adaptHeight();
1097
+ this.emit('afterChange', this.current);
1098
+ this._schedule();
1099
+ }
1100
+
1101
+ _updateSlideClasses(L) {
1102
+ const s = this.s;
1103
+ const show = this._show;
1104
+ const showC = this._showC;
1105
+ const half = Math.floor(showC / 2);
1106
+ const t = s.stacked ? 0 : this._pos(L);
1107
+ this.dom.forEach((e, k) => {
1108
+ const l = k - this.cloneCount;
1109
+ let active;
1110
+ let centered = false;
1111
+ if (s.stacked) {
1112
+ active = l === L;
1113
+ } else if (s.variableWidth) {
1114
+ const d = s.rtl ? this._mags[k] - t : this._mags[k] + t;
1115
+ active = d + this._sizes[k] > 0.5 && d < this.Lc - 0.5;
1116
+ centered = s.centerMode && l === L;
1117
+ } else if (s.centerMode) {
1118
+ active = l >= L - half && l <= L + half - (showC % 2 === 0 ? 1 : 0);
1119
+ centered = l === L;
1120
+ } else {
1121
+ active = l >= L && l <= L + showC - 1;
1122
+ }
1123
+ e.classList.toggle('mozert-active', active);
1124
+ e.classList.toggle('mozert-center', centered);
1125
+ e.classList.toggle('mozert-current', l === (this._sel != null ? this._sel : L));
1126
+ e.setAttribute('aria-hidden', active ? 'false' : 'true');
1127
+ if (s.stacked) e.style.zIndex = l === L ? s.zIndex : s.zIndex - 2;
1128
+ if (!e.classList.contains('mozert-cloned')) {
1129
+ e.querySelectorAll(FOCUSABLE).forEach((f) => {
1130
+ if (active) {
1131
+ if (f.hasAttribute('data-mozert-tabindex')) {
1132
+ const v = f.getAttribute('data-mozert-tabindex');
1133
+ if (v === '') f.removeAttribute('tabindex');
1134
+ else f.setAttribute('tabindex', v);
1135
+ f.removeAttribute('data-mozert-tabindex');
1136
+ }
1137
+ } else if (!f.hasAttribute('data-mozert-tabindex')) {
1138
+ f.setAttribute('data-mozert-tabindex', f.getAttribute('tabindex') || '');
1139
+ f.setAttribute('tabindex', '-1');
1140
+ }
1141
+ });
1142
+ }
1143
+ });
1144
+ }
1145
+
1146
+ /* ------------------------------------------------------------------ */
1147
+ /* Arrows and dots */
1148
+ /* ------------------------------------------------------------------ */
1149
+
1150
+ _buildArrows() {
1151
+ this._navNodes = this._navNodes || [];
1152
+ this._arrows = { prev: null, next: null };
1153
+ this._prev = this._next = null;
1154
+ const s = this.s;
1155
+ if (!s.arrows || !this.canNav) return;
1156
+ const mount = resolveNode(s.appendArrows) || this.el;
1157
+ const make = (val, html, cls, handler) => {
1158
+ const node = resolveNode(val) || htmlToEl(html);
1159
+ const created = !(val && !(typeof val === 'string' && val.trim().charAt(0) === '<'));
1160
+ node.classList.add('mozert-arrow', cls);
1161
+ if (!node.parentNode || created) mount.appendChild(node);
1162
+ const lab = this._label(cls === 'mozert-prev' ? 'prev' : 'next');
1163
+ if (!val || (!node.getAttribute('aria-label') && !node.textContent.trim())) node.setAttribute('aria-label', lab);
1164
+ node.addEventListener('click', handler);
1165
+ this._navNodes.push(() => {
1166
+ node.removeEventListener('click', handler);
1167
+ node.classList.remove('mozert-arrow', cls, 'mozert-disabled');
1168
+ node.removeAttribute('aria-disabled');
1169
+ if (created && node.parentNode) node.parentNode.removeChild(node);
1170
+ });
1171
+ return node;
1172
+ };
1173
+ this._arrows.prev = make(s.prevArrow, DEFAULT_PREV, 'mozert-prev', (e) => {
1174
+ e.preventDefault();
1175
+ this._user();
1176
+ this.prev();
1177
+ });
1178
+ this._arrows.next = make(s.nextArrow, DEFAULT_NEXT, 'mozert-next', (e) => {
1179
+ e.preventDefault();
1180
+ this._user();
1181
+ this.next();
1182
+ });
1183
+ this._prev = this._arrows.prev;
1184
+ this._next = this._arrows.next;
1185
+ }
1186
+
1187
+ _dotCount() {
1188
+ const s = this.s;
1189
+ if (!this.canNav) return 0;
1190
+ if (s.infinite) return Math.ceil(this.n / s.slidesToScroll);
1191
+ return Math.ceil(this.maxIndex / s.slidesToScroll) + 1;
1192
+ }
1193
+
1194
+ _buildDots() {
1195
+ this._dots = null;
1196
+ const s = this.s;
1197
+ if (!s.dots || !this.canNav) return;
1198
+ const count = this._dotCount();
1199
+ const ul = document.createElement('ul');
1200
+ ul.className = s.dotsClass;
1201
+ ul.setAttribute('role', 'tablist');
1202
+ for (let i = 0; i < count; i++) {
1203
+ const li = document.createElement('li');
1204
+ li.setAttribute('role', 'presentation');
1205
+ let inner = typeof s.customPaging === 'function' ? s.customPaging(this, i) : null;
1206
+ if (typeof inner === 'string') inner = htmlToEl(inner.trim().charAt(0) === '<' ? inner : '<button type="button">' + inner + '</button>');
1207
+ if (!isEl(inner)) {
1208
+ inner = document.createElement('button');
1209
+ inner.type = 'button';
1210
+ inner.setAttribute('aria-label', this._label('goTo', { n: i + 1 }));
1211
+ }
1212
+ inner.setAttribute('role', 'tab');
1213
+ li.appendChild(inner);
1214
+ li.addEventListener('click', (e) => {
1215
+ e.preventDefault();
1216
+ this._user();
1217
+ this._slideTo(this._dotTarget(i));
1218
+ });
1219
+ ul.appendChild(li);
1220
+ }
1221
+ (resolveNode(s.appendDots) || this.el).appendChild(ul);
1222
+ this._dots = ul;
1223
+ this._navNodes.push(() => ul.parentNode && ul.parentNode.removeChild(ul));
1224
+ if (s.pauseOnDotsHover) {
1225
+ const a = () => this._holdOn('dots');
1226
+ const b = () => this._holdOff('dots');
1227
+ ul.addEventListener('mouseenter', a);
1228
+ ul.addEventListener('mouseleave', b);
1229
+ }
1230
+ }
1231
+
1232
+ _dotTarget(i) {
1233
+ const s = this.s;
1234
+ const t = i * s.slidesToScroll;
1235
+ return s.infinite ? t : Math.min(t, this.maxIndex);
1236
+ }
1237
+
1238
+ _updateNav() {
1239
+ const s = this.s;
1240
+ if (this._dots) {
1241
+ const total = this._dots.children.length;
1242
+ const idx = s.infinite ? Math.floor(this.current / s.slidesToScroll) : Math.ceil(this.current / s.slidesToScroll);
1243
+ Array.from(this._dots.children).forEach((li, i) => {
1244
+ const on = i === Math.min(total - 1, idx);
1245
+ li.classList.toggle('mozert-active', on);
1246
+ if (li.firstElementChild) li.firstElementChild.setAttribute('aria-selected', on ? 'true' : 'false');
1247
+ });
1248
+ }
1249
+ const { prev, next } = this._arrows || {};
1250
+ if (!s.infinite && prev && next) {
1251
+ const setD = (node, d) => {
1252
+ node.classList.toggle('mozert-disabled', d);
1253
+ node.setAttribute('aria-disabled', d ? 'true' : 'false');
1254
+ if ('disabled' in node) node.disabled = d;
1255
+ };
1256
+ setD(prev, this.current <= 0);
1257
+ setD(next, this.current >= this.maxIndex);
1258
+ }
1259
+ }
1260
+
1261
+ /* ------------------------------------------------------------------ */
1262
+ /* Events: drag, keyboard, hover, focus */
1263
+ /* ------------------------------------------------------------------ */
1264
+
1265
+ _axis() {
1266
+ const s = this.s;
1267
+ if (s.vertical) return s.verticalSwiping ? 'y' : null;
1268
+ return 'x';
1269
+ }
1270
+
1271
+ _on(target, type, fn, opts) {
1272
+ target.addEventListener(type, fn, opts);
1273
+ this._off.push(() => target.removeEventListener(type, fn, opts));
1274
+ }
1275
+
1276
+ _bindEvents() {
1277
+ const s = this.s;
1278
+ const el = this.el;
1279
+ const list = this.list;
1280
+ const axis = this._axis();
1281
+ list.style.touchAction = axis === 'x' ? 'pan-y' : axis === 'y' ? 'pan-x' : '';
1282
+ if (s.accessibility) list.setAttribute('tabindex', '0');
1283
+
1284
+ // Keyboard
1285
+ this._on(el, 'keydown', (e) => {
1286
+ if (!s.accessibility || !this.canNav) return;
1287
+ const t = e.target;
1288
+ if (t && (/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName) || t.isContentEditable)) return;
1289
+ const rtl = s.rtl && !s.vertical;
1290
+ let handled = true;
1291
+ if (e.key === 'ArrowLeft' || (s.vertical && e.key === 'ArrowUp')) rtl ? this.next() : this.prev();
1292
+ else if (e.key === 'ArrowRight' || (s.vertical && e.key === 'ArrowDown')) rtl ? this.prev() : this.next();
1293
+ else if (e.key === 'Home') this.goTo(0);
1294
+ else if (e.key === 'End') this.goTo(this.n - 1);
1295
+ else handled = false;
1296
+ if (handled) e.preventDefault();
1297
+ });
1298
+
1299
+ // Pause on hover/focus
1300
+ if (s.pauseOnHover) {
1301
+ this._on(el, 'mouseenter', () => this._holdOn('hover'));
1302
+ this._on(el, 'mouseleave', () => this._holdOff('hover'));
1303
+ }
1304
+ if (s.pauseOnFocus) {
1305
+ this._on(el, 'focusin', () => this._holdOn('focus'));
1306
+ this._on(el, 'focusout', (e) => {
1307
+ if (!el.contains(e.relatedTarget)) this._holdOff('focus');
1308
+ });
1309
+ }
1310
+
1311
+ // Click handling (select-to-focus, suppress after drag)
1312
+ this._on(
1313
+ list,
1314
+ 'click',
1315
+ (e) => {
1316
+ if (this._dragged) {
1317
+ e.preventDefault();
1318
+ e.stopPropagation();
1319
+ this._dragged = false;
1320
+ return;
1321
+ }
1322
+ if (s.focusOnSelect && this.canNav) {
1323
+ const slide = e.target.closest ? e.target.closest('.mozert-slide') : null;
1324
+ if (slide && this.track.contains(slide)) {
1325
+ const i = parseInt(slide.getAttribute('data-mozert-index'), 10);
1326
+ if (!isNaN(i)) this.goTo(i);
1327
+ }
1328
+ }
1329
+ },
1330
+ true
1331
+ );
1332
+ this._on(list, 'dragstart', (e) => e.preventDefault());
1333
+ this._on(list, 'selectstart', (e) => {
1334
+ if (this._pointerDown) e.preventDefault();
1335
+ });
1336
+
1337
+ // Images that load late change heights
1338
+ this._on(
1339
+ this.track,
1340
+ 'load',
1341
+ () => {
1342
+ if (!this.active) return;
1343
+ clearTimeout(this._imgTimer);
1344
+ this._imgTimer = setTimeout(() => this.setPosition(), 30);
1345
+ },
1346
+ true
1347
+ );
1348
+
1349
+ if (axis && this.canNav) this._bindDrag(axis);
1350
+ }
1351
+
1352
+ _bindDrag(axis) {
1353
+ const s = this.s;
1354
+ const list = this.list;
1355
+ let d = null;
1356
+
1357
+ const onDown = (e) => {
1358
+ if (this._lockDrag) return;
1359
+ if (this.animating && s.waitForAnimate) return;
1360
+ if (e.pointerType === 'mouse') {
1361
+ if (e.button !== 0 || !s.draggable) return;
1362
+ } else if (!s.swipe) {
1363
+ return;
1364
+ }
1365
+ this._dragged = false;
1366
+ this._pointerDown = true;
1367
+ d = { id: e.pointerId, x: e.clientX, y: e.clientY, started: false, base: this._t, delta: 0, samples: [] };
1368
+ window.addEventListener('pointermove', onMove);
1369
+ window.addEventListener('pointerup', onUp);
1370
+ window.addEventListener('pointercancel', onCancel);
1371
+ };
1372
+
1373
+ const limits = () => {
1374
+ const a = this._pos(0);
1375
+ const b = this._pos(this.maxIndex);
1376
+ return [Math.min(a, b), Math.max(a, b)];
1377
+ };
1378
+
1379
+ const onMove = (e) => {
1380
+ if (!d || e.pointerId !== d.id) return;
1381
+ if (this._lockDrag && !d.started) return;
1382
+ const dx = e.clientX - d.x;
1383
+ const dy = e.clientY - d.y;
1384
+ const main = (axis === 'x' ? dx : dy) * s.touchRatio;
1385
+ const cross = axis === 'x' ? dy : dx;
1386
+ if (!d.started) {
1387
+ if (Math.abs(main) < s.dragThreshold || Math.abs(main) < Math.abs(cross)) return;
1388
+ d.started = true;
1389
+ this.el.classList.add('mozert-dragging');
1390
+ this._holdOn('drag');
1391
+ try {
1392
+ list.setPointerCapture(e.pointerId);
1393
+ } catch (err) {
1394
+ /* ignore */
1395
+ }
1396
+ this.track.style.transition = 'none';
1397
+ }
1398
+ d.delta = main;
1399
+ d.samples.push({ t: performance.now(), v: main });
1400
+ if (d.samples.length > 6) d.samples.shift();
1401
+ if (!s.touchMove) return;
1402
+ if (s.stacked) {
1403
+ const sgn = s.rtl && !s.vertical ? 1 : -1;
1404
+ let pv = this._logical + (sgn * main) / (axis === 'x' ? this.list.clientWidth : this.list.clientHeight);
1405
+ if (!s.infinite) {
1406
+ if (pv < 0) pv *= s.edgeFriction;
1407
+ else if (pv > this.maxIndex) pv = this.maxIndex + (pv - this.maxIndex) * s.edgeFriction;
1408
+ }
1409
+ this._fx(pv, false);
1410
+ return;
1411
+ }
1412
+ let raw = d.base + main;
1413
+ if (!s.infinite) {
1414
+ const [lo, hi] = limits();
1415
+ if (raw > hi) raw = hi + (raw - hi) * s.edgeFriction;
1416
+ else if (raw < lo) raw = lo - (lo - raw) * s.edgeFriction;
1417
+ }
1418
+ this._paint(raw);
1419
+ };
1420
+
1421
+ const end = () => {
1422
+ window.removeEventListener('pointermove', onMove);
1423
+ window.removeEventListener('pointerup', onUp);
1424
+ window.removeEventListener('pointercancel', onCancel);
1425
+ this._pointerDown = false;
1426
+ this.el.classList.remove('mozert-dragging');
1427
+ this._holdOff('drag');
1428
+ };
1429
+
1430
+ const onCancel = () => {
1431
+ if (!d) return;
1432
+ const started = d.started;
1433
+ d = null;
1434
+ end();
1435
+ if (started) this._snapBack();
1436
+ };
1437
+
1438
+ const onUp = (e) => {
1439
+ if (!d || e.pointerId !== d.id) return;
1440
+ const st = d;
1441
+ d = null;
1442
+ end();
1443
+ if (!st.started) return;
1444
+ this._dragged = true;
1445
+ const delta = st.delta;
1446
+ const size = axis === 'x' ? this.list.clientWidth : this.list.clientHeight;
1447
+ this._user();
1448
+ if (this._release && this._release(st, size, axis)) return;
1449
+ const fingerDir = axis === 'x' ? (delta < 0 ? 'left' : 'right') : delta < 0 ? 'up' : 'down';
1450
+ const rtl = s.rtl && !s.vertical;
1451
+ const forward = rtl ? delta > 0 : delta < 0;
1452
+ if (Math.abs(delta) < size / s.touchThreshold) {
1453
+ this._snapBack();
1454
+ return;
1455
+ }
1456
+ this.emit('swipe', fingerDir);
1457
+ let target;
1458
+ if (s.swipeToSlide) {
1459
+ const raw = st.base + delta;
1460
+ let best = this.current;
1461
+ let bestD = Infinity;
1462
+ const lo = s.infinite ? -this.cloneCount : 0;
1463
+ const hi = s.infinite ? this.n + this.cloneCount - 1 : this.maxIndex;
1464
+ for (let i = lo; i <= hi; i++) {
1465
+ const dist = Math.abs(this._pos(i) - raw);
1466
+ if (dist < bestD) {
1467
+ bestD = dist;
1468
+ best = i;
1469
+ }
1470
+ }
1471
+ target = best;
1472
+ } else {
1473
+ target = this.current + (forward ? 1 : -1) * s.slidesToScroll;
1474
+ }
1475
+ if (!s.infinite) {
1476
+ const clamped = clamp(target, 0, this.maxIndex);
1477
+ if (clamped === this.current) {
1478
+ this.emit('edge', fingerDir);
1479
+ this._snapBack();
1480
+ return;
1481
+ }
1482
+ target = clamped;
1483
+ }
1484
+ if (target === this.current) {
1485
+ this._snapBack();
1486
+ return;
1487
+ }
1488
+ this._slideTo(target);
1489
+ };
1490
+
1491
+ this._on(list, 'pointerdown', onDown);
1492
+ this._off.push(() => {
1493
+ window.removeEventListener('pointermove', onMove);
1494
+ window.removeEventListener('pointerup', onUp);
1495
+ window.removeEventListener('pointercancel', onCancel);
1496
+ });
1497
+ }
1498
+
1499
+ /* ------------------------------------------------------------------ */
1500
+ /* Autoplay */
1501
+ /* ------------------------------------------------------------------ */
1502
+
1503
+ _holdOn(reason) {
1504
+ this._pause.add(reason);
1505
+ clearTimeout(this._apTimer);
1506
+ }
1507
+
1508
+ _holdOff(reason) {
1509
+ this._pause.delete(reason);
1510
+ this._schedule();
1511
+ }
1512
+
1513
+ _live() {
1514
+ if (this.track) this.track.setAttribute('aria-live', this._autoOn && !this._pause.has('user') ? 'off' : 'polite');
1515
+ }
1516
+
1517
+ _schedule() {
1518
+ clearTimeout(this._apTimer);
1519
+ cancelAnimationFrame(this._apRaf);
1520
+ if (!this.active || !this.canNav || !this._autoOn) return;
1521
+ if (this._pause.size) return;
1522
+ const delay = this.s.autoplaySpeed;
1523
+ this._apTimer = setTimeout(() => this._tick(), delay);
1524
+ const cb = this.options.on && this.options.on.autoplayTimeLeft;
1525
+ if ((this._handlers.autoplayTimeLeft && this._handlers.autoplayTimeLeft.length) || cb) {
1526
+ const t0 = performance.now();
1527
+ const loop = () => {
1528
+ const left = Math.max(0, delay - (performance.now() - t0));
1529
+ this.emit('autoplayTimeLeft', left, left / delay);
1530
+ if (left > 0) this._apRaf = requestAnimationFrame(loop);
1531
+ };
1532
+ loop();
1533
+ }
1534
+ }
1535
+
1536
+ _tick() {
1537
+ const s = this.s;
1538
+ if (this._pause.size || !this.active) return;
1539
+ if (this.animating) {
1540
+ this._schedule();
1541
+ return;
1542
+ }
1543
+ const rev = s.autoplayReverse;
1544
+ if (s.infinite) {
1545
+ if (rev) this.prev();
1546
+ else this.next();
1547
+ return;
1548
+ }
1549
+ if (s.stopOnLastSlide && (rev ? this.current <= 0 : this.current >= this.maxIndex)) {
1550
+ this.pause();
1551
+ return;
1552
+ }
1553
+ if (this.current >= this.maxIndex) this._dir = -1;
1554
+ else if (this.current <= 0) this._dir = 1;
1555
+ if ((this._dir === 1) !== !!rev) this.next();
1556
+ else this.prev();
1557
+ }
1558
+
1559
+ /* ------------------------------------------------------------------ */
1560
+ /* Syncing and lazy loading */
1561
+ /* ------------------------------------------------------------------ */
1562
+
1563
+ _navTargets() {
1564
+ const v = this.s.asNavFor;
1565
+ if (!v) return [];
1566
+ let els = [];
1567
+ if (v instanceof Mozert) return [v];
1568
+ if (typeof v === 'string') els = Array.from(document.querySelectorAll(v));
1569
+ else if (isEl(v)) els = [v];
1570
+ else if (v.length) els = Array.from(v);
1571
+ return els.map((e) => registry.get(e)).filter((i) => i && i !== this && i.active && i.canNav);
1572
+ }
1573
+
1574
+ _syncNav(index) {
1575
+ this._navTargets().forEach((t) => t._slideTo(t.s.infinite ? index : clamp(index, 0, t.maxIndex), { fromSync: true, sel: index }));
1576
+ }
1577
+
1578
+ _lazy() {
1579
+ const s = this.s;
1580
+ if (!this.track || !this.n) return;
1581
+ const mode = s.lazyLoad;
1582
+ if (mode !== 'ondemand' && mode !== 'progressive') return;
1583
+ const from = this.current - (s.centerMode ? Math.floor(this._show / 2) : 0);
1584
+ const to = this.current + this._show - 1 + (s.centerMode ? Math.floor(this._show / 2) : 1);
1585
+ const seen = new Set();
1586
+ for (let i = from; i <= to; i++) {
1587
+ const r = ((i % this.n) + this.n) % this.n;
1588
+ if (!seen.has(r)) {
1589
+ seen.add(r);
1590
+ this._loadSlide(r);
1591
+ }
1592
+ }
1593
+ if (mode === 'progressive' && !this._progressing) this._progressive();
1594
+ }
1595
+
1596
+ _progressive() {
1597
+ this._progressing = true;
1598
+ const step = () => {
1599
+ if (!this.active) {
1600
+ this._progressing = false;
1601
+ return;
1602
+ }
1603
+ const next = this._units.findIndex((u) => u.querySelector('img[data-lazy]:not([data-mozert-loading]), [data-lazy-bg]:not([data-mozert-loading])'));
1604
+ if (next === -1) {
1605
+ this._progressing = false;
1606
+ return;
1607
+ }
1608
+ this._loadSlide(next, step);
1609
+ };
1610
+ step();
1611
+ }
1612
+
1613
+ _loadSlide(index, done) {
1614
+ const unit = this._units[index];
1615
+ if (!unit) return;
1616
+ const imgs = unit.querySelectorAll('img[data-lazy]');
1617
+ const bgs = unit.querySelectorAll('[data-lazy-bg]');
1618
+ if (!imgs.length && !bgs.length) {
1619
+ if (done) done();
1620
+ return;
1621
+ }
1622
+ let left = imgs.length + bgs.length;
1623
+ const fin = () => {
1624
+ if (--left === 0 && done) done();
1625
+ };
1626
+ bgs.forEach((el) => {
1627
+ if (el.hasAttribute('data-mozert-loading')) {
1628
+ fin();
1629
+ return;
1630
+ }
1631
+ el.setAttribute('data-mozert-loading', '');
1632
+ const src = el.getAttribute('data-lazy-bg');
1633
+ const k = el.getAttribute('data-mozert-bg');
1634
+ const apply = (n) => {
1635
+ n.style.backgroundImage = 'url("' + src.replace(/"/g, '%22') + '")';
1636
+ n.removeAttribute('data-lazy-bg');
1637
+ n.removeAttribute('data-mozert-loading');
1638
+ };
1639
+ const probe = new Image();
1640
+ probe.onload = () => {
1641
+ apply(el);
1642
+ this.dom.forEach((e) => {
1643
+ if (!e.classList.contains('mozert-cloned') || parseInt(e.getAttribute('data-mozert-index'), 10) !== index) return;
1644
+ const c = e.querySelector('[data-mozert-bg="' + k + '"]');
1645
+ if (c) apply(c);
1646
+ });
1647
+ this.emit('lazyLoaded', el, src);
1648
+ fin();
1649
+ };
1650
+ probe.onerror = () => {
1651
+ el.removeAttribute('data-lazy-bg');
1652
+ el.removeAttribute('data-mozert-loading');
1653
+ el.classList.add('mozert-lazy-error');
1654
+ this.emit('lazyLoadError', el, src);
1655
+ fin();
1656
+ };
1657
+ probe.src = src;
1658
+ });
1659
+ imgs.forEach((img) => {
1660
+ if (img.hasAttribute('data-mozert-loading')) {
1661
+ fin();
1662
+ return;
1663
+ }
1664
+ img.setAttribute('data-mozert-loading', '');
1665
+ img.classList.add('mozert-loading');
1666
+ const src = img.getAttribute('data-lazy');
1667
+ const srcset = img.getAttribute('data-lazy-srcset');
1668
+ const sizes = img.getAttribute('data-lazy-sizes');
1669
+ const probe = new Image();
1670
+ probe.onload = () => {
1671
+ if (srcset) img.srcset = srcset;
1672
+ if (sizes) img.sizes = sizes;
1673
+ img.src = src;
1674
+ img.removeAttribute('data-lazy');
1675
+ img.removeAttribute('data-mozert-loading');
1676
+ img.classList.remove('mozert-loading');
1677
+ img.classList.add('mozert-loaded');
1678
+ this._lazyClones(index, src, srcset, sizes, img);
1679
+ this.emit('lazyLoaded', img, src);
1680
+ fin();
1681
+ };
1682
+ probe.onerror = () => {
1683
+ img.removeAttribute('data-lazy');
1684
+ img.removeAttribute('data-mozert-loading');
1685
+ img.classList.remove('mozert-loading');
1686
+ img.classList.add('mozert-lazy-error');
1687
+ this.emit('lazyLoadError', img, src);
1688
+ fin();
1689
+ };
1690
+ if (srcset) probe.srcset = srcset;
1691
+ probe.src = src;
1692
+ });
1693
+ }
1694
+
1695
+ _lazyClones(index, src, srcset, sizes, realImg) {
1696
+ const imgsInUnit = Array.from(this._units[index].querySelectorAll('img'));
1697
+ const pos = imgsInUnit.indexOf(realImg);
1698
+ this.dom.forEach((e) => {
1699
+ if (!e.classList.contains('mozert-cloned') || parseInt(e.getAttribute('data-mozert-index'), 10) !== index) return;
1700
+ const c = e.querySelectorAll('img')[pos];
1701
+ if (!c) return;
1702
+ if (srcset) c.srcset = srcset;
1703
+ if (sizes) c.sizes = sizes;
1704
+ c.src = src;
1705
+ c.removeAttribute('data-lazy');
1706
+ c.classList.add('mozert-loaded');
1707
+ });
1708
+ }
1709
+ }
1710
+
1711
+ Mozert._mods = [];
1712
+ Mozert._boot = [];
1713
+ /** Register a module: { name, defaults?, create(slider) => { destroy() } | null }. */
1714
+ Mozert.use = function (mod) {
1715
+ if (mod && Mozert._mods.indexOf(mod) < 0) {
1716
+ Mozert._mods.push(mod);
1717
+ Object.keys(mod.defaults || {}).forEach((k) => {
1718
+ if (!(k in DEFAULTS)) DEFAULTS[k] = mod.defaults[k];
1719
+ });
1720
+ }
1721
+ return Mozert;
1722
+ };
1723
+ Mozert.version = VERSION;
1724
+ Mozert.defaults = DEFAULTS;
1725
+ Mozert._bridge = null;
1726
+
1727
+ //#endregion core
1728
+
1729
+ // Auto-init any element with a data-mozert attribute (set window.MozertAutoInit = false to opt out).
1730
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
1731
+ var auto = function () {
1732
+ if (window.MozertAutoInit === false) return;
1733
+ Mozert.initAll(document);
1734
+ };
1735
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', auto);
1736
+ else auto();
1737
+ }
1738
+ return Mozert;
1739
+ }));