UNPKG

reka-ui

Version:

Vue port for Radix UI Primitives.

296 lines (234 loc) 8.95 kB
/* * Implementation ported from from from https://github.com/melt-ui/melt-ui/blob/develop/src/lib/builders/calendar/create.ts */ import type { DateValue, DayOfWeek } from '@internationalized/date' import type { Grid } from './types' import type { DateRange } from '@/shared' import { CalendarDate, endOfMonth, endOfYear, getDayOfWeek, startOfMonth, startOfWeek, startOfYear } from '@internationalized/date' import { getDaysInMonth, getLastFirstDayOfWeek, getNextLastDayOfWeek } from './comparators' import { chunk } from './utils' export type WeekDayFormat = 'narrow' | 'short' | 'long' export type WeekStartsOn = 0 | 1 | 2 | 3 | 4 | 5 | 6 export type CreateSelectProps = { /** * The date object representing the date (usually the first day of the month/year). */ dateObj: DateValue } export type CreateMonthProps = { /** * The date object representing the month's date (usually the first day of the month). */ dateObj: DateValue /** * The day of the week to start the calendar on (0 for Sunday, 1 for Monday, etc.). */ weekStartsOn: WeekStartsOn /** * Whether to always render 6 weeks in the calendar, even if the month doesn't * span 6 weeks. */ fixedWeeks: boolean /** * The locale to use when creating the calendar month. */ locale: string } /** * Retrieves an array of date values representing the days between * the provided start and end dates. */ export function getDaysBetween(start: DateValue, end: DateValue) { const days: DateValue[] = [] let dCurrent = start.add({ days: 1 }) const dEnd = end while (dCurrent.compare(dEnd) < 0) { days.push(dCurrent) dCurrent = dCurrent.add({ days: 1 }) } return days } export function createMonth(props: CreateMonthProps): Grid<DateValue> { const { dateObj, weekStartsOn, fixedWeeks, locale } = props const daysInMonth = getDaysInMonth(dateObj) const datesArray = Array.from({ length: daysInMonth }, (_, i) => dateObj.set({ day: i + 1 })) const firstDayOfMonth = startOfMonth(dateObj) const lastDayOfMonth = endOfMonth(dateObj) const lastSunday = getLastFirstDayOfWeek(firstDayOfMonth, weekStartsOn, locale) const nextSaturday = getNextLastDayOfWeek(lastDayOfMonth, weekStartsOn, locale) const lastMonthDays = getDaysBetween(lastSunday.subtract({ days: 1 }), firstDayOfMonth) const nextMonthDays = getDaysBetween(lastDayOfMonth, nextSaturday.add({ days: 1 })) const totalDays = lastMonthDays.length + datesArray.length + nextMonthDays.length if (fixedWeeks && totalDays < 42) { const extraDays = 42 - totalDays let startFrom = nextMonthDays.at(-1) if (!startFrom) startFrom = endOfMonth(dateObj) const extraDaysArray = Array.from({ length: extraDays }, (_, i) => { const incr = i + 1 return startFrom.add({ days: incr }) }) nextMonthDays.push(...extraDaysArray) } const allDays = lastMonthDays.concat(datesArray, nextMonthDays) const weeks = chunk(allDays, 7) return { value: dateObj, cells: allDays, rows: weeks, } } type SetMonthProps = CreateMonthProps & { numberOfMonths: number | undefined currentMonths?: Grid<DateValue>[] } type SetYearProps = CreateSelectProps & { numberOfMonths?: number pagedNavigation?: boolean } type SetDecadeProps = CreateSelectProps & { startIndex?: number endIndex: number } export function startOfDecade(dateObj: DateValue) { // round to the lowest nearest 10 when building the decade return startOfYear(dateObj.subtract({ years: dateObj.year - Math.floor(dateObj.year / 10) * 10 }).set({ day: 1, month: 1 })) } export function endOfDecade(dateObj: DateValue) { // round to the lowest nearest 10 when building the decade return endOfYear(dateObj.add({ years: Math.ceil((dateObj.year + 1) / 10) * 10 - dateObj.year - 1 }).set({ day: 35, month: 12 })) } export function createDecade(props: SetDecadeProps): DateValue[] { const { dateObj, startIndex, endIndex } = props const decadeArray = Array.from({ length: Math.abs(startIndex ?? 0) + endIndex }, (_, i) => i <= Math.abs((startIndex ?? 0)) ? dateObj.subtract({ years: i }).set({ day: 1, month: 1 }) : dateObj.add({ years: i - endIndex }).set({ day: 1, month: 1 })) decadeArray.sort((a: DateValue, b: DateValue) => a.year - b.year) return decadeArray } export function createYear(props: SetYearProps): DateValue[] { const { dateObj, numberOfMonths = 1, pagedNavigation = false } = props if (numberOfMonths && pagedNavigation) { const monthsArray = Array.from({ length: Math.floor(12 / numberOfMonths) }, (_, i) => startOfMonth(dateObj.set({ month: i * numberOfMonths + 1 }))) return monthsArray } const monthsArray = Array.from({ length: 12 }, (_, i) => startOfMonth(dateObj.set({ month: i + 1 }))) return monthsArray } export function createMonths(props: SetMonthProps) { const { numberOfMonths, dateObj, ...monthProps } = props const months: Grid<DateValue>[] = [] if (!numberOfMonths || numberOfMonths === 1) { months.push( createMonth({ ...monthProps, dateObj, }), ) return months } months.push( createMonth({ ...monthProps, dateObj, }), ) // Create all the months, starting with the current month for (let i = 1; i < numberOfMonths; i++) { const nextMonth = dateObj.add({ months: i }) months.push( createMonth({ ...monthProps, dateObj: nextMonth, }), ) } return months } /** * Creates a 3x4 grid of months for a given year. */ export function createMonthGrid(props: CreateSelectProps): Grid<DateValue> { const { dateObj } = props const months = createYear({ dateObj }) return { value: dateObj, cells: months, rows: chunk(months, 4) } } /** * Creates a 3x4 grid of years (decade-aligned). * The grid starts from the decade that contains the given date. */ export function createYearGrid(props: CreateSelectProps & { yearsPerPage?: number, decadeAligned?: boolean }): Grid<DateValue> { const { dateObj, yearsPerPage = 12, decadeAligned = true } = props let startYear: number if (decadeAligned) { startYear = startOfDecade(dateObj).year } else { startYear = dateObj.year } const years = Array.from({ length: yearsPerPage }, (_, i) => startOfYear(dateObj.set({ year: startYear + i }))) const firstYear = years[0] return { value: firstYear, cells: years, rows: chunk(years, 4) } } export function createYearRange({ start, end }: DateRange): DateValue[] { const years: DateValue[] = [] if (!start || !end) return years let current = startOfYear(start) while (current.compare(end) <= 0) { years.push(current) // Move to the first day of the next year current = startOfYear(current.add({ years: 1 })) } return years } export function createDateRange({ start, end }: DateRange): DateValue[] { const dates: DateValue[] = [] if (!start || !end) return dates let current = start while (current.compare(end) <= 0) { dates.push(current) current = current.add({ days: 1 }) } return dates } /** * It's better to use `getWeekStart` from `@internationalized/date`, * but sadly it is not yet exported from the package. * And the `Intl.Locale` API is not supported well enough yet. */ export function getWeekStartsOn(locale: string): WeekStartsOn { // Jan 6, 2025 is a Monday (ISO day = 1) const monday = new CalendarDate(2025, 1, 6) const dayOfWeek = getDayOfWeek(monday, locale) // dayOfWeek tells us Monday's position in the locale's week (0-indexed) // If Monday is position 0 → week starts Monday (1) // If Monday is position 1 → week starts Sunday (0) return (1 - dayOfWeek + 7) % 7 as WeekStartsOn } /** * Returns the locale-specific week number */ export function getWeekNumber(date: DateValue, locale: string = 'en-US', firstDayOfWeek?: DayOfWeek): number { const jan1 = new CalendarDate(date.year, 1, 1) // Detect ISO locale by comparing JS day of week with locale day of week const usesISOWeek = jan1.toDate('UTC').getUTCDay() !== getDayOfWeek(jan1, locale) const weekStartsOn = firstDayOfWeek ?? (usesISOWeek ? 'mon' : 'sun') const firstWeekContainsDate = usesISOWeek ? 4 : 1 // Find the "deciding day" - its year determines which year's week numbering to use const dayOfWeek = getDayOfWeek(date, locale, weekStartsOn) const decidingDay = date.add({ days: 7 - firstWeekContainsDate - dayOfWeek }) const weekYear = decidingDay.year // Calculate week number from week 1 start const week1Ref = new CalendarDate(weekYear, 1, firstWeekContainsDate) const week1Start = startOfWeek(week1Ref, locale, weekStartsOn) const currentWeekStart = startOfWeek(date, locale, weekStartsOn) const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000 const daysDiff = Math.round( (currentWeekStart.toDate('UTC').getTime() - week1Start.toDate('UTC').getTime()) / MS_PER_WEEK, ) return daysDiff + 1 }