|
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