@@ -0,0 +1,429 @@
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
}