@topconsultnpm/sdkui-react 6.20.0-dev1.5 → 6.20.0-dev1.50
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 +302 -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 +2 -0
- package/lib/components/NewComponents/ContextMenu/index.js +1 -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 +38 -0
- package/lib/components/NewComponents/ContextMenu/types.js +1 -0
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.d.ts +4 -0
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +686 -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 +47 -0
- package/lib/components/NewComponents/FloatingMenuBar/styles.js +346 -0
- package/lib/components/NewComponents/FloatingMenuBar/types.d.ts +28 -0
- package/lib/components/NewComponents/FloatingMenuBar/types.js +1 -0
- package/lib/components/base/TMCustomButton.js +61 -17
- package/lib/components/base/TMDataGrid.d.ts +7 -4
- package/lib/components/base/TMDataGrid.js +112 -11
- package/lib/components/choosers/TMMetadataChooser.js +8 -1
- package/lib/components/editors/TMMetadataValues.js +23 -5
- package/lib/components/features/documents/TMDcmtForm.d.ts +13 -1
- package/lib/components/features/documents/TMDcmtForm.js +385 -193
- package/lib/components/features/documents/TMDcmtPreview.js +37 -66
- package/lib/components/features/documents/TMMasterDetailDcmts.js +1 -1
- 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/TMSearch.js +30 -5
- package/lib/components/features/search/TMSearchQueryPanel.js +13 -12
- package/lib/components/features/search/TMSearchResult.js +58 -208
- 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/TMTasksView.js +2 -2
- 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/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- 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 +2 -1
- package/lib/helper/SDKUI_Localizator.js +11 -1
- package/lib/helper/TMIcons.d.ts +1 -0
- package/lib/helper/TMIcons.js +3 -0
- package/lib/helper/TMPdfViewer.d.ts +8 -0
- package/lib/helper/TMPdfViewer.js +187 -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 +12 -2
- 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/package.json +5 -2
- package/lib/components/features/search/TMSearchResultCheckoutInfoForm.d.ts +0 -8
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
|
+
import { ContextMenu } from '../ContextMenu';
|
|
4
|
+
import ShowAlert from '../../base/TMAlert';
|
|
5
|
+
import TMTooltip from '../../base/TMTooltip';
|
|
6
|
+
import * as S from './styles';
|
|
7
|
+
import { IconAdd, IconApply, IconMenuVertical, IconPin, IconUndo, SDKUI_Globals, SDKUI_Localizator } from '../../../helper';
|
|
8
|
+
const IconDraggableDots = (props) => (_jsx("svg", { fontSize: 18, viewBox: "0 0 24 24", fill: "currentColor", height: "1em", width: "1em", ...props, children: _jsx("path", { d: "M9 3a2 2 0 11-4 0 2 2 0 014 0zm0 9a2 2 0 11-4 0 2 2 0 014 0zm0 9a2 2 0 11-4 0 2 2 0 014 0zm10-18a2 2 0 11-4 0 2 2 0 014 0zm0 9a2 2 0 11-4 0 2 2 0 014 0zm0 9a2 2 0 11-4 0 2 2 0 014 0z" }) }));
|
|
9
|
+
const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 100, }) => {
|
|
10
|
+
const percentToPixels = (percent, containerSize) => {
|
|
11
|
+
return (percent / 100) * containerSize;
|
|
12
|
+
};
|
|
13
|
+
const pixelsToPercent = (pixels, containerSize) => {
|
|
14
|
+
return (pixels / containerSize) * 100;
|
|
15
|
+
};
|
|
16
|
+
const isPixelFormat = (pos) => {
|
|
17
|
+
return pos.x > 100 || pos.y > 100;
|
|
18
|
+
};
|
|
19
|
+
const migrateToPercentage = (pixelPos) => {
|
|
20
|
+
const container = containerRef.current?.getBoundingClientRect();
|
|
21
|
+
const containerWidth = isConstrained && container ? container.width : window.innerWidth;
|
|
22
|
+
const containerHeight = isConstrained && container ? container.height : window.innerHeight;
|
|
23
|
+
return {
|
|
24
|
+
x: pixelsToPercent(pixelPos.x, containerWidth),
|
|
25
|
+
y: pixelsToPercent(pixelPos.y, containerHeight),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const getDefaultConfig = () => ({
|
|
29
|
+
orientation: 'horizontal',
|
|
30
|
+
savedItemIds: [],
|
|
31
|
+
position: defaultPosition,
|
|
32
|
+
});
|
|
33
|
+
const resetFloatingBarSettings = () => {
|
|
34
|
+
// Reset the floatingMenuBar settings in SDKUI_Globals to trigger save to localStorage
|
|
35
|
+
SDKUI_Globals.userSettings.searchSettings.floatingMenuBar = {
|
|
36
|
+
orientation: 'horizontal',
|
|
37
|
+
itemIds: [],
|
|
38
|
+
position: defaultPosition,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const loadConfig = () => {
|
|
42
|
+
try {
|
|
43
|
+
const settings = SDKUI_Globals.userSettings.searchSettings.floatingMenuBar;
|
|
44
|
+
// Validate that settings object exists and has required properties with correct types
|
|
45
|
+
if (!settings || typeof settings !== 'object') {
|
|
46
|
+
console.warn('FloatingMenuBar: Invalid settings object, resetting to defaults');
|
|
47
|
+
resetFloatingBarSettings();
|
|
48
|
+
return getDefaultConfig();
|
|
49
|
+
}
|
|
50
|
+
// Validate position
|
|
51
|
+
const hasValidPosition = settings.position &&
|
|
52
|
+
typeof settings.position.x === 'number' &&
|
|
53
|
+
typeof settings.position.y === 'number' &&
|
|
54
|
+
!Number.isNaN(settings.position.x) &&
|
|
55
|
+
!Number.isNaN(settings.position.y) &&
|
|
56
|
+
Number.isFinite(settings.position.x) &&
|
|
57
|
+
Number.isFinite(settings.position.y);
|
|
58
|
+
if (!hasValidPosition) {
|
|
59
|
+
console.warn('FloatingMenuBar: Invalid position, resetting to defaults');
|
|
60
|
+
resetFloatingBarSettings();
|
|
61
|
+
return getDefaultConfig();
|
|
62
|
+
}
|
|
63
|
+
// Ensure position is within reasonable viewport bounds
|
|
64
|
+
const maxX = globalThis.window?.innerWidth ? globalThis.window.innerWidth - 50 : 1000;
|
|
65
|
+
const maxY = globalThis.window?.innerHeight ? globalThis.window.innerHeight - 50 : 800;
|
|
66
|
+
if (settings.position.x < 0 || settings.position.x > maxX ||
|
|
67
|
+
settings.position.y < 0 || settings.position.y > maxY) {
|
|
68
|
+
console.warn('FloatingMenuBar: Position out of bounds, resetting to defaults');
|
|
69
|
+
resetFloatingBarSettings();
|
|
70
|
+
return getDefaultConfig();
|
|
71
|
+
}
|
|
72
|
+
// Validate orientation
|
|
73
|
+
const validOrientation = (settings.orientation === 'horizontal' || settings.orientation === 'vertical')
|
|
74
|
+
? settings.orientation
|
|
75
|
+
: 'horizontal';
|
|
76
|
+
// Validate itemIds
|
|
77
|
+
const validItemIds = Array.isArray(settings.itemIds) ? settings.itemIds : [];
|
|
78
|
+
if (validItemIds.length > 0) {
|
|
79
|
+
// Check if any ID looks like the old format (contains language-specific text or is too long)
|
|
80
|
+
const hasOldFormatIds = validItemIds.some(id => typeof id === 'string' && (id.length > 20 || id.includes(' ')));
|
|
81
|
+
if (hasOldFormatIds) {
|
|
82
|
+
console.warn('FloatingMenuBar: Detected old name-based configuration, resetting to defaults');
|
|
83
|
+
resetFloatingBarSettings();
|
|
84
|
+
return getDefaultConfig();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Migrate old pixel-based position to percentage-based
|
|
88
|
+
let finalPosition = settings.position;
|
|
89
|
+
if (isPixelFormat(settings.position) || settings.positionFormat === 'pixels') {
|
|
90
|
+
console.log('FloatingMenuBar: Migrating pixel-based position to percentage-based');
|
|
91
|
+
finalPosition = migrateToPercentage(settings.position);
|
|
92
|
+
// Save migrated position immediately
|
|
93
|
+
SDKUI_Globals.userSettings.searchSettings.floatingMenuBar = {
|
|
94
|
+
orientation: validOrientation,
|
|
95
|
+
itemIds: validItemIds,
|
|
96
|
+
position: finalPosition,
|
|
97
|
+
positionFormat: 'percentage',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
orientation: validOrientation,
|
|
102
|
+
savedItemIds: validItemIds,
|
|
103
|
+
position: finalPosition,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error('Failed to load FloatingMenuBar config:', error);
|
|
108
|
+
// Reset to defaults on any error
|
|
109
|
+
try {
|
|
110
|
+
resetFloatingBarSettings();
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
console.error('Failed to reset FloatingMenuBar settings:', e);
|
|
114
|
+
}
|
|
115
|
+
return getDefaultConfig();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const initialConfig = loadConfig();
|
|
119
|
+
const [state, setState] = useState({
|
|
120
|
+
position: initialConfig.position, // Stored as percentage
|
|
121
|
+
isDragging: false,
|
|
122
|
+
isConfigMode: false,
|
|
123
|
+
orientation: initialConfig.orientation,
|
|
124
|
+
items: [],
|
|
125
|
+
draggedItemIndex: null,
|
|
126
|
+
});
|
|
127
|
+
const floatingRef = useRef(null);
|
|
128
|
+
const dragOffset = useRef({ x: 0, y: 0 });
|
|
129
|
+
const [dragOverIndex, setDragOverIndex] = useState(null);
|
|
130
|
+
const [pixelPosition, setPixelPosition] = useState({ x: 0, y: 0 }); // Calculated pixel position
|
|
131
|
+
const containerSizeRef = useRef({ width: 0, height: 0 });
|
|
132
|
+
const stateSnapshot = useRef(null);
|
|
133
|
+
const floatingBarItemIds = useRef(new Set());
|
|
134
|
+
const floatingBarItemNames = useRef(new Set());
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
floatingBarItemIds.current = new Set(state.items.map(i => i.id));
|
|
137
|
+
floatingBarItemNames.current = new Set(state.items.map(i => i.name));
|
|
138
|
+
}, [state.items]);
|
|
139
|
+
// Calculate pixel position from percentage when container size or position changes
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const updatePixelPosition = () => {
|
|
142
|
+
if (!containerRef.current || !floatingRef.current)
|
|
143
|
+
return;
|
|
144
|
+
const container = containerRef.current.getBoundingClientRect();
|
|
145
|
+
const floating = floatingRef.current.getBoundingClientRect();
|
|
146
|
+
const containerWidth = isConstrained ? container.width : window.innerWidth;
|
|
147
|
+
const containerHeight = isConstrained ? container.height : window.innerHeight;
|
|
148
|
+
containerSizeRef.current = { width: containerWidth, height: containerHeight };
|
|
149
|
+
let newX = percentToPixels(state.position.x, containerWidth);
|
|
150
|
+
let newY = percentToPixels(state.position.y, containerHeight);
|
|
151
|
+
newX = Math.max(0, Math.min(newX, containerWidth - floating.width));
|
|
152
|
+
newY = Math.max(0, Math.min(newY, containerHeight - floating.height));
|
|
153
|
+
setPixelPosition({ x: newX, y: newY });
|
|
154
|
+
};
|
|
155
|
+
updatePixelPosition();
|
|
156
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
157
|
+
updatePixelPosition();
|
|
158
|
+
});
|
|
159
|
+
if (containerRef.current) {
|
|
160
|
+
resizeObserver.observe(containerRef.current);
|
|
161
|
+
}
|
|
162
|
+
if (!isConstrained) {
|
|
163
|
+
window.addEventListener('resize', updatePixelPosition);
|
|
164
|
+
}
|
|
165
|
+
return () => {
|
|
166
|
+
resizeObserver.disconnect();
|
|
167
|
+
if (!isConstrained) {
|
|
168
|
+
window.removeEventListener('resize', updatePixelPosition);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}, [state.position, isConstrained]);
|
|
172
|
+
const flattenMenuItems = useCallback((items, parentPath = '') => {
|
|
173
|
+
const result = [];
|
|
174
|
+
items.forEach((item, index) => {
|
|
175
|
+
const itemId = item.id || `${parentPath}${item.name}-${index}`;
|
|
176
|
+
if (item.onClick && !item.submenu) {
|
|
177
|
+
const isPinned = state.items.some(i => i.id === itemId);
|
|
178
|
+
result.push({
|
|
179
|
+
id: itemId,
|
|
180
|
+
name: item.name,
|
|
181
|
+
icon: item.icon || _jsx(IconPin, {}),
|
|
182
|
+
onClick: item.onClick,
|
|
183
|
+
disabled: item.disabled,
|
|
184
|
+
isPinned: isPinned,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// Recursively process submenus
|
|
188
|
+
if (item.submenu) {
|
|
189
|
+
result.push(...flattenMenuItems(item.submenu, `${itemId}/`));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return result;
|
|
193
|
+
}, [state.items]);
|
|
194
|
+
// Restore items on mount from savedItemIds
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (contextMenuItems.length > 0) {
|
|
197
|
+
const flatItems = flattenMenuItems(contextMenuItems);
|
|
198
|
+
// Restore items in the saved order from localStorage
|
|
199
|
+
const restoredItems = initialConfig.savedItemIds
|
|
200
|
+
.map((id) => flatItems.find(item => item.id === id))
|
|
201
|
+
.filter((item) => item !== undefined);
|
|
202
|
+
if (restoredItems.length > 0) {
|
|
203
|
+
setState(s => ({ ...s, items: restoredItems }));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}, []); // Only run once on mount
|
|
207
|
+
const togglePin = useCallback((item) => {
|
|
208
|
+
setState(s => {
|
|
209
|
+
const isInFloatingBar = s.items.some(i => i.id === item.id);
|
|
210
|
+
if (isInFloatingBar) {
|
|
211
|
+
// Remove from floating bar
|
|
212
|
+
const newItems = s.items.filter(i => i.id !== item.id);
|
|
213
|
+
return { ...s, items: newItems };
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// Add to floating bar
|
|
217
|
+
if (s.items.length >= maxItems) {
|
|
218
|
+
ShowAlert({
|
|
219
|
+
mode: 'warning',
|
|
220
|
+
title: 'Limite Massimo Raggiunto',
|
|
221
|
+
message: `Hai raggiunto il massimo di ${maxItems} elementi. Rimuovine uno prima di aggiungerne altri.`,
|
|
222
|
+
duration: 4000,
|
|
223
|
+
});
|
|
224
|
+
return s;
|
|
225
|
+
}
|
|
226
|
+
return { ...s, items: [...s.items, item] };
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}, [maxItems]);
|
|
230
|
+
// Get current item state (disabled and onClick) from contextMenuItems
|
|
231
|
+
const getCurrentItemState = useCallback((itemId) => {
|
|
232
|
+
const findInItems = (items) => {
|
|
233
|
+
for (let i = 0; i < items.length; i++) {
|
|
234
|
+
const item = items[i];
|
|
235
|
+
// Match by ID if the item has one
|
|
236
|
+
if (item.id === itemId)
|
|
237
|
+
return item;
|
|
238
|
+
// Check in submenu
|
|
239
|
+
if (item.submenu) {
|
|
240
|
+
const found = findInItems(item.submenu);
|
|
241
|
+
if (found)
|
|
242
|
+
return found;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
};
|
|
247
|
+
const foundItem = findInItems(contextMenuItems);
|
|
248
|
+
return {
|
|
249
|
+
disabled: foundItem?.disabled,
|
|
250
|
+
onClick: foundItem?.onClick
|
|
251
|
+
};
|
|
252
|
+
}, [contextMenuItems]);
|
|
253
|
+
const getPinContextMenuItems = useCallback(() => {
|
|
254
|
+
const flatItems = flattenMenuItems(contextMenuItems);
|
|
255
|
+
const currentItemIds = new Set(state.items.map(i => i.id));
|
|
256
|
+
const createPinItems = (items) => {
|
|
257
|
+
return items.map(item => {
|
|
258
|
+
const flatItem = flatItems.find(fi => fi.id === item.id);
|
|
259
|
+
const itemId = flatItem?.id || item.id || '';
|
|
260
|
+
const isAlreadyPinned = currentItemIds.has(itemId);
|
|
261
|
+
const pinItem = {
|
|
262
|
+
...item,
|
|
263
|
+
onClick: item.onClick && !item.submenu ? () => {
|
|
264
|
+
if (flatItem && !isAlreadyPinned) {
|
|
265
|
+
togglePin(flatItem);
|
|
266
|
+
}
|
|
267
|
+
} : undefined,
|
|
268
|
+
disabled: isAlreadyPinned,
|
|
269
|
+
};
|
|
270
|
+
if (item.submenu) {
|
|
271
|
+
pinItem.submenu = createPinItems(item.submenu);
|
|
272
|
+
}
|
|
273
|
+
return pinItem;
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
return createPinItems(contextMenuItems);
|
|
277
|
+
}, [contextMenuItems, flattenMenuItems, togglePin, state.items]);
|
|
278
|
+
const getContextMenuItemsWithPinIcons = useCallback(() => {
|
|
279
|
+
const flatItems = flattenMenuItems(contextMenuItems);
|
|
280
|
+
const currentItemIds = new Set(state.items.map(i => i.id));
|
|
281
|
+
const addPinIcons = (items) => {
|
|
282
|
+
return items.map(item => {
|
|
283
|
+
const flatItem = flatItems.find(fi => fi.id === item.id);
|
|
284
|
+
const itemId = flatItem?.id || item.id || '';
|
|
285
|
+
const isPinned = currentItemIds.has(itemId);
|
|
286
|
+
const itemWithPin = {
|
|
287
|
+
...item,
|
|
288
|
+
rightIcon: flatItem ? _jsx(IconPin, { color: isPinned ? 'red' : 'black' }) : undefined,
|
|
289
|
+
onRightIconClick: flatItem ? () => {
|
|
290
|
+
togglePin(flatItem);
|
|
291
|
+
} : undefined,
|
|
292
|
+
};
|
|
293
|
+
if (item.submenu) {
|
|
294
|
+
itemWithPin.submenu = addPinIcons(item.submenu);
|
|
295
|
+
}
|
|
296
|
+
return itemWithPin;
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
return addPinIcons(contextMenuItems);
|
|
300
|
+
}, [contextMenuItems, flattenMenuItems, togglePin, state.items]);
|
|
301
|
+
const handleMouseDown = (e) => {
|
|
302
|
+
if (state.isConfigMode)
|
|
303
|
+
return;
|
|
304
|
+
const containerRect = containerRef.current?.getBoundingClientRect();
|
|
305
|
+
if (containerRect) {
|
|
306
|
+
// Calculate drag offset based on positioning mode
|
|
307
|
+
if (isConstrained) {
|
|
308
|
+
// For absolute positioning, offset is relative to container
|
|
309
|
+
dragOffset.current = {
|
|
310
|
+
x: e.clientX - containerRect.left - pixelPosition.x,
|
|
311
|
+
y: e.clientY - containerRect.top - pixelPosition.y,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// For fixed positioning, offset is relative to viewport
|
|
316
|
+
dragOffset.current = {
|
|
317
|
+
x: e.clientX - pixelPosition.x,
|
|
318
|
+
y: e.clientY - pixelPosition.y,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
setState(s => ({ ...s, isDragging: true }));
|
|
323
|
+
};
|
|
324
|
+
const handleGripDoubleClick = () => {
|
|
325
|
+
if (state.isConfigMode)
|
|
326
|
+
return;
|
|
327
|
+
toggleOrientation();
|
|
328
|
+
};
|
|
329
|
+
const handleMouseMove = useCallback((e) => {
|
|
330
|
+
if (!state.isDragging || !containerRef.current || !floatingRef.current)
|
|
331
|
+
return;
|
|
332
|
+
const container = containerRef.current.getBoundingClientRect();
|
|
333
|
+
const floating = floatingRef.current.getBoundingClientRect();
|
|
334
|
+
let newX, newY;
|
|
335
|
+
if (isConstrained) {
|
|
336
|
+
// For constrained (absolute positioning), use container coordinates
|
|
337
|
+
newX = e.clientX - container.left - dragOffset.current.x;
|
|
338
|
+
newY = e.clientY - container.top - dragOffset.current.y;
|
|
339
|
+
// Constrain to container bounds
|
|
340
|
+
newX = Math.max(0, Math.min(newX, container.width - floating.width));
|
|
341
|
+
newY = Math.max(0, Math.min(newY, container.height - floating.height));
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
// For unconstrained (fixed positioning), use viewport coordinates
|
|
345
|
+
newX = e.clientX - dragOffset.current.x;
|
|
346
|
+
newY = e.clientY - dragOffset.current.y;
|
|
347
|
+
// Constrain to viewport bounds
|
|
348
|
+
newX = Math.max(0, Math.min(newX, window.innerWidth - floating.width));
|
|
349
|
+
newY = Math.max(0, Math.min(newY, window.innerHeight - floating.height));
|
|
350
|
+
}
|
|
351
|
+
// Update pixel position directly during drag
|
|
352
|
+
setPixelPosition({ x: newX, y: newY });
|
|
353
|
+
}, [state.isDragging, containerRef, isConstrained]);
|
|
354
|
+
const handleMouseUp = useCallback(() => {
|
|
355
|
+
if (state.isDragging && containerSizeRef.current.width > 0) {
|
|
356
|
+
// Convert final pixel position to percentage before updating state
|
|
357
|
+
const percentagePosition = {
|
|
358
|
+
x: pixelsToPercent(pixelPosition.x, containerSizeRef.current.width),
|
|
359
|
+
y: pixelsToPercent(pixelPosition.y, containerSizeRef.current.height),
|
|
360
|
+
};
|
|
361
|
+
setState(s => ({
|
|
362
|
+
...s,
|
|
363
|
+
isDragging: false,
|
|
364
|
+
position: percentagePosition,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
setState(s => ({ ...s, isDragging: false }));
|
|
369
|
+
}
|
|
370
|
+
}, [state.isDragging, pixelPosition]);
|
|
371
|
+
// Touch event handlers for tablet support
|
|
372
|
+
const handleTouchStart = (e) => {
|
|
373
|
+
if (state.isConfigMode)
|
|
374
|
+
return;
|
|
375
|
+
const touch = e.touches[0];
|
|
376
|
+
const containerRect = containerRef.current?.getBoundingClientRect();
|
|
377
|
+
if (containerRect) {
|
|
378
|
+
if (isConstrained) {
|
|
379
|
+
dragOffset.current = {
|
|
380
|
+
x: touch.clientX - containerRect.left - pixelPosition.x,
|
|
381
|
+
y: touch.clientY - containerRect.top - pixelPosition.y,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
dragOffset.current = {
|
|
386
|
+
x: touch.clientX - pixelPosition.x,
|
|
387
|
+
y: touch.clientY - pixelPosition.y,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
setState(s => ({ ...s, isDragging: true }));
|
|
392
|
+
};
|
|
393
|
+
const handleTouchMove = useCallback((e) => {
|
|
394
|
+
if (!state.isDragging || !containerRef.current || !floatingRef.current)
|
|
395
|
+
return;
|
|
396
|
+
const touch = e.touches[0];
|
|
397
|
+
const container = containerRef.current.getBoundingClientRect();
|
|
398
|
+
const floating = floatingRef.current.getBoundingClientRect();
|
|
399
|
+
let newX, newY;
|
|
400
|
+
if (isConstrained) {
|
|
401
|
+
newX = touch.clientX - container.left - dragOffset.current.x;
|
|
402
|
+
newY = touch.clientY - container.top - dragOffset.current.y;
|
|
403
|
+
newX = Math.max(0, Math.min(newX, container.width - floating.width));
|
|
404
|
+
newY = Math.max(0, Math.min(newY, container.height - floating.height));
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
newX = touch.clientX - dragOffset.current.x;
|
|
408
|
+
newY = touch.clientY - dragOffset.current.y;
|
|
409
|
+
newX = Math.max(0, Math.min(newX, window.innerWidth - floating.width));
|
|
410
|
+
newY = Math.max(0, Math.min(newY, window.innerHeight - floating.height));
|
|
411
|
+
}
|
|
412
|
+
setPixelPosition({ x: newX, y: newY });
|
|
413
|
+
}, [state.isDragging, containerRef, isConstrained]);
|
|
414
|
+
const handleTouchEnd = useCallback(() => {
|
|
415
|
+
if (state.isDragging && containerSizeRef.current.width > 0) {
|
|
416
|
+
const percentagePosition = {
|
|
417
|
+
x: pixelsToPercent(pixelPosition.x, containerSizeRef.current.width),
|
|
418
|
+
y: pixelsToPercent(pixelPosition.y, containerSizeRef.current.height),
|
|
419
|
+
};
|
|
420
|
+
setState(s => ({
|
|
421
|
+
...s,
|
|
422
|
+
isDragging: false,
|
|
423
|
+
position: percentagePosition,
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
setState(s => ({ ...s, isDragging: false }));
|
|
428
|
+
}
|
|
429
|
+
}, [state.isDragging, pixelPosition]);
|
|
430
|
+
useEffect(() => {
|
|
431
|
+
if (state.isDragging) {
|
|
432
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
433
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
434
|
+
document.addEventListener('touchmove', handleTouchMove);
|
|
435
|
+
document.addEventListener('touchend', handleTouchEnd);
|
|
436
|
+
return () => {
|
|
437
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
438
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
439
|
+
document.removeEventListener('touchmove', handleTouchMove);
|
|
440
|
+
document.removeEventListener('touchend', handleTouchEnd);
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
}, [state.isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]);
|
|
445
|
+
// Save to SDKUI_Globals.userSettings only when NOT in config mode (when applying changes)
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (state.isConfigMode)
|
|
448
|
+
return; // Don't save during edit mode
|
|
449
|
+
try {
|
|
450
|
+
// Replace the entire object to trigger the Proxy
|
|
451
|
+
SDKUI_Globals.userSettings.searchSettings.floatingMenuBar = {
|
|
452
|
+
orientation: state.orientation,
|
|
453
|
+
itemIds: state.items.map(item => item.id),
|
|
454
|
+
position: state.position,
|
|
455
|
+
positionFormat: 'percentage',
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
console.error('Failed to save FloatingMenuBar config:', error);
|
|
460
|
+
}
|
|
461
|
+
}, [state.orientation, state.items, state.position, state.isConfigMode]);
|
|
462
|
+
const toggleConfigMode = () => {
|
|
463
|
+
setState(s => {
|
|
464
|
+
if (!s.isConfigMode) {
|
|
465
|
+
stateSnapshot.current = {
|
|
466
|
+
items: [...s.items],
|
|
467
|
+
orientation: s.orientation,
|
|
468
|
+
position: { ...s.position },
|
|
469
|
+
};
|
|
470
|
+
return { ...s, isConfigMode: true };
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
// Exiting edit mode (applying changes) - clear snapshot
|
|
474
|
+
stateSnapshot.current = null;
|
|
475
|
+
return { ...s, isConfigMode: false };
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
};
|
|
479
|
+
// Auto-reposition when entering edit mode to ensure Apply/Undo buttons are visible
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
if (!state.isConfigMode || !floatingRef.current)
|
|
482
|
+
return;
|
|
483
|
+
requestAnimationFrame(() => {
|
|
484
|
+
if (!floatingRef.current)
|
|
485
|
+
return;
|
|
486
|
+
const floating = floatingRef.current.getBoundingClientRect();
|
|
487
|
+
let newX = state.position.x;
|
|
488
|
+
let newY = state.position.y;
|
|
489
|
+
let needsUpdate = false;
|
|
490
|
+
if (isConstrained && containerRef.current) {
|
|
491
|
+
const container = containerRef.current.getBoundingClientRect();
|
|
492
|
+
if (state.orientation === 'horizontal') {
|
|
493
|
+
if (state.position.x + floating.width > container.width) {
|
|
494
|
+
newX = Math.max(0, container.width - floating.width);
|
|
495
|
+
needsUpdate = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
if (state.position.y + floating.height > container.height) {
|
|
500
|
+
newY = Math.max(0, container.height - floating.height);
|
|
501
|
+
needsUpdate = true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
if (state.orientation === 'horizontal') {
|
|
507
|
+
if (state.position.x + floating.width > window.innerWidth) {
|
|
508
|
+
newX = Math.max(0, window.innerWidth - floating.width - 10);
|
|
509
|
+
needsUpdate = true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
if (state.position.y + floating.height > window.innerHeight) {
|
|
514
|
+
newY = Math.max(0, window.innerHeight - floating.height - 10);
|
|
515
|
+
needsUpdate = true;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (needsUpdate) {
|
|
520
|
+
setState(s => ({
|
|
521
|
+
...s,
|
|
522
|
+
position: { x: newX, y: newY },
|
|
523
|
+
}));
|
|
524
|
+
// Update snapshot position to the corrected position so Undo restores to visible position
|
|
525
|
+
if (stateSnapshot.current) {
|
|
526
|
+
stateSnapshot.current.position = { x: newX, y: newY };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}, [state.isConfigMode, state.orientation, isConstrained]);
|
|
531
|
+
const handleUndo = () => {
|
|
532
|
+
if (stateSnapshot.current) {
|
|
533
|
+
setState(s => ({
|
|
534
|
+
...s,
|
|
535
|
+
items: [...stateSnapshot.current.items],
|
|
536
|
+
orientation: stateSnapshot.current.orientation,
|
|
537
|
+
position: { ...stateSnapshot.current.position },
|
|
538
|
+
isConfigMode: true,
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
// Check if current state has changed from snapshot
|
|
543
|
+
const hasChanges = () => {
|
|
544
|
+
if (!stateSnapshot.current)
|
|
545
|
+
return false;
|
|
546
|
+
// Check if items have changed (different IDs or different order)
|
|
547
|
+
const currentIds = state.items.map(i => i.id).join(',');
|
|
548
|
+
const snapshotIds = stateSnapshot.current.items.map(i => i.id).join(',');
|
|
549
|
+
return currentIds !== snapshotIds;
|
|
550
|
+
};
|
|
551
|
+
const toggleOrientation = () => {
|
|
552
|
+
const newOrientation = state.orientation === 'horizontal' ? 'vertical' : 'horizontal';
|
|
553
|
+
if (state.orientation === 'horizontal' && newOrientation === 'vertical') {
|
|
554
|
+
if (floatingRef.current) {
|
|
555
|
+
const floating = floatingRef.current.getBoundingClientRect();
|
|
556
|
+
const screenHeight = window.innerHeight;
|
|
557
|
+
if (floating.width > screenHeight - 70) {
|
|
558
|
+
ShowAlert({
|
|
559
|
+
mode: 'warning',
|
|
560
|
+
title: 'Troppi elementi',
|
|
561
|
+
message: 'Ci sono troppi elementi nella barra mobile per la modalità verticale.',
|
|
562
|
+
duration: 4000,
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
setState(s => ({
|
|
569
|
+
...s,
|
|
570
|
+
orientation: newOrientation,
|
|
571
|
+
}));
|
|
572
|
+
requestAnimationFrame(() => {
|
|
573
|
+
requestAnimationFrame(() => {
|
|
574
|
+
if (containerRef.current && floatingRef.current) {
|
|
575
|
+
const container = containerRef.current.getBoundingClientRect();
|
|
576
|
+
const floating = floatingRef.current.getBoundingClientRect();
|
|
577
|
+
let newX, newY;
|
|
578
|
+
if (isConstrained) {
|
|
579
|
+
// For constrained mode, use container bounds
|
|
580
|
+
newX = Math.max(0, Math.min(state.position.x, container.width - floating.width));
|
|
581
|
+
newY = Math.max(0, Math.min(state.position.y, container.height - floating.height));
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// For unconstrained mode, use viewport bounds
|
|
585
|
+
newX = Math.max(0, Math.min(state.position.x, window.innerWidth - floating.width));
|
|
586
|
+
newY = Math.max(0, Math.min(state.position.y, window.innerHeight - floating.height));
|
|
587
|
+
}
|
|
588
|
+
// Only update position if it changed (to avoid unnecessary re-render)
|
|
589
|
+
if (newX !== state.position.x || newY !== state.position.y) {
|
|
590
|
+
setState(s => ({
|
|
591
|
+
...s,
|
|
592
|
+
position: { x: newX, y: newY },
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
};
|
|
599
|
+
const removeItem = (itemId) => {
|
|
600
|
+
setState(s => ({
|
|
601
|
+
...s,
|
|
602
|
+
items: s.items.filter(item => item.id !== itemId),
|
|
603
|
+
}));
|
|
604
|
+
};
|
|
605
|
+
// Drag and drop for reordering
|
|
606
|
+
const handleDragStart = (e, index) => {
|
|
607
|
+
if (!state.isConfigMode)
|
|
608
|
+
return;
|
|
609
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
610
|
+
e.dataTransfer.setData('text/plain', index.toString());
|
|
611
|
+
setState(s => ({ ...s, draggedItemIndex: index }));
|
|
612
|
+
};
|
|
613
|
+
const handleDragEnter = (e, index) => {
|
|
614
|
+
if (!state.isConfigMode)
|
|
615
|
+
return;
|
|
616
|
+
e.preventDefault();
|
|
617
|
+
if (state.draggedItemIndex !== index) {
|
|
618
|
+
setDragOverIndex(index);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
const handleDragOver = (e) => {
|
|
622
|
+
if (!state.isConfigMode)
|
|
623
|
+
return;
|
|
624
|
+
e.preventDefault();
|
|
625
|
+
e.dataTransfer.dropEffect = 'move';
|
|
626
|
+
};
|
|
627
|
+
const handleDragLeave = (_e, index) => {
|
|
628
|
+
if (!state.isConfigMode)
|
|
629
|
+
return;
|
|
630
|
+
// Only clear if we're actually leaving this specific item
|
|
631
|
+
if (dragOverIndex === index) {
|
|
632
|
+
setDragOverIndex(null);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
const handleDrop = (e, dropIndex) => {
|
|
636
|
+
if (!state.isConfigMode || state.draggedItemIndex === null)
|
|
637
|
+
return;
|
|
638
|
+
e.preventDefault();
|
|
639
|
+
e.stopPropagation();
|
|
640
|
+
const dragIndex = state.draggedItemIndex;
|
|
641
|
+
if (dragIndex === dropIndex) {
|
|
642
|
+
setDragOverIndex(null);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const newItems = [...state.items];
|
|
646
|
+
const [draggedItem] = newItems.splice(dragIndex, 1);
|
|
647
|
+
newItems.splice(dropIndex, 0, draggedItem);
|
|
648
|
+
setState(s => ({
|
|
649
|
+
...s,
|
|
650
|
+
items: newItems,
|
|
651
|
+
draggedItemIndex: null,
|
|
652
|
+
}));
|
|
653
|
+
setDragOverIndex(null);
|
|
654
|
+
};
|
|
655
|
+
const handleDragEnd = () => {
|
|
656
|
+
setState(s => ({ ...s, draggedItemIndex: null }));
|
|
657
|
+
setDragOverIndex(null);
|
|
658
|
+
};
|
|
659
|
+
return (_jsxs(_Fragment, { children: [_jsx(S.Overlay, { "$visible": state.isConfigMode }), _jsxs(S.FloatingContainer, { ref: floatingRef, "$x": pixelPosition.x, "$y": pixelPosition.y, "$orientation": state.orientation, "$isDragging": state.isDragging, "$isConfigMode": state.isConfigMode, "$isConstrained": isConstrained, onContextMenu: (e) => e.preventDefault(), children: [!state.isConfigMode ? (_jsx(ContextMenu, { items: [
|
|
660
|
+
{
|
|
661
|
+
name: SDKUI_Localizator.Configure,
|
|
662
|
+
onClick: () => {
|
|
663
|
+
if (!state.isConfigMode) {
|
|
664
|
+
toggleConfigMode();
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: state.orientation === 'horizontal' ? 'Floating bar verticale' : 'Floating bar orizzontale',
|
|
670
|
+
onClick: toggleOrientation,
|
|
671
|
+
},
|
|
672
|
+
], trigger: "right", children: _jsx(S.GripHandle, { "$orientation": state.orientation, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, onDoubleClick: handleGripDoubleClick, children: _jsx(IconDraggableDots, {}) }) })) : (_jsx(S.GripHandle, { "$orientation": state.orientation, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, onDoubleClick: handleGripDoubleClick, children: _jsx(IconDraggableDots, {}) })), _jsx(S.Separator, { "$orientation": state.orientation }), state.items.map((item, index) => {
|
|
673
|
+
// Get current state (disabled and onClick) from contextMenuItems
|
|
674
|
+
const currentState = getCurrentItemState(item.id);
|
|
675
|
+
const isDisabled = currentState.disabled || false;
|
|
676
|
+
const currentOnClick = currentState.onClick || item.onClick; // Fallback to stored onClick if not found
|
|
677
|
+
return (_jsxs(S.DraggableItem, { "$isDragging": state.draggedItemIndex === index, "$isDragOver": dragOverIndex === index && state.draggedItemIndex !== index, draggable: state.isConfigMode, onDragStart: (e) => handleDragStart(e, index), onDragEnter: (e) => handleDragEnter(e, index), onDragOver: handleDragOver, onDragLeave: (e) => handleDragLeave(e, index), onDrop: (e) => handleDrop(e, index), onDragEnd: handleDragEnd, children: [_jsx(TMTooltip, { content: item.name, position: "bottom", children: _jsx(S.MenuButton, { onClick: () => {
|
|
678
|
+
if (state.isConfigMode || isDisabled)
|
|
679
|
+
return;
|
|
680
|
+
if (currentOnClick) {
|
|
681
|
+
currentOnClick();
|
|
682
|
+
}
|
|
683
|
+
}, disabled: state.isConfigMode ? isDisabled && !state.isConfigMode : isDisabled, children: item.icon }) }), state.isConfigMode && (_jsx(S.RemoveButton, { onClick: () => removeItem(item.id), children: "\u00D7" }))] }, item.id));
|
|
684
|
+
}), !state.isConfigMode && contextMenuItems.length > 0 && (_jsx(ContextMenu, { items: getContextMenuItemsWithPinIcons(), trigger: "left", keepOpenOnClick: true, children: _jsx(S.ContextMenuButton, { children: _jsx(IconMenuVertical, {}) }) })), state.isConfigMode && state.items.length < maxItems && contextMenuItems.length > 0 && (_jsx(ContextMenu, { items: getPinContextMenuItems(), trigger: "left", keepOpenOnClick: true, children: _jsx(TMTooltip, { content: SDKUI_Localizator.Add, children: _jsx(S.AddButton, { children: _jsx(IconAdd, {}) }) }) })), state.isConfigMode && (_jsxs(_Fragment, { children: [_jsx(S.Separator, { "$orientation": state.orientation }), _jsxs(S.ButtonGroup, { "$orientation": state.orientation, children: [_jsx(TMTooltip, { content: SDKUI_Localizator.Undo, position: state.orientation === 'horizontal' ? 'right' : 'top', children: _jsx(S.UndoButton, { onClick: handleUndo, disabled: !hasChanges(), children: _jsx(IconUndo, { fontSize: 18 }) }) }), _jsx(TMTooltip, { content: state.items.length === 0 ? 'Devi aggiungere almeno un item' : SDKUI_Localizator.ApplyAndClose, position: state.orientation === 'horizontal' ? 'right' : 'top', children: _jsx(S.ApplyButton, { onClick: toggleConfigMode, disabled: state.items.length === 0, children: _jsx(IconApply, { fontSize: 20 }) }) })] })] }))] })] }));
|
|
685
|
+
};
|
|
686
|
+
export default TMFloatingMenuBar;
|