fix : wip
This commit is contained in:
151
frontend/components/ui/TimeSelect.vue
Normal file
151
frontend/components/ui/TimeSelect.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div ref="root" class="relative w-full">
|
||||
<button
|
||||
ref="trigger"
|
||||
type="button"
|
||||
class="w-full flex justify-between rounded-md border border-neutral-300 bg-white px-3 py-2 text-left text-sm text-neutral-900 focus:outline-none focus:border-primary-500 disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:text-neutral-500"
|
||||
:disabled="props.disabled"
|
||||
@click="toggleOpen"
|
||||
>
|
||||
{{ displayValue }}
|
||||
<Icon name="mdi:chevron-down" class="self-center"/>
|
||||
</button>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
ref="menu"
|
||||
class="fixed z-[120] overflow-y-auto rounded-md border border-neutral-300 bg-white shadow-sm"
|
||||
:style="menuStyle"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="block w-full px-2 py-2 text-left text-sm hover:bg-tertiary-500"
|
||||
@click="selectValue('')"
|
||||
>
|
||||
{{ placeholder }}
|
||||
</button>
|
||||
<button
|
||||
v-for="slot in timeSlots"
|
||||
:key="slot"
|
||||
type="button"
|
||||
class="block w-full px-2 py-2 text-left text-sm hover:bg-tertiary-500"
|
||||
@click="selectValue(slot)"
|
||||
>
|
||||
{{ slot }}
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
placeholder: '--',
|
||||
disabled: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
const trigger = ref<HTMLElement | null>(null)
|
||||
const menu = ref<HTMLElement | null>(null)
|
||||
const isOpen = ref(false)
|
||||
const menuStyle = ref<Record<string, string>>({
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
width: '0px',
|
||||
maxHeight: '224px'
|
||||
})
|
||||
|
||||
const timeSlots = computed(() => {
|
||||
const slots: string[] = []
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 15) {
|
||||
slots.push(`${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`)
|
||||
}
|
||||
}
|
||||
return slots
|
||||
})
|
||||
|
||||
const displayValue = computed(() => props.modelValue || props.placeholder)
|
||||
|
||||
const updateMenuPosition = () => {
|
||||
const triggerEl = trigger.value
|
||||
if (!triggerEl) return
|
||||
|
||||
const rect = triggerEl.getBoundingClientRect()
|
||||
const menuHeight = 224
|
||||
const belowTop = rect.bottom + 4
|
||||
const aboveTop = Math.max(8, rect.top - menuHeight - 4)
|
||||
const canOpenBelow = belowTop + menuHeight <= window.innerHeight - 8
|
||||
const top = canOpenBelow ? belowTop : aboveTop
|
||||
|
||||
menuStyle.value = {
|
||||
top: `${top}px`,
|
||||
left: `${rect.left}px`,
|
||||
width: `${rect.width}px`,
|
||||
maxHeight: `${menuHeight}px`
|
||||
}
|
||||
}
|
||||
|
||||
const toggleOpen = () => {
|
||||
if (props.disabled) return
|
||||
const next = !isOpen.value
|
||||
isOpen.value = next
|
||||
if (next) {
|
||||
nextTick(updateMenuPosition)
|
||||
}
|
||||
}
|
||||
|
||||
const selectValue = (value: string) => {
|
||||
if (props.disabled) return
|
||||
emit('update:modelValue', value)
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const onDocumentClick = (event: MouseEvent) => {
|
||||
const target = event.target as Node | null
|
||||
if (!target) return
|
||||
if (root.value?.contains(target) || menu.value?.contains(target)) return
|
||||
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const onWindowChange = () => {
|
||||
if (!isOpen.value) return
|
||||
updateMenuPosition()
|
||||
}
|
||||
|
||||
watch(isOpen, (open) => {
|
||||
if (open) {
|
||||
window.addEventListener('resize', onWindowChange)
|
||||
window.addEventListener('scroll', onWindowChange, true)
|
||||
nextTick(updateMenuPosition)
|
||||
} else {
|
||||
window.removeEventListener('resize', onWindowChange)
|
||||
window.removeEventListener('scroll', onWindowChange, true)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.disabled, (disabled) => {
|
||||
if (disabled) {
|
||||
isOpen.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDocumentClick)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDocumentClick)
|
||||
window.removeEventListener('resize', onWindowChange)
|
||||
window.removeEventListener('scroll', onWindowChange, true)
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user