vue3-carousel
Advanced tools
Comparing version 0.12.0 to 0.13.0
@@ -5,2 +5,9 @@ # Changelog | ||
## [0.12.0](https://github.com/ismail9k/vue3-carousel/releases/tag/v0.12.0) - 2024-12-26 | ||
- Generate cloned slides dynamically based on the active slides @ismail9k in #462, #465 | ||
- Add logo, footer, and features showcase to documentation by @ismail9k in #463 | ||
- Add fade in-out animation effect to carousel by @ismail9k in #464 | ||
- General fixes and enhancements | ||
## [0.11.0](https://github.com/ismail9k/vue3-carousel/releases/tag/v0.11.0) - 2024-12-23 | ||
@@ -7,0 +14,0 @@ |
@@ -1,8 +0,4 @@ | ||
import { Reactive, Ref, ShallowReactive, ComponentInternalInstance, ComputedRef } from 'vue'; | ||
import { Ref, Reactive, ComputedRef, ShallowReactive, ComponentInternalInstance } from 'vue'; | ||
// Use a symbol for inject provide to avoid any kind of collision with another lib | ||
// https://vuejs.org/guide/components/provide-inject#working-with-symbol-keys | ||
declare const injectCarousel = Symbol('carousel') as InjectionKey< | ||
InjectedCarousel | undefined | ||
> | ||
type BreakpointMode = (typeof BREAKPOINT_MODE_OPTIONS)[number] | ||
@@ -15,35 +11,35 @@ type Breakpoints = { | ||
type SlideEffect = (typeof SLIDE_EFFECTS)[number] | ||
type SnapAlign = (typeof SNAP_ALIGN_OPTIONS)[number] | ||
type Dir = (typeof DIR_OPTIONS)[number] | ||
type BreakpointMode = (typeof BREAKPOINT_MODE_OPTIONS)[number] | ||
type I18nKeys = keyof typeof I18N_DEFAULT_CONFIG | ||
type NonNormalizedDir = keyof typeof DIR_MAP | ||
type NormalizedDir = (typeof NORMALIZED_DIR_OPTIONS)[number] | ||
type NonNormalizedDir = keyof typeof DIR_MAP | ||
type SlideEffect = (typeof SLIDE_EFFECTS)[number] | ||
type I18nKeys = keyof typeof I18N_DEFAULT_CONFIG | ||
type SnapAlign = (typeof SNAP_ALIGN_OPTIONS)[number] | ||
interface CarouselConfig { | ||
enabled: boolean | ||
itemsToShow: number | ||
itemsToScroll: number | ||
modelValue?: number | ||
transition?: number | ||
gap: number | ||
type CarouselConfig = { | ||
autoplay?: number | ||
snapAlign: SnapAlign | ||
wrapAround?: boolean | ||
pauseAutoplayOnHover?: boolean | ||
mouseDrag?: boolean | ||
touchDrag?: boolean | ||
dir?: Dir | ||
breakpointMode?: BreakpointMode | ||
breakpoints?: Breakpoints | ||
dir?: Dir | ||
enabled: boolean | ||
gap: number | ||
height: string | number | ||
i18n: { [key in I18nKeys]?: string } | ||
ignoreAnimations: boolean | string[] | string | ||
itemsToScroll: number | ||
itemsToShow: number | 'auto' | ||
modelValue?: number | ||
mouseDrag?: boolean | ||
pauseAutoplayOnHover?: boolean | ||
preventExcessiveDragging: boolean | ||
slideEffect: SlideEffect | ||
snapAlign: SnapAlign | ||
touchDrag?: boolean | ||
transition?: number | ||
wrapAround?: boolean | ||
} | ||
@@ -53,11 +49,11 @@ | ||
declare const SNAP_ALIGN_OPTIONS = [ | ||
'center', | ||
'start', | ||
'end', | ||
'center-even', | ||
'center-odd', | ||
] as const | ||
declare const SLIDE_EFFECTS = ['slide', 'fade'] as const | ||
declare const BREAKPOINT_MODE_OPTIONS = ['viewport', 'carousel'] as const | ||
declare const DIR_MAP = { | ||
'bottom-to-top': 'btt', | ||
'left-to-right': 'ltr', | ||
'right-to-left': 'rtl', | ||
'top-to-bottom': 'ttb', | ||
} as const | ||
declare const DIR_OPTIONS = [ | ||
@@ -73,49 +69,59 @@ 'ltr', | ||
] as const | ||
declare const I18N_DEFAULT_CONFIG = { | ||
ariaGallery: 'Gallery', | ||
ariaNavigateToPage: 'Navigate to page {slideNumber}', | ||
ariaNavigateToSlide: 'Navigate to slide {slideNumber}', | ||
ariaNextSlide: 'Navigate to next slide', | ||
ariaPreviousSlide: 'Navigate to previous slide', | ||
ariaNavigateToSlide: 'Navigate to slide {slideNumber}', | ||
ariaNavigateToPage: 'Navigate to page {slideNumber}', | ||
ariaGallery: 'Gallery', | ||
itemXofY: 'Item {currentSlide} of {slidesCount}', | ||
iconArrowUp: 'Arrow pointing upwards', | ||
iconArrowDown: 'Arrow pointing downwards', | ||
iconArrowLeft: 'Arrow pointing to the left', | ||
iconArrowRight: 'Arrow pointing to the right', | ||
iconArrowLeft: 'Arrow pointing to the left', | ||
iconArrowUp: 'Arrow pointing upwards', | ||
itemXofY: 'Item {currentSlide} of {slidesCount}', | ||
} as const | ||
declare const DIR_MAP = { | ||
'left-to-right': 'ltr', | ||
'right-to-left': 'rtl', | ||
'top-to-bottom': 'ttb', | ||
'bottom-to-top': 'btt', | ||
} as const | ||
declare const NORMALIZED_DIR_OPTIONS = Object.values(DIR_MAP) | ||
declare const SLIDE_EFFECTS = ['slide', 'fade'] as const | ||
declare const SNAP_ALIGN_OPTIONS = [ | ||
'center', | ||
'start', | ||
'end', | ||
'center-even', | ||
'center-odd', | ||
] as const | ||
declare const DEFAULT_CONFIG: CarouselConfig = { | ||
autoplay: 0, | ||
breakpointMode: BREAKPOINT_MODE_OPTIONS[0], | ||
breakpoints: undefined, | ||
dir: DIR_OPTIONS[0], | ||
enabled: true, | ||
itemsToShow: 1, | ||
itemsToScroll: 1, | ||
modelValue: 0, | ||
transition: 300, | ||
autoplay: 0, | ||
gap: 0, | ||
height: 'auto', | ||
wrapAround: false, | ||
pauseAutoplayOnHover: false, | ||
mouseDrag: true, | ||
touchDrag: true, | ||
snapAlign: SNAP_ALIGN_OPTIONS[0], | ||
dir: DIR_OPTIONS[0], | ||
breakpointMode: BREAKPOINT_MODE_OPTIONS[0], | ||
breakpoints: undefined, | ||
i18n: I18N_DEFAULT_CONFIG, | ||
ignoreAnimations: false, | ||
itemsToScroll: 1, | ||
itemsToShow: 1, | ||
modelValue: 0, | ||
mouseDrag: true, | ||
pauseAutoplayOnHover: false, | ||
preventExcessiveDragging: false, | ||
slideEffect: SLIDE_EFFECTS[0], | ||
snapAlign: SNAP_ALIGN_OPTIONS[0], | ||
touchDrag: true, | ||
transition: 300, | ||
wrapAround: false, | ||
} | ||
// Use a symbol for inject provide to avoid any kind of collision with another lib | ||
// https://vuejs.org/guide/components/provide-inject#working-with-symbol-keys | ||
declare const injectCarousel = Symbol('carousel') as InjectionKey< | ||
InjectedCarousel | undefined | ||
> | ||
declare const createSlideRegistry = (emit: EmitFn) => { | ||
const slides = shallowReactive<Array<ComponentInternalInstance>>([]) | ||
const clonedSlides = shallowReactive<Array<ComponentInternalInstance>>([]) | ||
@@ -135,2 +141,8 @@ const updateSlideIndexes = (startIndex?: number) => { | ||
return { | ||
cleanup: () => { | ||
slides.splice(0, slides.length) | ||
}, | ||
getSlides: () => slides, | ||
registerSlide: (slide: ComponentInternalInstance, index?: number) => { | ||
@@ -140,3 +152,2 @@ if (!slide) return | ||
if (slide.props.isClone) { | ||
clonedSlides.push(slide) | ||
return | ||
@@ -160,9 +171,2 @@ } | ||
}, | ||
cleanup: () => { | ||
slides.splice(0, slides.length) | ||
}, | ||
getSlides: () => slides, | ||
getClonedSlides: () => clonedSlides, | ||
} | ||
@@ -173,47 +177,58 @@ } | ||
interface CarouselNav { | ||
slideTo: (index: number) => void | ||
next: (skipTransition?: boolean) => void | ||
prev: (skipTransition?: boolean) => void | ||
type ElRect = { | ||
height: number | ||
width: number | ||
} | ||
type InjectedCarousel = Reactive<{ | ||
config: CarouselConfig | ||
viewport: Ref<Element | null> | ||
slides: ShallowReactive<Array<ComponentInternalInstance>> | ||
slidesCount: ComputedRef<number> | ||
activeSlide: Ref<number> | ||
currentSlide: Ref<number> | ||
scrolledIndex: Ref<number> | ||
maxSlide: ComputedRef<number> | ||
minSlide: ComputedRef<number> | ||
slideSize: Ref<number> | ||
isVertical: ComputedRef<boolean> | ||
normalizedDir: ComputedRef<NormalizedDir> | ||
nav: CarouselNav | ||
isSliding: Ref<boolean> | ||
slideRegistry: SlideRegistry | ||
}> | ||
type Range = { | ||
min: number | ||
max: number | ||
} | ||
interface CarouselData { | ||
type CarouselData = { | ||
config: CarouselConfig | ||
slidesCount: Ref<number> | ||
slideSize: Ref<number> | ||
currentSlide: Ref<number> | ||
maxSlide: Ref<number> | ||
middleSlide: Ref<number> | ||
minSlide: Ref<number> | ||
middleSlide: Ref<number> | ||
slideSize: Ref<number> | ||
slidesCount: Ref<number> | ||
} | ||
interface CarouselMethods extends CarouselNav { | ||
type CarouselExposed = CarouselMethods & { | ||
data: Reactive<CarouselData> | ||
nav: CarouselNav | ||
} | ||
type CarouselMethods = CarouselNav & { | ||
restartCarousel: () => void | ||
updateBreakpointsConfig: () => void | ||
updateSlideSize: () => void | ||
updateSlidesData: () => void | ||
updateSlideSize: () => void | ||
restartCarousel: () => void | ||
} | ||
interface CarouselExposed extends CarouselMethods { | ||
nav: CarouselNav | ||
data: Reactive<CarouselData> | ||
type CarouselNav = { | ||
next: (skipTransition?: boolean) => void | ||
prev: (skipTransition?: boolean) => void | ||
slideTo: (index: number) => void | ||
} | ||
type InjectedCarousel = Reactive<{ | ||
activeSlide: Ref<number> | ||
config: CarouselConfig | ||
currentSlide: Ref<number> | ||
isSliding: Ref<boolean> | ||
isVertical: ComputedRef<boolean> | ||
maxSlide: ComputedRef<number> | ||
minSlide: ComputedRef<number> | ||
nav: CarouselNav | ||
normalizedDir: ComputedRef<NormalizedDir> | ||
slideRegistry: SlideRegistry | ||
slideSize: Ref<number> | ||
slides: ShallowReactive<Array<ComponentInternalInstance>> | ||
slidesCount: ComputedRef<number> | ||
viewport: Ref<Element | null> | ||
visibleRange: ComputedRef<Range> | ||
}> | ||
declare const Carousel = defineComponent({ | ||
@@ -223,11 +238,11 @@ name: 'VueCarousel', | ||
emits: [ | ||
'before-init', | ||
'drag', | ||
'init', | ||
'drag', | ||
'slide-start', | ||
'loop', | ||
'update:modelValue', | ||
'slide-end', | ||
'before-init', | ||
'slide-registered', | ||
'slide-start', | ||
'slide-unregistered', | ||
'update:modelValue', | ||
], | ||
@@ -260,8 +275,4 @@ setup(props: CarouselConfig, { slots, emit, expose }: SetupContext) { | ||
const middleSlideIndex = computed(() => Math.ceil((slidesCount.value - 1) / 2)) | ||
const maxSlideIndex = computed(() => { | ||
return getMaxSlideIndex({ config, slidesCount: slidesCount.value }) | ||
}) | ||
const minSlideIndex = computed(() => { | ||
return getMinSlideIndex({ config, slidesCount: slidesCount.value }) | ||
}) | ||
const maxSlideIndex = computed(() => slidesCount.value - 1) | ||
const minSlideIndex = computed(() => 0) | ||
@@ -281,3 +292,6 @@ let autoplayTimer: ReturnType<typeof setInterval> | null = null | ||
const isVertical = computed(() => ['ttb', 'btt'].includes(normalizedDir.value)) | ||
const isAuto = computed(() => config.itemsToShow === 'auto') | ||
const dimension = computed(() => (isVertical.value ? 'height' : 'width')) | ||
function updateBreakpointsConfig(): void { | ||
@@ -324,3 +338,2 @@ if (!mounted.value) { | ||
const totalGap = computed(() => (config.itemsToShow - 1) * config.gap) | ||
const transformElements = shallowReactive<Set<HTMLElement>>(new Set()) | ||
@@ -331,26 +344,46 @@ | ||
*/ | ||
const slidesRect = ref<Array<ElRect>>([]) | ||
function updateSlidesRectSize({ | ||
widthMultiplier, | ||
heightMultiplier, | ||
}: ScaleMultipliers): void { | ||
slidesRect.value = slides.map((slide) => { | ||
const rect = slide.exposed?.getBoundingRect() | ||
return { | ||
width: rect.width * widthMultiplier, | ||
height: rect.height * heightMultiplier, | ||
} | ||
}) | ||
} | ||
const viewportRect: Ref<ElRect> = ref({ | ||
width: 0, | ||
height: 0, | ||
}) | ||
function updateViewportRectSize({ | ||
widthMultiplier, | ||
heightMultiplier, | ||
}: ScaleMultipliers): void { | ||
const rect = viewport.value?.getBoundingClientRect() || { width: 0, height: 0 } | ||
viewportRect.value = { | ||
width: rect.width * widthMultiplier, | ||
height: rect.height * heightMultiplier, | ||
} | ||
} | ||
function updateSlideSize(): void { | ||
if (!viewport.value) return | ||
let multiplierWidth = 1 | ||
transformElements.forEach((el) => { | ||
const transformArr = getTransformValues(el) | ||
if (transformArr.length === 6) { | ||
multiplierWidth *= transformArr[0] | ||
} | ||
}) | ||
const scaleMultipliers = getScaleMultipliers(transformElements) | ||
// Calculate size based on orientation | ||
if (isVertical.value) { | ||
if (config.height !== 'auto') { | ||
const height = | ||
typeof config.height === 'string' && isNaN(parseInt(config.height)) | ||
? viewport.value.getBoundingClientRect().height | ||
: parseInt(config.height as string) | ||
updateViewportRectSize(scaleMultipliers) | ||
updateSlidesRectSize(scaleMultipliers) | ||
slideSize.value = (height - totalGap.value) / config.itemsToShow | ||
} | ||
if (isAuto.value) { | ||
slideSize.value = calculateAverage( | ||
slidesRect.value.map((slide) => slide[dimension.value]) | ||
) | ||
} else { | ||
const width = viewport.value.getBoundingClientRect().width | ||
slideSize.value = (width / multiplierWidth - totalGap.value) / config.itemsToShow | ||
const itemsToShow = Number(config.itemsToShow) | ||
const totalGap = (itemsToShow - 1) * config.gap | ||
slideSize.value = (viewportRect.value[dimension.value] - totalGap) / itemsToShow | ||
} | ||
@@ -369,7 +402,9 @@ } | ||
// Validate itemsToShow | ||
config.itemsToShow = getNumberInRange({ | ||
val: config.itemsToShow, | ||
max: slidesCount.value, | ||
min: 1, | ||
}) | ||
if (!isAuto.value) { | ||
config.itemsToShow = getNumberInRange({ | ||
val: Number(config.itemsToShow), | ||
max: slidesCount.value, | ||
min: 1, | ||
}) | ||
} | ||
} | ||
@@ -507,5 +542,5 @@ | ||
if (isReversed.value) { | ||
nav.next(true) | ||
next(true) | ||
} else { | ||
nav.prev(true) | ||
prev(true) | ||
} | ||
@@ -518,5 +553,5 @@ } | ||
if (isReversed.value) { | ||
nav.prev(true) | ||
prev(true) | ||
} else { | ||
nav.next(true) | ||
next(true) | ||
} | ||
@@ -674,3 +709,3 @@ } | ||
max: maxSlideIndex.value, | ||
min: 0, | ||
min: minSlideIndex.value, | ||
}) | ||
@@ -696,12 +731,10 @@ } | ||
const transitionCallback = (): void => { | ||
if (config.wrapAround) { | ||
if (mappedIndex !== targetIndex) { | ||
modelWatcher.resume() | ||
if (config.wrapAround && mappedIndex !== targetIndex) { | ||
modelWatcher.resume() | ||
currentSlideIndex.value = mappedIndex | ||
emit('loop', { | ||
currentSlideIndex: currentSlideIndex.value, | ||
slidingToIndex: slideIndex, | ||
}) | ||
} | ||
currentSlideIndex.value = mappedIndex | ||
emit('loop', { | ||
currentSlideIndex: currentSlideIndex.value, | ||
slidingToIndex: slideIndex, | ||
}) | ||
} | ||
@@ -730,43 +763,2 @@ | ||
const nav: CarouselNav = { slideTo, next, prev } | ||
const scrolledIndex = computed(() => | ||
getScrolledIndex({ | ||
config, | ||
currentSlide: currentSlideIndex.value, | ||
slidesCount: slidesCount.value, | ||
}) | ||
) | ||
const provided: InjectedCarousel = reactive({ | ||
config, | ||
slidesCount, | ||
viewport, | ||
slides, | ||
scrolledIndex, | ||
currentSlide: currentSlideIndex, | ||
activeSlide: activeSlideIndex, | ||
maxSlide: maxSlideIndex, | ||
minSlide: minSlideIndex, | ||
slideSize, | ||
isVertical, | ||
normalizedDir, | ||
nav, | ||
isSliding, | ||
slideRegistry, | ||
}) | ||
provide(injectCarousel, provided) | ||
/** @deprecated provides */ | ||
provide('config', config) | ||
provide('slidesCount', slidesCount) | ||
provide('currentSlide', currentSlideIndex) | ||
provide('maxSlide', maxSlideIndex) | ||
provide('minSlide', minSlideIndex) | ||
provide('slideSize', slideSize) | ||
provide('isVertical', isVertical) | ||
provide('normalizeDir', normalizedDir) | ||
provide('nav', nav) | ||
provide('isSliding', isSliding) | ||
function restartCarousel(): void { | ||
@@ -805,87 +797,266 @@ updateBreakpointsConfig() | ||
const data = reactive<CarouselData>({ | ||
config, | ||
slidesCount, | ||
slideSize, | ||
currentSlide: currentSlideIndex, | ||
maxSlide: maxSlideIndex, | ||
minSlide: minSlideIndex, | ||
middleSlide: middleSlideIndex, | ||
const clonedSlidesCount = computed(() => { | ||
if (!config.wrapAround) { | ||
return { before: 0, after: 0 } | ||
} | ||
if (isAuto.value) { | ||
return { before: slides.length, after: slides.length } | ||
} | ||
const itemsToShow = Number(config.itemsToShow) | ||
const slidesToClone = Math.ceil(itemsToShow + (config.itemsToScroll - 1)) | ||
const before = slidesToClone - activeSlideIndex.value | ||
const after = slidesToClone - (slidesCount.value - (activeSlideIndex.value + 1)) | ||
return { | ||
before: Math.max(0, before), | ||
after: Math.max(0, after), | ||
} | ||
}) | ||
expose<CarouselExposed>({ | ||
updateBreakpointsConfig, | ||
updateSlidesData, | ||
updateSlideSize, | ||
restartCarousel, | ||
slideTo, | ||
next, | ||
prev, | ||
nav, | ||
data, | ||
const clonedSlidesOffset = computed(() => { | ||
if (!clonedSlidesCount.value.before) { | ||
return 0 | ||
} | ||
if (isAuto.value) { | ||
return ( | ||
slidesRect.value | ||
.slice(-1 * clonedSlidesCount.value.before) | ||
.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) * -1 | ||
) | ||
} | ||
return clonedSlidesCount.value.before * effectiveSlideSize.value * -1 | ||
}) | ||
const trackHeight = computed(() => { | ||
// If the carousel is vertical and height is set to auto, calculate the height based on slide size and gap | ||
if (config.height === 'auto') { | ||
if (isVertical.value && slideSize.value) { | ||
return `${slideSize.value * config.itemsToShow + totalGap.value}px` | ||
} | ||
return undefined | ||
const snapAlignOffset = computed(() => { | ||
if (isAuto.value) { | ||
const slideIndex = | ||
((currentSlideIndex.value % slides.length) + slides.length) % slides.length | ||
return getSnapAlignOffset({ | ||
slideSize: slidesRect.value[slideIndex]?.[dimension.value], | ||
viewportSize: viewportRect.value[dimension.value], | ||
align: config.snapAlign, | ||
}) | ||
} | ||
if ( | ||
typeof config.height === 'number' || | ||
parseFloat(config.height).toString() === config.height | ||
) { | ||
return `${config.height}px` | ||
return getSnapAlignOffset({ | ||
align: config.snapAlign, | ||
itemsToShow: +config.itemsToShow, | ||
}) | ||
}) | ||
const scrolledOffset = computed(() => { | ||
let output = 0 | ||
if (isAuto.value) { | ||
if (currentSlideIndex.value < 0) { | ||
output = | ||
slidesRect.value | ||
.slice(currentSlideIndex.value) | ||
.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) * -1 | ||
} else { | ||
output = slidesRect.value | ||
.slice(0, currentSlideIndex.value) | ||
.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) | ||
} | ||
output -= snapAlignOffset.value | ||
// remove whitespace | ||
if (!config.wrapAround) { | ||
const maxSlidingValue = | ||
slidesRect.value.reduce( | ||
(acc, slide) => acc + slide[dimension.value] + config.gap, | ||
0 | ||
) - | ||
viewportRect.value[dimension.value] - | ||
config.gap | ||
output = getNumberInRange({ | ||
val: output, | ||
max: maxSlidingValue, | ||
min: 0, | ||
}) | ||
} | ||
} else { | ||
return config.height | ||
let scrolledSlides = currentSlideIndex.value - snapAlignOffset.value | ||
// remove whitespace | ||
if (!config.wrapAround) { | ||
scrolledSlides = getNumberInRange({ | ||
val: scrolledSlides, | ||
max: slidesCount.value - +config.itemsToShow, | ||
min: 0, | ||
}) | ||
} | ||
output = scrolledSlides * effectiveSlideSize.value | ||
} | ||
return output * (isReversed.value ? 1 : -1) | ||
}) | ||
const clonedSlidesCount = computed(() => { | ||
if (!config.wrapAround) { | ||
return { before: 0, after: 0 } | ||
const visibleRange = computed(() => { | ||
if (!isAuto.value) { | ||
const base = currentSlideIndex.value - snapAlignOffset.value | ||
if (config.wrapAround) { | ||
return { | ||
min: Math.floor(base), | ||
max: Math.ceil(base + Number(config.itemsToShow) - 1), | ||
} | ||
} | ||
return { | ||
min: Math.floor( | ||
getNumberInRange({ | ||
val: base, | ||
max: slidesCount.value - Number(config.itemsToShow), | ||
min: 0, | ||
}) | ||
), | ||
max: Math.ceil( | ||
getNumberInRange({ | ||
val: base + Number(config.itemsToShow) - 1, | ||
max: slidesCount.value - 1, | ||
min: 0, | ||
}) | ||
), | ||
} | ||
} | ||
const slidesToClone = Math.ceil(config.itemsToShow + (config.itemsToScroll - 1)) | ||
const before = slidesToClone - activeSlideIndex.value | ||
const after = slidesToClone - (slidesCount.value - (activeSlideIndex.value + 1)) | ||
// Auto width mode | ||
let minIndex = 0 | ||
{ | ||
let accumulatedSize = 0 | ||
let index = 0 - clonedSlidesCount.value.before | ||
const offset = Math.abs(scrolledOffset.value + clonedSlidesOffset.value) | ||
while (accumulatedSize <= offset) { | ||
const normalizedIndex = | ||
((index % slides.length) + slides.length) % slides.length | ||
accumulatedSize += | ||
slidesRect.value[normalizedIndex]?.[dimension.value] + config.gap | ||
index++ | ||
} | ||
minIndex = index - 1 | ||
} | ||
let maxIndex = 0 | ||
{ | ||
let index = minIndex | ||
let accumulatedSize = 0 | ||
if (index < 0) { | ||
accumulatedSize = | ||
slidesRect.value | ||
.slice(0, index) | ||
.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) - | ||
Math.abs(scrolledOffset.value + clonedSlidesOffset.value) | ||
} else { | ||
accumulatedSize = | ||
slidesRect.value | ||
.slice(0, index) | ||
.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) - | ||
Math.abs(scrolledOffset.value) | ||
} | ||
while (accumulatedSize < viewportRect.value[dimension.value]) { | ||
const normalizedIndex = | ||
((index % slides.length) + slides.length) % slides.length | ||
accumulatedSize += | ||
slidesRect.value[normalizedIndex]?.[dimension.value] + config.gap | ||
index++ | ||
} | ||
maxIndex = index - 1 | ||
} | ||
return { | ||
before: Math.max(0, before), | ||
after: Math.max(0, after), | ||
min: Math.floor(minIndex), | ||
max: Math.ceil(maxIndex), | ||
} | ||
}) | ||
const clonedSlidesOffset = computed( | ||
() => clonedSlidesCount.value.before * effectiveSlideSize.value * -1 | ||
) | ||
const trackTransform: ComputedRef<string> = computed(() => { | ||
const directionMultiplier = isReversed.value ? 1 : -1 | ||
const trackTransform: ComputedRef<string | undefined> = computed(() => { | ||
if (config.slideEffect === 'fade') { | ||
return undefined | ||
} | ||
const translateAxis = isVertical.value ? 'Y' : 'X' | ||
// Calculate the total offset for slide transformation | ||
const scrolledOffset = | ||
scrolledIndex.value * effectiveSlideSize.value * directionMultiplier | ||
// Include user drag interaction offset | ||
const dragOffset = isVertical.value ? dragged.y : dragged.x | ||
const totalOffset = scrolledOffset + dragOffset | ||
let totalOffset = scrolledOffset.value + dragOffset | ||
if (!config.wrapAround && config.preventExcessiveDragging) { | ||
let maxSlidingValue = 0 | ||
if (isAuto.value) { | ||
maxSlidingValue = slidesRect.value.reduce( | ||
(acc, slide) => acc + slide[dimension.value], | ||
0 | ||
) | ||
} else { | ||
maxSlidingValue = | ||
(slidesCount.value - Number(config.itemsToShow)) * effectiveSlideSize.value | ||
} | ||
const min = isReversed.value ? 0 : -1 * maxSlidingValue | ||
const max = isReversed.value ? maxSlidingValue : 0 | ||
totalOffset = getNumberInRange({ | ||
val: totalOffset, | ||
min, | ||
max, | ||
}) | ||
} | ||
return `translate${translateAxis}(${totalOffset}px)` | ||
}) | ||
const trackStyle = computed(() => ({ | ||
transform: config.slideEffect === 'slide' ? trackTransform.value : undefined, | ||
gap: config.gap > 0 ? `${config.gap}px` : undefined, | ||
'--vc-trk-transition-duration': isSliding.value | ||
? `${config.transition}ms` | ||
const carouselStyle = computed(() => ({ | ||
'--vc-transition-duration': isSliding.value | ||
? toCssValue(config.transition, 'ms') | ||
: undefined, | ||
'--vc-trk-height': trackHeight.value, | ||
'--vc-trk-cloned-offset': `${clonedSlidesOffset.value}px`, | ||
'--vc-slide-gap': toCssValue(config.gap), | ||
'--vc-carousel-height': toCssValue(config.height), | ||
'--vc-cloned-offset': toCssValue(clonedSlidesOffset.value), | ||
})) | ||
const nav: CarouselNav = { slideTo, next, prev } | ||
const provided: InjectedCarousel = reactive({ | ||
activeSlide: activeSlideIndex, | ||
config, | ||
currentSlide: currentSlideIndex, | ||
isSliding, | ||
isVertical, | ||
maxSlide: maxSlideIndex, | ||
minSlide: minSlideIndex, | ||
nav, | ||
normalizedDir, | ||
slideRegistry, | ||
slideSize, | ||
slides, | ||
slidesCount, | ||
viewport, | ||
visibleRange, | ||
}) | ||
provide(injectCarousel, provided) | ||
const data = reactive<CarouselData>({ | ||
config, | ||
currentSlide: currentSlideIndex, | ||
maxSlide: maxSlideIndex, | ||
middleSlide: middleSlideIndex, | ||
minSlide: minSlideIndex, | ||
slideSize, | ||
slidesCount, | ||
}) | ||
expose<CarouselExposed>({ | ||
data, | ||
nav, | ||
next, | ||
prev, | ||
restartCarousel, | ||
slideTo, | ||
updateBreakpointsConfig, | ||
updateSlideSize, | ||
updateSlidesData, | ||
}) | ||
return () => { | ||
@@ -927,3 +1098,3 @@ const slotSlides = slots.default || slots.slides | ||
class: 'carousel__track', | ||
style: trackStyle.value, | ||
style: { transform: trackTransform.value }, | ||
onMousedownCapture: config.mouseDrag ? handleDragStart : null, | ||
@@ -952,2 +1123,3 @@ onTouchstartPassiveCapture: config.touchDrag ? handleDragStart : null, | ||
dir: normalizedDir.value, | ||
style: carouselStyle.value, | ||
'aria-label': config.i18n['ariaGallery'], | ||
@@ -967,6 +1139,6 @@ tabindex: '0', | ||
declare enum IconName { | ||
arrowUp = 'arrowUp', | ||
arrowDown = 'arrowDown', | ||
arrowLeft = 'arrowLeft', | ||
arrowRight = 'arrowRight', | ||
arrowLeft = 'arrowLeft', | ||
arrowUp = 'arrowUp', | ||
} | ||
@@ -976,12 +1148,12 @@ | ||
interface IconProps { | ||
type IconProps = { | ||
name: IconNameValue | ||
title?: string | ||
name: IconNameValue | ||
} | ||
declare const icons = { | ||
arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z', | ||
arrowDown: 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z', | ||
arrowLeft: 'M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z', | ||
arrowRight: 'M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z', | ||
arrowLeft: 'M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z', | ||
arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z', | ||
} | ||
@@ -1012,4 +1184,3 @@ | ||
const iconTitle: string = | ||
carousel?.config.i18n[iconI18n(iconName)] || props.title! | ||
const iconTitle: string = carousel?.config.i18n[iconI18n(iconName)] || props.title! | ||
@@ -1046,6 +1217,6 @@ const titleEl = h('title', iconTitle) | ||
const directionIcons: Record<NormalizedDir, IconNameValue> = { | ||
btt: 'arrowDown', | ||
ltr: 'arrowLeft', | ||
rtl: 'arrowRight', | ||
ttb: 'arrowUp', | ||
btt: 'arrowDown', | ||
} | ||
@@ -1057,6 +1228,6 @@ | ||
const directionIcons: Record<NormalizedDir, IconNameValue> = { | ||
btt: 'arrowUp', | ||
ltr: 'arrowRight', | ||
rtl: 'arrowLeft', | ||
ttb: 'arrowDown', | ||
btt: 'arrowUp', | ||
} | ||
@@ -1067,4 +1238,8 @@ | ||
const prevDisabled = computed(() => !carousel.config.wrapAround && carousel.currentSlide <= carousel.minSlide) | ||
const nextDisabled = computed(() => !carousel.config.wrapAround && carousel.currentSlide >= carousel.maxSlide) | ||
const prevDisabled = computed( | ||
() => !carousel.config.wrapAround && carousel.currentSlide <= carousel.minSlide | ||
) | ||
const nextDisabled = computed( | ||
() => !carousel.config.wrapAround && carousel.currentSlide >= carousel.maxSlide | ||
) | ||
@@ -1084,3 +1259,3 @@ return () => { | ||
'carousel__prev', | ||
{'carousel__prev--disabled': prevDisabled.value}, | ||
{ 'carousel__prev--disabled': prevDisabled.value }, | ||
attrs.class, | ||
@@ -1102,3 +1277,3 @@ ], | ||
'carousel__next', | ||
{'carousel__next--disabled': nextDisabled.value}, | ||
{ 'carousel__next--disabled': nextDisabled.value }, | ||
attrs.class, | ||
@@ -1115,3 +1290,3 @@ ], | ||
interface PaginationProps { | ||
type PaginationProps = { | ||
disableOnClick?: boolean | ||
@@ -1138,14 +1313,16 @@ paginateByItemsToShow?: boolean | ||
const itemsToShow = computed(() => carousel.config.itemsToShow as number) | ||
const offset = computed(() => | ||
calculateOffset(carousel.config.snapAlign, carousel.config.itemsToShow) | ||
getSnapAlignOffset({ | ||
align: carousel.config.snapAlign, | ||
itemsToShow: itemsToShow.value, | ||
}) | ||
) | ||
const isPaginated = computed( | ||
() => props.paginateByItemsToShow && carousel.config.itemsToShow > 1 | ||
() => props.paginateByItemsToShow && itemsToShow.value > 1 | ||
) | ||
const currentPage = computed(() => | ||
Math.ceil((carousel.activeSlide - offset.value) / carousel.config.itemsToShow) | ||
Math.ceil((carousel.activeSlide - offset.value) / itemsToShow.value) | ||
) | ||
const pageCount = computed(() => | ||
Math.ceil(carousel.slidesCount / carousel.config.itemsToShow) | ||
) | ||
const pageCount = computed(() => Math.ceil(carousel.slidesCount / itemsToShow.value)) | ||
@@ -1198,3 +1375,3 @@ const isActive = (slide: number): boolean => | ||
isPaginated.value | ||
? slide * carousel.config.itemsToShow + offset.value | ||
? Math.floor(slide * +carousel.config.itemsToShow + offset.value) | ||
: slide | ||
@@ -1212,6 +1389,7 @@ ), | ||
interface SlideProps { | ||
type SlideProps = { | ||
id?: string | ||
index: number | ||
isClone?: boolean | ||
position?: 'before' | 'after' | ||
} | ||
@@ -1222,6 +1400,2 @@ | ||
props: { | ||
isClone: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
id: { | ||
@@ -1235,4 +1409,12 @@ type: String, | ||
}, | ||
isClone: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
position: { | ||
type: String, | ||
default: undefined, | ||
}, | ||
}, | ||
setup(props: DeepReadonly<SlideProps>, { slots, expose }: SetupContext) { | ||
setup(props: DeepReadonly<SlideProps>, { attrs, slots, expose }: SetupContext) { | ||
const carousel = inject(injectCarousel) | ||
@@ -1251,5 +1433,13 @@ provide(injectCarousel, undefined) // Don't provide for nested slides | ||
const instance = getCurrentInstance()! | ||
const getBoundingRect = () => { | ||
const el = instance.vnode.el as HTMLElement | ||
return el ? el.getBoundingClientRect() : { width: 0, height: 0 } | ||
} | ||
expose({ | ||
id: props.id, | ||
setIndex, | ||
getBoundingRect, | ||
}) | ||
@@ -1268,15 +1458,17 @@ | ||
() => | ||
currentIndex.value >= Math.floor(carousel.scrolledIndex) && | ||
currentIndex.value < | ||
Math.ceil(carousel.scrolledIndex) + carousel.config.itemsToShow | ||
currentIndex.value >= carousel.visibleRange.min && | ||
currentIndex.value <= carousel.visibleRange.max | ||
) | ||
const slideStyle = computed(() => { | ||
if (carousel.config.itemsToShow === 'auto') { | ||
return | ||
} | ||
const itemsToShow = carousel.config.itemsToShow | ||
const dimension = | ||
carousel.config.gap > 0 && carousel.config.itemsToShow > 1 | ||
? `calc(${100 / carousel.config.itemsToShow}% - ${ | ||
(carousel.config.gap * (carousel.config.itemsToShow - 1)) / | ||
carousel.config.itemsToShow | ||
carousel.config.gap > 0 && itemsToShow > 1 | ||
? `calc(${100 / itemsToShow}% - ${ | ||
(carousel.config.gap * (itemsToShow - 1)) / itemsToShow | ||
}px)` | ||
: `${100 / carousel.config.itemsToShow}%` | ||
: `${100 / itemsToShow}%` | ||
@@ -1286,4 +1478,2 @@ return carousel.isVertical ? { height: dimension } : { width: dimension } | ||
const instance = getCurrentInstance()! | ||
carousel.slideRegistry.registerSlide(instance, props.index) | ||
@@ -1312,3 +1502,3 @@ onUnmounted(() => { | ||
{ | ||
style: slideStyle.value, | ||
style: [attrs.style, { ...slideStyle.value }], | ||
class: { | ||
@@ -1334,2 +1524,3 @@ carousel__slide: true, | ||
slots.default?.({ | ||
currentIndex: currentIndex.value, | ||
isActive: isActive.value, | ||
@@ -1347,2 +1538,2 @@ isClone: props.isClone, | ||
export { BREAKPOINT_MODE_OPTIONS, type BreakpointMode, type Breakpoints, Carousel, type CarouselConfig, type CarouselData, type CarouselExposed, type CarouselMethods, type CarouselNav, DEFAULT_CONFIG, DIR_MAP, DIR_OPTIONS, type Dir, I18N_DEFAULT_CONFIG, type I18nKeys, Icon, IconName, type IconNameValue, type IconProps, type InjectedCarousel, NORMALIZED_DIR_OPTIONS, Navigation, type NavigationProps, type NonNormalizedDir, type NormalizedDir, Pagination, type PaginationProps, SLIDE_EFFECTS, SNAP_ALIGN_OPTIONS, Slide, type SlideEffect, type SlideProps, type SlideRegistry, type SnapAlign, type VueClass, createSlideRegistry, icons, injectCarousel }; | ||
export { BREAKPOINT_MODE_OPTIONS, type BreakpointMode, type Breakpoints, Carousel, type CarouselConfig, type CarouselData, type CarouselExposed, type CarouselMethods, type CarouselNav, DEFAULT_CONFIG, DIR_MAP, DIR_OPTIONS, type Dir, type ElRect, I18N_DEFAULT_CONFIG, type I18nKeys, Icon, IconName, type IconNameValue, type IconProps, type InjectedCarousel, NORMALIZED_DIR_OPTIONS, Navigation, type NavigationProps, type NonNormalizedDir, type NormalizedDir, Pagination, type PaginationProps, type Range, SLIDE_EFFECTS, SNAP_ALIGN_OPTIONS, Slide, type SlideEffect, type SlideProps, type SlideRegistry, type SnapAlign, type VueClass, createSlideRegistry, icons, injectCarousel }; |
/** | ||
* Vue 3 Carousel 0.12.0 | ||
* (c) 2024 | ||
* Vue 3 Carousel 0.13.0 | ||
* (c) 2025 | ||
* @license MIT | ||
@@ -12,15 +12,9 @@ */ | ||
// Use a symbol for inject provide to avoid any kind of collision with another lib | ||
// https://vuejs.org/guide/components/provide-inject#working-with-symbol-keys | ||
const injectCarousel = Symbol('carousel'); | ||
const SNAP_ALIGN_OPTIONS = [ | ||
'center', | ||
'start', | ||
'end', | ||
'center-even', | ||
'center-odd', | ||
]; | ||
const SLIDE_EFFECTS = ['slide', 'fade']; | ||
const BREAKPOINT_MODE_OPTIONS = ['viewport', 'carousel']; | ||
const DIR_MAP = { | ||
'bottom-to-top': 'btt', | ||
'left-to-right': 'ltr', | ||
'right-to-left': 'rtl', | ||
'top-to-bottom': 'ttb', | ||
}; | ||
const DIR_OPTIONS = [ | ||
@@ -37,45 +31,51 @@ 'ltr', | ||
const I18N_DEFAULT_CONFIG = { | ||
ariaGallery: 'Gallery', | ||
ariaNavigateToPage: 'Navigate to page {slideNumber}', | ||
ariaNavigateToSlide: 'Navigate to slide {slideNumber}', | ||
ariaNextSlide: 'Navigate to next slide', | ||
ariaPreviousSlide: 'Navigate to previous slide', | ||
ariaNavigateToSlide: 'Navigate to slide {slideNumber}', | ||
ariaNavigateToPage: 'Navigate to page {slideNumber}', | ||
ariaGallery: 'Gallery', | ||
itemXofY: 'Item {currentSlide} of {slidesCount}', | ||
iconArrowUp: 'Arrow pointing upwards', | ||
iconArrowDown: 'Arrow pointing downwards', | ||
iconArrowLeft: 'Arrow pointing to the left', | ||
iconArrowRight: 'Arrow pointing to the right', | ||
iconArrowLeft: 'Arrow pointing to the left', | ||
iconArrowUp: 'Arrow pointing upwards', | ||
itemXofY: 'Item {currentSlide} of {slidesCount}', | ||
}; | ||
const DIR_MAP = { | ||
'left-to-right': 'ltr', | ||
'right-to-left': 'rtl', | ||
'top-to-bottom': 'ttb', | ||
'bottom-to-top': 'btt', | ||
}; | ||
const NORMALIZED_DIR_OPTIONS = Object.values(DIR_MAP); | ||
const SLIDE_EFFECTS = ['slide', 'fade']; | ||
const SNAP_ALIGN_OPTIONS = [ | ||
'center', | ||
'start', | ||
'end', | ||
'center-even', | ||
'center-odd', | ||
]; | ||
const DEFAULT_CONFIG = { | ||
autoplay: 0, | ||
breakpointMode: BREAKPOINT_MODE_OPTIONS[0], | ||
breakpoints: undefined, | ||
dir: DIR_OPTIONS[0], | ||
enabled: true, | ||
itemsToShow: 1, | ||
itemsToScroll: 1, | ||
modelValue: 0, | ||
transition: 300, | ||
autoplay: 0, | ||
gap: 0, | ||
height: 'auto', | ||
wrapAround: false, | ||
pauseAutoplayOnHover: false, | ||
mouseDrag: true, | ||
touchDrag: true, | ||
snapAlign: SNAP_ALIGN_OPTIONS[0], | ||
dir: DIR_OPTIONS[0], | ||
breakpointMode: BREAKPOINT_MODE_OPTIONS[0], | ||
breakpoints: undefined, | ||
i18n: I18N_DEFAULT_CONFIG, | ||
ignoreAnimations: false, | ||
itemsToScroll: 1, | ||
itemsToShow: 1, | ||
modelValue: 0, | ||
mouseDrag: true, | ||
pauseAutoplayOnHover: false, | ||
preventExcessiveDragging: false, | ||
slideEffect: SLIDE_EFFECTS[0], | ||
snapAlign: SNAP_ALIGN_OPTIONS[0], | ||
touchDrag: true, | ||
transition: 300, | ||
wrapAround: false, | ||
}; | ||
// Use a symbol for inject provide to avoid any kind of collision with another lib | ||
// https://vuejs.org/guide/components/provide-inject#working-with-symbol-keys | ||
const injectCarousel = Symbol('carousel'); | ||
const createSlideRegistry = (emit) => { | ||
const slides = vue.shallowReactive([]); | ||
const clonedSlides = vue.shallowReactive([]); | ||
const updateSlideIndexes = (startIndex) => { | ||
@@ -96,2 +96,6 @@ if (startIndex !== undefined) { | ||
return { | ||
cleanup: () => { | ||
slides.splice(0, slides.length); | ||
}, | ||
getSlides: () => slides, | ||
registerSlide: (slide, index) => { | ||
@@ -101,3 +105,2 @@ if (!slide) | ||
if (slide.props.isClone) { | ||
clonedSlides.push(slide); | ||
return; | ||
@@ -118,10 +121,78 @@ } | ||
}, | ||
cleanup: () => { | ||
slides.splice(0, slides.length); | ||
}, | ||
getSlides: () => slides, | ||
getClonedSlides: () => clonedSlides, | ||
}; | ||
}; | ||
function calculateAverage(numbers) { | ||
if (numbers.length === 0) | ||
return 0; | ||
const sum = numbers.reduce((acc, num) => acc + num, 0); | ||
return sum / numbers.length; | ||
} | ||
function createCloneSlides({ slides, position, toShow }) { | ||
const clones = []; | ||
const isBefore = position === 'before'; | ||
const start = isBefore ? -toShow : 0; | ||
const end = isBefore ? 0 : toShow; | ||
if (slides.length <= 0) { | ||
return clones; | ||
} | ||
for (let i = start; i < end; i++) { | ||
const index = isBefore ? i : i + slides.length; | ||
const props = { | ||
index, | ||
isClone: true, | ||
position, | ||
id: undefined, // Make sure we don't duplicate the id which would be invalid html | ||
key: `clone-${position}-${i}`, | ||
}; | ||
const vnode = slides[((i % slides.length) + slides.length) % slides.length].vnode; | ||
const clone = vue.cloneVNode(vnode, props); | ||
clone.el = null; | ||
clones.push(clone); | ||
} | ||
return clones; | ||
} | ||
const FOCUSABLE_ELEMENTS_SELECTOR = 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'; | ||
/** | ||
* Disables keyboard tab navigation for all focusable child elements | ||
* @param node Vue virtual node containing the elements to disable | ||
*/ | ||
function disableChildrenTabbing(node) { | ||
if (!node.el || !(node.el instanceof Element)) { | ||
return; | ||
} | ||
const elements = node.el.querySelectorAll(FOCUSABLE_ELEMENTS_SELECTOR); | ||
for (const el of elements) { | ||
if (el instanceof HTMLElement && | ||
!el.hasAttribute('disabled') && | ||
el.getAttribute('aria-hidden') !== 'true') { | ||
el.setAttribute('tabindex', '-1'); | ||
} | ||
} | ||
} | ||
/** Useful function to destructure props without triggering reactivity for certain keys */ | ||
function except(obj, keys) { | ||
return Object.keys(obj).filter((k) => !keys.includes(k)) | ||
.reduce((acc, key) => (acc[key] = obj[key], acc), {}); | ||
} | ||
/** | ||
* Calculates the number of slides to move based on drag movement | ||
* @param params Configuration parameters for drag calculation | ||
* @returns Number of slides to move (positive or negative) | ||
*/ | ||
function getDraggedSlidesCount(params) { | ||
const { isVertical, isReversed, dragged, effectiveSlideSize } = params; | ||
// Get drag value based on direction | ||
const dragValue = isVertical ? dragged.y : dragged.x; | ||
// If no drag, return +0 explicitly | ||
if (dragValue === 0) | ||
return 0; | ||
const slidesDragged = Math.round(dragValue / effectiveSlideSize); | ||
return isReversed ? slidesDragged : -slidesDragged; | ||
} | ||
function getNumberInRange({ val, max, min }) { | ||
@@ -134,9 +205,36 @@ if (max < min) { | ||
const calculateOffset = (snapAlign, itemsToShow) => { | ||
switch (snapAlign) { | ||
default: | ||
function getTransformValues(el) { | ||
const { transform } = window.getComputedStyle(el); | ||
//add sanity check | ||
return transform | ||
.split(/[(,)]/) | ||
.slice(1, -1) | ||
.map((v) => parseFloat(v)); | ||
} | ||
function getScaleMultipliers(transformElements) { | ||
let widthMultiplier = 1; | ||
let heightMultiplier = 1; | ||
transformElements.forEach((el) => { | ||
const transformArr = getTransformValues(el); | ||
if (transformArr.length === 6) { | ||
widthMultiplier /= transformArr[0]; | ||
heightMultiplier /= transformArr[3]; | ||
} | ||
}); | ||
return { widthMultiplier, heightMultiplier }; | ||
} | ||
/** | ||
* Calculates the snap align offset for a carousel item based on items to show. | ||
* Returns the number of slides to offset. | ||
* | ||
* @param align - The alignment type. | ||
* @param itemsToShow - The number of items to show. | ||
* @returns The calculated offset. | ||
*/ | ||
function getSnapAlignOffsetByItemsToShow(align, itemsToShow) { | ||
switch (align) { | ||
case 'start': | ||
return 0; | ||
case 'center': | ||
return (itemsToShow - 1) / 2; | ||
case 'center-odd': | ||
@@ -148,63 +246,50 @@ return (itemsToShow - 1) / 2; | ||
return itemsToShow - 1; | ||
default: | ||
return 0; | ||
} | ||
}; | ||
function getScrolledIndex({ config, currentSlide, slidesCount, }) { | ||
const { snapAlign = 'center', wrapAround, itemsToShow = 1 } = config; | ||
// Calculate the offset based on snapAlign | ||
const offset = calculateOffset(snapAlign, itemsToShow); | ||
// Compute the index with or without wrapAround | ||
if (wrapAround) { | ||
return currentSlide - offset; | ||
} | ||
return getNumberInRange({ | ||
val: currentSlide - offset, | ||
max: slidesCount - itemsToShow, | ||
min: 0, | ||
}); | ||
} | ||
/** | ||
* Determines the minimum slide index based on the configuration. | ||
* Calculates the snap align offset for a carousel item based on slide and viewport size. | ||
* Returns the real width to offset. | ||
* | ||
* @param {GetMinSlideIndexArgs} args - The carousel configuration and slide count. | ||
* @returns {number} The minimum slide index. | ||
* @param align - The alignment type. | ||
* @param slideSize - The size of the slide. | ||
* @param viewportSize - The size of the viewport. | ||
* @returns The calculated offset. | ||
*/ | ||
function getMinSlideIndex({ config, slidesCount }) { | ||
const { snapAlign = 'center', wrapAround, itemsToShow = 1 } = config; | ||
// If wrapAround is enabled or itemsToShow exceeds slidesCount, the minimum index is always 0 | ||
if (wrapAround || itemsToShow > slidesCount) { | ||
return 0; | ||
function getSnapAlignOffsetBySlideAndViewport(align, slideSize, viewportSize) { | ||
switch (align) { | ||
case 'start': | ||
return 0; | ||
case 'center': | ||
case 'center-odd': | ||
return (viewportSize - slideSize) / 2; | ||
case 'center-even': | ||
return viewportSize / 2 - slideSize; | ||
case 'end': | ||
return viewportSize - slideSize; | ||
default: | ||
return 0; | ||
} | ||
// Return the calculated offset or default to 0 for invalid snapAlign values | ||
return Math.max(0, Math.floor(calculateOffset(snapAlign, itemsToShow))); | ||
} | ||
/** | ||
* Determines the maximum slide index based on the configuration. | ||
* Calculates the snap align offset for a carousel item. | ||
* | ||
* @param {Args} args - The carousel configuration and slide count. | ||
* @returns {number} The maximum slide index. | ||
* @param params - The parameters for calculating the offset. | ||
* @returns The calculated offset. | ||
*/ | ||
function getMaxSlideIndex({ config, slidesCount }) { | ||
const { snapAlign = 'center', wrapAround, itemsToShow = 1 } = config; | ||
// Map snapAlign values to calculation logic | ||
function snapAlignCalculations() { | ||
// If wrapAround is enabled, fallback to default which is the last slide | ||
switch (wrapAround ? '' : snapAlign) { | ||
case 'start': | ||
return Math.ceil(slidesCount - itemsToShow); | ||
case 'center': | ||
case 'center-odd': | ||
return slidesCount - Math.ceil((itemsToShow - 0.5) / 2); | ||
case 'center-even': | ||
return slidesCount - Math.ceil(itemsToShow / 2); | ||
case 'end': | ||
default: | ||
return Math.ceil(slidesCount - 1); | ||
} | ||
function getSnapAlignOffset({ slideSize, viewportSize, align, itemsToShow, }) { | ||
if (itemsToShow !== undefined) { | ||
return getSnapAlignOffsetByItemsToShow(align, itemsToShow); | ||
} | ||
// Return the result ensuring it's non-negative | ||
return Math.max(snapAlignCalculations(), 0); | ||
if (slideSize !== undefined && viewportSize !== undefined) { | ||
return getSnapAlignOffsetBySlideAndViewport(align, slideSize, viewportSize); | ||
} | ||
return 0; | ||
} | ||
function i18nFormatter(string = '', values = {}) { | ||
return Object.entries(values).reduce((acc, [key, value]) => acc.replace(`{${key}}`, String(value)), string); | ||
} | ||
function mapNumberToRange({ val, max, min = 0 }) { | ||
@@ -215,6 +300,2 @@ const mod = max - min + 1; | ||
function i18nFormatter(string = '', values = {}) { | ||
return Object.entries(values).reduce((acc, [key, value]) => acc.replace(`{${key}}`, String(value)), string); | ||
} | ||
/** | ||
@@ -259,76 +340,17 @@ * Returns a throttled version of the function using requestAnimationFrame. | ||
/** Useful function to destructure props without triggering reactivity for certain keys */ | ||
function except(obj, keys) { | ||
return Object.keys(obj).filter((k) => !keys.includes(k)) | ||
.reduce((acc, key) => (acc[key] = obj[key], acc), {}); | ||
} | ||
function getTransformValues(el) { | ||
const { transform } = window.getComputedStyle(el); | ||
//add sanity check | ||
return transform | ||
.split(/[(,)]/) | ||
.slice(1, -1) | ||
.map((v) => parseFloat(v)); | ||
} | ||
function createCloneSlides({ slides, position, toShow }) { | ||
const clones = []; | ||
const isBefore = position === 'before'; | ||
const start = isBefore ? -toShow : 0; | ||
const end = isBefore ? 0 : toShow; | ||
if (slides.length <= 0) { | ||
return clones; | ||
} | ||
for (let i = start; i < end; i++) { | ||
const index = isBefore ? i : i + slides.length; | ||
const props = { | ||
index, | ||
isClone: true, | ||
id: undefined, // Make sure we don't duplicate the id which would be invalid html | ||
key: `clone-${position}-${i}`, | ||
}; | ||
const vnode = slides[((i % slides.length) + slides.length) % slides.length].vnode; | ||
const clone = vue.cloneVNode(vnode, props); | ||
clone.el = null; | ||
clones.push(clone); | ||
} | ||
return clones; | ||
} | ||
const FOCUSABLE_ELEMENTS_SELECTOR = 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'; | ||
/** | ||
* Disables keyboard tab navigation for all focusable child elements | ||
* @param node Vue virtual node containing the elements to disable | ||
*/ | ||
function disableChildrenTabbing(node) { | ||
if (!node.el || !(node.el instanceof Element)) { | ||
return; | ||
* Converts a value to a CSS-compatible string. | ||
* @param value - The value to convert. | ||
* @returns The CSS-compatible string. | ||
**/ | ||
function toCssValue(value, unit = 'px') { | ||
if (value === null || value === undefined || value === '') { | ||
return undefined; | ||
} | ||
const elements = node.el.querySelectorAll(FOCUSABLE_ELEMENTS_SELECTOR); | ||
for (const el of elements) { | ||
if (el instanceof HTMLElement && | ||
!el.hasAttribute('disabled') && | ||
el.getAttribute('aria-hidden') !== 'true') { | ||
el.setAttribute('tabindex', '-1'); | ||
} | ||
if (typeof value === 'number' || parseFloat(value).toString() === value) { | ||
return `${value}${unit}`; | ||
} | ||
return value; | ||
} | ||
/** | ||
* Calculates the number of slides to move based on drag movement | ||
* @param params Configuration parameters for drag calculation | ||
* @returns Number of slides to move (positive or negative) | ||
*/ | ||
function getDraggedSlidesCount(params) { | ||
const { isVertical, isReversed, dragged, effectiveSlideSize } = params; | ||
// Get drag value based on direction | ||
const dragValue = isVertical ? dragged.y : dragged.x; | ||
// If no drag, return +0 explicitly | ||
if (dragValue === 0) | ||
return 0; | ||
const slidesDragged = Math.round(dragValue / effectiveSlideSize); | ||
return isReversed ? slidesDragged : -slidesDragged; | ||
} | ||
const ARIA = vue.defineComponent({ | ||
@@ -353,2 +375,19 @@ name: 'CarouselAria', | ||
const carouselProps = { | ||
// time to auto advance slides in ms | ||
autoplay: { | ||
default: DEFAULT_CONFIG.autoplay, | ||
type: Number, | ||
}, | ||
// an object to store breakpoints | ||
breakpoints: { | ||
default: DEFAULT_CONFIG.breakpoints, | ||
type: Object, | ||
}, | ||
// controls the breakpoint mode relative to the carousel container or the viewport | ||
breakpointMode: { | ||
default: DEFAULT_CONFIG.breakpointMode, | ||
validator(value) { | ||
return BREAKPOINT_MODE_OPTIONS.includes(value); | ||
}, | ||
}, | ||
// enable/disable the carousel component | ||
@@ -359,17 +398,2 @@ enabled: { | ||
}, | ||
// count of items to showed per view | ||
itemsToShow: { | ||
default: DEFAULT_CONFIG.itemsToShow, | ||
type: Number, | ||
}, | ||
// count of items to be scrolled | ||
itemsToScroll: { | ||
default: DEFAULT_CONFIG.itemsToScroll, | ||
type: Number, | ||
}, | ||
// control infinite scrolling mode | ||
wrapAround: { | ||
default: DEFAULT_CONFIG.wrapAround, | ||
type: Boolean, | ||
}, | ||
// control the gap between slides | ||
@@ -385,36 +409,21 @@ gap: { | ||
}, | ||
// control snap position alignment | ||
snapAlign: { | ||
default: DEFAULT_CONFIG.snapAlign, | ||
validator(value) { | ||
return SNAP_ALIGN_OPTIONS.includes(value); | ||
}, | ||
ignoreAnimations: { | ||
default: false, | ||
type: [Array, Boolean, String], | ||
}, | ||
// sliding transition time in ms | ||
transition: { | ||
default: DEFAULT_CONFIG.transition, | ||
// count of items to be scrolled | ||
itemsToScroll: { | ||
default: DEFAULT_CONFIG.itemsToScroll, | ||
type: Number, | ||
}, | ||
// controls the breakpoint mode relative to the carousel container or the viewport | ||
breakpointMode: { | ||
default: DEFAULT_CONFIG.breakpointMode, | ||
validator(value) { | ||
return BREAKPOINT_MODE_OPTIONS.includes(value); | ||
}, | ||
// count of items to showed per view | ||
itemsToShow: { | ||
default: DEFAULT_CONFIG.itemsToShow, | ||
type: [Number, String], | ||
}, | ||
// an object to store breakpoints | ||
breakpoints: { | ||
default: DEFAULT_CONFIG.breakpoints, | ||
// aria-labels and additional text labels | ||
i18n: { | ||
default: DEFAULT_CONFIG.i18n, | ||
type: Object, | ||
}, | ||
// time to auto advance slides in ms | ||
autoplay: { | ||
default: DEFAULT_CONFIG.autoplay, | ||
type: Number, | ||
}, | ||
// pause autoplay when mouse hover over the carousel | ||
pauseAutoplayOnHover: { | ||
default: DEFAULT_CONFIG.pauseAutoplayOnHover, | ||
type: Boolean, | ||
}, | ||
// slide number number of initial slide | ||
@@ -435,20 +444,23 @@ modelValue: { | ||
}, | ||
pauseAutoplayOnHover: { | ||
default: DEFAULT_CONFIG.pauseAutoplayOnHover, | ||
type: Boolean, | ||
}, | ||
preventExcessiveDragging: { | ||
default: false, | ||
type: Boolean, | ||
validator(value, props) { | ||
if (value && props.wrapAround) { | ||
console.warn(`[vue3-carousel warn]: "preventExcessiveDragging" cannot be used with wrapAround. The setting will be ignored.`); | ||
} | ||
return true; | ||
}, | ||
}, | ||
// control snap position alignment | ||
dir: { | ||
type: String, | ||
default: DEFAULT_CONFIG.dir, | ||
snapAlign: { | ||
default: DEFAULT_CONFIG.snapAlign, | ||
validator(value) { | ||
// The value must match one of these strings | ||
return DIR_OPTIONS.includes(value); | ||
return SNAP_ALIGN_OPTIONS.includes(value); | ||
}, | ||
}, | ||
// aria-labels and additional text labels | ||
i18n: { | ||
default: DEFAULT_CONFIG.i18n, | ||
type: Object, | ||
}, | ||
ignoreAnimations: { | ||
default: false, | ||
type: [Array, Boolean, String], | ||
}, | ||
slideEffect: { | ||
@@ -461,2 +473,29 @@ type: String, | ||
}, | ||
// sliding transition time in ms | ||
transition: { | ||
default: DEFAULT_CONFIG.transition, | ||
type: Number, | ||
}, | ||
// control the gap between slides | ||
dir: { | ||
type: String, | ||
default: DEFAULT_CONFIG.dir, | ||
validator(value, props) { | ||
// The value must match one of these strings | ||
if (!DIR_OPTIONS.includes(value)) { | ||
return false; | ||
} | ||
const normalizedDir = value in DIR_MAP ? DIR_MAP[value] : value; | ||
if (['ttb', 'btt'].includes(normalizedDir) && | ||
(!props.height || props.height === 'auto')) { | ||
console.warn(`[vue3-carousel warn]: The dir "${value}" is not supported with height "auto".`); | ||
} | ||
return true; | ||
}, | ||
}, | ||
// control infinite scrolling mode | ||
wrapAround: { | ||
default: DEFAULT_CONFIG.wrapAround, | ||
type: Boolean, | ||
}, | ||
}; | ||
@@ -468,11 +507,11 @@ | ||
emits: [ | ||
'before-init', | ||
'drag', | ||
'init', | ||
'drag', | ||
'slide-start', | ||
'loop', | ||
'update:modelValue', | ||
'slide-end', | ||
'before-init', | ||
'slide-registered', | ||
'slide-start', | ||
'slide-unregistered', | ||
'update:modelValue', | ||
], | ||
@@ -496,8 +535,4 @@ setup(props, { slots, emit, expose }) { | ||
const middleSlideIndex = vue.computed(() => Math.ceil((slidesCount.value - 1) / 2)); | ||
const maxSlideIndex = vue.computed(() => { | ||
return getMaxSlideIndex({ config, slidesCount: slidesCount.value }); | ||
}); | ||
const minSlideIndex = vue.computed(() => { | ||
return getMinSlideIndex({ config, slidesCount: slidesCount.value }); | ||
}); | ||
const maxSlideIndex = vue.computed(() => slidesCount.value - 1); | ||
const minSlideIndex = vue.computed(() => 0); | ||
let autoplayTimer = null; | ||
@@ -513,2 +548,4 @@ let transitionTimer = null; | ||
const isVertical = vue.computed(() => ['ttb', 'btt'].includes(normalizedDir.value)); | ||
const isAuto = vue.computed(() => config.itemsToShow === 'auto'); | ||
const dimension = vue.computed(() => (isVertical.value ? 'height' : 'width')); | ||
function updateBreakpointsConfig() { | ||
@@ -546,3 +583,2 @@ var _a; | ||
}); | ||
const totalGap = vue.computed(() => (config.itemsToShow - 1) * config.gap); | ||
const transformElements = vue.shallowReactive(new Set()); | ||
@@ -552,24 +588,38 @@ /** | ||
*/ | ||
const slidesRect = vue.ref([]); | ||
function updateSlidesRectSize({ widthMultiplier, heightMultiplier, }) { | ||
slidesRect.value = slides.map((slide) => { | ||
var _a; | ||
const rect = (_a = slide.exposed) === null || _a === void 0 ? void 0 : _a.getBoundingRect(); | ||
return { | ||
width: rect.width * widthMultiplier, | ||
height: rect.height * heightMultiplier, | ||
}; | ||
}); | ||
} | ||
const viewportRect = vue.ref({ | ||
width: 0, | ||
height: 0, | ||
}); | ||
function updateViewportRectSize({ widthMultiplier, heightMultiplier, }) { | ||
var _a; | ||
const rect = ((_a = viewport.value) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) || { width: 0, height: 0 }; | ||
viewportRect.value = { | ||
width: rect.width * widthMultiplier, | ||
height: rect.height * heightMultiplier, | ||
}; | ||
} | ||
function updateSlideSize() { | ||
if (!viewport.value) | ||
return; | ||
let multiplierWidth = 1; | ||
transformElements.forEach((el) => { | ||
const transformArr = getTransformValues(el); | ||
if (transformArr.length === 6) { | ||
multiplierWidth *= transformArr[0]; | ||
} | ||
}); | ||
// Calculate size based on orientation | ||
if (isVertical.value) { | ||
if (config.height !== 'auto') { | ||
const height = typeof config.height === 'string' && isNaN(parseInt(config.height)) | ||
? viewport.value.getBoundingClientRect().height | ||
: parseInt(config.height); | ||
slideSize.value = (height - totalGap.value) / config.itemsToShow; | ||
} | ||
const scaleMultipliers = getScaleMultipliers(transformElements); | ||
updateViewportRectSize(scaleMultipliers); | ||
updateSlidesRectSize(scaleMultipliers); | ||
if (isAuto.value) { | ||
slideSize.value = calculateAverage(slidesRect.value.map((slide) => slide[dimension.value])); | ||
} | ||
else { | ||
const width = viewport.value.getBoundingClientRect().width; | ||
slideSize.value = (width / multiplierWidth - totalGap.value) / config.itemsToShow; | ||
const itemsToShow = Number(config.itemsToShow); | ||
const totalGap = (itemsToShow - 1) * config.gap; | ||
slideSize.value = (viewportRect.value[dimension.value] - totalGap) / itemsToShow; | ||
} | ||
@@ -586,7 +636,9 @@ } | ||
// Validate itemsToShow | ||
config.itemsToShow = getNumberInRange({ | ||
val: config.itemsToShow, | ||
max: slidesCount.value, | ||
min: 1, | ||
}); | ||
if (!isAuto.value) { | ||
config.itemsToShow = getNumberInRange({ | ||
val: Number(config.itemsToShow), | ||
max: slidesCount.value, | ||
min: 1, | ||
}); | ||
} | ||
} | ||
@@ -708,6 +760,6 @@ const ignoreAnimations = vue.computed(() => { | ||
if (isReversed.value) { | ||
nav.next(true); | ||
next(true); | ||
} | ||
else { | ||
nav.prev(true); | ||
prev(true); | ||
} | ||
@@ -720,6 +772,6 @@ } | ||
if (isReversed.value) { | ||
nav.prev(true); | ||
prev(true); | ||
} | ||
else { | ||
nav.next(true); | ||
next(true); | ||
} | ||
@@ -851,3 +903,3 @@ } | ||
max: maxSlideIndex.value, | ||
min: 0, | ||
min: minSlideIndex.value, | ||
}); | ||
@@ -869,11 +921,9 @@ } | ||
const transitionCallback = () => { | ||
if (config.wrapAround) { | ||
if (mappedIndex !== targetIndex) { | ||
modelWatcher.resume(); | ||
currentSlideIndex.value = mappedIndex; | ||
emit('loop', { | ||
currentSlideIndex: currentSlideIndex.value, | ||
slidingToIndex: slideIndex, | ||
}); | ||
} | ||
if (config.wrapAround && mappedIndex !== targetIndex) { | ||
modelWatcher.resume(); | ||
currentSlideIndex.value = mappedIndex; | ||
emit('loop', { | ||
currentSlideIndex: currentSlideIndex.value, | ||
slidingToIndex: slideIndex, | ||
}); | ||
} | ||
@@ -896,37 +946,2 @@ emit('slide-end', { | ||
} | ||
const nav = { slideTo, next, prev }; | ||
const scrolledIndex = vue.computed(() => getScrolledIndex({ | ||
config, | ||
currentSlide: currentSlideIndex.value, | ||
slidesCount: slidesCount.value, | ||
})); | ||
const provided = vue.reactive({ | ||
config, | ||
slidesCount, | ||
viewport, | ||
slides, | ||
scrolledIndex, | ||
currentSlide: currentSlideIndex, | ||
activeSlide: activeSlideIndex, | ||
maxSlide: maxSlideIndex, | ||
minSlide: minSlideIndex, | ||
slideSize, | ||
isVertical, | ||
normalizedDir, | ||
nav, | ||
isSliding, | ||
slideRegistry, | ||
}); | ||
vue.provide(injectCarousel, provided); | ||
/** @deprecated provides */ | ||
vue.provide('config', config); | ||
vue.provide('slidesCount', slidesCount); | ||
vue.provide('currentSlide', currentSlideIndex); | ||
vue.provide('maxSlide', maxSlideIndex); | ||
vue.provide('minSlide', minSlideIndex); | ||
vue.provide('slideSize', slideSize); | ||
vue.provide('isVertical', isVertical); | ||
vue.provide('normalizeDir', normalizedDir); | ||
vue.provide('nav', nav); | ||
vue.provide('isSliding', isSliding); | ||
function restartCarousel() { | ||
@@ -950,38 +965,2 @@ updateBreakpointsConfig(); | ||
emit('before-init'); | ||
const data = vue.reactive({ | ||
config, | ||
slidesCount, | ||
slideSize, | ||
currentSlide: currentSlideIndex, | ||
maxSlide: maxSlideIndex, | ||
minSlide: minSlideIndex, | ||
middleSlide: middleSlideIndex, | ||
}); | ||
expose({ | ||
updateBreakpointsConfig, | ||
updateSlidesData, | ||
updateSlideSize, | ||
restartCarousel, | ||
slideTo, | ||
next, | ||
prev, | ||
nav, | ||
data, | ||
}); | ||
const trackHeight = vue.computed(() => { | ||
// If the carousel is vertical and height is set to auto, calculate the height based on slide size and gap | ||
if (config.height === 'auto') { | ||
if (isVertical.value && slideSize.value) { | ||
return `${slideSize.value * config.itemsToShow + totalGap.value}px`; | ||
} | ||
return undefined; | ||
} | ||
if (typeof config.height === 'number' || | ||
parseFloat(config.height).toString() === config.height) { | ||
return `${config.height}px`; | ||
} | ||
else { | ||
return config.height; | ||
} | ||
}); | ||
const clonedSlidesCount = vue.computed(() => { | ||
@@ -991,3 +970,7 @@ if (!config.wrapAround) { | ||
} | ||
const slidesToClone = Math.ceil(config.itemsToShow + (config.itemsToScroll - 1)); | ||
if (isAuto.value) { | ||
return { before: slides.length, after: slides.length }; | ||
} | ||
const itemsToShow = Number(config.itemsToShow); | ||
const slidesToClone = Math.ceil(itemsToShow + (config.itemsToScroll - 1)); | ||
const before = slidesToClone - activeSlideIndex.value; | ||
@@ -1000,22 +983,211 @@ const after = slidesToClone - (slidesCount.value - (activeSlideIndex.value + 1)); | ||
}); | ||
const clonedSlidesOffset = vue.computed(() => clonedSlidesCount.value.before * effectiveSlideSize.value * -1); | ||
const clonedSlidesOffset = vue.computed(() => { | ||
if (!clonedSlidesCount.value.before) { | ||
return 0; | ||
} | ||
if (isAuto.value) { | ||
return (slidesRect.value | ||
.slice(-1 * clonedSlidesCount.value.before) | ||
.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) * -1); | ||
} | ||
return clonedSlidesCount.value.before * effectiveSlideSize.value * -1; | ||
}); | ||
const snapAlignOffset = vue.computed(() => { | ||
var _a; | ||
if (isAuto.value) { | ||
const slideIndex = ((currentSlideIndex.value % slides.length) + slides.length) % slides.length; | ||
return getSnapAlignOffset({ | ||
slideSize: (_a = slidesRect.value[slideIndex]) === null || _a === void 0 ? void 0 : _a[dimension.value], | ||
viewportSize: viewportRect.value[dimension.value], | ||
align: config.snapAlign, | ||
}); | ||
} | ||
return getSnapAlignOffset({ | ||
align: config.snapAlign, | ||
itemsToShow: +config.itemsToShow, | ||
}); | ||
}); | ||
const scrolledOffset = vue.computed(() => { | ||
let output = 0; | ||
if (isAuto.value) { | ||
if (currentSlideIndex.value < 0) { | ||
output = | ||
slidesRect.value | ||
.slice(currentSlideIndex.value) | ||
.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) * -1; | ||
} | ||
else { | ||
output = slidesRect.value | ||
.slice(0, currentSlideIndex.value) | ||
.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0); | ||
} | ||
output -= snapAlignOffset.value; | ||
// remove whitespace | ||
if (!config.wrapAround) { | ||
const maxSlidingValue = slidesRect.value.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) - | ||
viewportRect.value[dimension.value] - | ||
config.gap; | ||
output = getNumberInRange({ | ||
val: output, | ||
max: maxSlidingValue, | ||
min: 0, | ||
}); | ||
} | ||
} | ||
else { | ||
let scrolledSlides = currentSlideIndex.value - snapAlignOffset.value; | ||
// remove whitespace | ||
if (!config.wrapAround) { | ||
scrolledSlides = getNumberInRange({ | ||
val: scrolledSlides, | ||
max: slidesCount.value - +config.itemsToShow, | ||
min: 0, | ||
}); | ||
} | ||
output = scrolledSlides * effectiveSlideSize.value; | ||
} | ||
return output * (isReversed.value ? 1 : -1); | ||
}); | ||
const visibleRange = vue.computed(() => { | ||
var _a, _b; | ||
if (!isAuto.value) { | ||
const base = currentSlideIndex.value - snapAlignOffset.value; | ||
if (config.wrapAround) { | ||
return { | ||
min: Math.floor(base), | ||
max: Math.ceil(base + Number(config.itemsToShow) - 1), | ||
}; | ||
} | ||
return { | ||
min: Math.floor(getNumberInRange({ | ||
val: base, | ||
max: slidesCount.value - Number(config.itemsToShow), | ||
min: 0, | ||
})), | ||
max: Math.ceil(getNumberInRange({ | ||
val: base + Number(config.itemsToShow) - 1, | ||
max: slidesCount.value - 1, | ||
min: 0, | ||
})), | ||
}; | ||
} | ||
// Auto width mode | ||
let minIndex = 0; | ||
{ | ||
let accumulatedSize = 0; | ||
let index = 0 - clonedSlidesCount.value.before; | ||
const offset = Math.abs(scrolledOffset.value + clonedSlidesOffset.value); | ||
while (accumulatedSize <= offset) { | ||
const normalizedIndex = ((index % slides.length) + slides.length) % slides.length; | ||
accumulatedSize += | ||
((_a = slidesRect.value[normalizedIndex]) === null || _a === void 0 ? void 0 : _a[dimension.value]) + config.gap; | ||
index++; | ||
} | ||
minIndex = index - 1; | ||
} | ||
let maxIndex = 0; | ||
{ | ||
let index = minIndex; | ||
let accumulatedSize = 0; | ||
if (index < 0) { | ||
accumulatedSize = | ||
slidesRect.value | ||
.slice(0, index) | ||
.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) - | ||
Math.abs(scrolledOffset.value + clonedSlidesOffset.value); | ||
} | ||
else { | ||
accumulatedSize = | ||
slidesRect.value | ||
.slice(0, index) | ||
.reduce((acc, slide) => acc + slide[dimension.value] + config.gap, 0) - | ||
Math.abs(scrolledOffset.value); | ||
} | ||
while (accumulatedSize < viewportRect.value[dimension.value]) { | ||
const normalizedIndex = ((index % slides.length) + slides.length) % slides.length; | ||
accumulatedSize += | ||
((_b = slidesRect.value[normalizedIndex]) === null || _b === void 0 ? void 0 : _b[dimension.value]) + config.gap; | ||
index++; | ||
} | ||
maxIndex = index - 1; | ||
} | ||
return { | ||
min: Math.floor(minIndex), | ||
max: Math.ceil(maxIndex), | ||
}; | ||
}); | ||
const trackTransform = vue.computed(() => { | ||
const directionMultiplier = isReversed.value ? 1 : -1; | ||
if (config.slideEffect === 'fade') { | ||
return undefined; | ||
} | ||
const translateAxis = isVertical.value ? 'Y' : 'X'; | ||
// Calculate the total offset for slide transformation | ||
const scrolledOffset = scrolledIndex.value * effectiveSlideSize.value * directionMultiplier; | ||
// Include user drag interaction offset | ||
const dragOffset = isVertical.value ? dragged.y : dragged.x; | ||
const totalOffset = scrolledOffset + dragOffset; | ||
let totalOffset = scrolledOffset.value + dragOffset; | ||
if (!config.wrapAround && config.preventExcessiveDragging) { | ||
let maxSlidingValue = 0; | ||
if (isAuto.value) { | ||
maxSlidingValue = slidesRect.value.reduce((acc, slide) => acc + slide[dimension.value], 0); | ||
} | ||
else { | ||
maxSlidingValue = | ||
(slidesCount.value - Number(config.itemsToShow)) * effectiveSlideSize.value; | ||
} | ||
const min = isReversed.value ? 0 : -1 * maxSlidingValue; | ||
const max = isReversed.value ? maxSlidingValue : 0; | ||
totalOffset = getNumberInRange({ | ||
val: totalOffset, | ||
min, | ||
max, | ||
}); | ||
} | ||
return `translate${translateAxis}(${totalOffset}px)`; | ||
}); | ||
const trackStyle = vue.computed(() => ({ | ||
transform: config.slideEffect === 'slide' ? trackTransform.value : undefined, | ||
gap: config.gap > 0 ? `${config.gap}px` : undefined, | ||
'--vc-trk-transition-duration': isSliding.value | ||
? `${config.transition}ms` | ||
const carouselStyle = vue.computed(() => ({ | ||
'--vc-transition-duration': isSliding.value | ||
? toCssValue(config.transition, 'ms') | ||
: undefined, | ||
'--vc-trk-height': trackHeight.value, | ||
'--vc-trk-cloned-offset': `${clonedSlidesOffset.value}px`, | ||
'--vc-slide-gap': toCssValue(config.gap), | ||
'--vc-carousel-height': toCssValue(config.height), | ||
'--vc-cloned-offset': toCssValue(clonedSlidesOffset.value), | ||
})); | ||
const nav = { slideTo, next, prev }; | ||
const provided = vue.reactive({ | ||
activeSlide: activeSlideIndex, | ||
config, | ||
currentSlide: currentSlideIndex, | ||
isSliding, | ||
isVertical, | ||
maxSlide: maxSlideIndex, | ||
minSlide: minSlideIndex, | ||
nav, | ||
normalizedDir, | ||
slideRegistry, | ||
slideSize, | ||
slides, | ||
slidesCount, | ||
viewport, | ||
visibleRange, | ||
}); | ||
vue.provide(injectCarousel, provided); | ||
const data = vue.reactive({ | ||
config, | ||
currentSlide: currentSlideIndex, | ||
maxSlide: maxSlideIndex, | ||
middleSlide: middleSlideIndex, | ||
minSlide: minSlideIndex, | ||
slideSize, | ||
slidesCount, | ||
}); | ||
expose({ | ||
data, | ||
nav, | ||
next, | ||
prev, | ||
restartCarousel, | ||
slideTo, | ||
updateBreakpointsConfig, | ||
updateSlideSize, | ||
updateSlidesData, | ||
}); | ||
return () => { | ||
@@ -1046,3 +1218,3 @@ var _a; | ||
class: 'carousel__track', | ||
style: trackStyle.value, | ||
style: { transform: trackTransform.value }, | ||
onMousedownCapture: config.mouseDrag ? handleDragStart : null, | ||
@@ -1066,2 +1238,3 @@ onTouchstartPassiveCapture: config.touchDrag ? handleDragStart : null, | ||
dir: normalizedDir.value, | ||
style: carouselStyle.value, | ||
'aria-label': config.i18n['ariaGallery'], | ||
@@ -1080,21 +1253,21 @@ tabindex: '0', | ||
(function (IconName) { | ||
IconName["arrowUp"] = "arrowUp"; | ||
IconName["arrowDown"] = "arrowDown"; | ||
IconName["arrowLeft"] = "arrowLeft"; | ||
IconName["arrowRight"] = "arrowRight"; | ||
IconName["arrowLeft"] = "arrowLeft"; | ||
IconName["arrowUp"] = "arrowUp"; | ||
})(IconName || (IconName = {})); | ||
const iconI18n = (name) => `icon${name.charAt(0).toUpperCase() + name.slice(1)}`; | ||
const icons = { | ||
arrowDown: 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z', | ||
arrowLeft: 'M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z', | ||
arrowRight: 'M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z', | ||
arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z', | ||
}; | ||
function isIconName(candidate) { | ||
return candidate in IconName; | ||
} | ||
const iconI18n = (name) => `icon${name.charAt(0).toUpperCase() + name.slice(1)}`; | ||
const validateIconName = (value) => { | ||
return value && isIconName(value); | ||
}; | ||
const icons = { | ||
arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z', | ||
arrowDown: 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z', | ||
arrowRight: 'M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z', | ||
arrowLeft: 'M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z', | ||
}; | ||
const Icon = vue.defineComponent({ | ||
@@ -1143,6 +1316,6 @@ props: { | ||
const directionIcons = { | ||
btt: 'arrowDown', | ||
ltr: 'arrowLeft', | ||
rtl: 'arrowRight', | ||
ttb: 'arrowUp', | ||
btt: 'arrowDown', | ||
}; | ||
@@ -1153,6 +1326,6 @@ return directionIcons[carousel.normalizedDir]; | ||
const directionIcons = { | ||
btt: 'arrowUp', | ||
ltr: 'arrowRight', | ||
rtl: 'arrowLeft', | ||
ttb: 'arrowDown', | ||
btt: 'arrowUp', | ||
}; | ||
@@ -1195,6 +1368,10 @@ return directionIcons[carousel.normalizedDir]; | ||
} | ||
const offset = vue.computed(() => calculateOffset(carousel.config.snapAlign, carousel.config.itemsToShow)); | ||
const isPaginated = vue.computed(() => props.paginateByItemsToShow && carousel.config.itemsToShow > 1); | ||
const currentPage = vue.computed(() => Math.ceil((carousel.activeSlide - offset.value) / carousel.config.itemsToShow)); | ||
const pageCount = vue.computed(() => Math.ceil(carousel.slidesCount / carousel.config.itemsToShow)); | ||
const itemsToShow = vue.computed(() => carousel.config.itemsToShow); | ||
const offset = vue.computed(() => getSnapAlignOffset({ | ||
align: carousel.config.snapAlign, | ||
itemsToShow: itemsToShow.value, | ||
})); | ||
const isPaginated = vue.computed(() => props.paginateByItemsToShow && itemsToShow.value > 1); | ||
const currentPage = vue.computed(() => Math.ceil((carousel.activeSlide - offset.value) / itemsToShow.value)); | ||
const pageCount = vue.computed(() => Math.ceil(carousel.slidesCount / itemsToShow.value)); | ||
const isActive = (slide) => mapNumberToRange(isPaginated.value | ||
@@ -1231,3 +1408,3 @@ ? { | ||
onClick: () => carousel.nav.slideTo(isPaginated.value | ||
? slide * carousel.config.itemsToShow + offset.value | ||
? Math.floor(slide * +carousel.config.itemsToShow + offset.value) | ||
: slide), | ||
@@ -1246,6 +1423,2 @@ }); | ||
props: { | ||
isClone: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
id: { | ||
@@ -1259,4 +1432,12 @@ type: String, | ||
}, | ||
isClone: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
position: { | ||
type: String, | ||
default: undefined, | ||
}, | ||
}, | ||
setup(props, { slots, expose }) { | ||
setup(props, { attrs, slots, expose }) { | ||
const carousel = vue.inject(injectCarousel); | ||
@@ -1271,5 +1452,11 @@ vue.provide(injectCarousel, undefined); // Don't provide for nested slides | ||
}; | ||
const instance = vue.getCurrentInstance(); | ||
const getBoundingRect = () => { | ||
const el = instance.vnode.el; | ||
return el ? el.getBoundingClientRect() : { width: 0, height: 0 }; | ||
}; | ||
expose({ | ||
id: props.id, | ||
setIndex, | ||
getBoundingRect, | ||
}); | ||
@@ -1279,13 +1466,14 @@ const isActive = vue.computed(() => currentIndex.value === carousel.activeSlide); | ||
const isNext = vue.computed(() => currentIndex.value === carousel.activeSlide + 1); | ||
const isVisible = vue.computed(() => currentIndex.value >= Math.floor(carousel.scrolledIndex) && | ||
currentIndex.value < | ||
Math.ceil(carousel.scrolledIndex) + carousel.config.itemsToShow); | ||
const isVisible = vue.computed(() => currentIndex.value >= carousel.visibleRange.min && | ||
currentIndex.value <= carousel.visibleRange.max); | ||
const slideStyle = vue.computed(() => { | ||
const dimension = carousel.config.gap > 0 && carousel.config.itemsToShow > 1 | ||
? `calc(${100 / carousel.config.itemsToShow}% - ${(carousel.config.gap * (carousel.config.itemsToShow - 1)) / | ||
carousel.config.itemsToShow}px)` | ||
: `${100 / carousel.config.itemsToShow}%`; | ||
if (carousel.config.itemsToShow === 'auto') { | ||
return; | ||
} | ||
const itemsToShow = carousel.config.itemsToShow; | ||
const dimension = carousel.config.gap > 0 && itemsToShow > 1 | ||
? `calc(${100 / itemsToShow}% - ${(carousel.config.gap * (itemsToShow - 1)) / itemsToShow}px)` | ||
: `${100 / itemsToShow}%`; | ||
return carousel.isVertical ? { height: dimension } : { width: dimension }; | ||
}); | ||
const instance = vue.getCurrentInstance(); | ||
carousel.slideRegistry.registerSlide(instance, props.index); | ||
@@ -1310,3 +1498,3 @@ vue.onUnmounted(() => { | ||
return vue.h('li', { | ||
style: slideStyle.value, | ||
style: [attrs.style, Object.assign({}, slideStyle.value)], | ||
class: { | ||
@@ -1331,2 +1519,3 @@ carousel__slide: true, | ||
}, (_b = slots.default) === null || _b === void 0 ? void 0 : _b.call(slots, { | ||
currentIndex: currentIndex.value, | ||
isActive: isActive.value, | ||
@@ -1333,0 +1522,0 @@ isClone: props.isClone, |
/** | ||
* Vue 3 Carousel 0.12.0 | ||
* (c) 2024 | ||
* Vue 3 Carousel 0.13.0 | ||
* (c) 2025 | ||
* @license MIT | ||
*/ | ||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("vue")):"function"==typeof define&&define.amd?define(["exports","vue"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).VueCarousel={},e.Vue)}(this,(function(e,t){"use strict";const i=Symbol("carousel"),n=["center","start","end","center-even","center-odd"],o=["slide","fade"],a=["viewport","carousel"],l=["ltr","left-to-right","rtl","right-to-left","ttb","top-to-bottom","btt","bottom-to-top"],r={ariaNextSlide:"Navigate to next slide",ariaPreviousSlide:"Navigate to previous slide",ariaNavigateToSlide:"Navigate to slide {slideNumber}",ariaNavigateToPage:"Navigate to page {slideNumber}",ariaGallery:"Gallery",itemXofY:"Item {currentSlide} of {slidesCount}",iconArrowUp:"Arrow pointing upwards",iconArrowDown:"Arrow pointing downwards",iconArrowRight:"Arrow pointing to the right",iconArrowLeft:"Arrow pointing to the left"},s={"left-to-right":"ltr","right-to-left":"rtl","top-to-bottom":"ttb","bottom-to-top":"btt"},u=Object.values(s),d={enabled:!0,itemsToShow:1,itemsToScroll:1,modelValue:0,transition:300,autoplay:0,gap:0,height:"auto",wrapAround:!1,pauseAutoplayOnHover:!1,mouseDrag:!0,touchDrag:!0,snapAlign:n[0],dir:l[0],breakpointMode:a[0],breakpoints:void 0,i18n:r,ignoreAnimations:!1,slideEffect:o[0]},c=e=>{const i=t.shallowReactive([]),n=t.shallowReactive([]),o=e=>{void 0!==e?i.slice(e).forEach(((t,i)=>{var n;null===(n=t.exposed)||void 0===n||n.setIndex(e+i)})):i.forEach(((e,t)=>{var i;null===(i=e.exposed)||void 0===i||i.setIndex(t)}))};return{registerSlide:(t,a)=>{if(!t)return;if(t.props.isClone)return void n.push(t);const l=null!=a?a:i.length;i.splice(l,0,t),o(l),e("slide-registered",{slide:t,index:l})},unregisterSlide:t=>{const n=i.indexOf(t);-1!==n&&(e("slide-unregistered",{slide:t,index:n}),i.splice(n,1),o(n))},cleanup:()=>{i.splice(0,i.length)},getSlides:()=>i,getClonedSlides:()=>n}};function v({val:e,max:t,min:i}){return t<i?e:Math.min(Math.max(e,isNaN(i)?e:i),isNaN(t)?e:t)}const p=(e,t)=>{switch(e){default:case"start":return 0;case"center":case"center-odd":return(t-1)/2;case"center-even":return(t-2)/2;case"end":return t-1}};function m({val:e,max:t,min:i=0}){const n=t-i+1;return((e-i)%n+n)%n+i}function f(e="",t={}){return Object.entries(t).reduce(((e,[t,i])=>e.replace(`{${t}}`,String(i))),e)}function g(e,t=0){let i=!1,n=0,o=null;function a(...a){if(i)return;i=!0;const l=()=>{o=requestAnimationFrame((o=>{o-n>t?(n=o,e(...a),i=!1):l()}))};l()}return a.cancel=()=>{o&&(cancelAnimationFrame(o),o=null,i=!1)},a}function h({slides:e,position:i,toShow:n}){const o=[],a="before"===i,l=a?-n:0,r=a?0:n;if(e.length<=0)return o;for(let n=l;n<r;n++){const l={index:a?n:n+e.length,isClone:!0,id:void 0,key:`clone-${i}-${n}`},r=e[(n%e.length+e.length)%e.length].vnode,s=t.cloneVNode(r,l);s.el=null,o.push(s)}return o}function w(e){if(!(e.el&&e.el instanceof Element))return;const t=e.el.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');for(const e of t)e instanceof HTMLElement&&!e.hasAttribute("disabled")&&"true"!==e.getAttribute("aria-hidden")&&e.setAttribute("tabindex","-1")}const S=t.defineComponent({name:"CarouselAria",setup(){const e=t.inject(i);return e?()=>t.h("div",{class:["carousel__liveregion","carousel__sr-only"],"aria-live":"polite","aria-atomic":"true"},f(e.config.i18n.itemXofY,{currentSlide:e.currentSlide+1,slidesCount:e.slidesCount})):()=>""}}),b={enabled:{default:d.enabled,type:Boolean},itemsToShow:{default:d.itemsToShow,type:Number},itemsToScroll:{default:d.itemsToScroll,type:Number},wrapAround:{default:d.wrapAround,type:Boolean},gap:{default:d.gap,type:Number},height:{default:d.height,type:[Number,String]},snapAlign:{default:d.snapAlign,validator:e=>n.includes(e)},transition:{default:d.transition,type:Number},breakpointMode:{default:d.breakpointMode,validator:e=>a.includes(e)},breakpoints:{default:d.breakpoints,type:Object},autoplay:{default:d.autoplay,type:Number},pauseAutoplayOnHover:{default:d.pauseAutoplayOnHover,type:Boolean},modelValue:{default:void 0,type:Number},mouseDrag:{default:d.mouseDrag,type:Boolean},touchDrag:{default:d.touchDrag,type:Boolean},dir:{type:String,default:d.dir,validator:e=>l.includes(e)},i18n:{default:d.i18n,type:Object},ignoreAnimations:{default:!1,type:[Array,Boolean,String]},slideEffect:{type:String,default:d.slideEffect,validator:e=>o.includes(e)}},y=t.defineComponent({name:"VueCarousel",props:b,emits:["init","drag","slide-start","loop","update:modelValue","slide-end","before-init","slide-registered","slide-unregistered"],setup(e,{slots:n,emit:o,expose:a}){var l;const r=c(o),u=r.getSlides(),f=t.computed((()=>u.length)),w=t.ref(null),b=t.ref(null),y=t.ref(0),x=t.computed((()=>{return Object.assign(Object.assign(Object.assign({},d),(t=e,i=["breakpoints","modelValue"],Object.keys(t).filter((e=>!i.includes(e))).reduce(((e,i)=>(e[i]=t[i],e)),{}))),{i18n:Object.assign(Object.assign({},d.i18n),e.i18n)});var t,i})),A=t.shallowReactive(Object.assign({},x.value)),T=t.ref(null!==(l=e.modelValue)&&void 0!==l?l:0),C=t.ref(T.value);t.watch(T,(e=>C.value=e));const _=t.ref(0),N=t.computed((()=>Math.ceil((f.value-1)/2))),O=t.computed((()=>function({config:e,slidesCount:t}){const{snapAlign:i="center",wrapAround:n,itemsToShow:o=1}=e;return Math.max(function(){switch(n?"":i){case"start":return Math.ceil(t-o);case"center":case"center-odd":return t-Math.ceil((o-.5)/2);case"center-even":return t-Math.ceil(o/2);default:return Math.ceil(t-1)}}(),0)}({config:A,slidesCount:f.value}))),E=t.computed((()=>function({config:e,slidesCount:t}){const{snapAlign:i="center",wrapAround:n,itemsToShow:o=1}=e;return n||o>t?0:Math.max(0,Math.floor(p(i,o)))}({config:A,slidesCount:f.value})));let I=null,L=null,M=null;const k=t.computed((()=>y.value+A.gap)),D=t.computed((()=>{const e=A.dir||"ltr";return e in s?s[e]:e})),R=t.computed((()=>["rtl","btt"].includes(D.value))),j=t.computed((()=>["ttb","btt"].includes(D.value)));function B(){var t;if(!q.value)return;const i=("carousel"===x.value.breakpointMode?null===(t=w.value)||void 0===t?void 0:t.getBoundingClientRect().width:"undefined"!=typeof window?window.innerWidth:0)||0,n=Object.keys(e.breakpoints||{}).map((e=>Number(e))).sort(((e,t)=>+t-+e)),o={};n.some((t=>i>=t&&(Object.assign(o,e.breakpoints[t]),o.i18n&&Object.assign(o.i18n,x.value.i18n,e.breakpoints[t].i18n),!0))),Object.assign(A,x.value,o)}const P=g((()=>{B(),U(),F()})),V=t.computed((()=>(A.itemsToShow-1)*A.gap)),z=t.shallowReactive(new Set);function F(){if(!b.value)return;let e=1;if(z.forEach((t=>{const i=function(e){const{transform:t}=window.getComputedStyle(e);return t.split(/[(,)]/).slice(1,-1).map((e=>parseFloat(e)))}(t);6===i.length&&(e*=i[0])})),j.value){if("auto"!==A.height){const e="string"==typeof A.height&&isNaN(parseInt(A.height))?b.value.getBoundingClientRect().height:parseInt(A.height);y.value=(e-V.value)/A.itemsToShow}}else{const t=b.value.getBoundingClientRect().width;y.value=(t/e-V.value)/A.itemsToShow}}function U(){!A.wrapAround&&f.value>0&&(T.value=v({val:T.value,max:O.value,min:E.value})),A.itemsToShow=v({val:A.itemsToShow,max:f.value,min:1})}const $=t.computed((()=>"string"==typeof e.ignoreAnimations?e.ignoreAnimations.split(","):Array.isArray(e.ignoreAnimations)?e.ignoreAnimations:!e.ignoreAnimations&&[]));let X;t.watchEffect((()=>U())),t.watchEffect((()=>{F()}));const Y=e=>{const t=e.target;if(!(!(null==t?void 0:t.contains(w.value))||Array.isArray($.value)&&$.value.includes(e.animationName)||(z.add(t),X))){const e=()=>{X=requestAnimationFrame((()=>{F(),e()}))};e()}},G=e=>{const t=e.target;t&&z.delete(t),X&&0===z.size&&(cancelAnimationFrame(X),F())},q=t.ref(!1);"undefined"!=typeof document&&t.watchEffect((()=>{q.value&&!1!==$.value?(document.addEventListener("animationstart",Y),document.addEventListener("animationend",G)):(document.removeEventListener("animationstart",Y),document.removeEventListener("animationend",G))})),t.onMounted((()=>{q.value=!0,B(),re(),w.value&&(M=new ResizeObserver(P),M.observe(w.value)),o("init")})),t.onBeforeUnmount((()=>{q.value=!1,r.cleanup(),L&&clearTimeout(L),X&&cancelAnimationFrame(X),I&&clearInterval(I),M&&(M.disconnect(),M=null),"undefined"!=typeof document&&ne(),w.value&&(w.value.removeEventListener("transitionend",F),w.value.removeEventListener("animationiteration",F))}));let H=!1;const W={x:0,y:0},K=t.reactive({x:0,y:0}),Z=t.ref(!1),J=t.ref(!1),Q=()=>{Z.value=!0},ee=()=>{Z.value=!1},te=g((e=>{if(!e.ctrlKey)switch(e.key){case"ArrowLeft":case"ArrowUp":j.value===e.key.endsWith("Up")&&(R.value?me.next(!0):me.prev(!0));break;case"ArrowRight":case"ArrowDown":j.value===e.key.endsWith("Down")&&(R.value?me.prev(!0):me.next(!0))}}),200),ie=()=>{document.addEventListener("keydown",te)},ne=()=>{document.removeEventListener("keydown",te)};function oe(e){const t=e.target.tagName;if(["INPUT","TEXTAREA","SELECT"].includes(t)||de.value)return;if(H="touchstart"===e.type,!H&&(e.preventDefault(),0!==e.button))return;W.x="touches"in e?e.touches[0].clientX:e.clientX,W.y="touches"in e?e.touches[0].clientY:e.clientY;const i=H?"touchmove":"mousemove",n=H?"touchend":"mouseup";document.addEventListener(i,ae,{passive:!1}),document.addEventListener(n,le,{passive:!0})}const ae=g((e=>{J.value=!0;const t="touches"in e?e.touches[0].clientX:e.clientX,i="touches"in e?e.touches[0].clientY:e.clientY;K.x=t-W.x,K.y=i-W.y;const n=function(e){const{isVertical:t,isReversed:i,dragged:n,effectiveSlideSize:o}=e,a=t?n.y:n.x;if(0===a)return 0;const l=Math.round(a/o);return i?l:-l}({isVertical:j.value,isReversed:R.value,dragged:K,effectiveSlideSize:k.value});C.value=A.wrapAround?T.value+n:v({val:T.value+n,max:O.value,min:E.value}),o("drag",{deltaX:K.x,deltaY:K.y})}));function le(){if(ae.cancel(),C.value!==T.value&&!H){const e=t=>{t.preventDefault(),window.removeEventListener("click",e)};window.addEventListener("click",e)}ce(C.value),K.x=0,K.y=0,J.value=!1;const e=H?"touchmove":"mousemove",t=H?"touchend":"mouseup";document.removeEventListener(e,ae),document.removeEventListener(t,le)}function re(){!A.autoplay||A.autoplay<=0||(I=setInterval((()=>{A.pauseAutoplayOnHover&&Z.value||ve()}),A.autoplay))}function se(){I&&(clearInterval(I),I=null)}function ue(){se(),re()}const de=t.ref(!1);function ce(e,t=!1){if(!t&&de.value)return;let i=e,n=e;_.value=T.value,A.wrapAround?n=m({val:i,max:O.value,min:0}):i=v({val:i,max:O.value,min:E.value}),o("slide-start",{slidingToIndex:e,currentSlideIndex:T.value,prevSlideIndex:_.value,slidesCount:f.value}),se(),de.value=!0,T.value=i,n!==i&&he.pause(),o("update:modelValue",n);L=setTimeout((()=>{A.wrapAround&&n!==i&&(he.resume(),T.value=n,o("loop",{currentSlideIndex:T.value,slidingToIndex:e})),o("slide-end",{currentSlideIndex:T.value,prevSlideIndex:_.value,slidesCount:f.value}),de.value=!1,ue()}),A.transition)}function ve(e=!1){ce(T.value+A.itemsToScroll,e)}function pe(e=!1){ce(T.value-A.itemsToScroll,e)}const me={slideTo:ce,next:ve,prev:pe},fe=t.computed((()=>function({config:e,currentSlide:t,slidesCount:i}){const{snapAlign:n="center",wrapAround:o,itemsToShow:a=1}=e,l=p(n,a);return o?t-l:v({val:t-l,max:i-a,min:0})}({config:A,currentSlide:T.value,slidesCount:f.value}))),ge=t.reactive({config:A,slidesCount:f,viewport:b,slides:u,scrolledIndex:fe,currentSlide:T,activeSlide:C,maxSlide:O,minSlide:E,slideSize:y,isVertical:j,normalizedDir:D,nav:me,isSliding:de,slideRegistry:r});t.provide(i,ge),t.provide("config",A),t.provide("slidesCount",f),t.provide("currentSlide",T),t.provide("maxSlide",O),t.provide("minSlide",E),t.provide("slideSize",y),t.provide("isVertical",j),t.provide("normalizeDir",D),t.provide("nav",me),t.provide("isSliding",de),t.watch((()=>[x.value,e.breakpoints]),(()=>B()),{deep:!0}),t.watch((()=>e.autoplay),(()=>ue()));const he=t.watch((()=>e.modelValue),(e=>{e!==T.value&&ce(Number(e),!0)}));o("before-init");const we=t.reactive({config:A,slidesCount:f,slideSize:y,currentSlide:T,maxSlide:O,minSlide:E,middleSlide:N});a({updateBreakpointsConfig:B,updateSlidesData:U,updateSlideSize:F,restartCarousel:function(){B(),U(),F(),ue()},slideTo:ce,next:ve,prev:pe,nav:me,data:we});const Se=t.computed((()=>"auto"===A.height?j.value&&y.value?`${y.value*A.itemsToShow+V.value}px`:void 0:"number"==typeof A.height||parseFloat(A.height).toString()===A.height?`${A.height}px`:A.height)),be=t.computed((()=>{if(!A.wrapAround)return{before:0,after:0};const e=Math.ceil(A.itemsToShow+(A.itemsToScroll-1)),t=e-C.value,i=e-(f.value-(C.value+1));return{before:Math.max(0,t),after:Math.max(0,i)}})),ye=t.computed((()=>be.value.before*k.value*-1)),xe=t.computed((()=>{const e=R.value?1:-1;return`translate${j.value?"Y":"X"}(${fe.value*k.value*e+(j.value?K.y:K.x)}px)`})),Ae=t.computed((()=>({transform:"slide"===A.slideEffect?xe.value:void 0,gap:A.gap>0?`${A.gap}px`:void 0,"--vc-trk-transition-duration":de.value?`${A.transition}ms`:void 0,"--vc-trk-height":Se.value,"--vc-trk-cloned-offset":`${ye.value}px`})));return()=>{var e;const i=n.default||n.slides,o=(null==i?void 0:i(we))||[],{before:a,after:l}=be.value,r=[...h({slides:u,position:"before",toShow:a}),...o,...h({slides:u,position:"after",toShow:l})];if(!A.enabled||!r.length)return t.h("section",{ref:w,class:["carousel","is-disabled"]},r);const s=(null===(e=n.addons)||void 0===e?void 0:e.call(n,we))||[],d=t.h("ol",{class:"carousel__track",style:Ae.value,onMousedownCapture:A.mouseDrag?oe:null,onTouchstartPassiveCapture:A.touchDrag?oe:null},r),c=t.h("div",{class:"carousel__viewport",ref:b},d);return t.h("section",{ref:w,class:["carousel",`is-${D.value}`,`is-effect-${A.slideEffect}`,{"is-vertical":j.value,"is-sliding":de.value,"is-dragging":J.value,"is-hover":Z.value}],dir:D.value,"aria-label":A.i18n.ariaGallery,tabindex:"0",onFocus:ie,onBlur:ne,onMouseenter:Q,onMouseleave:ee},[c,s,t.h(S)])}}});var x;!function(e){e.arrowUp="arrowUp",e.arrowDown="arrowDown",e.arrowRight="arrowRight",e.arrowLeft="arrowLeft"}(x||(x={}));const A=e=>`icon${e.charAt(0).toUpperCase()+e.slice(1)}`,T=e=>e&&e in x,C={arrowUp:"M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z",arrowDown:"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z",arrowRight:"M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z",arrowLeft:"M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z"},_=t.defineComponent({props:{name:{type:String,required:!0,validator:T},title:{type:String,default:e=>e.name?d.i18n[A(e.name)]:""}},setup(e){const n=t.inject(i,null);return()=>{const i=e.name;if(!i||!T(i))return;const o=C[i],a=t.h("path",{d:o}),l=(null==n?void 0:n.config.i18n[A(i)])||e.title,r=t.h("title",l);return t.h("svg",{class:"carousel__icon",viewBox:"0 0 24 24",role:"img","aria-label":l},[r,a])}}}),N=t.defineComponent({name:"CarouselNavigation",inheritAttrs:!1,setup(e,{slots:n,attrs:o}){const a=t.inject(i);if(!a)return()=>"";const{next:l,prev:r}=n,s=t.computed((()=>!a.config.wrapAround&&a.currentSlide<=a.minSlide)),u=t.computed((()=>!a.config.wrapAround&&a.currentSlide>=a.maxSlide));return()=>{const{i18n:e}=a.config;return[t.h("button",Object.assign(Object.assign({type:"button",disabled:s.value,"aria-label":e.ariaPreviousSlide,title:e.ariaPreviousSlide,onClick:a.nav.prev},o),{class:["carousel__prev",{"carousel__prev--disabled":s.value},o.class]}),(null==r?void 0:r())||t.h(_,{name:{ltr:"arrowLeft",rtl:"arrowRight",ttb:"arrowUp",btt:"arrowDown"}[a.normalizedDir]})),t.h("button",Object.assign(Object.assign({type:"button",disabled:u.value,"aria-label":e.ariaNextSlide,title:e.ariaNextSlide,onClick:a.nav.next},o),{class:["carousel__next",{"carousel__next--disabled":u.value},o.class]}),(null==l?void 0:l())||t.h(_,{name:{ltr:"arrowRight",rtl:"arrowLeft",ttb:"arrowDown",btt:"arrowUp"}[a.normalizedDir]}))]}}}),O=t.defineComponent({name:"CarouselPagination",props:{disableOnClick:{type:Boolean},paginateByItemsToShow:{type:Boolean}},setup(e){const n=t.inject(i);if(!n)return()=>"";const o=t.computed((()=>p(n.config.snapAlign,n.config.itemsToShow))),a=t.computed((()=>e.paginateByItemsToShow&&n.config.itemsToShow>1)),l=t.computed((()=>Math.ceil((n.activeSlide-o.value)/n.config.itemsToShow))),r=t.computed((()=>Math.ceil(n.slidesCount/n.config.itemsToShow))),s=e=>m(a.value?{val:l.value,max:r.value-1,min:0}:{val:n.activeSlide,max:n.maxSlide,min:n.minSlide})===e;return()=>{var i,l;const u=[];for(let d=a.value?0:n.minSlide;d<=(a.value?r.value-1:n.maxSlide);d++){const r=f(n.config.i18n[a.value?"ariaNavigateToPage":"ariaNavigateToSlide"],{slideNumber:d+1}),c=s(d),v=t.h("button",{type:"button",class:{"carousel__pagination-button":!0,"carousel__pagination-button--active":c},"aria-label":r,"aria-pressed":c,"aria-controls":null===(l=null===(i=n.slides[d])||void 0===i?void 0:i.exposed)||void 0===l?void 0:l.id,title:r,disabled:e.disableOnClick,onClick:()=>n.nav.slideTo(a.value?d*n.config.itemsToShow+o.value:d)}),p=t.h("li",{class:"carousel__pagination-item",key:d},v);u.push(p)}return t.h("ol",{class:"carousel__pagination"},u)}}}),E=t.defineComponent({name:"CarouselSlide",props:{isClone:{type:Boolean,default:!1},id:{type:String,default:e=>e.isClone?void 0:t.useId()},index:{type:Number,default:void 0}},setup(e,{slots:n,expose:o}){const a=t.inject(i);if(t.provide(i,void 0),!a)return()=>"";const l=t.ref(e.index);o({id:e.id,setIndex:e=>{l.value=e}});const r=t.computed((()=>l.value===a.activeSlide)),s=t.computed((()=>l.value===a.activeSlide-1)),u=t.computed((()=>l.value===a.activeSlide+1)),d=t.computed((()=>l.value>=Math.floor(a.scrolledIndex)&&l.value<Math.ceil(a.scrolledIndex)+a.config.itemsToShow)),c=t.computed((()=>{const e=a.config.gap>0&&a.config.itemsToShow>1?`calc(${100/a.config.itemsToShow}% - ${a.config.gap*(a.config.itemsToShow-1)/a.config.itemsToShow}px)`:100/a.config.itemsToShow+"%";return a.isVertical?{height:e}:{width:e}})),v=t.getCurrentInstance();return a.slideRegistry.registerSlide(v,e.index),t.onUnmounted((()=>{a.slideRegistry.unregisterSlide(v)})),e.isClone&&(t.onMounted((()=>{w(v.vnode)})),t.onUpdated((()=>{w(v.vnode)}))),()=>{var i,o;return a.config.enabled?t.h("li",{style:c.value,class:{carousel__slide:!0,"carousel__slide--clone":e.isClone,"carousel__slide--visible":d.value,"carousel__slide--active":r.value,"carousel__slide--prev":s.value,"carousel__slide--next":u.value,"carousel__slide--sliding":a.isSliding},onFocusin:()=>{a.viewport&&(a.viewport.scrollLeft=0),a.nav.slideTo(l.value)},id:e.isClone?void 0:e.id,"aria-hidden":e.isClone||void 0},null===(o=n.default)||void 0===o?void 0:o.call(n,{isActive:r.value,isClone:e.isClone,isPrev:s.value,isNext:u.value,isSliding:a.isSliding,isVisible:d.value})):null===(i=n.default)||void 0===i?void 0:i.call(n)}}});e.BREAKPOINT_MODE_OPTIONS=a,e.Carousel=y,e.DEFAULT_CONFIG=d,e.DIR_MAP=s,e.DIR_OPTIONS=l,e.I18N_DEFAULT_CONFIG=r,e.Icon=_,e.NORMALIZED_DIR_OPTIONS=u,e.Navigation=N,e.Pagination=O,e.SLIDE_EFFECTS=o,e.SNAP_ALIGN_OPTIONS=n,e.Slide=E,e.createSlideRegistry=c,e.icons=C,e.injectCarousel=i})); | ||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("vue")):"function"==typeof define&&define.amd?define(["exports","vue"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).VueCarousel={},e.Vue)}(this,(function(e,t){"use strict";const i=["viewport","carousel"],n={"bottom-to-top":"btt","left-to-right":"ltr","right-to-left":"rtl","top-to-bottom":"ttb"},a=["ltr","left-to-right","rtl","right-to-left","ttb","top-to-bottom","btt","bottom-to-top"],l={ariaGallery:"Gallery",ariaNavigateToPage:"Navigate to page {slideNumber}",ariaNavigateToSlide:"Navigate to slide {slideNumber}",ariaNextSlide:"Navigate to next slide",ariaPreviousSlide:"Navigate to previous slide",iconArrowDown:"Arrow pointing downwards",iconArrowLeft:"Arrow pointing to the left",iconArrowRight:"Arrow pointing to the right",iconArrowUp:"Arrow pointing upwards",itemXofY:"Item {currentSlide} of {slidesCount}"},o=Object.values(n),r=["slide","fade"],u=["center","start","end","center-even","center-odd"],s={autoplay:0,breakpointMode:i[0],breakpoints:void 0,dir:a[0],enabled:!0,gap:0,height:"auto",i18n:l,ignoreAnimations:!1,itemsToScroll:1,itemsToShow:1,modelValue:0,mouseDrag:!0,pauseAutoplayOnHover:!1,preventExcessiveDragging:!1,slideEffect:r[0],snapAlign:u[0],touchDrag:!0,transition:300,wrapAround:!1},d=Symbol("carousel"),c=e=>{const i=t.shallowReactive([]),n=e=>{void 0!==e?i.slice(e).forEach(((t,i)=>{var n;null===(n=t.exposed)||void 0===n||n.setIndex(e+i)})):i.forEach(((e,t)=>{var i;null===(i=e.exposed)||void 0===i||i.setIndex(t)}))};return{cleanup:()=>{i.splice(0,i.length)},getSlides:()=>i,registerSlide:(t,a)=>{if(!t)return;if(t.props.isClone)return;const l=null!=a?a:i.length;i.splice(l,0,t),n(l),e("slide-registered",{slide:t,index:l})},unregisterSlide:t=>{const a=i.indexOf(t);-1!==a&&(e("slide-unregistered",{slide:t,index:a}),i.splice(a,1),n(a))}}};function v({slides:e,position:i,toShow:n}){const a=[],l="before"===i,o=l?-n:0,r=l?0:n;if(e.length<=0)return a;for(let n=o;n<r;n++){const o={index:l?n:n+e.length,isClone:!0,position:i,id:void 0,key:`clone-${i}-${n}`},r=e[(n%e.length+e.length)%e.length].vnode,u=t.cloneVNode(r,o);u.el=null,a.push(u)}return a}function p(e){if(!(e.el&&e.el instanceof Element))return;const t=e.el.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');for(const e of t)e instanceof HTMLElement&&!e.hasAttribute("disabled")&&"true"!==e.getAttribute("aria-hidden")&&e.setAttribute("tabindex","-1")}function m({val:e,max:t,min:i}){return t<i?e:Math.min(Math.max(e,isNaN(i)?e:i),isNaN(t)?e:t)}function g(e){let t=1,i=1;return e.forEach((e=>{const n=function(e){const{transform:t}=window.getComputedStyle(e);return t.split(/[(,)]/).slice(1,-1).map((e=>parseFloat(e)))}(e);6===n.length&&(t/=n[0],i/=n[3])})),{widthMultiplier:t,heightMultiplier:i}}function f({slideSize:e,viewportSize:t,align:i,itemsToShow:n}){return void 0!==n?function(e,t){switch(e){case"start":default:return 0;case"center":case"center-odd":return(t-1)/2;case"center-even":return(t-2)/2;case"end":return t-1}}(i,n):void 0!==e&&void 0!==t?function(e,t,i){switch(e){case"start":default:return 0;case"center":case"center-odd":return(i-t)/2;case"center-even":return i/2-t;case"end":return i-t}}(i,e,t):0}function h(e="",t={}){return Object.entries(t).reduce(((e,[t,i])=>e.replace(`{${t}}`,String(i))),e)}function w({val:e,max:t,min:i=0}){const n=t-i+1;return((e-i)%n+n)%n+i}function b(e,t=0){let i=!1,n=0,a=null;function l(...l){if(i)return;i=!0;const o=()=>{a=requestAnimationFrame((a=>{a-n>t?(n=a,e(...l),i=!1):o()}))};o()}return l.cancel=()=>{a&&(cancelAnimationFrame(a),a=null,i=!1)},l}function S(e,t="px"){if(null!=e&&""!==e)return"number"==typeof e||parseFloat(e).toString()===e?`${e}${t}`:e}const y=t.defineComponent({name:"CarouselAria",setup(){const e=t.inject(d);return e?()=>t.h("div",{class:["carousel__liveregion","carousel__sr-only"],"aria-live":"polite","aria-atomic":"true"},h(e.config.i18n.itemXofY,{currentSlide:e.currentSlide+1,slidesCount:e.slidesCount})):()=>""}}),x={autoplay:{default:s.autoplay,type:Number},breakpoints:{default:s.breakpoints,type:Object},breakpointMode:{default:s.breakpointMode,validator:e=>i.includes(e)},enabled:{default:s.enabled,type:Boolean},gap:{default:s.gap,type:Number},height:{default:s.height,type:[Number,String]},ignoreAnimations:{default:!1,type:[Array,Boolean,String]},itemsToScroll:{default:s.itemsToScroll,type:Number},itemsToShow:{default:s.itemsToShow,type:[Number,String]},i18n:{default:s.i18n,type:Object},modelValue:{default:void 0,type:Number},mouseDrag:{default:s.mouseDrag,type:Boolean},touchDrag:{default:s.touchDrag,type:Boolean},pauseAutoplayOnHover:{default:s.pauseAutoplayOnHover,type:Boolean},preventExcessiveDragging:{default:!1,type:Boolean,validator:(e,t)=>(e&&t.wrapAround&&console.warn('[vue3-carousel warn]: "preventExcessiveDragging" cannot be used with wrapAround. The setting will be ignored.'),!0)},snapAlign:{default:s.snapAlign,validator:e=>u.includes(e)},slideEffect:{type:String,default:s.slideEffect,validator:e=>r.includes(e)},transition:{default:s.transition,type:Number},dir:{type:String,default:s.dir,validator(e,t){if(!a.includes(e))return!1;return!["ttb","btt"].includes(e in n?n[e]:e)||t.height&&"auto"!==t.height||console.warn(`[vue3-carousel warn]: The dir "${e}" is not supported with height "auto".`),!0}},wrapAround:{default:s.wrapAround,type:Boolean}},A=t.defineComponent({name:"VueCarousel",props:x,emits:["before-init","drag","init","loop","slide-end","slide-registered","slide-start","slide-unregistered","update:modelValue"],setup(e,{slots:i,emit:a,expose:l}){var o;const r=c(a),u=r.getSlides(),p=t.computed((()=>u.length)),h=t.ref(null),x=t.ref(null),A=t.ref(0),T=t.computed((()=>{return Object.assign(Object.assign(Object.assign({},s),(t=e,i=["breakpoints","modelValue"],Object.keys(t).filter((e=>!i.includes(e))).reduce(((e,i)=>(e[i]=t[i],e)),{}))),{i18n:Object.assign(Object.assign({},s.i18n),e.i18n)});var t,i})),N=t.shallowReactive(Object.assign({},T.value)),_=t.ref(null!==(o=e.modelValue)&&void 0!==o?o:0),C=t.ref(_.value);t.watch(_,(e=>C.value=e));const E=t.ref(0),M=t.computed((()=>Math.ceil((p.value-1)/2))),O=t.computed((()=>p.value-1)),L=t.computed((()=>0));let I=null,D=null,k=null;const R=t.computed((()=>A.value+N.gap)),j=t.computed((()=>{const e=N.dir||"ltr";return e in n?n[e]:e})),B=t.computed((()=>["rtl","btt"].includes(j.value))),z=t.computed((()=>["ttb","btt"].includes(j.value))),P=t.computed((()=>"auto"===N.itemsToShow)),V=t.computed((()=>z.value?"height":"width"));function F(){var t;if(!J.value)return;const i=("carousel"===T.value.breakpointMode?null===(t=h.value)||void 0===t?void 0:t.getBoundingClientRect().width:"undefined"!=typeof window?window.innerWidth:0)||0,n=Object.keys(e.breakpoints||{}).map((e=>Number(e))).sort(((e,t)=>+t-+e)),a={};n.some((t=>i>=t&&(Object.assign(a,e.breakpoints[t]),a.i18n&&Object.assign(a.i18n,T.value.i18n,e.breakpoints[t].i18n),!0))),Object.assign(N,T.value,a)}const U=b((()=>{F(),q(),G()})),$=t.shallowReactive(new Set),X=t.ref([]);const Y=t.ref({width:0,height:0});function G(){if(!x.value)return;const e=g($);if(function({widthMultiplier:e,heightMultiplier:t}){var i;const n=(null===(i=x.value)||void 0===i?void 0:i.getBoundingClientRect())||{width:0,height:0};Y.value={width:n.width*e,height:n.height*t}}(e),function({widthMultiplier:e,heightMultiplier:t}){X.value=u.map((i=>{var n;const a=null===(n=i.exposed)||void 0===n?void 0:n.getBoundingRect();return{width:a.width*e,height:a.height*t}}))}(e),P.value)A.value=0===(t=X.value.map((e=>e[V.value]))).length?0:t.reduce(((e,t)=>e+t),0)/t.length;else{const e=Number(N.itemsToShow),t=(e-1)*N.gap;A.value=(Y.value[V.value]-t)/e}var t}function q(){!N.wrapAround&&p.value>0&&(_.value=m({val:_.value,max:O.value,min:L.value})),P.value||(N.itemsToShow=m({val:Number(N.itemsToShow),max:p.value,min:1}))}const H=t.computed((()=>"string"==typeof e.ignoreAnimations?e.ignoreAnimations.split(","):Array.isArray(e.ignoreAnimations)?e.ignoreAnimations:!e.ignoreAnimations&&[]));let W;t.watchEffect((()=>q())),t.watchEffect((()=>{G()}));const K=e=>{const t=e.target;if(!(!(null==t?void 0:t.contains(h.value))||Array.isArray(H.value)&&H.value.includes(e.animationName)||($.add(t),W))){const e=()=>{W=requestAnimationFrame((()=>{G(),e()}))};e()}},Z=e=>{const t=e.target;t&&$.delete(t),W&&0===$.size&&(cancelAnimationFrame(W),G())},J=t.ref(!1);"undefined"!=typeof document&&t.watchEffect((()=>{J.value&&!1!==H.value?(document.addEventListener("animationstart",K),document.addEventListener("animationend",Z)):(document.removeEventListener("animationstart",K),document.removeEventListener("animationend",Z))})),t.onMounted((()=>{J.value=!0,F(),ve(),h.value&&(k=new ResizeObserver(U),k.observe(h.value)),a("init")})),t.onBeforeUnmount((()=>{J.value=!1,r.cleanup(),D&&clearTimeout(D),W&&cancelAnimationFrame(W),I&&clearInterval(I),k&&(k.disconnect(),k=null),"undefined"!=typeof document&&ue(),h.value&&(h.value.removeEventListener("transitionend",G),h.value.removeEventListener("animationiteration",G))}));let Q=!1;const ee={x:0,y:0},te=t.reactive({x:0,y:0}),ie=t.ref(!1),ne=t.ref(!1),ae=()=>{ie.value=!0},le=()=>{ie.value=!1},oe=b((e=>{if(!e.ctrlKey)switch(e.key){case"ArrowLeft":case"ArrowUp":z.value===e.key.endsWith("Up")&&(B.value?he(!0):we(!0));break;case"ArrowRight":case"ArrowDown":z.value===e.key.endsWith("Down")&&(B.value?we(!0):he(!0))}}),200),re=()=>{document.addEventListener("keydown",oe)},ue=()=>{document.removeEventListener("keydown",oe)};function se(e){const t=e.target.tagName;if(["INPUT","TEXTAREA","SELECT"].includes(t)||ge.value)return;if(Q="touchstart"===e.type,!Q&&(e.preventDefault(),0!==e.button))return;ee.x="touches"in e?e.touches[0].clientX:e.clientX,ee.y="touches"in e?e.touches[0].clientY:e.clientY;const i=Q?"touchmove":"mousemove",n=Q?"touchend":"mouseup";document.addEventListener(i,de,{passive:!1}),document.addEventListener(n,ce,{passive:!0})}const de=b((e=>{ne.value=!0;const t="touches"in e?e.touches[0].clientX:e.clientX,i="touches"in e?e.touches[0].clientY:e.clientY;te.x=t-ee.x,te.y=i-ee.y;const n=function(e){const{isVertical:t,isReversed:i,dragged:n,effectiveSlideSize:a}=e,l=t?n.y:n.x;if(0===l)return 0;const o=Math.round(l/a);return i?o:-o}({isVertical:z.value,isReversed:B.value,dragged:te,effectiveSlideSize:R.value});C.value=N.wrapAround?_.value+n:m({val:_.value+n,max:O.value,min:L.value}),a("drag",{deltaX:te.x,deltaY:te.y})}));function ce(){if(de.cancel(),C.value!==_.value&&!Q){const e=t=>{t.preventDefault(),window.removeEventListener("click",e)};window.addEventListener("click",e)}fe(C.value),te.x=0,te.y=0,ne.value=!1;const e=Q?"touchmove":"mousemove",t=Q?"touchend":"mouseup";document.removeEventListener(e,de),document.removeEventListener(t,ce)}function ve(){!N.autoplay||N.autoplay<=0||(I=setInterval((()=>{N.pauseAutoplayOnHover&&ie.value||he()}),N.autoplay))}function pe(){I&&(clearInterval(I),I=null)}function me(){pe(),ve()}const ge=t.ref(!1);function fe(e,t=!1){if(!t&&ge.value)return;let i=e,n=e;E.value=_.value,N.wrapAround?n=w({val:i,max:O.value,min:L.value}):i=m({val:i,max:O.value,min:L.value}),a("slide-start",{slidingToIndex:e,currentSlideIndex:_.value,prevSlideIndex:E.value,slidesCount:p.value}),pe(),ge.value=!0,_.value=i,n!==i&&be.pause(),a("update:modelValue",n);D=setTimeout((()=>{N.wrapAround&&n!==i&&(be.resume(),_.value=n,a("loop",{currentSlideIndex:_.value,slidingToIndex:e})),a("slide-end",{currentSlideIndex:_.value,prevSlideIndex:E.value,slidesCount:p.value}),ge.value=!1,me()}),N.transition)}function he(e=!1){fe(_.value+N.itemsToScroll,e)}function we(e=!1){fe(_.value-N.itemsToScroll,e)}t.watch((()=>[T.value,e.breakpoints]),(()=>F()),{deep:!0}),t.watch((()=>e.autoplay),(()=>me()));const be=t.watch((()=>e.modelValue),(e=>{e!==_.value&&fe(Number(e),!0)}));a("before-init");const Se=t.computed((()=>{if(!N.wrapAround)return{before:0,after:0};if(P.value)return{before:u.length,after:u.length};const e=Number(N.itemsToShow),t=Math.ceil(e+(N.itemsToScroll-1)),i=t-C.value,n=t-(p.value-(C.value+1));return{before:Math.max(0,i),after:Math.max(0,n)}})),ye=t.computed((()=>Se.value.before?P.value?-1*X.value.slice(-1*Se.value.before).reduce(((e,t)=>e+t[V.value]+N.gap),0):Se.value.before*R.value*-1:0)),xe=t.computed((()=>{var e;if(P.value){const t=(_.value%u.length+u.length)%u.length;return f({slideSize:null===(e=X.value[t])||void 0===e?void 0:e[V.value],viewportSize:Y.value[V.value],align:N.snapAlign})}return f({align:N.snapAlign,itemsToShow:+N.itemsToShow})})),Ae=t.computed((()=>{let e=0;if(P.value){if(e=_.value<0?-1*X.value.slice(_.value).reduce(((e,t)=>e+t[V.value]+N.gap),0):X.value.slice(0,_.value).reduce(((e,t)=>e+t[V.value]+N.gap),0),e-=xe.value,!N.wrapAround){e=m({val:e,max:X.value.reduce(((e,t)=>e+t[V.value]+N.gap),0)-Y.value[V.value]-N.gap,min:0})}}else{let t=_.value-xe.value;N.wrapAround||(t=m({val:t,max:p.value-+N.itemsToShow,min:0})),e=t*R.value}return e*(B.value?1:-1)})),Te=t.computed((()=>{var e,t;if(!P.value){const e=_.value-xe.value;return N.wrapAround?{min:Math.floor(e),max:Math.ceil(e+Number(N.itemsToShow)-1)}:{min:Math.floor(m({val:e,max:p.value-Number(N.itemsToShow),min:0})),max:Math.ceil(m({val:e+Number(N.itemsToShow)-1,max:p.value-1,min:0}))}}let i=0;{let t=0,n=0-Se.value.before;const a=Math.abs(Ae.value+ye.value);for(;t<=a;){const i=(n%u.length+u.length)%u.length;t+=(null===(e=X.value[i])||void 0===e?void 0:e[V.value])+N.gap,n++}i=n-1}let n=0;{let e=i,a=0;for(a=e<0?X.value.slice(0,e).reduce(((e,t)=>e+t[V.value]+N.gap),0)-Math.abs(Ae.value+ye.value):X.value.slice(0,e).reduce(((e,t)=>e+t[V.value]+N.gap),0)-Math.abs(Ae.value);a<Y.value[V.value];){const i=(e%u.length+u.length)%u.length;a+=(null===(t=X.value[i])||void 0===t?void 0:t[V.value])+N.gap,e++}n=e-1}return{min:Math.floor(i),max:Math.ceil(n)}})),Ne=t.computed((()=>{if("fade"===N.slideEffect)return;const e=z.value?"Y":"X",t=z.value?te.y:te.x;let i=Ae.value+t;if(!N.wrapAround&&N.preventExcessiveDragging){let e=0;e=P.value?X.value.reduce(((e,t)=>e+t[V.value]),0):(p.value-Number(N.itemsToShow))*R.value;i=m({val:i,min:B.value?0:-1*e,max:B.value?e:0})}return`translate${e}(${i}px)`})),_e=t.computed((()=>({"--vc-transition-duration":ge.value?S(N.transition,"ms"):void 0,"--vc-slide-gap":S(N.gap),"--vc-carousel-height":S(N.height),"--vc-cloned-offset":S(ye.value)}))),Ce={slideTo:fe,next:he,prev:we},Ee=t.reactive({activeSlide:C,config:N,currentSlide:_,isSliding:ge,isVertical:z,maxSlide:O,minSlide:L,nav:Ce,normalizedDir:j,slideRegistry:r,slideSize:A,slides:u,slidesCount:p,viewport:x,visibleRange:Te});t.provide(d,Ee);const Me=t.reactive({config:N,currentSlide:_,maxSlide:O,middleSlide:M,minSlide:L,slideSize:A,slidesCount:p});return l({data:Me,nav:Ce,next:he,prev:we,restartCarousel:function(){F(),q(),G(),me()},slideTo:fe,updateBreakpointsConfig:F,updateSlideSize:G,updateSlidesData:q}),()=>{var e;const n=i.default||i.slides,a=(null==n?void 0:n(Me))||[],{before:l,after:o}=Se.value,r=[...v({slides:u,position:"before",toShow:l}),...a,...v({slides:u,position:"after",toShow:o})];if(!N.enabled||!r.length)return t.h("section",{ref:h,class:["carousel","is-disabled"]},r);const s=(null===(e=i.addons)||void 0===e?void 0:e.call(i,Me))||[],d=t.h("ol",{class:"carousel__track",style:{transform:Ne.value},onMousedownCapture:N.mouseDrag?se:null,onTouchstartPassiveCapture:N.touchDrag?se:null},r),c=t.h("div",{class:"carousel__viewport",ref:x},d);return t.h("section",{ref:h,class:["carousel",`is-${j.value}`,`is-effect-${N.slideEffect}`,{"is-vertical":z.value,"is-sliding":ge.value,"is-dragging":ne.value,"is-hover":ie.value}],dir:j.value,style:_e.value,"aria-label":N.i18n.ariaGallery,tabindex:"0",onFocus:re,onBlur:ue,onMouseenter:ae,onMouseleave:le},[c,s,t.h(y)])}}});var T;!function(e){e.arrowDown="arrowDown",e.arrowLeft="arrowLeft",e.arrowRight="arrowRight",e.arrowUp="arrowUp"}(T||(T={}));const N=e=>`icon${e.charAt(0).toUpperCase()+e.slice(1)}`,_={arrowDown:"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z",arrowLeft:"M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z",arrowRight:"M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z",arrowUp:"M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"};const C=e=>e&&e in T,E=t.defineComponent({props:{name:{type:String,required:!0,validator:C},title:{type:String,default:e=>e.name?s.i18n[N(e.name)]:""}},setup(e){const i=t.inject(d,null);return()=>{const n=e.name;if(!n||!C(n))return;const a=_[n],l=t.h("path",{d:a}),o=(null==i?void 0:i.config.i18n[N(n)])||e.title,r=t.h("title",o);return t.h("svg",{class:"carousel__icon",viewBox:"0 0 24 24",role:"img","aria-label":o},[r,l])}}}),M=t.defineComponent({name:"CarouselNavigation",inheritAttrs:!1,setup(e,{slots:i,attrs:n}){const a=t.inject(d);if(!a)return()=>"";const{next:l,prev:o}=i,r=t.computed((()=>!a.config.wrapAround&&a.currentSlide<=a.minSlide)),u=t.computed((()=>!a.config.wrapAround&&a.currentSlide>=a.maxSlide));return()=>{const{i18n:e}=a.config;return[t.h("button",Object.assign(Object.assign({type:"button",disabled:r.value,"aria-label":e.ariaPreviousSlide,title:e.ariaPreviousSlide,onClick:a.nav.prev},n),{class:["carousel__prev",{"carousel__prev--disabled":r.value},n.class]}),(null==o?void 0:o())||t.h(E,{name:{btt:"arrowDown",ltr:"arrowLeft",rtl:"arrowRight",ttb:"arrowUp"}[a.normalizedDir]})),t.h("button",Object.assign(Object.assign({type:"button",disabled:u.value,"aria-label":e.ariaNextSlide,title:e.ariaNextSlide,onClick:a.nav.next},n),{class:["carousel__next",{"carousel__next--disabled":u.value},n.class]}),(null==l?void 0:l())||t.h(E,{name:{btt:"arrowUp",ltr:"arrowRight",rtl:"arrowLeft",ttb:"arrowDown"}[a.normalizedDir]}))]}}}),O=t.defineComponent({name:"CarouselPagination",props:{disableOnClick:{type:Boolean},paginateByItemsToShow:{type:Boolean}},setup(e){const i=t.inject(d);if(!i)return()=>"";const n=t.computed((()=>i.config.itemsToShow)),a=t.computed((()=>f({align:i.config.snapAlign,itemsToShow:n.value}))),l=t.computed((()=>e.paginateByItemsToShow&&n.value>1)),o=t.computed((()=>Math.ceil((i.activeSlide-a.value)/n.value))),r=t.computed((()=>Math.ceil(i.slidesCount/n.value))),u=e=>w(l.value?{val:o.value,max:r.value-1,min:0}:{val:i.activeSlide,max:i.maxSlide,min:i.minSlide})===e;return()=>{var n,o;const s=[];for(let d=l.value?0:i.minSlide;d<=(l.value?r.value-1:i.maxSlide);d++){const r=h(i.config.i18n[l.value?"ariaNavigateToPage":"ariaNavigateToSlide"],{slideNumber:d+1}),c=u(d),v=t.h("button",{type:"button",class:{"carousel__pagination-button":!0,"carousel__pagination-button--active":c},"aria-label":r,"aria-pressed":c,"aria-controls":null===(o=null===(n=i.slides[d])||void 0===n?void 0:n.exposed)||void 0===o?void 0:o.id,title:r,disabled:e.disableOnClick,onClick:()=>i.nav.slideTo(l.value?Math.floor(d*+i.config.itemsToShow+a.value):d)}),p=t.h("li",{class:"carousel__pagination-item",key:d},v);s.push(p)}return t.h("ol",{class:"carousel__pagination"},s)}}}),L=t.defineComponent({name:"CarouselSlide",props:{id:{type:String,default:e=>e.isClone?void 0:t.useId()},index:{type:Number,default:void 0},isClone:{type:Boolean,default:!1},position:{type:String,default:void 0}},setup(e,{attrs:i,slots:n,expose:a}){const l=t.inject(d);if(t.provide(d,void 0),!l)return()=>"";const o=t.ref(e.index),r=t.getCurrentInstance();a({id:e.id,setIndex:e=>{o.value=e},getBoundingRect:()=>{const e=r.vnode.el;return e?e.getBoundingClientRect():{width:0,height:0}}});const u=t.computed((()=>o.value===l.activeSlide)),s=t.computed((()=>o.value===l.activeSlide-1)),c=t.computed((()=>o.value===l.activeSlide+1)),v=t.computed((()=>o.value>=l.visibleRange.min&&o.value<=l.visibleRange.max)),m=t.computed((()=>{if("auto"===l.config.itemsToShow)return;const e=l.config.itemsToShow,t=l.config.gap>0&&e>1?`calc(${100/e}% - ${l.config.gap*(e-1)/e}px)`:100/e+"%";return l.isVertical?{height:t}:{width:t}}));return l.slideRegistry.registerSlide(r,e.index),t.onUnmounted((()=>{l.slideRegistry.unregisterSlide(r)})),e.isClone&&(t.onMounted((()=>{p(r.vnode)})),t.onUpdated((()=>{p(r.vnode)}))),()=>{var a,r;return l.config.enabled?t.h("li",{style:[i.style,Object.assign({},m.value)],class:{carousel__slide:!0,"carousel__slide--clone":e.isClone,"carousel__slide--visible":v.value,"carousel__slide--active":u.value,"carousel__slide--prev":s.value,"carousel__slide--next":c.value,"carousel__slide--sliding":l.isSliding},onFocusin:()=>{l.viewport&&(l.viewport.scrollLeft=0),l.nav.slideTo(o.value)},id:e.isClone?void 0:e.id,"aria-hidden":e.isClone||void 0},null===(r=n.default)||void 0===r?void 0:r.call(n,{currentIndex:o.value,isActive:u.value,isClone:e.isClone,isPrev:s.value,isNext:c.value,isSliding:l.isSliding,isVisible:v.value})):null===(a=n.default)||void 0===a?void 0:a.call(n)}}});e.BREAKPOINT_MODE_OPTIONS=i,e.Carousel=A,e.DEFAULT_CONFIG=s,e.DIR_MAP=n,e.DIR_OPTIONS=a,e.I18N_DEFAULT_CONFIG=l,e.Icon=E,e.NORMALIZED_DIR_OPTIONS=o,e.Navigation=M,e.Pagination=O,e.SLIDE_EFFECTS=r,e.SNAP_ALIGN_OPTIONS=u,e.Slide=L,e.createSlideRegistry=c,e.icons=_,e.injectCarousel=d})); | ||
//# sourceMappingURL=carousel.min.js.map |
@@ -24,3 +24,3 @@ import eslint from '@eslint/js' | ||
rules: { | ||
'no-console': 'error', | ||
'no-console': 'off', | ||
'@typescript-eslint/no-unused-vars': 'error', | ||
@@ -27,0 +27,0 @@ '@typescript-eslint/no-empty-function': 'off', |
{ | ||
"name": "vue3-carousel", | ||
"version": "0.12.0", | ||
"version": "0.13.0", | ||
"description": "A simple carousel component for Vue 3", | ||
@@ -5,0 +5,0 @@ "author": "Abdelrahman Ismail <dev@ismail9k.com>", |
@@ -1,6 +0,12 @@ | ||
# Vue 3 Carousel | ||
<p align="center"> | ||
<img src="docs/public/vue3-carousel-logo-light.svg" width="200" alt="Vue 3 Carousel Logo"> | ||
</p> | ||
<h1 align="center">Vue 3 Carousel</h1> | ||
<p align="center"> | ||
Modern lightweight Vue 3 carousel component | ||
</p> | ||
<p> | ||
<p align="center"> | ||
<a href="https://npm-stat.com/charts.html?package=vue3-carousel"><img src="https://img.shields.io/npm/dm/vue3-carousel.svg" alt="npm"/></a> | ||
@@ -11,35 +17,27 @@ <a href="https://www.npmjs.com/package/vue3-carousel"><img src="https://img.shields.io/npm/v/vue3-carousel.svg" alt="npm"/></a> | ||
## Documentation | ||
## ✨ Features | ||
https://vue3-carousel.ismail9k.com/ | ||
- 📱 **Responsive** - Breakpoints support | ||
- 🔄 **Infinite Scroll** - Wrap around sliding | ||
- 🖱️ **Mouse/Touch** - Dragging support | ||
- ⚡ **Auto Play** - Automatic sliding | ||
- 🎯 **Slide Classes** - Active & visible states | ||
- 🌐 **RTL** - Right-to-left support | ||
- ♿ **A11y** - Keyboard navigation & ARIA labels | ||
- 📊 **Vertical** - Vertical sliding mode | ||
## Features | ||
## 🚀 Installation | ||
- [x] Responsive breakpoints | ||
- [x] Mouse/touch dragging | ||
- [x] Infinity scroll (wrapping around) | ||
- [x] Auto play | ||
- [x] Add classes for active and for visible slides | ||
- [x] RTL | ||
- [x] Enrich a11y | ||
- [x] Vertical Slides | ||
## Nuxt Module | ||
If you're using Nuxt and prefer to use it via module, please refer to [vue3-carousel-nuxt](https://github.com/gaetansenn/vue3-carousel-nuxt?tab=readme-ov-file) | ||
## Getting started | ||
### Installation | ||
First step is to install it using `yarn` or `npm`: | ||
```bash | ||
npm install vue3-carousel | ||
# npm | ||
npm i vue3-carousel | ||
# or use yarn | ||
# yarn | ||
yarn add vue3-carousel | ||
# pnpm | ||
pnpm install vue3-carousel | ||
``` | ||
### Basic Using | ||
## 📖 Basic Usage | ||
@@ -49,7 +47,8 @@ ```vue | ||
// If you are using PurgeCSS, make sure to whitelist the carousel CSS classes | ||
import 'vue3-carousel/dist/carousel.css' | ||
import 'vue3-carousel/carousel.css' | ||
import { Carousel, Slide, Pagination, Navigation } from 'vue3-carousel' | ||
const config = { | ||
itemsToShow: 1.5 | ||
const carouselConfig = { | ||
itemsToShow: 2.5, | ||
wrapAround: true | ||
} | ||
@@ -59,5 +58,5 @@ </script> | ||
<template> | ||
<Carousel v-bind="config"> | ||
<Carousel v-bind="carouselConfig"> | ||
<Slide v-for="slide in 10" :key="slide"> | ||
{{ slide }} | ||
<div class="carousel__item">{{ slide }}</div> | ||
</Slide> | ||
@@ -72,1 +71,15 @@ | ||
``` | ||
## 📚 Documentation | ||
Visit our [documentation website](https://vue3-carousel.ismail9k.com/) for detailed usage and examples: | ||
- [Getting Started](https://vue3-carousel.ismail9k.com/getting-started) | ||
- [Carousel Config](https://vue3-carousel.ismail9k.com/config) | ||
- [Slide Component](https://vue3-carousel.ismail9k.com/components/slide) | ||
- [Navigation Component](https://vue3-carousel.ismail9k.com/components/navigation) | ||
- [Pagination Component](https://vue3-carousel.ismail9k.com/components/pagination) | ||
## 💚 Nuxt Module | ||
For Nuxt users, check out [vue3-carousel-nuxt](https://github.com/gaetansenn/vue3-carousel-nuxt) module. |
@@ -72,3 +72,3 @@ import { mount } from '@vue/test-utils' | ||
expect(wrapper.props('modelValue')).toBe(1) | ||
await wrapper.setProps({ dir: 'ttb' }) | ||
await wrapper.setProps({ dir: 'ttb', height: 200 }) | ||
await triggerKeyEvent('ArrowDown') | ||
@@ -83,3 +83,3 @@ expect(wrapper.props('modelValue')).toBe(2) | ||
await wrapper.setProps({ dir: 'btt' }) | ||
await wrapper.setProps({ dir: 'btt', height: 200 }) | ||
await triggerKeyEvent('ArrowDown') | ||
@@ -118,3 +118,3 @@ expect(wrapper.props('modelValue')).toBe(0) | ||
await wrapper.setProps({ itemsToShow: 10 }) | ||
const slides = wrapper.findAll('.carousel__slide--visible') | ||
const slides = wrapper.findAll('.carousel__slide') | ||
expect(slides.length).toBe(5) | ||
@@ -206,2 +206,3 @@ }) | ||
const [html, wrapper] = await renderSSR(App, { | ||
height: 200, | ||
wrapAround: true, | ||
@@ -208,0 +209,0 @@ modelValue: 1, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
703617
6746
82