Skip to content

Commit

Permalink
feat(VSlider): finish functionalities
Browse files Browse the repository at this point in the history
  • Loading branch information
timothejoubert committed Jun 11, 2024
1 parent 5f7392f commit 404802e
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ watch(slides, (value) => {
background-color: #ffd6d6;
&:nth-child(odd) :global(.marker) {
width: 4px;
width: 8px;
background-color: blue;
}
Expand Down
84 changes: 84 additions & 0 deletions components/molecules/VSlider/OneSlide.stories.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script setup lang="ts">
import type { ComponentPublicInstance } from 'vue'
import type { VSlider } from '#components'
import type { Slide } from '~/components/molecules/VSlider/VSlider.vue'
const itemLength = ref(6)
const slideIndex = ref(0)
const vSliderInstance = ref<ComponentPublicInstance<typeof VSlider> | null>(null)
const slides = computed(() => vSliderInstance.value?.slides as Slide[])
function setSnapMarkers(slides: Slide[]) {
slides.forEach((slide) => {
const leftMarker = slide.el?.querySelector('.marker-left') as HTMLElement
const rightMarker = slide.el?.querySelector('.marker-right') as HTMLElement
if (leftMarker) {
leftMarker.style.left = slide.snapLeft + 'px'
}
if (rightMarker) {
rightMarker.style.left = slide.snapRight + 'px'
}
})
}
watch(slides, (value) => {
if (value?.length) setSnapMarkers(value)
})
</script>

<template>
<NuxtStory>
<VSlider v-slot="{ itemClass }" ref="vSliderInstance" v-model="slideIndex" :class="$style.root">
<div v-for="item in itemLength" :key="item" :class="[$style.item, itemClass]">
<div class="marker marker-left"></div>
{{ item }}
<div class="marker marker-right"></div>
</div>
</VSlider>
</NuxtStory>
</template>

<style>
.marker-right,
.marker-left {
position: absolute;
z-index: 2;
left: 0;
width: 2px;
height: 100%;
background-color: red;
}
</style>

<style lang="scss" module>
.root {
--gutter: #{rem(24)};
--v-slider-scroll-snap-align: center;
position: relative;
background-color: #d8d8d8;
padding-block: rem(48);
:global(.nuxt-story__main) {
touch-action: pan-x;
}
}
.item {
display: flex;
width: 60%;
height: rem(260);
align-items: center;
justify-content: center;
border: 1px solid black;
background-color: #ffd6d6;
margin-inline: 20%;
&:nth-child(odd) :global(.marker) {
width: 4px;
background-color: blue;
}
}
</style>
5 changes: 5 additions & 0 deletions components/molecules/VSlider/SnapVisualizer.stories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ watch(slides, (value) => {
<div class="marker marker-right"></div>
</div>
</VSlider>

<div>
<button @click="() => (slideIndex = slideIndex - 1)">Prev</button>
<button @click="() => (slideIndex = slideIndex + 1)">Next</button>
</div>
</NuxtStory>
</template>

Expand Down
63 changes: 41 additions & 22 deletions components/molecules/VSlider/VSlider.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { getHtmlElement } from '~/utils/ref/get-html-element'
import { throttle } from 'throttle-debounce'
import { debounce, throttle } from 'throttle-debounce'

Check warning on line 3 in components/molecules/VSlider/VSlider.vue

View workflow job for this annotation

GitHub Actions / lint

'debounce' is defined but never used
type ScrollToOptions = {
scrollEndCallback?: () => void
Expand All @@ -10,6 +10,7 @@ export type Slide = {
left: number
snapLeft: number
snapRight: number
right: number
el: HTMLElement
}
Expand All @@ -26,7 +27,9 @@ const unwatchRootEl = watch(root, (el) => {
})
// Slider data
const syncedIndex = defineModel<number>({ default: 0 })
const SNAP_OFFSET = 50
const syncedIndex = defineModel<number>({ default: 0 }) // Update this value only when user stop interacting with slider
const slides = ref<Slide[]>([])
const scrollDirection = ref(1)
const isScrolling = ref(false)
Expand All @@ -35,7 +38,7 @@ const scrollLeft = ref(0)
const setScrollLeft = () => (scrollLeft.value = getHtmlElement(root)?.scrollLeft || 0)
watch(scrollLeft, (value, oldValue) => {
if (value == oldValue || !scrollLeft.value) return
if (value === oldValue) return
scrollDirection.value = oldValue > value ? -1 : 1
})
Expand All @@ -49,24 +52,26 @@ function setSlidesInfos() {
}
const slideList = [...element.children] as HTMLElement[]
const firstSlideOffset = slideList[0].getBoundingClientRect().left - element.getBoundingClientRect().left
slides.value = slideList.map((child) => {
const start = child.getBoundingClientRect().left - element.getBoundingClientRect().left + element.scrollLeft
const relativeLeft = child.getBoundingClientRect().left - element.getBoundingClientRect().left
const start = relativeLeft - firstSlideOffset + element.scrollLeft
const width = child.offsetWidth
const snapOffset = width > element.clientWidth * 0.7 ? width : width / 2
return {
left: start,
snapLeft: start - snapOffset,
snapRight: start + width + snapOffset,
snapLeft: start + SNAP_OFFSET,
right: start + width,
snapRight: start + width - SNAP_OFFSET,
el: child,
}
})
}
const currentIndex = computed(() => {
return slides.value[scrollDirection.value ? 'findLastIndex' : 'findIndex']((slide) => {
return scrollLeft.value > slide.snapLeft && scrollLeft.value < slide.snapRight
return slides.value.findIndex((slide) => {
return scrollLeft.value < slide[scrollDirection.value === 1 ? 'snapLeft' : 'snapRight']
})
})
Expand All @@ -85,39 +90,47 @@ const lastReachableIndex = computed(() => slides.value.length - visibleSlideLeng
const isDragging = ref(false)
const { setStyle, hasScroll } = useDraggableScroll({ element: root, onMouseUp, onMouseDown })
const hasTransition = ref(false)
function scrollTo(index: number, options?: ScrollToOptions) {
const element = getHtmlElement(root)
const invalidIndex = syncedIndex.value < 0 || syncedIndex.value > lastReachableIndex.value
const activeItemLeft = slides.value[index]?.left
const left = slides.value[index]?.left
if (!element || invalidIndex || typeof activeItemLeft !== 'number') {
if (!element || invalidIndex || typeof left !== 'number' || hasTransition.value) {
return
}
element.addEventListener(
'scrollend',
() => {
syncedIndex.value = index
options?.scrollEndCallback?.()
hasTransition.value = false
isScrolling.value = false
// Update v-model only when scroll animation is ended
syncedIndex.value = index
},
{ once: true },
)
hasTransition.value = true
element.scroll({
left: activeItemLeft,
left,
behavior: 'smooth',
})
}
// When slider is dragged
let scrollSnapTypeStored = ''
onMounted(() => {
scrollSnapTypeStored = getHtmlElement(root)?.style['scrollSnapType'] || 'x mandatory'
})
function onMouseDown() {
isDragging.value = true
const element = getHtmlElement(root)
if (!element) return
Expand All @@ -127,34 +140,40 @@ function onMouseDown() {
}
function onDragTransitionEnd() {
if (isDragging.value) return
isDragging.value = false
console.log('onDragTransitionEnd')
isScrolling.value = false
const element = getHtmlElement(root)
if (!element) return
// Sometimes scrollSnapType is set before scroll transition is fully done
// this cause a scroll jump
window.setTimeout(() => (element.style['scrollSnapType'] = scrollSnapTypeStored), 300)
element.style['scrollSnapType'] = scrollSnapTypeStored
}
function onMouseUp() {
isDragging.value = false
scrollTo(currentIndex.value, { scrollEndCallback: onDragTransitionEnd })
}
// Update slide index and slide direction
const onScrollCallback = throttle(150, setScrollLeft)
const onScrollCallback = throttle(100, setScrollLeft)
function onScroll() {
isScrolling.value = true
onScrollCallback()
}
function onScrollEnd() {
if (isDragging.value) return
if (isDragging.value || hasTransition.value) return
isScrolling.value = false
// Update v-model only when scroll animation is ended
getHtmlElement(root)!.style['scrollSnapType'] = scrollSnapTypeStored
// Update VModel on native scroll
syncedIndex.value = currentIndex.value
}
Expand Down Expand Up @@ -185,7 +204,6 @@ watch(hasScroll, () => {
updateSlides()
if (syncedIndex.value === currentIndex.value) return
scrollTo(syncedIndex.value)
})
Expand All @@ -208,6 +226,7 @@ defineExpose<{ slides: Ref<Slide[]> }>({ slides })
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none; /* for Firefox */
touch-action: pan-x;
&::-webkit-scrollbar {
display: none; /* for Chrome, Safari, and Opera */
Expand All @@ -216,6 +235,6 @@ defineExpose<{ slides: Ref<Slide[]> }>({ slides })
.item {
flex-shrink: 0;
scroll-snap-align: start;
scroll-snap-align: var(--v-slider-scroll-snap-align, start);
}
</style>
1 change: 1 addition & 0 deletions composables/use-draggable-scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export function useDraggableScroll(options: UseDraggableScrollOptions) {
resizeObserver = null
}

watch(isDragging, setStyle)
function setStyle(isGrabbing?: boolean) {
const element = getHtmlElement(options.element)
if (!element) return
Expand Down

0 comments on commit 404802e

Please sign in to comment.