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:
Azri Kahar
2021-12-24 10:43:25 +08:00
committed by GitHub
parent ee6cabd812
commit 9603fbcd3a
9 changed files with 578 additions and 350 deletions

View File

@@ -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);

View 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);
}

View File

@@ -0,0 +1,4 @@
import VDatePicker from './v-date-picker.vue';
export { VDatePicker };
export default VDatePicker;

View 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>

View File

@@ -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;
}

View File

@@ -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);
}

View 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,
};
}