Coding | February 2024Read in Outline
<template>
<Stack
ref="wrapper"
:style="{
'--height': '48px',
'--items': props.maxItems ?? 3,
height: 'calc((var(--items, 5)) * var(--height))'
}"
position="relative"
:css="{
_before: {
content: `' '`,
position: 'absolute',
top: 0,
width: '100%',
height: 'calc(50% - 3.5ex)',
pointerEvents: 'none',
background:
'linear-gradient(rgba(255, 255, 255, 1),65%,rgba(255,255,255,0))'
},
_after: {
content: `' '`,
position: 'absolute',
bottom: 0,
width: '100%',
height: 'calc(50% - 3.5ex)',
pointerEvents: 'none',
background:
'linear-gradient(rgba(255,255,255,0),35%,rgba(255,255,255,1))'
}
}"
>
<Stack
ref="scroller"
gap="0"
overflowY="auto"
scrollSnapType="y mandatory"
alignItems="center"
:style="{
paddingTop: 'calc(var(--items) / 2 * var(--height))',
paddingBottom: 'calc(var(--items) / 2 * var(--height))'
}"
overscrollBehavior="contain"
scrollbar="hidden"
@scroll="handleScroll"
>
<Divider />
<template v-for="option in props.options" :key="option.value">
<Text
scrollSnapAlign="center"
py="3"
px="8"
:data-value="option.value"
@click="setValue(option.value)"
>
{{ option.label }}
</Text>
<Divider />
</template>
</Stack>
</Stack>
</template>
<script lang="ts" setup>
import debounce from 'lodash/debounce'
import type Stack from '../layout/Stack'
const scroller = ref<ComponentPublicInstance<typeof Stack> | null>(null)
const model = defineModel<string>()
const props = defineProps<{
options: { label: string; value: string }[]
maxItems?: number
}>()
const emit = defineEmits(['change'])
let lastScrollPos = -1
let isScrolling = false
const setNotScrolling = debounce(() => {
isScrolling = false
}, 200)
const updateScroller = (value: string) => {
const container = scroller.value?.$el as HTMLDivElement
if (!container || isScrolling) {
return
}
const element = Array.from(container.children).find(
(child) => child.getAttribute('data-value') === value
)
element?.scrollIntoView({
block: 'center'
})
}
const handleScroll = debounce((e: UIEvent) => {
const target = e?.target as HTMLDivElement
const container = scroller.value?.$el as HTMLDivElement
const parent = container?.parentNode as HTMLDivElement
if (!e || !target || !container || target.scrollTop === lastScrollPos) {
return undefined
}
isScrolling = true
setNotScrolling()
lastScrollPos = target.scrollTop
const children = Array.from(container.children).filter(
(item) =>
item.attributes.getNamedItem('data-value')?.value !== undefined
)
const itemHeight = children[0].getBoundingClientRect().height
const parentOffset = parent.getBoundingClientRect().top
const value = children
.find((child) => {
const { top } = child.getBoundingClientRect()
const offset = top - parentOffset
const midPoint = offset + itemHeight / 2
return midPoint > itemHeight && midPoint < 2 * itemHeight
})
?.attributes.getNamedItem('data-value')?.value
if (!value) {
return
}
setValue(value)
}, 200)
const setValue = (value: string) => {
model.value = value
}
watchEffect(() => {
if (!model.value || isScrolling) {
return
}
updateScroller(model.value)
})
onUpdated(() => {
if (lastScrollPos >= 0) {
scroller.value?.$el.scrollTo({ top: lastScrollPos })
}
})
</script>
<style></style>
© 2023-2024 HamP, Assets used in the site belongs to respective owner | View Source