@topconsultnpm/sdkui-react 6.20.0-dev1.13 → 6.20.0-dev1.131
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/assets/Toppy-help-center.png +0 -0
- package/lib/assets/headergradient.svg +87 -0
- package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +322 -30
- package/lib/components/NewComponents/ContextMenu/hooks.d.ts +8 -1
- package/lib/components/NewComponents/ContextMenu/hooks.js +80 -8
- package/lib/components/NewComponents/ContextMenu/index.d.ts +3 -0
- package/lib/components/NewComponents/ContextMenu/index.js +2 -0
- package/lib/components/NewComponents/ContextMenu/styles.d.ts +9 -1
- package/lib/components/NewComponents/ContextMenu/styles.js +146 -47
- package/lib/components/NewComponents/ContextMenu/types.d.ts +22 -3
- 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.js +620 -125
- package/lib/components/NewComponents/FloatingMenuBar/styles.d.ts +25 -5
- package/lib/components/NewComponents/FloatingMenuBar/styles.js +215 -59
- package/lib/components/NewComponents/FloatingMenuBar/types.d.ts +12 -3
- package/lib/components/base/TMAccordionNew.js +35 -14
- package/lib/components/base/TMButton.js +6 -0
- package/lib/components/base/TMClosableList.js +4 -0
- package/lib/components/base/TMCustomButton.js +61 -17
- package/lib/components/base/TMDataGrid.d.ts +7 -4
- package/lib/components/base/TMDataGrid.js +153 -11
- package/lib/components/base/TMDropDownMenu.js +21 -18
- package/lib/components/base/TMFileManager.d.ts +4 -3
- package/lib/components/base/TMFileManager.js +32 -24
- package/lib/components/base/TMFileManagerDataGridView.d.ts +3 -2
- package/lib/components/base/TMFileManagerDataGridView.js +1 -11
- package/lib/components/base/TMFileManagerThumbnailItems.d.ts +7 -1
- package/lib/components/base/TMFileManagerThumbnailItems.js +5 -2
- package/lib/components/base/TMFileManagerThumbnailsView.d.ts +17 -4
- package/lib/components/base/TMFileManagerThumbnailsView.js +18 -6
- package/lib/components/base/TMFileManagerUtils.d.ts +0 -12
- package/lib/components/base/TMListView.js +33 -15
- package/lib/components/base/TMPanel.d.ts +1 -1
- package/lib/components/base/TMPanel.js +4 -2
- package/lib/components/base/TMPopUp.js +6 -0
- package/lib/components/base/TMToolbarCard.js +2 -0
- package/lib/components/base/TMTreeView.d.ts +2 -1
- package/lib/components/base/TMTreeView.js +33 -26
- package/lib/components/choosers/TMDataListItemChooser.d.ts +2 -0
- package/lib/components/choosers/TMDataListItemChooser.js +8 -2
- package/lib/components/choosers/TMDcmtTypeChooser.d.ts +1 -0
- package/lib/components/choosers/TMDcmtTypeChooser.js +11 -3
- package/lib/components/choosers/TMDistinctValues.js +2 -2
- package/lib/components/choosers/TMDynDataListItemChooser.d.ts +2 -0
- package/lib/components/choosers/TMDynDataListItemChooser.js +8 -2
- package/lib/components/choosers/TMInvoiceRetrieveFormats.js +1 -1
- package/lib/components/choosers/TMMetadataChooser.d.ts +2 -0
- package/lib/components/choosers/TMMetadataChooser.js +19 -4
- package/lib/components/choosers/TMOrderRetrieveFormats.js +1 -1
- package/lib/components/choosers/TMUserChooser.d.ts +2 -5
- package/lib/components/choosers/TMUserChooser.js +33 -47
- package/lib/components/editors/TMCheckBox.js +2 -0
- package/lib/components/editors/TMDateBox.js +18 -9
- package/lib/components/editors/TMEditorStyled.js +7 -0
- package/lib/components/editors/TMLocalizedTextBox.d.ts +3 -1
- package/lib/components/editors/TMLocalizedTextBox.js +16 -14
- package/lib/components/editors/TMMetadataEditor.d.ts +1 -0
- package/lib/components/editors/TMMetadataEditor.js +4 -4
- package/lib/components/editors/TMMetadataTextBox.d.ts +9 -0
- package/lib/components/editors/TMMetadataTextBox.js +92 -0
- package/lib/components/editors/TMMetadataValues.d.ts +2 -0
- package/lib/components/editors/TMMetadataValues.js +26 -8
- package/lib/components/editors/TMRadioButton.js +2 -0
- package/lib/components/editors/TMTextArea.js +18 -30
- package/lib/components/editors/TMTextBox.d.ts +1 -1
- package/lib/components/editors/TMTextBox.js +29 -4
- package/lib/components/editors/TMTextExpression.js +6 -91
- package/lib/components/features/archive/TMArchive.js +2 -2
- package/lib/components/features/assistant/TMToppyDraggableHelpCenter.d.ts +15 -0
- package/lib/components/features/assistant/TMToppyDraggableHelpCenter.js +462 -0
- package/lib/components/features/assistant/TMToppySpeechBubble.d.ts +11 -0
- package/lib/components/features/assistant/TMToppySpeechBubble.js +126 -0
- package/lib/components/features/documents/TMDcmtBlog.js +1 -1
- package/lib/components/features/documents/TMDcmtForm.d.ts +14 -2
- package/lib/components/features/documents/TMDcmtForm.js +576 -292
- package/lib/components/features/documents/TMDcmtPreview.js +42 -155
- package/lib/components/features/documents/TMDcmtTasks.js +9 -9
- package/lib/components/features/documents/TMMasterDetailDcmts.js +38 -53
- package/lib/components/features/documents/TMRelationViewer.d.ts +1 -1
- package/lib/components/features/documents/TMRelationViewer.js +2 -2
- package/lib/components/features/search/TMDcmtCheckoutInfoForm.d.ts +8 -0
- package/lib/components/features/search/{TMSearchResultCheckoutInfoForm.js → TMDcmtCheckoutInfoForm.js} +2 -2
- package/lib/components/features/search/TMSavedQuerySelector.js +72 -67
- package/lib/components/features/search/TMSearch.d.ts +3 -0
- package/lib/components/features/search/TMSearch.js +50 -11
- package/lib/components/features/search/TMSearchQueryEditor.d.ts +1 -0
- package/lib/components/features/search/TMSearchQueryEditor.js +10 -10
- package/lib/components/features/search/TMSearchQueryPanel.d.ts +1 -0
- package/lib/components/features/search/TMSearchQueryPanel.js +40 -25
- package/lib/components/features/search/TMSearchResult.d.ts +3 -0
- package/lib/components/features/search/TMSearchResult.js +370 -252
- package/lib/components/features/search/TMSearchResultsMenuItems.d.ts +3 -3
- package/lib/components/features/search/TMSearchResultsMenuItems.js +227 -171
- 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 +47 -52
- package/lib/components/features/tasks/TMTaskForm.js +75 -25
- package/lib/components/features/tasks/TMTasksAgenda.d.ts +3 -1
- package/lib/components/features/tasks/TMTasksAgenda.js +48 -9
- package/lib/components/features/tasks/TMTasksCalendar.d.ts +2 -0
- package/lib/components/features/tasks/TMTasksCalendar.js +19 -7
- package/lib/components/features/tasks/TMTasksUtils.d.ts +2 -2
- package/lib/components/features/tasks/TMTasksUtils.js +57 -37
- package/lib/components/features/tasks/TMTasksView.js +28 -19
- package/lib/components/features/workflow/TMWorkflowPopup.d.ts +33 -2
- package/lib/components/features/workflow/TMWorkflowPopup.js +140 -34
- package/lib/components/features/workflow/diagram/DiagramItemComponent.d.ts +2 -0
- package/lib/components/features/workflow/diagram/DiagramItemComponent.js +14 -7
- package/lib/components/features/workflow/diagram/DiagramItemForm.js +1 -1
- package/lib/components/features/workflow/diagram/RecipientList.js +3 -2
- package/lib/components/features/workflow/diagram/WFDiagram.d.ts +4 -0
- package/lib/components/features/workflow/diagram/WFDiagram.js +164 -13
- 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 +35 -7
- package/lib/components/forms/TMChooserForm.js +1 -1
- package/lib/components/grids/TMBlogsPost.js +56 -31
- package/lib/components/grids/TMRecentsManager.js +20 -10
- package/lib/components/grids/TMValidationItemsList.js +6 -0
- package/lib/components/index.d.ts +6 -3
- package/lib/components/index.js +6 -3
- package/lib/components/layout/panelManager/TMPanelManagerContext.js +13 -5
- package/lib/components/query/TMQueryEditor.d.ts +6 -1
- package/lib/components/query/TMQueryEditor.js +105 -101
- package/lib/components/settings/SettingsAppearance.d.ts +2 -1
- package/lib/components/settings/SettingsAppearance.js +99 -30
- package/lib/components/sidebar/TMHeader.js +11 -7
- package/lib/components/sidebar/TMSidebar.d.ts +0 -1
- package/lib/components/sidebar/TMSidebar.js +16 -44
- package/lib/components/sidebar/TMSidebarItem.js +36 -17
- package/lib/components/viewers/TMDataListItemViewer.d.ts +2 -1
- package/lib/components/viewers/TMDataListItemViewer.js +35 -71
- package/lib/components/viewers/TMDataUserIdItemViewer.d.ts +8 -0
- package/lib/components/viewers/TMDataUserIdItemViewer.js +39 -0
- package/lib/css/tm-sdkui.css +1 -1
- package/lib/helper/SDKUI_Globals.d.ts +22 -0
- package/lib/helper/SDKUI_Globals.js +10 -1
- package/lib/helper/SDKUI_Localizator.d.ts +21 -3
- package/lib/helper/SDKUI_Localizator.js +196 -10
- package/lib/helper/TMCommandsContextMenu.d.ts +4 -2
- package/lib/helper/TMCommandsContextMenu.js +15 -4
- package/lib/helper/TMIcons.d.ts +4 -0
- package/lib/helper/TMIcons.js +13 -3
- package/lib/helper/TMPdfViewer.d.ts +8 -0
- package/lib/helper/TMPdfViewer.js +373 -0
- package/lib/helper/TMToppyMessage.js +4 -0
- package/lib/helper/checkinCheckoutManager.d.ts +31 -1
- package/lib/helper/checkinCheckoutManager.js +112 -30
- package/lib/helper/devextremeCustomMessages.d.ts +30 -0
- package/lib/helper/devextremeCustomMessages.js +30 -0
- package/lib/helper/helpers.d.ts +30 -2
- package/lib/helper/helpers.js +132 -4
- package/lib/helper/index.d.ts +2 -0
- package/lib/helper/index.js +2 -0
- package/lib/helper/queryHelper.d.ts +2 -2
- package/lib/helper/queryHelper.js +80 -24
- package/lib/helper/workItemsHelper.d.ts +6 -0
- package/lib/helper/workItemsHelper.js +230 -0
- package/lib/hooks/useCheckInOutOperations.d.ts +28 -0
- package/lib/hooks/useCheckInOutOperations.js +223 -0
- package/lib/hooks/useDataListItem.d.ts +12 -0
- package/lib/hooks/useDataListItem.js +132 -0
- package/lib/hooks/useDataUserIdItem.d.ts +10 -0
- package/lib/hooks/useDataUserIdItem.js +96 -0
- package/lib/hooks/useFloatingBarPinnedItems.d.ts +11 -0
- package/lib/hooks/useFloatingBarPinnedItems.js +54 -0
- package/lib/hooks/useMetadataExpression.d.ts +19 -0
- package/lib/hooks/useMetadataExpression.js +99 -0
- package/lib/hooks/useSettingsFeedback.d.ts +11 -0
- package/lib/hooks/useSettingsFeedback.js +38 -0
- package/lib/hooks/useWorkflowApprove.d.ts +4 -0
- package/lib/hooks/useWorkflowApprove.js +14 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +3 -2
- package/lib/services/platform_services.d.ts +3 -3
- package/lib/ts/types.d.ts +61 -1
- package/lib/utils/theme.d.ts +1 -1
- package/lib/utils/theme.js +1 -1
- package/package.json +8 -6
- package/lib/components/NewComponents/Notification/Notification.d.ts +0 -4
- package/lib/components/NewComponents/Notification/Notification.js +0 -60
- package/lib/components/NewComponents/Notification/NotificationContainer.d.ts +0 -8
- package/lib/components/NewComponents/Notification/NotificationContainer.js +0 -33
- package/lib/components/NewComponents/Notification/index.d.ts +0 -2
- package/lib/components/NewComponents/Notification/index.js +0 -2
- package/lib/components/NewComponents/Notification/styles.d.ts +0 -21
- package/lib/components/NewComponents/Notification/styles.js +0 -180
- package/lib/components/NewComponents/Notification/types.d.ts +0 -18
- package/lib/components/NewComponents/Notification/types.js +0 -1
- package/lib/components/base/TMContextMenu.d.ts +0 -25
- package/lib/components/base/TMContextMenu.js +0 -109
- package/lib/components/base/TMContextMenuOLD.d.ts +0 -26
- package/lib/components/base/TMContextMenuOLD.js +0 -56
- package/lib/components/base/TMFloatingToolbar.d.ts +0 -9
- package/lib/components/base/TMFloatingToolbar.js +0 -101
- package/lib/components/features/assistant/ToppyDraggableHelpCenter.d.ts +0 -30
- package/lib/components/features/assistant/ToppyDraggableHelpCenter.js +0 -482
- package/lib/components/features/assistant/ToppySpeechBubble.d.ts +0 -9
- package/lib/components/features/assistant/ToppySpeechBubble.js +0 -117
- package/lib/components/features/search/TMSearchResultCheckoutInfoForm.d.ts +0 -8
|
@@ -1,70 +1,201 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
3
|
import { ContextMenu } from '../ContextMenu';
|
|
4
|
-
import
|
|
4
|
+
import ShowAlert from '../../base/TMAlert';
|
|
5
5
|
import TMTooltip from '../../base/TMTooltip';
|
|
6
6
|
import * as S from './styles';
|
|
7
|
-
import {
|
|
7
|
+
import { IconDelete, IconMenuVertical, IconPin, IconRotate, IconSeparator, SDKUI_Globals } from '../../../helper';
|
|
8
|
+
const Separator = (props) => (_jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", height: "1em", width: "1em", ...props, children: _jsx("path", { d: "M12 2v20", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round" }) }));
|
|
8
9
|
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 = [],
|
|
10
|
+
const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], isConstrained = false, defaultPosition = { x: 1, y: 90 }, defaultOrientation = 'horizontal', maxItems = 100, defaultPinnedItems = [], fixedItems = [], enableConfigMode = true, bgColor = undefined, hasContextMenu = true, pinnedItemIds: externalPinnedItemIds, onPinChange, }) => {
|
|
11
|
+
const percentToPixels = (percent, containerSize) => {
|
|
12
|
+
return (percent / 100) * containerSize;
|
|
13
|
+
};
|
|
14
|
+
const pixelsToPercent = (pixels, containerSize) => {
|
|
15
|
+
return (pixels / containerSize) * 100;
|
|
16
|
+
};
|
|
17
|
+
const isPixelFormat = (pos) => {
|
|
18
|
+
return pos.x > 100 || pos.y > 100;
|
|
19
|
+
};
|
|
20
|
+
const migrateToPercentage = (pixelPos) => {
|
|
21
|
+
const container = containerRef.current?.getBoundingClientRect();
|
|
22
|
+
const containerWidth = isConstrained && container ? container.width : window.innerWidth;
|
|
23
|
+
const containerHeight = isConstrained && container ? container.height : window.innerHeight;
|
|
24
|
+
return {
|
|
25
|
+
x: pixelsToPercent(pixelPos.x, containerWidth),
|
|
26
|
+
y: pixelsToPercent(pixelPos.y, containerHeight),
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const getDefaultConfig = () => ({
|
|
30
|
+
orientation: defaultOrientation,
|
|
31
|
+
savedItemIds: defaultPinnedItems,
|
|
32
|
+
position: defaultPosition,
|
|
33
|
+
});
|
|
34
|
+
const resetFloatingBarSettings = () => {
|
|
35
|
+
// Reset the floatingMenuBar settings in SDKUI_Globals to empty
|
|
36
|
+
SDKUI_Globals.userSettings.searchSettings.floatingMenuBar = {
|
|
37
|
+
orientation: undefined,
|
|
38
|
+
itemIds: undefined,
|
|
39
|
+
position: undefined,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
10
42
|
const loadConfig = () => {
|
|
43
|
+
// If config mode is disabled, use default position and orientation
|
|
44
|
+
if (!enableConfigMode) {
|
|
45
|
+
return {
|
|
46
|
+
orientation: defaultOrientation,
|
|
47
|
+
savedItemIds: defaultPinnedItems,
|
|
48
|
+
position: defaultPosition,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
11
51
|
try {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
52
|
+
const settings = SDKUI_Globals.userSettings.searchSettings.floatingMenuBar;
|
|
53
|
+
// If localStorage is empty (first time) or itemIds is empty array, use props as defaults
|
|
54
|
+
if (!settings || !settings.position || !settings.itemIds || settings.itemIds.length === 0) {
|
|
55
|
+
return getDefaultConfig();
|
|
56
|
+
}
|
|
57
|
+
// Validate position
|
|
58
|
+
const hasValidPosition = settings.position &&
|
|
59
|
+
typeof settings.position.x === 'number' &&
|
|
60
|
+
typeof settings.position.y === 'number' &&
|
|
61
|
+
!Number.isNaN(settings.position.x) &&
|
|
62
|
+
!Number.isNaN(settings.position.y) &&
|
|
63
|
+
Number.isFinite(settings.position.x) &&
|
|
64
|
+
Number.isFinite(settings.position.y);
|
|
65
|
+
if (!hasValidPosition) {
|
|
66
|
+
console.warn('FloatingMenuBar: Invalid position, resetting to defaults');
|
|
67
|
+
resetFloatingBarSettings();
|
|
68
|
+
return getDefaultConfig();
|
|
69
|
+
}
|
|
70
|
+
// Ensure position is within reasonable viewport bounds
|
|
71
|
+
const maxX = globalThis.window?.innerWidth ? globalThis.window.innerWidth - 50 : 1000;
|
|
72
|
+
const maxY = globalThis.window?.innerHeight ? globalThis.window.innerHeight - 50 : 800;
|
|
73
|
+
if (settings.position.x < 0 || settings.position.x > maxX ||
|
|
74
|
+
settings.position.y < 0 || settings.position.y > maxY) {
|
|
75
|
+
console.warn('FloatingMenuBar: Position out of bounds, resetting to defaults');
|
|
76
|
+
resetFloatingBarSettings();
|
|
77
|
+
return getDefaultConfig();
|
|
78
|
+
}
|
|
79
|
+
// Validate orientation
|
|
80
|
+
const validOrientation = (settings.orientation === 'horizontal' || settings.orientation === 'vertical')
|
|
81
|
+
? settings.orientation
|
|
82
|
+
: 'horizontal';
|
|
83
|
+
// Validate itemIds
|
|
84
|
+
const validItemIds = Array.isArray(settings.itemIds) ? settings.itemIds : [];
|
|
85
|
+
if (validItemIds.length > 0) {
|
|
86
|
+
// Check if any ID looks like the old format (contains language-specific text or is too long)
|
|
87
|
+
const hasOldFormatIds = validItemIds.some(id => typeof id === 'string' &&
|
|
88
|
+
!id.startsWith('separator-') &&
|
|
89
|
+
(id.length > 20 || id.includes(' ')));
|
|
90
|
+
if (hasOldFormatIds) {
|
|
91
|
+
console.warn('FloatingMenuBar: Detected old name-based configuration, resetting to defaults');
|
|
92
|
+
resetFloatingBarSettings();
|
|
93
|
+
return getDefaultConfig();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Migrate old pixel-based position to percentage-based
|
|
97
|
+
let finalPosition = settings.position;
|
|
98
|
+
if (isPixelFormat(settings.position) || settings.positionFormat === 'pixels') {
|
|
99
|
+
console.log('FloatingMenuBar: Migrating pixel-based position to percentage-based');
|
|
100
|
+
finalPosition = migrateToPercentage(settings.position);
|
|
101
|
+
// Save migrated position immediately
|
|
102
|
+
SDKUI_Globals.userSettings.searchSettings.floatingMenuBar = {
|
|
103
|
+
orientation: validOrientation,
|
|
104
|
+
itemIds: validItemIds,
|
|
105
|
+
position: finalPosition,
|
|
106
|
+
positionFormat: 'percentage',
|
|
19
107
|
};
|
|
20
108
|
}
|
|
109
|
+
return {
|
|
110
|
+
orientation: validOrientation,
|
|
111
|
+
savedItemIds: validItemIds,
|
|
112
|
+
position: finalPosition,
|
|
113
|
+
};
|
|
21
114
|
}
|
|
22
115
|
catch (error) {
|
|
23
116
|
console.error('Failed to load FloatingMenuBar config:', error);
|
|
117
|
+
// Reset to defaults on any error
|
|
118
|
+
try {
|
|
119
|
+
resetFloatingBarSettings();
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
console.error('Failed to reset FloatingMenuBar settings:', e);
|
|
123
|
+
}
|
|
124
|
+
return getDefaultConfig();
|
|
24
125
|
}
|
|
25
|
-
return {
|
|
26
|
-
orientation: 'horizontal',
|
|
27
|
-
pinnedItemIds: new Set(),
|
|
28
|
-
savedItemIds: [],
|
|
29
|
-
};
|
|
30
126
|
};
|
|
31
127
|
const initialConfig = loadConfig();
|
|
32
128
|
const [state, setState] = useState({
|
|
33
|
-
position:
|
|
129
|
+
position: initialConfig.position, // Stored as percentage
|
|
34
130
|
isDragging: false,
|
|
35
|
-
isConfigMode: false,
|
|
36
131
|
orientation: initialConfig.orientation,
|
|
132
|
+
verticalDirection: 'down',
|
|
37
133
|
items: [],
|
|
38
134
|
draggedItemIndex: null,
|
|
39
135
|
});
|
|
40
136
|
const floatingRef = useRef(null);
|
|
41
137
|
const dragOffset = useRef({ x: 0, y: 0 });
|
|
42
|
-
const [pinnedItemIds, setPinnedItemIds] = useState(initialConfig.pinnedItemIds);
|
|
43
138
|
const [dragOverIndex, setDragOverIndex] = useState(null);
|
|
44
|
-
const [
|
|
45
|
-
|
|
139
|
+
const [pixelPosition, setPixelPosition] = useState({ x: 0, y: 0 }); // Calculated pixel position
|
|
140
|
+
const [isOrientationChanging, setIsOrientationChanging] = useState(false); // Hide bar during orientation transition
|
|
141
|
+
const containerSizeRef = useRef({ width: 0, height: 0 });
|
|
142
|
+
const positionBeforeOrientationChange = useRef(null);
|
|
46
143
|
const floatingBarItemIds = useRef(new Set());
|
|
47
144
|
const floatingBarItemNames = useRef(new Set());
|
|
48
|
-
|
|
145
|
+
const isSyncingFromExternal = useRef(false);
|
|
146
|
+
const isLocalChange = useRef(false);
|
|
147
|
+
const dragStartPosition = useRef(null);
|
|
148
|
+
const isItemsInitialized = useRef(false);
|
|
49
149
|
useEffect(() => {
|
|
50
150
|
floatingBarItemIds.current = new Set(state.items.map(i => i.id));
|
|
51
151
|
floatingBarItemNames.current = new Set(state.items.map(i => i.name));
|
|
52
152
|
}, [state.items]);
|
|
53
|
-
//
|
|
153
|
+
// Calculate pixel position from percentage when container size or position changes
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const updatePixelPosition = () => {
|
|
156
|
+
if (!containerRef.current || !floatingRef.current)
|
|
157
|
+
return;
|
|
158
|
+
const container = containerRef.current.getBoundingClientRect();
|
|
159
|
+
const floating = floatingRef.current.getBoundingClientRect();
|
|
160
|
+
const containerWidth = isConstrained ? container.width : window.innerWidth;
|
|
161
|
+
const containerHeight = isConstrained ? container.height : window.innerHeight;
|
|
162
|
+
containerSizeRef.current = { width: containerWidth, height: containerHeight };
|
|
163
|
+
let newX = percentToPixels(state.position.x, containerWidth);
|
|
164
|
+
let newY = percentToPixels(state.position.y, containerHeight);
|
|
165
|
+
newX = Math.max(0, Math.min(newX, containerWidth - floating.width));
|
|
166
|
+
newY = Math.max(0, Math.min(newY, containerHeight - floating.height));
|
|
167
|
+
setPixelPosition({ x: newX, y: newY });
|
|
168
|
+
};
|
|
169
|
+
updatePixelPosition();
|
|
170
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
171
|
+
updatePixelPosition();
|
|
172
|
+
});
|
|
173
|
+
if (containerRef.current) {
|
|
174
|
+
resizeObserver.observe(containerRef.current);
|
|
175
|
+
}
|
|
176
|
+
if (!isConstrained) {
|
|
177
|
+
window.addEventListener('resize', updatePixelPosition);
|
|
178
|
+
}
|
|
179
|
+
return () => {
|
|
180
|
+
resizeObserver.disconnect();
|
|
181
|
+
if (!isConstrained) {
|
|
182
|
+
window.removeEventListener('resize', updatePixelPosition);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}, [state.position, isConstrained]);
|
|
54
186
|
const flattenMenuItems = useCallback((items, parentPath = '') => {
|
|
55
187
|
const result = [];
|
|
56
188
|
items.forEach((item, index) => {
|
|
57
|
-
const itemId = `${parentPath}${item.name}-${index}`;
|
|
58
|
-
// Only add items that have onClick (final actions, not submenu parents)
|
|
189
|
+
const itemId = item.id || `${parentPath}${item.name}-${index}`;
|
|
59
190
|
if (item.onClick && !item.submenu) {
|
|
191
|
+
const isPinned = state.items.some(i => i.id === itemId);
|
|
60
192
|
result.push({
|
|
61
193
|
id: itemId,
|
|
62
194
|
name: item.name,
|
|
63
|
-
icon: item.icon || _jsx(
|
|
195
|
+
icon: item.icon || _jsx(IconPin, {}),
|
|
64
196
|
onClick: item.onClick,
|
|
65
197
|
disabled: item.disabled,
|
|
66
|
-
isPinned:
|
|
67
|
-
originalMenuItem: item,
|
|
198
|
+
isPinned: isPinned,
|
|
68
199
|
});
|
|
69
200
|
}
|
|
70
201
|
// Recursively process submenus
|
|
@@ -73,56 +204,126 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], storageKey = '
|
|
|
73
204
|
}
|
|
74
205
|
});
|
|
75
206
|
return result;
|
|
76
|
-
}, [
|
|
77
|
-
// Restore items on mount from savedItemIds
|
|
207
|
+
}, [state.items]);
|
|
208
|
+
// Restore items on mount (and when contextMenuItems become available) from savedItemIds
|
|
78
209
|
useEffect(() => {
|
|
79
|
-
|
|
80
|
-
|
|
210
|
+
// For enableConfigMode=false: always sync with fixedItems
|
|
211
|
+
if (!enableConfigMode) {
|
|
212
|
+
if (fixedItems.length > 0) {
|
|
213
|
+
setState(s => ({ ...s, items: fixedItems }));
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// For enableConfigMode=true: only initialize once
|
|
218
|
+
if (isItemsInitialized.current)
|
|
219
|
+
return;
|
|
220
|
+
// Wait until contextMenuItems is populated
|
|
221
|
+
if (contextMenuItems.length === 0)
|
|
222
|
+
return;
|
|
223
|
+
const flatItems = flattenMenuItems(contextMenuItems);
|
|
224
|
+
if (flatItems.length === 0)
|
|
225
|
+
return;
|
|
226
|
+
let itemsToSet = [];
|
|
227
|
+
if (initialConfig.savedItemIds.length > 0) {
|
|
81
228
|
// Restore items in the saved order from localStorage
|
|
82
229
|
const restoredItems = initialConfig.savedItemIds
|
|
230
|
+
.map((id) => {
|
|
231
|
+
if (id.startsWith('separator-')) {
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
name: 'Separator',
|
|
235
|
+
icon: _jsx(Separator, {}),
|
|
236
|
+
onClick: () => { },
|
|
237
|
+
isSeparator: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return flatItems.find(item => item.id === id);
|
|
241
|
+
})
|
|
242
|
+
.filter((item) => item !== undefined);
|
|
243
|
+
itemsToSet = restoredItems;
|
|
244
|
+
}
|
|
245
|
+
// If still empty, try defaultPinnedItems
|
|
246
|
+
if (itemsToSet.length === 0 && defaultPinnedItems.length > 0) {
|
|
247
|
+
const resolvedDefaultItems = defaultPinnedItems
|
|
83
248
|
.map((id) => flatItems.find(item => item.id === id))
|
|
84
249
|
.filter((item) => item !== undefined);
|
|
85
|
-
|
|
86
|
-
|
|
250
|
+
itemsToSet = resolvedDefaultItems;
|
|
251
|
+
}
|
|
252
|
+
if (itemsToSet.length > 0) {
|
|
253
|
+
isItemsInitialized.current = true;
|
|
254
|
+
setState(s => ({ ...s, items: itemsToSet }));
|
|
255
|
+
}
|
|
256
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
257
|
+
}, [contextMenuItems, fixedItems, enableConfigMode]);
|
|
258
|
+
// Sync with external pinnedItemIds when they change (from parent component)
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
// Skip sync if a local change was just made (e.g. right-click remove/add separator)
|
|
261
|
+
if (isLocalChange.current) {
|
|
262
|
+
isLocalChange.current = false;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (externalPinnedItemIds === undefined || !enableConfigMode)
|
|
266
|
+
return;
|
|
267
|
+
// Skip sync if external is empty but we have initialized items (first login case)
|
|
268
|
+
// The parent hook starts with [] but we've initialized with defaults
|
|
269
|
+
if (externalPinnedItemIds.length === 0 && state.items.length > 0)
|
|
270
|
+
return;
|
|
271
|
+
const flatItems = flattenMenuItems(contextMenuItems);
|
|
272
|
+
// Build items from external pinned IDs
|
|
273
|
+
const itemsFromExternal = externalPinnedItemIds
|
|
274
|
+
.map((id) => {
|
|
275
|
+
if (id.startsWith('separator-')) {
|
|
276
|
+
return {
|
|
277
|
+
id,
|
|
278
|
+
name: 'Separator',
|
|
279
|
+
icon: _jsx(Separator, {}),
|
|
280
|
+
onClick: () => { },
|
|
281
|
+
isSeparator: true,
|
|
282
|
+
};
|
|
87
283
|
}
|
|
284
|
+
return flatItems.find(item => item.id === id);
|
|
285
|
+
})
|
|
286
|
+
.filter((item) => item !== undefined);
|
|
287
|
+
// Only update if different
|
|
288
|
+
const currentIds = state.items.map(i => i.id).join(',');
|
|
289
|
+
const newIds = itemsFromExternal.map(i => i.id).join(',');
|
|
290
|
+
if (currentIds !== newIds) {
|
|
291
|
+
isSyncingFromExternal.current = true; // Mark that we're syncing from external
|
|
292
|
+
setState(s => ({ ...s, items: itemsFromExternal }));
|
|
88
293
|
}
|
|
89
|
-
}, []);
|
|
294
|
+
}, [externalPinnedItemIds, contextMenuItems, enableConfigMode, flattenMenuItems]);
|
|
90
295
|
const togglePin = useCallback((item) => {
|
|
91
296
|
setState(s => {
|
|
92
|
-
const isInFloatingBar = s.items.some(i => i.id === item.id
|
|
297
|
+
const isInFloatingBar = s.items.some(i => i.id === item.id);
|
|
93
298
|
if (isInFloatingBar) {
|
|
94
299
|
// Remove from floating bar
|
|
95
|
-
const newItems = s.items.filter(i => i.id !== item.id
|
|
300
|
+
const newItems = s.items.filter(i => i.id !== item.id);
|
|
96
301
|
return { ...s, items: newItems };
|
|
97
302
|
}
|
|
98
303
|
else {
|
|
99
304
|
// Add to floating bar
|
|
100
305
|
if (s.items.length >= maxItems) {
|
|
101
|
-
|
|
102
|
-
|
|
306
|
+
ShowAlert({
|
|
307
|
+
mode: 'warning',
|
|
308
|
+
title: 'Limite Massimo Raggiunto',
|
|
309
|
+
message: `Hai raggiunto il massimo di ${maxItems} elementi. Rimuovine uno prima di aggiungerne altri.`,
|
|
310
|
+
duration: 4000,
|
|
311
|
+
});
|
|
103
312
|
return s;
|
|
104
313
|
}
|
|
105
314
|
return { ...s, items: [...s.items, item] };
|
|
106
315
|
}
|
|
107
316
|
});
|
|
108
|
-
// Update pinned IDs for context menu items
|
|
109
|
-
setPinnedItemIds(prev => {
|
|
110
|
-
const newSet = new Set(prev);
|
|
111
|
-
if (newSet.has(item.id)) {
|
|
112
|
-
newSet.delete(item.id);
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
newSet.add(item.id);
|
|
116
|
-
}
|
|
117
|
-
return newSet;
|
|
118
|
-
});
|
|
119
317
|
}, [maxItems]);
|
|
120
318
|
// Get current item state (disabled and onClick) from contextMenuItems
|
|
121
|
-
const getCurrentItemState = useCallback((
|
|
319
|
+
const getCurrentItemState = useCallback((itemId) => {
|
|
122
320
|
const findInItems = (items) => {
|
|
123
|
-
for (
|
|
124
|
-
|
|
321
|
+
for (let i = 0; i < items.length; i++) {
|
|
322
|
+
const item = items[i];
|
|
323
|
+
// Match by ID if the item has one
|
|
324
|
+
if (item.id === itemId)
|
|
125
325
|
return item;
|
|
326
|
+
// Check in submenu
|
|
126
327
|
if (item.submenu) {
|
|
127
328
|
const found = findInItems(item.submenu);
|
|
128
329
|
if (found)
|
|
@@ -136,58 +337,92 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], storageKey = '
|
|
|
136
337
|
disabled: foundItem?.disabled,
|
|
137
338
|
onClick: foundItem?.onClick
|
|
138
339
|
};
|
|
139
|
-
}, [contextMenuItems]);
|
|
140
|
-
|
|
340
|
+
}, [contextMenuItems]);
|
|
341
|
+
// Remove trailing separators from items array
|
|
342
|
+
const removeTrailingSeparators = useCallback((items) => {
|
|
343
|
+
const result = [...items];
|
|
344
|
+
while (result.length > 0 && result.at(-1)?.isSeparator) {
|
|
345
|
+
result.pop();
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}, []);
|
|
349
|
+
// Create a new separator item
|
|
350
|
+
const createSeparator = useCallback(() => {
|
|
351
|
+
const separatorId = `separator-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
352
|
+
return {
|
|
353
|
+
id: separatorId,
|
|
354
|
+
name: 'Separator',
|
|
355
|
+
icon: _jsx(Separator, {}),
|
|
356
|
+
onClick: () => { },
|
|
357
|
+
isSeparator: true,
|
|
358
|
+
};
|
|
359
|
+
}, []);
|
|
360
|
+
// Add separator to items
|
|
361
|
+
const addSeparator = useCallback(() => {
|
|
362
|
+
if (state.items.length >= maxItems) {
|
|
363
|
+
ShowAlert({
|
|
364
|
+
mode: 'warning',
|
|
365
|
+
title: 'Limite Massimo Raggiunto',
|
|
366
|
+
message: `Hai raggiunto il massimo di ${maxItems} elementi. Rimuovine uno prima di aggiungerne altri.`,
|
|
367
|
+
duration: 4000,
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const separator = createSeparator();
|
|
372
|
+
setState(s => ({ ...s, items: [...s.items, separator] }));
|
|
373
|
+
}, [state.items.length, maxItems, createSeparator]);
|
|
374
|
+
const getContextMenuItemsWithPinIcons = useCallback(() => {
|
|
141
375
|
const flatItems = flattenMenuItems(contextMenuItems);
|
|
142
|
-
// Calculate current pinned items directly from state.items (not refs)
|
|
143
376
|
const currentItemIds = new Set(state.items.map(i => i.id));
|
|
144
|
-
const
|
|
145
|
-
const enhanceItems = (items) => {
|
|
377
|
+
const addPinIcons = (items) => {
|
|
146
378
|
return items.map(item => {
|
|
147
|
-
const flatItem = flatItems.find(fi => fi.
|
|
148
|
-
const itemId = flatItem?.id || '';
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const enhanced = {
|
|
379
|
+
const flatItem = flatItems.find(fi => fi.id === item.id);
|
|
380
|
+
const itemId = flatItem?.id || item.id || '';
|
|
381
|
+
const isPinned = currentItemIds.has(itemId);
|
|
382
|
+
const itemWithPin = {
|
|
152
383
|
...item,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
384
|
+
rightIconProps: flatItem ? {
|
|
385
|
+
icon: _jsx(IconPin, {}),
|
|
386
|
+
activeColor: 'red',
|
|
387
|
+
inactiveColor: 'black',
|
|
388
|
+
isActive: isPinned,
|
|
389
|
+
onClick: () => togglePin(flatItem),
|
|
158
390
|
} : undefined,
|
|
159
391
|
};
|
|
160
392
|
if (item.submenu) {
|
|
161
|
-
|
|
393
|
+
itemWithPin.submenu = addPinIcons(item.submenu);
|
|
162
394
|
}
|
|
163
|
-
return
|
|
395
|
+
return itemWithPin;
|
|
164
396
|
});
|
|
165
397
|
};
|
|
166
|
-
return
|
|
398
|
+
return addPinIcons(contextMenuItems);
|
|
167
399
|
}, [contextMenuItems, flattenMenuItems, togglePin, state.items]);
|
|
168
400
|
const handleMouseDown = (e) => {
|
|
169
|
-
if (state.isConfigMode)
|
|
170
|
-
return;
|
|
171
401
|
const containerRect = containerRef.current?.getBoundingClientRect();
|
|
172
402
|
if (containerRect) {
|
|
173
403
|
// Calculate drag offset based on positioning mode
|
|
174
404
|
if (isConstrained) {
|
|
175
405
|
// For absolute positioning, offset is relative to container
|
|
176
406
|
dragOffset.current = {
|
|
177
|
-
x: e.clientX - containerRect.left -
|
|
178
|
-
y: e.clientY - containerRect.top -
|
|
407
|
+
x: e.clientX - containerRect.left - pixelPosition.x,
|
|
408
|
+
y: e.clientY - containerRect.top - pixelPosition.y,
|
|
179
409
|
};
|
|
180
410
|
}
|
|
181
411
|
else {
|
|
182
412
|
// For fixed positioning, offset is relative to viewport
|
|
183
413
|
dragOffset.current = {
|
|
184
|
-
x: e.clientX -
|
|
185
|
-
y: e.clientY -
|
|
414
|
+
x: e.clientX - pixelPosition.x,
|
|
415
|
+
y: e.clientY - pixelPosition.y,
|
|
186
416
|
};
|
|
187
417
|
}
|
|
188
418
|
}
|
|
419
|
+
// Save starting position to detect if user actually moved the bar
|
|
420
|
+
dragStartPosition.current = { ...pixelPosition };
|
|
189
421
|
setState(s => ({ ...s, isDragging: true }));
|
|
190
422
|
};
|
|
423
|
+
const handleGripDoubleClick = () => {
|
|
424
|
+
toggleOrientation();
|
|
425
|
+
};
|
|
191
426
|
const handleMouseMove = useCallback((e) => {
|
|
192
427
|
if (!state.isDragging || !containerRef.current || !floatingRef.current)
|
|
193
428
|
return;
|
|
@@ -210,50 +445,230 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], storageKey = '
|
|
|
210
445
|
newX = Math.max(0, Math.min(newX, window.innerWidth - floating.width));
|
|
211
446
|
newY = Math.max(0, Math.min(newY, window.innerHeight - floating.height));
|
|
212
447
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
position: { x: newX, y: newY },
|
|
216
|
-
}));
|
|
448
|
+
// Update pixel position directly during drag
|
|
449
|
+
setPixelPosition({ x: newX, y: newY });
|
|
217
450
|
}, [state.isDragging, containerRef, isConstrained]);
|
|
218
451
|
const handleMouseUp = useCallback(() => {
|
|
219
|
-
|
|
220
|
-
|
|
452
|
+
if (state.isDragging && containerSizeRef.current.width > 0) {
|
|
453
|
+
// Convert final pixel position to percentage before updating state
|
|
454
|
+
const percentagePosition = {
|
|
455
|
+
x: pixelsToPercent(pixelPosition.x, containerSizeRef.current.width),
|
|
456
|
+
y: pixelsToPercent(pixelPosition.y, containerSizeRef.current.height),
|
|
457
|
+
};
|
|
458
|
+
// Only clear saved position if user actually moved the bar significantly (more than 5 pixels)
|
|
459
|
+
if (dragStartPosition.current) {
|
|
460
|
+
const dx = Math.abs(pixelPosition.x - dragStartPosition.current.x);
|
|
461
|
+
const dy = Math.abs(pixelPosition.y - dragStartPosition.current.y);
|
|
462
|
+
if (dx > 5 || dy > 5) {
|
|
463
|
+
positionBeforeOrientationChange.current = null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
dragStartPosition.current = null;
|
|
467
|
+
setState(s => ({
|
|
468
|
+
...s,
|
|
469
|
+
isDragging: false,
|
|
470
|
+
position: percentagePosition,
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
setState(s => ({ ...s, isDragging: false }));
|
|
475
|
+
}
|
|
476
|
+
}, [state.isDragging, pixelPosition]);
|
|
477
|
+
// Touch event handlers for tablet support
|
|
478
|
+
const handleTouchStart = (e) => {
|
|
479
|
+
const touch = e.touches[0];
|
|
480
|
+
const containerRect = containerRef.current?.getBoundingClientRect();
|
|
481
|
+
if (containerRect) {
|
|
482
|
+
if (isConstrained) {
|
|
483
|
+
dragOffset.current = {
|
|
484
|
+
x: touch.clientX - containerRect.left - pixelPosition.x,
|
|
485
|
+
y: touch.clientY - containerRect.top - pixelPosition.y,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
dragOffset.current = {
|
|
490
|
+
x: touch.clientX - pixelPosition.x,
|
|
491
|
+
y: touch.clientY - pixelPosition.y,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Save starting position to detect if user actually moved the bar
|
|
496
|
+
dragStartPosition.current = { ...pixelPosition };
|
|
497
|
+
setState(s => ({ ...s, isDragging: true }));
|
|
498
|
+
};
|
|
499
|
+
const handleTouchMove = useCallback((e) => {
|
|
500
|
+
if (!state.isDragging || !containerRef.current || !floatingRef.current)
|
|
501
|
+
return;
|
|
502
|
+
const touch = e.touches[0];
|
|
503
|
+
const container = containerRef.current.getBoundingClientRect();
|
|
504
|
+
const floating = floatingRef.current.getBoundingClientRect();
|
|
505
|
+
let newX, newY;
|
|
506
|
+
if (isConstrained) {
|
|
507
|
+
newX = touch.clientX - container.left - dragOffset.current.x;
|
|
508
|
+
newY = touch.clientY - container.top - dragOffset.current.y;
|
|
509
|
+
newX = Math.max(0, Math.min(newX, container.width - floating.width));
|
|
510
|
+
newY = Math.max(0, Math.min(newY, container.height - floating.height));
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
newX = touch.clientX - dragOffset.current.x;
|
|
514
|
+
newY = touch.clientY - dragOffset.current.y;
|
|
515
|
+
newX = Math.max(0, Math.min(newX, window.innerWidth - floating.width));
|
|
516
|
+
newY = Math.max(0, Math.min(newY, window.innerHeight - floating.height));
|
|
517
|
+
}
|
|
518
|
+
setPixelPosition({ x: newX, y: newY });
|
|
519
|
+
}, [state.isDragging, containerRef, isConstrained]);
|
|
520
|
+
const handleTouchEnd = useCallback(() => {
|
|
521
|
+
if (state.isDragging && containerSizeRef.current.width > 0) {
|
|
522
|
+
const percentagePosition = {
|
|
523
|
+
x: pixelsToPercent(pixelPosition.x, containerSizeRef.current.width),
|
|
524
|
+
y: pixelsToPercent(pixelPosition.y, containerSizeRef.current.height),
|
|
525
|
+
};
|
|
526
|
+
// Only clear saved position if user actually moved the bar significantly (more than 5 pixels)
|
|
527
|
+
if (dragStartPosition.current) {
|
|
528
|
+
const dx = Math.abs(pixelPosition.x - dragStartPosition.current.x);
|
|
529
|
+
const dy = Math.abs(pixelPosition.y - dragStartPosition.current.y);
|
|
530
|
+
if (dx > 5 || dy > 5) {
|
|
531
|
+
positionBeforeOrientationChange.current = null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
dragStartPosition.current = null;
|
|
535
|
+
setState(s => ({
|
|
536
|
+
...s,
|
|
537
|
+
isDragging: false,
|
|
538
|
+
position: percentagePosition,
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
setState(s => ({ ...s, isDragging: false }));
|
|
543
|
+
}
|
|
544
|
+
}, [state.isDragging, pixelPosition]);
|
|
221
545
|
useEffect(() => {
|
|
222
546
|
if (state.isDragging) {
|
|
223
547
|
document.addEventListener('mousemove', handleMouseMove);
|
|
224
548
|
document.addEventListener('mouseup', handleMouseUp);
|
|
549
|
+
document.addEventListener('touchmove', handleTouchMove);
|
|
550
|
+
document.addEventListener('touchend', handleTouchEnd);
|
|
225
551
|
return () => {
|
|
226
552
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
227
553
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
554
|
+
document.removeEventListener('touchmove', handleTouchMove);
|
|
555
|
+
document.removeEventListener('touchend', handleTouchEnd);
|
|
228
556
|
};
|
|
229
557
|
}
|
|
230
558
|
return undefined;
|
|
231
|
-
}, [state.isDragging, handleMouseMove, handleMouseUp]);
|
|
232
|
-
// Save to
|
|
559
|
+
}, [state.isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]);
|
|
560
|
+
// Save to SDKUI_Globals.userSettings
|
|
233
561
|
useEffect(() => {
|
|
562
|
+
if (!enableConfigMode)
|
|
563
|
+
return; // Don't save if config mode is disabled
|
|
564
|
+
const pinnedIds = state.items.map(item => item.id);
|
|
234
565
|
try {
|
|
235
|
-
|
|
566
|
+
// Replace the entire object to trigger the Proxy
|
|
567
|
+
SDKUI_Globals.userSettings.searchSettings.floatingMenuBar = {
|
|
236
568
|
orientation: state.orientation,
|
|
237
|
-
|
|
238
|
-
|
|
569
|
+
itemIds: pinnedIds,
|
|
570
|
+
position: state.position,
|
|
571
|
+
positionFormat: 'percentage',
|
|
239
572
|
};
|
|
240
|
-
localStorage.setItem(storageKey, JSON.stringify(config));
|
|
241
573
|
}
|
|
242
574
|
catch (error) {
|
|
243
575
|
console.error('Failed to save FloatingMenuBar config:', error);
|
|
244
576
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
577
|
+
// Notify parent about pin changes only if not syncing from external
|
|
578
|
+
// This prevents infinite loop: external change -> sync -> save -> onPinChange -> external change
|
|
579
|
+
if (!isSyncingFromExternal.current) {
|
|
580
|
+
onPinChange?.(pinnedIds);
|
|
581
|
+
}
|
|
582
|
+
isSyncingFromExternal.current = false; // Reset the flag
|
|
583
|
+
}, [state.orientation, state.items, state.position, enableConfigMode, onPinChange]);
|
|
249
584
|
const toggleOrientation = () => {
|
|
250
585
|
const newOrientation = state.orientation === 'horizontal' ? 'vertical' : 'horizontal';
|
|
251
|
-
//
|
|
586
|
+
// When switching from vertical back to horizontal, restore the original position
|
|
587
|
+
// Use visibility hiding only for this case to prevent flicker during position restoration
|
|
588
|
+
if (state.orientation === 'vertical' && newOrientation === 'horizontal') {
|
|
589
|
+
if (positionBeforeOrientationChange.current) {
|
|
590
|
+
setIsOrientationChanging(true); // Hide only when restoring position
|
|
591
|
+
const savedPosition = positionBeforeOrientationChange.current.position;
|
|
592
|
+
const savedPixelPosition = positionBeforeOrientationChange.current.pixelPosition;
|
|
593
|
+
setPixelPosition(savedPixelPosition);
|
|
594
|
+
setState(s => ({
|
|
595
|
+
...s,
|
|
596
|
+
orientation: newOrientation,
|
|
597
|
+
verticalDirection: 'down',
|
|
598
|
+
position: savedPosition,
|
|
599
|
+
}));
|
|
600
|
+
positionBeforeOrientationChange.current = null;
|
|
601
|
+
// Show the bar after the state has been applied
|
|
602
|
+
requestAnimationFrame(() => {
|
|
603
|
+
setIsOrientationChanging(false);
|
|
604
|
+
});
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// When switching to vertical, save current position and check if we need to expand upward
|
|
609
|
+
// Use opacity hiding (doesn't affect focus unlike visibility:hidden)
|
|
610
|
+
if (state.orientation === 'horizontal' && newOrientation === 'vertical') {
|
|
611
|
+
setIsOrientationChanging(true);
|
|
612
|
+
// Save the current position before changing orientation
|
|
613
|
+
positionBeforeOrientationChange.current = {
|
|
614
|
+
position: { ...state.position },
|
|
615
|
+
pixelPosition: { ...pixelPosition },
|
|
616
|
+
};
|
|
617
|
+
if (floatingRef.current) {
|
|
618
|
+
const floating = floatingRef.current.getBoundingClientRect();
|
|
619
|
+
const containerHeight = isConstrained && containerRef.current
|
|
620
|
+
? containerRef.current.getBoundingClientRect().height
|
|
621
|
+
: window.innerHeight;
|
|
622
|
+
// Estimate vertical height (horizontal width becomes vertical height)
|
|
623
|
+
const estimatedVerticalHeight = floating.width;
|
|
624
|
+
if (estimatedVerticalHeight > containerHeight - 70) {
|
|
625
|
+
ShowAlert({
|
|
626
|
+
mode: 'warning',
|
|
627
|
+
title: 'Troppi elementi',
|
|
628
|
+
message: 'Ci sono troppi elementi nella barra mobile per la modalità verticale.',
|
|
629
|
+
duration: 4000,
|
|
630
|
+
});
|
|
631
|
+
positionBeforeOrientationChange.current = null; // Clear saved position since we're not changing
|
|
632
|
+
setIsOrientationChanging(false); // Show bar again since we're not changing
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
// Check if we're in the bottom part of the screen and don't have enough space below
|
|
636
|
+
const spaceBelow = containerHeight - floating.bottom;
|
|
637
|
+
const spaceAbove = floating.top;
|
|
638
|
+
const needsVerticalSpace = estimatedVerticalHeight - floating.height; // Additional space needed
|
|
639
|
+
// If not enough space below but enough space above, expand upward
|
|
640
|
+
if (spaceBelow < needsVerticalSpace && spaceAbove >= needsVerticalSpace) {
|
|
641
|
+
// Calculate the new Y position so the bottom of the bar stays at the same place
|
|
642
|
+
const currentBottom = pixelPosition.y + floating.height;
|
|
643
|
+
const newPixelY = currentBottom - estimatedVerticalHeight;
|
|
644
|
+
setState(s => ({
|
|
645
|
+
...s,
|
|
646
|
+
orientation: newOrientation,
|
|
647
|
+
verticalDirection: 'up',
|
|
648
|
+
}));
|
|
649
|
+
// Update pixel position and percentage after orientation change
|
|
650
|
+
requestAnimationFrame(() => {
|
|
651
|
+
requestAnimationFrame(() => {
|
|
652
|
+
const newY = Math.max(0, newPixelY);
|
|
653
|
+
setPixelPosition(prev => ({ ...prev, y: newY }));
|
|
654
|
+
const newPercentY = pixelsToPercent(newY, containerHeight);
|
|
655
|
+
setState(s => ({
|
|
656
|
+
...s,
|
|
657
|
+
position: { ...s.position, y: newPercentY },
|
|
658
|
+
}));
|
|
659
|
+
setIsOrientationChanging(false);
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Default case: just change orientation without special positioning
|
|
252
667
|
setState(s => ({
|
|
253
668
|
...s,
|
|
254
669
|
orientation: newOrientation,
|
|
670
|
+
verticalDirection: 'down',
|
|
255
671
|
}));
|
|
256
|
-
// Then, after DOM updates, adjust position to stay in bounds
|
|
257
672
|
requestAnimationFrame(() => {
|
|
258
673
|
requestAnimationFrame(() => {
|
|
259
674
|
if (containerRef.current && floatingRef.current) {
|
|
@@ -278,31 +693,97 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], storageKey = '
|
|
|
278
693
|
}));
|
|
279
694
|
}
|
|
280
695
|
}
|
|
696
|
+
setIsOrientationChanging(false);
|
|
281
697
|
});
|
|
282
698
|
});
|
|
283
699
|
};
|
|
284
700
|
const removeItem = (itemId) => {
|
|
701
|
+
isLocalChange.current = true;
|
|
285
702
|
setState(s => ({
|
|
286
703
|
...s,
|
|
287
704
|
items: s.items.filter(item => item.id !== itemId),
|
|
288
705
|
}));
|
|
289
|
-
// Also remove from pinned items if it was pinned
|
|
290
|
-
setPinnedItemIds(prev => {
|
|
291
|
-
const newSet = new Set(prev);
|
|
292
|
-
newSet.delete(itemId);
|
|
293
|
-
return newSet;
|
|
294
|
-
});
|
|
295
706
|
};
|
|
707
|
+
const getItemRightClickMenuItems = useCallback((item, index) => {
|
|
708
|
+
const hasSeparatorOnRight = index < state.items.length - 1 && state.items[index + 1]?.isSeparator;
|
|
709
|
+
const hasSeparatorOnLeft = index > 0 && state.items[index - 1]?.isSeparator;
|
|
710
|
+
return [
|
|
711
|
+
{
|
|
712
|
+
name: 'Rimuovi',
|
|
713
|
+
icon: _jsx(IconDelete, { fontSize: 16 }),
|
|
714
|
+
onClick: () => removeItem(item.id),
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
name: 'Aggiungi separatore a sinistra',
|
|
718
|
+
icon: _jsx(IconSeparator, { fontSize: 16 }),
|
|
719
|
+
disabled: hasSeparatorOnLeft,
|
|
720
|
+
onClick: () => {
|
|
721
|
+
isLocalChange.current = true;
|
|
722
|
+
const separator = createSeparator();
|
|
723
|
+
setState(s => {
|
|
724
|
+
const newItems = [...s.items];
|
|
725
|
+
newItems.splice(index, 0, separator);
|
|
726
|
+
return { ...s, items: newItems };
|
|
727
|
+
});
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
name: 'Aggiungi separatore a destra',
|
|
732
|
+
icon: _jsx(IconSeparator, { style: { transform: 'rotate(-90deg)' }, fontSize: 16 }),
|
|
733
|
+
disabled: hasSeparatorOnRight,
|
|
734
|
+
onClick: () => {
|
|
735
|
+
isLocalChange.current = true;
|
|
736
|
+
const separator = createSeparator();
|
|
737
|
+
setState(s => {
|
|
738
|
+
const newItems = [...s.items];
|
|
739
|
+
newItems.splice(index + 1, 0, separator);
|
|
740
|
+
return { ...s, items: newItems };
|
|
741
|
+
});
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
beginGroup: true,
|
|
746
|
+
name: state.orientation === 'horizontal' ? 'Floating bar verticale' : 'Floating bar orizzontale',
|
|
747
|
+
icon: _jsx(IconRotate, { fontSize: 16 }),
|
|
748
|
+
onClick: toggleOrientation,
|
|
749
|
+
},
|
|
750
|
+
];
|
|
751
|
+
}, [state.items, state.orientation, removeItem, createSeparator, toggleOrientation]);
|
|
752
|
+
const getSeparatorRightClickMenuItems = useCallback((index) => {
|
|
753
|
+
return [
|
|
754
|
+
{
|
|
755
|
+
name: 'Rimuovi',
|
|
756
|
+
icon: _jsx(IconDelete, { fontSize: 16 }),
|
|
757
|
+
onClick: () => removeItem(state.items[index].id),
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
name: 'Aggiungi separatore a sinistra',
|
|
761
|
+
icon: _jsx(IconSeparator, { fontSize: 16 }),
|
|
762
|
+
disabled: true,
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: 'Aggiungi separatore a destra',
|
|
766
|
+
icon: _jsx(IconSeparator, { style: { transform: 'rotate(-90deg)' }, fontSize: 16 }),
|
|
767
|
+
disabled: true,
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
beginGroup: true,
|
|
771
|
+
name: state.orientation === 'horizontal' ? 'Floating bar verticale' : 'Floating bar orizzontale',
|
|
772
|
+
icon: _jsx(IconRotate, { fontSize: 16 }),
|
|
773
|
+
onClick: toggleOrientation,
|
|
774
|
+
},
|
|
775
|
+
];
|
|
776
|
+
}, [state.items, state.orientation, removeItem, toggleOrientation]);
|
|
296
777
|
// Drag and drop for reordering
|
|
297
778
|
const handleDragStart = (e, index) => {
|
|
298
|
-
if (!
|
|
779
|
+
if (!enableConfigMode)
|
|
299
780
|
return;
|
|
300
781
|
e.dataTransfer.effectAllowed = 'move';
|
|
301
782
|
e.dataTransfer.setData('text/plain', index.toString());
|
|
302
783
|
setState(s => ({ ...s, draggedItemIndex: index }));
|
|
303
784
|
};
|
|
304
785
|
const handleDragEnter = (e, index) => {
|
|
305
|
-
if (!
|
|
786
|
+
if (!enableConfigMode)
|
|
306
787
|
return;
|
|
307
788
|
e.preventDefault();
|
|
308
789
|
if (state.draggedItemIndex !== index) {
|
|
@@ -310,13 +791,13 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], storageKey = '
|
|
|
310
791
|
}
|
|
311
792
|
};
|
|
312
793
|
const handleDragOver = (e) => {
|
|
313
|
-
if (!
|
|
794
|
+
if (!enableConfigMode)
|
|
314
795
|
return;
|
|
315
796
|
e.preventDefault();
|
|
316
797
|
e.dataTransfer.dropEffect = 'move';
|
|
317
798
|
};
|
|
318
799
|
const handleDragLeave = (_e, index) => {
|
|
319
|
-
if (!
|
|
800
|
+
if (!enableConfigMode)
|
|
320
801
|
return;
|
|
321
802
|
// Only clear if we're actually leaving this specific item
|
|
322
803
|
if (dragOverIndex === index) {
|
|
@@ -324,7 +805,7 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], storageKey = '
|
|
|
324
805
|
}
|
|
325
806
|
};
|
|
326
807
|
const handleDrop = (e, dropIndex) => {
|
|
327
|
-
if (!
|
|
808
|
+
if (!enableConfigMode || state.draggedItemIndex === null)
|
|
328
809
|
return;
|
|
329
810
|
e.preventDefault();
|
|
330
811
|
e.stopPropagation();
|
|
@@ -333,6 +814,7 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], storageKey = '
|
|
|
333
814
|
setDragOverIndex(null);
|
|
334
815
|
return;
|
|
335
816
|
}
|
|
817
|
+
isLocalChange.current = true;
|
|
336
818
|
const newItems = [...state.items];
|
|
337
819
|
const [draggedItem] = newItems.splice(dragIndex, 1);
|
|
338
820
|
newItems.splice(dropIndex, 0, draggedItem);
|
|
@@ -347,24 +829,37 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], storageKey = '
|
|
|
347
829
|
setState(s => ({ ...s, draggedItemIndex: null }));
|
|
348
830
|
setDragOverIndex(null);
|
|
349
831
|
};
|
|
350
|
-
return (_jsxs(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
832
|
+
return (_jsxs(S.FloatingContainer, { ref: floatingRef, "$x": pixelPosition.x, "$y": pixelPosition.y, "$orientation": state.orientation, "$verticalDirection": state.verticalDirection, "$isDragging": state.isDragging, "$isConfigMode": false, "$isConstrained": isConstrained, "$isHidden": isOrientationChanging, "$bgColor": bgColor, onContextMenu: undefined, children: [_jsx(ContextMenu, { items: [
|
|
833
|
+
{
|
|
834
|
+
name: state.orientation === 'horizontal' ? 'Floating bar verticale' : 'Floating bar orizzontale',
|
|
835
|
+
icon: _jsx(IconRotate, { fontSize: 16, style: { transform: state.orientation === 'horizontal' ? 'rotate(90deg)' : 'rotate(0deg)' } }),
|
|
836
|
+
onClick: toggleOrientation,
|
|
837
|
+
},
|
|
838
|
+
], trigger: "right", children: _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) => {
|
|
839
|
+
// Handle separator items
|
|
840
|
+
if (item.isSeparator) {
|
|
841
|
+
return (_jsx(S.DraggableItem, { "$isDragging": state.draggedItemIndex === index, "$isDragOver": dragOverIndex === index && state.draggedItemIndex !== index, draggable: enableConfigMode, 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: enableConfigMode ? (_jsx(ContextMenu, { items: getSeparatorRightClickMenuItems(index), trigger: "right", children: _jsx(S.ItemSeparator, { "$orientation": state.orientation, "$isConfigMode": false }) })) : (_jsx(S.ItemSeparator, { "$orientation": state.orientation, "$isConfigMode": false })) }, item.id));
|
|
842
|
+
}
|
|
843
|
+
// Get current state (disabled and onClick) from contextMenuItems
|
|
844
|
+
const currentState = getCurrentItemState(item.id);
|
|
845
|
+
// Prefer currentState.disabled if contextMenuItems has items, otherwise use item.disabled
|
|
846
|
+
const isDisabled = (contextMenuItems.length > 0 && currentState.disabled !== undefined)
|
|
847
|
+
? currentState.disabled === true
|
|
848
|
+
: item.disabled === true;
|
|
849
|
+
const currentOnClick = currentState.onClick || item.onClick; // Fallback to stored onClick if not found
|
|
850
|
+
return (_jsx(S.DraggableItem, { "$isDragging": state.draggedItemIndex === index, "$isDragOver": dragOverIndex === index && state.draggedItemIndex !== index, draggable: enableConfigMode, 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: enableConfigMode ? (_jsx(ContextMenu, { items: getItemRightClickMenuItems(item, index), trigger: "right", children: item.staticItem ? (_jsx("div", { children: item.staticItem })) : (_jsx(TMTooltip, { content: item.name, position: "bottom", children: _jsx(S.MenuButton, { onClick: () => {
|
|
851
|
+
if (isDisabled)
|
|
852
|
+
return;
|
|
853
|
+
if (currentOnClick) {
|
|
854
|
+
currentOnClick();
|
|
855
|
+
}
|
|
856
|
+
}, disabled: isDisabled, "$isActive": item.isToggle, children: item.icon }) })) })) : (item.staticItem ? (_jsx("div", { children: item.staticItem })) : (_jsx(TMTooltip, { content: item.name, position: "bottom", children: _jsx(S.MenuButton, { onClick: () => {
|
|
857
|
+
if (isDisabled)
|
|
858
|
+
return;
|
|
859
|
+
if (currentOnClick) {
|
|
860
|
+
currentOnClick();
|
|
861
|
+
}
|
|
862
|
+
}, disabled: isDisabled, "$isActive": item.isToggle, children: item.icon }) }))) }, item.id));
|
|
863
|
+
}), enableConfigMode && hasContextMenu && contextMenuItems.length > 0 && (_jsx(ContextMenu, { items: getContextMenuItemsWithPinIcons(), trigger: "left", keepOpenOnClick: false, children: _jsx(S.ContextMenuButton, { children: _jsx(IconMenuVertical, {}) }) }))] }));
|
|
369
864
|
};
|
|
370
865
|
export default TMFloatingMenuBar;
|