funda-ui 4.7.625 → 4.7.701
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/CascadingSelect/index.js +2 -2
- package/CascadingSelectE2E/index.js +2 -2
- package/Chatbox/index.js +3 -17
- package/Checkbox/index.js +3 -3
- package/ColorPicker/index.js +3 -18
- package/Date/index.js +3 -18
- package/EventCalendarTimeline/index.d.ts +1 -1
- package/EventCalendarTimeline/index.js +11 -1
- package/File/index.d.ts +9 -0
- package/File/index.js +245 -93
- package/Input/index.js +3 -18
- package/LiveSearch/index.js +3 -18
- package/NativeSelect/index.js +3 -3
- package/NumberInput/index.js +3 -18
- package/Popover/index.css +198 -0
- package/Popover/index.d.ts +4 -0
- package/Popover/index.js +1808 -0
- package/README.md +1 -0
- package/Radio/index.js +3 -3
- package/RangeSlider/index.js +3 -18
- package/SearchBar/index.js +3 -18
- package/Select/index.js +3 -2
- package/Switch/index.js +3 -3
- package/TagInput/index.css +31 -31
- package/TagInput/index.js +12 -23
- package/Textarea/index.js +3 -17
- package/Utils/useSSE.d.ts +9 -0
- package/Utils/useSSE.js +211 -0
- package/all.d.ts +1 -0
- package/all.js +1 -0
- package/lib/cjs/CascadingSelect/index.js +2 -2
- package/lib/cjs/CascadingSelectE2E/index.js +2 -2
- package/lib/cjs/Chatbox/index.js +3 -17
- package/lib/cjs/Checkbox/index.js +3 -3
- package/lib/cjs/ColorPicker/index.js +3 -18
- package/lib/cjs/Date/index.js +3 -18
- package/lib/cjs/EventCalendarTimeline/index.d.ts +1 -1
- package/lib/cjs/EventCalendarTimeline/index.js +11 -1
- package/lib/cjs/File/index.d.ts +9 -0
- package/lib/cjs/File/index.js +245 -93
- package/lib/cjs/Input/index.js +3 -18
- package/lib/cjs/LiveSearch/index.js +3 -18
- package/lib/cjs/NativeSelect/index.js +3 -3
- package/lib/cjs/NumberInput/index.js +3 -18
- package/lib/cjs/Popover/index.d.ts +4 -0
- package/lib/cjs/Popover/index.js +1808 -0
- package/lib/cjs/Radio/index.js +3 -3
- package/lib/cjs/RangeSlider/index.js +3 -18
- package/lib/cjs/SearchBar/index.js +3 -18
- package/lib/cjs/Select/index.js +3 -2
- package/lib/cjs/Switch/index.js +3 -3
- package/lib/cjs/TagInput/index.js +12 -23
- package/lib/cjs/Textarea/index.js +3 -17
- package/lib/cjs/Utils/useSSE.d.ts +9 -0
- package/lib/cjs/Utils/useSSE.js +211 -0
- package/lib/cjs/index.d.ts +1 -0
- package/lib/cjs/index.js +1 -0
- package/lib/css/Popover/index.css +198 -0
- package/lib/css/TagInput/index.css +31 -31
- package/lib/esm/CascadingSelect/index.tsx +2 -2
- package/lib/esm/CascadingSelectE2E/index.tsx +2 -2
- package/lib/esm/Checkbox/index.tsx +3 -3
- package/lib/esm/ColorPicker/index.tsx +4 -15
- package/lib/esm/EventCalendarTimeline/index.tsx +11 -2
- package/lib/esm/File/index.tsx +148 -23
- package/lib/esm/Input/index.tsx +6 -17
- package/lib/esm/NativeSelect/index.tsx +3 -3
- package/lib/esm/NumberInput/index.tsx +7 -15
- package/lib/esm/Popover/Popover.tsx +251 -0
- package/lib/esm/Popover/PopoverClose.tsx +51 -0
- package/lib/esm/Popover/PopoverContent.tsx +72 -0
- package/lib/esm/Popover/PopoverTrigger.tsx +62 -0
- package/lib/esm/Popover/index.scss +272 -0
- package/lib/esm/Popover/index.tsx +4 -0
- package/lib/esm/Radio/index.tsx +3 -3
- package/lib/esm/SearchBar/index.tsx +8 -12
- package/lib/esm/Select/index.tsx +2 -2
- package/lib/esm/Switch/index.tsx +3 -3
- package/lib/esm/TagInput/index.scss +24 -24
- package/lib/esm/TagInput/index.tsx +13 -20
- package/lib/esm/Textarea/index.tsx +6 -14
- package/lib/esm/Utils/hooks/useSSE.tsx +109 -0
- package/lib/esm/index.js +1 -0
- package/package.json +1 -1
package/lib/esm/File/index.tsx
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from 'funda-utils/dist/cjs/buffer';
|
|
9
9
|
import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
|
|
10
10
|
|
|
11
|
+
|
|
12
|
+
|
|
11
13
|
export type FileProps = {
|
|
12
14
|
contentRef?: React.ForwardedRef<any>;
|
|
13
15
|
wrapperClassName?: string;
|
|
@@ -41,6 +43,15 @@ export type FileProps = {
|
|
|
41
43
|
fetchFuncAsync?: any;
|
|
42
44
|
fetchFuncMethod?: string;
|
|
43
45
|
fetchFuncMethodParams?: any[];
|
|
46
|
+
formDataAppend?: (formData: FormData, files: FileList) => void;
|
|
47
|
+
/** Enable chunked upload for large files */
|
|
48
|
+
enableChunkedUpload?: boolean;
|
|
49
|
+
/** Chunk size in bytes (default: 2MB) */
|
|
50
|
+
chunkSize?: number;
|
|
51
|
+
/** Custom function to append chunk data to FormData. Receives (formData: FormData, chunk: Blob, chunkIndex: number, totalChunks: number, file: File) */
|
|
52
|
+
chunkDataAppend?: (formData: FormData, chunk: Blob, chunkIndex: number, totalChunks: number, file: File) => void;
|
|
53
|
+
/** Callback for chunk upload progress. Receives (uploadedBytes: number, totalBytes: number, file: File, chunkIndex: number, totalChunks: number) */
|
|
54
|
+
onChunkProgress?: (uploadedBytes: number, totalBytes: number, file: File, chunkIndex: number, totalChunks: number) => void;
|
|
44
55
|
onChange?: (e: any, e2: any, value: any) => void;
|
|
45
56
|
onComplete?: (e: any, e2: any, callback: any, incomingData: string | null | undefined) => void;
|
|
46
57
|
onProgress?: (files: any, e: any, e2: any) => void;
|
|
@@ -79,9 +90,17 @@ const File = forwardRef((props: FileProps, externalRef: any) => {
|
|
|
79
90
|
fetchFuncAsync,
|
|
80
91
|
fetchFuncMethod,
|
|
81
92
|
fetchFuncMethodParams,
|
|
93
|
+
formDataAppend,
|
|
82
94
|
onChange,
|
|
83
95
|
onComplete,
|
|
84
96
|
onProgress,
|
|
97
|
+
|
|
98
|
+
// Upload file in chunks
|
|
99
|
+
enableChunkedUpload,
|
|
100
|
+
chunkSize = 2 * 1024 * 1024, // Default 2MB
|
|
101
|
+
chunkDataAppend,
|
|
102
|
+
onChunkProgress,
|
|
103
|
+
|
|
85
104
|
...attributes
|
|
86
105
|
} = props;
|
|
87
106
|
|
|
@@ -144,10 +163,9 @@ const File = forwardRef((props: FileProps, externalRef: any) => {
|
|
|
144
163
|
|
|
145
164
|
let res = {};
|
|
146
165
|
if (typeof window === 'undefined') return res;
|
|
147
|
-
|
|
148
|
-
|
|
166
|
+
|
|
149
167
|
try {
|
|
150
|
-
const response = await fetch(url + '?' + new URLSearchParams(fetchParams as any), {
|
|
168
|
+
const response = await fetch(typeof fetchParams !== 'undefined' ? url + '?' + new URLSearchParams(fetchParams as any) : url, {
|
|
151
169
|
method: methord,
|
|
152
170
|
body: data,
|
|
153
171
|
...rest
|
|
@@ -165,6 +183,75 @@ const File = forwardRef((props: FileProps, externalRef: any) => {
|
|
|
165
183
|
|
|
166
184
|
}
|
|
167
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Upload file in chunks
|
|
188
|
+
*/
|
|
189
|
+
async function uploadFileInChunks(file: File, url: string, method: string = 'POST'): Promise<any> {
|
|
190
|
+
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
191
|
+
const uploadId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
192
|
+
let uploadedBytes = 0;
|
|
193
|
+
|
|
194
|
+
// Upload each chunk sequentially
|
|
195
|
+
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
|
196
|
+
const start = chunkIndex * chunkSize;
|
|
197
|
+
const end = Math.min(start + chunkSize, file.size);
|
|
198
|
+
const chunk = file.slice(start, end);
|
|
199
|
+
|
|
200
|
+
const formData = new FormData();
|
|
201
|
+
|
|
202
|
+
if (chunkDataAppend) {
|
|
203
|
+
// Use custom chunk data append function
|
|
204
|
+
chunkDataAppend(formData, chunk, chunkIndex, totalChunks, file);
|
|
205
|
+
} else {
|
|
206
|
+
// Default chunk upload format
|
|
207
|
+
formData.append('chunk', chunk);
|
|
208
|
+
formData.append('chunkIndex', chunkIndex.toString());
|
|
209
|
+
formData.append('totalChunks', totalChunks.toString());
|
|
210
|
+
formData.append('uploadId', uploadId);
|
|
211
|
+
formData.append('fileName', file.name);
|
|
212
|
+
formData.append('fileSize', file.size.toString());
|
|
213
|
+
formData.append('fileType', file.type);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Update progress
|
|
217
|
+
uploadedBytes += (end - start);
|
|
218
|
+
onChunkProgress?.(uploadedBytes, file.size, file, chunkIndex, totalChunks);
|
|
219
|
+
|
|
220
|
+
// Upload chunk
|
|
221
|
+
const response: any = await fetchDataDefault(
|
|
222
|
+
url,
|
|
223
|
+
formData,
|
|
224
|
+
method,
|
|
225
|
+
{
|
|
226
|
+
headers: { 'Content-Type': 'multipart/form-data' }
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Check if chunk upload failed
|
|
231
|
+
if (response.error) {
|
|
232
|
+
throw new Error(`Chunk ${chunkIndex + 1}/${totalChunks} upload failed: ${response.error}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If server returns a specific response indicating chunk upload success/failure, handle it
|
|
236
|
+
if (response.code !== undefined && response.code !== 0 && response.code !== 200) {
|
|
237
|
+
throw new Error(`Chunk ${chunkIndex + 1}/${totalChunks} upload failed: ${response.message || 'Unknown error'}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// After all chunks are uploaded, return the final response
|
|
242
|
+
// The server should merge chunks and return the final result
|
|
243
|
+
return {
|
|
244
|
+
code: 0,
|
|
245
|
+
message: 'Upload completed',
|
|
246
|
+
data: {
|
|
247
|
+
uploadId,
|
|
248
|
+
fileName: file.name,
|
|
249
|
+
fileSize: file.size,
|
|
250
|
+
totalChunks
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
168
255
|
|
|
169
256
|
function handleLabelEnter(event: any) {
|
|
170
257
|
event.currentTarget.getElementsByTagName('button')[0].className = labelHoverClassName ? labelHoverClassName : 'btn btn-primary';
|
|
@@ -192,26 +279,70 @@ const File = forwardRef((props: FileProps, externalRef: any) => {
|
|
|
192
279
|
setProgressing(true);
|
|
193
280
|
|
|
194
281
|
|
|
282
|
+
// Use the default "fetch()" method to handle file references
|
|
195
283
|
if (fetchUrl) {
|
|
196
284
|
|
|
197
|
-
|
|
198
|
-
|
|
285
|
+
// Check if chunked upload is enabled
|
|
286
|
+
if (enableChunkedUpload) {
|
|
287
|
+
// Upload files in chunks
|
|
288
|
+
(async () => {
|
|
289
|
+
try {
|
|
290
|
+
const uploadPromises: Promise<any>[] = [];
|
|
291
|
+
|
|
292
|
+
for (let i = 0; i < curFiles.length; i++) {
|
|
293
|
+
const file = curFiles[i];
|
|
294
|
+
uploadPromises.push(uploadFileInChunks(file, fetchUrl, fetchMethod ? fetchMethod : 'POST'));
|
|
295
|
+
}
|
|
199
296
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
297
|
+
const results = await Promise.all(uploadPromises);
|
|
298
|
+
|
|
299
|
+
// Combine results from all files
|
|
300
|
+
const jsonData = {
|
|
301
|
+
code: 0,
|
|
302
|
+
message: 'All files uploaded successfully',
|
|
303
|
+
data: results
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
onComplete?.(fileInputRef.current, submitRef.current, jsonData, incomingData);
|
|
307
|
+
setProgressing(false);
|
|
308
|
+
resetDefaultVal();
|
|
309
|
+
} catch (error: any) {
|
|
310
|
+
const errorResponse = {
|
|
311
|
+
code: -1,
|
|
312
|
+
message: error.message || 'Upload failed',
|
|
313
|
+
error: error
|
|
314
|
+
};
|
|
315
|
+
onComplete?.(fileInputRef.current, submitRef.current, errorResponse, incomingData);
|
|
316
|
+
setProgressing(false);
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
} else {
|
|
320
|
+
// Original non-chunked upload
|
|
321
|
+
const formData = new FormData();
|
|
322
|
+
|
|
323
|
+
if (formDataAppend) {
|
|
324
|
+
// Use custom formData.append function
|
|
325
|
+
formDataAppend(formData, curFiles);
|
|
326
|
+
} else {
|
|
327
|
+
// Default behavior
|
|
328
|
+
formData.append('action', 'upload_plug_action');
|
|
329
|
+
for (let i = 0; i < curFiles.length; i++) {
|
|
330
|
+
formData.append("clientFiles", curFiles[i]);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
203
333
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
334
|
+
fetchDataDefault(fetchUrl, formData, fetchMethod ? fetchMethod : 'POST', {
|
|
335
|
+
headers: { 'Content-Type': 'multipart/form-data' }
|
|
336
|
+
}).then(function (response: any) {
|
|
337
|
+
const jsonData = response;
|
|
338
|
+
onComplete?.(fileInputRef.current, submitRef.current, jsonData, incomingData);
|
|
339
|
+
setProgressing(false);
|
|
210
340
|
|
|
211
|
-
|
|
212
|
-
|
|
341
|
+
// update default value
|
|
342
|
+
resetDefaultVal();
|
|
213
343
|
|
|
214
|
-
|
|
344
|
+
});
|
|
345
|
+
}
|
|
215
346
|
} else {
|
|
216
347
|
|
|
217
348
|
|
|
@@ -322,12 +453,6 @@ const File = forwardRef((props: FileProps, externalRef: any) => {
|
|
|
322
453
|
setDefaultValue(null);
|
|
323
454
|
|
|
324
455
|
|
|
325
|
-
//----
|
|
326
|
-
//remove focus style
|
|
327
|
-
if (val === '') {
|
|
328
|
-
rootRef.current?.classList.remove('focus');
|
|
329
|
-
}
|
|
330
|
-
|
|
331
456
|
//
|
|
332
457
|
onChange?.(fileInputRef.current, submitRef.current, fileInputRef.current.files);
|
|
333
458
|
|
package/lib/esm/Input/index.tsx
CHANGED
|
@@ -354,8 +354,6 @@ const Input = forwardRef((props: InputProps, externalRef: any) => {
|
|
|
354
354
|
|
|
355
355
|
|
|
356
356
|
function handleFocus(event: FocusEvent<HTMLInputElement>) {
|
|
357
|
-
rootRef.current?.classList.add('focus');
|
|
358
|
-
|
|
359
357
|
//
|
|
360
358
|
if (valRef.current) {
|
|
361
359
|
onFocus?.(event, onComposition, valRef.current);
|
|
@@ -365,13 +363,6 @@ const Input = forwardRef((props: InputProps, externalRef: any) => {
|
|
|
365
363
|
function handleChange(event: ChangeEvent<HTMLInputElement> | KeyboardEvent<HTMLInputElement> | null, curVal: string) {
|
|
366
364
|
setChangedVal(curVal);
|
|
367
365
|
|
|
368
|
-
|
|
369
|
-
//----
|
|
370
|
-
//remove focus style
|
|
371
|
-
if (curVal === '') {
|
|
372
|
-
rootRef.current?.classList.remove('focus');
|
|
373
|
-
}
|
|
374
|
-
|
|
375
366
|
//
|
|
376
367
|
if (valRef.current) {
|
|
377
368
|
onChange?.(event, onComposition, valRef.current, curVal);
|
|
@@ -390,13 +381,6 @@ const Input = forwardRef((props: InputProps, externalRef: any) => {
|
|
|
390
381
|
const el = event.target;
|
|
391
382
|
const val = event.target.value;
|
|
392
383
|
|
|
393
|
-
|
|
394
|
-
//----
|
|
395
|
-
//remove focus style
|
|
396
|
-
if (val === '') {
|
|
397
|
-
rootRef.current?.classList.remove('focus');
|
|
398
|
-
}
|
|
399
|
-
|
|
400
384
|
//
|
|
401
385
|
if (valRef.current) {
|
|
402
386
|
onBlur?.(event, onComposition, valRef.current);
|
|
@@ -520,7 +504,12 @@ const Input = forwardRef((props: InputProps, externalRef: any) => {
|
|
|
520
504
|
return (
|
|
521
505
|
<>
|
|
522
506
|
|
|
523
|
-
<div className={
|
|
507
|
+
<div className={combinedCls(
|
|
508
|
+
clsWrite(wrapperClassName, 'mb-3 position-relative'),
|
|
509
|
+
{
|
|
510
|
+
'focus-floating': changedVal !== ''
|
|
511
|
+
}
|
|
512
|
+
)} ref={rootRef}>
|
|
524
513
|
{label ? <>{typeof label === 'string' ? <label htmlFor={idRes} className="form-label" dangerouslySetInnerHTML={{ __html: `${label}` }}></label> : <label htmlFor={idRes} className="form-label">{label}</label>}</> : null}
|
|
525
514
|
|
|
526
515
|
<div className={combinedCls(
|
|
@@ -260,7 +260,7 @@ const NativeSelect = forwardRef((props: NativeSelectProps, externalRef: any) =>
|
|
|
260
260
|
|
|
261
261
|
//
|
|
262
262
|
function handleFocus(event: any) {
|
|
263
|
-
rootRef.current?.classList.add('focus');
|
|
263
|
+
rootRef.current?.classList.add('focus-floating');
|
|
264
264
|
|
|
265
265
|
//
|
|
266
266
|
onFocus?.(event);
|
|
@@ -283,7 +283,7 @@ const NativeSelect = forwardRef((props: NativeSelectProps, externalRef: any) =>
|
|
|
283
283
|
|
|
284
284
|
//----
|
|
285
285
|
//remove focus style
|
|
286
|
-
rootRef.current?.classList.remove('focus');
|
|
286
|
+
rootRef.current?.classList.remove('focus-floating');
|
|
287
287
|
|
|
288
288
|
//
|
|
289
289
|
if ( typeof(onChange) === 'function' ) {
|
|
@@ -298,7 +298,7 @@ const NativeSelect = forwardRef((props: NativeSelectProps, externalRef: any) =>
|
|
|
298
298
|
|
|
299
299
|
//----
|
|
300
300
|
//remove focus style
|
|
301
|
-
rootRef.current?.classList.remove('focus');
|
|
301
|
+
rootRef.current?.classList.remove('focus-floating');
|
|
302
302
|
|
|
303
303
|
//
|
|
304
304
|
onBlur?.(event);
|
|
@@ -183,7 +183,6 @@ const NumberInput = forwardRef((props: NumberInputProps, externalRef: any) => {
|
|
|
183
183
|
|
|
184
184
|
|
|
185
185
|
function handleFocus(event: FocusEvent<HTMLInputElement>) {
|
|
186
|
-
rootRef.current?.classList.add('focus');
|
|
187
186
|
|
|
188
187
|
//
|
|
189
188
|
onFocus?.(event, valRef.current);
|
|
@@ -220,12 +219,7 @@ const NumberInput = forwardRef((props: NumberInputProps, externalRef: any) => {
|
|
|
220
219
|
setChangedVal(resToInput);
|
|
221
220
|
|
|
222
221
|
|
|
223
|
-
|
|
224
|
-
//remove focus style
|
|
225
|
-
if (val === '') {
|
|
226
|
-
rootRef.current?.classList.remove('focus');
|
|
227
|
-
}
|
|
228
|
-
|
|
222
|
+
|
|
229
223
|
//
|
|
230
224
|
onChange?.(event, valRef.current, Number(!isNumeric(newVal) ? '0' : resToInput));
|
|
231
225
|
|
|
@@ -247,13 +241,6 @@ const NumberInput = forwardRef((props: NumberInputProps, externalRef: any) => {
|
|
|
247
241
|
setChangedVal('0');
|
|
248
242
|
}
|
|
249
243
|
|
|
250
|
-
|
|
251
|
-
//----
|
|
252
|
-
//remove focus style
|
|
253
|
-
if (val === '') {
|
|
254
|
-
rootRef.current?.classList.remove('focus');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
244
|
//
|
|
258
245
|
onBlur?.(event, valRef.current);
|
|
259
246
|
|
|
@@ -286,7 +273,12 @@ const NumberInput = forwardRef((props: NumberInputProps, externalRef: any) => {
|
|
|
286
273
|
return (
|
|
287
274
|
<>
|
|
288
275
|
|
|
289
|
-
<div className={
|
|
276
|
+
<div className={combinedCls(
|
|
277
|
+
clsWrite(wrapperClassName, 'mb-3 position-relative'),
|
|
278
|
+
{
|
|
279
|
+
'focus-floating': changedVal !== ''
|
|
280
|
+
}
|
|
281
|
+
)} ref={rootRef}>
|
|
290
282
|
{label ? <>{typeof label === 'string' ? <label htmlFor={idRes} className="form-label" dangerouslySetInnerHTML={{__html: `${label}`}}></label> : <label htmlFor={idRes} className="form-label">{label}</label>}</> : null}
|
|
291
283
|
|
|
292
284
|
<div className="position-relative">
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useImperativeHandle, forwardRef, useMemo, createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import useComId from 'funda-utils/dist/cjs/useComId';
|
|
4
|
+
import useClickOutside from 'funda-utils/dist/cjs/useClickOutside';
|
|
5
|
+
import {
|
|
6
|
+
getAbsolutePositionOfStage
|
|
7
|
+
} from 'funda-utils/dist/cjs/getElementProperty';
|
|
8
|
+
import { getElCSS } from 'funda-utils/dist/cjs/inputsCalculation';
|
|
9
|
+
|
|
10
|
+
// Context for Popover state management
|
|
11
|
+
export interface PopoverContextValue {
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
setIsOpen: (open: boolean) => void;
|
|
14
|
+
triggerRef: React.RefObject<HTMLElement>;
|
|
15
|
+
contentRef: React.RefObject<HTMLDivElement>;
|
|
16
|
+
id: string;
|
|
17
|
+
direction?: string;
|
|
18
|
+
offset?: number;
|
|
19
|
+
exceededSidePosOffset?: number;
|
|
20
|
+
size?: string;
|
|
21
|
+
popupArrowColor?: number[];
|
|
22
|
+
popupContentStyle?: React.CSSProperties;
|
|
23
|
+
wrapperClassName?: string;
|
|
24
|
+
position: { x: string; y: string };
|
|
25
|
+
setPosition: (pos: { x: string; y: string }) => void;
|
|
26
|
+
popupArrowStyle?: React.CSSProperties;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const PopoverContext = createContext<PopoverContextValue | null>(null);
|
|
30
|
+
|
|
31
|
+
export const usePopoverContext = () => {
|
|
32
|
+
const context = useContext(PopoverContext);
|
|
33
|
+
if (!context) {
|
|
34
|
+
throw new Error('Popover components must be used within a Popover');
|
|
35
|
+
}
|
|
36
|
+
return context;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Main Popover component (container)
|
|
40
|
+
export type PopoverProps = {
|
|
41
|
+
/** The direction of the tip. Defaults to `top`. Possible values are: `top`, `top-right`, `top-left`, `bottom`, `bottom-right`, `bottom-left` */
|
|
42
|
+
direction?: string;
|
|
43
|
+
/** Position offset */
|
|
44
|
+
offset?: number;
|
|
45
|
+
/** Offset px that exceeds the far right or left side of the screen */
|
|
46
|
+
exceededSidePosOffset?: number;
|
|
47
|
+
/** The size of the content area. Defaults to `auto`. Possible values are: `auto`, `large`, `medium`, `small` */
|
|
48
|
+
size?: string;
|
|
49
|
+
/** Custom color for the popup arrow */
|
|
50
|
+
popupArrowColor?: number[];
|
|
51
|
+
/** Custom style for the popup content */
|
|
52
|
+
popupContentStyle?: React.CSSProperties;
|
|
53
|
+
/** The class name of the control wrapper. */
|
|
54
|
+
wrapperClassName?: string;
|
|
55
|
+
/** -- */
|
|
56
|
+
id?: string;
|
|
57
|
+
/** Controls whether the Popover is open (controlled mode) */
|
|
58
|
+
open?: boolean;
|
|
59
|
+
/** Callback fired when the open state changes (controlled mode) */
|
|
60
|
+
onOpenChange?: (open: boolean) => void;
|
|
61
|
+
children: React.ReactNode;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const Popover = forwardRef<any, PopoverProps>((props, ref) => {
|
|
65
|
+
const {
|
|
66
|
+
direction,
|
|
67
|
+
offset,
|
|
68
|
+
exceededSidePosOffset,
|
|
69
|
+
size,
|
|
70
|
+
popupArrowColor,
|
|
71
|
+
popupContentStyle,
|
|
72
|
+
wrapperClassName,
|
|
73
|
+
id,
|
|
74
|
+
open,
|
|
75
|
+
onOpenChange,
|
|
76
|
+
children
|
|
77
|
+
} = props;
|
|
78
|
+
|
|
79
|
+
const POS_OFFSET = Number(offset) || 4;
|
|
80
|
+
const EXCEEDED_SIDE_POS_OFFSET = Number(exceededSidePosOffset) || 15;
|
|
81
|
+
const uniqueID = useComId();
|
|
82
|
+
const idRes = id || uniqueID;
|
|
83
|
+
const triggerRef = useRef<HTMLElement>(null);
|
|
84
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
85
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState<boolean>(false);
|
|
86
|
+
const isControlled = typeof open === 'boolean';
|
|
87
|
+
const isOpen = isControlled ? !!open : uncontrolledOpen;
|
|
88
|
+
const [position, setPosition] = useState<{ x: string; y: string }>({
|
|
89
|
+
x: '0',
|
|
90
|
+
y: '0'
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const setIsOpen = (nextOpen: boolean) => {
|
|
94
|
+
if (!isControlled) {
|
|
95
|
+
setUncontrolledOpen(nextOpen);
|
|
96
|
+
}
|
|
97
|
+
if (typeof onOpenChange === 'function') {
|
|
98
|
+
onOpenChange(nextOpen);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const popupArrowStyle = useMemo(() => {
|
|
103
|
+
if (
|
|
104
|
+
typeof popupArrowColor !== 'undefined' &&
|
|
105
|
+
Array.isArray(popupArrowColor) &&
|
|
106
|
+
popupArrowColor.length === 4
|
|
107
|
+
) {
|
|
108
|
+
return {
|
|
109
|
+
'--cus-popover-arrow-bg-top': `url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%28${popupArrowColor[0]},%20${popupArrowColor[1]},%20${popupArrowColor[2]},%20${popupArrowColor[3]}%29%22%20transform%3D%22rotate%280%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat`,
|
|
110
|
+
'--cus-popover-arrow-bg-bottom': `url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%28${popupArrowColor[0]},%20${popupArrowColor[1]},%20${popupArrowColor[2]},%20${popupArrowColor[3]}%29%22%20transform%3D%22rotate%28180%2018%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat`,
|
|
111
|
+
'--cus-popover-arrow-bg-left': `url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%28${popupArrowColor[0]},%20${popupArrowColor[1]},%20${popupArrowColor[2]},%20${popupArrowColor[3]}%29%22%20transform%3D%22rotate%28-90%2018%2018%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat`,
|
|
112
|
+
'--cus-popover-arrow-bg-right': `url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%28${popupArrowColor[0]},%20${popupArrowColor[1]},%20${popupArrowColor[2]},%20${popupArrowColor[3]}%29%22%20transform%3D%22rotate%2890%206%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat`,
|
|
113
|
+
} as React.CSSProperties;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}, [popupArrowColor]);
|
|
117
|
+
|
|
118
|
+
// Calculate position based on trigger element
|
|
119
|
+
const calculatePosition = () => {
|
|
120
|
+
const triggerEl = triggerRef.current;
|
|
121
|
+
if (!triggerEl) return;
|
|
122
|
+
|
|
123
|
+
const { x, y, width, height } = getAbsolutePositionOfStage(triggerEl);
|
|
124
|
+
const pos = direction || 'top';
|
|
125
|
+
|
|
126
|
+
if (pos.indexOf('top') >= 0) {
|
|
127
|
+
setPosition({
|
|
128
|
+
x: x + (width / 2) + 'px',
|
|
129
|
+
y: y - height - POS_OFFSET + 'px'
|
|
130
|
+
});
|
|
131
|
+
} else if (pos.indexOf('bottom') >= 0) {
|
|
132
|
+
setPosition({
|
|
133
|
+
x: x + (width / 2) + 'px',
|
|
134
|
+
y: y + height + POS_OFFSET + 'px'
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Expose show/hide methods to parent via ref
|
|
140
|
+
useImperativeHandle(ref, () => ({
|
|
141
|
+
show: () => {
|
|
142
|
+
calculatePosition();
|
|
143
|
+
setIsOpen(true);
|
|
144
|
+
},
|
|
145
|
+
hide: () => {
|
|
146
|
+
setIsOpen(false);
|
|
147
|
+
}
|
|
148
|
+
}), [direction, POS_OFFSET]);
|
|
149
|
+
|
|
150
|
+
// Click outside to close
|
|
151
|
+
useClickOutside({
|
|
152
|
+
enabled: isOpen && triggerRef.current !== null,
|
|
153
|
+
isOutside: (event: any) => {
|
|
154
|
+
// Prevent closing when clicking inside the popover wrapper
|
|
155
|
+
if (contentRef.current && contentRef.current.contains(event.target)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return (
|
|
159
|
+
triggerRef.current !== event.target &&
|
|
160
|
+
!triggerRef.current?.contains(event.target as HTMLElement)
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
handle: () => {
|
|
164
|
+
setIsOpen(false);
|
|
165
|
+
}
|
|
166
|
+
}, [isOpen]);
|
|
167
|
+
|
|
168
|
+
const exceededOffsetInit = () => {
|
|
169
|
+
// Determine whether it exceeds the far right or left side of the screen
|
|
170
|
+
//------------------
|
|
171
|
+
const _modalRef: any = contentRef.current;
|
|
172
|
+
if (_modalRef === null) return;
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
const _modalContent = _modalRef.querySelector('.cus-popover__content');
|
|
176
|
+
if (!_modalContent) return;
|
|
177
|
+
|
|
178
|
+
const _modalBox = _modalContent.getBoundingClientRect();
|
|
179
|
+
if (typeof _modalContent.dataset.offset === 'undefined' && _modalBox.left > 0) {
|
|
180
|
+
|
|
181
|
+
// Adjust the coordinates due to height
|
|
182
|
+
//------------------
|
|
183
|
+
const triggerEl: any = document.querySelector(`[data-overlay-id="${_modalRef.id}"]`);
|
|
184
|
+
if (triggerEl !== null) {
|
|
185
|
+
let pos = triggerEl.dataset.microtipPosition;
|
|
186
|
+
if (typeof pos === 'undefined') pos = 'top';
|
|
187
|
+
|
|
188
|
+
const _offsetY = _modalBox.height - getElCSS(_modalContent, 'font-size', true) - getElCSS(_modalContent, 'padding-top', true) - getElCSS(_modalContent, 'padding-bottom', true);
|
|
189
|
+
|
|
190
|
+
if (pos.indexOf('top') >= 0) {
|
|
191
|
+
_modalRef.style.transform = `translateY(-${_offsetY}px)`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 10 pixels is used to account for some bias in mobile devices
|
|
196
|
+
if ((_modalBox.right + 10) > window.innerWidth) {
|
|
197
|
+
const _modalOffsetPosition = _modalBox.right - window.innerWidth + EXCEEDED_SIDE_POS_OFFSET;
|
|
198
|
+
_modalContent.dataset.offset = _modalOffsetPosition;
|
|
199
|
+
_modalContent.style.marginLeft = `-${_modalOffsetPosition}px`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if ((_modalBox.left - 10) < 0) {
|
|
203
|
+
const _modalOffsetPosition = Math.abs(_modalBox.left) + EXCEEDED_SIDE_POS_OFFSET;
|
|
204
|
+
_modalContent.dataset.offset = _modalOffsetPosition;
|
|
205
|
+
_modalContent.style.marginLeft = `${_modalOffsetPosition}px`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Update position when opening
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (isOpen) {
|
|
213
|
+
calculatePosition();
|
|
214
|
+
}
|
|
215
|
+
}, [isOpen, direction, POS_OFFSET]);
|
|
216
|
+
|
|
217
|
+
// Prevent element data from being unable to be obtained when ref is null
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
exceededOffsetInit();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
const contextValue: PopoverContextValue = {
|
|
225
|
+
isOpen,
|
|
226
|
+
setIsOpen,
|
|
227
|
+
triggerRef,
|
|
228
|
+
contentRef,
|
|
229
|
+
id: idRes,
|
|
230
|
+
direction,
|
|
231
|
+
offset,
|
|
232
|
+
exceededSidePosOffset,
|
|
233
|
+
size,
|
|
234
|
+
popupArrowColor,
|
|
235
|
+
popupContentStyle,
|
|
236
|
+
wrapperClassName,
|
|
237
|
+
position,
|
|
238
|
+
setPosition,
|
|
239
|
+
popupArrowStyle
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<PopoverContext.Provider value={contextValue}>
|
|
244
|
+
{children}
|
|
245
|
+
</PopoverContext.Provider>
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
Popover.displayName = 'Popover';
|
|
250
|
+
|
|
251
|
+
export default Popover;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { combinedCls } from 'funda-utils/dist/cjs/cls';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import { usePopoverContext } from './Popover';
|
|
6
|
+
|
|
7
|
+
export type PopoverCloseProps = {
|
|
8
|
+
asChild?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
} & React.HTMLAttributes<HTMLElement>;
|
|
12
|
+
|
|
13
|
+
const PopoverClose = forwardRef<HTMLElement, PopoverCloseProps>((props, ref) => {
|
|
14
|
+
const { asChild, className, children, onClick, ...rest } = props;
|
|
15
|
+
const { setIsOpen } = usePopoverContext();
|
|
16
|
+
|
|
17
|
+
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
18
|
+
if (typeof onClick === 'function') {
|
|
19
|
+
onClick(event);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!event.defaultPrevented) {
|
|
23
|
+
setIsOpen(false);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const closeProps: any = {
|
|
28
|
+
ref,
|
|
29
|
+
className: combinedCls('cus-popover__close', className),
|
|
30
|
+
onClick: handleClick,
|
|
31
|
+
...rest
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (asChild && React.isValidElement(children)) {
|
|
35
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
36
|
+
...closeProps,
|
|
37
|
+
...(children.props || {})
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<button type="button" {...closeProps}>
|
|
43
|
+
{children || 'Close'}
|
|
44
|
+
</button>
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
PopoverClose.displayName = 'PopoverClose';
|
|
49
|
+
|
|
50
|
+
export default PopoverClose;
|
|
51
|
+
|