vanilla-jui 1.4.8 → 1.5.2

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 (58) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +402 -977
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -3
  7. package/src/components/accordion.ts +467 -349
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +199 -326
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +281 -361
  13. package/src/components/pagination.ts +207 -374
  14. package/src/components/swiper.ts +688 -1168
  15. package/src/components/tabs.ts +468 -788
  16. package/src/components/toc.ts +191 -244
  17. package/src/core/component.ts +332 -0
  18. package/src/core/motion.ts +298 -0
  19. package/src/core/presence.ts +233 -0
  20. package/src/core/scheduler.ts +91 -0
  21. package/src/core/view.ts +31 -0
  22. package/src/css/accordion.css +94 -3
  23. package/src/css/color.css +4 -5
  24. package/src/css/color_p3.css +6 -5
  25. package/src/css/content.css +18 -24
  26. package/src/css/drop.css +4 -1
  27. package/src/css/form.css +8 -0
  28. package/src/css/gradient.css +64 -131
  29. package/src/css/layout.css +9 -23
  30. package/src/css/menu.css +10 -5
  31. package/src/css/modal.css +2 -1
  32. package/src/css/offcanvas.css +1 -17
  33. package/src/css/swiper.css +2 -1
  34. package/src/css/tabs.css +8 -17
  35. package/src/css/themes/default/accordion.css +0 -48
  36. package/src/css/themes/default/swiper.css +1 -1
  37. package/src/css/themes/default/tabs.css +4 -6
  38. package/src/css/toast.css +1 -17
  39. package/src/css/tokens.css +18 -35
  40. package/src/css/tooltip.css +2 -1
  41. package/src/index.ts +8 -3
  42. package/src/locales/index.ts +3 -0
  43. package/src/primitives/drop.ts +253 -262
  44. package/src/primitives/loading.ts +22 -4
  45. package/src/primitives/parabola.ts +123 -164
  46. package/src/primitives/sticky.ts +297 -0
  47. package/src/primitives/theme.ts +164 -222
  48. package/src/primitives/toast.ts +309 -284
  49. package/src/primitives/tooltip.ts +65 -66
  50. package/src/utilities/browser.ts +38 -0
  51. package/src/utilities/dom.ts +0 -54
  52. package/src/utilities/refs.ts +61 -0
  53. package/src/utilities/state.ts +73 -0
  54. package/src/utilities/timer.ts +11 -31
  55. package/src/validation/validator.ts +289 -389
  56. package/src/components/sticky.ts +0 -297
  57. package/src/core/Component.ts +0 -377
  58. package/src/utilities/scheduler.ts +0 -156
@@ -1,30 +1,23 @@
1
1
  import {
2
- bindAttr,
3
- bindClass,
2
+ For,
4
3
  createDeepStore,
5
- createRoot,
6
- h,
4
+ createEffect,
5
+ createMemo,
6
+ flushSync,
7
+ jsx,
7
8
  } from 'vanilla-signal';
8
9
 
9
- import Component, {
10
- type ComponentDOM,
11
- type ComponentRuntime,
12
- } from '../core/Component.ts';
10
+ import {
11
+ type FunctionalComponent,
12
+ defineComponent,
13
+ } from '../core/component.ts';
13
14
  import { icon } from '../primitives/icons.ts';
15
+ import { createLoading } from '../primitives/loading.ts';
14
16
  import { joinClasses } from '../utilities/class-name.ts';
15
- import {
16
- all,
17
- isElement,
18
- normalizeContentNodes,
19
- q,
20
- type RenderableContent,
21
- } from '../utilities/dom.ts';
17
+ import { all, q, type RenderableContent } from '../utilities/dom.ts';
18
+ import { createEventManager } from '../utilities/events.ts';
22
19
  import { randomId } from '../utilities/id.ts';
23
- import {
24
- createStateSync,
25
- stateSnapshot,
26
- trackStoreVersion,
27
- } from '../utilities/scheduler.ts';
20
+ import { createScheduledTask } from '../core/scheduler.ts';
28
21
  import {
29
22
  type ResolveSchema,
30
23
  resolveProps,
@@ -51,9 +44,6 @@ export interface SwiperClassNames {
51
44
  next: string;
52
45
  active: string;
53
46
  disabled: string;
54
- loading: string;
55
- loaded: string;
56
- error: string;
57
47
  }
58
48
 
59
49
  export type SwiperClassNameConfig = Partial<SwiperClassNames>;
@@ -70,6 +60,14 @@ export interface SwiperDataItem extends Record<string, unknown> {
70
60
  interface NormalizedSwiperDataItem extends SwiperDataItem {
71
61
  blank: boolean;
72
62
  index: number;
63
+ key: string;
64
+ }
65
+
66
+ interface TrackItem {
67
+ item: NormalizedSwiperDataItem;
68
+ index: number;
69
+ clone: 'first' | 'last' | null;
70
+ key: string;
73
71
  }
74
72
 
75
73
  export interface SwiperSlideContext {
@@ -78,9 +76,15 @@ export interface SwiperSlideContext {
78
76
  index: number;
79
77
  }
80
78
 
79
+ export type SwiperDataLoader = (
80
+ swiper: Swiper
81
+ ) => SwiperDataItem[] | Promise<SwiperDataItem[]>;
82
+
83
+ export type SwiperDataSource = SwiperDataItem[] | SwiperDataLoader;
84
+
81
85
  export interface SwiperProps extends Record<string, unknown> {
82
86
  id?: string | null;
83
- data?: SwiperDataItem[];
87
+ data?: SwiperDataSource;
84
88
  loop?: boolean;
85
89
  autoplay?: boolean;
86
90
  delay?: number;
@@ -98,7 +102,7 @@ export interface SwiperProps extends Record<string, unknown> {
98
102
 
99
103
  interface ResolvedSwiperProps extends Record<string, unknown> {
100
104
  id: string | null;
101
- data: SwiperDataItem[];
105
+ data: SwiperDataSource;
102
106
  loop: boolean;
103
107
  autoplay: boolean;
104
108
  delay: number;
@@ -116,6 +120,7 @@ interface ResolvedSwiperProps extends Record<string, unknown> {
116
120
 
117
121
  interface SwiperState extends Record<string, unknown> {
118
122
  data: SwiperDataItem[];
123
+ loading: boolean;
119
124
  index: number;
120
125
  trackIndex: number;
121
126
  transform: number;
@@ -123,20 +128,6 @@ interface SwiperState extends Record<string, unknown> {
123
128
  width: number;
124
129
  }
125
130
 
126
- interface SwiperDOM extends ComponentDOM {
127
- root: HTMLElement | null;
128
- createdRoot: boolean;
129
- wrapper: HTMLElement | null;
130
- slides: HTMLElement[];
131
- pagination: HTMLElement | null;
132
- prevButton: HTMLButtonElement | null;
133
- nextButton: HTMLButtonElement | null;
134
- bullets: HTMLButtonElement[];
135
- createdPagination: boolean;
136
- createdPrevButton: boolean;
137
- createdNextButton: boolean;
138
- }
139
-
140
131
  interface SwipeLog {
141
132
  x: number;
142
133
  y: number;
@@ -150,28 +141,28 @@ interface SwipePoint {
150
141
  clientY?: number;
151
142
  }
152
143
 
153
- interface SwiperRuntime extends ComponentRuntime {
154
- built: boolean;
155
- logs: SwipeLog[];
156
- startTarget: EventTarget | null;
157
- touching: boolean;
158
- scrolling: boolean;
159
- swiping: boolean;
160
- clickPrevented: boolean;
161
- timer: ReturnType<typeof setInterval> | null;
162
- mountRefreshId: number | null;
163
- imageCleanups: Set<() => void>;
164
- realCount: number;
144
+ interface SwiperActions {
145
+ slideTo(index: number): void;
146
+ slideToTrack(index: number): void;
147
+ next(): void;
148
+ prev(): void;
149
+ play(): void;
150
+ pause(): void;
151
+ resume(): void;
152
+ restartAutoplay(): void;
165
153
  }
166
154
 
167
- interface SwiperCleanupExtras {
168
- bindings?: (() => void) | null;
169
- navBindings?: (() => void) | null;
170
- data?: (() => void) | null;
171
- }
155
+ type SwiperBase = FunctionalComponent<
156
+ ResolvedSwiperProps,
157
+ SwiperState,
158
+ HTMLElement,
159
+ SwiperActions
160
+ >;
172
161
 
173
- type SwiperDirection = 'prev' | 'next';
174
- type CreatedNavigationKey = 'createdPrevButton' | 'createdNextButton';
162
+ export type Swiper = SwiperBase & {
163
+ readonly realCount: number;
164
+ readonly realIndex: number;
165
+ };
175
166
 
176
167
  const DEFAULT_CLASS_NAMES: SwiperClassNames = {
177
168
  root: 'j-swiper',
@@ -190,9 +181,6 @@ const DEFAULT_CLASS_NAMES: SwiperClassNames = {
190
181
  next: 'is-next',
191
182
  active: 'is-active',
192
183
  disabled: 'is-disabled',
193
- loading: 'loading',
194
- loaded: 'loaded',
195
- error: 'error',
196
184
  };
197
185
 
198
186
  const SWIPER_OPTIONS_SCHEMA = {
@@ -203,44 +191,18 @@ const SWIPER_OPTIONS_SCHEMA = {
203
191
  normalize: (value: unknown) =>
204
192
  typeof value === 'string' ? value.trim() : value,
205
193
  },
206
- data: { default: [], type: 'array' },
194
+ data: { default: [], types: ['array', 'function'] },
207
195
  loop: { default: true, type: 'boolean' },
208
196
  autoplay: { default: true, type: 'boolean' },
209
- delay: {
210
- default: 3000,
211
- type: 'number',
212
- min: 0,
213
- },
197
+ delay: { default: 3000, type: 'number', min: 0 },
214
198
  lazyload: { default: true, type: 'boolean' },
215
199
  pagination: { default: true, type: 'boolean' },
216
200
  navigation: { default: true, type: 'boolean' },
217
- speed: {
218
- default: 300,
219
- type: 'number',
220
- min: 0,
221
- },
222
- touchRatio: {
223
- default: 1,
224
- type: 'number',
225
- greaterThan: 0,
226
- },
227
- touchAngle: {
228
- default: 45,
229
- type: 'number',
230
- min: 0,
231
- max: 90,
232
- },
233
- longSwipesMs: {
234
- default: 300,
235
- type: 'number',
236
- min: 0,
237
- },
238
- longSwipesRatio: {
239
- default: 0.05,
240
- type: 'number',
241
- min: 0,
242
- max: 1,
243
- },
201
+ speed: { default: 300, type: 'number', min: 0 },
202
+ touchRatio: { default: 1, type: 'number', greaterThan: 0 },
203
+ touchAngle: { default: 45, type: 'number', min: 0, max: 90 },
204
+ longSwipesMs: { default: 300, type: 'number', min: 0 },
205
+ longSwipesRatio: { default: 0.05, type: 'number', min: 0, max: 1 },
244
206
  preventClick: { default: true, type: 'boolean' },
245
207
  className: {
246
208
  default: DEFAULT_CLASS_NAMES,
@@ -265,7 +227,8 @@ const SWIPER_DATA_ITEM_RULE = {
265
227
  };
266
228
 
267
229
  const SWIPER_STATE_SCHEMA = {
268
- data: SWIPER_OPTIONS_SCHEMA.data,
230
+ data: { type: 'array' },
231
+ loading: { type: 'boolean' },
269
232
  index: { type: 'number' },
270
233
  trackIndex: { type: 'number' },
271
234
  transform: { type: 'number' },
@@ -287,16 +250,12 @@ function normalizeNumber(value: unknown, fallback = 0): number {
287
250
  return Number.isFinite(number) ? number : fallback;
288
251
  }
289
252
 
290
- function getRenderableNodes(
291
- content: RenderableContent<SwiperSlideContext>,
292
- context: SwiperSlideContext
293
- ): Node[] {
294
- return normalizeContentNodes(content, context);
253
+ function cloneData(data: SwiperDataItem[] | undefined): SwiperDataItem[] {
254
+ return Array.isArray(data) ? data.map((item) => ({ ...item })) : [];
295
255
  }
296
256
 
297
- function cloneData(data: SwiperDataItem[] | undefined): SwiperDataItem[] {
298
- if (!Array.isArray(data)) return [];
299
- return data.map((item) => ({ ...item }));
257
+ function cloneDataSource(source: SwiperDataSource): SwiperDataSource {
258
+ return Array.isArray(source) ? cloneData(source) : source;
300
259
  }
301
260
 
302
261
  function normalizeProps(input: SwiperProps = {}): ResolvedSwiperProps {
@@ -308,1146 +267,707 @@ function normalizeProps(input: SwiperProps = {}): ResolvedSwiperProps {
308
267
  return {
309
268
  ...props,
310
269
  id: props.id?.trim() || null,
311
- data: cloneData(props.data),
270
+ data: cloneDataSource(props.data),
312
271
  className: { ...props.className },
313
272
  };
314
273
  }
315
274
 
316
- function findUniqueElementById(id: string): HTMLElement | null {
317
- const matches = Array.from(
318
- document.querySelectorAll<HTMLElement>('[id]')
319
- ).filter((element) => element.id === id);
320
- validateParam(
321
- 'id',
322
- matches,
323
- {
324
- type: 'array',
325
- maxLength: 1,
326
- },
327
- 'Swiper.props'
328
- );
329
- return matches[0] || null;
330
- }
331
-
332
- /**
333
- * 轻量轮播组件,继承 Component。
334
- *
335
- * 支持链接 slide、图片 lazyload、分页、导航、loop 和桌面/移动端拖拽滑动。
336
- * 使用 vanilla-signal 响应式管理 pagination 和 navigation 状态。
337
- */
338
- class SwiperComponent extends Component<
339
- ResolvedSwiperProps,
340
- SwiperState,
341
- SwiperDOM
342
- > {
343
- declare props: ResolvedSwiperProps;
344
- declare state: SwiperState;
345
- declare dom: SwiperDOM;
346
- declare runtime: SwiperRuntime;
347
- declare cleanup: Component['cleanup'] & SwiperCleanupExtras;
348
-
349
- /**
350
- * 创建轮播实例。
351
- * @param {object} [props={}] Swiper 配置。
352
- */
353
- constructor(props: SwiperProps = {}) {
354
- const resolvedOptions = normalizeProps(props);
355
-
356
- super(resolvedOptions);
357
-
358
- this.dom.createdRoot = false;
359
- this.dom.wrapper = null;
360
- this.dom.slides = [];
361
- this.dom.pagination = null;
362
- this.dom.prevButton = null;
363
- this.dom.nextButton = null;
364
- this.dom.bullets = [];
365
- this.dom.createdPagination = false;
366
- this.dom.createdPrevButton = false;
367
- this.dom.createdNextButton = false;
368
-
369
- this.cleanup.data = null;
370
- this.cleanup.bindings = null;
371
- this.cleanup.navBindings = null;
372
-
373
- this.runtime.built = false;
374
- this.runtime.logs = [];
375
- this.runtime.startTarget = null;
376
- this.runtime.touching = false;
377
- this.runtime.scrolling = false;
378
- this.runtime.swiping = false;
379
- this.runtime.clickPrevented = false;
380
- this.runtime.timer = null;
381
- this.runtime.mountRefreshId = null;
382
- this.runtime.imageCleanups = new Set();
383
-
384
- this.runtime.realCount = 0;
385
-
386
- this.state = createDeepStore({
387
- data: cloneData(this.props.data),
388
- index: 0,
389
- trackIndex: 0,
390
- transform: 0,
391
- animating: false,
392
- width: 0,
393
- });
394
- }
395
-
396
- /**
397
- * 构建 Swiper DOM。
398
- * @returns {Swiper} 当前实例。
399
- */
400
- build(): this {
401
- if (this.runtime.destroyed)
402
- throw new Error('Swiper.build: instance destroyed');
403
- if (this.runtime.built) return this;
404
-
405
- const boundRoot = this.props.id
406
- ? findUniqueElementById(this.props.id)
407
- : null;
408
- const mountRoot =
409
- boundRoot ||
410
- (h('div', {
411
- id: this.props.id || randomId(),
412
- className: this.props.className.root,
413
- 'data-swiper': 'root',
414
- }) as HTMLElement);
415
-
416
- try {
417
- this.dom.root = mountRoot;
418
- this.dom.createdRoot = !boundRoot;
419
-
420
- const wrapper = this.dom.createdRoot
421
- ? this.createDataView(mountRoot, this.state.data)
422
- : q<HTMLElement>('[data-swiper-wrapper]', mountRoot);
423
- validateParam(
424
- 'wrapper',
425
- wrapper,
426
- {
427
- validate: isElement,
428
- message: 'expects [data-swiper-wrapper] in the root element.',
429
- },
430
- 'Swiper'
431
- );
432
-
433
- this.dom.wrapper = wrapper as HTMLElement;
434
- this.dom.slides = all<HTMLElement>(
435
- '[data-swiper-slide]',
436
- this.dom.wrapper
437
- );
438
- this.dom.pagination = q<HTMLElement>(
439
- '[data-swiper-pagination]',
440
- mountRoot
441
- );
442
- this.dom.prevButton = q<HTMLButtonElement>(
443
- '[data-action="prev"]',
444
- mountRoot
445
- );
446
- this.dom.nextButton = q<HTMLButtonElement>(
447
- '[data-action="next"]',
448
- mountRoot
449
- );
450
- this.dom.bullets = [];
451
- this.dom.createdPagination = false;
452
- this.dom.createdPrevButton = false;
453
- this.dom.createdNextButton = false;
454
-
455
- this.runtime.realCount = this.dom.slides.length;
456
- this.runtime.built = true;
457
-
458
- if (this.dom.slides.length > 0) this.onInit();
459
- if (this.dom.createdRoot) this.bindStateData();
460
- this.queueMountRefresh();
461
- } catch (error) {
462
- this.destroy();
463
- throw error;
464
- }
465
-
466
- this.emit('init', this.props);
467
- return this;
468
- }
469
-
470
- set index(v: unknown) {
471
- this.setState({ index: normalizeNumber(v) });
472
- }
473
-
474
- set trackIndex(v: unknown) {
475
- this.setState({ trackIndex: normalizeNumber(v) });
476
- }
477
-
478
- set transform(v: unknown) {
479
- this.setState({ transform: normalizeNumber(v) });
480
- }
481
-
482
- set animating(v: unknown) {
483
- this.setState({ animating: !!v });
484
- }
485
-
486
- set width(v: unknown) {
487
- this.setState({ width: normalizeNumber(v) });
488
- }
489
-
490
- get realCount(): number {
491
- if (this.runtime?.destroyed) return 0;
492
- return this.runtime?.realCount || 0;
493
- }
494
-
495
- get realIndex(): number {
496
- return this.toRealIndex();
497
- }
498
-
499
- private assertBuilt(method: string): void {
500
- if (!this.runtime.built) {
501
- throw new Error(`Swiper.${method}: call build() first.`);
502
- }
503
- }
504
-
505
- private createDataView(
506
- root: HTMLElement,
275
+ export function createSwiper(input: SwiperProps = {}): Swiper {
276
+ const props = normalizeProps(input);
277
+ const initialData = Array.isArray(props.data) ? props.data : [];
278
+ const state = createDeepStore({
279
+ data: cloneData(initialData),
280
+ loading: false,
281
+ index: 0,
282
+ trackIndex: 0,
283
+ transform: 0,
284
+ animating: false,
285
+ width: 0,
286
+ }) as SwiperState;
287
+ const events = createEventManager();
288
+ const itemKeys = new WeakMap<object, string>();
289
+ const imageCleanups = new Set<() => void>();
290
+ let swiper!: Swiper;
291
+ let wrapper: HTMLElement | null = null;
292
+ let slides: HTMLElement[] = [];
293
+ let normalizedItems: () => NormalizedSwiperDataItem[] = () => [];
294
+ let trackItems: () => TrackItem[] = () => [];
295
+ let dataLoadId = 0;
296
+ let timer: ReturnType<typeof setInterval> | null = null;
297
+ let mountFrame: number | null = null;
298
+ let logs: SwipeLog[] = [];
299
+ let startTarget: EventTarget | null = null;
300
+ let touching = false;
301
+ let scrolling = false;
302
+ let swiping = false;
303
+ let clickPrevented = false;
304
+
305
+ const itemKey = (item: SwiperDataItem): string => {
306
+ if (!itemKeys.has(item)) itemKeys.set(item, randomId());
307
+ return itemKeys.get(item) as string;
308
+ };
309
+ const normalizeData = (
507
310
  data: SwiperDataItem[]
508
- ): HTMLElement {
509
- const items = this.normalizeData(data);
510
- const wrapper = h('div', {
511
- className: this.props.className.wrapper,
512
- 'data-swiper-wrapper': '',
513
- 'aria-live': 'polite',
514
- }) as HTMLElement;
515
- root.textContent = '';
516
-
517
- items.forEach((item, index) => {
518
- wrapper.appendChild(this.createDataSlide(item, index));
519
- });
520
-
521
- root.appendChild(wrapper);
522
- return wrapper;
523
- }
524
-
525
- private bindStateData(): void {
526
- if (this.cleanup.data) return;
527
-
528
- this.cleanup.data = createStateSync(
529
- () => trackStoreVersion(this.state.data),
530
- (data) => this.syncStateData(data)
531
- );
532
- }
533
-
534
- private syncStateData(data: SwiperDataItem[]): void {
535
- if (!this.runtime.built || !this.dom.createdRoot) return;
536
- const nextData = cloneData(stateSnapshot(data));
537
- validateParam('data', nextData, SWIPER_OPTIONS_SCHEMA.data, 'Swiper.state');
538
-
539
- const realIndex = this.state.index;
540
- let wrapper = this.dom.wrapper;
541
- if (!wrapper && this.dom.root) {
542
- wrapper = this.createDataView(this.dom.root, nextData);
543
- this.dom.wrapper = wrapper;
544
- }
545
- if (!wrapper) return;
546
-
547
- this.pause();
548
- this.clearImageCleanups();
549
- this.cleanup.bindings?.();
550
- this.cleanup.bindings = null;
551
- this.cleanup.navBindings?.();
552
- this.cleanup.navBindings = null;
553
- this.cleanup.events.clear();
554
-
555
- all('[data-clone]', wrapper).forEach((slide) => slide.remove());
556
- wrapper.textContent = '';
557
- this.normalizeData(nextData).forEach((item, index) => {
558
- wrapper.appendChild(this.createDataSlide(item, index));
559
- });
560
- this.refreshSlides();
561
- this.runtime.realCount = this.dom.slides.length;
562
- this.setState({
563
- index: Math.min(realIndex, Math.max(0, this.realCount - 1)),
564
- });
565
- this.reInitView();
566
- }
567
-
568
- private normalizeData(data: SwiperDataItem[]): NormalizedSwiperDataItem[] {
569
- data.forEach((item, index) => {
311
+ ): NormalizedSwiperDataItem[] => {
312
+ const items = data.map((item, index) => {
570
313
  validateParam(
571
314
  String(index),
572
315
  item,
573
316
  SWIPER_DATA_ITEM_RULE,
574
- 'Swiper.options.data'
317
+ 'Swiper.state.data'
575
318
  );
319
+ return {
320
+ ...item,
321
+ blank: item.blank !== false,
322
+ index,
323
+ key: itemKey(item),
324
+ };
576
325
  });
577
-
578
- const items: NormalizedSwiperDataItem[] = data.map((item, index) => ({
579
- ...item,
580
- blank: item.blank !== false,
581
- index,
582
- }));
583
326
  if (!items.some((item) => item.sort != null)) return items;
584
-
585
327
  return items.sort((a, b) => {
586
328
  if (a.sort == null && b.sort == null) return a.index - b.index;
587
329
  if (a.sort == null) return 1;
588
330
  if (b.sort == null) return -1;
589
331
  return a.sort - b.sort || a.index - b.index;
590
332
  });
591
- }
592
-
593
- private createDataSlide(
594
- item: NormalizedSwiperDataItem,
595
- index: number
596
- ): HTMLElement {
597
- const slide = h(item.url ? 'a' : 'div', {
598
- className: this.props.className.slide,
599
- href: item.url || undefined,
600
- target: item.url ? (item.blank ? '_blank' : '_self') : undefined,
601
- 'data-swiper-slide': String(index),
602
- 'data-swiper-index': String(index),
603
- role: 'group',
604
- 'aria-label': `Slide ${index + 1}`,
605
- }) as HTMLElement;
606
-
607
- if (item.children != null) {
608
- slide.append(
609
- ...getRenderableNodes(item.children, { swiper: this, item, index })
610
- );
611
- return slide;
612
- }
613
-
614
- if (item.image) {
615
- const img = h('img', {
616
- className: this.props.className.image,
617
- alt: item.title || '',
618
- loading: 'lazy',
619
- }) as HTMLImageElement;
620
- if (this.props.lazyload) img.dataset.lazy = item.image;
621
- else img.src = item.image;
622
- slide.appendChild(img);
623
- }
624
-
625
- if (item.title) {
626
- slide.appendChild(
627
- h('span', {
628
- className: this.props.className.title,
629
- children: item.title,
630
- }) as HTMLElement
631
- );
632
- }
633
-
634
- return slide;
635
- }
636
-
637
- protected onInit(): void {
638
- if (!this.dom.root || !this.dom.wrapper) return;
639
- this.dom.wrapper.setAttribute('aria-live', 'polite');
640
- this.updateSize();
641
-
642
- if (this.props.loop && this.realCount > 1) {
643
- this.initLoop();
644
- }
645
-
646
- this.setupStyles();
647
-
648
- this.setTrackIndex(this.props.loop ? 1 : 0, false);
649
- this.render(false);
650
- this.bindEvents();
651
-
652
- if (this.props.pagination) this.initPagination();
653
- if (this.props.navigation) this.initNavigation();
654
- if (this.props.lazyload) this.loadImages();
655
- if (this.props.autoplay) this.play();
656
- }
657
-
658
- protected onDestroy(): void {
659
- this.pause();
660
- this.cancelMountRefresh();
661
- this.clearImageCleanups();
662
- this.cleanup.events.clear();
663
- this.cleanup.data?.();
664
- this.cleanup.data = null;
665
- this.cleanup.bindings?.();
666
- this.cleanup.bindings = null;
667
- this.cleanup.navBindings?.();
668
- this.cleanup.navBindings = null;
669
-
670
- if (this.dom.wrapper) {
671
- all('[data-clone]', this.dom.wrapper).forEach((slide) => slide.remove());
672
- }
673
- if (this.dom.createdPagination) this.dom.pagination?.remove();
674
- if (this.dom.createdPrevButton) this.dom.prevButton?.remove();
675
- if (this.dom.createdNextButton) this.dom.nextButton?.remove();
676
- if (this.dom.createdRoot) this.dom.root?.remove();
677
- this.runtime.built = false;
678
- }
679
-
680
- private updateSize(): void {
681
- this.assertBuilt('updateSize');
682
- if (!this.dom.root) return;
683
- const width = this.dom.root.clientWidth || this.dom.root.offsetWidth;
684
- if (width > 0 || !this.dom.root.isConnected || this.state.width === 0) {
685
- this.width = width;
686
- }
687
- }
688
-
689
- refresh(): this {
690
- this.assertBuilt('refresh');
691
- this.updateSize();
692
- this.setupStyles();
693
- this.setTrackIndex(this.trackIndexForRealIndex(this.state.index), false);
694
- if (this.props.lazyload) this.loadImages();
695
- if (this.props.autoplay && this.hasLayout()) this.resume();
696
- else this.pause();
697
- return this;
698
- }
699
-
700
- private hasLayout(): boolean {
701
- return !!this.dom.root?.isConnected && this.state.width > 0;
702
- }
703
-
704
- private syncLayout(): boolean {
705
- this.updateSize();
706
- if (!this.hasLayout()) return false;
707
- this.setupStyles();
708
- return true;
709
- }
710
-
711
- private queueMountRefresh(): void {
712
- if (!this.dom.createdRoot || !this.dom.root || this.runtime.mountRefreshId)
713
- return;
714
- if (typeof requestAnimationFrame !== 'function') {
715
- queueMicrotask(() => {
716
- if (
717
- !this.runtime.destroyed &&
718
- this.runtime.built &&
719
- this.dom.root?.isConnected
720
- ) {
721
- this.refresh();
722
- }
333
+ };
334
+ const createTrackItems = (items: NormalizedSwiperDataItem[]): TrackItem[] => {
335
+ const real = items.map((item, index) => ({
336
+ item,
337
+ index,
338
+ clone: null,
339
+ key: `real:${item.key}:${Boolean(item.url)}`,
340
+ })) satisfies TrackItem[];
341
+ if (!props.loop || items.length <= 1) return real;
342
+ const first = items[0];
343
+ const last = items[items.length - 1];
344
+ return [
345
+ {
346
+ item: last,
347
+ index: items.length - 1,
348
+ clone: 'last',
349
+ key: `clone:last:${last.key}:${Boolean(last.url)}`,
350
+ },
351
+ ...real,
352
+ {
353
+ item: first,
354
+ index: 0,
355
+ clone: 'first',
356
+ key: `clone:first:${first.key}:${Boolean(first.url)}`,
357
+ },
358
+ ];
359
+ };
360
+ const realCount = (): number => normalizedItems().length;
361
+ const trackCount = (): number => trackItems().length;
362
+ const toRealIndex = (index = state.trackIndex): number => {
363
+ const count = realCount();
364
+ if (!count) return 0;
365
+ if (!props.loop || count <= 1)
366
+ return Math.max(0, Math.min(index, count - 1));
367
+ if (index === 0) return count - 1;
368
+ if (index === trackCount() - 1) return 0;
369
+ return index - 1;
370
+ };
371
+ const trackIndexForRealIndex = (index: number): number => {
372
+ const count = realCount();
373
+ if (!count) return 0;
374
+ const target = Math.max(0, Math.min(index, count - 1));
375
+ return props.loop && count > 1 ? target + 1 : target;
376
+ };
377
+ const refreshSlides = (): void => {
378
+ slides = wrapper ? all<HTMLElement>('[data-swiper-slide]', wrapper) : [];
379
+ };
380
+ const clearImageCleanups = (): void => {
381
+ for (const cleanup of imageCleanups) cleanup();
382
+ imageCleanups.clear();
383
+ };
384
+ const updateSize = (): void => {
385
+ const root = swiper.element;
386
+ if (!root) return;
387
+ const width = root.clientWidth || root.offsetWidth;
388
+ if (width > 0 || !root.isConnected || state.width === 0) {
389
+ flushSync(() => {
390
+ state.width = width;
723
391
  });
724
- return;
725
392
  }
726
- this.runtime.mountRefreshId = requestAnimationFrame(() => {
727
- this.runtime.mountRefreshId = null;
728
- if (
729
- !this.runtime.destroyed &&
730
- this.runtime.built &&
731
- this.dom.root?.isConnected
732
- ) {
733
- this.refresh();
734
- }
393
+ };
394
+ const hasLayout = (): boolean =>
395
+ !!swiper.element?.isConnected && state.width > 0;
396
+ const syncLayout = (): boolean => {
397
+ updateSize();
398
+ return hasLayout();
399
+ };
400
+ const setTrackIndex = (index: number, animate = true): void => {
401
+ const target = normalizeNumber(index);
402
+ const transform = -target * state.width;
403
+ const shouldAnimate =
404
+ animate &&
405
+ props.speed > 0 &&
406
+ state.width > 0 &&
407
+ state.transform !== transform;
408
+ flushSync(() => {
409
+ state.trackIndex = target;
410
+ state.index = toRealIndex(target);
411
+ state.transform = transform;
412
+ state.animating = shouldAnimate;
735
413
  });
736
- }
737
-
738
- private cancelMountRefresh(): void {
739
- if (
740
- this.runtime.mountRefreshId == null ||
741
- typeof cancelAnimationFrame !== 'function'
742
- ) {
743
- this.runtime.mountRefreshId = null;
744
- return;
414
+ };
415
+ const loadImages = (): void => {
416
+ const indices = [state.trackIndex];
417
+ if (state.trackIndex > 0) indices.push(state.trackIndex - 1);
418
+ if (state.trackIndex < slides.length - 1)
419
+ indices.push(state.trackIndex + 1);
420
+ for (const index of indices) {
421
+ const image = slides[index]
422
+ ? q<HTMLImageElement>('img[data-lazy]', slides[index])
423
+ : null;
424
+ if (!image) continue;
425
+ const hasSource = !!image.getAttribute('src');
426
+ if (hasSource && !image.complete && (image.onload || image.onerror)) {
427
+ continue;
428
+ }
429
+ image.dataset.status = 'loading';
430
+ const cleanup = (): void => {
431
+ image.onload = null;
432
+ image.onerror = null;
433
+ imageCleanups.delete(cleanup);
434
+ };
435
+ imageCleanups.add(cleanup);
436
+ const markLoaded = (): void => {
437
+ image.dataset.status = 'loaded';
438
+ cleanup();
439
+ };
440
+ const markError = (): void => {
441
+ image.dataset.status = 'error';
442
+ cleanup();
443
+ };
444
+ image.onload = markLoaded;
445
+ image.onerror = markError;
446
+ if (!hasSource) image.src = image.dataset.lazy || '';
447
+ if (image.complete) {
448
+ if (image.naturalWidth > 0) markLoaded();
449
+ else markError();
450
+ }
745
451
  }
746
- cancelAnimationFrame(this.runtime.mountRefreshId);
747
- this.runtime.mountRefreshId = null;
748
- }
749
-
750
- private refreshSlides(): void {
751
- if (!this.dom.wrapper) return;
752
- this.dom.slides = all<HTMLElement>('[data-swiper-slide]', this.dom.wrapper);
753
- this.runtime.realCount = this.dom.slides.filter(
754
- (slide) => !slide.hasAttribute('data-clone')
755
- ).length;
756
- }
757
-
758
- private initLoop(): void {
759
- if (!this.dom.wrapper || this.dom.slides.length === 0) return;
760
-
761
- const first = this.dom.slides[0].cloneNode(true) as HTMLElement;
762
- const last = this.dom.slides[this.dom.slides.length - 1].cloneNode(
763
- true
764
- ) as HTMLElement;
765
-
766
- first.setAttribute('data-clone', '');
767
- last.setAttribute('data-clone', '');
768
- first.removeAttribute('data-swiper-index');
769
- last.removeAttribute('data-swiper-index');
770
-
771
- this.dom.wrapper.appendChild(first);
772
- this.dom.wrapper.insertBefore(last, this.dom.slides[0]);
773
- this.refreshSlides();
774
- }
452
+ };
775
453
 
776
- private setupStyles(): void {
777
- if (!this.dom.root) return;
778
- this.dom.root.style.setProperty(
779
- '--swiper-slide-width',
780
- `${this.state.width}px`
454
+ const pause = (): void => {
455
+ if (!timer) return;
456
+ clearInterval(timer);
457
+ timer = null;
458
+ };
459
+ const play = (): void => {
460
+ if (swiper.runtime.destroyed || timer || realCount() <= 1) return;
461
+ if (!syncLayout()) return;
462
+ timer = setInterval(
463
+ () => swiper.next(),
464
+ Math.max(props.delay, AUTOPLAY_DELAY_FLOOR)
781
465
  );
782
- }
783
-
784
- private reInitView(): void {
785
- if (!this.dom.wrapper) return;
786
- this.pause();
787
- this.clearImageCleanups();
788
- this.cleanup.bindings?.();
789
- this.cleanup.bindings = null;
790
- this.cleanup.navBindings?.();
791
- this.cleanup.navBindings = null;
792
- this.cleanup.events.clear();
793
-
794
- all('[data-clone]', this.dom.wrapper).forEach((slide) => slide.remove());
795
-
796
- this.refreshSlides();
797
-
798
- if (this.props.loop && this.realCount > 1) {
799
- this.initLoop();
466
+ };
467
+ const resume = (): void => {
468
+ if (props.autoplay && !timer && !swiper.runtime.destroyed) play();
469
+ };
470
+ const restartAutoplay = (): void => {
471
+ pause();
472
+ if (props.autoplay) play();
473
+ };
474
+ const pushLog = (point: SwipePoint): void => {
475
+ logs.push({
476
+ x: point.pageX ?? point.clientX ?? 0,
477
+ y: point.pageY ?? point.clientY ?? 0,
478
+ time: Date.now(),
479
+ });
480
+ if (logs.length > 5) logs.shift();
481
+ };
482
+ const offset = (): { x: number; y: number } => {
483
+ if (!logs.length) return { x: 0, y: 0 };
484
+ return {
485
+ x: logs[logs.length - 1].x - logs[0].x,
486
+ y: logs[logs.length - 1].y - logs[0].y,
487
+ };
488
+ };
489
+ const resetDrag = (animate = true): void => {
490
+ const target = -state.trackIndex * state.width;
491
+ const shouldAnimate = animate && state.transform !== target;
492
+ touching = false;
493
+ scrolling = false;
494
+ swiping = false;
495
+ logs = [];
496
+ startTarget = null;
497
+ flushSync(() => {
498
+ state.transform = target;
499
+ state.animating = shouldAnimate;
500
+ });
501
+ resume();
502
+ };
503
+ const onStart = (point: SwipePoint, target: EventTarget | null): void => {
504
+ if (state.animating || !syncLayout()) return;
505
+ logs = [];
506
+ pushLog(point);
507
+ startTarget = target;
508
+ touching = true;
509
+ scrolling = false;
510
+ swiping = false;
511
+ clickPrevented = false;
512
+ pause();
513
+ flushSync(() => {
514
+ state.animating = false;
515
+ });
516
+ };
517
+ const onMove = (point: SwipePoint, event: Event): void => {
518
+ if (!touching || scrolling || state.animating) return;
519
+ pushLog(point);
520
+ const current = offset();
521
+ const x = Math.abs(current.x);
522
+ const y = Math.abs(current.y);
523
+ if (x < SWIPE_THRESHOLD && y < SWIPE_THRESHOLD) return;
524
+ if (!swiping) {
525
+ const angle = (Math.atan2(y, x) * 180) / Math.PI;
526
+ if (angle < props.touchAngle) {
527
+ swiping = true;
528
+ clickPrevented = isInteractiveTarget(startTarget);
529
+ } else {
530
+ scrolling = true;
531
+ resetDrag(true);
532
+ return;
533
+ }
800
534
  }
801
-
802
- this.updateSize();
803
- this.setupStyles();
804
- this.setTrackIndex(this.trackIndexForRealIndex(this.state.index), false);
805
- this.render(false);
806
- this.bindEvents();
807
-
808
- if (this.props.pagination) this.initPagination();
809
- else this.clearPagination();
810
-
811
- if (this.props.navigation) this.initNavigation();
812
- else this.clearNavigation();
813
-
814
- if (this.props.lazyload) this.loadImages();
815
- if (this.props.autoplay) this.play();
816
- }
817
-
818
- private clearPagination(): void {
819
- this.cleanup.bindings?.();
820
- this.cleanup.bindings = null;
821
- this.dom.bullets.forEach((_, index) => {
822
- this.cleanup.events.off(`bullet:${index}:click`);
823
- this.cleanup.events.off(`bullet:${index}:keydown`);
535
+ event.preventDefault();
536
+ flushSync(() => {
537
+ state.transform =
538
+ -state.trackIndex * state.width + current.x * props.touchRatio;
824
539
  });
825
- this.dom.bullets = [];
826
- if (this.dom.createdPagination) {
827
- this.dom.pagination?.remove();
828
- this.dom.pagination = null;
829
- this.dom.createdPagination = false;
830
- } else if (this.dom.pagination) {
831
- this.dom.pagination.textContent = '';
540
+ };
541
+ const onEnd = (): void => {
542
+ if (!touching) return;
543
+ touching = false;
544
+ if (!swiping || state.width <= 0) {
545
+ resetDrag(true);
546
+ return;
832
547
  }
833
- }
834
-
835
- private clearNavigation(): void {
836
- this.cleanup.navBindings?.();
837
- this.cleanup.navBindings = null;
838
- this.cleanup.events.off('nav:prev');
839
- this.cleanup.events.off('nav:next');
840
- if (this.dom.createdPrevButton) {
841
- this.dom.prevButton?.remove();
842
- this.dom.prevButton = null;
843
- this.dom.createdPrevButton = false;
548
+ const duration = logs.length
549
+ ? logs[logs.length - 1].time - logs[0].time
550
+ : 0;
551
+ const x = offset().x;
552
+ const distance = Math.abs(x) / state.width;
553
+ let steps = 0;
554
+ if (duration > props.longSwipesMs) {
555
+ steps = Math.ceil(distance - props.longSwipesRatio);
556
+ } else if (distance >= props.longSwipesRatio) {
557
+ steps = Math.max(1, Math.ceil(distance - props.longSwipesRatio));
844
558
  }
845
- if (this.dom.createdNextButton) {
846
- this.dom.nextButton?.remove();
847
- this.dom.nextButton = null;
848
- this.dom.createdNextButton = false;
559
+ swiper.slideToTrack(state.trackIndex + steps * (x > 0 ? -1 : 1));
560
+ logs = [];
561
+ resume();
562
+ };
563
+ const onTransitionEnd = (event: Event): void => {
564
+ if (
565
+ event.target !== wrapper ||
566
+ !('propertyName' in event) ||
567
+ event.propertyName !== 'transform'
568
+ ) {
569
+ return;
849
570
  }
850
- }
571
+ flushSync(() => {
572
+ state.animating = false;
573
+ });
574
+ if (!props.loop || realCount() <= 1) return;
575
+ if (state.trackIndex === 0) setTrackIndex(realCount(), false);
576
+ else if (state.trackIndex === trackCount() - 1) setTrackIndex(1, false);
577
+ };
851
578
 
852
- private bindEvents(): void {
853
- const root = this.dom.root;
854
- const wrapper = this.dom.wrapper;
579
+ const bindEvents = (): void => {
580
+ const root = swiper.element;
855
581
  if (!root || !wrapper) return;
856
-
857
- this.cleanup.events.on(
582
+ events.clear();
583
+ events.on(
858
584
  'touchstart',
859
585
  wrapper,
860
586
  'touchstart',
861
587
  (event) => {
862
- const touchEvent = event as TouchEvent;
863
- if (touchEvent.touches[0])
864
- this.onStart(touchEvent.touches[0], touchEvent.target);
588
+ const touch = (event as TouchEvent).touches[0];
589
+ if (touch) onStart(touch, event.target);
865
590
  },
866
591
  { passive: true }
867
592
  );
868
- this.cleanup.events.on(
593
+ events.on(
869
594
  'touchmove',
870
595
  wrapper,
871
596
  'touchmove',
872
597
  (event) => {
873
- const touchEvent = event as TouchEvent;
874
- if (touchEvent.touches[0]) this.onMove(touchEvent.touches[0], event);
598
+ const touch = (event as TouchEvent).touches[0];
599
+ if (touch) onMove(touch, event);
875
600
  },
876
601
  { passive: false }
877
602
  );
878
- this.cleanup.events.on('touchend', wrapper, 'touchend', (event) => {
879
- const touchEvent = event as TouchEvent;
880
- if (touchEvent.changedTouches[0])
881
- this.pushLog(touchEvent.changedTouches[0]);
882
- this.onEnd();
883
- });
884
- this.cleanup.events.on('touchcancel', wrapper, 'touchcancel', () => {
885
- this.resetDrag(true);
886
- });
887
- this.cleanup.events.on('window:touchcancel', window, 'touchcancel', () => {
888
- this.resetDrag(true);
889
- });
890
- this.cleanup.events.on('mousedown', wrapper, 'mousedown', (event) => {
891
- const mouseEvent = event as MouseEvent;
892
- if (mouseEvent.button !== 0) return;
893
- this.onStart(mouseEvent, mouseEvent.target);
894
- wrapper.style.cursor = 'grabbing';
895
- });
896
- this.cleanup.events.on('mousemove', wrapper, 'mousemove', (event) => {
897
- const mouseEvent = event as MouseEvent;
898
- if (mouseEvent.buttons === 1) this.onMove(mouseEvent, event);
603
+ events.on('touchend', wrapper, 'touchend', (event) => {
604
+ const touch = (event as TouchEvent).changedTouches[0];
605
+ if (touch) pushLog(touch);
606
+ onEnd();
899
607
  });
900
- this.cleanup.events.on('window:mousemove', window, 'mousemove', (event) => {
901
- const mouseEvent = event as MouseEvent;
902
- if (this.runtime.touching && mouseEvent.buttons === 1)
903
- this.onMove(mouseEvent, event);
608
+ events.on('touchcancel', window, 'touchcancel', () => resetDrag(true));
609
+ events.on('mousedown', wrapper, 'mousedown', (event) => {
610
+ const mouse = event as MouseEvent;
611
+ if (mouse.button !== 0) return;
612
+ onStart(mouse, mouse.target);
613
+ wrapper?.style.setProperty('cursor', 'grabbing');
904
614
  });
905
- this.cleanup.events.on('mouseup', wrapper, 'mouseup', (event) => {
906
- wrapper.style.cursor = 'grab';
907
- this.pushLog(event as MouseEvent);
908
- this.onEnd();
615
+ events.on('mousemove', window, 'mousemove', (event) => {
616
+ const mouse = event as MouseEvent;
617
+ if (touching && mouse.buttons === 1) onMove(mouse, event);
909
618
  });
910
- this.cleanup.events.on('window:mouseup', window, 'mouseup', (event) => {
911
- if (!this.runtime.touching) return;
912
- wrapper.style.cursor = 'grab';
913
- this.pushLog(event as MouseEvent);
914
- this.onEnd();
619
+ events.on('mouseup', window, 'mouseup', (event) => {
620
+ if (!touching) return;
621
+ wrapper?.style.setProperty('cursor', 'grab');
622
+ pushLog(event as MouseEvent);
623
+ onEnd();
915
624
  });
916
- this.cleanup.events.on('wrapper:mouseleave', wrapper, 'mouseleave', () => {
917
- wrapper.style.cursor = 'grab';
918
- });
919
- this.cleanup.events.on(
625
+ events.on(
920
626
  'click',
921
627
  wrapper,
922
628
  'click',
923
629
  (event) => {
924
- if (!this.runtime.clickPrevented || !this.props.preventClick) return;
630
+ if (!clickPrevented || !props.preventClick) return;
925
631
  event.preventDefault();
926
632
  event.stopPropagation();
927
- this.runtime.clickPrevented = false;
633
+ clickPrevented = false;
928
634
  },
929
635
  { capture: true }
930
636
  );
931
- this.cleanup.events.on('dragstart', wrapper, 'dragstart', (event) =>
637
+ events.on('dragstart', wrapper, 'dragstart', (event) =>
932
638
  event.preventDefault()
933
639
  );
934
- this.cleanup.events.on('transitionend', wrapper, 'transitionend', (event) =>
935
- this.onTransitionEnd(event)
936
- );
937
- this.cleanup.events.on('window:resize', window, 'resize', () =>
938
- this.refresh()
939
- );
940
- this.cleanup.events.on('root:mouseenter', root, 'mouseenter', () =>
941
- this.pause()
942
- );
943
- this.cleanup.events.on('root:mouseleave', root, 'mouseleave', () =>
944
- this.resume()
945
- );
946
- }
947
-
948
- private onStart(point: SwipePoint, target: EventTarget | null = null): void {
949
- if (this.state.animating || !this.syncLayout()) return;
950
-
951
- this.runtime.logs = [];
952
- this.pushLog(point);
953
- this.runtime.startTarget = target;
954
- this.runtime.touching = true;
955
- this.runtime.scrolling = false;
956
- this.runtime.swiping = false;
957
- this.runtime.clickPrevented = false;
958
-
959
- this.pause();
960
- if (this.dom.wrapper) this.dom.wrapper.style.transition = 'none';
961
- }
962
-
963
- private onMove(point: SwipePoint, event: Event): void {
964
- if (
965
- !this.runtime.touching ||
966
- this.runtime.scrolling ||
967
- this.state.animating
968
- )
969
- return;
970
-
971
- this.pushLog(point);
972
-
973
- const offset = this.getOffset();
974
- const ax = Math.abs(offset.x);
975
- const ay = Math.abs(offset.y);
976
- if (ax < SWIPE_THRESHOLD && ay < SWIPE_THRESHOLD) return;
977
-
978
- if (!this.runtime.swiping) {
979
- const angle = (Math.atan2(ay, ax) * 180) / Math.PI;
980
-
981
- if (angle < this.props.touchAngle) {
982
- this.runtime.swiping = true;
983
- this.runtime.clickPrevented = isInteractiveTarget(
984
- this.runtime.startTarget
985
- );
986
- } else {
987
- this.runtime.scrolling = true;
988
- this.resetDrag(true);
989
- return;
990
- }
991
- }
992
-
993
- if (this.runtime.swiping) {
994
- event.preventDefault();
995
- this.transform =
996
- -this.state.trackIndex * this.state.width +
997
- offset.x * this.props.touchRatio;
998
- this.render(false);
999
- }
1000
- }
1001
-
1002
- private onEnd(): void {
1003
- if (!this.runtime.touching) return;
1004
-
1005
- this.runtime.touching = false;
1006
-
1007
- if (!this.runtime.swiping) {
1008
- this.resetDrag(true);
1009
- return;
1010
- }
1011
-
1012
- const duration = this.getDuration();
1013
- const offset = this.getOffset();
1014
- const ox = offset.x;
1015
- let target = this.state.trackIndex;
1016
-
1017
- if (duration > this.props.longSwipesMs) {
1018
- const steps = Math.ceil(
1019
- Math.abs(ox) / this.state.width - this.props.longSwipesRatio
640
+ events.on('transitionend', wrapper, 'transitionend', onTransitionEnd);
641
+ events.on('resize', window, 'resize', syncAfterData);
642
+ events.on('mouseenter', root, 'mouseenter', pause);
643
+ events.on('mouseleave', root, 'mouseleave', resume);
644
+ events.on('controls', root, 'click', (event) => {
645
+ if (!(event.target instanceof Element)) return;
646
+ const control = event.target.closest<HTMLElement>(
647
+ '[data-action], [data-swiper-bullet]'
1020
648
  );
1021
- if (steps > 0) target = this.state.trackIndex + steps * (ox > 0 ? -1 : 1);
1022
- } else {
1023
- const distance = Math.abs(ox) / this.state.width;
1024
- const slides =
1025
- distance >= this.props.longSwipesRatio
1026
- ? Math.max(1, Math.ceil(distance - this.props.longSwipesRatio))
1027
- : 0;
1028
- if (slides > 0)
1029
- target =
1030
- ox > 0
1031
- ? this.state.trackIndex - slides
1032
- : this.state.trackIndex + slides;
1033
- }
1034
-
1035
- this.slideToTrack(target);
1036
- this.runtime.logs = [];
1037
- this.resume();
1038
- }
1039
-
1040
- private resetDrag(animate = true): void {
1041
- const target =
1042
- this.state.trackIndex === 0
1043
- ? 0
1044
- : -this.state.trackIndex * this.state.width;
1045
- const shouldAnimate = animate && this.state.transform !== target;
1046
-
1047
- this.runtime.touching = false;
1048
- this.runtime.scrolling = false;
1049
- this.runtime.swiping = false;
1050
- this.runtime.logs = [];
1051
- this.runtime.startTarget = null;
1052
- this.transform = target;
1053
- this.render(shouldAnimate);
1054
- this.resume();
1055
- }
1056
-
1057
- private onTransitionEnd(event?: Event): void {
1058
- if (
1059
- event &&
1060
- (event.target !== this.dom.wrapper ||
1061
- !('propertyName' in event) ||
1062
- event.propertyName !== 'transform')
1063
- )
1064
- return;
1065
-
1066
- this.animating = false;
1067
-
1068
- if (this.props.loop) {
1069
- if (this.state.trackIndex === 0) {
1070
- this.setTrackIndex(this.realCount, false);
1071
- this.render(false);
1072
- } else if (this.state.trackIndex === this.dom.slides.length - 1) {
1073
- this.setTrackIndex(1, false);
1074
- this.render(false);
649
+ if (!control || !root.contains(control)) return;
650
+ if (control.dataset.action === 'prev') swiper.prev();
651
+ else if (control.dataset.action === 'next') swiper.next();
652
+ else if (control.dataset.swiperBullet != null) {
653
+ swiper.slideTo(Number(control.dataset.swiperBullet));
1075
654
  }
1076
- }
1077
- }
1078
-
1079
- private pushLog(point: SwipePoint): void {
1080
- this.runtime.logs.push({
1081
- x: point.pageX ?? point.clientX ?? 0,
1082
- y: point.pageY ?? point.clientY ?? 0,
1083
- time: Date.now(),
1084
655
  });
1085
- if (this.runtime.logs.length > 5) this.runtime.logs.shift();
1086
- }
1087
-
1088
- private getDuration(): number {
1089
- if (this.runtime.logs.length === 0) return 0;
1090
- return (
1091
- this.runtime.logs[this.runtime.logs.length - 1].time -
1092
- this.runtime.logs[0].time
1093
- );
1094
- }
1095
-
1096
- private getOffset(): { x: number; y: number } {
1097
- if (this.runtime.logs.length === 0) return { x: 0, y: 0 };
1098
- const first = this.runtime.logs[0];
1099
- const last = this.runtime.logs[this.runtime.logs.length - 1];
1100
- return { x: last.x - first.x, y: last.y - first.y };
1101
- }
1102
-
1103
- toRealIndex(index = this.state.trackIndex): number {
1104
- if (!this.realCount) return 0;
1105
- if (!this.props.loop) return index;
1106
- if (index === 0) return this.realCount - 1;
1107
- if (index === this.dom.slides.length - 1) return 0;
1108
- return index - 1;
1109
- }
1110
-
1111
- trackIndexForRealIndex(index: number): number {
1112
- if (!this.realCount) return 0;
1113
- const target = Math.max(0, Math.min(index, this.realCount - 1));
1114
- return this.props.loop && this.realCount > 1 ? target + 1 : target;
1115
- }
656
+ };
1116
657
 
1117
- private setTrackIndex(
1118
- trackIndex: number,
1119
- animate: boolean | null = true
1120
- ): void {
1121
- const target = normalizeNumber(trackIndex);
1122
- this.setState({
1123
- trackIndex: target,
1124
- index: this.toRealIndex(target),
1125
- transform: -target * this.state.width,
658
+ const syncAfterData = (): void => {
659
+ if (!swiper.runtime.built) return;
660
+ pause();
661
+ clearImageCleanups();
662
+ refreshSlides();
663
+ const clamped = Math.min(state.index, Math.max(0, realCount() - 1));
664
+ updateSize();
665
+ setTrackIndex(trackIndexForRealIndex(clamped), false);
666
+ if (props.lazyload) loadImages();
667
+ if (props.autoplay && hasLayout()) resume();
668
+ };
669
+ const dataTask = createScheduledTask(syncAfterData);
670
+ const queueLayoutSync = (): void => {
671
+ if (mountFrame != null || typeof requestAnimationFrame !== 'function')
672
+ return;
673
+ mountFrame = requestAnimationFrame(() => {
674
+ mountFrame = null;
675
+ if (!swiper.runtime.destroyed && swiper.element?.isConnected)
676
+ syncAfterData();
1126
677
  });
1127
- if (animate != null) this.render(animate);
1128
- }
1129
-
1130
- slideTo(index: number): void {
1131
- return this.slideToTrack(this.trackIndexForRealIndex(index));
1132
- }
1133
-
1134
- slideToTrack(idx: number): void {
1135
- this.assertBuilt('slideToTrack');
1136
- if (this.state.animating) return;
1137
- if (this.dom.slides.length === 0) return;
1138
- if (!this.syncLayout()) return;
1139
-
1140
- let target = idx;
1141
-
1142
- if (this.props.loop) {
1143
- if (idx < 0) target = 0;
1144
- else if (idx >= this.dom.slides.length)
1145
- target = this.dom.slides.length - 1;
1146
- } else {
1147
- target = Math.max(0, Math.min(idx, this.dom.slides.length - 1));
1148
- }
1149
-
1150
- this.setTrackIndex(target, true);
1151
-
1152
- if (this.props.lazyload) this.loadImages();
1153
- }
1154
-
1155
- next(): void {
1156
- this.assertBuilt('next');
1157
- if (this.state.animating) return;
1158
- this.slideToTrack(this.state.trackIndex + 1);
1159
- }
1160
-
1161
- prev(): void {
1162
- this.assertBuilt('prev');
1163
- if (this.state.animating) return;
1164
- this.slideToTrack(this.state.trackIndex - 1);
1165
- }
1166
-
1167
- private render(animate: boolean): void {
1168
- this.assertBuilt('render');
1169
- if (!this.dom.wrapper) return;
1170
- const transform = `translate3d(${this.state.transform}px, 0, 0)`;
1171
- const shouldAnimate =
1172
- animate &&
1173
- this.props.speed > 0 &&
1174
- this.state.width > 0 &&
1175
- this.dom.wrapper.style.transform !== transform;
1176
-
1177
- if (shouldAnimate) {
1178
- this.dom.wrapper.style.transition = `transform ${this.props.speed}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
1179
- this.animating = true;
1180
- } else {
1181
- this.dom.wrapper.style.transition = 'none';
1182
- this.animating = false;
1183
- }
1184
-
1185
- this.dom.wrapper.style.transform = transform;
1186
- }
1187
-
1188
- private loadImages(): void {
1189
- this.assertBuilt('loadImages');
1190
- const indices = [this.state.trackIndex];
1191
- if (this.state.trackIndex > 0) indices.push(this.state.trackIndex - 1);
1192
- if (this.state.trackIndex < this.dom.slides.length - 1)
1193
- indices.push(this.state.trackIndex + 1);
1194
-
1195
- indices.forEach((index) => {
1196
- const slide = this.dom.slides[index];
1197
- if (!slide) return;
1198
-
1199
- const img = q<HTMLImageElement>('img[data-lazy]', slide);
1200
- if (!img || img.src) return;
1201
-
1202
- img.classList.add(this.props.className.loading);
1203
- img.src = img.dataset.lazy || '';
1204
- const cleanup = () => {
1205
- img.onload = null;
1206
- img.onerror = null;
1207
- this.runtime.imageCleanups.delete(cleanup);
1208
- };
1209
- this.runtime.imageCleanups.add(cleanup);
1210
- img.onload = () => {
1211
- img.classList.remove(this.props.className.loading);
1212
- img.classList.add(this.props.className.loaded);
1213
- cleanup();
1214
- };
1215
- img.onerror = () => {
1216
- img.classList.remove(this.props.className.loading);
1217
- img.classList.add(this.props.className.error);
1218
- cleanup();
1219
- };
678
+ };
679
+ const loadData = async (): Promise<void> => {
680
+ if (typeof props.data !== 'function') return;
681
+ const requestId = ++dataLoadId;
682
+ flushSync(() => {
683
+ state.loading = true;
1220
684
  });
1221
- }
1222
-
1223
- private clearImageCleanups(): void {
1224
- this.runtime.imageCleanups?.forEach((cleanup) => cleanup());
1225
- this.runtime.imageCleanups?.clear();
1226
- }
1227
-
1228
- private initPagination(): void {
1229
- if (!this.dom.root) return;
1230
-
1231
- if (!this.dom.pagination) {
1232
- this.dom.pagination = h('div', {
1233
- className: joinClasses(
1234
- this.props.className.pagination,
1235
- this.props.className.paginationHorizontal,
1236
- this.props.className.paginationClickable,
1237
- this.props.className.paginationBulletGroup
1238
- ),
1239
- 'data-swiper-pagination': '',
1240
- }) as HTMLElement;
1241
- this.dom.root.appendChild(this.dom.pagination);
1242
- this.dom.createdPagination = true;
1243
- } else {
1244
- this.dom.pagination.classList.add(
1245
- this.props.className.paginationHorizontal,
1246
- this.props.className.paginationClickable,
1247
- this.props.className.paginationBulletGroup
1248
- );
1249
- this.dom.pagination.setAttribute('data-swiper-pagination', '');
1250
- }
1251
-
1252
- const pagination = this.dom.pagination;
1253
- if (!pagination) return;
1254
-
1255
- pagination.textContent = '';
1256
- this.dom.bullets = [];
1257
-
1258
- for (let index = 0; index < this.realCount; index++) {
1259
- const bullet = h('button', {
1260
- type: 'button',
1261
- className: joinClasses(
1262
- this.props.className.indicator,
1263
- this.props.className.bullet
1264
- ),
1265
- 'data-swiper-bullet': String(index),
1266
- 'aria-label': `Go to slide ${index + 1}`,
1267
- }) as HTMLButtonElement;
1268
- this.cleanup.events.on(`bullet:${index}:click`, bullet, 'click', () => {
1269
- this.slideTo(index);
685
+ try {
686
+ const result = await Promise.resolve(props.data(swiper));
687
+ if (swiper.runtime.destroyed || requestId !== dataLoadId) return;
688
+ validateParam('data', result, { type: 'array' }, 'Swiper.data');
689
+ const data = cloneData(result);
690
+ normalizeData(data);
691
+ flushSync(() => {
692
+ state.data = data;
1270
693
  });
1271
- this.cleanup.events.on(
1272
- `bullet:${index}:keydown`,
1273
- bullet,
1274
- 'keydown',
1275
- (event) => {
1276
- const keyboardEvent = event as KeyboardEvent;
1277
- if (keyboardEvent.key !== 'Enter' && keyboardEvent.key !== ' ')
1278
- return;
1279
- event.preventDefault();
1280
- this.slideTo(index);
1281
- }
1282
- );
1283
- this.dom.bullets.push(bullet);
1284
- pagination.appendChild(bullet);
1285
- }
1286
-
1287
- this.cleanup.bindings = createRoot((dispose) => {
1288
- this.dom.bullets.forEach((bullet, i) => {
1289
- bindClass(bullet, this.props.className.active, () => {
1290
- return this.state.index === i;
694
+ } finally {
695
+ if (!swiper.runtime.destroyed && requestId === dataLoadId) {
696
+ flushSync(() => {
697
+ state.loading = false;
1291
698
  });
1292
- bindAttr(bullet, 'aria-current', () =>
1293
- this.state.index === i ? 'true' : null
1294
- );
1295
- });
1296
- return dispose;
699
+ }
700
+ }
701
+ };
702
+ const requestData = (): void => {
703
+ void loadData().catch((error: unknown) => {
704
+ if (!swiper.runtime.destroyed) console.error('Swiper.data error:', error);
1297
705
  });
1298
- }
1299
-
1300
- private initNavigation(): void {
1301
- const prevButton = this.ensureNavigation('prev', 'arrow-left');
1302
- const nextButton = this.ensureNavigation('next', 'arrow-right');
1303
- this.dom.prevButton = prevButton;
1304
- this.dom.nextButton = nextButton;
706
+ };
1305
707
 
1306
- this.cleanup.events.on('nav:prev', prevButton, 'click', () => this.prev());
1307
- this.cleanup.events.on('nav:next', nextButton, 'click', () => this.next());
708
+ const slideView = (itemAccessor: () => TrackItem): HTMLElement => {
709
+ const initial = itemAccessor();
710
+ const tag = initial.item.url ? 'a' : 'div';
711
+ return jsx(tag, {
712
+ className: props.className.slide,
713
+ href: () => itemAccessor().item.url || undefined,
714
+ target: () =>
715
+ itemAccessor().item.url
716
+ ? itemAccessor().item.blank
717
+ ? '_blank'
718
+ : '_self'
719
+ : undefined,
720
+ 'data-swiper-slide': () => String(itemAccessor().index),
721
+ 'data-swiper-index': () =>
722
+ itemAccessor().clone ? null : String(itemAccessor().index),
723
+ 'data-clone': () => itemAccessor().clone,
724
+ role: 'group',
725
+ 'aria-label': () => `Slide ${itemAccessor().index + 1}`,
726
+ children: () => {
727
+ const { item, index } = itemAccessor();
728
+ const context = { swiper, item, index };
729
+ if (item.children != null) {
730
+ return typeof item.children === 'function'
731
+ ? item.children(context)
732
+ : item.children;
733
+ }
734
+ return [
735
+ item.image
736
+ ? jsx('img', {
737
+ className: props.className.image,
738
+ alt: item.title || '',
739
+ loading: 'lazy',
740
+ ...(props.lazyload
741
+ ? { 'data-lazy': item.image }
742
+ : { src: item.image }),
743
+ })
744
+ : null,
745
+ item.title
746
+ ? jsx('span', {
747
+ className: props.className.title,
748
+ children: item.title,
749
+ })
750
+ : null,
751
+ ];
752
+ },
753
+ }) as HTMLElement;
754
+ };
1308
755
 
1309
- if (!this.props.loop) {
1310
- this.cleanup.navBindings = createRoot((dispose) => {
1311
- bindClass(
1312
- prevButton,
1313
- this.props.className.disabled,
1314
- () => this.state.trackIndex <= 0
1315
- );
1316
- bindAttr(prevButton, 'disabled', () =>
1317
- this.state.trackIndex <= 0 ? '' : null
756
+ swiper = defineComponent<
757
+ ResolvedSwiperProps,
758
+ SwiperState,
759
+ HTMLElement,
760
+ SwiperActions
761
+ >({
762
+ name: 'Swiper',
763
+ props,
764
+ state,
765
+ actions: {
766
+ slideTo(index) {
767
+ swiper.slideToTrack(trackIndexForRealIndex(index));
768
+ },
769
+ slideToTrack(index) {
770
+ if (!swiper.runtime.built) {
771
+ throw new Error('Swiper.slideToTrack: call build() first.');
772
+ }
773
+ if (state.animating || !slides.length || !syncLayout()) return;
774
+ const target = props.loop
775
+ ? Math.max(0, Math.min(index, slides.length - 1))
776
+ : Math.max(0, Math.min(index, realCount() - 1));
777
+ setTrackIndex(target, true);
778
+ if (props.lazyload) loadImages();
779
+ },
780
+ next() {
781
+ if (!state.animating) swiper.slideToTrack(state.trackIndex + 1);
782
+ },
783
+ prev() {
784
+ if (!state.animating) swiper.slideToTrack(state.trackIndex - 1);
785
+ },
786
+ play,
787
+ pause,
788
+ resume,
789
+ restartAutoplay,
790
+ },
791
+ normalizeStatePatch(patch) {
792
+ return {
793
+ ...patch,
794
+ ...(Object.hasOwn(patch, 'data') && Array.isArray(patch.data)
795
+ ? { data: cloneData(patch.data) }
796
+ : {}),
797
+ };
798
+ },
799
+ validateStatePatch(patch) {
800
+ validateParam('state', patch, { type: 'plainObject' }, 'Swiper.setState');
801
+ for (const key of Object.keys(patch)) {
802
+ if (!Object.hasOwn(SWIPER_STATE_SCHEMA, key)) {
803
+ throw new Error(
804
+ `Swiper.setState: "${key}" is not a supported state key.`
805
+ );
806
+ }
807
+ validateParam(
808
+ key,
809
+ patch[key as keyof SwiperState],
810
+ SWIPER_STATE_SCHEMA[key as keyof typeof SWIPER_STATE_SCHEMA],
811
+ 'Swiper.setState'
1318
812
  );
1319
- bindClass(
1320
- nextButton,
1321
- this.props.className.disabled,
1322
- () => this.state.trackIndex >= this.dom.slides.length - 1
813
+ }
814
+ if (Array.isArray(patch.data)) normalizeData(patch.data);
815
+ },
816
+ view: () => {
817
+ normalizedItems = createMemo(() => normalizeData(state.data));
818
+ trackItems = createMemo(() => createTrackItems(normalizedItems()));
819
+ const loadingView = (): HTMLDivElement => {
820
+ const view = createLoading();
821
+ view.setAttribute('data-swiper-loading', '');
822
+ return view;
823
+ };
824
+ wrapper = jsx('div', {
825
+ className: props.className.wrapper,
826
+ 'data-swiper-wrapper': '',
827
+ 'aria-live': 'polite',
828
+ children: () =>
829
+ state.loading
830
+ ? loadingView()
831
+ : For({
832
+ each: trackItems,
833
+ key: (item: TrackItem) => item.key,
834
+ children: slideView,
835
+ }),
836
+ }) as HTMLElement;
837
+ const root = jsx('div', {
838
+ id: props.id || randomId(),
839
+ className: props.className.root,
840
+ 'data-swiper': 'root',
841
+ 'data-status': () => (state.loading ? 'loading' : 'loaded'),
842
+ children: [
843
+ wrapper,
844
+ props.pagination
845
+ ? jsx('div', {
846
+ className: joinClasses(
847
+ props.className.pagination,
848
+ props.className.paginationHorizontal,
849
+ props.className.paginationClickable,
850
+ props.className.paginationBulletGroup
851
+ ),
852
+ 'data-swiper-pagination': '',
853
+ children: For({
854
+ each: normalizedItems,
855
+ key: (item: NormalizedSwiperDataItem) => item.key,
856
+ children: (
857
+ _itemAccessor: () => NormalizedSwiperDataItem,
858
+ indexAccessor: () => number
859
+ ) =>
860
+ jsx('button', {
861
+ type: 'button',
862
+ className: () =>
863
+ joinClasses(
864
+ props.className.indicator,
865
+ props.className.bullet,
866
+ state.index === indexAccessor()
867
+ ? props.className.active
868
+ : ''
869
+ ),
870
+ 'data-swiper-bullet': () => String(indexAccessor()),
871
+ 'aria-label': () => `Go to slide ${indexAccessor() + 1}`,
872
+ 'aria-current': () =>
873
+ state.index === indexAccessor() ? 'true' : null,
874
+ }),
875
+ }),
876
+ })
877
+ : null,
878
+ props.navigation
879
+ ? jsx('button', {
880
+ type: 'button',
881
+ className: () =>
882
+ joinClasses(
883
+ props.className.navigation,
884
+ props.className.prev,
885
+ !props.loop && state.index <= 0
886
+ ? props.className.disabled
887
+ : ''
888
+ ),
889
+ 'data-action': 'prev',
890
+ 'data-swiper-navigation': 'prev',
891
+ 'aria-label': 'Previous slide',
892
+ disabled: () => !props.loop && state.index <= 0,
893
+ children: icon('arrow-left'),
894
+ })
895
+ : null,
896
+ props.navigation
897
+ ? jsx('button', {
898
+ type: 'button',
899
+ className: () =>
900
+ joinClasses(
901
+ props.className.navigation,
902
+ props.className.next,
903
+ !props.loop && state.index >= realCount() - 1
904
+ ? props.className.disabled
905
+ : ''
906
+ ),
907
+ 'data-action': 'next',
908
+ 'data-swiper-navigation': 'next',
909
+ 'aria-label': 'Next slide',
910
+ disabled: () => !props.loop && state.index >= realCount() - 1,
911
+ children: icon('arrow-right'),
912
+ })
913
+ : null,
914
+ ],
915
+ }) as HTMLElement;
916
+ createEffect(() => {
917
+ wrapper?.style.setProperty(
918
+ 'transform',
919
+ `translate3d(${state.transform}px, 0, 0)`
1323
920
  );
1324
- bindAttr(nextButton, 'disabled', () =>
1325
- this.state.trackIndex >= this.dom.slides.length - 1 ? '' : null
921
+ wrapper?.style.setProperty(
922
+ 'transition',
923
+ state.animating
924
+ ? `transform ${props.speed}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`
925
+ : 'none'
1326
926
  );
1327
- return dispose;
927
+ root.style.setProperty('--swiper-slide-width', `${state.width}px`);
1328
928
  });
1329
- }
1330
- }
1331
-
1332
- private ensureNavigation(
1333
- direction: SwiperDirection,
1334
- iconName: 'arrow-left' | 'arrow-right'
1335
- ): HTMLButtonElement {
1336
- if (!this.dom.root) {
1337
- throw new Error('Swiper.ensureNavigation: root not found.');
1338
- }
1339
-
1340
- const key: CreatedNavigationKey =
1341
- direction === 'prev' ? 'createdPrevButton' : 'createdNextButton';
1342
- const directionClass =
1343
- direction === 'prev'
1344
- ? this.props.className.prev
1345
- : this.props.className.next;
1346
- const className = joinClasses(
1347
- this.props.className.navigation,
1348
- directionClass
1349
- );
1350
- let button = q<HTMLButtonElement>(
1351
- `[data-action="${direction}"]`,
1352
- this.dom.root
1353
- );
1354
-
1355
- if (!button) {
1356
- button = h('button', {
1357
- type: 'button',
1358
- className,
1359
- 'data-action': direction,
1360
- 'data-swiper-navigation': direction,
1361
- }) as HTMLButtonElement;
1362
- this.dom.root.appendChild(button);
1363
- this.dom[key] = true;
1364
- } else if (button.tagName.toLowerCase() !== 'button') {
1365
- button.setAttribute('role', 'button');
1366
- button.setAttribute('tabindex', '0');
1367
- button.classList.add(this.props.className.navigation, directionClass);
1368
- button.setAttribute('data-swiper-navigation', direction);
1369
- } else {
1370
- button.classList.add(this.props.className.navigation, directionClass);
1371
- button.setAttribute('data-swiper-navigation', direction);
1372
- }
1373
-
1374
- button.setAttribute(
1375
- 'aria-label',
1376
- direction === 'prev' ? 'Previous slide' : 'Next slide'
1377
- );
1378
- if (!q('svg', button)) {
1379
- button.textContent = '';
1380
- button.appendChild(icon(iconName));
1381
- }
1382
-
1383
- return button;
1384
- }
1385
-
1386
- play(): void {
1387
- this.assertBuilt('play');
1388
- if (this.runtime.destroyed || this.runtime.timer) return;
1389
- if (this.realCount <= 1) return;
1390
- if (!this.syncLayout()) return;
1391
- const delay = Math.max(this.props.delay, AUTOPLAY_DELAY_FLOOR);
1392
- this.runtime.timer = setInterval(() => this.next(), delay);
1393
- }
1394
-
1395
- pause(): void {
1396
- if (!this.runtime.timer) return;
1397
- clearInterval(this.runtime.timer);
1398
- this.runtime.timer = null;
1399
- }
1400
-
1401
- resume(): void {
1402
- if (this.runtime.destroyed) return;
1403
- if (this.props.autoplay && !this.runtime.timer) this.play();
1404
- }
1405
-
1406
- restartAutoplay(): void {
1407
- this.pause();
1408
- if (this.props.autoplay) this.play();
1409
- }
1410
-
1411
- protected override normalizeStatePatch(
1412
- patch: Partial<SwiperState>
1413
- ): Partial<SwiperState> {
1414
- const nextPatch = { ...patch };
1415
- if (Object.hasOwn(nextPatch, 'data') && Array.isArray(nextPatch.data)) {
1416
- nextPatch.data = cloneData(nextPatch.data);
1417
- }
1418
- return nextPatch;
1419
- }
1420
-
1421
- protected override validateStatePatch(patch: Partial<SwiperState>): void {
1422
- validateParam(
1423
- 'state',
1424
- patch,
1425
- {
1426
- type: 'plainObject',
1427
- },
1428
- 'Swiper.setState'
1429
- );
1430
-
1431
- for (const key of Object.keys(patch)) {
1432
- if (!Object.hasOwn(SWIPER_STATE_SCHEMA, key)) {
1433
- throw new Error(
1434
- `Swiper.setState: "${key}" is not a supported state key.`
1435
- );
1436
- }
1437
- validateParam(
1438
- key,
1439
- patch[key as keyof SwiperState],
1440
- SWIPER_STATE_SCHEMA[key as keyof typeof SWIPER_STATE_SCHEMA],
1441
- 'Swiper.setState'
1442
- );
1443
- }
1444
-
1445
- if (Array.isArray(patch.data)) this.normalizeData(patch.data);
1446
- }
1447
- }
929
+ createEffect(() => {
930
+ const token = normalizedItems()
931
+ .map((item) => `${item.key}:${item.sort ?? ''}:${Boolean(item.url)}`)
932
+ .join('|');
933
+ if (token || state.data.length === 0) dataTask.schedule();
934
+ });
935
+ return root;
936
+ },
937
+ onBuild(context) {
938
+ const root = context.element;
939
+ if (!root || !wrapper) return;
940
+ wrapper.setAttribute('aria-live', 'polite');
941
+ refreshSlides();
942
+ bindEvents();
943
+ updateSize();
944
+ setTrackIndex(trackIndexForRealIndex(state.index), false);
945
+ if (props.lazyload) loadImages();
946
+ if (props.autoplay && hasLayout()) play();
947
+ context.own(() => events.clear());
948
+ requestData();
949
+ queueLayoutSync();
950
+ },
951
+ onMount() {
952
+ syncAfterData();
953
+ queueLayoutSync();
954
+ },
955
+ onDestroy() {
956
+ pause();
957
+ dataTask.dispose();
958
+ dataLoadId += 1;
959
+ clearImageCleanups();
960
+ if (mountFrame != null) cancelAnimationFrame(mountFrame);
961
+ mountFrame = null;
962
+ wrapper = null;
963
+ slides = [];
964
+ },
965
+ }) as Swiper;
1448
966
 
1449
- export type Swiper = SwiperComponent;
967
+ Object.defineProperties(swiper, {
968
+ realCount: { enumerable: true, get: realCount },
969
+ realIndex: { enumerable: true, get: () => toRealIndex() },
970
+ });
1450
971
 
1451
- export function createSwiper(input: SwiperProps = {}): Swiper {
1452
- return new SwiperComponent(input);
972
+ return swiper;
1453
973
  }