feat(datepicker) script setup
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
Dominik Pschenitschni 2022-07-10 15:49:01 +02:00
parent 3440d71e74
commit b87436c35c
Signed by: dpschen
GPG Key ID: B257AC0149F43A77
6 changed files with 229 additions and 241 deletions

View File

@ -1,71 +1,22 @@
<template>
<div class="datepicker">
<BaseButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
<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" ref="datepickerPopup">
<div v-if="show" class="datepicker-popup">
<BaseButton
v-if="(new Date()).getHours() < 21"
v-for="({dayInterval, label, icon}) of selectDateOptions"
:key="dayInterval"
class="datepicker__quick-select-date"
@click.stop="setDate('today')"
@click="setDate(dayInterval)"
>
<span class="icon"><icon :icon="['far', 'calendar-alt']"/></span>
<span class="icon"><icon :icon="icon"/></span>
<span class="text">
<span>{{ $t('input.datepicker.today') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('today') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@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>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@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>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@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>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@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>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@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>{{ label }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval(dayInterval) }}</span>
</span>
</BaseButton>
@ -78,7 +29,7 @@
<x-button
class="datepicker__close-button"
:shadow="false"
@click="close"
@click="closeDatePopup"
v-cy="'closeDatepicker'"
>
{{ $t('misc.confirm') }}
@ -88,155 +39,197 @@
</div>
</template>
<script lang="ts">
import {defineComponent} from 'vue'
<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 {i18n} from '@/i18n'
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 {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {createDateFromString} from '@/helpers/time/createDateFromString'
import {formatDateShort} from '@/helpers/time/formatDate'
export default defineComponent({
name: 'datepicker',
data() {
return {
date: null,
show: false,
changed: false,
}
},
components: {
flatPickr,
BaseButton,
},
props: {
modelValue: {
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
},
chooseDateLabel: {
type: String,
default() {
return i18n.global.t('input.datepicker.chooseDate')
},
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue', 'change', 'close', 'close-on-change'],
mounted() {
document.addEventListener('click', this.hideDatePopup)
},
beforeUnmount() {
document.removeEventListener('click', this.hideDatePopup)
},
watch: {
modelValue: {
handler: 'setDateValue',
immediate: true,
},
},
computed: {
flatPickerConfig() {
return {
altFormat: this.$t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: this.$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.
flatPickrDate: {
set(newValue) {
this.date = createDateFromString(newValue)
this.updateData()
},
get() {
if (!this.date) {
return ''
}
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
}
return format(this.date, 'yyy-LL-dd H:mm')
},
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')
},
},
methods: {
setDateValue(newVal) {
if (newVal === null) {
this.date = null
return
}
this.date = createDateFromString(newVal)
},
updateData() {
this.changed = true
this.$emit('update:modelValue', this.date)
this.$emit('change', this.date)
},
toggleDatePopup() {
if (this.disabled) {
return
}
this.show = !this.show
},
hideDatePopup(e) {
if (this.show) {
closeWhenClickedOutside(e, this.$refs.datepickerPopup, this.close)
}
},
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(() => {
this.show = false
this.$emit('close', this.changed)
if (this.changed) {
this.changed = false
this.$emit('close-on-change', this.changed)
}
}, 200)
},
setDate(date) {
if (this.date === null) {
this.date = 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)
this.date = newDate
this.flatPickrDate = newDate
this.updateData()
},
getDayIntervalFromString(date) {
return calculateDayInterval(date)
},
getWeekdayFromStringInterval(date) {
const interval = calculateDayInterval(date)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
return format(newDate, 'E')
},
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>

View File

@ -36,8 +36,8 @@
<strong>{{ $t('task.attributes.reminders') }}</strong>
<reminders
@change="editTaskSubmit()"
v-model="taskEditTask.reminderDates"
@update:model-value="editTaskSubmit()"
/>
<div class="field">

View File

@ -3,22 +3,25 @@
<div
v-for="(r, index) in reminders"
:key="index"
:class="{ 'overdue': r < new Date()}"
class="reminder-input"
:class="{ 'overdue': r < new Date()}"
>
<Datepicker
v-model="reminders[index]"
@update:model-value="addReminderDate(index)"
:disabled="disabled"
@close-on-change="() => addReminderDate(index)"
/>
<BaseButton @click="removeReminderByIndex(index)" v-if="!disabled" class="remove">
<icon icon="times"></icon>
<BaseButton
v-if="!disabled" class="remove"
@click="removeReminderByIndex(index)"
>
<icon icon="times" />
</BaseButton>
</div>
<div class="reminder-input" v-if="!disabled">
<Datepicker
v-model="newReminder"
@close-on-change="() => addReminderDate()"
@update:model-value="addReminderDate"
:choose-date-label="$t('task.addReminder')"
/>
</div>
@ -26,15 +29,13 @@
</template>
<script setup lang="ts">
import {PropType, ref, onMounted, watch} from 'vue'
import {type PropType, ref, toRef, watch} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Datepicker from '@/components/input/datepicker.vue'
type Reminder = Date | string
const props = defineProps({
modelValue: {
type: Array as PropType<Reminder[]>,
@ -67,26 +68,21 @@ const emit = defineEmits(['update:modelValue', 'change'])
const reminders = ref<Reminder[]>([])
onMounted(() => {
reminders.value = [...props.modelValue]
})
watch(
() => props.modelValue,
toRef(props, 'modelValue'),
(newVal) => {
for (const i in newVal) {
if (typeof newVal[i] === 'string') {
newVal[i] = new Date(newVal[i])
}
}
reminders.value = newVal
reminders.value = newVal.map(item => {
return typeof item === 'string'
? new Date(item)
: item
})
},
{ immediate: true },
)
function updateData() {
emit('update:modelValue', reminders.value)
emit('change')
}
const newReminder = ref(null)
@ -98,7 +94,7 @@ function addReminderDate(index : number | null = null) {
}
reminders.value.push(new Date(newReminder.value))
newReminder.value = null
} else if(reminders.value[index] === null) {
} else if (reminders.value[index] === null) {
return
}
@ -112,23 +108,21 @@ function removeReminderByIndex(index: number) {
</script>
<style lang="scss" scoped>
.reminders {
.reminder-input {
display: flex;
align-items: center;
.reminder-input {
display: flex;
align-items: center;
&.overdue :deep(.datepicker .show) {
color: var(--danger);
}
&.overdue :deep(.datepicker .show) {
color: var(--danger);
}
&:last-child {
margin-bottom: 0.75rem;
}
&:last-child {
margin-bottom: 0.75rem;
}
}
.remove {
color: var(--danger);
padding-left: .5rem;
}
}
.remove {
color: var(--danger);
padding-left: .5rem;
}
</style>

View File

@ -1,4 +1,4 @@
export function calculateDayInterval(date, currentDay = (new Date().getDay())) {
export function calculateDayInterval(date, currentDay = (new Date().getDay())): number {
switch (date) {
case 'today':
return 0

View File

@ -6,7 +6,7 @@
* @param dateString
* @returns {Date}
*/
export const createDateFromString = dateString => {
export function createDateFromString(dateString: Date | string) : Date {
if (dateString instanceof Date) {
return dateString
}

View File

@ -54,7 +54,7 @@
<div class="date-input">
<datepicker
v-model="task.dueDate"
@close-on-change="() => saveTask()"
@update:model-value="saveTask"
:choose-date-label="$t('task.detail.chooseDueDate')"
:disabled="taskService.loading || !canWrite"
ref="dueDate"
@ -94,7 +94,7 @@
<div class="date-input">
<datepicker
v-model="task.startDate"
@close-on-change="() => saveTask()"
@update:model-value="saveTask"
:choose-date-label="$t('task.detail.chooseStartDate')"
:disabled="taskService.loading || !canWrite"
ref="startDate"
@ -121,7 +121,7 @@
<div class="date-input">
<datepicker
v-model="task.endDate"
@close-on-change="() => saveTask()"
@update:model-value="saveTask"
:choose-date-label="$t('task.detail.chooseEndDate')"
:disabled="taskService.loading || !canWrite"
ref="endDate"
@ -146,9 +146,10 @@
</div>
<reminders
:disabled="!canWrite"
@change="saveTask"
ref="reminders"
v-model="task.reminderDates"/>
v-model="task.reminderDates"
@update:model-value="saveTask"
/>
</div>
</transition>
<transition name="flash-background" appear>
@ -781,10 +782,10 @@ $flash-background-duration: 750ms;
}
.remove {
color: var(--danger);
vertical-align: middle;
padding-left: .5rem;
line-height: 1;
color: var(--danger);
vertical-align: middle;
padding-left: .5rem;
line-height: 1;
}
:deep(.datepicker) {