vanilla-jui 1.3.9 → 1.3.11

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.
Files changed (76) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.umd.js +1 -1
  3. package/dist/style.css +1 -1
  4. package/package.json +3 -2
  5. package/src/components/accordion.js +373 -0
  6. package/src/components/components.js +0 -0
  7. package/src/components/drop.js +437 -0
  8. package/src/components/flow.js +1133 -0
  9. package/src/components/form.js +585 -0
  10. package/src/components/icons.js +83 -0
  11. package/src/components/menu.js +422 -0
  12. package/src/components/modal.js +1174 -0
  13. package/src/components/offcanvas.js +332 -0
  14. package/src/components/pagination.js +536 -0
  15. package/src/components/parabola.js +295 -0
  16. package/src/components/sticky.js +271 -0
  17. package/src/components/swiper.js +1123 -0
  18. package/src/components/tabs.js +746 -0
  19. package/src/components/theme.js +353 -0
  20. package/src/components/toast.js +329 -0
  21. package/src/components/toc.js +226 -0
  22. package/src/components/tooltip.js +86 -0
  23. package/src/components/validator.js +586 -0
  24. package/src/core/Component.js +248 -0
  25. package/src/css/accordion.css +72 -0
  26. package/src/css/animation.css +177 -0
  27. package/src/css/avatar.css +62 -0
  28. package/src/css/badge.css +66 -0
  29. package/src/css/breadcrumb.css +106 -0
  30. package/src/css/button.css +296 -0
  31. package/src/css/card.css +151 -0
  32. package/src/css/color.css +1163 -0
  33. package/src/css/color_p3.css +1172 -0
  34. package/src/css/components.css +26 -0
  35. package/src/css/content.css +716 -0
  36. package/src/css/divider.css +9 -0
  37. package/src/css/drop.css +28 -0
  38. package/src/css/flow.css +86 -0
  39. package/src/css/font.css +23 -0
  40. package/src/css/form.css +919 -0
  41. package/src/css/gradient.css +224 -0
  42. package/src/css/icon.css +63 -0
  43. package/src/css/index.css +12 -0
  44. package/src/css/layout.css +782 -0
  45. package/src/css/loading.css +38 -0
  46. package/src/css/menu.css +564 -0
  47. package/src/css/modal.css +94 -0
  48. package/src/css/offcanvas.css +104 -0
  49. package/src/css/pagination.css +25 -0
  50. package/src/css/popup.css +69 -0
  51. package/src/css/skeleton.css +76 -0
  52. package/src/css/status.css +56 -0
  53. package/src/css/swiper.css +160 -0
  54. package/src/css/table.css +52 -0
  55. package/src/css/tabs.css +95 -0
  56. package/src/css/tag.css +97 -0
  57. package/src/css/theme-palette.css +59 -0
  58. package/src/css/tip.css +84 -0
  59. package/src/css/toast.css +154 -0
  60. package/src/css/toc.css +38 -0
  61. package/src/css/tokens.css +1295 -0
  62. package/src/css/toolbar.css +115 -0
  63. package/src/css/tooltip.css +19 -0
  64. package/src/icons/index.js +30 -0
  65. package/src/index.js +45 -0
  66. package/src/locales/index.js +45 -0
  67. package/src/locales/index.min.js +1 -0
  68. package/src/utilities/browser.js +111 -0
  69. package/src/utilities/cache.js +159 -0
  70. package/src/utilities/core.js +502 -0
  71. package/src/utilities/debug.js +103 -0
  72. package/src/utilities/dom.js +313 -0
  73. package/src/utilities/events.js +69 -0
  74. package/src/utilities/http.js +30 -0
  75. package/src/utilities/legacy-dom.js +179 -0
  76. package/src/utilities/storage.js +46 -0
@@ -0,0 +1,1123 @@
1
+ import {
2
+ bindAttr,
3
+ bindClass,
4
+ createDeepStore,
5
+ createRoot,
6
+ h,
7
+ } from 'vanilla-signal';
8
+
9
+ import Component from '../core/Component.js';
10
+ import { resolveProps, validateParam } from '../utilities/core.js';
11
+ import {
12
+ all,
13
+ isElement,
14
+ isRenderableContent,
15
+ normalizeContentNodes,
16
+ q,
17
+ requireContainer,
18
+ requireRenderDOM,
19
+ } from '../utilities/dom.js';
20
+ import { icon } from './icons.js';
21
+
22
+ const SWIPE_THRESHOLD = 6;
23
+ const AUTOPLAY_DELAY_FLOOR = 16;
24
+
25
+ const SWIPER_OPTIONS_SCHEMA = {
26
+ data: {
27
+ default: null,
28
+ validate: (value) => value == null || Array.isArray(value),
29
+ message: 'expects an array or null.',
30
+ },
31
+ loop: { default: true, type: 'boolean' },
32
+ autoplay: { default: true, type: 'boolean' },
33
+ delay: {
34
+ default: 3000,
35
+ type: 'number',
36
+ validate: (value) => value >= 0,
37
+ message: 'expects a positive number or 0.',
38
+ },
39
+ lazyload: { default: true, type: 'boolean' },
40
+ pagination: { default: true, type: 'boolean' },
41
+ navigation: { default: true, type: 'boolean' },
42
+ speed: {
43
+ default: 300,
44
+ type: 'number',
45
+ validate: (value) => value >= 0,
46
+ message: 'expects a positive number or 0.',
47
+ },
48
+ touchRatio: {
49
+ default: 1,
50
+ type: 'number',
51
+ validate: (value) => value > 0,
52
+ message: 'expects a number greater than 0.',
53
+ },
54
+ touchAngle: {
55
+ default: 45,
56
+ type: 'number',
57
+ validate: (value) => value >= 0 && value <= 90,
58
+ message: 'expects a number between 0 and 90.',
59
+ },
60
+ longSwipesMs: {
61
+ default: 300,
62
+ type: 'number',
63
+ validate: (value) => value >= 0,
64
+ message: 'expects a positive number or 0.',
65
+ },
66
+ longSwipesRatio: {
67
+ default: 0.05,
68
+ type: 'number',
69
+ validate: (value) => value >= 0 && value <= 1,
70
+ message: 'expects a number between 0 and 1.',
71
+ },
72
+ preventClick: { default: true, type: 'boolean' },
73
+ };
74
+
75
+ const SWIPER_DATA_ITEM_RULE = {
76
+ validate: (value) => {
77
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
78
+ return false;
79
+ }
80
+
81
+ return (
82
+ (value.image == null || typeof value.image === 'string') &&
83
+ (value.url == null || typeof value.url === 'string') &&
84
+ (value.title == null || typeof value.title === 'string') &&
85
+ (value.sort == null || typeof value.sort === 'number') &&
86
+ (value.blank == null || typeof value.blank === 'boolean') &&
87
+ (value.children == null || isRenderableContent(value.children))
88
+ );
89
+ },
90
+ message:
91
+ 'expects items with optional image, url, title, sort, blank and children fields.',
92
+ };
93
+
94
+ function isInteractiveTarget(target) {
95
+ return !!target?.closest?.(
96
+ 'a, button, input, textarea, select, label, [data-swiper-ignore]'
97
+ );
98
+ }
99
+
100
+ function normalizeNumber(value, fallback = 0) {
101
+ const number = Number(value);
102
+ return Number.isFinite(number) ? number : fallback;
103
+ }
104
+
105
+ function getRenderableNodes(content, context) {
106
+ return normalizeContentNodes(content, context);
107
+ }
108
+
109
+ function resolveSwiperRoot(container) {
110
+ const roots = all('.j-swiper', container);
111
+ validateParam(
112
+ 'container',
113
+ roots,
114
+ {
115
+ validate: (value) => value.length === 1,
116
+ message: 'expects exactly one .j-swiper descendant.',
117
+ },
118
+ 'Swiper'
119
+ );
120
+ return roots[0];
121
+ }
122
+
123
+ /**
124
+ * 轻量轮播组件,继承 Component。
125
+ *
126
+ * 支持链接 slide、图片 lazyload、分页、导航、loop 和桌面/移动端拖拽滑动。
127
+ * 使用 vanilla-signal 响应式管理 pagination 和 navigation 状态。
128
+ */
129
+ export class Swiper extends Component {
130
+ /**
131
+ * 创建轮播实例。
132
+ * @param {Element|Node|string|Array} container 挂载容器、选择器或 JSX/h 返回节点。
133
+ * @param {object} [options={}] Swiper 配置。
134
+ */
135
+ constructor(container, options = {}) {
136
+ const resolvedOptions = resolveProps(
137
+ options,
138
+ SWIPER_OPTIONS_SCHEMA,
139
+ 'Swiper.options'
140
+ );
141
+
142
+ super(resolvedOptions);
143
+
144
+ this.dom.container = container;
145
+ this._built = false;
146
+
147
+ this.dom.mountTarget = null;
148
+ this.dom.createdRoot = false;
149
+ this.dom.createdSlides = false;
150
+ this.dom.wrapper = null;
151
+ this.dom.slides = [];
152
+ this.dom.pagination = null;
153
+ this.dom.prevButton = null;
154
+ this.dom.nextButton = null;
155
+ this.dom.bullets = [];
156
+ this.dom.createdPagination = false;
157
+ this.dom.createdPrevButton = false;
158
+ this.dom.createdNextButton = false;
159
+
160
+ this.runtime.logs = [];
161
+ this.runtime.startTarget = null;
162
+ this.runtime.touching = false;
163
+ this.runtime.scrolling = false;
164
+ this.runtime.swiping = false;
165
+ this.runtime.clickPrevented = false;
166
+ this.runtime.timer = null;
167
+ this.runtime.imageCleanups = new Set();
168
+
169
+ this.runtime.realCount = 0;
170
+
171
+ this.state = createDeepStore({
172
+ index: 0,
173
+ trackIndex: 0,
174
+ transform: 0,
175
+ animating: false,
176
+ width: 0,
177
+ });
178
+ }
179
+
180
+ /**
181
+ * 构建或绑定 Swiper DOM。
182
+ * @returns {Swiper} 当前实例。
183
+ */
184
+ build() {
185
+ if (this.runtime.destroyed)
186
+ throw new Error('Swiper.build: instance destroyed');
187
+ if (this._built) return this;
188
+
189
+ requireRenderDOM('Swiper');
190
+
191
+ const hasData = Array.isArray(this.props.data);
192
+ const root = requireContainer(this.dom.container, 'Swiper');
193
+
194
+ let mountRoot = null;
195
+ let mountTarget = null;
196
+
197
+ if (hasData) {
198
+ mountTarget = root;
199
+ mountRoot = h('div', { className: 'j-swiper' });
200
+ mountTarget.appendChild(mountRoot);
201
+ } else {
202
+ mountRoot = resolveSwiperRoot(root);
203
+ }
204
+
205
+ try {
206
+ this.dom.root = mountRoot;
207
+ this.dom.mountTarget = mountTarget;
208
+ this.dom.createdRoot = hasData;
209
+ this.dom.createdSlides = hasData;
210
+
211
+ const wrapper = hasData
212
+ ? this.createDataView(mountRoot, this.props.data)
213
+ : q('.swiper-wrapper', mountRoot);
214
+ validateParam(
215
+ 'wrapper',
216
+ wrapper,
217
+ {
218
+ validate: isElement,
219
+ message: 'expects .swiper-wrapper in the root element.',
220
+ },
221
+ 'Swiper'
222
+ );
223
+
224
+ this.dom.wrapper = wrapper;
225
+ this.dom.slides = all('.swiper-slide', wrapper);
226
+ this.dom.pagination = q('.swiper-pagination', mountRoot);
227
+ this.dom.prevButton = q('.swiper-navigation.is-prev', mountRoot);
228
+ this.dom.nextButton = q('.swiper-navigation.is-next', mountRoot);
229
+ this.dom.bullets = [];
230
+ this.dom.createdPagination = false;
231
+ this.dom.createdPrevButton = false;
232
+ this.dom.createdNextButton = false;
233
+
234
+ this.runtime.realCount = this.dom.slides.length;
235
+ this._built = true;
236
+
237
+ if (this.dom.slides.length > 0) this.onInit();
238
+ } catch (error) {
239
+ this.destroy();
240
+ throw error;
241
+ }
242
+
243
+ this.emit('init', this.props);
244
+ return this;
245
+ }
246
+
247
+ set index(v) {
248
+ this.setState({ index: normalizeNumber(v) });
249
+ }
250
+
251
+ set trackIndex(v) {
252
+ this.setState({ trackIndex: normalizeNumber(v) });
253
+ }
254
+
255
+ set transform(v) {
256
+ this.setState({ transform: normalizeNumber(v) });
257
+ }
258
+
259
+ set animating(v) {
260
+ this.setState({ animating: !!v });
261
+ }
262
+
263
+ set width(v) {
264
+ this.setState({ width: normalizeNumber(v) });
265
+ }
266
+
267
+ get realCount() {
268
+ if (this.runtime?.destroyed) return 0;
269
+ return this.runtime?.realCount || 0;
270
+ }
271
+
272
+ get realIndex() {
273
+ return this.toRealIndex();
274
+ }
275
+
276
+ assertBuilt(method) {
277
+ if (!this._built) {
278
+ throw new Error(`Swiper.${method}: call build() first.`);
279
+ }
280
+ }
281
+
282
+ createDataView(root, data) {
283
+ const items = this.normalizeData(data);
284
+ const wrapper = h('div', {
285
+ className: 'swiper-wrapper',
286
+ 'aria-live': 'polite',
287
+ });
288
+ root.textContent = '';
289
+
290
+ items.forEach((item, index) => {
291
+ wrapper.appendChild(this.createDataSlide(item, index));
292
+ });
293
+
294
+ root.appendChild(wrapper);
295
+ return wrapper;
296
+ }
297
+
298
+ normalizeData(data) {
299
+ data.forEach((item, index) => {
300
+ validateParam(
301
+ String(index),
302
+ item,
303
+ SWIPER_DATA_ITEM_RULE,
304
+ 'Swiper.options.data'
305
+ );
306
+ });
307
+
308
+ const items = data.map((item, index) => ({
309
+ ...item,
310
+ blank: item.blank !== false,
311
+ index,
312
+ }));
313
+ if (!items.some((item) => item.sort != null)) return items;
314
+
315
+ return items.sort((a, b) => {
316
+ if (a.sort == null && b.sort == null) return a.index - b.index;
317
+ if (a.sort == null) return 1;
318
+ if (b.sort == null) return -1;
319
+ return a.sort - b.sort || a.index - b.index;
320
+ });
321
+ }
322
+
323
+ createDataSlide(item, index) {
324
+ const slide = h(item.url ? 'a' : 'div', {
325
+ className: 'swiper-slide',
326
+ href: item.url || undefined,
327
+ target: item.url ? (item.blank ? '_blank' : '_self') : undefined,
328
+ 'data-swiper-index': String(index),
329
+ role: 'group',
330
+ 'aria-label': `Slide ${index + 1}`,
331
+ });
332
+
333
+ if (item.children != null) {
334
+ slide.append(
335
+ ...getRenderableNodes(item.children, { swiper: this, item, index })
336
+ );
337
+ return slide;
338
+ }
339
+
340
+ if (item.image) {
341
+ const img = h('img', {
342
+ className: 'swiper-image',
343
+ alt: item.title || '',
344
+ loading: 'lazy',
345
+ });
346
+ if (this.props.lazyload) img.dataset.lazy = item.image;
347
+ else img.src = item.image;
348
+ slide.appendChild(img);
349
+ }
350
+
351
+ if (item.title) {
352
+ slide.appendChild(
353
+ h('span', {
354
+ className: 'swiper-slide-title',
355
+ children: item.title,
356
+ })
357
+ );
358
+ }
359
+
360
+ return slide;
361
+ }
362
+
363
+ onInit() {
364
+ this.dom.wrapper.setAttribute('aria-live', 'polite');
365
+ this.updateSize();
366
+
367
+ if (this.props.loop && this.realCount > 1) {
368
+ this.initLoop();
369
+ }
370
+
371
+ this.setupStyles();
372
+
373
+ this.setTrackIndex(this.props.loop ? 1 : 0, false);
374
+ this.render(false);
375
+ this.bindEvents();
376
+
377
+ if (this.props.pagination) this.initPagination();
378
+ if (this.props.navigation) this.initNavigation();
379
+ if (this.props.lazyload) this.loadImages();
380
+ if (this.props.autoplay) this.play();
381
+ }
382
+
383
+ onDestroy() {
384
+ this.pause();
385
+ this.clearImageCleanups();
386
+ this.cleanup.events.clear();
387
+ this.cleanup.bindings?.();
388
+ this.cleanup.bindings = null;
389
+ this.cleanup.navBindings?.();
390
+ this.cleanup.navBindings = null;
391
+
392
+ if (this.dom.wrapper) {
393
+ all('[data-clone]', this.dom.wrapper).forEach((slide) => slide.remove());
394
+ }
395
+ if (this.dom.createdPagination) this.dom.pagination?.remove();
396
+ if (this.dom.createdPrevButton) this.dom.prevButton?.remove();
397
+ if (this.dom.createdNextButton) this.dom.nextButton?.remove();
398
+ if (this.dom.createdRoot) this.dom.root?.remove();
399
+ this._built = false;
400
+ }
401
+
402
+ updateSize() {
403
+ this.assertBuilt('updateSize');
404
+ this.width = this.dom.root.clientWidth || this.dom.root.offsetWidth;
405
+ }
406
+
407
+ refreshSlides() {
408
+ this.dom.slides = all('.swiper-slide', this.dom.wrapper);
409
+ this.runtime.realCount = this.dom.slides.filter(
410
+ (slide) => !slide.hasAttribute('data-clone')
411
+ ).length;
412
+ }
413
+
414
+ initLoop() {
415
+ const first = this.dom.slides[0].cloneNode(true);
416
+ const last = this.dom.slides[this.dom.slides.length - 1].cloneNode(true);
417
+
418
+ first.setAttribute('data-clone', '');
419
+ last.setAttribute('data-clone', '');
420
+ first.removeAttribute('data-swiper-index');
421
+ last.removeAttribute('data-swiper-index');
422
+
423
+ this.dom.wrapper.appendChild(first);
424
+ this.dom.wrapper.insertBefore(last, this.dom.slides[0]);
425
+ this.refreshSlides();
426
+ }
427
+
428
+ setupStyles() {
429
+ this.dom.root.style.setProperty(
430
+ '--swiper-slide-width',
431
+ `${this.state.width}px`
432
+ );
433
+ }
434
+
435
+ reInitView() {
436
+ this.pause();
437
+ this.clearImageCleanups();
438
+ this.cleanup.bindings?.();
439
+ this.cleanup.bindings = null;
440
+ this.cleanup.navBindings?.();
441
+ this.cleanup.navBindings = null;
442
+ this.cleanup.events.clear();
443
+
444
+ all('[data-clone]', this.dom.wrapper).forEach((slide) => slide.remove());
445
+
446
+ this.refreshSlides();
447
+
448
+ if (this.props.loop && this.realCount > 1) {
449
+ this.initLoop();
450
+ }
451
+
452
+ this.updateSize();
453
+ this.setupStyles();
454
+ this.setTrackIndex(this.trackIndexForRealIndex(this.state.index), false);
455
+ this.render(false);
456
+ this.bindEvents();
457
+
458
+ if (this.props.pagination) this.initPagination();
459
+ else this.clearPagination();
460
+
461
+ if (this.props.navigation) this.initNavigation();
462
+ else this.clearNavigation();
463
+
464
+ if (this.props.lazyload) this.loadImages();
465
+ if (this.props.autoplay) this.play();
466
+ }
467
+
468
+ clearPagination() {
469
+ this.cleanup.bindings?.();
470
+ this.cleanup.bindings = null;
471
+ this.dom.bullets.forEach((_, index) => {
472
+ this.cleanup.events.off(`bullet:${index}:click`);
473
+ this.cleanup.events.off(`bullet:${index}:keydown`);
474
+ });
475
+ this.dom.bullets = [];
476
+ if (this.dom.createdPagination) {
477
+ this.dom.pagination?.remove();
478
+ this.dom.pagination = null;
479
+ this.dom.createdPagination = false;
480
+ } else if (this.dom.pagination) {
481
+ this.dom.pagination.textContent = '';
482
+ }
483
+ }
484
+
485
+ clearNavigation() {
486
+ this.cleanup.navBindings?.();
487
+ this.cleanup.navBindings = null;
488
+ this.cleanup.events.off('nav:prev');
489
+ this.cleanup.events.off('nav:next');
490
+ if (this.dom.createdPrevButton) {
491
+ this.dom.prevButton?.remove();
492
+ this.dom.prevButton = null;
493
+ this.dom.createdPrevButton = false;
494
+ }
495
+ if (this.dom.createdNextButton) {
496
+ this.dom.nextButton?.remove();
497
+ this.dom.nextButton = null;
498
+ this.dom.createdNextButton = false;
499
+ }
500
+ }
501
+
502
+ bindEvents() {
503
+ this.cleanup.events.on(
504
+ 'touchstart',
505
+ this.dom.wrapper,
506
+ 'touchstart',
507
+ (event) => {
508
+ if (event.touches[0]) this.onStart(event.touches[0], event.target);
509
+ },
510
+ { passive: true }
511
+ );
512
+ this.cleanup.events.on(
513
+ 'touchmove',
514
+ this.dom.wrapper,
515
+ 'touchmove',
516
+ (event) => {
517
+ if (event.touches[0]) this.onMove(event.touches[0], event);
518
+ },
519
+ { passive: false }
520
+ );
521
+ this.cleanup.events.on(
522
+ 'touchend',
523
+ this.dom.wrapper,
524
+ 'touchend',
525
+ (event) => {
526
+ if (event.changedTouches[0]) this.pushLog(event.changedTouches[0]);
527
+ this.onEnd();
528
+ }
529
+ );
530
+ this.cleanup.events.on(
531
+ 'touchcancel',
532
+ this.dom.wrapper,
533
+ 'touchcancel',
534
+ () => {
535
+ this.resetDrag(true);
536
+ }
537
+ );
538
+ this.cleanup.events.on('window:touchcancel', window, 'touchcancel', () => {
539
+ this.resetDrag(true);
540
+ });
541
+ this.cleanup.events.on(
542
+ 'mousedown',
543
+ this.dom.wrapper,
544
+ 'mousedown',
545
+ (event) => {
546
+ if (event.button !== 0) return;
547
+ this.onStart(event, event.target);
548
+ this.dom.wrapper.style.cursor = 'grabbing';
549
+ }
550
+ );
551
+ this.cleanup.events.on(
552
+ 'mousemove',
553
+ this.dom.wrapper,
554
+ 'mousemove',
555
+ (event) => {
556
+ if (event.buttons === 1) this.onMove(event, event);
557
+ }
558
+ );
559
+ this.cleanup.events.on('window:mousemove', window, 'mousemove', (event) => {
560
+ if (this.runtime.touching && event.buttons === 1)
561
+ this.onMove(event, event);
562
+ });
563
+ this.cleanup.events.on('mouseup', this.dom.wrapper, 'mouseup', (event) => {
564
+ this.dom.wrapper.style.cursor = 'grab';
565
+ this.pushLog(event);
566
+ this.onEnd();
567
+ });
568
+ this.cleanup.events.on('window:mouseup', window, 'mouseup', (event) => {
569
+ if (!this.runtime.touching) return;
570
+ this.dom.wrapper.style.cursor = 'grab';
571
+ this.pushLog(event);
572
+ this.onEnd();
573
+ });
574
+ this.cleanup.events.on(
575
+ 'wrapper:mouseleave',
576
+ this.dom.wrapper,
577
+ 'mouseleave',
578
+ () => {
579
+ this.dom.wrapper.style.cursor = 'grab';
580
+ }
581
+ );
582
+ this.cleanup.events.on(
583
+ 'click',
584
+ this.dom.wrapper,
585
+ 'click',
586
+ (event) => {
587
+ if (!this.runtime.clickPrevented || !this.props.preventClick) return;
588
+ event.preventDefault();
589
+ event.stopPropagation();
590
+ this.runtime.clickPrevented = false;
591
+ },
592
+ { capture: true }
593
+ );
594
+ this.cleanup.events.on(
595
+ 'dragstart',
596
+ this.dom.wrapper,
597
+ 'dragstart',
598
+ (event) => event.preventDefault()
599
+ );
600
+ this.cleanup.events.on(
601
+ 'transitionend',
602
+ this.dom.wrapper,
603
+ 'transitionend',
604
+ (event) => this.onTransitionEnd(event)
605
+ );
606
+ this.cleanup.events.on('window:resize', window, 'resize', () =>
607
+ this.update(null, { force: true })
608
+ );
609
+ this.cleanup.events.on('root:mouseenter', this.dom.root, 'mouseenter', () =>
610
+ this.pause()
611
+ );
612
+ this.cleanup.events.on('root:mouseleave', this.dom.root, 'mouseleave', () =>
613
+ this.resume()
614
+ );
615
+ }
616
+
617
+ onStart(point, target = null) {
618
+ if (this.state.animating) return;
619
+
620
+ this.runtime.logs = [];
621
+ this.pushLog(point);
622
+ this.runtime.startTarget = target;
623
+ this.runtime.touching = true;
624
+ this.runtime.scrolling = false;
625
+ this.runtime.swiping = false;
626
+ this.runtime.clickPrevented = false;
627
+
628
+ this.pause();
629
+ this.dom.wrapper.style.transition = 'none';
630
+ }
631
+
632
+ onMove(point, event) {
633
+ if (
634
+ !this.runtime.touching ||
635
+ this.runtime.scrolling ||
636
+ this.state.animating
637
+ )
638
+ return;
639
+
640
+ this.pushLog(point);
641
+
642
+ const offset = this.getOffset();
643
+ const ax = Math.abs(offset.x);
644
+ const ay = Math.abs(offset.y);
645
+ if (ax < SWIPE_THRESHOLD && ay < SWIPE_THRESHOLD) return;
646
+
647
+ if (!this.runtime.swiping) {
648
+ const angle = (Math.atan2(ay, ax) * 180) / Math.PI;
649
+
650
+ if (angle < this.props.touchAngle) {
651
+ this.runtime.swiping = true;
652
+ this.runtime.clickPrevented = isInteractiveTarget(
653
+ this.runtime.startTarget
654
+ );
655
+ } else {
656
+ this.runtime.scrolling = true;
657
+ this.resetDrag(true);
658
+ return;
659
+ }
660
+ }
661
+
662
+ if (this.runtime.swiping) {
663
+ event.preventDefault();
664
+ this.transform =
665
+ -this.state.trackIndex * this.state.width +
666
+ offset.x * this.props.touchRatio;
667
+ this.render(false);
668
+ }
669
+ }
670
+
671
+ onEnd() {
672
+ if (!this.runtime.touching) return;
673
+
674
+ this.runtime.touching = false;
675
+
676
+ if (!this.runtime.swiping) {
677
+ this.resetDrag(true);
678
+ return;
679
+ }
680
+
681
+ const duration = this.getDuration();
682
+ const offset = this.getOffset();
683
+ const ox = offset.x;
684
+ let target = this.state.trackIndex;
685
+
686
+ if (duration > this.props.longSwipesMs) {
687
+ const steps = Math.ceil(
688
+ Math.abs(ox) / this.state.width - this.props.longSwipesRatio
689
+ );
690
+ if (steps > 0) target = this.state.trackIndex + steps * (ox > 0 ? -1 : 1);
691
+ } else {
692
+ const distance = Math.abs(ox) / this.state.width;
693
+ const slides =
694
+ distance >= this.props.longSwipesRatio
695
+ ? Math.max(1, Math.ceil(distance - this.props.longSwipesRatio))
696
+ : 0;
697
+ if (slides > 0)
698
+ target =
699
+ ox > 0
700
+ ? this.state.trackIndex - slides
701
+ : this.state.trackIndex + slides;
702
+ }
703
+
704
+ this.slideToTrack(target);
705
+ this.runtime.logs = [];
706
+ this.resume();
707
+ }
708
+
709
+ resetDrag(animate = true) {
710
+ const target =
711
+ this.state.trackIndex === 0
712
+ ? 0
713
+ : -this.state.trackIndex * this.state.width;
714
+ const shouldAnimate = animate && this.state.transform !== target;
715
+
716
+ this.runtime.touching = false;
717
+ this.runtime.scrolling = false;
718
+ this.runtime.swiping = false;
719
+ this.runtime.logs = [];
720
+ this.runtime.startTarget = null;
721
+ this.transform = target;
722
+ this.render(shouldAnimate);
723
+ this.resume();
724
+ }
725
+
726
+ onTransitionEnd(event) {
727
+ if (
728
+ event &&
729
+ (event.target !== this.dom.wrapper || event.propertyName !== 'transform')
730
+ )
731
+ return;
732
+
733
+ this.animating = false;
734
+
735
+ if (this.props.loop) {
736
+ if (this.state.trackIndex === 0) {
737
+ this.setTrackIndex(this.realCount, false);
738
+ this.render(false);
739
+ } else if (this.state.trackIndex === this.dom.slides.length - 1) {
740
+ this.setTrackIndex(1, false);
741
+ this.render(false);
742
+ }
743
+ }
744
+ }
745
+
746
+ pushLog(point) {
747
+ this.runtime.logs.push({
748
+ x: point.pageX ?? point.clientX ?? 0,
749
+ y: point.pageY ?? point.clientY ?? 0,
750
+ time: Date.now(),
751
+ });
752
+ if (this.runtime.logs.length > 5) this.runtime.logs.shift();
753
+ }
754
+
755
+ getDuration() {
756
+ if (this.runtime.logs.length === 0) return 0;
757
+ return (
758
+ this.runtime.logs[this.runtime.logs.length - 1].time -
759
+ this.runtime.logs[0].time
760
+ );
761
+ }
762
+
763
+ getOffset() {
764
+ if (this.runtime.logs.length === 0) return { x: 0, y: 0 };
765
+ const first = this.runtime.logs[0];
766
+ const last = this.runtime.logs[this.runtime.logs.length - 1];
767
+ return { x: last.x - first.x, y: last.y - first.y };
768
+ }
769
+
770
+ toRealIndex(index = this.state.trackIndex) {
771
+ if (!this.realCount) return 0;
772
+ if (!this.props.loop) return index;
773
+ if (index === 0) return this.realCount - 1;
774
+ if (index === this.dom.slides.length - 1) return 0;
775
+ return index - 1;
776
+ }
777
+
778
+ trackIndexForRealIndex(index) {
779
+ if (!this.realCount) return 0;
780
+ const target = Math.max(0, Math.min(index, this.realCount - 1));
781
+ return this.props.loop && this.realCount > 1 ? target + 1 : target;
782
+ }
783
+
784
+ setTrackIndex(trackIndex, animate = true) {
785
+ const target = normalizeNumber(trackIndex);
786
+ this.setState({
787
+ trackIndex: target,
788
+ index: this.toRealIndex(target),
789
+ transform: -target * this.state.width,
790
+ });
791
+ if (animate != null) this.render(animate);
792
+ }
793
+
794
+ slideTo(index) {
795
+ return this.slideToTrack(this.trackIndexForRealIndex(index));
796
+ }
797
+
798
+ slideToTrack(idx) {
799
+ this.assertBuilt('slideToTrack');
800
+ if (this.state.animating) return;
801
+ if (this.dom.slides.length === 0) return;
802
+
803
+ let target = idx;
804
+
805
+ if (this.props.loop) {
806
+ if (idx < 0) target = 0;
807
+ else if (idx >= this.dom.slides.length)
808
+ target = this.dom.slides.length - 1;
809
+ } else {
810
+ target = Math.max(0, Math.min(idx, this.dom.slides.length - 1));
811
+ }
812
+
813
+ this.setTrackIndex(target, true);
814
+
815
+ if (this.props.lazyload) this.loadImages();
816
+ }
817
+
818
+ next() {
819
+ this.assertBuilt('next');
820
+ if (this.state.animating) return;
821
+ this.slideToTrack(this.state.trackIndex + 1);
822
+ }
823
+
824
+ prev() {
825
+ this.assertBuilt('prev');
826
+ if (this.state.animating) return;
827
+ this.slideToTrack(this.state.trackIndex - 1);
828
+ }
829
+
830
+ render(animate) {
831
+ this.assertBuilt('render');
832
+ if (animate) {
833
+ this.dom.wrapper.style.transition = `transform ${this.props.speed}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
834
+ this.animating = true;
835
+ } else {
836
+ this.dom.wrapper.style.transition = 'none';
837
+ this.animating = false;
838
+ }
839
+
840
+ this.dom.wrapper.style.transform = `translate3d(${this.state.transform}px, 0, 0)`;
841
+ }
842
+
843
+ loadImages() {
844
+ this.assertBuilt('loadImages');
845
+ const indices = [this.state.trackIndex];
846
+ if (this.state.trackIndex > 0) indices.push(this.state.trackIndex - 1);
847
+ if (this.state.trackIndex < this.dom.slides.length - 1)
848
+ indices.push(this.state.trackIndex + 1);
849
+
850
+ indices.forEach((index) => {
851
+ const slide = this.dom.slides[index];
852
+ if (!slide) return;
853
+
854
+ const img = q('img[data-lazy]', slide);
855
+ if (!img || img.src) return;
856
+
857
+ img.classList.add('loading');
858
+ img.src = img.dataset.lazy;
859
+ const cleanup = () => {
860
+ img.onload = null;
861
+ img.onerror = null;
862
+ this.runtime.imageCleanups.delete(cleanup);
863
+ };
864
+ this.runtime.imageCleanups.add(cleanup);
865
+ img.onload = () => {
866
+ img.classList.remove('loading');
867
+ img.classList.add('loaded');
868
+ cleanup();
869
+ };
870
+ img.onerror = () => {
871
+ img.classList.remove('loading');
872
+ img.classList.add('error');
873
+ cleanup();
874
+ };
875
+ });
876
+ }
877
+
878
+ clearImageCleanups() {
879
+ this.runtime.imageCleanups?.forEach((cleanup) => cleanup());
880
+ this.runtime.imageCleanups?.clear();
881
+ }
882
+
883
+ initPagination() {
884
+ if (!this.dom.pagination) {
885
+ this.dom.pagination = h('div', {
886
+ className: 'swiper-pagination is-horizontal is-clickable is-bullet',
887
+ });
888
+ this.dom.root.appendChild(this.dom.pagination);
889
+ this.dom.createdPagination = true;
890
+ } else {
891
+ this.dom.pagination.classList.add(
892
+ 'is-horizontal',
893
+ 'is-clickable',
894
+ 'is-bullet'
895
+ );
896
+ }
897
+
898
+ this.dom.pagination.textContent = '';
899
+ this.dom.bullets = [];
900
+
901
+ for (let index = 0; index < this.realCount; index++) {
902
+ const bullet = h('button', {
903
+ type: 'button',
904
+ className: 'swiper-pagination-indicator swiper-pagination-bullet',
905
+ 'aria-label': `Go to slide ${index + 1}`,
906
+ });
907
+ this.cleanup.events.on(`bullet:${index}:click`, bullet, 'click', () => {
908
+ this.slideTo(index);
909
+ });
910
+ this.cleanup.events.on(
911
+ `bullet:${index}:keydown`,
912
+ bullet,
913
+ 'keydown',
914
+ (event) => {
915
+ if (event.key !== 'Enter' && event.key !== ' ') return;
916
+ event.preventDefault();
917
+ this.slideTo(index);
918
+ }
919
+ );
920
+ this.dom.bullets.push(bullet);
921
+ this.dom.pagination.appendChild(bullet);
922
+ }
923
+
924
+ this.cleanup.bindings = createRoot((dispose) => {
925
+ this.dom.bullets.forEach((bullet, i) => {
926
+ bindClass(bullet, 'is-active', () => this.state.index === i);
927
+ bindAttr(bullet, 'aria-current', () =>
928
+ this.state.index === i ? 'true' : null
929
+ );
930
+ });
931
+ return dispose;
932
+ });
933
+ }
934
+
935
+ initNavigation() {
936
+ this.dom.prevButton = this.ensureNavigation('prev', 'arrow-left');
937
+ this.dom.nextButton = this.ensureNavigation('next', 'arrow-right');
938
+
939
+ this.cleanup.events.on('nav:prev', this.dom.prevButton, 'click', () =>
940
+ this.prev()
941
+ );
942
+ this.cleanup.events.on('nav:next', this.dom.nextButton, 'click', () =>
943
+ this.next()
944
+ );
945
+
946
+ if (!this.props.loop) {
947
+ this.cleanup.navBindings = createRoot((dispose) => {
948
+ bindClass(
949
+ this.dom.prevButton,
950
+ 'is-disabled',
951
+ () => this.state.trackIndex <= 0
952
+ );
953
+ bindAttr(this.dom.prevButton, 'disabled', () =>
954
+ this.state.trackIndex <= 0 ? '' : null
955
+ );
956
+ bindClass(
957
+ this.dom.nextButton,
958
+ 'is-disabled',
959
+ () => this.state.trackIndex >= this.dom.slides.length - 1
960
+ );
961
+ bindAttr(this.dom.nextButton, 'disabled', () =>
962
+ this.state.trackIndex >= this.dom.slides.length - 1 ? '' : null
963
+ );
964
+ return dispose;
965
+ });
966
+ }
967
+ }
968
+
969
+ ensureNavigation(direction, iconName) {
970
+ const key =
971
+ direction === 'prev' ? 'createdPrevButton' : 'createdNextButton';
972
+ const className = `swiper-navigation is-${direction}`;
973
+ let button = q(`.swiper-navigation.is-${direction}`, this.dom.root);
974
+
975
+ if (!button) {
976
+ button = h('button', { type: 'button', className });
977
+ this.dom.root.appendChild(button);
978
+ this.dom[key] = true;
979
+ } else if (!button.matches('button')) {
980
+ button.setAttribute('role', 'button');
981
+ button.setAttribute('tabindex', '0');
982
+ button.classList.add('swiper-navigation', `is-${direction}`);
983
+ }
984
+
985
+ button.setAttribute(
986
+ 'aria-label',
987
+ direction === 'prev' ? 'Previous slide' : 'Next slide'
988
+ );
989
+ if (!button.querySelector('svg')) {
990
+ button.textContent = '';
991
+ button.appendChild(icon(iconName));
992
+ }
993
+
994
+ return button;
995
+ }
996
+
997
+ play() {
998
+ this.assertBuilt('play');
999
+ if (this.runtime.destroyed || this.runtime.timer) return;
1000
+ if (this.realCount <= 1) return;
1001
+ const delay = Math.max(this.props.delay, AUTOPLAY_DELAY_FLOOR);
1002
+ this.runtime.timer = setInterval(() => this.next(), delay);
1003
+ }
1004
+
1005
+ pause() {
1006
+ if (!this.runtime.timer) return;
1007
+ clearInterval(this.runtime.timer);
1008
+ this.runtime.timer = null;
1009
+ }
1010
+
1011
+ resume() {
1012
+ if (this.runtime.destroyed) return;
1013
+ if (this.props.autoplay && !this.runtime.timer) this.play();
1014
+ }
1015
+
1016
+ restartAutoplay() {
1017
+ this.pause();
1018
+ if (this.props.autoplay) this.play();
1019
+ }
1020
+
1021
+ update(propsPatch = {}, { force = false } = {}) {
1022
+ if (this.runtime.destroyed)
1023
+ throw new Error('Component.update: instance destroyed');
1024
+ this.assertBuilt('update');
1025
+
1026
+ const patch =
1027
+ propsPatch && typeof propsPatch === 'object' && !Array.isArray(propsPatch)
1028
+ ? propsPatch
1029
+ : {};
1030
+ const nextProps = resolveProps(
1031
+ Object.assign({}, this.props, patch),
1032
+ SWIPER_OPTIONS_SCHEMA,
1033
+ 'Swiper.options'
1034
+ );
1035
+ const normalizedPatch = {};
1036
+
1037
+ Object.keys(patch).forEach((key) => {
1038
+ normalizedPatch[key] = nextProps[key];
1039
+ });
1040
+
1041
+ this.props = nextProps;
1042
+ this.emit('beforeUpdate', normalizedPatch, { force });
1043
+ this.onUpdate(normalizedPatch, { force });
1044
+ this.emit('afterUpdate', normalizedPatch, { force });
1045
+ return this;
1046
+ }
1047
+
1048
+ onUpdate(propsPatch = {}) {
1049
+ if (
1050
+ propsPatch &&
1051
+ Object.prototype.hasOwnProperty.call(propsPatch, 'data')
1052
+ ) {
1053
+ this.updateData(propsPatch.data);
1054
+ return;
1055
+ }
1056
+
1057
+ const needsReInit = ['loop', 'pagination', 'navigation', 'lazyload'].some(
1058
+ (key) => Object.prototype.hasOwnProperty.call(propsPatch, key)
1059
+ );
1060
+
1061
+ if (needsReInit) {
1062
+ this.reInitView();
1063
+ return;
1064
+ }
1065
+
1066
+ this.updateSize();
1067
+ this.setupStyles();
1068
+ this.setTrackIndex(this.trackIndexForRealIndex(this.state.index), false);
1069
+
1070
+ if (
1071
+ Object.prototype.hasOwnProperty.call(propsPatch, 'autoplay') ||
1072
+ Object.prototype.hasOwnProperty.call(propsPatch, 'delay')
1073
+ ) {
1074
+ this.restartAutoplay();
1075
+ } else if (this.props.autoplay) this.resume();
1076
+ else this.pause();
1077
+ }
1078
+
1079
+ updateData(data = this.props.data) {
1080
+ if (this.runtime.destroyed)
1081
+ throw new Error('Swiper.updateData: instance destroyed');
1082
+ this.assertBuilt('updateData');
1083
+ if (!this.dom.createdRoot)
1084
+ throw new Error(
1085
+ 'Swiper.updateData: not supported on DOM-bound instances.'
1086
+ );
1087
+
1088
+ validateParam('data', data, SWIPER_OPTIONS_SCHEMA.data, 'Swiper.options');
1089
+ if (!Array.isArray(data)) {
1090
+ this.props.data = data;
1091
+ this.reInitView();
1092
+ return this;
1093
+ }
1094
+
1095
+ this.props.data = data;
1096
+ const realIndex = this.state.index;
1097
+
1098
+ this.pause();
1099
+ this.clearImageCleanups();
1100
+ this.cleanup.bindings?.();
1101
+ this.cleanup.bindings = null;
1102
+ this.cleanup.navBindings?.();
1103
+ this.cleanup.navBindings = null;
1104
+ this.cleanup.events.clear();
1105
+
1106
+ const items = this.normalizeData(data);
1107
+ this.dom.wrapper.textContent = '';
1108
+ items.forEach((item, index) => {
1109
+ this.dom.wrapper.appendChild(this.createDataSlide(item, index));
1110
+ });
1111
+ this.refreshSlides();
1112
+ this.runtime.realCount = this.dom.slides.length;
1113
+ this.setState({
1114
+ index: Math.min(realIndex, Math.max(0, this.realCount - 1)),
1115
+ });
1116
+ this.reInitView();
1117
+ return this;
1118
+ }
1119
+ }
1120
+
1121
+ export function createSwiper(container, input = {}) {
1122
+ return new Swiper(input);
1123
+ }