funda-ui 4.5.888 → 4.6.101
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/Chatbox/index.css +84 -6
- package/Chatbox/index.d.ts +16 -3
- package/Chatbox/index.js +521 -61
- package/Textarea/index.js +9 -5
- package/Utils/useAutosizeTextArea.js +9 -5
- package/Utils/useGlobalUrlListener.d.ts +2 -0
- package/Utils/useGlobalUrlListener.js +157 -0
- package/Utils/useSessionStorageListener.d.ts +2 -0
- package/Utils/useSessionStorageListener.js +157 -0
- package/lib/cjs/Chatbox/index.d.ts +16 -3
- package/lib/cjs/Chatbox/index.js +521 -61
- package/lib/cjs/Textarea/index.js +9 -5
- package/lib/cjs/Utils/useAutosizeTextArea.js +9 -5
- package/lib/cjs/Utils/useGlobalUrlListener.d.ts +2 -0
- package/lib/cjs/Utils/useGlobalUrlListener.js +157 -0
- package/lib/cjs/Utils/useSessionStorageListener.d.ts +2 -0
- package/lib/cjs/Utils/useSessionStorageListener.js +157 -0
- package/lib/css/Chatbox/index.css +84 -6
- package/lib/esm/Chatbox/index.scss +106 -5
- package/lib/esm/Chatbox/index.tsx +199 -17
- package/lib/esm/Chatbox/utils/func.ts +62 -4
- package/lib/esm/Textarea/index.tsx +0 -1
- package/lib/esm/Utils/hooks/useAutosizeTextArea.tsx +13 -5
- package/lib/esm/Utils/hooks/useGlobalUrlListener.tsx +46 -0
- package/lib/esm/Utils/hooks/useSessionStorageListener.tsx +45 -0
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ import RootPortal from 'funda-root-portal';
|
|
|
8
8
|
import useComId from 'funda-utils/dist/cjs/useComId';
|
|
9
9
|
import useDebounce from 'funda-utils/dist/cjs/useDebounce';
|
|
10
10
|
import useThrottle from 'funda-utils/dist/cjs/useThrottle';
|
|
11
|
-
|
|
11
|
+
import useClickOutside from 'funda-utils/dist/cjs/useClickOutside';
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
// loader
|
|
@@ -20,11 +20,16 @@ import {
|
|
|
20
20
|
formatLatestDisplayContent,
|
|
21
21
|
formatName,
|
|
22
22
|
fixHtmlTags,
|
|
23
|
-
isStreamResponse
|
|
23
|
+
isStreamResponse,
|
|
24
|
+
htmlEncode
|
|
24
25
|
} from './utils/func';
|
|
25
26
|
|
|
26
27
|
import useStreamController from './useStreamController';
|
|
27
28
|
|
|
29
|
+
export interface CustomMethod {
|
|
30
|
+
name: string;
|
|
31
|
+
func: (...args: any[]) => any;
|
|
32
|
+
}
|
|
28
33
|
|
|
29
34
|
export type MessageDetail = {
|
|
30
35
|
sender: string; // Sender's name
|
|
@@ -37,6 +42,14 @@ export interface FloatingButton {
|
|
|
37
42
|
label: string; // HTML string
|
|
38
43
|
value: string;
|
|
39
44
|
onClick: string;
|
|
45
|
+
isSelect?: boolean; // Mark whether it is a drop-down selection button
|
|
46
|
+
[key: string]: any; // Allows dynamic `onSelect__<number>` attributes, such as `onSelect__1`, `onSelect__2`, ...
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FloatingButtonSelectOption {
|
|
50
|
+
label: string;
|
|
51
|
+
value: string;
|
|
52
|
+
onClick: string;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
export interface RequestConfig {
|
|
@@ -45,18 +58,18 @@ export interface RequestConfig {
|
|
|
45
58
|
responseExtractor: string; // JSON path to extract response
|
|
46
59
|
}
|
|
47
60
|
|
|
48
|
-
type CustomRequestConfig = {
|
|
61
|
+
export type CustomRequestConfig = {
|
|
49
62
|
requestBody: any;
|
|
50
63
|
apiUrl: string;
|
|
51
64
|
headers: any;
|
|
52
65
|
};
|
|
53
66
|
|
|
54
|
-
type CustomRequestResponse = {
|
|
67
|
+
export type CustomRequestResponse = {
|
|
55
68
|
content: string | Response | null;
|
|
56
69
|
isStream: boolean;
|
|
57
70
|
};
|
|
58
71
|
|
|
59
|
-
type CustomRequestFunction = (
|
|
72
|
+
export type CustomRequestFunction = (
|
|
60
73
|
message: string,
|
|
61
74
|
config: CustomRequestConfig
|
|
62
75
|
) => Promise<CustomRequestResponse>;
|
|
@@ -90,11 +103,13 @@ export type ChatboxProps = {
|
|
|
90
103
|
contextData?: Record<string, any>; // Dynamic JSON data
|
|
91
104
|
toolkitButtons?: FloatingButton[];
|
|
92
105
|
newChatButton?: FloatingButton;
|
|
106
|
+
customMethods?: CustomMethod[]; // [{"name": "method1", "func": "() => { console.log('test'); }"}, ...]
|
|
93
107
|
customRequest?: CustomRequestFunction;
|
|
94
108
|
renderParser?: (input: string) => Promise<string>;
|
|
95
109
|
requestBodyFormatter?: (body: any, contextData: Record<string, any>, conversationHistory: MessageDetail[]) => Promise<Record<string, any>>;
|
|
96
110
|
nameFormatter?: (input: string) => string;
|
|
97
111
|
onInputChange?: (controlRef: React.RefObject<any>, val: string) => any;
|
|
112
|
+
onInputCallback?: (input: string) => Promise<string>;
|
|
98
113
|
onChunk?: (controlRef: React.RefObject<any>, lastContent: string, conversationHistory: MessageDetail[]) => any;
|
|
99
114
|
onComplete?: (controlRef: React.RefObject<any>, lastContent: string, conversationHistory: MessageDetail[]) => any;
|
|
100
115
|
};
|
|
@@ -148,6 +163,21 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
148
163
|
//================================================================
|
|
149
164
|
// helper
|
|
150
165
|
//================================================================
|
|
166
|
+
const customMethodsRef = useRef<Record<string, Function>>({});
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (props.customMethods && Array.isArray(props.customMethods)) {
|
|
169
|
+
const methodsMap: Record<string, Function> = {};
|
|
170
|
+
|
|
171
|
+
props.customMethods.forEach(method => {
|
|
172
|
+
if (typeof method.func === 'function') {
|
|
173
|
+
methodsMap[method.name] = method.func;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
customMethodsRef.current = methodsMap;
|
|
178
|
+
}
|
|
179
|
+
}, [props.customMethods]);
|
|
180
|
+
|
|
151
181
|
const exposedMethods = () => {
|
|
152
182
|
return {
|
|
153
183
|
chatOpen: () => {
|
|
@@ -187,7 +217,24 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
187
217
|
},
|
|
188
218
|
setMessages: (v: MessageDetail[]) => {
|
|
189
219
|
setMsgList(v);
|
|
190
|
-
}
|
|
220
|
+
},
|
|
221
|
+
//
|
|
222
|
+
getCustomMethods: () => {
|
|
223
|
+
return Object.keys(customMethodsRef.current);
|
|
224
|
+
},
|
|
225
|
+
executeCustomMethod: (methodName: string, ...args: any[]) => {
|
|
226
|
+
if (methodName in customMethodsRef.current) {
|
|
227
|
+
try {
|
|
228
|
+
return customMethodsRef.current[methodName](...args);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error(`Error executing custom method ${methodName}:`, error);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
console.warn(`Custom method ${methodName} not found`);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
},
|
|
191
238
|
|
|
192
239
|
};
|
|
193
240
|
};
|
|
@@ -233,6 +280,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
233
280
|
requestBodyFormatter,
|
|
234
281
|
nameFormatter,
|
|
235
282
|
onInputChange,
|
|
283
|
+
onInputCallback,
|
|
236
284
|
onChunk,
|
|
237
285
|
onComplete,
|
|
238
286
|
} = currentProps;
|
|
@@ -304,6 +352,7 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
304
352
|
requestBodyFormatter,
|
|
305
353
|
nameFormatter,
|
|
306
354
|
onInputChange,
|
|
355
|
+
onInputCallback,
|
|
307
356
|
onChunk,
|
|
308
357
|
onComplete,
|
|
309
358
|
|
|
@@ -325,7 +374,21 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
325
374
|
//================================================================
|
|
326
375
|
// Custom buttons
|
|
327
376
|
//================================================================
|
|
377
|
+
const toolkitBtnsRef = useRef<any>(null);
|
|
328
378
|
const [activeButtons, setActiveButtons] = useState<Record<string, boolean>>({});
|
|
379
|
+
const closeDropdowns = () => {
|
|
380
|
+
setActiveButtons(prev => {
|
|
381
|
+
const newState = { ...prev };
|
|
382
|
+
// Turn off only buttons with "isSelect"
|
|
383
|
+
args().toolkitButtons?.forEach((btn, index) => {
|
|
384
|
+
if (btn.isSelect) {
|
|
385
|
+
const _id = `${args().prefix || 'custom-'}chatbox-btn-tools-${chatId}${index}`;
|
|
386
|
+
newState[_id] = false;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
return newState;
|
|
390
|
+
});
|
|
391
|
+
};
|
|
329
392
|
const executeButtonAction = (actionStr: string, buttonId: string, buttonElement: HTMLButtonElement) => {
|
|
330
393
|
try {
|
|
331
394
|
// Create a new function to execute
|
|
@@ -352,6 +415,66 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
352
415
|
}
|
|
353
416
|
};
|
|
354
417
|
|
|
418
|
+
// options
|
|
419
|
+
const [selectedOpt, setSelectedOpt] = useState<Record<string, string | number>>({});
|
|
420
|
+
const getButtonOptions = (btn: FloatingButton): FloatingButtonSelectOption[] => {
|
|
421
|
+
const options: FloatingButtonSelectOption[] = [];
|
|
422
|
+
let index = 1;
|
|
423
|
+
|
|
424
|
+
while (true) {
|
|
425
|
+
const optionKey = `onSelect__${index}`;
|
|
426
|
+
if (!(optionKey in btn)) break;
|
|
427
|
+
|
|
428
|
+
const [label, value, onClick] = btn[optionKey].split('{#}').map((s: string) => s.trim());
|
|
429
|
+
options.push({ label, value, onClick });
|
|
430
|
+
index++;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return options;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const handleExecuteButtonSelect = (buttonId: string, option: FloatingButtonSelectOption, index: number, value: string) => {
|
|
437
|
+
|
|
438
|
+
if (option.value === "cancel") {
|
|
439
|
+
setSelectedOpt(prev => {
|
|
440
|
+
const newLabels = { ...prev };
|
|
441
|
+
delete newLabels[buttonId]; // Deletes the saved selected label, which displays the default label
|
|
442
|
+
return {
|
|
443
|
+
...newLabels,
|
|
444
|
+
curIndex: index,
|
|
445
|
+
curValue: value
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
} else {
|
|
450
|
+
setSelectedOpt(prev => ({
|
|
451
|
+
...prev,
|
|
452
|
+
[buttonId]: option.label,
|
|
453
|
+
curIndex: index,
|
|
454
|
+
curValue: value
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
executeButtonAction(option.onClick, buttonId, document.getElementById(buttonId) as HTMLButtonElement);
|
|
460
|
+
|
|
461
|
+
// Close the drop-down
|
|
462
|
+
closeDropdowns();
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// click outside
|
|
466
|
+
useClickOutside({
|
|
467
|
+
enabled: Object.values(activeButtons).some(isActive => isActive),
|
|
468
|
+
isOutside: (event: any) => {
|
|
469
|
+
return event.target.closest('.toolkit-select-wrapper') === null;
|
|
470
|
+
},
|
|
471
|
+
handle: (event: any) => {
|
|
472
|
+
closeDropdowns();
|
|
473
|
+
}
|
|
474
|
+
}, [toolkitBtnsRef, activeButtons]);
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
|
|
355
478
|
//================================================================
|
|
356
479
|
// Conversation History
|
|
357
480
|
//================================================================
|
|
@@ -635,13 +758,18 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
635
758
|
if (rootRef.current === null || msgContainerRef.current === null || msInput.current === null) return;
|
|
636
759
|
|
|
637
760
|
const messageInput: any = msInput.current;
|
|
638
|
-
|
|
761
|
+
let message = htmlEncode(messageInput.value);
|
|
762
|
+
|
|
763
|
+
// It fires in real time as the user enters
|
|
764
|
+
// Sanitizing input is the process of securing/cleaning/filtering input data.
|
|
765
|
+
if (typeof args().onInputCallback === 'function') {
|
|
766
|
+
message = await args().onInputCallback(message);
|
|
767
|
+
}
|
|
639
768
|
|
|
640
769
|
if (message.trim() === '') {
|
|
641
770
|
return;
|
|
642
771
|
}
|
|
643
772
|
|
|
644
|
-
|
|
645
773
|
// Start the timer
|
|
646
774
|
setElapsedTime(0); // Reset elapsed time
|
|
647
775
|
timer.current = setInterval(() => {
|
|
@@ -1247,18 +1375,72 @@ const Chatbox = (props: ChatboxProps) => {
|
|
|
1247
1375
|
|
|
1248
1376
|
{/**------------- TOOLKIT BUTTONS -------------*/}
|
|
1249
1377
|
{args().toolkitButtons && args().toolkitButtons.length > 0 && (
|
|
1250
|
-
<div className="toolkit-btns">
|
|
1378
|
+
<div className="toolkit-btns" ref={toolkitBtnsRef}>
|
|
1251
1379
|
{args().toolkitButtons.map((btn: FloatingButton, index: number) => {
|
|
1252
1380
|
const _id = `${args().prefix || 'custom-'}chatbox-btn-tools-${chatId}${index}`;
|
|
1253
1381
|
const isActive = activeButtons[_id];
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1382
|
+
|
|
1383
|
+
if (btn.isSelect) {
|
|
1384
|
+
const options = getButtonOptions(btn);
|
|
1385
|
+
|
|
1386
|
+
return (
|
|
1387
|
+
<div key={index} className="toolkit-select-wrapper">
|
|
1388
|
+
<button
|
|
1389
|
+
id={_id}
|
|
1390
|
+
className={`toolkit-select-btn ${btn.value || ''} ${isActive ? 'active' : ''} ${selectedOpt.curValue !== 'cancel' && typeof selectedOpt.curValue !== 'undefined' && selectedOpt.curValue !== '' ? 'opt-active' : ''}`}
|
|
1391
|
+
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
1392
|
+
e.preventDefault();
|
|
1393
|
+
setActiveButtons(prev => ({
|
|
1394
|
+
...prev,
|
|
1395
|
+
[_id]: !prev[_id]
|
|
1396
|
+
}));
|
|
1397
|
+
}}
|
|
1398
|
+
>
|
|
1399
|
+
<span dangerouslySetInnerHTML={{
|
|
1400
|
+
__html: selectedOpt[_id] as string || btn.label
|
|
1401
|
+
}}></span>
|
|
1402
|
+
|
|
1403
|
+
<span className="toolkit-select-arrow"><svg width="5px" height="5px" viewBox="0 -4.5 20 20">
|
|
1404
|
+
<g stroke="none" strokeWidth="1" fill="none">
|
|
1405
|
+
<g transform="translate(-180.000000, -6684.000000)" className="arrow-fill-g" fill="currentColor">
|
|
1406
|
+
<g transform="translate(56.000000, 160.000000)">
|
|
1407
|
+
<path d="M144,6525.39 L142.594,6524 L133.987,6532.261 L133.069,6531.38 L133.074,6531.385 L125.427,6524.045 L124,6525.414 C126.113,6527.443 132.014,6533.107 133.987,6535 C135.453,6533.594 134.024,6534.965 144,6525.39">
|
|
1408
|
+
</path>
|
|
1409
|
+
</g>
|
|
1410
|
+
</g>
|
|
1411
|
+
</g>
|
|
1412
|
+
</svg></span>
|
|
1413
|
+
</button>
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
<div className={`toolkit-select-options ${isActive ? 'active' : ''}`}>
|
|
1418
|
+
{options.map((option: FloatingButtonSelectOption, optIndex: number) => (
|
|
1419
|
+
<div
|
|
1420
|
+
key={optIndex}
|
|
1421
|
+
className={`toolkit-select-option ${option.value || ''} ${selectedOpt.curIndex === optIndex ? 'selected' : ''}`}
|
|
1422
|
+
onClick={() => handleExecuteButtonSelect(_id, option, optIndex, option.value)}
|
|
1423
|
+
>
|
|
1424
|
+
<span dangerouslySetInnerHTML={{ __html: option.label }}></span>
|
|
1425
|
+
</div>
|
|
1426
|
+
))}
|
|
1427
|
+
</div>
|
|
1428
|
+
</div>
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// The rendering of the normal button
|
|
1433
|
+
return (
|
|
1434
|
+
<button
|
|
1435
|
+
key={index}
|
|
1436
|
+
id={_id}
|
|
1437
|
+
className={`${btn.value || ''} ${isActive ? 'active' : ''}`}
|
|
1438
|
+
onClick={(e: React.MouseEvent<HTMLButtonElement>) =>
|
|
1439
|
+
executeButtonAction(btn.onClick, _id, e.currentTarget)}
|
|
1440
|
+
>
|
|
1441
|
+
<span dangerouslySetInnerHTML={{ __html: btn.label }}></span>
|
|
1442
|
+
</button>
|
|
1443
|
+
);
|
|
1262
1444
|
})}
|
|
1263
1445
|
</div>
|
|
1264
1446
|
)}
|
|
@@ -40,12 +40,15 @@ export function formatName(str: any, isAnswer: boolean, props: ChatboxProps) {
|
|
|
40
40
|
answerNameIcon,
|
|
41
41
|
nameFormatter
|
|
42
42
|
} = props;
|
|
43
|
+
|
|
44
|
+
let res = str.replace(/\{icon\}/g, `${isAnswer ? answerNameIcon : questionNameIcon}`);
|
|
43
45
|
|
|
44
46
|
if (typeof nameFormatter === 'function') {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return str.replace(/\{icon\}/g, `${isAnswer ? answerNameIcon : questionNameIcon}`);
|
|
47
|
+
const newVal = nameFormatter(res);
|
|
48
|
+
return newVal;
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
return res;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
|
|
@@ -122,4 +125,59 @@ export function isStreamResponse(response: Response): boolean {
|
|
|
122
125
|
|
|
123
126
|
// Method 3: Check if response.body is ReadableStream
|
|
124
127
|
return response.body instanceof ReadableStream;
|
|
125
|
-
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* HTML entities encode
|
|
134
|
+
*
|
|
135
|
+
* @param {String} str Input text
|
|
136
|
+
* @return {String} Filtered text
|
|
137
|
+
*/
|
|
138
|
+
export function htmlEncode(str) {
|
|
139
|
+
|
|
140
|
+
return str.replace(/[&<>'"]/g, tag => ({
|
|
141
|
+
'&': '&',
|
|
142
|
+
'<': '<',
|
|
143
|
+
'>': '>',
|
|
144
|
+
"'": ''',
|
|
145
|
+
'"': '"'
|
|
146
|
+
}[tag]));
|
|
147
|
+
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* HTML entities decode
|
|
153
|
+
*
|
|
154
|
+
* @param {String} str Input text
|
|
155
|
+
* @return {String} Filtered text
|
|
156
|
+
*/
|
|
157
|
+
export function htmlDecode(str) {
|
|
158
|
+
|
|
159
|
+
let res = '';
|
|
160
|
+
const entities = [
|
|
161
|
+
['amp', '&'],
|
|
162
|
+
['apos', '\''],
|
|
163
|
+
['#x27', '\''],
|
|
164
|
+
['#x2F', '/'],
|
|
165
|
+
['#39', '\''],
|
|
166
|
+
['#47', '/'],
|
|
167
|
+
['lt', '<'],
|
|
168
|
+
['gt', '>'],
|
|
169
|
+
['nbsp', ' '],
|
|
170
|
+
['quot', '"'],
|
|
171
|
+
['#60', '<'],
|
|
172
|
+
['#62', '>']
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
for (let i = 0, max = entities.length; i < max; i++) {
|
|
176
|
+
str = str.replace(new RegExp('&' + entities[i][0] + ';', 'g'), entities[i][1]);
|
|
177
|
+
}
|
|
178
|
+
res = str;
|
|
179
|
+
|
|
180
|
+
return res;
|
|
181
|
+
|
|
182
|
+
}
|
|
183
|
+
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef, forwardRef, ChangeEvent, KeyboardEvent, useImperativeHandle } from 'react';
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
import useComId from 'funda-utils/dist/cjs/useComId';
|
|
6
5
|
import useAutosizeTextArea from 'funda-utils/dist/cjs/useAutosizeTextArea';
|
|
7
6
|
import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
|
|
@@ -88,7 +88,13 @@ const useAutosizeTextArea = ({
|
|
|
88
88
|
|
|
89
89
|
// initialize default row height
|
|
90
90
|
if (el.scrollHeight > 0 && !defaultRowHeightInit) {
|
|
91
|
-
|
|
91
|
+
|
|
92
|
+
let _defaultRowHeight = el.scrollHeight + parseInt(style.borderTopWidth) + parseInt(style.borderBottomWidth);
|
|
93
|
+
if (maxHeight != 0 && _defaultRowHeight >= maxHeight) {
|
|
94
|
+
_defaultRowHeight = maxHeight;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setDefaultRowHeight(_defaultRowHeight);
|
|
92
98
|
setDefaultRowHeightInit(true);
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -106,12 +112,14 @@ const useAutosizeTextArea = ({
|
|
|
106
112
|
|
|
107
113
|
// !!! Compare initial height and changed height
|
|
108
114
|
if (scrollHeight > defaultRowHeight && defaultRowHeight > 0) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
|
|
116
|
+
let _scrollHeight = scrollHeight;
|
|
117
|
+
if (maxHeight != 0 && _scrollHeight >= maxHeight) {
|
|
118
|
+
_scrollHeight = maxHeight;
|
|
113
119
|
}
|
|
114
120
|
|
|
121
|
+
el.style.height = _scrollHeight + "px";
|
|
122
|
+
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
cb?.([_controlWidth, scrollHeight]);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Url Listener (including micro frontends, frameworks, hashes, etc., applicable to multiple react app)
|
|
3
|
+
*
|
|
4
|
+
* @usage:
|
|
5
|
+
|
|
6
|
+
const App = () => {
|
|
7
|
+
const url = useGlobalUrlListener();
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
console.log("URL changed:", url);
|
|
11
|
+
}, [url]);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
|
|
17
|
+
const useGlobalUrlListener = (): string => {
|
|
18
|
+
// Initialize state with empty string to avoid SSR issues
|
|
19
|
+
const [url, setUrl] = useState<string>('');
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
// Type guard for SSR
|
|
23
|
+
if (typeof window === 'undefined') return;
|
|
24
|
+
|
|
25
|
+
// Initialize the URL on the client side
|
|
26
|
+
setUrl(window.location.href);
|
|
27
|
+
|
|
28
|
+
// Create MutationObserver instance
|
|
29
|
+
const observer: MutationObserver = new MutationObserver(() => {
|
|
30
|
+
setUrl(window.location.href);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Start observing
|
|
34
|
+
observer.observe(document, {
|
|
35
|
+
subtree: true,
|
|
36
|
+
childList: true
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Cleanup function
|
|
40
|
+
return () => observer.disconnect();
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
return url;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default useGlobalUrlListener;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Storage Listener
|
|
3
|
+
*
|
|
4
|
+
* @usage:
|
|
5
|
+
|
|
6
|
+
const App = () => {
|
|
7
|
+
const myValue = useSessionStorageListener("myKey");
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
console.log("sessionStorage change:", myValue);
|
|
11
|
+
}, [myValue]);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
|
|
17
|
+
const useSessionStorageListener = (key: string) => {
|
|
18
|
+
const [value, setValue] = useState(sessionStorage.getItem(key) || "");
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const handleStorageChange = (event: Event) => {
|
|
22
|
+
if (event instanceof CustomEvent && event.detail.key === key) {
|
|
23
|
+
setValue(event.detail.value);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
window.addEventListener("sessionStorageChange", handleStorageChange);
|
|
28
|
+
|
|
29
|
+
return () => window.removeEventListener("sessionStorageChange", handleStorageChange);
|
|
30
|
+
}, [key]);
|
|
31
|
+
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// interception sessionStorage.setItem
|
|
36
|
+
const originalSetItem = sessionStorage.setItem;
|
|
37
|
+
sessionStorage.setItem = function (key, value) {
|
|
38
|
+
const event = new CustomEvent("sessionStorageChange", {
|
|
39
|
+
detail: { key, value },
|
|
40
|
+
});
|
|
41
|
+
window.dispatchEvent(event);
|
|
42
|
+
originalSetItem.apply(this, [key, value]);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default useSessionStorageListener;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "UIUX Lab",
|
|
3
3
|
"email": "uiuxlab@gmail.com",
|
|
4
4
|
"name": "funda-ui",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.6.101",
|
|
6
6
|
"description": "React components using pure Bootstrap 5+ which does not contain any external style and script libraries.",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|