@topconsultnpm/sdkui-react 6.19.0-dev2.37 → 6.19.0-dev2.39
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 +6 -9
- package/lib/components/base/TMFileManagerDataGridView.js +1 -1
- package/lib/components/features/blog/TMBlogCommentForm.d.ts +2 -0
- package/lib/components/features/blog/TMBlogCommentForm.js +6 -3
- package/lib/helper/dcmtsHelper.d.ts +1 -2
- package/lib/helper/dcmtsHelper.js +62 -19
- 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
|
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState, useMemo } from 'react';
|
|
|
4
4
|
import TMModal from './TMModal';
|
|
5
5
|
import styled from 'styled-components';
|
|
6
6
|
import { TMLayoutWaitingContainer } from '../..';
|
|
7
|
-
import { processButtonAttributes
|
|
7
|
+
import { processButtonAttributes } from '../../helper/dcmtsHelper';
|
|
8
8
|
const IframeContainer = styled.div `
|
|
9
9
|
display: flex;
|
|
10
10
|
height: 100%;
|
|
@@ -19,8 +19,9 @@ const TMCustomButton = (props) => {
|
|
|
19
19
|
const { button, isModal = true, formData, selectedItems, onClose } = props;
|
|
20
20
|
const { appName: scriptUrl, arguments: args } = button;
|
|
21
21
|
const iframeRef = useRef(null);
|
|
22
|
-
const attributes = useMemo(() => processButtonAttributes(args, formData), [args, formData]);
|
|
23
|
-
const selectedItemsProcessed =
|
|
22
|
+
const attributes = useMemo(() => processButtonAttributes(args, formData, selectedItems), [args, formData, selectedItems]);
|
|
23
|
+
const selectedItemsProcessed = []; //da eliminare
|
|
24
|
+
//const selectedItemsProcessed = useMemo(() => processSelectedItems(args, formData, selectedItems), [args, formData, selectedItems]);
|
|
24
25
|
const RunOnce = button.mode === "RunOnce";
|
|
25
26
|
const [loading, setLoading] = useState(true);
|
|
26
27
|
const [error, setError] = useState(false);
|
|
@@ -61,10 +62,7 @@ const TMCustomButton = (props) => {
|
|
|
61
62
|
const checkIframe = setInterval(() => {
|
|
62
63
|
if (iframeRef.current?.contentWindow) {
|
|
63
64
|
clearInterval(checkIframe);
|
|
64
|
-
iframeRef.current.contentWindow.postMessage({
|
|
65
|
-
"options": item,
|
|
66
|
-
"session": SDK_Globals.tmSession
|
|
67
|
-
}, targetOrigin);
|
|
65
|
+
iframeRef.current.contentWindow.postMessage({ "options": item }, targetOrigin);
|
|
68
66
|
// Attendi prima di passare al prossimo
|
|
69
67
|
setTimeout(() => {
|
|
70
68
|
setWaitPanelValue(index + 1);
|
|
@@ -95,9 +93,8 @@ const TMCustomButton = (props) => {
|
|
|
95
93
|
}
|
|
96
94
|
else if (iframeRef.current?.contentWindow) {
|
|
97
95
|
// Modalità RunOnce: invia dati all'iframe quando è caricato
|
|
98
|
-
const mergedAttributes = { ...attributes, selectedItems: selectedItemsProcessed };
|
|
99
96
|
iframeRef.current.contentWindow.postMessage({
|
|
100
|
-
"options":
|
|
97
|
+
"options": attributes,
|
|
101
98
|
"session": SDK_Globals.tmSession
|
|
102
99
|
}, targetOrigin);
|
|
103
100
|
//clearTimeout(timeoutIframe);
|
|
@@ -96,6 +96,6 @@ const TMFileManagerDataGridView = (props) => {
|
|
|
96
96
|
{ dataField: "creationTime", caption: SDKUI_Localizator.CreationTime, dataType: 'datetime', format: 'dd/MM/yyyy HH:mm', cellRender: cellDatetimeRender },
|
|
97
97
|
];
|
|
98
98
|
}, [searchText]);
|
|
99
|
-
return _jsx(TMDataGrid, { dataSource: items ?? [], dataColumns: dataColumns, focusedRowKey: focusedRowKey, selectedRowKeys: selectedRowKeys, onFocusedRowChanged: onFocusedRowChanged, onSelectionChanged: onSelectionChanged, onCellDblClick: onCellDblClick, onContextMenuPreparing: onContextMenuPreparing, showSearchPanel: false, noDataText: SDKUI_Localizator.FolderIsEmpty });
|
|
99
|
+
return _jsx(TMDataGrid, { dataSource: items ?? [], dataColumns: dataColumns, focusedRowKey: focusedRowKey, selectedRowKeys: selectedRowKeys, onFocusedRowChanged: onFocusedRowChanged, onSelectionChanged: onSelectionChanged, onCellDblClick: onCellDblClick, onContextMenuPreparing: onContextMenuPreparing, showSearchPanel: false, noDataText: SDKUI_Localizator.FolderIsEmpty }, items.length);
|
|
100
100
|
};
|
|
101
101
|
export default TMFileManagerDataGridView;
|
|
@@ -8,8 +8,10 @@ interface TMBlogCommentFormProps {
|
|
|
8
8
|
showAttachmentsSection?: boolean;
|
|
9
9
|
selectedAttachments?: Array<FileItem>;
|
|
10
10
|
selectedAttachmentDid?: Array<number>;
|
|
11
|
+
title?: string;
|
|
11
12
|
allFileItems?: FileItem;
|
|
12
13
|
allArchivedDocumentsFileItems?: Array<FileItem>;
|
|
14
|
+
onSavedCallback?: () => void;
|
|
13
15
|
onFilterCreated?: (predicate: (post: BlogPost) => boolean) => void;
|
|
14
16
|
refreshCallback?: () => Promise<void>;
|
|
15
17
|
}
|
|
@@ -29,7 +29,7 @@ const getNonDirectoryFiles = (items, exclude) => {
|
|
|
29
29
|
};
|
|
30
30
|
const TMBlogCommentForm = (props) => {
|
|
31
31
|
const maxLength = 1000;
|
|
32
|
-
const { participants, selectedAttachments, selectedAttachmentDid, allFileItems, allArchivedDocumentsFileItems = [], onClose, context, showAttachmentsSection = true, onFilterCreated, refreshCallback } = props;
|
|
32
|
+
const { title = SDKUI_Localizator.AddNewComment, participants, selectedAttachments, selectedAttachmentDid, allFileItems, allArchivedDocumentsFileItems = [], onClose, context, showAttachmentsSection = true, onSavedCallback, onFilterCreated, refreshCallback } = props;
|
|
33
33
|
// Initialize state with combined array
|
|
34
34
|
const [dataSource, setDataSource] = useState(() => [...getNonDirectoryFiles(allFileItems?.items || [], []), ...allArchivedDocumentsFileItems]);
|
|
35
35
|
const [isEditorEnabled, setIsEditorEnabled] = useState(true);
|
|
@@ -171,7 +171,10 @@ const TMBlogCommentForm = (props) => {
|
|
|
171
171
|
finally {
|
|
172
172
|
// Hide the loading spinner and close the operation (e.g., close a modal or cleanup)
|
|
173
173
|
TMSpinner.hide();
|
|
174
|
-
|
|
174
|
+
if (onSavedCallback)
|
|
175
|
+
onSavedCallback();
|
|
176
|
+
else
|
|
177
|
+
onClose();
|
|
175
178
|
}
|
|
176
179
|
};
|
|
177
180
|
const onCloseCallback = () => {
|
|
@@ -216,7 +219,7 @@ const TMBlogCommentForm = (props) => {
|
|
|
216
219
|
// Update the state with selected draft items
|
|
217
220
|
setCurrentDraftAttachments(selectedDraftItems);
|
|
218
221
|
};
|
|
219
|
-
return _jsx(TMSaveForm, { id: 1, title:
|
|
222
|
+
return _jsx(TMSaveForm, { id: 1, title: title, showTitleFormMode: false, showErrorCount: false, customSaveButton: _jsx("i", { className: 'dx-icon-send' }), customTooltipSaveButton: SDKUI_Localizator.Send, showUndoButton: false, hasNavigation: false, skipIsModifiedCheck: true, isModal: true, width: calcResponsiveSizes(deviceType, '800px', '800px', '95%'), height: '550px', formMode: FormModes.Create, validationItems: validationItems, exception: exception, isModified: calcIsModified(formData, formDataOrig), onSaveAsync: onSaveAsync, onClose: onCloseCallback, customToolbarElements: _jsx("div", { style: { display: 'flex', gap: '2px' }, children: _jsx(TMButton, { btnStyle: "toolbar", icon: isEditorEnabled ? _jsx("i", { className: 'dx-icon-font' }) : _jsx("i", { className: 'dx-icon-background' }), caption: isEditorEnabled ? SDKUI_Localizator.HideFormattingOptions : SDKUI_Localizator.ShowFormattingOptions, onClick: toggleEditorMode }) }), children: _jsxs("div", { style: { width: "100%", height: "100%" }, children: [_jsxs("div", { style: { width: "100%", height: "100%" }, children: [_jsx("div", { style: { width: "100%", height: showAttachmentsSection ? "calc(100% - 60px)" : "100%" }, children: _jsx(TMHtmlEditor, { width: '100%', height: '100%', isEditorEnabled: isEditorEnabled, validationItems: validationItems, onValueChanged: onValueChanged, mentionsConfig: mentionsConfig, autoFocus: true, maxLength: maxLength }) }), showAttachmentsSection && _jsxs("div", { style: { display: 'flex', alignItems: 'center', height: '60px', marginTop: '10px' }, children: [_jsx("div", { style: {
|
|
220
223
|
width: 'calc(100% - 60px)',
|
|
221
224
|
overflowX: 'auto',
|
|
222
225
|
whiteSpace: 'nowrap',
|
|
@@ -4,5 +4,4 @@ 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
|
|
8
|
-
export declare const processSelectedItems: (args: string | undefined, formData: MetadataValueDescriptorEx[] | undefined, selectedItems: Array<any> | undefined) => any[][];
|
|
7
|
+
export declare const processButtonAttributes: (args: string | undefined, formData: MetadataValueDescriptorEx[] | undefined, selectedItems: Array<any> | undefined) => any[] | Record<string, any> | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RelationCacheService, RelationTypes } from "@topconsultnpm/sdk-ts";
|
|
1
|
+
import { RelationCacheService, RelationTypes, SDK_Globals } from "@topconsultnpm/sdk-ts";
|
|
2
2
|
/** Check if dcmtType (mTID) has configured Detail or Many-to-Many relations */
|
|
3
3
|
export const hasDetailRelations = async (mTID) => {
|
|
4
4
|
let allRelations = await RelationCacheService.GetAllAsync();
|
|
@@ -24,27 +24,70 @@ export const isXMLFileExt = (fileExt) => {
|
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
/*utility functions for TMCustomButton*/
|
|
27
|
-
export const processButtonAttributes = (args, formData) => args && formData ? formDataMap(formData, args) : undefined;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
//
|
|
41
|
-
|
|
27
|
+
export const processButtonAttributes = (args, formData, selectedItems) => args && formData ? formDataMap(formData, args, selectedItems) : undefined;
|
|
28
|
+
const processSelectedItems = (selectedItems) => selectedItems && selectedItems.map(item => item["DID"]) || [];
|
|
29
|
+
const formDataMap = (data, args, selectedItems, returnObject = true) => {
|
|
30
|
+
// Regex per catturare sia chiave={@campo} che {@campo} o {valore}
|
|
31
|
+
const tokens = args.match(/(\w+)=\{@?[^}]+\}|\{@?[^}]+\}/g) || [];
|
|
32
|
+
const session = SDK_Globals.tmSession;
|
|
33
|
+
const sessionDescr = session?.SessionDescr;
|
|
34
|
+
const results = tokens.map(token => {
|
|
35
|
+
let value;
|
|
36
|
+
let fieldName = '';
|
|
37
|
+
// Verifica se ha formato chiave={@campo}
|
|
38
|
+
const keyValueMatch = token.match(/(\w+)=\{@?([^}]+)\}/);
|
|
39
|
+
if (keyValueMatch) {
|
|
40
|
+
// Formato: Test={@Campo}
|
|
41
|
+
fieldName = keyValueMatch[1]; // Test
|
|
42
|
+
const innerField = keyValueMatch[2]; // Campo
|
|
43
|
+
const md = data.find(md => md.md?.name === innerField);
|
|
44
|
+
value = md?.value;
|
|
45
|
+
}
|
|
46
|
+
else if (token.startsWith('{@')) {
|
|
47
|
+
// Formato: {@campo}
|
|
48
|
+
fieldName = token.slice(2, -1);
|
|
42
49
|
const md = data.find(md => md.md?.name === fieldName);
|
|
43
|
-
|
|
50
|
+
switch (fieldName) {
|
|
51
|
+
case 'SelectedDIDs':
|
|
52
|
+
value = processSelectedItems(selectedItems);
|
|
53
|
+
break;
|
|
54
|
+
case 'AuthenticationMode':
|
|
55
|
+
value = sessionDescr?.authenticationMode ?? null;
|
|
56
|
+
break;
|
|
57
|
+
case 'ArchiveID':
|
|
58
|
+
value = sessionDescr?.archiveID ?? null;
|
|
59
|
+
break;
|
|
60
|
+
case 'CultureID':
|
|
61
|
+
value = sessionDescr?.cultureID ?? null;
|
|
62
|
+
break;
|
|
63
|
+
case 'Domain':
|
|
64
|
+
value = sessionDescr?.domain ?? null;
|
|
65
|
+
break;
|
|
66
|
+
case 'UserID':
|
|
67
|
+
value = sessionDescr?.userID ?? null;
|
|
68
|
+
break;
|
|
69
|
+
case 'UserName':
|
|
70
|
+
value = sessionDescr?.userName ?? null;
|
|
71
|
+
break;
|
|
72
|
+
case 'Session':
|
|
73
|
+
value = session ?? null;
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
value = md?.value;
|
|
77
|
+
}
|
|
44
78
|
}
|
|
45
79
|
else {
|
|
46
|
-
//
|
|
47
|
-
|
|
80
|
+
// Formato: {valore}
|
|
81
|
+
fieldName = token.slice(1, -1);
|
|
82
|
+
value = fieldName;
|
|
48
83
|
}
|
|
84
|
+
return { fieldName, value };
|
|
49
85
|
});
|
|
86
|
+
if (returnObject) {
|
|
87
|
+
return results.reduce((acc, { fieldName, value }) => {
|
|
88
|
+
acc[fieldName] = value;
|
|
89
|
+
return acc;
|
|
90
|
+
}, {});
|
|
91
|
+
}
|
|
92
|
+
return results.map(r => r.value);
|
|
50
93
|
};
|
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.39",
|
|
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;
|