@chayns-components/date
Version:
A set of beautiful React components for developing your own applications with chayns.
306 lines (305 loc) • 12.1 kB
JavaScript
import { ComboBox, Icon } from '@chayns-components/core';
import { Language } from 'chayns-api';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CalendarType } from '../../types/calendar';
import { getNewDate, getYearsBetween, isDateInRange } from '../../utils/calendar';
import { addYears, differenceInCalendarMonths, isSameDay, isSameMonth, isWithinInterval, subYears } from '../../utils/date';
import { StyledCalendar, StyledCalendarIconWrapper, StyledCalendarIconWrapperPseudo, StyledPseudoMonthYearPicker } from './Calendar.styles';
import MonthWrapper from './month-wrapper/MonthWrapper';
const DEFAULT_MAX_DATE = addYears(new Date(), 1);
const DEFAULT_MIN_DATE = subYears(new Date(), 1);
const Calendar = _ref => {
let {
locale = Language.German,
maxDate = DEFAULT_MAX_DATE,
minDate = DEFAULT_MIN_DATE,
highlightedDates,
onChange,
customThumbColors,
selectedDate,
selectedDates,
selectedDateInterval,
categories,
isDisabled,
type = CalendarType.Single,
shouldShowHighlightsInMonthOverlay = true,
disabledDates = [],
showMonthYearPickers: showMonthYearPickersProp,
onShownDatesChange = () => {}
} = _ref;
const [currentDate, setCurrentDate] = useState();
const [shouldRenderTwoMonths, setShouldRenderTwoMonths] = useState(true);
const [internalSelectedDate, setInternalSelectedDate] = useState(() => type === CalendarType.Multiple ? [] : undefined);
const [direction, setDirection] = useState();
const [width, setWidth] = useState(0);
const showMonthYearPickers = useMemo(() => {
const hasMultipleMonths = differenceInCalendarMonths(maxDate, minDate) > 0;
const hasMultipleYears = getYearsBetween(minDate, maxDate).length > 1;
return !!(showMonthYearPickersProp && (hasMultipleMonths || hasMultipleYears));
}, [minDate, maxDate, showMonthYearPickersProp]);
const calendarRef = useRef(null);
useEffect(() => {
if (currentDate) {
const start = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
if (shouldRenderTwoMonths) {
const end = new Date(currentDate.getFullYear(), currentDate.getMonth() + 2, 0);
onShownDatesChange({
start,
end
});
} else {
const end = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0);
onShownDatesChange({
start,
end
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentDate, shouldRenderTwoMonths]);
useEffect(() => {
const bounds = {
start: minDate,
end: maxDate
};
if (type === CalendarType.Single) {
if (selectedDate) {
const isDisabledDate = disabledDates.some(disabledDate => isSameDay(selectedDate, disabledDate));
const isDateInBounds = isWithinInterval(selectedDate, bounds);
if (!isDisabledDate && isDateInBounds) {
setInternalSelectedDate(selectedDate);
} else {
console.warn('[@chayns-components/date] Warning: Failed to set selectedDate, because it is disabled or out of bounds.', '\nselectedDate:', selectedDate, ...(isDisabledDate ? ['\nselectedDate is disabled'] : []), ...(isDateInBounds ? [] : ['\nselectedDate is outside of bounds:', {
minDate,
maxDate
}]));
setInternalSelectedDate(() => undefined);
}
} else {
setInternalSelectedDate(() => undefined);
}
} else if (type === CalendarType.Multiple) {
if (selectedDates) {
const disabledSelectedDates = [];
const datesOutsideOfBounds = [];
const filteredDates = selectedDates.filter(date => {
if (disabledDates.some(disabledDate => isSameDay(date, disabledDate))) {
disabledSelectedDates.push(date);
return false;
}
if (!isWithinInterval(date, bounds)) {
datesOutsideOfBounds.push(date);
return false;
}
return true;
});
if (disabledSelectedDates.length > 0 || datesOutsideOfBounds.length > 0) {
console.warn('[@chayns-components/date] Warning: Failed to set all selectedDates, because some are disabled or out of bounds.', ...(disabledSelectedDates.length > 0 ? ['\nselectedDates that are disabled:', disabledSelectedDates] : []), ...(datesOutsideOfBounds.length > 0 ? ['\nselectedDates that are outside of bounds:', datesOutsideOfBounds, 'bounds:', {
minDate,
maxDate
}] : []));
}
setInternalSelectedDate(filteredDates);
} else {
setInternalSelectedDate([]);
}
} else if (type === CalendarType.Interval) {
if (selectedDateInterval) {
const intervalIncludesDisabledDate = selectedDateInterval.end && disabledDates.some(disabledDate => isWithinInterval(disabledDate, {
start: selectedDateInterval.start,
end: selectedDateInterval.end
}));
const intervalIsInBounds = isWithinInterval(selectedDateInterval.start, bounds) && (!selectedDateInterval.end || isWithinInterval(selectedDateInterval.end, bounds));
if (!intervalIncludesDisabledDate && intervalIsInBounds) {
setInternalSelectedDate(selectedDateInterval);
} else {
console.warn('[@chayns-components/date] Warning: Failed to set selectedDateInterval, because it includes disabled dates or dates that are out of bounds.', '\nselectedDateInterval:', selectedDateInterval, ...(intervalIncludesDisabledDate ? ['\ndisabled dates:', disabledDates] : []), ...(intervalIsInBounds ? [] : ['\nbounds:', {
minDate,
maxDate
}]));
setInternalSelectedDate(() => undefined);
}
}
}
}, [type, selectedDate, selectedDates, selectedDateInterval, disabledDates, minDate, maxDate]);
useEffect(() => {
if (calendarRef.current) {
const resizeObserver = new ResizeObserver(entries => {
if (entries && entries[0]) {
const observedWidth = entries[0].contentRect.width;
setWidth(observedWidth - 30);
if (observedWidth < 430) {
setShouldRenderTwoMonths(false);
} else {
setShouldRenderTwoMonths(true);
}
}
});
resizeObserver.observe(calendarRef.current);
return () => {
resizeObserver.disconnect();
};
}
return () => {};
}, []);
useEffect(() => {
setCurrentDate(prevDate => isDateInRange({
minDate,
maxDate,
currentDate: prevDate || new Date()
}));
}, [maxDate, minDate]);
const handleLeftArrowClick = useCallback(() => {
if (direction) return;
setDirection('left');
setCurrentDate(prevDate => {
if (!prevDate) {
return prevDate;
}
const newDate = getNewDate(-1, prevDate);
return isDateInRange({
minDate,
maxDate,
currentDate: newDate
});
});
}, [maxDate, minDate, direction]);
const handleRightArrowClick = useCallback(() => {
if (direction) return;
setDirection('right');
setCurrentDate(prevDate => {
if (!prevDate) {
return prevDate;
}
const newDate = getNewDate(1, prevDate);
return isDateInRange({
minDate,
maxDate,
currentDate: newDate
});
});
}, [maxDate, minDate, direction]);
const handleSelect = useCallback(date => {
setInternalSelectedDate(prevDate => {
let onChangePayload = null;
let newInternalSelectedDate;
if (type === CalendarType.Single) {
onChangePayload = date;
newInternalSelectedDate = date;
} else if (type === CalendarType.Multiple) {
const prevSelectedDates = prevDate;
// Selects or unselects date , depending on if it is already selected.
if (prevSelectedDates.some(d => isSameDay(d, date))) {
newInternalSelectedDate = prevSelectedDates.filter(d => !isSameDay(d, date));
} else {
newInternalSelectedDate = [...prevSelectedDates, date];
}
onChangePayload = newInternalSelectedDate;
} else if (type === CalendarType.Interval) {
const prevSelectedDateInterval = prevDate;
const updateInterval = (start, end) => {
const newInterval = {
start,
end
};
onChangePayload = newInterval;
newInternalSelectedDate = newInterval;
};
// Sets first selection as interval start.
if (!prevSelectedDateInterval) {
updateInterval(date);
} else if (prevSelectedDateInterval.start && !prevSelectedDateInterval.end) {
// Sets second selection as interval start, if it is earlier than the previous interval start.
// Else sets it as interval end.
if (date < prevSelectedDateInterval.start) {
updateInterval(date);
} else {
updateInterval(prevSelectedDateInterval.start, date);
}
} else {
// Resets interval if a third date is selected.
updateInterval(date);
}
}
if (typeof onChange === 'function' && onChangePayload) {
onChange(onChangePayload);
}
return newInternalSelectedDate;
});
}, [type, onChange]);
const handleAnimationFinished = () => {
setDirection(() => undefined);
};
const ShouldShowLeftArrow = useMemo(() => {
if (!currentDate) {
return false;
}
return !isSameMonth(currentDate, minDate);
}, [currentDate, minDate]);
const ShouldShowRightArrow = useMemo(() => {
if (!currentDate) {
return false;
}
return !isSameMonth(currentDate, maxDate);
}, [currentDate, maxDate]);
return /*#__PURE__*/React.createElement(StyledCalendar, {
ref: calendarRef,
$isDisabled: isDisabled
}, ShouldShowLeftArrow ? /*#__PURE__*/React.createElement(StyledCalendarIconWrapper, {
onClick: handleLeftArrowClick
}, /*#__PURE__*/React.createElement("div", {
// TODO Use styled-components instead of inline styles
style: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
height: 'fit-content'
}
}, showMonthYearPickers && /*#__PURE__*/React.createElement(StyledPseudoMonthYearPicker, null, /*#__PURE__*/React.createElement(ComboBox, {
lists: [{
list: []
}],
placeholder: ""
})), /*#__PURE__*/React.createElement(Icon, {
icons: ['fa fa-angle-left']
}))) : /*#__PURE__*/React.createElement(StyledCalendarIconWrapperPseudo, null), currentDate && /*#__PURE__*/React.createElement(MonthWrapper, {
shouldRenderTwo: shouldRenderTwoMonths,
currentDate: currentDate,
width: width,
locale: locale,
direction: direction,
customThumbColors: customThumbColors,
onSelect: handleSelect,
selectedDate: internalSelectedDate,
highlightedDates: highlightedDates,
categories: categories,
onAnimationFinished: handleAnimationFinished,
minDate: minDate,
maxDate: maxDate,
type: type,
disabledDates: disabledDates,
setCurrentDate: setCurrentDate,
shouldShowHighlightsInMonthOverlay: shouldShowHighlightsInMonthOverlay,
showMonthYearPickers: showMonthYearPickers
}), ShouldShowRightArrow ? /*#__PURE__*/React.createElement(StyledCalendarIconWrapper, {
onClick: handleRightArrowClick
}, /*#__PURE__*/React.createElement("div", {
// TODO Use styled-components instead of inline styles
style: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
height: 'fit-content'
}
}, showMonthYearPickers && /*#__PURE__*/React.createElement(StyledPseudoMonthYearPicker, null, /*#__PURE__*/React.createElement(ComboBox, {
lists: [{
list: []
}],
placeholder: ""
})), /*#__PURE__*/React.createElement(Icon, {
icons: ['fa fa-angle-right']
}))) : /*#__PURE__*/React.createElement(StyledCalendarIconWrapperPseudo, null));
};
Calendar.displayName = 'Calendar';
export default Calendar;
//# sourceMappingURL=Calendar.js.map