300 lines
6.7 KiB
Vue
300 lines
6.7 KiB
Vue
<template>
|
|
<div class="datepicker" ref="datepicker">
|
|
<BaseButton @click="toggleDatePopup" class="show" :disabled="disabled || undefined">
|
|
{{ state.date === null ? chooseDateLabel : formatDateShort(state.date) }}
|
|
</BaseButton>
|
|
|
|
<transition name="fade">
|
|
<div v-if="show" class="datepicker-popup">
|
|
|
|
<BaseButton
|
|
v-for="({dayInterval, label, icon}) of selectDateOptions"
|
|
:key="dayInterval"
|
|
class="datepicker__quick-select-date"
|
|
@click="setDate(dayInterval)"
|
|
>
|
|
<span class="icon"><icon :icon="icon"/></span>
|
|
<span class="text">
|
|
<span>{{ label }}</span>
|
|
<span class="weekday">{{ getWeekdayFromStringInterval(dayInterval) }}</span>
|
|
</span>
|
|
</BaseButton>
|
|
|
|
<flat-pickr
|
|
:config="flatPickerConfig"
|
|
class="input"
|
|
v-model="flatPickrDate"
|
|
/>
|
|
|
|
<x-button
|
|
class="datepicker__close-button"
|
|
:shadow="false"
|
|
@click="closeDatePopup"
|
|
v-cy="'closeDatepicker'"
|
|
>
|
|
{{ $t('misc.confirm') }}
|
|
</x-button>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import {ref, reactive, computed, watch, type PropType } from 'vue'
|
|
import {useI18n} from 'vue-i18n'
|
|
import {useStore} from 'vuex'
|
|
import {useNow, onClickOutside} from '@vueuse/core'
|
|
|
|
import flatPickr from 'vue-flatpickr-component'
|
|
import 'flatpickr/dist/flatpickr.css'
|
|
|
|
import BaseButton from '@/components/base/BaseButton.vue'
|
|
|
|
import {format} from 'date-fns'
|
|
|
|
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
|
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
|
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
|
import {formatDateShort} from '@/helpers/time/formatDate'
|
|
|
|
function getDateFromDayInterval(dayInterval: string) {
|
|
const interval = calculateDayInterval(dayInterval)
|
|
const newDate = new Date()
|
|
newDate.setDate(newDate.getDate() + interval)
|
|
newDate.setHours(calculateNearestHours(newDate))
|
|
newDate.setMinutes(0)
|
|
newDate.setSeconds(0)
|
|
return newDate
|
|
}
|
|
|
|
function getWeekdayFromStringInterval(dateInterval: string) {
|
|
const interval = calculateDayInterval(dateInterval)
|
|
const newDate = new Date(now.value)
|
|
newDate.setDate(newDate.getDate() + interval)
|
|
return format(newDate, 'E')
|
|
}
|
|
|
|
const props = defineProps({
|
|
// FIXME: should only accept Date objects. Then we wouldn't need all the conversion
|
|
modelValue: {
|
|
type: [Date, null, String] as PropType<Date | null | string>,
|
|
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
|
|
default: null,
|
|
},
|
|
chooseDateLabel: {
|
|
type: String as PropType<string>,
|
|
default() {
|
|
const {t} = useI18n({useScope: 'global'})
|
|
return t('input.datepicker.chooseDate')
|
|
},
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const {t} = useI18n({useScope: 'global'})
|
|
const store = useStore()
|
|
|
|
const show = ref(false)
|
|
|
|
// FIXME: replace with popup
|
|
const datepicker = ref<HTMLElement | null>(null)
|
|
onClickOutside(datepicker, closeDatePopup)
|
|
|
|
function toggleDatePopup() {
|
|
if (props.disabled) {
|
|
return
|
|
}
|
|
|
|
show.value = !show.value
|
|
}
|
|
|
|
function closeDatePopup() {
|
|
show.value = false
|
|
}
|
|
|
|
const state : {
|
|
date: Date | null,
|
|
initialDate: Date | null,
|
|
}= reactive({
|
|
date: null,
|
|
initialDate: null,
|
|
})
|
|
// const date = ref<Date | null>(null)
|
|
// const initialDate = ref<Date | null>(null)
|
|
watch(
|
|
() => props.modelValue,
|
|
(value: null | Date | string) => {
|
|
const newDate = value === null ? null : createDateFromString(value)
|
|
Object.assign(state, {
|
|
date: newDate,
|
|
initialDate: newDate,
|
|
})
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
function isSameDate(dateA: Date | null, dateB: Date | null) {
|
|
return (dateA !== null ? new Date(dateA).toISOString() : null) ===
|
|
(dateB !== null ? new Date(dateB).toISOString() : null)
|
|
}
|
|
|
|
watch(show, (newIsShown) => {
|
|
newIsShown && console.log(state.date, state.initialDate)
|
|
if (
|
|
newIsShown === false &&
|
|
!isSameDate(state.initialDate, state.date)
|
|
) {
|
|
console.log('initialDate', state.initialDate !== null ? new Date(state.initialDate).toISOString() : null)
|
|
console.log('date', state.date !== null ? new Date(state.date).toISOString() : null)
|
|
// make copy of date
|
|
emit('update:modelValue', state.date !== null ? new Date(state.date) : null)
|
|
}
|
|
})
|
|
|
|
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,
|
|
},
|
|
}))
|
|
|
|
// Since flatpickr dates are strings, we need to convert them to native date objects.
|
|
// To make that work, we need a separate variable since flatpickr does not have a change event.
|
|
const flatPickrDate = computed({
|
|
get() {
|
|
if (!state.date) {
|
|
return ''
|
|
}
|
|
|
|
return format(state.date, 'yyy-LL-dd H:mm')
|
|
},
|
|
set(newValue: string) {
|
|
state.date = createDateFromString(newValue)
|
|
},
|
|
})
|
|
|
|
const now = useNow()
|
|
|
|
const selectDateOptions = computed(() => {
|
|
const options = [
|
|
{
|
|
condition: now.value.getHours() < 21,
|
|
dayInterval: 'today',
|
|
icon: ['far', 'calendar-alt'],
|
|
label: t('input.datepicker.today'),
|
|
},
|
|
{
|
|
dayInterval: 'tomorrow',
|
|
icon: ['far', 'sun'],
|
|
label: t('input.datepicker.tomorrow'),
|
|
},
|
|
{
|
|
dayInterval: 'nextMonday',
|
|
icon: 'coffee',
|
|
label: t('input.datepicker.nextMonday'),
|
|
|
|
},
|
|
{
|
|
dayInterval: 'thisWeekend',
|
|
icon: 'cocktail',
|
|
label: t('input.datepicker.thisWeekend'),
|
|
|
|
},
|
|
{
|
|
dayInterval: 'laterThisWeek',
|
|
icon: 'chess-knight',
|
|
label: t('input.datepicker.laterThisWeek'),
|
|
},
|
|
{
|
|
dayInterval: 'nextWeek',
|
|
icon: 'forward',
|
|
label: t('input.datepicker.nextWeek'),
|
|
},
|
|
]
|
|
|
|
return options.filter((option) => option.condition || option.condition === undefined)
|
|
})
|
|
|
|
function setDate(dayInterval: string) {
|
|
state.date = getDateFromDayInterval(dayInterval)
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.datepicker {
|
|
input.input {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.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);
|
|
}
|
|
}
|
|
|
|
.datepicker__quick-select-date {
|
|
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(--grey-100);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
}
|
|
|
|
.datepicker__close-button {
|
|
margin: 1rem;
|
|
width: calc(100% - 2rem);
|
|
}
|
|
|
|
:deep(.flatpickr-calendar) {
|
|
margin: 0 auto 8px;
|
|
box-shadow: none;
|
|
}
|
|
</style> |