vue3-carousel
Advanced tools
Comparing version 0.8.1 to 0.9.0
@@ -5,2 +5,7 @@ # Changelog | ||
## [0.8.1](https://github.com/ismail9k/vue3-carousel/releases/tag/v0.8.1) - 2024-11-26 | ||
- fix: cannot set properties on null (setting 'index') issue by @ismail9k in #427 | ||
- Better support for typescript and add CarouselExposed type by @ismail9k in #429 | ||
## [0.8.0](https://github.com/ismail9k/vue3-carousel/releases/tag/v0.8.0) - 2024-11-24 | ||
@@ -7,0 +12,0 @@ |
@@ -1,8 +0,47 @@ | ||
import * as vue from 'vue'; | ||
import { Ref, VNode } from 'vue'; | ||
import { Reactive, Ref, ShallowReactive, ComponentInternalInstance, ComputedRef } from 'vue'; | ||
interface Data$1 { | ||
[key: string]: unknown | ||
// 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 Breakpoints = { | ||
[key: number]: Partial< | ||
Omit<CarouselConfig, 'breakpoints' | 'modelValue' | 'breakpointMode'> | ||
> | ||
} | ||
type SnapAlign = (typeof SNAP_ALIGN_OPTIONS)[number] | ||
type Dir = (typeof DIR_OPTIONS)[number] | ||
type BreakpointMode = (typeof BREAKPOINT_MODE_OPTIONS)[number] | ||
type NormalizedDir = (typeof NORMALIZED_DIR_OPTIONS)[number] | ||
type NonNormalizedDir = keyof typeof DIR_MAP | ||
type I18nKeys = keyof typeof I18N_DEFAULT_CONFIG | ||
interface CarouselConfig { | ||
enabled: boolean | ||
itemsToShow: number | ||
itemsToScroll: number | ||
modelValue?: number | ||
transition?: number | ||
gap: number | ||
autoplay?: number | ||
snapAlign: SnapAlign | ||
wrapAround?: boolean | ||
pauseAutoplayOnHover?: boolean | ||
mouseDrag?: boolean | ||
touchDrag?: boolean | ||
dir?: Dir | ||
breakpointMode?: BreakpointMode | ||
breakpoints?: Breakpoints | ||
height: string | number | ||
i18n: { [key in I18nKeys]?: string } | ||
} | ||
declare const SNAP_ALIGN_OPTIONS = ['center', 'start', 'end', 'center-even', 'center-odd'] | ||
@@ -32,37 +71,194 @@ declare const BREAKPOINT_MODE_OPTIONS = ['viewport', 'carousel'] | ||
type Breakpoints = { [key: number]: Partial<CarouselConfig> } | ||
declare const DIR_MAP = { | ||
'left-to-right': 'ltr', | ||
'right-to-left': 'rtl', | ||
'top-to-bottom': 'ttb', | ||
'bottom-to-top': 'btt', | ||
} as const | ||
type SnapAlign = (typeof SNAP_ALIGN_OPTIONS)[number] | ||
declare const NORMALIZED_DIR_OPTIONS = Object.values(DIR_MAP) | ||
type Dir = (typeof DIR_OPTIONS)[number] | ||
declare const DEFAULT_CONFIG: CarouselConfig = { | ||
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, | ||
} | ||
type BreakpointMode = (typeof BREAKPOINT_MODE_OPTIONS)[number] | ||
type I18nKeys = keyof typeof I18N_DEFAULT_CONFIG | ||
interface CarouselConfig { | ||
enabled: boolean | ||
itemsToShow: number | ||
itemsToScroll: number | ||
modelValue?: number | ||
transition?: number | ||
gap: number | ||
autoplay?: number | ||
snapAlign: SnapAlign | ||
wrapAround?: boolean | ||
pauseAutoplayOnHover?: boolean | ||
mouseDrag?: boolean | ||
touchDrag?: boolean | ||
dir?: Dir | ||
breakpointMode?: string | ||
breakpoints?: Breakpoints | ||
height: string | number | ||
i18n: { [key in I18nKeys]?: string } | ||
interface SlideProps { | ||
id: string | ||
index: number | ||
isClone?: boolean | ||
} | ||
declare const Slide = defineComponent({ | ||
name: 'CarouselSlide', | ||
props: { | ||
isClone: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
id: { | ||
type: String, | ||
default: (props: { isClone?: boolean }) => (props.isClone ? undefined : useId()), | ||
}, | ||
index: { | ||
type: Number, | ||
default: 0, | ||
}, | ||
}, | ||
setup(props: SlideProps, { slots, expose }: SetupContext) { | ||
const carousel = inject(injectCarousel) | ||
provide(injectCarousel, undefined) // Don't provide for nested slides | ||
if (!carousel) { | ||
return null // Don't render, let vue warn about the missing provide | ||
} | ||
const index = ref(props.index) | ||
watch(() => props.index, (i) => index.value = i) | ||
const isActive: ComputedRef<boolean> = computed( | ||
() => index.value === carousel.currentSlide | ||
) | ||
const isPrev: ComputedRef<boolean> = computed( | ||
() => index.value === carousel.currentSlide - 1 | ||
) | ||
const isNext: ComputedRef<boolean> = computed( | ||
() => index.value === carousel.currentSlide + 1 | ||
) | ||
const isVisible: ComputedRef<boolean> = computed( | ||
() => | ||
index.value >= Math.floor(carousel.scrolledIndex) && | ||
index.value < Math.ceil(carousel.scrolledIndex) + carousel.config.itemsToShow | ||
) | ||
const slideStyle = 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}%` | ||
return carousel.isVertical ? { height: dimension } : { width: dimension } | ||
}) | ||
const instance = getCurrentInstance()! | ||
if (!props.isClone) { | ||
carousel.registerSlide( | ||
instance, | ||
(resolvedIndex: number) => (index.value = resolvedIndex) | ||
) | ||
onUnmounted(() => { | ||
carousel.unregisterSlide(instance) | ||
}) | ||
} else { | ||
const makeUnfocusable = (node: VNode) => { | ||
;[ | ||
...(node?.el | ||
? node.el.querySelectorAll( | ||
'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])' | ||
) | ||
: []), | ||
] | ||
.filter((el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden')) | ||
.forEach((el) => el.setAttribute('tabindex', '-1')) | ||
} | ||
// Prevent cloned slides from being focusable | ||
onMounted(() => { | ||
makeUnfocusable(instance.vnode) | ||
}) | ||
onUpdated(() => { | ||
makeUnfocusable(instance.vnode) | ||
}) | ||
} | ||
expose({ | ||
id: props.id, | ||
}) | ||
return () => { | ||
if (!carousel.config.enabled) { | ||
return slots.default?.() | ||
} | ||
return h( | ||
'li', | ||
{ | ||
style: slideStyle.value, | ||
class: { | ||
carousel__slide: true, | ||
'carousel__slide--clone': props.isClone, | ||
'carousel__slide--visible': isVisible.value, | ||
'carousel__slide--active': isActive.value, | ||
'carousel__slide--prev': isPrev.value, | ||
'carousel__slide--next': isNext.value, | ||
'carousel__slide--sliding': carousel.isSliding, | ||
}, | ||
onFocusin: () => { | ||
// Prevent the viewport being scrolled by the focus | ||
if (carousel.viewport) { | ||
carousel.viewport.scrollLeft = 0 | ||
} | ||
carousel.nav.slideTo(index.value) | ||
}, | ||
id: props.isClone ? undefined : props.id, | ||
'aria-hidden': props.isClone || undefined, | ||
}, | ||
slots.default?.({ | ||
isActive: isActive.value, | ||
isClone: props.isClone, | ||
isPrev: isPrev.value, | ||
isNext: isNext.value, | ||
isSliding: carousel.isSliding, | ||
isVisible: isVisible.value, | ||
}) | ||
) | ||
} | ||
}, | ||
}) | ||
interface CarouselNav { | ||
slideTo: (index: number) => void | ||
next: () => void | ||
prev: () => void | ||
next: (skipTransition?: boolean) => void | ||
prev: (skipTransition?: boolean) => void | ||
} | ||
type InjectedCarousel = Reactive<{ | ||
config: CarouselConfig | ||
viewport: Ref<Element | null> | ||
slides: ShallowReactive<Array<ComponentInternalInstance>> | ||
slidesCount: ComputedRef<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> | ||
registerSlide: ( | ||
slide: ComponentInternalInstance, | ||
indexCb: (idx: number) => void | ||
) => void | ||
unregisterSlide: (slide: ComponentInternalInstance) => void | ||
}> | ||
interface CarouselData { | ||
@@ -86,112 +282,880 @@ config: CarouselConfig | ||
nav: CarouselNav | ||
data: CarouselData | ||
data: Reactive<CarouselData> | ||
} | ||
declare const _default$1: vue.DefineComponent<{ | ||
enabled: boolean; | ||
itemsToShow: number; | ||
itemsToScroll: number; | ||
modelValue?: number | undefined; | ||
transition?: number | undefined; | ||
gap: number; | ||
autoplay?: number | undefined; | ||
snapAlign: SnapAlign; | ||
wrapAround?: boolean | undefined; | ||
pauseAutoplayOnHover?: boolean | undefined; | ||
mouseDrag?: boolean | undefined; | ||
touchDrag?: boolean | undefined; | ||
dir?: Dir | undefined; | ||
breakpointMode?: string | undefined; | ||
breakpoints?: Breakpoints | undefined; | ||
height: string | number; | ||
i18n: { [key in I18nKeys]?: string; }; | ||
}, () => VNode<vue.RendererNode, vue.RendererElement, { | ||
[key: string]: any; | ||
}>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, "drag" | "init" | "slide-start" | "loop" | "update:modelValue" | "slide-end" | "before-init", vue.PublicProps, Readonly<{ | ||
enabled: boolean; | ||
itemsToShow: number; | ||
itemsToScroll: number; | ||
modelValue?: number | undefined; | ||
transition?: number | undefined; | ||
gap: number; | ||
autoplay?: number | undefined; | ||
snapAlign: SnapAlign; | ||
wrapAround?: boolean | undefined; | ||
pauseAutoplayOnHover?: boolean | undefined; | ||
mouseDrag?: boolean | undefined; | ||
touchDrag?: boolean | undefined; | ||
dir?: Dir | undefined; | ||
breakpointMode?: string | undefined; | ||
breakpoints?: Breakpoints | undefined; | ||
height: string | number; | ||
i18n: { [key in I18nKeys]?: string; }; | ||
}> & Readonly<{}>, { | ||
snapAlign: string; | ||
enabled: boolean; | ||
itemsToShow: number; | ||
itemsToScroll: number; | ||
modelValue: number; | ||
transition: number; | ||
gap: number; | ||
autoplay: number; | ||
wrapAround: boolean; | ||
pauseAutoplayOnHover: boolean; | ||
mouseDrag: boolean; | ||
touchDrag: boolean; | ||
dir: string; | ||
breakpointMode: string; | ||
breakpoints: Record<string, any>; | ||
height: string | number; | ||
i18n: Record<string, any>; | ||
}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>; | ||
declare const Carousel = defineComponent({ | ||
name: 'VueCarousel', | ||
props: carouselProps, | ||
emits: [ | ||
'init', | ||
'drag', | ||
'slide-start', | ||
'loop', | ||
'update:modelValue', | ||
'slide-end', | ||
'before-init', | ||
], | ||
setup(props: CarouselConfig, { slots, emit, expose }: SetupContext) { | ||
const root: Ref<Element | null> = ref(null) | ||
const viewport: Ref<Element | null> = ref(null) | ||
const slides = shallowReactive<Array<ComponentInternalInstance>>([]) | ||
const slideSize: Ref<number> = ref(0) | ||
const slidesCount = computed(() => slides.length) | ||
interface Data { | ||
[key: string]: unknown; | ||
const fallbackConfig = computed(() => ({ | ||
...DEFAULT_CONFIG, | ||
...props, | ||
i18n: { ...DEFAULT_CONFIG.i18n, ...props.i18n }, | ||
breakpoints: undefined, | ||
})) | ||
// current active config | ||
const config = reactive<CarouselConfig>({ ...fallbackConfig.value }) | ||
// slides | ||
const currentSlideIndex = ref(props.modelValue ?? 0) | ||
const prevSlideIndex = ref(0) | ||
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 }) | ||
}) | ||
let autoplayTimer: ReturnType<typeof setInterval> | null = null | ||
let transitionTimer: ReturnType<typeof setTimeout> | null = null | ||
let resizeObserver: ResizeObserver | null = null | ||
const effectiveSlideSize = computed(() => slideSize.value + config.gap) | ||
const normalizedDir = computed<NormalizedDir>(() => { | ||
const dir = config.dir || 'ltr' | ||
return dir in DIR_MAP ? DIR_MAP[dir as NonNormalizedDir] : (dir as NormalizedDir) | ||
}) | ||
const indexCbs: Array<(index: number) => void> = [] | ||
const registerSlide: InjectedCarousel['registerSlide'] = (slide, indexCb) => { | ||
indexCb(slides.length) | ||
slides.push(slide) | ||
indexCbs.push(indexCb) | ||
} | ||
const unregisterSlide: InjectedCarousel['unregisterSlide'] = (slide) => { | ||
const found = slides.indexOf(slide) | ||
if (found >= 0) { | ||
slides.splice(found, 1) | ||
indexCbs.splice(found, 1) | ||
// Update indexes after the one that was removed | ||
indexCbs.slice(found).forEach((cb, index) => cb(found + index)) | ||
} | ||
} | ||
const isReversed = computed(() => ['rtl', 'btt'].includes(normalizedDir.value)) | ||
const isVertical = computed(() => ['ttb', 'btt'].includes(normalizedDir.value)) | ||
const clonedSlidesCount = computed(() => Math.ceil(config.itemsToShow) + 1) | ||
function updateBreakpointsConfig(): void { | ||
// Determine the width source based on the 'breakpointMode' config | ||
const widthSource = | ||
(fallbackConfig.value.breakpointMode === 'carousel' | ||
? root.value?.getBoundingClientRect().width | ||
: typeof window !== 'undefined' | ||
? window.innerWidth | ||
: 0) || 0 | ||
const breakpointsArray = Object.keys(props.breakpoints || {}) | ||
.map((key) => Number(key)) | ||
.sort((a, b) => +b - +a) | ||
const newConfig: Partial<CarouselConfig> = {} | ||
breakpointsArray.some((breakpoint) => { | ||
if (widthSource >= breakpoint) { | ||
Object.assign(newConfig, props.breakpoints![breakpoint]) | ||
if (newConfig.i18n) { | ||
Object.assign( | ||
newConfig.i18n, | ||
fallbackConfig.value.i18n, | ||
props.breakpoints![breakpoint].i18n | ||
) | ||
} | ||
return true | ||
} | ||
return false | ||
}) | ||
Object.assign(config, fallbackConfig.value, newConfig) | ||
} | ||
const handleResize = throttle(() => { | ||
updateBreakpointsConfig() | ||
updateSlidesData() | ||
updateSlideSize() | ||
}) | ||
const totalGap = computed(() => (config.itemsToShow - 1) * config.gap) | ||
const transformElements = shallowReactive<Set<HTMLElement>>(new Set()) | ||
/** | ||
* Setup functions | ||
*/ | ||
function updateSlideSize(): void { | ||
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 as string) | ||
slideSize.value = (height - totalGap.value) / config.itemsToShow | ||
} | ||
} else { | ||
const width = viewport.value.getBoundingClientRect().width | ||
slideSize.value = (width / multiplierWidth - totalGap.value) / config.itemsToShow | ||
} | ||
} | ||
function updateSlidesData(): void { | ||
if (!config.wrapAround && slidesCount.value > 0) { | ||
currentSlideIndex.value = getNumberInRange({ | ||
val: currentSlideIndex.value, | ||
max: maxSlideIndex.value, | ||
min: minSlideIndex.value, | ||
}) | ||
} | ||
} | ||
watchEffect(() => updateSlidesData()) | ||
watchEffect(() => { | ||
// Call updateSlideSize when viewport is ready and track deps | ||
updateSlideSize() | ||
}) | ||
let animationInterval: number | ||
const setAnimationInterval = (event: AnimationEvent | TransitionEvent) => { | ||
const target = event.target as HTMLElement | ||
if (target) { | ||
transformElements.add(target) | ||
} | ||
if (!animationInterval) { | ||
const stepAnimation = () => { | ||
animationInterval = requestAnimationFrame(() => { | ||
updateSlideSize() | ||
stepAnimation() | ||
}) | ||
} | ||
stepAnimation() | ||
} | ||
} | ||
const finishAnimation = (event: AnimationEvent | TransitionEvent) => { | ||
const target = event.target as HTMLElement | ||
if (target) { | ||
transformElements.delete(target) | ||
} | ||
if (animationInterval && transformElements.size === 0) { | ||
cancelAnimationFrame(animationInterval) | ||
updateSlideSize() | ||
} | ||
} | ||
updateBreakpointsConfig() | ||
onMounted((): void => { | ||
if (fallbackConfig.value.breakpointMode === 'carousel') { | ||
updateBreakpointsConfig() | ||
} | ||
initAutoplay() | ||
if (document) { | ||
document.addEventListener('animationstart', setAnimationInterval) | ||
document.addEventListener('animationend', finishAnimation) | ||
} | ||
if (root.value) { | ||
resizeObserver = new ResizeObserver(handleResize) | ||
resizeObserver.observe(root.value) | ||
} | ||
emit('init') | ||
}) | ||
onBeforeUnmount(() => { | ||
// Empty the slides before they unregister for better performance | ||
slides.splice(0, slides.length) | ||
indexCbs.splice(0, indexCbs.length) | ||
if (transitionTimer) { | ||
clearTimeout(transitionTimer) | ||
} | ||
if (animationInterval) { | ||
cancelAnimationFrame(animationInterval) | ||
} | ||
if (autoplayTimer) { | ||
clearInterval(autoplayTimer) | ||
} | ||
if (resizeObserver) { | ||
resizeObserver.disconnect() | ||
resizeObserver = null | ||
} | ||
if (document) { | ||
document.removeEventListener('keydown', handleArrowKeys) | ||
document.removeEventListener('animationstart', setAnimationInterval) | ||
document.removeEventListener('animationend', finishAnimation) | ||
} | ||
if (root.value) { | ||
root.value.removeEventListener('transitionend', updateSlideSize) | ||
root.value.removeEventListener('animationiteration', updateSlideSize) | ||
} | ||
}) | ||
/** | ||
* Carousel Event listeners | ||
*/ | ||
let isTouch = false | ||
const startPosition = { x: 0, y: 0 } | ||
const dragged = reactive({ x: 0, y: 0 }) | ||
const isHover = ref(false) | ||
const isDragging = ref(false) | ||
const handleMouseEnter = (): void => { | ||
isHover.value = true | ||
} | ||
const handleMouseLeave = (): void => { | ||
isHover.value = false | ||
} | ||
const handleArrowKeys = throttle((event: KeyboardEvent): void => { | ||
if (event.ctrlKey) return | ||
switch (event.key) { | ||
case 'ArrowLeft': | ||
case 'ArrowUp': | ||
if (isVertical.value === event.key.endsWith('Up')) { | ||
if (isReversed.value) { | ||
nav.next(true) | ||
} else { | ||
nav.prev(true) | ||
} | ||
} | ||
break | ||
case 'ArrowRight': | ||
case 'ArrowDown': | ||
if (isVertical.value === event.key.endsWith('Down')) { | ||
if (isReversed.value) { | ||
nav.prev(true) | ||
} else { | ||
nav.next(true) | ||
} | ||
} | ||
break | ||
} | ||
}, 200) | ||
const handleFocus = (): void => { | ||
document.addEventListener('keydown', handleArrowKeys) | ||
} | ||
const handleBlur = (): void => { | ||
document.removeEventListener('keydown', handleArrowKeys) | ||
} | ||
function handleDragStart(event: MouseEvent | TouchEvent): void { | ||
// Prevent drag initiation on input elements or if already sliding | ||
const targetTagName = (event.target as HTMLElement).tagName | ||
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(targetTagName) || isSliding.value) { | ||
return | ||
} | ||
// Detect if the event is a touchstart or mousedown event | ||
isTouch = event.type === 'touchstart' | ||
// For mouse events, prevent default to avoid text selection | ||
if (!isTouch) { | ||
event.preventDefault() | ||
if ((event as MouseEvent).button !== 0) { | ||
// Ignore non-left-click mouse events | ||
return | ||
} | ||
} | ||
// Initialize start positions for the drag | ||
startPosition.x = 'touches' in event ? event.touches[0].clientX : event.clientX | ||
startPosition.y = 'touches' in event ? event.touches[0].clientY : event.clientY | ||
// Attach event listeners for dragging and drag end | ||
const moveEvent = isTouch ? 'touchmove' : 'mousemove' | ||
const endEvent = isTouch ? 'touchend' : 'mouseup' | ||
document.addEventListener(moveEvent, handleDragging, { passive: false }) | ||
document.addEventListener(endEvent, handleDragEnd, { passive: true }) | ||
} | ||
const handleDragging = throttle((event: TouchEvent | MouseEvent): void => { | ||
isDragging.value = true | ||
// Get the current position based on the interaction type (touch or mouse) | ||
const currentX = 'touches' in event ? event.touches[0].clientX : event.clientX | ||
const currentY = 'touches' in event ? event.touches[0].clientY : event.clientY | ||
// Calculate deltas for X and Y axes | ||
const deltaX = currentX - startPosition.x | ||
const deltaY = currentY - startPosition.y | ||
// Update dragged state reactively | ||
dragged.x = deltaX | ||
dragged.y = deltaY | ||
// Emit a drag event for further customization if needed | ||
emit('drag', { deltaX, deltaY }) | ||
}) | ||
function handleDragEnd(): void { | ||
handleDragging.cancel() | ||
// Determine the active axis and direction multiplier | ||
const dragAxis = isVertical.value ? 'y' : 'x' | ||
const directionMultiplier = isReversed.value ? -1 : 1 | ||
// Calculate dragged slides with a tolerance to account for incomplete drags | ||
const tolerance = Math.sign(dragged[dragAxis]) * 0.4 // Smooth out small drags | ||
const draggedSlides = | ||
Math.round(dragged[dragAxis] / effectiveSlideSize.value + tolerance) * | ||
directionMultiplier | ||
// Prevent accidental clicks when there is a slide drag | ||
if (draggedSlides && !isTouch) { | ||
const preventClick = (e: MouseEvent) => { | ||
e.preventDefault() | ||
window.removeEventListener('click', preventClick) | ||
} | ||
window.addEventListener('click', preventClick) | ||
} | ||
// Slide to the appropriate slide index | ||
const targetSlideIndex = currentSlideIndex.value - draggedSlides | ||
slideTo(targetSlideIndex) | ||
// Reset drag state | ||
dragged.x = 0 | ||
dragged.y = 0 | ||
isDragging.value = false | ||
const moveEvent = isTouch ? 'touchmove' : 'mousemove' | ||
const endEvent = isTouch ? 'touchend' : 'mouseup' | ||
document.removeEventListener(moveEvent, handleDragging) | ||
document.removeEventListener(endEvent, handleDragEnd) | ||
} | ||
/** | ||
* Autoplay | ||
*/ | ||
function initAutoplay(): void { | ||
if (!config.autoplay || config.autoplay <= 0) { | ||
return | ||
} | ||
autoplayTimer = setInterval(() => { | ||
if (config.pauseAutoplayOnHover && isHover.value) { | ||
return | ||
} | ||
next() | ||
}, config.autoplay) | ||
} | ||
function stopAutoplay(): void { | ||
if (autoplayTimer) { | ||
clearInterval(autoplayTimer) | ||
autoplayTimer = null | ||
} | ||
} | ||
function resetAutoplay(): void { | ||
stopAutoplay() | ||
initAutoplay() | ||
} | ||
/** | ||
* Navigation function | ||
*/ | ||
const isSliding = ref(false) | ||
function slideTo(slideIndex: number, skipTransition = false): void { | ||
const currentVal = config.wrapAround | ||
? slideIndex | ||
: getNumberInRange({ | ||
val: slideIndex, | ||
max: maxSlideIndex.value, | ||
min: minSlideIndex.value, | ||
}) | ||
if ( | ||
currentSlideIndex.value === currentVal || | ||
(!skipTransition && isSliding.value) | ||
) { | ||
return | ||
} | ||
emit('slide-start', { | ||
slidingToIndex: slideIndex, | ||
currentSlideIndex: currentSlideIndex.value, | ||
prevSlideIndex: prevSlideIndex.value, | ||
slidesCount: slidesCount.value, | ||
}) | ||
stopAutoplay() | ||
isSliding.value = true | ||
prevSlideIndex.value = currentSlideIndex.value | ||
const mappedNumber = config.wrapAround | ||
? mapNumberToRange({ | ||
val: currentVal, | ||
max: maxSlideIndex.value, | ||
min: 0, | ||
}) | ||
: currentVal | ||
currentSlideIndex.value = currentVal | ||
if (mappedNumber !== currentVal) { | ||
modelWatcher.pause() | ||
} | ||
emit('update:modelValue', mappedNumber) | ||
transitionTimer = setTimeout((): void => { | ||
if (config.wrapAround) { | ||
if (mappedNumber !== currentVal) { | ||
modelWatcher.resume() | ||
currentSlideIndex.value = mappedNumber | ||
emit('loop', { | ||
currentSlideIndex: currentSlideIndex.value, | ||
slidingToIndex: slideIndex, | ||
}) | ||
} | ||
} | ||
emit('slide-end', { | ||
currentSlideIndex: currentSlideIndex.value, | ||
prevSlideIndex: prevSlideIndex.value, | ||
slidesCount: slidesCount.value, | ||
}) | ||
isSliding.value = false | ||
resetAutoplay() | ||
}, config.transition) | ||
} | ||
function next(skipTransition = false): void { | ||
slideTo(currentSlideIndex.value + config.itemsToScroll, skipTransition) | ||
} | ||
function prev(skipTransition = false): void { | ||
slideTo(currentSlideIndex.value - config.itemsToScroll, skipTransition) | ||
} | ||
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, | ||
maxSlide: maxSlideIndex, | ||
minSlide: minSlideIndex, | ||
slideSize, | ||
isVertical, | ||
normalizedDir, | ||
nav, | ||
isSliding, | ||
registerSlide, | ||
unregisterSlide, | ||
}) | ||
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 { | ||
updateBreakpointsConfig() | ||
updateSlidesData() | ||
updateSlideSize() | ||
resetAutoplay() | ||
} | ||
// Update the carousel on props change | ||
watch( | ||
() => [fallbackConfig.value, props.breakpoints], | ||
() => updateBreakpointsConfig(), | ||
{ deep: true } | ||
) | ||
watch( | ||
() => props.autoplay, | ||
() => resetAutoplay() | ||
) | ||
// Handle changing v-model value | ||
const modelWatcher = watch( | ||
() => props.modelValue, | ||
(val) => { | ||
if (val === currentSlideIndex.value) { | ||
return | ||
} | ||
slideTo(Number(val), true) | ||
} | ||
) | ||
// Init carousel | ||
emit('before-init') | ||
const data = reactive<CarouselData>({ | ||
config, | ||
slidesCount, | ||
slideSize, | ||
currentSlide: currentSlideIndex, | ||
maxSlide: maxSlideIndex, | ||
minSlide: minSlideIndex, | ||
middleSlide: middleSlideIndex, | ||
}) | ||
expose<CarouselExposed>({ | ||
updateBreakpointsConfig, | ||
updateSlidesData, | ||
updateSlideSize, | ||
restartCarousel, | ||
slideTo, | ||
next, | ||
prev, | ||
nav, | ||
data, | ||
}) | ||
const trackHeight = computed(() => { | ||
if (isVertical.value && slideSize.value && config.height === 'auto') { | ||
return `${slideSize.value * config.itemsToShow + totalGap.value}px` | ||
} | ||
return config.height !== 'auto' | ||
? typeof config.height === 'number' || | ||
parseInt(config.height).toString() === config.height | ||
? `${config.height}px` | ||
: config.height | ||
: undefined | ||
}) | ||
/** | ||
* Track style | ||
*/ | ||
const trackTransform: ComputedRef<string> = computed(() => { | ||
// Calculate the scrolled index with wrapping offset if applicable | ||
const cloneOffset = config.wrapAround ? clonedSlidesCount.value : 0 | ||
// Determine direction multiplier for orientation | ||
const directionMultiplier = isReversed.value ? -1 : 1 | ||
// Calculate the total offset for slide transformation | ||
const totalOffset = | ||
(scrolledIndex.value + cloneOffset) * | ||
effectiveSlideSize.value * | ||
directionMultiplier | ||
// Include user drag interaction offset | ||
const dragOffset = isVertical.value ? dragged.y : dragged.x | ||
// Generate the appropriate CSS transformation | ||
const translateAxis = isVertical.value ? 'Y' : 'X' | ||
return `translate${translateAxis}(${dragOffset - totalOffset}px)` | ||
}) | ||
return () => { | ||
const slotSlides = slots.default || slots.slides | ||
const slotAddons = slots.addons | ||
let output: VNode[] | Array<Array<VNode>> = slotSlides?.(data) || [] | ||
if (!config.enabled || !output.length) { | ||
return h( | ||
'section', | ||
{ | ||
ref: root, | ||
class: ['carousel', 'is-disabled'], | ||
}, | ||
output | ||
) | ||
} | ||
const addonsElements = slotAddons?.(data) || [] | ||
if (config.wrapAround) { | ||
// Ensure scoped CSS tracks properly | ||
const scopeId = output.length > 0 ? output[0].scopeId : null | ||
pushScopeId(scopeId) | ||
const toShow = clonedSlidesCount.value | ||
const slidesBefore = createCloneSlides({ slides, position: 'before', toShow }) | ||
const slidesAfter = createCloneSlides({ slides, position: 'after', toShow }) | ||
popScopeId() | ||
output = [...slidesBefore, ...output, ...slidesAfter] | ||
} | ||
const trackEl = h( | ||
'ol', | ||
{ | ||
class: 'carousel__track', | ||
style: { | ||
transform: trackTransform.value, | ||
'transition-duration': isSliding.value ? `${config.transition}ms` : undefined, | ||
gap: config.gap > 0 ? `${config.gap}px` : undefined, | ||
}, | ||
onMousedownCapture: config.mouseDrag ? handleDragStart : null, | ||
onTouchstartPassiveCapture: config.touchDrag ? handleDragStart : null, | ||
}, | ||
output | ||
) | ||
const viewPortEl = h('div', { class: 'carousel__viewport', ref: viewport }, trackEl) | ||
return h( | ||
'section', | ||
{ | ||
ref: root, | ||
class: [ | ||
'carousel', | ||
`is-${normalizedDir.value}`, | ||
{ | ||
'is-vertical': isVertical.value, | ||
'is-sliding': isSliding.value, | ||
'is-dragging': isDragging.value, | ||
'is-hover': isHover.value, | ||
}, | ||
], | ||
style: { | ||
'--vc-trk-height': trackHeight.value, | ||
}, | ||
dir: normalizedDir.value, | ||
'aria-label': config.i18n['ariaGallery'], | ||
tabindex: '0', | ||
onFocus: handleFocus, | ||
onBlur: handleBlur, | ||
onMouseenter: handleMouseEnter, | ||
onMouseleave: handleMouseLeave, | ||
}, | ||
[viewPortEl, addonsElements, h(ARIAComponent)] | ||
) | ||
} | ||
}, | ||
}) | ||
declare enum IconName { | ||
arrowUp = 'arrowUp', | ||
arrowDown = 'arrowDown', | ||
arrowRight = 'arrowRight', | ||
arrowLeft = 'arrowLeft', | ||
} | ||
declare const Icon: { | ||
(props: Data): vue.VNode<vue.RendererNode, vue.RendererElement, { | ||
[key: string]: any; | ||
}> | undefined; | ||
props: { | ||
name: StringConstructor; | ||
title: StringConstructor; | ||
}; | ||
}; | ||
type IconNameValue = `${IconName}` | ||
declare const Navigation: (props: any, { slots, attrs }: any) => vue.VNode<vue.RendererNode, vue.RendererElement, { | ||
[key: string]: any; | ||
}>[]; | ||
interface IconProps { | ||
title?: string | ||
name?: IconNameValue | ||
} | ||
declare const Pagination: () => VNode<vue.RendererNode, vue.RendererElement, { | ||
[key: string]: any; | ||
}>; | ||
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', | ||
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', | ||
} | ||
declare const _default: vue.DefineComponent<vue.ExtractPropTypes<{ | ||
index: { | ||
type: NumberConstructor; | ||
default: number; | ||
}; | ||
isClone: { | ||
type: BooleanConstructor; | ||
default: boolean; | ||
}; | ||
}>, () => vue.VNode<vue.RendererNode, vue.RendererElement, { | ||
[key: string]: any; | ||
}> | vue.VNode<vue.RendererNode, vue.RendererElement, { | ||
[key: string]: any; | ||
}>[] | undefined, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{ | ||
index: { | ||
type: NumberConstructor; | ||
default: number; | ||
}; | ||
isClone: { | ||
type: BooleanConstructor; | ||
default: boolean; | ||
}; | ||
}>> & Readonly<{}>, { | ||
index: number; | ||
isClone: boolean; | ||
}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>; | ||
declare const Icon = defineComponent({ | ||
props: { | ||
name: { | ||
type: String as PropType<IconNameValue>, | ||
required: true, | ||
validator: validateIconName, | ||
}, | ||
title: { | ||
type: String, | ||
default: (props: { name: IconNameValue }) => | ||
props.name ? DEFAULT_CONFIG.i18n[iconI18n(props.name)] : '', | ||
}, | ||
}, | ||
setup(props: IconProps) { | ||
const carousel = inject(injectCarousel, null) | ||
export { type BreakpointMode, type Breakpoints, _default$1 as Carousel, type CarouselConfig, type CarouselData, type CarouselExposed, type CarouselMethods, type CarouselNav, type Data$1 as Data, type Dir, type I18nKeys, Icon, Navigation, Pagination, _default as Slide, type SnapAlign }; | ||
return () => { | ||
const iconName = props.name | ||
if (!iconName || !validateIconName(iconName)) return | ||
const path = icons[iconName] | ||
const pathEl = h('path', { d: path }) | ||
const iconTitle: string = | ||
carousel?.config.i18n[iconI18n(iconName)] || props.title || iconName | ||
const titleEl = h('title', iconTitle) | ||
return h( | ||
'svg', | ||
{ | ||
class: 'carousel__icon', | ||
viewBox: '0 0 24 24', | ||
role: 'img', | ||
'aria-label': iconTitle, | ||
}, | ||
[titleEl, pathEl] | ||
) | ||
} | ||
}, | ||
}) | ||
interface NavigationProps { | ||
class: any | ||
} | ||
declare const Navigation = defineComponent({ | ||
name: 'CarouselNavigation', | ||
setup(props: NavigationProps, { slots }) { | ||
const carousel = inject(injectCarousel) | ||
if (!carousel) { | ||
return null // Don't render, let vue warn about the missing provide | ||
} | ||
const { next: slotNext, prev: slotPrev } = slots | ||
const getPrevIcon = () => { | ||
const directionIcons: Record<NormalizedDir, IconNameValue> = { | ||
ltr: 'arrowLeft', | ||
rtl: 'arrowRight', | ||
ttb: 'arrowUp', | ||
btt: 'arrowDown', | ||
} | ||
return directionIcons[carousel.normalizedDir] | ||
} | ||
const getNextIcon = () => { | ||
const directionIcons: Record<NormalizedDir, IconNameValue> = { | ||
ltr: 'arrowRight', | ||
rtl: 'arrowLeft', | ||
ttb: 'arrowDown', | ||
btt: 'arrowUp', | ||
} | ||
return directionIcons[carousel.normalizedDir] | ||
} | ||
return () => { | ||
const { wrapAround, i18n } = carousel.config | ||
const prevButton = h( | ||
'button', | ||
{ | ||
type: 'button', | ||
class: [ | ||
'carousel__prev', | ||
!wrapAround && | ||
carousel.currentSlide <= carousel.minSlide && | ||
'carousel__prev--disabled', | ||
props.class, | ||
], | ||
'aria-label': i18n['ariaPreviousSlide'], | ||
title: i18n['ariaPreviousSlide'], | ||
onClick: carousel.nav.prev, | ||
}, | ||
slotPrev?.() || h(Icon, { name: getPrevIcon() }) | ||
) | ||
const nextButton = h( | ||
'button', | ||
{ | ||
type: 'button', | ||
class: [ | ||
'carousel__next', | ||
!wrapAround && | ||
carousel.currentSlide >= carousel.maxSlide && | ||
'carousel__next--disabled', | ||
props.class, | ||
], | ||
'aria-label': i18n['ariaNextSlide'], | ||
title: i18n['ariaNextSlide'], | ||
onClick: carousel.nav.next, | ||
}, | ||
slotNext?.() || h(Icon, { name: getNextIcon() }) | ||
) | ||
return [prevButton, nextButton] | ||
} | ||
}, | ||
}) | ||
interface PaginationProps { | ||
disableOnClick?: boolean | ||
key?: string | ||
} | ||
declare const Pagination = defineComponent({ | ||
name: 'CarouselPagination', | ||
setup(props: PaginationProps) { | ||
const carousel = inject(injectCarousel) | ||
if (!carousel) { | ||
return null // Don't render, let vue warn about the missing provide | ||
} | ||
const isActive = (slide: number): boolean => | ||
mapNumberToRange({ | ||
val: carousel.currentSlide, | ||
max: carousel.maxSlide, | ||
min: 0, | ||
}) === slide | ||
return () => { | ||
const children: Array<VNode> = [] | ||
for (let slide = carousel.minSlide; slide <= carousel.maxSlide; slide++) { | ||
const buttonLabel = i18nFormatter(carousel.config.i18n.ariaNavigateToSlide, { | ||
slideNumber: slide + 1, | ||
}) | ||
const active = isActive(slide) | ||
const button = h('button', { | ||
type: 'button', | ||
class: { | ||
'carousel__pagination-button': true, | ||
'carousel__pagination-button--active': active, | ||
}, | ||
'aria-label': buttonLabel, | ||
'aria-pressed': active, | ||
'aria-controls': carousel.slides[slide]?.exposed?.id, | ||
title: buttonLabel, | ||
onClick: props.disableOnClick ? undefined : () => carousel.nav.slideTo(slide), | ||
}) | ||
const item = h('li', { class: 'carousel__pagination-item', key: slide }, button) | ||
children.push(item) | ||
} | ||
return h('ol', { class: 'carousel__pagination' }, children) | ||
} | ||
}, | ||
}) | ||
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, SNAP_ALIGN_OPTIONS, Slide, type SlideProps, type SnapAlign, icons, injectCarousel }; |
/** | ||
* Vue 3 Carousel 0.8.1 | ||
* Vue 3 Carousel 0.9.0 | ||
* (c) 2024 | ||
* @license MIT | ||
*/ | ||
import { Fragment, defineComponent, inject, reactive, ref, h, computed, provide, onMounted, nextTick, onUnmounted, watch, cloneVNode } from 'vue'; | ||
import { defineComponent, useId, inject, provide, ref, watch, computed, getCurrentInstance, onUnmounted, onMounted, onUpdated, h, cloneVNode, shallowReactive, reactive, watchEffect, onBeforeUnmount, pushScopeId, popScopeId } 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 | ||
const injectCarousel$1 = Symbol('carousel'); | ||
/** | ||
* Determines the minimum slide index based on the configuration. | ||
* | ||
* @param {GetMinSlideIndexArgs} args - The carousel configuration and slide count. | ||
* @returns {number} The minimum slide index. | ||
*/ | ||
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; | ||
} | ||
// Map snapAlign values to calculation logic | ||
function snapAlignCalculations() { | ||
switch (snapAlign) { | ||
case 'end': | ||
return Math.floor(itemsToShow - 1); | ||
case 'center': | ||
case 'center-odd': | ||
return Math.floor((itemsToShow - 1) / 2); | ||
case 'center-even': | ||
return Math.floor((itemsToShow - 2) / 2); | ||
} | ||
return 0; | ||
} | ||
// Return the calculated offset or default to 0 for invalid snapAlign values | ||
return Math.max(0, snapAlignCalculations()); | ||
} | ||
/** | ||
* Determines the maximum slide index based on the configuration. | ||
* | ||
* @param {Args} args - The carousel configuration and slide count. | ||
* @returns {number} The maximum slide index. | ||
*/ | ||
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); | ||
} | ||
} | ||
// Return the result ensuring it's non-negative | ||
return Math.max(snapAlignCalculations(), 0); | ||
} | ||
function getNumberInRange({ val, max, min }) { | ||
if (max < min) { | ||
return val; | ||
} | ||
return Math.min(Math.max(val, isNaN(min) ? val : min), isNaN(max) ? val : max); | ||
} | ||
function mapNumberToRange({ val, max, min = 0 }) { | ||
const mod = max - min + 1; | ||
return ((((val - min) % mod) + mod) % mod) + min; | ||
} | ||
const calculateOffset = (snapAlign, itemsToShow) => { | ||
var _a; | ||
const offsetMap = { | ||
start: 0, | ||
center: (itemsToShow - 1) / 2, | ||
'center-odd': (itemsToShow - 1) / 2, | ||
'center-even': (itemsToShow - 2) / 2, | ||
end: itemsToShow - 1, | ||
}; | ||
return (_a = offsetMap[snapAlign]) !== null && _a !== void 0 ? _a : 0; // Fallback to 0 for unknown snapAlign | ||
}; | ||
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 getNumberInRange({ | ||
val: currentSlide - offset, | ||
max: slidesCount - itemsToShow, | ||
min: 0, | ||
}); | ||
} | ||
else { | ||
return mapNumberToRange({ | ||
val: currentSlide - offset, | ||
max: slidesCount + itemsToShow, | ||
min: 0 - itemsToShow, | ||
}); | ||
} | ||
} | ||
function i18nFormatter(string = '', values = {}) { | ||
return Object.entries(values).reduce((acc, [key, value]) => acc.replace(`{${key}}`, String(value)), string); | ||
} | ||
/** | ||
* Returns a throttled version of the function using requestAnimationFrame. | ||
* | ||
* @param fn - The function to throttle. | ||
* @param ms - The number of milliseconds to wait for the throttled function to be called again | ||
*/ | ||
function throttle(fn, ms = 0) { | ||
let isThrottled = false; | ||
let start = 0; | ||
let frameId = null; | ||
function throttled(...args) { | ||
if (isThrottled) | ||
return; | ||
isThrottled = true; | ||
const step = () => { | ||
frameId = requestAnimationFrame((time) => { | ||
const elapsed = time - start; | ||
if (elapsed > ms) { | ||
start = time; | ||
fn(...args); | ||
isThrottled = false; | ||
} | ||
else { | ||
step(); | ||
} | ||
}); | ||
}; | ||
step(); | ||
} | ||
throttled.cancel = () => { | ||
if (frameId) { | ||
cancelAnimationFrame(frameId); | ||
frameId = null; | ||
isThrottled = false; | ||
} | ||
}; | ||
return throttled; | ||
} | ||
function getTransformValues(el) { | ||
const { transform } = window.getComputedStyle(el); | ||
//add sanity check | ||
return transform | ||
.split(/[(,)]/) | ||
.slice(1, -1) | ||
.map((v) => parseFloat(v)); | ||
} | ||
const Slide = defineComponent({ | ||
name: 'CarouselSlide', | ||
props: { | ||
isClone: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
id: { | ||
type: String, | ||
default: (props) => (props.isClone ? undefined : useId()), | ||
}, | ||
index: { | ||
type: Number, | ||
default: 0, | ||
}, | ||
}, | ||
setup(props, { slots, expose }) { | ||
const carousel = inject(injectCarousel$1); | ||
provide(injectCarousel$1, undefined); // Don't provide for nested slides | ||
if (!carousel) { | ||
return null; // Don't render, let vue warn about the missing provide | ||
} | ||
const index = ref(props.index); | ||
watch(() => props.index, (i) => index.value = i); | ||
const isActive = computed(() => index.value === carousel.currentSlide); | ||
const isPrev = computed(() => index.value === carousel.currentSlide - 1); | ||
const isNext = computed(() => index.value === carousel.currentSlide + 1); | ||
const isVisible = computed(() => index.value >= Math.floor(carousel.scrolledIndex) && | ||
index.value < Math.ceil(carousel.scrolledIndex) + carousel.config.itemsToShow); | ||
const slideStyle = 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}%`; | ||
return carousel.isVertical ? { height: dimension } : { width: dimension }; | ||
}); | ||
const instance = getCurrentInstance(); | ||
if (!props.isClone) { | ||
carousel.registerSlide(instance, (resolvedIndex) => (index.value = resolvedIndex)); | ||
onUnmounted(() => { | ||
carousel.unregisterSlide(instance); | ||
}); | ||
} | ||
else { | ||
const makeUnfocusable = (node) => { | ||
[ | ||
...((node === null || node === void 0 ? void 0 : node.el) | ||
? node.el.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])') | ||
: []), | ||
] | ||
.filter((el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden')) | ||
.forEach((el) => el.setAttribute('tabindex', '-1')); | ||
}; | ||
// Prevent cloned slides from being focusable | ||
onMounted(() => { | ||
makeUnfocusable(instance.vnode); | ||
}); | ||
onUpdated(() => { | ||
makeUnfocusable(instance.vnode); | ||
}); | ||
} | ||
expose({ | ||
id: props.id, | ||
}); | ||
return () => { | ||
var _a, _b; | ||
if (!carousel.config.enabled) { | ||
return (_a = slots.default) === null || _a === void 0 ? void 0 : _a.call(slots); | ||
} | ||
return h('li', { | ||
style: slideStyle.value, | ||
class: { | ||
carousel__slide: true, | ||
'carousel__slide--clone': props.isClone, | ||
'carousel__slide--visible': isVisible.value, | ||
'carousel__slide--active': isActive.value, | ||
'carousel__slide--prev': isPrev.value, | ||
'carousel__slide--next': isNext.value, | ||
'carousel__slide--sliding': carousel.isSliding, | ||
}, | ||
onFocusin: () => { | ||
// Prevent the viewport being scrolled by the focus | ||
if (carousel.viewport) { | ||
carousel.viewport.scrollLeft = 0; | ||
} | ||
carousel.nav.slideTo(index.value); | ||
}, | ||
id: props.isClone ? undefined : props.id, | ||
'aria-hidden': props.isClone || undefined, | ||
}, (_b = slots.default) === null || _b === void 0 ? void 0 : _b.call(slots, { | ||
isActive: isActive.value, | ||
isClone: props.isClone, | ||
isPrev: isPrev.value, | ||
isNext: isNext.value, | ||
isSliding: carousel.isSliding, | ||
isVisible: isVisible.value, | ||
})); | ||
}; | ||
}, | ||
}); | ||
function createCloneSlides({ slides, position, toShow }) { | ||
const clones = []; | ||
const isBefore = position === 'before'; | ||
const start = isBefore ? -toShow : 0; | ||
const end = isBefore ? 0 : toShow; | ||
for (let i = start; i < end; i++) { | ||
const index = isBefore ? i : slides.length > 0 ? i + slides.length : i + 99999; | ||
const props = { | ||
index, | ||
isClone: true, | ||
key: `clone-${position}-${i}`, | ||
}; | ||
clones.push(slides.length > 0 | ||
? cloneVNode(slides[(i + slides.length) % slides.length].vnode, props) | ||
: h(Slide, props)); | ||
} | ||
return clones; | ||
} | ||
const ARIA = defineComponent({ | ||
name: 'CarouselAria', | ||
setup() { | ||
const carousel = inject(injectCarousel$1); | ||
if (!carousel) { | ||
return; | ||
} | ||
return () => h('div', { | ||
class: ['carousel__liveregion', 'carousel__sr-only'], | ||
'aria-live': 'polite', | ||
'aria-atomic': 'true', | ||
}, i18nFormatter(carousel.config.i18n['itemXofY'], { | ||
currentSlide: carousel.currentSlide + 1, | ||
slidesCount: carousel.slidesCount, | ||
})); | ||
}, | ||
}); | ||
// 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']; | ||
@@ -31,2 +330,9 @@ const BREAKPOINT_MODE_OPTIONS = ['viewport', 'carousel']; | ||
}; | ||
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 DEFAULT_CONFIG = { | ||
@@ -134,2 +440,3 @@ enabled: true, | ||
dir: { | ||
type: String, | ||
default: DEFAULT_CONFIG.dir, | ||
@@ -148,168 +455,4 @@ validator(value) { | ||
/** | ||
* Determines the maximum slide index based on the configuration. | ||
* | ||
* @param {Args} args - The carousel configuration and slide count. | ||
* @returns {number} The maximum slide index. | ||
*/ | ||
function getMaxSlideIndex({ config, slidesCount }) { | ||
var _a; | ||
const { snapAlign = 'N/A', wrapAround, itemsToShow = 1 } = config; | ||
// If wrapAround is enabled, the max index is the last slide | ||
if (wrapAround) { | ||
return Math.max(slidesCount - 1, 0); | ||
} | ||
// Map snapAlign values to calculation logic | ||
const snapAlignCalculations = { | ||
start: Math.ceil(slidesCount - itemsToShow), | ||
end: Math.ceil(slidesCount - 1), | ||
center: slidesCount - Math.ceil((itemsToShow - 0.5) / 2), | ||
'center-odd': slidesCount - Math.ceil((itemsToShow - 0.5) / 2), | ||
'center-even': slidesCount - Math.ceil(itemsToShow / 2), | ||
}; | ||
// Compute the max index based on snapAlign, or default to 0 | ||
const calculateMaxIndex = (_a = snapAlignCalculations[snapAlign]) !== null && _a !== void 0 ? _a : 0; | ||
// Return the result ensuring it's non-negative | ||
return Math.max(calculateMaxIndex, 0); | ||
} | ||
/** | ||
* Determines the minimum slide index based on the configuration. | ||
* | ||
* @param {Args} args - The carousel configuration and slide count. | ||
* @returns {number} The minimum slide index. | ||
*/ | ||
function getMinSlideIndex({ config, slidesCount }) { | ||
var _a; | ||
const { snapAlign = 'N/A', wrapAround, itemsToShow = 1 } = config; | ||
// If wrapAround is enabled or itemsToShow exceeds slidesCount, the minimum index is always 0 | ||
if (wrapAround || itemsToShow > slidesCount) { | ||
return 0; | ||
} | ||
// Map of snapAlign to offset calculations | ||
const snapAlignCalculations = { | ||
start: 0, | ||
end: Math.floor(itemsToShow - 1), | ||
center: Math.floor((itemsToShow - 1) / 2), | ||
'center-odd': Math.floor((itemsToShow - 1) / 2), | ||
'center-even': Math.floor((itemsToShow - 2) / 2), | ||
}; | ||
// Return the calculated offset or default to 0 for invalid snapAlign values | ||
return (_a = snapAlignCalculations[snapAlign]) !== null && _a !== void 0 ? _a : 0; | ||
} | ||
function getNumberInRange({ val, max, min }) { | ||
if (max < min) { | ||
return val; | ||
} | ||
return Math.min(Math.max(val, min), max); | ||
} | ||
const calculateOffset = (snapAlign, itemsToShow) => { | ||
var _a; | ||
const offsetMap = { | ||
start: 0, | ||
center: (itemsToShow - 1) / 2, | ||
'center-odd': (itemsToShow - 1) / 2, | ||
'center-even': (itemsToShow - 2) / 2, | ||
end: itemsToShow - 1, | ||
}; | ||
return (_a = offsetMap[snapAlign]) !== null && _a !== void 0 ? _a : 0; // Fallback to 0 for unknown snapAlign | ||
}; | ||
function getScrolledIndex({ config, currentSlide, slidesCount }) { | ||
const { snapAlign = 'N/A', 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 getNumberInRange({ | ||
val: currentSlide - offset, | ||
max: slidesCount - itemsToShow, | ||
min: 0, | ||
}); | ||
} | ||
return currentSlide - offset; | ||
} | ||
function getSlidesVNodes(vNode) { | ||
if (!vNode) | ||
return []; | ||
return vNode.reduce((acc, node) => { | ||
var _a; | ||
if (node.type === Fragment) { | ||
return [...acc, ...getSlidesVNodes(node.children)]; | ||
} | ||
if (((_a = node.type) === null || _a === void 0 ? void 0 : _a.name) === 'CarouselSlide') { | ||
return [...acc, node]; | ||
} | ||
return acc; | ||
}, []); | ||
} | ||
function mapNumberToRange({ val, max, min = 0 }) { | ||
const mod = max - min + 1; | ||
return ((val - min) % mod + mod) % mod + min; | ||
} | ||
/** | ||
* return a throttle version of the function | ||
* Throttling | ||
* | ||
*/ | ||
// eslint-disable-next-line no-unused-vars | ||
function throttle(fn) { | ||
let isRunning = false; | ||
return function (...args) { | ||
if (!isRunning) { | ||
isRunning = true; | ||
requestAnimationFrame(() => { | ||
fn.apply(this, args); | ||
isRunning = false; | ||
}); | ||
} | ||
}; | ||
} | ||
/** | ||
* return a debounced version of the function | ||
* @param fn | ||
* @param delay | ||
*/ | ||
// eslint-disable-next-line no-unused-vars | ||
function debounce(fn, delay) { | ||
let timerId; | ||
return function (...args) { | ||
if (timerId) { | ||
clearTimeout(timerId); | ||
} | ||
timerId = setTimeout(() => { | ||
fn(...args); | ||
timerId = null; | ||
}, delay); | ||
}; | ||
} | ||
function i18nFormatter(string = '', values = {}) { | ||
return Object.entries(values).reduce((acc, [key, value]) => acc.replace(`{${key}}`, String(value)), string); | ||
} | ||
var ARIAComponent = defineComponent({ | ||
name: 'ARIA', | ||
setup() { | ||
const config = inject('config', reactive(Object.assign({}, DEFAULT_CONFIG))); | ||
const currentSlide = inject('currentSlide', ref(0)); | ||
const slidesCount = inject('slidesCount', ref(0)); | ||
return () => h('div', { | ||
class: ['carousel__liveregion', 'carousel__sr-only'], | ||
'aria-live': 'polite', | ||
'aria-atomic': 'true', | ||
}, i18nFormatter(config.i18n['itemXofY'], { | ||
currentSlide: currentSlide.value + 1, | ||
slidesCount: slidesCount.value, | ||
})); | ||
}, | ||
}); | ||
var Carousel = defineComponent({ | ||
name: 'Carousel', | ||
const Carousel = defineComponent({ | ||
name: 'VueCarousel', | ||
props: carouselProps, | ||
@@ -329,5 +472,5 @@ emits: [ | ||
const viewport = ref(null); | ||
const slides = ref([]); | ||
const slides = shallowReactive([]); | ||
const slideSize = ref(0); | ||
const slidesCount = ref(0); | ||
const slidesCount = computed(() => slides.length); | ||
const fallbackConfig = computed(() => (Object.assign(Object.assign(Object.assign({}, DEFAULT_CONFIG), props), { i18n: Object.assign(Object.assign({}, DEFAULT_CONFIG.i18n), props.i18n), breakpoints: undefined }))); | ||
@@ -339,5 +482,9 @@ // current active config | ||
const prevSlideIndex = ref(0); | ||
const middleSlideIndex = ref(0); | ||
const maxSlideIndex = ref(0); | ||
const minSlideIndex = ref(0); | ||
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 }); | ||
}); | ||
let autoplayTimer = null; | ||
@@ -347,35 +494,42 @@ let transitionTimer = null; | ||
const effectiveSlideSize = computed(() => slideSize.value + config.gap); | ||
const normalizeDir = computed(() => { | ||
const dir = config.dir || 'lrt'; | ||
const dirMap = { | ||
'left-to-right': 'ltr', | ||
'right-to-left': 'rtl', | ||
'top-to-bottom': 'ttb', | ||
'bottom-to-top': 'btt', | ||
}; | ||
return dirMap[dir] || dir; | ||
const normalizedDir = computed(() => { | ||
const dir = config.dir || 'ltr'; | ||
return dir in DIR_MAP ? DIR_MAP[dir] : dir; | ||
}); | ||
const isVertical = computed(() => ['ttb', 'btt'].includes(normalizeDir.value)); | ||
provide('config', config); | ||
provide('slidesCount', slidesCount); | ||
provide('currentSlide', currentSlideIndex); | ||
provide('maxSlide', maxSlideIndex); | ||
provide('minSlide', minSlideIndex); | ||
provide('slideSize', slideSize); | ||
provide('isVertical', isVertical); | ||
provide('normalizeDir', normalizeDir); | ||
const indexCbs = []; | ||
const registerSlide = (slide, indexCb) => { | ||
indexCb(slides.length); | ||
slides.push(slide); | ||
indexCbs.push(indexCb); | ||
}; | ||
const unregisterSlide = (slide) => { | ||
const found = slides.indexOf(slide); | ||
if (found >= 0) { | ||
slides.splice(found, 1); | ||
indexCbs.splice(found, 1); | ||
// Update indexes after the one that was removed | ||
indexCbs.slice(found).forEach((cb, index) => cb(found + index)); | ||
} | ||
}; | ||
const isReversed = computed(() => ['rtl', 'btt'].includes(normalizedDir.value)); | ||
const isVertical = computed(() => ['ttb', 'btt'].includes(normalizedDir.value)); | ||
const clonedSlidesCount = computed(() => Math.ceil(config.itemsToShow) + 1); | ||
function updateBreakpointsConfig() { | ||
var _a; | ||
// Determine the width source based on the 'breakpointMode' config | ||
const widthSource = (config.breakpointMode === 'carousel' | ||
const widthSource = (fallbackConfig.value.breakpointMode === 'carousel' | ||
? (_a = root.value) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width | ||
: window.innerWidth) || 0; | ||
: typeof window !== 'undefined' | ||
? window.innerWidth | ||
: 0) || 0; | ||
const breakpointsArray = Object.keys(props.breakpoints || {}) | ||
.map((key) => Number(key)) | ||
.sort((a, b) => +b - +a); | ||
let newConfig = Object.assign({}, fallbackConfig.value); | ||
const newConfig = {}; | ||
breakpointsArray.some((breakpoint) => { | ||
var _a; | ||
if (widthSource >= breakpoint) { | ||
newConfig = Object.assign(Object.assign({}, newConfig), (_a = props.breakpoints) === null || _a === void 0 ? void 0 : _a[breakpoint]); | ||
Object.assign(newConfig, props.breakpoints[breakpoint]); | ||
if (newConfig.i18n) { | ||
Object.assign(newConfig.i18n, fallbackConfig.value.i18n, props.breakpoints[breakpoint].i18n); | ||
} | ||
return true; | ||
@@ -385,9 +539,11 @@ } | ||
}); | ||
Object.assign(config, newConfig); | ||
Object.assign(config, fallbackConfig.value, newConfig); | ||
} | ||
const handleResize = debounce(() => { | ||
const handleResize = throttle(() => { | ||
updateBreakpointsConfig(); | ||
updateSlidesData(); | ||
updateSlideSize(); | ||
}, 16); | ||
}); | ||
const totalGap = computed(() => (config.itemsToShow - 1) * config.gap); | ||
const transformElements = shallowReactive(new Set()); | ||
/** | ||
@@ -399,20 +555,25 @@ * Setup functions | ||
return; | ||
const rect = viewport.value.getBoundingClientRect(); | ||
// Calculate the total gap space | ||
const totalGap = (config.itemsToShow - 1) * config.gap; | ||
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) { | ||
slideSize.value = (rect.height - totalGap) / config.itemsToShow; | ||
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; | ||
} | ||
} | ||
else { | ||
slideSize.value = (rect.width - totalGap) / config.itemsToShow; | ||
const width = viewport.value.getBoundingClientRect().width; | ||
slideSize.value = (width / multiplierWidth - totalGap.value) / config.itemsToShow; | ||
} | ||
} | ||
function updateSlidesData() { | ||
if (slidesCount.value <= 0) | ||
return; | ||
middleSlideIndex.value = Math.ceil((slidesCount.value - 1) / 2); | ||
maxSlideIndex.value = getMaxSlideIndex({ config, slidesCount: slidesCount.value }); | ||
minSlideIndex.value = getMinSlideIndex({ config, slidesCount: slidesCount.value }); | ||
if (!config.wrapAround) { | ||
if (!config.wrapAround && slidesCount.value > 0) { | ||
currentSlideIndex.value = getNumberInRange({ | ||
@@ -425,11 +586,45 @@ val: currentSlideIndex.value, | ||
} | ||
watchEffect(() => updateSlidesData()); | ||
watchEffect(() => { | ||
// Call updateSlideSize when viewport is ready and track deps | ||
updateSlideSize(); | ||
}); | ||
let animationInterval; | ||
const setAnimationInterval = (event) => { | ||
const target = event.target; | ||
if (target) { | ||
transformElements.add(target); | ||
} | ||
if (!animationInterval) { | ||
const stepAnimation = () => { | ||
animationInterval = requestAnimationFrame(() => { | ||
updateSlideSize(); | ||
stepAnimation(); | ||
}); | ||
}; | ||
stepAnimation(); | ||
} | ||
}; | ||
const finishAnimation = (event) => { | ||
const target = event.target; | ||
if (target) { | ||
transformElements.delete(target); | ||
} | ||
if (animationInterval && transformElements.size === 0) { | ||
cancelAnimationFrame(animationInterval); | ||
updateSlideSize(); | ||
} | ||
}; | ||
updateBreakpointsConfig(); | ||
onMounted(() => { | ||
nextTick(() => updateSlideSize()); | ||
// Overcome some edge cases | ||
setTimeout(() => updateSlideSize(), 1000); | ||
updateBreakpointsConfig(); | ||
if (fallbackConfig.value.breakpointMode === 'carousel') { | ||
updateBreakpointsConfig(); | ||
} | ||
initAutoplay(); | ||
window.addEventListener('resize', handleResize, { passive: true }); | ||
resizeObserver = new ResizeObserver(handleResize); | ||
if (document) { | ||
document.addEventListener('animationstart', setAnimationInterval); | ||
document.addEventListener('animationend', finishAnimation); | ||
} | ||
if (root.value) { | ||
resizeObserver = new ResizeObserver(handleResize); | ||
resizeObserver.observe(root.value); | ||
@@ -439,16 +634,28 @@ } | ||
}); | ||
onUnmounted(() => { | ||
onBeforeUnmount(() => { | ||
// Empty the slides before they unregister for better performance | ||
slides.splice(0, slides.length); | ||
indexCbs.splice(0, indexCbs.length); | ||
if (transitionTimer) { | ||
clearTimeout(transitionTimer); | ||
} | ||
if (animationInterval) { | ||
cancelAnimationFrame(animationInterval); | ||
} | ||
if (autoplayTimer) { | ||
clearInterval(autoplayTimer); | ||
} | ||
if (resizeObserver && root.value) { | ||
resizeObserver.unobserve(root.value); | ||
if (resizeObserver) { | ||
resizeObserver.disconnect(); | ||
resizeObserver = null; | ||
} | ||
window.removeEventListener('resize', handleResize, { | ||
passive: true, | ||
}); | ||
if (document) { | ||
document.removeEventListener('keydown', handleArrowKeys); | ||
document.removeEventListener('animationstart', setAnimationInterval); | ||
document.removeEventListener('animationend', finishAnimation); | ||
} | ||
if (root.value) { | ||
root.value.removeEventListener('transitionend', updateSlideSize); | ||
root.value.removeEventListener('animationiteration', updateSlideSize); | ||
} | ||
}); | ||
@@ -469,2 +676,36 @@ /** | ||
}; | ||
const handleArrowKeys = throttle((event) => { | ||
if (event.ctrlKey) | ||
return; | ||
switch (event.key) { | ||
case 'ArrowLeft': | ||
case 'ArrowUp': | ||
if (isVertical.value === event.key.endsWith('Up')) { | ||
if (isReversed.value) { | ||
nav.next(true); | ||
} | ||
else { | ||
nav.prev(true); | ||
} | ||
} | ||
break; | ||
case 'ArrowRight': | ||
case 'ArrowDown': | ||
if (isVertical.value === event.key.endsWith('Down')) { | ||
if (isReversed.value) { | ||
nav.prev(true); | ||
} | ||
else { | ||
nav.next(true); | ||
} | ||
} | ||
break; | ||
} | ||
}, 200); | ||
const handleFocus = () => { | ||
document.addEventListener('keydown', handleArrowKeys); | ||
}; | ||
const handleBlur = () => { | ||
document.removeEventListener('keydown', handleArrowKeys); | ||
}; | ||
function handleDragStart(event) { | ||
@@ -487,4 +728,4 @@ // Prevent drag initiation on input elements or if already sliding | ||
// Initialize start positions for the drag | ||
startPosition.x = isTouch ? event.touches[0].clientX : event.clientX; | ||
startPosition.y = isTouch ? event.touches[0].clientY : event.clientY; | ||
startPosition.x = 'touches' in event ? event.touches[0].clientX : event.clientX; | ||
startPosition.y = 'touches' in event ? event.touches[0].clientY : event.clientY; | ||
// Attach event listeners for dragging and drag end | ||
@@ -499,4 +740,4 @@ const moveEvent = isTouch ? 'touchmove' : 'mousemove'; | ||
// Get the current position based on the interaction type (touch or mouse) | ||
const currentX = isTouch ? event.touches[0].clientX : event.clientX; | ||
const currentY = isTouch ? event.touches[0].clientY : event.clientY; | ||
const currentX = 'touches' in event ? event.touches[0].clientX : event.clientX; | ||
const currentY = 'touches' in event ? event.touches[0].clientY : event.clientY; | ||
// Calculate deltas for X and Y axes | ||
@@ -512,5 +753,6 @@ const deltaX = currentX - startPosition.x; | ||
function handleDragEnd() { | ||
handleDragging.cancel(); | ||
// Determine the active axis and direction multiplier | ||
const dragAxis = isVertical.value ? 'y' : 'x'; | ||
const directionMultiplier = ['rtl', 'btt'].includes(normalizeDir.value) ? -1 : 1; | ||
const directionMultiplier = isReversed.value ? -1 : 1; | ||
// Calculate dragged slides with a tolerance to account for incomplete drags | ||
@@ -554,3 +796,3 @@ const tolerance = Math.sign(dragged[dragAxis]) * 0.4; // Smooth out small drags | ||
} | ||
function resetAutoplay() { | ||
function stopAutoplay() { | ||
if (autoplayTimer) { | ||
@@ -560,2 +802,5 @@ clearInterval(autoplayTimer); | ||
} | ||
} | ||
function resetAutoplay() { | ||
stopAutoplay(); | ||
initAutoplay(); | ||
@@ -567,3 +812,3 @@ } | ||
const isSliding = ref(false); | ||
function slideTo(slideIndex) { | ||
function slideTo(slideIndex, skipTransition = false) { | ||
const currentVal = config.wrapAround | ||
@@ -576,3 +821,4 @@ ? slideIndex | ||
}); | ||
if (currentSlideIndex.value === currentVal || isSliding.value) { | ||
if (currentSlideIndex.value === currentVal || | ||
(!skipTransition && isSliding.value)) { | ||
return; | ||
@@ -586,13 +832,21 @@ } | ||
}); | ||
stopAutoplay(); | ||
isSliding.value = true; | ||
prevSlideIndex.value = currentSlideIndex.value; | ||
const mappedNumber = config.wrapAround | ||
? mapNumberToRange({ | ||
val: currentVal, | ||
max: maxSlideIndex.value, | ||
min: 0, | ||
}) | ||
: currentVal; | ||
currentSlideIndex.value = currentVal; | ||
if (mappedNumber !== currentVal) { | ||
modelWatcher.pause(); | ||
} | ||
emit('update:modelValue', mappedNumber); | ||
transitionTimer = setTimeout(() => { | ||
if (config.wrapAround) { | ||
const mappedNumber = mapNumberToRange({ | ||
val: currentVal, | ||
max: maxSlideIndex.value, | ||
min: 0, | ||
}); | ||
if (mappedNumber !== currentSlideIndex.value) { | ||
if (mappedNumber !== currentVal) { | ||
modelWatcher.resume(); | ||
currentSlideIndex.value = mappedNumber; | ||
@@ -605,3 +859,2 @@ emit('loop', { | ||
} | ||
emit('update:modelValue', currentSlideIndex.value); | ||
emit('slide-end', { | ||
@@ -616,9 +869,41 @@ currentSlideIndex: currentSlideIndex.value, | ||
} | ||
function next() { | ||
slideTo(currentSlideIndex.value + config.itemsToScroll); | ||
function next(skipTransition = false) { | ||
slideTo(currentSlideIndex.value + config.itemsToScroll, skipTransition); | ||
} | ||
function prev() { | ||
slideTo(currentSlideIndex.value - config.itemsToScroll); | ||
function prev(skipTransition = false) { | ||
slideTo(currentSlideIndex.value - config.itemsToScroll, skipTransition); | ||
} | ||
const nav = { slideTo, next, prev }; | ||
const scrolledIndex = computed(() => getScrolledIndex({ | ||
config, | ||
currentSlide: currentSlideIndex.value, | ||
slidesCount: slidesCount.value, | ||
})); | ||
const provided = reactive({ | ||
config, | ||
slidesCount, | ||
viewport, | ||
slides, | ||
scrolledIndex, | ||
currentSlide: currentSlideIndex, | ||
maxSlide: maxSlideIndex, | ||
minSlide: minSlideIndex, | ||
slideSize, | ||
isVertical, | ||
normalizedDir, | ||
nav, | ||
isSliding, | ||
registerSlide, | ||
unregisterSlide, | ||
}); | ||
provide(injectCarousel$1, 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); | ||
@@ -633,15 +918,14 @@ provide('isSliding', isSliding); | ||
// Update the carousel on props change | ||
watch(() => (Object.assign({}, props)), restartCarousel, { deep: true }); | ||
watch(() => [fallbackConfig.value, props.breakpoints], () => updateBreakpointsConfig(), { deep: true }); | ||
watch(() => props.autoplay, () => resetAutoplay()); | ||
// Handle changing v-model value | ||
watch(() => props['modelValue'], (val) => { | ||
const modelWatcher = watch(() => props.modelValue, (val) => { | ||
if (val === currentSlideIndex.value) { | ||
return; | ||
} | ||
slideTo(Number(val)); | ||
slideTo(Number(val), true); | ||
}); | ||
// Handel when slides added/removed | ||
watch(slidesCount, updateSlidesData); | ||
// Init carousel | ||
emit('before-init'); | ||
const data = { | ||
const data = reactive({ | ||
config, | ||
@@ -654,3 +938,3 @@ slidesCount, | ||
middleSlide: middleSlideIndex, | ||
}; | ||
}); | ||
expose({ | ||
@@ -667,2 +951,13 @@ updateBreakpointsConfig, | ||
}); | ||
const trackHeight = computed(() => { | ||
if (isVertical.value && slideSize.value && config.height === 'auto') { | ||
return `${slideSize.value * config.itemsToShow + totalGap.value}px`; | ||
} | ||
return config.height !== 'auto' | ||
? typeof config.height === 'number' || | ||
parseInt(config.height).toString() === config.height | ||
? `${config.height}px` | ||
: config.height | ||
: undefined; | ||
}); | ||
/** | ||
@@ -673,13 +968,9 @@ * Track style | ||
// Calculate the scrolled index with wrapping offset if applicable | ||
const scrolledIndex = getScrolledIndex({ | ||
config, | ||
currentSlide: currentSlideIndex.value, | ||
slidesCount: slidesCount.value, | ||
}); | ||
const cloneOffset = config.wrapAround ? slidesCount.value : 0; | ||
const cloneOffset = config.wrapAround ? clonedSlidesCount.value : 0; | ||
// Determine direction multiplier for orientation | ||
const isReverseDirection = ['rtl', 'btt'].includes(normalizeDir.value); | ||
const directionMultiplier = isReverseDirection ? -1 : 1; | ||
const directionMultiplier = isReversed.value ? -1 : 1; | ||
// Calculate the total offset for slide transformation | ||
const totalOffset = (scrolledIndex + cloneOffset) * effectiveSlideSize.value * directionMultiplier; | ||
const totalOffset = (scrolledIndex.value + cloneOffset) * | ||
effectiveSlideSize.value * | ||
directionMultiplier; | ||
// Include user drag interaction offset | ||
@@ -691,38 +982,23 @@ const dragOffset = isVertical.value ? dragged.y : dragged.x; | ||
}); | ||
const slotSlides = slots.default || slots.slides; | ||
const slotAddons = slots.addons; | ||
const slotsProps = reactive(data); | ||
return () => { | ||
if (!config.enabled) { | ||
const slotSlides = slots.default || slots.slides; | ||
const slotAddons = slots.addons; | ||
let output = (slotSlides === null || slotSlides === void 0 ? void 0 : slotSlides(data)) || []; | ||
if (!config.enabled || !output.length) { | ||
return h('section', { | ||
ref: root, | ||
class: ['carousel', 'is-disabled'], | ||
}, slotSlides === null || slotSlides === void 0 ? void 0 : slotSlides()); | ||
}, output); | ||
} | ||
const slidesElements = getSlidesVNodes(slotSlides === null || slotSlides === void 0 ? void 0 : slotSlides(slotsProps)); | ||
const addonsElements = (slotAddons === null || slotAddons === void 0 ? void 0 : slotAddons(slotsProps)) || []; | ||
slidesElements.forEach((el, index) => { | ||
if (el.props) { | ||
el.props.index = index; | ||
} | ||
else { | ||
el.props = { index }; | ||
} | ||
}); | ||
let output = slidesElements; | ||
const addonsElements = (slotAddons === null || slotAddons === void 0 ? void 0 : slotAddons(data)) || []; | ||
if (config.wrapAround) { | ||
const slidesBefore = slidesElements.map((el, index) => cloneVNode(el, { | ||
index: -slidesElements.length + index, | ||
isClone: true, | ||
key: `clone-before-${index}`, | ||
})); | ||
const slidesAfter = slidesElements.map((el, index) => cloneVNode(el, { | ||
index: slidesElements.length + index, | ||
isClone: true, | ||
key: `clone-after-${index}`, | ||
})); | ||
output = [...slidesBefore, ...slidesElements, ...slidesAfter]; | ||
// Ensure scoped CSS tracks properly | ||
const scopeId = output.length > 0 ? output[0].scopeId : null; | ||
pushScopeId(scopeId); | ||
const toShow = clonedSlidesCount.value; | ||
const slidesBefore = createCloneSlides({ slides, position: 'before', toShow }); | ||
const slidesAfter = createCloneSlides({ slides, position: 'after', toShow }); | ||
popScopeId(); | ||
output = [...slidesBefore, ...output, ...slidesAfter]; | ||
} | ||
slides.value = slidesElements; | ||
slidesCount.value = Math.max(slidesElements.length, 1); | ||
const trackEl = h('ol', { | ||
@@ -732,4 +1008,4 @@ class: 'carousel__track', | ||
transform: trackTransform.value, | ||
transition: `${isSliding.value ? config.transition : 0}ms`, | ||
gap: `${config.gap}px`, | ||
'transition-duration': isSliding.value ? `${config.transition}ms` : undefined, | ||
gap: config.gap > 0 ? `${config.gap}px` : undefined, | ||
}, | ||
@@ -744,3 +1020,3 @@ onMousedownCapture: config.mouseDrag ? handleDragStart : null, | ||
'carousel', | ||
`is-${normalizeDir.value}`, | ||
`is-${normalizedDir.value}`, | ||
{ | ||
@@ -754,10 +1030,12 @@ 'is-vertical': isVertical.value, | ||
style: { | ||
'--vc-trk-height': `${typeof config.height === 'number' ? `${config.height}px` : config.height}`, | ||
'--vc-trk-height': trackHeight.value, | ||
}, | ||
dir: normalizeDir.value, | ||
dir: normalizedDir.value, | ||
'aria-label': config.i18n['ariaGallery'], | ||
tabindex: '0', | ||
onFocus: handleFocus, | ||
onBlur: handleBlur, | ||
onMouseenter: handleMouseEnter, | ||
onMouseleave: handleMouseLeave, | ||
}, [viewPortEl, addonsElements, h(ARIAComponent)]); | ||
}, [viewPortEl, addonsElements, h(ARIA)]); | ||
}; | ||
@@ -774,2 +1052,10 @@ }, | ||
})(IconName || (IconName = {})); | ||
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 = { | ||
@@ -781,170 +1067,129 @@ arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z', | ||
}; | ||
function isIconName(candidate) { | ||
return candidate in IconName; | ||
} | ||
const Icon = (props) => { | ||
const config = inject('config', reactive(Object.assign({}, DEFAULT_CONFIG))); | ||
const iconName = String(props.name); | ||
const iconI18n = `icon${iconName.charAt(0).toUpperCase() + iconName.slice(1)}`; | ||
if (!iconName || typeof iconName !== 'string' || !isIconName(iconName)) { | ||
return; | ||
} | ||
const path = icons[iconName]; | ||
const pathEl = h('path', { d: path }); | ||
const iconTitle = config.i18n[iconI18n] || props.title || iconName; | ||
const titleEl = h('title', iconTitle); | ||
return h('svg', { | ||
class: 'carousel__icon', | ||
viewBox: '0 0 24 24', | ||
role: 'img', | ||
'aria-label': iconTitle, | ||
}, [titleEl, pathEl]); | ||
}; | ||
Icon.props = { name: String, title: String }; | ||
const Navigation = (props, { slots, attrs }) => { | ||
const { next: slotNext, prev: slotPrev } = slots || {}; | ||
const config = inject('config', reactive(Object.assign({}, DEFAULT_CONFIG))); | ||
const maxSlide = inject('maxSlide', ref(1)); | ||
const minSlide = inject('minSlide', ref(1)); | ||
const normalizeDir = inject('normalizeDir', ref('ltr')); | ||
const currentSlide = inject('currentSlide', ref(1)); | ||
const nav = inject('nav', {}); | ||
const { wrapAround, i18n } = config; | ||
const getPrevIcon = () => { | ||
const directionIcons = { | ||
ltr: 'arrowLeft', | ||
rtl: 'arrowRight', | ||
ttb: 'arrowUp', | ||
btt: 'arrowDown', | ||
}; | ||
return directionIcons[normalizeDir.value]; | ||
}; | ||
const getNextIcon = () => { | ||
const directionIcons = { | ||
ltr: 'arrowRight', | ||
rtl: 'arrowLeft', | ||
ttb: 'arrowDown', | ||
btt: 'arrowUp', | ||
}; | ||
return directionIcons[normalizeDir.value]; | ||
}; | ||
const prevButton = h('button', { | ||
type: 'button', | ||
class: [ | ||
'carousel__prev', | ||
!wrapAround && currentSlide.value <= minSlide.value && 'carousel__prev--disabled', | ||
attrs === null || attrs === void 0 ? void 0 : attrs.class, | ||
], | ||
'aria-label': i18n['ariaPreviousSlide'], | ||
title: i18n['ariaPreviousSlide'], | ||
onClick: nav.prev, | ||
}, (slotPrev === null || slotPrev === void 0 ? void 0 : slotPrev()) || h(Icon, { name: getPrevIcon() })); | ||
const nextButton = h('button', { | ||
type: 'button', | ||
class: [ | ||
'carousel__next', | ||
!wrapAround && currentSlide.value >= maxSlide.value && 'carousel__next--disabled', | ||
attrs === null || attrs === void 0 ? void 0 : attrs.class, | ||
], | ||
'aria-label': i18n['ariaNextSlide'], | ||
title: i18n['ariaNextSlide'], | ||
onClick: nav.next, | ||
}, (slotNext === null || slotNext === void 0 ? void 0 : slotNext()) || h(Icon, { name: getNextIcon() })); | ||
return [prevButton, nextButton]; | ||
}; | ||
const Pagination = () => { | ||
const config = inject('config', reactive(Object.assign({}, DEFAULT_CONFIG))); | ||
const maxSlide = inject('maxSlide', ref(1)); | ||
const minSlide = inject('minSlide', ref(1)); | ||
const currentSlide = inject('currentSlide', ref(1)); | ||
const nav = inject('nav', {}); | ||
const isActive = (slide) => mapNumberToRange({ | ||
val: currentSlide.value, | ||
max: maxSlide.value, | ||
min: 0, | ||
}) === slide; | ||
const children = []; | ||
for (let slide = minSlide.value; slide < maxSlide.value + 1; slide++) { | ||
const buttonLabel = i18nFormatter(config.i18n['ariaNavigateToSlide'], { | ||
slideNumber: slide + 1, | ||
}); | ||
const button = h('button', { | ||
type: 'button', | ||
class: { | ||
'carousel__pagination-button': true, | ||
'carousel__pagination-button--active': isActive(slide), | ||
}, | ||
'aria-label': buttonLabel, | ||
title: buttonLabel, | ||
onClick: () => nav.slideTo(slide), | ||
}); | ||
const item = h('li', { class: 'carousel__pagination-item', key: slide }, button); | ||
children.push(item); | ||
} | ||
return h('ol', { class: 'carousel__pagination' }, children); | ||
}; | ||
var Slide = defineComponent({ | ||
name: 'CarouselSlide', | ||
const Icon = defineComponent({ | ||
props: { | ||
index: { | ||
type: Number, | ||
default: 1, | ||
name: { | ||
type: String, | ||
required: true, | ||
validator: validateIconName, | ||
}, | ||
isClone: { | ||
type: Boolean, | ||
default: false, | ||
title: { | ||
type: String, | ||
default: (props) => props.name ? DEFAULT_CONFIG.i18n[iconI18n(props.name)] : '', | ||
}, | ||
}, | ||
setup(props) { | ||
const carousel = inject(injectCarousel$1, null); | ||
return () => { | ||
const iconName = props.name; | ||
if (!iconName || !validateIconName(iconName)) | ||
return; | ||
const path = icons[iconName]; | ||
const pathEl = h('path', { d: path }); | ||
const iconTitle = (carousel === null || carousel === void 0 ? void 0 : carousel.config.i18n[iconI18n(iconName)]) || props.title || iconName; | ||
const titleEl = h('title', iconTitle); | ||
return h('svg', { | ||
class: 'carousel__icon', | ||
viewBox: '0 0 24 24', | ||
role: 'img', | ||
'aria-label': iconTitle, | ||
}, [titleEl, pathEl]); | ||
}; | ||
}, | ||
}); | ||
const Navigation = defineComponent({ | ||
name: 'CarouselNavigation', | ||
setup(props, { slots }) { | ||
const config = inject('config', reactive(Object.assign({}, DEFAULT_CONFIG))); | ||
const currentSlide = inject('currentSlide', ref(0)); | ||
const slidesToScroll = inject('slidesToScroll', ref(0)); | ||
const isSliding = inject('isSliding', ref(false)); | ||
const isVertical = inject('isVertical', ref(false)); | ||
const slideSize = inject('slideSize', ref(0)); | ||
const isActive = computed(() => props.index === currentSlide.value); | ||
const isPrev = computed(() => props.index === currentSlide.value - 1); | ||
const isNext = computed(() => props.index === currentSlide.value + 1); | ||
const isVisible = computed(() => { | ||
const min = Math.floor(slidesToScroll.value); | ||
const max = Math.ceil(slidesToScroll.value + config.itemsToShow - 1); | ||
return props.index >= min && props.index <= max; | ||
}); | ||
const slideStyle = computed(() => { | ||
const dimension = config.gap | ||
? `${slideSize.value}px` | ||
: `${100 / config.itemsToShow}%`; | ||
return isVertical.value | ||
? { height: dimension, width: '' } | ||
: { width: dimension, height: '' }; | ||
}); | ||
const carousel = inject(injectCarousel$1); | ||
if (!carousel) { | ||
return null; // Don't render, let vue warn about the missing provide | ||
} | ||
const { next: slotNext, prev: slotPrev } = slots; | ||
const getPrevIcon = () => { | ||
const directionIcons = { | ||
ltr: 'arrowLeft', | ||
rtl: 'arrowRight', | ||
ttb: 'arrowUp', | ||
btt: 'arrowDown', | ||
}; | ||
return directionIcons[carousel.normalizedDir]; | ||
}; | ||
const getNextIcon = () => { | ||
const directionIcons = { | ||
ltr: 'arrowRight', | ||
rtl: 'arrowLeft', | ||
ttb: 'arrowDown', | ||
btt: 'arrowUp', | ||
}; | ||
return directionIcons[carousel.normalizedDir]; | ||
}; | ||
return () => { | ||
const { wrapAround, i18n } = carousel.config; | ||
const prevButton = h('button', { | ||
type: 'button', | ||
class: [ | ||
'carousel__prev', | ||
!wrapAround && | ||
carousel.currentSlide <= carousel.minSlide && | ||
'carousel__prev--disabled', | ||
props.class, | ||
], | ||
'aria-label': i18n['ariaPreviousSlide'], | ||
title: i18n['ariaPreviousSlide'], | ||
onClick: carousel.nav.prev, | ||
}, (slotPrev === null || slotPrev === void 0 ? void 0 : slotPrev()) || h(Icon, { name: getPrevIcon() })); | ||
const nextButton = h('button', { | ||
type: 'button', | ||
class: [ | ||
'carousel__next', | ||
!wrapAround && | ||
carousel.currentSlide >= carousel.maxSlide && | ||
'carousel__next--disabled', | ||
props.class, | ||
], | ||
'aria-label': i18n['ariaNextSlide'], | ||
title: i18n['ariaNextSlide'], | ||
onClick: carousel.nav.next, | ||
}, (slotNext === null || slotNext === void 0 ? void 0 : slotNext()) || h(Icon, { name: getNextIcon() })); | ||
return [prevButton, nextButton]; | ||
}; | ||
}, | ||
}); | ||
const Pagination = defineComponent({ | ||
name: 'CarouselPagination', | ||
setup(props) { | ||
const carousel = inject(injectCarousel$1); | ||
if (!carousel) { | ||
return null; // Don't render, let vue warn about the missing provide | ||
} | ||
const isActive = (slide) => mapNumberToRange({ | ||
val: carousel.currentSlide, | ||
max: carousel.maxSlide, | ||
min: 0, | ||
}) === slide; | ||
return () => { | ||
var _a, _b; | ||
if (!config.enabled) { | ||
return (_a = slots.default) === null || _a === void 0 ? void 0 : _a.call(slots); | ||
const children = []; | ||
for (let slide = carousel.minSlide; slide <= carousel.maxSlide; slide++) { | ||
const buttonLabel = i18nFormatter(carousel.config.i18n.ariaNavigateToSlide, { | ||
slideNumber: slide + 1, | ||
}); | ||
const active = isActive(slide); | ||
const button = h('button', { | ||
type: 'button', | ||
class: { | ||
'carousel__pagination-button': true, | ||
'carousel__pagination-button--active': active, | ||
}, | ||
'aria-label': buttonLabel, | ||
'aria-pressed': active, | ||
'aria-controls': (_b = (_a = carousel.slides[slide]) === null || _a === void 0 ? void 0 : _a.exposed) === null || _b === void 0 ? void 0 : _b.id, | ||
title: buttonLabel, | ||
onClick: props.disableOnClick ? undefined : () => carousel.nav.slideTo(slide), | ||
}); | ||
const item = h('li', { class: 'carousel__pagination-item', key: slide }, button); | ||
children.push(item); | ||
} | ||
return h('li', { | ||
style: slideStyle.value, | ||
class: { | ||
carousel__slide: true, | ||
'carousel__slide--clone': props.isClone, | ||
'carousel__slide--visible': isVisible.value, | ||
'carousel__slide--active': isActive.value, | ||
'carousel__slide--prev': isPrev.value, | ||
'carousel__slide--next': isNext.value, | ||
'carousel__slide--sliding': isSliding.value, | ||
}, | ||
'aria-hidden': !isVisible.value, | ||
}, (_b = slots.default) === null || _b === void 0 ? void 0 : _b.call(slots, { | ||
isActive: isActive.value, | ||
isClone: props.isClone, | ||
isPrev: isPrev.value, | ||
isNext: isNext.value, | ||
isSliding: isSliding.value, | ||
isVisible: isVisible.value, | ||
})); | ||
return h('ol', { class: 'carousel__pagination' }, children); | ||
}; | ||
@@ -954,2 +1199,2 @@ }, | ||
export { Carousel, Icon, Navigation, Pagination, Slide }; | ||
export { BREAKPOINT_MODE_OPTIONS, Carousel, DEFAULT_CONFIG, DIR_MAP, DIR_OPTIONS, I18N_DEFAULT_CONFIG, Icon, NORMALIZED_DIR_OPTIONS, Navigation, Pagination, SNAP_ALIGN_OPTIONS, Slide, icons, injectCarousel }; |
/** | ||
* Vue 3 Carousel 0.8.1 | ||
* Vue 3 Carousel 0.9.0 | ||
* (c) 2024 | ||
* @license MIT | ||
*/ | ||
import{Fragment as e,defineComponent as t,inject as l,reactive as a,ref as n,h as i,computed as o,provide as r,onMounted as u,nextTick as s,onUnmounted as d,watch as c,cloneVNode as v}from"vue";const p=["center","start","end","center-even","center-odd"],m=["viewport","carousel"],g=["ltr","left-to-right","rtl","right-to-left","ttb","top-to-bottom","btt","bottom-to-top"],f={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:p[0],dir:g[0],breakpointMode:m[0],breakpoints:void 0,i18n:{ariaNextSlide:"Navigate to next slide",ariaPreviousSlide:"Navigate to previous slide",ariaNavigateToSlide:"Navigate to slide {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"}},h={enabled:{default:f.enabled,type:Boolean},itemsToShow:{default:f.itemsToShow,type:Number},itemsToScroll:{default:f.itemsToScroll,type:Number},wrapAround:{default:f.wrapAround,type:Boolean},gap:{default:f.gap,type:Number},height:{default:f.height,type:[Number,String]},snapAlign:{default:f.snapAlign,validator:e=>p.includes(e)},transition:{default:f.transition,type:Number},breakpointMode:{default:f.breakpointMode,validator:e=>m.includes(e)},breakpoints:{default:f.breakpoints,type:Object},autoplay:{default:f.autoplay,type:Number},pauseAutoplayOnHover:{default:f.pauseAutoplayOnHover,type:Boolean},modelValue:{default:void 0,type:Number},mouseDrag:{default:f.mouseDrag,type:Boolean},touchDrag:{default:f.touchDrag,type:Boolean},dir:{default:f.dir,validator:e=>g.includes(e)},i18n:{default:f.i18n,type:Object}};function b({val:e,max:t,min:l}){return t<l?e:Math.min(Math.max(e,l),t)}function w(t){return t?t.reduce(((t,l)=>{var a;return l.type===e?[...t,...w(l.children)]:"CarouselSlide"===(null===(a=l.type)||void 0===a?void 0:a.name)?[...t,l]:t}),[]):[]}function S({val:e,max:t,min:l=0}){const a=t-l+1;return((e-l)%a+a)%a+l}function x(e="",t={}){return Object.entries(t).reduce(((e,[t,l])=>e.replace(`{${t}}`,String(l))),e)}var y,A=t({name:"ARIA",setup(){const e=l("config",a(Object.assign({},f))),t=l("currentSlide",n(0)),o=l("slidesCount",n(0));return()=>i("div",{class:["carousel__liveregion","carousel__sr-only"],"aria-live":"polite","aria-atomic":"true"},x(e.i18n.itemXofY,{currentSlide:t.value+1,slidesCount:o.value}))}}),_=t({name:"Carousel",props:h,emits:["init","drag","slide-start","loop","update:modelValue","slide-end","before-init"],setup(e,{slots:t,emit:l,expose:p}){var m;const g=n(null),h=n(null),x=n([]),y=n(0),_=n(0),T=o((()=>Object.assign(Object.assign(Object.assign({},f),e),{i18n:Object.assign(Object.assign({},f.i18n),e.i18n),breakpoints:void 0}))),C=a(Object.assign({},T.value)),M=n(null!==(m=e.modelValue)&&void 0!==m?m:0),N=n(0),O=n(0),k=n(0),L=n(0);let j=null,D=null,z=null;const E=o((()=>y.value+C.gap)),I=o((()=>{const e=C.dir||"lrt";return{"left-to-right":"ltr","right-to-left":"rtl","top-to-bottom":"ttb","bottom-to-top":"btt"}[e]||e})),$=o((()=>["ttb","btt"].includes(I.value)));function R(){var t;const l=("carousel"===C.breakpointMode?null===(t=g.value)||void 0===t?void 0:t.getBoundingClientRect().width:window.innerWidth)||0,a=Object.keys(e.breakpoints||{}).map((e=>Number(e))).sort(((e,t)=>+t-+e));let n=Object.assign({},T.value);a.some((t=>{var a;return l>=t&&(n=Object.assign(Object.assign({},n),null===(a=e.breakpoints)||void 0===a?void 0:a[t]),!0)})),Object.assign(C,n)}r("config",C),r("slidesCount",_),r("currentSlide",M),r("maxSlide",k),r("minSlide",L),r("slideSize",y),r("isVertical",$),r("normalizeDir",I);const B=function(e,t){let l;return function(...a){l&&clearTimeout(l),l=setTimeout((()=>{e(...a),l=null}),t)}}((()=>{R(),X(),V()}),16);function V(){if(!h.value)return;const e=h.value.getBoundingClientRect(),t=(C.itemsToShow-1)*C.gap;$.value?y.value=(e.height-t)/C.itemsToShow:y.value=(e.width-t)/C.itemsToShow}function X(){_.value<=0||(O.value=Math.ceil((_.value-1)/2),k.value=function({config:e,slidesCount:t}){var l;const{snapAlign:a="N/A",wrapAround:n,itemsToShow:i=1}=e;if(n)return Math.max(t-1,0);const o=null!==(l={start:Math.ceil(t-i),end:Math.ceil(t-1),center:t-Math.ceil((i-.5)/2),"center-odd":t-Math.ceil((i-.5)/2),"center-even":t-Math.ceil(i/2)}[a])&&void 0!==l?l:0;return Math.max(o,0)}({config:C,slidesCount:_.value}),L.value=function({config:e,slidesCount:t}){var l;const{snapAlign:a="N/A",wrapAround:n,itemsToShow:i=1}=e;return n||i>t?0:null!==(l={start:0,end:Math.floor(i-1),center:Math.floor((i-1)/2),"center-odd":Math.floor((i-1)/2),"center-even":Math.floor((i-2)/2)}[a])&&void 0!==l?l:0}({config:C,slidesCount:_.value}),C.wrapAround||(M.value=b({val:M.value,max:k.value,min:L.value})))}u((()=>{s((()=>V())),setTimeout((()=>V()),1e3),R(),Q(),window.addEventListener("resize",B,{passive:!0}),z=new ResizeObserver(B),g.value&&z.observe(g.value),l("init")})),d((()=>{D&&clearTimeout(D),j&&clearInterval(j),z&&g.value&&(z.unobserve(g.value),z=null),window.removeEventListener("resize",B,{passive:!0})}));let U=!1;const Y={x:0,y:0},P=a({x:0,y:0}),H=n(!1),G=n(!1),q=()=>{H.value=!0},F=()=>{H.value=!1};function W(e){const t=e.target.tagName;if(["INPUT","TEXTAREA","SELECT"].includes(t)||ee.value)return;if(U="touchstart"===e.type,!U&&(e.preventDefault(),0!==e.button))return;Y.x=U?e.touches[0].clientX:e.clientX,Y.y=U?e.touches[0].clientY:e.clientY;const l=U?"touchmove":"mousemove",a=U?"touchend":"mouseup";document.addEventListener(l,J,{passive:!1}),document.addEventListener(a,K,{passive:!0})}const J=function(e){let t=!1;return function(...l){t||(t=!0,requestAnimationFrame((()=>{e.apply(this,l),t=!1})))}}((e=>{G.value=!0;const t=U?e.touches[0].clientX:e.clientX,a=U?e.touches[0].clientY:e.clientY,n=t-Y.x,i=a-Y.y;P.x=n,P.y=i,l("drag",{deltaX:n,deltaY:i})}));function K(){const e=$.value?"y":"x",t=["rtl","btt"].includes(I.value)?-1:1,l=.4*Math.sign(P[e]),a=Math.round(P[e]/E.value+l)*t;if(a&&!U){const e=t=>{t.preventDefault(),window.removeEventListener("click",e)};window.addEventListener("click",e)}te(M.value-a),P.x=0,P.y=0,G.value=!1;const n=U?"touchmove":"mousemove",i=U?"touchend":"mouseup";document.removeEventListener(n,J),document.removeEventListener(i,K)}function Q(){!C.autoplay||C.autoplay<=0||(j=setInterval((()=>{C.pauseAutoplayOnHover&&H.value||le()}),C.autoplay))}function Z(){j&&(clearInterval(j),j=null),Q()}const ee=n(!1);function te(e){const t=C.wrapAround?e:b({val:e,max:k.value,min:L.value});M.value===t||ee.value||(l("slide-start",{slidingToIndex:e,currentSlideIndex:M.value,prevSlideIndex:N.value,slidesCount:_.value}),ee.value=!0,N.value=M.value,M.value=t,D=setTimeout((()=>{if(C.wrapAround){const a=S({val:t,max:k.value,min:0});a!==M.value&&(M.value=a,l("loop",{currentSlideIndex:M.value,slidingToIndex:e}))}l("update:modelValue",M.value),l("slide-end",{currentSlideIndex:M.value,prevSlideIndex:N.value,slidesCount:_.value}),ee.value=!1,Z()}),C.transition))}function le(){te(M.value+C.itemsToScroll)}function ae(){te(M.value-C.itemsToScroll)}const ne={slideTo:te,next:le,prev:ae};function ie(){R(),X(),V(),Z()}r("nav",ne),r("isSliding",ee),c((()=>Object.assign({},e)),ie,{deep:!0}),c((()=>e.modelValue),(e=>{e!==M.value&&te(Number(e))})),c(_,X),l("before-init");const oe={config:C,slidesCount:_,slideSize:y,currentSlide:M,maxSlide:k,minSlide:L,middleSlide:O};p({updateBreakpointsConfig:R,updateSlidesData:X,updateSlideSize:V,restartCarousel:ie,slideTo:te,next:le,prev:ae,nav:ne,data:oe});const re=o((()=>{const e=function({config:e,currentSlide:t,slidesCount:l}){const{snapAlign:a="N/A",wrapAround:n,itemsToShow:i=1}=e,o=((e,t)=>{var l;return null!==(l={start:0,center:(t-1)/2,"center-odd":(t-1)/2,"center-even":(t-2)/2,end:t-1}[e])&&void 0!==l?l:0})(a,i);return n?t-o:b({val:t-o,max:l-i,min:0})}({config:C,currentSlide:M.value,slidesCount:_.value}),t=C.wrapAround?_.value:0,l=["rtl","btt"].includes(I.value)?-1:1,a=(e+t)*E.value*l,n=$.value?P.y:P.x;return`translate${$.value?"Y":"X"}(${n-a}px)`})),ue=t.default||t.slides,se=t.addons,de=a(oe);return()=>{if(!C.enabled)return i("section",{ref:g,class:["carousel","is-disabled"]},null==ue?void 0:ue());const e=w(null==ue?void 0:ue(de)),t=(null==se?void 0:se(de))||[];e.forEach(((e,t)=>{e.props?e.props.index=t:e.props={index:t}}));let l=e;if(C.wrapAround){const t=e.map(((t,l)=>v(t,{index:-e.length+l,isClone:!0,key:`clone-before-${l}`}))),a=e.map(((t,l)=>v(t,{index:e.length+l,isClone:!0,key:`clone-after-${l}`})));l=[...t,...e,...a]}x.value=e,_.value=Math.max(e.length,1);const a=i("ol",{class:"carousel__track",style:{transform:re.value,transition:`${ee.value?C.transition:0}ms`,gap:`${C.gap}px`},onMousedownCapture:C.mouseDrag?W:null,onTouchstartPassiveCapture:C.touchDrag?W:null},l),n=i("div",{class:"carousel__viewport",ref:h},a);return i("section",{ref:g,class:["carousel",`is-${I.value}`,{"is-vertical":$.value,"is-sliding":ee.value,"is-dragging":G.value,"is-hover":H.value}],style:{"--vc-trk-height":`${"number"==typeof C.height?`${C.height}px`:C.height}`},dir:I.value,"aria-label":C.i18n.ariaGallery,tabindex:"0",onMouseenter:q,onMouseleave:F},[n,t,i(A)])}}});!function(e){e.arrowUp="arrowUp",e.arrowDown="arrowDown",e.arrowRight="arrowRight",e.arrowLeft="arrowLeft"}(y||(y={}));const T={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 C=e=>{const t=l("config",a(Object.assign({},f))),n=String(e.name),o=`icon${n.charAt(0).toUpperCase()+n.slice(1)}`;if(!n||"string"!=typeof n||!(n in y))return;const r=i("path",{d:T[n]}),u=t.i18n[o]||e.title||n,s=i("title",u);return i("svg",{class:"carousel__icon",viewBox:"0 0 24 24",role:"img","aria-label":u},[s,r])};C.props={name:String,title:String};const M=(e,{slots:t,attrs:o})=>{const{next:r,prev:u}=t||{},s=l("config",a(Object.assign({},f))),d=l("maxSlide",n(1)),c=l("minSlide",n(1)),v=l("normalizeDir",n("ltr")),p=l("currentSlide",n(1)),m=l("nav",{}),{wrapAround:g,i18n:h}=s;return[i("button",{type:"button",class:["carousel__prev",!g&&p.value<=c.value&&"carousel__prev--disabled",null==o?void 0:o.class],"aria-label":h.ariaPreviousSlide,title:h.ariaPreviousSlide,onClick:m.prev},(null==u?void 0:u())||i(C,{name:{ltr:"arrowLeft",rtl:"arrowRight",ttb:"arrowUp",btt:"arrowDown"}[v.value]})),i("button",{type:"button",class:["carousel__next",!g&&p.value>=d.value&&"carousel__next--disabled",null==o?void 0:o.class],"aria-label":h.ariaNextSlide,title:h.ariaNextSlide,onClick:m.next},(null==r?void 0:r())||i(C,{name:{ltr:"arrowRight",rtl:"arrowLeft",ttb:"arrowDown",btt:"arrowUp"}[v.value]}))]},N=()=>{const e=l("config",a(Object.assign({},f))),t=l("maxSlide",n(1)),o=l("minSlide",n(1)),r=l("currentSlide",n(1)),u=l("nav",{}),s=e=>S({val:r.value,max:t.value,min:0})===e,d=[];for(let l=o.value;l<t.value+1;l++){const t=x(e.i18n.ariaNavigateToSlide,{slideNumber:l+1}),a=i("button",{type:"button",class:{"carousel__pagination-button":!0,"carousel__pagination-button--active":s(l)},"aria-label":t,title:t,onClick:()=>u.slideTo(l)}),n=i("li",{class:"carousel__pagination-item",key:l},a);d.push(n)}return i("ol",{class:"carousel__pagination"},d)};var O=t({name:"CarouselSlide",props:{index:{type:Number,default:1},isClone:{type:Boolean,default:!1}},setup(e,{slots:t}){const r=l("config",a(Object.assign({},f))),u=l("currentSlide",n(0)),s=l("slidesToScroll",n(0)),d=l("isSliding",n(!1)),c=l("isVertical",n(!1)),v=l("slideSize",n(0)),p=o((()=>e.index===u.value)),m=o((()=>e.index===u.value-1)),g=o((()=>e.index===u.value+1)),h=o((()=>{const t=Math.floor(s.value),l=Math.ceil(s.value+r.itemsToShow-1);return e.index>=t&&e.index<=l})),b=o((()=>{const e=r.gap?`${v.value}px`:100/r.itemsToShow+"%";return c.value?{height:e,width:""}:{width:e,height:""}}));return()=>{var l,a;return r.enabled?i("li",{style:b.value,class:{carousel__slide:!0,"carousel__slide--clone":e.isClone,"carousel__slide--visible":h.value,"carousel__slide--active":p.value,"carousel__slide--prev":m.value,"carousel__slide--next":g.value,"carousel__slide--sliding":d.value},"aria-hidden":!h.value},null===(a=t.default)||void 0===a?void 0:a.call(t,{isActive:p.value,isClone:e.isClone,isPrev:m.value,isNext:g.value,isSliding:d.value,isVisible:h.value})):null===(l=t.default)||void 0===l?void 0:l.call(t)}}});export{_ as Carousel,C as Icon,M as Navigation,N as Pagination,O as Slide}; | ||
import{defineComponent as e,useId as t,inject as n,provide as i,ref as o,watch as a,computed as l,getCurrentInstance as r,onUnmounted as s,onMounted as u,onUpdated as c,h as d,cloneVNode as v,shallowReactive as p,reactive as m,watchEffect as g,onBeforeUnmount as h,pushScopeId as f,popScopeId as w}from"vue";const b=Symbol("carousel");function S({val:e,max:t,min:n}){return t<n?e:Math.min(Math.max(e,isNaN(n)?e:n),isNaN(t)?e:t)}function x({val:e,max:t,min:n=0}){const i=t-n+1;return((e-n)%i+i)%i+n}function y(e="",t={}){return Object.entries(t).reduce(((e,[t,n])=>e.replace(`{${t}}`,String(n))),e)}function A(e,t=0){let n=!1,i=0,o=null;function a(...a){if(n)return;n=!0;const l=()=>{o=requestAnimationFrame((o=>{o-i>t?(i=o,e(...a),n=!1):l()}))};l()}return a.cancel=()=>{o&&(cancelAnimationFrame(o),o=null,n=!1)},a}const C=e({name:"CarouselSlide",props:{isClone:{type:Boolean,default:!1},id:{type:String,default:e=>e.isClone?void 0:t()},index:{type:Number,default:0}},setup(e,{slots:t,expose:v}){const p=n(b);if(i(b,void 0),!p)return null;const m=o(e.index);a((()=>e.index),(e=>m.value=e));const g=l((()=>m.value===p.currentSlide)),h=l((()=>m.value===p.currentSlide-1)),f=l((()=>m.value===p.currentSlide+1)),w=l((()=>m.value>=Math.floor(p.scrolledIndex)&&m.value<Math.ceil(p.scrolledIndex)+p.config.itemsToShow)),S=l((()=>{const e=p.config.gap>0&&p.config.itemsToShow>1?`calc(${100/p.config.itemsToShow}% - ${p.config.gap*(p.config.itemsToShow-1)/p.config.itemsToShow}px)`:100/p.config.itemsToShow+"%";return p.isVertical?{height:e}:{width:e}})),x=r();if(e.isClone){const e=e=>{[...(null==e?void 0:e.el)?e.el.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'):[]].filter((e=>!e.hasAttribute("disabled")&&!e.getAttribute("aria-hidden"))).forEach((e=>e.setAttribute("tabindex","-1")))};u((()=>{e(x.vnode)})),c((()=>{e(x.vnode)}))}else p.registerSlide(x,(e=>m.value=e)),s((()=>{p.unregisterSlide(x)}));return v({id:e.id}),()=>{var n,i;return p.config.enabled?d("li",{style:S.value,class:{carousel__slide:!0,"carousel__slide--clone":e.isClone,"carousel__slide--visible":w.value,"carousel__slide--active":g.value,"carousel__slide--prev":h.value,"carousel__slide--next":f.value,"carousel__slide--sliding":p.isSliding},onFocusin:()=>{p.viewport&&(p.viewport.scrollLeft=0),p.nav.slideTo(m.value)},id:e.isClone?void 0:e.id,"aria-hidden":e.isClone||void 0},null===(i=t.default)||void 0===i?void 0:i.call(t,{isActive:g.value,isClone:e.isClone,isPrev:h.value,isNext:f.value,isSliding:p.isSliding,isVisible:w.value})):null===(n=t.default)||void 0===n?void 0:n.call(t)}}});function _({slides:e,position:t,toShow:n}){const i=[],o="before"===t,a=o?0:n;for(let l=o?-n:0;l<a;l++){const n={index:o?l:e.length>0?l+e.length:l+99999,isClone:!0,key:`clone-${t}-${l}`};i.push(e.length>0?v(e[(l+e.length)%e.length].vnode,n):d(C,n))}return i}const T=e({name:"CarouselAria",setup(){const e=n(b);if(e)return()=>d("div",{class:["carousel__liveregion","carousel__sr-only"],"aria-live":"polite","aria-atomic":"true"},y(e.config.i18n.itemXofY,{currentSlide:e.currentSlide+1,slidesCount:e.slidesCount}))}}),k=Symbol("carousel"),L=["center","start","end","center-even","center-odd"],N=["viewport","carousel"],M=["ltr","left-to-right","rtl","right-to-left","ttb","top-to-bottom","btt","bottom-to-top"],D={ariaNextSlide:"Navigate to next slide",ariaPreviousSlide:"Navigate to previous slide",ariaNavigateToSlide:"Navigate to slide {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"},E={"left-to-right":"ltr","right-to-left":"rtl","top-to-bottom":"ttb","bottom-to-top":"btt"},O=Object.values(E),I={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:L[0],dir:M[0],breakpointMode:N[0],breakpoints:void 0,i18n:D},j=e({name:"VueCarousel",props:{enabled:{default:I.enabled,type:Boolean},itemsToShow:{default:I.itemsToShow,type:Number},itemsToScroll:{default:I.itemsToScroll,type:Number},wrapAround:{default:I.wrapAround,type:Boolean},gap:{default:I.gap,type:Number},height:{default:I.height,type:[Number,String]},snapAlign:{default:I.snapAlign,validator:e=>L.includes(e)},transition:{default:I.transition,type:Number},breakpointMode:{default:I.breakpointMode,validator:e=>N.includes(e)},breakpoints:{default:I.breakpoints,type:Object},autoplay:{default:I.autoplay,type:Number},pauseAutoplayOnHover:{default:I.pauseAutoplayOnHover,type:Boolean},modelValue:{default:void 0,type:Number},mouseDrag:{default:I.mouseDrag,type:Boolean},touchDrag:{default:I.touchDrag,type:Boolean},dir:{type:String,default:I.dir,validator:e=>M.includes(e)},i18n:{default:I.i18n,type:Object}},emits:["init","drag","slide-start","loop","update:modelValue","slide-end","before-init"],setup(e,{slots:t,emit:n,expose:r}){var s;const c=o(null),v=o(null),y=p([]),C=o(0),k=l((()=>y.length)),L=l((()=>Object.assign(Object.assign(Object.assign({},I),e),{i18n:Object.assign(Object.assign({},I.i18n),e.i18n),breakpoints:void 0}))),N=m(Object.assign({},L.value)),M=o(null!==(s=e.modelValue)&&void 0!==s?s:0),D=o(0),O=l((()=>Math.ceil((k.value-1)/2))),j=l((()=>function({config:e,slidesCount:t}){const{snapAlign:n="center",wrapAround:i,itemsToShow:o=1}=e;return Math.max(function(){switch(i?"":n){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:N,slidesCount:k.value}))),z=l((()=>function({config:e,slidesCount:t}){const{snapAlign:n="center",wrapAround:i,itemsToShow:o=1}=e;return i||o>t?0:Math.max(0,function(){switch(n){case"end":return Math.floor(o-1);case"center":case"center-odd":return Math.floor((o-1)/2);case"center-even":return Math.floor((o-2)/2)}return 0}())}({config:N,slidesCount:k.value})));let $=null,B=null,R=null;const V=l((()=>C.value+N.gap)),U=l((()=>{const e=N.dir||"ltr";return e in E?E[e]:e})),X=[],F=l((()=>["rtl","btt"].includes(U.value))),Y=l((()=>["ttb","btt"].includes(U.value))),P=l((()=>Math.ceil(N.itemsToShow)+1));function q(){var t;const n=("carousel"===L.value.breakpointMode?null===(t=c.value)||void 0===t?void 0:t.getBoundingClientRect().width:"undefined"!=typeof window?window.innerWidth:0)||0,i=Object.keys(e.breakpoints||{}).map((e=>Number(e))).sort(((e,t)=>+t-+e)),o={};i.some((t=>n>=t&&(Object.assign(o,e.breakpoints[t]),o.i18n&&Object.assign(o.i18n,L.value.i18n,e.breakpoints[t].i18n),!0))),Object.assign(N,L.value,o)}const H=A((()=>{q(),J(),K()})),G=l((()=>(N.itemsToShow-1)*N.gap)),W=p(new Set);function K(){if(!v.value)return;let e=1;if(W.forEach((t=>{const n=function(e){const{transform:t}=window.getComputedStyle(e);return t.split(/[(,)]/).slice(1,-1).map((e=>parseFloat(e)))}(t);6===n.length&&(e*=n[0])})),Y.value){if("auto"!==N.height){const e="string"==typeof N.height&&isNaN(parseInt(N.height))?v.value.getBoundingClientRect().height:parseInt(N.height);C.value=(e-G.value)/N.itemsToShow}}else{const t=v.value.getBoundingClientRect().width;C.value=(t/e-G.value)/N.itemsToShow}}function J(){!N.wrapAround&&k.value>0&&(M.value=S({val:M.value,max:j.value,min:z.value}))}let Q;g((()=>J())),g((()=>{K()}));const Z=e=>{const t=e.target;if(t&&W.add(t),!Q){const e=()=>{Q=requestAnimationFrame((()=>{K(),e()}))};e()}},ee=e=>{const t=e.target;t&&W.delete(t),Q&&0===W.size&&(cancelAnimationFrame(Q),K())};q(),u((()=>{"carousel"===L.value.breakpointMode&&q(),me(),document&&(document.addEventListener("animationstart",Z),document.addEventListener("animationend",ee)),c.value&&(R=new ResizeObserver(H),R.observe(c.value)),n("init")})),h((()=>{y.splice(0,y.length),X.splice(0,X.length),B&&clearTimeout(B),Q&&cancelAnimationFrame(Q),$&&clearInterval($),R&&(R.disconnect(),R=null),document&&(document.removeEventListener("keydown",se),document.removeEventListener("animationstart",Z),document.removeEventListener("animationend",ee)),c.value&&(c.value.removeEventListener("transitionend",K),c.value.removeEventListener("animationiteration",K))}));let te=!1;const ne={x:0,y:0},ie=m({x:0,y:0}),oe=o(!1),ae=o(!1),le=()=>{oe.value=!0},re=()=>{oe.value=!1},se=A((e=>{if(!e.ctrlKey)switch(e.key){case"ArrowLeft":case"ArrowUp":Y.value===e.key.endsWith("Up")&&(F.value?xe.next(!0):xe.prev(!0));break;case"ArrowRight":case"ArrowDown":Y.value===e.key.endsWith("Down")&&(F.value?xe.prev(!0):xe.next(!0))}}),200),ue=()=>{document.addEventListener("keydown",se)},ce=()=>{document.removeEventListener("keydown",se)};function de(e){const t=e.target.tagName;if(["INPUT","TEXTAREA","SELECT"].includes(t)||fe.value)return;if(te="touchstart"===e.type,!te&&(e.preventDefault(),0!==e.button))return;ne.x="touches"in e?e.touches[0].clientX:e.clientX,ne.y="touches"in e?e.touches[0].clientY:e.clientY;const n=te?"touchmove":"mousemove",i=te?"touchend":"mouseup";document.addEventListener(n,ve,{passive:!1}),document.addEventListener(i,pe,{passive:!0})}const ve=A((e=>{ae.value=!0;const t="touches"in e?e.touches[0].clientX:e.clientX,i="touches"in e?e.touches[0].clientY:e.clientY,o=t-ne.x,a=i-ne.y;ie.x=o,ie.y=a,n("drag",{deltaX:o,deltaY:a})}));function pe(){ve.cancel();const e=Y.value?"y":"x",t=F.value?-1:1,n=.4*Math.sign(ie[e]),i=Math.round(ie[e]/V.value+n)*t;if(i&&!te){const e=t=>{t.preventDefault(),window.removeEventListener("click",e)};window.addEventListener("click",e)}we(M.value-i),ie.x=0,ie.y=0,ae.value=!1;const o=te?"touchmove":"mousemove",a=te?"touchend":"mouseup";document.removeEventListener(o,ve),document.removeEventListener(a,pe)}function me(){!N.autoplay||N.autoplay<=0||($=setInterval((()=>{N.pauseAutoplayOnHover&&oe.value||be()}),N.autoplay))}function ge(){$&&(clearInterval($),$=null)}function he(){ge(),me()}const fe=o(!1);function we(e,t=!1){const i=N.wrapAround?e:S({val:e,max:j.value,min:z.value});if(M.value===i||!t&&fe.value)return;n("slide-start",{slidingToIndex:e,currentSlideIndex:M.value,prevSlideIndex:D.value,slidesCount:k.value}),ge(),fe.value=!0,D.value=M.value;const o=N.wrapAround?x({val:i,max:j.value,min:0}):i;M.value=i,o!==i&&Ce.pause(),n("update:modelValue",o),B=setTimeout((()=>{N.wrapAround&&o!==i&&(Ce.resume(),M.value=o,n("loop",{currentSlideIndex:M.value,slidingToIndex:e})),n("slide-end",{currentSlideIndex:M.value,prevSlideIndex:D.value,slidesCount:k.value}),fe.value=!1,he()}),N.transition)}function be(e=!1){we(M.value+N.itemsToScroll,e)}function Se(e=!1){we(M.value-N.itemsToScroll,e)}const xe={slideTo:we,next:be,prev:Se},ye=l((()=>function({config:e,currentSlide:t,slidesCount:n}){const{snapAlign:i="center",wrapAround:o,itemsToShow:a=1}=e,l=((e,t)=>{var n;return null!==(n={start:0,center:(t-1)/2,"center-odd":(t-1)/2,"center-even":(t-2)/2,end:t-1}[e])&&void 0!==n?n:0})(i,a);return o?x({val:t-l,max:n+a,min:0-a}):S({val:t-l,max:n-a,min:0})}({config:N,currentSlide:M.value,slidesCount:k.value}))),Ae=m({config:N,slidesCount:k,viewport:v,slides:y,scrolledIndex:ye,currentSlide:M,maxSlide:j,minSlide:z,slideSize:C,isVertical:Y,normalizedDir:U,nav:xe,isSliding:fe,registerSlide:(e,t)=>{t(y.length),y.push(e),X.push(t)},unregisterSlide:e=>{const t=y.indexOf(e);t>=0&&(y.splice(t,1),X.splice(t,1),X.slice(t).forEach(((e,n)=>e(t+n))))}});i(b,Ae),i("config",N),i("slidesCount",k),i("currentSlide",M),i("maxSlide",j),i("minSlide",z),i("slideSize",C),i("isVertical",Y),i("normalizeDir",U),i("nav",xe),i("isSliding",fe),a((()=>[L.value,e.breakpoints]),(()=>q()),{deep:!0}),a((()=>e.autoplay),(()=>he()));const Ce=a((()=>e.modelValue),(e=>{e!==M.value&&we(Number(e),!0)}));n("before-init");const _e=m({config:N,slidesCount:k,slideSize:C,currentSlide:M,maxSlide:j,minSlide:z,middleSlide:O});r({updateBreakpointsConfig:q,updateSlidesData:J,updateSlideSize:K,restartCarousel:function(){q(),J(),K(),he()},slideTo:we,next:be,prev:Se,nav:xe,data:_e});const Te=l((()=>Y.value&&C.value&&"auto"===N.height?`${C.value*N.itemsToShow+G.value}px`:"auto"!==N.height?"number"==typeof N.height||parseInt(N.height).toString()===N.height?`${N.height}px`:N.height:void 0)),ke=l((()=>{const e=N.wrapAround?P.value:0,t=F.value?-1:1,n=(ye.value+e)*V.value*t,i=Y.value?ie.y:ie.x;return`translate${Y.value?"Y":"X"}(${i-n}px)`}));return()=>{const e=t.default||t.slides,n=t.addons;let i=(null==e?void 0:e(_e))||[];if(!N.enabled||!i.length)return d("section",{ref:c,class:["carousel","is-disabled"]},i);const o=(null==n?void 0:n(_e))||[];if(N.wrapAround){const e=i.length>0?i[0].scopeId:null;f(e);const t=P.value,n=_({slides:y,position:"before",toShow:t}),o=_({slides:y,position:"after",toShow:t});w(),i=[...n,...i,...o]}const a=d("ol",{class:"carousel__track",style:{transform:ke.value,"transition-duration":fe.value?`${N.transition}ms`:void 0,gap:N.gap>0?`${N.gap}px`:void 0},onMousedownCapture:N.mouseDrag?de:null,onTouchstartPassiveCapture:N.touchDrag?de:null},i),l=d("div",{class:"carousel__viewport",ref:v},a);return d("section",{ref:c,class:["carousel",`is-${U.value}`,{"is-vertical":Y.value,"is-sliding":fe.value,"is-dragging":ae.value,"is-hover":oe.value}],style:{"--vc-trk-height":Te.value},dir:U.value,"aria-label":N.i18n.ariaGallery,tabindex:"0",onFocus:ue,onBlur:ce,onMouseenter:le,onMouseleave:re},[l,o,d(T)])}}});var z;!function(e){e.arrowUp="arrowUp",e.arrowDown="arrowDown",e.arrowRight="arrowRight",e.arrowLeft="arrowLeft"}(z||(z={}));const $=e=>`icon${e.charAt(0).toUpperCase()+e.slice(1)}`,B=e=>e&&e in z,R={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"},V=e({props:{name:{type:String,required:!0,validator:B},title:{type:String,default:e=>e.name?I.i18n[$(e.name)]:""}},setup(e){const t=n(b,null);return()=>{const n=e.name;if(!n||!B(n))return;const i=d("path",{d:R[n]}),o=(null==t?void 0:t.config.i18n[$(n)])||e.title||n,a=d("title",o);return d("svg",{class:"carousel__icon",viewBox:"0 0 24 24",role:"img","aria-label":o},[a,i])}}}),U=e({name:"CarouselNavigation",setup(e,{slots:t}){const i=n(b);if(!i)return null;const{next:o,prev:a}=t;return()=>{const{wrapAround:t,i18n:n}=i.config;return[d("button",{type:"button",class:["carousel__prev",!t&&i.currentSlide<=i.minSlide&&"carousel__prev--disabled",e.class],"aria-label":n.ariaPreviousSlide,title:n.ariaPreviousSlide,onClick:i.nav.prev},(null==a?void 0:a())||d(V,{name:{ltr:"arrowLeft",rtl:"arrowRight",ttb:"arrowUp",btt:"arrowDown"}[i.normalizedDir]})),d("button",{type:"button",class:["carousel__next",!t&&i.currentSlide>=i.maxSlide&&"carousel__next--disabled",e.class],"aria-label":n.ariaNextSlide,title:n.ariaNextSlide,onClick:i.nav.next},(null==o?void 0:o())||d(V,{name:{ltr:"arrowRight",rtl:"arrowLeft",ttb:"arrowDown",btt:"arrowUp"}[i.normalizedDir]}))]}}}),X=e({name:"CarouselPagination",setup(e){const t=n(b);if(!t)return null;const i=e=>x({val:t.currentSlide,max:t.maxSlide,min:0})===e;return()=>{var n,o;const a=[];for(let l=t.minSlide;l<=t.maxSlide;l++){const r=y(t.config.i18n.ariaNavigateToSlide,{slideNumber:l+1}),s=i(l),u=d("button",{type:"button",class:{"carousel__pagination-button":!0,"carousel__pagination-button--active":s},"aria-label":r,"aria-pressed":s,"aria-controls":null===(o=null===(n=t.slides[l])||void 0===n?void 0:n.exposed)||void 0===o?void 0:o.id,title:r,onClick:e.disableOnClick?void 0:()=>t.nav.slideTo(l)}),c=d("li",{class:"carousel__pagination-item",key:l},u);a.push(c)}return d("ol",{class:"carousel__pagination"},a)}}});export{N as BREAKPOINT_MODE_OPTIONS,j as Carousel,I as DEFAULT_CONFIG,E as DIR_MAP,M as DIR_OPTIONS,D as I18N_DEFAULT_CONFIG,V as Icon,O as NORMALIZED_DIR_OPTIONS,U as Navigation,X as Pagination,L as SNAP_ALIGN_OPTIONS,C as Slide,R as icons,k as injectCarousel}; |
1156
dist/carousel.js
/** | ||
* Vue 3 Carousel 0.8.1 | ||
* Vue 3 Carousel 0.9.0 | ||
* (c) 2024 | ||
@@ -12,2 +12,301 @@ * @license MIT | ||
// 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$1 = Symbol('carousel'); | ||
/** | ||
* Determines the minimum slide index based on the configuration. | ||
* | ||
* @param {GetMinSlideIndexArgs} args - The carousel configuration and slide count. | ||
* @returns {number} The minimum slide index. | ||
*/ | ||
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; | ||
} | ||
// Map snapAlign values to calculation logic | ||
function snapAlignCalculations() { | ||
switch (snapAlign) { | ||
case 'end': | ||
return Math.floor(itemsToShow - 1); | ||
case 'center': | ||
case 'center-odd': | ||
return Math.floor((itemsToShow - 1) / 2); | ||
case 'center-even': | ||
return Math.floor((itemsToShow - 2) / 2); | ||
} | ||
return 0; | ||
} | ||
// Return the calculated offset or default to 0 for invalid snapAlign values | ||
return Math.max(0, snapAlignCalculations()); | ||
} | ||
/** | ||
* Determines the maximum slide index based on the configuration. | ||
* | ||
* @param {Args} args - The carousel configuration and slide count. | ||
* @returns {number} The maximum slide index. | ||
*/ | ||
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); | ||
} | ||
} | ||
// Return the result ensuring it's non-negative | ||
return Math.max(snapAlignCalculations(), 0); | ||
} | ||
function getNumberInRange({ val, max, min }) { | ||
if (max < min) { | ||
return val; | ||
} | ||
return Math.min(Math.max(val, isNaN(min) ? val : min), isNaN(max) ? val : max); | ||
} | ||
function mapNumberToRange({ val, max, min = 0 }) { | ||
const mod = max - min + 1; | ||
return ((((val - min) % mod) + mod) % mod) + min; | ||
} | ||
const calculateOffset = (snapAlign, itemsToShow) => { | ||
var _a; | ||
const offsetMap = { | ||
start: 0, | ||
center: (itemsToShow - 1) / 2, | ||
'center-odd': (itemsToShow - 1) / 2, | ||
'center-even': (itemsToShow - 2) / 2, | ||
end: itemsToShow - 1, | ||
}; | ||
return (_a = offsetMap[snapAlign]) !== null && _a !== void 0 ? _a : 0; // Fallback to 0 for unknown snapAlign | ||
}; | ||
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 getNumberInRange({ | ||
val: currentSlide - offset, | ||
max: slidesCount - itemsToShow, | ||
min: 0, | ||
}); | ||
} | ||
else { | ||
return mapNumberToRange({ | ||
val: currentSlide - offset, | ||
max: slidesCount + itemsToShow, | ||
min: 0 - itemsToShow, | ||
}); | ||
} | ||
} | ||
function i18nFormatter(string = '', values = {}) { | ||
return Object.entries(values).reduce((acc, [key, value]) => acc.replace(`{${key}}`, String(value)), string); | ||
} | ||
/** | ||
* Returns a throttled version of the function using requestAnimationFrame. | ||
* | ||
* @param fn - The function to throttle. | ||
* @param ms - The number of milliseconds to wait for the throttled function to be called again | ||
*/ | ||
function throttle(fn, ms = 0) { | ||
let isThrottled = false; | ||
let start = 0; | ||
let frameId = null; | ||
function throttled(...args) { | ||
if (isThrottled) | ||
return; | ||
isThrottled = true; | ||
const step = () => { | ||
frameId = requestAnimationFrame((time) => { | ||
const elapsed = time - start; | ||
if (elapsed > ms) { | ||
start = time; | ||
fn(...args); | ||
isThrottled = false; | ||
} | ||
else { | ||
step(); | ||
} | ||
}); | ||
}; | ||
step(); | ||
} | ||
throttled.cancel = () => { | ||
if (frameId) { | ||
cancelAnimationFrame(frameId); | ||
frameId = null; | ||
isThrottled = false; | ||
} | ||
}; | ||
return throttled; | ||
} | ||
function getTransformValues(el) { | ||
const { transform } = window.getComputedStyle(el); | ||
//add sanity check | ||
return transform | ||
.split(/[(,)]/) | ||
.slice(1, -1) | ||
.map((v) => parseFloat(v)); | ||
} | ||
const Slide = vue.defineComponent({ | ||
name: 'CarouselSlide', | ||
props: { | ||
isClone: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
id: { | ||
type: String, | ||
default: (props) => (props.isClone ? undefined : vue.useId()), | ||
}, | ||
index: { | ||
type: Number, | ||
default: 0, | ||
}, | ||
}, | ||
setup(props, { slots, expose }) { | ||
const carousel = vue.inject(injectCarousel$1); | ||
vue.provide(injectCarousel$1, undefined); // Don't provide for nested slides | ||
if (!carousel) { | ||
return null; // Don't render, let vue warn about the missing provide | ||
} | ||
const index = vue.ref(props.index); | ||
vue.watch(() => props.index, (i) => index.value = i); | ||
const isActive = vue.computed(() => index.value === carousel.currentSlide); | ||
const isPrev = vue.computed(() => index.value === carousel.currentSlide - 1); | ||
const isNext = vue.computed(() => index.value === carousel.currentSlide + 1); | ||
const isVisible = vue.computed(() => index.value >= Math.floor(carousel.scrolledIndex) && | ||
index.value < Math.ceil(carousel.scrolledIndex) + carousel.config.itemsToShow); | ||
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}%`; | ||
return carousel.isVertical ? { height: dimension } : { width: dimension }; | ||
}); | ||
const instance = vue.getCurrentInstance(); | ||
if (!props.isClone) { | ||
carousel.registerSlide(instance, (resolvedIndex) => (index.value = resolvedIndex)); | ||
vue.onUnmounted(() => { | ||
carousel.unregisterSlide(instance); | ||
}); | ||
} | ||
else { | ||
const makeUnfocusable = (node) => { | ||
[ | ||
...((node === null || node === void 0 ? void 0 : node.el) | ||
? node.el.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])') | ||
: []), | ||
] | ||
.filter((el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden')) | ||
.forEach((el) => el.setAttribute('tabindex', '-1')); | ||
}; | ||
// Prevent cloned slides from being focusable | ||
vue.onMounted(() => { | ||
makeUnfocusable(instance.vnode); | ||
}); | ||
vue.onUpdated(() => { | ||
makeUnfocusable(instance.vnode); | ||
}); | ||
} | ||
expose({ | ||
id: props.id, | ||
}); | ||
return () => { | ||
var _a, _b; | ||
if (!carousel.config.enabled) { | ||
return (_a = slots.default) === null || _a === void 0 ? void 0 : _a.call(slots); | ||
} | ||
return vue.h('li', { | ||
style: slideStyle.value, | ||
class: { | ||
carousel__slide: true, | ||
'carousel__slide--clone': props.isClone, | ||
'carousel__slide--visible': isVisible.value, | ||
'carousel__slide--active': isActive.value, | ||
'carousel__slide--prev': isPrev.value, | ||
'carousel__slide--next': isNext.value, | ||
'carousel__slide--sliding': carousel.isSliding, | ||
}, | ||
onFocusin: () => { | ||
// Prevent the viewport being scrolled by the focus | ||
if (carousel.viewport) { | ||
carousel.viewport.scrollLeft = 0; | ||
} | ||
carousel.nav.slideTo(index.value); | ||
}, | ||
id: props.isClone ? undefined : props.id, | ||
'aria-hidden': props.isClone || undefined, | ||
}, (_b = slots.default) === null || _b === void 0 ? void 0 : _b.call(slots, { | ||
isActive: isActive.value, | ||
isClone: props.isClone, | ||
isPrev: isPrev.value, | ||
isNext: isNext.value, | ||
isSliding: carousel.isSliding, | ||
isVisible: isVisible.value, | ||
})); | ||
}; | ||
}, | ||
}); | ||
function createCloneSlides({ slides, position, toShow }) { | ||
const clones = []; | ||
const isBefore = position === 'before'; | ||
const start = isBefore ? -toShow : 0; | ||
const end = isBefore ? 0 : toShow; | ||
for (let i = start; i < end; i++) { | ||
const index = isBefore ? i : slides.length > 0 ? i + slides.length : i + 99999; | ||
const props = { | ||
index, | ||
isClone: true, | ||
key: `clone-${position}-${i}`, | ||
}; | ||
clones.push(slides.length > 0 | ||
? vue.cloneVNode(slides[(i + slides.length) % slides.length].vnode, props) | ||
: vue.h(Slide, props)); | ||
} | ||
return clones; | ||
} | ||
const ARIA = vue.defineComponent({ | ||
name: 'CarouselAria', | ||
setup() { | ||
const carousel = vue.inject(injectCarousel$1); | ||
if (!carousel) { | ||
return; | ||
} | ||
return () => vue.h('div', { | ||
class: ['carousel__liveregion', 'carousel__sr-only'], | ||
'aria-live': 'polite', | ||
'aria-atomic': 'true', | ||
}, i18nFormatter(carousel.config.i18n['itemXofY'], { | ||
currentSlide: carousel.currentSlide + 1, | ||
slidesCount: carousel.slidesCount, | ||
})); | ||
}, | ||
}); | ||
// 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']; | ||
@@ -36,2 +335,9 @@ const BREAKPOINT_MODE_OPTIONS = ['viewport', 'carousel']; | ||
}; | ||
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 DEFAULT_CONFIG = { | ||
@@ -139,2 +445,3 @@ enabled: true, | ||
dir: { | ||
type: String, | ||
default: DEFAULT_CONFIG.dir, | ||
@@ -153,168 +460,4 @@ validator(value) { | ||
/** | ||
* Determines the maximum slide index based on the configuration. | ||
* | ||
* @param {Args} args - The carousel configuration and slide count. | ||
* @returns {number} The maximum slide index. | ||
*/ | ||
function getMaxSlideIndex({ config, slidesCount }) { | ||
var _a; | ||
const { snapAlign = 'N/A', wrapAround, itemsToShow = 1 } = config; | ||
// If wrapAround is enabled, the max index is the last slide | ||
if (wrapAround) { | ||
return Math.max(slidesCount - 1, 0); | ||
} | ||
// Map snapAlign values to calculation logic | ||
const snapAlignCalculations = { | ||
start: Math.ceil(slidesCount - itemsToShow), | ||
end: Math.ceil(slidesCount - 1), | ||
center: slidesCount - Math.ceil((itemsToShow - 0.5) / 2), | ||
'center-odd': slidesCount - Math.ceil((itemsToShow - 0.5) / 2), | ||
'center-even': slidesCount - Math.ceil(itemsToShow / 2), | ||
}; | ||
// Compute the max index based on snapAlign, or default to 0 | ||
const calculateMaxIndex = (_a = snapAlignCalculations[snapAlign]) !== null && _a !== void 0 ? _a : 0; | ||
// Return the result ensuring it's non-negative | ||
return Math.max(calculateMaxIndex, 0); | ||
} | ||
/** | ||
* Determines the minimum slide index based on the configuration. | ||
* | ||
* @param {Args} args - The carousel configuration and slide count. | ||
* @returns {number} The minimum slide index. | ||
*/ | ||
function getMinSlideIndex({ config, slidesCount }) { | ||
var _a; | ||
const { snapAlign = 'N/A', wrapAround, itemsToShow = 1 } = config; | ||
// If wrapAround is enabled or itemsToShow exceeds slidesCount, the minimum index is always 0 | ||
if (wrapAround || itemsToShow > slidesCount) { | ||
return 0; | ||
} | ||
// Map of snapAlign to offset calculations | ||
const snapAlignCalculations = { | ||
start: 0, | ||
end: Math.floor(itemsToShow - 1), | ||
center: Math.floor((itemsToShow - 1) / 2), | ||
'center-odd': Math.floor((itemsToShow - 1) / 2), | ||
'center-even': Math.floor((itemsToShow - 2) / 2), | ||
}; | ||
// Return the calculated offset or default to 0 for invalid snapAlign values | ||
return (_a = snapAlignCalculations[snapAlign]) !== null && _a !== void 0 ? _a : 0; | ||
} | ||
function getNumberInRange({ val, max, min }) { | ||
if (max < min) { | ||
return val; | ||
} | ||
return Math.min(Math.max(val, min), max); | ||
} | ||
const calculateOffset = (snapAlign, itemsToShow) => { | ||
var _a; | ||
const offsetMap = { | ||
start: 0, | ||
center: (itemsToShow - 1) / 2, | ||
'center-odd': (itemsToShow - 1) / 2, | ||
'center-even': (itemsToShow - 2) / 2, | ||
end: itemsToShow - 1, | ||
}; | ||
return (_a = offsetMap[snapAlign]) !== null && _a !== void 0 ? _a : 0; // Fallback to 0 for unknown snapAlign | ||
}; | ||
function getScrolledIndex({ config, currentSlide, slidesCount }) { | ||
const { snapAlign = 'N/A', 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 getNumberInRange({ | ||
val: currentSlide - offset, | ||
max: slidesCount - itemsToShow, | ||
min: 0, | ||
}); | ||
} | ||
return currentSlide - offset; | ||
} | ||
function getSlidesVNodes(vNode) { | ||
if (!vNode) | ||
return []; | ||
return vNode.reduce((acc, node) => { | ||
var _a; | ||
if (node.type === vue.Fragment) { | ||
return [...acc, ...getSlidesVNodes(node.children)]; | ||
} | ||
if (((_a = node.type) === null || _a === void 0 ? void 0 : _a.name) === 'CarouselSlide') { | ||
return [...acc, node]; | ||
} | ||
return acc; | ||
}, []); | ||
} | ||
function mapNumberToRange({ val, max, min = 0 }) { | ||
const mod = max - min + 1; | ||
return ((val - min) % mod + mod) % mod + min; | ||
} | ||
/** | ||
* return a throttle version of the function | ||
* Throttling | ||
* | ||
*/ | ||
// eslint-disable-next-line no-unused-vars | ||
function throttle(fn) { | ||
let isRunning = false; | ||
return function (...args) { | ||
if (!isRunning) { | ||
isRunning = true; | ||
requestAnimationFrame(() => { | ||
fn.apply(this, args); | ||
isRunning = false; | ||
}); | ||
} | ||
}; | ||
} | ||
/** | ||
* return a debounced version of the function | ||
* @param fn | ||
* @param delay | ||
*/ | ||
// eslint-disable-next-line no-unused-vars | ||
function debounce(fn, delay) { | ||
let timerId; | ||
return function (...args) { | ||
if (timerId) { | ||
clearTimeout(timerId); | ||
} | ||
timerId = setTimeout(() => { | ||
fn(...args); | ||
timerId = null; | ||
}, delay); | ||
}; | ||
} | ||
function i18nFormatter(string = '', values = {}) { | ||
return Object.entries(values).reduce((acc, [key, value]) => acc.replace(`{${key}}`, String(value)), string); | ||
} | ||
var ARIAComponent = vue.defineComponent({ | ||
name: 'ARIA', | ||
setup() { | ||
const config = vue.inject('config', vue.reactive(Object.assign({}, DEFAULT_CONFIG))); | ||
const currentSlide = vue.inject('currentSlide', vue.ref(0)); | ||
const slidesCount = vue.inject('slidesCount', vue.ref(0)); | ||
return () => vue.h('div', { | ||
class: ['carousel__liveregion', 'carousel__sr-only'], | ||
'aria-live': 'polite', | ||
'aria-atomic': 'true', | ||
}, i18nFormatter(config.i18n['itemXofY'], { | ||
currentSlide: currentSlide.value + 1, | ||
slidesCount: slidesCount.value, | ||
})); | ||
}, | ||
}); | ||
var Carousel = vue.defineComponent({ | ||
name: 'Carousel', | ||
const Carousel = vue.defineComponent({ | ||
name: 'VueCarousel', | ||
props: carouselProps, | ||
@@ -334,5 +477,5 @@ emits: [ | ||
const viewport = vue.ref(null); | ||
const slides = vue.ref([]); | ||
const slides = vue.shallowReactive([]); | ||
const slideSize = vue.ref(0); | ||
const slidesCount = vue.ref(0); | ||
const slidesCount = vue.computed(() => slides.length); | ||
const fallbackConfig = vue.computed(() => (Object.assign(Object.assign(Object.assign({}, DEFAULT_CONFIG), props), { i18n: Object.assign(Object.assign({}, DEFAULT_CONFIG.i18n), props.i18n), breakpoints: undefined }))); | ||
@@ -344,5 +487,9 @@ // current active config | ||
const prevSlideIndex = vue.ref(0); | ||
const middleSlideIndex = vue.ref(0); | ||
const maxSlideIndex = vue.ref(0); | ||
const minSlideIndex = vue.ref(0); | ||
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 }); | ||
}); | ||
let autoplayTimer = null; | ||
@@ -352,35 +499,42 @@ let transitionTimer = null; | ||
const effectiveSlideSize = vue.computed(() => slideSize.value + config.gap); | ||
const normalizeDir = vue.computed(() => { | ||
const dir = config.dir || 'lrt'; | ||
const dirMap = { | ||
'left-to-right': 'ltr', | ||
'right-to-left': 'rtl', | ||
'top-to-bottom': 'ttb', | ||
'bottom-to-top': 'btt', | ||
}; | ||
return dirMap[dir] || dir; | ||
const normalizedDir = vue.computed(() => { | ||
const dir = config.dir || 'ltr'; | ||
return dir in DIR_MAP ? DIR_MAP[dir] : dir; | ||
}); | ||
const isVertical = vue.computed(() => ['ttb', 'btt'].includes(normalizeDir.value)); | ||
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', normalizeDir); | ||
const indexCbs = []; | ||
const registerSlide = (slide, indexCb) => { | ||
indexCb(slides.length); | ||
slides.push(slide); | ||
indexCbs.push(indexCb); | ||
}; | ||
const unregisterSlide = (slide) => { | ||
const found = slides.indexOf(slide); | ||
if (found >= 0) { | ||
slides.splice(found, 1); | ||
indexCbs.splice(found, 1); | ||
// Update indexes after the one that was removed | ||
indexCbs.slice(found).forEach((cb, index) => cb(found + index)); | ||
} | ||
}; | ||
const isReversed = vue.computed(() => ['rtl', 'btt'].includes(normalizedDir.value)); | ||
const isVertical = vue.computed(() => ['ttb', 'btt'].includes(normalizedDir.value)); | ||
const clonedSlidesCount = vue.computed(() => Math.ceil(config.itemsToShow) + 1); | ||
function updateBreakpointsConfig() { | ||
var _a; | ||
// Determine the width source based on the 'breakpointMode' config | ||
const widthSource = (config.breakpointMode === 'carousel' | ||
const widthSource = (fallbackConfig.value.breakpointMode === 'carousel' | ||
? (_a = root.value) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width | ||
: window.innerWidth) || 0; | ||
: typeof window !== 'undefined' | ||
? window.innerWidth | ||
: 0) || 0; | ||
const breakpointsArray = Object.keys(props.breakpoints || {}) | ||
.map((key) => Number(key)) | ||
.sort((a, b) => +b - +a); | ||
let newConfig = Object.assign({}, fallbackConfig.value); | ||
const newConfig = {}; | ||
breakpointsArray.some((breakpoint) => { | ||
var _a; | ||
if (widthSource >= breakpoint) { | ||
newConfig = Object.assign(Object.assign({}, newConfig), (_a = props.breakpoints) === null || _a === void 0 ? void 0 : _a[breakpoint]); | ||
Object.assign(newConfig, props.breakpoints[breakpoint]); | ||
if (newConfig.i18n) { | ||
Object.assign(newConfig.i18n, fallbackConfig.value.i18n, props.breakpoints[breakpoint].i18n); | ||
} | ||
return true; | ||
@@ -390,9 +544,11 @@ } | ||
}); | ||
Object.assign(config, newConfig); | ||
Object.assign(config, fallbackConfig.value, newConfig); | ||
} | ||
const handleResize = debounce(() => { | ||
const handleResize = throttle(() => { | ||
updateBreakpointsConfig(); | ||
updateSlidesData(); | ||
updateSlideSize(); | ||
}, 16); | ||
}); | ||
const totalGap = vue.computed(() => (config.itemsToShow - 1) * config.gap); | ||
const transformElements = vue.shallowReactive(new Set()); | ||
/** | ||
@@ -404,20 +560,25 @@ * Setup functions | ||
return; | ||
const rect = viewport.value.getBoundingClientRect(); | ||
// Calculate the total gap space | ||
const totalGap = (config.itemsToShow - 1) * config.gap; | ||
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) { | ||
slideSize.value = (rect.height - totalGap) / config.itemsToShow; | ||
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; | ||
} | ||
} | ||
else { | ||
slideSize.value = (rect.width - totalGap) / config.itemsToShow; | ||
const width = viewport.value.getBoundingClientRect().width; | ||
slideSize.value = (width / multiplierWidth - totalGap.value) / config.itemsToShow; | ||
} | ||
} | ||
function updateSlidesData() { | ||
if (slidesCount.value <= 0) | ||
return; | ||
middleSlideIndex.value = Math.ceil((slidesCount.value - 1) / 2); | ||
maxSlideIndex.value = getMaxSlideIndex({ config, slidesCount: slidesCount.value }); | ||
minSlideIndex.value = getMinSlideIndex({ config, slidesCount: slidesCount.value }); | ||
if (!config.wrapAround) { | ||
if (!config.wrapAround && slidesCount.value > 0) { | ||
currentSlideIndex.value = getNumberInRange({ | ||
@@ -430,11 +591,45 @@ val: currentSlideIndex.value, | ||
} | ||
vue.watchEffect(() => updateSlidesData()); | ||
vue.watchEffect(() => { | ||
// Call updateSlideSize when viewport is ready and track deps | ||
updateSlideSize(); | ||
}); | ||
let animationInterval; | ||
const setAnimationInterval = (event) => { | ||
const target = event.target; | ||
if (target) { | ||
transformElements.add(target); | ||
} | ||
if (!animationInterval) { | ||
const stepAnimation = () => { | ||
animationInterval = requestAnimationFrame(() => { | ||
updateSlideSize(); | ||
stepAnimation(); | ||
}); | ||
}; | ||
stepAnimation(); | ||
} | ||
}; | ||
const finishAnimation = (event) => { | ||
const target = event.target; | ||
if (target) { | ||
transformElements.delete(target); | ||
} | ||
if (animationInterval && transformElements.size === 0) { | ||
cancelAnimationFrame(animationInterval); | ||
updateSlideSize(); | ||
} | ||
}; | ||
updateBreakpointsConfig(); | ||
vue.onMounted(() => { | ||
vue.nextTick(() => updateSlideSize()); | ||
// Overcome some edge cases | ||
setTimeout(() => updateSlideSize(), 1000); | ||
updateBreakpointsConfig(); | ||
if (fallbackConfig.value.breakpointMode === 'carousel') { | ||
updateBreakpointsConfig(); | ||
} | ||
initAutoplay(); | ||
window.addEventListener('resize', handleResize, { passive: true }); | ||
resizeObserver = new ResizeObserver(handleResize); | ||
if (document) { | ||
document.addEventListener('animationstart', setAnimationInterval); | ||
document.addEventListener('animationend', finishAnimation); | ||
} | ||
if (root.value) { | ||
resizeObserver = new ResizeObserver(handleResize); | ||
resizeObserver.observe(root.value); | ||
@@ -444,16 +639,28 @@ } | ||
}); | ||
vue.onUnmounted(() => { | ||
vue.onBeforeUnmount(() => { | ||
// Empty the slides before they unregister for better performance | ||
slides.splice(0, slides.length); | ||
indexCbs.splice(0, indexCbs.length); | ||
if (transitionTimer) { | ||
clearTimeout(transitionTimer); | ||
} | ||
if (animationInterval) { | ||
cancelAnimationFrame(animationInterval); | ||
} | ||
if (autoplayTimer) { | ||
clearInterval(autoplayTimer); | ||
} | ||
if (resizeObserver && root.value) { | ||
resizeObserver.unobserve(root.value); | ||
if (resizeObserver) { | ||
resizeObserver.disconnect(); | ||
resizeObserver = null; | ||
} | ||
window.removeEventListener('resize', handleResize, { | ||
passive: true, | ||
}); | ||
if (document) { | ||
document.removeEventListener('keydown', handleArrowKeys); | ||
document.removeEventListener('animationstart', setAnimationInterval); | ||
document.removeEventListener('animationend', finishAnimation); | ||
} | ||
if (root.value) { | ||
root.value.removeEventListener('transitionend', updateSlideSize); | ||
root.value.removeEventListener('animationiteration', updateSlideSize); | ||
} | ||
}); | ||
@@ -474,2 +681,36 @@ /** | ||
}; | ||
const handleArrowKeys = throttle((event) => { | ||
if (event.ctrlKey) | ||
return; | ||
switch (event.key) { | ||
case 'ArrowLeft': | ||
case 'ArrowUp': | ||
if (isVertical.value === event.key.endsWith('Up')) { | ||
if (isReversed.value) { | ||
nav.next(true); | ||
} | ||
else { | ||
nav.prev(true); | ||
} | ||
} | ||
break; | ||
case 'ArrowRight': | ||
case 'ArrowDown': | ||
if (isVertical.value === event.key.endsWith('Down')) { | ||
if (isReversed.value) { | ||
nav.prev(true); | ||
} | ||
else { | ||
nav.next(true); | ||
} | ||
} | ||
break; | ||
} | ||
}, 200); | ||
const handleFocus = () => { | ||
document.addEventListener('keydown', handleArrowKeys); | ||
}; | ||
const handleBlur = () => { | ||
document.removeEventListener('keydown', handleArrowKeys); | ||
}; | ||
function handleDragStart(event) { | ||
@@ -492,4 +733,4 @@ // Prevent drag initiation on input elements or if already sliding | ||
// Initialize start positions for the drag | ||
startPosition.x = isTouch ? event.touches[0].clientX : event.clientX; | ||
startPosition.y = isTouch ? event.touches[0].clientY : event.clientY; | ||
startPosition.x = 'touches' in event ? event.touches[0].clientX : event.clientX; | ||
startPosition.y = 'touches' in event ? event.touches[0].clientY : event.clientY; | ||
// Attach event listeners for dragging and drag end | ||
@@ -504,4 +745,4 @@ const moveEvent = isTouch ? 'touchmove' : 'mousemove'; | ||
// Get the current position based on the interaction type (touch or mouse) | ||
const currentX = isTouch ? event.touches[0].clientX : event.clientX; | ||
const currentY = isTouch ? event.touches[0].clientY : event.clientY; | ||
const currentX = 'touches' in event ? event.touches[0].clientX : event.clientX; | ||
const currentY = 'touches' in event ? event.touches[0].clientY : event.clientY; | ||
// Calculate deltas for X and Y axes | ||
@@ -517,5 +758,6 @@ const deltaX = currentX - startPosition.x; | ||
function handleDragEnd() { | ||
handleDragging.cancel(); | ||
// Determine the active axis and direction multiplier | ||
const dragAxis = isVertical.value ? 'y' : 'x'; | ||
const directionMultiplier = ['rtl', 'btt'].includes(normalizeDir.value) ? -1 : 1; | ||
const directionMultiplier = isReversed.value ? -1 : 1; | ||
// Calculate dragged slides with a tolerance to account for incomplete drags | ||
@@ -559,3 +801,3 @@ const tolerance = Math.sign(dragged[dragAxis]) * 0.4; // Smooth out small drags | ||
} | ||
function resetAutoplay() { | ||
function stopAutoplay() { | ||
if (autoplayTimer) { | ||
@@ -565,2 +807,5 @@ clearInterval(autoplayTimer); | ||
} | ||
} | ||
function resetAutoplay() { | ||
stopAutoplay(); | ||
initAutoplay(); | ||
@@ -572,3 +817,3 @@ } | ||
const isSliding = vue.ref(false); | ||
function slideTo(slideIndex) { | ||
function slideTo(slideIndex, skipTransition = false) { | ||
const currentVal = config.wrapAround | ||
@@ -581,3 +826,4 @@ ? slideIndex | ||
}); | ||
if (currentSlideIndex.value === currentVal || isSliding.value) { | ||
if (currentSlideIndex.value === currentVal || | ||
(!skipTransition && isSliding.value)) { | ||
return; | ||
@@ -591,13 +837,21 @@ } | ||
}); | ||
stopAutoplay(); | ||
isSliding.value = true; | ||
prevSlideIndex.value = currentSlideIndex.value; | ||
const mappedNumber = config.wrapAround | ||
? mapNumberToRange({ | ||
val: currentVal, | ||
max: maxSlideIndex.value, | ||
min: 0, | ||
}) | ||
: currentVal; | ||
currentSlideIndex.value = currentVal; | ||
if (mappedNumber !== currentVal) { | ||
modelWatcher.pause(); | ||
} | ||
emit('update:modelValue', mappedNumber); | ||
transitionTimer = setTimeout(() => { | ||
if (config.wrapAround) { | ||
const mappedNumber = mapNumberToRange({ | ||
val: currentVal, | ||
max: maxSlideIndex.value, | ||
min: 0, | ||
}); | ||
if (mappedNumber !== currentSlideIndex.value) { | ||
if (mappedNumber !== currentVal) { | ||
modelWatcher.resume(); | ||
currentSlideIndex.value = mappedNumber; | ||
@@ -610,3 +864,2 @@ emit('loop', { | ||
} | ||
emit('update:modelValue', currentSlideIndex.value); | ||
emit('slide-end', { | ||
@@ -621,9 +874,41 @@ currentSlideIndex: currentSlideIndex.value, | ||
} | ||
function next() { | ||
slideTo(currentSlideIndex.value + config.itemsToScroll); | ||
function next(skipTransition = false) { | ||
slideTo(currentSlideIndex.value + config.itemsToScroll, skipTransition); | ||
} | ||
function prev() { | ||
slideTo(currentSlideIndex.value - config.itemsToScroll); | ||
function prev(skipTransition = false) { | ||
slideTo(currentSlideIndex.value - config.itemsToScroll, skipTransition); | ||
} | ||
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, | ||
maxSlide: maxSlideIndex, | ||
minSlide: minSlideIndex, | ||
slideSize, | ||
isVertical, | ||
normalizedDir, | ||
nav, | ||
isSliding, | ||
registerSlide, | ||
unregisterSlide, | ||
}); | ||
vue.provide(injectCarousel$1, 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); | ||
@@ -638,15 +923,14 @@ vue.provide('isSliding', isSliding); | ||
// Update the carousel on props change | ||
vue.watch(() => (Object.assign({}, props)), restartCarousel, { deep: true }); | ||
vue.watch(() => [fallbackConfig.value, props.breakpoints], () => updateBreakpointsConfig(), { deep: true }); | ||
vue.watch(() => props.autoplay, () => resetAutoplay()); | ||
// Handle changing v-model value | ||
vue.watch(() => props['modelValue'], (val) => { | ||
const modelWatcher = vue.watch(() => props.modelValue, (val) => { | ||
if (val === currentSlideIndex.value) { | ||
return; | ||
} | ||
slideTo(Number(val)); | ||
slideTo(Number(val), true); | ||
}); | ||
// Handel when slides added/removed | ||
vue.watch(slidesCount, updateSlidesData); | ||
// Init carousel | ||
emit('before-init'); | ||
const data = { | ||
const data = vue.reactive({ | ||
config, | ||
@@ -659,3 +943,3 @@ slidesCount, | ||
middleSlide: middleSlideIndex, | ||
}; | ||
}); | ||
expose({ | ||
@@ -672,2 +956,13 @@ updateBreakpointsConfig, | ||
}); | ||
const trackHeight = vue.computed(() => { | ||
if (isVertical.value && slideSize.value && config.height === 'auto') { | ||
return `${slideSize.value * config.itemsToShow + totalGap.value}px`; | ||
} | ||
return config.height !== 'auto' | ||
? typeof config.height === 'number' || | ||
parseInt(config.height).toString() === config.height | ||
? `${config.height}px` | ||
: config.height | ||
: undefined; | ||
}); | ||
/** | ||
@@ -678,13 +973,9 @@ * Track style | ||
// Calculate the scrolled index with wrapping offset if applicable | ||
const scrolledIndex = getScrolledIndex({ | ||
config, | ||
currentSlide: currentSlideIndex.value, | ||
slidesCount: slidesCount.value, | ||
}); | ||
const cloneOffset = config.wrapAround ? slidesCount.value : 0; | ||
const cloneOffset = config.wrapAround ? clonedSlidesCount.value : 0; | ||
// Determine direction multiplier for orientation | ||
const isReverseDirection = ['rtl', 'btt'].includes(normalizeDir.value); | ||
const directionMultiplier = isReverseDirection ? -1 : 1; | ||
const directionMultiplier = isReversed.value ? -1 : 1; | ||
// Calculate the total offset for slide transformation | ||
const totalOffset = (scrolledIndex + cloneOffset) * effectiveSlideSize.value * directionMultiplier; | ||
const totalOffset = (scrolledIndex.value + cloneOffset) * | ||
effectiveSlideSize.value * | ||
directionMultiplier; | ||
// Include user drag interaction offset | ||
@@ -696,38 +987,23 @@ const dragOffset = isVertical.value ? dragged.y : dragged.x; | ||
}); | ||
const slotSlides = slots.default || slots.slides; | ||
const slotAddons = slots.addons; | ||
const slotsProps = vue.reactive(data); | ||
return () => { | ||
if (!config.enabled) { | ||
const slotSlides = slots.default || slots.slides; | ||
const slotAddons = slots.addons; | ||
let output = (slotSlides === null || slotSlides === void 0 ? void 0 : slotSlides(data)) || []; | ||
if (!config.enabled || !output.length) { | ||
return vue.h('section', { | ||
ref: root, | ||
class: ['carousel', 'is-disabled'], | ||
}, slotSlides === null || slotSlides === void 0 ? void 0 : slotSlides()); | ||
}, output); | ||
} | ||
const slidesElements = getSlidesVNodes(slotSlides === null || slotSlides === void 0 ? void 0 : slotSlides(slotsProps)); | ||
const addonsElements = (slotAddons === null || slotAddons === void 0 ? void 0 : slotAddons(slotsProps)) || []; | ||
slidesElements.forEach((el, index) => { | ||
if (el.props) { | ||
el.props.index = index; | ||
} | ||
else { | ||
el.props = { index }; | ||
} | ||
}); | ||
let output = slidesElements; | ||
const addonsElements = (slotAddons === null || slotAddons === void 0 ? void 0 : slotAddons(data)) || []; | ||
if (config.wrapAround) { | ||
const slidesBefore = slidesElements.map((el, index) => vue.cloneVNode(el, { | ||
index: -slidesElements.length + index, | ||
isClone: true, | ||
key: `clone-before-${index}`, | ||
})); | ||
const slidesAfter = slidesElements.map((el, index) => vue.cloneVNode(el, { | ||
index: slidesElements.length + index, | ||
isClone: true, | ||
key: `clone-after-${index}`, | ||
})); | ||
output = [...slidesBefore, ...slidesElements, ...slidesAfter]; | ||
// Ensure scoped CSS tracks properly | ||
const scopeId = output.length > 0 ? output[0].scopeId : null; | ||
vue.pushScopeId(scopeId); | ||
const toShow = clonedSlidesCount.value; | ||
const slidesBefore = createCloneSlides({ slides, position: 'before', toShow }); | ||
const slidesAfter = createCloneSlides({ slides, position: 'after', toShow }); | ||
vue.popScopeId(); | ||
output = [...slidesBefore, ...output, ...slidesAfter]; | ||
} | ||
slides.value = slidesElements; | ||
slidesCount.value = Math.max(slidesElements.length, 1); | ||
const trackEl = vue.h('ol', { | ||
@@ -737,4 +1013,4 @@ class: 'carousel__track', | ||
transform: trackTransform.value, | ||
transition: `${isSliding.value ? config.transition : 0}ms`, | ||
gap: `${config.gap}px`, | ||
'transition-duration': isSliding.value ? `${config.transition}ms` : undefined, | ||
gap: config.gap > 0 ? `${config.gap}px` : undefined, | ||
}, | ||
@@ -749,3 +1025,3 @@ onMousedownCapture: config.mouseDrag ? handleDragStart : null, | ||
'carousel', | ||
`is-${normalizeDir.value}`, | ||
`is-${normalizedDir.value}`, | ||
{ | ||
@@ -759,10 +1035,12 @@ 'is-vertical': isVertical.value, | ||
style: { | ||
'--vc-trk-height': `${typeof config.height === 'number' ? `${config.height}px` : config.height}`, | ||
'--vc-trk-height': trackHeight.value, | ||
}, | ||
dir: normalizeDir.value, | ||
dir: normalizedDir.value, | ||
'aria-label': config.i18n['ariaGallery'], | ||
tabindex: '0', | ||
onFocus: handleFocus, | ||
onBlur: handleBlur, | ||
onMouseenter: handleMouseEnter, | ||
onMouseleave: handleMouseLeave, | ||
}, [viewPortEl, addonsElements, vue.h(ARIAComponent)]); | ||
}, [viewPortEl, addonsElements, vue.h(ARIA)]); | ||
}; | ||
@@ -779,2 +1057,10 @@ }, | ||
})(IconName || (IconName = {})); | ||
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 = { | ||
@@ -786,170 +1072,129 @@ arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z', | ||
}; | ||
function isIconName(candidate) { | ||
return candidate in IconName; | ||
} | ||
const Icon = (props) => { | ||
const config = vue.inject('config', vue.reactive(Object.assign({}, DEFAULT_CONFIG))); | ||
const iconName = String(props.name); | ||
const iconI18n = `icon${iconName.charAt(0).toUpperCase() + iconName.slice(1)}`; | ||
if (!iconName || typeof iconName !== 'string' || !isIconName(iconName)) { | ||
return; | ||
} | ||
const path = icons[iconName]; | ||
const pathEl = vue.h('path', { d: path }); | ||
const iconTitle = config.i18n[iconI18n] || props.title || iconName; | ||
const titleEl = vue.h('title', iconTitle); | ||
return vue.h('svg', { | ||
class: 'carousel__icon', | ||
viewBox: '0 0 24 24', | ||
role: 'img', | ||
'aria-label': iconTitle, | ||
}, [titleEl, pathEl]); | ||
}; | ||
Icon.props = { name: String, title: String }; | ||
const Navigation = (props, { slots, attrs }) => { | ||
const { next: slotNext, prev: slotPrev } = slots || {}; | ||
const config = vue.inject('config', vue.reactive(Object.assign({}, DEFAULT_CONFIG))); | ||
const maxSlide = vue.inject('maxSlide', vue.ref(1)); | ||
const minSlide = vue.inject('minSlide', vue.ref(1)); | ||
const normalizeDir = vue.inject('normalizeDir', vue.ref('ltr')); | ||
const currentSlide = vue.inject('currentSlide', vue.ref(1)); | ||
const nav = vue.inject('nav', {}); | ||
const { wrapAround, i18n } = config; | ||
const getPrevIcon = () => { | ||
const directionIcons = { | ||
ltr: 'arrowLeft', | ||
rtl: 'arrowRight', | ||
ttb: 'arrowUp', | ||
btt: 'arrowDown', | ||
}; | ||
return directionIcons[normalizeDir.value]; | ||
}; | ||
const getNextIcon = () => { | ||
const directionIcons = { | ||
ltr: 'arrowRight', | ||
rtl: 'arrowLeft', | ||
ttb: 'arrowDown', | ||
btt: 'arrowUp', | ||
}; | ||
return directionIcons[normalizeDir.value]; | ||
}; | ||
const prevButton = vue.h('button', { | ||
type: 'button', | ||
class: [ | ||
'carousel__prev', | ||
!wrapAround && currentSlide.value <= minSlide.value && 'carousel__prev--disabled', | ||
attrs === null || attrs === void 0 ? void 0 : attrs.class, | ||
], | ||
'aria-label': i18n['ariaPreviousSlide'], | ||
title: i18n['ariaPreviousSlide'], | ||
onClick: nav.prev, | ||
}, (slotPrev === null || slotPrev === void 0 ? void 0 : slotPrev()) || vue.h(Icon, { name: getPrevIcon() })); | ||
const nextButton = vue.h('button', { | ||
type: 'button', | ||
class: [ | ||
'carousel__next', | ||
!wrapAround && currentSlide.value >= maxSlide.value && 'carousel__next--disabled', | ||
attrs === null || attrs === void 0 ? void 0 : attrs.class, | ||
], | ||
'aria-label': i18n['ariaNextSlide'], | ||
title: i18n['ariaNextSlide'], | ||
onClick: nav.next, | ||
}, (slotNext === null || slotNext === void 0 ? void 0 : slotNext()) || vue.h(Icon, { name: getNextIcon() })); | ||
return [prevButton, nextButton]; | ||
}; | ||
const Pagination = () => { | ||
const config = vue.inject('config', vue.reactive(Object.assign({}, DEFAULT_CONFIG))); | ||
const maxSlide = vue.inject('maxSlide', vue.ref(1)); | ||
const minSlide = vue.inject('minSlide', vue.ref(1)); | ||
const currentSlide = vue.inject('currentSlide', vue.ref(1)); | ||
const nav = vue.inject('nav', {}); | ||
const isActive = (slide) => mapNumberToRange({ | ||
val: currentSlide.value, | ||
max: maxSlide.value, | ||
min: 0, | ||
}) === slide; | ||
const children = []; | ||
for (let slide = minSlide.value; slide < maxSlide.value + 1; slide++) { | ||
const buttonLabel = i18nFormatter(config.i18n['ariaNavigateToSlide'], { | ||
slideNumber: slide + 1, | ||
}); | ||
const button = vue.h('button', { | ||
type: 'button', | ||
class: { | ||
'carousel__pagination-button': true, | ||
'carousel__pagination-button--active': isActive(slide), | ||
}, | ||
'aria-label': buttonLabel, | ||
title: buttonLabel, | ||
onClick: () => nav.slideTo(slide), | ||
}); | ||
const item = vue.h('li', { class: 'carousel__pagination-item', key: slide }, button); | ||
children.push(item); | ||
} | ||
return vue.h('ol', { class: 'carousel__pagination' }, children); | ||
}; | ||
var Slide = vue.defineComponent({ | ||
name: 'CarouselSlide', | ||
const Icon = vue.defineComponent({ | ||
props: { | ||
index: { | ||
type: Number, | ||
default: 1, | ||
name: { | ||
type: String, | ||
required: true, | ||
validator: validateIconName, | ||
}, | ||
isClone: { | ||
type: Boolean, | ||
default: false, | ||
title: { | ||
type: String, | ||
default: (props) => props.name ? DEFAULT_CONFIG.i18n[iconI18n(props.name)] : '', | ||
}, | ||
}, | ||
setup(props) { | ||
const carousel = vue.inject(injectCarousel$1, null); | ||
return () => { | ||
const iconName = props.name; | ||
if (!iconName || !validateIconName(iconName)) | ||
return; | ||
const path = icons[iconName]; | ||
const pathEl = vue.h('path', { d: path }); | ||
const iconTitle = (carousel === null || carousel === void 0 ? void 0 : carousel.config.i18n[iconI18n(iconName)]) || props.title || iconName; | ||
const titleEl = vue.h('title', iconTitle); | ||
return vue.h('svg', { | ||
class: 'carousel__icon', | ||
viewBox: '0 0 24 24', | ||
role: 'img', | ||
'aria-label': iconTitle, | ||
}, [titleEl, pathEl]); | ||
}; | ||
}, | ||
}); | ||
const Navigation = vue.defineComponent({ | ||
name: 'CarouselNavigation', | ||
setup(props, { slots }) { | ||
const config = vue.inject('config', vue.reactive(Object.assign({}, DEFAULT_CONFIG))); | ||
const currentSlide = vue.inject('currentSlide', vue.ref(0)); | ||
const slidesToScroll = vue.inject('slidesToScroll', vue.ref(0)); | ||
const isSliding = vue.inject('isSliding', vue.ref(false)); | ||
const isVertical = vue.inject('isVertical', vue.ref(false)); | ||
const slideSize = vue.inject('slideSize', vue.ref(0)); | ||
const isActive = vue.computed(() => props.index === currentSlide.value); | ||
const isPrev = vue.computed(() => props.index === currentSlide.value - 1); | ||
const isNext = vue.computed(() => props.index === currentSlide.value + 1); | ||
const isVisible = vue.computed(() => { | ||
const min = Math.floor(slidesToScroll.value); | ||
const max = Math.ceil(slidesToScroll.value + config.itemsToShow - 1); | ||
return props.index >= min && props.index <= max; | ||
}); | ||
const slideStyle = vue.computed(() => { | ||
const dimension = config.gap | ||
? `${slideSize.value}px` | ||
: `${100 / config.itemsToShow}%`; | ||
return isVertical.value | ||
? { height: dimension, width: '' } | ||
: { width: dimension, height: '' }; | ||
}); | ||
const carousel = vue.inject(injectCarousel$1); | ||
if (!carousel) { | ||
return null; // Don't render, let vue warn about the missing provide | ||
} | ||
const { next: slotNext, prev: slotPrev } = slots; | ||
const getPrevIcon = () => { | ||
const directionIcons = { | ||
ltr: 'arrowLeft', | ||
rtl: 'arrowRight', | ||
ttb: 'arrowUp', | ||
btt: 'arrowDown', | ||
}; | ||
return directionIcons[carousel.normalizedDir]; | ||
}; | ||
const getNextIcon = () => { | ||
const directionIcons = { | ||
ltr: 'arrowRight', | ||
rtl: 'arrowLeft', | ||
ttb: 'arrowDown', | ||
btt: 'arrowUp', | ||
}; | ||
return directionIcons[carousel.normalizedDir]; | ||
}; | ||
return () => { | ||
const { wrapAround, i18n } = carousel.config; | ||
const prevButton = vue.h('button', { | ||
type: 'button', | ||
class: [ | ||
'carousel__prev', | ||
!wrapAround && | ||
carousel.currentSlide <= carousel.minSlide && | ||
'carousel__prev--disabled', | ||
props.class, | ||
], | ||
'aria-label': i18n['ariaPreviousSlide'], | ||
title: i18n['ariaPreviousSlide'], | ||
onClick: carousel.nav.prev, | ||
}, (slotPrev === null || slotPrev === void 0 ? void 0 : slotPrev()) || vue.h(Icon, { name: getPrevIcon() })); | ||
const nextButton = vue.h('button', { | ||
type: 'button', | ||
class: [ | ||
'carousel__next', | ||
!wrapAround && | ||
carousel.currentSlide >= carousel.maxSlide && | ||
'carousel__next--disabled', | ||
props.class, | ||
], | ||
'aria-label': i18n['ariaNextSlide'], | ||
title: i18n['ariaNextSlide'], | ||
onClick: carousel.nav.next, | ||
}, (slotNext === null || slotNext === void 0 ? void 0 : slotNext()) || vue.h(Icon, { name: getNextIcon() })); | ||
return [prevButton, nextButton]; | ||
}; | ||
}, | ||
}); | ||
const Pagination = vue.defineComponent({ | ||
name: 'CarouselPagination', | ||
setup(props) { | ||
const carousel = vue.inject(injectCarousel$1); | ||
if (!carousel) { | ||
return null; // Don't render, let vue warn about the missing provide | ||
} | ||
const isActive = (slide) => mapNumberToRange({ | ||
val: carousel.currentSlide, | ||
max: carousel.maxSlide, | ||
min: 0, | ||
}) === slide; | ||
return () => { | ||
var _a, _b; | ||
if (!config.enabled) { | ||
return (_a = slots.default) === null || _a === void 0 ? void 0 : _a.call(slots); | ||
const children = []; | ||
for (let slide = carousel.minSlide; slide <= carousel.maxSlide; slide++) { | ||
const buttonLabel = i18nFormatter(carousel.config.i18n.ariaNavigateToSlide, { | ||
slideNumber: slide + 1, | ||
}); | ||
const active = isActive(slide); | ||
const button = vue.h('button', { | ||
type: 'button', | ||
class: { | ||
'carousel__pagination-button': true, | ||
'carousel__pagination-button--active': active, | ||
}, | ||
'aria-label': buttonLabel, | ||
'aria-pressed': active, | ||
'aria-controls': (_b = (_a = carousel.slides[slide]) === null || _a === void 0 ? void 0 : _a.exposed) === null || _b === void 0 ? void 0 : _b.id, | ||
title: buttonLabel, | ||
onClick: props.disableOnClick ? undefined : () => carousel.nav.slideTo(slide), | ||
}); | ||
const item = vue.h('li', { class: 'carousel__pagination-item', key: slide }, button); | ||
children.push(item); | ||
} | ||
return vue.h('li', { | ||
style: slideStyle.value, | ||
class: { | ||
carousel__slide: true, | ||
'carousel__slide--clone': props.isClone, | ||
'carousel__slide--visible': isVisible.value, | ||
'carousel__slide--active': isActive.value, | ||
'carousel__slide--prev': isPrev.value, | ||
'carousel__slide--next': isNext.value, | ||
'carousel__slide--sliding': isSliding.value, | ||
}, | ||
'aria-hidden': !isVisible.value, | ||
}, (_b = slots.default) === null || _b === void 0 ? void 0 : _b.call(slots, { | ||
isActive: isActive.value, | ||
isClone: props.isClone, | ||
isPrev: isPrev.value, | ||
isNext: isNext.value, | ||
isSliding: isSliding.value, | ||
isVisible: isVisible.value, | ||
})); | ||
return vue.h('ol', { class: 'carousel__pagination' }, children); | ||
}; | ||
@@ -959,8 +1204,17 @@ }, | ||
exports.BREAKPOINT_MODE_OPTIONS = BREAKPOINT_MODE_OPTIONS; | ||
exports.Carousel = Carousel; | ||
exports.DEFAULT_CONFIG = DEFAULT_CONFIG; | ||
exports.DIR_MAP = DIR_MAP; | ||
exports.DIR_OPTIONS = DIR_OPTIONS; | ||
exports.I18N_DEFAULT_CONFIG = I18N_DEFAULT_CONFIG; | ||
exports.Icon = Icon; | ||
exports.NORMALIZED_DIR_OPTIONS = NORMALIZED_DIR_OPTIONS; | ||
exports.Navigation = Navigation; | ||
exports.Pagination = Pagination; | ||
exports.SNAP_ALIGN_OPTIONS = SNAP_ALIGN_OPTIONS; | ||
exports.Slide = Slide; | ||
exports.icons = icons; | ||
exports.injectCarousel = injectCarousel; | ||
})); |
/** | ||
* Vue 3 Carousel 0.8.1 | ||
* Vue 3 Carousel 0.9.0 | ||
* (c) 2024 | ||
* @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 n=["center","start","end","center-even","center-odd"],i=["viewport","carousel"],a=["ltr","left-to-right","rtl","right-to-left","ttb","top-to-bottom","btt","bottom-to-top"],l={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:a[0],breakpointMode:i[0],breakpoints:void 0,i18n:{ariaNextSlide:"Navigate to next slide",ariaPreviousSlide:"Navigate to previous slide",ariaNavigateToSlide:"Navigate to slide {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"}},o={enabled:{default:l.enabled,type:Boolean},itemsToShow:{default:l.itemsToShow,type:Number},itemsToScroll:{default:l.itemsToScroll,type:Number},wrapAround:{default:l.wrapAround,type:Boolean},gap:{default:l.gap,type:Number},height:{default:l.height,type:[Number,String]},snapAlign:{default:l.snapAlign,validator:e=>n.includes(e)},transition:{default:l.transition,type:Number},breakpointMode:{default:l.breakpointMode,validator:e=>i.includes(e)},breakpoints:{default:l.breakpoints,type:Object},autoplay:{default:l.autoplay,type:Number},pauseAutoplayOnHover:{default:l.pauseAutoplayOnHover,type:Boolean},modelValue:{default:void 0,type:Number},mouseDrag:{default:l.mouseDrag,type:Boolean},touchDrag:{default:l.touchDrag,type:Boolean},dir:{default:l.dir,validator:e=>a.includes(e)},i18n:{default:l.i18n,type:Object}};function r({val:e,max:t,min:n}){return t<n?e:Math.min(Math.max(e,n),t)}function u(e){return e?e.reduce(((e,n)=>{var i;return n.type===t.Fragment?[...e,...u(n.children)]:"CarouselSlide"===(null===(i=n.type)||void 0===i?void 0:i.name)?[...e,n]:e}),[]):[]}function s({val:e,max:t,min:n=0}){const i=t-n+1;return((e-n)%i+i)%i+n}function c(e="",t={}){return Object.entries(t).reduce(((e,[t,n])=>e.replace(`{${t}}`,String(n))),e)}var d,v=t.defineComponent({name:"ARIA",setup(){const e=t.inject("config",t.reactive(Object.assign({},l))),n=t.inject("currentSlide",t.ref(0)),i=t.inject("slidesCount",t.ref(0));return()=>t.h("div",{class:["carousel__liveregion","carousel__sr-only"],"aria-live":"polite","aria-atomic":"true"},c(e.i18n.itemXofY,{currentSlide:n.value+1,slidesCount:i.value}))}}),p=t.defineComponent({name:"Carousel",props:o,emits:["init","drag","slide-start","loop","update:modelValue","slide-end","before-init"],setup(e,{slots:n,emit:i,expose:a}){var o;const c=t.ref(null),d=t.ref(null),p=t.ref([]),f=t.ref(0),m=t.ref(0),g=t.computed((()=>Object.assign(Object.assign(Object.assign({},l),e),{i18n:Object.assign(Object.assign({},l.i18n),e.i18n),breakpoints:void 0}))),h=t.reactive(Object.assign({},g.value)),b=t.ref(null!==(o=e.modelValue)&&void 0!==o?o:0),w=t.ref(0),S=t.ref(0),x=t.ref(0),y=t.ref(0);let j=null,A=null,_=null;const C=t.computed((()=>f.value+h.gap)),T=t.computed((()=>{const e=h.dir||"lrt";return{"left-to-right":"ltr","right-to-left":"rtl","top-to-bottom":"ttb","bottom-to-top":"btt"}[e]||e})),M=t.computed((()=>["ttb","btt"].includes(T.value)));function N(){var t;const n=("carousel"===h.breakpointMode?null===(t=c.value)||void 0===t?void 0:t.getBoundingClientRect().width:window.innerWidth)||0,i=Object.keys(e.breakpoints||{}).map((e=>Number(e))).sort(((e,t)=>+t-+e));let a=Object.assign({},g.value);i.some((t=>{var i;return n>=t&&(a=Object.assign(Object.assign({},a),null===(i=e.breakpoints)||void 0===i?void 0:i[t]),!0)})),Object.assign(h,a)}t.provide("config",h),t.provide("slidesCount",m),t.provide("currentSlide",b),t.provide("maxSlide",x),t.provide("minSlide",y),t.provide("slideSize",f),t.provide("isVertical",M),t.provide("normalizeDir",T);const O=function(e,t){let n;return function(...i){n&&clearTimeout(n),n=setTimeout((()=>{e(...i),n=null}),t)}}((()=>{N(),L(),k()}),16);function k(){if(!d.value)return;const e=d.value.getBoundingClientRect(),t=(h.itemsToShow-1)*h.gap;M.value?f.value=(e.height-t)/h.itemsToShow:f.value=(e.width-t)/h.itemsToShow}function L(){m.value<=0||(S.value=Math.ceil((m.value-1)/2),x.value=function({config:e,slidesCount:t}){var n;const{snapAlign:i="N/A",wrapAround:a,itemsToShow:l=1}=e;if(a)return Math.max(t-1,0);const o=null!==(n={start:Math.ceil(t-l),end:Math.ceil(t-1),center:t-Math.ceil((l-.5)/2),"center-odd":t-Math.ceil((l-.5)/2),"center-even":t-Math.ceil(l/2)}[i])&&void 0!==n?n:0;return Math.max(o,0)}({config:h,slidesCount:m.value}),y.value=function({config:e,slidesCount:t}){var n;const{snapAlign:i="N/A",wrapAround:a,itemsToShow:l=1}=e;return a||l>t?0:null!==(n={start:0,end:Math.floor(l-1),center:Math.floor((l-1)/2),"center-odd":Math.floor((l-1)/2),"center-even":Math.floor((l-2)/2)}[i])&&void 0!==n?n:0}({config:h,slidesCount:m.value}),h.wrapAround||(b.value=r({val:b.value,max:x.value,min:y.value})))}t.onMounted((()=>{t.nextTick((()=>k())),setTimeout((()=>k()),1e3),N(),Y(),window.addEventListener("resize",O,{passive:!0}),_=new ResizeObserver(O),c.value&&_.observe(c.value),i("init")})),t.onUnmounted((()=>{A&&clearTimeout(A),j&&clearInterval(j),_&&c.value&&(_.unobserve(c.value),_=null),window.removeEventListener("resize",O,{passive:!0})}));let D=!1;const I={x:0,y:0},z=t.reactive({x:0,y:0}),E=t.ref(!1),V=t.ref(!1),$=()=>{E.value=!0},R=()=>{E.value=!1};function B(e){const t=e.target.tagName;if(["INPUT","TEXTAREA","SELECT"].includes(t)||H.value)return;if(D="touchstart"===e.type,!D&&(e.preventDefault(),0!==e.button))return;I.x=D?e.touches[0].clientX:e.clientX,I.y=D?e.touches[0].clientY:e.clientY;const n=D?"touchmove":"mousemove",i=D?"touchend":"mouseup";document.addEventListener(n,U,{passive:!1}),document.addEventListener(i,X,{passive:!0})}const U=function(e){let t=!1;return function(...n){t||(t=!0,requestAnimationFrame((()=>{e.apply(this,n),t=!1})))}}((e=>{V.value=!0;const t=D?e.touches[0].clientX:e.clientX,n=D?e.touches[0].clientY:e.clientY,a=t-I.x,l=n-I.y;z.x=a,z.y=l,i("drag",{deltaX:a,deltaY:l})}));function X(){const e=M.value?"y":"x",t=["rtl","btt"].includes(T.value)?-1:1,n=.4*Math.sign(z[e]),i=Math.round(z[e]/C.value+n)*t;if(i&&!D){const e=t=>{t.preventDefault(),window.removeEventListener("click",e)};window.addEventListener("click",e)}G(b.value-i),z.x=0,z.y=0,V.value=!1;const a=D?"touchmove":"mousemove",l=D?"touchend":"mouseup";document.removeEventListener(a,U),document.removeEventListener(l,X)}function Y(){!h.autoplay||h.autoplay<=0||(j=setInterval((()=>{h.pauseAutoplayOnHover&&E.value||q()}),h.autoplay))}function P(){j&&(clearInterval(j),j=null),Y()}const H=t.ref(!1);function G(e){const t=h.wrapAround?e:r({val:e,max:x.value,min:y.value});b.value===t||H.value||(i("slide-start",{slidingToIndex:e,currentSlideIndex:b.value,prevSlideIndex:w.value,slidesCount:m.value}),H.value=!0,w.value=b.value,b.value=t,A=setTimeout((()=>{if(h.wrapAround){const n=s({val:t,max:x.value,min:0});n!==b.value&&(b.value=n,i("loop",{currentSlideIndex:b.value,slidingToIndex:e}))}i("update:modelValue",b.value),i("slide-end",{currentSlideIndex:b.value,prevSlideIndex:w.value,slidesCount:m.value}),H.value=!1,P()}),h.transition))}function q(){G(b.value+h.itemsToScroll)}function F(){G(b.value-h.itemsToScroll)}const W={slideTo:G,next:q,prev:F};function J(){N(),L(),k(),P()}t.provide("nav",W),t.provide("isSliding",H),t.watch((()=>Object.assign({},e)),J,{deep:!0}),t.watch((()=>e.modelValue),(e=>{e!==b.value&&G(Number(e))})),t.watch(m,L),i("before-init");const K={config:h,slidesCount:m,slideSize:f,currentSlide:b,maxSlide:x,minSlide:y,middleSlide:S};a({updateBreakpointsConfig:N,updateSlidesData:L,updateSlideSize:k,restartCarousel:J,slideTo:G,next:q,prev:F,nav:W,data:K});const Q=t.computed((()=>{const e=function({config:e,currentSlide:t,slidesCount:n}){const{snapAlign:i="N/A",wrapAround:a,itemsToShow:l=1}=e,o=((e,t)=>{var n;return null!==(n={start:0,center:(t-1)/2,"center-odd":(t-1)/2,"center-even":(t-2)/2,end:t-1}[e])&&void 0!==n?n:0})(i,l);return a?t-o:r({val:t-o,max:n-l,min:0})}({config:h,currentSlide:b.value,slidesCount:m.value}),t=h.wrapAround?m.value:0,n=["rtl","btt"].includes(T.value)?-1:1,i=(e+t)*C.value*n,a=M.value?z.y:z.x;return`translate${M.value?"Y":"X"}(${a-i}px)`})),Z=n.default||n.slides,ee=n.addons,te=t.reactive(K);return()=>{if(!h.enabled)return t.h("section",{ref:c,class:["carousel","is-disabled"]},null==Z?void 0:Z());const e=u(null==Z?void 0:Z(te)),n=(null==ee?void 0:ee(te))||[];e.forEach(((e,t)=>{e.props?e.props.index=t:e.props={index:t}}));let i=e;if(h.wrapAround){const n=e.map(((n,i)=>t.cloneVNode(n,{index:-e.length+i,isClone:!0,key:`clone-before-${i}`}))),a=e.map(((n,i)=>t.cloneVNode(n,{index:e.length+i,isClone:!0,key:`clone-after-${i}`})));i=[...n,...e,...a]}p.value=e,m.value=Math.max(e.length,1);const a=t.h("ol",{class:"carousel__track",style:{transform:Q.value,transition:`${H.value?h.transition:0}ms`,gap:`${h.gap}px`},onMousedownCapture:h.mouseDrag?B:null,onTouchstartPassiveCapture:h.touchDrag?B:null},i),l=t.h("div",{class:"carousel__viewport",ref:d},a);return t.h("section",{ref:c,class:["carousel",`is-${T.value}`,{"is-vertical":M.value,"is-sliding":H.value,"is-dragging":V.value,"is-hover":E.value}],style:{"--vc-trk-height":`${"number"==typeof h.height?`${h.height}px`:h.height}`},dir:T.value,"aria-label":h.i18n.ariaGallery,tabindex:"0",onMouseenter:$,onMouseleave:R},[l,n,t.h(v)])}}});!function(e){e.arrowUp="arrowUp",e.arrowDown="arrowDown",e.arrowRight="arrowRight",e.arrowLeft="arrowLeft"}(d||(d={}));const f={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 m=e=>{const n=t.inject("config",t.reactive(Object.assign({},l))),i=String(e.name),a=`icon${i.charAt(0).toUpperCase()+i.slice(1)}`;if(!i||"string"!=typeof i||!(i in d))return;const o=f[i],r=t.h("path",{d:o}),u=n.i18n[a]||e.title||i,s=t.h("title",u);return t.h("svg",{class:"carousel__icon",viewBox:"0 0 24 24",role:"img","aria-label":u},[s,r])};m.props={name:String,title:String};var g=t.defineComponent({name:"CarouselSlide",props:{index:{type:Number,default:1},isClone:{type:Boolean,default:!1}},setup(e,{slots:n}){const i=t.inject("config",t.reactive(Object.assign({},l))),a=t.inject("currentSlide",t.ref(0)),o=t.inject("slidesToScroll",t.ref(0)),r=t.inject("isSliding",t.ref(!1)),u=t.inject("isVertical",t.ref(!1)),s=t.inject("slideSize",t.ref(0)),c=t.computed((()=>e.index===a.value)),d=t.computed((()=>e.index===a.value-1)),v=t.computed((()=>e.index===a.value+1)),p=t.computed((()=>{const t=Math.floor(o.value),n=Math.ceil(o.value+i.itemsToShow-1);return e.index>=t&&e.index<=n})),f=t.computed((()=>{const e=i.gap?`${s.value}px`:100/i.itemsToShow+"%";return u.value?{height:e,width:""}:{width:e,height:""}}));return()=>{var a,l;return i.enabled?t.h("li",{style:f.value,class:{carousel__slide:!0,"carousel__slide--clone":e.isClone,"carousel__slide--visible":p.value,"carousel__slide--active":c.value,"carousel__slide--prev":d.value,"carousel__slide--next":v.value,"carousel__slide--sliding":r.value},"aria-hidden":!p.value},null===(l=n.default)||void 0===l?void 0:l.call(n,{isActive:c.value,isClone:e.isClone,isPrev:d.value,isNext:v.value,isSliding:r.value,isVisible:p.value})):null===(a=n.default)||void 0===a?void 0:a.call(n)}}});e.Carousel=p,e.Icon=m,e.Navigation=(e,{slots:n,attrs:i})=>{const{next:a,prev:o}=n||{},r=t.inject("config",t.reactive(Object.assign({},l))),u=t.inject("maxSlide",t.ref(1)),s=t.inject("minSlide",t.ref(1)),c=t.inject("normalizeDir",t.ref("ltr")),d=t.inject("currentSlide",t.ref(1)),v=t.inject("nav",{}),{wrapAround:p,i18n:f}=r;return[t.h("button",{type:"button",class:["carousel__prev",!p&&d.value<=s.value&&"carousel__prev--disabled",null==i?void 0:i.class],"aria-label":f.ariaPreviousSlide,title:f.ariaPreviousSlide,onClick:v.prev},(null==o?void 0:o())||t.h(m,{name:{ltr:"arrowLeft",rtl:"arrowRight",ttb:"arrowUp",btt:"arrowDown"}[c.value]})),t.h("button",{type:"button",class:["carousel__next",!p&&d.value>=u.value&&"carousel__next--disabled",null==i?void 0:i.class],"aria-label":f.ariaNextSlide,title:f.ariaNextSlide,onClick:v.next},(null==a?void 0:a())||t.h(m,{name:{ltr:"arrowRight",rtl:"arrowLeft",ttb:"arrowDown",btt:"arrowUp"}[c.value]}))]},e.Pagination=()=>{const e=t.inject("config",t.reactive(Object.assign({},l))),n=t.inject("maxSlide",t.ref(1)),i=t.inject("minSlide",t.ref(1)),a=t.inject("currentSlide",t.ref(1)),o=t.inject("nav",{}),r=e=>s({val:a.value,max:n.value,min:0})===e,u=[];for(let a=i.value;a<n.value+1;a++){const n=c(e.i18n.ariaNavigateToSlide,{slideNumber:a+1}),i=t.h("button",{type:"button",class:{"carousel__pagination-button":!0,"carousel__pagination-button--active":r(a)},"aria-label":n,title:n,onClick:()=>o.slideTo(a)}),l=t.h("li",{class:"carousel__pagination-item",key:a},i);u.push(l)}return t.h("ol",{class:"carousel__pagination"},u)},e.Slide=g})); | ||
!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 n=Symbol("carousel");function o({val:e,max:t,min:n}){return t<n?e:Math.min(Math.max(e,isNaN(n)?e:n),isNaN(t)?e:t)}function i({val:e,max:t,min:n=0}){const o=t-n+1;return((e-n)%o+o)%o+n}function a(e="",t={}){return Object.entries(t).reduce(((e,[t,n])=>e.replace(`{${t}}`,String(n))),e)}function l(e,t=0){let n=!1,o=0,i=null;function a(...a){if(n)return;n=!0;const l=()=>{i=requestAnimationFrame((i=>{i-o>t?(o=i,e(...a),n=!1):l()}))};l()}return a.cancel=()=>{i&&(cancelAnimationFrame(i),i=null,n=!1)},a}const r=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:0}},setup(e,{slots:o,expose:i}){const a=t.inject(n);if(t.provide(n,void 0),!a)return null;const l=t.ref(e.index);t.watch((()=>e.index),(e=>l.value=e));const r=t.computed((()=>l.value===a.currentSlide)),u=t.computed((()=>l.value===a.currentSlide-1)),s=t.computed((()=>l.value===a.currentSlide+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();if(e.isClone){const e=e=>{[...(null==e?void 0:e.el)?e.el.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'):[]].filter((e=>!e.hasAttribute("disabled")&&!e.getAttribute("aria-hidden"))).forEach((e=>e.setAttribute("tabindex","-1")))};t.onMounted((()=>{e(v.vnode)})),t.onUpdated((()=>{e(v.vnode)}))}else a.registerSlide(v,(e=>l.value=e)),t.onUnmounted((()=>{a.unregisterSlide(v)}));return i({id:e.id}),()=>{var n,i;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":u.value,"carousel__slide--next":s.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===(i=o.default)||void 0===i?void 0:i.call(o,{isActive:r.value,isClone:e.isClone,isPrev:u.value,isNext:s.value,isSliding:a.isSliding,isVisible:d.value})):null===(n=o.default)||void 0===n?void 0:n.call(o)}}});function u({slides:e,position:n,toShow:o}){const i=[],a="before"===n,l=a?0:o;for(let u=a?-o:0;u<l;u++){const o={index:a?u:e.length>0?u+e.length:u+99999,isClone:!0,key:`clone-${n}-${u}`};i.push(e.length>0?t.cloneVNode(e[(u+e.length)%e.length].vnode,o):t.h(r,o))}return i}const s=t.defineComponent({name:"CarouselAria",setup(){const e=t.inject(n);if(e)return()=>t.h("div",{class:["carousel__liveregion","carousel__sr-only"],"aria-live":"polite","aria-atomic":"true"},a(e.config.i18n.itemXofY,{currentSlide:e.currentSlide+1,slidesCount:e.slidesCount}))}}),d=Symbol("carousel"),c=["center","start","end","center-even","center-odd"],v=["viewport","carousel"],p=["ltr","left-to-right","rtl","right-to-left","ttb","top-to-bottom","btt","bottom-to-top"],m={ariaNextSlide:"Navigate to next slide",ariaPreviousSlide:"Navigate to previous slide",ariaNavigateToSlide:"Navigate to slide {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"},f={"left-to-right":"ltr","right-to-left":"rtl","top-to-bottom":"ttb","bottom-to-top":"btt"},h=Object.values(f),g={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:c[0],dir:p[0],breakpointMode:v[0],breakpoints:void 0,i18n:m},w={enabled:{default:g.enabled,type:Boolean},itemsToShow:{default:g.itemsToShow,type:Number},itemsToScroll:{default:g.itemsToScroll,type:Number},wrapAround:{default:g.wrapAround,type:Boolean},gap:{default:g.gap,type:Number},height:{default:g.height,type:[Number,String]},snapAlign:{default:g.snapAlign,validator:e=>c.includes(e)},transition:{default:g.transition,type:Number},breakpointMode:{default:g.breakpointMode,validator:e=>v.includes(e)},breakpoints:{default:g.breakpoints,type:Object},autoplay:{default:g.autoplay,type:Number},pauseAutoplayOnHover:{default:g.pauseAutoplayOnHover,type:Boolean},modelValue:{default:void 0,type:Number},mouseDrag:{default:g.mouseDrag,type:Boolean},touchDrag:{default:g.touchDrag,type:Boolean},dir:{type:String,default:g.dir,validator:e=>p.includes(e)},i18n:{default:g.i18n,type:Object}},b=t.defineComponent({name:"VueCarousel",props:w,emits:["init","drag","slide-start","loop","update:modelValue","slide-end","before-init"],setup(e,{slots:a,emit:r,expose:d}){var c;const v=t.ref(null),p=t.ref(null),m=t.shallowReactive([]),h=t.ref(0),w=t.computed((()=>m.length)),b=t.computed((()=>Object.assign(Object.assign(Object.assign({},g),e),{i18n:Object.assign(Object.assign({},g.i18n),e.i18n),breakpoints:void 0}))),S=t.reactive(Object.assign({},b.value)),x=t.ref(null!==(c=e.modelValue)&&void 0!==c?c:0),y=t.ref(0),A=t.computed((()=>Math.ceil((w.value-1)/2))),C=t.computed((()=>function({config:e,slidesCount:t}){const{snapAlign:n="center",wrapAround:o,itemsToShow:i=1}=e;return Math.max(function(){switch(o?"":n){case"start":return Math.ceil(t-i);case"center":case"center-odd":return t-Math.ceil((i-.5)/2);case"center-even":return t-Math.ceil(i/2);default:return Math.ceil(t-1)}}(),0)}({config:S,slidesCount:w.value}))),_=t.computed((()=>function({config:e,slidesCount:t}){const{snapAlign:n="center",wrapAround:o,itemsToShow:i=1}=e;return o||i>t?0:Math.max(0,function(){switch(n){case"end":return Math.floor(i-1);case"center":case"center-odd":return Math.floor((i-1)/2);case"center-even":return Math.floor((i-2)/2)}return 0}())}({config:S,slidesCount:w.value})));let T=null,N=null,I=null;const L=t.computed((()=>h.value+S.gap)),M=t.computed((()=>{const e=S.dir||"ltr";return e in f?f[e]:e})),O=[],k=t.computed((()=>["rtl","btt"].includes(M.value))),D=t.computed((()=>["ttb","btt"].includes(M.value))),E=t.computed((()=>Math.ceil(S.itemsToShow)+1));function j(){var t;const n=("carousel"===b.value.breakpointMode?null===(t=v.value)||void 0===t?void 0:t.getBoundingClientRect().width:"undefined"!=typeof window?window.innerWidth:0)||0,o=Object.keys(e.breakpoints||{}).map((e=>Number(e))).sort(((e,t)=>+t-+e)),i={};o.some((t=>n>=t&&(Object.assign(i,e.breakpoints[t]),i.i18n&&Object.assign(i.i18n,b.value.i18n,e.breakpoints[t].i18n),!0))),Object.assign(S,b.value,i)}const R=l((()=>{j(),B(),z()})),P=t.computed((()=>(S.itemsToShow-1)*S.gap)),U=t.shallowReactive(new Set);function z(){if(!p.value)return;let e=1;if(U.forEach((t=>{const n=function(e){const{transform:t}=window.getComputedStyle(e);return t.split(/[(,)]/).slice(1,-1).map((e=>parseFloat(e)))}(t);6===n.length&&(e*=n[0])})),D.value){if("auto"!==S.height){const e="string"==typeof S.height&&isNaN(parseInt(S.height))?p.value.getBoundingClientRect().height:parseInt(S.height);h.value=(e-P.value)/S.itemsToShow}}else{const t=p.value.getBoundingClientRect().width;h.value=(t/e-P.value)/S.itemsToShow}}function B(){!S.wrapAround&&w.value>0&&(x.value=o({val:x.value,max:C.value,min:_.value}))}let V;t.watchEffect((()=>B())),t.watchEffect((()=>{z()}));const $=e=>{const t=e.target;if(t&&U.add(t),!V){const e=()=>{V=requestAnimationFrame((()=>{z(),e()}))};e()}},F=e=>{const t=e.target;t&&U.delete(t),V&&0===U.size&&(cancelAnimationFrame(V),z())};j(),t.onMounted((()=>{"carousel"===b.value.breakpointMode&&j(),oe(),document&&(document.addEventListener("animationstart",$),document.addEventListener("animationend",F)),v.value&&(I=new ResizeObserver(R),I.observe(v.value)),r("init")})),t.onBeforeUnmount((()=>{m.splice(0,m.length),O.splice(0,O.length),N&&clearTimeout(N),V&&cancelAnimationFrame(V),T&&clearInterval(T),I&&(I.disconnect(),I=null),document&&(document.removeEventListener("keydown",Z),document.removeEventListener("animationstart",$),document.removeEventListener("animationend",F)),v.value&&(v.value.removeEventListener("transitionend",z),v.value.removeEventListener("animationiteration",z))}));let X=!1;const Y={x:0,y:0},G=t.reactive({x:0,y:0}),q=t.ref(!1),H=t.ref(!1),W=()=>{q.value=!0},K=()=>{q.value=!1},Z=l((e=>{if(!e.ctrlKey)switch(e.key){case"ArrowLeft":case"ArrowUp":D.value===e.key.endsWith("Up")&&(k.value?de.next(!0):de.prev(!0));break;case"ArrowRight":case"ArrowDown":D.value===e.key.endsWith("Down")&&(k.value?de.prev(!0):de.next(!0))}}),200),J=()=>{document.addEventListener("keydown",Z)},Q=()=>{document.removeEventListener("keydown",Z)};function ee(e){const t=e.target.tagName;if(["INPUT","TEXTAREA","SELECT"].includes(t)||le.value)return;if(X="touchstart"===e.type,!X&&(e.preventDefault(),0!==e.button))return;Y.x="touches"in e?e.touches[0].clientX:e.clientX,Y.y="touches"in e?e.touches[0].clientY:e.clientY;const n=X?"touchmove":"mousemove",o=X?"touchend":"mouseup";document.addEventListener(n,te,{passive:!1}),document.addEventListener(o,ne,{passive:!0})}const te=l((e=>{H.value=!0;const t="touches"in e?e.touches[0].clientX:e.clientX,n="touches"in e?e.touches[0].clientY:e.clientY,o=t-Y.x,i=n-Y.y;G.x=o,G.y=i,r("drag",{deltaX:o,deltaY:i})}));function ne(){te.cancel();const e=D.value?"y":"x",t=k.value?-1:1,n=.4*Math.sign(G[e]),o=Math.round(G[e]/L.value+n)*t;if(o&&!X){const e=t=>{t.preventDefault(),window.removeEventListener("click",e)};window.addEventListener("click",e)}re(x.value-o),G.x=0,G.y=0,H.value=!1;const i=X?"touchmove":"mousemove",a=X?"touchend":"mouseup";document.removeEventListener(i,te),document.removeEventListener(a,ne)}function oe(){!S.autoplay||S.autoplay<=0||(T=setInterval((()=>{S.pauseAutoplayOnHover&&q.value||ue()}),S.autoplay))}function ie(){T&&(clearInterval(T),T=null)}function ae(){ie(),oe()}const le=t.ref(!1);function re(e,t=!1){const n=S.wrapAround?e:o({val:e,max:C.value,min:_.value});if(x.value===n||!t&&le.value)return;r("slide-start",{slidingToIndex:e,currentSlideIndex:x.value,prevSlideIndex:y.value,slidesCount:w.value}),ie(),le.value=!0,y.value=x.value;const a=S.wrapAround?i({val:n,max:C.value,min:0}):n;x.value=n,a!==n&&pe.pause(),r("update:modelValue",a),N=setTimeout((()=>{S.wrapAround&&a!==n&&(pe.resume(),x.value=a,r("loop",{currentSlideIndex:x.value,slidingToIndex:e})),r("slide-end",{currentSlideIndex:x.value,prevSlideIndex:y.value,slidesCount:w.value}),le.value=!1,ae()}),S.transition)}function ue(e=!1){re(x.value+S.itemsToScroll,e)}function se(e=!1){re(x.value-S.itemsToScroll,e)}const de={slideTo:re,next:ue,prev:se},ce=t.computed((()=>function({config:e,currentSlide:t,slidesCount:n}){const{snapAlign:a="center",wrapAround:l,itemsToShow:r=1}=e,u=((e,t)=>{var n;return null!==(n={start:0,center:(t-1)/2,"center-odd":(t-1)/2,"center-even":(t-2)/2,end:t-1}[e])&&void 0!==n?n:0})(a,r);return l?i({val:t-u,max:n+r,min:0-r}):o({val:t-u,max:n-r,min:0})}({config:S,currentSlide:x.value,slidesCount:w.value}))),ve=t.reactive({config:S,slidesCount:w,viewport:p,slides:m,scrolledIndex:ce,currentSlide:x,maxSlide:C,minSlide:_,slideSize:h,isVertical:D,normalizedDir:M,nav:de,isSliding:le,registerSlide:(e,t)=>{t(m.length),m.push(e),O.push(t)},unregisterSlide:e=>{const t=m.indexOf(e);t>=0&&(m.splice(t,1),O.splice(t,1),O.slice(t).forEach(((e,n)=>e(t+n))))}});t.provide(n,ve),t.provide("config",S),t.provide("slidesCount",w),t.provide("currentSlide",x),t.provide("maxSlide",C),t.provide("minSlide",_),t.provide("slideSize",h),t.provide("isVertical",D),t.provide("normalizeDir",M),t.provide("nav",de),t.provide("isSliding",le),t.watch((()=>[b.value,e.breakpoints]),(()=>j()),{deep:!0}),t.watch((()=>e.autoplay),(()=>ae()));const pe=t.watch((()=>e.modelValue),(e=>{e!==x.value&&re(Number(e),!0)}));r("before-init");const me=t.reactive({config:S,slidesCount:w,slideSize:h,currentSlide:x,maxSlide:C,minSlide:_,middleSlide:A});d({updateBreakpointsConfig:j,updateSlidesData:B,updateSlideSize:z,restartCarousel:function(){j(),B(),z(),ae()},slideTo:re,next:ue,prev:se,nav:de,data:me});const fe=t.computed((()=>D.value&&h.value&&"auto"===S.height?`${h.value*S.itemsToShow+P.value}px`:"auto"!==S.height?"number"==typeof S.height||parseInt(S.height).toString()===S.height?`${S.height}px`:S.height:void 0)),he=t.computed((()=>{const e=S.wrapAround?E.value:0,t=k.value?-1:1,n=(ce.value+e)*L.value*t,o=D.value?G.y:G.x;return`translate${D.value?"Y":"X"}(${o-n}px)`}));return()=>{const e=a.default||a.slides,n=a.addons;let o=(null==e?void 0:e(me))||[];if(!S.enabled||!o.length)return t.h("section",{ref:v,class:["carousel","is-disabled"]},o);const i=(null==n?void 0:n(me))||[];if(S.wrapAround){const e=o.length>0?o[0].scopeId:null;t.pushScopeId(e);const n=E.value,i=u({slides:m,position:"before",toShow:n}),a=u({slides:m,position:"after",toShow:n});t.popScopeId(),o=[...i,...o,...a]}const l=t.h("ol",{class:"carousel__track",style:{transform:he.value,"transition-duration":le.value?`${S.transition}ms`:void 0,gap:S.gap>0?`${S.gap}px`:void 0},onMousedownCapture:S.mouseDrag?ee:null,onTouchstartPassiveCapture:S.touchDrag?ee:null},o),r=t.h("div",{class:"carousel__viewport",ref:p},l);return t.h("section",{ref:v,class:["carousel",`is-${M.value}`,{"is-vertical":D.value,"is-sliding":le.value,"is-dragging":H.value,"is-hover":q.value}],style:{"--vc-trk-height":fe.value},dir:M.value,"aria-label":S.i18n.ariaGallery,tabindex:"0",onFocus:J,onBlur:Q,onMouseenter:W,onMouseleave:K},[r,i,t.h(s)])}}});var S;!function(e){e.arrowUp="arrowUp",e.arrowDown="arrowDown",e.arrowRight="arrowRight",e.arrowLeft="arrowLeft"}(S||(S={}));const x=e=>`icon${e.charAt(0).toUpperCase()+e.slice(1)}`,y=e=>e&&e in S,A={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"},C=t.defineComponent({props:{name:{type:String,required:!0,validator:y},title:{type:String,default:e=>e.name?g.i18n[x(e.name)]:""}},setup(e){const o=t.inject(n,null);return()=>{const n=e.name;if(!n||!y(n))return;const i=A[n],a=t.h("path",{d:i}),l=(null==o?void 0:o.config.i18n[x(n)])||e.title||n,r=t.h("title",l);return t.h("svg",{class:"carousel__icon",viewBox:"0 0 24 24",role:"img","aria-label":l},[r,a])}}}),_=t.defineComponent({name:"CarouselNavigation",setup(e,{slots:o}){const i=t.inject(n);if(!i)return null;const{next:a,prev:l}=o;return()=>{const{wrapAround:n,i18n:o}=i.config;return[t.h("button",{type:"button",class:["carousel__prev",!n&&i.currentSlide<=i.minSlide&&"carousel__prev--disabled",e.class],"aria-label":o.ariaPreviousSlide,title:o.ariaPreviousSlide,onClick:i.nav.prev},(null==l?void 0:l())||t.h(C,{name:{ltr:"arrowLeft",rtl:"arrowRight",ttb:"arrowUp",btt:"arrowDown"}[i.normalizedDir]})),t.h("button",{type:"button",class:["carousel__next",!n&&i.currentSlide>=i.maxSlide&&"carousel__next--disabled",e.class],"aria-label":o.ariaNextSlide,title:o.ariaNextSlide,onClick:i.nav.next},(null==a?void 0:a())||t.h(C,{name:{ltr:"arrowRight",rtl:"arrowLeft",ttb:"arrowDown",btt:"arrowUp"}[i.normalizedDir]}))]}}}),T=t.defineComponent({name:"CarouselPagination",setup(e){const o=t.inject(n);if(!o)return null;const l=e=>i({val:o.currentSlide,max:o.maxSlide,min:0})===e;return()=>{var n,i;const r=[];for(let u=o.minSlide;u<=o.maxSlide;u++){const s=a(o.config.i18n.ariaNavigateToSlide,{slideNumber:u+1}),d=l(u),c=t.h("button",{type:"button",class:{"carousel__pagination-button":!0,"carousel__pagination-button--active":d},"aria-label":s,"aria-pressed":d,"aria-controls":null===(i=null===(n=o.slides[u])||void 0===n?void 0:n.exposed)||void 0===i?void 0:i.id,title:s,onClick:e.disableOnClick?void 0:()=>o.nav.slideTo(u)}),v=t.h("li",{class:"carousel__pagination-item",key:u},c);r.push(v)}return t.h("ol",{class:"carousel__pagination"},r)}}});e.BREAKPOINT_MODE_OPTIONS=v,e.Carousel=b,e.DEFAULT_CONFIG=g,e.DIR_MAP=f,e.DIR_OPTIONS=p,e.I18N_DEFAULT_CONFIG=m,e.Icon=C,e.NORMALIZED_DIR_OPTIONS=h,e.Navigation=_,e.Pagination=T,e.SNAP_ALIGN_OPTIONS=c,e.Slide=r,e.icons=A,e.injectCarousel=d})); |
{ | ||
"name": "vue3-carousel", | ||
"version": "0.8.1", | ||
"version": "0.9.0", | ||
"type": "module", | ||
"scripts": { | ||
"build": "rollup -c", | ||
"dev": "rollup -cw", | ||
"dev": "vite playground", | ||
"docs:dev": "vitepress dev docs", | ||
"docs:build": "vitepress build docs", | ||
"docs:serve": "vitepress serve docs", | ||
"prepublishOnly": "$npm_execpath run build", | ||
"release": "sh scripts/new-release.sh", | ||
"lint": "eslint . --ext .ts", | ||
"lint:fix": "yarn lint --fix", | ||
"lint": "eslint .", | ||
"lint:fix": "$npm_execpath run lint -- --fix", | ||
"prettier": "prettier . --check", | ||
"prettier:fix": "yarn prettier --write", | ||
"test": "vitest", | ||
"test:watch": "npm run test -- --watchAll" | ||
"prettier:fix": "$npm_execpath run prettier --write", | ||
"typecheck": "tsc -p .", | ||
"typecheck:watch": "tsc -p . --watch --preserveWatchOutput", | ||
"test": "vitest run", | ||
"test:update": "vitest run --u", | ||
"test:watch": "vitest watch", | ||
"prepare": "husky" | ||
}, | ||
@@ -26,51 +31,58 @@ "repository": { | ||
"types": "./dist/carousel.d.ts", | ||
"import": "./dist/carousel.es.js", | ||
"browser": "./dist/carousel.min.js", | ||
"require": "./dist/carousel.js", | ||
"browser": "./dist/carousel.min.js" | ||
"import": "./dist/carousel.es.js" | ||
}, | ||
"./dist/carousel": { | ||
"types": "./dist/carousel.d.ts", | ||
"import": "./dist/carousel.es.js", | ||
"browser": "./dist/carousel.min.js", | ||
"require": "./dist/carousel.js", | ||
"browser": "./dist/carousel.min.js" | ||
"import": "./dist/carousel.es.js" | ||
}, | ||
"./dist/carousel.min.js": { | ||
"import": "./dist/carousel.es.min.js", | ||
"require": "./dist/carousel.min.js" | ||
"require": "./dist/carousel.min.js", | ||
"import": "./dist/carousel.es.min.js" | ||
}, | ||
"./dist/*.css": { | ||
"import": "./dist/*.css", | ||
"require": "./dist/*.css" | ||
} | ||
"require": "./dist/*.css", | ||
"import": "./dist/*.css" | ||
}, | ||
"./carousel.css": "./dist/carousel.css" | ||
}, | ||
"main": "dist/carousel.js", | ||
"module": "dist/carousel.es.js", | ||
"style": "dist/carousel.css", | ||
"types": "dist/carousel.d.ts", | ||
"unpkg": "dist/carousel.min.js", | ||
"jsdelivr": "dist/carousel.min.js", | ||
"main": "./dist/carousel.js", | ||
"module": "./dist/carousel.es.js", | ||
"style": "./dist/carousel.css", | ||
"types": "./dist/carousel.d.ts", | ||
"unpkg": "./dist/carousel.min.js", | ||
"jsdelivr": "./dist/carousel.min.js", | ||
"devDependencies": { | ||
"@eslint/js": "^9.16.0", | ||
"@rollup/plugin-terser": "^0.4.4", | ||
"@rollup/plugin-typescript": "^12.1.1", | ||
"@stackblitz/sdk": "^1.11.0", | ||
"@typescript-eslint/eslint-plugin": "^5.38.1", | ||
"@typescript-eslint/parser": "^5.38.1", | ||
"@typescript-eslint/eslint-plugin": "^8.16.0", | ||
"@typescript-eslint/parser": "^8.16.0", | ||
"@vitejs/plugin-vue": "^5.2.1", | ||
"@vue/test-utils": "^2.4.6", | ||
"eslint": "^7.32.0", | ||
"eslint-config-prettier": "^8.5.0", | ||
"eslint-import-resolver-typescript": "^3.5.1", | ||
"eslint-plugin-import": "^2.26.0", | ||
"eslint-plugin-prettier": "^4.2.1", | ||
"eslint-plugin-vue": "^8.3.0", | ||
"eslint": "^9.16.0", | ||
"eslint-config-prettier": "^9.1.0", | ||
"eslint-define-config": "^2.1.0", | ||
"eslint-import-resolver-typescript": "^3.6.3", | ||
"eslint-plugin-import": "^2.31.0", | ||
"eslint-plugin-prettier": "^5.2.1", | ||
"eslint-plugin-vue": "^9.32.0", | ||
"globals": "^15.13.0", | ||
"husky": "^9.1.7", | ||
"jsdom": "^25.0.1", | ||
"prettier": "^2.3.2", | ||
"rollup": "^4.22.4", | ||
"prettier": "^3.4.1", | ||
"rollup": "^4.28.0", | ||
"rollup-plugin-css-only": "^4.5.2", | ||
"rollup-plugin-delete": "^2.0.0", | ||
"rollup-plugin-dts": "^6.1.0", | ||
"rollup-plugin-delete": "^2.1.0", | ||
"rollup-plugin-dts": "^6.1.1", | ||
"rollup-plugin-typescript-paths": "^1.5.0", | ||
"typescript": "^5.4.3", | ||
"vite-tsconfig-paths": "^5.0.1", | ||
"vitepress": "^1.3.4", | ||
"vitest": "^2.1.4", | ||
"typescript": "^5.7.2", | ||
"typescript-eslint": "^8.16.0", | ||
"vite": "^6", | ||
"vitepress": "^1.5.0", | ||
"vitest": "^2.1.6", | ||
"vue": "^3.2.0" | ||
@@ -82,7 +94,8 @@ }, | ||
"browserslist": [ | ||
"> 1%", | ||
"> 0.5%", | ||
"last 2 versions", | ||
"not dead" | ||
"not dead", | ||
"not IE 11" | ||
], | ||
"license": "MIT" | ||
} |
@@ -0,1 +1,3 @@ | ||
import terser from '@rollup/plugin-terser' | ||
import typescript from '@rollup/plugin-typescript' | ||
import css from 'rollup-plugin-css-only' | ||
@@ -5,6 +7,4 @@ import del from 'rollup-plugin-delete' | ||
import { typescriptPaths } from 'rollup-plugin-typescript-paths' | ||
import typescript from '@rollup/plugin-typescript' | ||
import terser from '@rollup/plugin-terser' | ||
import pkg from './package.json' assert { type: 'json' } | ||
import pkg from './package.json' with { type: 'json' } | ||
@@ -11,0 +11,0 @@ const banner = `/** |
@@ -15,3 +15,4 @@ { | ||
"paths": { | ||
"@/*": ["src/*"] | ||
"@/*": ["src/*"], | ||
"rollup/parseAst": ["./node_modules/rollup/dist/parseAst"] | ||
} | ||
@@ -18,0 +19,0 @@ }, |
import { resolve } from 'path' | ||
import vue from '@vitejs/plugin-vue' | ||
import { defineConfig } from 'vitest/config' | ||
@@ -11,3 +12,3 @@ | ||
key.replace('/*', ''), | ||
resolve(__dirname, value[0].replace('/*', '')), | ||
resolve(__dirname, value[0].replace('/*', '/')), | ||
]) | ||
@@ -18,4 +19,5 @@ ) | ||
export default defineConfig({ | ||
plugins: [vue()], | ||
test: { | ||
setupFiles: ['./vitest.setup.ts'], | ||
setupFiles: './vitest.setup.ts', | ||
environment: 'jsdom', | ||
@@ -22,0 +24,0 @@ globals: true, |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
24
3994
204777
30
1