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_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;
|
||||||
|
|
||||||
|
@ -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 = {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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> {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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>
|
||||||
|
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 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,
|
||||||
|
@ -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