mirror of
https://github.com/frappe/books.git
synced 2024-12-23 03:19:01 +00:00
Merge pull request #502 from frappe/datetime-component
feat: Datetime component
This commit is contained in:
commit
6f8d73c677
@ -9,7 +9,7 @@ import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
DEFAULT_DISPLAY_PRECISION,
|
||||
DEFAULT_LOCALE
|
||||
DEFAULT_LOCALE,
|
||||
} from './consts';
|
||||
|
||||
export function format(
|
||||
@ -66,6 +66,10 @@ function toDatetime(value: DocValue) {
|
||||
}
|
||||
|
||||
function formatDatetime(value: DocValue, fyo: Fyo): string {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dateFormat =
|
||||
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;
|
||||
const formattedDatetime = toDatetime(value).toFormat(
|
||||
@ -80,6 +84,10 @@ function formatDatetime(value: DocValue, fyo: Fyo): string {
|
||||
}
|
||||
|
||||
function formatDate(value: DocValue, fyo: Fyo): string {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dateFormat =
|
||||
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;
|
||||
|
||||
|
@ -436,7 +436,7 @@ export abstract class Invoice extends Transactional {
|
||||
|
||||
return defaults?.purchaseInvoiceTerms ?? '';
|
||||
},
|
||||
date: () => new Date().toISOString().slice(0, 10),
|
||||
date: () => new Date(),
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
|
@ -40,7 +40,7 @@ export class JournalEntry extends Transactional {
|
||||
|
||||
static defaults: DefaultMap = {
|
||||
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
|
||||
date: () => DateTime.local().toISODate(),
|
||||
date: () => new Date(),
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
|
@ -381,7 +381,7 @@ export class Payment extends Transactional {
|
||||
|
||||
static defaults: DefaultMap = {
|
||||
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
|
||||
date: () => new Date().toISOString(),
|
||||
date: () => new Date(),
|
||||
};
|
||||
|
||||
async _getAccountsMap(): Promise<AccountTypeMap> {
|
||||
|
@ -43,7 +43,7 @@ export abstract class StockTransfer extends Transfer {
|
||||
|
||||
return defaults?.purchaseReceiptTerms ?? '';
|
||||
},
|
||||
date: () => new Date().toISOString().slice(0, 10),
|
||||
date: () => new Date(),
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
|
@ -1,42 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="labelClasses" v-if="showLabel">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import Datetime from './Datetime.vue';
|
||||
|
||||
<DatePicker
|
||||
ref="input"
|
||||
:show-mandatory="showMandatory"
|
||||
: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,
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return { selectTime: false };
|
||||
},
|
||||
computed: {
|
||||
inputType() {
|
||||
return 'date';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatValue(value) {
|
||||
return fyo.format(value, this.df);
|
||||
},
|
||||
},
|
||||
};
|
||||
extends: Datetime,
|
||||
});
|
||||
</script>
|
||||
|
95
src/components/Controls/Datetime.vue
Normal file
95
src/components/Controls/Datetime.vue
Normal 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>
|
540
src/components/Controls/DatetimePicker.vue
Normal file
540
src/components/Controls/DatetimePicker.vue
Normal 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>
|
@ -8,6 +8,7 @@ import Color from './Color.vue';
|
||||
import Currency from './Currency.vue';
|
||||
import Data from './Data.vue';
|
||||
import Date from './Date.vue';
|
||||
import Datetime from './Datetime.vue';
|
||||
import DynamicLink from './DynamicLink.vue';
|
||||
import Float from './Float.vue';
|
||||
import Int from './Int.vue';
|
||||
@ -24,7 +25,7 @@ const components = {
|
||||
Select,
|
||||
Link,
|
||||
Date,
|
||||
Datetime: Date,
|
||||
Datetime,
|
||||
Table,
|
||||
AutoComplete,
|
||||
DynamicLink,
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user