-
-
Save tanvirraj/8e1c85954b641f73d5e0b0b74d2517e6 to your computer and use it in GitHub Desktop.
Calendar Hook
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import addDays from 'date-fns/add_days' | |
import isBefore from 'date-fns/is_before' | |
import isToday from 'date-fns/is_today' | |
import startOfDay from 'date-fns/start_of_day' | |
import differenceInCalendarMonths from 'date-fns/difference_in_calendar_months' | |
export default function useCalendar({ | |
date = new Date(), | |
offset: userOffset = 0, | |
onDateChange, | |
selected, | |
minDate, | |
maxDate, | |
monthsToDisplay = 1, | |
firstDayOfWeek = 0, | |
showOutsideDays = false, | |
}) { | |
const [resolvedOffset, setOffset] = React.useState(userOffset) | |
const onDateChangeRef = React.useRef() | |
onDateChangeRef.current = onDateChange | |
React.useEffect(() => { | |
setOffset(userOffset) | |
}, [userOffset]) | |
const calendars = React.useMemo( | |
() => | |
getCalendars({ | |
date, | |
selected, | |
monthsToDisplay, | |
minDate, | |
maxDate, | |
offset: resolvedOffset, | |
firstDayOfWeek, | |
showOutsideDays, | |
}), | |
[ | |
date, | |
firstDayOfWeek, | |
maxDate, | |
minDate, | |
monthsToDisplay, | |
resolvedOffset, | |
selected, | |
showOutsideDays, | |
] | |
) | |
const getBackProps = React.useCallback( | |
({ | |
onClick, | |
offset = 1, | |
calendars = requiredProp('getBackProps', 'calendars'), | |
...rest | |
} = {}) => { | |
return { | |
onClick: composeEventHandlers(onClick, () => { | |
setOffset( | |
resolvedOffset - subtractMonth({ calendars, offset, minDate }) | |
) | |
}), | |
disabled: isBackDisabled({ calendars, offset, minDate }), | |
'aria-label': `Go back ${offset} month${offset === 1 ? '' : 's'}`, | |
...rest, | |
} | |
}, | |
[minDate, resolvedOffset] | |
) | |
const getForwardProps = React.useCallback( | |
({ | |
onClick, | |
offset = 1, | |
calendars = requiredProp('getForwardProps', 'calendars'), | |
...rest | |
} = {}) => { | |
return { | |
onClick: composeEventHandlers(onClick, () => { | |
setOffset(resolvedOffset + addMonth({ calendars, offset, maxDate })) | |
}), | |
disabled: isForwardDisabled({ calendars, offset, maxDate }), | |
'aria-label': `Go forward ${offset} month${offset === 1 ? '' : 's'}`, | |
...rest, | |
} | |
}, | |
[maxDate, resolvedOffset] | |
) | |
const getDateProps = React.useCallback( | |
( | |
dateObj = requiredProp('getDateProps', 'dateObj'), | |
{ onClick, ...rest } = {} | |
) => { | |
return { | |
onClick: composeEventHandlers(onClick, () => { | |
onDateChangeRef.current(dateObj) | |
}), | |
disabled: !dateObj.selectable, | |
'aria-label': dateObj.date.toDateString(), | |
'aria-pressed': dateObj.selected, | |
role: 'button', | |
...rest, | |
} | |
}, | |
[] | |
) | |
return { | |
offset: resolvedOffset, | |
calendars, | |
getBackProps, | |
getForwardProps, | |
getDateProps, | |
} | |
} | |
function getCalendars({ | |
date, | |
selected, | |
monthsToDisplay, | |
offset, | |
minDate, | |
maxDate, | |
firstDayOfWeek, | |
showOutsideDays, | |
}) { | |
const months = [] | |
const startDate = getStartDate(date, minDate, maxDate) | |
for (let i = 0; i < monthsToDisplay; i++) { | |
const calendarDates = getMonths({ | |
month: startDate.getMonth() + i + offset, | |
year: startDate.getFullYear(), | |
selectedDates: selected, | |
minDate, | |
maxDate, | |
firstDayOfWeek, | |
showOutsideDays, | |
}) | |
months.push(calendarDates) | |
} | |
return months | |
} | |
function getStartDate(date, minDate, maxDate) { | |
let startDate = startOfDay(date) | |
if (minDate) { | |
const minDateNormalized = startOfDay(minDate) | |
if (isBefore(startDate, minDateNormalized)) { | |
startDate = minDateNormalized | |
} | |
} | |
if (maxDate) { | |
const maxDateNormalized = startOfDay(maxDate) | |
if (isBefore(maxDateNormalized, startDate)) { | |
startDate = maxDateNormalized | |
} | |
} | |
return startDate | |
} | |
function getMonths({ | |
month, | |
year, | |
selectedDates, | |
minDate, | |
maxDate, | |
firstDayOfWeek, | |
showOutsideDays, | |
}) { | |
// Get the normalized month and year, along with days in the month. | |
const daysMonthYear = getNumDaysMonthYear(month, year) | |
const daysInMonth = daysMonthYear.daysInMonth | |
month = daysMonthYear.month | |
year = daysMonthYear.year | |
// Fill out the dates for the month. | |
const dates = [] | |
for (let day = 1; day <= daysInMonth; day++) { | |
const date = new Date(year, month, day) | |
const dateObj = { | |
date, | |
selected: isSelected(selectedDates, date), | |
selectable: isSelectable(minDate, maxDate, date), | |
today: isToday(date), | |
prevMonth: false, | |
nextMonth: false, | |
} | |
dates.push(dateObj) | |
} | |
const firstDayOfMonth = new Date(year, month, 1) | |
const lastDayOfMonth = new Date(year, month, daysInMonth) | |
const frontWeekBuffer = fillFrontWeek({ | |
firstDayOfMonth, | |
minDate, | |
maxDate, | |
selectedDates, | |
firstDayOfWeek, | |
showOutsideDays, | |
}) | |
const backWeekBuffer = fillBackWeek({ | |
lastDayOfMonth, | |
minDate, | |
maxDate, | |
selectedDates, | |
firstDayOfWeek, | |
showOutsideDays, | |
}) | |
dates.unshift(...frontWeekBuffer) | |
dates.push(...backWeekBuffer) | |
// Get the filled out weeks for the | |
// given dates. | |
const weeks = getWeeks(dates) | |
// return the calendar data. | |
return { | |
firstDayOfMonth, | |
lastDayOfMonth, | |
month, | |
year, | |
weeks, | |
} | |
} | |
function getNumDaysMonthYear(month, year) { | |
// If a parameter you specify is outside of the expected range for Month or Day, | |
// JS Date attempts to update the date information in the Date object accordingly! | |
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMonth | |
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setDate | |
// Let Date handle the overflow of the month, | |
// which should return the normalized month and year. | |
const normalizedMonthYear = new Date(year, month, 1) | |
month = normalizedMonthYear.getMonth() | |
year = normalizedMonthYear.getFullYear() | |
// Overflow the date to the next month, then subtract the difference | |
// to get the number of days in the previous month. | |
// This will also account for leap years! | |
const daysInMonth = 32 - new Date(year, month, 32).getDate() | |
return { daysInMonth, month, year } | |
} | |
function isSelectable(minDate, maxDate, date) { | |
if ( | |
(minDate && isBefore(date, minDate)) || | |
(maxDate && isBefore(maxDate, date)) | |
) { | |
return false | |
} | |
return true | |
} | |
function isSelected(selectedDates, date) { | |
selectedDates = Array.isArray(selectedDates) ? selectedDates : [selectedDates] | |
return selectedDates.some(selectedDate => { | |
if ( | |
selectedDate instanceof Date && | |
startOfDay(selectedDate).getTime() === startOfDay(date).getTime() | |
) { | |
return true | |
} | |
return false | |
}) | |
} | |
function getWeeks(dates) { | |
const weeksLength = Math.ceil(dates.length / 7) | |
const weeks = [] | |
for (let i = 0; i < weeksLength; i++) { | |
weeks[i] = [] | |
for (let x = 0; x < 7; x++) { | |
weeks[i].push(dates[i * 7 + x]) | |
} | |
} | |
return weeks | |
} | |
function fillFrontWeek({ | |
firstDayOfMonth, | |
minDate, | |
maxDate, | |
selectedDates, | |
firstDayOfWeek, | |
showOutsideDays, | |
}) { | |
const dates = [] | |
let firstDay = (firstDayOfMonth.getDay() + 7 - firstDayOfWeek) % 7 | |
if (showOutsideDays) { | |
const lastDayOfPrevMonth = addDays(firstDayOfMonth, -1) | |
const prevDate = lastDayOfPrevMonth.getDate() | |
const prevDateMonth = lastDayOfPrevMonth.getMonth() | |
const prevDateYear = lastDayOfPrevMonth.getFullYear() | |
// Fill out front week for days from | |
// preceding month with dates from previous month. | |
let counter = 0 | |
while (counter < firstDay) { | |
const date = new Date(prevDateYear, prevDateMonth, prevDate - counter) | |
const dateObj = { | |
date, | |
selected: isSelected(selectedDates, date), | |
selectable: isSelectable(minDate, maxDate, date), | |
today: false, | |
prevMonth: true, | |
nextMonth: false, | |
} | |
dates.unshift(dateObj) | |
counter++ | |
} | |
} else { | |
// Fill out front week for days from | |
// preceding month with buffer. | |
while (firstDay > 0) { | |
dates.unshift('') | |
firstDay-- | |
} | |
} | |
return dates | |
} | |
function fillBackWeek({ | |
lastDayOfMonth, | |
minDate, | |
maxDate, | |
selectedDates, | |
firstDayOfWeek, | |
showOutsideDays, | |
}) { | |
const dates = [] | |
let lastDay = (lastDayOfMonth.getDay() + 7 - firstDayOfWeek) % 7 | |
if (showOutsideDays) { | |
const firstDayOfNextMonth = addDays(lastDayOfMonth, 1) | |
const nextDateMonth = firstDayOfNextMonth.getMonth() | |
const nextDateYear = firstDayOfNextMonth.getFullYear() | |
// Fill out back week for days from | |
// following month with dates from next month. | |
let counter = 0 | |
while (counter < 6 - lastDay) { | |
const date = new Date(nextDateYear, nextDateMonth, 1 + counter) | |
const dateObj = { | |
date, | |
selected: isSelected(selectedDates, date), | |
selectable: isSelectable(minDate, maxDate, date), | |
today: false, | |
prevMonth: false, | |
nextMonth: true, | |
} | |
dates.push(dateObj) | |
counter++ | |
} | |
} else { | |
// Fill out back week for days from | |
// following month with buffer. | |
while (lastDay < 6) { | |
dates.push('') | |
lastDay++ | |
} | |
} | |
return dates | |
} | |
function requiredProp(fnName, propName) { | |
throw new Error(`The property "${propName}" is required in "${fnName}"`) | |
} | |
function composeEventHandlers(...fns) { | |
return (event, ...args) => | |
fns.some(fn => { | |
fn && fn(event, ...args) | |
return event.defaultPrevented | |
}) | |
} | |
function isBackDisabled({ calendars, minDate }) { | |
if (!minDate) { | |
return false | |
} | |
const { firstDayOfMonth } = calendars[0] | |
const firstDayOfMonthMinusOne = addDays(firstDayOfMonth, -1) | |
if (isBefore(firstDayOfMonthMinusOne, minDate)) { | |
return true | |
} | |
return false | |
} | |
function isForwardDisabled({ calendars, maxDate }) { | |
if (!maxDate) { | |
return false | |
} | |
const { lastDayOfMonth } = calendars[calendars.length - 1] | |
const lastDayOfMonthPlusOne = addDays(lastDayOfMonth, 1) | |
if (isBefore(maxDate, lastDayOfMonthPlusOne)) { | |
return true | |
} | |
return false | |
} | |
function addMonth({ calendars, offset, maxDate }) { | |
if (offset > 1 && maxDate) { | |
const { lastDayOfMonth } = calendars[calendars.length - 1] | |
const diffInMonths = differenceInCalendarMonths(maxDate, lastDayOfMonth) | |
if (diffInMonths < offset) { | |
offset = diffInMonths | |
} | |
} | |
return offset | |
} | |
function subtractMonth({ calendars, offset, minDate }) { | |
if (offset > 1 && minDate) { | |
const { firstDayOfMonth } = calendars[0] | |
const diffInMonths = differenceInCalendarMonths(firstDayOfMonth, minDate) | |
if (diffInMonths < offset) { | |
offset = diffInMonths | |
} | |
} | |
return offset | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment