UNPKG

reka-ui

Version:

Vue port for Radix UI Primitives.

355 lines (294 loc) 11.3 kB
/* * Adapted from https://github.com/melt-ui/melt-ui/blob/develop/src/lib/builders/calendar/create.ts */ import type { DateFields, DateValue } from '@internationalized/date' import type { Ref } from 'vue' import type { Grid, Matcher, WeekDayFormat } from '@/date' import type { DateFormatterOptions } from '@/shared/useDateFormatter' import { isEqualMonth, isSameDay } from '@internationalized/date' import { computed, ref, watch } from 'vue' import { createMonths, getDaysInMonth, isAfter, isBefore, toDate } from '@/date' import { useDateFormatter } from '@/shared' export type UseCalendarProps = { locale: Ref<string> placeholder: Ref<DateValue> weekStartsOn: Ref<0 | 1 | 2 | 3 | 4 | 5 | 6> fixedWeeks: Ref<boolean> numberOfMonths: Ref<number> minValue: Ref<DateValue | undefined> maxValue: Ref<DateValue | undefined> disabled: Ref<boolean> weekdayFormat: Ref<WeekDayFormat> pagedNavigation: Ref<boolean> isDateDisabled?: Matcher isDateUnavailable?: Matcher calendarLabel: Ref<string | undefined> nextPage: Ref<((placeholder: DateValue) => DateValue) | undefined> prevPage: Ref<((placeholder: DateValue) => DateValue) | undefined> } export type UseCalendarStateProps = { isDateDisabled: Matcher isDateUnavailable: Matcher date: Ref<DateValue | DateValue[] | undefined> } export function useCalendarState(props: UseCalendarStateProps) { function isDateSelected(dateObj: DateValue) { if (Array.isArray(props.date.value)) return props.date.value.some(d => isSameDay(d, dateObj)) else if (!props.date.value) return false else return isSameDay(props.date.value, dateObj) } const isInvalid = computed( () => { if (Array.isArray(props.date.value)) { if (!props.date.value.length) return false for (const dateObj of props.date.value) { if (props.isDateDisabled?.(dateObj)) return true if (props.isDateUnavailable?.(dateObj)) return true } } else { if (!props.date.value) return false if (props.isDateDisabled?.(props.date.value)) return true if (props.isDateUnavailable?.(props.date.value)) return true } return false }, ) return { isDateSelected, isInvalid, } } function handleNextDisabled(lastPeriodInView: DateValue, nextPageFunc: (date: DateValue) => DateValue): DateValue { const firstPeriodOfNextPage = nextPageFunc(lastPeriodInView) const diff = firstPeriodOfNextPage.compare(lastPeriodInView) const duration: DateFields = {} if (diff >= 7) duration.day = 1 if (diff >= getDaysInMonth(lastPeriodInView)) duration.month = 1 return firstPeriodOfNextPage.set({ ...duration }) } function handlePrevDisabled(firstPeriodInView: DateValue, prevPageFunc: (date: DateValue) => DateValue): DateValue { const lastPeriodOfPrevPage = prevPageFunc(firstPeriodInView) const diff = firstPeriodInView.compare(lastPeriodOfPrevPage) const duration: DateFields = {} if (diff >= 7) duration.day = 35 if (diff >= getDaysInMonth(firstPeriodInView)) duration.month = 13 return lastPeriodOfPrevPage.set({ ...duration }) } function handleNextPage(date: DateValue, nextPageFunc: (date: DateValue) => DateValue): DateValue { return nextPageFunc(date) } function handlePrevPage(date: DateValue, prevPageFunc: (date: DateValue) => DateValue): DateValue { return prevPageFunc(date) } export function useCalendar(props: UseCalendarProps) { const formatter = useDateFormatter(props.locale.value) const headingFormatOptions = computed(() => { const options: DateFormatterOptions = { calendar: props.placeholder.value.calendar.identifier, } if (props.placeholder.value.calendar.identifier === 'gregory' && props.placeholder.value.era === 'BC') options.era = 'short' return options }) const grid = ref<Grid<DateValue>[]>(createMonths({ dateObj: props.placeholder.value, weekStartsOn: props.weekStartsOn.value, locale: props.locale.value, fixedWeeks: props.fixedWeeks.value, numberOfMonths: props.numberOfMonths.value, })) as Ref<Grid<DateValue>[]> const visibleView = computed(() => { return grid.value.map(month => month.value) }) function isOutsideVisibleView(date: DateValue) { return !visibleView.value.some(month => isEqualMonth(date, month)) } const isNextButtonDisabled = (nextPageFunc?: (date: DateValue) => DateValue) => { if (!props.maxValue.value || !grid.value.length) return false if (props.disabled.value) return true const lastPeriodInView = grid.value[grid.value.length - 1].value if (!nextPageFunc && !props.nextPage.value) { const firstPeriodOfNextPage = lastPeriodInView.add({ months: 1 }).set({ day: 1 }) return isAfter(firstPeriodOfNextPage, props.maxValue.value) } const firstPeriodOfNextPage = handleNextDisabled(lastPeriodInView, nextPageFunc || props.nextPage.value!) return isAfter(firstPeriodOfNextPage, props.maxValue.value) } const isPrevButtonDisabled = (prevPageFunc?: (date: DateValue) => DateValue) => { if (!props.minValue.value || !grid.value.length) return false if (props.disabled.value) return true const firstPeriodInView = grid.value[0].value if (!prevPageFunc && !props.prevPage.value) { const lastPeriodOfPrevPage = firstPeriodInView.subtract({ months: 1 }).set({ day: 35 }) return isBefore(lastPeriodOfPrevPage, props.minValue.value) } const lastPeriodOfPrevPage = handlePrevDisabled(firstPeriodInView, prevPageFunc || props.prevPage.value!) return isBefore(lastPeriodOfPrevPage, props.minValue.value) } function isDateDisabled(dateObj: DateValue) { if (props.isDateDisabled?.(dateObj) || props.disabled.value) return true if (props.maxValue.value && isAfter(dateObj, props.maxValue.value)) return true if (props.minValue.value && isBefore(dateObj, props.minValue.value)) return true return false } const isDateUnavailable = (date: DateValue) => { if (props.isDateUnavailable?.(date)) return true return false } const weekdays = computed(() => { if (!grid.value.length) return [] return grid.value[0].rows[0].map((date) => { return formatter.dayOfWeek(toDate(date), props.weekdayFormat.value) }) }) const nextPage = (nextPageFunc?: (date: DateValue) => DateValue) => { const firstDate = grid.value[0].value if (!nextPageFunc && !props.nextPage.value) { const newDate = firstDate.add({ months: props.pagedNavigation.value ? props.numberOfMonths.value : 1 }) const newGrid = createMonths({ dateObj: newDate, weekStartsOn: props.weekStartsOn.value, locale: props.locale.value, fixedWeeks: props.fixedWeeks.value, numberOfMonths: props.numberOfMonths.value, }) grid.value = newGrid props.placeholder.value = newGrid[0].value.set({ day: 1 }) return } const newDate = handleNextPage(firstDate, nextPageFunc || props.nextPage.value!) const newGrid = createMonths({ dateObj: newDate, weekStartsOn: props.weekStartsOn.value, locale: props.locale.value, fixedWeeks: props.fixedWeeks.value, numberOfMonths: props.numberOfMonths.value, }) grid.value = newGrid const duration: DateFields = {} // Do not adjust the placeholder if the nextPageFunc is defined (overwrite) if (!nextPageFunc) { const diff = newGrid[0].value.compare(firstDate) if (diff >= getDaysInMonth(firstDate)) duration.day = 1 if (diff >= 365) duration.month = 1 } props.placeholder.value = newGrid[0].value.set({ ...duration }) } const prevPage = (prevPageFunc?: (date: DateValue) => DateValue) => { const firstDate = grid.value[0].value if (!prevPageFunc && !props.prevPage.value) { const newDate = firstDate.subtract({ months: props.pagedNavigation.value ? props.numberOfMonths.value : 1 }) const newGrid = createMonths({ dateObj: newDate, weekStartsOn: props.weekStartsOn.value, locale: props.locale.value, fixedWeeks: props.fixedWeeks.value, numberOfMonths: props.numberOfMonths.value, }) grid.value = newGrid props.placeholder.value = newGrid[0].value.set({ day: 1 }) return } const newDate = handlePrevPage(firstDate, prevPageFunc || props.prevPage.value!) const newGrid = createMonths({ dateObj: newDate, weekStartsOn: props.weekStartsOn.value, locale: props.locale.value, fixedWeeks: props.fixedWeeks.value, numberOfMonths: props.numberOfMonths.value, }) grid.value = newGrid const duration: DateFields = {} // Do not adjust the placeholder if the prevPageFunc is defined (overwrite) if (!prevPageFunc) { const diff = firstDate.compare(newGrid[0].value) if (diff >= getDaysInMonth(firstDate)) duration.day = 1 if (diff >= 365) duration.month = 1 } props.placeholder.value = newGrid[0].value.set({ ...duration }) } watch(props.placeholder, (value) => { if (visibleView.value.some(month => isEqualMonth(month, value))) return grid.value = createMonths({ dateObj: value, weekStartsOn: props.weekStartsOn.value, locale: props.locale.value, fixedWeeks: props.fixedWeeks.value, numberOfMonths: props.numberOfMonths.value, }) }) watch([props.locale, props.weekStartsOn, props.fixedWeeks, props.numberOfMonths], () => { grid.value = createMonths({ dateObj: props.placeholder.value, weekStartsOn: props.weekStartsOn.value, locale: props.locale.value, fixedWeeks: props.fixedWeeks.value, numberOfMonths: props.numberOfMonths.value, }) }) const headingValue = computed(() => { if (!grid.value.length) return '' if (props.locale.value !== formatter.getLocale()) formatter.setLocale(props.locale.value) if (grid.value.length === 1) { const month = grid.value[0].value return `${formatter.fullMonthAndYear(toDate(month), headingFormatOptions.value)}` } const startMonth = toDate(grid.value[0].value) const endMonth = toDate(grid.value[grid.value.length - 1].value) const startMonthName = formatter.fullMonth(startMonth, headingFormatOptions.value) const endMonthName = formatter.fullMonth(endMonth, headingFormatOptions.value) const startMonthYear = formatter.fullYear(startMonth, headingFormatOptions.value) const endMonthYear = formatter.fullYear(endMonth, headingFormatOptions.value) const content = startMonthYear === endMonthYear ? `${startMonthName} - ${endMonthName} ${endMonthYear}` : `${startMonthName} ${startMonthYear} - ${endMonthName} ${endMonthYear}` return content }) const fullCalendarLabel = computed(() => `${props.calendarLabel.value ?? 'Event Date'}, ${headingValue.value}`) return { isDateDisabled, isDateUnavailable, isNextButtonDisabled, isPrevButtonDisabled, grid, weekdays, visibleView, isOutsideVisibleView, formatter, nextPage, prevPage, headingValue, fullCalendarLabel, } }