|
1 | | -import React, { |
2 | | - ReactNode, |
3 | | - useCallback, |
4 | | - useEffect, |
5 | | - useMemo, |
6 | | - useRef, |
7 | | - useState, |
8 | | -} from 'react'; |
9 | | -import {Box, ButtonBox, TextBox} from '../atoms'; |
| 1 | +import React, {useCallback} from 'react'; |
| 2 | +import {ArrowLeft, ArrowRight, Box, ButtonBox, TextBox} from '../atoms'; |
10 | 3 | import {ScrollView} from 'react-native'; |
11 | | -import {getDaysOfYear} from '../../utils/date.util'; |
12 | | -import { |
13 | | - DateFormatType, |
14 | | - DayItemType, |
15 | | - MonthOfYearType, |
16 | | - SelectedDateType, |
17 | | -} from '../../model'; |
| 4 | +import {CalendarBoxProps, MonthOfYearType} from '../../model'; |
18 | 5 | import {CalendarItemBox} from '../molecules'; |
19 | | - |
20 | | -interface Props { |
21 | | - format?: DateFormatType; |
22 | | - initDate?: string; |
23 | | - selectedDates?: SelectedDateType; |
24 | | - width?: number; |
25 | | - height?: number; |
26 | | - hideExtraDays?: boolean; |
27 | | - disablePressExtraDays?: boolean; |
28 | | - enableSpecialStyleExtraDays?: boolean; |
29 | | - classToday?: string; |
30 | | - classTextToday?: string; |
31 | | - classSelected?: string; |
32 | | - classTextSelected?: string; |
33 | | - classDay?: string; |
34 | | - classTextDay?: string; |
35 | | - classExtraDay?: string; |
36 | | - classTextExtraDay?: string; |
37 | | - horizontal?: boolean; |
38 | | - onChangeDate?: (date: { |
39 | | - year: number; |
40 | | - month: number; |
41 | | - day: number; |
42 | | - dateString: string; |
43 | | - }) => void; |
44 | | - renderDateItem?: (params: { |
45 | | - date: DayItemType; |
46 | | - dot?: boolean; |
47 | | - classDot?: string; |
48 | | - classBox: string; |
49 | | - classText: string; |
50 | | - }) => ReactNode; |
51 | | -} |
| 6 | +import {classNames} from '../../utils'; |
| 7 | +import {CALENDAR} from '../../config/Calendar'; |
| 8 | +import useCalendarBox from '../../hook/useCalendarBox'; |
52 | 9 |
|
53 | 10 | export const CalendarBox = ({ |
54 | 11 | width = 0, |
55 | 12 | height, |
56 | 13 | initDate, |
57 | 14 | selectedDates = {}, |
58 | | - format = 'YYYY-MM-DD', |
59 | 15 | hideExtraDays, |
60 | 16 | disablePressExtraDays = true, |
61 | 17 | enableSpecialStyleExtraDays, |
62 | 18 | horizontal = true, |
| 19 | + scrollEnabled = true, |
| 20 | + monthType = 'default', |
| 21 | + months, |
| 22 | + classBox, |
| 23 | + gap = 3, |
| 24 | + colorArrowLeft = '#000', |
| 25 | + colorArrowRight = '#000', |
| 26 | + enableControl = false, |
| 27 | + firstDay = 0, |
63 | 28 | onChangeDate, |
| 29 | + renderMonth, |
| 30 | + renderHeader, |
64 | 31 | ...rest |
65 | | -}: Props) => { |
66 | | - const refMonth = useRef<ScrollView>(null); |
67 | | - const [currentIndex, setCurrentIndex] = useState<number>(0); |
68 | | - const [offsetWidth, setOffsetWidth] = useState(width); |
69 | | - const [scrollEnabled, setScrollEnabled] = useState(true); |
70 | | - const [months, setMonths] = useState<MonthOfYearType[]>([]); |
71 | | - const firstRender = useRef(true); |
72 | | - const refMonthUpdate = useRef<NodeJS.Timeout>(); |
73 | | - |
74 | | - const widthDay: number = useMemo(() => { |
75 | | - if (offsetWidth > 0) { |
76 | | - return Math.floor(((offsetWidth - 1) / 7) * 10) / 10; |
77 | | - } |
78 | | - return 0; |
79 | | - }, [offsetWidth]); |
80 | | - |
81 | | - useEffect(() => { |
82 | | - const initMonths = () => { |
83 | | - const targetDate = initDate ? new Date(initDate) : new Date(); |
84 | | - const year = targetDate.getFullYear(); |
85 | | - const currentMonth = getDaysOfYear(year, format); |
86 | | - const preMonth = getDaysOfYear(year - 1, format); |
87 | | - const nextMonth = getDaysOfYear(year + 1, format); |
88 | | - const monthIndex = targetDate.getMonth(); |
89 | | - setMonths([...preMonth, ...currentMonth, ...nextMonth]); |
90 | | - setCurrentIndex(12 + monthIndex); |
91 | | - }; |
92 | | - initMonths(); |
93 | | - }, [format, initDate]); |
94 | | - |
95 | | - useEffect(() => { |
96 | | - if ( |
97 | | - firstRender.current && |
98 | | - months.length > 0 && |
99 | | - offsetWidth > 0 && |
100 | | - currentIndex >= 0 |
101 | | - ) { |
102 | | - setTimeout(() => { |
103 | | - scrollToIndex(currentIndex); |
104 | | - }, 250); |
105 | | - firstRender.current = false; |
106 | | - } |
107 | | - }, [offsetWidth, months, currentIndex]); |
108 | | - |
109 | | - const getMoreMonth = (type: 'prev' | 'next' = 'next', index = 0) => { |
110 | | - const currentMonth = months[index]; |
111 | | - if (type === 'next') { |
112 | | - const perMonths = getDaysOfYear(currentMonth.year + 1); |
113 | | - setMonths([...months, ...perMonths]); |
114 | | - return; |
115 | | - } |
116 | | - const perMonths = getDaysOfYear(currentMonth.year - 1); |
117 | | - setMonths([...perMonths, ...months]); |
118 | | - }; |
119 | | - |
120 | | - const scrollToIndex = (index: number) => { |
121 | | - setTimeout(() => { |
122 | | - if (refMonth.current) { |
123 | | - const params = horizontal |
124 | | - ? {x: offsetWidth * index + 1, y: 0, animated: false} |
125 | | - : {y: offsetWidth * index + 1, x: 0, animated: false}; |
126 | | - refMonth.current.scrollTo(params); |
127 | | - } |
128 | | - }, 0); |
129 | | - }; |
130 | | - |
131 | | - const currentMonth = useMemo(() => months?.[currentIndex], [currentIndex]); |
| 32 | +}: CalendarBoxProps) => { |
| 33 | + const { |
| 34 | + monthsData, |
| 35 | + weekData, |
| 36 | + widthDay, |
| 37 | + currentMonth, |
| 38 | + offsetWidth, |
| 39 | + refScroll, |
| 40 | + currentIndex, |
| 41 | + firstRender, |
| 42 | + isLoading, |
| 43 | + blockUpdateIndex, |
| 44 | + controlMonth, |
| 45 | + setState, |
| 46 | + } = useCalendarBox({ |
| 47 | + initDate, |
| 48 | + width, |
| 49 | + firstDay, |
| 50 | + horizontal, |
| 51 | + weeks: rest?.weeks, |
| 52 | + weekType: rest?.weekType, |
| 53 | + format: rest?.format, |
| 54 | + minYear: rest?.minYear, |
| 55 | + maxYear: rest?.maxYear, |
| 56 | + }); |
132 | 57 |
|
133 | 58 | const renderWeek = useCallback(() => { |
134 | | - return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(item => ( |
| 59 | + return weekData.map(item => ( |
135 | 60 | <Box |
136 | 61 | key={`week-${item}`} |
| 62 | + className={rest?.classWeek} |
137 | 63 | style={{ |
138 | 64 | width: widthDay, |
139 | 65 | }}> |
140 | | - <TextBox className="text-center font-bold" key={`week-${item}`}> |
| 66 | + <TextBox |
| 67 | + className={classNames('text-center font-bold', rest?.classTextWeek)} |
| 68 | + key={`week-${item}`}> |
141 | 69 | {item} |
142 | 70 | </TextBox> |
143 | 71 | </Box> |
144 | 72 | )); |
145 | | - }, [widthDay]); |
| 73 | + }, [widthDay, rest?.classTextWeek, rest?.classWeek, weekData]); |
146 | 74 |
|
147 | | - const renderMonth = useCallback( |
148 | | - () => ( |
149 | | - <ButtonBox className="row self-center mt-4"> |
150 | | - <TextBox className="text-center text-black font-bold"> |
151 | | - {`Month ${currentMonth?.month} ${currentMonth?.year}`} |
152 | | - </TextBox> |
153 | | - </ButtonBox> |
154 | | - ), |
155 | | - [offsetWidth, currentMonth], |
| 75 | + const renderMonthItem = useCallback( |
| 76 | + () => |
| 77 | + renderHeader ? ( |
| 78 | + renderHeader(currentMonth) |
| 79 | + ) : ( |
| 80 | + <Box |
| 81 | + className={classNames( |
| 82 | + 'row-center w-full justify-center py-1', |
| 83 | + rest?.classBoxHeader, |
| 84 | + )}> |
| 85 | + {enableControl && ( |
| 86 | + <ButtonBox |
| 87 | + onPress={() => controlMonth('prev')} |
| 88 | + className={classNames('absolute left-4', rest.classBoxArrowLeft)}> |
| 89 | + <ArrowLeft width={16} fill={colorArrowLeft} /> |
| 90 | + </ButtonBox> |
| 91 | + )} |
| 92 | + <ButtonBox> |
| 93 | + {renderMonth ? ( |
| 94 | + renderMonth({ |
| 95 | + year: currentMonth?.year, |
| 96 | + month: currentMonth?.month, |
| 97 | + }) |
| 98 | + ) : ( |
| 99 | + <TextBox |
| 100 | + className={classNames( |
| 101 | + 'text-center text-black font-bold text-lg', |
| 102 | + rest.classTextMonth, |
| 103 | + )}> |
| 104 | + {months |
| 105 | + ? months[currentMonth?.month - 1] |
| 106 | + : monthType === 'default' |
| 107 | + ? currentMonth?.month |
| 108 | + : CALENDAR.month[monthType][currentMonth?.month - 1]}{' '} |
| 109 | + /{' '} |
| 110 | + <TextBox className={rest.classTextYear}> |
| 111 | + {currentMonth?.year} |
| 112 | + </TextBox> |
| 113 | + </TextBox> |
| 114 | + )} |
| 115 | + </ButtonBox> |
| 116 | + {enableControl && ( |
| 117 | + <ButtonBox |
| 118 | + onPress={() => controlMonth('next')} |
| 119 | + className={classNames( |
| 120 | + 'absolute right-4', |
| 121 | + rest.classBoxArrowRight, |
| 122 | + )}> |
| 123 | + <ArrowRight width={16} fill={colorArrowRight} /> |
| 124 | + </ButtonBox> |
| 125 | + )} |
| 126 | + </Box> |
| 127 | + ), |
| 128 | + [ |
| 129 | + months, |
| 130 | + offsetWidth, |
| 131 | + currentMonth, |
| 132 | + rest?.classTextYear, |
| 133 | + rest?.classTextMonth, |
| 134 | + rest?.classBoxHeader, |
| 135 | + rest?.classBoxArrowRight, |
| 136 | + rest?.classBoxArrowLeft, |
| 137 | + colorArrowRight, |
| 138 | + colorArrowLeft, |
| 139 | + monthType, |
| 140 | + ], |
156 | 141 | ); |
157 | 142 |
|
158 | 143 | const renderDate = useCallback( |
@@ -190,55 +175,51 @@ export const CalendarBox = ({ |
190 | 175 | <Box |
191 | 176 | onLayout={({nativeEvent}) => { |
192 | 177 | if (!offsetWidth) { |
193 | | - setOffsetWidth(Number(nativeEvent.layout.width.toFixed(1))); |
| 178 | + setState(pre => ({ |
| 179 | + ...pre, |
| 180 | + offsetWidth: Number(nativeEvent.layout.width.toFixed(1)), |
| 181 | + })); |
194 | 182 | } |
195 | 183 | }} |
196 | | - className="w-full gap-4" |
| 184 | + className={classNames(`w-full gap-${gap}`, classBox)} |
197 | 185 | style={{width: offsetWidth || undefined, height}}> |
198 | | - {months.length > 0 && ( |
| 186 | + {monthsData.length > 0 && ( |
199 | 187 | <> |
200 | | - {renderMonth()} |
| 188 | + {renderMonthItem()} |
201 | 189 | <Box className="row flex-wrap">{renderWeek()}</Box> |
202 | 190 | <ScrollView |
203 | | - ref={refMonth} |
204 | | - scrollEnabled={scrollEnabled} |
| 191 | + ref={refScroll} |
| 192 | + scrollEnabled={scrollEnabled && !isLoading} |
205 | 193 | scrollEventThrottle={15} |
| 194 | + showsHorizontalScrollIndicator={false} |
| 195 | + showsVerticalScrollIndicator={false} |
206 | 196 | onScroll={({nativeEvent}) => { |
207 | | - if (!firstRender.current) { |
| 197 | + if (!firstRender.current && !isLoading) { |
208 | 198 | const index = Math.round( |
209 | 199 | nativeEvent.contentOffset.x / offsetWidth, |
210 | 200 | ); |
211 | | - if (refMonthUpdate.current) { |
212 | | - clearTimeout(refMonthUpdate.current); |
| 201 | + |
| 202 | + if (index !== currentIndex && !blockUpdateIndex) { |
| 203 | + setState(pre => ({ |
| 204 | + ...pre, |
| 205 | + currentIndex: index, |
| 206 | + })); |
| 207 | + } else { |
| 208 | + setState(pre => ({ |
| 209 | + ...pre, |
| 210 | + blockUpdateIndex: false, |
| 211 | + })); |
213 | 212 | } |
214 | | - refMonthUpdate.current = setTimeout(() => { |
215 | | - if (index !== currentIndex) { |
216 | | - if (index < 12 && months.length > 0) { |
217 | | - setScrollEnabled(false); |
218 | | - setTimeout(() => { |
219 | | - getMoreMonth('prev', index); |
220 | | - scrollToIndex(index + 12); |
221 | | - setCurrentIndex(index + 12); |
222 | | - setScrollEnabled(true); |
223 | | - }, 120); |
224 | | - } else if (months.length - index < 12) { |
225 | | - getMoreMonth('next', index); |
226 | | - setCurrentIndex(index); |
227 | | - } else { |
228 | | - setCurrentIndex(index); |
229 | | - } |
230 | | - } |
231 | | - }, 25); |
232 | 213 | } |
233 | 214 | }} |
234 | 215 | horizontal={horizontal} |
235 | 216 | pagingEnabled> |
236 | | - {months.map((item, index) => { |
| 217 | + {monthsData.map((item, index) => { |
237 | 218 | if (index > currentIndex + 2 || index < currentIndex - 1) { |
238 | 219 | return ( |
239 | 220 | <Box |
240 | 221 | key={item.month + '-' + item.year} |
241 | | - style={{width: offsetWidth}}></Box> |
| 222 | + style={{width: offsetWidth, height: offsetWidth}}></Box> |
242 | 223 | ); |
243 | 224 | } |
244 | 225 | return renderDate(item); |
|
0 commit comments