@bagelink/vue 1.14.15 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/components/Alert.vue.d.ts.map +1 -1
  2. package/dist/components/Badge.vue.d.ts.map +1 -1
  3. package/dist/components/Btn.vue.d.ts.map +1 -1
  4. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  5. package/dist/components/Image.vue.d.ts.map +1 -1
  6. package/dist/components/ListItem.vue.d.ts.map +1 -1
  7. package/dist/components/MapEmbed/Index.vue.d.ts.map +1 -1
  8. package/dist/components/Pagination.vue.d.ts.map +1 -1
  9. package/dist/components/Swiper.vue.d.ts.map +1 -1
  10. package/dist/components/Toast.vue.d.ts.map +1 -1
  11. package/dist/components/form/index.d.ts.map +1 -1
  12. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  13. package/dist/components/index.d.ts.map +1 -1
  14. package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
  15. package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
  16. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
  17. package/dist/components/layout/Panel.vue.d.ts.map +1 -1
  18. package/dist/components/layout/Resizable.vue.d.ts.map +1 -1
  19. package/dist/components/layout/TabsNav.vue.d.ts.map +1 -1
  20. package/dist/components/layout/appLayoutContext.d.ts +24 -0
  21. package/dist/components/layout/appLayoutContext.d.ts.map +1 -0
  22. package/dist/components/layout/index.d.ts.map +1 -1
  23. package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  24. package/dist/composables/index.d.ts.map +1 -1
  25. package/dist/composables/useDevice.d.ts.map +1 -1
  26. package/dist/composables/useEscapeKey.d.ts +12 -0
  27. package/dist/composables/useEscapeKey.d.ts.map +1 -0
  28. package/dist/composables/useSchemaField.d.ts.map +1 -1
  29. package/dist/composables/useTheme.d.ts.map +1 -1
  30. package/dist/form-flow/FormFlow.vue.d.ts.map +1 -1
  31. package/dist/form-flow/form-flow.d.ts.map +1 -1
  32. package/dist/index.cjs +203 -207
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.mjs +25819 -28870
  35. package/dist/style.css +1 -1
  36. package/dist/types/BagelForm.d.ts.map +1 -1
  37. package/dist/types/BtnOptions.d.ts.map +1 -1
  38. package/dist/utils/constants.d.ts.map +1 -1
  39. package/dist/utils/index.d.ts.map +1 -1
  40. package/package.json +3 -6
  41. package/src/components/Alert.vue +34 -14
  42. package/src/components/Badge.vue +145 -22
  43. package/src/components/Btn.vue +43 -31
  44. package/src/components/Dropdown.vue +5 -12
  45. package/src/components/FilterQuery.vue +1 -1
  46. package/src/components/Image.vue +3 -2
  47. package/src/components/JSONSchema.vue +2 -2
  48. package/src/components/JsonBuilder.vue +1 -1
  49. package/src/components/ListItem.vue +1 -3
  50. package/src/components/MapEmbed/Index.vue +10 -9
  51. package/src/components/NavBar.vue +2 -2
  52. package/src/components/Spreadsheet/Index.vue +1 -1
  53. package/src/components/Swiper.vue +3 -1
  54. package/src/components/Toast.vue +23 -8
  55. package/src/components/calendar/Index.vue +4 -4
  56. package/src/components/calendar/views/MonthView.vue +3 -3
  57. package/src/components/form/index.ts +0 -4
  58. package/src/components/form/inputs/EmailInput.vue +1 -1
  59. package/src/components/form/inputs/NumberInput.vue +1 -1
  60. package/src/components/form/inputs/OTP.vue +2 -2
  61. package/src/components/form/inputs/SelectInput.vue +3 -3
  62. package/src/components/form/inputs/TelInput.vue +2 -2
  63. package/src/components/form/inputs/TextInput.vue +1 -1
  64. package/src/components/form/inputs/Upload/upload.css +2 -2
  65. package/src/components/index.ts +2 -6
  66. package/src/components/layout/AppContent.vue +5 -19
  67. package/src/components/layout/AppLayout.vue +47 -18
  68. package/src/components/layout/AppSidebar.vue +16 -33
  69. package/src/components/layout/Resizable.vue +5 -2
  70. package/src/components/layout/TabsNav.vue +5 -5
  71. package/src/components/layout/appLayoutContext.ts +44 -0
  72. package/src/components/layout/index.ts +2 -0
  73. package/src/components/lightbox/Lightbox.vue +3 -9
  74. package/src/composables/index.ts +1 -0
  75. package/src/composables/useDevice.ts +2 -1
  76. package/src/composables/useEscapeKey.ts +56 -0
  77. package/src/composables/useSchemaField.ts +2 -17
  78. package/src/composables/useTheme.ts +23 -19
  79. package/src/form-flow/FormFlow.vue +2 -0
  80. package/src/form-flow/form-flow.ts +7 -0
  81. package/src/index.ts +0 -2
  82. package/src/styles/inputs.css +1 -1
  83. package/src/types/BagelForm.ts +46 -151
  84. package/src/types/BtnOptions.ts +5 -3
  85. package/src/utils/constants.ts +7 -0
  86. package/src/utils/index.ts +19 -3
  87. package/src/utils/sizeParsing.ts +5 -5
  88. package/vite.config.ts +5 -1
  89. package/src/components/Carousel.vue +0 -724
  90. package/src/components/ImportData.vue +0 -1749
  91. package/src/components/Pill.vue +0 -150
  92. package/src/components/Slider.vue +0 -1446
  93. package/src/components/Title.vue +0 -23
  94. package/src/components/ToolBar.vue +0 -9
  95. package/src/components/form/BagelForm.vue +0 -219
  96. package/src/components/form/BglFieldSet.vue +0 -14
  97. package/src/components/form/BglMultiStepForm.vue +0 -469
  98. package/src/components/form/FieldArray.vue +0 -422
  99. package/src/components/form/useBagelFormState.ts +0 -76
  100. package/src/composables/useFormField.ts +0 -38
  101. package/src/dialog/DialogOLD.vue +0 -358
  102. package/src/utils/BagelFormUtils.ts +0 -684
@@ -1,1446 +0,0 @@
1
- <!-- eslint-disable vue/multi-word-component-names -->
2
- <script setup lang="ts">
3
- import { sleep, useDevice } from '@bagelink/vue'
4
- import { ref, onMounted, onBeforeUnmount, watch, computed, nextTick, onUpdated } from 'vue'
5
-
6
- type Easing = 'ease-in-out' | 'ease-in' | 'ease-out' | 'linear'
7
- type AutoplayMode = 'disabled' | 'standard' | 'linear'
8
- type AutoplayValue = boolean | AutoplayMode
9
-
10
- interface CarouselOptions {
11
- speed?: number
12
- easing?: Easing
13
- perPage?: number | { [key: number]: number }
14
- slideWidth?: number
15
- slideGap?: number
16
- startIndex?: number
17
- draggable?: boolean
18
- multipleDrag?: boolean
19
- threshold?: number
20
- loop?: boolean
21
- rtl?: boolean
22
- autoplay?: AutoplayValue
23
- autoplayInterval?: number
24
- autoplaySpeed?: number
25
- pauseOnHover?: boolean
26
- dots?: boolean
27
- onInit?: () => void
28
- onChange?: () => void
29
- }
30
-
31
- interface DragState {
32
- startX: number
33
- endX: number
34
- startY: number
35
- letItGo: boolean | null
36
- preventClick: boolean
37
- }
38
-
39
- const props = withDefaults(defineProps<CarouselOptions>(), {
40
- speed: 350,
41
- easing: 'ease-out',
42
- startIndex: 0,
43
- draggable: true,
44
- multipleDrag: true,
45
- threshold: 20,
46
- loop: true,
47
- rtl: false,
48
- autoplay: 'disabled',
49
- autoplayInterval: 5000,
50
- pauseOnHover: true,
51
- dots: false,
52
- slideWidth: 300,
53
- slideGap: 1
54
- })
55
-
56
- const emit = defineEmits<{
57
- (event: 'init'): void
58
- (event: 'change'): void
59
- }>()
60
-
61
- // Basic state refs
62
- const carouselRef = ref<HTMLElement | null>(null)
63
- const sliderFrame = ref<HTMLDivElement | null>(null)
64
- const innerElements = ref<Element[]>([])
65
- const currentSlide = ref(0)
66
- const selectorWidth = ref(0)
67
- const { innerWidth } = useDevice()
68
- const transformProperty = ref('transform')
69
- const pointerDown = ref(false)
70
-
71
- // Add a flag to track if we're in the middle of initializing or rebuilding
72
- const isBuilding = ref(false)
73
-
74
- // Drag-related refs
75
- const drag = ref<DragState>({
76
- startX: 0,
77
- endX: 0,
78
- startY: 0,
79
- letItGo: null,
80
- preventClick: false
81
- })
82
-
83
- // Add a new variable to track actual dragging state
84
- const isDragging = ref(false)
85
- const dragThreshold = 10 // Increase threshold slightly for better detection
86
-
87
- // Add autoplay-related refs
88
- const autoplayTimer = ref<number | null>(null)
89
- const linearAnimationFrame = ref<number | null>(null)
90
- const isHovering = ref(false)
91
- const linearOffset = ref(0)
92
-
93
- // Add a counter to track rebuild attempts with no items
94
- const emptyRebuildAttempts = ref(0)
95
- const MAX_EMPTY_REBUILD_ATTEMPTS = 3
96
-
97
- // Modified to work with optional parameters and ensure compatibility with Required type
98
- type ComputedConfig = Omit<Required<CarouselOptions>, 'autoplay'> & { autoplay: AutoplayMode }
99
-
100
- // perPage calculation - independent of config
101
- const perPage = computed(() => {
102
- if (typeof props.perPage === 'number') {
103
- return props.perPage
104
- }
105
-
106
- if (typeof props.perPage === 'object') {
107
- return Object.entries(props.perPage)
108
- .reduce((acc, [viewport, value]) => innerWidth.value >= Number(viewport) ? value : acc, 1)
109
- }
110
-
111
- // If perPage is undefined, calculate based on slideWidth
112
- return Math.max(1, Math.ceil(selectorWidth.value / props.slideWidth))
113
- })
114
-
115
- // Core state
116
- const config = computed<ComputedConfig>(() => {
117
- // Convert boolean autoplay values to string mode
118
- let autoplayMode: AutoplayMode = 'disabled'
119
-
120
- if (typeof props.autoplay === 'boolean') {
121
- autoplayMode = props.autoplay ? 'standard' : 'disabled'
122
- } else {
123
- autoplayMode = props.autoplay as AutoplayMode
124
- }
125
-
126
- // Calculate perPage fallback based on config
127
- const calculatedPerPage = perPage.value
128
-
129
- return {
130
- speed: props.speed,
131
- easing: props.easing,
132
- perPage: calculatedPerPage,
133
- startIndex: props.startIndex,
134
- draggable: props.draggable,
135
- multipleDrag: props.multipleDrag,
136
- threshold: props.threshold,
137
- loop: props.loop,
138
- rtl: props.rtl,
139
- autoplay: autoplayMode,
140
- autoplayInterval: props.autoplayInterval,
141
- autoplaySpeed: props.speed / 4,
142
- pauseOnHover: props.pauseOnHover,
143
- dots: props.dots,
144
- slideWidth: props.slideWidth,
145
- slideGap: props.slideGap,
146
- onInit: () => { emit('init') },
147
- onChange: () => { emit('change') }
148
- }
149
- })
150
-
151
- // Computed property for total number of dots to display
152
- const totalDots = computed(() => {
153
- if (!innerElements.value.length) { return 0 }
154
- return config.value.loop ? innerElements.value.length : Math.ceil(innerElements.value.length - perPage.value + 1)
155
- })
156
-
157
- // Add MutationObserver ref
158
- const slotObserver = ref<MutationObserver | null>(null)
159
-
160
- // Add a flag to track component mounted state
161
- const isMounted = ref(false)
162
-
163
- // Add a reference to the slot container
164
- const slotContainer = ref<HTMLDivElement | null>(null)
165
-
166
- // Lifecycle hooks
167
- onMounted(() => {
168
- // Set the mounted flag first
169
- isMounted.value = true
170
-
171
- // Wait a tick to ensure slot content is rendered
172
- nextTick(() => {
173
- init()
174
- })
175
- })
176
-
177
- onBeforeUnmount(() => {
178
- // Clean up
179
- isMounted.value = false
180
- stopAutoplay()
181
- detachEvents()
182
-
183
- // Remove the observer
184
- if (slotObserver.value) {
185
- slotObserver.value.disconnect()
186
- slotObserver.value = null
187
- }
188
-
189
- // Clear DOM content
190
- if (carouselRef.value) {
191
- carouselRef.value.innerHTML = ''
192
- }
193
- })
194
-
195
- // Add an onUpdated hook that checks for slot content changes
196
- onUpdated(() => {
197
- // Only check if we're already initialized
198
- if (isMounted.value && !isBuilding.value && emptyRebuildAttempts.value < MAX_EMPTY_REBUILD_ATTEMPTS) {
199
- // Check if slot content changed
200
- if (slotContainer.value) {
201
- const slotElements = Array.from(slotContainer.value.children)
202
-
203
- // Only update if count changed or first initialization
204
- if (slotElements.length !== innerElements.value.length || innerElements.value.length === 0) {
205
- // Clone the elements from slot
206
- innerElements.value = slotElements.map(el => el.cloneNode(true) as Element)
207
-
208
- // Schedule rebuild
209
- nextTick(() => {
210
- updateSlider()
211
- })
212
- }
213
- }
214
- }
215
- })
216
-
217
- // Watch for changes to props
218
- watch(() => props, () => {
219
- // Props changed, might need to rebuild
220
- resizeHandler()
221
- }, { deep: true })
222
-
223
- // Helper functions
224
- function webkitOrNot(): string {
225
- const { style } = document.documentElement
226
- if (typeof style.transform === 'string') {
227
- return 'transform'
228
- }
229
- return 'WebkitTransform'
230
- }
231
-
232
- // Event handlers
233
- function attachEvents(): void {
234
- window.addEventListener('resize', resizeHandler)
235
-
236
- if (config.value.draggable && carouselRef.value) {
237
- const eventOptions = { passive: true }
238
- const touchMoveOptions = { passive: false }
239
- const mouseMoveOptions = { passive: false } // Non-passive option for mouse move
240
- const mouseDownOptions = { passive: false } // Non-passive option for mouse down
241
-
242
- // Touch events
243
- carouselRef.value.addEventListener('touchstart', touchstartHandler, eventOptions)
244
- carouselRef.value.addEventListener('touchend', touchendHandler, eventOptions)
245
- carouselRef.value.addEventListener('touchmove', touchmoveHandler, touchMoveOptions)
246
-
247
- // Mouse events
248
- carouselRef.value.addEventListener('mousedown', mousedownHandler, mouseDownOptions)
249
- carouselRef.value.addEventListener('mouseup', mouseupHandler, eventOptions)
250
- carouselRef.value.addEventListener('mouseleave', mouseleaveHandler, eventOptions)
251
- carouselRef.value.addEventListener('mousemove', mousemoveHandler, mouseMoveOptions)
252
-
253
- // Prevent default dragging of images and links
254
- carouselRef.value.addEventListener('dragstart', dragstartHandler)
255
-
256
- // Click
257
- carouselRef.value.addEventListener('click', clickHandler)
258
- }
259
-
260
- // Add mouse enter/leave events for pauseOnHover functionality
261
- if (config.value.autoplay !== 'disabled' && config.value.pauseOnHover && carouselRef.value) {
262
- carouselRef.value.addEventListener('mouseenter', mouseenterHandler)
263
- carouselRef.value.addEventListener('mouseleave', mouseleaveAutoplayHandler)
264
- }
265
- }
266
-
267
- function detachEvents(): void {
268
- window.removeEventListener('resize', resizeHandler)
269
-
270
- if (carouselRef.value) {
271
- carouselRef.value.removeEventListener('touchstart', touchstartHandler)
272
- carouselRef.value.removeEventListener('touchend', touchendHandler)
273
- carouselRef.value.removeEventListener('touchmove', touchmoveHandler)
274
- carouselRef.value.removeEventListener('mousedown', mousedownHandler)
275
- carouselRef.value.removeEventListener('mouseup', mouseupHandler)
276
- carouselRef.value.removeEventListener('mouseleave', mouseleaveHandler)
277
- carouselRef.value.removeEventListener('mousemove', mousemoveHandler)
278
- carouselRef.value.removeEventListener('dragstart', dragstartHandler)
279
- carouselRef.value.removeEventListener('click', clickHandler)
280
- carouselRef.value.removeEventListener('mouseenter', mouseenterHandler)
281
- carouselRef.value.removeEventListener('mouseleave', mouseleaveAutoplayHandler)
282
- }
283
- }
284
-
285
- // Core functionality
286
- async function init(): Promise<void> {
287
- if (!carouselRef.value || !slotContainer.value || !isMounted.value) { return }
288
-
289
- transformProperty.value = webkitOrNot()
290
-
291
- // Add a small delay to ensure DOM is ready
292
- await sleep(10)
293
-
294
- // Create references by cloning slot content
295
- innerElements.value = Array.from(slotContainer.value.children).map(
296
- el => el.cloneNode(true) as Element
297
- )
298
-
299
- if (!innerElements.value.length) {
300
- console.warn('No carousel items found during initialization')
301
- return
302
- }
303
-
304
- selectorWidth.value = carouselRef.value.offsetWidth
305
-
306
- // Set current slide
307
- currentSlide.value = config.value.loop
308
- ? config.value.startIndex % innerElements.value.length
309
- : Math.max(0, Math.min(config.value.startIndex, innerElements.value.length - perPage.value))
310
-
311
- // Hide overflow
312
- carouselRef.value.style.overflow = 'hidden'
313
-
314
- // Set RTL direction if needed
315
- carouselRef.value.style.direction = config.value.rtl ? 'rtl' : 'ltr'
316
-
317
- // Attach events
318
- attachEvents()
319
-
320
- // Set up the MutationObserver to watch for slot changes
321
- setupSlotObserver()
322
-
323
- // Build slider frame
324
- buildSliderFrame()
325
-
326
- // Start autoplay if enabled
327
- if (config.value.autoplay !== 'disabled') {
328
- startAutoplay()
329
- }
330
-
331
- // Call onInit callback
332
- config.value.onInit()
333
- }
334
-
335
- function buildSliderFrame(): void {
336
- if (!carouselRef.value || !isMounted.value) { return }
337
-
338
- // Set building flag to prevent interactions during rebuild
339
- isBuilding.value = true
340
-
341
- try {
342
- // Ensure we have elements to build the slider
343
- if (!innerElements.value.length) {
344
- console.warn('No carousel items found')
345
- emptyRebuildAttempts.value++
346
- isBuilding.value = false
347
- return
348
- }
349
-
350
- // Reset counter since we have elements
351
- emptyRebuildAttempts.value = 0
352
-
353
- const originalCount = innerElements.value.length
354
- const widthItem = selectorWidth.value / perPage.value
355
-
356
- // Calculate how many times to repeat items to fill big screens
357
- const minItemsNeeded = Math.max(perPage.value * 3, 20) // Ensure we have enough content for big screens
358
- const repetitions = Math.max(1, Math.ceil(minItemsNeeded / originalCount))
359
- const totalMainItems = originalCount * repetitions
360
-
361
- // Ultra-aggressive buffer size for really big screens
362
- const bufferSize = Math.max(perPage.value * 8, 20) // Even more buffer for huge screens
363
- const itemsToBuild = config.value.loop
364
- ? totalMainItems + (2 * bufferSize)
365
- : totalMainItems
366
-
367
- // Create a clean slate for the slider
368
- carouselRef.value.innerHTML = ''
369
-
370
- // Create or recreate the slider frame
371
- if (!sliderFrame.value) {
372
- sliderFrame.value = document.createElement('div')
373
- } else {
374
- // Clear existing content
375
- while (sliderFrame.value.firstChild) {
376
- sliderFrame.value.removeChild(sliderFrame.value.firstChild)
377
- }
378
- }
379
-
380
- sliderFrame.value.style.width = `${widthItem * itemsToBuild}px`
381
- enableTransition()
382
-
383
- if (config.value.draggable) {
384
- carouselRef.value.style.cursor = '-webkit-grab'
385
- }
386
-
387
- // Create a fragment to hold all slider content
388
- const docFragment = document.createDocumentFragment()
389
-
390
- // Add clones before original items (for loop mode)
391
- if (config.value.loop && innerElements.value.length > 0) {
392
- for (let i = originalCount - bufferSize; i < originalCount; i++) {
393
- if (i >= 0 && i < originalCount) {
394
- const original = innerElements.value[i]
395
- const clone = original.cloneNode(true) as Element
396
-
397
- if (clone instanceof HTMLElement) {
398
- clone.setAttribute('data-clone', 'before')
399
- copyAttributesAndEvents(original, clone)
400
- }
401
-
402
- const wrapped = buildSliderFrameItem(clone)
403
- docFragment.appendChild(wrapped)
404
- }
405
- }
406
- }
407
-
408
- // Add main items repeated multiple times to fill big screens
409
- for (let rep = 0; rep < repetitions; rep++) {
410
- for (let i = 0; i < originalCount; i++) {
411
- const element = innerElements.value[i]
412
- const clone = element.cloneNode(true) as Element
413
-
414
- if (clone instanceof HTMLElement) {
415
- clone.setAttribute('data-repetition', rep.toString())
416
- }
417
-
418
- const wrapped = buildSliderFrameItem(clone)
419
- docFragment.appendChild(wrapped)
420
- }
421
- }
422
-
423
- // Add clones after original items (for loop mode)
424
- if (config.value.loop && innerElements.value.length > 0) {
425
- for (let i = 0; i < bufferSize; i++) {
426
- if (i >= 0 && i < originalCount) {
427
- const original = innerElements.value[i]
428
- const clone = original.cloneNode(true) as Element
429
-
430
- if (clone instanceof HTMLElement) {
431
- clone.setAttribute('data-clone', 'after')
432
- copyAttributesAndEvents(original, clone)
433
- }
434
-
435
- const wrapped = buildSliderFrameItem(clone)
436
- docFragment.appendChild(wrapped)
437
- }
438
- }
439
- }
440
-
441
- // Append all content to the slider frame
442
- sliderFrame.value.appendChild(docFragment)
443
-
444
- // Add the slider frame to the carousel
445
- carouselRef.value.appendChild(sliderFrame.value)
446
-
447
- // Update currentSlide if needed to stay within bounds
448
- if (currentSlide.value >= innerElements.value.length) {
449
- currentSlide.value = Math.max(0, innerElements.value.length - 1)
450
- }
451
-
452
- // Set initial position
453
- slideToCurrent()
454
- } catch (error) {
455
- console.error('Error building slider frame:', error)
456
- } finally {
457
- // Always make sure to reset the building flag
458
- isBuilding.value = false
459
- }
460
- }
461
-
462
- // Helper function to copy attributes and events from original to clone
463
- function copyAttributesAndEvents(original: Element, clone: Element): void {
464
- if (!(original instanceof HTMLElement) || !(clone instanceof HTMLElement)) {
465
- return
466
- }
467
-
468
- // Copy all attributes
469
- const allAttrs = original.attributes
470
- for (let i = 0; i < allAttrs.length; i++) {
471
- const attr = allAttrs[i]
472
- if (attr.name !== 'id') { // Skip id to avoid duplicate IDs
473
- clone.setAttribute(attr.name, attr.value)
474
- }
475
- }
476
-
477
- // For Vue-specific elements, add a custom click handler that relays to the original
478
- clone.addEventListener('click', (e) => {
479
- // Check if original has a click handler
480
- if (typeof (original as any).click === 'function') {
481
- // We need to simulate the click on the original element
482
- // Create and dispatch a new click event on the original element
483
- const clickEvent = new MouseEvent('click', {
484
- bubbles: true,
485
- cancelable: true,
486
- view: window
487
- })
488
- original.dispatchEvent(clickEvent)
489
-
490
- // Stop propagation of the clone's event
491
- e.stopPropagation()
492
- }
493
- })
494
-
495
- // Special handling for links
496
- if (original.tagName === 'A' && clone.tagName === 'A') {
497
- const originalHref = original.getAttribute('href')
498
- if (originalHref) {
499
- clone.addEventListener('click', (e) => {
500
- e.preventDefault()
501
-
502
- // Get original onClick behavior
503
- if (original.onclick) {
504
- original.onclick(new PointerEvent('click'))
505
- } else {
506
- // Default behavior - navigate like original link would
507
- const targetBlank = original.getAttribute('target') === '_blank'
508
- if (targetBlank) {
509
- window.open(originalHref, '_blank')
510
- } else {
511
- window.location.href = originalHref
512
- }
513
- }
514
- })
515
- }
516
- }
517
- }
518
-
519
- function buildSliderFrameItem(elm: Element): HTMLElement {
520
- const elementContainer = document.createElement('div')
521
- elementContainer.style.cssFloat = config.value.rtl ? 'right' : 'left'
522
- elementContainer.style.float = config.value.rtl ? 'right' : 'left'
523
- elementContainer.style.padding = config.value.slideGap ? `${config.value.slideGap / 2}rem` : '0'
524
-
525
- // Calculate total items including repetitions for big screens
526
- const originalCount = innerElements.value.length
527
- const minItemsNeeded = Math.max(perPage.value * 3, 20)
528
- const repetitions = Math.max(1, Math.ceil(minItemsNeeded / originalCount))
529
- const totalMainItems = originalCount * repetitions
530
-
531
- // Ultra-aggressive buffer size - must match buildSliderFrame buffer size
532
- const itemBufferSize = Math.max(perPage.value * 8, 20) // Even more buffer for huge screens
533
- const percentage = config.value.loop
534
- ? 100 / (totalMainItems + (2 * itemBufferSize))
535
- : 100 / totalMainItems
536
-
537
- elementContainer.style.width = `${percentage}%`
538
- elementContainer.appendChild(elm)
539
- return elementContainer
540
- }
541
-
542
- function slideToCurrent(enableTransitionFlag?: boolean): void {
543
- try {
544
- // Ensure component is still mounted
545
- if (!isMounted.value || !innerElements.value.length || !sliderFrame.value) { return }
546
-
547
- // Ultra-aggressive buffer size - must match buildSliderFrame buffer size
548
- const slideBufferSize = Math.max(perPage.value * 8, 20) // Even more buffer for huge screens
549
- const currentSlideValue = config.value.loop
550
- ? currentSlide.value + slideBufferSize
551
- : currentSlide.value
552
-
553
- const offset = (config.value.rtl ? 1 : -1) * currentSlideValue * (selectorWidth.value / perPage.value)
554
-
555
- if (enableTransitionFlag && sliderFrame.value) {
556
- // Use requestAnimationFrame for smooth transitions
557
- requestAnimationFrame(() => {
558
- // Check again that we're still mounted and have the frame
559
- if (!isMounted.value || !sliderFrame.value) { return }
560
-
561
- requestAnimationFrame(() => {
562
- if (!isMounted.value || !sliderFrame.value) { return }
563
-
564
- enableTransition()
565
- sliderFrame.value.style[transformProperty.value as any] = `translate3d(${offset}px, 0, 0)`
566
- })
567
- })
568
- } else if (sliderFrame.value) {
569
- sliderFrame.value.style[transformProperty.value as any] = `translate3d(${offset}px, 0, 0)`
570
- }
571
- } catch (error) {
572
- console.error('Error in slideToCurrent:', error)
573
- }
574
- }
575
-
576
- function prev(howManySlides: number = 1): void {
577
- if (!isMounted.value || isBuilding.value || !innerElements.value.length) { return }
578
-
579
- const beforeChange = currentSlide.value
580
-
581
- if (config.value.loop) {
582
- const isNewIndexClone = currentSlide.value - howManySlides < 0
583
-
584
- if (isNewIndexClone) {
585
- disableTransition()
586
-
587
- const mirrorSlideIndex = currentSlide.value + innerElements.value.length
588
- const mirrorSlideIndexOffset = perPage.value
589
- const moveTo = mirrorSlideIndex + mirrorSlideIndexOffset
590
- const offset = (config.value.rtl ? 1 : -1) * moveTo * (selectorWidth.value / perPage.value)
591
- const dragDistance = config.value.draggable ? drag.value.endX - drag.value.startX : 0
592
-
593
- if (sliderFrame.value) {
594
- sliderFrame.value.style[transformProperty.value as any] = `translate3d(${offset + dragDistance}px, 0, 0)`
595
- }
596
-
597
- currentSlide.value = mirrorSlideIndex - howManySlides
598
- } else {
599
- currentSlide.value -= howManySlides
600
- }
601
- } else {
602
- currentSlide.value = Math.max(currentSlide.value - howManySlides, 0)
603
- }
604
-
605
- if (beforeChange !== currentSlide.value) {
606
- slideToCurrent(config.value.loop)
607
- config.value.onChange()
608
- }
609
- if (config.value.autoplay !== 'disabled') {
610
- startAutoplay()
611
- }
612
- }
613
-
614
- function next(howManySlides: number = 1): void {
615
- if (!isMounted.value || isBuilding.value || !innerElements.value.length) { return }
616
-
617
- const beforeChange = currentSlide.value
618
-
619
- if (config.value.loop) {
620
- const isNewIndexClone = currentSlide.value + howManySlides > innerElements.value.length - perPage.value
621
-
622
- if (isNewIndexClone) {
623
- disableTransition()
624
-
625
- const mirrorSlideIndex = currentSlide.value - innerElements.value.length
626
- const mirrorSlideIndexOffset = perPage.value
627
- const moveTo = mirrorSlideIndex + mirrorSlideIndexOffset
628
- const offset = (config.value.rtl ? 1 : -1) * moveTo * (selectorWidth.value / perPage.value)
629
- const dragDistance = config.value.draggable ? drag.value.endX - drag.value.startX : 0
630
-
631
- if (sliderFrame.value) {
632
- sliderFrame.value.style[transformProperty.value as any] = `translate3d(${offset + dragDistance}px, 0, 0)`
633
- }
634
-
635
- currentSlide.value = mirrorSlideIndex + howManySlides
636
- } else {
637
- currentSlide.value += howManySlides
638
- }
639
- } else {
640
- currentSlide.value = Math.min(currentSlide.value + howManySlides, innerElements.value.length - perPage.value)
641
- }
642
-
643
- if (beforeChange !== currentSlide.value) {
644
- slideToCurrent(config.value.loop)
645
- config.value.onChange()
646
- }
647
- if (config.value.autoplay !== 'disabled') {
648
- startAutoplay()
649
- }
650
- }
651
-
652
- function goTo(index: number): void {
653
- if (innerElements.value.length <= perPage.value) {
654
- return
655
- }
656
-
657
- const beforeChange = currentSlide.value
658
-
659
- currentSlide.value = config.value.loop
660
- ? index % innerElements.value.length
661
- : Math.min(Math.max(index, 0), innerElements.value.length - perPage.value)
662
-
663
- if (beforeChange !== currentSlide.value) {
664
- slideToCurrent()
665
- config.value.onChange()
666
- }
667
- }
668
-
669
- function disableTransition(): void {
670
- if (sliderFrame.value) {
671
- sliderFrame.value.style.webkitTransition = `all 0ms ${config.value.easing}`
672
- sliderFrame.value.style.transition = `all 0ms ${config.value.easing}`
673
- }
674
- }
675
-
676
- function enableTransition(): void {
677
- if (sliderFrame.value) {
678
- sliderFrame.value.style.webkitTransition = `all ${config.value.speed}ms ${config.value.easing}`
679
- sliderFrame.value.style.transition = `all ${config.value.speed}ms ${config.value.easing}`
680
- }
681
- }
682
-
683
- function updateAfterDrag(): void {
684
- if (!isMounted.value) { return }
685
-
686
- const movement = (config.value.rtl ? -1 : 1) * (drag.value.endX - drag.value.startX)
687
- const movementDistance = Math.abs(movement)
688
- const howManySliderToSlide = config.value.multipleDrag
689
- ? Math.ceil(movementDistance / (selectorWidth.value / perPage.value))
690
- : 1
691
-
692
- const slideToNegativeClone = movement > 0 && currentSlide.value - howManySliderToSlide < 0
693
- const slideToPositiveClone = movement < 0
694
- && currentSlide.value + howManySliderToSlide > innerElements.value.length - perPage.value
695
-
696
- if (movement > 0 && movementDistance > config.value.threshold && innerElements.value.length > perPage.value) {
697
- prev(howManySliderToSlide)
698
- } else if (movement < 0
699
- && movementDistance > config.value.threshold
700
- && innerElements.value.length > perPage.value) {
701
- next(howManySliderToSlide)
702
- }
703
-
704
- slideToCurrent(slideToNegativeClone || slideToPositiveClone)
705
- }
706
-
707
- function resizeHandler(): void {
708
- // Recalculate current slide
709
- if (currentSlide.value + perPage.value > innerElements.value.length) {
710
- currentSlide.value = innerElements.value.length <= perPage.value
711
- ? 0
712
- : innerElements.value.length - perPage.value
713
- }
714
-
715
- if (carouselRef.value) {
716
- selectorWidth.value = carouselRef.value.offsetWidth
717
- }
718
-
719
- buildSliderFrame()
720
- }
721
-
722
- function clearDrag(): void {
723
- drag.value = {
724
- startX: 0,
725
- endX: 0,
726
- startY: 0,
727
- letItGo: null,
728
- preventClick: drag.value.preventClick
729
- }
730
- }
731
-
732
- // Touch event handlers
733
- function touchstartHandler(e: TouchEvent): void {
734
- if (isBuilding.value || !isMounted.value) { return }
735
-
736
- const target = e.target as HTMLElement
737
- const ignoreTags = ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].includes(target.nodeName)
738
-
739
- if (ignoreTags) {
740
- return
741
- }
742
-
743
- // Pause autoplay during touch interaction
744
- stopAutoplay()
745
-
746
- e.stopPropagation()
747
- pointerDown.value = true
748
- isDragging.value = false // Reset dragging state
749
- drag.value.startX = e.touches[0].pageX
750
- drag.value.startY = e.touches[0].pageY
751
-
752
- // Mark link-related elements for potential preventClick
753
- const isLink = target.tagName === 'A' || target.closest('a')
754
- const isImage = target.tagName === 'IMG'
755
- if (isLink || isImage) {
756
- drag.value.preventClick = false
757
- }
758
-
759
- // When starting drag during linear autoplay, save the current position
760
- if (config.value.autoplay === 'linear' && sliderFrame.value) {
761
- // Get the current transform value and save it to apply during dragging
762
- const currentTransform = getComputedStyle(sliderFrame.value)[transformProperty.value as any]
763
- if (currentTransform && currentTransform !== 'none') {
764
- try {
765
- // Parse the transform matrix to get the current X position
766
- const matrix = new DOMMatrix(currentTransform)
767
- const currentX = matrix.m41
768
-
769
- // Convert position to slides and update currentSlide
770
- const itemWidth = selectorWidth.value / perPage.value
771
- if (config.value.loop) {
772
- // Adjust for the perPage offset in loop mode
773
- const totalPosition = Math.abs(currentX)
774
- const slidePosition = Math.round(totalPosition / itemWidth) - perPage.value
775
- currentSlide.value = slidePosition >= 0
776
- ? slidePosition % innerElements.value.length
777
- : (innerElements.value.length + (slidePosition % innerElements.value.length)) % innerElements.value.length
778
- } else {
779
- // In non-loop mode, just calculate the current slide
780
- const slidePosition = Math.round(Math.abs(currentX) / itemWidth)
781
- currentSlide.value = Math.min(Math.max(slidePosition, 0), innerElements.value.length - perPage.value)
782
- }
783
- } catch (e) {
784
- console.error('Error parsing transform matrix:', e)
785
- }
786
- }
787
-
788
- // Stop the animation immediately
789
- stopAutoplay()
790
- }
791
- }
792
-
793
- function touchendHandler(e: TouchEvent): void {
794
- if (isBuilding.value || !isMounted.value) { return }
795
-
796
- e.stopPropagation()
797
-
798
- // If we were dragging, prevent clicks by adding a short-lived capture event listener
799
- if (isDragging.value) {
800
- // Add a temporary, one-time capture event listener to block the next click
801
- document.addEventListener('click', function preventClickAfterDrag(event) {
802
- event.preventDefault()
803
- event.stopPropagation()
804
- event.stopImmediatePropagation() // This is crucial to stop other handlers from executing
805
-
806
- // Remove this listener after it's been triggered once
807
- document.removeEventListener('click', preventClickAfterDrag, true)
808
- }, { capture: true, once: true })
809
- }
810
-
811
- pointerDown.value = false
812
- isDragging.value = false
813
- enableTransition()
814
-
815
- if (drag.value.endX) {
816
- updateAfterDrag()
817
- }
818
-
819
- clearDrag()
820
-
821
- // Restart autoplay after touch interaction
822
- if (config.value.autoplay !== 'disabled' && !isHovering.value && isMounted.value) {
823
- startAutoplay()
824
- }
825
- }
826
-
827
- function touchmoveHandler(e: TouchEvent): void {
828
- if (isBuilding.value || !isMounted.value) { return }
829
-
830
- e.stopPropagation()
831
-
832
- if (drag.value.letItGo === null) {
833
- drag.value.letItGo = Math.abs(drag.value.startY - e.touches[0].pageY)
834
- < Math.abs(drag.value.startX - e.touches[0].pageX)
835
- }
836
-
837
- if (pointerDown.value && drag.value.letItGo) {
838
- e.preventDefault()
839
- drag.value.endX = e.touches[0].pageX
840
-
841
- // Calculate movement distance
842
- const dragDistance = Math.abs(drag.value.endX - drag.value.startX)
843
-
844
- // If dragged more than the threshold, mark as a real drag operation
845
- if (dragDistance > dragThreshold) {
846
- isDragging.value = true
847
- drag.value.preventClick = true
848
- }
849
-
850
- if (sliderFrame.value) {
851
- disableTransition()
852
-
853
- const currentSlideValue = config.value.loop
854
- ? currentSlide.value + perPage.value
855
- : currentSlide.value
856
-
857
- const currentOffset = currentSlideValue * (selectorWidth.value / perPage.value)
858
- const dragOffset = drag.value.endX - drag.value.startX
859
- const offset = config.value.rtl
860
- ? currentOffset + dragOffset
861
- : currentOffset - dragOffset
862
-
863
- sliderFrame.value.style[transformProperty.value as any]
864
- = `translate3d(${(config.value.rtl ? 1 : -1) * offset}px, 0, 0)`
865
- }
866
- }
867
- }
868
-
869
- // Mouse event handlers
870
- function mousedownHandler(e: MouseEvent): void {
871
- if (isBuilding.value || !isMounted.value) { return }
872
-
873
- const target = e.target as HTMLElement
874
- const ignoreTags = ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].includes(target.nodeName)
875
-
876
- if (ignoreTags) { return }
877
-
878
- // Pause autoplay during mouse interaction
879
- stopAutoplay()
880
-
881
- e.preventDefault()
882
- e.stopPropagation()
883
- pointerDown.value = true
884
- isDragging.value = false // Reset dragging state on mousedown
885
- drag.value.startX = e.pageX
886
-
887
- // Mark link-related elements for potential preventClick
888
- const isLink = target.tagName === 'A' || target.closest('a')
889
- const isImage = target.tagName === 'IMG'
890
- if (isLink || isImage) {
891
- drag.value.preventClick = false // Start with false, will be set to true if actual dragging occurs
892
- }
893
-
894
- // When starting drag during linear autoplay, save the current position
895
- if (config.value.autoplay === 'linear' && sliderFrame.value) {
896
- // Get the current transform value and save it to apply during dragging
897
- const currentTransform = getComputedStyle(sliderFrame.value)[transformProperty.value as any]
898
- if (currentTransform && currentTransform !== 'none') {
899
- try {
900
- // Parse the transform matrix to get the current X position
901
- const matrix = new DOMMatrix(currentTransform)
902
- const currentX = matrix.m41
903
-
904
- // Convert position to slides and update currentSlide
905
- const itemWidth = selectorWidth.value / perPage.value
906
- if (config.value.loop) {
907
- // Adjust for the perPage offset in loop mode
908
- const totalPosition = Math.abs(currentX)
909
- const slidePosition = Math.round(totalPosition / itemWidth) - perPage.value
910
- currentSlide.value = slidePosition >= 0
911
- ? slidePosition % innerElements.value.length
912
- : (innerElements.value.length + (slidePosition % innerElements.value.length)) % innerElements.value.length
913
- } else {
914
- // In non-loop mode, just calculate the current slide
915
- const slidePosition = Math.round(Math.abs(currentX) / itemWidth)
916
- currentSlide.value = Math.min(Math.max(slidePosition, 0), innerElements.value.length - perPage.value)
917
- }
918
- } catch (e) {
919
- console.error('Error parsing transform matrix:', e)
920
- }
921
- }
922
-
923
- // Stop the animation immediately
924
- stopAutoplay()
925
- }
926
- }
927
-
928
- function mouseupHandler(e: MouseEvent): void {
929
- if (isBuilding.value || !isMounted.value) { return }
930
-
931
- e.stopPropagation()
932
-
933
- // If we were dragging, prevent clicks by adding a short-lived capture event listener
934
- if (isDragging.value) {
935
- // Add a temporary, one-time capture event listener to block the next click
936
- document.addEventListener('click', function preventClickAfterDrag(event) {
937
- event.preventDefault()
938
- event.stopPropagation()
939
- event.stopImmediatePropagation() // This is crucial to stop other handlers from executing
940
-
941
- // Remove this listener after it's been triggered once
942
- document.removeEventListener('click', preventClickAfterDrag, true)
943
- }, { capture: true, once: true })
944
- }
945
-
946
- pointerDown.value = false
947
- isDragging.value = false
948
-
949
- if (carouselRef.value) {
950
- carouselRef.value.style.cursor = '-webkit-grab'
951
- }
952
-
953
- enableTransition()
954
-
955
- if (drag.value.endX) {
956
- updateAfterDrag()
957
- }
958
-
959
- clearDrag()
960
-
961
- // Restart autoplay after mouse interaction if not hovering
962
- if (config.value.autoplay !== 'disabled' && !isHovering.value && isMounted.value) {
963
- startAutoplay()
964
- }
965
- }
966
-
967
- function mousemoveHandler(e: MouseEvent): void {
968
- if (isBuilding.value || !isMounted.value) { return }
969
-
970
- e.preventDefault()
971
-
972
- if (pointerDown.value) {
973
- drag.value.endX = e.pageX
974
-
975
- // Calculate movement distance
976
- const dragDistance = Math.abs(drag.value.endX - drag.value.startX)
977
-
978
- // If dragged more than the threshold, mark as a real drag operation
979
- if (dragDistance > dragThreshold) {
980
- isDragging.value = true
981
- drag.value.preventClick = true
982
- }
983
-
984
- if (carouselRef.value) {
985
- carouselRef.value.style.cursor = '-webkit-grabbing'
986
- }
987
-
988
- if (sliderFrame.value) {
989
- disableTransition()
990
-
991
- const currentSlideValue = config.value.loop
992
- ? currentSlide.value + perPage.value
993
- : currentSlide.value
994
-
995
- const currentOffset = currentSlideValue * (selectorWidth.value / perPage.value)
996
- const dragOffset = drag.value.endX - drag.value.startX
997
- const offset = config.value.rtl
998
- ? currentOffset + dragOffset
999
- : currentOffset - dragOffset
1000
-
1001
- sliderFrame.value.style[transformProperty.value as any]
1002
- = `translate3d(${(config.value.rtl ? 1 : -1) * offset}px, 0, 0)`
1003
- }
1004
- }
1005
- }
1006
-
1007
- function mouseleaveHandler(e: MouseEvent): void {
1008
- if (isBuilding.value || !isMounted.value) { return }
1009
-
1010
- if (pointerDown.value) {
1011
- pointerDown.value = false
1012
-
1013
- if (carouselRef.value) {
1014
- carouselRef.value.style.cursor = '-webkit-grab'
1015
- }
1016
-
1017
- drag.value.endX = e.pageX
1018
- drag.value.preventClick = false
1019
- enableTransition()
1020
- updateAfterDrag()
1021
- clearDrag()
1022
- }
1023
- }
1024
-
1025
- function clickHandler(e: MouseEvent): void {
1026
- if (isBuilding.value || !isMounted.value) { return }
1027
-
1028
- if (drag.value.preventClick || isDragging.value) {
1029
- e.preventDefault()
1030
- e.stopPropagation()
1031
- e.stopImmediatePropagation() // This stops other handlers from firing
1032
- drag.value.preventClick = false
1033
- }
1034
-
1035
- drag.value.preventClick = false
1036
- }
1037
-
1038
- // Add this new function to handle dragstart events
1039
- function dragstartHandler(e: DragEvent): void {
1040
- if (isBuilding.value || !isMounted.value) { return } // Prevent interaction during build
1041
-
1042
- // Prevent default drag behavior for images and links
1043
- if (config.value.draggable) {
1044
- const target = e.target as HTMLElement
1045
- const isDraggableElement = target.tagName === 'IMG'
1046
- || target.tagName === 'A'
1047
- || target.parentElement?.tagName === 'A'
1048
-
1049
- if (isDraggableElement) {
1050
- e.preventDefault()
1051
- }
1052
- }
1053
- }
1054
-
1055
- // Public API
1056
- function remove(index: number): void {
1057
- if (index < 0 || index >= innerElements.value.length) {
1058
- throw new Error('Item to remove doesn\'t exist 😭')
1059
- }
1060
-
1061
- // Shift sliderFrame back by one if needed
1062
- const lowerIndex = index < currentSlide.value
1063
- const lastItem = currentSlide.value + perPage.value - 1 === index
1064
-
1065
- if (lowerIndex || lastItem) {
1066
- currentSlide.value--
1067
- }
1068
-
1069
- innerElements.value.splice(index, 1)
1070
- buildSliderFrame()
1071
- }
1072
-
1073
- function insert(item: Element, index: number): void {
1074
- if (index < 0 || index > innerElements.value.length + 1) {
1075
- throw new Error('Unable to insert at this index 😭')
1076
- }
1077
-
1078
- if (innerElements.value.includes(item)) {
1079
- throw new Error('The same item in a carousel? Really? Nope 😭')
1080
- }
1081
-
1082
- // Avoid shifting content
1083
- const shouldItShift = index <= currentSlide.value && innerElements.value.length > 0
1084
-
1085
- if (shouldItShift) {
1086
- currentSlide.value++
1087
- }
1088
-
1089
- innerElements.value.splice(index, 0, item)
1090
- buildSliderFrame()
1091
- }
1092
-
1093
- function prepend(item: Element): void {
1094
- insert(item, 0)
1095
- }
1096
-
1097
- function append(item: Element): void {
1098
- insert(item, innerElements.value.length)
1099
- }
1100
-
1101
- function destroy(restoreMarkup: boolean = false): void {
1102
- detachEvents()
1103
-
1104
- if (carouselRef.value) {
1105
- carouselRef.value.style.cursor = 'auto'
1106
-
1107
- if (restoreMarkup) {
1108
- const slides = document.createDocumentFragment()
1109
-
1110
- for (let i = 0; i < innerElements.value.length; i++) {
1111
- slides.appendChild(innerElements.value[i])
1112
- }
1113
-
1114
- carouselRef.value.innerHTML = ''
1115
- carouselRef.value.appendChild(slides)
1116
- carouselRef.value.removeAttribute('style')
1117
- }
1118
- }
1119
- }
1120
-
1121
- // Add after init()
1122
- async function startAutoplay(): Promise<void> {
1123
- if (!isMounted.value) { return } // Don't start if not mounted
1124
-
1125
- stopAutoplay() // Clear any existing timers first
1126
-
1127
- if (config.value.autoplay === 'standard') {
1128
- autoplayTimer.value = window.setInterval(() => {
1129
- // Check if component is still mounted before advancing
1130
- if (isMounted.value && !isHovering.value && !isBuilding.value) {
1131
- next()
1132
- }
1133
- }, config.value.autoplayInterval)
1134
- } else if (config.value.autoplay === 'linear') {
1135
- startLinearAutoplay()
1136
- }
1137
- }
1138
-
1139
- function stopAutoplay(): void {
1140
- if (autoplayTimer.value !== null) {
1141
- clearInterval(autoplayTimer.value)
1142
- autoplayTimer.value = null
1143
- }
1144
-
1145
- if (linearAnimationFrame.value !== null) {
1146
- cancelAnimationFrame(linearAnimationFrame.value)
1147
- linearAnimationFrame.value = null
1148
- }
1149
- }
1150
-
1151
- function startLinearAutoplay(): void {
1152
- if (!isMounted.value || !innerElements.value.length) { return }
1153
-
1154
- // Reset the linear offset to the current slide position
1155
- const itemWidth = selectorWidth.value / perPage.value
1156
- linearOffset.value = currentSlide.value * itemWidth
1157
- let lastTimestamp = 0
1158
-
1159
- // Animation function
1160
- const animate = (timestamp: number) => {
1161
- // Always verify that the component is still mounted
1162
- if (!isMounted.value || !sliderFrame.value) {
1163
- stopAutoplay()
1164
- return
1165
- }
1166
-
1167
- if (!lastTimestamp) { lastTimestamp = timestamp }
1168
-
1169
- // Calculate time delta and convert to pixels based on speed
1170
- const elapsed = timestamp - lastTimestamp
1171
- lastTimestamp = timestamp
1172
-
1173
- // If hovering or not visible, just continue the animation loop without changing position
1174
- if (isHovering.value) {
1175
- linearAnimationFrame.value = requestAnimationFrame(animate)
1176
- return
1177
- }
1178
-
1179
- // Calculate how many pixels to move based on time elapsed and speed
1180
- const pixelsToMove = (elapsed / 1000) * config.value.autoplaySpeed
1181
- linearOffset.value += pixelsToMove
1182
-
1183
- // Calculate total width of all items
1184
- const totalWidth = itemWidth * innerElements.value.length
1185
-
1186
- // If we've scrolled past the entire content, reset
1187
- if (linearOffset.value >= totalWidth) {
1188
- if (config.value.loop) {
1189
- linearOffset.value = 0
1190
- } else {
1191
- // If not looping, stop at the end
1192
- linearOffset.value = totalWidth - itemWidth
1193
- stopAutoplay()
1194
- }
1195
- }
1196
-
1197
- // Apply the offset using transforms
1198
- if (sliderFrame.value && !isBuilding.value) {
1199
- const direction = config.value.rtl ? 1 : -1
1200
- sliderFrame.value.style.transition = 'none' // Smooth scrolling, no easing
1201
-
1202
- // In loop mode, we need to offset by perPage
1203
- let displayOffset = linearOffset.value
1204
- if (config.value.loop) {
1205
- displayOffset = linearOffset.value + (perPage.value * itemWidth)
1206
- }
1207
-
1208
- sliderFrame.value.style[transformProperty.value as any]
1209
- = `translate3d(${direction * displayOffset}px, 0, 0)`
1210
- }
1211
-
1212
- // Update current slide based on the offset
1213
- const newSlide = Math.floor(linearOffset.value / itemWidth) % innerElements.value.length
1214
- if (newSlide !== currentSlide.value) {
1215
- currentSlide.value = newSlide
1216
- config.value.onChange()
1217
- }
1218
-
1219
- // Continue animation loop only if mounted
1220
- if (isMounted.value) {
1221
- linearAnimationFrame.value = requestAnimationFrame(animate)
1222
- }
1223
- }
1224
-
1225
- // Start the animation only if mounted
1226
- if (isMounted.value) {
1227
- linearAnimationFrame.value = requestAnimationFrame(animate)
1228
- }
1229
- }
1230
-
1231
- // Event handlers for autoplay pausing
1232
- function mouseenterHandler(): void {
1233
- isHovering.value = true
1234
- }
1235
-
1236
- function mouseleaveAutoplayHandler(): void {
1237
- isHovering.value = false
1238
- }
1239
-
1240
- // Add new public methods for autoplay control
1241
- function pauseAutoplay(): void {
1242
- stopAutoplay()
1243
- }
1244
-
1245
- function resumeAutoplay(): void {
1246
- if (config.value.autoplay !== 'disabled') {
1247
- startAutoplay()
1248
- }
1249
- }
1250
-
1251
- // Add function to set up the MutationObserver
1252
- function setupSlotObserver(): void {
1253
- if (!slotContainer.value || slotObserver.value) { return }
1254
-
1255
- // Create a new MutationObserver
1256
- slotObserver.value = new MutationObserver((mutations) => {
1257
- // Skip if building or over rebuild limit
1258
- if (isBuilding.value || !isMounted.value
1259
- || emptyRebuildAttempts.value >= MAX_EMPTY_REBUILD_ATTEMPTS) {
1260
- return
1261
- }
1262
-
1263
- let needsRebuild = false
1264
-
1265
- for (const mutation of mutations) {
1266
- if (mutation.type === 'childList') {
1267
- needsRebuild = true
1268
- break
1269
- }
1270
- }
1271
-
1272
- if (needsRebuild) {
1273
- nextTick(() => {
1274
- if (isMounted.value && slotContainer.value) {
1275
- // Update by cloning the new content
1276
- innerElements.value = Array.from(slotContainer.value.children).map(
1277
- el => el.cloneNode(true) as Element
1278
- )
1279
-
1280
- updateSlider()
1281
- }
1282
- })
1283
- }
1284
- })
1285
-
1286
- // Start observing
1287
- slotObserver.value.observe(slotContainer.value, {
1288
- childList: true,
1289
- subtree: false,
1290
- attributes: false,
1291
- characterData: false
1292
- })
1293
- }
1294
-
1295
- // Modify updateSlider to safely handle DOM references and always use slotContainer
1296
- function updateSlider(): void {
1297
- if (!carouselRef.value || !slotContainer.value || isBuilding.value || !isMounted.value) { return }
1298
-
1299
- // Prevent too many empty rebuild attempts
1300
- if (emptyRebuildAttempts.value >= MAX_EMPTY_REBUILD_ATTEMPTS) {
1301
- console.warn('Too many rebuild attempts with no items, stopping')
1302
- return
1303
- }
1304
-
1305
- try {
1306
- // Temporarily disconnect observer to avoid recursive calls
1307
- if (slotObserver.value) {
1308
- slotObserver.value.disconnect()
1309
- }
1310
-
1311
- // Clone the slot content always
1312
- const slotElements = Array.from(slotContainer.value.children)
1313
- .map(el => el.cloneNode(true) as Element)
1314
-
1315
- // Only proceed if we have elements to work with or if we need to clear for the first time
1316
- if (slotElements.length > 0 || (innerElements.value.length > 0 && emptyRebuildAttempts.value === 0)) {
1317
- // Update elements with clones of slot content
1318
- innerElements.value = slotElements
1319
-
1320
- // Adjust current slide if needed
1321
- if (currentSlide.value >= innerElements.value.length) {
1322
- currentSlide.value = Math.max(0, innerElements.value.length - 1)
1323
- }
1324
- // Stop autoplay during rebuild to prevent errors
1325
- const wasAutoplay = config.value.autoplay !== 'disabled'
1326
- if (wasAutoplay) {
1327
- stopAutoplay()
1328
- }
1329
-
1330
- // Rebuild the slider frame
1331
- buildSliderFrame()
1332
-
1333
- // Resume autoplay if it was active and not hovering
1334
- if (wasAutoplay && !isHovering.value && innerElements.value.length > 0) {
1335
- // Small delay for stability
1336
- setTimeout(() => {
1337
- startAutoplay()
1338
- }, 50)
1339
- }
1340
- }
1341
- } catch (error) {
1342
- console.error('Error updating slider:', error)
1343
- } finally {
1344
- // Always reconnect observer to the slot container
1345
- if (slotObserver.value && slotContainer.value) {
1346
- slotObserver.value.observe(slotContainer.value, {
1347
- childList: true,
1348
- subtree: false,
1349
- attributes: false,
1350
- characterData: false
1351
- })
1352
- }
1353
- }
1354
- }
1355
-
1356
- // Update resetSlider to use slotContainer instead of carouselRef
1357
- function resetSlider(): void {
1358
- // Reset the counter to allow rebuilding again
1359
- emptyRebuildAttempts.value = 0
1360
-
1361
- // Check if we have content now
1362
- if (slotContainer.value) {
1363
- const slotElements = Array.from(slotContainer.value.children)
1364
- .map(el => el.cloneNode(true) as Element)
1365
-
1366
- if (slotElements.length > 0) {
1367
- // We have content now, rebuild
1368
- innerElements.value = slotElements
1369
- nextTick(() => { buildSliderFrame() })
1370
- }
1371
- }
1372
- }
1373
-
1374
- // Expose public methods
1375
- defineExpose({
1376
- prev,
1377
- next,
1378
- goTo,
1379
- remove,
1380
- insert,
1381
- prepend,
1382
- append,
1383
- destroy,
1384
- currentSlide,
1385
- pauseAutoplay,
1386
- resumeAutoplay,
1387
- updateSlider,
1388
- resetSlider,
1389
- })
1390
- </script>
1391
-
1392
- <template>
1393
- <div class="carousel-wrapper">
1394
- <!-- Hidden container to hold the original slot content -->
1395
- <div ref="slotContainer" class="carousel-slot-container" style="display: none;">
1396
- <slot />
1397
- </div>
1398
-
1399
- <!-- Visible carousel container that we will manually populate -->
1400
- <div ref="carouselRef" class="carousel-container">
1401
- <!-- We'll populate this with cloned content programmatically -->
1402
- </div>
1403
- <!-- Dots navigation (Vue-managed) -->
1404
- <div v-if="props.dots && totalDots > 1" class="carousel-dots">
1405
- <button
1406
- v-for="i in totalDots" :key="i" type="button" class="carousel-dot"
1407
- :class="[{ active: (i - 1) === currentSlide }]" :aria-label="`Go to slide ${i}`" @click="goTo(i - 1)"
1408
- />
1409
- </div>
1410
- </div>
1411
- </template>
1412
-
1413
- <style scoped>
1414
- .carousel-wrapper {
1415
- position: relative;
1416
- width: 100%;
1417
- }
1418
-
1419
- .carousel-container {
1420
- margin: 0 auto;
1421
- overflow: hidden;
1422
- }
1423
-
1424
- .carousel-dots {
1425
- display: flex;
1426
- justify-content: center;
1427
- gap: 8px;
1428
- margin-top: 16px;
1429
- }
1430
-
1431
- .carousel-dot {
1432
- width: 12px;
1433
- height: 12px;
1434
- border-radius: 50px;
1435
- background-color: var(--bgl-gray-light);
1436
- border: none;
1437
- padding: 0;
1438
- cursor: pointer;
1439
- transition: all 0.3s ease-in-out;
1440
- }
1441
-
1442
- .carousel-dot.active {
1443
- background-color: var(--bgl-primary);
1444
- width: 26px;
1445
- }
1446
- </style>