funda-ui 4.5.680 → 4.5.682

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 (35) hide show
  1. package/DragDropList/index.css +188 -0
  2. package/DragDropList/index.d.ts +43 -0
  3. package/DragDropList/index.js +1589 -0
  4. package/MultipleSelect/index.css +237 -144
  5. package/MultipleSelect/index.d.ts +23 -10
  6. package/MultipleSelect/index.js +2242 -1225
  7. package/README.md +3 -1
  8. package/Utils/useBoundedDrag.d.ts +127 -0
  9. package/Utils/useBoundedDrag.js +382 -0
  10. package/Utils/useDragDropPosition.d.ts +169 -0
  11. package/Utils/useDragDropPosition.js +456 -0
  12. package/all.d.ts +1 -0
  13. package/all.js +1 -0
  14. package/lib/cjs/DragDropList/index.d.ts +43 -0
  15. package/lib/cjs/DragDropList/index.js +1589 -0
  16. package/lib/cjs/MultipleSelect/index.d.ts +23 -10
  17. package/lib/cjs/MultipleSelect/index.js +2242 -1225
  18. package/lib/cjs/Utils/useBoundedDrag.d.ts +127 -0
  19. package/lib/cjs/Utils/useBoundedDrag.js +382 -0
  20. package/lib/cjs/Utils/useDragDropPosition.d.ts +169 -0
  21. package/lib/cjs/Utils/useDragDropPosition.js +456 -0
  22. package/lib/cjs/index.d.ts +1 -0
  23. package/lib/cjs/index.js +1 -0
  24. package/lib/css/DragDropList/index.css +188 -0
  25. package/lib/css/MultipleSelect/index.css +237 -144
  26. package/lib/esm/DragDropList/index.scss +245 -0
  27. package/lib/esm/DragDropList/index.tsx +493 -0
  28. package/lib/esm/MultipleSelect/index.scss +288 -183
  29. package/lib/esm/MultipleSelect/index.tsx +304 -166
  30. package/lib/esm/MultipleSelect/utils/func.ts +21 -1
  31. package/lib/esm/Utils/hooks/useBoundedDrag.tsx +303 -0
  32. package/lib/esm/Utils/hooks/useDragDropPosition.tsx +420 -0
  33. package/lib/esm/index.js +1 -0
  34. package/package.json +1 -1
  35. package/lib/esm/MultipleSelect/ItemList.tsx +0 -323
@@ -0,0 +1,493 @@
1
+ import React, { useState, useRef, useEffect, forwardRef } from 'react';
2
+
3
+ import {
4
+ convertTree,
5
+ addTreeDepth,
6
+ } from 'funda-utils/dist/cjs/tree';
7
+ import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
8
+ import useBoundedDrag from 'funda-utils/dist/cjs/useBoundedDrag';
9
+
10
+
11
+ export interface ListItem {
12
+ id: number;
13
+ parentId?: number;
14
+ label: string;
15
+ listItemLabel: string;
16
+ value: string;
17
+ queryString: string;
18
+ depth?: number;
19
+ children?: ListItem[];
20
+ disabled?: boolean;
21
+ appendControl?: React.ReactNode;
22
+ }
23
+
24
+
25
+ export interface DragDropListProps {
26
+ wrapperClassName?: string;
27
+ prefix?: string;
28
+ data?: ListItem[];
29
+ draggable?: boolean;
30
+ handleHide?: boolean;
31
+ handleIcon?: string;
32
+ handlePos?: 'left' | 'right';
33
+ dragMode?: 'handle' | 'block';
34
+ editable?: boolean;
35
+ itemStyle?: React.CSSProperties;
36
+ hierarchical?: boolean;
37
+ indentation?: string;
38
+ doubleIndent?: boolean;
39
+ alternateCollapse?: boolean;
40
+ arrow?: React.ReactNode;
41
+ onUpdate?: (items: ListItem[], curId: number) => void;
42
+ }
43
+
44
+ export interface EditValue {
45
+ [propName: string]: string | number;
46
+ }
47
+
48
+ export interface TouchOffset {
49
+ x: number;
50
+ y: number;
51
+ }
52
+ export interface OptionConfig {
53
+ [propName: string]: string | number | boolean | Function | any[];
54
+ }
55
+
56
+ const DragDropList = forwardRef((props: DragDropListProps, externalRef: any) => {
57
+ const {
58
+ wrapperClassName,
59
+ prefix = 'custom',
60
+ data,
61
+ draggable = true,
62
+ handleHide = false,
63
+ handleIcon = '☰',
64
+ handlePos = 'left',
65
+ dragMode = 'handle',
66
+ editable = false,
67
+ itemStyle,
68
+ hierarchical = true,
69
+ indentation,
70
+ doubleIndent,
71
+ alternateCollapse,
72
+ arrow = <><svg viewBox="0 0 22 22" width="8px"><path d="m345.44 248.29l-194.29 194.28c-12.359 12.365-32.397 12.365-44.75 0-12.354-12.354-12.354-32.391 0-44.744l171.91-171.91-171.91-171.9c-12.354-12.359-12.354-32.394 0-44.748 12.354-12.359 32.391-12.359 44.75 0l194.29 194.28c6.177 6.18 9.262 14.271 9.262 22.366 0 8.099-3.091 16.196-9.267 22.373" transform="matrix(.03541-.00013.00013.03541 2.98 3.02)" fill="#a5a5a5" /></svg></>,
73
+ onUpdate,
74
+ ...attributes
75
+ } = props;
76
+
77
+
78
+ const INDENT_PLACEHOLDER = doubleIndent ? `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;` : `&nbsp;&nbsp;&nbsp;&nbsp;`;
79
+ const INDENT_LAST_PLACEHOLDER = `${typeof indentation !== 'undefined' && indentation !== '' ? `${indentation}&nbsp;&nbsp;` : ''}`;
80
+
81
+ const rootRef = useRef<any>(null);
82
+ const [items, setItems] = useState<ListItem[]>([]);
83
+ const [editingItem, setEditingItem] = useState<number | null>(null);
84
+
85
+ const dragHandle = useRef<HTMLSpanElement | null>(null);
86
+
87
+
88
+ // Edit
89
+ const [editValue, setEditValue] = useState<Record<string, string | number>>({});
90
+
91
+ // Collapse/Expand
92
+ const [collapsedItems, setCollapsedItems] = useState<Set<number>>(new Set());
93
+
94
+
95
+ // Get editable field list
96
+ const getEditableFields = (item: ListItem): string[] => {
97
+ // Exclude fields that don't need to be edited
98
+ const excludeFields = ['id', 'parentId', 'depth', 'children', 'disabled', 'appendControl', 'parentItem'];
99
+ return Object.keys(item).filter(key => !excludeFields.includes(key));
100
+ };
101
+
102
+
103
+ // ================================================================
104
+ // General
105
+ // ================================================================
106
+ const deepCloneWithReactNode = (obj: any): any => {
107
+ if (obj === null || typeof obj !== 'object') {
108
+ return obj;
109
+ }
110
+
111
+ // Handle array
112
+ if (Array.isArray(obj)) {
113
+ return obj.map(item => deepCloneWithReactNode(item));
114
+ }
115
+
116
+ // Handle object
117
+ const clonedObj: any = {};
118
+ for (const key in obj) {
119
+ if (key === 'appendControl') {
120
+ clonedObj[key] = obj[key];
121
+ } else {
122
+ clonedObj[key] = deepCloneWithReactNode(obj[key]);
123
+ }
124
+ }
125
+ return clonedObj;
126
+ };
127
+
128
+
129
+ const getItemWithChildrenIndices = (items: ListItem[], startIndex: number): number[] => {
130
+ const indices = [startIndex];
131
+ const startItem = items[startIndex];
132
+ const startDepth = startItem.depth || 0;
133
+
134
+ // Check if subsequent items are child items
135
+ for (let i = startIndex + 1; i < items.length; i++) {
136
+ const currentItem = items[i];
137
+ const currentDepth = currentItem.depth || 0;
138
+ if (currentDepth > startDepth) {
139
+ indices.push(i);
140
+ } else {
141
+ break;
142
+ }
143
+ }
144
+
145
+ return indices;
146
+ };
147
+
148
+
149
+ const getIndentStr = (item: ListItem): number | string => {
150
+
151
+ // Add indent placeholder
152
+ let indent = '';
153
+ const depthData = item.depth;
154
+ if (depthData) {
155
+ Array(depthData).fill(0).forEach((k, i) => {
156
+ indent += INDENT_PLACEHOLDER;
157
+ if (i === depthData - 1) {
158
+ indent += INDENT_LAST_PLACEHOLDER;
159
+ }
160
+ });
161
+ }
162
+
163
+ return indent;
164
+ };
165
+
166
+
167
+
168
+ // ================================================================
169
+ // Collapse/Expand
170
+ // ================================================================
171
+ // Add collapse/expand handler function
172
+ const handleCollapse = (itemId: number, e: React.MouseEvent) => {
173
+ e.preventDefault();
174
+ e.stopPropagation();
175
+
176
+ setCollapsedItems(prev => {
177
+ const newCollapsed = new Set(prev);
178
+ if (newCollapsed.has(itemId)) {
179
+ newCollapsed.delete(itemId);
180
+ } else {
181
+ newCollapsed.add(itemId);
182
+ }
183
+ return newCollapsed;
184
+ });
185
+ };
186
+
187
+ // Helper function to determine if an item should be displayed
188
+ const shouldShowItem = (item: ListItem): boolean => {
189
+ if (!alternateCollapse) return true;
190
+
191
+ let currentId = item.parentId;
192
+ while (currentId) {
193
+ if (collapsedItems.has(currentId)) {
194
+ return false;
195
+ }
196
+ const parentItem = items.find(i => i.id === currentId);
197
+ currentId = parentItem?.parentId;
198
+ }
199
+ return true;
200
+ };
201
+
202
+ const hasChildren = (itemId: number): boolean => {
203
+ return items.some(item => item.parentId === itemId);
204
+ };
205
+
206
+
207
+
208
+
209
+ // ================================================================
210
+ // Drag & Drop Handlers (Desktop & Touch)
211
+ // ================================================================
212
+ const { isDragging, dragHandlers } = useBoundedDrag({
213
+ dragMode,
214
+ boundarySelector: `.${prefix}-draggable-list`,
215
+ itemSelector: `.${prefix}-draggable-list__item`,
216
+ dragHandleSelector: `.${prefix}-draggable-list__handle`,
217
+ onDragStart: (index: number) => {
218
+ // Additional drag start logic if needed
219
+ },
220
+ onDragOver: (dragIndex: number | null, dropIndex: number | null) => {
221
+ // Additional drag over logic if needed
222
+ },
223
+ onDragEnd: (dragIndex: number | null, dropIndex: number | null) => {
224
+ if (dragIndex !== null && dropIndex !== null && dragIndex !== dropIndex) {
225
+ // Handle item movement
226
+ const newItems = deepCloneWithReactNode(items);
227
+ const itemsToMove = getItemWithChildrenIndices(newItems, dragIndex);
228
+ const itemsBeingMoved = itemsToMove.map(index => newItems[index]);
229
+
230
+ // ... rest of your existing drag end logic ...
231
+
232
+ const _targetId = newItems[dragIndex]?.id;
233
+
234
+ // Calculate depth difference
235
+ const draggedDepth = newItems[dragIndex]?.depth || 0;
236
+ const dropDepth = newItems[dropIndex]?.depth || 0;
237
+ const depthDiff = dropDepth - draggedDepth;
238
+
239
+ // Adjust depth for all moving items
240
+ itemsBeingMoved.forEach(item => {
241
+ if (item.depth !== undefined) {
242
+ item.depth += depthDiff;
243
+ }
244
+ });
245
+
246
+ // Remove all items from their original location (from back to front to keep indexing correct)
247
+ itemsToMove.reverse().forEach(index => {
248
+ newItems.splice(index, 1);
249
+ });
250
+
251
+ // Calculate new insert position
252
+ let insertIndex = dropIndex;
253
+ if (dropIndex > dragIndex) {
254
+ insertIndex -= itemsToMove.length;
255
+ }
256
+
257
+ // Insert all items
258
+ newItems.splice(insertIndex, 0, ...itemsBeingMoved);
259
+
260
+ // Rebuild tree structure
261
+ const tree = hierarchical ? convertTree(newItems, '', 'id', 'parentId') : newItems;
262
+ const updatedItems = hierarchical ? addTreeDepth(tree) : tree;
263
+
264
+ setItems(updatedItems);
265
+ onUpdate?.(updatedItems, _targetId);
266
+ }
267
+ }
268
+ });
269
+
270
+
271
+
272
+ // ================================================================
273
+ // Editable
274
+ // ================================================================
275
+
276
+ const handleDoubleClick = (item: ListItem) => {
277
+ if (!editable) return;
278
+
279
+ setEditingItem(item.id);
280
+ // Only editable fields are copied
281
+ const editableFields = getEditableFields(item);
282
+ const editableValues = editableFields.reduce((acc, field) => ({
283
+ ...acc,
284
+ [field]: item[field as keyof ListItem]
285
+ }), {});
286
+
287
+ setEditValue(editableValues);
288
+ };
289
+
290
+ const handleEditCancel = () => {
291
+ setEditingItem(null);
292
+ setEditValue({});
293
+ };
294
+
295
+ const handleEditSave = (itemId: number) => {
296
+ const newItems: ListItem[] = items.map(item => {
297
+ if (item.id === itemId) {
298
+ return {
299
+ ...item,
300
+ ...editValue
301
+ };
302
+ }
303
+ return item;
304
+ });
305
+
306
+ setItems(newItems);
307
+ onUpdate?.(newItems, itemId);
308
+ setEditingItem(null);
309
+ setEditValue({});
310
+ };
311
+
312
+ const handleInputChange = (field: string, value: any) => {
313
+ setEditValue(prev => ({
314
+ ...prev,
315
+ [field]: value
316
+ }));
317
+ };
318
+
319
+
320
+ const handleKeyDown = (e: any, itemId: number) => {
321
+ if (e.key === 'Enter') {
322
+ handleEditSave(itemId);
323
+ } else if (e.key === 'Escape') {
324
+ handleEditCancel();
325
+ }
326
+ };
327
+
328
+
329
+
330
+ const renderEditForm = (item: ListItem) => {
331
+ const editableFields = getEditableFields(item);
332
+
333
+ return (
334
+ <div className={`${prefix}-draggable-list__edit-form`}>
335
+ {editableFields.map((field) => (
336
+ <div key={field} className={`${prefix}-draggable-list__edit-field`}>
337
+ <label>{field}:</label>
338
+ <input
339
+ type="text"
340
+ value={editValue[field] || ''}
341
+ onChange={(e) => handleInputChange(field, e.target.value)}
342
+ onKeyDown={(e) => handleKeyDown(e, item.id)}
343
+ placeholder={field}
344
+ autoFocus={field === editableFields[0]}
345
+ />
346
+ </div>
347
+ ))}
348
+
349
+ <div className={`${prefix}-draggable-list__edit-buttons`}>
350
+ <button onClick={() => handleEditSave(item.id)}>✓</button>
351
+ <button onClick={handleEditCancel}>✕</button>
352
+ </div>
353
+ </div>
354
+ );
355
+ };
356
+
357
+ useEffect(() => {
358
+
359
+ // data init
360
+ //--------------
361
+ if (typeof data !== 'undefined' && Array.isArray(data)) {
362
+ const tree = hierarchical ? convertTree(data, '', 'id', 'parentId') : data;
363
+ const _ORGIN_DATA = hierarchical ? addTreeDepth(tree) : tree;
364
+ setItems(_ORGIN_DATA);
365
+ }
366
+ }, [data]);
367
+
368
+
369
+
370
+ return (
371
+ <ul
372
+ {...attributes}
373
+ ref={(node) => {
374
+ rootRef.current = node;
375
+ if (typeof externalRef === 'function') {
376
+ externalRef(node);
377
+ } else if (externalRef) {
378
+ externalRef.current = node;
379
+ }
380
+ }}
381
+ className={combinedCls(
382
+ `${prefix}-draggable-list`,
383
+ clsWrite(wrapperClassName, 'mb-3'),
384
+ clsWrite(dragMode, 'handle'),
385
+ `handle-pos-${handlePos ?? 'left'}`,
386
+ {
387
+ 'draggable': draggable,
388
+ 'icon-hide': handleHide,
389
+ 'alternate-collapse': alternateCollapse
390
+ }
391
+ )}
392
+ >
393
+ {items.map((item: ListItem, index: number) => {
394
+
395
+ // If the item should be hidden, the rendering is skipped
396
+ if (!shouldShowItem(item)) return null;
397
+
398
+ // collapse
399
+ const hasChildItems = hasChildren(item.id);
400
+ const isCollapsed = collapsedItems.has(item.id);
401
+
402
+
403
+ return <li
404
+ key={item.id}
405
+ data-index={index}
406
+ data-id={item.id}
407
+ data-parent-id={item.parentId}
408
+ data-value={item.value}
409
+ data-label={item.label}
410
+ data-listitemlabel={item.listItemLabel}
411
+ className={combinedCls(
412
+ `${prefix}-draggable-list__item`,
413
+ clsWrite(dragMode, 'handle'),
414
+ {
415
+ 'disabled': item.disabled,
416
+ 'draggable': draggable,
417
+ 'editing': editingItem === item.id,
418
+
419
+ // collapse
420
+ 'has-children': hasChildItems,
421
+ 'collapsed': isCollapsed
422
+ }
423
+ )}
424
+ draggable={!draggable ? undefined : editingItem !== item.id && "true"}
425
+ onDragStart={!draggable ? undefined : (e) => dragHandlers.handleDragStart(e, index)}
426
+ onDragOver={!draggable ? undefined : dragHandlers.handleDragOver}
427
+ onDragEnd={!draggable ? undefined : dragHandlers.handleDragEnd}
428
+ onTouchStart={!draggable ? undefined : (e) => dragHandlers.handleDragStart(e, index)}
429
+ onTouchMove={!draggable ? undefined : dragHandlers.handleDragOver}
430
+ onTouchEnd={!draggable ? undefined : dragHandlers.handleDragEnd}
431
+ style={itemStyle}
432
+ onDoubleClick={() => handleDoubleClick(item)}
433
+ >
434
+ <div className={`${prefix}-draggable-list__itemcontent`}>
435
+
436
+ {/** DRAG HANDLE */}
437
+ {/* Fix the problem that mobile terminals cannot be touched, DO NOT USE "<svg>" */}
438
+ {draggable && !handleHide ? <span ref={dragHandle} className={`${prefix}-draggable-list__handle ${handlePos ?? 'left'}`} draggable={dragMode === 'handle'} dangerouslySetInnerHTML={{
439
+ __html: `${handleIcon}`
440
+ }}></span> : null}
441
+ {/** /DRAG HANDLE */}
442
+
443
+ {editingItem === item.id ? (
444
+ renderEditForm(item)
445
+ ) : (
446
+ <div className={`${prefix}-draggable-list__itemcontent-inner`}>
447
+
448
+ <div className={`${prefix}-draggable-list__itemlabel`}>
449
+
450
+
451
+ {/** LABEL */}
452
+
453
+ <span dangerouslySetInnerHTML={{
454
+ __html: `${getIndentStr(item)}${typeof item.listItemLabel === 'undefined' ? item.label : item.listItemLabel}`
455
+ }} />
456
+ {/** /LABEL */}
457
+
458
+
459
+
460
+
461
+ {/** COLLOPSE */}
462
+ {alternateCollapse && hasChildItems && (
463
+ <span
464
+ className={`${prefix}-draggable-list__collapse-arrow`}
465
+ onClick={(e) => handleCollapse(item.id, e)}
466
+ >
467
+ {arrow || (isCollapsed ? '▶' : '▼')}
468
+ </span>
469
+ )}
470
+ {/** /COLLOPSE */}
471
+
472
+ </div>
473
+
474
+
475
+ {/** EXTENDS */}
476
+ {item.appendControl ? <>
477
+ <div className={`${prefix}-draggable-list__itemext`} id={`${prefix}-draggable-list__itemext-${item.value}`}>
478
+ {item.appendControl}
479
+ </div>
480
+ </> : null}
481
+ {/** /EXTENDS */}
482
+
483
+ </div>
484
+ )}
485
+ </div>
486
+ </li>
487
+ })}
488
+ </ul>
489
+ );
490
+ });
491
+
492
+
493
+ export default DragDropList;