2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 11:29:03 +00:00

Merge pull request #502 from frappe/datetime-component

feat: Datetime component
This commit is contained in:
Alan 2022-12-05 03:15:18 -08:00 committed by GitHub
commit 6f8d73c677
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 658 additions and 326 deletions

View File

@ -9,7 +9,7 @@ import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT,
DEFAULT_DISPLAY_PRECISION, DEFAULT_DISPLAY_PRECISION,
DEFAULT_LOCALE DEFAULT_LOCALE,
} from './consts'; } from './consts';
export function format( export function format(
@ -66,6 +66,10 @@ function toDatetime(value: DocValue) {
} }
function formatDatetime(value: DocValue, fyo: Fyo): string { function formatDatetime(value: DocValue, fyo: Fyo): string {
if (value == null) {
return '';
}
const dateFormat = const dateFormat =
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT; (fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;
const formattedDatetime = toDatetime(value).toFormat( const formattedDatetime = toDatetime(value).toFormat(
@ -80,6 +84,10 @@ function formatDatetime(value: DocValue, fyo: Fyo): string {
} }
function formatDate(value: DocValue, fyo: Fyo): string { function formatDate(value: DocValue, fyo: Fyo): string {
if (value == null) {
return '';
}
const dateFormat = const dateFormat =
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT; (fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;

View File

@ -436,7 +436,7 @@ export abstract class Invoice extends Transactional {
return defaults?.purchaseInvoiceTerms ?? ''; return defaults?.purchaseInvoiceTerms ?? '';
}, },
date: () => new Date().toISOString().slice(0, 10), date: () => new Date(),
}; };
static filters: FiltersMap = { static filters: FiltersMap = {

View File

@ -40,7 +40,7 @@ export class JournalEntry extends Transactional {
static defaults: DefaultMap = { static defaults: DefaultMap = {
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo), numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
date: () => DateTime.local().toISODate(), date: () => new Date(),
}; };
static filters: FiltersMap = { static filters: FiltersMap = {

View File

@ -381,7 +381,7 @@ export class Payment extends Transactional {
static defaults: DefaultMap = { static defaults: DefaultMap = {
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo), numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
date: () => new Date().toISOString(), date: () => new Date(),
}; };
async _getAccountsMap(): Promise<AccountTypeMap> { async _getAccountsMap(): Promise<AccountTypeMap> {

View File

@ -43,7 +43,7 @@ export abstract class StockTransfer extends Transfer {
return defaults?.purchaseReceiptTerms ?? ''; return defaults?.purchaseReceiptTerms ?? '';
}, },
date: () => new Date().toISOString().slice(0, 10), date: () => new Date(),
}; };
static filters: FiltersMap = { static filters: FiltersMap = {

View File

@ -1,42 +1,11 @@
<template> <script lang="ts">
<div> import { defineComponent } from 'vue';
<div :class="labelClasses" v-if="showLabel"> import Datetime from './Datetime.vue';
{{ df.label }}
</div>
<DatePicker export default defineComponent({
ref="input" data() {
:show-mandatory="showMandatory" return { selectTime: false };
:input-class="['bg-transparent', inputClasses, containerClasses]"
:value="value"
:placeholder="inputPlaceholder"
:readonly="isReadOnly"
:format-value="formatValue"
@change="(value) => triggerChange(value)"
/>
</div>
</template>
<script>
import { fyo } from 'src/initFyo';
import DatePicker from '../DatePicker/DatePicker';
import Base from './Base';
export default {
name: 'Date',
extends: Base,
components: {
DatePicker,
}, },
computed: { extends: Datetime,
inputType() { });
return 'date';
},
},
methods: {
formatValue(value) {
return fyo.format(value, this.df);
},
},
};
</script> </script>

View File

@ -0,0 +1,95 @@
<template>
<Popover>
<!-- Datetime Selected Display -->
<template #target="{ togglePopover }">
<div :class="labelClasses" v-if="showLabel">
{{ df?.label }}
</div>
<div :class="[containerClasses, sizeClasses]" class="flex">
<p
:class="[baseInputClasses]"
class="overflow-auto no-scrollbar whitespace-nowrap"
v-if="!isEmpty"
>
{{ formattedValue }}
</p>
<p v-else-if="inputPlaceholder" class="text-base text-gray-500 w-full">
{{ inputPlaceholder }}
</p>
<button
v-if="!isReadOnly"
class="p-0.5 rounded -mr-1 ml-1"
:class="showMandatory ? 'bg-red-300' : 'bg-gray-300'"
@click="togglePopover"
>
<FeatherIcon
name="calendar"
class="w-4 h-4"
:class="showMandatory ? 'text-red-600' : 'text-gray-600'"
/>
</button>
</div>
</template>
<!-- Datetime Input Popover -->
<template #content>
<DatetimePicker
:show-clear="!isRequired"
:select-time="selectTime"
:model-value="internalValue"
:format-value="formatValue"
@update:model-value="(value) => triggerChange(value)"
/>
</template>
</Popover>
</template>
<script lang="ts">
import { Field } from 'schemas/types';
import { defineComponent, PropType } from 'vue';
import DatetimePicker from './DatetimePicker.vue';
import FeatherIcon from '../FeatherIcon.vue';
import Popover from '../Popover.vue';
import Base from './Base.vue';
export default defineComponent({
extends: Base,
props: { value: [Date, String], df: Object as PropType<Field> },
components: { Popover, FeatherIcon, DatetimePicker },
data() {
return { selectTime: true };
},
computed: {
internalValue() {
if (this.value == null) {
return undefined;
}
if (typeof this.value === 'string') {
return new Date(this.value);
}
return this.value;
},
formattedValue() {
return this.formatValue(this.internalValue);
},
},
methods: {
triggerChange(value: Date | null) {
this.$emit('change', value);
},
formatValue(value?: Date | null) {
if (value == null) {
return '';
}
return this.fyo.format(
value,
this.df ?? (this.selectTime ? 'Datetime' : 'Date')
);
},
},
});
</script>

View File

@ -0,0 +1,540 @@
<template>
<div>
<!-- Datetime header -->
<div class="flex justify-between items-center text-sm px-4 pt-4">
<div class="text-blue-500">
{{ datetimeString }}
</div>
<!-- Next and Previous Month Buttons -->
<div class="flex items-center">
<button
class="font-mono text-gray-600 cursor-pointer"
@click="prevClicked"
>
<FeatherIcon name="chevron-left" class="w-4 h-4" />
</button>
<button
class="
font-mono
cursor-pointer
w-2
h-2
rounded-full
border-gray-400 border-2
"
@click="selectToday"
/>
<button
class="font-mono text-gray-600 cursor-pointer"
@click="nextClicked"
>
<FeatherIcon name="chevron-right" class="w-4 h-4" />
</button>
</div>
</div>
<!-- Date Input -->
<div class="flex">
<!-- Weekday Titles -->
<div class="px-3 pt-4" :class="selectTime ? 'pb-4' : ''">
<div class="grid grid-cols-7 gap-1">
<div
v-for="day of weekdays"
:key="day"
class="
w-7
h-7
flex
items-center
justify-center
text-xs text-gray-600
"
>
{{ day }}
</div>
</div>
<!-- Weekday Grid -->
<div class="grid grid-cols-7 gap-1">
<div
v-for="item of weekdayList"
:key="`${item.year}-${item.month}-${item.day}`"
class="
w-7
h-7
flex
items-center
justify-center
text-xs
rounded-full
cursor-pointer
hover:bg-gray-100
"
@click="select(item)"
:class="getDayClass(item)"
>
{{ item.day }}
</div>
</div>
</div>
<!-- Month and Year Selectors -->
<div
v-if="selectMonthYear"
class="border-l flex flex-col justify-between"
>
<!-- Month Selector -->
<div
class="flex flex-col gap-2 overflow-auto m-4"
style="height: calc(248px - 79.5px - 1px - 2rem)"
>
<div
v-for="(m, i) of months"
:key="m"
ref="monthDivs"
class="text-xs cursor-pointer"
:class="getMonthClass(i)"
@click="change(i, 'month')"
>
{{ m }}
</div>
</div>
<!-- Year Selector -->
<div
class="border-t w-full px-4 pt-4"
:class="selectTime ? 'pb-4' : ''"
>
<label class="date-selector-label">Year</label>
<input
class="date-selector-input"
type="number"
min="1000"
max="9999"
@change="(e) => change(e, 'year')"
:value="year"
/>
</div>
</div>
</div>
<!-- Time Selector -->
<div
v-if="selectTime"
class="px-4 pt-4 grid gap-4 border-t"
style="grid-template-columns: repeat(3, minmax(0, 1fr))"
>
<div>
<label class="date-selector-label">Hours</label>
<input
class="date-selector-input"
type="number"
min="0"
max="23"
@change="(e) => change(e, 'hours')"
:value="hours"
/>
</div>
<div>
<label class="date-selector-label">Minutes</label>
<input
class="date-selector-input"
type="number"
min="0"
max="59"
@change="(e) => change(e, 'minutes')"
:value="minutes"
/>
</div>
<div>
<label class="date-selector-label">Seconds</label>
<input
class="date-selector-input"
type="number"
min="0"
max="59"
@change="(e) => change(e, 'seconds')"
:value="seconds"
/>
</div>
</div>
<!-- Footer -->
<div class="flex p-4 w-full justify-between">
<button
class="text-xs text-gray-600 hover:text-gray-600"
@click="selectMonthYear = !selectMonthYear"
>
{{ selectMonthYear ? t`Hide Month/Year` : t`Show Month/Year` }}
</button>
<button
v-if="showClear"
class="text-xs text-gray-600 hover:text-gray-600 ml-auto"
@click="clearClicked"
>
{{ t`Clear` }}
</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, nextTick, PropType } from 'vue';
import FeatherIcon from '../FeatherIcon.vue';
type WeekListItem = {
year: number;
month: number;
day: number;
weekday: number;
};
type DatetimeValues = {
year: number;
month: number;
day: number;
hours: number;
minutes: number;
seconds: number;
ms: number;
};
export default defineComponent({
emits: ['update:modelValue'],
props: {
modelValue: { type: Date },
selectTime: { type: Boolean, default: true },
showClear: { type: Boolean, default: true },
formatValue: { type: Function as PropType<(value: Date | null) => string> },
},
mounted() {
this.viewMonth = this.month;
this.viewYear = this.year;
},
data() {
return {
selectedMonth: 0,
selectedYear: 1000,
viewMonth: 0,
viewYear: 1000,
selectMonthYear: false,
} as {
selectedMonth: number;
selectedYear: number;
selectMonthYear: boolean;
viewMonth: number;
viewYear: number;
};
},
watch: {
async selectMonthYear(value) {
if (!value) {
return;
}
await nextTick();
const monthDivs = this.$refs.monthDivs as HTMLDivElement[];
if (!monthDivs?.length) {
return;
}
monthDivs[this.month]?.scrollIntoView({
block: 'center',
inline: 'center',
});
},
},
computed: {
today() {
return new Date();
},
internalValue(): Date {
if (this.modelValue == null) {
return this.today;
}
return this.modelValue;
},
year() {
// 1000 to 9999
return this.internalValue?.getFullYear() ?? 1000;
},
month() {
// 0 to 11
return this.internalValue?.getMonth() ?? 0;
},
day() {
// 1 to 31
return this.internalValue?.getDate() ?? 1;
},
hours() {
// 0 to 23
return this.internalValue?.getHours() ?? 0;
},
minutes() {
// 0 to 59
return this.internalValue?.getMinutes() ?? 0;
},
seconds() {
// 0 to 59
return this.internalValue?.getSeconds() ?? 0;
},
ms() {
// 0 to 999
return this.internalValue?.getMilliseconds() ?? 0;
},
weekdayList() {
return getWeekdayList(this.viewYear, this.viewMonth);
},
datetimeString() {
if (this.formatValue) {
return this.formatValue(this.internalValue);
}
const dateString = this.internalValue
.toDateString()
.split(' ')
.slice(1)
.join(' ');
if (!this.selectTime) {
return dateString;
}
const timeString = this.internalValue?.toTimeString().split(' ')[0] ?? '';
return `${dateString} ${timeString}]`;
},
months() {
return [
this.t`January`,
this.t`February`,
this.t`March`,
this.t`April`,
this.t`May`,
this.t`June`,
this.t`July`,
this.t`August`,
this.t`September`,
this.t`October`,
this.t`November`,
this.t`December`,
];
},
weekdays() {
return [
this.t`Su`,
this.t`Mo`,
this.t`Tu`,
this.t`We`,
this.t`Th`,
this.t`Fr`,
this.t`Sa`,
];
},
},
methods: {
getDayClass(item: WeekListItem) {
let dclass = [];
const today = this.today;
const todayDay = today.getDate();
const todayMonth = today.getMonth();
const isToday = item.day === todayDay && item.month === todayMonth;
const isSelected = item.day === this.day && item.month === this.month;
if (item.month !== this.viewMonth && !isToday) {
dclass.push('text-gray-600');
}
if (isSelected) {
dclass.push('font-semibold');
}
if (isSelected && this.modelValue != null) {
dclass.push('bg-gray-100', 'text-blue-500');
} else if (isToday && !isSelected) {
dclass.push('text-blue-500');
}
return dclass;
},
getMonthClass(item: number) {
let dclass = [];
if (item === this.month) {
dclass.push('font-semibold');
}
if (this.modelValue != null && item === this.month) {
dclass.push('text-blue-500');
}
return dclass;
},
change(e: number | Event, name: keyof DatetimeValues) {
let value: number;
if (typeof e === 'number' && name === 'month') {
value = e;
} else if (typeof e !== 'number') {
value = Number((e.target as HTMLInputElement).value);
} else {
return;
}
if (Number.isNaN(value)) {
return;
}
if (name === 'year' && value >= 1000 && value <= 9999) {
return this.select({ year: value });
}
if (name === 'month' && value >= 0 && value <= 11) {
return this.select({ month: value });
}
if (name === 'day' && value >= 1 && value <= 31) {
return this.select({ day: value });
}
if (name === 'hours' && value >= 0 && value <= 23) {
return this.select({ hours: value });
}
if (name === 'minutes' && value >= 0 && value <= 59) {
return this.select({ minutes: value });
}
if (name === 'seconds' && value >= 0 && value <= 59) {
return this.select({ seconds: value });
}
if (name === 'ms' && value >= 0 && value <= 999) {
return this.select({ ms: value });
}
},
select(values: Partial<DatetimeValues>) {
values.year ??= this.year;
values.month ??= this.month;
values.day ??= this.day;
values.hours ??= this.hours;
values.minutes ??= this.minutes;
values.seconds ??= this.seconds;
values.ms ??= this.ms;
const date = new Date(
values.year,
values.month,
values.day,
values.hours,
values.minutes,
values.seconds,
values.ms
);
this.viewMonth = values.month;
this.viewYear = values.year;
this.emitChange(date);
},
selectToday() {
return this.emitChange(new Date());
},
clearClicked() {
this.emitChange(null);
},
emitChange(value: null | Date) {
if (value == null) {
this.viewMonth = this.today.getMonth();
this.viewYear = this.today.getFullYear();
} else {
this.viewMonth = value.getMonth();
this.viewYear = value.getFullYear();
}
this.$emit('update:modelValue', value);
},
nextClicked() {
const d = new Date(this.viewYear, this.viewMonth + 1, 1);
this.viewYear = d.getFullYear();
this.viewMonth = d.getMonth();
},
prevClicked() {
const d = new Date(this.viewYear, this.viewMonth - 1, 1);
this.viewYear = d.getFullYear();
this.viewMonth = d.getMonth();
},
},
components: { FeatherIcon },
});
function getWeekdayList(startYear: number, startMonth: number): WeekListItem[] {
/**
* Weekday:
*
* S M T W T F S
* 0 1 2 3 4 5 6
*
* 0: Sunday
* 6: Saturday
*/
let year = startYear;
let month = startMonth;
let day = 1;
const weekdayList: WeekListItem[] = [];
/**
* Push days of the current month into weeklist
*/
while (month === startMonth) {
const date = new Date(year, month, day);
if (date.getMonth() !== month) {
break;
}
weekdayList.push({ year, month, day, weekday: date.getDay() });
year = date.getFullYear();
month = date.getMonth();
day += 1;
}
/**
* Unshift days of the previous month into weeklist
* until the first day is Sunday
*/
while (weekdayList[0]?.weekday !== 0) {
const { year, month, day } = weekdayList[0] ?? {};
if (year === undefined || month === undefined || day === undefined) {
break;
}
const date = new Date(year, month, day - 1);
weekdayList.unshift({
year: date.getFullYear(),
month: date.getMonth(),
day: date.getDate(),
weekday: date.getDay(),
});
}
/**
* Push days of the next month into weeklist
* until the last day is Saturday
*/
while (weekdayList.length !== 42) {
const { year, month, day } = weekdayList.at(-1) ?? {};
if (year === undefined || month === undefined || day === undefined) {
break;
}
const date = new Date(year, month, day + 1);
weekdayList.push({
year: date.getFullYear(),
month: date.getMonth(),
day: date.getDate(),
weekday: date.getDay(),
});
}
return weekdayList;
}
</script>
<style scoped>
.date-selector-label {
@apply text-xs text-gray-600 block mb-0.5;
}
.date-selector-input {
@apply text-sm text-gray-900 p-1 border rounded w-full;
}
input[type='number']::-webkit-inner-spin-button {
appearance: auto;
}
</style>

View File

@ -8,6 +8,7 @@ import Color from './Color.vue';
import Currency from './Currency.vue'; import Currency from './Currency.vue';
import Data from './Data.vue'; import Data from './Data.vue';
import Date from './Date.vue'; import Date from './Date.vue';
import Datetime from './Datetime.vue';
import DynamicLink from './DynamicLink.vue'; import DynamicLink from './DynamicLink.vue';
import Float from './Float.vue'; import Float from './Float.vue';
import Int from './Int.vue'; import Int from './Int.vue';
@ -24,7 +25,7 @@ const components = {
Select, Select,
Link, Link,
Date, Date,
Datetime: Date, Datetime,
Table, Table,
AutoComplete, AutoComplete,
DynamicLink, DynamicLink,

View File

@ -1,281 +0,0 @@
<template>
<Popover @open="selectCurrentMonthYear">
<template #target="{ togglePopover, handleBlur }">
<div :class="showMandatory ? 'show-mandatory' : ''">
<input
type="text"
:class="inputClass"
:value="value && formatValue ? formatValue(value) : value || ''"
:placeholder="placeholder"
readonly
@focus="!readonly ? togglePopover() : null"
@blur="handleBlur"
/>
</div>
</template>
<template #content="{ togglePopover }">
<div class="text-left p-3 select-none">
<div class="flex items-center justify-between">
<span class="font-medium text-blue-500 text-base">
{{ formatMonth }}
</span>
<span class="flex">
<div
class="
w-5
h-5
hover:bg-gray-100
rounded-md
flex-center
cursor-pointer
"
>
<feather-icon
@click="prevMonth"
name="chevron-left"
class="w-4 h-4"
/>
</div>
<div
class="
ml-2
w-5
h-5
hover:bg-gray-100
rounded-md
flex-center
cursor-pointer
"
>
<feather-icon
@click="nextMonth"
name="chevron-right"
class="w-4 h-4"
/>
</div>
</span>
</div>
<div class="mt-2 text-sm">
<div class="flex w-full text-gray-600">
<div
class="w-6 h-6 mr-1 last:mr-0 flex-center text-center"
v-for="(d, i) in ['S', 'M', 'T', 'W', 'T', 'F', 'S']"
:key="i"
>
{{ d }}
</div>
</div>
<div
v-for="(week, i) in datesAsWeeks"
:key="`${i}-${Math.random().toString(36)}`"
class="mt-1"
>
<div class="flex w-full">
<div
v-for="date in week"
:key="`${toValue(date)}-${Math.random().toString(36)}`"
class="
w-6
h-6
mr-1
last:mr-0
flex-center
cursor-pointer
rounded-md
hover:bg-blue-100 hover:text-blue-500
"
:class="{
'text-gray-600': date.getMonth() !== currentMonth - 1,
'text-blue-500': toValue(date) === toValue(today),
'bg-blue-100 font-semibold text-blue-500':
toValue(date) === value,
}"
@click="
selectDate(date);
togglePopover();
"
>
{{ date.getDate() }}
</div>
</div>
</div>
</div>
<div class="w-full flex justify-end mt-2">
<div
class="
text-sm
hover:bg-gray-100
px-2
py-1
rounded-md
cursor-pointer
"
@click="
selectDate('');
togglePopover();
"
>
{{ t`Clear` }}
</div>
</div>
</div>
</template>
</Popover>
</template>
<script>
import { DateTime } from 'luxon';
import Popover from '../Popover';
export default {
name: 'DatePicker',
props: [
'value',
'placeholder',
'readonly',
'formatValue',
'inputClass',
'showMandatory',
],
emits: ['change'],
components: {
Popover,
},
data() {
return {
currentYear: null,
currentMonth: null,
};
},
created() {
this.selectCurrentMonthYear();
},
computed: {
today() {
return this.getDate();
},
datesAsWeeks() {
let datesAsWeeks = [];
let dates = this.dates.slice();
while (dates.length) {
let week = dates.splice(0, 7);
datesAsWeeks.push(week);
}
return datesAsWeeks;
},
dates() {
if (!(this.currentYear && this.currentMonth)) {
return [];
}
let monthIndex = this.currentMonth - 1;
let year = this.currentYear;
let firstDayOfMonth = this.getDate(year, monthIndex, 1);
let lastDayOfMonth = this.getDate(year, monthIndex + 1, 0);
let leftPaddingCount = firstDayOfMonth.getDay();
let rightPaddingCount = 6 - lastDayOfMonth.getDay();
let leftPadding = this.getDatesAfter(firstDayOfMonth, -leftPaddingCount);
let rightPadding = this.getDatesAfter(lastDayOfMonth, rightPaddingCount);
let daysInMonth = this.getDaysInMonth(monthIndex, year);
let datesInMonth = this.getDatesAfter(firstDayOfMonth, daysInMonth - 1);
let dates = [
...leftPadding,
firstDayOfMonth,
...datesInMonth,
...rightPadding,
];
if (dates.length < 42) {
const finalPadding = this.getDatesAfter(
dates.at(-1),
42 - dates.length
);
dates = dates.concat(...finalPadding);
}
return dates;
},
formatMonth() {
let date = this.getDate(this.currentYear, this.currentMonth - 1, 1);
return date.toLocaleString('en-US', { month: 'short', year: 'numeric' });
},
},
methods: {
selectDate(date) {
this.$emit('change', this.toValue(date));
},
selectCurrentMonthYear() {
let date = this.value ? this.getDate(this.value) : this.getDate();
this.currentYear = date.getFullYear();
this.currentMonth = date.getMonth() + 1;
},
prevMonth() {
this.changeMonth(-1);
},
nextMonth() {
this.changeMonth(1);
},
changeMonth(adder) {
this.currentMonth = this.currentMonth + adder;
if (this.currentMonth < 1) {
this.currentMonth = 12;
this.currentYear = this.currentYear - 1;
}
if (this.currentMonth > 12) {
this.currentMonth = 1;
this.currentYear = this.currentYear + 1;
}
},
getDatesAfter(date, count) {
let incrementer = 1;
if (count < 0) {
incrementer = -1;
count = Math.abs(count);
}
let dates = [];
while (count) {
date = this.getDate(
date.getFullYear(),
date.getMonth(),
date.getDate() + incrementer
);
dates.push(date);
count--;
}
if (incrementer === -1) {
return dates.reverse();
}
return dates;
},
getDaysInMonth(monthIndex, year) {
let daysInMonthMap = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let daysInMonth = daysInMonthMap[monthIndex];
if (monthIndex === 1 && this.isLeapYear(year)) {
return 29;
}
return daysInMonth;
},
isLeapYear(year) {
if (year % 400 === 0) return true;
if (year % 100 === 0) return false;
if (year % 4 === 0) return true;
return false;
},
toValue(date) {
if (!date) {
return '';
}
return DateTime.fromJSDate(date).toISODate();
},
getDate(...args) {
let d = new Date(...args);
return d;
},
},
};
</script>