UNPKG

vue3-carousel

Version:

A simple carousel component for Vue 3

1 lines 112 kB
{"version":3,"file":"carousel.cjs","sources":["../src/shared/constants.ts","../src/shared/injectSymbols.ts","../src/shared/slideRegistry.ts","../src/utils/calculateAverage.ts","../src/utils/createCloneSlides.ts","../src/utils/disableChildrenTabbing.ts","../src/utils/except.ts","../src/utils/getDraggedSlidesCount.ts","../src/utils/getNumberInRange.ts","../src/utils/getScaleMultipliers.ts","../src/utils/getSnapAlignOffset.ts","../src/utils/i18nFormatter.ts","../src/utils/mapNumberToRange.ts","../src/utils/throttle.ts","../src/utils/toCssValue.ts","../src/components/ARIA/ARIA.ts","../src/composables/useDrag.ts","../src/composables/useHover.ts","../src/composables/useWheel.ts","../src/components/Carousel/carouselProps.ts","../src/components/Carousel/Carousel.ts","../src/components/Icon/Icon.types.ts","../src/components/Icon/Icon.ts","../src/components/Navigation/Navigation.ts","../src/components/Pagination/Pagination.ts","../src/components/Slide/Slide.ts"],"sourcesContent":["import { CarouselConfig } from './types'\n\nexport const BREAKPOINT_MODE_OPTIONS = ['viewport', 'carousel'] as const\n\nexport const DIR_MAP = {\n 'bottom-to-top': 'btt',\n 'left-to-right': 'ltr',\n 'right-to-left': 'rtl',\n 'top-to-bottom': 'ttb',\n} as const\n\nexport const DIR_OPTIONS = [\n 'ltr',\n 'left-to-right',\n 'rtl',\n 'right-to-left',\n 'ttb',\n 'top-to-bottom',\n 'btt',\n 'bottom-to-top',\n] as const\n\nexport const I18N_DEFAULT_CONFIG = {\n ariaGallery: 'Gallery',\n ariaNavigateToPage: 'Navigate to page {slideNumber}',\n ariaNavigateToSlide: 'Navigate to slide {slideNumber}',\n ariaNextSlide: 'Navigate to next slide',\n ariaPreviousSlide: 'Navigate to previous slide',\n iconArrowDown: 'Arrow pointing downwards',\n iconArrowLeft: 'Arrow pointing to the left',\n iconArrowRight: 'Arrow pointing to the right',\n iconArrowUp: 'Arrow pointing upwards',\n itemXofY: 'Item {currentSlide} of {slidesCount}',\n} as const\n\nexport const NORMALIZED_DIR_OPTIONS = Object.values(DIR_MAP)\n\nexport const SLIDE_EFFECTS = ['slide', 'fade'] as const\n\nexport const SNAP_ALIGN_OPTIONS = [\n 'center',\n 'start',\n 'end',\n 'center-even',\n 'center-odd',\n] as const\n\nexport const DEFAULT_MOUSE_WHEEL_THRESHOLD = 10\nexport const DEFAULT_DRAG_THRESHOLD = 0.3\n\nexport const DEFAULT_CONFIG: CarouselConfig = {\n autoplay: 0,\n breakpointMode: BREAKPOINT_MODE_OPTIONS[0],\n breakpoints: undefined,\n dir: DIR_OPTIONS[0],\n enabled: true,\n gap: 0,\n height: 'auto',\n i18n: I18N_DEFAULT_CONFIG,\n ignoreAnimations: false,\n itemsToScroll: 1,\n itemsToShow: 1,\n modelValue: 0,\n mouseDrag: true,\n mouseWheel: false,\n pauseAutoplayOnHover: false,\n preventExcessiveDragging: false,\n slideEffect: SLIDE_EFFECTS[0],\n snapAlign: SNAP_ALIGN_OPTIONS[0],\n touchDrag: true,\n transition: 300,\n wrapAround: false,\n}\n","import { InjectionKey } from 'vue'\n\nimport { InjectedCarousel } from '@/components/Carousel'\n\n// Use a symbol for inject provide to avoid any kind of collision with another lib\n// https://vuejs.org/guide/components/provide-inject#working-with-symbol-keys\nexport const injectCarousel = Symbol('carousel') as InjectionKey<\n InjectedCarousel | undefined\n>\n","import { ComponentInternalInstance, EmitFn, shallowReactive } from 'vue'\n\nconst createSlideRegistry = (emit: EmitFn) => {\n const slides = shallowReactive<Array<ComponentInternalInstance>>([])\n\n const updateSlideIndexes = (startIndex?: number) => {\n if (startIndex !== undefined) {\n slides.slice(startIndex).forEach((slide, offset) => {\n slide.exposed?.setIndex(startIndex + offset)\n })\n } else {\n slides.forEach((slide, index) => {\n slide.exposed?.setIndex(index)\n })\n }\n }\n\n return {\n cleanup: () => {\n slides.splice(0, slides.length)\n },\n\n getSlides: () => slides,\n\n registerSlide: (slide: ComponentInternalInstance, index?: number) => {\n if (!slide) return\n\n if (slide.props.isClone) {\n return\n }\n\n const slideIndex = index ?? slides.length\n slides.splice(slideIndex, 0, slide)\n updateSlideIndexes(slideIndex)\n emit('slide-registered', { slide, index: slideIndex })\n },\n\n unregisterSlide: (slide: ComponentInternalInstance) => {\n const slideIndex = slides.indexOf(slide)\n if (slideIndex === -1) return\n\n emit('slide-unregistered', { slide, index: slideIndex })\n\n slides.splice(slideIndex, 1)\n updateSlideIndexes(slideIndex)\n },\n }\n}\n\nexport type SlideRegistry = ReturnType<typeof createSlideRegistry>\n\nexport { createSlideRegistry }\n","export function calculateAverage(numbers: number[]) {\n if (numbers.length === 0) return 0\n const sum = numbers.reduce((acc, num) => acc + num, 0)\n return sum / numbers.length\n}\n","import { cloneVNode, ComponentInternalInstance, VNode } from 'vue'\n\ntype CreateCloneSlidesArgs = {\n slides: Array<ComponentInternalInstance>\n position: 'before' | 'after'\n toShow: number\n}\n\nexport function createCloneSlides({ slides, position, toShow }: CreateCloneSlidesArgs) {\n const clones: VNode[] = []\n const isBefore = position === 'before'\n const start = isBefore ? -toShow : 0\n const end = isBefore ? 0 : toShow\n\n if (slides.length <= 0) {\n return clones\n }\n\n for (let i = start; i < end; i++) {\n const index = isBefore ? i : i + slides.length\n const props = {\n index,\n isClone: true,\n id: undefined, // Make sure we don't duplicate the id which would be invalid html\n key: `clone-${position}-${i}`,\n }\n const vnode = slides[((i % slides.length) + slides.length) % slides.length].vnode\n const clone = cloneVNode(vnode, props)\n clone.el = null\n clones.push(clone)\n }\n\n return clones\n}\n","import { VNode } from 'vue'\n\nconst FOCUSABLE_ELEMENTS_SELECTOR =\n 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex=\"-1\"])'\n\n/**\n * Disables keyboard tab navigation for all focusable child elements\n * @param node Vue virtual node containing the elements to disable\n */\nexport function disableChildrenTabbing(node: VNode) {\n if (!node.el || !(node.el instanceof Element)) {\n return\n }\n\n const elements = node.el.querySelectorAll(FOCUSABLE_ELEMENTS_SELECTOR)\n\n for (const el of elements) {\n if (\n el instanceof HTMLElement &&\n !el.hasAttribute('disabled') &&\n el.getAttribute('aria-hidden') !== 'true'\n ) {\n el.setAttribute('tabindex', '-1')\n }\n }\n}\n","\n/** Useful function to destructure props without triggering reactivity for certain keys */\nexport function except<Obj extends object, Keys extends string>(obj: Obj, keys: Keys[]): Omit<Obj, Keys> {\n return (Object.keys(obj).filter((k) => !(keys as string[]).includes(k)) as Array<Exclude<keyof Obj, Keys>>)\n .reduce((acc, key) => (acc[key] = obj[key], acc), {} as Omit<Obj, Keys>)\n}","type DragParams = {\n isVertical: boolean\n isReversed: boolean\n dragged: { x: number; y: number }\n effectiveSlideSize: number\n threshold: number\n}\n\n/**\n * Calculates the number of slides to move based on drag movement\n * @param params Configuration parameters for drag calculation\n * @returns Number of slides to move (positive or negative)\n */\nexport function getDraggedSlidesCount(params: DragParams): number {\n const { isVertical, isReversed, dragged, effectiveSlideSize, threshold } = params\n\n // Get drag value based on direction\n const dragValue = isVertical ? dragged.y : dragged.x\n\n // If no drag, return +0 explicitly\n if (dragValue === 0) return 0\n\n const dragRatio = dragValue / effectiveSlideSize\n const absRatio = Math.abs(dragRatio)\n\n // If below the threshold, consider it no movement\n if (absRatio < threshold) return 0\n \n // For drags less than a full slide, move one slide in the drag direction\n // For drags of a full slide or more, move the corresponding number of slides\n const slidesDragged = absRatio < 1 ? Math.sign(dragRatio) : Math.round(dragRatio)\n\n return isReversed ? slidesDragged : -slidesDragged\n}\n","type GetNumberInRangeArgs = {\n val: number\n max: number\n min: number\n}\n\nexport function getNumberInRange({ val, max, min }: GetNumberInRangeArgs): number {\n if (max < min) {\n return val\n }\n return Math.min(Math.max(val, isNaN(min) ? val : min), isNaN(max) ? val : max)\n}\n","export function getTransformValues(el: HTMLElement) {\n const { transform } = window.getComputedStyle(el)\n\n //add sanity check\n return transform\n .split(/[(,)]/)\n .slice(1, -1)\n .map((v) => parseFloat(v))\n}\n\nexport type ScaleMultipliers = {\n widthMultiplier: number\n heightMultiplier: number\n}\nexport function getScaleMultipliers(\n transformElements: Set<HTMLElement>\n): ScaleMultipliers {\n let widthMultiplier = 1\n let heightMultiplier = 1\n transformElements.forEach((el) => {\n const transformArr = getTransformValues(el)\n\n if (transformArr.length === 6) {\n widthMultiplier /= transformArr[0]\n heightMultiplier /= transformArr[3]\n }\n })\n\n return { widthMultiplier, heightMultiplier }\n}\n","import { SnapAlign } from '@/shared'\n\ntype SnapAlignOffsetParams = {\n align: SnapAlign\n slideSize?: number\n viewportSize?: number\n itemsToShow?: number\n}\n\n/**\n * Calculates the snap align offset for a carousel item based on items to show.\n * Returns the number of slides to offset.\n *\n * @param align - The alignment type.\n * @param itemsToShow - The number of items to show.\n * @returns The calculated offset.\n */\nfunction getSnapAlignOffsetByItemsToShow(align: SnapAlign, itemsToShow: number): number {\n switch (align) {\n case 'start':\n return 0\n case 'center':\n case 'center-odd':\n return (itemsToShow - 1) / 2\n case 'center-even':\n return (itemsToShow - 2) / 2\n case 'end':\n return itemsToShow - 1\n default:\n return 0\n }\n}\n\n/**\n * Calculates the snap align offset for a carousel item based on slide and viewport size.\n * Returns the real width to offset.\n *\n * @param align - The alignment type.\n * @param slideSize - The size of the slide.\n * @param viewportSize - The size of the viewport.\n * @returns The calculated offset.\n */\nfunction getSnapAlignOffsetBySlideAndViewport(\n align: SnapAlign,\n slideSize: number,\n viewportSize: number\n): number {\n switch (align) {\n case 'start':\n return 0\n case 'center':\n case 'center-odd':\n return (viewportSize - slideSize) / 2\n case 'center-even':\n return viewportSize / 2 - slideSize\n case 'end':\n return viewportSize - slideSize\n default:\n return 0\n }\n}\n\n/**\n * Calculates the snap align offset for a carousel item.\n *\n * @param params - The parameters for calculating the offset.\n * @returns The calculated offset.\n */\nexport function getSnapAlignOffset({\n slideSize,\n viewportSize,\n align,\n itemsToShow,\n}: SnapAlignOffsetParams): number {\n if (itemsToShow !== undefined) {\n return getSnapAlignOffsetByItemsToShow(align, itemsToShow)\n }\n if (slideSize !== undefined && viewportSize !== undefined) {\n return getSnapAlignOffsetBySlideAndViewport(align, slideSize, viewportSize)\n }\n\n return 0\n}\n","export function i18nFormatter(string = '', values = {}) {\n return Object.entries(values).reduce(\n (acc, [key, value]) => acc.replace(`{${key}}`, String(value)),\n string\n )\n}\n","type MapNumberToRangeArgs = {\n val: number\n max: number\n min?: number\n}\n\nexport function mapNumberToRange({ val, max, min = 0 }: MapNumberToRangeArgs): number {\n const mod = max - min + 1\n return ((((val - min) % mod) + mod) % mod) + min\n}\n","/**\n * Returns a throttled version of the function using requestAnimationFrame.\n *\n * @param fn - The function to throttle.\n * @param ms - The number of milliseconds to wait for the throttled function to be called again\n */\nexport function throttle<Args extends Array<unknown>>(\n fn: (...args: Args) => void,\n ms = 0\n): { (...args: Args): void; cancel: () => void } {\n let isThrottled = false\n let start = 0\n let frameId: number | null = null\n\n function throttled(...args: Args) {\n if (isThrottled) return\n\n isThrottled = true\n const step = () => {\n frameId = requestAnimationFrame((time) => {\n const elapsed = time - start\n if (elapsed > ms) {\n start = time\n fn(...args)\n isThrottled = false\n } else {\n step()\n }\n })\n }\n step()\n }\n\n throttled.cancel = () => {\n if (frameId) {\n cancelAnimationFrame(frameId)\n frameId = null\n isThrottled = false\n }\n }\n\n return throttled\n}\n","/**\n * Converts a value to a CSS-compatible string.\n * @param value - The value to convert.\n * @returns The CSS-compatible string.\n **/\nexport function toCssValue(\n value?: string | number,\n unit: string = 'px'\n): string | undefined {\n if (value === null || value === undefined || value === '') {\n return undefined\n }\n\n if (typeof value === 'number' || parseFloat(value).toString() === value) {\n return `${value}${unit}`\n }\n return value\n}\n","import { defineComponent, h, inject } from 'vue'\n\nimport { injectCarousel } from '@/shared'\nimport { i18nFormatter } from '@/utils'\n\nexport const ARIA = defineComponent({\n name: 'CarouselAria',\n setup() {\n const carousel = inject(injectCarousel)\n\n if (!carousel) {\n return () => ''\n }\n\n return () =>\n h(\n 'div',\n {\n class: ['carousel__liveregion', 'carousel__sr-only'],\n 'aria-live': 'polite',\n 'aria-atomic': 'true',\n },\n i18nFormatter(carousel.config.i18n['itemXofY'], {\n currentSlide: carousel.currentSlide + 1,\n slidesCount: carousel.slidesCount,\n })\n )\n },\n})\n","import { ref, reactive, computed, Ref } from 'vue'\n\nimport { throttle } from '@/utils'\n\nexport type DragEventData = {\n deltaX: number\n deltaY: number\n isTouch: boolean\n}\nexport interface UseDragOptions {\n isSliding: boolean | Ref<boolean>\n onDrag?: ({ deltaX, deltaY, isTouch }: DragEventData) => void\n onDragStart?: () => void\n onDragEnd?: () => void\n}\n\nexport function useDrag(options: UseDragOptions) {\n let isTouch = false\n const startPosition = { x: 0, y: 0 }\n const dragged = reactive({ x: 0, y: 0 })\n const isDragging = ref(false)\n\n const { isSliding } = options\n\n const sliding = computed(() => {\n return typeof isSliding === 'boolean' ? isSliding : isSliding.value\n })\n\n const handleDragStart = (event: MouseEvent | TouchEvent): void => {\n // Prevent drag initiation on input elements or if already sliding\n const targetTagName = (event.target as HTMLElement).tagName\n if (['INPUT', 'TEXTAREA', 'SELECT'].includes(targetTagName) || sliding.value) {\n return\n }\n\n isTouch = event.type === 'touchstart'\n\n if (!isTouch) {\n event.preventDefault()\n if ((event as MouseEvent).button !== 0) {\n return\n }\n }\n\n startPosition.x = isTouch\n ? (event as TouchEvent).touches[0].clientX\n : (event as MouseEvent).clientX\n startPosition.y = isTouch\n ? (event as TouchEvent).touches[0].clientY\n : (event as MouseEvent).clientY\n\n const moveEvent = isTouch ? 'touchmove' : 'mousemove'\n const endEvent = isTouch ? 'touchend' : 'mouseup'\n document.addEventListener(moveEvent, handleDrag, { passive: false })\n document.addEventListener(endEvent, handleDragEnd, { passive: true })\n\n options.onDragStart?.()\n }\n\n const handleDrag = throttle((event: TouchEvent | MouseEvent): void => {\n isDragging.value = true\n\n const currentX = isTouch\n ? (event as TouchEvent).touches[0].clientX\n : (event as MouseEvent).clientX\n const currentY = isTouch\n ? (event as TouchEvent).touches[0].clientY\n : (event as MouseEvent).clientY\n\n dragged.x = currentX - startPosition.x\n dragged.y = currentY - startPosition.y\n\n options.onDrag?.({ deltaX: dragged.x, deltaY: dragged.y, isTouch })\n })\n\n const handleDragEnd = (): void => {\n handleDrag.cancel()\n\n if (!isTouch) {\n const preventClick = (e: MouseEvent) => {\n e.preventDefault()\n window.removeEventListener('click', preventClick)\n }\n window.addEventListener('click', preventClick)\n }\n\n options.onDragEnd?.()\n\n dragged.x = 0\n dragged.y = 0\n isDragging.value = false\n\n const moveEvent = isTouch ? 'touchmove' : 'mousemove'\n const endEvent = isTouch ? 'touchend' : 'mouseup'\n document.removeEventListener(moveEvent, handleDrag)\n document.removeEventListener(endEvent, handleDragEnd)\n }\n\n return {\n dragged,\n isDragging,\n handleDragStart,\n }\n}\n","import { ref } from 'vue'\n\nexport function useHover() {\n const isHover = ref(false)\n\n const handleMouseEnter = (): void => {\n isHover.value = true\n }\n\n const handleMouseLeave = (): void => {\n isHover.value = false\n }\n\n return {\n isHover,\n handleMouseEnter,\n handleMouseLeave,\n }\n}\n","import { ComputedRef, Ref, computed } from 'vue'\n\nimport { CarouselConfig } from '@/shared'\nimport { DEFAULT_MOUSE_WHEEL_THRESHOLD } from '@/shared/constants'\n\nexport type WheelEventData = {\n deltaX: number\n deltaY: number\n isScrollingForward: boolean\n}\n\nexport type UseWheelOptions = {\n isVertical: boolean | ComputedRef<boolean>\n isSliding: boolean | Ref<boolean>\n config: CarouselConfig\n onWheel?: (data: WheelEventData) => void\n}\n\nexport function useWheel(options: UseWheelOptions) {\n const { isVertical, isSliding, config } = options\n\n // Create computed values to handle both reactive and non-reactive inputs\n const vertical = computed(() => {\n return typeof isVertical === 'boolean' ? isVertical : isVertical.value\n })\n\n const sliding = computed(() => {\n return typeof isSliding === 'boolean' ? isSliding : isSliding.value\n })\n\n const handleScroll = (event: WheelEvent): void => {\n event.preventDefault()\n\n if (!config.mouseWheel || sliding.value) {\n return\n }\n\n // Add sensitivity threshold to prevent small movements from triggering navigation\n const threshold =\n typeof config.mouseWheel === 'object'\n ? (config.mouseWheel.threshold ?? DEFAULT_MOUSE_WHEEL_THRESHOLD)\n : DEFAULT_MOUSE_WHEEL_THRESHOLD\n\n // Determine scroll direction\n const deltaY = Math.abs(event.deltaY) > threshold ? event.deltaY : 0\n const deltaX = Math.abs(event.deltaX) > threshold ? event.deltaX : 0\n\n // If neither delta exceeds the threshold, don't navigate\n if (deltaY === 0 && deltaX === 0) {\n return\n }\n\n // Determine primary delta based on carousel orientation\n const primaryDelta = vertical.value ? deltaY : deltaX\n\n // If primaryDelta is 0, use the other delta as fallback\n const effectiveDelta =\n primaryDelta !== 0 ? primaryDelta : vertical.value ? deltaX : deltaY\n\n // Positive delta means scrolling down/right\n const isScrollingForward = effectiveDelta > 0\n\n options.onWheel?.({ deltaX, deltaY, isScrollingForward })\n }\n\n return {\n handleScroll,\n }\n}\n","import { PropType } from 'vue'\n\nimport {\n BREAKPOINT_MODE_OPTIONS,\n DEFAULT_CONFIG,\n DIR_MAP,\n DIR_OPTIONS,\n SLIDE_EFFECTS,\n SNAP_ALIGN_OPTIONS,\n} from '@/shared'\n\nimport type {\n BreakpointMode,\n CarouselConfig,\n Dir,\n NonNormalizedDir,\n NormalizedDir,\n SlideEffect,\n SnapAlign,\n DragConfig,\n WheelConfig,\n} from '@/shared'\n\nexport const carouselProps = {\n // time to auto advance slides in ms\n autoplay: {\n default: DEFAULT_CONFIG.autoplay,\n type: Number,\n },\n // an object to store breakpoints\n breakpoints: {\n default: DEFAULT_CONFIG.breakpoints,\n type: Object as PropType<CarouselConfig['breakpoints']>,\n },\n // controls the breakpoint mode relative to the carousel container or the viewport\n breakpointMode: {\n default: DEFAULT_CONFIG.breakpointMode,\n validator(value: BreakpointMode) {\n return BREAKPOINT_MODE_OPTIONS.includes(value)\n },\n },\n clamp: {\n type: Boolean,\n },\n // control the direction of the carousel\n dir: {\n type: String as PropType<Dir>,\n default: DEFAULT_CONFIG.dir,\n validator(value: Dir, props: { height?: string }) {\n // The value must match one of these strings\n if (!DIR_OPTIONS.includes(value)) {\n return false\n }\n\n const normalizedDir =\n value in DIR_MAP ? DIR_MAP[value as NonNormalizedDir] : (value as NormalizedDir)\n if (\n ['ttb', 'btt'].includes(normalizedDir) &&\n (!props.height || props.height === 'auto')\n ) {\n console.warn(\n `[vue3-carousel]: The dir \"${value}\" is not supported with height \"auto\".`\n )\n }\n return true\n },\n },\n // enable/disable the carousel component\n enabled: {\n default: DEFAULT_CONFIG.enabled,\n type: Boolean,\n },\n // control the gap between slides\n gap: {\n default: DEFAULT_CONFIG.gap,\n type: Number,\n },\n // set carousel height\n height: {\n default: DEFAULT_CONFIG.height,\n type: [Number, String],\n },\n // aria-labels and additional text labels\n i18n: {\n default: DEFAULT_CONFIG.i18n,\n type: Object as PropType<typeof DEFAULT_CONFIG.i18n>,\n },\n ignoreAnimations: {\n default: false,\n type: [Array, Boolean, String] as PropType<CarouselConfig['ignoreAnimations']>,\n },\n // count of items to be scrolled\n itemsToScroll: {\n default: DEFAULT_CONFIG.itemsToScroll,\n type: Number,\n },\n // count of items to showed per view\n itemsToShow: {\n default: DEFAULT_CONFIG.itemsToShow,\n type: [Number, String],\n },\n // slide number number of initial slide\n modelValue: {\n default: undefined,\n type: Number,\n },\n // toggle mouse dragging\n mouseDrag: {\n default: DEFAULT_CONFIG.mouseDrag,\n type: [Boolean, Object] as PropType<boolean | DragConfig>,\n },\n // toggle mouse wheel scrolling\n mouseWheel: {\n default: DEFAULT_CONFIG.mouseWheel,\n type: [Boolean, Object] as PropType<boolean | WheelConfig>,\n },\n // control mouse scroll threshold\n mouseScrollThreshold: {\n default: DEFAULT_CONFIG.mouseScrollThreshold,\n type: Number,\n },\n pauseAutoplayOnHover: {\n default: DEFAULT_CONFIG.pauseAutoplayOnHover,\n type: Boolean,\n },\n preventExcessiveDragging: {\n default: false,\n type: Boolean,\n validator(value: boolean, props: { wrapAround?: boolean }) {\n if (value && props.wrapAround) {\n console.warn(\n `[vue3-carousel]: \"preventExcessiveDragging\" cannot be used with wrapAround. The setting will be ignored.`\n )\n }\n return true\n },\n },\n slideEffect: {\n type: String as PropType<SlideEffect>,\n default: DEFAULT_CONFIG.slideEffect,\n validator(value: SlideEffect) {\n return SLIDE_EFFECTS.includes(value)\n },\n },\n // control snap position alignment\n snapAlign: {\n default: DEFAULT_CONFIG.snapAlign,\n validator(value: SnapAlign) {\n return SNAP_ALIGN_OPTIONS.includes(value)\n },\n },\n // toggle touch dragging\n touchDrag: {\n default: DEFAULT_CONFIG.touchDrag,\n type: [Boolean, Object] as PropType<boolean | DragConfig>,\n },\n // sliding transition time in ms\n transition: {\n default: DEFAULT_CONFIG.transition,\n type: Number,\n },\n // control infinite scrolling mode\n wrapAround: {\n default: DEFAULT_CONFIG.wrapAround,\n type: Boolean,\n },\n}\n","import {\n ComputedRef,\n computed,\n defineComponent,\n h,\n onBeforeUnmount,\n onMounted,\n provide,\n reactive,\n Ref,\n ref,\n SetupContext,\n shallowReactive,\n watch,\n watchEffect,\n toRefs,\n} from 'vue'\n\nimport { ARIA as ARIAComponent } from '@/components/ARIA'\nimport { DragEventData, useDrag, useHover, useWheel, WheelEventData } from '@/composables'\nimport {\n CarouselConfig,\n createSlideRegistry,\n DEFAULT_CONFIG,\n DEFAULT_DRAG_THRESHOLD,\n DIR_MAP,\n injectCarousel,\n NonNormalizedDir,\n NormalizedDir,\n} from '@/shared'\nimport {\n calculateAverage,\n createCloneSlides,\n except,\n getDraggedSlidesCount,\n getNumberInRange,\n getScaleMultipliers,\n getSnapAlignOffset,\n mapNumberToRange,\n ScaleMultipliers,\n throttle,\n toCssValue,\n} from '@/utils'\n\nimport {\n CarouselData,\n CarouselExposed,\n CarouselNav,\n ElRect,\n InjectedCarousel,\n} from './Carousel.types'\nimport { carouselProps } from './carouselProps'\n\nexport const Carousel = defineComponent({\n name: 'VueCarousel',\n props: carouselProps,\n emits: [\n 'before-init',\n 'drag',\n 'init',\n 'loop',\n 'slide-end',\n 'slide-registered',\n 'slide-start',\n 'slide-unregistered',\n 'update:modelValue',\n 'wheel',\n ],\n setup(props: CarouselConfig, { slots, emit, expose }: SetupContext) {\n const slideRegistry = createSlideRegistry(emit)\n const slides = slideRegistry.getSlides()\n const slidesCount = computed(() => slides.length)\n\n const root: Ref<Element | null> = ref(null)\n const viewport: Ref<Element | null> = ref(null)\n const slideSize: Ref<number> = ref(0)\n\n const fallbackConfig = computed(() => ({\n ...DEFAULT_CONFIG,\n // Avoid reactivity tracking in breakpoints and vModel which would trigger unnecessary updates\n ...except(props, ['breakpoints', 'modelValue']),\n i18n: { ...DEFAULT_CONFIG.i18n, ...props.i18n },\n }))\n\n // current active config\n const config = shallowReactive<CarouselConfig>({ ...fallbackConfig.value })\n\n // slides\n const currentSlideIndex = ref(props.modelValue ?? 0)\n const activeSlideIndex = ref(currentSlideIndex.value)\n\n watch(currentSlideIndex, (val) => (activeSlideIndex.value = val))\n const prevSlideIndex = ref(0)\n const middleSlideIndex = computed(() => Math.ceil((slidesCount.value - 1) / 2))\n const maxSlideIndex = computed(() => slidesCount.value - 1)\n const minSlideIndex = computed(() => 0)\n\n let autoplayTimer: ReturnType<typeof setInterval> | null = null\n let transitionTimer: ReturnType<typeof setTimeout> | null = null\n let resizeObserver: ResizeObserver | null = null\n\n const effectiveSlideSize = computed(() => slideSize.value + config.gap)\n\n const normalizedDir = computed<NormalizedDir>(() => {\n const dir = config.dir || 'ltr'\n return dir in DIR_MAP ? DIR_MAP[dir as NonNormalizedDir] : (dir as NormalizedDir)\n })\n\n const isReversed = computed(() => ['rtl', 'btt'].includes(normalizedDir.value))\n const isVertical = computed(() => ['ttb', 'btt'].includes(normalizedDir.value))\n const isAuto = computed(() => config.itemsToShow === 'auto')\n\n const dimension = computed(() => (isVertical.value ? 'height' : 'width'))\n\n function updateBreakpointsConfig(): void {\n if (!mounted.value) {\n return\n }\n // Determine the width source based on the 'breakpointMode' config\n const widthSource =\n (fallbackConfig.value.breakpointMode === 'carousel'\n ? root.value?.getBoundingClientRect().width\n : typeof window !== 'undefined'\n ? window.innerWidth\n : 0) || 0\n\n const breakpointsArray = Object.keys(props.breakpoints || {})\n .map((key) => Number(key))\n .sort((a, b) => +b - +a)\n\n const newConfig: Partial<CarouselConfig> = {}\n breakpointsArray.some((breakpoint) => {\n if (widthSource >= breakpoint) {\n Object.assign(newConfig, props.breakpoints![breakpoint])\n if (newConfig.i18n) {\n Object.assign(\n newConfig.i18n,\n fallbackConfig.value.i18n,\n props.breakpoints![breakpoint].i18n\n )\n }\n return true\n }\n return false\n })\n\n Object.assign(config, fallbackConfig.value, newConfig)\n\n // Validate itemsToShow\n if (!isAuto.value) {\n config.itemsToShow = getNumberInRange({\n val: Number(config.itemsToShow),\n max: props.clamp ? slidesCount.value : Infinity,\n min: 1,\n })\n }\n }\n\n const handleResize = throttle(() => {\n updateBreakpointsConfig()\n updateSlidesData()\n updateSlideSize()\n })\n\n const transformElements = shallowReactive<Set<HTMLElement>>(new Set())\n\n /**\n * Setup functions\n */\n const slidesRect = ref<Array<ElRect>>([])\n function updateSlidesRectSize({\n widthMultiplier,\n heightMultiplier,\n }: ScaleMultipliers): void {\n slidesRect.value = slides.map((slide) => {\n const rect = slide.exposed?.getBoundingRect()\n return {\n width: rect.width * widthMultiplier,\n height: rect.height * heightMultiplier,\n }\n })\n }\n const viewportRect: Ref<ElRect> = ref({\n width: 0,\n height: 0,\n })\n function updateViewportRectSize({\n widthMultiplier,\n heightMultiplier,\n }: ScaleMultipliers): void {\n const rect = viewport.value?.getBoundingClientRect() || { width: 0, height: 0 }\n viewportRect.value = {\n width: rect.width * widthMultiplier,\n height: rect.height * heightMultiplier,\n }\n }\n\n function updateSlideSize(): void {\n if (!viewport.value) return\n\n const scaleMultipliers = getScaleMultipliers(transformElements)\n\n updateViewportRectSize(scaleMultipliers)\n updateSlidesRectSize(scaleMultipliers)\n\n if (isAuto.value) {\n slideSize.value = calculateAverage(\n slidesRect.value.map((slide) => slide[dimension.value])\n )\n } else {\n const itemsToShow = Number(config.itemsToShow)\n const totalGap = (itemsToShow - 1) * config.gap\n slideSize.value = (viewportRect.value[dimension.value] - totalGap) / itemsToShow\n }\n }\n\n function updateSlidesData(): void {\n if (!config.wrapAround && slidesCount.value > 0) {\n currentSlideIndex.value = getNumberInRange({\n val: currentSlideIndex.value,\n max: maxSlideIndex.value,\n min: minSlideIndex.value,\n })\n }\n }\n\n const ignoreAnimations = computed<false | string[]>(() => {\n if (typeof props.ignoreAnimations === 'string') {\n return props.ignoreAnimations.split(',')\n } else if (Array.isArray(props.ignoreAnimations)) {\n return props.ignoreAnimations\n } else if (!props.ignoreAnimations) {\n return []\n }\n return false\n })\n\n watchEffect(() => updateSlidesData())\n\n watchEffect(() => {\n // Call updateSlideSize when viewport is ready and track deps\n updateSlideSize()\n })\n\n let animationInterval: number\n\n const setAnimationInterval = (event: AnimationEvent) => {\n const target = event.target as HTMLElement\n if (\n !target?.contains(root.value) ||\n (Array.isArray(ignoreAnimations.value) &&\n ignoreAnimations.value.includes(event.animationName))\n ) {\n return\n }\n\n transformElements.add(target)\n if (!animationInterval) {\n const stepAnimation = () => {\n animationInterval = requestAnimationFrame(() => {\n updateSlideSize()\n stepAnimation()\n })\n }\n stepAnimation()\n }\n }\n const finishAnimation = (event: AnimationEvent | TransitionEvent) => {\n const target = event.target as HTMLElement\n if (target) {\n transformElements.delete(target)\n }\n if (animationInterval && transformElements.size === 0) {\n cancelAnimationFrame(animationInterval)\n updateSlideSize()\n }\n }\n\n const mounted = ref(false)\n\n if (typeof document !== 'undefined') {\n watchEffect(() => {\n if (mounted.value && ignoreAnimations.value !== false) {\n document.addEventListener('animationstart', setAnimationInterval)\n document.addEventListener('animationend', finishAnimation)\n } else {\n document.removeEventListener('animationstart', setAnimationInterval)\n document.removeEventListener('animationend', finishAnimation)\n }\n })\n }\n\n onMounted((): void => {\n mounted.value = true\n updateBreakpointsConfig()\n initAutoplay()\n\n if (root.value) {\n resizeObserver = new ResizeObserver(handleResize)\n resizeObserver.observe(root.value)\n }\n\n emit('init')\n })\n\n onBeforeUnmount(() => {\n mounted.value = false\n\n slideRegistry.cleanup()\n\n if (transitionTimer) {\n clearTimeout(transitionTimer)\n }\n if (animationInterval) {\n cancelAnimationFrame(animationInterval)\n }\n if (autoplayTimer) {\n clearInterval(autoplayTimer)\n }\n if (resizeObserver) {\n resizeObserver.disconnect()\n resizeObserver = null\n }\n\n if (typeof document !== 'undefined') {\n handleBlur()\n }\n if (root.value) {\n root.value.removeEventListener('transitionend', updateSlideSize)\n root.value.removeEventListener('animationiteration', updateSlideSize)\n }\n })\n\n /**\n * Carousel Event listeners\n */\n const { isHover, handleMouseEnter, handleMouseLeave } = useHover()\n\n const handleArrowKeys = throttle((event: KeyboardEvent): void => {\n if (event.ctrlKey) return\n switch (event.key) {\n case 'ArrowLeft':\n case 'ArrowUp':\n if (isVertical.value === event.key.endsWith('Up')) {\n if (isReversed.value) {\n next(true)\n } else {\n prev(true)\n }\n }\n break\n case 'ArrowRight':\n case 'ArrowDown':\n if (isVertical.value === event.key.endsWith('Down')) {\n if (isReversed.value) {\n prev(true)\n } else {\n next(true)\n }\n }\n break\n }\n }, 200)\n const handleFocus = (): void => {\n document.addEventListener('keydown', handleArrowKeys)\n }\n const handleBlur = (): void => {\n document.removeEventListener('keydown', handleArrowKeys)\n }\n\n /**\n * Autoplay\n */\n function initAutoplay(): void {\n if (!config.autoplay || config.autoplay <= 0) {\n return\n }\n\n autoplayTimer = setInterval(() => {\n if (config.pauseAutoplayOnHover && isHover.value) {\n return\n }\n\n next()\n }, config.autoplay)\n }\n\n function stopAutoplay(): void {\n if (autoplayTimer) {\n clearInterval(autoplayTimer)\n autoplayTimer = null\n }\n }\n\n function resetAutoplay(): void {\n stopAutoplay()\n initAutoplay()\n }\n\n /**\n * Navigation function\n */\n const isSliding = ref(false)\n\n const onDrag = ({ deltaX, deltaY, isTouch }: DragEventData) => {\n emit('drag', { deltaX, deltaY })\n\n const threshold = isTouch\n ? typeof config.touchDrag === 'object'\n ? (config.touchDrag?.threshold ?? DEFAULT_DRAG_THRESHOLD)\n : DEFAULT_DRAG_THRESHOLD\n : typeof config.mouseDrag === 'object'\n ? (config.mouseDrag?.threshold ?? DEFAULT_DRAG_THRESHOLD)\n : DEFAULT_DRAG_THRESHOLD\n\n const draggedSlides = getDraggedSlidesCount({\n isVertical: isVertical.value,\n isReversed: isReversed.value,\n dragged: { x: deltaX, y: deltaY },\n effectiveSlideSize: effectiveSlideSize.value,\n threshold,\n })\n\n activeSlideIndex.value = config.wrapAround\n ? currentSlideIndex.value + draggedSlides\n : getNumberInRange({\n val: currentSlideIndex.value + draggedSlides,\n max: maxSlideIndex.value,\n min: minSlideIndex.value,\n })\n }\n\n const onDragEnd = () => slideTo(activeSlideIndex.value)\n\n const { dragged, isDragging, handleDragStart } = useDrag({\n isSliding,\n onDrag,\n onDragEnd,\n })\n\n const onWheel = ({ deltaX, deltaY, isScrollingForward }: WheelEventData) => {\n emit('wheel', { deltaX, deltaY })\n\n if (isScrollingForward) {\n // Scrolling down/right\n if (isReversed.value) {\n prev()\n } else {\n next()\n }\n } else {\n // Scrolling up/left\n if (isReversed.value) {\n next()\n } else {\n prev()\n }\n }\n }\n\n const { handleScroll } = useWheel({\n isVertical,\n isSliding,\n config,\n onWheel,\n })\n\n function slideTo(slideIndex: number, skipTransition = false): void {\n if (!skipTransition && isSliding.value) {\n return\n }\n\n let targetIndex = slideIndex\n let mappedIndex = slideIndex\n\n prevSlideIndex.value = currentSlideIndex.value\n\n if (!config.wrapAround) {\n targetIndex = getNumberInRange({\n val: targetIndex,\n max: maxSlideIndex.value,\n min: minSlideIndex.value,\n })\n } else {\n mappedIndex = mapNumberToRange({\n val: targetIndex,\n max: maxSlideIndex.value,\n min: minSlideIndex.value,\n })\n }\n\n emit('slide-start', {\n slidingToIndex: slideIndex,\n currentSlideIndex: currentSlideIndex.value,\n prevSlideIndex: prevSlideIndex.value,\n slidesCount: slidesCount.value,\n })\n\n stopAutoplay()\n isSliding.value = true\n\n currentSlideIndex.value = targetIndex\n if (mappedIndex !== targetIndex) {\n modelWatcher.pause()\n }\n emit('update:modelValue', mappedIndex)\n\n const transitionCallback = (): void => {\n if (config.wrapAround && mappedIndex !== targetIndex) {\n modelWatcher.resume()\n\n currentSlideIndex.value = mappedIndex\n emit('loop', {\n currentSlideIndex: currentSlideIndex.value,\n slidingToIndex: slideIndex,\n })\n }\n\n emit('slide-end', {\n currentSlideIndex: currentSlideIndex.value,\n prevSlideIndex: prevSlideIndex.value,\n slidesCount: slidesCount.value,\n })\n\n isSliding.value = false\n resetAutoplay()\n }\n\n transitionTimer = setTimeout(transitionCallback, config.transition)\n }\n\n function next(skipTransition = false): void {\n slideTo(currentSlideIndex.value + config.itemsToScroll, skipTransition)\n }\n\n function prev(skipTransition = false): void {\n slideTo(currentSlideIndex.value - config.itemsToScroll, skipTransition)\n }\n\n function restartCarousel(): void {\n updateBreakpointsConfig()\n updateSlidesData()\n updateSlideSize()\n resetAutoplay()\n }\n\n // Update the carousel on props change\n watch(\n () => [fallbackConfig.value, props.breakpoints],\n () => updateBreakpointsConfig(),\n { deep: true }\n )\n\n watch(\n () => props.autoplay,\n () => resetAutoplay()\n )\n\n // Handle changing v-model value\n const modelWatcher = watch(\n () => props.modelValue,\n (val) => {\n if (val === currentSlideIndex.value) {\n return\n }\n slideTo(Number(val), true)\n }\n )\n\n // Init carousel\n emit('before-init')\n\n const clonedSlidesCount = computed(() => {\n if (!config.wrapAround) {\n return { before: 0, after: 0 }\n }\n if (isAuto.value) {\n return { before: slides.length, after: slides.length }\n }\n\n const itemsToShow = Number(config.itemsToShow)\n const slidesToClone = Math.ceil(itemsToShow + (config.itemsToScroll - 1))\n const before = slidesToClone - activeSlideIndex.value\n const after = slidesToClone - (slidesCount.value - (activeSlideIndex.value + 1))\n\n return {\n before: Math.max(0, before),\n after: Math.max(0, after),\n }\n })\n\n const clonedSlidesOffset = computed(() => {\n if (!clonedSlidesCount.value.before) {\n return 0\n }\n if (isAuto.value) {\n return (\n slidesRect.value\n .slice(-1 * clonedSlidesCount.value.before)\n .reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) * -1\n )\n }\n\n return clonedSlidesCount.value.before * effectiveSlideSize.value * -1\n })\n\n const snapAlignOffset = computed(() => {\n if (isAuto.value) {\n const slideIndex =\n ((currentSlideIndex.value % slides.length) + slides.length) % slides.length\n return getSnapAlignOffset({\n slideSize: slidesRect.value[slideIndex]?.[dimension.value],\n viewportSize: viewportRect.value[dimension.value],\n align: config.snapAlign,\n })\n }\n\n return getSnapAlignOffset({\n align: config.snapAlign,\n itemsToShow: +config.itemsToShow,\n })\n })\n const scrolledOffset = computed(() => {\n let output = 0\n\n if (isAuto.value) {\n if (currentSlideIndex.value < 0) {\n output =\n slidesRect.value\n .slice(currentSlideIndex.value)\n .reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) * -1\n } else {\n output = slidesRect.value\n .slice(0, currentSlideIndex.value)\n .reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0)\n }\n output -= snapAlignOffset.value\n\n // remove whitespace\n if (!config.wrapAround) {\n const maxSlidingValue =\n slidesRect.value.reduce(\n (acc, slide) => acc + slide[dimension.value] + config.gap,\n 0\n ) -\n viewportRect.value[dimension.value] -\n config.gap\n\n output = getNumberInRange({\n val: output,\n max: maxSlidingValue,\n min: 0,\n })\n }\n } else {\n let scrolledSlides = currentSlideIndex.value - snapAlignOffset.value\n\n // remove whitespace\n if (!config.wrapAround) {\n scrolledSlides = getNumberInRange({\n val: scrolledSlides,\n max: slidesCount.value - +config.itemsToShow,\n min: 0,\n })\n }\n output = scrolledSlides * effectiveSlideSize.value\n }\n\n return output * (isReversed.value ? 1 : -1)\n })\n\n const visibleRange = computed(() => {\n if (!isAuto.value) {\n const base = currentSlideIndex.value - snapAlignOffset.value\n if (config.wrapAround) {\n return {\n min: Math.floor(base),\n max: Math.ceil(base + Number(config.itemsToShow) - 1),\n }\n }\n return {\n min: Math.floor(\n getNumberInRange({\n val: base,\n max: slidesCount.value - Number(config.itemsToShow),\n min: 0,\n })\n ),\n max: Math.ceil(\n getNumberInRange({\n val: base + Number(config.itemsToShow) - 1,\n max: slidesCount.value - 1,\n min: 0,\n })\n ),\n }\n }\n\n // Auto width mode\n let minIndex = 0\n {\n let accumulatedSize = 0\n let index = 0 - clonedSlidesCount.value.before\n const offset = Math.abs(scrolledOffset.value + clonedSlidesOffset.value)\n\n while (accumulatedSize <= offset) {\n const normalizedIndex =\n ((index % slides.length) + slides.length) % slides.length\n accumulatedSize +=\n slidesRect.value[normalizedIndex]?.[dimension.value] + config.gap\n index++\n }\n minIndex = index - 1\n }\n\n let maxIndex = 0\n {\n let index = minIndex\n let accumulatedSize = 0\n if (index < 0) {\n accumulatedSize =\n slidesRect.value\n .slice(0, index)\n .reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) -\n Math.abs(scrolledOffset.value + clonedSlidesOffset.value)\n } else {\n accumulatedSize =\n slidesRect.value\n .slice(0, index)\n .reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) -\n Math.abs(scrolledOffset.value)\n }\n\n while (accumulatedSize < viewportRect.value[dimension.value]) {\n const normalizedIndex =\n ((index % slides.length) + slides.length) % slides.length\n accumulatedSize +=\n slidesRect.value[normalizedIndex]?.[dimension.value] + config.gap\n index++\n }\n maxIndex = index - 1\n }\n\n return {\n min: Math.floor(minIndex),\n max: Math.ceil(maxIndex),\n }\n })\n\n const trackTransform: ComputedRef<string | undefined> = computed(() => {\n if (config.slideEffect === 'fade') {\n return undefined\n }\n\n const translateAxis = isVertical.value ? 'Y' : 'X'\n\n // Include user drag interaction offset\n const dragOffset = isVertical.value ? dragged.y : dragged.x\n\n let totalOffset = scrolledOffset.value + dragOffset\n\n if (!config.wrapAround && config.preventExcessiveDragging) {\n let maxSlidingValue = 0\n if (isAuto.value) {\n maxSlidingValue = slidesRect.value.reduce(\n (acc, slide) => acc + slide[dimension.value],\n 0\n )\n } else {\n maxSlidingValue =\n (slidesCount.value - Number(config.itemsToShow)) * effectiveSlideSize.value\n }\n const min = isReversed.value ? 0 : -1 * maxSlidingValue\n const max = isReversed.value ? maxSlidingValue : 0\n totalOffset = getNumberInRange({\n val: totalOffset,\n min,\n max,\n })\n }\n return `translate${translateAxis}(${totalOffset}px)`\n })\n\n const carouselStyle = computed(() => ({\n '--vc-transition-duration': isSliding.value\n ? toCssValue(config.transition, 'ms')\n : undefined,\n '--vc-slide-gap': toCssValue(config.gap),\n '--vc-carousel-height': toCssValue(config.height),\n '--vc-cloned-offset': toCssValue(clonedSlidesOffset.value),\n }))\n\n const nav: CarouselNav = { slideTo, next, prev }\n\n const provided: InjectedCarousel = reactive({\n activeSlide: activeSlideIndex,\n config,\n currentSlide: currentSlideIndex,\n isSliding,\n isVertical,\n maxSlide: maxSlideIndex,\n minSlide: minSlideIndex,\n nav,\n normalizedDir,\n slideRegistry,\n slideSize,\n slides,\n slidesCount,\n viewport,\n visibleRange,\n })\n\n provide(injectCarousel, provided)\n\n const data = reactive<CarouselData>({\n config,\n currentSlide: currentSlideIndex,\n maxSlide: maxSlideIndex,\n middleSlide: middleSlideIndex,\n minSlide: minSlideIndex,\n slideSize,\n slidesCount,\n })\n\n expose<CarouselExposed>(\n reactive({\n data,\n next,\n prev,\n restartCarousel,\n slideTo,\n updateBreakpointsConfig,\n updateSlideSize,\n updateSlidesData,\n ...toRefs(provided),\n })\n )\n\n return () => {\n const slotSlides = slots.default || slots.slides\n const outputSlides = slotSlides?.(data) || []\n\n const { before, after } = clonedSlidesCount.value\n const slidesBefore = createCloneSlides({\n slides,\n position: 'before',\n toShow: before,\n })\n\n const slidesAfter = createCloneSlides({\n slides,\n position: 'after',\n toShow: after,\n })\n\n const output = [...slidesBefore, ...outputSlides, ...slidesAfter]\n\n if (!config.enabled || !output.length) {\n return h(\n 'section',\n {\n ref: root,\n class: ['carousel', 'is-disabled'],\n },\n output\n )\n }\n\n const addonsElements = slots.addons?.(data) || []\n\n const trackEl = h(\n 'ol',\n {\n class: 'carousel__track',\n style: { transform: trackTransform.value },\n onMousedownCapture: config.mouseDrag ? handleDragStart : null,\n onTouchstartPassiveCapture: config.touchDrag ? handleDragStart : null,\n onWheel: config.mouseWheel ? handleScroll : null,\n },\n output\n )\n const viewPortEl = h('div', { class: 'carousel__viewport', ref: viewport }, trackEl)\n\n return h(\n 'section',\n {\n ref: root,\n class: [\n 'carousel',\n `is-${normalizedDir.value}`,\n `is-effect-${config.slideEffect}`,\n {\n 'is-vertical': isVertical.value,\n 'is-sliding': isSliding.value,\n 'is-dragging': isDragging.value,\n 'is-hover': isHover.value,\n },\n ],\n dir: normalizedDir.value,\n style: carouselStyle.value,\n 'aria-label': config.i18n['ariaGallery'],\n tabindex: '0',\n onFocus: handleFocus,\n onBlur: handleBlur,\n onMouse