|
@@ -0,0 +1,435 @@
|
|
|
|
|
+import {
|
|
|
|
|
+ useImperativeHandle,
|
|
|
|
|
+ useLayoutEffect,
|
|
|
|
|
+ forwardRef,
|
|
|
|
|
+ useEffect,
|
|
|
|
|
+ useState,
|
|
|
|
|
+ type Ref,
|
|
|
|
|
+ useRef
|
|
|
|
|
+} from "react";
|
|
|
|
|
+import {
|
|
|
|
|
+ TouchableOpacity,
|
|
|
|
|
+ type ViewStyle,
|
|
|
|
|
+ View
|
|
|
|
|
+} from "react-native";
|
|
|
|
|
+import type {
|
|
|
|
|
+ IYearSelectorRef,
|
|
|
|
|
+ CalendarYear
|
|
|
|
|
+} from "./type";
|
|
|
|
|
+import type IYearSelectorProps from "./type";
|
|
|
|
|
+import stylesheet, {
|
|
|
|
|
+ useStyles
|
|
|
|
|
+} from "./stylesheet";
|
|
|
|
|
+import {
|
|
|
|
|
+ dateSelect
|
|
|
|
|
+} from "./util";
|
|
|
|
|
+import {
|
|
|
|
|
+ NCoreUIKitLocalize,
|
|
|
|
|
+ NCoreUIKitTheme,
|
|
|
|
|
+ NCoreUIKitToast
|
|
|
|
|
+} from "../../core/hooks";
|
|
|
|
|
+import type {
|
|
|
|
|
+ Mutable
|
|
|
|
|
+} from "../../types";
|
|
|
|
|
+import {
|
|
|
|
|
+ ChevronRight as ChevronRightIcon,
|
|
|
|
|
+ ChevronLeft as ChevronLeftIcon
|
|
|
|
|
+} from "lucide-react-native";
|
|
|
|
|
+import moment from "moment";
|
|
|
|
|
+import Text from "../text";
|
|
|
|
|
+
|
|
|
|
|
+const YearSelector = ({
|
|
|
|
|
+ multipleSelectMinimumRequiredDayCount,
|
|
|
|
|
+ isShowTodayIndicator = true,
|
|
|
|
|
+ localeBasedFirstDayOfWeek,
|
|
|
|
|
+ yearLengthType = "long",
|
|
|
|
|
+ viewDate: viewDateProp,
|
|
|
|
|
+ multipleSelectDayLimit,
|
|
|
|
|
+ setIsSheetContentReady,
|
|
|
|
|
+ selectMultipleObject,
|
|
|
|
|
+ selectObject,
|
|
|
|
|
+ dateRange,
|
|
|
|
|
+ maxDate,
|
|
|
|
|
+ minDate,
|
|
|
|
|
+ variant,
|
|
|
|
|
+ date
|
|
|
|
|
+}: IYearSelectorProps, ref: Ref<IYearSelectorRef>) => {
|
|
|
|
|
+ const {
|
|
|
|
|
+ radiuses,
|
|
|
|
|
+ borders,
|
|
|
|
|
+ spaces,
|
|
|
|
|
+ colors
|
|
|
|
|
+ } = NCoreUIKitTheme.useContext();
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ localize
|
|
|
|
|
+ } = NCoreUIKitLocalize.useContext();
|
|
|
|
|
+
|
|
|
|
|
+ const selectDate = dateSelect({
|
|
|
|
|
+ dateRange,
|
|
|
|
|
+ variant,
|
|
|
|
|
+ date
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const [
|
|
|
|
|
+ viewDate,
|
|
|
|
|
+ setViewDate
|
|
|
|
|
+ ] = useState(viewDateProp ? viewDateProp : selectDate);
|
|
|
|
|
+
|
|
|
|
|
+ const [
|
|
|
|
|
+ monthDays,
|
|
|
|
|
+ setMonthDays
|
|
|
|
|
+ ] = useState<Array<CalendarYear>>([]);
|
|
|
|
|
+
|
|
|
|
|
+ const allSelectedDays = useRef<Array<Date>>([]);
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ nextPrevToolChevronButton: nextPrevToolChevronButtonDynamicStyle,
|
|
|
|
|
+ nextPrevToolContainer: nextPrevToolContainerDynamicStyle,
|
|
|
|
|
+ todayIndicator: todayIndicatorDynamicStyle,
|
|
|
|
|
+ dayOfWeek: dayOfWeekDynamicStyle,
|
|
|
|
|
+ day: dayDynamicStyle
|
|
|
|
|
+ } = useStyles({
|
|
|
|
|
+ radiuses,
|
|
|
|
|
+ borders,
|
|
|
|
|
+ colors,
|
|
|
|
|
+ spaces
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const allDays = getDaysOfViewMonth({
|
|
|
|
|
+ tDate: viewDate
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ setMonthDays(allDays);
|
|
|
|
|
+ }, [
|
|
|
|
|
+ dateRange,
|
|
|
|
|
+ viewDate,
|
|
|
|
|
+ date
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ useLayoutEffect(() => {
|
|
|
|
|
+ if(monthDays) setIsSheetContentReady(true);
|
|
|
|
|
+ }, [
|
|
|
|
|
+ monthDays
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if(viewDateProp) {
|
|
|
|
|
+ setViewDate(viewDateProp);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [viewDateProp]);
|
|
|
|
|
+
|
|
|
|
|
+ useImperativeHandle(
|
|
|
|
|
+ ref,
|
|
|
|
|
+ () => ({
|
|
|
|
|
+ changeCurrentView: (date: Date) => {
|
|
|
|
|
+ setViewDate(date);
|
|
|
|
|
+ }
|
|
|
|
|
+ }),
|
|
|
|
|
+ []
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const getDaysOfViewMonth = ({
|
|
|
|
|
+ tDate
|
|
|
|
|
+ }: {
|
|
|
|
|
+ tDate: Date
|
|
|
|
|
+ }): Array<CalendarYear> => {
|
|
|
|
|
+ allSelectedDays.current = [];
|
|
|
|
|
+
|
|
|
|
|
+ const targetDate = moment(new Date(tDate));
|
|
|
|
|
+
|
|
|
|
|
+ const startOfCalendar = targetDate.clone().startOf("month").startOf("week");
|
|
|
|
|
+ const endOfCalendar = targetDate.clone().endOf("month").endOf("week");
|
|
|
|
|
+
|
|
|
|
|
+ const totalDays = endOfCalendar.diff(startOfCalendar, "days") + 1;
|
|
|
|
|
+
|
|
|
|
|
+ const allDays = Array.from({
|
|
|
|
|
+ length: totalDays
|
|
|
|
|
+ }).map((_, index) => {
|
|
|
|
|
+ const currentDay = startOfCalendar.clone().add(index, "days");
|
|
|
|
|
+
|
|
|
|
|
+ let isSelected = false;
|
|
|
|
|
+
|
|
|
|
|
+ if(variant === "single" && date) {
|
|
|
|
|
+ allSelectedDays.current = [date];
|
|
|
|
|
+
|
|
|
|
|
+ if(moment(currentDay).isSame(date, "day")) isSelected = true;
|
|
|
|
|
+ } else if(variant === "range" && dateRange && dateRange.start) {
|
|
|
|
|
+ if(dateRange.end) {
|
|
|
|
|
+ allSelectedDays.current = Array.from({
|
|
|
|
|
+ length: moment(dateRange.end).diff(dateRange.start, "days") + 1
|
|
|
|
|
+ }, (_, i) =>
|
|
|
|
|
+ moment(dateRange.start).clone().add(i, "days").toDate()
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if(moment(currentDay).isBetween(dateRange.start, dateRange.end, "day", "[]")) isSelected = true;
|
|
|
|
|
+ } else if(moment(currentDay).isSame(dateRange.start)) {
|
|
|
|
|
+ allSelectedDays.current = [dateRange.start];
|
|
|
|
|
+
|
|
|
|
|
+ isSelected = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let isDisabled = false;
|
|
|
|
|
+
|
|
|
|
|
+ if(minDate && moment(currentDay).isBefore(minDate)) {
|
|
|
|
|
+ isDisabled = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(maxDate && moment(currentDay).isAfter(maxDate)) {
|
|
|
|
|
+ isDisabled = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ originalIndex: allSelectedDays.current.findIndex((day) => moment(currentDay).isSame(day, "day")),
|
|
|
|
|
+ isCurrentView: currentDay.isSame(targetDate, "month"),
|
|
|
|
|
+ isToday: currentDay.isSame(moment(), "day"),
|
|
|
|
|
+ dayOfWeek: currentDay.weekday(),
|
|
|
|
|
+ dayNumber: currentDay.date(),
|
|
|
|
|
+ date: currentDay,
|
|
|
|
|
+ isDisabled,
|
|
|
|
|
+ isSelected
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return allDays;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const renderDay = ({
|
|
|
|
|
+ weekIndex,
|
|
|
|
|
+ dayIndex,
|
|
|
|
|
+ dayItem
|
|
|
|
|
+ }: {
|
|
|
|
|
+ dayItem: CalendarYear;
|
|
|
|
|
+ weekIndex: number;
|
|
|
|
|
+ dayIndex: number;
|
|
|
|
|
+ }) => {
|
|
|
|
|
+ const isLastItemSelected = dayItem.isSelected && dayItem.originalIndex === allSelectedDays.current.length - 1;
|
|
|
|
|
+ const isFirstItemSelected = dayItem.isSelected && dayItem.originalIndex === 0;
|
|
|
|
|
+
|
|
|
|
|
+ const selectionStyle: Array<Mutable<ViewStyle> | null> = [
|
|
|
|
|
+ isFirstItemSelected && allSelectedDays.current.length > 1 ? {
|
|
|
|
|
+ borderBottomRightRadius: 0,
|
|
|
|
|
+ borderTopRightRadius: 0
|
|
|
|
|
+ } : null,
|
|
|
|
|
+ isLastItemSelected && allSelectedDays.current.length > 1 ? {
|
|
|
|
|
+ borderBottomLeftRadius: 0,
|
|
|
|
|
+ borderTopLeftRadius: 0
|
|
|
|
|
+ } : null
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ let dayTitleColor: keyof NCoreUIKit.TextContentColors = "mid";
|
|
|
|
|
+
|
|
|
|
|
+ if(dayItem.isSelected && !isLastItemSelected && !isFirstItemSelected) {
|
|
|
|
|
+ dayTitleColor = "emphasized";
|
|
|
|
|
+
|
|
|
|
|
+ selectionStyle.push({
|
|
|
|
|
+ borderBottomRightRadius: 0,
|
|
|
|
|
+ borderBottomLeftRadius: 0,
|
|
|
|
|
+ borderTopRightRadius: 0,
|
|
|
|
|
+ borderTopLeftRadius: 0
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if(dayItem.isSelected) {
|
|
|
|
|
+ dayTitleColor = "onPrimary";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(!dayItem.isCurrentView || dayItem.isDisabled) {
|
|
|
|
|
+ dayTitleColor = "disabled";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return <TouchableOpacity
|
|
|
|
|
+ disabled={!dayItem.isCurrentView || dayItem.isDisabled}
|
|
|
|
|
+ key={`day-${weekIndex}-${dayIndex}`}
|
|
|
|
|
+ onPress={() => {
|
|
|
|
|
+ if(!dayItem.isCurrentView) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(variant === "single") {
|
|
|
|
|
+ selectObject(dayItem.date.toDate());
|
|
|
|
|
+ } else if(variant === "range") {
|
|
|
|
|
+ if(!dateRange || !dateRange.start) {
|
|
|
|
|
+ selectMultipleObject({
|
|
|
|
|
+ start: new Date(dayItem.date.toDate().setHours(0, 0, 0)),
|
|
|
|
|
+ end: undefined
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(dateRange && moment(dateRange.start).isSame(dayItem.date, "day")) {
|
|
|
|
|
+ selectMultipleObject({
|
|
|
|
|
+ start: undefined,
|
|
|
|
|
+ end: undefined
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(dateRange && !dateRange.end) {
|
|
|
|
|
+ if(multipleSelectDayLimit && moment(dayItem.date).diff(dateRange.start, "days") >= multipleSelectDayLimit) {
|
|
|
|
|
+ NCoreUIKitToast.open({
|
|
|
|
|
+ title: localize("maximum-selection-number-of-days-limit-exceeds")
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(multipleSelectMinimumRequiredDayCount && moment(dayItem.date).diff(dateRange.start, "days") + 1 < multipleSelectMinimumRequiredDayCount) {
|
|
|
|
|
+ NCoreUIKitToast.open({
|
|
|
|
|
+ title: localize("minimum-selection-number-of-days-required-not-provided")
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(moment(dayItem.date).isBefore(dateRange.start)) {
|
|
|
|
|
+ selectMultipleObject({
|
|
|
|
|
+ start: new Date(dayItem.date.toDate().setHours(0, 0, 0)),
|
|
|
|
|
+ end: undefined
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ selectMultipleObject({
|
|
|
|
|
+ end: new Date(dayItem.date.toDate().setHours(23, 59, 59)),
|
|
|
|
|
+ start: dateRange?.start
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ selectMultipleObject({
|
|
|
|
|
+ start: dayItem.date.toDate(),
|
|
|
|
|
+ end: undefined
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ style={[
|
|
|
|
|
+ stylesheet.day,
|
|
|
|
|
+ dayDynamicStyle,
|
|
|
|
|
+ dayItem.isSelected ? {
|
|
|
|
|
+ backgroundColor: colors.content.container.primary,
|
|
|
|
|
+ borderColor: colors.content.container.primary
|
|
|
|
|
+ } : null,
|
|
|
|
|
+ dayItem.isSelected && !isLastItemSelected && !isFirstItemSelected ? {
|
|
|
|
|
+ backgroundColor: colors.content.container.emphasized,
|
|
|
|
|
+ borderColor: colors.content.container.emphasized
|
|
|
|
|
+ } : null,
|
|
|
|
|
+ dayItem.isSelected && (dayItem.isDisabled || !dayItem.isCurrentView) ? {
|
|
|
|
|
+ opacity: 0.33
|
|
|
|
|
+ } : null,
|
|
|
|
|
+ ...selectionStyle
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text
|
|
|
|
|
+ variant="labelLargeSize"
|
|
|
|
|
+ color={dayTitleColor}
|
|
|
|
|
+ >
|
|
|
|
|
+ {dayItem.dayNumber}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ {isShowTodayIndicator && dayItem.isToday ? <View
|
|
|
|
|
+ style={[
|
|
|
|
|
+ stylesheet.todayIndicator,
|
|
|
|
|
+ selectionStyle,
|
|
|
|
|
+ todayIndicatorDynamicStyle,
|
|
|
|
|
+ dayItem.isSelected ? {
|
|
|
|
|
+ borderColor: colors.content.border.subtle
|
|
|
|
|
+ } : null
|
|
|
|
|
+ ]}
|
|
|
|
|
+ /> : null}
|
|
|
|
|
+ </TouchableOpacity>;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const renderNextPrevTool = () => {
|
|
|
|
|
+ return <View
|
|
|
|
|
+ style={[
|
|
|
|
|
+ stylesheet.nextPrevToolContainer,
|
|
|
|
|
+ nextPrevToolContainerDynamicStyle
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <TouchableOpacity
|
|
|
|
|
+ onPress={() => {
|
|
|
|
|
+ const prevViewDate = moment(viewDate).clone().subtract(1, "months");
|
|
|
|
|
+ setViewDate(prevViewDate.toDate());
|
|
|
|
|
+ }}
|
|
|
|
|
+ style={[
|
|
|
|
|
+ nextPrevToolChevronButtonDynamicStyle
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <ChevronLeftIcon
|
|
|
|
|
+ color={colors.content.icon.default}
|
|
|
|
|
+ size={26}
|
|
|
|
|
+ />
|
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
|
+ <Text
|
|
|
|
|
+ variant="labelLargeSize"
|
|
|
|
|
+ color="high"
|
|
|
|
|
+ >
|
|
|
|
|
+ {moment(viewDate).format("MMMM YYYY")}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ <TouchableOpacity
|
|
|
|
|
+ onPress={() => {
|
|
|
|
|
+ const nextViewDate = moment(viewDate).clone().add(1, "months");
|
|
|
|
|
+ setViewDate(nextViewDate.toDate());
|
|
|
|
|
+ }}
|
|
|
|
|
+ style={[
|
|
|
|
|
+ nextPrevToolChevronButtonDynamicStyle
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <ChevronRightIcon
|
|
|
|
|
+ color={colors.content.icon.default}
|
|
|
|
|
+ size={26}
|
|
|
|
|
+ />
|
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
|
+ </View>;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let weekDays = moment.weekdays(localeBasedFirstDayOfWeek ? true : false);
|
|
|
|
|
+
|
|
|
|
|
+ if(yearLengthType === "short") {
|
|
|
|
|
+ weekDays = moment.weekdaysShort(localeBasedFirstDayOfWeek ? true : false);
|
|
|
|
|
+ } else if(yearLengthType === "very-short") {
|
|
|
|
|
+ weekDays = moment.weekdaysMin(localeBasedFirstDayOfWeek ? true : false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return <View
|
|
|
|
|
+ style={[
|
|
|
|
|
+ stylesheet.container
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <View>
|
|
|
|
|
+ {renderNextPrevTool()}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <View
|
|
|
|
|
+ style={[
|
|
|
|
|
+ stylesheet.contentContainer
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ {weekDays.map((weekItem, weekIndex) => {
|
|
|
|
|
+ const days = monthDays.filter((dayItem) => dayItem.dayOfWeek === weekIndex);
|
|
|
|
|
+
|
|
|
|
|
+ return <View
|
|
|
|
|
+ key={`day-column-${weekIndex}`}
|
|
|
|
|
+ style={[
|
|
|
|
|
+ stylesheet.columnContainer
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text
|
|
|
|
|
+ variant="labelMediumSize"
|
|
|
|
|
+ numberOfLines={1}
|
|
|
|
|
+ color="low"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ ...dayOfWeekDynamicStyle
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {weekItem}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ {days.map((dayItem, dayIndex) => {
|
|
|
|
|
+ return renderDay({
|
|
|
|
|
+ weekIndex,
|
|
|
|
|
+ dayIndex,
|
|
|
|
|
+ dayItem
|
|
|
|
|
+ });
|
|
|
|
|
+ })}
|
|
|
|
|
+ </View>;
|
|
|
|
|
+ })}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>;
|
|
|
|
|
+};
|
|
|
|
|
+export default forwardRef(YearSelector);
|