store/resources/js/components/ui/carousel/DraggableCarousel.vue

137 lines
3.7 KiB
Vue

<script setup lang="ts">
import { ChevronLeft, ChevronRight } from '@lucide/vue';
import { ref, onMounted, nextTick } from 'vue';
const track = ref<HTMLElement | null>(null);
const canScrollLeft = ref(false);
const canScrollRight = ref(false);
// Drag state
const isDragging = ref(false);
const dragStartX = ref(0);
const dragScrollStart = ref(0);
const hasDragged = ref(false);
let dragRafId = 0;
const updateScrollButtons = () => {
const el = track.value;
if (!el) {
return;
}
canScrollLeft.value = el.scrollLeft > 4;
canScrollRight.value = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
};
const scrollTo = (direction: 'left' | 'right') => {
const el = track.value;
if (!el) {
return;
}
const amount = el.clientWidth * 0.6;
el.style.scrollBehavior = 'smooth';
el.scrollBy({ left: direction === 'left' ? -amount : amount });
requestAnimationFrame(() => {
el.style.scrollBehavior = '';
});
};
const onDragStart = (e: MouseEvent | TouchEvent) => {
const el = track.value;
if (!el) {
return;
}
isDragging.value = true;
hasDragged.value = false;
dragStartX.value = 'touches' in e ? e.touches[0].pageX : e.pageX;
dragScrollStart.value = el.scrollLeft;
};
const onDragMove = (e: MouseEvent | TouchEvent) => {
if (!isDragging.value) {
return;
}
e.preventDefault();
const el = track.value;
if (!el) {
return;
}
const currentX = 'touches' in e ? e.touches[0].pageX : e.pageX;
const delta = currentX - dragStartX.value;
if (Math.abs(delta) > 3) {
hasDragged.value = true;
}
cancelAnimationFrame(dragRafId);
dragRafId = requestAnimationFrame(() => {
el.scrollLeft = dragScrollStart.value - delta;
});
};
const onDragEnd = () => {
isDragging.value = false;
cancelAnimationFrame(dragRafId);
nextTick(() => updateScrollButtons());
};
const preventClick = (e: Event) => {
if (hasDragged.value) {
e.preventDefault();
e.stopPropagation();
}
};
onMounted(() => {
nextTick(() => updateScrollButtons());
});
defineExpose({ updateScrollButtons });
</script>
<template>
<div class="relative flex items-center min-w-0">
<button v-show="canScrollLeft" @click="scrollTo('left')"
class="absolute left-0 z-10 flex items-center justify-center w-7 h-7 rounded-full bg-white/90 border border-slate-200 shadow-sm hover:bg-amber-50 hover:border-amber-300 transition-all cursor-pointer -ml-1">
<ChevronLeft class="w-4 h-4 text-slate-500" />
</button>
<div ref="track" @scroll="updateScrollButtons"
@mousedown="onDragStart" @mousemove="onDragMove" @mouseup="onDragEnd" @mouseleave="onDragEnd"
@touchstart.passive="onDragStart" @touchmove="onDragMove" @touchend="onDragEnd"
@click.capture="preventClick"
class="flex gap-2 overflow-x-auto scrollbar-hide px-1 py-1 select-none touch-pan-y will-change-scroll"
:class="[
{ 'mx-8': canScrollLeft || canScrollRight },
isDragging ? 'cursor-grabbing' : 'cursor-grab'
]">
<slot />
</div>
<button v-show="canScrollRight" @click="scrollTo('right')"
class="absolute right-0 z-10 flex items-center justify-center w-7 h-7 rounded-full bg-white/90 border border-slate-200 shadow-sm hover:bg-amber-50 hover:border-amber-300 transition-all cursor-pointer -mr-1">
<ChevronRight class="w-4 h-4 text-slate-500" />
</button>
</div>
</template>
<style scoped>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>