@topconsultnpm/sdkui-react 6.20.0-dev1.6 → 6.20.0-dev1.60
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/lib/components/NewComponents/ContextMenu/TMContextMenu.d.ts +4 -0
- package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +416 -0
- package/lib/components/NewComponents/ContextMenu/hooks.d.ts +13 -0
- package/lib/components/NewComponents/ContextMenu/hooks.js +61 -0
- package/lib/components/NewComponents/ContextMenu/index.d.ts +5 -0
- package/lib/components/NewComponents/ContextMenu/index.js +3 -0
- package/lib/components/NewComponents/ContextMenu/styles.d.ts +31 -0
- package/lib/components/NewComponents/ContextMenu/styles.js +336 -0
- package/lib/components/NewComponents/ContextMenu/types.d.ts +39 -0
- package/lib/components/NewComponents/ContextMenu/types.js +1 -0
- package/lib/components/NewComponents/ContextMenu/useLongPress.d.ts +21 -0
- package/lib/components/NewComponents/ContextMenu/useLongPress.js +112 -0
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.d.ts +4 -0
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +745 -0
- package/lib/components/NewComponents/FloatingMenuBar/index.d.ts +2 -0
- package/lib/components/NewComponents/FloatingMenuBar/index.js +2 -0
- package/lib/components/NewComponents/FloatingMenuBar/styles.d.ts +51 -0
- package/lib/components/NewComponents/FloatingMenuBar/styles.js +385 -0
- package/lib/components/NewComponents/FloatingMenuBar/types.d.ts +29 -0
- package/lib/components/NewComponents/FloatingMenuBar/types.js +1 -0
- package/lib/components/base/TMAccordionNew.js +35 -14
- package/lib/components/base/TMCustomButton.js +61 -17
- package/lib/components/base/TMDataGrid.d.ts +7 -4
- package/lib/components/base/TMDataGrid.js +142 -11
- package/lib/components/choosers/TMMetadataChooser.js +8 -1
- package/lib/components/editors/TMMetadataValues.js +23 -5
- package/lib/components/editors/TMTextBox.js +6 -3
- package/lib/components/features/documents/TMDcmtForm.d.ts +13 -1
- package/lib/components/features/documents/TMDcmtForm.js +386 -194
- package/lib/components/features/documents/TMDcmtPreview.js +40 -69
- package/lib/components/features/documents/TMMasterDetailDcmts.js +37 -52
- package/lib/components/features/search/TMDcmtCheckoutInfoForm.d.ts +8 -0
- package/lib/components/features/search/{TMSearchResultCheckoutInfoForm.js → TMDcmtCheckoutInfoForm.js} +5 -10
- package/lib/components/features/search/TMSavedQuerySelector.js +72 -67
- package/lib/components/features/search/TMSearch.js +30 -5
- package/lib/components/features/search/TMSearchQueryPanel.js +13 -12
- package/lib/components/features/search/TMSearchResult.js +57 -216
- package/lib/components/features/search/TMSearchResultsMenuItems.d.ts +3 -3
- package/lib/components/features/search/TMSearchResultsMenuItems.js +205 -169
- package/lib/components/features/search/TMSignSettingsForm.js +1 -1
- package/lib/components/features/search/TMSignatureInfoContent.d.ts +6 -0
- package/lib/components/features/search/TMSignatureInfoContent.js +140 -0
- package/lib/components/features/search/TMViewHistoryDcmt.js +1 -1
- package/lib/components/features/tasks/TMTaskForm.js +20 -1
- package/lib/components/features/tasks/TMTasksUtils.d.ts +2 -2
- package/lib/components/features/tasks/TMTasksUtils.js +62 -52
- package/lib/components/features/tasks/TMTasksView.js +6 -6
- package/lib/components/features/workflow/TMWorkflowPopup.d.ts +32 -2
- package/lib/components/features/workflow/TMWorkflowPopup.js +112 -14
- package/lib/components/features/workflow/diagram/WFDiagram.js +2 -2
- package/lib/components/forms/Login/LoginValidatorService.d.ts +2 -0
- package/lib/components/forms/Login/LoginValidatorService.js +7 -2
- package/lib/components/forms/Login/TMLoginForm.js +34 -6
- package/lib/components/forms/TMChooserForm.js +1 -1
- package/lib/components/grids/TMBlogsPost.js +55 -30
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +2 -0
- package/lib/components/viewers/TMDataListItemViewer.d.ts +2 -1
- package/lib/components/viewers/TMDataListItemViewer.js +12 -11
- package/lib/css/tm-sdkui.css +1 -1
- package/lib/helper/SDKUI_Globals.d.ts +17 -0
- package/lib/helper/SDKUI_Globals.js +9 -0
- package/lib/helper/SDKUI_Localizator.d.ts +9 -1
- package/lib/helper/SDKUI_Localizator.js +87 -1
- package/lib/helper/TMIcons.d.ts +2 -0
- package/lib/helper/TMIcons.js +6 -0
- package/lib/helper/TMPdfViewer.d.ts +8 -0
- package/lib/helper/TMPdfViewer.js +368 -0
- package/lib/helper/checkinCheckoutManager.d.ts +32 -2
- package/lib/helper/checkinCheckoutManager.js +115 -38
- package/lib/helper/devextremeCustomMessages.d.ts +30 -0
- package/lib/helper/devextremeCustomMessages.js +30 -0
- package/lib/helper/helpers.d.ts +2 -1
- package/lib/helper/helpers.js +14 -3
- package/lib/helper/index.d.ts +1 -0
- package/lib/helper/index.js +1 -0
- package/lib/helper/queryHelper.js +29 -0
- package/lib/hooks/useCheckInOutOperations.d.ts +28 -0
- package/lib/hooks/useCheckInOutOperations.js +223 -0
- package/lib/hooks/useWorkflowApprove.d.ts +4 -0
- package/lib/hooks/useWorkflowApprove.js +14 -1
- package/lib/ts/types.d.ts +56 -1
- package/package.json +5 -2
- package/lib/components/features/search/TMSearchResultCheckoutInfoForm.d.ts +0 -8
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import * as S from './styles';
|
|
5
|
+
import { useIsMobile, useMenuPosition, useIsIOS } from './hooks';
|
|
6
|
+
import { IconArrowLeft } from '../../../helper';
|
|
7
|
+
const TMContextMenu = ({ items, trigger = 'right', children, target, externalControl, keepOpenOnClick = false }) => {
|
|
8
|
+
const [menuState, setMenuState] = useState({
|
|
9
|
+
visible: false,
|
|
10
|
+
position: { x: 0, y: 0 },
|
|
11
|
+
submenuStack: [items],
|
|
12
|
+
parentNames: [],
|
|
13
|
+
});
|
|
14
|
+
const [hoveredSubmenus, setHoveredSubmenus] = useState([]);
|
|
15
|
+
const isMobile = useIsMobile();
|
|
16
|
+
const isIOS = useIsIOS();
|
|
17
|
+
const menuRef = useRef(null);
|
|
18
|
+
const triggerRef = useRef(null);
|
|
19
|
+
const submenuTimeoutRef = useRef(null);
|
|
20
|
+
const longPressTimeoutRef = useRef(null);
|
|
21
|
+
const touchStartPos = useRef(null);
|
|
22
|
+
const { openLeft, openUp, isCalculated } = useMenuPosition(menuRef, menuState.position);
|
|
23
|
+
const handleClose = () => {
|
|
24
|
+
if (externalControl) {
|
|
25
|
+
externalControl.onClose();
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
setMenuState(prev => ({
|
|
29
|
+
...prev,
|
|
30
|
+
visible: false,
|
|
31
|
+
submenuStack: [items],
|
|
32
|
+
parentNames: [],
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
setHoveredSubmenus([]);
|
|
36
|
+
};
|
|
37
|
+
// Sync with external control when provided
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (externalControl) {
|
|
40
|
+
setMenuState(prev => ({
|
|
41
|
+
...prev,
|
|
42
|
+
visible: externalControl.visible,
|
|
43
|
+
position: externalControl.position,
|
|
44
|
+
submenuStack: [items],
|
|
45
|
+
parentNames: [],
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
}, [externalControl, items]);
|
|
49
|
+
// iOS long-press support: attach touch listeners to target elements
|
|
50
|
+
// On long-press, dispatch synthetic contextmenu event to trigger existing handlers
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!target || !isIOS)
|
|
53
|
+
return;
|
|
54
|
+
const elements = document.querySelectorAll(target);
|
|
55
|
+
if (elements.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
const touchStateMap = new WeakMap();
|
|
58
|
+
const handleTouchStart = (e) => {
|
|
59
|
+
const touchEvent = e;
|
|
60
|
+
const element = e.currentTarget;
|
|
61
|
+
const touch = touchEvent.touches[0];
|
|
62
|
+
let state = touchStateMap.get(element);
|
|
63
|
+
if (!state) {
|
|
64
|
+
state = { timeout: null, startX: 0, startY: 0, longPressTriggered: false };
|
|
65
|
+
touchStateMap.set(element, state);
|
|
66
|
+
}
|
|
67
|
+
state.startX = touch.clientX;
|
|
68
|
+
state.startY = touch.clientY;
|
|
69
|
+
state.longPressTriggered = false;
|
|
70
|
+
if (state.timeout)
|
|
71
|
+
clearTimeout(state.timeout);
|
|
72
|
+
state.timeout = setTimeout(() => {
|
|
73
|
+
if (state)
|
|
74
|
+
state.longPressTriggered = true;
|
|
75
|
+
// Haptic feedback
|
|
76
|
+
if ('vibrate' in navigator)
|
|
77
|
+
navigator.vibrate(50);
|
|
78
|
+
const syntheticEvent = new MouseEvent('contextmenu', {
|
|
79
|
+
bubbles: true,
|
|
80
|
+
cancelable: true,
|
|
81
|
+
clientX: touch.clientX,
|
|
82
|
+
clientY: touch.clientY,
|
|
83
|
+
});
|
|
84
|
+
element.dispatchEvent(syntheticEvent);
|
|
85
|
+
if (state)
|
|
86
|
+
state.timeout = null;
|
|
87
|
+
}, 500);
|
|
88
|
+
};
|
|
89
|
+
const handleTouchMove = (e) => {
|
|
90
|
+
const touchEvent = e;
|
|
91
|
+
const element = e.currentTarget;
|
|
92
|
+
const state = touchStateMap.get(element);
|
|
93
|
+
if (!state?.timeout)
|
|
94
|
+
return;
|
|
95
|
+
const touch = touchEvent.touches[0];
|
|
96
|
+
const dx = Math.abs(touch.clientX - state.startX);
|
|
97
|
+
const dy = Math.abs(touch.clientY - state.startY);
|
|
98
|
+
if (dx > 10 || dy > 10) {
|
|
99
|
+
clearTimeout(state.timeout);
|
|
100
|
+
state.timeout = null;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const handleTouchEnd = (e) => {
|
|
104
|
+
const element = e.currentTarget;
|
|
105
|
+
const state = touchStateMap.get(element);
|
|
106
|
+
if (state?.timeout) {
|
|
107
|
+
clearTimeout(state.timeout);
|
|
108
|
+
state.timeout = null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
// Prevent click event after long-press was triggered
|
|
112
|
+
const handleClick = (e) => {
|
|
113
|
+
const element = e.currentTarget;
|
|
114
|
+
const state = touchStateMap.get(element);
|
|
115
|
+
if (state?.longPressTriggered) {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
e.stopPropagation();
|
|
118
|
+
e.stopImmediatePropagation();
|
|
119
|
+
state.longPressTriggered = false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
// Attach listeners to all matching elements
|
|
123
|
+
elements.forEach(element => {
|
|
124
|
+
const el = element;
|
|
125
|
+
// Prevent iOS native callout
|
|
126
|
+
const style = el.style;
|
|
127
|
+
style.webkitTouchCallout = 'none';
|
|
128
|
+
style.webkitUserSelect = 'none';
|
|
129
|
+
el.addEventListener('touchstart', handleTouchStart, { passive: true });
|
|
130
|
+
el.addEventListener('touchmove', handleTouchMove, { passive: true });
|
|
131
|
+
el.addEventListener('touchend', handleTouchEnd);
|
|
132
|
+
el.addEventListener('touchcancel', handleTouchEnd);
|
|
133
|
+
el.addEventListener('click', handleClick, { capture: true });
|
|
134
|
+
});
|
|
135
|
+
return () => {
|
|
136
|
+
elements.forEach(element => {
|
|
137
|
+
const el = element;
|
|
138
|
+
const style = el.style;
|
|
139
|
+
style.webkitTouchCallout = '';
|
|
140
|
+
style.webkitUserSelect = '';
|
|
141
|
+
el.removeEventListener('touchstart', handleTouchStart);
|
|
142
|
+
el.removeEventListener('touchmove', handleTouchMove);
|
|
143
|
+
el.removeEventListener('touchend', handleTouchEnd);
|
|
144
|
+
el.removeEventListener('touchcancel', handleTouchEnd);
|
|
145
|
+
el.removeEventListener('click', handleClick, { capture: true });
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
}, [target, isIOS]);
|
|
149
|
+
// Update items when they change while menu is visible (for keepOpenOnClick behavior)
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!keepOpenOnClick)
|
|
152
|
+
return;
|
|
153
|
+
if (!externalControl && menuState.visible) {
|
|
154
|
+
setMenuState(prev => ({
|
|
155
|
+
...prev,
|
|
156
|
+
submenuStack: [items],
|
|
157
|
+
}));
|
|
158
|
+
// Update hoveredSubmenus with fresh items while keeping them open
|
|
159
|
+
setHoveredSubmenus(prev => {
|
|
160
|
+
if (prev.length === 0)
|
|
161
|
+
return prev;
|
|
162
|
+
// Rebuild hoveredSubmenus with updated items from the new items structure
|
|
163
|
+
return prev.map(submenu => {
|
|
164
|
+
// Find the matching submenu in the new items structure
|
|
165
|
+
const findSubmenuInItems = (searchItems) => {
|
|
166
|
+
for (const item of searchItems) {
|
|
167
|
+
if (item.submenu && item.submenu.length > 0) {
|
|
168
|
+
// Check if this submenu matches (compare first item name as identifier)
|
|
169
|
+
if (submenu.items.length > 0 && item.submenu.length > 0 &&
|
|
170
|
+
item.submenu[0].name === submenu.items[0].name) {
|
|
171
|
+
return item.submenu;
|
|
172
|
+
}
|
|
173
|
+
// Recursively search in nested submenus
|
|
174
|
+
const found = findSubmenuInItems(item.submenu);
|
|
175
|
+
if (found)
|
|
176
|
+
return found;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
const updatedItems = findSubmenuInItems(items);
|
|
182
|
+
return {
|
|
183
|
+
...submenu,
|
|
184
|
+
items: updatedItems || submenu.items, // Use updated items if found, otherwise keep old
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}, [items, menuState.visible, externalControl, keepOpenOnClick]);
|
|
190
|
+
// Track when the menu was opened to prevent immediate close on iOS
|
|
191
|
+
const menuOpenedAtRef = useRef(0);
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (menuState.visible) {
|
|
194
|
+
menuOpenedAtRef.current = Date.now();
|
|
195
|
+
}
|
|
196
|
+
}, [menuState.visible]);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!menuState.visible)
|
|
199
|
+
return;
|
|
200
|
+
const handleClickOutside = (event) => {
|
|
201
|
+
// On iOS, prevent closing immediately after opening (within 300ms)
|
|
202
|
+
// This handles the case where touchend from long-press triggers touchstart listener
|
|
203
|
+
if (Date.now() - menuOpenedAtRef.current < 300) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const target = event.target;
|
|
207
|
+
// Check if click is inside main menu
|
|
208
|
+
if (menuRef.current?.contains(target)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Check if click is inside any submenu
|
|
212
|
+
const submenus = document.querySelectorAll('[data-submenu="true"]');
|
|
213
|
+
for (const submenu of Array.from(submenus)) {
|
|
214
|
+
if (submenu.contains(target)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Click is outside all menus, close them
|
|
219
|
+
handleClose();
|
|
220
|
+
};
|
|
221
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
222
|
+
document.addEventListener('touchstart', handleClickOutside);
|
|
223
|
+
return () => {
|
|
224
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
225
|
+
document.removeEventListener('touchstart', handleClickOutside);
|
|
226
|
+
};
|
|
227
|
+
}, [menuState.visible]);
|
|
228
|
+
const handleContextMenu = (e) => {
|
|
229
|
+
if (trigger === 'right') {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
setMenuState({
|
|
232
|
+
visible: true,
|
|
233
|
+
position: { x: e.clientX, y: e.clientY },
|
|
234
|
+
submenuStack: [items],
|
|
235
|
+
parentNames: [],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const handleClick = (e) => {
|
|
240
|
+
if (trigger === 'left') {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
setMenuState({
|
|
243
|
+
visible: true,
|
|
244
|
+
position: { x: e.clientX, y: e.clientY },
|
|
245
|
+
submenuStack: [items],
|
|
246
|
+
parentNames: [],
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
// iOS-specific touch handlers for long press
|
|
251
|
+
const handleTouchStart = (e) => {
|
|
252
|
+
if (!isIOS || trigger !== 'right')
|
|
253
|
+
return;
|
|
254
|
+
const touch = e.touches[0];
|
|
255
|
+
touchStartPos.current = { x: touch.clientX, y: touch.clientY };
|
|
256
|
+
if (longPressTimeoutRef.current) {
|
|
257
|
+
clearTimeout(longPressTimeoutRef.current);
|
|
258
|
+
}
|
|
259
|
+
longPressTimeoutRef.current = setTimeout(() => {
|
|
260
|
+
if (touchStartPos.current) {
|
|
261
|
+
if ('vibrate' in navigator) {
|
|
262
|
+
navigator.vibrate(50);
|
|
263
|
+
}
|
|
264
|
+
setMenuState({
|
|
265
|
+
visible: true,
|
|
266
|
+
position: { x: touchStartPos.current.x, y: touchStartPos.current.y },
|
|
267
|
+
submenuStack: [items],
|
|
268
|
+
parentNames: [],
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}, 500);
|
|
272
|
+
};
|
|
273
|
+
const handleTouchMove = (e) => {
|
|
274
|
+
if (!isIOS || trigger !== 'right' || !touchStartPos.current)
|
|
275
|
+
return;
|
|
276
|
+
const touch = e.touches[0];
|
|
277
|
+
const moveThreshold = 10; // pixels
|
|
278
|
+
// If finger moved too much, cancel long press
|
|
279
|
+
const deltaX = Math.abs(touch.clientX - touchStartPos.current.x);
|
|
280
|
+
const deltaY = Math.abs(touch.clientY - touchStartPos.current.y);
|
|
281
|
+
if (deltaX > moveThreshold || deltaY > moveThreshold) {
|
|
282
|
+
if (longPressTimeoutRef.current) {
|
|
283
|
+
clearTimeout(longPressTimeoutRef.current);
|
|
284
|
+
longPressTimeoutRef.current = null;
|
|
285
|
+
}
|
|
286
|
+
touchStartPos.current = null;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const handleTouchEnd = () => {
|
|
290
|
+
if (!isIOS || trigger !== 'right')
|
|
291
|
+
return;
|
|
292
|
+
if (longPressTimeoutRef.current) {
|
|
293
|
+
clearTimeout(longPressTimeoutRef.current);
|
|
294
|
+
longPressTimeoutRef.current = null;
|
|
295
|
+
}
|
|
296
|
+
touchStartPos.current = null;
|
|
297
|
+
};
|
|
298
|
+
const handleItemClick = (item) => {
|
|
299
|
+
if (item.disabled)
|
|
300
|
+
return;
|
|
301
|
+
if (item.onClick) {
|
|
302
|
+
item.onClick();
|
|
303
|
+
}
|
|
304
|
+
if (item.submenu && item.submenu.length > 0) {
|
|
305
|
+
if (isMobile) {
|
|
306
|
+
setMenuState(prev => ({
|
|
307
|
+
...prev,
|
|
308
|
+
submenuStack: [...prev.submenuStack, item.submenu],
|
|
309
|
+
parentNames: [...prev.parentNames, item.name],
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
if (item.onClick || !keepOpenOnClick) {
|
|
315
|
+
handleClose();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const handleBack = () => {
|
|
320
|
+
setMenuState(prev => ({
|
|
321
|
+
...prev,
|
|
322
|
+
submenuStack: prev.submenuStack.slice(0, -1),
|
|
323
|
+
parentNames: prev.parentNames.slice(0, -1),
|
|
324
|
+
}));
|
|
325
|
+
};
|
|
326
|
+
const handleMouseEnter = (item, event, depth = 0) => {
|
|
327
|
+
if (isMobile || !item.submenu || item.submenu.length === 0)
|
|
328
|
+
return;
|
|
329
|
+
if (submenuTimeoutRef.current) {
|
|
330
|
+
clearTimeout(submenuTimeoutRef.current);
|
|
331
|
+
submenuTimeoutRef.current = null;
|
|
332
|
+
}
|
|
333
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
334
|
+
// Calculate if submenu should open upward based on available space
|
|
335
|
+
// Estimate submenu height: ~35px per item (accounting for smaller padding) + container padding
|
|
336
|
+
const estimatedSubmenuHeight = (item.submenu.length * 35) + 8;
|
|
337
|
+
const spaceBelow = window.innerHeight - rect.top;
|
|
338
|
+
const spaceAbove = rect.bottom;
|
|
339
|
+
// Open upward only if there's not enough space below AND there's more space above
|
|
340
|
+
const shouldOpenUp = spaceBelow < estimatedSubmenuHeight && spaceAbove > spaceBelow;
|
|
341
|
+
// Remove all submenus at this depth and deeper
|
|
342
|
+
setHoveredSubmenus(prev => {
|
|
343
|
+
const filtered = prev.filter(sub => sub.depth < depth);
|
|
344
|
+
if (!item.submenu)
|
|
345
|
+
return filtered;
|
|
346
|
+
return [
|
|
347
|
+
...filtered,
|
|
348
|
+
{
|
|
349
|
+
items: item.submenu,
|
|
350
|
+
parentRect: rect,
|
|
351
|
+
depth: depth,
|
|
352
|
+
openUp: shouldOpenUp,
|
|
353
|
+
}
|
|
354
|
+
];
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
const handleMouseLeave = (depth = 0) => {
|
|
358
|
+
if (isMobile)
|
|
359
|
+
return;
|
|
360
|
+
if (submenuTimeoutRef.current) {
|
|
361
|
+
clearTimeout(submenuTimeoutRef.current);
|
|
362
|
+
}
|
|
363
|
+
const targetDepth = depth;
|
|
364
|
+
submenuTimeoutRef.current = setTimeout(() => {
|
|
365
|
+
setHoveredSubmenus(prev => prev.filter(sub => sub.depth < targetDepth));
|
|
366
|
+
}, 300);
|
|
367
|
+
};
|
|
368
|
+
const handleSubmenuMouseEnter = () => {
|
|
369
|
+
if (submenuTimeoutRef.current) {
|
|
370
|
+
clearTimeout(submenuTimeoutRef.current);
|
|
371
|
+
submenuTimeoutRef.current = null;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
return () => {
|
|
376
|
+
if (submenuTimeoutRef.current) {
|
|
377
|
+
clearTimeout(submenuTimeoutRef.current);
|
|
378
|
+
}
|
|
379
|
+
if (longPressTimeoutRef.current) {
|
|
380
|
+
clearTimeout(longPressTimeoutRef.current);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}, []);
|
|
384
|
+
const renderMenuItems = (menuItems, depth = 0) => {
|
|
385
|
+
return menuItems
|
|
386
|
+
.filter(item => item.visible !== false)
|
|
387
|
+
.map((item, idx) => {
|
|
388
|
+
const itemKey = `${item.name}-${idx}`.replaceAll(/\s+/g, '-');
|
|
389
|
+
const handleClick = (e) => {
|
|
390
|
+
if (item.disabled)
|
|
391
|
+
return;
|
|
392
|
+
e.stopPropagation();
|
|
393
|
+
handleItemClick(item);
|
|
394
|
+
};
|
|
395
|
+
const handleRightIconClick = (e) => {
|
|
396
|
+
e.stopPropagation();
|
|
397
|
+
// if (item.disabled) return;
|
|
398
|
+
item.onRightIconClick?.();
|
|
399
|
+
handleClose();
|
|
400
|
+
};
|
|
401
|
+
return (_jsxs(S.MenuItem, { "$disabled": item.disabled, "$hasSubmenu": !!item.submenu && item.submenu.length > 0, "$beginGroup": item.beginGroup, onMouseDown: handleClick, onMouseEnter: (e) => !isMobile && handleMouseEnter(item, e, depth + 1), onMouseLeave: () => !isMobile && handleMouseLeave(depth + 1), title: item.tooltip, children: [_jsxs(S.MenuItemContent, { children: [item.icon && _jsx(S.IconWrapper, { children: item.icon }), _jsx(S.MenuItemName, { children: item.name })] }), item.rightIcon && item.onRightIconClick && (_jsx(S.RightIconButton, { onClick: handleRightIconClick, onMouseDown: (e) => e.stopPropagation(), "aria-label": `Action for ${item.name}`, children: item.rightIcon })), item.submenu && item.submenu.length > 0 && (_jsx(S.SubmenuIndicator, { "$isMobile": isMobile, children: isMobile ? '›' : '▸' }))] }, itemKey));
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
const currentMenu = menuState.submenuStack.at(-1) || items;
|
|
405
|
+
const currentParentName = menuState.parentNames.at(-1) || '';
|
|
406
|
+
return (_jsxs(_Fragment, { children: [!externalControl && children && (_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onTouchCancel: handleTouchEnd, onKeyDown: (e) => {
|
|
407
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
408
|
+
handleClick(e);
|
|
409
|
+
}
|
|
410
|
+
}, role: "button", tabIndex: 0, style: {
|
|
411
|
+
display: 'inline-block',
|
|
412
|
+
WebkitTouchCallout: isIOS ? 'none' : undefined,
|
|
413
|
+
WebkitUserSelect: isIOS ? 'none' : undefined,
|
|
414
|
+
}, children: children })), menuState.visible && createPortal(_jsxs(_Fragment, { children: [_jsxs(S.MenuContainer, { ref: menuRef, "$x": menuState.position.x, "$y": menuState.position.y, "$openLeft": openLeft, "$openUp": openUp, "$isPositioned": isCalculated, "$externalControl": !!externalControl, children: [isMobile && menuState.parentNames.length > 0 && (_jsxs(S.MobileMenuHeader, { children: [_jsx(S.BackButton, { onClick: handleBack, "aria-label": "Go back", children: _jsx(IconArrowLeft, {}) }), _jsx(S.HeaderTitle, { children: currentParentName })] })), renderMenuItems(currentMenu, 0)] }), !isMobile && hoveredSubmenus.map((submenu, idx) => (_jsx(S.Submenu, { "$parentRect": submenu.parentRect, "$openUp": submenu.openUp, "data-submenu": "true", onMouseEnter: handleSubmenuMouseEnter, onMouseLeave: () => handleMouseLeave(submenu.depth), children: renderMenuItems(submenu.items, submenu.depth) }, `submenu-${submenu.depth}-${idx}`)))] }), document.body)] }));
|
|
415
|
+
};
|
|
416
|
+
export default TMContextMenu;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const useIsIOS: () => boolean;
|
|
2
|
+
export declare const useIsMobile: () => boolean;
|
|
3
|
+
export declare const useClickOutside: (callback: () => void) => import("react").RefObject<HTMLDivElement>;
|
|
4
|
+
interface Position {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const useMenuPosition: (menuRef: React.RefObject<HTMLDivElement | null>, position: Position) => {
|
|
9
|
+
isCalculated: boolean;
|
|
10
|
+
openLeft: boolean;
|
|
11
|
+
openUp: boolean;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useState, useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
export const useIsIOS = () => {
|
|
3
|
+
const [isIOS, setIsIOS] = useState(false);
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
|
6
|
+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
7
|
+
setIsIOS(iOS);
|
|
8
|
+
}, []);
|
|
9
|
+
return isIOS;
|
|
10
|
+
};
|
|
11
|
+
export const useIsMobile = () => {
|
|
12
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const checkMobile = () => {
|
|
15
|
+
const mobile = globalThis.innerWidth <= 768 || 'ontouchstart' in globalThis;
|
|
16
|
+
setIsMobile(mobile);
|
|
17
|
+
};
|
|
18
|
+
checkMobile();
|
|
19
|
+
window.addEventListener('resize', checkMobile);
|
|
20
|
+
return () => window.removeEventListener('resize', checkMobile);
|
|
21
|
+
}, []);
|
|
22
|
+
return isMobile;
|
|
23
|
+
};
|
|
24
|
+
export const useClickOutside = (callback) => {
|
|
25
|
+
const ref = useRef(null);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleClick = (event) => {
|
|
28
|
+
if (ref.current && !ref.current.contains(event.target)) {
|
|
29
|
+
callback();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
document.addEventListener('mousedown', handleClick);
|
|
33
|
+
document.addEventListener('touchstart', handleClick);
|
|
34
|
+
return () => {
|
|
35
|
+
document.removeEventListener('mousedown', handleClick);
|
|
36
|
+
document.removeEventListener('touchstart', handleClick);
|
|
37
|
+
};
|
|
38
|
+
}, [callback]);
|
|
39
|
+
return ref;
|
|
40
|
+
};
|
|
41
|
+
export const useMenuPosition = (menuRef, position) => {
|
|
42
|
+
const [adjustedPosition, setAdjustedPosition] = useState({ openLeft: false, openUp: false });
|
|
43
|
+
const [isCalculated, setIsCalculated] = useState(false);
|
|
44
|
+
useLayoutEffect(() => {
|
|
45
|
+
if (!menuRef.current) {
|
|
46
|
+
setIsCalculated(false);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const menuRect = menuRef.current.getBoundingClientRect();
|
|
50
|
+
const viewportWidth = window.innerWidth;
|
|
51
|
+
const viewportHeight = window.innerHeight;
|
|
52
|
+
const spaceRight = viewportWidth - position.x;
|
|
53
|
+
const spaceBottom = viewportHeight - position.y;
|
|
54
|
+
setAdjustedPosition({
|
|
55
|
+
openLeft: spaceRight < menuRect.width + 20,
|
|
56
|
+
openUp: spaceBottom < menuRect.height + 20,
|
|
57
|
+
});
|
|
58
|
+
setIsCalculated(true);
|
|
59
|
+
}, [position, menuRef]);
|
|
60
|
+
return { ...adjustedPosition, isCalculated };
|
|
61
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as ContextMenu } from './TMContextMenu';
|
|
2
|
+
export type { TMContextMenuItemProps, TMContextMenuProps } from './types';
|
|
3
|
+
export { useLongPress, triggerContextMenuEvent } from './useLongPress';
|
|
4
|
+
export type { UseLongPressOptions } from './useLongPress';
|
|
5
|
+
export { useIsIOS } from './hooks';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const MenuContainer: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
2
|
+
$x: number;
|
|
3
|
+
$y: number;
|
|
4
|
+
$openLeft: boolean;
|
|
5
|
+
$openUp: boolean;
|
|
6
|
+
$isPositioned: boolean;
|
|
7
|
+
$externalControl?: boolean;
|
|
8
|
+
}>> & string;
|
|
9
|
+
export declare const MenuItem: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
10
|
+
$disabled?: boolean;
|
|
11
|
+
$hasSubmenu?: boolean;
|
|
12
|
+
$beginGroup?: boolean;
|
|
13
|
+
}>> & string;
|
|
14
|
+
export declare const MenuItemContent: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
15
|
+
export declare const IconWrapper: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never>> & string;
|
|
16
|
+
export declare const MenuItemName: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never>> & string;
|
|
17
|
+
export declare const RightIconButton: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("styled-components").FastOmit<import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, Omit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & {
|
|
18
|
+
ref?: ((instance: HTMLButtonElement | null) => void | import("react").DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof import("react").DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) | import("react").RefObject<HTMLButtonElement> | null | undefined;
|
|
19
|
+
}>, never>, never>> & string;
|
|
20
|
+
export declare const SubmenuIndicator: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, {
|
|
21
|
+
$isMobile?: boolean;
|
|
22
|
+
}>> & string;
|
|
23
|
+
export declare const Submenu: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
24
|
+
$parentRect: DOMRect;
|
|
25
|
+
$openUp?: boolean;
|
|
26
|
+
}>> & string;
|
|
27
|
+
export declare const MobileMenuHeader: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
28
|
+
export declare const BackButton: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>> & string;
|
|
29
|
+
export declare const HeaderTitle: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>, never>> & string;
|
|
30
|
+
export declare const MenuDivider: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
31
|
+
export declare const Overlay: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|