funda-ui 4.7.101 → 4.7.105

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 (54) hide show
  1. package/Chatbox/index.js +6 -1
  2. package/Checkbox/index.js +10 -1
  3. package/Date/index.js +12 -2
  4. package/Input/index.js +6 -1
  5. package/LiveSearch/index.js +5 -0
  6. package/MultipleCheckboxes/index.js +27 -1
  7. package/NumberInput/index.js +6 -1
  8. package/Radio/index.js +22 -1
  9. package/RangeSlider/index.js +6 -1
  10. package/Stepper/index.css +109 -34
  11. package/Stepper/index.d.ts +1 -1
  12. package/Stepper/index.js +55 -2
  13. package/TagInput/index.js +10 -1
  14. package/Textarea/index.js +6 -1
  15. package/Toast/index.css +23 -75
  16. package/Toast/index.d.ts +3 -34
  17. package/Toast/index.js +652 -175
  18. package/lib/cjs/Chatbox/index.js +6 -1
  19. package/lib/cjs/Checkbox/index.js +10 -1
  20. package/lib/cjs/Date/index.js +12 -2
  21. package/lib/cjs/Input/index.js +6 -1
  22. package/lib/cjs/LiveSearch/index.js +5 -0
  23. package/lib/cjs/MultipleCheckboxes/index.js +27 -1
  24. package/lib/cjs/NumberInput/index.js +6 -1
  25. package/lib/cjs/Radio/index.js +22 -1
  26. package/lib/cjs/RangeSlider/index.js +6 -1
  27. package/lib/cjs/Stepper/index.d.ts +1 -1
  28. package/lib/cjs/Stepper/index.js +55 -2
  29. package/lib/cjs/TagInput/index.js +10 -1
  30. package/lib/cjs/Textarea/index.js +6 -1
  31. package/lib/cjs/Toast/index.d.ts +3 -34
  32. package/lib/cjs/Toast/index.js +652 -175
  33. package/lib/css/Stepper/index.css +109 -34
  34. package/lib/css/Toast/index.css +23 -75
  35. package/lib/esm/Checkbox/index.tsx +12 -1
  36. package/lib/esm/Date/index.tsx +8 -1
  37. package/lib/esm/Input/index.tsx +8 -1
  38. package/lib/esm/LiveSearch/index.tsx +7 -0
  39. package/lib/esm/MultipleCheckboxes/index.tsx +19 -1
  40. package/lib/esm/NumberInput/index.tsx +8 -1
  41. package/lib/esm/Radio/index.tsx +17 -1
  42. package/lib/esm/Stepper/index.scss +135 -36
  43. package/lib/esm/Stepper/index.tsx +51 -3
  44. package/lib/esm/TagInput/index.tsx +8 -1
  45. package/lib/esm/Textarea/index.tsx +8 -1
  46. package/lib/esm/Toast/Item.tsx +52 -11
  47. package/lib/esm/Toast/Toast.tsx +391 -0
  48. package/lib/esm/Toast/ToastContext.tsx +104 -0
  49. package/lib/esm/Toast/__toast.vanilla.js +422 -0
  50. package/lib/esm/Toast/index.scss +24 -96
  51. package/lib/esm/Toast/index.tsx +3 -374
  52. package/lib/esm/Toast/types.ts +60 -0
  53. package/lib/esm/Toast/useToast.tsx +72 -0
  54. package/package.json +1 -1
@@ -0,0 +1,391 @@
1
+ import React, { useRef, useEffect, useState, useCallback } from 'react';
2
+
3
+
4
+ import RootPortal from 'funda-root-portal';
5
+ import useComId from 'funda-utils/dist/cjs/useComId';
6
+ import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
7
+
8
+
9
+ import Item from './Item';
10
+
11
+ import type { ToastOptions } from './types';
12
+
13
+ export interface ToastProps {
14
+ data: Array<ToastOptions>;
15
+
16
+ // default props
17
+ defaultWrapperClassName?: string;
18
+ defaultOnlyShowOne?: boolean;
19
+ defaultDirection?: ToastOptions['direction'];
20
+ defaultCascading?: boolean;
21
+ defaultReverseDisplay?: boolean;
22
+
23
+ onUpdate?: (updatedData: Array<ToastOptions>) => void;
24
+ }
25
+
26
+
27
+ export const Toast: React.FC<ToastProps> = ({
28
+ data,
29
+
30
+ // default props
31
+ defaultWrapperClassName,
32
+ defaultOnlyShowOne,
33
+ defaultDirection,
34
+ defaultCascading,
35
+ defaultReverseDisplay,
36
+
37
+ onUpdate,
38
+ }) => {
39
+ const ANIM_SPEED = 300;
40
+ const DEFAULT_AUTO_CLOSE_TIME = 3000;
41
+ const uniqueID = useComId();
42
+ const rootRef = useRef<any>(null);
43
+
44
+ // action id
45
+ const [currentActionId, setCurrentActionId] = useState<string | number | null | undefined>(undefined);
46
+
47
+ // 追踪每个 toast 的动画状态
48
+ const [animatedToasts, setAnimatedToasts] = useState<Set<string>>(new Set());
49
+
50
+ // force display
51
+ const [initPopRoot, setInitPopRoot] = useState<boolean>(false);
52
+
53
+ // Get the global configuration from the first toast item (if it exists)
54
+ const firstToast = data[0] || {};
55
+
56
+ // Use default values but allow individual toast overrides
57
+ const wrapperClassName = firstToast.wrapperClassName || defaultWrapperClassName;
58
+ const direction = firstToast.direction || defaultDirection;
59
+ const cascadingEnabled = typeof firstToast.cascading !== 'undefined' ? firstToast.cascading : defaultCascading;
60
+ const onlyShowOne = typeof firstToast.onlyShowOne !== 'undefined' ? firstToast.onlyShowOne : defaultOnlyShowOne;
61
+ const reverseDisplay = typeof firstToast.reverseDisplay !== 'undefined' ? firstToast.reverseDisplay : defaultReverseDisplay;
62
+
63
+ const depth: number = data.length + 1;
64
+
65
+ // Processes the order of data based on the "direction" and "reverseDisplay" parameters
66
+ const getProcessedData = useCallback(() => {
67
+ let processedData = [...data];
68
+
69
+ if (
70
+ (direction?.startsWith('top-') || direction?.startsWith('vertical-')) &&
71
+ reverseDisplay
72
+ ) {
73
+ processedData.reverse();
74
+ } else if (
75
+ direction?.startsWith('bottom-') &&
76
+ !reverseDisplay
77
+ ) {
78
+ processedData.reverse();
79
+ }
80
+
81
+ // If only one is displayed, only the last one is returned
82
+ return onlyShowOne ? [processedData[processedData.length - 1]] : processedData;
83
+ }, [data, direction, reverseDisplay, onlyShowOne]);
84
+
85
+
86
+
87
+ // Store the status of each toast
88
+ const progressPausedRef = useRef<Map<string, boolean>>(new Map());
89
+ const progressObjRef = useRef<Map<string, any>>(new Map());
90
+ const progressIntervalRef = useRef<Map<string, NodeJS.Timeout | null>>(new Map());
91
+
92
+ const startProgressTimer = useCallback((el: any, toastId: string, i: number) => {
93
+ // If the toast already has a timer running, do not add it again
94
+ if (progressIntervalRef.current.has(toastId)) {
95
+ return;
96
+ }
97
+ const currentToast = data[i];
98
+
99
+ // progress animation
100
+ const PROGRESS_TRANSITION_TIME: any = typeof (currentToast.autoCloseTime) === 'undefined' || currentToast.autoCloseTime === false ? DEFAULT_AUTO_CLOSE_TIME as number : currentToast.autoCloseTime;
101
+
102
+ // init progress
103
+ let progressCurrentChunk = 100 / (PROGRESS_TRANSITION_TIME / 100);
104
+ el.firstChild.style.width = 100 + '%';
105
+ el.firstChild.ariaValueNow = 100;
106
+
107
+ // animation
108
+ const intervalId = setInterval(() => {
109
+ // console.log('toast setInterval');
110
+
111
+
112
+ if (!progressPausedRef.current.get(toastId)) {
113
+ const progPercent = 100 - progressCurrentChunk;
114
+
115
+ el.firstChild.style.width = progPercent + '%';
116
+ el.firstChild.ariaValueNow = progPercent;
117
+ progressCurrentChunk++;
118
+
119
+
120
+ //
121
+ if (progPercent === 0 || progPercent < 1) { // may be 0.xxx
122
+ el.classList.add('complete');
123
+
124
+ // stop current animation
125
+ stopProgressTimer(toastId);
126
+
127
+ // hide toast item
128
+ const currentItem = el.closest('.toast-container');
129
+ handleClose(null, i, currentItem);
130
+ }
131
+ }
132
+ }, PROGRESS_TRANSITION_TIME / 100);
133
+
134
+ // Save the timer ID
135
+ progressIntervalRef.current.set(toastId, intervalId);
136
+
137
+ }, [data]);
138
+
139
+ const clearAllProgressTimer = useCallback(() => {
140
+ progressIntervalRef.current.forEach((timer, id) => {
141
+ if (timer) {
142
+ clearInterval(timer);
143
+ progressIntervalRef.current.set(id, null);
144
+ }
145
+ });
146
+ progressIntervalRef.current.clear();
147
+ }, []);
148
+
149
+ const stopProgressTimer = useCallback((toastId: string) => {
150
+ const timer = progressIntervalRef.current.get(toastId);
151
+ if (timer) {
152
+ clearInterval(timer);
153
+ progressIntervalRef.current.delete(toastId);
154
+ }
155
+ }, []);
156
+
157
+
158
+ function handleProgressPaused(e: any) {
159
+ const toastId = e.currentTarget.dataset.toastId;
160
+ progressPausedRef.current.set(toastId, true);
161
+ }
162
+
163
+ function handleProgressStart(e: any) {
164
+ const toastId = e.currentTarget.dataset.toastId;
165
+ progressPausedRef.current.set(toastId, false);
166
+ }
167
+
168
+ function handleClose(e: any, index: number, currentItem: HTMLDivElement) {
169
+ if (typeof e !== 'undefined' && e !== null) e.preventDefault();
170
+ if (rootRef.current === null) return;
171
+
172
+ const curIndex = Number(index);
173
+ const currentToast = data[curIndex];
174
+ const toastId = currentToast.id as string;
175
+ const _list: HTMLDivElement[] = [].slice.call(rootRef.current.querySelectorAll('.toast-container'));
176
+ currentItem.classList.add('hide-start');
177
+
178
+
179
+ //Let the removed animation show
180
+ setTimeout(() => {
181
+ _list.forEach((node: any) => {
182
+ node.classList.remove('hide-start');
183
+ });
184
+
185
+ // remove current
186
+ currentItem.classList.add('hide-end');
187
+
188
+ // rearrange
189
+ if (cascadingEnabled) {
190
+ _list.filter((node: any) => !node.classList.contains('hide-end'))
191
+ .forEach((node: any, k: number) => {
192
+ node.style.transform = `perspective(100px) translateZ(-${2 * k}px) translateY(${35 * k}px)`;
193
+ });
194
+ }
195
+
196
+
197
+ // stop all animations or current animation
198
+ stopProgressTimer(toastId);
199
+
200
+ // close callback
201
+ const currentToast = data[curIndex];
202
+ if (currentToast.onClose) {
203
+ currentToast.onClose(
204
+ rootRef.current,
205
+ curIndex,
206
+ _list.filter((node: HTMLDivElement) => !node.classList.contains('hide-end'))
207
+ );
208
+ }
209
+
210
+ // After the animation ends, remove the item from the data source
211
+ if (onUpdate) {
212
+ const newData = [...data];
213
+ newData.splice(curIndex, 1);
214
+ onUpdate(newData);
215
+ }
216
+
217
+
218
+
219
+ }, ANIM_SPEED);
220
+ }
221
+
222
+ useEffect(() => {
223
+ if (initPopRoot) {
224
+ const $toast = rootRef.current;
225
+ if (!$toast) return;
226
+
227
+ // When "onlyShowOne" is true, only the latest toast is kept
228
+ //--------------
229
+ if (onlyShowOne && data.length > 1) {
230
+ // Clear all old timers
231
+ data.slice(0, -1).forEach((toast, index) => {
232
+ stopProgressTimer(toast.id as string);
233
+ });
234
+
235
+ //Only keep the latest toast
236
+ if (onUpdate) {
237
+ onUpdate([data[data.length - 1]]);
238
+ }
239
+
240
+ return;
241
+ }
242
+
243
+
244
+ // Auto hide
245
+ //--------------
246
+ data.forEach((toast, i) => {
247
+ // auto close
248
+ const AUTO_CLOSE_TIME: any = typeof (toast.autoCloseTime) === 'undefined' || toast.autoCloseTime === false ? false : toast.autoCloseTime;
249
+
250
+ if (AUTO_CLOSE_TIME !== false) {
251
+ const el = progressObjRef.current.get(toast.id as string);
252
+ if (el) {
253
+ startProgressTimer(el, toast.id as string, i);
254
+ }
255
+ }
256
+ });
257
+
258
+ }
259
+
260
+ }, [data, initPopRoot, onlyShowOne]);
261
+
262
+ // Handling animation when data changes
263
+ useEffect(() => {
264
+ if (initPopRoot && data.length > 0) {
265
+ // Add animation to the new toast
266
+ const newToasts = data.filter(toast => !animatedToasts.has(toast.id as string));
267
+
268
+ if (newToasts.length > 0) {
269
+ // Keep the new toast hidden first
270
+ newToasts.forEach(toast => {
271
+ const progressEl = progressObjRef.current.get(toast.id as string);
272
+ if (progressEl) {
273
+ const currentItem = progressEl.closest('.toast-container');
274
+ currentItem.classList.add('animate-ready');
275
+ }
276
+ });
277
+
278
+ // Use "requestAnimationFrame" to ensure the DOM is updated before adding animation classes
279
+ requestAnimationFrame(() => {
280
+ setTimeout(() => {
281
+ newToasts.forEach(toast => {
282
+ const progressEl = progressObjRef.current.get(toast.id as string);
283
+ if (progressEl) {
284
+ const currentItem = progressEl.closest('.toast-container');
285
+ currentItem.classList.remove('animate-ready');
286
+ currentItem.classList.add('animate-in');
287
+ }
288
+ });
289
+
290
+ // Update the animated toast list
291
+ setAnimatedToasts(prev => {
292
+ const newSet = new Set(prev);
293
+ newToasts.forEach(toast => newSet.add(toast.id as string));
294
+ return newSet;
295
+ });
296
+ }, 50); // A small delay ensures that animate-ready styles are applied
297
+ });
298
+ }
299
+ }
300
+ }, [data, initPopRoot]);
301
+
302
+ // Monitor the currently displayed toast id
303
+ useEffect(() => {
304
+ if (onlyShowOne && data.length > 0) {
305
+ const latestToast = data[data.length - 1];
306
+ setCurrentActionId(latestToast.actionId);
307
+ } else if (data.length > 0) {
308
+ const currentIds = data.map(toast => toast.actionId);
309
+ setCurrentActionId(currentIds[currentIds.length - 1]);
310
+ } else {
311
+ setCurrentActionId(null);
312
+ }
313
+ }, [data, onlyShowOne]);
314
+
315
+ // The timer and data are emptied each time "useToast().show()" is executed
316
+ useEffect(() => {
317
+ if (currentActionId === null || typeof currentActionId === 'undefined') return;
318
+ if (onUpdate) {
319
+ onUpdate([data[data.length - 1]]);
320
+ }
321
+
322
+ // Remove the global list of events, especially as scroll and interval.
323
+ //--------------
324
+ return () => {
325
+ clearAllProgressTimer();
326
+ };
327
+ }, [currentActionId]);
328
+
329
+
330
+ // init
331
+ useEffect(() => {
332
+ setInitPopRoot(true);
333
+
334
+ // Remove the global list of events, especially as scroll and interval.
335
+ //--------------
336
+ return () => {
337
+ clearAllProgressTimer();
338
+ };
339
+ }, []);
340
+
341
+
342
+
343
+
344
+ return (
345
+ <RootPortal show={initPopRoot} containerClassName="Toast">
346
+ <div
347
+ id={`toasts__wrapper-${uniqueID}`}
348
+ className={combinedCls(
349
+ 'toasts__wrapper',
350
+ `toasts__wrapper--${direction}`,
351
+ clsWrite(wrapperClassName, ''),
352
+ {
353
+ 'toasts__wrapper--cascading': cascadingEnabled,
354
+ 'toasts__wrapper--only-one"': onlyShowOne
355
+ }
356
+ )}
357
+ ref={rootRef}
358
+ >
359
+ <div className="toasts">
360
+ {getProcessedData().map((item, i) => (
361
+ <Item
362
+ ref={el => progressObjRef.current.set(item.id as string, el)}
363
+ key={item.id}
364
+ uniqueID={item.id}
365
+ isNew={!progressObjRef.current.has(item.id as string)} // Mark the new toast
366
+ onlyOne={data.length === 1}
367
+ depth={depth - i}
368
+ index={i}
369
+ title={item.title}
370
+ note={item.note}
371
+ theme={item.theme}
372
+ lock={item.lock}
373
+ cascading={cascadingEnabled}
374
+ schemeBody={item.schemeBody}
375
+ schemeHeader={item.schemeHeader}
376
+ closeBtnColor={item.closeBtnColor}
377
+ closeDisabled={item.closeDisabled}
378
+ message={item.message}
379
+ autoCloseTime={item.autoCloseTime}
380
+ evStart={handleProgressStart}
381
+ evPause={handleProgressPaused}
382
+ evClose={handleClose}
383
+ />
384
+ ))}
385
+ </div>
386
+ </div>
387
+ </RootPortal>
388
+ );
389
+ };
390
+
391
+ export default Toast;
@@ -0,0 +1,104 @@
1
+ import React, { createContext, useContext, useReducer } from 'react';
2
+ import { Toast } from './Toast';
3
+ import type { ToastOptions, ToastGlobalConfig } from './types';
4
+
5
+ interface ToastItem extends ToastOptions {
6
+ id: string;
7
+ }
8
+
9
+ interface ToastContextState {
10
+ toasts: ToastItem[];
11
+ config: ToastGlobalConfig;
12
+ }
13
+
14
+ type ToastAction =
15
+ | { type: 'ADD_TOAST'; payload: ToastItem }
16
+ | { type: 'REMOVE_TOAST'; payload: string }
17
+ | { type: 'REMOVE_ALL' }
18
+ | { type: 'UPDATE_CONFIG'; payload: ToastGlobalConfig };
19
+
20
+
21
+ const defaultConfig: ToastGlobalConfig = {
22
+ defaultWrapperClassName: '',
23
+ defaultOnlyShowOne: false,
24
+ defaultDirection: 'bottom-center',
25
+ defaultCascading: false,
26
+ defaultReverseDisplay: false
27
+ };
28
+
29
+
30
+ const ToastContext = createContext<{
31
+ state: ToastContextState;
32
+ dispatch: React.Dispatch<ToastAction>;
33
+ } | undefined>(undefined);
34
+
35
+ const toastReducer = (state: ToastContextState, action: ToastAction): ToastContextState => {
36
+ switch (action.type) {
37
+ case 'ADD_TOAST':
38
+ return {
39
+ ...state,
40
+ toasts: [...state.toasts, action.payload],
41
+ };
42
+ case 'REMOVE_TOAST':
43
+ return {
44
+ ...state,
45
+ toasts: state.toasts.filter((toast) => toast.id !== action.payload),
46
+ };
47
+ case 'REMOVE_ALL':
48
+ return {
49
+ ...state,
50
+ toasts: [],
51
+ };
52
+ case 'UPDATE_CONFIG':
53
+ return {
54
+ ...state,
55
+ config: { ...state.config, ...action.payload }
56
+ };
57
+ default:
58
+ return state;
59
+ }
60
+ };
61
+
62
+ export const ToastProvider: React.FC<{
63
+ children: React.ReactNode;
64
+ config?: ToastGlobalConfig;
65
+ }> = ({ children, config = {} }) => {
66
+ const [state, dispatch] = useReducer(toastReducer, {
67
+ toasts: [],
68
+ config: { ...defaultConfig, ...config }
69
+ });
70
+
71
+ return (
72
+ <ToastContext.Provider value={{ state, dispatch }}>
73
+ {children}
74
+ <Toast
75
+ data={state.toasts}
76
+
77
+ // default props
78
+ defaultWrapperClassName={state.config.defaultWrapperClassName}
79
+ defaultDirection={state.config.defaultDirection}
80
+ defaultOnlyShowOne={state.config.defaultOnlyShowOne}
81
+ defaultCascading={state.config.defaultCascading}
82
+ defaultReverseDisplay={state.config.defaultReverseDisplay}
83
+
84
+ //
85
+ onUpdate={(updatedData) => {
86
+ // Iterate through the current toasts and remove the toasts that are not in the "updatedData"
87
+ state.toasts.forEach(toast => {
88
+ if (!updatedData.find(item => item.id === toast.id)) {
89
+ dispatch({ type: 'REMOVE_TOAST', payload: toast.id });
90
+ }
91
+ });
92
+ }}
93
+ />
94
+ </ToastContext.Provider>
95
+ );
96
+ };
97
+
98
+ export const useToastContext = () => {
99
+ const context = useContext(ToastContext);
100
+ if (!context) {
101
+ throw new Error('useToastContext must be used within a ToastProvider');
102
+ }
103
+ return context;
104
+ };