@topconsultnpm/sdkui-react 6.19.0 → 6.20.0-dev1.100

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.
Files changed (166) hide show
  1. package/lib/assets/Toppy-help-center.png +0 -0
  2. package/lib/components/NewComponents/ContextMenu/TMContextMenu.d.ts +4 -0
  3. package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +441 -0
  4. package/lib/components/NewComponents/ContextMenu/hooks.d.ts +18 -0
  5. package/lib/components/NewComponents/ContextMenu/hooks.js +120 -0
  6. package/lib/components/NewComponents/ContextMenu/index.d.ts +5 -0
  7. package/lib/components/NewComponents/ContextMenu/index.js +3 -0
  8. package/lib/components/NewComponents/ContextMenu/styles.d.ts +35 -0
  9. package/lib/components/NewComponents/ContextMenu/styles.js +428 -0
  10. package/lib/components/NewComponents/ContextMenu/types.d.ts +39 -0
  11. package/lib/components/NewComponents/ContextMenu/types.js +1 -0
  12. package/lib/components/NewComponents/ContextMenu/useLongPress.d.ts +21 -0
  13. package/lib/components/NewComponents/ContextMenu/useLongPress.js +112 -0
  14. package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.d.ts +4 -0
  15. package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +821 -0
  16. package/lib/components/NewComponents/FloatingMenuBar/index.d.ts +2 -0
  17. package/lib/components/NewComponents/FloatingMenuBar/index.js +2 -0
  18. package/lib/components/NewComponents/FloatingMenuBar/styles.d.ts +54 -0
  19. package/lib/components/NewComponents/FloatingMenuBar/styles.js +419 -0
  20. package/lib/components/NewComponents/FloatingMenuBar/types.d.ts +36 -0
  21. package/lib/components/NewComponents/FloatingMenuBar/types.js +1 -0
  22. package/lib/components/base/TMAccordion.js +2 -2
  23. package/lib/components/base/TMAccordionNew.js +35 -14
  24. package/lib/components/base/TMCustomButton.js +61 -17
  25. package/lib/components/base/TMDataGrid.d.ts +7 -4
  26. package/lib/components/base/TMDataGrid.js +153 -11
  27. package/lib/components/base/TMDropDownMenu.js +19 -18
  28. package/lib/components/base/TMFileManager.d.ts +4 -3
  29. package/lib/components/base/TMFileManager.js +32 -24
  30. package/lib/components/base/TMFileManagerDataGridView.d.ts +3 -2
  31. package/lib/components/base/TMFileManagerDataGridView.js +1 -11
  32. package/lib/components/base/TMFileManagerThumbnailItems.d.ts +7 -1
  33. package/lib/components/base/TMFileManagerThumbnailItems.js +5 -2
  34. package/lib/components/base/TMFileManagerThumbnailsView.d.ts +17 -4
  35. package/lib/components/base/TMFileManagerThumbnailsView.js +18 -6
  36. package/lib/components/base/TMFileManagerUtils.d.ts +0 -12
  37. package/lib/components/base/TMListView.js +33 -15
  38. package/lib/components/base/TMPanel.d.ts +1 -1
  39. package/lib/components/base/TMPanel.js +1 -1
  40. package/lib/components/choosers/TMDistinctValues.js +1 -1
  41. package/lib/components/choosers/TMInvoiceRetrieveFormats.js +1 -1
  42. package/lib/components/choosers/TMMetadataChooser.js +8 -1
  43. package/lib/components/choosers/TMOrderRetrieveFormats.js +1 -1
  44. package/lib/components/choosers/TMUserChooser.d.ts +0 -5
  45. package/lib/components/choosers/TMUserChooser.js +25 -45
  46. package/lib/components/editors/TMDateBox.js +18 -9
  47. package/lib/components/editors/TMHtmlEditor.js +1 -1
  48. package/lib/components/editors/TMMetadataValues.js +37 -5
  49. package/lib/components/editors/TMTextArea.js +18 -30
  50. package/lib/components/editors/TMTextBox.js +6 -3
  51. package/lib/components/features/archive/TMArchive.js +2 -2
  52. package/lib/components/features/assistant/TMToppyDraggableHelpCenter.d.ts +15 -0
  53. package/lib/components/features/assistant/TMToppyDraggableHelpCenter.js +460 -0
  54. package/lib/components/features/assistant/TMToppySpeechBubble.d.ts +11 -0
  55. package/lib/components/features/assistant/TMToppySpeechBubble.js +126 -0
  56. package/lib/components/features/documents/TMDcmtBlog.d.ts +1 -7
  57. package/lib/components/features/documents/TMDcmtBlog.js +29 -2
  58. package/lib/components/features/documents/TMDcmtForm.d.ts +14 -2
  59. package/lib/components/features/documents/TMDcmtForm.js +466 -232
  60. package/lib/components/features/documents/TMDcmtPreview.js +44 -78
  61. package/lib/components/features/documents/TMDcmtTasks.js +9 -9
  62. package/lib/components/features/documents/TMMasterDetailDcmts.js +38 -53
  63. package/lib/components/features/documents/TMRelationViewer.d.ts +1 -1
  64. package/lib/components/features/documents/TMRelationViewer.js +2 -2
  65. package/lib/components/features/search/TMDcmtCheckoutInfoForm.d.ts +8 -0
  66. package/lib/components/features/search/{TMSearchResultCheckoutInfoForm.js → TMDcmtCheckoutInfoForm.js} +6 -11
  67. package/lib/components/features/search/TMSavedQuerySelector.js +72 -67
  68. package/lib/components/features/search/TMSearch.d.ts +3 -0
  69. package/lib/components/features/search/TMSearch.js +45 -10
  70. package/lib/components/features/search/TMSearchQueryPanel.d.ts +1 -0
  71. package/lib/components/features/search/TMSearchQueryPanel.js +19 -18
  72. package/lib/components/features/search/TMSearchResult.d.ts +3 -0
  73. package/lib/components/features/search/TMSearchResult.js +229 -160
  74. package/lib/components/features/search/TMSearchResultsMenuItems.d.ts +3 -3
  75. package/lib/components/features/search/TMSearchResultsMenuItems.js +216 -180
  76. package/lib/components/features/search/TMSignSettingsForm.js +1 -1
  77. package/lib/components/features/search/TMSignatureInfoContent.d.ts +6 -0
  78. package/lib/components/features/search/TMSignatureInfoContent.js +140 -0
  79. package/lib/components/features/search/{TMViewHistoryDcmtForm.d.ts → TMViewHistoryDcmt.d.ts} +3 -3
  80. package/lib/components/features/search/{TMViewHistoryDcmtForm.js → TMViewHistoryDcmt.js} +83 -13
  81. package/lib/components/features/tasks/TMTaskForm.js +20 -1
  82. package/lib/components/features/tasks/TMTasksAgenda.d.ts +3 -1
  83. package/lib/components/features/tasks/TMTasksAgenda.js +48 -9
  84. package/lib/components/features/tasks/TMTasksCalendar.d.ts +2 -0
  85. package/lib/components/features/tasks/TMTasksCalendar.js +19 -7
  86. package/lib/components/features/tasks/TMTasksUtils.d.ts +2 -2
  87. package/lib/components/features/tasks/TMTasksUtils.js +43 -36
  88. package/lib/components/features/tasks/TMTasksView.js +28 -19
  89. package/lib/components/features/workflow/TMWorkflowPopup.d.ts +33 -2
  90. package/lib/components/features/workflow/TMWorkflowPopup.js +139 -34
  91. package/lib/components/features/workflow/diagram/DiagramItemComponent.d.ts +2 -0
  92. package/lib/components/features/workflow/diagram/DiagramItemComponent.js +12 -7
  93. package/lib/components/features/workflow/diagram/RecipientList.js +3 -2
  94. package/lib/components/features/workflow/diagram/WFDiagram.d.ts +4 -0
  95. package/lib/components/features/workflow/diagram/WFDiagram.js +164 -13
  96. package/lib/components/forms/Login/LoginValidatorService.d.ts +2 -0
  97. package/lib/components/forms/Login/LoginValidatorService.js +7 -2
  98. package/lib/components/forms/Login/TMLoginForm.js +34 -6
  99. package/lib/components/forms/TMChooserForm.js +1 -1
  100. package/lib/components/grids/TMBlogsPost.js +56 -31
  101. package/lib/components/grids/TMRecentsManager.js +20 -10
  102. package/lib/components/index.d.ts +5 -3
  103. package/lib/components/index.js +5 -3
  104. package/lib/components/query/TMQueryEditor.d.ts +2 -1
  105. package/lib/components/query/TMQueryEditor.js +92 -92
  106. package/lib/components/settings/SettingsAppearance.d.ts +2 -1
  107. package/lib/components/settings/SettingsAppearance.js +99 -30
  108. package/lib/components/viewers/TMDataListItemViewer.d.ts +2 -1
  109. package/lib/components/viewers/TMDataListItemViewer.js +35 -71
  110. package/lib/components/viewers/TMDataUserIdItemViewer.d.ts +8 -0
  111. package/lib/components/viewers/TMDataUserIdItemViewer.js +39 -0
  112. package/lib/css/tm-sdkui.css +1 -1
  113. package/lib/helper/SDKUI_Globals.d.ts +22 -14
  114. package/lib/helper/SDKUI_Globals.js +6 -0
  115. package/lib/helper/SDKUI_Localizator.d.ts +24 -1
  116. package/lib/helper/SDKUI_Localizator.js +255 -1
  117. package/lib/helper/TMCommandsContextMenu.d.ts +4 -2
  118. package/lib/helper/TMCommandsContextMenu.js +15 -4
  119. package/lib/helper/TMIcons.d.ts +4 -0
  120. package/lib/helper/TMIcons.js +13 -3
  121. package/lib/helper/TMPdfViewer.d.ts +8 -0
  122. package/lib/helper/TMPdfViewer.js +373 -0
  123. package/lib/helper/TMUtils.d.ts +3 -1
  124. package/lib/helper/TMUtils.js +51 -0
  125. package/lib/helper/checkinCheckoutManager.d.ts +85 -0
  126. package/lib/helper/checkinCheckoutManager.js +348 -0
  127. package/lib/helper/devextremeCustomMessages.d.ts +30 -0
  128. package/lib/helper/devextremeCustomMessages.js +30 -0
  129. package/lib/helper/helpers.d.ts +3 -1
  130. package/lib/helper/helpers.js +31 -4
  131. package/lib/helper/index.d.ts +3 -0
  132. package/lib/helper/index.js +3 -0
  133. package/lib/helper/queryHelper.d.ts +1 -1
  134. package/lib/helper/queryHelper.js +46 -4
  135. package/lib/helper/workItemsHelper.d.ts +6 -0
  136. package/lib/helper/workItemsHelper.js +230 -0
  137. package/lib/hooks/useCheckInOutOperations.d.ts +28 -0
  138. package/lib/hooks/useCheckInOutOperations.js +223 -0
  139. package/lib/hooks/useDataListItem.d.ts +12 -0
  140. package/lib/hooks/useDataListItem.js +132 -0
  141. package/lib/hooks/useDataUserIdItem.d.ts +10 -0
  142. package/lib/hooks/useDataUserIdItem.js +96 -0
  143. package/lib/hooks/useSettingsFeedback.d.ts +11 -0
  144. package/lib/hooks/useSettingsFeedback.js +38 -0
  145. package/lib/hooks/useWorkflowApprove.d.ts +4 -0
  146. package/lib/hooks/useWorkflowApprove.js +14 -1
  147. package/lib/index.d.ts +1 -0
  148. package/lib/index.js +3 -2
  149. package/lib/services/platform_services.d.ts +3 -3
  150. package/lib/ts/types.d.ts +61 -1
  151. package/lib/utils/theme.d.ts +1 -1
  152. package/lib/utils/theme.js +1 -1
  153. package/package.json +55 -52
  154. package/lib/components/base/TMContextMenu.d.ts +0 -25
  155. package/lib/components/base/TMContextMenu.js +0 -109
  156. package/lib/components/base/TMContextMenuOLD.d.ts +0 -26
  157. package/lib/components/base/TMContextMenuOLD.js +0 -56
  158. package/lib/components/base/TMFloatingToolbar.d.ts +0 -9
  159. package/lib/components/base/TMFloatingToolbar.js +0 -101
  160. package/lib/components/features/assistant/ToppyDraggableHelpCenter.d.ts +0 -30
  161. package/lib/components/features/assistant/ToppyDraggableHelpCenter.js +0 -482
  162. package/lib/components/features/assistant/ToppySpeechBubble.d.ts +0 -9
  163. package/lib/components/features/assistant/ToppySpeechBubble.js +0 -117
  164. package/lib/components/features/search/TMSearchResultCheckoutInfoForm.d.ts +0 -8
  165. package/lib/helper/cicoHelper.d.ts +0 -31
  166. package/lib/helper/cicoHelper.js +0 -155
Binary file
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import type { TMContextMenuProps } from './types';
3
+ declare const TMContextMenu: React.FC<TMContextMenuProps>;
4
+ export default TMContextMenu;
@@ -0,0 +1,441 @@
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
+ // Get current menu to pass items count to positioning hook
23
+ const currentMenu = menuState.submenuStack.at(-1) || items;
24
+ const { openLeft, openUp, isCalculated, needsScroll, maxHeight } = useMenuPosition(menuRef, menuState.position, currentMenu.length);
25
+ const handleClose = () => {
26
+ if (externalControl) {
27
+ externalControl.onClose();
28
+ }
29
+ else {
30
+ setMenuState(prev => ({
31
+ ...prev,
32
+ visible: false,
33
+ submenuStack: [items],
34
+ parentNames: [],
35
+ }));
36
+ }
37
+ setHoveredSubmenus([]);
38
+ };
39
+ // Sync with external control when provided
40
+ useEffect(() => {
41
+ if (externalControl) {
42
+ setMenuState(prev => ({
43
+ ...prev,
44
+ visible: externalControl.visible,
45
+ position: externalControl.position,
46
+ submenuStack: [items],
47
+ parentNames: [],
48
+ }));
49
+ }
50
+ }, [externalControl, items]);
51
+ // iOS long-press support: attach touch listeners to target elements
52
+ // On long-press, dispatch synthetic contextmenu event to trigger existing handlers
53
+ useEffect(() => {
54
+ if (!target || !isIOS)
55
+ return;
56
+ const elements = document.querySelectorAll(target);
57
+ if (elements.length === 0)
58
+ return;
59
+ const touchStateMap = new WeakMap();
60
+ const handleTouchStart = (e) => {
61
+ const touchEvent = e;
62
+ const element = e.currentTarget;
63
+ const touch = touchEvent.touches[0];
64
+ // Prevent text selection during long press
65
+ e.preventDefault();
66
+ let state = touchStateMap.get(element);
67
+ if (!state) {
68
+ state = { timeout: null, startX: 0, startY: 0, longPressTriggered: false };
69
+ touchStateMap.set(element, state);
70
+ }
71
+ state.startX = touch.clientX;
72
+ state.startY = touch.clientY;
73
+ state.longPressTriggered = false;
74
+ if (state.timeout)
75
+ clearTimeout(state.timeout);
76
+ state.timeout = setTimeout(() => {
77
+ if (state)
78
+ state.longPressTriggered = true;
79
+ // Haptic feedback
80
+ if ('vibrate' in navigator)
81
+ navigator.vibrate(50);
82
+ const syntheticEvent = new MouseEvent('contextmenu', {
83
+ bubbles: true,
84
+ cancelable: true,
85
+ clientX: touch.clientX,
86
+ clientY: touch.clientY,
87
+ });
88
+ element.dispatchEvent(syntheticEvent);
89
+ if (state)
90
+ state.timeout = null;
91
+ }, 500);
92
+ };
93
+ const handleTouchMove = (e) => {
94
+ const touchEvent = e;
95
+ const element = e.currentTarget;
96
+ const state = touchStateMap.get(element);
97
+ if (!state?.timeout)
98
+ return;
99
+ const touch = touchEvent.touches[0];
100
+ const dx = Math.abs(touch.clientX - state.startX);
101
+ const dy = Math.abs(touch.clientY - state.startY);
102
+ if (dx > 10 || dy > 10) {
103
+ clearTimeout(state.timeout);
104
+ state.timeout = null;
105
+ }
106
+ };
107
+ const handleTouchEnd = (e) => {
108
+ const element = e.currentTarget;
109
+ const state = touchStateMap.get(element);
110
+ if (state?.timeout) {
111
+ clearTimeout(state.timeout);
112
+ state.timeout = null;
113
+ }
114
+ };
115
+ // Prevent click event after long-press was triggered
116
+ const handleClick = (e) => {
117
+ const element = e.currentTarget;
118
+ const state = touchStateMap.get(element);
119
+ if (state?.longPressTriggered) {
120
+ e.preventDefault();
121
+ e.stopPropagation();
122
+ e.stopImmediatePropagation();
123
+ state.longPressTriggered = false;
124
+ }
125
+ };
126
+ // Prevent default iOS context menu
127
+ const handleContextMenu = (e) => {
128
+ e.preventDefault();
129
+ e.stopPropagation();
130
+ return false;
131
+ };
132
+ // Attach listeners to all matching elements
133
+ elements.forEach(element => {
134
+ const el = element;
135
+ // Prevent iOS native callout and text selection
136
+ const style = el.style;
137
+ style.webkitTouchCallout = 'none';
138
+ style.webkitUserSelect = 'none';
139
+ style.userSelect = 'none';
140
+ el.addEventListener('touchstart', handleTouchStart, { passive: false });
141
+ el.addEventListener('touchmove', handleTouchMove, { passive: false });
142
+ el.addEventListener('touchend', handleTouchEnd);
143
+ el.addEventListener('touchcancel', handleTouchEnd);
144
+ el.addEventListener('contextmenu', handleContextMenu);
145
+ el.addEventListener('click', handleClick, { capture: true });
146
+ });
147
+ return () => {
148
+ elements.forEach(element => {
149
+ const el = element;
150
+ const style = el.style;
151
+ style.webkitTouchCallout = '';
152
+ style.webkitUserSelect = '';
153
+ style.userSelect = '';
154
+ el.removeEventListener('touchstart', handleTouchStart);
155
+ el.removeEventListener('touchmove', handleTouchMove);
156
+ el.removeEventListener('touchend', handleTouchEnd);
157
+ el.removeEventListener('touchcancel', handleTouchEnd);
158
+ el.removeEventListener('contextmenu', handleContextMenu);
159
+ el.removeEventListener('click', handleClick, { capture: true });
160
+ });
161
+ };
162
+ }, [target, isIOS]);
163
+ // Update items when they change while menu is visible (for keepOpenOnClick behavior)
164
+ useEffect(() => {
165
+ if (!keepOpenOnClick)
166
+ return;
167
+ if (!externalControl && menuState.visible) {
168
+ setMenuState(prev => ({
169
+ ...prev,
170
+ submenuStack: [items],
171
+ }));
172
+ // Update hoveredSubmenus with fresh items while keeping them open
173
+ setHoveredSubmenus(prev => {
174
+ if (prev.length === 0)
175
+ return prev;
176
+ // Rebuild hoveredSubmenus with updated items from the new items structure
177
+ return prev.map(submenu => {
178
+ // Find the matching submenu in the new items structure
179
+ const findSubmenuInItems = (searchItems) => {
180
+ for (const item of searchItems) {
181
+ if (item.submenu && item.submenu.length > 0) {
182
+ // Check if this submenu matches (compare first item name as identifier)
183
+ if (submenu.items.length > 0 && item.submenu.length > 0 &&
184
+ item.submenu[0].name === submenu.items[0].name) {
185
+ return item.submenu;
186
+ }
187
+ // Recursively search in nested submenus
188
+ const found = findSubmenuInItems(item.submenu);
189
+ if (found)
190
+ return found;
191
+ }
192
+ }
193
+ return null;
194
+ };
195
+ const updatedItems = findSubmenuInItems(items);
196
+ return {
197
+ ...submenu,
198
+ items: updatedItems || submenu.items, // Use updated items if found, otherwise keep old
199
+ };
200
+ });
201
+ });
202
+ }
203
+ }, [items, menuState.visible, externalControl, keepOpenOnClick]);
204
+ // Track when the menu was opened to prevent immediate close on iOS
205
+ const menuOpenedAtRef = useRef(0);
206
+ useEffect(() => {
207
+ if (menuState.visible) {
208
+ menuOpenedAtRef.current = Date.now();
209
+ }
210
+ }, [menuState.visible]);
211
+ useEffect(() => {
212
+ if (!menuState.visible)
213
+ return;
214
+ const handleClickOutside = (event) => {
215
+ // On iOS, prevent closing immediately after opening (within 300ms)
216
+ // This handles the case where touchend from long-press triggers touchstart listener
217
+ if (Date.now() - menuOpenedAtRef.current < 300) {
218
+ return;
219
+ }
220
+ const target = event.target;
221
+ // Check if click is inside main menu
222
+ if (menuRef.current?.contains(target)) {
223
+ return;
224
+ }
225
+ // Check if click is inside any submenu
226
+ const submenus = document.querySelectorAll('[data-submenu="true"]');
227
+ for (const submenu of Array.from(submenus)) {
228
+ if (submenu.contains(target)) {
229
+ return;
230
+ }
231
+ }
232
+ // Click is outside all menus, close them
233
+ handleClose();
234
+ };
235
+ document.addEventListener('mousedown', handleClickOutside);
236
+ document.addEventListener('touchstart', handleClickOutside);
237
+ document.addEventListener('contextmenu', handleClickOutside); // Close on right-click outside
238
+ return () => {
239
+ document.removeEventListener('mousedown', handleClickOutside);
240
+ document.removeEventListener('touchstart', handleClickOutside);
241
+ document.removeEventListener('contextmenu', handleClickOutside);
242
+ };
243
+ }, [menuState.visible]);
244
+ const handleContextMenu = (e) => {
245
+ if (trigger === 'right') {
246
+ e.preventDefault();
247
+ e.stopPropagation(); // Prevent event from bubbling to close other menus prematurely
248
+ // Small delay to ensure other menus receive the contextmenu event and close first
249
+ setTimeout(() => {
250
+ setMenuState({
251
+ visible: true,
252
+ position: { x: e.clientX, y: e.clientY },
253
+ submenuStack: [items],
254
+ parentNames: [],
255
+ });
256
+ }, 0);
257
+ }
258
+ };
259
+ const handleClick = (e) => {
260
+ if (trigger === 'left') {
261
+ e.preventDefault();
262
+ setMenuState({
263
+ visible: true,
264
+ position: { x: e.clientX, y: e.clientY },
265
+ submenuStack: [items],
266
+ parentNames: [],
267
+ });
268
+ }
269
+ };
270
+ // iOS-specific touch handlers for long press
271
+ const handleTouchStart = (e) => {
272
+ if (!isIOS || trigger !== 'right')
273
+ return;
274
+ const touch = e.touches[0];
275
+ touchStartPos.current = { x: touch.clientX, y: touch.clientY };
276
+ if (longPressTimeoutRef.current) {
277
+ clearTimeout(longPressTimeoutRef.current);
278
+ }
279
+ longPressTimeoutRef.current = setTimeout(() => {
280
+ if (touchStartPos.current) {
281
+ if ('vibrate' in navigator) {
282
+ navigator.vibrate(50);
283
+ }
284
+ setMenuState({
285
+ visible: true,
286
+ position: { x: touchStartPos.current.x, y: touchStartPos.current.y },
287
+ submenuStack: [items],
288
+ parentNames: [],
289
+ });
290
+ }
291
+ }, 500);
292
+ };
293
+ const handleTouchMove = (e) => {
294
+ if (!isIOS || trigger !== 'right' || !touchStartPos.current)
295
+ return;
296
+ const touch = e.touches[0];
297
+ const moveThreshold = 10; // pixels
298
+ // If finger moved too much, cancel long press
299
+ const deltaX = Math.abs(touch.clientX - touchStartPos.current.x);
300
+ const deltaY = Math.abs(touch.clientY - touchStartPos.current.y);
301
+ if (deltaX > moveThreshold || deltaY > moveThreshold) {
302
+ if (longPressTimeoutRef.current) {
303
+ clearTimeout(longPressTimeoutRef.current);
304
+ longPressTimeoutRef.current = null;
305
+ }
306
+ touchStartPos.current = null;
307
+ }
308
+ };
309
+ const handleTouchEnd = () => {
310
+ if (!isIOS || trigger !== 'right')
311
+ return;
312
+ if (longPressTimeoutRef.current) {
313
+ clearTimeout(longPressTimeoutRef.current);
314
+ longPressTimeoutRef.current = null;
315
+ }
316
+ touchStartPos.current = null;
317
+ };
318
+ const handleItemClick = (item) => {
319
+ if (item.disabled)
320
+ return;
321
+ if (item.onClick) {
322
+ item.onClick();
323
+ }
324
+ if (item.submenu && item.submenu.length > 0) {
325
+ if (isMobile) {
326
+ setMenuState(prev => ({
327
+ ...prev,
328
+ submenuStack: [...prev.submenuStack, item.submenu],
329
+ parentNames: [...prev.parentNames, item.name],
330
+ }));
331
+ }
332
+ }
333
+ else {
334
+ if (!keepOpenOnClick) {
335
+ handleClose();
336
+ }
337
+ }
338
+ };
339
+ const handleBack = () => {
340
+ setMenuState(prev => ({
341
+ ...prev,
342
+ submenuStack: prev.submenuStack.slice(0, -1),
343
+ parentNames: prev.parentNames.slice(0, -1),
344
+ }));
345
+ };
346
+ const handleMouseEnter = (item, event, depth = 0) => {
347
+ if (isMobile || !item.submenu || item.submenu.length === 0 || item.disabled)
348
+ return;
349
+ if (submenuTimeoutRef.current) {
350
+ clearTimeout(submenuTimeoutRef.current);
351
+ submenuTimeoutRef.current = null;
352
+ }
353
+ const rect = event.currentTarget.getBoundingClientRect();
354
+ // Calculate submenu height dynamically
355
+ const estimatedSubmenuHeight = (item.submenu.length * 35) + 8;
356
+ const spaceBelow = window.innerHeight - rect.top;
357
+ const spaceAbove = rect.bottom;
358
+ const padding = 8;
359
+ // Determine if submenu should open upward
360
+ const shouldOpenUp = spaceBelow < estimatedSubmenuHeight && spaceAbove > spaceBelow;
361
+ // Calculate if submenu needs scroll and max-height
362
+ const availableSpace = shouldOpenUp ? spaceAbove : spaceBelow;
363
+ const needsScroll = estimatedSubmenuHeight > availableSpace - padding;
364
+ const maxHeight = needsScroll ? availableSpace - padding : undefined;
365
+ // Remove all submenus at this depth and deeper
366
+ setHoveredSubmenus(prev => {
367
+ const filtered = prev.filter(sub => sub.depth < depth);
368
+ if (!item.submenu)
369
+ return filtered;
370
+ return [
371
+ ...filtered,
372
+ {
373
+ items: item.submenu,
374
+ parentRect: rect,
375
+ depth: depth,
376
+ openUp: shouldOpenUp,
377
+ needsScroll,
378
+ maxHeight,
379
+ }
380
+ ];
381
+ });
382
+ };
383
+ const handleMouseLeave = (depth = 0) => {
384
+ if (isMobile)
385
+ return;
386
+ if (submenuTimeoutRef.current) {
387
+ clearTimeout(submenuTimeoutRef.current);
388
+ }
389
+ const targetDepth = depth;
390
+ submenuTimeoutRef.current = setTimeout(() => {
391
+ setHoveredSubmenus(prev => prev.filter(sub => sub.depth < targetDepth));
392
+ }, 300);
393
+ };
394
+ const handleSubmenuMouseEnter = () => {
395
+ if (submenuTimeoutRef.current) {
396
+ clearTimeout(submenuTimeoutRef.current);
397
+ submenuTimeoutRef.current = null;
398
+ }
399
+ };
400
+ useEffect(() => {
401
+ return () => {
402
+ if (submenuTimeoutRef.current) {
403
+ clearTimeout(submenuTimeoutRef.current);
404
+ }
405
+ if (longPressTimeoutRef.current) {
406
+ clearTimeout(longPressTimeoutRef.current);
407
+ }
408
+ };
409
+ }, []);
410
+ const renderMenuItems = (menuItems, depth = 0) => {
411
+ return menuItems
412
+ .filter(item => item.visible !== false)
413
+ .map((item, idx) => {
414
+ const itemKey = `${item.name}-${idx}`.replaceAll(/\s+/g, '-');
415
+ const handleClick = (e) => {
416
+ if (item.disabled)
417
+ return;
418
+ e.stopPropagation();
419
+ handleItemClick(item);
420
+ };
421
+ const handleRightIconClick = (e) => {
422
+ e.stopPropagation();
423
+ // if (item.disabled) return;
424
+ item.onRightIconClick?.();
425
+ handleClose();
426
+ };
427
+ return (_jsxs(S.MenuItem, { "$disabled": item.disabled, "$hasSubmenu": !!item.submenu && item.submenu.length > 0, "$beginGroup": item.beginGroup, "data-disabled": item.disabled ? "true" : undefined, 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));
428
+ });
429
+ };
430
+ const currentParentName = menuState.parentNames.at(-1) || '';
431
+ return (_jsxs(_Fragment, { children: [!externalControl && children && (_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onTouchCancel: handleTouchEnd, onKeyDown: (e) => {
432
+ if (e.key === 'Enter' || e.key === ' ') {
433
+ handleClick(e);
434
+ }
435
+ }, role: "button", tabIndex: 0, style: {
436
+ display: 'inline-block',
437
+ WebkitTouchCallout: isIOS ? 'none' : undefined,
438
+ WebkitUserSelect: isIOS ? 'none' : undefined,
439
+ }, 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, "$needsScroll": needsScroll, "$maxHeight": maxHeight, 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)] }));
440
+ };
441
+ export default TMContextMenu;
@@ -0,0 +1,18 @@
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, itemsCount?: number) => {
9
+ isCalculated: boolean;
10
+ openLeft: boolean;
11
+ openUp: boolean;
12
+ needsScroll: boolean;
13
+ maxHeight: number | undefined;
14
+ };
15
+ export declare const getContextMenuTarget: <T extends {
16
+ id: string;
17
+ }>(event: React.MouseEvent | undefined, focusedItem: T | undefined, selectedItem: T | undefined, dataSource: T[], idPrefix: string, isMobile: boolean) => T | undefined;
18
+ export {};
@@ -0,0 +1,120 @@
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, itemsCount) => {
42
+ const [adjustedPosition, setAdjustedPosition] = useState({ openLeft: false, openUp: false, needsScroll: false, maxHeight: undefined });
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 isMobile = viewportWidth <= 768;
53
+ const spaceRight = viewportWidth - position.x;
54
+ const spaceBottom = viewportHeight - position.y;
55
+ const spaceAbove = position.y;
56
+ const padding = 8; // Minimal padding from viewport edges - be more aggressive about using available space
57
+ // Use scrollHeight to get natural content height, not constrained height
58
+ const menuHeight = menuRef.current.scrollHeight;
59
+ // Mobile: Always calculate max-height based on position to prevent overflow
60
+ if (isMobile) {
61
+ const maxHeightFromBottom = spaceBottom - padding;
62
+ const maxHeightFromTop = spaceAbove - padding;
63
+ const mobileMaxHeight = Math.max(maxHeightFromBottom, maxHeightFromTop);
64
+ setAdjustedPosition({
65
+ openLeft: spaceRight < menuRect.width + 20,
66
+ openUp: maxHeightFromTop > maxHeightFromBottom && menuHeight > maxHeightFromBottom,
67
+ needsScroll: menuHeight > mobileMaxHeight,
68
+ maxHeight: mobileMaxHeight,
69
+ });
70
+ setIsCalculated(true);
71
+ return;
72
+ }
73
+ // Desktop: Check if menu is too tall to fit in either direction
74
+ const fitsBelow = menuHeight + padding <= spaceBottom;
75
+ const fitsAbove = menuHeight + padding <= spaceAbove;
76
+ const needsScroll = !fitsBelow && !fitsAbove;
77
+ // Calculate max height when scrolling is needed
78
+ let maxHeight = undefined;
79
+ let shouldOpenUp = false;
80
+ if (needsScroll) {
81
+ // When scrolling is needed, open in the direction with MORE space
82
+ const availableSpace = Math.max(spaceBottom, spaceAbove);
83
+ maxHeight = availableSpace - padding;
84
+ shouldOpenUp = spaceAbove > spaceBottom; // Open upward if more space above
85
+ }
86
+ else {
87
+ // Normal behavior: open up only if it fits above but not below
88
+ shouldOpenUp = !fitsBelow && fitsAbove;
89
+ }
90
+ setAdjustedPosition({
91
+ openLeft: spaceRight < menuRect.width + 20,
92
+ openUp: shouldOpenUp,
93
+ needsScroll,
94
+ maxHeight,
95
+ });
96
+ setIsCalculated(true);
97
+ }, [position, menuRef, itemsCount]); // Added itemsCount to recalculate when menu content changes
98
+ return { ...adjustedPosition, isCalculated };
99
+ };
100
+ export const getContextMenuTarget = (event, focusedItem, selectedItem, dataSource, idPrefix, isMobile) => {
101
+ if (!event)
102
+ return undefined;
103
+ let targetItem = focusedItem ?? selectedItem;
104
+ if (!focusedItem && isMobile) {
105
+ // Find the actual item that was long-pressed by traversing up from the event target
106
+ let element = event.target;
107
+ while (element && element !== event.currentTarget) {
108
+ if (element.id && element.id.startsWith(`${idPrefix}-`)) {
109
+ const itemId = element.id.replace(`${idPrefix}-`, '');
110
+ const foundItem = dataSource.find(item => item.id === itemId);
111
+ if (foundItem) {
112
+ targetItem = foundItem;
113
+ break;
114
+ }
115
+ }
116
+ element = element.parentElement;
117
+ }
118
+ }
119
+ return targetItem;
120
+ };
@@ -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,3 @@
1
+ export { default as ContextMenu } from './TMContextMenu';
2
+ export { useLongPress, triggerContextMenuEvent } from './useLongPress';
3
+ export { useIsIOS } from './hooks';
@@ -0,0 +1,35 @@
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
+ $needsScroll?: boolean;
9
+ $maxHeight?: number;
10
+ }>> & string;
11
+ 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>, {
12
+ $disabled?: boolean;
13
+ $hasSubmenu?: boolean;
14
+ $beginGroup?: boolean;
15
+ }>> & string;
16
+ 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;
17
+ 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;
18
+ 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;
19
+ 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"> & {
20
+ 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;
21
+ }>, never>, never>> & string;
22
+ 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>, {
23
+ $isMobile?: boolean;
24
+ }>> & string;
25
+ 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>, {
26
+ $parentRect: DOMRect;
27
+ $openUp?: boolean;
28
+ $needsScroll?: boolean;
29
+ $maxHeight?: number;
30
+ }>> & string;
31
+ 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;
32
+ 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;
33
+ 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;
34
+ 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;
35
+ 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;