index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. import {
  2. useImperativeHandle,
  3. useLayoutEffect,
  4. forwardRef,
  5. useEffect,
  6. useState,
  7. type Ref,
  8. useRef
  9. } from "react";
  10. import {
  11. TouchableOpacity,
  12. type ViewStyle,
  13. View
  14. } from "react-native";
  15. import type {
  16. IMonthSelectorRef,
  17. CalendarMonth
  18. } from "./type";
  19. import type IMonthSelectorProps from "./type";
  20. import stylesheet, {
  21. useStyles
  22. } from "./stylesheet";
  23. import {
  24. dateSelect
  25. } from "./util";
  26. import {
  27. NCoreUIKitLocalize,
  28. NCoreUIKitTheme,
  29. NCoreUIKitToast
  30. } from "../../core/hooks";
  31. import {
  32. type Mutable,
  33. webStyle
  34. } from "../../types";
  35. import {
  36. ChevronRight as ChevronRightIcon,
  37. ChevronLeft as ChevronLeftIcon
  38. } from "lucide-react-native";
  39. import moment from "moment";
  40. import Text from "../text";
  41. const MONTH_LINES = [
  42. [
  43. 0,
  44. 1,
  45. 2
  46. ],
  47. [
  48. 3,
  49. 4,
  50. 5
  51. ],
  52. [
  53. 6,
  54. 7,
  55. 8
  56. ],
  57. [
  58. 9,
  59. 10,
  60. 11
  61. ]
  62. ];
  63. const MonthSelector = ({
  64. multipleSelectMinimumRequiredDayCount,
  65. monthOfYearLengthType = "long",
  66. isShowTodayIndicator = true,
  67. viewDate: viewDateProp,
  68. multipleSelectDayLimit,
  69. setIsSheetContentReady,
  70. selectMultipleObject,
  71. selectObject,
  72. dateRange,
  73. maxDate,
  74. minDate,
  75. variant,
  76. date
  77. }: IMonthSelectorProps, ref: Ref<IMonthSelectorRef>) => {
  78. const {
  79. radiuses,
  80. borders,
  81. spaces,
  82. colors
  83. } = NCoreUIKitTheme.useContext();
  84. const {
  85. localize
  86. } = NCoreUIKitLocalize.useContext();
  87. const selectDate = dateSelect({
  88. dateRange,
  89. variant,
  90. date
  91. });
  92. const [
  93. viewDate,
  94. setViewDate
  95. ] = useState(viewDateProp ? viewDateProp : selectDate);
  96. const [
  97. yearMonths,
  98. setYearMonths
  99. ] = useState<Array<CalendarMonth>>([]);
  100. const allSelectedMonths = useRef<Array<Date>>([]);
  101. const {
  102. nextPrevToolChevronButton: nextPrevToolChevronButtonDynamicStyle,
  103. nextPrevToolContainer: nextPrevToolContainerDynamicStyle,
  104. todayIndicator: todayIndicatorDynamicStyle,
  105. day: dayDynamicStyle
  106. } = useStyles({
  107. radiuses,
  108. borders,
  109. colors,
  110. spaces
  111. });
  112. useEffect(() => {
  113. const allMonths = getMonthsOfViewYear({
  114. tDate: viewDate
  115. });
  116. setYearMonths(allMonths);
  117. }, [
  118. dateRange,
  119. viewDate,
  120. date
  121. ]);
  122. useLayoutEffect(() => {
  123. if(yearMonths && yearMonths.length) setIsSheetContentReady(true);
  124. }, [
  125. yearMonths
  126. ]);
  127. useEffect(() => {
  128. if(viewDateProp) {
  129. setViewDate(viewDateProp);
  130. }
  131. }, [viewDateProp]);
  132. useImperativeHandle(
  133. ref,
  134. () => ({
  135. changeCurrentYear: (date: Date) => {
  136. setViewDate(date);
  137. }
  138. }),
  139. []
  140. );
  141. const getMonthsOfViewYear = ({
  142. tDate
  143. }: {
  144. tDate: Date
  145. }): Array<CalendarMonth> => {
  146. allSelectedMonths.current = [];
  147. const targetDate = moment(new Date(tDate));
  148. const startOfCalendar = targetDate.clone().startOf("year");
  149. const months = monthOfYearLengthType === "short" ? moment.monthsShort() : moment.months();
  150. const totalMonths = months.length;
  151. const allMonths = Array.from({
  152. length: totalMonths
  153. }).map((_, index) => {
  154. const currentMonth = startOfCalendar.clone().add(index, "months");
  155. let isSelected = false;
  156. if(variant === "single" && date) {
  157. allSelectedMonths.current = [date];
  158. if(moment(currentMonth).isSame(date, "month")) isSelected = true;
  159. } else if(variant === "range" && dateRange && dateRange.start) {
  160. if(dateRange.end) {
  161. allSelectedMonths.current = Array.from({
  162. length: moment(dateRange.end).diff(dateRange.start, "months") + 1
  163. }, (_, i) =>
  164. moment(dateRange.start).clone().add(i, "months").toDate()
  165. );
  166. if(moment(currentMonth).isBetween(dateRange.start, dateRange.end, "month", "[]")) isSelected = true;
  167. } else if(moment(currentMonth).isSame(dateRange.start)) {
  168. allSelectedMonths.current = [dateRange.start];
  169. isSelected = true;
  170. }
  171. }
  172. let isDisabled = false;
  173. if(minDate && moment(currentMonth).isBefore(moment(minDate).startOf("month"))) {
  174. isDisabled = true;
  175. }
  176. if(maxDate && moment(currentMonth).isAfter(moment(maxDate).endOf("month"))) {
  177. isDisabled = true;
  178. }
  179. const monthNumber = currentMonth.month();
  180. return {
  181. originalIndex: allSelectedMonths.current.findIndex((month) => moment(currentMonth).isSame(month, "month")),
  182. isCurrentYear: currentMonth.isSame(targetDate, "year"),
  183. isToday: currentMonth.isSame(moment(), "month"),
  184. title: months[monthNumber] as string,
  185. monthNumber: monthNumber + 1,
  186. monthOfYear: monthNumber,
  187. date: currentMonth,
  188. isDisabled,
  189. isSelected
  190. };
  191. });
  192. return allMonths;
  193. };
  194. const renderDay = ({
  195. monthIndex,
  196. monthItem
  197. }: {
  198. monthItem: CalendarMonth;
  199. monthIndex: number;
  200. }) => {
  201. const isLastItemSelected = monthItem.isSelected && monthItem.originalIndex === allSelectedMonths.current.length - 1;
  202. const isFirstItemSelected = monthItem.isSelected && monthItem.originalIndex === 0;
  203. const selectionStyle: Array<Mutable<ViewStyle> | null> = [
  204. isFirstItemSelected && allSelectedMonths.current.length > 1 ? {
  205. borderBottomRightRadius: 0,
  206. borderTopRightRadius: 0
  207. } : null,
  208. isLastItemSelected && allSelectedMonths.current.length > 1 ? {
  209. borderBottomLeftRadius: 0,
  210. borderTopLeftRadius: 0
  211. } : null
  212. ];
  213. let dayTitleColor: keyof NCoreUIKit.TextContentColors = "mid";
  214. if(monthItem.isSelected && !isLastItemSelected && !isFirstItemSelected) {
  215. dayTitleColor = "emphasized";
  216. selectionStyle.push({
  217. borderBottomRightRadius: 0,
  218. borderBottomLeftRadius: 0,
  219. borderTopRightRadius: 0,
  220. borderTopLeftRadius: 0
  221. });
  222. } else if(monthItem.isSelected) {
  223. dayTitleColor = "onPrimary";
  224. }
  225. if(!monthItem.isCurrentYear || monthItem.isDisabled) {
  226. dayTitleColor = "disabled";
  227. }
  228. return <TouchableOpacity
  229. disabled={!monthItem.isCurrentYear || monthItem.isDisabled}
  230. key={`month-${monthIndex}`}
  231. onPress={() => {
  232. if(!monthItem.isCurrentYear) {
  233. return;
  234. }
  235. if(variant === "single") {
  236. const newDate = date as Date;
  237. newDate.setMonth(monthItem.date.toDate().getMonth());
  238. selectObject(newDate);
  239. } else if(variant === "range") {
  240. if(!dateRange || !dateRange.start) {
  241. selectMultipleObject({
  242. start: new Date(monthItem.date.toDate().setHours(0, 0, 0)),
  243. end: undefined
  244. });
  245. return;
  246. }
  247. if(dateRange && moment(dateRange.start).isSame(monthItem.date, "month")) {
  248. selectMultipleObject({
  249. start: undefined,
  250. end: undefined
  251. });
  252. return;
  253. }
  254. if(dateRange && !dateRange.end) {
  255. if(multipleSelectDayLimit && moment(monthItem.date).diff(dateRange.start, "month") >= multipleSelectDayLimit) {
  256. NCoreUIKitToast.open({
  257. title: localize("maximum-selection-number-of-days-limit-exceeds")
  258. });
  259. return;
  260. }
  261. if(multipleSelectMinimumRequiredDayCount && moment(monthItem.date).diff(dateRange.start, "month") + 1 < multipleSelectMinimumRequiredDayCount) {
  262. NCoreUIKitToast.open({
  263. title: localize("minimum-selection-number-of-days-required-not-provided")
  264. });
  265. return;
  266. }
  267. if(moment(monthItem.date).isBefore(dateRange.start)) {
  268. selectMultipleObject({
  269. start: new Date(monthItem.date.toDate().setHours(0, 0, 0)),
  270. end: undefined
  271. });
  272. } else {
  273. selectMultipleObject({
  274. end: new Date(monthItem.date.toDate().setHours(23, 59, 59)),
  275. start: dateRange?.start
  276. });
  277. }
  278. return;
  279. }
  280. selectMultipleObject({
  281. start: monthItem.date.toDate(),
  282. end: undefined
  283. });
  284. }
  285. }}
  286. style={[
  287. stylesheet.day,
  288. dayDynamicStyle,
  289. monthItem.isSelected ? {
  290. backgroundColor: colors.content.container.primary,
  291. borderColor: colors.content.container.primary
  292. } : null,
  293. monthItem.isSelected && !isLastItemSelected && !isFirstItemSelected ? {
  294. backgroundColor: colors.content.container.emphasized,
  295. borderColor: colors.content.container.emphasized
  296. } : null,
  297. monthItem.isSelected && (monthItem.isDisabled || !monthItem.isCurrentYear) ? {
  298. opacity: 0.33
  299. } : null,
  300. ...selectionStyle
  301. ]}
  302. >
  303. <Text
  304. variant="labelLargeSize"
  305. color={dayTitleColor}
  306. >
  307. {monthItem.title}
  308. </Text>
  309. {isShowTodayIndicator && monthItem.isToday ? <View
  310. style={[
  311. stylesheet.todayIndicator,
  312. selectionStyle,
  313. todayIndicatorDynamicStyle,
  314. monthItem.isSelected ? {
  315. borderColor: colors.content.border.subtle
  316. } : null
  317. ]}
  318. /> : null}
  319. </TouchableOpacity>;
  320. };
  321. const renderNextPrevTool = () => {
  322. return <View
  323. style={[
  324. stylesheet.nextPrevToolContainer,
  325. nextPrevToolContainerDynamicStyle
  326. ]}
  327. >
  328. <TouchableOpacity
  329. onPress={() => {
  330. const prevViewDate = moment(viewDate).clone().subtract(1, "years");
  331. setViewDate(prevViewDate.toDate());
  332. }}
  333. style={[
  334. nextPrevToolChevronButtonDynamicStyle
  335. ]}
  336. >
  337. <ChevronLeftIcon
  338. color={colors.content.icon.default}
  339. size={26}
  340. />
  341. </TouchableOpacity>
  342. <Text
  343. variant="labelLargeSize"
  344. color="high"
  345. >
  346. {moment(viewDate).format("YYYY")}
  347. </Text>
  348. <TouchableOpacity
  349. onPress={() => {
  350. const nextViewDate = moment(viewDate).clone().add(1, "years");
  351. setViewDate(nextViewDate.toDate());
  352. }}
  353. style={[
  354. nextPrevToolChevronButtonDynamicStyle
  355. ]}
  356. >
  357. <ChevronRightIcon
  358. color={colors.content.icon.default}
  359. size={26}
  360. />
  361. </TouchableOpacity>
  362. </View>;
  363. };
  364. return <View
  365. style={[
  366. stylesheet.container
  367. ]}
  368. >
  369. <View>
  370. {renderNextPrevTool()}
  371. </View>
  372. <View
  373. style={[
  374. stylesheet.contentContainer
  375. ]}
  376. >
  377. {yearMonths && yearMonths.length ? MONTH_LINES.map((mItem, mIndex) => {
  378. const currentMonths = mItem.map(mI => yearMonths[mI]);
  379. return <View
  380. key={`month-row-${mIndex}`}
  381. style={[
  382. stylesheet.rowContainer,
  383. webStyle({
  384. width: "100%"
  385. })
  386. ]}
  387. >
  388. {currentMonths.map((monthItem, monthIndex) => {
  389. return renderDay({
  390. monthItem: monthItem as CalendarMonth,
  391. monthIndex
  392. });
  393. })}
  394. </View>;
  395. }) : null}
  396. </View>
  397. </View>;
  398. };
  399. export default forwardRef(MonthSelector);