daliry-mobile-date-picker 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +10 -10
- package/.gitattributes +4 -4
- package/.yarnrc.yml +1 -1
- package/README.md +138 -26
- package/build.js +15 -15
- package/daliry-mobile-date-picker-1.0.2.tgz +0 -0
- package/dist/MobileDatePicker.d.ts +11 -7
- package/dist/MobileDatePicker.d.ts.map +1 -0
- package/dist/MobileDatePicker.js +105 -0
- package/dist/index.css +34 -15
- package/dist/index.d.ts +3 -3
- package/dist/index.js +155 -127
- package/dist/src/MobileDatePicker.d.ts +21 -0
- package/package.json +17 -7
- package/src/MobileDatePicker.tsx +144 -92
- package/src/assets/images/daliry-mobile-date-picker102.PNG +0 -0
- package/src/global.d.ts +1 -0
- package/src/mobileDatePicker.css +36 -15
- package/tsconfig.json +18 -18
- package/.idea/vcs.xml +0 -6
- package/.idea/workspace.xml +0 -813
- package/src/index.tsx +0 -12
package/src/MobileDatePicker.tsx
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { RefObject, UIEvent } from "react";
|
|
3
|
+
import moment from "moment-jalaali";
|
|
4
|
+
import type { Moment } from "moment";
|
|
3
5
|
import "./mobileDatePicker.css";
|
|
4
|
-
// export * from '.';
|
|
5
6
|
|
|
6
7
|
export interface IDate {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
year: number;
|
|
9
|
+
month: number;
|
|
10
|
+
day: number;
|
|
11
|
+
formatted: string;
|
|
11
12
|
date: string;
|
|
12
13
|
gDate: string;
|
|
13
14
|
moment: Moment;
|
|
@@ -16,108 +17,159 @@ export interface IDate {
|
|
|
16
17
|
interface IMobileDatePicker {
|
|
17
18
|
onDateChange: (date: IDate) => void;
|
|
18
19
|
isBirthdate?: boolean;
|
|
20
|
+
isGregorian?: boolean;
|
|
21
|
+
backgroundColor?: string;
|
|
22
|
+
textColor?: string;
|
|
23
|
+
selectedColor?: string;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
"مهر", "آبان", "آذر", "دی", "بهمن", "اسفند", ""
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
const years = Array.from({ length: 1308 }, (_, i) => 1308 + i);
|
|
27
|
-
const days = Array.from({ length: 33 }, (_, i) => i);
|
|
28
|
-
|
|
29
|
-
const MobileDatePicker = ({ onDateChange, isBirthdate = false }: IMobileDatePicker) => {
|
|
30
|
-
const today = moment();
|
|
31
|
-
const [selectedYear, setSelectedYear] = useState(isBirthdate ? 1388 : today.jYear() - 1);
|
|
32
|
-
const [selectedMonth, setSelectedMonth] = useState(isBirthdate ? 6 : today.jMonth());
|
|
33
|
-
const [selectedDay, setSelectedDay] = useState(isBirthdate ? 2 : today.jDate() - 1);
|
|
34
|
-
|
|
35
|
-
const listRef = useRef<HTMLDivElement>(null);
|
|
36
|
-
const listRefMonths = useRef<HTMLDivElement>(null);
|
|
37
|
-
const listRefDays = useRef<HTMLDivElement>(null);
|
|
38
|
-
|
|
39
|
-
const selectDateFunction = () => {
|
|
40
|
-
const gDate = moment(`${selectedYear}-${selectedMonth}-${selectedDay}`, "jYYYY-jMM-jDD").format("YYYY-MM-DD");
|
|
41
|
-
onDateChange({
|
|
42
|
-
jYear: selectedYear,
|
|
43
|
-
jMonth: selectedMonth,
|
|
44
|
-
jDay: selectedDay,
|
|
45
|
-
jDate: `${selectedYear}-${selectedMonth}-${selectedDay}`,
|
|
46
|
-
date: gDate,
|
|
47
|
-
gDate,
|
|
48
|
-
moment: moment(`${selectedYear}-${selectedMonth}-${selectedDay}`, "jYYYY-jMM-jDD"),
|
|
49
|
-
});
|
|
50
|
-
};
|
|
26
|
+
const JALALI_MONTHS = ["فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند"];
|
|
27
|
+
const GREGORIAN_MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
51
28
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
29
|
+
const ITEM_HEIGHT = 45;
|
|
30
|
+
|
|
31
|
+
const DaliryMobileDatePicker = ({
|
|
32
|
+
onDateChange,
|
|
33
|
+
isBirthdate = false,
|
|
34
|
+
isGregorian = false,
|
|
35
|
+
backgroundColor = "#f5f5f5",
|
|
36
|
+
textColor = "#bbb",
|
|
37
|
+
selectedColor = "#333",
|
|
38
|
+
}: IMobileDatePicker) => {
|
|
39
|
+
const now = moment();
|
|
40
|
+
|
|
41
|
+
const initialYear = useMemo(() => {
|
|
42
|
+
const currentYear = isGregorian ? now.year() : now.jYear();
|
|
43
|
+
return isBirthdate ? currentYear - 18 : currentYear;
|
|
44
|
+
}, [isGregorian, isBirthdate]);
|
|
45
|
+
|
|
46
|
+
const initialMonth = isGregorian ? now.month() + 1 : now.jMonth() + 1;
|
|
47
|
+
const initialDay = isGregorian ? now.date() : now.jDate();
|
|
48
|
+
|
|
49
|
+
const [selectedYear, setSelectedYear] = useState<number>(initialYear);
|
|
50
|
+
const [selectedMonth, setSelectedMonth] = useState<number>(initialMonth);
|
|
51
|
+
const [selectedDay, setSelectedDay] = useState<number>(initialDay);
|
|
52
|
+
|
|
53
|
+
const yearRef = useRef<HTMLDivElement | null>(null);
|
|
54
|
+
const monthRef = useRef<HTMLDivElement | null>(null);
|
|
55
|
+
const dayRef = useRef<HTMLDivElement | null>(null);
|
|
56
|
+
|
|
57
|
+
const months = isGregorian ? GREGORIAN_MONTHS : JALALI_MONTHS;
|
|
58
|
+
|
|
59
|
+
const years = useMemo(() => {
|
|
60
|
+
const start = isGregorian ? 1930 : 1300;
|
|
61
|
+
const end = (isGregorian ? now.year() : now.jYear()) + 20;
|
|
62
|
+
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
63
|
+
}, [isGregorian, now]);
|
|
64
|
+
|
|
65
|
+
const daysInMonth = useMemo(() => {
|
|
66
|
+
if (isGregorian) {
|
|
67
|
+
return moment(`${selectedYear}-${selectedMonth}-01`, "YYYY-M-DD").daysInMonth();
|
|
68
|
+
}
|
|
69
|
+
return moment.jDaysInMonth(selectedYear, selectedMonth - 1);
|
|
70
|
+
}, [isGregorian, selectedYear, selectedMonth]);
|
|
71
|
+
|
|
72
|
+
const days = useMemo(() => {
|
|
73
|
+
return Array.from({ length: daysInMonth }, (_, i) => i + 1);
|
|
74
|
+
}, [daysInMonth]);
|
|
55
75
|
|
|
56
76
|
useEffect(() => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}, []);
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
77
|
+
if (selectedDay > daysInMonth) {
|
|
78
|
+
setSelectedDay(daysInMonth);
|
|
79
|
+
}
|
|
80
|
+
}, [selectedDay, daysInMonth]);
|
|
81
|
+
|
|
82
|
+
const buildDateObject = (year: number, month: number, day: number): IDate => {
|
|
83
|
+
const m = isGregorian
|
|
84
|
+
? moment(`${year}-${month}-${day}`, "YYYY-M-D")
|
|
85
|
+
: moment(`${year}-${month}-${day}`, "jYYYY-jM-jD");
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
year,
|
|
89
|
+
month,
|
|
90
|
+
day,
|
|
91
|
+
formatted: isGregorian ? m.format("YYYY/MM/DD") : m.format("jYYYY/jMM/jDD"),
|
|
92
|
+
date: m.format("YYYY-MM-DD"),
|
|
93
|
+
gDate: m.format("YYYY-MM-DD"),
|
|
94
|
+
moment: m,
|
|
95
|
+
};
|
|
65
96
|
};
|
|
66
97
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
98
|
+
// پیادهسازی Debounce برای ارسال تاریخ
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
onDateChange(buildDateObject(selectedYear, selectedMonth, selectedDay));
|
|
102
|
+
}, 500); // ۵۰۰ میلیثانیه صبر بعد از آخرین تغییر
|
|
71
103
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
setSelectedDay(index + 1);
|
|
75
|
-
};
|
|
104
|
+
return () => clearTimeout(timer);
|
|
105
|
+
}, [selectedYear, selectedMonth, selectedDay, isGregorian]);
|
|
76
106
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<div
|
|
83
|
-
key={i}
|
|
84
|
-
className={`scroll-item ${day === selectedDay ? "selected" : ""}`}
|
|
85
|
-
>
|
|
86
|
-
{day}
|
|
87
|
-
</div>
|
|
88
|
-
))}
|
|
89
|
-
</div>
|
|
107
|
+
const scrollToIndex = (ref: RefObject<HTMLDivElement | null>, index: number) => {
|
|
108
|
+
if (ref.current) {
|
|
109
|
+
ref.current.scrollTop = index * ITEM_HEIGHT;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
90
112
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const monthIndex = Math.max(0, selectedMonth - 1);
|
|
115
|
+
const dayIndex = Math.max(0, selectedDay - 1);
|
|
116
|
+
const yearIndex = Math.max(0, years.indexOf(selectedYear));
|
|
117
|
+
|
|
118
|
+
const timer = window.setTimeout(() => {
|
|
119
|
+
scrollToIndex(dayRef, dayIndex);
|
|
120
|
+
scrollToIndex(monthRef, monthIndex);
|
|
121
|
+
scrollToIndex(yearRef, yearIndex);
|
|
122
|
+
}, 0);
|
|
123
|
+
|
|
124
|
+
return () => window.clearTimeout(timer);
|
|
125
|
+
}, [years]); // فقط وقتی لیست سالها (تقویم) عوض شد ریاسکرول کن
|
|
126
|
+
|
|
127
|
+
const handleScroll = (e: UIEvent<HTMLDivElement>, type: "day" | "month" | "year") => {
|
|
128
|
+
const index = Math.round(e.currentTarget.scrollTop / ITEM_HEIGHT);
|
|
129
|
+
|
|
130
|
+
if (type === "day") {
|
|
131
|
+
const nextDay = days[index];
|
|
132
|
+
if (nextDay && nextDay !== selectedDay) setSelectedDay(nextDay);
|
|
133
|
+
} else if (type === "month") {
|
|
134
|
+
const nextMonth = index + 1;
|
|
135
|
+
if (nextMonth >= 1 && nextMonth <= 12 && nextMonth !== selectedMonth) setSelectedMonth(nextMonth);
|
|
136
|
+
} else {
|
|
137
|
+
const nextYear = years[index];
|
|
138
|
+
if (nextYear && nextYear !== selectedYear) setSelectedYear(nextYear);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
101
141
|
|
|
102
|
-
|
|
103
|
-
|
|
142
|
+
const renderList = (items: Array<string | number>, selectedValue: string | number, ref: RefObject<HTMLDivElement | null>, type: "day" | "month" | "year") => {
|
|
143
|
+
return (
|
|
144
|
+
<div className="scroll-list" onScroll={(e) => handleScroll(e, type)} ref={ref}>
|
|
145
|
+
<div style={{ height: ITEM_HEIGHT }} />
|
|
146
|
+
{items.map((item, index) => {
|
|
147
|
+
const isSelected = type === "month" ? (index + 1) === selectedMonth : item === selectedValue;
|
|
148
|
+
return (
|
|
104
149
|
<div
|
|
105
|
-
key={
|
|
106
|
-
className={`scroll-item ${
|
|
150
|
+
key={`${type}-${item}`}
|
|
151
|
+
className={`scroll-item ${isSelected ? "selected" : ""}`}
|
|
152
|
+
style={{ color: isSelected ? selectedColor : textColor }}
|
|
107
153
|
>
|
|
108
|
-
{
|
|
154
|
+
{item}
|
|
109
155
|
</div>
|
|
110
|
-
)
|
|
111
|
-
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
<div style={{ height: ITEM_HEIGHT }} />
|
|
112
159
|
</div>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
113
162
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
163
|
+
return (
|
|
164
|
+
<div className="datepicker-container">
|
|
165
|
+
<div className="scroll-lists" style={{ backgroundColor }}>
|
|
166
|
+
{renderList(days, selectedDay, dayRef, "day")}
|
|
167
|
+
{renderList(months, months[selectedMonth - 1], monthRef, "month")}
|
|
168
|
+
{renderList(years, selectedYear, yearRef, "year")}
|
|
169
|
+
<div className="selection-highlight" style={{ top: ITEM_HEIGHT, height: ITEM_HEIGHT }} />
|
|
170
|
+
</div>
|
|
119
171
|
</div>
|
|
120
172
|
);
|
|
121
173
|
};
|
|
122
174
|
|
|
123
|
-
export default
|
|
175
|
+
export default DaliryMobileDatePicker;
|
|
Binary file
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css";
|
package/src/mobileDatePicker.css
CHANGED
|
@@ -2,26 +2,30 @@
|
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
align-items: center;
|
|
5
|
-
gap: 16px;
|
|
6
5
|
width: 100%;
|
|
6
|
+
min-width: 350px;
|
|
7
|
+
user-select: none;
|
|
8
|
+
padding: 10px;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
.scroll-lists {
|
|
10
12
|
display: flex;
|
|
11
13
|
justify-content: space-between;
|
|
12
|
-
background-color: #f5f5f5;
|
|
13
14
|
border-radius: 12px;
|
|
14
|
-
height:
|
|
15
|
+
height: 135px; /* 45px * 3 */
|
|
15
16
|
width: 100%;
|
|
17
|
+
position: relative;
|
|
18
|
+
overflow: hidden;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
.scroll-list {
|
|
19
|
-
|
|
20
|
-
height:
|
|
22
|
+
flex: 1;
|
|
23
|
+
height: 135px;
|
|
21
24
|
overflow-y: auto;
|
|
22
25
|
scroll-snap-type: y mandatory;
|
|
23
26
|
scrollbar-width: none;
|
|
24
27
|
-ms-overflow-style: none;
|
|
28
|
+
z-index: 2;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
.scroll-list::-webkit-scrollbar {
|
|
@@ -33,27 +37,44 @@
|
|
|
33
37
|
line-height: 45px;
|
|
34
38
|
text-align: center;
|
|
35
39
|
scroll-snap-align: center;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
font-weight: normal;
|
|
40
|
+
font-size: 19px;
|
|
41
|
+
opacity: 0.4;
|
|
42
|
+
filter: blur(2px);
|
|
43
|
+
transform: scale(0.8);
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
.scroll-item.selected {
|
|
44
|
-
color: #333;
|
|
45
|
-
font-size: 20px;
|
|
46
47
|
font-weight: bold;
|
|
47
48
|
opacity: 1;
|
|
48
49
|
filter: none;
|
|
50
|
+
transform: scale(1);
|
|
51
|
+
transition: all 0.1s ease;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.selection-highlight {
|
|
55
|
+
position: absolute;
|
|
56
|
+
top: 45px; /* دقیقا ردیف وسط */
|
|
57
|
+
left: 10px;
|
|
58
|
+
right: 10px;
|
|
59
|
+
height: 45px;
|
|
60
|
+
border-top: 1px solid rgba(0,0,0,0.05);
|
|
61
|
+
border-bottom: 1px solid rgba(0,0,0,0.05);
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
z-index: 1;
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
.apply-button {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
width: 100%;
|
|
68
|
+
margin-top: 16px;
|
|
69
|
+
padding: 12px;
|
|
55
70
|
border: none;
|
|
56
71
|
border-radius: 8px;
|
|
57
72
|
font-size: 16px;
|
|
73
|
+
font-weight: bold;
|
|
58
74
|
cursor: pointer;
|
|
75
|
+
transition: opacity 0.2s;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.apply-button:active {
|
|
79
|
+
opacity: 0.8;
|
|
59
80
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"declaration": true,
|
|
4
|
-
"emitDeclarationOnly": true,
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"allowSyntheticDefaultImports": true
|
|
15
|
-
},
|
|
16
|
-
"include": ["src"],
|
|
17
|
-
"exclude": ["node_modules", "dist"]
|
|
18
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"declaration": true,
|
|
4
|
+
"emitDeclarationOnly": true,
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"module": "ESNext",
|
|
11
|
+
"target": "ESNext",
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"strict": true,
|
|
14
|
+
"allowSyntheticDefaultImports": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|