Files
template-vue-hucky/src/components/ui/calendar/Calendar.vue

226 lines
6.6 KiB
Vue
Raw Normal View History

2026-01-31 00:00:46 +08:00
<script lang="ts" setup>
2026-01-31 02:54:00 +08:00
import type { CalendarRootEmits, CalendarRootProps, DateValue } from "reka-ui";
import type { HTMLAttributes, Ref } from "vue";
import type { LayoutTypes } from ".";
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 ".";
2026-01-31 00:00:46 +08:00
2026-01-31 02:54:00 +08:00
const props = withDefaults(
defineProps<
CalendarRootProps & {
class?: HTMLAttributes["class"];
layout?: LayoutTypes;
yearRange?: DateValue[];
}
>(),
{
modelValue: undefined,
layout: undefined,
},
);
const emits = defineEmits<CalendarRootEmits>();
2026-01-31 00:00:46 +08:00
2026-01-31 02:54:00 +08:00
const delegatedProps = reactiveOmit(props, "class", "layout", "placeholder");
2026-01-31 00:00:46 +08:00
const placeholder = useVModel(props, "placeholder", emits, {
passive: true,
defaultValue: props.defaultPlaceholder ?? today(getLocalTimeZone()),
2026-01-31 02:54:00 +08:00
}) as Ref<DateValue>;
2026-01-31 00:00:46 +08:00
2026-01-31 02:54:00 +08:00
const formatter = useDateFormatter(props.locale ?? "en");
2026-01-31 00:00:46 +08:00
const yearRange = computed(() => {
2026-01-31 02:54:00 +08:00
return (
props.yearRange ??
createYearRange({
start:
props?.minValue ??
(
toRaw(props.placeholder) ??
props.defaultPlaceholder ??
today(getLocalTimeZone())
).cycle("year", -100),
2026-01-31 00:00:46 +08:00
2026-01-31 02:54:00 +08:00
end:
props?.maxValue ??
(
toRaw(props.placeholder) ??
props.defaultPlaceholder ??
today(getLocalTimeZone())
).cycle("year", 10),
})
);
});
2026-01-31 00:00:46 +08:00
2026-01-31 02:54:00 +08:00
const [DefineMonthTemplate, ReuseMonthTemplate] = createReusableTemplate<{
date: DateValue;
}>();
const [DefineYearTemplate, ReuseYearTemplate] = createReusableTemplate<{
date: DateValue;
}>();
2026-01-31 00:00:46 +08:00
2026-01-31 02:54:00 +08:00
const forwarded = useForwardPropsEmits(delegatedProps, emits);
2026-01-31 00:00:46 +08:00
</script>
<template>
<DefineMonthTemplate v-slot="{ date }">
<div class="**:data-[slot=native-select-icon]:right-1">
<div class="relative">
2026-01-31 02:54:00 +08:00
<div
class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none"
>
{{ formatter.custom(toDate(date), { month: "short" }) }}
2026-01-31 00:00:46 +08:00
</div>
<NativeSelect
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
2026-01-31 02:54:00 +08:00
@change="
(e: Event) => {
placeholder = placeholder.set({
month: Number((e?.target as any)?.value),
});
}
"
2026-01-31 00:00:46 +08:00
>
2026-01-31 02:54:00 +08:00
<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" }) }}
2026-01-31 00:00:46 +08:00
</NativeSelectOption>
</NativeSelect>
</div>
</div>
</DefineMonthTemplate>
<DefineYearTemplate v-slot="{ date }">
<div class="**:data-[slot=native-select-icon]:right-1">
<div class="relative">
2026-01-31 02:54:00 +08:00
<div
class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none"
>
{{ formatter.custom(toDate(date), { year: "numeric" }) }}
2026-01-31 00:00:46 +08:00
</div>
<NativeSelect
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
2026-01-31 02:54:00 +08:00
@change="
(e: Event) => {
placeholder = placeholder.set({
year: Number((e?.target as any)?.value),
});
}
"
2026-01-31 00:00:46 +08:00
>
2026-01-31 02:54:00 +08:00
<NativeSelectOption
v-for="year in yearRange"
:key="year.toString()"
:value="year.year"
:selected="date.year === year.year"
>
{{ formatter.custom(toDate(year), { year: "numeric" }) }}
2026-01-31 00:00:46 +08:00
</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">
2026-01-31 02:54:00 +08:00
<nav
class="flex items-center gap-1 absolute top-0 inset-x-0 justify-between"
>
2026-01-31 00:00:46 +08:00
<CalendarPrevButton>
<slot name="calendar-prev-icon" />
</CalendarPrevButton>
<CalendarNextButton>
<slot name="calendar-next-icon" />
</CalendarNextButton>
</nav>
2026-01-31 02:54:00 +08:00
<slot
name="calendar-heading"
:date="date"
:month="ReuseMonthTemplate"
:year="ReuseYearTemplate"
>
2026-01-31 00:00:46 +08:00
<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" />
2026-01-31 02:54:00 +08:00
{{ formatter.custom(toDate(date), { year: "numeric" }) }}
2026-01-31 00:00:46 +08:00
</div>
</template>
<template v-else-if="layout === 'year-only'">
<div class="flex items-center justify-center gap-1">
2026-01-31 02:54:00 +08:00
{{ formatter.custom(toDate(date), { month: "short" }) }}
2026-01-31 00:00:46 +08:00
<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>
2026-01-31 02:54:00 +08:00
<CalendarHeadCell v-for="day in weekDays" :key="day">
2026-01-31 00:00:46 +08:00
{{ day }}
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody>
2026-01-31 02:54:00 +08:00
<CalendarGridRow
v-for="(weekDates, index) in month.rows"
:key="`weekDate-${index}`"
class="mt-2 w-full"
>
2026-01-31 00:00:46 +08:00
<CalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
>
2026-01-31 02:54:00 +08:00
<CalendarCellTrigger :day="weekDate" :month="month.value" />
2026-01-31 00:00:46 +08:00
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</div>
</CalendarRoot>
</template>