mirror of
https://github.com/directus/directus.git
synced 2026-02-11 22:14:56 -05:00
Add v-date-picker base component & use it in datetime interface (#10438)
* WIP * clean up emitted values & add locale support * fix styling and add dynamic width * fix logic for getting the last key * lock flatpickr version * add "set to now" button * add locale & fix input flash * fix locale issue * fix initial value not setting * use v-menu & reuse date-fns locales * add max-height-none prop to v-menu * remove unused styles * touch up style * use flatpickr locale constructed from date-fns * minor style tweak * Various style tweaks Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -51,6 +51,7 @@ import VTemplateInput from './v-template-input.vue';
|
||||
import VTextOverflow from './v-text-overflow.vue';
|
||||
import VTextarea from './v-textarea';
|
||||
import VUpload from './v-upload';
|
||||
import VDatePicker from './v-date-picker';
|
||||
|
||||
export function registerComponents(app: App): void {
|
||||
app.component('VAvatar', VAvatar);
|
||||
@@ -108,6 +109,7 @@ export function registerComponents(app: App): void {
|
||||
app.component('VTextarea', VTextarea);
|
||||
app.component('VTextOverflow', VTextOverflow);
|
||||
app.component('VUpload', VUpload);
|
||||
app.component('VDatePicker', VDatePicker);
|
||||
|
||||
app.component('TransitionBounce', TransitionBounce);
|
||||
app.component('TransitionDialog', TransitionDialog);
|
||||
|
||||
319
app/src/components/v-date-picker/flatpickr-overrides.css
Normal file
319
app/src/components/v-date-picker/flatpickr-overrides.css
Normal file
@@ -0,0 +1,319 @@
|
||||
.flatpickr-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flatpickr-calendar {
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
font-family: var(--v-input-font-family);
|
||||
background: var(--card-face-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.flatpickr-calendar.inline {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.flatpickr-calendar.animate.open {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.flatpickr-calendar .flatpickr-calendar.arrowTop::after {
|
||||
border-bottom-color: var(--background-normal);
|
||||
}
|
||||
|
||||
.flatpickr-calendar.arrowBottom::after {
|
||||
border-top-color: var(--background-normal);
|
||||
}
|
||||
|
||||
.flatpickr-months .flatpickr-month {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--foreground-normal-alt);
|
||||
background: var(--background-normal);
|
||||
fill: none;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.flatpickr-months .flatpickr-prev-month,
|
||||
.flatpickr-months .flatpickr-next-month {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.flatpickr-months .flatpickr-prev-month svg,
|
||||
.flatpickr-months .flatpickr-next-month svg {
|
||||
width: auto;
|
||||
height: auto;
|
||||
fill: var(--foreground-normal-alt);
|
||||
}
|
||||
|
||||
.flatpickr-months .flatpickr-prev-month:hover svg,
|
||||
.flatpickr-months .flatpickr-next-month:hover svg {
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
.flatpickr-current-month {
|
||||
left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
font-weight: inherit;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months {
|
||||
border-radius: var(--border-radius);
|
||||
appearance: none;
|
||||
text-align: right;
|
||||
font-size: 1rem;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months:hover {
|
||||
background-color: var(--background-normal-alt);
|
||||
}
|
||||
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month {
|
||||
background-color: var(--background-normal);
|
||||
}
|
||||
|
||||
.flatpickr-current-month .numInputWrapper {
|
||||
background: var(--background-normal);
|
||||
border-radius: var(--border-radius);
|
||||
transition: background var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
.flatpickr-current-month .numInputWrapper input {
|
||||
font-size: 1rem;
|
||||
height: 20px;
|
||||
vertical-align: 4px;
|
||||
}
|
||||
|
||||
.flatpickr-current-month .numInputWrapper:hover {
|
||||
background: var(--background-normal-alt);
|
||||
}
|
||||
|
||||
.flatpickr-current-month .numInputWrapper {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.flatpickr-current-month .numInputWrapper span.arrowUp {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flatpickr-current-month .numInputWrapper span.arrowDown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flatpickr-weekdays {
|
||||
padding: 10px 4px;
|
||||
background: var(--background-normal);
|
||||
}
|
||||
|
||||
.flatpickr-innerContainer,
|
||||
.flatpickr-innerContainer .flatpickr-rContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.flatpickr-days {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flatpickr-days .dayContainer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
width: auto;
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
span.flatpickr-weekday {
|
||||
color: var(--foreground-normal);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.flatpickr-day {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
color: var(--foreground-normal-alt);
|
||||
line-height: 36px;
|
||||
transition: var(--fast) var(--transition);
|
||||
transition-property: background, border-color, color;
|
||||
}
|
||||
|
||||
.flatpickr-day.inRange,
|
||||
.flatpickr-day.prevMonthDay.inRange,
|
||||
.flatpickr-day.nextMonthDay.inRange,
|
||||
.flatpickr-day.today.inRange,
|
||||
.flatpickr-day.prevMonthDay.today.inRange,
|
||||
.flatpickr-day.nextMonthDay.today.inRange,
|
||||
.flatpickr-day:hover,
|
||||
.flatpickr-day.prevMonthDay:hover,
|
||||
.flatpickr-day.nextMonthDay:hover,
|
||||
.flatpickr-day:focus,
|
||||
.flatpickr-day.prevMonthDay:focus,
|
||||
.flatpickr-day.nextMonthDay:focus {
|
||||
color: var(--foreground-normal);
|
||||
background: var(--background-highlight);
|
||||
border-color: var(--background-highlight);
|
||||
}
|
||||
|
||||
.flatpickr-day.today {
|
||||
border-color: var(--primary);
|
||||
border-width: var(--border-width);
|
||||
}
|
||||
|
||||
.flatpickr-day.today:hover,
|
||||
.flatpickr-day.today:focus {
|
||||
color: var(--foreground-normal);
|
||||
background: var(--background-normal-alt);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.flatpickr-day.selected,
|
||||
.flatpickr-day.startRange,
|
||||
.flatpickr-day.endRange,
|
||||
.flatpickr-day.selected.inRange,
|
||||
.flatpickr-day.startRange.inRange,
|
||||
.flatpickr-day.endRange.inRange,
|
||||
.flatpickr-day.selected:focus,
|
||||
.flatpickr-day.startRange:focus,
|
||||
.flatpickr-day.endRange:focus,
|
||||
.flatpickr-day.selected:hover,
|
||||
.flatpickr-day.startRange:hover,
|
||||
.flatpickr-day.endRange:hover,
|
||||
.flatpickr-day.selected.prevMonthDay,
|
||||
.flatpickr-day.startRange.prevMonthDay,
|
||||
.flatpickr-day.endRange.prevMonthDay,
|
||||
.flatpickr-day.selected.nextMonthDay,
|
||||
.flatpickr-day.startRange.nextMonthDay,
|
||||
.flatpickr-day.endRange.nextMonthDay {
|
||||
color: var(--primary-alt);
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n + 1)),
|
||||
.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n + 1)),
|
||||
.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n + 1)) {
|
||||
box-shadow: -10px 0 0 var(--primary);
|
||||
}
|
||||
|
||||
.flatpickr-day.inRange {
|
||||
border-radius: 0;
|
||||
box-shadow: -5px 0 0 var(--foreground-normal-alt), 5px 0 0 var(--foreground-normal-alt);
|
||||
}
|
||||
|
||||
.flatpickr-day.flatpickr-disabled,
|
||||
.flatpickr-day.flatpickr-disabled:hover,
|
||||
.flatpickr-day.prevMonthDay,
|
||||
.flatpickr-day.nextMonthDay,
|
||||
.flatpickr-day.notAllowed,
|
||||
.flatpickr-day.notAllowed.prevMonthDay,
|
||||
.flatpickr-day.notAllowed.nextMonthDay {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
.flatpickr-day.week.selected {
|
||||
border-radius: 0;
|
||||
box-shadow: -5px 0 0 var(--primary), 5px 0 0 var(--primary);
|
||||
}
|
||||
|
||||
.flatpickr-weekwrapper span.flatpickr-day,
|
||||
.flatpickr-weekwrapper span.flatpickr-day:hover {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
|
||||
/* Time */
|
||||
|
||||
.flatpickr-time {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.flatpickr-time > * {
|
||||
flex-grow: 0 !important;
|
||||
width: max-content !important;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.flatpickr-time .numInputWrapper span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.flatpickr-time .numInputWrapper span.arrowUp {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flatpickr-time .numInputWrapper span.arrowDown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flatpickr-time input {
|
||||
color: var(--v-input-color);
|
||||
background: var(--v-input-background-color);
|
||||
transition: var(--fast) var(--transition);
|
||||
transition-property: color, background;
|
||||
}
|
||||
|
||||
.flatpickr-time input.numInput {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.flatpickr-calendar.hasTime .flatpickr-time {
|
||||
height: 43px;
|
||||
margin-top: 8px;
|
||||
border-top: var(--border-width) solid var(--border-subdued);
|
||||
}
|
||||
|
||||
.flatpickr-calendar.noCalendar .flatpickr-time {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.flatpickr-time .flatpickr-time-separator {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.flatpickr-time .flatpickr-time-separator,
|
||||
.flatpickr-time .flatpickr-am-pm {
|
||||
color: var(--foreground-normal-alt);
|
||||
}
|
||||
|
||||
.flatpickr-time input:hover,
|
||||
.flatpickr-time .flatpickr-am-pm:hover {
|
||||
background-color: var(--background-normal);
|
||||
}
|
||||
|
||||
.flatpickr-time input:focus,
|
||||
.flatpickr-time .flatpickr-am-pm:focus {
|
||||
background-color: var(--background-input);
|
||||
}
|
||||
|
||||
.flatpickr-time input::selection {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.flatpickr-calendar .set-to-now-button {
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
color: var(--primary);
|
||||
border-top: var(--border-width) solid var(--border-subdued);
|
||||
transition: background-color var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
.flatpickr-calendar .set-to-now-button:hover {
|
||||
background-color: var(--background-highlight);
|
||||
}
|
||||
4
app/src/components/v-date-picker/index.ts
Normal file
4
app/src/components/v-date-picker/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import VDatePicker from './v-date-picker.vue';
|
||||
|
||||
export { VDatePicker };
|
||||
export default VDatePicker;
|
||||
138
app/src/components/v-date-picker/v-date-picker.vue
Normal file
138
app/src/components/v-date-picker/v-date-picker.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="v-date-picker">
|
||||
<input class="input" type="text" :placeholder="t('enter_a_value')" data-input />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref, onMounted, onBeforeUnmount, computed, PropType, watch } from 'vue';
|
||||
import Flatpickr from 'flatpickr';
|
||||
import { format, formatISO } from 'date-fns';
|
||||
import { getFlatpickrLocale } from '@/utils/get-flatpickr-locale';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'timestamp' | 'dateTime' | 'time' | 'date'>,
|
||||
required: true,
|
||||
validator: (val: string) => ['dateTime', 'date', 'time', 'timestamp'].includes(val),
|
||||
},
|
||||
includeSeconds: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
use24: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const wrapper = ref<HTMLElement | null>(null);
|
||||
let flatpickr: Flatpickr.Instance | null;
|
||||
|
||||
onMounted(async () => {
|
||||
if (wrapper.value) {
|
||||
const flatpickrLocale = await getFlatpickrLocale();
|
||||
flatpickr = Flatpickr(wrapper.value, { ...flatpickrOptions.value, locale: flatpickrLocale });
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
if (props.modelValue) {
|
||||
flatpickr?.setDate(props.modelValue, false);
|
||||
} else {
|
||||
flatpickr?.clear();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (flatpickr) {
|
||||
flatpickr.destroy();
|
||||
flatpickr = null;
|
||||
}
|
||||
});
|
||||
|
||||
const defaultOptions = {
|
||||
static: true,
|
||||
inline: true,
|
||||
nextArrow:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6-6-6z"/></svg>',
|
||||
prevArrow:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12l4.58-4.59z"/></svg>',
|
||||
wrap: true,
|
||||
|
||||
onChange(selectedDates: Date[], _dateStr: string, _instance: Flatpickr.Instance) {
|
||||
const selectedDate = selectedDates.length > 0 ? selectedDates[0] : null;
|
||||
emitValue(selectedDate);
|
||||
},
|
||||
onReady(_selectedDates: Date[], _dateStr: string, instance: Flatpickr.Instance) {
|
||||
const setToNowButton: HTMLElement = document.createElement('button');
|
||||
setToNowButton.innerHTML = t('interfaces.datetime.set_to_now');
|
||||
setToNowButton.classList.add('set-to-now-button');
|
||||
setToNowButton.tabIndex = -1;
|
||||
setToNowButton.addEventListener('click', setToNow);
|
||||
instance.calendarContainer.appendChild(setToNowButton);
|
||||
},
|
||||
};
|
||||
|
||||
const flatpickrOptions = computed<Record<string, any>>(() => {
|
||||
return Object.assign({}, defaultOptions, {
|
||||
enableSeconds: props.includeSeconds,
|
||||
enableTime: ['dateTime', 'time', 'timestamp'].includes(props.type),
|
||||
noCalendar: props.type === 'time',
|
||||
time_24hr: props.use24,
|
||||
});
|
||||
});
|
||||
|
||||
function emitValue(value: Date | null) {
|
||||
if (!value) return emit('update:modelValue', null);
|
||||
|
||||
switch (props.type) {
|
||||
case 'dateTime':
|
||||
return emit('update:modelValue', format(value, "yyyy-MM-dd'T'HH:mm:ss"));
|
||||
case 'date':
|
||||
return emit('update:modelValue', format(value, 'yyyy-MM-dd'));
|
||||
case 'time':
|
||||
return emit('update:modelValue', format(value, 'HH:mm:ss'));
|
||||
case 'timestamp':
|
||||
return emit('update:modelValue', formatISO(value));
|
||||
}
|
||||
}
|
||||
|
||||
function setToNow() {
|
||||
flatpickr?.setDate(new Date(), true);
|
||||
}
|
||||
|
||||
return { t, wrapper };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import 'flatpickr/dist/flatpickr.css';
|
||||
@import './flatpickr-overrides.css';
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-date-picker {
|
||||
.input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -38,6 +38,7 @@
|
||||
<div class="arrow" :class="{ active: showArrow && isActive }" :style="arrowStyles" data-popper-arrow />
|
||||
<div
|
||||
class="v-menu-content"
|
||||
:class="{ 'full-height': fullHeight, seamless }"
|
||||
@click.stop="onContentClick"
|
||||
@pointerenter.stop="onPointerEnter"
|
||||
@pointerleave.stop="onPointerLeave"
|
||||
@@ -111,6 +112,14 @@ export default defineComponent({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
fullHeight: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
seamless: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
@@ -401,6 +410,14 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.v-menu-content.full-height {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.v-menu-content.seamless {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[data-placement='top'] > .v-menu-content {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
@@ -1,65 +1,29 @@
|
||||
<template>
|
||||
<v-menu :close-on-content-click="false" attached :disabled="disabled">
|
||||
<v-menu :close-on-content-click="false" attached :disabled="disabled" full-height seamless>
|
||||
<template #activator="{ toggle, active }">
|
||||
<v-input
|
||||
:active="active"
|
||||
clickable
|
||||
readonly
|
||||
:model-value="displayValue"
|
||||
:disabled="disabled"
|
||||
:placeholder="t('enter_a_value')"
|
||||
@click="toggle"
|
||||
>
|
||||
<v-input :active="active" clickable readonly :model-value="displayValue" :disabled="disabled" @click="toggle">
|
||||
<template v-if="!disabled" #append>
|
||||
<v-icon :name="value ? 'close' : 'today'" :class="{ active }" @click.stop="unsetValue" />
|
||||
</template>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<div v-if="type === 'timestamp' || type === 'dateTime' || type === 'date'" class="date-selects">
|
||||
<div class="month">
|
||||
<v-select v-model="month" :placeholder="t('month')" :items="monthItems" />
|
||||
</div>
|
||||
<div class="date">
|
||||
<v-select v-model="date" :placeholder="t('date')" :items="dateItems" />
|
||||
</div>
|
||||
<div class="year">
|
||||
<v-select v-model="year" :placeholder="t('year')" :items="yearItems" allow-other />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider v-if="type === 'timestamp' || type === 'dateTime'" />
|
||||
|
||||
<div
|
||||
v-if="type === 'timestamp' || type === 'dateTime' || type === 'time'"
|
||||
class="time-selects"
|
||||
:class="{ seconds: includeSeconds, 'use-24': use24 }"
|
||||
>
|
||||
<div class="hour">
|
||||
<v-select v-model="hours" :placeholder="t('hours')" :items="hourItems" />
|
||||
</div>
|
||||
<div class="minutes">
|
||||
<v-select v-model="minutes" :placeholder="t('minutes')" :items="minutesSecondItems" />
|
||||
</div>
|
||||
<div v-if="includeSeconds" class="seconds">
|
||||
<v-select v-model="seconds" :items="minutesSecondItems" />
|
||||
</div>
|
||||
<div v-if="use24 === false" class="period">
|
||||
<v-select v-model="period" :items="['am', 'pm']" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<button class="to-now" @click="setToNow">{{ t('interfaces.datetime.set_to_now') }}</button>
|
||||
<v-date-picker
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
:include-seconds="includeSeconds"
|
||||
:use-24="use24"
|
||||
:model-value="value"
|
||||
@update:model-value="$emit('input', $event)"
|
||||
/>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref, watch, computed, PropType } from 'vue';
|
||||
import { defineComponent, PropType, ref, watch } from 'vue';
|
||||
import formatLocalized from '@/utils/localized-format';
|
||||
import { format, formatISO, parse, parseISO, setSeconds } from 'date-fns';
|
||||
import { parse, parseISO } from 'date-fns';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -89,319 +53,52 @@ export default defineComponent({
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const { internalValue, year, month, date, hours, minutes, seconds, period } = useLocalValue();
|
||||
const { yearItems, monthItems, dateItems, hourItems, minutesSecondItems } = useOptions();
|
||||
const { displayValue } = useDisplayValue();
|
||||
|
||||
return {
|
||||
t,
|
||||
year,
|
||||
month,
|
||||
date,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
period,
|
||||
setToNow,
|
||||
yearItems,
|
||||
monthItems,
|
||||
dateItems,
|
||||
hourItems,
|
||||
minutesSecondItems,
|
||||
displayValue,
|
||||
unsetValue,
|
||||
};
|
||||
function useDisplayValue() {
|
||||
const displayValue = ref<string | null>(null);
|
||||
|
||||
watch(() => props.value, setDisplayValue, { immediate: true });
|
||||
|
||||
return { displayValue };
|
||||
|
||||
async function setDisplayValue() {
|
||||
if (!props.value) {
|
||||
displayValue.value = null;
|
||||
return;
|
||||
}
|
||||
const timeFormat = props.includeSeconds ? 'date-fns_time' : 'date-fns_time_no_seconds';
|
||||
let format = `${t('date-fns_date')} ${t(timeFormat)}`;
|
||||
if (props.type === 'date') format = String(t('date-fns_date'));
|
||||
if (props.type === 'time') format = String(t(timeFormat));
|
||||
|
||||
displayValue.value = await formatLocalized(parseValue(props.value), format);
|
||||
}
|
||||
|
||||
function parseValue(value: string): Date {
|
||||
switch (props.type) {
|
||||
case 'dateTime':
|
||||
return parse(value, "yyyy-MM-dd'T'HH:mm:ss", new Date());
|
||||
case 'date':
|
||||
return parse(value, 'yyyy-MM-dd', new Date());
|
||||
case 'time':
|
||||
return parse(value, 'HH:mm:ss', new Date());
|
||||
case 'timestamp':
|
||||
return parseISO(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unsetValue() {
|
||||
emit('input', null);
|
||||
}
|
||||
|
||||
function useLocalValue() {
|
||||
const internalValue = computed({
|
||||
get() {
|
||||
if (!props.value) return null;
|
||||
|
||||
if (props.type === 'timestamp') {
|
||||
return parseISO(props.value);
|
||||
} else if (props.type === 'dateTime') {
|
||||
return parse(props.value, "yyyy-MM-dd'T'HH:mm:ss", new Date());
|
||||
} else if (props.type === 'date') {
|
||||
return parse(props.value, 'yyyy-MM-dd', new Date());
|
||||
} else if (props.type === 'time') {
|
||||
return parse(props.value, 'HH:mm:ss', new Date());
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
set(newValue: Date | null) {
|
||||
if (newValue === null) return emit('input', null);
|
||||
|
||||
if (props.type === 'timestamp') {
|
||||
emit('input', formatISO(newValue));
|
||||
} else if (props.type === 'dateTime') {
|
||||
emit('input', format(newValue, "yyyy-MM-dd'T'HH:mm:ss"));
|
||||
} else if (props.type === 'date') {
|
||||
emit('input', format(newValue, 'yyyy-MM-dd'));
|
||||
} else if (props.type === 'time') {
|
||||
emit('input', format(newValue, 'HH:mm:ss'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const year = computed({
|
||||
get() {
|
||||
if (!internalValue.value) return null;
|
||||
return internalValue.value.getFullYear();
|
||||
},
|
||||
set(newYear: number | null) {
|
||||
const newValue = internalValue.value ? new Date(internalValue.value) : new Date(0);
|
||||
newValue.setFullYear(newYear || 0);
|
||||
internalValue.value = newValue;
|
||||
},
|
||||
});
|
||||
|
||||
const month = computed({
|
||||
get() {
|
||||
if (!internalValue.value) return null;
|
||||
return internalValue.value.getMonth();
|
||||
},
|
||||
set(newMonth: number | null) {
|
||||
const newValue = internalValue.value ? new Date(internalValue.value) : new Date();
|
||||
newValue.setMonth(newMonth || 0);
|
||||
internalValue.value = newValue;
|
||||
},
|
||||
});
|
||||
|
||||
const date = computed({
|
||||
get() {
|
||||
if (!internalValue.value) return null;
|
||||
return internalValue.value.getDate();
|
||||
},
|
||||
set(newDate: number | null) {
|
||||
const newValue = internalValue.value ? new Date(internalValue.value) : new Date();
|
||||
newValue.setDate(newDate || 1);
|
||||
internalValue.value = newValue;
|
||||
},
|
||||
});
|
||||
|
||||
const hours = computed({
|
||||
get() {
|
||||
if (!internalValue.value) return null;
|
||||
const hours = internalValue.value.getHours();
|
||||
|
||||
if (props.use24 === false) {
|
||||
return hours % 12;
|
||||
}
|
||||
|
||||
return hours;
|
||||
},
|
||||
set(newHours: number | null) {
|
||||
const newValue = internalValue.value ? new Date(internalValue.value) : new Date();
|
||||
newValue.setHours(newHours || 0);
|
||||
internalValue.value = newValue;
|
||||
},
|
||||
});
|
||||
|
||||
const minutes = computed({
|
||||
get() {
|
||||
if (!internalValue.value) return null;
|
||||
return internalValue.value.getMinutes();
|
||||
},
|
||||
set(newMinutes: number | null) {
|
||||
const newValue = internalValue.value ? new Date(internalValue.value) : new Date();
|
||||
newValue.setMinutes(newMinutes || 0);
|
||||
internalValue.value = newValue;
|
||||
},
|
||||
});
|
||||
|
||||
const seconds = computed({
|
||||
get() {
|
||||
if (!internalValue.value) return null;
|
||||
return internalValue.value.getSeconds();
|
||||
},
|
||||
set(newSeconds: number | null) {
|
||||
const newValue = internalValue.value ? new Date(internalValue.value) : new Date();
|
||||
newValue.setSeconds(newSeconds || 0);
|
||||
internalValue.value = newValue;
|
||||
},
|
||||
});
|
||||
|
||||
const period = computed({
|
||||
get() {
|
||||
if (!internalValue.value) return null;
|
||||
return internalValue.value.getHours() >= 12 ? 'pm' : 'am';
|
||||
},
|
||||
set(newAMPM: 'am' | 'pm' | null) {
|
||||
const newValue = internalValue.value ? new Date(internalValue.value) : new Date();
|
||||
const current = newValue.getHours() >= 12 ? 'pm' : 'am';
|
||||
|
||||
if (current !== newAMPM) {
|
||||
if (newAMPM === 'am') {
|
||||
newValue.setHours(newValue.getHours() - 12);
|
||||
} else {
|
||||
newValue.setHours(newValue.getHours() + 12);
|
||||
}
|
||||
}
|
||||
|
||||
internalValue.value = newValue;
|
||||
},
|
||||
});
|
||||
|
||||
return { internalValue, year, month, date, hours, minutes, seconds, period };
|
||||
}
|
||||
|
||||
function setToNow() {
|
||||
internalValue.value = props.includeSeconds ? new Date() : setSeconds(new Date(), 0);
|
||||
}
|
||||
|
||||
function useDisplayValue() {
|
||||
const displayValue = ref<string | null>(null);
|
||||
|
||||
watch(internalValue, setDisplayValue, { immediate: true });
|
||||
|
||||
return { displayValue };
|
||||
|
||||
async function setDisplayValue() {
|
||||
if (!props.value || !internalValue.value) {
|
||||
displayValue.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const timeFormat = props.includeSeconds ? 'date-fns_time' : 'date-fns_time_no_seconds';
|
||||
|
||||
let format = `${t('date-fns_date')} ${t(timeFormat)}`;
|
||||
|
||||
if (props.type === 'date') format = String(t('date-fns_date'));
|
||||
if (props.type === 'time') format = String(t(timeFormat));
|
||||
|
||||
displayValue.value = await formatLocalized(internalValue.value, format);
|
||||
}
|
||||
}
|
||||
|
||||
function useOptions() {
|
||||
const yearItems = computed(() => {
|
||||
const current = internalValue.value?.getFullYear() || new Date().getFullYear();
|
||||
const years = [];
|
||||
|
||||
for (let i = current - 5; i <= current + 5; i++) {
|
||||
years.push({
|
||||
text: String(i),
|
||||
value: i,
|
||||
});
|
||||
}
|
||||
|
||||
return years;
|
||||
});
|
||||
|
||||
const monthItems = computed(() =>
|
||||
[
|
||||
t('months.january'),
|
||||
t('months.february'),
|
||||
t('months.march'),
|
||||
t('months.april'),
|
||||
t('months.may'),
|
||||
t('months.june'),
|
||||
t('months.july'),
|
||||
t('months.august'),
|
||||
t('months.september'),
|
||||
t('months.october'),
|
||||
t('months.november'),
|
||||
t('months.december'),
|
||||
].map((text, index) => ({
|
||||
text: text,
|
||||
value: index,
|
||||
}))
|
||||
);
|
||||
|
||||
const dateItems = computed(() => {
|
||||
const dates = [];
|
||||
|
||||
for (let i = 1; i <= 31; i++) {
|
||||
dates.push(`${i}`);
|
||||
}
|
||||
|
||||
return dates;
|
||||
});
|
||||
|
||||
const hourItems = computed(() => {
|
||||
const hours = [];
|
||||
|
||||
const hoursInADay = props.use24 ? 24 : 12;
|
||||
|
||||
for (let i = 0; i < hoursInADay; i++) {
|
||||
let hour = String(i);
|
||||
if (hour.length === 1) hour = '0' + hour;
|
||||
|
||||
hours.push({
|
||||
text: hour,
|
||||
value: i,
|
||||
});
|
||||
}
|
||||
|
||||
return hours;
|
||||
});
|
||||
|
||||
const minutesSecondItems = computed(() => {
|
||||
const values = [];
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
let val = String(i);
|
||||
if (val.length === 1) val = '0' + val;
|
||||
values.push({
|
||||
text: val,
|
||||
value: i,
|
||||
});
|
||||
}
|
||||
|
||||
return values;
|
||||
});
|
||||
|
||||
return { yearItems, monthItems, dateItems, hourItems, minutesSecondItems };
|
||||
}
|
||||
return { displayValue, unsetValue };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.date-selects,
|
||||
.time-selects {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
width: 100%;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.date-selects {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.time-selects {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
&.seconds {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
&.use-24 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
&.seconds {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.month {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
.to-now {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
color: var(--primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.v-icon.active {
|
||||
--v-icon-color: var(--primary);
|
||||
}
|
||||
|
||||
36
app/src/utils/get-flatpickr-locale.ts
Normal file
36
app/src/utils/get-flatpickr-locale.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import localizedFormat from '@/utils/localized-format';
|
||||
import { set, add, startOfWeek } from 'date-fns';
|
||||
|
||||
// Flatpickr locale object reference: https://github.com/flatpickr/flatpickr/blob/master/src/l10n/default.ts
|
||||
export async function getFlatpickrLocale() {
|
||||
const now = new Date();
|
||||
const firstDayOfWeekForDate = startOfWeek(now);
|
||||
const weekdaysShorthand = await Promise.all(
|
||||
[...Array(7).keys()].map((_, i) => localizedFormat(add(firstDayOfWeekForDate, { days: i }), 'E'))
|
||||
);
|
||||
const weekdaysLonghand = await Promise.all(
|
||||
[...Array(7).keys()].map((_, i) => localizedFormat(add(firstDayOfWeekForDate, { days: i }), 'EEEE'))
|
||||
);
|
||||
const monthsShorthand = await Promise.all(
|
||||
[...Array(12).keys()].map((_, i) => localizedFormat(set(now, { month: i }), 'LLL'))
|
||||
);
|
||||
const monthsLonghand = await Promise.all(
|
||||
[...Array(12).keys()].map((_, i) => localizedFormat(set(now, { month: i }), 'LLLL'))
|
||||
);
|
||||
const amPM = await Promise.all([
|
||||
localizedFormat(set(now, { hours: 0, minutes: 0, seconds: 0 }), 'a'),
|
||||
localizedFormat(set(now, { hours: 23, minutes: 59, seconds: 59 }), 'a'),
|
||||
]);
|
||||
|
||||
return {
|
||||
weekdays: {
|
||||
longhand: weekdaysLonghand,
|
||||
shorthand: weekdaysShorthand,
|
||||
},
|
||||
months: {
|
||||
longhand: monthsLonghand,
|
||||
shorthand: monthsShorthand,
|
||||
},
|
||||
amPM,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user