import { useCallback } from 'react'; import { useLatest } from '../../hooks'; import type { CalendarDate, CalendarDates, ValidRangeType } from './types'; export type DisableWeekDaysType = number[]; export function showWeekDay(dayIndex: number, disableWeekDays?: DisableWeekDaysType): boolean { return !(disableWeekDays && disableWeekDays.some(di => di === dayIndex)); } export function dateToUnix(d: Date): number { return Math.round(d.getTime() / 1000); } export function addMonths(date: Date, count: number) { const n = date.getDate(); const n2 = new Date(date.getTime()); n2.setDate(1); n2.setMonth(n2.getMonth() + count); n2.setDate(Math.min(n, getDaysInMonth({ year: n2.getFullYear(), month: n2.getMonth() }))); return n2; } // https://stackoverflow.com/a/1185068/2508481 // pass in any date as parameter anyDateInMonth based on dayjs export function getDaysInMonth({ year, month }: { year: number; month: number }): number { return [31, isLeapYear({ year }) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; } export function getFirstDayOfMonth({ year, month }: { year: number; month: number }): number { return new Date(year, month, 1).getDay(); } export function useRangeChecker(validRange: ValidRangeType | undefined) { const validStart = validRange?.startDate; const validEnd = validRange?.endDate; const startUnix = validStart instanceof Date ? dateToUnix(getStartOfDay(validStart)) : undefined; const endUnix = validEnd instanceof Date ? dateToUnix(getEndOfDay(validEnd)) : undefined; const validDisabledDatesRef = useLatest(validRange?.disabledDates); const isWithinValidRange = useCallback( (day: Date) => { return isDateWithinOptionalRange(day, { startUnix: startUnix, endUnix: endUnix, }); }, [startUnix, endUnix], ); const isDisabled = useCallback( (day: Date) => { return validDisabledDatesRef.current ? validDisabledDatesRef.current.some(disabledDate => areDatesOnSameDay(disabledDate, day), ) : false; }, [validDisabledDatesRef], ); return { isDisabled, isWithinValidRange, validStart, validEnd }; } export function areDatesOnSameDay(a: Date, b?: Date | null | undefined) { if (!b) { return false; } return ( a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() ); } export function isDateBetween( date: Date, { startDate, endDate, }: { startDate?: Date | null | undefined; endDate?: Date | null | undefined; }, ): boolean { if (!startDate || !endDate) { return false; } return date <= endDate && date >= startDate; } /** * Check if a date is within an optional range. * * If the range doesn't exist, it defaults to `true`. */ export function isDateWithinOptionalRange( date: Date, { startUnix, endUnix }: { startUnix: number | undefined; endUnix: number | undefined }, ) { const dateUnix = dateToUnix(date); // if startUnix is provided and date is before start if (startUnix && dateUnix < startUnix) { return false; } // if endUnix is provided and date is after end if (endUnix && dateUnix > endUnix) { return false; } return true; } export function isLeapYear({ year }: { year: number }) { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } export const daySize = 40; export const estimatedMonthHeight = 360; // TODO This number startAtIndex is not adding any significance to the date picker, So we probably will change the logic. export const startAtIndex = 1200; export const totalMonths = startAtIndex * 2; export const beginOffset = estimatedMonthHeight * startAtIndex; export const gridCounts = new Array(totalMonths); export function getGridCount(index: number) { const cHeight = gridCounts[index]; if (cHeight) { return cHeight + 1; } const monthDate = addMonths(new Date(), getRealIndex(index)); const h = getGridCountForDate(monthDate); gridCounts[index] = h; return h; } export function getGridCountForDate(date: Date) { const year = date.getFullYear(); const month = date.getMonth(); const daysInMonth = getDaysInMonth({ year, month }); const dayOfWeek = getFirstDayOfMonth({ year, month }); return Math.ceil((daysInMonth + dayOfWeek) / 7); } export function getRealIndex(index: number) { return index - startAtIndex; } export function getInitialIndex(date: Date | undefined) { if (!date) { return startAtIndex; } const today = new Date(); const months = differenceInMonths(today, date); return startAtIndex + months; } export function getStartOfDay(d: Date): Date { return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0); } export function getEndOfDay(d: Date): Date { return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59); } export function differenceInMonths(firstDate: Date, secondDate: Date) { let diffMonths = (secondDate.getFullYear() - firstDate.getFullYear()) * 12; diffMonths -= firstDate.getMonth(); diffMonths += secondDate.getMonth(); return diffMonths; } type GenerateCalendarGridArgs = { year: number; month: number; monthGrid: (index: number) => { days: any[]; weekGrid: number }[]; index: number; isDisabled: (day: Date) => boolean; startDate: CalendarDate; endDate: CalendarDate; mode: 'single' | 'range' | 'multiple'; dates: CalendarDates; date: CalendarDate; isWithinValidRange: (day: Date) => boolean; }; export const generateCalendarGrid = ({ year, month, monthGrid, index, isDisabled, startDate, endDate, mode, dates, date, isWithinValidRange, }: GenerateCalendarGridArgs) => { const today = new Date(); const daysInMonth = getDaysInMonth({ year, month }); const dayOfWeek = getFirstDayOfMonth({ year, month }); const emptyDays = dayOfWeek; return monthGrid(index).map(({ days, weekGrid }) => { return { weekIndex: weekGrid, generatedDays: days.map((_, dayIndex) => { const isFirstWeek = weekGrid === 0; const realDayIndex = emptyDays - dayIndex; const beforeWeekDay = isFirstWeek && realDayIndex > 0; const dayOfMonth = weekGrid * 7 + dayIndex - emptyDays + 1; const afterWeekDay = dayOfMonth > daysInMonth; const day = new Date(year, month, dayOfMonth); const isToday = areDatesOnSameDay(day, today); let inRange = false; let disabled = isDisabled(day); let selected = false; let leftCrop = dayOfMonth === 1; let rightCrop = dayOfMonth === daysInMonth; const isFirstDayOfMonth = dayOfMonth === 1; const isLastDayOfMonth = dayOfMonth === daysInMonth; if (mode === 'range') { const selectedStartDay = areDatesOnSameDay(day, startDate); const selectedEndDay = areDatesOnSameDay(day, endDate); selected = selectedStartDay || selectedEndDay; inRange = isDateBetween(day, { startDate, endDate, }); if (selectedStartDay) { leftCrop = true; } if (selectedEndDay) { rightCrop = true; } if (dayIndex === 0 && !selectedStartDay) { leftCrop = false; } if (dayIndex === 6 && !selectedEndDay) { rightCrop = false; } if ( (isFirstDayOfMonth && selectedEndDay) || (isLastDayOfMonth && selectedStartDay) ) { inRange = false; } } else if (mode === 'multiple') { const safeDates = dates || []; selected = safeDates.some(d => areDatesOnSameDay(day, d)); const yesterday = new Date(year, month, dayOfMonth - 1); const tomorrow = new Date(year, month, dayOfMonth + 1); const yesterdaySelected = safeDates.some(d => areDatesOnSameDay(d, yesterday)); const tomorrowSelected = safeDates.some(d => areDatesOnSameDay(d, tomorrow)); if (selected) { if (tomorrowSelected && yesterdaySelected) { inRange = true; } if (tomorrowSelected && !yesterdaySelected) { inRange = true; leftCrop = true; } if (yesterdaySelected && !tomorrowSelected) { inRange = true; rightCrop = true; } if (isFirstDayOfMonth && !tomorrowSelected) { inRange = false; } if (isLastDayOfMonth && !yesterdaySelected) { inRange = false; } if (inRange && !leftCrop && !rightCrop) { selected = false; } } } else if (mode === 'single') { selected = areDatesOnSameDay(day, date); } const isWithinOptionalValidRange = isWithinValidRange(day); if (inRange && !disabled) { disabled = false; } if (!isWithinOptionalValidRange) { disabled = true; } return { beforeWeekDay, afterWeekDay, year, month, dayOfMonth, dayIndex, mode, selected, inRange, leftCrop, rightCrop, isToday, disabled, }; }), }; }); };