This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/components/input/datepicker.vue

307 lines
6.7 KiB
Vue

<template>
<div class="datepicker" :class="{'disabled': disabled}">
<a @click.stop="toggleDatePopup" class="show">
<template v-if="date === null">
{{ chooseDateLabel }}
</template>
<template v-else>
{{ formatDateShort(date) }}
</template>
</a>
<transition name="fade">
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
<a @click.stop="() => setDate('today')" v-if="(new Date()).getHours() < 21">
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
<span class="text">
<span>
{{ $t('input.datepicker.today') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('today') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('tomorrow')">
<span class="icon">
<icon :icon="['far', 'sun']"/>
</span>
<span class="text">
<span>
{{ $t('input.datepicker.tomorrow') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('tomorrow') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('nextMonday')">
<span class="icon">
<icon icon="coffee"/>
</span>
<span class="text">
<span>
{{ $t('input.datepicker.nextMonday') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('nextMonday') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('thisWeekend')">
<span class="icon">
<icon icon="cocktail"/>
</span>
<span class="text">
<span>
{{ $t('input.datepicker.thisWeekend') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('thisWeekend') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('laterThisWeek')">
<span class="icon">
<icon icon="chess-knight"/>
</span>
<span class="text">
<span>
{{ $t('input.datepicker.laterThisWeek') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('laterThisWeek') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('nextWeek')">
<span class="icon">
<icon icon="forward"/>
</span>
<span class="text">
<span>
{{ $t('input.datepicker.nextWeek') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('nextWeek') }}
</span>
</span>
</a>
<flat-pickr
:config="flatPickerConfig"
class="input"
v-model="flatPickrDate"
/>
<x-button
class="is-fullwidth"
:shadow="false"
@click="close"
v-cy="'closeDatepicker'"
>
{{ $t('misc.confirm') }}
</x-button>
</div>
</transition>
</div>
</template>
<script lang="ts" setup>
import {ref, computed, watch, onMounted, onBeforeUnmount} from 'vue'
import {useI18n} from 'vue-i18n'
import {useStore} from 'vuex'
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import {format} from 'date-fns'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {createDateFromString} from '@/helpers/time/createDateFromString'
const props = defineProps({
modelValue: {
type: [Date, String],
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
},
chooseDateLabel: {
type: String,
default: () => t('input.datepicker.chooseDate'),
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change', 'close', 'close-on-change'])
const {t} = useI18n()
const store = useStore()
const date = ref<Date>()
const show = ref(false)
const changed = ref(false)
onMounted(() => document.addEventListener('click', hideDatePopup))
onBeforeUnmount(() => document.removeEventListener('click', hideDatePopup))
watch(
() => props.modelValue,
(value) => setDateValue(value),
{ immediate: true },
)
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: store.state.auth.settings.weekStart,
},
}))
function setDateValue(newVal) {
if (newVal === null) {
date.value = undefined
return
}
date.value = createDateFromString(newVal)
}
function updateData() {
changed.value = true
emit('update:modelValue', date.value)
emit('change', date.value)
}
function toggleDatePopup() {
if (props.disabled) {
return
}
show.value = !show.value
}
const datepickerPopup = ref<HTMLElement | null>(null)
function hideDatePopup(e) {
if (show.value) {
closeWhenClickedOutside(e, datepickerPopup?.value, close)
}
}
function close() {
// Kind of dirty, but the timeout allows us to enter a time and click on "confirm" without
// having to click on another input field before it is actually used.
setTimeout(() => {
show.value = false
emit('close', changed.value)
if (changed.value) {
changed.value = false
emit('close-on-change', changed.value)
}
}, 200)
}
const flatPickrDate = ref()
function setDate(date) {
if (date.value === null) {
date.value = new Date()
}
const interval = calculateDayInterval(date)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
newDate.setHours(calculateNearestHours(newDate))
newDate.setMinutes(0)
newDate.setSeconds(0)
date.value = newDate
flatPickrDate.value = newDate
updateData()
}
function getWeekdayFromStringInterval(date) {
const interval = calculateDayInterval(date)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
return format(newDate, 'E')
}
</script>
<style lang="scss" scoped>
.datepicker {
input.input {
display: none;
}
&.disabled a {
cursor: default;
}
.datepicker-popup {
position: absolute;
z-index: 99;
width: 320px;
background: var(--white);
border-radius: $radius;
box-shadow: $shadow;
@media screen and (max-width: ($tablet)) {
width: calc(100vw - 5rem);
}
a:not(.button) {
display: flex;
align-items: center;
padding: 0 .5rem;
width: 100%;
height: 2.25rem;
color: var(--text);
transition: all $transition;
&:first-child {
border-radius: $radius $radius 0 0;
}
&:hover {
background: var(--light);
}
.text {
width: 100%;
font-size: .85rem;
display: flex;
justify-content: space-between;
padding-right: .25rem;
.weekday {
color: var(--text-light);
text-transform: capitalize;
}
}
.icon {
width: 2rem;
text-align: center;
}
}
a.button {
margin: 1rem;
width: calc(100% - 2rem);
}
:deep(.flatpickr-calendar) {
margin: 0 auto 8px;
box-shadow: none;
}
}
}
</style>