CSS Time Picker V2

Coding | January 2024Read in Outline

<template>
    <HStack
        p="2"
        justifyContent="center"
        alignItems="center"
        position="relative"
        bgColor="white"
        :css="{
            '--height': '25px',
            _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 height="calc(5 * var(--height))" justifyContent="center">
            <Stack
                ref="hourScroll"
                gap="0"
                overflowY="auto"
                scrollSnapType="y mandatory"
                py="calc(2 * var(--height))"
                alignItems="center"
                overscrollBehavior="contain"
                scrollbar="hidden"
                @scrollend="handleHourScroll"
            >
                <Divider />
                <template v-for="i in 24" :key="i">
                    <Text
                        scrollSnapAlign="center"
                        py="3"
                        px="8"
                        @click="setHours(i - 1)"
                    >
                        {{ i - 1 }}
                    </Text>
                    <Divider />
                </template>
            </Stack>
        </Stack>
        <Text> : </Text>
        <Stack height="calc(5 * var(--height))" justifyContent="center">
            <Stack
                ref="minuteScroll"
                gap="0"
                overflowY="auto"
                scrollSnapAlign="center"
                scrollSnapType="y mandatory"
                py="calc(2 * var(--height))"
                alignItems="center"
                overscrollBehavior="contain"
                scrollbar="hidden"
                @scrollend="handleMinuteScroll"
            >
                <template v-for="i in 60" :key="i">
                    <Text
                        scrollSnapAlign="center"
                        py="3"
                        px="8"
                        @click="setMinutes(i - 1)"
                    >
                        {{ (i - 1).toString().padStart(2, '0') }}
                    </Text>
                    <Divider />
                </template>
            </Stack>
        </Stack>
    </HStack>
</template>

<script lang="ts" setup>
    import debounce from 'lodash/debounce'
    import type Stack from '../layout/Stack'

    const hourScroll = ref<ComponentPublicInstance<typeof Stack> | null>(null)
    const minuteScroll = ref<ComponentPublicInstance<typeof Stack> | null>(null)

    const time = ref('')
    const model = defineModel<string>()

    const emit = defineEmits(['change'])

    let lastHourScrollPos = -1
    let lastMinuteScrollPos = -1

    let isScrolling = false

    const setNotScrolling = debounce(() => {
        isScrolling = false
    }, 200)

    const updateTimer = (hour: string, minutes: string) => {
        const hourContainer = hourScroll.value?.$el as HTMLDivElement
        const minuteContainer = minuteScroll.value?.$el as HTMLDivElement
        if (!hourContainer || !minuteContainer || isScrolling) {
            return
        }
        console.log('setting scroller', hour, minutes)
        const hourElement = Array.from(hourContainer.children).find(
            (child) => child.textContent === hour
        )
        const minuteElement = Array.from(minuteContainer.children).find(
            (child) => child.textContent === minutes
        )
        hourElement?.scrollIntoView({
            block: 'center'
        })
        minuteElement?.scrollIntoView({
            block: 'center'
        })
    }

    const determineSnapped = (
        container: HTMLElement,
        e: Event,
        lastScrollPos: number
    ) => {
        isScrolling = true
        setNotScrolling()
        if (!container) {
            return
        }
        const target = e?.target as HTMLDivElement

        if (!e || !target || target.scrollTop === lastScrollPos) {
            return undefined
        }

        const viewportHeight = window.innerHeight
        container.style.gap = '100vh'
        container.getBoundingClientRect()
        const value = Array.from(container.children).find((child) => {
            const { top } = child.getBoundingClientRect()
            return top > 0 && top < viewportHeight
        })?.textContent
        container.style.gap = ''
        return {
            value,
            scrollPos: target.scrollTop
        }
    }

    const handleHourScroll = (e: UIEvent) => {
        const res = determineSnapped(
            hourScroll.value?.$el,
            e,
            lastHourScrollPos
        )
        if (!res) return
        const { value, scrollPos } = res
        lastHourScrollPos = scrollPos
        if (!value) return
        setValue({ hours: value })
    }

    const handleMinuteScroll = (e: UIEvent) => {
        const res = determineSnapped(
            minuteScroll.value?.$el,
            e,
            lastMinuteScrollPos
        )
        if (!res) return
        const { value, scrollPos } = res
        lastMinuteScrollPos = scrollPos
        if (!value) return
        setValue({ minutes: value })
    }

    const setValue = ({
        hours = model.value?.split(':')[0] ?? new Date().getHours().toString(),
        minutes = model.value?.split(':')[1] ??
            new Date().getMinutes().toString().padStart(2, '0')
    }: {
        hours?: string
        minutes?: string
    }) => {
        model.value = `${hours}:${minutes}`
    }

    watchEffect(() => {
        if (model.value) {
            // time.value = model.value
        }
        updateTimer(
            model.value?.split(':')[0] ?? new Date().getHours().toString(),
            model.value?.split(':')[1] ??
                new Date().getMinutes().toString().padStart(2, '0')
        )
    })

    onUpdated(() => {
        if (lastMinuteScrollPos >= 0)
            minuteScroll.value?.$el.scrollTo({ top: lastMinuteScrollPos })
        if (lastHourScrollPos >= 0)
            hourScroll.value?.$el.scrollTo({ top: lastHourScrollPos })
    })
    const setMinutes = (minutes: number) =>
        setValue({ minutes: minutes.toString() })
    const setHours = (hours: number) => setValue({ hours: hours.toString() })
</script>

<style></style>

© 2023-2024 HamP, Assets used in the site belongs to respective owner | View Source