237 lines
7.6 KiB
Vue
237 lines
7.6 KiB
Vue
<script setup>
|
|
import { getLocalTimeZone, today } from "@internationalized/date";
|
|
import { createReusableTemplate, reactiveOmit, useVModel } from "@vueuse/core";
|
|
import { CalendarRoot, useDateFormatter, useForwardPropsEmits } from "reka-ui";
|
|
import { createYear, createYearRange, toDate } from "reka-ui/date";
|
|
import { computed, toRaw } from "vue";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
NativeSelect,
|
|
NativeSelectOption,
|
|
} from '@/components/ui/native-select';
|
|
import {
|
|
CalendarCell,
|
|
CalendarCellTrigger,
|
|
CalendarGrid,
|
|
CalendarGridBody,
|
|
CalendarGridHead,
|
|
CalendarGridRow,
|
|
CalendarHeadCell,
|
|
CalendarHeader,
|
|
CalendarHeading,
|
|
CalendarNextButton,
|
|
CalendarPrevButton,
|
|
} from ".";
|
|
|
|
const props = defineProps({
|
|
defaultValue: { type: null, required: false },
|
|
defaultPlaceholder: { type: null, required: false },
|
|
placeholder: { type: null, required: false },
|
|
pagedNavigation: { type: Boolean, required: false },
|
|
preventDeselect: { type: Boolean, required: false },
|
|
weekStartsOn: { type: Number, required: false },
|
|
weekdayFormat: { type: String, required: false },
|
|
calendarLabel: { type: String, required: false },
|
|
fixedWeeks: { type: Boolean, required: false },
|
|
maxValue: { type: null, required: false },
|
|
minValue: { type: null, required: false },
|
|
locale: { type: String, required: false },
|
|
numberOfMonths: { type: Number, required: false },
|
|
disabled: { type: Boolean, required: false },
|
|
readonly: { type: Boolean, required: false },
|
|
initialFocus: { type: Boolean, required: false },
|
|
isDateDisabled: { type: Function, required: false },
|
|
isDateUnavailable: { type: Function, required: false },
|
|
dir: { type: String, required: false },
|
|
nextPage: { type: Function, required: false },
|
|
prevPage: { type: Function, required: false },
|
|
modelValue: { type: null, required: false, default: undefined },
|
|
multiple: { type: Boolean, required: false },
|
|
disableDaysOutsideCurrentView: { type: Boolean, required: false },
|
|
asChild: { type: Boolean, required: false },
|
|
as: { type: null, required: false },
|
|
class: { type: null, required: false },
|
|
layout: { type: null, required: false, default: undefined },
|
|
yearRange: { type: Array, required: false },
|
|
});
|
|
const emits = defineEmits(["update:modelValue", "update:placeholder"]);
|
|
|
|
const delegatedProps = reactiveOmit(props, "class", "layout", "placeholder");
|
|
|
|
const placeholder = useVModel(props, "placeholder", emits, {
|
|
passive: true,
|
|
defaultValue: props.defaultPlaceholder ?? today(getLocalTimeZone()),
|
|
});
|
|
|
|
const formatter = useDateFormatter(props.locale ?? "en");
|
|
|
|
const yearRange = computed(() => {
|
|
return (
|
|
props.yearRange ??
|
|
createYearRange({
|
|
start:
|
|
props?.minValue ??
|
|
(
|
|
toRaw(props.placeholder) ??
|
|
props.defaultPlaceholder ??
|
|
today(getLocalTimeZone())
|
|
).cycle("year", -100),
|
|
|
|
end:
|
|
props?.maxValue ??
|
|
(
|
|
toRaw(props.placeholder) ??
|
|
props.defaultPlaceholder ??
|
|
today(getLocalTimeZone())
|
|
).cycle("year", 10),
|
|
})
|
|
);
|
|
});
|
|
|
|
const [DefineMonthTemplate, ReuseMonthTemplate] = createReusableTemplate();
|
|
const [DefineYearTemplate, ReuseYearTemplate] = createReusableTemplate();
|
|
|
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
</script>
|
|
|
|
<template>
|
|
<DefineMonthTemplate v-slot="{ date }">
|
|
<div class="**:data-[slot=native-select-icon]:right-1">
|
|
<div class="relative">
|
|
<div
|
|
class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none"
|
|
>
|
|
{{ formatter.custom(toDate(date), { month: "short" }) }}
|
|
</div>
|
|
<NativeSelect
|
|
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
|
|
@change="
|
|
(e) => {
|
|
placeholder = placeholder.set({
|
|
month: Number(e?.target?.value),
|
|
});
|
|
}
|
|
"
|
|
>
|
|
<NativeSelectOption
|
|
v-for="month in createYear({ dateObj: date })"
|
|
:key="month.toString()"
|
|
:value="month.month"
|
|
:selected="date.month === month.month"
|
|
>
|
|
{{ formatter.custom(toDate(month), { month: "short" }) }}
|
|
</NativeSelectOption>
|
|
</NativeSelect>
|
|
</div>
|
|
</div>
|
|
</DefineMonthTemplate>
|
|
|
|
<DefineYearTemplate v-slot="{ date }">
|
|
<div class="**:data-[slot=native-select-icon]:right-1">
|
|
<div class="relative">
|
|
<div
|
|
class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none"
|
|
>
|
|
{{ formatter.custom(toDate(date), { year: "numeric" }) }}
|
|
</div>
|
|
<NativeSelect
|
|
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
|
|
@change="
|
|
(e) => {
|
|
placeholder = placeholder.set({
|
|
year: Number(e?.target?.value),
|
|
});
|
|
}
|
|
"
|
|
>
|
|
<NativeSelectOption
|
|
v-for="year in yearRange"
|
|
:key="year.toString()"
|
|
:value="year.year"
|
|
:selected="date.year === year.year"
|
|
>
|
|
{{ formatter.custom(toDate(year), { year: "numeric" }) }}
|
|
</NativeSelectOption>
|
|
</NativeSelect>
|
|
</div>
|
|
</div>
|
|
</DefineYearTemplate>
|
|
|
|
<CalendarRoot
|
|
v-slot="{ grid, weekDays, date }"
|
|
v-bind="forwarded"
|
|
v-model:placeholder="placeholder"
|
|
data-slot="calendar"
|
|
:class="cn('p-3', props.class)"
|
|
>
|
|
<CalendarHeader class="pt-0">
|
|
<nav
|
|
class="flex items-center gap-1 absolute top-0 inset-x-0 justify-between"
|
|
>
|
|
<CalendarPrevButton>
|
|
<slot name="calendar-prev-icon" />
|
|
</CalendarPrevButton>
|
|
<CalendarNextButton>
|
|
<slot name="calendar-next-icon" />
|
|
</CalendarNextButton>
|
|
</nav>
|
|
|
|
<slot
|
|
name="calendar-heading"
|
|
:date="date"
|
|
:month="ReuseMonthTemplate"
|
|
:year="ReuseYearTemplate"
|
|
>
|
|
<template v-if="layout === 'month-and-year'">
|
|
<div class="flex items-center justify-center gap-1">
|
|
<ReuseMonthTemplate :date="date" />
|
|
<ReuseYearTemplate :date="date" />
|
|
</div>
|
|
</template>
|
|
<template v-else-if="layout === 'month-only'">
|
|
<div class="flex items-center justify-center gap-1">
|
|
<ReuseMonthTemplate :date="date" />
|
|
{{ formatter.custom(toDate(date), { year: "numeric" }) }}
|
|
</div>
|
|
</template>
|
|
<template v-else-if="layout === 'year-only'">
|
|
<div class="flex items-center justify-center gap-1">
|
|
{{ formatter.custom(toDate(date), { month: "short" }) }}
|
|
<ReuseYearTemplate :date="date" />
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<CalendarHeading />
|
|
</template>
|
|
</slot>
|
|
</CalendarHeader>
|
|
|
|
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
|
|
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
|
|
<CalendarGridHead>
|
|
<CalendarGridRow>
|
|
<CalendarHeadCell v-for="day in weekDays" :key="day">
|
|
{{ day }}
|
|
</CalendarHeadCell>
|
|
</CalendarGridRow>
|
|
</CalendarGridHead>
|
|
<CalendarGridBody>
|
|
<CalendarGridRow
|
|
v-for="(weekDates, index) in month.rows"
|
|
:key="`weekDate-${index}`"
|
|
class="mt-2 w-full"
|
|
>
|
|
<CalendarCell
|
|
v-for="weekDate in weekDates"
|
|
:key="weekDate.toString()"
|
|
:date="weekDate"
|
|
>
|
|
<CalendarCellTrigger :day="weekDate" :month="month.value" />
|
|
</CalendarCell>
|
|
</CalendarGridRow>
|
|
</CalendarGridBody>
|
|
</CalendarGrid>
|
|
</div>
|
|
</CalendarRoot>
|
|
</template>
|