ar-design 0.4.53 → 0.4.54
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/dist/assets/css/components/charts/gantt/styles.css +49 -0
- package/dist/assets/css/components/navigation/menu/styles.css +2 -2
- package/dist/components/charts/gantt/index.d.ts +4 -0
- package/dist/components/charts/gantt/index.js +193 -0
- package/dist/components/form/input/phone/Phone.js +9 -19
- package/dist/components/form/select/Props.d.ts +3 -0
- package/dist/components/form/select/index.js +47 -75
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -0
- package/dist/libs/core/application/hooks/useValidation.js +104 -100
- package/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.ar-gantt-chart {
|
|
2
|
+
background-color: var(--gray-700);
|
|
3
|
+
fill: var(--gray-700);
|
|
4
|
+
border-radius: var(--border-radius-lg);
|
|
5
|
+
font-family: var(--system);
|
|
6
|
+
box-shadow: 0px 10px 15px -5px rgba(var(--black-rgb), 0.1);
|
|
7
|
+
|
|
8
|
+
> .header {
|
|
9
|
+
fill: var(--white);
|
|
10
|
+
height: 75px;
|
|
11
|
+
|
|
12
|
+
> .title-group {
|
|
13
|
+
> .title {
|
|
14
|
+
fill: var(--gray-700);
|
|
15
|
+
font-size: 16px;
|
|
16
|
+
font-weight: bold;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
> .title-description {
|
|
20
|
+
fill: var(--gray-500);
|
|
21
|
+
font-size: 13.28px;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
> .body {
|
|
27
|
+
> .time-and-bars {
|
|
28
|
+
transition: transform 250ms ease-in-out;
|
|
29
|
+
|
|
30
|
+
&.dragging {
|
|
31
|
+
transition: none !important;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
> .left-axis {
|
|
36
|
+
fill: var(--white);
|
|
37
|
+
|
|
38
|
+
> .label-list {
|
|
39
|
+
> .label-row {
|
|
40
|
+
> .label-text {
|
|
41
|
+
fill: var(--gray-700);
|
|
42
|
+
font-size: 14px;
|
|
43
|
+
dominant-baseline: central;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
li {
|
|
15
15
|
list-style: none;
|
|
16
|
-
height: 2rem
|
|
16
|
+
height: auto; /* DÜZELTİLMEDİ: Sabit 2rem kaldırıldı, li'nin alt menüyle büyümesi sağlandı */
|
|
17
17
|
|
|
18
18
|
> ul.submenu {
|
|
19
19
|
display: grid;
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
align-items: center;
|
|
47
47
|
gap: 0.5rem;
|
|
48
48
|
width: 100%;
|
|
49
|
-
height: inherit
|
|
49
|
+
height: 2rem; /* DÜZELTİLMEDİ: inherit yerine ana eleman yüksekliği buraya sabitlendi */
|
|
50
50
|
padding: 0.3rem;
|
|
51
51
|
white-space: nowrap;
|
|
52
52
|
cursor: pointer;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React, { useCallback, useRef, useState } from "react";
|
|
2
|
+
import "../../../assets/css/components/charts/gantt/styles.css";
|
|
3
|
+
const tasks = [
|
|
4
|
+
{
|
|
5
|
+
id: "ffa5c782-f482-49ed-acd5-f5cab8b2c39e",
|
|
6
|
+
name: "Turksat2LoV10000",
|
|
7
|
+
start: "2026-04-22T09:30:00Z",
|
|
8
|
+
end: "2026-04-22T14:30:00Z",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: "ff68947f-b2c2-4621-b5db-fd57702d037a",
|
|
12
|
+
name: "Turksat2LoV10000",
|
|
13
|
+
start: "2026-08-31T09:30:00Z",
|
|
14
|
+
end: "2026-08-31T14:30:00Z",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "ff2e87f5-9b3c-470d-bda9-dfea1851680c",
|
|
18
|
+
name: "IRD 6",
|
|
19
|
+
start: "2026-04-15T10:30:00Z",
|
|
20
|
+
end: "2026-04-15T17:30:00Z",
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
const Gantt = () => {
|
|
24
|
+
// refs
|
|
25
|
+
const _svg = useRef(null);
|
|
26
|
+
const _mapIsMoveField = useRef(null);
|
|
27
|
+
// states
|
|
28
|
+
const [scrollX, setScrollX] = useState(0);
|
|
29
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
30
|
+
const [startX, setStartX] = useState(0);
|
|
31
|
+
// variables
|
|
32
|
+
const TIMELINE = generateGanttTimeline(tasks);
|
|
33
|
+
const SVG_WIDTH = "100%";
|
|
34
|
+
const SVG_HEIGHT = 2400;
|
|
35
|
+
const HEADER_HEIGHT = 75;
|
|
36
|
+
const STROKE_WIDTH = 0.5;
|
|
37
|
+
const LABEL_WIDTH = 120;
|
|
38
|
+
const ROW_HEIGHT = 45;
|
|
39
|
+
let PREVMATCHMONT = 0;
|
|
40
|
+
let PREVMATCHDAY = 0;
|
|
41
|
+
// methods
|
|
42
|
+
const handleMouseDown = (e) => {
|
|
43
|
+
if (e.button !== 0)
|
|
44
|
+
return;
|
|
45
|
+
setIsDragging(true);
|
|
46
|
+
// Tıklanılan ilk X pozisyonu ile mevcut kaydırma değerini hafızaya alıyoruz.
|
|
47
|
+
setStartX(e.clientX + Math.abs(scrollX));
|
|
48
|
+
};
|
|
49
|
+
const handleMouseMove = useCallback((e) => {
|
|
50
|
+
if (!isDragging)
|
|
51
|
+
return;
|
|
52
|
+
let newScrollX = startX - e.clientX;
|
|
53
|
+
if (newScrollX < 0)
|
|
54
|
+
newScrollX = 0;
|
|
55
|
+
setScrollX(newScrollX);
|
|
56
|
+
}, [isDragging, startX]);
|
|
57
|
+
const handleMouseUpOrLeave = () => {
|
|
58
|
+
const svgRect = _svg.current?.getBoundingClientRect();
|
|
59
|
+
const mapIsMoveFieldRect = _mapIsMoveField.current?.getBoundingClientRect();
|
|
60
|
+
if (svgRect && mapIsMoveFieldRect) {
|
|
61
|
+
if (svgRect.right > mapIsMoveFieldRect.right) {
|
|
62
|
+
const targetLeft = svgRect.width - mapIsMoveFieldRect.width;
|
|
63
|
+
setScrollX(targetLeft - LABEL_WIDTH);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
setIsDragging(false);
|
|
67
|
+
};
|
|
68
|
+
return (React.createElement("svg", { ref: _svg, xmlns: "http://www.w3.org/2000/svg",
|
|
69
|
+
// viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`}
|
|
70
|
+
width: SVG_WIDTH, height: SVG_HEIGHT, className: "ar-gantt-chart" },
|
|
71
|
+
React.createElement("g", { className: "header", width: "100%" },
|
|
72
|
+
React.createElement("rect", { x: 0, y: 0, width: "100%", height: HEADER_HEIGHT }),
|
|
73
|
+
React.createElement("g", { transform: `translate(25, ${HEADER_HEIGHT / 2})`, className: "title-group" },
|
|
74
|
+
React.createElement("text", { className: "title" }, "Ar Gantt Chart"),
|
|
75
|
+
React.createElement("text", { y: 20, className: "title-description" }, "Daily View")),
|
|
76
|
+
React.createElement("line", { x1: "0", y1: HEADER_HEIGHT, x2: "100%", y2: HEADER_HEIGHT, opacity: 0.25, stroke: "var(--black)", strokeWidth: 1 })),
|
|
77
|
+
React.createElement("g", { className: "body", transform: `translate(0, ${HEADER_HEIGHT + ROW_HEIGHT * 2})` },
|
|
78
|
+
React.createElement("g", { className: `${isDragging ? "dragging" : "no-dragging"} time-and-bars`, transform: `translate(${LABEL_WIDTH - Math.abs(scrollX)}, 0)`, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUpOrLeave, onMouseLeave: handleMouseUpOrLeave, style: { cursor: isDragging ? "grabbing" : "grab", userSelect: "none" } },
|
|
79
|
+
React.createElement("g", null,
|
|
80
|
+
TIMELINE.days.map((day, index) => {
|
|
81
|
+
const xPos = (index + 1) * 60;
|
|
82
|
+
const nextDay = new Date(day.date);
|
|
83
|
+
nextDay.setDate(nextDay.getDate() + 1);
|
|
84
|
+
const isLastDayOfMonth = day.date.getMonth() !== nextDay.getMonth();
|
|
85
|
+
const isSunday = day.date.getDay() === 0;
|
|
86
|
+
if (!isSunday && !isLastDayOfMonth)
|
|
87
|
+
return;
|
|
88
|
+
const currentMonthNum = day.date.getMonth();
|
|
89
|
+
const currentDayNum = day.date.getDate();
|
|
90
|
+
const dayDiff = currentDayNum - (currentMonthNum !== PREVMATCHMONT ? 0 : PREVMATCHDAY);
|
|
91
|
+
// Bir sonraki turda kullanabilmek için hafızayı güncelliyoruz.
|
|
92
|
+
PREVMATCHMONT = currentMonthNum;
|
|
93
|
+
PREVMATCHDAY = currentDayNum;
|
|
94
|
+
return (React.createElement("g", { key: index },
|
|
95
|
+
React.createElement("line", { x1: xPos, y1: -ROW_HEIGHT * 2, x2: xPos, y2: 0, opacity: 0.25, stroke: "var(--white)", strokeWidth: STROKE_WIDTH }),
|
|
96
|
+
React.createElement("text", { x: xPos - (dayDiff * 60) / 2, y: -ROW_HEIGHT * 2 + ROW_HEIGHT / 2, fill: "var(--white)", fontSize: "12", textAnchor: "middle", dominantBaseline: "central" }, day.date.toLocaleDateString("tr-TR", { month: "long" }))));
|
|
97
|
+
}),
|
|
98
|
+
TIMELINE.days.map((day, index) => {
|
|
99
|
+
const xPos = (index + 1) * 60; // 01:00 -> 60px, 02:00 -> 120px...
|
|
100
|
+
return (React.createElement("g", { key: index },
|
|
101
|
+
React.createElement("text", { x: (index + 1) * 60 - 30, y: -ROW_HEIGHT + ROW_HEIGHT / 2, fill: day.isWeekend ? "var(--red-500)" : "var(--white)", fontSize: "12", textAnchor: "middle", dominantBaseline: "central" },
|
|
102
|
+
String(day.number).padStart(2, "0"),
|
|
103
|
+
" ",
|
|
104
|
+
day.name),
|
|
105
|
+
React.createElement("line", { x1: xPos, y1: 0, x2: xPos, y2: SVG_HEIGHT, opacity: 0.25, stroke: "var(--white)", strokeWidth: STROKE_WIDTH, strokeDasharray: "5,5" })));
|
|
106
|
+
}),
|
|
107
|
+
React.createElement("line", { x1: 0, y1: -ROW_HEIGHT, x2: TIMELINE.days.length * 60, y2: -ROW_HEIGHT, opacity: 0.25, stroke: "var(--white)", strokeWidth: 1 }),
|
|
108
|
+
React.createElement("line", { x1: 0, y1: 0, x2: TIMELINE.days.length * 60, y2: 0, opacity: 0.25, stroke: "var(--white)", strokeWidth: 1 })),
|
|
109
|
+
React.createElement("g", { transform: `translate(0, 0)` }, tasks.map((task, index) => {
|
|
110
|
+
const taskStart = new Date(task.start);
|
|
111
|
+
const taskEnd = new Date(task.end);
|
|
112
|
+
// 1. Proje başlangıcından bu görevin başlangıcına kadar geçen toplam milisaniye
|
|
113
|
+
const diffMsFromStart = taskStart.getTime() - Number(TIMELINE.timelineStart?.getTime());
|
|
114
|
+
// Milisaniyeyi saate çeviriyoruz
|
|
115
|
+
const hoursFromStart = diffMsFromStart / (1000 * 60 * 60);
|
|
116
|
+
// X Konumu: Geçen toplam saati, saat başına düşen piksel genişliğiyle çarpıyoruz
|
|
117
|
+
const x = hoursFromStart * (60 / 24);
|
|
118
|
+
// 2. Görevin toplam süresini saat cinsinden buluyoruz
|
|
119
|
+
const durationHours = (taskEnd.getTime() - taskStart.getTime()) / (1000 * 60 * 60);
|
|
120
|
+
// Genişlik (Width): Süreyi saat başına düşen pikselle çarpıyoruz
|
|
121
|
+
const width = durationHours * (60 / 24);
|
|
122
|
+
// 3. Dikey Konumlandırma (Senin mevcut mantığın)
|
|
123
|
+
const height = ROW_HEIGHT / 1.5;
|
|
124
|
+
const y = index * ROW_HEIGHT + height / 4;
|
|
125
|
+
return (React.createElement("g", { key: task.id },
|
|
126
|
+
React.createElement("rect", { x: x, y: y, width: width, height: height, fill: "#000", rx: 3 }),
|
|
127
|
+
width > 60 && (React.createElement("text", { x: x + width / 2, y: y + height / 2 + 4, fontSize: 12, fontWeight: "600", fill: "var(--black)", textAnchor: "middle" // Metni X koordinatına göre tam ortalar
|
|
128
|
+
}, task.name))));
|
|
129
|
+
})),
|
|
130
|
+
React.createElement("rect", { ref: _mapIsMoveField, x: 0, y: -ROW_HEIGHT, width: TIMELINE.days.length * 60, height: SVG_HEIGHT, fill: "transparent", pointerEvents: "all" })),
|
|
131
|
+
React.createElement("g", { className: "left-axis" },
|
|
132
|
+
React.createElement("rect", { x: 0, y: -ROW_HEIGHT * 2, width: LABEL_WIDTH, height: SVG_HEIGHT }),
|
|
133
|
+
React.createElement("g", { className: "label-list" }, tasks.map((task, index) => {
|
|
134
|
+
const y = index * ROW_HEIGHT;
|
|
135
|
+
return (React.createElement("g", { key: task.id, className: "label-row" },
|
|
136
|
+
React.createElement("text", { x: "10", y: y + ROW_HEIGHT / 2, className: "label-text" }, task.name),
|
|
137
|
+
React.createElement("line", { x1: "0", y1: y + ROW_HEIGHT, x2: LABEL_WIDTH, y2: y + ROW_HEIGHT, stroke: "var(--black)", strokeWidth: "0.5", opacity: 0.25 }),
|
|
138
|
+
React.createElement("line", { x1: LABEL_WIDTH, y1: y + ROW_HEIGHT, x2: 24 * 60, y2: y + ROW_HEIGHT, opacity: 0.25, stroke: "var(--white)", strokeWidth: STROKE_WIDTH, strokeDasharray: "5,5" })));
|
|
139
|
+
}))))));
|
|
140
|
+
};
|
|
141
|
+
const generateGanttTimeline = (tasks) => {
|
|
142
|
+
if (!tasks || tasks.length === 0) {
|
|
143
|
+
return { minDate: null, maxDate: null, months: [], days: [] };
|
|
144
|
+
}
|
|
145
|
+
// 1. En erken başlangıç ve en geç bitiş tarihlerini buluyoruz.
|
|
146
|
+
let minDate = new Date(tasks[0].start);
|
|
147
|
+
let maxDate = new Date(tasks[0].end);
|
|
148
|
+
tasks.forEach((task) => {
|
|
149
|
+
const start = new Date(task.start);
|
|
150
|
+
const end = new Date(task.end);
|
|
151
|
+
if (start < minDate)
|
|
152
|
+
minDate = start;
|
|
153
|
+
if (end > maxDate)
|
|
154
|
+
maxDate = end;
|
|
155
|
+
});
|
|
156
|
+
// Şemanın düzgün görünmesi için başlangıcı o ayın 1'ine,
|
|
157
|
+
// bitişi ise o ayın son gününe yuvarlamak yerleşim açısından daha iyi sonuç verir.
|
|
158
|
+
const startTimeline = new Date(minDate.getFullYear(), minDate.getMonth(), 1);
|
|
159
|
+
const endTimeline = new Date(maxDate.getFullYear(), maxDate.getMonth() + 1, 0); // Ayın son günü
|
|
160
|
+
const months = [];
|
|
161
|
+
const days = [];
|
|
162
|
+
// 2. Günleri ve Ayları döngüyle oluşturuyoruz.
|
|
163
|
+
const current = new Date(startTimeline);
|
|
164
|
+
while (current <= endTimeline) {
|
|
165
|
+
// Gün listesini doldur.
|
|
166
|
+
const dayOfWeek = current.getDay();
|
|
167
|
+
days.push({
|
|
168
|
+
date: new Date(current),
|
|
169
|
+
number: current.getDate(),
|
|
170
|
+
name: current.toLocaleDateString("tr-TR", { weekday: "short" }), // Pzt, Sal...
|
|
171
|
+
isWeekend: dayOfWeek === 0 || dayOfWeek === 6, // Hafta sonu kontrolü (Gantt'ta boyamak için).
|
|
172
|
+
});
|
|
173
|
+
// Ay listesini doldur. (Eğer listede bu ay henüz yoksa ekle)
|
|
174
|
+
const year = current.getFullYear();
|
|
175
|
+
const number = current.getMonth();
|
|
176
|
+
const name = current.toLocaleDateString("tr-TR", { month: "long" }); // Ocak, Şubat...
|
|
177
|
+
const monthExists = months.some((m) => m.year === year && m.number === number);
|
|
178
|
+
if (!monthExists) {
|
|
179
|
+
// O ayın toplam gün sayısını bulalım.
|
|
180
|
+
const totalDays = new Date(year, number + 1, 0).getDate();
|
|
181
|
+
months.push({ year, number, name, totalDays });
|
|
182
|
+
}
|
|
183
|
+
// Bir sonraki güne geç.
|
|
184
|
+
current.setDate(current.getDate() + 1);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
timelineStart: startTimeline,
|
|
188
|
+
timelineEnd: endTimeline,
|
|
189
|
+
months, // Üst zaman bandı için (Örn: Ocak, Şubat)
|
|
190
|
+
days, // Alt zaman bandı için (Örn: 1, 2, 3... veya haftalık kırılım için)
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
export default Gantt;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import Input from "..";
|
|
4
4
|
import Select from "../../select";
|
|
5
5
|
import PHONE from "../../../../libs/infrastructure/shared/PHONE";
|
|
@@ -9,6 +9,9 @@ const Phone = ({ variant, color, options, values, onSelected, validation, ...att
|
|
|
9
9
|
// states
|
|
10
10
|
const [_value, setValue] = useState("");
|
|
11
11
|
const [selected, setSelected] = useState(undefined);
|
|
12
|
+
// options referans kararlılığı için JSON key
|
|
13
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
|
+
const optionsKey = useMemo(() => JSON.stringify(options), [options]);
|
|
12
15
|
// methods
|
|
13
16
|
const handleClick = () => {
|
|
14
17
|
const input = _input.current;
|
|
@@ -21,31 +24,18 @@ const Phone = ({ variant, color, options, values, onSelected, validation, ...att
|
|
|
21
24
|
useEffect(() => {
|
|
22
25
|
setValue(values.value);
|
|
23
26
|
setSelected(options?.find((option) => option.value === values.option));
|
|
24
|
-
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
28
|
+
}, [values.value, values.option, optionsKey]);
|
|
25
29
|
return (React.createElement("div", { className: "ar-input-phone-wrapper" },
|
|
26
30
|
options && (React.createElement(Select, { style: { width: 130 }, variant: "outlined", color: "light", options: options, value: selected, onChange: (option) => {
|
|
27
31
|
onSelected?.(option);
|
|
28
32
|
setSelected(option);
|
|
29
|
-
} })),
|
|
33
|
+
}, validation: validation, config: { validation: { text: "hidden" } } })),
|
|
30
34
|
React.createElement(Input, { ref: _input, ...attributes, ...(!options ? { style: { borderRadius: "var(--border-radius-sm)" } } : {}), variant: variant, color: color, value: PHONE.FormatByMask(selected?.value, _value), type: "tel", inputMode: "decimal", onChange: (event) => {
|
|
31
35
|
if (attributes.disabled)
|
|
32
36
|
return;
|
|
33
|
-
(
|
|
34
|
-
|
|
35
|
-
const { id, name, value, type, dataset } = event.target;
|
|
36
|
-
attributes.onChange({
|
|
37
|
-
...event,
|
|
38
|
-
target: {
|
|
39
|
-
...event.target,
|
|
40
|
-
id: id,
|
|
41
|
-
name: name,
|
|
42
|
-
value: value,
|
|
43
|
-
type: type,
|
|
44
|
-
dataset: dataset,
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
})();
|
|
37
|
+
setValue(event.target.value);
|
|
38
|
+
attributes.onChange?.(event);
|
|
49
39
|
}, onClick: handleClick, validation: validation })));
|
|
50
40
|
};
|
|
51
41
|
export default Phone;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import React, { useEffect, useRef, useState, useMemo } from "react";
|
|
3
3
|
import Input from "../input";
|
|
4
4
|
import "../../../assets/css/components/form/select/styles.css";
|
|
5
5
|
import Chip from "../../data-display/chip";
|
|
6
6
|
import Checkbox from "../checkbox";
|
|
7
7
|
import Utils from "../../../libs/infrastructure/shared/Utils";
|
|
8
8
|
import ReactDOM from "react-dom";
|
|
9
|
-
const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }, style, options, value, onChange, onSearch, onClick, onCreate, multiple, placeholder, validation, upperCase, disabled, readOnly, config = { clear: true }, }) => {
|
|
9
|
+
const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }, style, options, value, onChange, onSearch, onClick, onCreate, multiple, placeholder, validation, upperCase, disabled, readOnly, config = { clear: true, validation: { text: "visible" } }, }) => {
|
|
10
10
|
const _selectionClassName = ["selections"];
|
|
11
11
|
// refs
|
|
12
12
|
const _arSelect = useRef(null);
|
|
@@ -16,16 +16,18 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
|
|
|
16
16
|
const _options = useRef(null);
|
|
17
17
|
const _optionItems = useRef([]);
|
|
18
18
|
const _searchField = useRef(null);
|
|
19
|
-
// const _searchTimeOut = useRef<NodeJS.Timeout | null>(null);
|
|
20
19
|
let _otoFocus = useRef(null).current;
|
|
21
20
|
let _navigationIndex = useRef(0);
|
|
22
21
|
// states
|
|
23
22
|
const [optionsOpen, setOptionsOpen] = useState(false);
|
|
24
|
-
const [filteredOptions, setFilteredOptions] = useState([]);
|
|
23
|
+
const [filteredOptions, setFilteredOptions] = useState(() => options ?? []);
|
|
25
24
|
const [isSearchTextEqual, setIsSearchTextEqual] = useState(false);
|
|
26
25
|
const [searchText, setSearchText] = useState("");
|
|
27
26
|
const [singleInputText, setSingleInputText] = useState("");
|
|
28
27
|
const [navigationIndex, setNavigationIndex] = useState(0);
|
|
28
|
+
// options referans kararlılığı için JSON key
|
|
29
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
+
const optionsKey = useMemo(() => JSON.stringify(options), [options]);
|
|
29
31
|
_selectionClassName.push(...Utils.GetClassName(variant, undefined, validation?.text ? "red" : "light", border, undefined, undefined, undefined));
|
|
30
32
|
// methods
|
|
31
33
|
const handleClickOutSide = (event) => {
|
|
@@ -38,22 +40,14 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
|
|
|
38
40
|
const optionItems = _optionItems.current.filter((optionItem) => optionItem !== null);
|
|
39
41
|
if (key === "ArrowUp" || key === "ArrowLeft") {
|
|
40
42
|
setNavigationIndex((prev) => {
|
|
41
|
-
|
|
42
|
-
if (prev > 0)
|
|
43
|
-
result = prev - 1;
|
|
44
|
-
if (prev === 0)
|
|
45
|
-
result = optionItems.length - 1;
|
|
43
|
+
const result = prev > 0 ? prev - 1 : optionItems.length - 1;
|
|
46
44
|
_navigationIndex.current = result;
|
|
47
45
|
return result;
|
|
48
46
|
});
|
|
49
47
|
}
|
|
50
48
|
else if (key === "ArrowDown" || key === "ArrowRight") {
|
|
51
49
|
setNavigationIndex((prev) => {
|
|
52
|
-
|
|
53
|
-
if (prev === optionItems.length - 1)
|
|
54
|
-
result = 0;
|
|
55
|
-
if (prev < optionItems.length - 1)
|
|
56
|
-
result = prev + 1;
|
|
50
|
+
const result = prev === optionItems.length - 1 ? 0 : prev + 1;
|
|
57
51
|
_navigationIndex.current = result;
|
|
58
52
|
return result;
|
|
59
53
|
});
|
|
@@ -63,17 +57,10 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
|
|
|
63
57
|
return;
|
|
64
58
|
optionItems[_navigationIndex.current]?.click();
|
|
65
59
|
}
|
|
66
|
-
else if (key === "Escape")
|
|
60
|
+
else if (key === "Escape") {
|
|
67
61
|
setOptionsOpen(false);
|
|
62
|
+
}
|
|
68
63
|
};
|
|
69
|
-
// const handleSearch = (value: string) => {
|
|
70
|
-
// if (searchText.length === 0 || !onSearch) return;
|
|
71
|
-
// if (_searchTimeOut.current) clearTimeout(_searchTimeOut.current);
|
|
72
|
-
// _searchTimeOut.current = setTimeout(() => {
|
|
73
|
-
// setSearchText(value);
|
|
74
|
-
// onSearch(value);
|
|
75
|
-
// }, 750);
|
|
76
|
-
// };
|
|
77
64
|
const handlePosition = () => {
|
|
78
65
|
if (_options.current) {
|
|
79
66
|
const optionRect = _options.current.getBoundingClientRect();
|
|
@@ -134,7 +121,6 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
|
|
|
134
121
|
React.createElement("span", { className: "plus" }, "+"),
|
|
135
122
|
singleInputText.length !== 0 ? singleInputText : searchText))));
|
|
136
123
|
};
|
|
137
|
-
// Özel büyük harfe dönüştürme işlevi.
|
|
138
124
|
const convertToUpperCase = (str) => {
|
|
139
125
|
return str
|
|
140
126
|
.replace(/ş/g, "S")
|
|
@@ -151,45 +137,37 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
|
|
|
151
137
|
.replace(/Ü/g, "U")
|
|
152
138
|
.replace(/[a-z]/g, (match) => match.toUpperCase());
|
|
153
139
|
};
|
|
154
|
-
//
|
|
140
|
+
// value değiştiğinde input metnini güncelle
|
|
155
141
|
useEffect(() => {
|
|
156
142
|
if (multiple)
|
|
157
143
|
setSearchText("");
|
|
158
144
|
else
|
|
159
145
|
setSingleInputText(value?.text ?? "");
|
|
160
|
-
}, [value]);
|
|
161
|
-
|
|
146
|
+
}, [value, multiple]);
|
|
147
|
+
// options dışarıdan değiştiğinde filtreyi güncelle (searchText ile uyumlu)
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
setFilteredOptions((options ?? []).filter((option) => option.text.toLocaleLowerCase().includes(searchText.toLocaleLowerCase())));
|
|
150
|
+
setIsSearchTextEqual((options ?? []).some((option) => option.text.toLocaleLowerCase() === searchText.toLocaleLowerCase()));
|
|
151
|
+
// optionsKey kullanarak referans karşılaştırması yerine içerik karşılaştırması yapıyoruz
|
|
152
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
153
|
+
}, [optionsKey]);
|
|
154
|
+
// options paneli açılıp kapandığında
|
|
162
155
|
useEffect(() => {
|
|
163
156
|
if (optionsOpen) {
|
|
164
157
|
setTimeout(() => handlePosition(), 0);
|
|
165
|
-
if (!multiple) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// if (_options.current) {
|
|
169
|
-
// const list = _options.current.querySelector("ul") as HTMLUListElement;
|
|
170
|
-
// list.scrollTo({
|
|
171
|
-
// top: optionItems[_selectedItemIndex.current].offsetTop,
|
|
172
|
-
// behavior: "smooth",
|
|
173
|
-
// });
|
|
174
|
-
// }
|
|
175
|
-
if (_singleInput.current) {
|
|
176
|
-
setSingleInputText("");
|
|
177
|
-
_singleInput.current.placeholder = value?.text || `${validation ? "* " : ""}${placeholder ?? ""}` || "";
|
|
178
|
-
}
|
|
158
|
+
if (!multiple && _singleInput.current) {
|
|
159
|
+
setSingleInputText("");
|
|
160
|
+
_singleInput.current.placeholder = value?.text || `${validation ? "* " : ""}${placeholder ?? ""}` || "";
|
|
179
161
|
}
|
|
180
|
-
// Options açıldıktan 100ms sonra arama kutusuna otomatik olarak focus oluyor.
|
|
181
162
|
_otoFocus = setTimeout(() => {
|
|
182
163
|
if (_searchField.current)
|
|
183
164
|
_searchField.current.focus();
|
|
184
165
|
}, 250);
|
|
185
|
-
|
|
186
|
-
window.addEventListener("blur", () => setOptionsOpen(false));
|
|
166
|
+
window.addEventListener("blur", handleBlur);
|
|
187
167
|
document.addEventListener("click", handleClickOutSide);
|
|
188
168
|
document.addEventListener("keydown", handleKeys);
|
|
189
169
|
}
|
|
190
170
|
else {
|
|
191
|
-
// Options paneli kapanma süresi 250ms.
|
|
192
|
-
// 300ms sonra temizlenmesinin sebebi kapanırken birder veriler gerliyor ve panel yüksekliği artıyor.
|
|
193
171
|
setTimeout(() => setSearchText(""), 300);
|
|
194
172
|
if (multiple) {
|
|
195
173
|
if (_searchField.current)
|
|
@@ -204,54 +182,51 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
|
|
|
204
182
|
}
|
|
205
183
|
return () => {
|
|
206
184
|
_otoFocus && clearTimeout(_otoFocus);
|
|
207
|
-
window.removeEventListener("blur",
|
|
185
|
+
window.removeEventListener("blur", handleBlur);
|
|
208
186
|
document.removeEventListener("click", handleClickOutSide);
|
|
209
187
|
document.removeEventListener("keydown", handleKeys);
|
|
210
188
|
};
|
|
189
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
211
190
|
}, [optionsOpen]);
|
|
191
|
+
// arama metni değiştiğinde filtrele
|
|
212
192
|
useEffect(() => {
|
|
213
193
|
if (searchText.length > 0 && onSearch) {
|
|
214
194
|
onSearch(searchText);
|
|
215
195
|
}
|
|
216
196
|
else {
|
|
217
|
-
|
|
218
|
-
setFilteredOptions(options?.filter((option) => {
|
|
197
|
+
setFilteredOptions((options ?? []).filter((option) => {
|
|
219
198
|
if (!optionsOpen)
|
|
220
|
-
return
|
|
199
|
+
return true;
|
|
221
200
|
return option.text.toLocaleLowerCase().includes(searchText.toLocaleLowerCase());
|
|
222
201
|
}));
|
|
223
|
-
setIsSearchTextEqual(options
|
|
202
|
+
setIsSearchTextEqual((options ?? []).some((option) => {
|
|
224
203
|
if (!optionsOpen)
|
|
225
|
-
return
|
|
226
|
-
return option.text.toLocaleLowerCase()
|
|
204
|
+
return false;
|
|
205
|
+
return option.text.toLocaleLowerCase() === searchText.toLocaleLowerCase();
|
|
227
206
|
}));
|
|
228
207
|
}
|
|
229
|
-
// Arama yapılması durumunda değerleri sıfırla.
|
|
230
208
|
setNavigationIndex(0);
|
|
231
209
|
_navigationIndex.current = 0;
|
|
232
|
-
|
|
233
|
-
const optionItems = _optionItems.current.filter((optionItem) => optionItem !== null);
|
|
210
|
+
const optionItems = _optionItems.current.filter((item) => item !== null);
|
|
234
211
|
optionItems[_navigationIndex.current]?.classList.add("navigate-with-arrow-keys");
|
|
235
|
-
// Yeniden konumlandır.
|
|
236
212
|
setTimeout(() => handlePosition(), 0);
|
|
213
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
237
214
|
}, [searchText]);
|
|
215
|
+
// klavye navigasyonu highlight
|
|
238
216
|
useEffect(() => {
|
|
239
|
-
// Seçilen öğeye 'navigate-with-arrow-keys' sınıfını ekle
|
|
240
217
|
_optionItems.current
|
|
241
|
-
.filter((
|
|
218
|
+
.filter((item) => item !== null)
|
|
242
219
|
.forEach((item, index) => {
|
|
243
220
|
if (index === navigationIndex) {
|
|
244
221
|
item?.classList.add("navigate-with-arrow-keys");
|
|
245
|
-
item.scrollIntoView({
|
|
246
|
-
behavior: "smooth",
|
|
247
|
-
block: "nearest",
|
|
248
|
-
});
|
|
222
|
+
item.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
249
223
|
}
|
|
250
224
|
else {
|
|
251
225
|
item?.classList.remove("navigate-with-arrow-keys");
|
|
252
226
|
}
|
|
253
227
|
});
|
|
254
228
|
}, [navigationIndex]);
|
|
229
|
+
const handleBlur = () => setOptionsOpen(false);
|
|
255
230
|
return (React.createElement("div", { ref: _arSelect, className: "ar-select-wrapper" },
|
|
256
231
|
React.createElement("div", { ref: _multipleInput, className: "ar-select" },
|
|
257
232
|
multiple ? (React.createElement("div", { className: "wrapper" },
|
|
@@ -270,11 +245,9 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
|
|
|
270
245
|
)`,
|
|
271
246
|
},
|
|
272
247
|
}
|
|
273
|
-
: {}), className: _selectionClassName.
|
|
248
|
+
: {}), className: _selectionClassName.join(" "), onClick: () => {
|
|
274
249
|
onClick && onClick();
|
|
275
|
-
(() =>
|
|
276
|
-
setOptionsOpen((prev) => !prev);
|
|
277
|
-
})();
|
|
250
|
+
setOptionsOpen((prev) => !prev);
|
|
278
251
|
} },
|
|
279
252
|
React.createElement("div", { className: "items" }, value.map((_value, index) => (React.createElement(Chip, { key: index, variant: status?.selected?.variant || "filled", color: status?.selected?.color || status?.color, text: _value.text }))))),
|
|
280
253
|
React.createElement("span", { ref: _placeholder, className: `placeholder ${value.length > 0 ? "visible" : "hidden"}`, onClick: () => {
|
|
@@ -282,13 +255,9 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
|
|
|
282
255
|
setOptionsOpen((prev) => !prev);
|
|
283
256
|
} },
|
|
284
257
|
validation ? "* " : "",
|
|
285
|
-
placeholder))) : (React.createElement(Input, { ref: _singleInput, style: { ...style, paddingRight: config.clear === false ? "1.5rem" : "3.5rem" }, variant: variant, color: !Utils.IsNullOrEmpty(validation?.text) ? "red" : color,
|
|
286
|
-
// status={!Utils.IsNullOrEmpty(validation?.text) ? "danger" : status}
|
|
287
|
-
border: { radius: border.radius }, value: singleInputText, onClick: () => {
|
|
258
|
+
placeholder))) : (React.createElement(Input, { ref: _singleInput, style: { ...style, paddingRight: config.clear === false ? "1.5rem" : "3.5rem" }, variant: variant, color: !Utils.IsNullOrEmpty(validation?.text) ? "red" : color, border: { radius: border.radius }, value: singleInputText, onClick: () => {
|
|
288
259
|
onClick && onClick();
|
|
289
|
-
(() =>
|
|
290
|
-
setOptionsOpen((prev) => !prev);
|
|
291
|
-
})();
|
|
260
|
+
setOptionsOpen((prev) => !prev);
|
|
292
261
|
}, onChange: (event) => {
|
|
293
262
|
!optionsOpen && setOptionsOpen(true);
|
|
294
263
|
if (upperCase)
|
|
@@ -298,7 +267,10 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
|
|
|
298
267
|
if (event.key === "Enter")
|
|
299
268
|
return;
|
|
300
269
|
setSearchText(event.currentTarget.value);
|
|
301
|
-
}, placeholder: placeholder, validation:
|
|
270
|
+
}, placeholder: placeholder, validation: {
|
|
271
|
+
...validation,
|
|
272
|
+
text: config.validation?.text === "visible" ? validation?.text : "",
|
|
273
|
+
}, disabled: disabled, readOnly: readOnly })),
|
|
302
274
|
React.createElement("div", { className: "buttons" },
|
|
303
275
|
config?.clear === true && (React.createElement("span", { className: `button-clear ${!disabled && (multiple ? value.length > 0 : value) ? "opened" : "closed"}`, onClick: (event) => {
|
|
304
276
|
if (disabled)
|
|
@@ -313,7 +285,7 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
|
|
|
313
285
|
event.stopPropagation();
|
|
314
286
|
setOptionsOpen((prev) => !prev);
|
|
315
287
|
} })),
|
|
316
|
-
multiple && validation && React.createElement("span", { className: "validation" }, validation.text)),
|
|
288
|
+
multiple && validation && config.validation?.text === "visible" && (React.createElement("span", { className: "validation" }, validation.text))),
|
|
317
289
|
!disabled &&
|
|
318
290
|
optionsOpen &&
|
|
319
291
|
ReactDOM.createPortal(React.createElement("div", { ref: _options, className: "ar-select-options" },
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import Select from "./components/form/select";
|
|
|
10
10
|
import Switch from "./components/form/switch";
|
|
11
11
|
import TextEditor from "./components/form/text-editor";
|
|
12
12
|
import Upload from "./components/form/upload";
|
|
13
|
+
import Gantt from "./components/charts/gantt";
|
|
13
14
|
import Calendar from "./components/data-display/calendar";
|
|
14
15
|
import Card from "./components/data-display/card";
|
|
15
16
|
import Chip from "./components/data-display/chip";
|
|
@@ -34,4 +35,4 @@ import Pagination from "./components/navigation/pagination";
|
|
|
34
35
|
import Steps from "./components/navigation/steps";
|
|
35
36
|
import Grid from "./components/data-display/grid-system";
|
|
36
37
|
import Layout from "./components/layout";
|
|
37
|
-
export { Button, ButtonAction, ButtonGroup, Checkbox, DatePicker, Input, Radio, Select, Switch, TextEditor, Upload, Calendar, Card, Chip, Diagram, Divider, DnD, KanbanBoard, Paper, SyntaxHighlighter, Table, Tabs, Typography, Alert, Drawer, Modal, Popover, Progress, Tooltip, Breadcrumb, Menu, Pagination, Steps, Grid, Layout, };
|
|
38
|
+
export { Button, ButtonAction, ButtonGroup, Checkbox, DatePicker, Input, Radio, Select, Switch, TextEditor, Upload, Gantt, Calendar, Card, Chip, Diagram, Divider, DnD, KanbanBoard, Paper, SyntaxHighlighter, Table, Tabs, Typography, Alert, Drawer, Modal, Popover, Progress, Tooltip, Breadcrumb, Menu, Pagination, Steps, Grid, Layout, };
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,8 @@ import Select from "./components/form/select";
|
|
|
11
11
|
import Switch from "./components/form/switch";
|
|
12
12
|
import TextEditor from "./components/form/text-editor";
|
|
13
13
|
import Upload from "./components/form/upload";
|
|
14
|
+
// Charts
|
|
15
|
+
import Gantt from "./components/charts/gantt";
|
|
14
16
|
// Data Display
|
|
15
17
|
import Calendar from "./components/data-display/calendar";
|
|
16
18
|
import Card from "./components/data-display/card";
|
|
@@ -42,6 +44,8 @@ import Layout from "./components/layout";
|
|
|
42
44
|
export {
|
|
43
45
|
// Form Elements
|
|
44
46
|
Button, ButtonAction, ButtonGroup, Checkbox, DatePicker, Input, Radio, Select, Switch, TextEditor, Upload,
|
|
47
|
+
// Charts
|
|
48
|
+
Gantt,
|
|
45
49
|
// Data Display
|
|
46
50
|
Calendar, Card, Chip, Diagram, Divider, DnD, KanbanBoard, Paper, SyntaxHighlighter, Table, Tabs, Typography,
|
|
47
51
|
// Feedback
|
|
@@ -1,147 +1,151 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
|
|
3
3
|
import Utils from "../../../infrastructure/shared/Utils";
|
|
4
4
|
const useValidation = function (data, params, step) {
|
|
5
|
-
// refs
|
|
6
5
|
const _errors = useRef({});
|
|
7
|
-
// states
|
|
8
6
|
const [errors, setErrors] = useState({});
|
|
9
7
|
const [submit, setSubmit] = useState(false);
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
const paramsKey = useMemo(() => JSON.stringify(params), [params]);
|
|
9
|
+
const setError = useCallback((key, message, paramStep, trackByValue) => {
|
|
10
|
+
let _key = paramStep ? `${paramStep}_${key}` : key;
|
|
11
|
+
if (trackByValue !== undefined)
|
|
12
|
+
_key = `${_key}_${trackByValue}`;
|
|
13
|
+
_errors.current[_key] = message;
|
|
14
|
+
}, []);
|
|
15
|
+
const paramsShape = useCallback((param, value, trackByValue) => {
|
|
16
|
+
const vLength = value ? value.length : 0;
|
|
17
|
+
const getKey = (subkey) => {
|
|
18
|
+
if (!subkey)
|
|
19
|
+
return param.key;
|
|
20
|
+
const levels = subkey.split(".");
|
|
21
|
+
return levels[levels.length - 1];
|
|
22
|
+
};
|
|
23
|
+
const handleValidation = (key, s) => {
|
|
24
|
+
// Zorunluluk Kontrolleri (Geliştirildi).
|
|
25
|
+
if (s.type === "required" && Utils.IsNullOrEmpty(value)) {
|
|
26
|
+
setError(key, s.message, param.step, trackByValue);
|
|
27
|
+
return;
|
|
23
28
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
const validationTypes = ["phone", "email", "iban", "account-number"];
|
|
30
|
+
if (validationTypes.includes(s.type) && Utils.IsNullOrEmpty(value)) {
|
|
31
|
+
setError(key, s.message, param.step, trackByValue);
|
|
32
|
+
return;
|
|
27
33
|
}
|
|
28
|
-
|
|
34
|
+
// Uzunluk Kontrolleri.
|
|
35
|
+
if (s.type === "minimum" && vLength < s.value) {
|
|
36
|
+
setError(key, Utils.StringFormat(s.message, s.value), param.step, trackByValue);
|
|
37
|
+
}
|
|
38
|
+
if (s.type === "maximum" && vLength > s.value) {
|
|
39
|
+
setError(key, Utils.StringFormat(s.message, s.value), param.step, trackByValue);
|
|
40
|
+
}
|
|
41
|
+
// Format (Regex) Kontrolleri (Sadece değer doluysa çalışır).
|
|
42
|
+
if (value && !Utils.IsNullOrEmpty(value)) {
|
|
43
|
+
const phoneRegex = /^\d{7,14}$/;
|
|
44
|
+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
45
|
+
const ibanRegex = /^TR\d{24}$/;
|
|
46
|
+
const accountNumberRegex = /^\d{6,16}$/;
|
|
47
|
+
if (s.type === "phone" && !phoneRegex.test(value.replace(/\D/g, ""))) {
|
|
48
|
+
setError(key, s.message, param.step, trackByValue);
|
|
49
|
+
}
|
|
50
|
+
if (s.type === "email" && !emailRegex.test(value)) {
|
|
51
|
+
setError(key, s.message, param.step, trackByValue);
|
|
52
|
+
}
|
|
53
|
+
if (s.type === "iban" && !ibanRegex.test(value.replace(/\s/g, ""))) {
|
|
54
|
+
setError(key, s.message, param.step, trackByValue);
|
|
55
|
+
}
|
|
56
|
+
if (s.type === "account-number" && !accountNumberRegex.test(value)) {
|
|
57
|
+
setError(key, s.message, param.step, trackByValue);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
param.shape?.forEach((s) => {
|
|
62
|
+
const key = getKey(param.subkey);
|
|
63
|
+
// ÖNEMLİ KONTROL: Eğer bu alan için zaten bir hata basıldıysa,
|
|
64
|
+
// shape içindeki sonraki kuralları kontrol etme, döngüdeki bu adımı atla!
|
|
65
|
+
let currentKey = param.step ? `${param.step}_${key}` : key;
|
|
66
|
+
if (trackByValue !== undefined)
|
|
67
|
+
currentKey = `${currentKey}_${trackByValue}`;
|
|
68
|
+
// Hafızada zaten bu alanın hatası var, sonrakileri çalıştırma.
|
|
69
|
+
if (_errors.current[currentKey])
|
|
70
|
+
return;
|
|
71
|
+
if (param.where) {
|
|
72
|
+
if (param.where(data)) {
|
|
73
|
+
setError(param.subkey ? key : param.key, s.message, param.step, trackByValue);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
handleValidation(key, s);
|
|
29
78
|
});
|
|
30
|
-
};
|
|
31
|
-
const
|
|
32
|
-
let _key = step ? `${step}_${key}` : key;
|
|
33
|
-
if (trackByValue !== undefined)
|
|
34
|
-
_key = `${_key}_${trackByValue}`;
|
|
35
|
-
setErrors((prev) => ({ ...prev, [_key]: message }));
|
|
36
|
-
_errors.current = { ..._errors.current, [_key]: message };
|
|
37
|
-
};
|
|
38
|
-
const handleParams = (param) => {
|
|
79
|
+
}, [data, setError]);
|
|
80
|
+
const handleParams = useCallback((param) => {
|
|
39
81
|
const value = data[param.key];
|
|
40
|
-
// Eğer subkey varsa, onunla işlem yapılacak.
|
|
41
82
|
if (param.subkey) {
|
|
42
83
|
if (param.subkey.includes(".")) {
|
|
43
|
-
// Subkey içinde birden fazla seviye varsa, her seviyeye inerek değer alınacak.
|
|
44
84
|
const levels = param.subkey.split(".");
|
|
45
85
|
let currentData = value;
|
|
46
86
|
for (const key of levels) {
|
|
47
|
-
// Eğer currentData null ya da undefined ise, işlem sonlandırılır.
|
|
48
87
|
if (!currentData) {
|
|
49
88
|
paramsShape(param, currentData);
|
|
50
89
|
return;
|
|
51
90
|
}
|
|
52
|
-
// Seviye bazında ilerleyerek veriye ulaşılır.
|
|
53
91
|
currentData = currentData[key];
|
|
54
92
|
}
|
|
55
|
-
// Son seviyedeki veriyi paramsShape fonksiyonuna gönder.
|
|
56
93
|
paramsShape(param, currentData);
|
|
57
94
|
}
|
|
58
95
|
else {
|
|
59
96
|
if (Array.isArray(value)) {
|
|
60
|
-
// Eğer value bir dizi ise ve subkey sadece bir seviye ise,
|
|
61
|
-
// dizinin her bir elemanına subkey uygulanabilir.
|
|
62
97
|
const extractedValues = value.map((item) => ({
|
|
63
98
|
value: item?.[param.subkey],
|
|
64
99
|
trackByValue: item?.trackByValue,
|
|
65
100
|
}));
|
|
66
|
-
|
|
67
|
-
extractedValues.map((extractedValue) => paramsShape(param, extractedValue.value, extractedValue.trackByValue));
|
|
101
|
+
extractedValues.forEach((extractedValue) => paramsShape(param, extractedValue.value, extractedValue.trackByValue));
|
|
68
102
|
}
|
|
69
103
|
else {
|
|
70
|
-
// Value bir obje ise, subkey doğrudan kullanılır.
|
|
71
104
|
paramsShape(param, value?.[param.subkey]);
|
|
72
105
|
}
|
|
73
106
|
}
|
|
74
107
|
}
|
|
75
108
|
else {
|
|
76
|
-
// Eğer subkey yoksa, doğrudan param.key üzerinden işlem yapılır.
|
|
77
109
|
paramsShape(param, value);
|
|
78
110
|
}
|
|
111
|
+
}, [data, paramsShape]);
|
|
112
|
+
// Tüm kuralları senkron çalıştırıp sonucu dönen fonksiyon.
|
|
113
|
+
const validateAll = useCallback(() => {
|
|
114
|
+
_errors.current = {};
|
|
115
|
+
params.forEach((param) => handleParams(param));
|
|
116
|
+
setErrors({ ..._errors.current });
|
|
117
|
+
if (!data || Object.keys(data).length === 0 || params.length === 0)
|
|
118
|
+
return false;
|
|
119
|
+
if (step) {
|
|
120
|
+
const filteredErrors = Object.keys(_errors.current).filter((k) => k.startsWith(`${step}_`));
|
|
121
|
+
return filteredErrors.length === 0;
|
|
122
|
+
}
|
|
123
|
+
return Object.keys(_errors.current).length === 0;
|
|
124
|
+
}, [data, paramsKey, step, handleParams]);
|
|
125
|
+
const onSubmit = (callback) => {
|
|
126
|
+
setSubmit(true);
|
|
127
|
+
const isValid = validateAll();
|
|
128
|
+
callback(isValid);
|
|
79
129
|
};
|
|
80
|
-
const paramsShape = (param, value, trackByValue) => {
|
|
81
|
-
const vLenght = value ? value.length : 0;
|
|
82
|
-
const getKey = (subkey) => {
|
|
83
|
-
if (!subkey)
|
|
84
|
-
return param.key;
|
|
85
|
-
const levels = subkey.split(".");
|
|
86
|
-
return levels[levels.length - 1];
|
|
87
|
-
};
|
|
88
|
-
const handleValidation = (key, s) => {
|
|
89
|
-
if (s.type === "required" && Utils.IsNullOrEmpty(value)) {
|
|
90
|
-
setError(key, s.message, param.step, trackByValue);
|
|
91
|
-
}
|
|
92
|
-
if (s.type === "minimum" && vLenght < s.value) {
|
|
93
|
-
setError(key, Utils.StringFormat(s.message, s.value), param.step, trackByValue);
|
|
94
|
-
}
|
|
95
|
-
if (s.type === "maximum" && vLenght > s.value) {
|
|
96
|
-
setError(key, Utils.StringFormat(s.message, s.value), param.step, trackByValue);
|
|
97
|
-
}
|
|
98
|
-
// Regexes
|
|
99
|
-
// const phoneRegex = /^((\+90|0)?([2-5]\d{2})\d{7}|\+[1-9]\d{7,14})$/;
|
|
100
|
-
const phoneRegex = /^\d{7,14}$/;
|
|
101
|
-
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
102
|
-
const ibanRegex = /^TR\d{24}$/;
|
|
103
|
-
const accountNumberRegex = /^\d{6,16}$/;
|
|
104
|
-
if (s.type === "phone" && value && !phoneRegex.test(value.replace(/\D/g, ""))) {
|
|
105
|
-
setError(key, s.message, param.step, trackByValue);
|
|
106
|
-
}
|
|
107
|
-
if (s.type === "email" && value && !emailRegex.test(value)) {
|
|
108
|
-
setError(key, s.message, param.step, trackByValue);
|
|
109
|
-
}
|
|
110
|
-
if (s.type === "iban" && value && !ibanRegex.test(value.replace(/\s/g, ""))) {
|
|
111
|
-
setError(key, s.message, param.step, trackByValue);
|
|
112
|
-
}
|
|
113
|
-
if (s.type === "account-number" && value && !accountNumberRegex.test(value)) {
|
|
114
|
-
setError(key, s.message, param.step, trackByValue);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
param.shape?.forEach((s) => {
|
|
118
|
-
const key = getKey(param.subkey);
|
|
119
|
-
if (param.where) {
|
|
120
|
-
if (param.where(data)) {
|
|
121
|
-
setError(param.subkey ? key : param.key, s.message, param.step, trackByValue);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
handleValidation(key, s);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
// useEffects
|
|
130
130
|
useEffect(() => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
if (!submit) {
|
|
132
|
+
_errors.current = {};
|
|
133
|
+
setErrors({});
|
|
134
134
|
return;
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
}
|
|
136
|
+
validateAll();
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
|
+
}, [data, submit, paramsKey]);
|
|
139
|
+
// Adım filtresi.
|
|
140
|
+
const filteredErrors = step
|
|
141
|
+
? Object.fromEntries(Object.entries(errors)
|
|
142
|
+
.filter(([key]) => key.startsWith(`${step}_`))
|
|
143
|
+
.map(([key, value]) => [key.replace(/^\d+_/, ""), String(value)]))
|
|
144
|
+
: errors;
|
|
137
145
|
return {
|
|
138
146
|
onSubmit,
|
|
139
147
|
setSubmit,
|
|
140
|
-
errors:
|
|
141
|
-
? Object.fromEntries(Object.entries(errors)
|
|
142
|
-
.filter(([key]) => key.startsWith(`${step}_`))
|
|
143
|
-
.map(([key, value]) => [key.replace(/^\d+_/, ""), String(value)]))
|
|
144
|
-
: errors,
|
|
148
|
+
errors: filteredErrors,
|
|
145
149
|
};
|
|
146
150
|
};
|
|
147
151
|
export default useValidation;
|