@topconsultnpm/sdkui-react 6.17.0-test10 → 6.17.0-test11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/lib/components/base/TMButton.d.ts +1 -0
  2. package/lib/components/base/TMButton.js +6 -6
  3. package/lib/components/base/TMCustomButton.d.ts +1 -1
  4. package/lib/components/base/TMCustomButton.js +28 -26
  5. package/lib/components/base/TMFileManagerDataGridView.js +1 -1
  6. package/lib/components/base/TMModal.d.ts +2 -0
  7. package/lib/components/base/TMModal.js +48 -3
  8. package/lib/components/base/TMPopUp.js +4 -1
  9. package/lib/components/base/TMWaitPanel.js +8 -2
  10. package/lib/components/choosers/TMDataListItemChooser.js +1 -1
  11. package/lib/components/choosers/TMMetadataChooser.js +3 -1
  12. package/lib/components/choosers/TMUserChooser.d.ts +4 -0
  13. package/lib/components/choosers/TMUserChooser.js +21 -5
  14. package/lib/components/editors/TMTextArea.d.ts +1 -0
  15. package/lib/components/editors/TMTextArea.js +43 -9
  16. package/lib/components/editors/TMTextBox.js +33 -3
  17. package/lib/components/editors/TMTextExpression.js +36 -28
  18. package/lib/components/features/assistant/ToppyDraggableHelpCenter.d.ts +30 -0
  19. package/lib/components/features/assistant/ToppyDraggableHelpCenter.js +459 -0
  20. package/lib/components/features/assistant/ToppySpeechBubble.d.ts +9 -0
  21. package/lib/components/features/assistant/ToppySpeechBubble.js +117 -0
  22. package/lib/components/features/blog/TMBlogCommentForm.d.ts +2 -0
  23. package/lib/components/features/blog/TMBlogCommentForm.js +18 -6
  24. package/lib/components/features/documents/TMDcmtBlog.js +1 -1
  25. package/lib/components/features/documents/TMDcmtForm.js +5 -5
  26. package/lib/components/features/documents/TMDcmtPreview.js +45 -8
  27. package/lib/components/features/search/TMSearchQueryPanel.js +2 -3
  28. package/lib/components/features/search/TMSearchResult.js +12 -13
  29. package/lib/components/features/tasks/TMTaskForm.js +2 -2
  30. package/lib/components/features/workflow/TMWorkflowPopup.js +1 -1
  31. package/lib/components/forms/TMSaveForm.js +2 -2
  32. package/lib/components/grids/TMBlogsPost.d.ts +7 -5
  33. package/lib/components/grids/TMBlogsPost.js +56 -10
  34. package/lib/components/grids/TMBlogsPostUtils.d.ts +1 -0
  35. package/lib/components/grids/TMBlogsPostUtils.js +10 -0
  36. package/lib/components/index.d.ts +1 -1
  37. package/lib/components/index.js +1 -1
  38. package/lib/helper/SDKUI_Localizator.d.ts +5 -0
  39. package/lib/helper/SDKUI_Localizator.js +50 -0
  40. package/lib/helper/TMIcons.d.ts +2 -0
  41. package/lib/helper/TMIcons.js +9 -0
  42. package/lib/helper/TMToppyMessage.js +2 -1
  43. package/lib/helper/dcmtsHelper.d.ts +2 -2
  44. package/lib/helper/dcmtsHelper.js +54 -25
  45. package/lib/helper/helpers.d.ts +1 -1
  46. package/lib/helper/helpers.js +10 -16
  47. package/lib/ts/types.d.ts +2 -0
  48. package/package.json +2 -2
  49. package/lib/components/features/assistant/ToppyHelpCenter.d.ts +0 -12
  50. package/lib/components/features/assistant/ToppyHelpCenter.js +0 -173
@@ -3,7 +3,7 @@ import { useState, useEffect, useRef } from 'react';
3
3
  import styled from 'styled-components';
4
4
  import { StyledEditor, StyledEditorContainer, StyledEditorIcon, StyledEditorLabel, editorColorManager } from './TMEditorStyled';
5
5
  import { FontSize, TMColors } from '../../utils/theme';
6
- import { genUniqueId, IconClearButton, IconHide, IconShow, SDKUI_Localizator } from '../../helper';
6
+ import { genUniqueId, IconClearButton, IconDataList, IconHide, IconShow, SDKUI_Localizator } from '../../helper';
7
7
  import ShowAlert from '../base/TMAlert';
8
8
  import { TMExceptionBoxManager } from '../base/TMPopUp';
9
9
  import TMLayoutContainer, { TMLayoutItem } from '../base/TMLayout';
@@ -11,6 +11,8 @@ import TMVilViewer from '../base/TMVilViewer';
11
11
  import TMTooltip from '../base/TMTooltip';
12
12
  import { ContextMenu } from 'devextreme-react';
13
13
  import { DeviceType, useDeviceType } from '../base/TMDeviceProvider';
14
+ import TMChooserForm from '../forms/TMChooserForm';
15
+ import { FormulaItemHelper } from './TMTextExpression';
14
16
  const StyledShowPasswordIcon = styled.div `
15
17
  color: ${props => !props.$disabled ? (props.$vil.length === 0) ? !props.$isModified ? TMColors.text_normal : TMColors.isModified : editorColorManager(props.$vil) : TMColors.disabled};
16
18
  position: absolute;
@@ -37,6 +39,7 @@ const TMTextBox = ({ autoFocus, maxLength, labelColor, precision, fromModal = fa
37
39
  const [currentType, setCurrentType] = useState(type);
38
40
  const [currentValue, setCurrentValue] = useState(value);
39
41
  const [formulaMenuItems, setFormulaMenuItems] = useState([]);
42
+ const [showFormulaItemsChooser, setShowFormulaItemsChooser] = useState(false);
40
43
  const [isFocused, setIsFocused] = useState(false);
41
44
  const inputRef = useRef(null);
42
45
  const deviceType = useDeviceType();
@@ -219,6 +222,30 @@ const TMTextBox = ({ autoFocus, maxLength, labelColor, precision, fromModal = fa
219
222
  setCurrentValue(inputValue);
220
223
  onValueChanged?.({ target: { value: inputValue } }); // Passa il valore filtrato
221
224
  };
225
+ function getFormulaDataSorce() {
226
+ let fiarray = [];
227
+ if (!formulaItems)
228
+ return [];
229
+ let i = 0;
230
+ for (const f of formulaItems) {
231
+ let fi = new FormulaItemHelper();
232
+ fi.id = i;
233
+ fi.paramName = f;
234
+ fiarray.push(fi);
235
+ i++;
236
+ }
237
+ return fiarray;
238
+ }
239
+ const openFormulaItemsChooser = () => {
240
+ return (showFormulaItemsChooser ?
241
+ _jsx(TMChooserForm, { title: SDKUI_Localizator.Parameters, height: '350', width: '300', hasShowId: false, showDefaultColumns: false, allowMultipleSelection: true, columns: [
242
+ { dataField: 'paramName', caption: SDKUI_Localizator.Name, dataType: 'string', sortOrder: 'asc', alignment: 'left' }
243
+ ], dataSource: getFormulaDataSorce(), onClose: () => { setShowFormulaItemsChooser(false); }, onChoose: (IDs) => {
244
+ let expr = value?.toString() ?? '';
245
+ IDs.map(i => expr += formulaItems[i]);
246
+ onValueChanged?.({ target: { value: expr } });
247
+ } }) : _jsx(_Fragment, {}));
248
+ };
222
249
  const renderInputField = () => {
223
250
  const bulletEntity = '\u2022'; // •
224
251
  const displayedValue = initialType === 'secureText'
@@ -233,13 +260,16 @@ const TMTextBox = ({ autoFocus, maxLength, labelColor, precision, fromModal = fa
233
260
  e.preventDefault();
234
261
  }
235
262
  }, "$isMobile": deviceType === DeviceType.MOBILE, "$disabled": disabled, "$vil": validationItems, "$isModified": isModifiedWhen, "$fontSize": fontSize, "$maxValue": maxValue, "$width": width, "$type": currentType, "$borderRadius": borderRadius }), (initialType === 'password' || initialType === 'secureText') && _jsx(StyledShowPasswordIcon, { onClick: toggleShowPassword, "$disabled": disabled, "$vil": validationItems, "$isModified": isModifiedWhen, children: showPasswordIcon() }), initialType !== 'password' &&
236
- _jsxs("div", { style: { display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', position: 'absolute', right: type === 'number' ? '25px' : '6px', top: label.length > 0 ? '20px' : '7px', pointerEvents: disabled ? 'none' : 'auto', opacity: disabled ? 0.4 : 1 }, children: [showClearButton && currentValue &&
263
+ _jsxs("div", { style: { display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', position: 'absolute', right: type === 'number' ? '25px' : '6px', top: label.length > 0 ? '20px' : '7px', pointerEvents: disabled ? 'none' : 'auto', opacity: disabled ? 0.4 : 1 }, children: [formulaItems.length > 0 &&
264
+ _jsx(StyledTextBoxEditorButton, { onClick: () => {
265
+ setShowFormulaItemsChooser(true);
266
+ }, children: _jsx(IconDataList, {}) }), showClearButton && currentValue &&
237
267
  _jsx(StyledTextBoxEditorButton, { onClick: () => {
238
268
  onValueChanged?.({ target: { value: undefined } });
239
269
  onBlur?.(undefined);
240
270
  }, children: _jsx(IconClearButton, {}) }), buttons.map((buttonItem, index) => {
241
271
  return (_jsx(StyledTextBoxEditorButton, { onClick: buttonItem.onClick, children: _jsx(TMTooltip, { content: buttonItem.text, children: buttonItem.icon }) }, buttonItem.text));
242
- })] }), formulaItems.length > 0 && (_jsx(ContextMenu, { dataSource: formulaMenuItems, target: `#text-${id}`, onItemClick: (e) => { insertText(e.itemData?.text ?? ''); } })), _jsx(TMVilViewer, { vil: validationItems })] }));
272
+ })] }), openFormulaItemsChooser(), formulaItems.length > 0 && (_jsx(ContextMenu, { dataSource: formulaMenuItems, target: `#text-${id}`, onItemClick: (e) => { insertText(e.itemData?.text ?? ''); } })), _jsx(TMVilViewer, { vil: validationItems })] }));
243
273
  };
244
274
  const renderedLeftLabelTextBox = () => {
245
275
  return (_jsxs(TMLayoutContainer, { direction: 'horizontal', children: [icon && _jsx(TMLayoutItem, { width: '20px', children: _jsx(StyledEditorIcon, { "$disabled": disabled, "$vil": validationItems, "$isModified": isModifiedWhen, children: icon }) }), _jsx(TMLayoutItem, { children: _jsxs(StyledEditorContainer, { "$width": width, children: [label && _jsx(StyledEditorLabel, { "$color": labelColor, "$isFocused": isFocused, "$labelPosition": labelPosition, "$disabled": disabled, children: label }), renderInputField()] }) })] }));
@@ -1,10 +1,9 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useState } from 'react';
3
3
  import { DcmtTypeListCacheService } from '@topconsultnpm/sdk-ts';
4
- import { IconClear, IconColumns, IconDataList, SDKUI_Localizator, stringIsNullOrEmpty } from '../../helper';
4
+ import { IconClear, IconColumns, SDKUI_Localizator, stringIsNullOrEmpty } from '../../helper';
5
5
  import { TMExceptionBoxManager } from '../base/TMPopUp';
6
6
  import { TMMetadataChooserForm } from '../choosers/TMMetadataChooser';
7
- import TMChooserForm from '../forms/TMChooserForm';
8
7
  import TMTextBox from './TMTextBox';
9
8
  import TMTextArea from './TMTextArea';
10
9
  const TMTextExpression = (props) => {
@@ -96,8 +95,7 @@ const TMTextExpression = (props) => {
96
95
  let buttons = [];
97
96
  if (props.qd || props.tid)
98
97
  buttons.push({ icon: _jsx(IconColumns, {}), text: SDKUI_Localizator.MetadataReferenceInsert, onClick: () => { setShowMetadataChooser(true); } });
99
- if (props.formulaItems && props.formulaItems.length > 0)
100
- buttons.push({ icon: _jsx(IconDataList, {}), text: SDKUI_Localizator.Parameters, onClick: () => setShowFormulaChooser(true) });
98
+ // if (props.formulaItems && props.formulaItems.length > 0) buttons.push({ icon: <IconDataList />, text: SDKUI_Localizator.Parameters, onClick: () => setShowFormulaChooser(true) })
101
99
  buttons.push({ icon: _jsx(IconClear, {}), text: SDKUI_Localizator.Clear, onClick: () => props.onValueChanged?.('') });
102
100
  return buttons;
103
101
  };
@@ -120,20 +118,18 @@ const TMTextExpression = (props) => {
120
118
  }
121
119
  return output;
122
120
  }
123
- function getFormulaDataSorce() {
124
- let fiarray = [];
125
- if (!props.formulaItems)
126
- return [];
127
- let i = 0;
128
- for (const f of props.formulaItems) {
129
- let fi = new FormulaItemHelper();
130
- fi.id = i;
131
- fi.paramName = f;
132
- fiarray.push(fi);
133
- i++;
134
- }
135
- return fiarray;
136
- }
121
+ // function getFormulaDataSorce() {
122
+ // let fiarray: FormulaItemHelper[] = []
123
+ // if (!props.formulaItems) return []
124
+ // let i: number = 0;
125
+ // for (const f of props.formulaItems!) {
126
+ // let fi = new FormulaItemHelper();
127
+ // fi.id = i; fi.paramName = f;
128
+ // fiarray.push(fi);
129
+ // i++;
130
+ // }
131
+ // return fiarray;
132
+ // }
137
133
  const openMetadataChooseForm = () => {
138
134
  return (showMetadataChooser ?
139
135
  _jsx(TMMetadataChooserForm, { allowMultipleSelection: true, qd: props.qd, tids: props.tid ? [props.tid] : undefined, qdShowOnlySelectItems: true, allowSysMetadata: true, onClose: () => setShowMetadataChooser(false), onChoose: (tid_mid) => {
@@ -146,18 +142,30 @@ const TMTextExpression = (props) => {
146
142
  { dataField: 'paramName', caption: props.captionColumnChooser ?? SDKUI_Localizator.Name, dataType: 'string', sortOrder: 'asc', alignment: 'left' }
147
143
  ];
148
144
  }, []);
149
- const openFormulaChooseForm = () => {
150
- return (showFormulaChooser ?
151
- _jsx(TMChooserForm, { title: props.titleChooser ?? SDKUI_Localizator.Parameters, height: props.higthChooser ?? '350', width: props.widthChooser ?? '300', hasShowId: false, showDefaultColumns: false, allowMultipleSelection: props.disableMultipleSelection !== true, columns: dataColumns, dataSource: getFormulaDataSorce(), onClose: () => { setShowFormulaChooser(false); }, onChoose: (IDs) => {
152
- let expr = props.value ?? '';
153
- IDs.map(i => expr += props.formulaItems[i]);
154
- props.onValueChanged?.(expr);
155
- } }) : _jsx(_Fragment, {}));
156
- };
145
+ // const openFormulaChooseForm = () => {
146
+ // return (showFormulaChooser ?
147
+ // <TMChooserForm
148
+ // title={props.titleChooser ?? SDKUI_Localizator.Parameters}
149
+ // height={props.higthChooser ?? '350'}
150
+ // width={props.widthChooser ?? '300'}
151
+ // hasShowId={false}
152
+ // showDefaultColumns={false}
153
+ // allowMultipleSelection={props.disableMultipleSelection !== true}
154
+ // columns={dataColumns}
155
+ // dataSource={getFormulaDataSorce()}
156
+ // onClose={() => { setShowFormulaChooser(false); }}
157
+ // onChoose={(IDs: number[]) => {
158
+ // let expr: string = props.value ?? '';
159
+ // IDs.map(i => expr += props.formulaItems![i]);
160
+ // props.onValueChanged?.(expr)
161
+ // }}
162
+ // /> : <></>
163
+ // )
164
+ // }
157
165
  return (_jsxs(_Fragment, { children: [props.rows === undefined ?
158
- _jsx(TMTextBox, { buttons: renderButtons(), isModifiedWhen: props.value != props.valueOrig, label: props.label, value: Expression_IDs2Names(props.value) ?? '', validationItems: props.validationItems, onValueChanged: (e) => { props.onValueChanged?.(Expression_Names2IDs(e.target.value)); } })
166
+ _jsx(TMTextBox, { buttons: renderButtons(), formulaItems: props.formulaItems, isModifiedWhen: props.value != props.valueOrig, label: props.label, value: Expression_IDs2Names(props.value) ?? '', validationItems: props.validationItems, onValueChanged: (e) => { props.onValueChanged?.(Expression_Names2IDs(e.target.value)); } })
159
167
  :
160
- _jsx(TMTextArea, { buttons: renderButtons(), isModifiedWhen: props.value != props.valueOrig, label: props.label, rows: props.rows, resize: false, placeHolder: props.placeHolder, value: Expression_IDs2Names(props.value) ?? '', validationItems: props.validationItems, onValueChanged: (e) => { props.onValueChanged?.(Expression_Names2IDs(e.target.value)); } }), openMetadataChooseForm(), " ", openFormulaChooseForm()] }));
168
+ _jsx(TMTextArea, { buttons: renderButtons(), formulaItems: props.formulaItems, isModifiedWhen: props.value != props.valueOrig, label: props.label, rows: props.rows, resize: false, placeHolder: props.placeHolder, value: Expression_IDs2Names(props.value) ?? '', validationItems: props.validationItems, onValueChanged: (e) => { props.onValueChanged?.(Expression_Names2IDs(e.target.value)); } }), openMetadataChooseForm(), " "] }));
161
169
  };
162
170
  class MetatadaHelper {
163
171
  constructor(mid, metadataName) {
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { DeviceType } from '../../base/TMDeviceProvider';
3
+ /**
4
+ * Proprietà del componente ToppyDraggableHelpCenter
5
+ */
6
+ interface ToppyDraggableHelpCenterProps {
7
+ /** Contenuto da visualizzare nella speech bubble */
8
+ content?: React.ReactNode;
9
+ /** Tipo di dispositivo per adattare il comportamento */
10
+ deviceType?: DeviceType;
11
+ /** Allineamento iniziale del componente (destra o sinistra) */
12
+ align?: 'right' | 'left';
13
+ /** Stato iniziale collassato/espanso */
14
+ initialIsCollapsed?: boolean;
15
+ /** Callback chiamato quando si clicca sull'immagine di Toppy */
16
+ onToppyImageClick?: () => void;
17
+ /** Visibilità del componente */
18
+ isVisible?: boolean;
19
+ /** Se true, renderizza Toppy nel document.body tramite Portal invece che nel parent */
20
+ usePortal?: boolean;
21
+ }
22
+ /**
23
+ * Componente ToppyDraggableHelpCenter
24
+ *
25
+ * Renderizza un assistente virtuale (Toppy) draggable che può mostrare contenuti
26
+ * in una speech bubble. Il componente può essere trascinato all'interno del suo
27
+ * contenitore e può essere collassato/espanso con un doppio click.
28
+ */
29
+ declare const ToppyDraggableHelpCenter: React.FC<ToppyDraggableHelpCenterProps>;
30
+ export default ToppyDraggableHelpCenter;
@@ -0,0 +1,459 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef, useState, useEffect } from 'react';
3
+ import ReactDOM from 'react-dom';
4
+ import styled from 'styled-components';
5
+ import Toppy from '../../../assets/Toppy-generico.png';
6
+ import { DeviceType } from '../../base/TMDeviceProvider';
7
+ import ToppySpeechBubble from './ToppySpeechBubble';
8
+ import { SDKUI_Localizator } from '../../../helper';
9
+ import { IconWindowMaximize } from '../../../helper/TMIcons';
10
+ /**
11
+ * Styled component per il contenitore di Toppy
12
+ * Gestisce il posizionamento, le dimensioni e le animazioni
13
+ */
14
+ const ToppyButton = styled.div.attrs((props) => ({
15
+ style: {
16
+ // Applica left/top come stili inline per evitare la generazione di troppe classi CSS
17
+ ...(props.$x !== undefined && props.$y !== undefined
18
+ ? {
19
+ left: `${props.$x}px`,
20
+ top: `${props.$y}px`,
21
+ bottom: 'auto',
22
+ right: 'auto',
23
+ }
24
+ : {}),
25
+ // Cursore applicato come stile inline
26
+ cursor: props.$isDragging ? 'grabbing' : 'grab',
27
+ },
28
+ })) `
29
+ /* Visibilità controllata dalla prop */
30
+ display: ${(props) => (props.$isVisible ? 'flex' : 'none')};
31
+ position: ${(props) => props.$usePortal ? 'fixed' : 'absolute'};
32
+
33
+ /* Posizionamento di default quando non è draggato (x e y non sono definiti) */
34
+ ${(props) => props.$x === undefined || props.$y === undefined
35
+ ? `
36
+ bottom: ${props.$isMobile ? '0px' : '-20px'};
37
+ ${props.$align === 'left' ? 'left: 10px;' : 'right: 10px;'};
38
+ `
39
+ : ''}
40
+
41
+ /* Z-index alto per assicurare che sia sempre in primo piano */
42
+ z-index: 2147483647;
43
+ background-color: transparent;
44
+ border: none;
45
+ padding: 0;
46
+ outline: none;
47
+
48
+ /* Dimensioni dinamiche in base allo stato collassato e al tipo di dispositivo
49
+ Usa min() per adattarsi su schermi piccoli */
50
+ width: ${(props) => {
51
+ if (props.$isMobile) {
52
+ return props.$isCollapsed ? 'min(40px, 100%)' : '80px';
53
+ }
54
+ return props.$isCollapsed ? 'min(60px, 100%)' : '120px';
55
+ }};
56
+ height: ${(props) => {
57
+ if (props.$isMobile) {
58
+ return props.$isCollapsed ? 'min(45px, 100%)' : '95px';
59
+ }
60
+ return props.$isCollapsed ? 'min(70px, 100%)' : '140px';
61
+ }};
62
+ max-width: 100%;
63
+ max-height: 100%;
64
+
65
+ user-select: none;
66
+
67
+ img {
68
+ /* Dimensioni dell'immagine in base allo stato collassato e al tipo di dispositivo */
69
+ width: ${(props) => {
70
+ if (props.$isMobile) {
71
+ return props.$isCollapsed ? '40px' : '80px';
72
+ }
73
+ return props.$isCollapsed ? '60px' : '120px';
74
+ }};
75
+ height: ${(props) => {
76
+ if (props.$isMobile) {
77
+ return props.$isCollapsed ? '40px' : '95px';
78
+ }
79
+ return props.$isCollapsed ? '60px' : '140px';
80
+ }};
81
+ pointer-events: ${(props) => (props.$isMobile ? 'auto' : 'none')};
82
+ border-radius: 50%; /* Rende l'immagine circolare */
83
+ /* Rotazione leggera in base all'allineamento */
84
+ transform: ${(props) => props.$isCollapsed
85
+ ? 'rotate(0deg)'
86
+ : props.$align === 'left' ? 'rotate(20deg)' : 'rotate(-20deg)'};
87
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
88
+
89
+ /* Animazione di pulsazione quando è collassato per attirare l'attenzione */
90
+ ${(props) => props.$isCollapsed && `animation: toppyPulse 1.5s infinite;`}
91
+ }
92
+
93
+ /* Keyframes per l'animazione di pulsazione */
94
+ @keyframes toppyPulse {
95
+ 0% {
96
+ /* Dimensione normale all'inizio */
97
+ transform: scale(1) rotate(0deg);
98
+ box-shadow: 0 0 0 rgba(0, 113, 188, 0.5);
99
+ }
100
+ 50% {
101
+ /* Ingrandimento e glow al 50% dell'animazione */
102
+ transform: scale(1.1) rotate(0deg);
103
+ box-shadow: 0 0 15px 5px rgba(0, 113, 188, 0.6);
104
+ }
105
+ 100% {
106
+ /* Ritorno alla dimensione normale */
107
+ transform: scale(1) rotate(0deg);
108
+ box-shadow: 0 0 0 rgba(0, 113, 188, 0.5);
109
+ }
110
+ }
111
+ `;
112
+ /**
113
+ * Pulsante di espansione quando Toppy è minimizzato
114
+ */
115
+ const ExpandButton = styled.button `
116
+ position: absolute;
117
+ top: -8px;
118
+ right: -8px;
119
+ width: 24px;
120
+ height: 24px;
121
+ border-radius: 50%;
122
+ border: 2px solid rgba(255, 255, 255, 0.9);
123
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
124
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4), 0 0 20px rgba(118, 75, 162, 0.2);
125
+ cursor: pointer;
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
130
+ z-index: 10001;
131
+ color: white;
132
+ font-size: 14px;
133
+
134
+ &:hover {
135
+ background: linear-gradient(135deg, #764ba2 0%, #f093fb 100%);
136
+ box-shadow: 0 6px 20px rgba(118, 75, 162, 0.6), 0 0 30px rgba(240, 147, 251, 0.4);
137
+ border-color: rgba(255, 255, 255, 1);
138
+ transform: scale(1.1);
139
+ }
140
+
141
+ &:active {
142
+ transform: scale(0.95);
143
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
144
+ }
145
+
146
+ &:focus {
147
+ outline: none;
148
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4), 0 0 0 3px rgba(102, 126, 234, 0.2);
149
+ }
150
+ `;
151
+ /**
152
+ * Overlay trasparente per bloccare le interazioni durante il drag
153
+ * Previene problemi di performance con iframe e altri elementi interattivi
154
+ */
155
+ const DragOverlay = styled.div `
156
+ position: fixed;
157
+ top: 0;
158
+ left: 0;
159
+ right: 0;
160
+ bottom: 0;
161
+ z-index: 2147483646;
162
+ cursor: grabbing;
163
+ `;
164
+ /**
165
+ * Componente ToppyDraggableHelpCenter
166
+ *
167
+ * Renderizza un assistente virtuale (Toppy) draggable che può mostrare contenuti
168
+ * in una speech bubble. Il componente può essere trascinato all'interno del suo
169
+ * contenitore e può essere collassato/espanso con un doppio click.
170
+ */
171
+ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onToppyImageClick, initialIsCollapsed, isVisible = true, usePortal = false, }) => {
172
+ // Ref per il contenitore principale
173
+ const buttonRef = useRef(null);
174
+ // Stato per controllare se il componente è collassato o espanso
175
+ const [isCollapsed, setIsCollapsed] = useState(initialIsCollapsed ?? false);
176
+ // Stato per tracciare se il componente è in fase di trascinamento
177
+ const [isDragging, setIsDragging] = useState(false);
178
+ // Posizione corrente del componente (null = posizione di default)
179
+ const [position, setPosition] = useState(null);
180
+ // Offset del mouse rispetto all'angolo superiore sinistro del componente durante il drag
181
+ const dragOffset = useRef({ x: 0, y: 0 });
182
+ // Ref e stato per tracciare le dimensioni della speech bubble
183
+ const bubbleRef = useRef(null);
184
+ const [bubbleSize, setBubbleSize] = useState({ width: 0, height: 0 });
185
+ // Ref per tracciare il dragging dell'ExpandButton
186
+ const isExpandButtonDraggingRef = useRef(false);
187
+ const expandButtonMouseDownPosRef = useRef(null);
188
+ const isMobile = deviceType === DeviceType.MOBILE;
189
+ /**
190
+ * Effect per aggiornare le dimensioni della bubble quando cambia il contenuto
191
+ * o lo stato di collassamento. Necessario per calcolare correttamente i limiti
192
+ * di trascinamento.
193
+ */
194
+ useEffect(() => {
195
+ if (bubbleRef.current) {
196
+ const rect = bubbleRef.current.getBoundingClientRect();
197
+ setBubbleSize({ width: rect.width, height: rect.height });
198
+ }
199
+ }, [content, isCollapsed]);
200
+ /**
201
+ * Effect per verificare e aggiustare la posizione quando cambia lo stato di collassamento
202
+ */
203
+ useEffect(() => {
204
+ if (!buttonRef.current || !position)
205
+ return;
206
+ const rect = buttonRef.current.getBoundingClientRect();
207
+ // Spazio extra occupato dalla bubble quando non è collassato
208
+ const extraHeight = !isCollapsed ? bubbleSize.height : 0;
209
+ const extraWidth = !isCollapsed ? bubbleSize.width : 0;
210
+ let minX = 0;
211
+ let maxX;
212
+ let maxY;
213
+ const bubbleBuffer = 5;
214
+ if (usePortal) {
215
+ // Calcola i limiti usando le dimensioni del viewport
216
+ maxX = window.innerWidth - rect.width;
217
+ maxY = window.innerHeight - rect.height + 20;
218
+ }
219
+ else {
220
+ // Calcola i limiti usando le dimensioni del parent
221
+ const parent = buttonRef.current.offsetParent;
222
+ if (!parent)
223
+ return;
224
+ const parentRect = parent.getBoundingClientRect();
225
+ maxX = parentRect.width - rect.width;
226
+ maxY = parentRect.height - rect.height + 20;
227
+ }
228
+ if (!isCollapsed) {
229
+ if (align === 'right') {
230
+ minX = Math.max(0, extraWidth - rect.width);
231
+ }
232
+ else {
233
+ maxX = Math.min(maxX, (usePortal ? window.innerWidth : buttonRef.current.offsetParent?.getBoundingClientRect().width || 0) - extraWidth);
234
+ }
235
+ }
236
+ // Verifica se la posizione corrente è fuori dai limiti
237
+ const isOutOfBounds = position.x < minX ||
238
+ position.x > maxX ||
239
+ position.y < extraHeight + bubbleBuffer ||
240
+ position.y > maxY;
241
+ // Se è fuori dai limiti, aggiusta la posizione
242
+ if (isOutOfBounds) {
243
+ const adjustedX = Math.max(minX, Math.min(position.x, maxX));
244
+ const adjustedY = Math.max(extraHeight + bubbleBuffer, Math.min(position.y, maxY));
245
+ setPosition({ x: adjustedX, y: adjustedY });
246
+ }
247
+ }, [isCollapsed, bubbleSize, usePortal, align]);
248
+ /**
249
+ * Effect per resettare la posizione quando il parent o la finestra vengono ridimensionati
250
+ */
251
+ useEffect(() => {
252
+ if (!buttonRef.current || !position)
253
+ return;
254
+ const handleResize = () => {
255
+ if (!buttonRef.current || !position)
256
+ return;
257
+ const rect = buttonRef.current.getBoundingClientRect();
258
+ const extraHeight = !isCollapsed ? bubbleSize.height : 0;
259
+ const extraWidth = !isCollapsed ? bubbleSize.width : 0;
260
+ let minX = 0;
261
+ let maxX;
262
+ let maxY;
263
+ const bubbleBuffer = 5;
264
+ if (usePortal) {
265
+ maxX = window.innerWidth - rect.width;
266
+ maxY = window.innerHeight - rect.height + 20;
267
+ }
268
+ else {
269
+ const parent = buttonRef.current.offsetParent;
270
+ if (!parent)
271
+ return;
272
+ const parentRect = parent.getBoundingClientRect();
273
+ maxX = parentRect.width - rect.width;
274
+ maxY = parentRect.height - rect.height + 20;
275
+ }
276
+ if (!isCollapsed) {
277
+ if (align === 'right') {
278
+ minX = Math.max(0, extraWidth - rect.width);
279
+ }
280
+ else {
281
+ maxX = Math.min(maxX, (usePortal ? window.innerWidth : buttonRef.current.offsetParent?.getBoundingClientRect().width || 0) - extraWidth);
282
+ }
283
+ }
284
+ const isOutOfBounds = position.x < minX ||
285
+ position.x > maxX ||
286
+ position.y < extraHeight + bubbleBuffer ||
287
+ position.y > maxY;
288
+ if (isOutOfBounds) {
289
+ setPosition(null);
290
+ }
291
+ };
292
+ // Observer per il parent se non usa Portal
293
+ let resizeObserver;
294
+ if (!usePortal) {
295
+ const parent = buttonRef.current.offsetParent;
296
+ if (parent) {
297
+ resizeObserver = new ResizeObserver(handleResize);
298
+ resizeObserver.observe(parent);
299
+ }
300
+ }
301
+ window.addEventListener('resize', handleResize);
302
+ return () => {
303
+ resizeObserver?.disconnect();
304
+ window.removeEventListener('resize', handleResize);
305
+ };
306
+ }, [position, isCollapsed, bubbleSize, align, usePortal]);
307
+ /**
308
+ * Effect per impostare automaticamente lo stato collassato su dispositivi mobile
309
+ * se non è stato specificato un valore iniziale.
310
+ */
311
+ useEffect(() => {
312
+ if (initialIsCollapsed === undefined && deviceType === DeviceType.MOBILE) {
313
+ setIsCollapsed(true);
314
+ }
315
+ }, [deviceType, initialIsCollapsed]);
316
+ /**
317
+ * Gestisce il toggle dello stato collassato/espanso
318
+ * Chiamato dal doppio click sul componente
319
+ */
320
+ const toggleCollapse = (e) => {
321
+ e?.stopPropagation();
322
+ setIsCollapsed(!isCollapsed);
323
+ onToppyImageClick?.();
324
+ };
325
+ /**
326
+ * Gestisce l'inizio del trascinamento
327
+ * Salva la posizione relativa del mouse rispetto al componente (offset)
328
+ * per mantenere il punto di presa durante il trascinamento
329
+ */
330
+ const handleMouseDown = (e) => {
331
+ if (!buttonRef.current)
332
+ return;
333
+ const rect = buttonRef.current.getBoundingClientRect();
334
+ // Calcola l'offset tra il punto di click e l'angolo superiore sinistro del componente
335
+ dragOffset.current = {
336
+ x: e.clientX - rect.left,
337
+ y: e.clientY - rect.top,
338
+ };
339
+ setIsDragging(true);
340
+ e.preventDefault();
341
+ };
342
+ /**
343
+ * Gestisce il movimento durante il trascinamento
344
+ * Calcola la nuova posizione rispettando i limiti del parent o viewport
345
+ * e tenendo conto delle dimensioni della speech bubble quando espansa
346
+ */
347
+ const handleMouseMove = (e) => {
348
+ if (!isDragging || !buttonRef.current)
349
+ return;
350
+ const rect = buttonRef.current.getBoundingClientRect();
351
+ // Spazio extra occupato dalla bubble quando non è collassato
352
+ const extraHeight = !isCollapsed ? bubbleSize.height : 0;
353
+ const extraWidth = !isCollapsed ? bubbleSize.width : 0;
354
+ let minX = 0;
355
+ let maxX;
356
+ let maxY;
357
+ let newX;
358
+ let newY;
359
+ const bubbleBuffer = 5;
360
+ if (usePortal) {
361
+ // Calcola i limiti usando il viewport
362
+ maxX = window.innerWidth - rect.width;
363
+ maxY = window.innerHeight - rect.height + 20;
364
+ if (!isCollapsed) {
365
+ if (align === 'right') {
366
+ minX = Math.max(0, extraWidth - rect.width);
367
+ }
368
+ else {
369
+ maxX = Math.min(maxX, window.innerWidth - extraWidth);
370
+ }
371
+ }
372
+ newX = Math.max(minX, Math.min(e.clientX - dragOffset.current.x, maxX));
373
+ newY = Math.max(extraHeight + bubbleBuffer, Math.min(e.clientY - dragOffset.current.y, maxY));
374
+ }
375
+ else {
376
+ // Calcola i limiti usando il parent
377
+ const parent = buttonRef.current.offsetParent;
378
+ if (!parent)
379
+ return;
380
+ const parentRect = parent.getBoundingClientRect();
381
+ maxX = parentRect.width - rect.width;
382
+ maxY = parentRect.height - rect.height + 20;
383
+ if (!isCollapsed) {
384
+ if (align === 'right') {
385
+ minX = Math.max(0, extraWidth - rect.width);
386
+ }
387
+ else {
388
+ maxX = Math.min(maxX, parentRect.width - extraWidth);
389
+ }
390
+ }
391
+ newX = Math.max(minX, Math.min(e.clientX - parentRect.left - dragOffset.current.x, maxX));
392
+ newY = Math.max(extraHeight + bubbleBuffer, Math.min(e.clientY - parentRect.top - dragOffset.current.y, maxY));
393
+ }
394
+ setPosition({ x: newX, y: newY });
395
+ };
396
+ /**
397
+ * Gestisce il rilascio del mouse alla fine del trascinamento
398
+ * Se il movimento è stato minimo (< 5px), viene interpretato come un click
399
+ * e viene chiamato il callback onToppyImageClick
400
+ */
401
+ const handleMouseUp = (e) => {
402
+ if (isDragging) {
403
+ setIsDragging(false);
404
+ const rect = buttonRef.current?.getBoundingClientRect();
405
+ if (rect) {
406
+ // Calcola la distanza totale del movimento usando il teorema di Pitagora
407
+ const moveDistance = Math.hypot(e.clientX - (rect.left + dragOffset.current.x), e.clientY - (rect.top + dragOffset.current.y));
408
+ // Se il movimento è stato minimo, trattalo come un click
409
+ if (moveDistance < 5 && onToppyImageClick) {
410
+ onToppyImageClick();
411
+ }
412
+ }
413
+ }
414
+ };
415
+ /**
416
+ * Effect per gestire gli event listener durante il trascinamento
417
+ * Gli eventi sono registrati sul document per catturare il movimento
418
+ * anche quando il mouse esce dal componente
419
+ */
420
+ useEffect(() => {
421
+ if (isDragging) {
422
+ document.addEventListener('mousemove', handleMouseMove);
423
+ document.addEventListener('mouseup', handleMouseUp);
424
+ return () => {
425
+ document.removeEventListener('mousemove', handleMouseMove);
426
+ document.removeEventListener('mouseup', handleMouseUp);
427
+ };
428
+ }
429
+ return undefined;
430
+ }, [isDragging]);
431
+ // Renderizza l'overlay solo durante il drag
432
+ const renderDragOverlay = isDragging && _jsx(DragOverlay, {});
433
+ const toppyContent = (_jsxs(_Fragment, { children: [renderDragOverlay, _jsxs(ToppyButton, { ref: buttonRef, "$align": align, "$isDragging": isDragging, "$x": position?.x, "$y": position?.y, "$isVisible": isVisible, "$isCollapsed": isCollapsed, "$isMobile": isMobile, "$usePortal": usePortal, onMouseDown: !isMobile ? handleMouseDown : undefined, onContextMenu: (e) => e.preventDefault(), onDoubleClick: !isMobile ? toggleCollapse : undefined, children: [(content && !isCollapsed) && (_jsx(ToppySpeechBubble, { ref: bubbleRef, align: align, onClose: toggleCollapse, children: content })), (content && isCollapsed) && (_jsx(ExpandButton, { onMouseDown: (e) => {
434
+ isExpandButtonDraggingRef.current = false;
435
+ expandButtonMouseDownPosRef.current = { x: e.clientX, y: e.clientY };
436
+ }, onMouseMove: (e) => {
437
+ if (!expandButtonMouseDownPosRef.current)
438
+ return;
439
+ const deltaX = Math.abs(e.clientX - expandButtonMouseDownPosRef.current.x);
440
+ const deltaY = Math.abs(e.clientY - expandButtonMouseDownPosRef.current.y);
441
+ // Considera drag solo se il movimento supera 3px
442
+ if (deltaX > 3 || deltaY > 3) {
443
+ isExpandButtonDraggingRef.current = true;
444
+ }
445
+ }, onMouseUp: () => {
446
+ expandButtonMouseDownPosRef.current = null;
447
+ }, onClick: (e) => {
448
+ if (isExpandButtonDraggingRef.current) {
449
+ e.stopPropagation();
450
+ e.preventDefault();
451
+ return;
452
+ }
453
+ e.stopPropagation();
454
+ toggleCollapse(e);
455
+ }, onContextMenu: (e) => e.preventDefault(), "aria-label": SDKUI_Localizator.Maximize, title: SDKUI_Localizator.Maximize, type: "button", children: _jsx(IconWindowMaximize, {}) })), _jsx("img", { src: Toppy, alt: "Toppy Help", draggable: false, onClick: isMobile ? toggleCollapse : undefined })] })] }));
456
+ // Renderizza nel document.body usando un Portal se usePortal è true, altrimenti renderizza normalmente
457
+ return usePortal ? ReactDOM.createPortal(toppyContent, document.body) : toppyContent;
458
+ };
459
+ export default ToppyDraggableHelpCenter;