react-native-ui-datepicker
Version:
Customizable date picker for React Native
471 lines (467 loc) • 15.7 kB
JavaScript
import React, { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
import { I18nManager } from 'react-native';
import { dateToUnix, getEndOfDay, getStartOfDay, areDatesOnSameDay, removeTime } from './utils';
import { CalendarContext } from './calendar-context';
import { CalendarActionKind, CONTAINER_HEIGHT, WEEKDAYS_HEIGHT } from './enums';
import Calendar from './components/calendar';
import { useDeepCompareMemo } from './utils';
import dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData';
import relativeTime from 'dayjs/plugin/relativeTime';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import duration from 'dayjs/plugin/duration';
import { usePrevious } from './hooks/use-previous';
import jalaliday from 'jalali-plugin-dayjs';
dayjs.extend(localeData);
dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(duration);
dayjs.extend(jalaliday);
const DateTimePicker = props => {
const {
mode = 'single',
calendar = 'gregory',
locale = 'en',
numerals = 'latn',
timeZone,
showOutsideDays = false,
timePicker = false,
firstDayOfWeek,
// startYear,
// endYear,
minDate,
maxDate,
enabledDates,
disabledDates,
date,
startDate,
endDate,
dates,
min,
max,
onChange,
initialView = 'day',
containerHeight = CONTAINER_HEIGHT,
weekdaysHeight = WEEKDAYS_HEIGHT,
style = {},
className = '',
classNames = {},
styles = {},
navigationPosition,
weekdaysFormat = 'min',
monthsFormat = 'full',
monthCaptionFormat = 'full',
multiRangeMode,
hideHeader,
hideWeekdays,
disableMonthPicker,
disableYearPicker,
components = {},
month,
year,
onMonthChange = () => {},
onYearChange = () => {},
use12Hours
} = props;
dayjs.tz.setDefault(timeZone);
dayjs.calendar(calendar);
dayjs.locale(locale);
const prevTimezone = usePrevious(timeZone);
const initialCalendarView = useMemo(() => mode !== 'single' && initialView === 'time' ? 'day' : initialView, [mode, initialView]);
const firstDay = useMemo(() => firstDayOfWeek && firstDayOfWeek > 0 && firstDayOfWeek <= 6 ? firstDayOfWeek : 0, [firstDayOfWeek]);
const initialState = useMemo(() => {
let initialDate = dayjs().tz(timeZone);
if (mode === 'single' && date) {
initialDate = dayjs(date);
}
if (mode === 'range' && startDate) {
initialDate = dayjs(startDate);
}
if (mode === 'multiple' && dates && dates.length > 0) {
initialDate = dayjs(dates[0]);
}
if (minDate && initialDate.isBefore(minDate)) {
initialDate = dayjs(minDate);
}
if (month !== undefined && month && month >= 0 && month <= 11) {
initialDate = initialDate.month(month);
}
if (year !== undefined && year >= 0) {
initialDate = initialDate.year(year);
}
let _date = date ? dayjs(date) : date;
if (_date && maxDate && dayjs(_date).isAfter(maxDate)) {
_date = dayjs(maxDate);
}
if (_date && minDate && dayjs(_date).isBefore(minDate)) {
_date = dayjs(minDate);
}
let start = startDate ? dayjs(startDate) : startDate;
if (start && maxDate && dayjs(start).isAfter(maxDate)) {
start = dayjs(maxDate);
}
if (start && minDate && dayjs(start).isBefore(minDate)) {
start = dayjs(minDate);
}
let end = endDate ? dayjs(endDate) : endDate;
if (end && maxDate && dayjs(end).isAfter(maxDate)) {
end = dayjs(maxDate);
}
if (end && minDate && dayjs(end).isBefore(minDate)) {
end = dayjs(minDate);
}
return {
date: _date,
startDate: start,
endDate: end,
dates,
calendarView: initialCalendarView,
currentDate: initialDate,
currentYear: initialDate.year(),
isRTL: calendar === 'jalali' || I18nManager.isRTL
};
}, [mode, calendar, date, startDate, endDate, dates, minDate, maxDate, month, year, timeZone, initialCalendarView]);
const [state, dispatch] = useReducer((prevState, action) => {
switch (action.type) {
case CalendarActionKind.SET_CALENDAR_VIEW:
return {
...prevState,
calendarView: action.payload
};
case CalendarActionKind.CHANGE_CURRENT_DATE:
return {
...prevState,
currentDate: action.payload
};
case CalendarActionKind.CHANGE_CURRENT_YEAR:
return {
...prevState,
currentYear: action.payload
};
case CalendarActionKind.CHANGE_SELECTED_DATE:
const {
date: selectedDate
} = action.payload;
return {
...prevState,
date: selectedDate,
currentDate: selectedDate
};
case CalendarActionKind.CHANGE_SELECTED_RANGE:
const {
startDate: start,
endDate: end
} = action.payload;
return {
...prevState,
startDate: start,
endDate: end
};
case CalendarActionKind.CHANGE_SELECTED_MULTIPLE:
const {
dates: selectedDates
} = action.payload;
return {
...prevState,
dates: selectedDates
};
case CalendarActionKind.SET_IS_RTL:
return {
...prevState,
isRTL: action.payload
};
case CalendarActionKind.RESET_STATE:
return action.payload;
default:
return prevState;
}
}, initialState);
const stateRef = useRef(state);
stateRef.current = state;
useEffect(() => {
const newState = {
...initialState,
isRTL: calendar === 'jalali' || I18nManager.isRTL
};
dispatch({
type: CalendarActionKind.RESET_STATE,
payload: newState
});
}, [calendar]);
useEffect(() => {
if (prevTimezone !== timeZone) {
const newDate = dayjs().tz(timeZone);
dispatch({
type: CalendarActionKind.CHANGE_CURRENT_DATE,
payload: newDate
});
}
}, [timeZone, prevTimezone]);
useEffect(() => {
if (mode === 'single') {
let _date = (date && (timePicker ? dayjs.tz(date, timeZone) : getStartOfDay(dayjs.tz(date, timeZone)))) ?? date;
if (_date && maxDate && dayjs.tz(_date, timeZone).isAfter(maxDate)) {
_date = dayjs.tz(maxDate, timeZone);
}
if (_date && minDate && dayjs.tz(_date, timeZone).isBefore(minDate)) {
_date = dayjs.tz(minDate, timeZone);
}
dispatch({
type: CalendarActionKind.CHANGE_SELECTED_DATE,
payload: {
date: _date
}
});
if (prevTimezone !== timeZone) {
onChange({
date: _date ? dayjs(_date).toDate() : _date
});
}
} else if (mode === 'range') {
let start = startDate ? dayjs.tz(startDate, timeZone) : startDate;
if (start && maxDate && dayjs.tz(start, timeZone).isAfter(maxDate)) {
start = dayjs.tz(maxDate, timeZone);
}
if (start && minDate && dayjs.tz(start, timeZone).isBefore(minDate)) {
start = dayjs.tz(minDate, timeZone);
}
let end = endDate ? dayjs.tz(endDate, timeZone) : endDate;
if (end && maxDate && dayjs.tz(end, timeZone).isAfter(maxDate)) {
end = dayjs.tz(maxDate, timeZone);
}
if (end && minDate && dayjs.tz(end, timeZone).isBefore(minDate)) {
end = dayjs.tz(minDate, timeZone);
}
dispatch({
type: CalendarActionKind.CHANGE_SELECTED_RANGE,
payload: {
startDate: start,
endDate: end
}
});
if (prevTimezone !== timeZone) {
onChange({
startDate: start ? dayjs(start).toDate() : start,
endDate: end ? dayjs(end).toDate() : end
});
}
} else if (mode === 'multiple') {
const _dates = dates === null || dates === void 0 ? void 0 : dates.map(date => dayjs(date).tz(timeZone));
dispatch({
type: CalendarActionKind.CHANGE_SELECTED_MULTIPLE,
payload: {
dates: _dates
}
});
if (prevTimezone !== timeZone) {
onChange({
dates: _dates.map(item => dayjs(item).toDate()),
change: 'updated'
});
}
}
}, [mode, date, startDate, endDate, dates, minDate, maxDate, timePicker, prevTimezone, timeZone, calendar]);
const setCalendarView = useCallback(view => {
dispatch({
type: CalendarActionKind.SET_CALENDAR_VIEW,
payload: view
});
}, []);
const onSelectDate = useCallback(selectedDate => {
if (onChange) {
if (mode === 'single') {
const newDate = timePicker ? dayjs.tz(selectedDate, timeZone) : dayjs.tz(getStartOfDay(selectedDate), timeZone);
dispatch({
type: CalendarActionKind.CHANGE_CURRENT_DATE,
payload: newDate
});
onChange({
date: newDate ? dayjs(newDate).toDate() : newDate
});
} else if (mode === 'range') {
// set time to 00:00:00
let start = removeTime(stateRef.current.startDate, timeZone);
let end = removeTime(stateRef.current.endDate, timeZone);
const selected = removeTime(selectedDate, timeZone);
let isStart = true;
let isReset = false;
if (dateToUnix(selected) !== dateToUnix(end) && dateToUnix(selected) >= dateToUnix(start) && dateToUnix(start) !== dateToUnix(end)) {
isStart = false;
} else if (start && dateToUnix(selected) === dateToUnix(start)) {
isReset = true;
}
if (start && end) {
if (dateToUnix(start) === dateToUnix(end) && dateToUnix(selected) > dateToUnix(start)) {
isStart = false;
}
if (dateToUnix(selected) > dateToUnix(start) && dateToUnix(selected) === dateToUnix(end)) {
end = undefined;
}
}
if (start && !end && dateToUnix(selected) < dateToUnix(start)) {
end = start;
}
if (isStart && end && (min || max)) {
const numberOfDays = dayjs(end).diff(selected, 'day');
if (max && numberOfDays > max || min && numberOfDays < min) {
isStart = true;
end = undefined;
}
}
if (!isStart && start && (min || max)) {
const numberOfDays = dayjs(selected).diff(start, 'day');
if (dateToUnix(selected) === dateToUnix(start)) {
isReset = true;
} else if (max && numberOfDays > max || min && numberOfDays < min) {
isStart = true;
end = undefined;
}
}
if (isReset) {
onChange({
startDate: undefined,
endDate: undefined
});
} else {
onChange({
startDate: isStart ? dayjs(selected).toDate() : start ? dayjs.tz(start).toDate() : start,
endDate: !isStart ? dayjs.tz(getEndOfDay(selected), timeZone).toDate() : end ? dayjs.tz(getEndOfDay(end), timeZone).toDate() : end
});
}
} else if (mode === 'multiple') {
const safeDates = stateRef.current.dates || [];
const newDate = dayjs(selectedDate, timeZone).startOf('day');
const exists = safeDates.some(ed => areDatesOnSameDay(ed, newDate));
const newDates = exists ? safeDates.filter(ed => !areDatesOnSameDay(ed, newDate)) : [...safeDates, newDate];
if (max && newDates.length > max) {
return;
}
newDates.sort((a, b) => dayjs(a).isAfter(dayjs(b)) ? 1 : -1);
const _dates = newDates.map(date => dayjs(date).tz(timeZone));
onChange({
dates: _dates.map(item => dayjs(item).toDate()),
datePressed: newDate ? dayjs(newDate).toDate() : newDate,
change: exists ? 'removed' : 'added'
});
}
}
}, [mode, timePicker, min, max, timeZone]);
// set the active displayed month
const onSelectMonth = useCallback(value => {
const currentMonth = dayjs(stateRef.current.currentDate).month();
const newDate = dayjs(stateRef.current.currentDate).month(value);
// Only call onMonthChange if the month actually changed
if (value !== currentMonth) {
onMonthChange(value);
}
dispatch({
type: CalendarActionKind.CHANGE_CURRENT_DATE,
payload: newDate
});
setCalendarView('day');
}, [setCalendarView, onMonthChange]);
// set the active displayed year
const onSelectYear = useCallback(value => {
const currentYear = dayjs(stateRef.current.currentDate).year();
const newDate = dayjs(stateRef.current.currentDate).year(value);
// Only call onYearChange if the year actually changed
if (value !== currentYear) {
onYearChange(value);
}
dispatch({
type: CalendarActionKind.CHANGE_CURRENT_DATE,
payload: newDate
});
setCalendarView('day');
}, [setCalendarView, onYearChange]);
const onChangeMonth = useCallback(value => {
const newDate = dayjs(stateRef.current.currentDate).add(value, 'month');
dispatch({
type: CalendarActionKind.CHANGE_CURRENT_DATE,
payload: dayjs(newDate)
});
}, [stateRef, dispatch]);
const onChangeYear = useCallback(value => {
dispatch({
type: CalendarActionKind.CHANGE_CURRENT_YEAR,
payload: value
});
}, [dispatch]);
useEffect(() => {
if (month !== undefined && month >= 0 && month <= 11) {
onSelectMonth(month);
}
}, [month]);
useEffect(() => {
if (year !== undefined && year >= 0) {
onSelectYear(year);
}
}, [year]);
const memoizedStyles = useDeepCompareMemo({
...styles
}, [styles]);
const memoizedClassNames = useDeepCompareMemo({
...classNames
}, [classNames]);
const memoizedComponents = useMemo(() => ({
...components
}), [components]);
const baseContextValue = useMemo(() => ({
mode,
calendar,
locale,
numerals,
timeZone,
showOutsideDays,
timePicker,
minDate,
maxDate,
min,
max,
enabledDates,
disabledDates,
firstDayOfWeek: firstDay,
containerHeight,
weekdaysHeight,
navigationPosition,
weekdaysFormat,
monthsFormat,
monthCaptionFormat,
multiRangeMode,
hideHeader,
hideWeekdays,
disableMonthPicker,
disableYearPicker,
style,
className,
use12Hours
}), [mode, calendar, locale, numerals, timeZone, showOutsideDays, timePicker, minDate, maxDate, min, max, enabledDates, disabledDates, firstDay, containerHeight, weekdaysHeight, navigationPosition, weekdaysFormat, monthsFormat, monthCaptionFormat, multiRangeMode, hideHeader, hideWeekdays, disableMonthPicker, disableYearPicker, style, className, use12Hours]);
const handlerContextValue = useMemo(() => ({
setCalendarView,
onSelectDate,
onSelectMonth,
onSelectYear,
onChangeMonth,
onChangeYear
}), [setCalendarView, onSelectDate, onSelectMonth, onSelectYear, onChangeMonth, onChangeYear]);
const styleContextValue = useMemo(() => ({
classNames: memoizedClassNames,
styles: memoizedStyles
}), [memoizedClassNames, memoizedStyles]);
const memoizedValue = useMemo(() => ({
...state,
...baseContextValue,
...handlerContextValue,
...styleContextValue,
components: memoizedComponents
}), [state, baseContextValue, handlerContextValue, styleContextValue, memoizedComponents]);
return /*#__PURE__*/React.createElement(CalendarContext.Provider, {
value: memoizedValue
}, /*#__PURE__*/React.createElement(Calendar, null));
};
export default DateTimePicker;
//# sourceMappingURL=datetime-picker.js.map