@vertisanpro/flowbite-react
Version:
Non-Official React components built for Flowbite and Tailwind CSS
153 lines (152 loc) • 8.42 kB
JavaScript
'use client';
import { HiArrowLeft, HiArrowRight, HiCalendar } from '@vertisanpro/react-icons/hi';
import { twMerge } from '@vertisanpro/tailwind-merge';
import React, { useEffect, useRef, useState } from 'react';
import { mergeDeep } from '../../helpers/merge-deep';
import { getTheme } from '../../theme-store';
import { TextInput } from '../TextInput';
import { DatepickerContext } from './DatepickerContext';
import { DatepickerViewsDays } from './Views/Days';
import { DatepickerViewsDecades } from './Views/Decades';
import { DatepickerViewsMonth } from './Views/Months';
import { DatepickerViewsYears } from './Views/Years';
import { Views, WeekStart, addMonths, addYears, getFirstDateInRange, getFormattedDate, isDateEqual, startOfYearPeriod, } from './helpers';
export const Datepicker = ({ title, open, inline = false, autoHide = true, // Hide when selected the day
showClearButton = true, labelClearButton = 'Clear', showTodayButton = true, labelTodayButton = 'Today', defaultDate = new Date(), minDate, maxDate, language = 'en', weekStart = WeekStart.Sunday, className, theme: customTheme = {}, onSelectedDateChanged, ...props }) => {
const theme = mergeDeep(getTheme().datepicker, customTheme);
// Default date should respect the range
defaultDate = getFirstDateInRange(defaultDate, minDate, maxDate);
const [isOpen, setIsOpen] = useState(open);
const [view, setView] = useState(Views.Days);
// selectedDate is the date selected by the user
const [selectedDate, setSelectedDate] = useState(defaultDate);
// viewDate is only for navigation
const [viewDate, setViewDate] = useState(defaultDate);
const inputRef = useRef(null);
const datepickerRef = useRef(null);
// Triggers when user select the date
const changeSelectedDate = (date, useAutohide) => {
setSelectedDate(date);
if (onSelectedDateChanged) {
onSelectedDateChanged(date);
}
if (autoHide && view === Views.Days && useAutohide == true && !inline) {
setIsOpen(false);
}
};
// Render the DatepickerView* node
const renderView = (type) => {
switch (type) {
case Views.Decades:
return React.createElement(DatepickerViewsDecades, { theme: theme.views.decades });
case Views.Years:
return React.createElement(DatepickerViewsYears, { theme: theme.views.years });
case Views.Months:
return React.createElement(DatepickerViewsMonth, { theme: theme.views.months });
case Views.Days:
default:
return React.createElement(DatepickerViewsDays, { theme: theme.views.days });
}
};
// Coordinate the next view based on current view (statemachine-like)
const getNextView = () => {
switch (view) {
case Views.Days:
return Views.Months;
case Views.Months:
return Views.Years;
case Views.Years:
return Views.Decades;
}
return view;
};
// Get the view title based on active View
const getViewTitle = () => {
switch (view) {
case Views.Decades:
return `${startOfYearPeriod(viewDate, 100)} - ${startOfYearPeriod(viewDate, 100) + 90}`;
case Views.Years:
return `${startOfYearPeriod(viewDate, 10)} - ${startOfYearPeriod(viewDate, 10) + 9}`;
case Views.Months:
return getFormattedDate(language, viewDate, { year: 'numeric' });
case Views.Days:
default:
return getFormattedDate(language, viewDate, { month: 'long', year: 'numeric' });
}
};
// Navigate to prev/next for given view's date by value
const getViewDatePage = (view, date, value) => {
switch (view) {
case Views.Days:
return new Date(addMonths(date, value));
case Views.Months:
return new Date(addYears(date, value));
case Views.Years:
return new Date(addYears(date, value * 10));
case Views.Decades:
return new Date(addYears(date, value * 100));
default:
return new Date(addYears(date, value * 10));
}
};
useEffect(() => {
const handleClickOutside = (event) => {
const clickedInsideDatepicker = datepickerRef?.current?.contains(event.target);
const clickedInsideInput = inputRef?.current?.contains(event.target);
if (!clickedInsideDatepicker && !clickedInsideInput) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [inputRef, datepickerRef, setIsOpen]);
return (React.createElement(DatepickerContext.Provider, { value: {
theme,
language,
minDate,
maxDate,
weekStart,
isOpen,
setIsOpen,
view,
setView,
viewDate,
setViewDate,
selectedDate,
setSelectedDate,
changeSelectedDate,
} },
React.createElement("div", { className: twMerge(theme.root.base, className) },
!inline && (React.createElement(TextInput, { theme: theme.root.input, icon: HiCalendar, ref: inputRef, onFocus: () => {
if (!isDateEqual(viewDate, selectedDate)) {
setViewDate(selectedDate);
}
setIsOpen(true);
}, value: selectedDate && getFormattedDate(language, selectedDate), readOnly: true, ...props })),
(isOpen || inline) && (React.createElement("div", { ref: datepickerRef, className: twMerge(theme.popup.root.base, inline && theme.popup.root.inline) },
React.createElement("div", { className: theme.popup.root.inner },
React.createElement("div", { className: theme.popup.header.base },
title && React.createElement("div", { className: theme.popup.header.title }, title),
React.createElement("div", { className: theme.popup.header.selectors.base },
React.createElement("button", { type: "button", className: twMerge(theme.popup.header.selectors.button.base, theme.popup.header.selectors.button.prev), onClick: () => setViewDate(getViewDatePage(view, viewDate, -1)) },
React.createElement(HiArrowLeft, null)),
React.createElement("button", { type: "button", className: twMerge(theme.popup.header.selectors.button.base, theme.popup.header.selectors.button.view), onClick: () => setView(getNextView()) }, getViewTitle()),
React.createElement("button", { type: "button", className: twMerge(theme.popup.header.selectors.button.base, theme.popup.header.selectors.button.next), onClick: () => setViewDate(getViewDatePage(view, viewDate, 1)) },
React.createElement(HiArrowRight, null)))),
React.createElement("div", { className: theme.popup.view.base }, renderView(view)),
(showClearButton || showTodayButton) && (React.createElement("div", { className: theme.popup.footer.base },
showTodayButton && (React.createElement("button", { type: "button", className: twMerge(theme.popup.footer.button.base, theme.popup.footer.button.today), onClick: () => {
const today = new Date();
changeSelectedDate(today, true);
setViewDate(today);
} }, labelTodayButton)),
showClearButton && (React.createElement("button", { type: "button", className: twMerge(theme.popup.footer.button.base, theme.popup.footer.button.clear), onClick: () => {
changeSelectedDate(defaultDate, true);
if (defaultDate) {
setViewDate(defaultDate);
}
} }, labelClearButton))))))))));
};
Datepicker.displayName = 'Datepicker';