react-calendar
Version:
Ultimate calendar for your React app.
443 lines (442 loc) • 17.7 kB
JavaScript
'use client';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
import clsx from 'clsx';
import Navigation from './Calendar/Navigation.js';
import CenturyView from './CenturyView.js';
import DecadeView from './DecadeView.js';
import MonthView from './MonthView.js';
import YearView from './YearView.js';
import { getBegin, getBeginNext, getEnd, getValueRange } from './shared/dates.js';
import { between } from './shared/utils.js';
const baseClassName = 'react-calendar';
const allViews = ['century', 'decade', 'year', 'month'];
const allValueTypes = ['decade', 'year', 'month', 'day'];
const defaultMinDate = new Date();
defaultMinDate.setFullYear(1, 0, 1);
defaultMinDate.setHours(0, 0, 0, 0);
const defaultMaxDate = new Date(8.64e15);
function toDate(value) {
if (value instanceof Date) {
return value;
}
return new Date(value);
}
/**
* Returns views array with disallowed values cut off.
*/
function getLimitedViews(minDetail, maxDetail) {
return allViews.slice(allViews.indexOf(minDetail), allViews.indexOf(maxDetail) + 1);
}
/**
* Determines whether a given view is allowed with currently applied settings.
*/
function isViewAllowed(view, minDetail, maxDetail) {
const views = getLimitedViews(minDetail, maxDetail);
return views.indexOf(view) !== -1;
}
/**
* Gets either provided view if allowed by minDetail and maxDetail, or gets
* the default view if not allowed.
*/
function getView(view, minDetail, maxDetail) {
if (!view) {
return maxDetail;
}
if (isViewAllowed(view, minDetail, maxDetail)) {
return view;
}
return maxDetail;
}
/**
* Returns value type that can be returned with currently applied settings.
*/
function getValueType(view) {
const index = allViews.indexOf(view);
return allValueTypes[index];
}
function getValue(value, index) {
const rawValue = Array.isArray(value) ? value[index] : value;
if (!rawValue) {
return null;
}
const valueDate = toDate(rawValue);
if (Number.isNaN(valueDate.getTime())) {
throw new Error(`Invalid date: ${value}`);
}
return valueDate;
}
function getDetailValue({ value, minDate, maxDate, maxDetail }, index) {
const valuePiece = getValue(value, index);
if (!valuePiece) {
return null;
}
const valueType = getValueType(maxDetail);
const detailValueFrom = (() => {
switch (index) {
case 0:
return getBegin(valueType, valuePiece);
case 1:
return getEnd(valueType, valuePiece);
default:
throw new Error(`Invalid index value: ${index}`);
}
})();
return between(detailValueFrom, minDate, maxDate);
}
const getDetailValueFrom = (args) => getDetailValue(args, 0);
const getDetailValueTo = (args) => getDetailValue(args, 1);
const getDetailValueArray = (args) => [getDetailValueFrom, getDetailValueTo].map((fn) => fn(args));
function getActiveStartDate({ maxDate, maxDetail, minDate, minDetail, value, view, }) {
const rangeType = getView(view, minDetail, maxDetail);
const valueFrom = getDetailValueFrom({
value,
minDate,
maxDate,
maxDetail,
}) || new Date();
return getBegin(rangeType, valueFrom);
}
function getInitialActiveStartDate({ activeStartDate, defaultActiveStartDate, defaultValue, defaultView, maxDate, maxDetail, minDate, minDetail, value, view, }) {
const rangeType = getView(view, minDetail, maxDetail);
const valueFrom = activeStartDate || defaultActiveStartDate;
if (valueFrom) {
return getBegin(rangeType, valueFrom);
}
return getActiveStartDate({
maxDate,
maxDetail,
minDate,
minDetail,
value: value || defaultValue,
view: view || defaultView,
});
}
function getIsSingleValue(value) {
return value && (!Array.isArray(value) || value.length === 1);
}
function areDatesEqual(date1, date2) {
return date1 instanceof Date && date2 instanceof Date && date1.getTime() === date2.getTime();
}
const Calendar = forwardRef(function Calendar(props, ref) {
const { activeStartDate: activeStartDateProps, allowPartialRange, calendarType, className, 'data-testid': dataTestId, defaultActiveStartDate, defaultValue, defaultView, formatDay, formatLongDate, formatMonth, formatMonthYear, formatShortWeekday, formatWeekday, formatYear, goToRangeStartOnSelect = true, inputRef, locale, maxDate = defaultMaxDate, maxDetail = 'month', minDate = defaultMinDate, minDetail = 'century', navigationAriaLabel, navigationAriaLive, navigationLabel, next2AriaLabel, next2Label, nextAriaLabel, nextLabel, onActiveStartDateChange, onChange: onChangeProps, onClickDay, onClickDecade, onClickMonth, onClickWeekNumber, onClickYear, onDrillDown, onDrillUp, onViewChange, prev2AriaLabel, prev2Label, prevAriaLabel, prevLabel, returnValue = 'start', selectRange, showDoubleView, showFixedNumberOfWeeks, showNavigation = true, showNeighboringCentury, showNeighboringDecade, showNeighboringMonth = true, showWeekNumbers, tileClassName, tileContent, tileDisabled, value: valueProps, view: viewProps, } = props;
const [activeStartDateState, setActiveStartDateState] = useState(defaultActiveStartDate);
const [hoverState, setHoverState] = useState(null);
const [valueState, setValueState] = useState(Array.isArray(defaultValue)
? defaultValue.map((el) => (el !== null ? toDate(el) : null))
: defaultValue !== null && defaultValue !== undefined
? toDate(defaultValue)
: null);
const [viewState, setViewState] = useState(defaultView);
const activeStartDate = activeStartDateProps ||
activeStartDateState ||
getInitialActiveStartDate({
activeStartDate: activeStartDateProps,
defaultActiveStartDate,
defaultValue,
defaultView,
maxDate,
maxDetail,
minDate,
minDetail,
value: valueProps,
view: viewProps,
});
const value = (() => {
const rawValue = (() => {
// In the middle of range selection, use value from state
if (selectRange && getIsSingleValue(valueState)) {
return valueState;
}
return valueProps !== undefined ? valueProps : valueState;
})();
if (!rawValue) {
return null;
}
return Array.isArray(rawValue)
? rawValue.map((el) => (el !== null ? toDate(el) : null))
: rawValue !== null
? toDate(rawValue)
: null;
})();
const valueType = getValueType(maxDetail);
const view = getView(viewProps || viewState, minDetail, maxDetail);
const views = getLimitedViews(minDetail, maxDetail);
const hover = selectRange ? hoverState : null;
const drillDownAvailable = views.indexOf(view) < views.length - 1;
const drillUpAvailable = views.indexOf(view) > 0;
const getProcessedValue = useCallback((value) => {
const processFunction = (() => {
switch (returnValue) {
case 'start':
return getDetailValueFrom;
case 'end':
return getDetailValueTo;
case 'range':
return getDetailValueArray;
default:
throw new Error('Invalid returnValue.');
}
})();
return processFunction({
maxDate,
maxDetail,
minDate,
value,
});
}, [maxDate, maxDetail, minDate, returnValue]);
const setActiveStartDate = useCallback((nextActiveStartDate, action) => {
setActiveStartDateState(nextActiveStartDate);
const args = {
action,
activeStartDate: nextActiveStartDate,
value,
view,
};
if (onActiveStartDateChange && !areDatesEqual(activeStartDate, nextActiveStartDate)) {
onActiveStartDateChange(args);
}
}, [activeStartDate, onActiveStartDateChange, value, view]);
const onClickTile = useCallback((value, event) => {
const callback = (() => {
switch (view) {
case 'century':
return onClickDecade;
case 'decade':
return onClickYear;
case 'year':
return onClickMonth;
case 'month':
return onClickDay;
default:
throw new Error(`Invalid view: ${view}.`);
}
})();
if (callback)
callback(value, event);
}, [onClickDay, onClickDecade, onClickMonth, onClickYear, view]);
const drillDown = useCallback((nextActiveStartDate, event) => {
if (!drillDownAvailable) {
return;
}
onClickTile(nextActiveStartDate, event);
const nextView = views[views.indexOf(view) + 1];
if (!nextView) {
throw new Error('Attempted to drill down from the lowest view.');
}
setActiveStartDateState(nextActiveStartDate);
setViewState(nextView);
const args = {
action: 'drillDown',
activeStartDate: nextActiveStartDate,
value,
view: nextView,
};
if (onActiveStartDateChange && !areDatesEqual(activeStartDate, nextActiveStartDate)) {
onActiveStartDateChange(args);
}
if (onViewChange && view !== nextView) {
onViewChange(args);
}
if (onDrillDown) {
onDrillDown(args);
}
}, [
activeStartDate,
drillDownAvailable,
onActiveStartDateChange,
onClickTile,
onDrillDown,
onViewChange,
value,
view,
views,
]);
const drillUp = useCallback(() => {
if (!drillUpAvailable) {
return;
}
const nextView = views[views.indexOf(view) - 1];
if (!nextView) {
throw new Error('Attempted to drill up from the highest view.');
}
const nextActiveStartDate = getBegin(nextView, activeStartDate);
setActiveStartDateState(nextActiveStartDate);
setViewState(nextView);
const args = {
action: 'drillUp',
activeStartDate: nextActiveStartDate,
value,
view: nextView,
};
if (onActiveStartDateChange && !areDatesEqual(activeStartDate, nextActiveStartDate)) {
onActiveStartDateChange(args);
}
if (onViewChange && view !== nextView) {
onViewChange(args);
}
if (onDrillUp) {
onDrillUp(args);
}
}, [
activeStartDate,
drillUpAvailable,
onActiveStartDateChange,
onDrillUp,
onViewChange,
value,
view,
views,
]);
const onChange = useCallback((rawNextValue, event) => {
const previousValue = value;
onClickTile(rawNextValue, event);
const isFirstValueInRange = selectRange && !getIsSingleValue(previousValue);
let nextValue;
if (selectRange) {
// Range selection turned on
if (isFirstValueInRange) {
// Value has 0 or 2 elements - either way we're starting a new array
// First value
nextValue = getBegin(valueType, rawNextValue);
}
else {
if (!previousValue) {
throw new Error('previousValue is required');
}
if (Array.isArray(previousValue)) {
throw new Error('previousValue must not be an array');
}
// Second value
nextValue = getValueRange(valueType, previousValue, rawNextValue);
}
}
else {
// Range selection turned off
nextValue = getProcessedValue(rawNextValue);
}
const nextActiveStartDate =
// Range selection turned off
!selectRange ||
// Range selection turned on, first value
isFirstValueInRange ||
// Range selection turned on, second value, goToRangeStartOnSelect toggled on
goToRangeStartOnSelect
? getActiveStartDate({
maxDate,
maxDetail,
minDate,
minDetail,
value: nextValue,
view,
})
: null;
event.persist();
setActiveStartDateState(nextActiveStartDate);
setValueState(nextValue);
const args = {
action: 'onChange',
activeStartDate: nextActiveStartDate,
value: nextValue,
view,
};
if (onActiveStartDateChange && !areDatesEqual(activeStartDate, nextActiveStartDate)) {
onActiveStartDateChange(args);
}
if (onChangeProps) {
if (selectRange) {
const isSingleValue = getIsSingleValue(nextValue);
if (!isSingleValue) {
onChangeProps(nextValue || null, event);
}
else if (allowPartialRange) {
if (Array.isArray(nextValue)) {
throw new Error('value must not be an array');
}
onChangeProps([nextValue || null, null], event);
}
}
else {
onChangeProps(nextValue || null, event);
}
}
}, [
activeStartDate,
allowPartialRange,
getProcessedValue,
goToRangeStartOnSelect,
maxDate,
maxDetail,
minDate,
minDetail,
onActiveStartDateChange,
onChangeProps,
onClickTile,
selectRange,
value,
valueType,
view,
]);
function onMouseOver(nextHover) {
setHoverState(nextHover);
}
function onMouseLeave() {
setHoverState(null);
}
useImperativeHandle(ref, () => ({
activeStartDate,
drillDown,
drillUp,
onChange,
setActiveStartDate,
value,
view,
}), [activeStartDate, drillDown, drillUp, onChange, setActiveStartDate, value, view]);
function renderContent(next) {
const currentActiveStartDate = next
? getBeginNext(view, activeStartDate)
: getBegin(view, activeStartDate);
const onClick = drillDownAvailable ? drillDown : onChange;
const commonProps = {
activeStartDate: currentActiveStartDate,
hover,
locale,
maxDate,
minDate,
onClick,
onMouseOver: selectRange ? onMouseOver : undefined,
tileClassName,
tileContent,
tileDisabled,
value,
valueType,
};
switch (view) {
case 'century': {
return (_jsx(CenturyView, { formatYear: formatYear, showNeighboringCentury: showNeighboringCentury, ...commonProps }));
}
case 'decade': {
return (_jsx(DecadeView, { formatYear: formatYear, showNeighboringDecade: showNeighboringDecade, ...commonProps }));
}
case 'year': {
return (_jsx(YearView, { formatMonth: formatMonth, formatMonthYear: formatMonthYear, ...commonProps }));
}
case 'month': {
return (_jsx(MonthView, { calendarType: calendarType, formatDay: formatDay, formatLongDate: formatLongDate, formatShortWeekday: formatShortWeekday, formatWeekday: formatWeekday, onClickWeekNumber: onClickWeekNumber, onMouseLeave: selectRange ? onMouseLeave : undefined, showFixedNumberOfWeeks: typeof showFixedNumberOfWeeks !== 'undefined'
? showFixedNumberOfWeeks
: showDoubleView, showNeighboringMonth: showNeighboringMonth, showWeekNumbers: showWeekNumbers, ...commonProps }));
}
default:
throw new Error(`Invalid view: ${view}.`);
}
}
function renderNavigation() {
if (!showNavigation) {
return null;
}
return (_jsx(Navigation, { activeStartDate: activeStartDate, drillUp: drillUp, formatMonthYear: formatMonthYear, formatYear: formatYear, locale: locale, maxDate: maxDate, minDate: minDate, navigationAriaLabel: navigationAriaLabel, navigationAriaLive: navigationAriaLive, navigationLabel: navigationLabel, next2AriaLabel: next2AriaLabel, next2Label: next2Label, nextAriaLabel: nextAriaLabel, nextLabel: nextLabel, prev2AriaLabel: prev2AriaLabel, prev2Label: prev2Label, prevAriaLabel: prevAriaLabel, prevLabel: prevLabel, setActiveStartDate: setActiveStartDate, showDoubleView: showDoubleView, view: view, views: views }));
}
const valueArray = Array.isArray(value) ? value : [value];
return (_jsxs("div", { className: clsx(baseClassName, selectRange && valueArray.length === 1 && `${baseClassName}--selectRange`, showDoubleView && `${baseClassName}--doubleView`, className), "data-testid": dataTestId, ref: inputRef, children: [renderNavigation(), _jsxs("div", { className: `${baseClassName}__viewContainer`, onBlur: selectRange ? onMouseLeave : undefined, onMouseLeave: selectRange ? onMouseLeave : undefined, children: [renderContent(), showDoubleView ? renderContent(true) : null] })] }));
});
export default Calendar;