akfatimeline 1.1.1 → 2.0.0
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/CHANGELOG.md +32 -0
- package/dist/Timeline.js +16 -48
- package/{src/components/Timeline/DailyView.js → dist/components/Timeline/DailyView.jsx} +1 -0
- package/{src/components/Timeline/EventTooltip.js → dist/components/Timeline/EventTooltip.jsx} +207 -206
- package/{src/components/Timeline/Indicator.js → dist/components/Timeline/Indicator.jsx} +27 -26
- package/{src/components/Timeline/MasterHeader.js → dist/components/Timeline/MasterHeader.jsx} +105 -104
- package/{src/components/Timeline/Resources.js → dist/components/Timeline/Resources.jsx} +54 -53
- package/{src/components/Timeline/ResourcesHeader.js → dist/components/Timeline/ResourcesHeader.jsx} +15 -14
- package/{src/components/Timeline/Timeline.js → dist/components/Timeline/Timeline.jsx} +572 -607
- package/{src/components/Timeline/TimelineContent.js → dist/components/Timeline/TimelineContent.jsx} +837 -838
- package/{src/components/Timeline/TimelineHeader.js → dist/components/Timeline/TimelineHeader.jsx} +55 -54
- package/dist/components/Timeline/TimelineMonthContainer.js +2 -2
- package/package.json +55 -4
- package/src/components/Timeline/AutocompleteSelect.jsx +150 -0
- package/src/components/Timeline/ContextMenu.jsx +149 -0
- package/src/components/Timeline/DailyView.jsx +256 -0
- package/src/components/Timeline/EventBadge.jsx +26 -0
- package/src/components/Timeline/EventDetailModal.jsx +138 -0
- package/src/components/Timeline/EventIcon.jsx +47 -0
- package/src/components/Timeline/EventTooltip.jsx +207 -0
- package/src/components/Timeline/Indicator.jsx +27 -0
- package/src/components/Timeline/LoadingSpinner.jsx +48 -0
- package/src/components/Timeline/MasterHeader.jsx +105 -0
- package/src/components/Timeline/Resources.jsx +54 -0
- package/src/components/Timeline/ResourcesHeader.jsx +15 -0
- package/src/components/Timeline/Timeline.jsx +572 -0
- package/src/components/Timeline/TimelineContent.jsx +837 -0
- package/src/components/Timeline/TimelineHeader.jsx +55 -0
- package/src/components/Timeline/TimelineMonthContainer.js +2 -2
- package/src/library.js +8 -8
- /package/{src/components/Timeline/AutocompleteSelect.js → dist/components/Timeline/AutocompleteSelect.jsx} +0 -0
- /package/{src/components/Timeline/ContextMenu.js → dist/components/Timeline/ContextMenu.jsx} +0 -0
- /package/{src/components/Timeline/EventBadge.js → dist/components/Timeline/EventBadge.jsx} +0 -0
- /package/{src/components/Timeline/EventDetailModal.js → dist/components/Timeline/EventDetailModal.jsx} +0 -0
- /package/{src/components/Timeline/EventIcon.js → dist/components/Timeline/EventIcon.jsx} +0 -0
- /package/{src/components/Timeline/LoadingSpinner.js → dist/components/Timeline/LoadingSpinner.jsx} +0 -0
package/{src/components/Timeline/TimelineHeader.js → dist/components/Timeline/TimelineHeader.jsx}
RENAMED
|
@@ -1,54 +1,55 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import "./Timeline.css"; // CSS dosyasını import etmeyi unutma
|
|
3
|
-
import { parseDate } from "../../utils/dateUtils";
|
|
4
|
-
|
|
5
|
-
const TimelineHeader = ({ dates, monthHeaders, highlightWeekends = false }) => {
|
|
6
|
-
return (
|
|
7
|
-
<div className="timeline-header-container">
|
|
8
|
-
{/* Ay ve Yıl Başlıkları */}
|
|
9
|
-
<div className="timeline-header-month-row">
|
|
10
|
-
{monthHeaders.map((monthHeader, index) => (
|
|
11
|
-
<div
|
|
12
|
-
key={index}
|
|
13
|
-
className="timeline-header-month-cell"
|
|
14
|
-
style={{
|
|
15
|
-
flex: monthHeader.endIndex - monthHeader.startIndex + 1,
|
|
16
|
-
borderRight:
|
|
17
|
-
index < monthHeaders.length - 1 ? "1px solid var(--border-color)" : "none",
|
|
18
|
-
}}
|
|
19
|
-
>
|
|
20
|
-
{monthHeader.monthName} {monthHeader.year}
|
|
21
|
-
</div>
|
|
22
|
-
))}
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
{/* Günlük Hücreler */}
|
|
26
|
-
<div className="timeline-header-day-row">
|
|
27
|
-
{dates.map((date, index) => {
|
|
28
|
-
// Hafta sonu kontrolü
|
|
29
|
-
let isWeekend = false;
|
|
30
|
-
if (highlightWeekends) {
|
|
31
|
-
const cellDate = parseDate(date.fullDate);
|
|
32
|
-
const dayOfWeek = cellDate.getDay(); // 0 = Pazar, 6 = Cumartesi
|
|
33
|
-
isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div
|
|
38
|
-
key={index}
|
|
39
|
-
className={`timeline-header-day-cell ${isWeekend ? "timeline-header-day-weekend" : ""}`}
|
|
40
|
-
style={{
|
|
41
|
-
flex: 1,
|
|
42
|
-
borderRight: index < dates.length - 1 ? "1px solid var(--border-color)" : "none",
|
|
43
|
-
}}
|
|
44
|
-
>
|
|
45
|
-
{date.display}
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
})}
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export default TimelineHeader;
|
|
1
|
+
import React from "react";
|
|
2
|
+
import "./Timeline.css"; // CSS dosyasını import etmeyi unutma
|
|
3
|
+
import { parseDate } from "../../utils/dateUtils";
|
|
4
|
+
|
|
5
|
+
const TimelineHeader = ({ dates, monthHeaders, highlightWeekends = false }) => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="timeline-header-container">
|
|
8
|
+
{/* Ay ve Yıl Başlıkları */}
|
|
9
|
+
<div className="timeline-header-month-row">
|
|
10
|
+
{monthHeaders.map((monthHeader, index) => (
|
|
11
|
+
<div
|
|
12
|
+
key={index}
|
|
13
|
+
className="timeline-header-month-cell"
|
|
14
|
+
style={{
|
|
15
|
+
flex: monthHeader.endIndex - monthHeader.startIndex + 1,
|
|
16
|
+
borderRight:
|
|
17
|
+
index < monthHeaders.length - 1 ? "1px solid var(--border-color)" : "none",
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
{monthHeader.monthName} {monthHeader.year}
|
|
21
|
+
</div>
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
{/* Günlük Hücreler */}
|
|
26
|
+
<div className="timeline-header-day-row">
|
|
27
|
+
{dates.map((date, index) => {
|
|
28
|
+
// Hafta sonu kontrolü
|
|
29
|
+
let isWeekend = false;
|
|
30
|
+
if (highlightWeekends) {
|
|
31
|
+
const cellDate = parseDate(date.fullDate);
|
|
32
|
+
const dayOfWeek = cellDate.getDay(); // 0 = Pazar, 6 = Cumartesi
|
|
33
|
+
isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
key={index}
|
|
39
|
+
className={`timeline-header-day-cell ${isWeekend ? "timeline-header-day-weekend" : ""}`}
|
|
40
|
+
style={{
|
|
41
|
+
flex: 1,
|
|
42
|
+
borderRight: index < dates.length - 1 ? "1px solid var(--border-color)" : "none",
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{date.display}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
})}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default TimelineHeader;
|
|
55
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import TimelineHeader from "./TimelineHeader";
|
|
3
|
-
import TimelineContent from "./TimelineContent";
|
|
2
|
+
import TimelineHeader from "./TimelineHeader.jsx";
|
|
3
|
+
import TimelineContent from "./TimelineContent.jsx";
|
|
4
4
|
|
|
5
5
|
const TimelineMonthContainer = ({
|
|
6
6
|
dates,
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akfatimeline",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A customizable timeline component for React applications",
|
|
5
|
-
"main": "
|
|
6
|
-
"module": "src/library.js",
|
|
5
|
+
"main": "./src/library.js",
|
|
6
|
+
"module": "./src/library.js",
|
|
7
|
+
"types": "./src/library.js",
|
|
7
8
|
"files": [
|
|
8
9
|
"dist",
|
|
9
10
|
"src",
|
|
@@ -12,11 +13,61 @@
|
|
|
12
13
|
],
|
|
13
14
|
"exports": {
|
|
14
15
|
".": {
|
|
16
|
+
"types": "./src/library.js",
|
|
15
17
|
"import": "./src/library.js",
|
|
16
18
|
"require": "./dist/Timeline.js",
|
|
17
19
|
"default": "./src/library.js"
|
|
18
20
|
},
|
|
19
|
-
"./
|
|
21
|
+
"./css": "./src/components/Timeline/Timeline.css",
|
|
22
|
+
"./components/Timeline/Timeline.css": "./src/components/Timeline/Timeline.css",
|
|
23
|
+
"./Timeline": {
|
|
24
|
+
"types": "./src/components/Timeline/Timeline.jsx",
|
|
25
|
+
"import": "./src/components/Timeline/Timeline.jsx",
|
|
26
|
+
"require": "./dist/components/Timeline/Timeline.js",
|
|
27
|
+
"default": "./src/components/Timeline/Timeline.jsx"
|
|
28
|
+
},
|
|
29
|
+
"./DailyView": {
|
|
30
|
+
"types": "./src/components/Timeline/DailyView.jsx",
|
|
31
|
+
"import": "./src/components/Timeline/DailyView.jsx",
|
|
32
|
+
"require": "./dist/components/Timeline/DailyView.js",
|
|
33
|
+
"default": "./src/components/Timeline/DailyView.jsx"
|
|
34
|
+
},
|
|
35
|
+
"./ContextMenu": {
|
|
36
|
+
"types": "./src/components/Timeline/ContextMenu.jsx",
|
|
37
|
+
"import": "./src/components/Timeline/ContextMenu.jsx",
|
|
38
|
+
"require": "./dist/components/Timeline/ContextMenu.js",
|
|
39
|
+
"default": "./src/components/Timeline/ContextMenu.jsx"
|
|
40
|
+
},
|
|
41
|
+
"./EventDetailModal": {
|
|
42
|
+
"types": "./src/components/Timeline/EventDetailModal.jsx",
|
|
43
|
+
"import": "./src/components/Timeline/EventDetailModal.jsx",
|
|
44
|
+
"require": "./dist/components/Timeline/EventDetailModal.js",
|
|
45
|
+
"default": "./src/components/Timeline/EventDetailModal.jsx"
|
|
46
|
+
},
|
|
47
|
+
"./EventIcon": {
|
|
48
|
+
"types": "./src/components/Timeline/EventIcon.jsx",
|
|
49
|
+
"import": "./src/components/Timeline/EventIcon.jsx",
|
|
50
|
+
"require": "./dist/components/Timeline/EventIcon.js",
|
|
51
|
+
"default": "./src/components/Timeline/EventIcon.jsx"
|
|
52
|
+
},
|
|
53
|
+
"./EventBadge": {
|
|
54
|
+
"types": "./src/components/Timeline/EventBadge.jsx",
|
|
55
|
+
"import": "./src/components/Timeline/EventBadge.jsx",
|
|
56
|
+
"require": "./dist/components/Timeline/EventBadge.js",
|
|
57
|
+
"default": "./src/components/Timeline/EventBadge.jsx"
|
|
58
|
+
},
|
|
59
|
+
"./LoadingSpinner": {
|
|
60
|
+
"types": "./src/components/Timeline/LoadingSpinner.jsx",
|
|
61
|
+
"import": "./src/components/Timeline/LoadingSpinner.jsx",
|
|
62
|
+
"require": "./dist/components/Timeline/LoadingSpinner.js",
|
|
63
|
+
"default": "./src/components/Timeline/LoadingSpinner.jsx"
|
|
64
|
+
},
|
|
65
|
+
"./AutocompleteSelect": {
|
|
66
|
+
"types": "./src/components/Timeline/AutocompleteSelect.jsx",
|
|
67
|
+
"import": "./src/components/Timeline/AutocompleteSelect.jsx",
|
|
68
|
+
"require": "./dist/components/Timeline/AutocompleteSelect.js",
|
|
69
|
+
"default": "./src/components/Timeline/AutocompleteSelect.jsx"
|
|
70
|
+
}
|
|
20
71
|
},
|
|
21
72
|
"dependencies": {
|
|
22
73
|
"react": "^19.2.3",
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
+
import "./Timeline.css";
|
|
3
|
+
|
|
4
|
+
const AutocompleteSelect = ({
|
|
5
|
+
options = [],
|
|
6
|
+
value = null,
|
|
7
|
+
onChange = () => {},
|
|
8
|
+
placeholder = "Seçiniz...",
|
|
9
|
+
getOptionLabel = (option) => option?.label || option?.name || String(option),
|
|
10
|
+
getOptionValue = (option) => option?.value || option?.id || option,
|
|
11
|
+
filterOptions = (options, inputValue) => {
|
|
12
|
+
if (!inputValue) return options;
|
|
13
|
+
const lowerInput = inputValue.toLowerCase();
|
|
14
|
+
return options.filter((option) => {
|
|
15
|
+
const label = getOptionLabel(option).toLowerCase();
|
|
16
|
+
return label.includes(lowerInput);
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
}) => {
|
|
20
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
21
|
+
const [inputValue, setInputValue] = useState("");
|
|
22
|
+
const [filteredOptions, setFilteredOptions] = useState(options);
|
|
23
|
+
const containerRef = useRef(null);
|
|
24
|
+
const inputRef = useRef(null);
|
|
25
|
+
|
|
26
|
+
// Seçili değerin label'ını bul
|
|
27
|
+
const selectedOption = options.find(
|
|
28
|
+
(opt) => getOptionValue(opt) === value
|
|
29
|
+
);
|
|
30
|
+
const displayValue = selectedOption
|
|
31
|
+
? getOptionLabel(selectedOption)
|
|
32
|
+
: inputValue || placeholder;
|
|
33
|
+
|
|
34
|
+
// Input değiştiğinde filtrele
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (isOpen) {
|
|
37
|
+
const filtered = filterOptions(options, inputValue);
|
|
38
|
+
setFilteredOptions(filtered);
|
|
39
|
+
} else {
|
|
40
|
+
setFilteredOptions(options);
|
|
41
|
+
}
|
|
42
|
+
}, [inputValue, isOpen, options, filterOptions]);
|
|
43
|
+
|
|
44
|
+
// Dışarı tıklandığında kapat
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const handleClickOutside = (event) => {
|
|
47
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
48
|
+
setIsOpen(false);
|
|
49
|
+
setInputValue("");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (isOpen) {
|
|
54
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
55
|
+
return () => {
|
|
56
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}, [isOpen]);
|
|
60
|
+
|
|
61
|
+
const handleInputChange = (e) => {
|
|
62
|
+
const newValue = e.target.value;
|
|
63
|
+
setInputValue(newValue);
|
|
64
|
+
setIsOpen(true);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleSelect = (option) => {
|
|
68
|
+
const optionValue = getOptionValue(option);
|
|
69
|
+
onChange(optionValue, option);
|
|
70
|
+
setInputValue("");
|
|
71
|
+
setIsOpen(false);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleFocus = () => {
|
|
75
|
+
setIsOpen(true);
|
|
76
|
+
if (selectedOption) {
|
|
77
|
+
setInputValue(getOptionLabel(selectedOption));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleBlur = () => {
|
|
82
|
+
// Input blur olduğunda hemen kapatma, click outside ile kapatılacak
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
if (!containerRef.current?.contains(document.activeElement)) {
|
|
85
|
+
setIsOpen(false);
|
|
86
|
+
if (selectedOption) {
|
|
87
|
+
setInputValue("");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, 200);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="autocomplete-select-container" ref={containerRef}>
|
|
95
|
+
<div
|
|
96
|
+
className={`autocomplete-select-input ${isOpen ? "open" : ""}`}
|
|
97
|
+
onClick={() => {
|
|
98
|
+
setIsOpen(!isOpen);
|
|
99
|
+
inputRef.current?.focus();
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<input
|
|
103
|
+
ref={inputRef}
|
|
104
|
+
type="text"
|
|
105
|
+
value={isOpen ? inputValue : displayValue}
|
|
106
|
+
onChange={handleInputChange}
|
|
107
|
+
onFocus={handleFocus}
|
|
108
|
+
onBlur={handleBlur}
|
|
109
|
+
placeholder={placeholder}
|
|
110
|
+
className="autocomplete-select-input-field"
|
|
111
|
+
/>
|
|
112
|
+
<span className="autocomplete-select-arrow">
|
|
113
|
+
{isOpen ? "▲" : "▼"}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{isOpen && (
|
|
118
|
+
<div className="autocomplete-select-dropdown">
|
|
119
|
+
{filteredOptions.length > 0 ? (
|
|
120
|
+
filteredOptions.map((option, index) => {
|
|
121
|
+
const optionValue = getOptionValue(option);
|
|
122
|
+
const optionLabel = getOptionLabel(option);
|
|
123
|
+
const isSelected = optionValue === value;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div
|
|
127
|
+
key={index}
|
|
128
|
+
className={`autocomplete-select-option ${
|
|
129
|
+
isSelected ? "selected" : ""
|
|
130
|
+
}`}
|
|
131
|
+
onClick={() => handleSelect(option)}
|
|
132
|
+
onMouseDown={(e) => e.preventDefault()} // Blur'u engelle
|
|
133
|
+
>
|
|
134
|
+
{optionLabel}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
) : (
|
|
139
|
+
<div className="autocomplete-select-no-results">
|
|
140
|
+
Sonuç bulunamadı
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default AutocompleteSelect;
|
|
150
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import './Timeline.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context Menu Component
|
|
6
|
+
* Sağ tık menüsü için özelleştirilebilir menü bileşeni
|
|
7
|
+
*/
|
|
8
|
+
const ContextMenu = ({
|
|
9
|
+
isOpen,
|
|
10
|
+
position,
|
|
11
|
+
onClose,
|
|
12
|
+
menuItems = [],
|
|
13
|
+
resource = null,
|
|
14
|
+
date = null,
|
|
15
|
+
}) => {
|
|
16
|
+
const menuRef = useRef(null);
|
|
17
|
+
const [adjustedPosition, setAdjustedPosition] = useState(position);
|
|
18
|
+
|
|
19
|
+
// Menü pozisyonunu mouse'a yakın tut ve ekran sınırları içinde tut
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!menuRef.current || !position || !isOpen) {
|
|
22
|
+
setAdjustedPosition(position);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Menü render edildikten sonra pozisyonu ayarla
|
|
27
|
+
const updatePosition = () => {
|
|
28
|
+
const menuRect = menuRef.current.getBoundingClientRect();
|
|
29
|
+
const viewportWidth = window.innerWidth;
|
|
30
|
+
const viewportHeight = window.innerHeight;
|
|
31
|
+
|
|
32
|
+
let adjustedX = position.x;
|
|
33
|
+
let adjustedY = position.y;
|
|
34
|
+
|
|
35
|
+
// Sağa taşma kontrolü
|
|
36
|
+
if (position.x + menuRect.width > viewportWidth) {
|
|
37
|
+
adjustedX = position.x - menuRect.width;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Aşağıya taşma kontrolü
|
|
41
|
+
if (position.y + menuRect.height > viewportHeight) {
|
|
42
|
+
adjustedY = position.y - menuRect.height;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Sola taşma kontrolü
|
|
46
|
+
if (adjustedX < 10) {
|
|
47
|
+
adjustedX = 10;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Yukarıya taşma kontrolü
|
|
51
|
+
if (adjustedY < 10) {
|
|
52
|
+
adjustedY = 10;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setAdjustedPosition({ x: adjustedX, y: adjustedY });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Menü render edildikten sonra pozisyonu güncelle
|
|
59
|
+
setTimeout(updatePosition, 0);
|
|
60
|
+
}, [position, isOpen]);
|
|
61
|
+
|
|
62
|
+
// Menü dışına tıklanınca kapat
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const handleClickOutside = (event) => {
|
|
65
|
+
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
|
66
|
+
onClose();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleEscape = (event) => {
|
|
71
|
+
if (event.key === 'Escape') {
|
|
72
|
+
onClose();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (isOpen) {
|
|
77
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
78
|
+
document.addEventListener('keydown', handleEscape);
|
|
79
|
+
// Scroll olduğunda menüyü kapat
|
|
80
|
+
document.addEventListener('scroll', onClose, true);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
85
|
+
document.removeEventListener('keydown', handleEscape);
|
|
86
|
+
document.removeEventListener('scroll', onClose, true);
|
|
87
|
+
};
|
|
88
|
+
}, [isOpen, onClose]);
|
|
89
|
+
|
|
90
|
+
if (!isOpen || !position) return null;
|
|
91
|
+
|
|
92
|
+
const handleItemClick = (item) => {
|
|
93
|
+
if (item.onClick) {
|
|
94
|
+
item.onClick(resource, date);
|
|
95
|
+
}
|
|
96
|
+
if (item.closeOnClick !== false) {
|
|
97
|
+
onClose();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
ref={menuRef}
|
|
104
|
+
className="context-menu"
|
|
105
|
+
style={{
|
|
106
|
+
position: 'fixed',
|
|
107
|
+
left: `${(adjustedPosition?.x ?? position.x) - 150}px`,
|
|
108
|
+
top: `${(adjustedPosition?.y ?? position.y) - 150}px`,
|
|
109
|
+
zIndex: 10005,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<div className="context-menu-content">
|
|
113
|
+
{menuItems.length === 0 ? (
|
|
114
|
+
<div className="context-menu-item context-menu-item-disabled">
|
|
115
|
+
Menü öğesi yok
|
|
116
|
+
</div>
|
|
117
|
+
) : (
|
|
118
|
+
menuItems.map((item, index) => {
|
|
119
|
+
if (item.separator) {
|
|
120
|
+
return <div key={`separator-${index}`} className="context-menu-separator" />;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (item.hidden) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
key={item.id || index}
|
|
130
|
+
className={`context-menu-item ${item.disabled ? 'context-menu-item-disabled' : ''} ${item.danger ? 'context-menu-item-danger' : ''}`}
|
|
131
|
+
onClick={() => !item.disabled && handleItemClick(item)}
|
|
132
|
+
title={item.tooltip || item.label}
|
|
133
|
+
>
|
|
134
|
+
{item.icon && <span className="context-menu-item-icon">{item.icon}</span>}
|
|
135
|
+
<span className="context-menu-item-label">{item.label}</span>
|
|
136
|
+
{item.shortcut && (
|
|
137
|
+
<span className="context-menu-item-shortcut">{item.shortcut}</span>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
})
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export default ContextMenu;
|
|
149
|
+
|