390 lines
9.6 KiB
TypeScript
Executable File
390 lines
9.6 KiB
TypeScript
Executable File
import dayjs from "dayjs";
|
|
import React, {
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from "react";
|
|
|
|
import { CALENDAR_SIZE, DATE_FORMAT } from "../../constants";
|
|
import DatepickerContext from "../../contexts/DatepickerContext";
|
|
import {
|
|
formatDate,
|
|
getDaysInMonth,
|
|
getFirstDayInMonth,
|
|
getFirstDaysInMonth,
|
|
getLastDaysInMonth,
|
|
getNumberOfDay,
|
|
loadLanguageModule,
|
|
nextMonth,
|
|
previousMonth,
|
|
} from "../../helpers";
|
|
import {
|
|
ChevronLeftIcon,
|
|
ChevronRightIcon,
|
|
DoubleChevronLeftIcon,
|
|
DoubleChevronRightIcon,
|
|
RoundedButton,
|
|
} from "../utils";
|
|
|
|
import Days from "./Days";
|
|
import Months from "./Months";
|
|
import Week from "./Week";
|
|
import Years from "./Years";
|
|
|
|
import { DateType } from "../../types";
|
|
|
|
interface Props {
|
|
date: dayjs.Dayjs;
|
|
minDate?: DateType | null;
|
|
maxDate?: DateType | null;
|
|
onClickPrevious: () => void;
|
|
onClickNext: () => void;
|
|
changeMonth: (month: number) => void;
|
|
changeYear: (year: number) => void;
|
|
mode?: "monthly" | "daily";
|
|
onMark?: (day: number, date: Date) => any;
|
|
style?: string
|
|
}
|
|
|
|
const Calendar: React.FC<Props> = ({
|
|
date,
|
|
minDate,
|
|
maxDate,
|
|
onClickPrevious,
|
|
onClickNext,
|
|
changeMonth,
|
|
changeYear,
|
|
onMark,
|
|
mode = "daily",
|
|
style = "prasi"
|
|
}) => {
|
|
// Contexts
|
|
const {
|
|
period,
|
|
changePeriod,
|
|
changeDayHover,
|
|
showFooter,
|
|
changeDatepickerValue,
|
|
hideDatepicker,
|
|
asSingle,
|
|
i18n,
|
|
startWeekOn,
|
|
input,
|
|
} = useContext(DatepickerContext);
|
|
loadLanguageModule(i18n);
|
|
|
|
// States
|
|
const [showMonths, setShowMonths] = useState(false);
|
|
const [showYears, setShowYears] = useState(false);
|
|
const [year, setYear] = useState(date.year());
|
|
useEffect(() => {
|
|
if (mode === "monthly") {
|
|
setShowMonths(true);
|
|
hideYears();
|
|
}
|
|
}, []);
|
|
// Functions
|
|
const previous = useCallback(() => {
|
|
return getLastDaysInMonth(
|
|
previousMonth(date),
|
|
getNumberOfDay(getFirstDayInMonth(date).ddd, startWeekOn)
|
|
);
|
|
}, [date, startWeekOn]);
|
|
|
|
const current = useCallback(() => {
|
|
return getDaysInMonth(formatDate(date));
|
|
}, [date]);
|
|
|
|
const next = useCallback(() => {
|
|
return getFirstDaysInMonth(
|
|
previousMonth(date),
|
|
CALENDAR_SIZE - (previous().length + current().length)
|
|
);
|
|
}, [current, date, previous]);
|
|
|
|
const hideMonths = useCallback(() => {
|
|
showMonths && setShowMonths(false);
|
|
}, [showMonths]);
|
|
|
|
const hideYears = useCallback(() => {
|
|
showYears && setShowYears(false);
|
|
}, [showYears]);
|
|
|
|
const clickMonth = useCallback(
|
|
(month: number) => {
|
|
setTimeout(() => {
|
|
changeMonth(month);
|
|
if (mode === "daily") {
|
|
setShowMonths(!showMonths);
|
|
} else {
|
|
hideDatepicker();
|
|
clickDay(1, month, date.year());
|
|
}
|
|
}, 250);
|
|
},
|
|
[changeMonth, showMonths]
|
|
);
|
|
|
|
const clickYear = useCallback(
|
|
(year: number) => {
|
|
setTimeout(() => {
|
|
changeYear(year);
|
|
setShowYears(!showYears);
|
|
if (mode === "monthly") {
|
|
setShowMonths(true);
|
|
clickDay(1, date.month() + 1, year);
|
|
}
|
|
}, 250);
|
|
},
|
|
[changeYear, showYears]
|
|
);
|
|
|
|
const clickDay = useCallback(
|
|
(day: number, month = date.month() + 1, year = date.year()) => {
|
|
const fullDay = `${year}-${month}-${day}`;
|
|
let newStart;
|
|
let newEnd = null;
|
|
function chosePeriod(start: string, end: string) {
|
|
const ipt = input?.current;
|
|
changeDatepickerValue(
|
|
{
|
|
startDate: dayjs(start).format(DATE_FORMAT),
|
|
endDate: dayjs(end).format(DATE_FORMAT),
|
|
},
|
|
ipt
|
|
);
|
|
hideDatepicker();
|
|
}
|
|
|
|
if (period.start && period.end) {
|
|
if (changeDayHover) {
|
|
changeDayHover(null);
|
|
}
|
|
changePeriod({
|
|
start: null,
|
|
end: null,
|
|
});
|
|
}
|
|
|
|
if ((!period.start && !period.end) || (period.start && period.end)) {
|
|
if (!period.start && !period.end) {
|
|
changeDayHover(fullDay);
|
|
}
|
|
newStart = fullDay;
|
|
if (asSingle) {
|
|
newEnd = fullDay;
|
|
chosePeriod(fullDay, fullDay);
|
|
}
|
|
} else {
|
|
if (period.start && !period.end) {
|
|
// start not null
|
|
// end null
|
|
const condition =
|
|
dayjs(fullDay).isSame(dayjs(period.start)) ||
|
|
dayjs(fullDay).isAfter(dayjs(period.start));
|
|
newStart = condition ? period.start : fullDay;
|
|
newEnd = condition ? fullDay : period.start;
|
|
} else {
|
|
// Start null
|
|
// End not null
|
|
const condition =
|
|
dayjs(fullDay).isSame(dayjs(period.end)) ||
|
|
dayjs(fullDay).isBefore(dayjs(period.end));
|
|
newStart = condition ? fullDay : period.start;
|
|
newEnd = condition ? period.end : fullDay;
|
|
}
|
|
|
|
if (!showFooter) {
|
|
if (newStart && newEnd) {
|
|
chosePeriod(newStart, newEnd);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!(newEnd && newStart) || showFooter) {
|
|
changePeriod({
|
|
start: newStart,
|
|
end: newEnd,
|
|
});
|
|
}
|
|
},
|
|
[
|
|
asSingle,
|
|
changeDatepickerValue,
|
|
changeDayHover,
|
|
changePeriod,
|
|
date,
|
|
hideDatepicker,
|
|
period.end,
|
|
period.start,
|
|
showFooter,
|
|
input,
|
|
]
|
|
);
|
|
|
|
const clickPreviousDays = useCallback(
|
|
(day: number) => {
|
|
const newDate = previousMonth(date);
|
|
clickDay(day, newDate.month() + 1, newDate.year());
|
|
onClickPrevious();
|
|
},
|
|
[clickDay, date, onClickPrevious]
|
|
);
|
|
|
|
const clickNextDays = useCallback(
|
|
(day: number) => {
|
|
const newDate = nextMonth(date);
|
|
clickDay(day, newDate.month() + 1, newDate.year());
|
|
onClickNext();
|
|
},
|
|
[clickDay, date, onClickNext]
|
|
);
|
|
|
|
// UseEffects & UseLayoutEffect
|
|
useEffect(() => {
|
|
setYear(date.year());
|
|
}, [date]);
|
|
|
|
// Variables
|
|
const calendarData = useMemo(() => {
|
|
return {
|
|
date: date,
|
|
days: {
|
|
previous: previous(),
|
|
current: current(),
|
|
next: next(),
|
|
},
|
|
};
|
|
}, [current, date, next, previous]);
|
|
const minYear = React.useMemo(
|
|
() => (minDate && dayjs(minDate).isValid() ? dayjs(minDate).year() : null),
|
|
[minDate]
|
|
);
|
|
const maxYear = React.useMemo(
|
|
() => (maxDate && dayjs(maxDate).isValid() ? dayjs(maxDate).year() : null),
|
|
[maxDate]
|
|
);
|
|
|
|
return (
|
|
<div className="c-w-full md:c-w-[296px] md:c-min-w-[296px] calender">
|
|
<div
|
|
className={cx(
|
|
"c-flex c-items-stretch c-space-x-1.5 c-px-2 c-py-1.5",
|
|
css`
|
|
border-bottom: 1px solid #d1d5db;
|
|
`
|
|
)}
|
|
>
|
|
{!showMonths && !showYears && (
|
|
<div className="c-flex-none">
|
|
<RoundedButton roundedFull={true} onClick={onClickPrevious}>
|
|
<ChevronLeftIcon className="c-h-5 c-w-5" />
|
|
</RoundedButton>
|
|
</div>
|
|
)}
|
|
|
|
{showYears && (
|
|
<div className="c-flex-none">
|
|
<RoundedButton
|
|
roundedFull={true}
|
|
onClick={() => {
|
|
setYear(year - 12);
|
|
}}
|
|
>
|
|
<DoubleChevronLeftIcon className="c-h-5 c-w-5" />
|
|
</RoundedButton>
|
|
</div>
|
|
)}
|
|
|
|
<div className=" c-flex c-flex-1 c-items-stretch c-space-x-1.5">
|
|
<div className="c-w-1/2 c-flex c-items-stretch">
|
|
<RoundedButton
|
|
onClick={() => {
|
|
setShowMonths(!showMonths);
|
|
hideYears();
|
|
}}
|
|
>
|
|
<>{calendarData.date.locale(i18n).format(style === "google" ? "MMMM" :"MMM")}</>
|
|
</RoundedButton>
|
|
</div>
|
|
|
|
<div className="c-w-1/2 c-flex c-items-stretch">
|
|
<RoundedButton
|
|
onClick={() => {
|
|
setShowYears(!showYears);
|
|
hideMonths();
|
|
}}
|
|
>
|
|
<div className="c-py-2">{calendarData.date.year()}</div>
|
|
</RoundedButton>
|
|
</div>
|
|
</div>
|
|
|
|
{showYears && (
|
|
<div className="c-flex-none">
|
|
<RoundedButton
|
|
roundedFull={true}
|
|
onClick={() => {
|
|
setYear(year + 12);
|
|
}}
|
|
>
|
|
<DoubleChevronRightIcon className="c-h-5 c-w-5" />
|
|
</RoundedButton>
|
|
</div>
|
|
)}
|
|
|
|
{!showMonths && !showYears && (
|
|
<div className="c-flex-none">
|
|
<RoundedButton roundedFull={true} onClick={onClickNext}>
|
|
<ChevronRightIcon className="c-h-5 c-w-5" />
|
|
</RoundedButton>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className={cx("c-mt-0.5 c-min-h-[285px] calender-body")}>
|
|
{showMonths && (
|
|
<Months
|
|
currentMonth={calendarData.date.month() + 1}
|
|
clickMonth={clickMonth}
|
|
style={style}
|
|
/>
|
|
)}
|
|
|
|
{showYears && (
|
|
<Years
|
|
year={year}
|
|
minYear={minYear}
|
|
maxYear={maxYear}
|
|
currentYear={calendarData.date.year()}
|
|
clickYear={clickYear}
|
|
/>
|
|
)}
|
|
|
|
{!showMonths && !showYears && (
|
|
<>
|
|
<Week style={style}/>
|
|
|
|
<Days
|
|
calendarData={calendarData}
|
|
onClickPreviousDays={clickPreviousDays}
|
|
onClickDay={clickDay}
|
|
onClickNextDays={clickNextDays}
|
|
style={style}
|
|
onIcon={(day, date) => {
|
|
if(typeof onMark === "function"){
|
|
return onMark(day, date)
|
|
}
|
|
return <></>
|
|
}}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Calendar;
|