vanilla-jui 1.4.7 → 1.5.1

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