Skip to content

Commit 16071c0

Browse files
authored
Merge pull request #36 from sangyuo/feat/custom-calendar
feat: Custom styles calendar
2 parents 26d3e00 + 699cfef commit 16071c0

File tree

6 files changed

+432
-161
lines changed

6 files changed

+432
-161
lines changed

App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import {SafeAreaView} from 'react-native';
33
import {CalendarBox} from './src/atomic/organisms/CalendarBox';
44
import {formatDate} from './src/utils/date.util';
5+
import {TextBox} from './src';
56

67
function App(): React.JSX.Element {
78
const [value, setValue] = React.useState(formatDate('2024-02-02'));
@@ -21,8 +22,13 @@ function App(): React.JSX.Element {
2122
return (
2223
<SafeAreaView style={{flex: 1, backgroundColor: 'white'}}>
2324
<CalendarBox
25+
monthType="long"
26+
firstDay={1}
27+
minYear={2020}
28+
maxYear={2026}
2429
initDate={value}
2530
selectedDates={selectedDates}
31+
months={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]}
2632
onChangeDate={({dateString}) => {
2733
setSelectedDates({[dateString]: {}});
2834
}}

src/atomic/organisms/CalendarBox.tsx

Lines changed: 139 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,143 @@
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';
103
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';
185
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';
529

5310
export const CalendarBox = ({
5411
width = 0,
5512
height,
5613
initDate,
5714
selectedDates = {},
58-
format = 'YYYY-MM-DD',
5915
hideExtraDays,
6016
disablePressExtraDays = true,
6117
enableSpecialStyleExtraDays,
6218
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,
6328
onChangeDate,
29+
renderMonth,
30+
renderHeader,
6431
...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+
});
13257

13358
const renderWeek = useCallback(() => {
134-
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(item => (
59+
return weekData.map(item => (
13560
<Box
13661
key={`week-${item}`}
62+
className={rest?.classWeek}
13763
style={{
13864
width: widthDay,
13965
}}>
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}`}>
14169
{item}
14270
</TextBox>
14371
</Box>
14472
));
145-
}, [widthDay]);
73+
}, [widthDay, rest?.classTextWeek, rest?.classWeek, weekData]);
14674

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+
],
156141
);
157142

158143
const renderDate = useCallback(
@@ -190,55 +175,51 @@ export const CalendarBox = ({
190175
<Box
191176
onLayout={({nativeEvent}) => {
192177
if (!offsetWidth) {
193-
setOffsetWidth(Number(nativeEvent.layout.width.toFixed(1)));
178+
setState(pre => ({
179+
...pre,
180+
offsetWidth: Number(nativeEvent.layout.width.toFixed(1)),
181+
}));
194182
}
195183
}}
196-
className="w-full gap-4"
184+
className={classNames(`w-full gap-${gap}`, classBox)}
197185
style={{width: offsetWidth || undefined, height}}>
198-
{months.length > 0 && (
186+
{monthsData.length > 0 && (
199187
<>
200-
{renderMonth()}
188+
{renderMonthItem()}
201189
<Box className="row flex-wrap">{renderWeek()}</Box>
202190
<ScrollView
203-
ref={refMonth}
204-
scrollEnabled={scrollEnabled}
191+
ref={refScroll}
192+
scrollEnabled={scrollEnabled && !isLoading}
205193
scrollEventThrottle={15}
194+
showsHorizontalScrollIndicator={false}
195+
showsVerticalScrollIndicator={false}
206196
onScroll={({nativeEvent}) => {
207-
if (!firstRender.current) {
197+
if (!firstRender.current && !isLoading) {
208198
const index = Math.round(
209199
nativeEvent.contentOffset.x / offsetWidth,
210200
);
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+
}));
213212
}
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);
232213
}
233214
}}
234215
horizontal={horizontal}
235216
pagingEnabled>
236-
{months.map((item, index) => {
217+
{monthsData.map((item, index) => {
237218
if (index > currentIndex + 2 || index < currentIndex - 1) {
238219
return (
239220
<Box
240221
key={item.month + '-' + item.year}
241-
style={{width: offsetWidth}}></Box>
222+
style={{width: offsetWidth, height: offsetWidth}}></Box>
242223
);
243224
}
244225
return renderDate(item);

0 commit comments

Comments
 (0)