@topconsultnpm/sdkui-react 6.19.0-dev2.36 → 6.19.0-dev2.38
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/base/TMCustomButton.js +1 -1
- package/lib/components/base/TMWaitPanel.js +8 -2
- package/lib/components/features/documents/TMDcmtPreview.js +17 -1
- package/lib/helper/SDKUI_Localizator.d.ts +1 -0
- package/lib/helper/SDKUI_Localizator.js +10 -0
- package/lib/helper/dcmtsHelper.d.ts +2 -2
- package/lib/helper/dcmtsHelper.js +14 -17
- package/package.json +2 -2
- package/lib/components/NewComponents/ContextMenu/TMContextMenu.d.ts +0 -4
- package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +0 -187
- package/lib/components/NewComponents/ContextMenu/hooks.d.ts +0 -11
- package/lib/components/NewComponents/ContextMenu/hooks.js +0 -48
- package/lib/components/NewComponents/ContextMenu/index.d.ts +0 -2
- package/lib/components/NewComponents/ContextMenu/index.js +0 -1
- package/lib/components/NewComponents/ContextMenu/styles.d.ts +0 -27
- package/lib/components/NewComponents/ContextMenu/styles.js +0 -308
- package/lib/components/NewComponents/ContextMenu/types.d.ts +0 -26
- package/lib/components/NewComponents/ContextMenu/types.js +0 -1
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.d.ts +0 -4
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +0 -370
- package/lib/components/NewComponents/FloatingMenuBar/index.d.ts +0 -2
- package/lib/components/NewComponents/FloatingMenuBar/index.js +0 -2
- package/lib/components/NewComponents/FloatingMenuBar/styles.d.ts +0 -38
- package/lib/components/NewComponents/FloatingMenuBar/styles.js +0 -267
- package/lib/components/NewComponents/FloatingMenuBar/types.d.ts +0 -30
- package/lib/components/NewComponents/FloatingMenuBar/types.js +0 -1
- 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
|
@@ -20,7 +20,7 @@ const TMCustomButton = (props) => {
|
|
|
20
20
|
const { appName: scriptUrl, arguments: args } = button;
|
|
21
21
|
const iframeRef = useRef(null);
|
|
22
22
|
const attributes = useMemo(() => processButtonAttributes(args, formData), [args, formData]);
|
|
23
|
-
const selectedItemsProcessed = useMemo(() => processSelectedItems(selectedItems), [selectedItems]);
|
|
23
|
+
const selectedItemsProcessed = useMemo(() => processSelectedItems(args, formData, selectedItems), [args, formData, selectedItems]);
|
|
24
24
|
const RunOnce = button.mode === "RunOnce";
|
|
25
25
|
const [loading, setLoading] = useState(true);
|
|
26
26
|
const [error, setError] = useState(false);
|
|
@@ -33,8 +33,14 @@ const StyledProgressText = styled.p ` font-weight: bold; color: #333; margin: 0;
|
|
|
33
33
|
const StyledMessage = styled.p ` color: #666; font-size: 0.9em; margin-top: 10px; `;
|
|
34
34
|
const StyledAbortButton = styled.button ` background: #ff4d4d; color: white; border: none; border-radius: 5px; padding: 10px 20px; font-size: 1em; cursor: pointer; margin-top: 20px; &:hover { background: #ff6666; } `;
|
|
35
35
|
export const TMWaitPanel = (props) => {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const calculateProgress = (value = 0, maxValue = 0) => {
|
|
37
|
+
if (maxValue === 0)
|
|
38
|
+
return 0;
|
|
39
|
+
const progress = (value / maxValue) * 100;
|
|
40
|
+
return Number.isFinite(progress) ? progress : 0;
|
|
41
|
+
};
|
|
42
|
+
let progressValue1 = calculateProgress(props.valuePrimary, props.maxValuePrimary);
|
|
43
|
+
let progressValue2 = calculateProgress(props.valueSecondary, props.maxValueSecondary);
|
|
38
44
|
return (_jsx(StyledWaitPanelOverlay, { children: _jsxs(StyledWaitPanel, { "$height": (props.showPrimary && props.showSecondary) ? '350px' : '250px', children: [_jsx(StyledTitle, { children: props.title }), props.showPrimary &&
|
|
39
45
|
_jsxs("div", { style: { width: '100%', height: '100px' }, children: [_jsx(StyledProgressBarContainer, { children: _jsx(StyledProgressBar, { style: { width: `${progressValue1.toFixed(2)}%` } }) }), _jsxs(StyledProgressText, { children: [progressValue1.toFixed(2), "%"] }), _jsx(StyledMessage, { children: props.textPrimary })] }), props.showSecondary &&
|
|
40
46
|
_jsxs("div", { style: { width: '100%', height: '100px' }, children: [_jsx(StyledProgressBarContainer, { children: _jsx(StyledProgressBar, { style: { width: `${progressValue2.toFixed(2)}%` } }) }), _jsxs(StyledProgressText, { children: [progressValue2.toFixed(2), "%"] }), _jsx(StyledMessage, { children: props.textSecondary })] }), props.isCancelable && _jsx(StyledAbortButton, { onClick: () => props.onAbortClick?.(props.abortController), children: SDKUI_Localizator.Abort })] }) }));
|
|
@@ -15,11 +15,18 @@ import { TMSaveFormButtonPrevious, TMSaveFormButtonNext } from '../../forms/TMSa
|
|
|
15
15
|
import { StyledAnimatedComponentOpacity } from '../../base/Styled';
|
|
16
16
|
import TMPanel from '../../base/TMPanel';
|
|
17
17
|
import TMTooltip from '../../base/TMTooltip';
|
|
18
|
+
const ErrorContent = ({ error, isAbortError, onRetry }) => {
|
|
19
|
+
if (isAbortError) {
|
|
20
|
+
return (_jsx(StyledAnimatedComponentOpacity, { style: { width: '100%', height: '100%' }, children: _jsxs(StyledPanelStatusContainer, { children: [_jsx(IconCloseOutline, { fontSize: 92, color: TMColors.error }), _jsxs(StyledPreviewNotAvailable, { children: [_jsx("div", { children: error }), _jsx("div", { children: SDKUI_Localizator.PreviewNotAvailable })] }), _jsx(TMButton, { caption: SDKUI_Localizator.TryAgain, onClick: onRetry, showTooltip: false })] }) }));
|
|
21
|
+
}
|
|
22
|
+
return _jsx(TMNothingToShow, { icon: _jsx(IconCloseOutline, { fontSize: 92, color: TMColors.error }), text: error });
|
|
23
|
+
};
|
|
18
24
|
const TMDcmtPreview = ({ dcmtData, isResizingActive, isVisible, canNext, canPrev, onClosePanel, onNext, onPrev, allowMaximize = true, onMaximizePanel }) => {
|
|
19
25
|
const [dcmtBlob, setDcmtBlob] = useState(undefined);
|
|
20
26
|
const [showPreview, setShowPreview] = useState(false);
|
|
21
27
|
const [isFromCache, setIsFromCache] = useState(false);
|
|
22
28
|
const [error, setError] = useState('');
|
|
29
|
+
const [isAbortError, setIsAbortError] = useState(false);
|
|
23
30
|
const { abortController, showWaitPanel, waitPanelTitle, showPrimary, waitPanelTextPrimary, waitPanelValuePrimary, waitPanelMaxValuePrimary, showSecondary, waitPanelTextSecondary, waitPanelValueSecondary, waitPanelMaxValueSecondary, getDcmtFileAsync, clearDcmtsFileCache, removeDcmtsFileCache, isDcmtFileInCache } = useDcmtOperations();
|
|
24
31
|
const cacheKey = dcmtData ? `${dcmtData.tid}-${dcmtData.did}` : '00';
|
|
25
32
|
const [hasLoadedDataOnce, setHasLoadedDataOnce] = useState(false);
|
|
@@ -29,6 +36,7 @@ const TMDcmtPreview = ({ dcmtData, isResizingActive, isVisible, canNext, canPrev
|
|
|
29
36
|
setLastLoadedDid(undefined); // Reset
|
|
30
37
|
setDcmtBlob(undefined);
|
|
31
38
|
setError('');
|
|
39
|
+
setIsAbortError(false);
|
|
32
40
|
setShowPreview(false);
|
|
33
41
|
return;
|
|
34
42
|
}
|
|
@@ -42,6 +50,7 @@ const TMDcmtPreview = ({ dcmtData, isResizingActive, isVisible, canNext, canPrev
|
|
|
42
50
|
if (shouldFetch) {
|
|
43
51
|
setDcmtBlob(undefined);
|
|
44
52
|
setError('');
|
|
53
|
+
setIsAbortError(false);
|
|
45
54
|
if ((extensionHandler(dcmtData.fileExt) !== FileExtensionHandler.NONE) && ((dcmtData.fileSize ?? 0) <= (SDKUI_Globals.userSettings.searchSettings.previewThreshold * 1024))) {
|
|
46
55
|
loadDocumentWithCache();
|
|
47
56
|
setShowPreview(true);
|
|
@@ -65,15 +74,19 @@ const TMDcmtPreview = ({ dcmtData, isResizingActive, isVisible, canNext, canPrev
|
|
|
65
74
|
let dcmtFile = await getDcmtFileAsync({ TID: dcmtData?.tid, DID: dcmtData?.did, FILEEXT: dcmtData?.fileExt }, rfo, 'Anteprima', false);
|
|
66
75
|
setDcmtBlob(dcmtFile?.file);
|
|
67
76
|
setIsFromCache(!!dcmtFile?.isFromCache);
|
|
77
|
+
setError('');
|
|
78
|
+
setIsAbortError(false);
|
|
68
79
|
}
|
|
69
80
|
catch (ex) {
|
|
70
81
|
const err = ex;
|
|
71
82
|
if (err.name === 'CanceledError') {
|
|
72
83
|
setError('Operazione annullata.');
|
|
84
|
+
setIsAbortError(true);
|
|
73
85
|
ShowAlert({ message: err.message, mode: 'warning', duration: 3000, title: 'Abort' });
|
|
74
86
|
}
|
|
75
87
|
else {
|
|
76
88
|
setError(getExceptionMessage(ex));
|
|
89
|
+
setIsAbortError(false);
|
|
77
90
|
TMExceptionBoxManager.show({ exception: ex });
|
|
78
91
|
}
|
|
79
92
|
}
|
|
@@ -101,6 +114,9 @@ const TMDcmtPreview = ({ dcmtData, isResizingActive, isVisible, canNext, canPrev
|
|
|
101
114
|
const reOpenDcmt = async () => {
|
|
102
115
|
removeDcmtsFileCache(cacheKey);
|
|
103
116
|
setIsFromCache(false);
|
|
117
|
+
setError('');
|
|
118
|
+
setIsAbortError(false);
|
|
119
|
+
setDcmtBlob(undefined);
|
|
104
120
|
try {
|
|
105
121
|
await loadDocumentWithCache();
|
|
106
122
|
}
|
|
@@ -112,7 +128,7 @@ const TMDcmtPreview = ({ dcmtData, isResizingActive, isVisible, canNext, canPrev
|
|
|
112
128
|
{ icon: _jsx(IconCloseCircle, {}), text: SDKUI_Localizator.RemoveFromCache, onClick: () => { removeDcmtsFileCache(cacheKey); setIsFromCache(false); } },
|
|
113
129
|
{ icon: _jsx(IconClear, {}), text: SDKUI_Localizator.ClearCache, onClick: () => { clearDcmtsFileCache(); setIsFromCache(false); } },
|
|
114
130
|
] }, "btn13") }), _jsx(StyledHeaderIcon, { onClick: reOpenDcmt, "$color": TMColors.primaryColor, children: _jsx(TMTooltip, { content: SDKUI_Localizator.ReopenDocument, children: _jsx(IconRefresh, {}) }) })] }), children: error
|
|
115
|
-
? _jsx(
|
|
131
|
+
? _jsx(ErrorContent, { error: error, isAbortError: isAbortError, onRetry: reOpenDcmt })
|
|
116
132
|
: renderedPreview(dcmtData?.tid, dcmtData?.did, dcmtData?.fileExt, dcmtData?.fileSize, dcmtData?.fileCount, extensionHandler(dcmtData?.fileExt), showPreview, isResizingActive, () => { loadDocumentWithCache(); setShowPreview(true); }, dcmtBlob) }) }));
|
|
117
133
|
};
|
|
118
134
|
export default TMDcmtPreview;
|
|
@@ -505,6 +505,7 @@ export declare class SDKUI_Localizator {
|
|
|
505
505
|
static get RemovingFromList(): string;
|
|
506
506
|
static get RememberCredentials(): "Anmeldedaten merken" | "Remember credentials" | "Recordar credenciales" | "Se souvenir des identifiants" | "Lembrar credenciais" | "Ricorda credenziali";
|
|
507
507
|
static get ReopenDocument(): string;
|
|
508
|
+
static get TryAgain(): string;
|
|
508
509
|
static get ReplaceDocument(): "Dokument ersetzen" | "Replace Document" | "Reemplazar Documento" | "Remplacer le Document" | "Substituir Documento" | "Sostituisci Documento";
|
|
509
510
|
static get Request(): string;
|
|
510
511
|
static get RequestTo(): string;
|
|
@@ -4978,6 +4978,16 @@ export class SDKUI_Localizator {
|
|
|
4978
4978
|
default: return "Riapri documento";
|
|
4979
4979
|
}
|
|
4980
4980
|
}
|
|
4981
|
+
static get TryAgain() {
|
|
4982
|
+
switch (this._cultureID) {
|
|
4983
|
+
case CultureIDs.De_DE: return "Erneut versuchen";
|
|
4984
|
+
case CultureIDs.En_US: return "Try Again";
|
|
4985
|
+
case CultureIDs.Es_ES: return "Intentar de nuevo";
|
|
4986
|
+
case CultureIDs.Fr_FR: return "Réessayer";
|
|
4987
|
+
case CultureIDs.Pt_PT: return "Tentar novamente";
|
|
4988
|
+
default: return "Riprova";
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4981
4991
|
static get ReplaceDocument() {
|
|
4982
4992
|
switch (this._cultureID) {
|
|
4983
4993
|
case CultureIDs.De_DE: return "Dokument ersetzen";
|
|
@@ -4,5 +4,5 @@ export declare const hasDetailRelations: (mTID: number | undefined) => Promise<b
|
|
|
4
4
|
/** Check if dcmtType (mTID) has configured Master or Many-to-Many relations */
|
|
5
5
|
export declare const hasMasterRelations: (mTID: number | undefined) => Promise<boolean>;
|
|
6
6
|
export declare const isXMLFileExt: (fileExt: string | undefined) => boolean;
|
|
7
|
-
export declare const processButtonAttributes: (args: string | undefined, formData: MetadataValueDescriptorEx[] | undefined) => string[] | undefined;
|
|
8
|
-
export declare const processSelectedItems: (selectedItems: Array<any> | undefined) => any[]
|
|
7
|
+
export declare const processButtonAttributes: (args: string | undefined, formData: MetadataValueDescriptorEx[] | undefined) => (string | null)[] | undefined;
|
|
8
|
+
export declare const processSelectedItems: (args: string | undefined, formData: MetadataValueDescriptorEx[] | undefined, selectedItems: Array<any> | undefined) => any[][];
|
|
@@ -25,29 +25,26 @@ export const isXMLFileExt = (fileExt) => {
|
|
|
25
25
|
};
|
|
26
26
|
/*utility functions for TMCustomButton*/
|
|
27
27
|
export const processButtonAttributes = (args, formData) => args && formData ? formDataMap(formData, args) : undefined;
|
|
28
|
-
export const processSelectedItems = (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}, {});
|
|
38
|
-
});
|
|
39
|
-
const formDataMap = (data, args) => {
|
|
28
|
+
export const processSelectedItems = (args, formData, selectedItems) => {
|
|
29
|
+
if (!args || !selectedItems)
|
|
30
|
+
return [];
|
|
31
|
+
const MidList = formData ? formDataMap(formData, args, true) : [];
|
|
32
|
+
return selectedItems.map(item =>
|
|
33
|
+
//salvo il did come primo elemento dell'array
|
|
34
|
+
[item["DID"], ...MidList.map(key => key && item[key])]);
|
|
35
|
+
};
|
|
36
|
+
const formDataMap = (data, args, returnMid = false) => {
|
|
40
37
|
const tokens = args.match(/\{@?[^}]+\}/g) || [];
|
|
41
38
|
return tokens.map(token => {
|
|
42
39
|
if (token.startsWith('{@')) {
|
|
43
40
|
// Campo dinamico: {@campo} -> cerca in formData
|
|
44
|
-
const fieldName = token.slice(2, -1);
|
|
41
|
+
const fieldName = token.slice(2, -1);
|
|
45
42
|
const md = data.find(md => md.md?.name === fieldName);
|
|
46
|
-
return md?.value;
|
|
43
|
+
return returnMid ? (md ? `${md.tid}_${md.mid}` : null) : (md?.value ?? null);
|
|
47
44
|
}
|
|
48
45
|
else {
|
|
49
|
-
// Campo statico: {valore} -> ritorna il valore
|
|
50
|
-
return token.slice(1, -1);
|
|
46
|
+
// Campo statico: {valore} -> ritorna il valore o null
|
|
47
|
+
return returnMid ? null : token.slice(1, -1);
|
|
51
48
|
}
|
|
52
|
-
})
|
|
49
|
+
});
|
|
53
50
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topconsultnpm/sdkui-react",
|
|
3
|
-
"version": "6.19.0-dev2.
|
|
3
|
+
"version": "6.19.0-dev2.38",
|
|
4
4
|
"description": "",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"lib"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@topconsultnpm/sdk-ts": "6.19.0-dev2.
|
|
42
|
+
"@topconsultnpm/sdk-ts": "6.19.0-dev2.5",
|
|
43
43
|
"buffer": "^6.0.3",
|
|
44
44
|
"devextreme": "25.1.7",
|
|
45
45
|
"devextreme-react": "25.1.7",
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useRef, useEffect } from 'react';
|
|
3
|
-
import * as S from './styles';
|
|
4
|
-
import { useIsMobile, useMenuPosition } from './hooks';
|
|
5
|
-
const TMContextMenu = ({ items, trigger = 'right', children }) => {
|
|
6
|
-
const [menuState, setMenuState] = useState({
|
|
7
|
-
visible: false,
|
|
8
|
-
position: { x: 0, y: 0 },
|
|
9
|
-
submenuStack: [items],
|
|
10
|
-
parentNames: [],
|
|
11
|
-
});
|
|
12
|
-
const [hoveredSubmenus, setHoveredSubmenus] = useState([]);
|
|
13
|
-
const isMobile = useIsMobile();
|
|
14
|
-
const menuRef = useRef(null);
|
|
15
|
-
const triggerRef = useRef(null);
|
|
16
|
-
const submenuTimeoutRef = useRef(null);
|
|
17
|
-
const { openLeft, openUp } = useMenuPosition(menuRef, menuState.position);
|
|
18
|
-
const handleClose = () => {
|
|
19
|
-
setMenuState(prev => ({
|
|
20
|
-
...prev,
|
|
21
|
-
visible: false,
|
|
22
|
-
submenuStack: [items],
|
|
23
|
-
parentNames: [],
|
|
24
|
-
}));
|
|
25
|
-
setHoveredSubmenus([]);
|
|
26
|
-
};
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
if (!menuState.visible)
|
|
29
|
-
return;
|
|
30
|
-
const handleClickOutside = (event) => {
|
|
31
|
-
const target = event.target;
|
|
32
|
-
// Check if click is inside main menu
|
|
33
|
-
if (menuRef.current?.contains(target)) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
// Check if click is inside any submenu
|
|
37
|
-
const submenus = document.querySelectorAll('[data-submenu="true"]');
|
|
38
|
-
for (const submenu of Array.from(submenus)) {
|
|
39
|
-
if (submenu.contains(target)) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// Click is outside all menus, close them
|
|
44
|
-
handleClose();
|
|
45
|
-
};
|
|
46
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
47
|
-
document.addEventListener('touchstart', handleClickOutside);
|
|
48
|
-
return () => {
|
|
49
|
-
document.removeEventListener('mousedown', handleClickOutside);
|
|
50
|
-
document.removeEventListener('touchstart', handleClickOutside);
|
|
51
|
-
};
|
|
52
|
-
}, [menuState.visible]);
|
|
53
|
-
const handleContextMenu = (e) => {
|
|
54
|
-
if (trigger === 'right') {
|
|
55
|
-
e.preventDefault();
|
|
56
|
-
setMenuState({
|
|
57
|
-
visible: true,
|
|
58
|
-
position: { x: e.clientX, y: e.clientY },
|
|
59
|
-
submenuStack: [items],
|
|
60
|
-
parentNames: [],
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
const handleClick = (e) => {
|
|
65
|
-
if (trigger === 'left') {
|
|
66
|
-
e.preventDefault();
|
|
67
|
-
setMenuState({
|
|
68
|
-
visible: true,
|
|
69
|
-
position: { x: e.clientX, y: e.clientY },
|
|
70
|
-
submenuStack: [items],
|
|
71
|
-
parentNames: [],
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
const handleItemClick = (item) => {
|
|
76
|
-
if (item.disabled)
|
|
77
|
-
return;
|
|
78
|
-
// Always execute onClick if present
|
|
79
|
-
if (item.onClick) {
|
|
80
|
-
item.onClick();
|
|
81
|
-
}
|
|
82
|
-
if (item.submenu && item.submenu.length > 0) {
|
|
83
|
-
if (isMobile) {
|
|
84
|
-
// Mobile: Push submenu to stack
|
|
85
|
-
setMenuState(prev => ({
|
|
86
|
-
...prev,
|
|
87
|
-
submenuStack: [...prev.submenuStack, item.submenu],
|
|
88
|
-
parentNames: [...prev.parentNames, item.name],
|
|
89
|
-
}));
|
|
90
|
-
}
|
|
91
|
-
// Desktop: Submenus are handled by hover, don't close menu
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
// No submenu: close menu after executing action
|
|
95
|
-
handleClose();
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
const handleBack = () => {
|
|
99
|
-
setMenuState(prev => ({
|
|
100
|
-
...prev,
|
|
101
|
-
submenuStack: prev.submenuStack.slice(0, -1),
|
|
102
|
-
parentNames: prev.parentNames.slice(0, -1),
|
|
103
|
-
}));
|
|
104
|
-
};
|
|
105
|
-
const handleMouseEnter = (item, event, depth = 0) => {
|
|
106
|
-
if (isMobile || !item.submenu || item.submenu.length === 0)
|
|
107
|
-
return;
|
|
108
|
-
if (submenuTimeoutRef.current) {
|
|
109
|
-
clearTimeout(submenuTimeoutRef.current);
|
|
110
|
-
submenuTimeoutRef.current = null;
|
|
111
|
-
}
|
|
112
|
-
const rect = event.currentTarget.getBoundingClientRect();
|
|
113
|
-
// Calculate if submenu should open upward based on available space
|
|
114
|
-
// Estimate submenu height: ~35px per item (accounting for smaller padding) + container padding
|
|
115
|
-
const estimatedSubmenuHeight = (item.submenu.length * 35) + 8;
|
|
116
|
-
const spaceBelow = window.innerHeight - rect.top;
|
|
117
|
-
const spaceAbove = rect.bottom;
|
|
118
|
-
// Open upward only if there's not enough space below AND there's more space above
|
|
119
|
-
const shouldOpenUp = spaceBelow < estimatedSubmenuHeight && spaceAbove > spaceBelow;
|
|
120
|
-
// Remove all submenus at this depth and deeper
|
|
121
|
-
setHoveredSubmenus(prev => {
|
|
122
|
-
const filtered = prev.filter(sub => sub.depth < depth);
|
|
123
|
-
if (!item.submenu)
|
|
124
|
-
return filtered;
|
|
125
|
-
return [
|
|
126
|
-
...filtered,
|
|
127
|
-
{
|
|
128
|
-
items: item.submenu,
|
|
129
|
-
parentRect: rect,
|
|
130
|
-
depth: depth,
|
|
131
|
-
openUp: shouldOpenUp,
|
|
132
|
-
}
|
|
133
|
-
];
|
|
134
|
-
});
|
|
135
|
-
};
|
|
136
|
-
const handleMouseLeave = (depth = 0) => {
|
|
137
|
-
if (isMobile)
|
|
138
|
-
return;
|
|
139
|
-
if (submenuTimeoutRef.current) {
|
|
140
|
-
clearTimeout(submenuTimeoutRef.current);
|
|
141
|
-
}
|
|
142
|
-
const targetDepth = depth;
|
|
143
|
-
submenuTimeoutRef.current = setTimeout(() => {
|
|
144
|
-
setHoveredSubmenus(prev => prev.filter(sub => sub.depth < targetDepth));
|
|
145
|
-
}, 300);
|
|
146
|
-
};
|
|
147
|
-
const handleSubmenuMouseEnter = () => {
|
|
148
|
-
if (submenuTimeoutRef.current) {
|
|
149
|
-
clearTimeout(submenuTimeoutRef.current);
|
|
150
|
-
submenuTimeoutRef.current = null;
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
useEffect(() => {
|
|
154
|
-
return () => {
|
|
155
|
-
if (submenuTimeoutRef.current) {
|
|
156
|
-
clearTimeout(submenuTimeoutRef.current);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
}, []);
|
|
160
|
-
const renderMenuItems = (menuItems, depth = 0) => {
|
|
161
|
-
return menuItems
|
|
162
|
-
.filter(item => item.visible !== false)
|
|
163
|
-
.map((item, idx) => {
|
|
164
|
-
const itemKey = `${item.name}-${idx}`.replaceAll(/\s+/g, '-');
|
|
165
|
-
const handleClick = (e) => {
|
|
166
|
-
if (item.disabled)
|
|
167
|
-
return;
|
|
168
|
-
e.stopPropagation();
|
|
169
|
-
handleItemClick(item);
|
|
170
|
-
};
|
|
171
|
-
const handleRightIconClick = (e) => {
|
|
172
|
-
e.stopPropagation();
|
|
173
|
-
// if (item.disabled) return;
|
|
174
|
-
item.onRightIconClick?.();
|
|
175
|
-
};
|
|
176
|
-
return (_jsxs(S.MenuItem, { "$disabled": item.disabled, "$hasSubmenu": !!item.submenu && item.submenu.length > 0, "$beginGroup": item.beginGroup, onMouseDown: handleClick, onMouseEnter: (e) => !isMobile && handleMouseEnter(item, e, depth + 1), onMouseLeave: () => !isMobile && handleMouseLeave(depth + 1), children: [_jsxs(S.MenuItemContent, { children: [item.icon && _jsx(S.IconWrapper, { children: item.icon }), _jsx(S.MenuItemName, { children: item.name })] }), item.rightIcon && item.onRightIconClick && (_jsx(S.RightIconButton, { onClick: handleRightIconClick, onMouseDown: (e) => e.stopPropagation(), "aria-label": `Action for ${item.name}`, children: item.rightIcon })), item.submenu && item.submenu.length > 0 && (_jsx(S.SubmenuIndicator, { "$isMobile": isMobile, children: isMobile ? '›' : '▸' }))] }, itemKey));
|
|
177
|
-
});
|
|
178
|
-
};
|
|
179
|
-
const currentMenu = menuState.submenuStack.at(-1) || items;
|
|
180
|
-
const currentParentName = menuState.parentNames.at(-1) || '';
|
|
181
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onKeyDown: (e) => {
|
|
182
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
183
|
-
handleClick(e);
|
|
184
|
-
}
|
|
185
|
-
}, role: "button", tabIndex: 0, style: { display: 'inline-block' }, children: children }), menuState.visible && (_jsxs(_Fragment, { children: [_jsx(S.Overlay, { onClick: handleClose }), _jsxs(S.MenuContainer, { ref: menuRef, "$x": menuState.position.x, "$y": menuState.position.y, "$openLeft": openLeft, "$openUp": openUp, children: [isMobile && menuState.parentNames.length > 0 && (_jsxs(S.MobileMenuHeader, { children: [_jsx(S.BackButton, { onClick: handleBack, "aria-label": "Go back", children: "\u2190" }), _jsx(S.HeaderTitle, { children: currentParentName })] })), renderMenuItems(currentMenu, 0)] }), !isMobile && hoveredSubmenus.map((submenu, idx) => (_jsx(S.Submenu, { "$parentRect": submenu.parentRect, "$openUp": submenu.openUp, "data-submenu": "true", onMouseEnter: handleSubmenuMouseEnter, onMouseLeave: () => handleMouseLeave(submenu.depth), children: renderMenuItems(submenu.items, submenu.depth) }, `submenu-${submenu.depth}-${idx}`)))] }))] }));
|
|
186
|
-
};
|
|
187
|
-
export default TMContextMenu;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export declare const useIsMobile: () => boolean;
|
|
2
|
-
export declare const useClickOutside: (callback: () => void) => import("react").RefObject<HTMLDivElement>;
|
|
3
|
-
interface Position {
|
|
4
|
-
x: number;
|
|
5
|
-
y: number;
|
|
6
|
-
}
|
|
7
|
-
export declare const useMenuPosition: (menuRef: React.RefObject<HTMLDivElement | null>, position: Position) => {
|
|
8
|
-
openLeft: boolean;
|
|
9
|
-
openUp: boolean;
|
|
10
|
-
};
|
|
11
|
-
export {};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from 'react';
|
|
2
|
-
export const useIsMobile = () => {
|
|
3
|
-
const [isMobile, setIsMobile] = useState(false);
|
|
4
|
-
useEffect(() => {
|
|
5
|
-
const checkMobile = () => {
|
|
6
|
-
const mobile = globalThis.innerWidth <= 768 || 'ontouchstart' in globalThis;
|
|
7
|
-
setIsMobile(mobile);
|
|
8
|
-
};
|
|
9
|
-
checkMobile();
|
|
10
|
-
window.addEventListener('resize', checkMobile);
|
|
11
|
-
return () => window.removeEventListener('resize', checkMobile);
|
|
12
|
-
}, []);
|
|
13
|
-
return isMobile;
|
|
14
|
-
};
|
|
15
|
-
export const useClickOutside = (callback) => {
|
|
16
|
-
const ref = useRef(null);
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
const handleClick = (event) => {
|
|
19
|
-
if (ref.current && !ref.current.contains(event.target)) {
|
|
20
|
-
callback();
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
document.addEventListener('mousedown', handleClick);
|
|
24
|
-
document.addEventListener('touchstart', handleClick);
|
|
25
|
-
return () => {
|
|
26
|
-
document.removeEventListener('mousedown', handleClick);
|
|
27
|
-
document.removeEventListener('touchstart', handleClick);
|
|
28
|
-
};
|
|
29
|
-
}, [callback]);
|
|
30
|
-
return ref;
|
|
31
|
-
};
|
|
32
|
-
export const useMenuPosition = (menuRef, position) => {
|
|
33
|
-
const [adjustedPosition, setAdjustedPosition] = useState({ openLeft: false, openUp: false });
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (!menuRef.current)
|
|
36
|
-
return;
|
|
37
|
-
const menuRect = menuRef.current.getBoundingClientRect();
|
|
38
|
-
const viewportWidth = window.innerWidth;
|
|
39
|
-
const viewportHeight = window.innerHeight;
|
|
40
|
-
const spaceRight = viewportWidth - position.x;
|
|
41
|
-
const spaceBottom = viewportHeight - position.y;
|
|
42
|
-
setAdjustedPosition({
|
|
43
|
-
openLeft: spaceRight < menuRect.width + 20,
|
|
44
|
-
openUp: spaceBottom < menuRect.height + 20,
|
|
45
|
-
});
|
|
46
|
-
}, [position, menuRef]);
|
|
47
|
-
return adjustedPosition;
|
|
48
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default as ContextMenu } from './TMContextMenu';
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export declare const MenuContainer: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
2
|
-
$x: number;
|
|
3
|
-
$y: number;
|
|
4
|
-
$openLeft: boolean;
|
|
5
|
-
$openUp: boolean;
|
|
6
|
-
}>> & string;
|
|
7
|
-
export declare const MenuItem: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
8
|
-
$disabled?: boolean;
|
|
9
|
-
$hasSubmenu?: boolean;
|
|
10
|
-
$beginGroup?: boolean;
|
|
11
|
-
}>> & string;
|
|
12
|
-
export declare const MenuItemContent: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
13
|
-
export declare const IconWrapper: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never>> & string;
|
|
14
|
-
export declare const MenuItemName: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never>> & string;
|
|
15
|
-
export declare const RightIconButton: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>> & string;
|
|
16
|
-
export declare const SubmenuIndicator: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, {
|
|
17
|
-
$isMobile?: boolean;
|
|
18
|
-
}>> & string;
|
|
19
|
-
export declare const Submenu: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
20
|
-
$parentRect: DOMRect;
|
|
21
|
-
$openUp?: boolean;
|
|
22
|
-
}>> & string;
|
|
23
|
-
export declare const MobileMenuHeader: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
24
|
-
export declare const BackButton: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>> & string;
|
|
25
|
-
export declare const HeaderTitle: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>, never>> & string;
|
|
26
|
-
export declare const MenuDivider: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
27
|
-
export declare const Overlay: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|