@bagelink/vue 1.2.65 → 1.2.69

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 (46) hide show
  1. package/bin/experimentalGenTypedRoutes.ts +309 -0
  2. package/dist/components/Alert.vue.d.ts +2 -0
  3. package/dist/components/Alert.vue.d.ts.map +1 -1
  4. package/dist/components/Carousel.vue.d.ts +12 -3
  5. package/dist/components/Carousel.vue.d.ts.map +1 -1
  6. package/dist/components/Carousel2.vue.d.ts +89 -0
  7. package/dist/components/Carousel2.vue.d.ts.map +1 -0
  8. package/dist/components/form/inputs/EmailInput.vue.d.ts +48 -0
  9. package/dist/components/form/inputs/EmailInput.vue.d.ts.map +1 -0
  10. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  11. package/dist/components/form/inputs/TelInput.vue.d.ts +60 -0
  12. package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -0
  13. package/dist/components/form/inputs/TextInput.vue.d.ts.map +1 -1
  14. package/dist/components/form/inputs/index.d.ts +2 -1
  15. package/dist/components/form/inputs/index.d.ts.map +1 -1
  16. package/dist/components/index.d.ts +1 -0
  17. package/dist/components/index.d.ts.map +1 -1
  18. package/dist/composables/useDevice.d.ts.map +1 -1
  19. package/dist/composables/useSchemaField.d.ts.map +1 -1
  20. package/dist/directives/pattern.d.ts.map +1 -1
  21. package/dist/index.cjs +11366 -10332
  22. package/dist/index.mjs +11367 -10333
  23. package/dist/style.css +1839 -1701
  24. package/dist/utils/BagelFormUtils.d.ts +7 -0
  25. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  26. package/dist/utils/constants.d.ts +1 -0
  27. package/dist/utils/constants.d.ts.map +1 -1
  28. package/package.json +7 -3
  29. package/src/components/Alert.vue +29 -7
  30. package/src/components/Carousel.vue +8 -0
  31. package/src/components/Carousel2.vue +1012 -0
  32. package/src/components/form/inputs/EmailInput.vue +476 -0
  33. package/src/components/form/inputs/SelectInput.vue +1 -1
  34. package/src/components/form/inputs/TelInput.vue +210 -350
  35. package/src/components/form/inputs/TextInput.vue +1 -1
  36. package/src/components/form/inputs/index.ts +2 -1
  37. package/src/components/index.ts +1 -0
  38. package/src/composables/useDevice.ts +1 -0
  39. package/src/composables/useSchemaField.ts +3 -1
  40. package/src/directives/pattern.ts +3 -2
  41. package/src/styles/inputs.css +137 -138
  42. package/src/styles/layout.css +1466 -1459
  43. package/src/styles/mobilLayout.css +11 -0
  44. package/src/utils/BagelFormUtils.ts +24 -0
  45. package/src/utils/constants.ts +1 -0
  46. package/src/components/form/inputs/PhoneInput.vue +0 -352
@@ -0,0 +1,1012 @@
1
+ <script setup lang="ts">
2
+ import { sleep, useDevice } from '@bagelink/vue'
3
+ import { ref, onMounted, onBeforeUnmount, watch, computed, nextTick } from 'vue'
4
+
5
+ type Easing = 'ease-in-out' | 'ease-in' | 'ease-out' | 'linear'
6
+ type AutoplayMode = 'disabled' | 'standard' | 'linear'
7
+ type AutoplayValue = boolean | AutoplayMode
8
+
9
+ interface CarouselOptions {
10
+ duration?: number
11
+ easing?: Easing
12
+ perPage?: number | { [key: number]: number }
13
+ startIndex?: number
14
+ draggable?: boolean
15
+ multipleDrag?: boolean
16
+ threshold?: number
17
+ loop?: boolean
18
+ rtl?: boolean
19
+ autoplay?: AutoplayValue
20
+ autoplayInterval?: number
21
+ autoplaySpeed?: number
22
+ pauseOnHover?: boolean
23
+ dots?: boolean
24
+ slideWidth?: number
25
+ onInit?: () => void
26
+ onChange?: () => void
27
+ }
28
+
29
+ interface DragState {
30
+ startX: number
31
+ endX: number
32
+ startY: number
33
+ letItGo: boolean | null
34
+ preventClick: boolean
35
+ }
36
+
37
+ const props = withDefaults(defineProps<CarouselOptions>(), {
38
+ duration: 200,
39
+ easing: 'ease-out',
40
+ startIndex: 0,
41
+ draggable: true,
42
+ multipleDrag: true,
43
+ threshold: 20,
44
+ loop: true,
45
+ rtl: false,
46
+ autoplay: 'disabled',
47
+ autoplayInterval: 3000,
48
+ autoplaySpeed: 50,
49
+ pauseOnHover: true,
50
+ dots: false,
51
+ slideWidth: 300,
52
+ })
53
+
54
+ const emit = defineEmits<{
55
+ (event: 'init'): void
56
+ (event: 'change'): void
57
+ }>()
58
+
59
+ // Basic state refs
60
+ const carouselRef = ref<HTMLElement | null>(null)
61
+ const sliderFrame = ref<HTMLDivElement | null>(null)
62
+ const innerElements = ref<Element[]>([])
63
+ const currentSlide = ref(0)
64
+ const selectorWidth = ref(0)
65
+ const { innerWidth } = useDevice()
66
+ const transformProperty = ref('transform')
67
+ const pointerDown = ref(false)
68
+
69
+ // Drag-related refs
70
+ const drag = ref<DragState>({
71
+ startX: 0,
72
+ endX: 0,
73
+ startY: 0,
74
+ letItGo: null,
75
+ preventClick: false
76
+ })
77
+
78
+ // Add a new variable to track actual dragging state
79
+ const isDragging = ref(false)
80
+ const dragThreshold = 10 // Increase threshold slightly for better detection
81
+
82
+ // Add autoplay-related refs
83
+ const autoplayTimer = ref<number | null>(null)
84
+ const linearAnimationFrame = ref<number | null>(null)
85
+ const isHovering = ref(false)
86
+ const linearOffset = ref(0)
87
+
88
+ // Modified to work with optional parameters and ensure compatibility with Required type
89
+ type ComputedConfig = Omit<Required<CarouselOptions>, 'autoplay'> & { autoplay: AutoplayMode }
90
+
91
+ // perPage calculation - independent of config
92
+ const perPage = computed(() => {
93
+ if (typeof props.perPage === 'number') {
94
+ return props.perPage
95
+ }
96
+
97
+ if (typeof props.perPage === 'object') {
98
+ return Object.entries(props.perPage)
99
+ .reduce((acc, [viewport, value]) => innerWidth.value >= Number(viewport) ? value : acc, 1)
100
+ }
101
+
102
+ // If perPage is undefined, calculate based on slideWidth
103
+ return Math.max(1, Math.ceil(selectorWidth.value / props.slideWidth))
104
+ })
105
+
106
+ // Core state
107
+ const config = computed<ComputedConfig>(() => {
108
+ // Convert boolean autoplay values to string mode
109
+ let autoplayMode: AutoplayMode = 'disabled'
110
+
111
+ if (typeof props.autoplay === 'boolean') {
112
+ autoplayMode = props.autoplay ? 'standard' : 'disabled'
113
+ } else {
114
+ autoplayMode = props.autoplay as AutoplayMode
115
+ }
116
+
117
+ // Calculate perPage fallback based on config
118
+ const calculatedPerPage = perPage.value
119
+
120
+ return {
121
+ duration: props.duration,
122
+ easing: props.easing,
123
+ perPage: calculatedPerPage,
124
+ startIndex: props.startIndex,
125
+ draggable: props.draggable,
126
+ multipleDrag: props.multipleDrag,
127
+ threshold: props.threshold,
128
+ loop: props.loop,
129
+ rtl: props.rtl,
130
+ autoplay: autoplayMode,
131
+ autoplayInterval: props.autoplayInterval,
132
+ autoplaySpeed: props.autoplaySpeed,
133
+ pauseOnHover: props.pauseOnHover,
134
+ dots: props.dots,
135
+ slideWidth: props.slideWidth,
136
+ onInit: () => { emit('init') },
137
+ onChange: () => { emit('change') }
138
+ }
139
+ })
140
+
141
+ // Computed property for total number of dots to display
142
+ const totalDots = computed(() => {
143
+ if (!innerElements.value.length) return 0
144
+ return config.value.loop ? innerElements.value.length : Math.ceil(innerElements.value.length - perPage.value + 1)
145
+ })
146
+
147
+ // Lifecycle hooks
148
+ onMounted(() => init())
149
+
150
+ onBeforeUnmount(() => {
151
+ stopAutoplay()
152
+ detachEvents()
153
+ })
154
+
155
+ // Watch for changes to props
156
+ watch(() => props, () => {
157
+ // Props changed, might need to rebuild
158
+ resizeHandler()
159
+ }, { deep: true })
160
+
161
+ // Helper functions
162
+ function webkitOrNot(): string {
163
+ const { style } = document.documentElement
164
+ if (typeof style.transform === 'string') {
165
+ return 'transform'
166
+ }
167
+ return 'WebkitTransform'
168
+ }
169
+
170
+ // Event handlers
171
+ function attachEvents(): void {
172
+ window.addEventListener('resize', resizeHandler)
173
+
174
+ if (config.value.draggable && carouselRef.value) {
175
+ const eventOptions = { passive: true }
176
+ const touchMoveOptions = { passive: false }
177
+ const mouseMoveOptions = { passive: false } // Non-passive option for mouse move
178
+ const mouseDownOptions = { passive: false } // Non-passive option for mouse down
179
+
180
+ // Touch events
181
+ carouselRef.value.addEventListener('touchstart', touchstartHandler, eventOptions)
182
+ carouselRef.value.addEventListener('touchend', touchendHandler, eventOptions)
183
+ carouselRef.value.addEventListener('touchmove', touchmoveHandler, touchMoveOptions)
184
+
185
+ // Mouse events
186
+ carouselRef.value.addEventListener('mousedown', mousedownHandler, mouseDownOptions)
187
+ carouselRef.value.addEventListener('mouseup', mouseupHandler, eventOptions)
188
+ carouselRef.value.addEventListener('mouseleave', mouseleaveHandler, eventOptions)
189
+ carouselRef.value.addEventListener('mousemove', mousemoveHandler, mouseMoveOptions)
190
+
191
+ // Prevent default dragging of images and links
192
+ carouselRef.value.addEventListener('dragstart', dragstartHandler)
193
+
194
+ // Click
195
+ carouselRef.value.addEventListener('click', clickHandler)
196
+ }
197
+
198
+ // Add mouse enter/leave events for pauseOnHover functionality
199
+ if (config.value.autoplay !== 'disabled' && config.value.pauseOnHover && carouselRef.value) {
200
+ carouselRef.value.addEventListener('mouseenter', mouseenterHandler)
201
+ carouselRef.value.addEventListener('mouseleave', mouseleaveAutoplayHandler)
202
+ }
203
+ }
204
+
205
+ function detachEvents(): void {
206
+ window.removeEventListener('resize', resizeHandler)
207
+
208
+ if (carouselRef.value) {
209
+ carouselRef.value.removeEventListener('touchstart', touchstartHandler)
210
+ carouselRef.value.removeEventListener('touchend', touchendHandler)
211
+ carouselRef.value.removeEventListener('touchmove', touchmoveHandler)
212
+ carouselRef.value.removeEventListener('mousedown', mousedownHandler)
213
+ carouselRef.value.removeEventListener('mouseup', mouseupHandler)
214
+ carouselRef.value.removeEventListener('mouseleave', mouseleaveHandler)
215
+ carouselRef.value.removeEventListener('mousemove', mousemoveHandler)
216
+ carouselRef.value.removeEventListener('dragstart', dragstartHandler)
217
+ carouselRef.value.removeEventListener('click', clickHandler)
218
+ carouselRef.value.removeEventListener('mouseenter', mouseenterHandler)
219
+ carouselRef.value.removeEventListener('mouseleave', mouseleaveAutoplayHandler)
220
+ }
221
+ }
222
+
223
+ // Core functionality
224
+ async function init(): Promise<void> {
225
+ if (!carouselRef.value) return
226
+
227
+ transformProperty.value = webkitOrNot()
228
+
229
+ // Add a small delay to ensure DOM is ready
230
+ await sleep(10)
231
+
232
+ // Create references
233
+ innerElements.value = Array.from(carouselRef.value.children)
234
+
235
+ if (!innerElements.value.length) {
236
+ console.warn('No carousel items found during initialization')
237
+ return
238
+ }
239
+
240
+ selectorWidth.value = carouselRef.value.offsetWidth
241
+
242
+ // Set current slide
243
+ currentSlide.value = config.value.loop
244
+ ? config.value.startIndex % innerElements.value.length
245
+ : Math.max(0, Math.min(config.value.startIndex, innerElements.value.length - perPage.value))
246
+
247
+ // Hide overflow
248
+ carouselRef.value.style.overflow = 'hidden'
249
+
250
+ // Set RTL direction if needed
251
+ carouselRef.value.style.direction = config.value.rtl ? 'rtl' : 'ltr'
252
+
253
+ // Attach events
254
+ attachEvents()
255
+
256
+ await nextTick()
257
+
258
+ // Build slider frame
259
+ buildSliderFrame()
260
+
261
+ // Start autoplay if enabled
262
+ if (config.value.autoplay !== 'disabled') {
263
+ startAutoplay()
264
+ }
265
+
266
+ // Call onInit callback
267
+ config.value.onInit()
268
+ }
269
+
270
+ function buildSliderFrame(): void {
271
+ if (!carouselRef.value) return
272
+
273
+ // Ensure we have elements to build the slider
274
+ if (!innerElements.value.length) {
275
+ console.warn('No carousel items found')
276
+ return
277
+ }
278
+
279
+ const widthItem = selectorWidth.value / perPage.value
280
+ const itemsToBuild = config.value.loop
281
+ ? innerElements.value.length + (2 * perPage.value)
282
+ : innerElements.value.length
283
+
284
+ // Create frame
285
+ if (!sliderFrame.value) {
286
+ sliderFrame.value = document.createElement('div')
287
+ }
288
+
289
+ sliderFrame.value.style.width = `${widthItem * itemsToBuild}px`
290
+ enableTransition()
291
+
292
+ if (config.value.draggable && carouselRef.value) {
293
+ carouselRef.value.style.cursor = '-webkit-grab'
294
+ }
295
+
296
+ // Create document fragment for slides
297
+ const docFragment = document.createDocumentFragment()
298
+
299
+ // Add clones before original elements if loop is true
300
+ if (config.value.loop && innerElements.value.length > 0) {
301
+ for (let i = innerElements.value.length - perPage.value; i < innerElements.value.length; i++) {
302
+ // Ensure the index is valid
303
+ if (i >= 0 && i < innerElements.value.length) {
304
+ const element = buildSliderFrameItem(innerElements.value[i].cloneNode(true) as Element)
305
+ docFragment.appendChild(element)
306
+ }
307
+ }
308
+ }
309
+
310
+ // Add original elements
311
+ for (let i = 0; i < innerElements.value.length; i++) {
312
+ const element = buildSliderFrameItem(innerElements.value[i])
313
+ docFragment.appendChild(element)
314
+ }
315
+
316
+ // Add clones after original elements if loop is true
317
+ if (config.value.loop && innerElements.value.length > 0) {
318
+ for (let i = 0; i < perPage.value; i++) {
319
+ // Ensure the index is valid
320
+ if (i >= 0 && i < innerElements.value.length) {
321
+ const element = buildSliderFrameItem(innerElements.value[i].cloneNode(true) as Element)
322
+ docFragment.appendChild(element)
323
+ }
324
+ }
325
+ }
326
+
327
+ // Clear selector and append the frame
328
+ if (sliderFrame.value) {
329
+ sliderFrame.value.innerHTML = ''
330
+ sliderFrame.value.appendChild(docFragment)
331
+
332
+ if (carouselRef.value) {
333
+ carouselRef.value.innerHTML = ''
334
+ carouselRef.value.appendChild(sliderFrame.value)
335
+ }
336
+ }
337
+
338
+ // Go to current slide
339
+ slideToCurrent()
340
+ }
341
+
342
+ function buildSliderFrameItem(elm: Element): HTMLElement {
343
+ const elementContainer = document.createElement('div')
344
+ elementContainer.style.cssFloat = config.value.rtl ? 'right' : 'left'
345
+ elementContainer.style.float = config.value.rtl ? 'right' : 'left'
346
+
347
+ const percentage = config.value.loop
348
+ ? 100 / (innerElements.value.length + (perPage.value * 2))
349
+ : 100 / innerElements.value.length
350
+
351
+ elementContainer.style.width = `${percentage}%`
352
+ elementContainer.appendChild(elm)
353
+
354
+ return elementContainer
355
+ }
356
+
357
+ function slideToCurrent(enableTransitionFlag?: boolean): void {
358
+ const currentSlideValue = config.value.loop
359
+ ? currentSlide.value + perPage.value
360
+ : currentSlide.value
361
+
362
+ const offset = (config.value.rtl ? 1 : -1) * currentSlideValue * (selectorWidth.value / perPage.value)
363
+
364
+ if (enableTransitionFlag && sliderFrame.value) {
365
+ // Use requestAnimationFrame for smooth transitions
366
+ requestAnimationFrame(() => {
367
+ requestAnimationFrame(() => {
368
+ enableTransition()
369
+ if (sliderFrame.value) {
370
+ sliderFrame.value.style[transformProperty.value as any] = `translate3d(${offset}px, 0, 0)`
371
+ }
372
+ })
373
+ })
374
+ } else if (sliderFrame.value) {
375
+ sliderFrame.value.style[transformProperty.value as any] = `translate3d(${offset}px, 0, 0)`
376
+ }
377
+ }
378
+
379
+ function prev(howManySlides: number = 1): void {
380
+ if (innerElements.value.length <= perPage.value) {
381
+ return
382
+ }
383
+
384
+ const beforeChange = currentSlide.value
385
+
386
+ if (config.value.loop) {
387
+ const isNewIndexClone = currentSlide.value - howManySlides < 0
388
+
389
+ if (isNewIndexClone) {
390
+ disableTransition()
391
+
392
+ const mirrorSlideIndex = currentSlide.value + innerElements.value.length
393
+ const mirrorSlideIndexOffset = perPage.value
394
+ const moveTo = mirrorSlideIndex + mirrorSlideIndexOffset
395
+ const offset = (config.value.rtl ? 1 : -1) * moveTo * (selectorWidth.value / perPage.value)
396
+ const dragDistance = config.value.draggable ? drag.value.endX - drag.value.startX : 0
397
+
398
+ if (sliderFrame.value) {
399
+ sliderFrame.value.style[transformProperty.value as any] = `translate3d(${offset + dragDistance}px, 0, 0)`
400
+ }
401
+
402
+ currentSlide.value = mirrorSlideIndex - howManySlides
403
+ } else {
404
+ currentSlide.value = currentSlide.value - howManySlides
405
+ }
406
+ } else {
407
+ currentSlide.value = Math.max(currentSlide.value - howManySlides, 0)
408
+ }
409
+
410
+ if (beforeChange !== currentSlide.value) {
411
+ slideToCurrent(config.value.loop)
412
+ config.value.onChange()
413
+ }
414
+ }
415
+
416
+ function next(howManySlides: number = 1): void {
417
+ if (innerElements.value.length <= perPage.value) {
418
+ return
419
+ }
420
+
421
+ const beforeChange = currentSlide.value
422
+
423
+ if (config.value.loop) {
424
+ const isNewIndexClone = currentSlide.value + howManySlides > innerElements.value.length - perPage.value
425
+
426
+ if (isNewIndexClone) {
427
+ disableTransition()
428
+
429
+ const mirrorSlideIndex = currentSlide.value - innerElements.value.length
430
+ const mirrorSlideIndexOffset = perPage.value
431
+ const moveTo = mirrorSlideIndex + mirrorSlideIndexOffset
432
+ const offset = (config.value.rtl ? 1 : -1) * moveTo * (selectorWidth.value / perPage.value)
433
+ const dragDistance = config.value.draggable ? drag.value.endX - drag.value.startX : 0
434
+
435
+ if (sliderFrame.value) {
436
+ sliderFrame.value.style[transformProperty.value as any] = `translate3d(${offset + dragDistance}px, 0, 0)`
437
+ }
438
+
439
+ currentSlide.value = mirrorSlideIndex + howManySlides
440
+ } else {
441
+ currentSlide.value = currentSlide.value + howManySlides
442
+ }
443
+ } else {
444
+ currentSlide.value = Math.min(currentSlide.value + howManySlides, innerElements.value.length - perPage.value)
445
+ }
446
+
447
+ if (beforeChange !== currentSlide.value) {
448
+ slideToCurrent(config.value.loop)
449
+ config.value.onChange()
450
+ }
451
+ }
452
+
453
+ function goTo(index: number): void {
454
+ if (innerElements.value.length <= perPage.value) {
455
+ return
456
+ }
457
+
458
+ const beforeChange = currentSlide.value
459
+
460
+ currentSlide.value = config.value.loop
461
+ ? index % innerElements.value.length
462
+ : Math.min(Math.max(index, 0), innerElements.value.length - perPage.value)
463
+
464
+ if (beforeChange !== currentSlide.value) {
465
+ slideToCurrent()
466
+ config.value.onChange()
467
+ }
468
+ }
469
+
470
+ function disableTransition(): void {
471
+ if (sliderFrame.value) {
472
+ sliderFrame.value.style.webkitTransition = `all 0ms ${config.value.easing}`
473
+ sliderFrame.value.style.transition = `all 0ms ${config.value.easing}`
474
+ }
475
+ }
476
+
477
+ function enableTransition(): void {
478
+ if (sliderFrame.value) {
479
+ sliderFrame.value.style.webkitTransition = `all ${config.value.duration}ms ${config.value.easing}`
480
+ sliderFrame.value.style.transition = `all ${config.value.duration}ms ${config.value.easing}`
481
+ }
482
+ }
483
+
484
+ function updateAfterDrag(): void {
485
+ const movement = (config.value.rtl ? -1 : 1) * (drag.value.endX - drag.value.startX)
486
+ const movementDistance = Math.abs(movement)
487
+ const howManySliderToSlide = config.value.multipleDrag
488
+ ? Math.ceil(movementDistance / (selectorWidth.value / perPage.value))
489
+ : 1
490
+
491
+ const slideToNegativeClone = movement > 0 && currentSlide.value - howManySliderToSlide < 0
492
+ const slideToPositiveClone = movement < 0
493
+ && currentSlide.value + howManySliderToSlide > innerElements.value.length - perPage.value
494
+
495
+ if (movement > 0 && movementDistance > config.value.threshold && innerElements.value.length > perPage.value) {
496
+ prev(howManySliderToSlide)
497
+ } else if (movement < 0
498
+ && movementDistance > config.value.threshold
499
+ && innerElements.value.length > perPage.value) {
500
+ next(howManySliderToSlide)
501
+ }
502
+
503
+ slideToCurrent(slideToNegativeClone || slideToPositiveClone)
504
+ }
505
+
506
+ function resizeHandler(): void {
507
+ // Recalculate current slide
508
+ if (currentSlide.value + perPage.value > innerElements.value.length) {
509
+ currentSlide.value = innerElements.value.length <= perPage.value
510
+ ? 0
511
+ : innerElements.value.length - perPage.value
512
+ }
513
+
514
+ if (carouselRef.value) {
515
+ selectorWidth.value = carouselRef.value.offsetWidth
516
+ }
517
+
518
+ buildSliderFrame()
519
+ }
520
+
521
+ function clearDrag(): void {
522
+ drag.value = {
523
+ startX: 0,
524
+ endX: 0,
525
+ startY: 0,
526
+ letItGo: null,
527
+ preventClick: drag.value.preventClick
528
+ }
529
+ }
530
+
531
+ // Touch event handlers
532
+ function touchstartHandler(e: TouchEvent): void {
533
+ const target = e.target as HTMLElement
534
+ const ignoreTags = ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].includes(target.nodeName)
535
+
536
+ if (ignoreTags) {
537
+ return
538
+ }
539
+
540
+ // Pause autoplay during touch interaction
541
+ stopAutoplay()
542
+
543
+ e.stopPropagation()
544
+ pointerDown.value = true
545
+ isDragging.value = false // Reset dragging state
546
+ drag.value.startX = e.touches[0].pageX
547
+ drag.value.startY = e.touches[0].pageY
548
+
549
+ // Mark link-related elements for potential preventClick
550
+ const isLink = target.tagName === 'A' || target.closest('a')
551
+ const isImage = target.tagName === 'IMG'
552
+ if (isLink || isImage) {
553
+ drag.value.preventClick = false
554
+ }
555
+ }
556
+
557
+ function touchendHandler(e: TouchEvent): void {
558
+ e.stopPropagation()
559
+
560
+ // If we were dragging, prevent clicks by adding a short-lived capture event listener
561
+ if (isDragging.value) {
562
+ // Add a temporary, one-time capture event listener to block the next click
563
+ document.addEventListener('click', function preventClickAfterDrag(event) {
564
+ event.preventDefault()
565
+ event.stopPropagation()
566
+ event.stopImmediatePropagation() // This is crucial to stop other handlers from executing
567
+
568
+ // Remove this listener after it's been triggered once
569
+ document.removeEventListener('click', preventClickAfterDrag, true)
570
+ }, { capture: true, once: true })
571
+ }
572
+
573
+ pointerDown.value = false
574
+ isDragging.value = false
575
+ enableTransition()
576
+
577
+ if (drag.value.endX) {
578
+ updateAfterDrag()
579
+ }
580
+
581
+ clearDrag()
582
+
583
+ // Restart autoplay after touch interaction
584
+ if (config.value.autoplay !== 'disabled') {
585
+ startAutoplay()
586
+ }
587
+ }
588
+
589
+ function touchmoveHandler(e: TouchEvent): void {
590
+ e.stopPropagation()
591
+
592
+ if (drag.value.letItGo === null) {
593
+ drag.value.letItGo = Math.abs(drag.value.startY - e.touches[0].pageY)
594
+ < Math.abs(drag.value.startX - e.touches[0].pageX)
595
+ }
596
+
597
+ if (pointerDown.value && drag.value.letItGo) {
598
+ e.preventDefault()
599
+ drag.value.endX = e.touches[0].pageX
600
+
601
+ // Calculate movement distance
602
+ const dragDistance = Math.abs(drag.value.endX - drag.value.startX)
603
+
604
+ // If dragged more than the threshold, mark as a real drag operation
605
+ if (dragDistance > dragThreshold) {
606
+ isDragging.value = true
607
+ drag.value.preventClick = true
608
+ }
609
+
610
+ if (sliderFrame.value) {
611
+ disableTransition()
612
+
613
+ const currentSlideValue = config.value.loop
614
+ ? currentSlide.value + perPage.value
615
+ : currentSlide.value
616
+
617
+ const currentOffset = currentSlideValue * (selectorWidth.value / perPage.value)
618
+ const dragOffset = drag.value.endX - drag.value.startX
619
+ const offset = config.value.rtl
620
+ ? currentOffset + dragOffset
621
+ : currentOffset - dragOffset
622
+
623
+ sliderFrame.value.style[transformProperty.value as any]
624
+ = `translate3d(${(config.value.rtl ? 1 : -1) * offset}px, 0, 0)`
625
+ }
626
+ }
627
+ }
628
+
629
+ // Mouse event handlers
630
+ function mousedownHandler(e: MouseEvent): void {
631
+ const target = e.target as HTMLElement
632
+ const ignoreTags = ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].includes(target.nodeName)
633
+
634
+ if (ignoreTags) return
635
+
636
+ // Pause autoplay during mouse interaction
637
+ stopAutoplay()
638
+
639
+ e.preventDefault()
640
+ e.stopPropagation()
641
+ pointerDown.value = true
642
+ isDragging.value = false // Reset dragging state on mousedown
643
+ drag.value.startX = e.pageX
644
+
645
+ // Mark link-related elements for potential preventClick
646
+ const isLink = target.tagName === 'A' || target.closest('a')
647
+ const isImage = target.tagName === 'IMG'
648
+ if (isLink || isImage) {
649
+ drag.value.preventClick = false // Start with false, will be set to true if actual dragging occurs
650
+ }
651
+ }
652
+
653
+ function mouseupHandler(e: MouseEvent): void {
654
+ e.stopPropagation()
655
+
656
+ // If we were dragging, prevent clicks by adding a short-lived capture event listener
657
+ if (isDragging.value) {
658
+ // Add a temporary, one-time capture event listener to block the next click
659
+ document.addEventListener('click', function preventClickAfterDrag(event) {
660
+ event.preventDefault()
661
+ event.stopPropagation()
662
+ event.stopImmediatePropagation() // This is crucial to stop other handlers from executing
663
+
664
+ // Remove this listener after it's been triggered once
665
+ document.removeEventListener('click', preventClickAfterDrag, true)
666
+ }, { capture: true, once: true })
667
+ }
668
+
669
+ pointerDown.value = false
670
+ isDragging.value = false
671
+
672
+ if (carouselRef.value) {
673
+ carouselRef.value.style.cursor = '-webkit-grab'
674
+ }
675
+
676
+ enableTransition()
677
+
678
+ if (drag.value.endX) {
679
+ updateAfterDrag()
680
+ }
681
+
682
+ clearDrag()
683
+
684
+ // Restart autoplay after mouse interaction if not hovering
685
+ if (config.value.autoplay !== 'disabled' && !isHovering.value) {
686
+ startAutoplay()
687
+ }
688
+ }
689
+
690
+ function mousemoveHandler(e: MouseEvent): void {
691
+ e.preventDefault()
692
+
693
+ if (pointerDown.value) {
694
+ drag.value.endX = e.pageX
695
+
696
+ // Calculate movement distance
697
+ const dragDistance = Math.abs(drag.value.endX - drag.value.startX)
698
+
699
+ // If dragged more than the threshold, mark as a real drag operation
700
+ if (dragDistance > dragThreshold) {
701
+ isDragging.value = true
702
+ drag.value.preventClick = true
703
+ }
704
+
705
+ if (carouselRef.value) {
706
+ carouselRef.value.style.cursor = '-webkit-grabbing'
707
+ }
708
+
709
+ if (sliderFrame.value) {
710
+ disableTransition()
711
+
712
+ const currentSlideValue = config.value.loop
713
+ ? currentSlide.value + perPage.value
714
+ : currentSlide.value
715
+
716
+ const currentOffset = currentSlideValue * (selectorWidth.value / perPage.value)
717
+ const dragOffset = drag.value.endX - drag.value.startX
718
+ const offset = config.value.rtl
719
+ ? currentOffset + dragOffset
720
+ : currentOffset - dragOffset
721
+
722
+ sliderFrame.value.style[transformProperty.value as any]
723
+ = `translate3d(${(config.value.rtl ? 1 : -1) * offset}px, 0, 0)`
724
+ }
725
+ }
726
+ }
727
+
728
+ function mouseleaveHandler(e: MouseEvent): void {
729
+ if (pointerDown.value) {
730
+ pointerDown.value = false
731
+
732
+ if (carouselRef.value) {
733
+ carouselRef.value.style.cursor = '-webkit-grab'
734
+ }
735
+
736
+ drag.value.endX = e.pageX
737
+ drag.value.preventClick = false
738
+ enableTransition()
739
+ updateAfterDrag()
740
+ clearDrag()
741
+ }
742
+ }
743
+
744
+ function clickHandler(e: MouseEvent): void {
745
+ if (drag.value.preventClick || isDragging.value) {
746
+ e.preventDefault()
747
+ e.stopPropagation()
748
+ e.stopImmediatePropagation() // This stops other handlers from firing
749
+ drag.value.preventClick = false
750
+ }
751
+
752
+ drag.value.preventClick = false
753
+ }
754
+
755
+ // Add this new function to handle dragstart events
756
+ function dragstartHandler(e: DragEvent): void {
757
+ // Prevent default drag behavior for images and links
758
+ if (config.value.draggable) {
759
+ const target = e.target as HTMLElement
760
+ const isDraggableElement = target.tagName === 'IMG'
761
+ || target.tagName === 'A'
762
+ || target.parentElement?.tagName === 'A'
763
+
764
+ if (isDraggableElement) {
765
+ e.preventDefault()
766
+ }
767
+ }
768
+ }
769
+
770
+ // Public API
771
+ function remove(index: number): void {
772
+ if (index < 0 || index >= innerElements.value.length) {
773
+ throw new Error('Item to remove doesn\'t exist 😭')
774
+ }
775
+
776
+ // Shift sliderFrame back by one if needed
777
+ const lowerIndex = index < currentSlide.value
778
+ const lastItem = currentSlide.value + perPage.value - 1 === index
779
+
780
+ if (lowerIndex || lastItem) {
781
+ currentSlide.value--
782
+ }
783
+
784
+ innerElements.value.splice(index, 1)
785
+ buildSliderFrame()
786
+ }
787
+
788
+ function insert(item: Element, index: number): void {
789
+ if (index < 0 || index > innerElements.value.length + 1) {
790
+ throw new Error('Unable to insert at this index 😭')
791
+ }
792
+
793
+ if (innerElements.value.includes(item)) {
794
+ throw new Error('The same item in a carousel? Really? Nope 😭')
795
+ }
796
+
797
+ // Avoid shifting content
798
+ const shouldItShift = index <= currentSlide.value && innerElements.value.length > 0
799
+
800
+ if (shouldItShift) {
801
+ currentSlide.value++
802
+ }
803
+
804
+ innerElements.value.splice(index, 0, item)
805
+ buildSliderFrame()
806
+ }
807
+
808
+ function prepend(item: Element): void {
809
+ insert(item, 0)
810
+ }
811
+
812
+ function append(item: Element): void {
813
+ insert(item, innerElements.value.length)
814
+ }
815
+
816
+ function destroy(restoreMarkup: boolean = false): void {
817
+ detachEvents()
818
+
819
+ if (carouselRef.value) {
820
+ carouselRef.value.style.cursor = 'auto'
821
+
822
+ if (restoreMarkup) {
823
+ const slides = document.createDocumentFragment()
824
+
825
+ for (let i = 0; i < innerElements.value.length; i++) {
826
+ slides.appendChild(innerElements.value[i])
827
+ }
828
+
829
+ carouselRef.value.innerHTML = ''
830
+ carouselRef.value.appendChild(slides)
831
+ carouselRef.value.removeAttribute('style')
832
+ }
833
+ }
834
+ }
835
+
836
+ // Add after init()
837
+ async function startAutoplay(): Promise<void> {
838
+ stopAutoplay() // Clear any existing timers first
839
+
840
+ if (config.value.autoplay === 'standard') {
841
+ autoplayTimer.value = window.setInterval(() => {
842
+ if (!isHovering.value) {
843
+ next()
844
+ }
845
+ }, config.value.autoplayInterval)
846
+ } else if (config.value.autoplay === 'linear') {
847
+ startLinearAutoplay()
848
+ }
849
+ }
850
+
851
+ function stopAutoplay(): void {
852
+ if (autoplayTimer.value !== null) {
853
+ clearInterval(autoplayTimer.value)
854
+ autoplayTimer.value = null
855
+ }
856
+
857
+ if (linearAnimationFrame.value !== null) {
858
+ cancelAnimationFrame(linearAnimationFrame.value)
859
+ linearAnimationFrame.value = null
860
+ }
861
+ }
862
+
863
+ function startLinearAutoplay(): void {
864
+ // Reset the linear offset
865
+ linearOffset.value = 0
866
+ let lastTimestamp = 0
867
+
868
+ // Animation function
869
+ const animate = (timestamp: number) => {
870
+ if (!lastTimestamp) lastTimestamp = timestamp
871
+
872
+ // Calculate time delta and convert to pixels based on speed
873
+ const elapsed = timestamp - lastTimestamp
874
+ lastTimestamp = timestamp
875
+
876
+ // If hovering or not visible, just continue the animation loop without changing position
877
+ if (isHovering.value) {
878
+ linearAnimationFrame.value = requestAnimationFrame(animate)
879
+ return
880
+ }
881
+
882
+ // Calculate how many pixels to move based on time elapsed and speed
883
+ const pixelsToMove = (elapsed / 1000) * config.value.autoplaySpeed
884
+ linearOffset.value += pixelsToMove
885
+
886
+ // Calculate total width of all items
887
+ const itemWidth = selectorWidth.value / perPage.value
888
+ const totalWidth = itemWidth * innerElements.value.length
889
+
890
+ // If we've scrolled past the entire content, reset
891
+ if (linearOffset.value >= totalWidth) {
892
+ if (config.value.loop) {
893
+ linearOffset.value = 0
894
+ } else {
895
+ // If not looping, stop at the end
896
+ linearOffset.value = totalWidth - itemWidth
897
+ stopAutoplay()
898
+ return
899
+ }
900
+ }
901
+
902
+ // Apply the offset using transforms
903
+ if (sliderFrame.value) {
904
+ const direction = config.value.rtl ? 1 : -1
905
+ sliderFrame.value.style.transition = 'none' // Smooth scrolling, no easing
906
+ sliderFrame.value.style[transformProperty.value as any]
907
+ = `translate3d(${direction * linearOffset.value}px, 0, 0)`
908
+ }
909
+
910
+ // Update current slide based on the offset
911
+ const newSlide = Math.floor(linearOffset.value / itemWidth) % innerElements.value.length
912
+ if (newSlide !== currentSlide.value) {
913
+ currentSlide.value = newSlide
914
+ config.value.onChange()
915
+ }
916
+
917
+ // Continue animation loop
918
+ linearAnimationFrame.value = requestAnimationFrame(animate)
919
+ }
920
+
921
+ // Start the animation
922
+ linearAnimationFrame.value = requestAnimationFrame(animate)
923
+ }
924
+
925
+ // Event handlers for autoplay pausing
926
+ function mouseenterHandler(): void {
927
+ isHovering.value = true
928
+ }
929
+
930
+ function mouseleaveAutoplayHandler(): void {
931
+ isHovering.value = false
932
+ }
933
+
934
+ // Add new public methods for autoplay control
935
+ function pauseAutoplay(): void {
936
+ stopAutoplay()
937
+ }
938
+
939
+ function resumeAutoplay(): void {
940
+ if (config.value.autoplay !== 'disabled') {
941
+ startAutoplay()
942
+ }
943
+ }
944
+
945
+ // Expose public methods
946
+ defineExpose({
947
+ prev,
948
+ next,
949
+ goTo,
950
+ remove,
951
+ insert,
952
+ prepend,
953
+ append,
954
+ destroy,
955
+ currentSlide,
956
+ pauseAutoplay,
957
+ resumeAutoplay,
958
+ })
959
+ </script>
960
+
961
+ <template>
962
+ <div class="carousel-wrapper">
963
+ <div ref="carouselRef" class="carousel-container">
964
+ <slot />
965
+ </div>
966
+ <div v-if="props.dots && totalDots > 1" class="carousel-dots">
967
+ <button
968
+ v-for="i in totalDots"
969
+ :key="i"
970
+ type="button"
971
+ class="carousel-dot" :class="[{ active: (i - 1) === currentSlide }]"
972
+ :aria-label="`Go to slide ${i}`"
973
+ @click="goTo(i - 1)"
974
+ />
975
+ </div>
976
+ </div>
977
+ </template>
978
+
979
+ <style scoped>
980
+ .carousel-wrapper {
981
+ position: relative;
982
+ width: 100%;
983
+ }
984
+
985
+ .carousel-container {
986
+ margin: 0 auto;
987
+ overflow: hidden;
988
+ }
989
+
990
+ .carousel-dots {
991
+ display: flex;
992
+ justify-content: center;
993
+ gap: 8px;
994
+ margin-top: 16px;
995
+ }
996
+
997
+ .carousel-dot {
998
+ width: 12px;
999
+ height: 12px;
1000
+ border-radius: 50px;
1001
+ background-color: #ccc;
1002
+ border: none;
1003
+ padding: 0;
1004
+ cursor: pointer;
1005
+ transition: all 0.3s ease-in-out;
1006
+ }
1007
+
1008
+ .carousel-dot.active {
1009
+ background-color: #333;
1010
+ width: 26px;
1011
+ }
1012
+ </style>