WIP: REFERENCE datepicker script setup alternative #1972
|
@ -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,
|
||||
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
|
||||
}
|
||||
},
|
||||
components: {
|
||||
flatPickr,
|
||||
BaseButton,
|
||||
},
|
||||
props: {
|
||||
|
||||
function getWeekdayFromStringInterval(dateInterval: string) {
|
||||
dpschen marked this conversation as resolved
Outdated
|
||||
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,
|
||||
type: String as PropType<string>,
|
||||
default() {
|
||||
return i18n.global.t('input.datepicker.chooseDate')
|
||||
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,
|
||||
})
|
||||
},
|
||||
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'),
|
||||
{ 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: this.$store.state.auth.settings.weekStart,
|
||||
},
|
||||
}
|
||||
firstDayOfWeek: store.state.auth.settings.weekStart,
|
||||
},
|
||||
}))
|
||||
|
||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
Please remove this. Please remove this.
|
||||
// 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()
|
||||
},
|
||||
const flatPickrDate = computed({
|
||||
get() {
|
||||
if (!this.date) {
|
||||
if (!state.date) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return format(this.date, 'yyy-LL-dd H:mm')
|
||||
},
|
||||
},
|
||||
},
|
||||
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')
|
||||
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',
|
||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
I think this doesn't actually work? But that doesn't really work right now either, more of a mental note to properly fix this some time. I think this doesn't actually work? But that doesn't really work right now either, more of a mental note to properly fix this some time.
|
||||
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>
|
||||
|
|
|
@ -36,8 +36,8 @@
|
|||
|
||||
<strong>{{ $t('task.attributes.reminders') }}</strong>
|
||||
<reminders
|
||||
@change="editTaskSubmit()"
|
||||
v-model="taskEditTask.reminderDates"
|
||||
@update:model-value="editTaskSubmit()"
|
||||
/>
|
||||
|
||||
<div class="field">
|
||||
|
|
|
@ -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)
|
||||
|
@ -112,7 +108,6 @@ function removeReminderByIndex(index: number) {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.reminders {
|
||||
.reminder-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -124,11 +119,10 @@ function removeReminderByIndex(index: number) {
|
|||
&:last-child {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: var(--danger);
|
||||
padding-left: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Reference in New Issue
Why don't you add the
useI18n
before the props declaration?That wouldn't work. Because afaik the props, events declaration and imports are extracted from script setup and the rest will be wrapped by the setup function. Meaning that if I would use
use18n
beforedefineProps
it would still be inside the setup block after beeing compiled => no access inside the props.