form-driver 0.4.21 → 0.4.22

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.
@@ -1,21 +1,41 @@
1
1
  import { HolderOutlined } from "@ant-design/icons";
2
2
  import {
3
- draggable,
4
- dropTargetForElements,
5
- monitorForElements,
6
- } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
3
+ DndContext,
4
+ closestCenter,
5
+ DragOverlay,
6
+ TouchSensor,
7
+ PointerSensor,
8
+ KeyboardSensor,
9
+ useSensor,
10
+ useSensors,
11
+ DragStartEvent,
12
+ DragEndEvent,
13
+ } from "@dnd-kit/core";
14
+ import {
15
+ SortableContext,
16
+ useSortable,
17
+ arrayMove,
18
+ verticalListSortingStrategy,
19
+ horizontalListSortingStrategy,
20
+ sortableKeyboardCoordinates,
21
+ } from "@dnd-kit/sortable";
22
+ import { CSS } from "@dnd-kit/utilities";
7
23
  import clsx from "clsx";
8
24
  import React, {
9
25
  memo,
10
26
  ReactNode,
11
27
  useCallback,
12
- useEffect,
13
- useRef,
14
28
  useState,
29
+ useMemo,
15
30
  } from "react";
16
31
 
32
+ import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities";
33
+
17
34
  import "./EnhancedSortDrag.less";
18
35
 
36
+ // 使用 @dnd-kit 原生的 listeners 类型
37
+ type DndListeners = SyntheticListenerMap | undefined;
38
+
19
39
  /**
20
40
  * 拖拽项数据类型定义
21
41
  */
@@ -44,7 +64,7 @@ export interface EnhancedSortDragProps {
44
64
  /** 拖拽失败回调 */
45
65
  onDragFail?: (error: DragError) => void;
46
66
  /** 自定义拖拽句柄渲染 */
47
- renderDragHandle?: (item: DragItem, isDragging: boolean) => ReactNode;
67
+ renderDragHandle?: (item: DragItem, isDragging: boolean, listeners?: DndListeners) => ReactNode;
48
68
  /** 拖拽预览内容 */
49
69
  renderDragPreview?: (item: DragItem) => ReactNode;
50
70
  /** 是否为表格行模式 */
@@ -79,6 +99,119 @@ interface ValidationResult {
79
99
  error?: DragError;
80
100
  }
81
101
 
102
+ /**
103
+ * 可排序的拖拽项子组件
104
+ */
105
+ const SortableEnhancedItem: React.FC<{
106
+ item: DragItem;
107
+ index: number;
108
+ enableAnimation: boolean;
109
+ direction: "vertical" | "horizontal";
110
+ renderDragHandle?: (item: DragItem, isDragging: boolean, listeners?: DndListeners) => ReactNode;
111
+ isTableRow: boolean;
112
+ }> = memo(({ item, index, enableAnimation, direction, renderDragHandle, isTableRow }) => {
113
+ const {
114
+ attributes,
115
+ listeners,
116
+ setNodeRef,
117
+ transform,
118
+ transition,
119
+ isDragging,
120
+ } = useSortable({
121
+ id: item.id,
122
+ disabled: !item.isChecked,
123
+ });
124
+
125
+ const style = {
126
+ transform: CSS.Transform.toString(transform),
127
+ transition: enableAnimation ? transition : undefined,
128
+ };
129
+
130
+ const renderDefaultDragHandle = () => (
131
+ <div className="enhanced-dragHandleWrapper">
132
+ <HolderOutlined
133
+ className="enhanced-dragHandleIcon"
134
+ style={{
135
+ opacity: isDragging ? 0.5 : 1,
136
+ transition: enableAnimation ? "opacity 0.2s" : "none",
137
+ }}
138
+ />
139
+ </div>
140
+ );
141
+
142
+ // 表格行模式
143
+ if (isTableRow) {
144
+ return (
145
+ <tr
146
+ ref={setNodeRef}
147
+ key={item.id}
148
+ data-drag-id={item.id}
149
+ data-drag-index={index}
150
+ style={{
151
+ opacity: isDragging ? 0.5 : 1,
152
+ ...style,
153
+ }}
154
+ {...attributes}
155
+ >
156
+ {renderDragHandle ? (
157
+ renderDragHandle(item, isDragging, listeners)
158
+ ) : (
159
+ <td
160
+ {...listeners}
161
+ style={{
162
+ cursor: isDragging ? "grabbing" : "grab",
163
+ textAlign: "center",
164
+ width: "40px",
165
+ }}
166
+ >
167
+ <HolderOutlined
168
+ style={{
169
+ opacity: isDragging ? 0.5 : 1,
170
+ transition: enableAnimation ? "opacity 0.2s" : "none",
171
+ }}
172
+ />
173
+ </td>
174
+ )}
175
+ {item.cpn}
176
+ </tr>
177
+ );
178
+ }
179
+
180
+ // 普通模式
181
+ const itemClass = clsx("enhanced-dragItem", {
182
+ dragging: isDragging,
183
+ disabled: !item.isChecked,
184
+ [`direction-${direction}`]: true,
185
+ });
186
+
187
+ return (
188
+ <div
189
+ className={itemClass}
190
+ ref={setNodeRef}
191
+ key={item.id}
192
+ data-drag-id={item.id}
193
+ data-drag-index={index}
194
+ style={style}
195
+ {...attributes}
196
+ >
197
+ <div className="enhanced-dragBody">{item.cpn}</div>
198
+ <div className="enhanced-dragHandle">
199
+ {item.isChecked && (
200
+ renderDragHandle
201
+ ? renderDragHandle(item, isDragging, listeners)
202
+ : (
203
+ <div {...listeners}>
204
+ {renderDefaultDragHandle()}
205
+ </div>
206
+ )
207
+ )}
208
+ </div>
209
+ </div>
210
+ );
211
+ });
212
+
213
+ SortableEnhancedItem.displayName = "SortableEnhancedItem";
214
+
82
215
  /**
83
216
  * 增强版通用拖拽排序组件
84
217
  * 解决原组件的性能问题和功能局限性
@@ -96,23 +229,33 @@ const EnhancedSortDrag: React.FC<EnhancedSortDragProps> = memo((props) => {
96
229
  isTableRow = false,
97
230
  } = props;
98
231
 
99
- // 使用 useRef 缓存 items 避免不必要的重渲染
100
- const itemsRef = useRef<DragItem[]>(items);
101
- itemsRef.current = items;
102
-
103
232
  // 拖拽状态管理
233
+ const [activeId, setActiveId] = useState<string | null>(null);
104
234
  const [dragState, setDragState] = useState<DragState>({
105
235
  draggingId: null,
106
236
  droppingId: null,
107
237
  isAnimating: false,
108
238
  });
109
239
 
110
- // 使用 Map 替代对象,提高查找性能
111
- const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
112
- const dragHandleRefs = useRef<Map<string, HTMLSpanElement>>(new Map());
113
-
114
- // 清理函数缓存,避免重复注册
115
- const cleanupRefs = useRef<Map<string, () => void>>(new Map());
240
+ /**
241
+ * 配置传感器
242
+ */
243
+ const sensors = useSensors(
244
+ useSensor(TouchSensor, {
245
+ activationConstraint: {
246
+ delay: 200,
247
+ tolerance: 5,
248
+ },
249
+ }),
250
+ useSensor(PointerSensor, {
251
+ activationConstraint: {
252
+ distance: 10,
253
+ },
254
+ }),
255
+ useSensor(KeyboardSensor, {
256
+ coordinateGetter: sortableKeyboardCoordinates,
257
+ })
258
+ );
116
259
 
117
260
  /**
118
261
  * 验证拖拽操作是否有效
@@ -194,304 +337,171 @@ const EnhancedSortDrag: React.FC<EnhancedSortDragProps> = memo((props) => {
194
337
  );
195
338
 
196
339
  /**
197
- * 交换数据逻辑 - 核心拖拽算法
340
+ * 拖拽开始事件处理
198
341
  */
199
- const swapItems = useCallback(
200
- (fromId: string, toId: string) => {
201
- try {
202
- const currentItems = [...itemsRef.current];
342
+ const handleDragStart = useCallback((event: DragStartEvent) => {
343
+ const { active } = event;
344
+ setActiveId(active.id as string);
345
+ setDragState((prev) => ({
346
+ ...prev,
347
+ draggingId: active.id as string,
348
+ isAnimating: !!enableAnimation,
349
+ }));
350
+ }, [enableAnimation]);
203
351
 
204
- // 验证拖拽操作
205
- const validation = validateDragOperation(fromId, toId, currentItems);
352
+ /**
353
+ * 拖拽结束事件处理
354
+ */
355
+ const handleDragEnd = useCallback((event: DragEndEvent) => {
356
+ const { active, over } = event;
357
+ setActiveId(null);
358
+ setDragState((prev) => ({
359
+ ...prev,
360
+ draggingId: null,
361
+ droppingId: null,
362
+ isAnimating: false,
363
+ }));
364
+
365
+ if (over && active.id !== over.id) {
366
+ const oldIndex = items.findIndex((item) => item.id === active.id);
367
+ const newIndex = items.findIndex((item) => item.id === over.id);
368
+
369
+ if (oldIndex === -1 || newIndex === -1) {
370
+ return;
371
+ }
372
+
373
+ try {
374
+ const validation = validateDragOperation(
375
+ active.id as string,
376
+ over.id as string,
377
+ items
378
+ );
206
379
  if (!validation.isValid) {
207
380
  throw new Error(validation.error?.message || "拖拽验证失败");
208
381
  }
209
382
 
210
- const fromIndex = currentItems.findIndex((item) => item.id === fromId);
211
- const toIndex = currentItems.findIndex((item) => item.id === toId);
212
-
213
- // 边界条件检查
214
- if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) {
215
- return;
216
- }
217
-
218
- const [moved] = currentItems.splice(fromIndex, 1);
219
- currentItems.splice(toIndex, 0, moved);
220
-
221
- // 更新引用
222
- itemsRef.current = currentItems;
223
-
224
- // 触发回调
225
- onChange(currentItems);
383
+ const newItems = arrayMove(items, oldIndex, newIndex);
384
+ onChange(newItems);
226
385
  } catch (error) {
227
386
  console.error("拖拽交换数据失败:", error);
228
-
229
- // 触发错误回调
230
387
  const dragError: DragError = {
231
388
  type: "DATA_ERROR",
232
389
  message: error instanceof Error ? error.message : "未知错误",
233
- sourceId: fromId,
234
- targetId: toId,
390
+ sourceId: active.id as string,
391
+ targetId: over.id as string,
235
392
  };
236
-
237
393
  onDragFail?.(dragError);
238
394
  }
239
- },
240
- [onChange, onDragFail, validateDragOperation]
241
- );
395
+ }
396
+ }, [items, onChange, onDragFail, validateDragOperation]);
242
397
 
243
398
  /**
244
- * 注册单个项目的拖拽监听器
399
+ * 获取当前拖拽项
245
400
  */
246
- const registerDragListeners = useCallback(
247
- (item: DragItem, index: number) => {
248
- const el = itemRefs.current.get(item.id);
249
- const dragHandle = dragHandleRefs.current.get(item.id);
250
-
251
- if (!el) return;
252
-
253
- // 清理之前的监听器
254
- const existingCleanup = cleanupRefs.current.get(item.id);
255
- if (existingCleanup) {
256
- existingCleanup();
257
- }
258
-
259
- const cleanups: (() => void)[] = [];
260
-
261
- // 注册为可拖拽元素
262
- cleanups.push(
263
- draggable({
264
- element: el,
265
- dragHandle: dragHandle,
266
- canDrag: () => item.isChecked,
267
- getInitialData: () => ({
268
- id: item.id,
269
- index,
270
- label: item.label,
271
- }),
272
- onDragStart: () => {
273
- setDragState((prev) => ({
274
- ...prev,
275
- draggingId: item.id,
276
- isAnimating: !!enableAnimation,
277
- }));
278
- console.log("开始拖拽:", item.label);
279
- },
280
- onDrop: () => {
281
- setDragState((prev) => ({
282
- ...prev,
283
- draggingId: null,
284
- droppingId: null,
285
- isAnimating: false,
286
- }));
287
- },
288
- })
289
- );
290
-
291
- // 注册为拖拽目标
292
- cleanups.push(
293
- dropTargetForElements({
294
- element: el,
295
- canDrop: () => item.isChecked,
296
- onDragEnter: (args) => {
297
- const sourceId = args.source.data.id as string;
298
- if (sourceId === item.id) return;
299
-
300
- console.log("拖拽进入:", item.id);
301
- setDragState((prev) => ({
302
- ...prev,
303
- droppingId: item.id,
304
- }));
305
- },
306
- onDragLeave: () => {
307
- setDragState((prev) => ({
308
- ...prev,
309
- droppingId: null,
310
- }));
311
- },
312
- onDrop: ({ source }) => {
313
- if (!source) return;
314
- const sourceId = source.data.id as string;
315
- if (sourceId === item.id) return;
316
-
317
- // 执行数据交换
318
- swapItems(sourceId, item.id);
319
- },
320
- })
321
- );
322
-
323
- // 存储清理函数
324
- const combinedCleanup = () => {
325
- cleanups.forEach((fn) => fn());
326
- };
327
- cleanupRefs.current.set(item.id, combinedCleanup);
328
- },
329
- [swapItems, enableAnimation]
401
+ const activeItem = useMemo(
402
+ () => items.find((item) => item.id === activeId),
403
+ [items, activeId]
330
404
  );
331
405
 
332
406
  /**
333
- * 性能优化:只在必要的时候注册监听器
407
+ * 选择排序策略
334
408
  */
335
- useEffect(() => {
336
- // 清理所有旧的监听器
337
- cleanupRefs.current.forEach((cleanup) => cleanup());
338
- cleanupRefs.current.clear();
339
-
340
- // 注册所有项目的监听器
341
- items.forEach((item, index) => {
342
- registerDragListeners(item, index);
343
- });
344
-
345
- // 监听全局拖拽结束
346
- const monitorCleanup = monitorForElements({
347
- onDrop: () => {
348
- setDragState((prev) => ({
349
- ...prev,
350
- draggingId: null,
351
- droppingId: null,
352
- isAnimating: false,
353
- }));
354
- },
355
- });
356
- cleanupRefs.current.set("monitor", monitorCleanup);
357
-
358
- return () => {
359
- cleanupRefs.current.forEach((cleanup) => cleanup());
360
- cleanupRefs.current.clear();
361
- };
362
- }, [items.length, registerDragListeners]); // 只在列表长度变化时重新注册
363
-
364
- /**
365
- * 渲染默认拖拽句柄
366
- */
367
- const renderDefaultDragHandle = useCallback(
368
- (item: DragItem, isDragging: boolean) => (
369
- <HolderOutlined
370
- ref={(el) => {
371
- if (el) {
372
- dragHandleRefs.current.set(item.id, el);
373
- }
374
- }}
375
- style={{
376
- cursor: isDragging ? "grabbing" : "grab",
377
- marginLeft: "8px",
378
- opacity: isDragging ? 0.5 : 1,
379
- transition: enableAnimation ? "opacity 0.2s" : "none",
380
- }}
381
- />
382
- ),
383
- [enableAnimation]
409
+ const sortingStrategy = useMemo(
410
+ () =>
411
+ direction === "vertical"
412
+ ? verticalListSortingStrategy
413
+ : horizontalListSortingStrategy,
414
+ [direction]
384
415
  );
385
416
 
386
417
  /**
387
- * 渲染拖拽项
418
+ * 缓存 item id 列表,避免每次渲染创建新数组引用
388
419
  */
389
- const renderDragItem = useCallback(
390
- (item: DragItem, index: number) => {
391
- const isDragging = dragState.draggingId === item.id;
392
- const isDropping = dragState.droppingId === item.id;
393
-
394
- // 如果是表格行模式,直接返回组件内容
395
- if (isTableRow) {
396
- // console.log("表格行数据", item);
397
- return (
398
- <tr
399
- ref={(el) => {
400
- if (el) {
401
- itemRefs.current.set(item.id, el as unknown as HTMLDivElement);
402
- }
403
- }}
404
- key={item.id}
405
- data-drag-id={item.id}
406
- data-drag-index={index}
407
- style={{
408
- transition: enableAnimation ? "all 0.3s ease" : "none",
409
- opacity: isDragging ? 0.5 : 1,
410
- backgroundColor: isDropping ? "#e6f7ff" : "transparent",
411
- }}
412
- >
413
- {/* 如果提供了自定义拖拽句柄,则将其作为第一列 */}
414
- {renderDragHandle && renderDragHandle(item, isDragging)}
415
- {!renderDragHandle && (
416
- <td
417
- ref={(el) => {
418
- if (el) {
419
- dragHandleRefs.current.set(
420
- item.id,
421
- el as unknown as HTMLSpanElement
422
- );
423
- }
424
- }}
425
- style={{
426
- cursor: isDragging ? "grabbing" : "grab",
427
- textAlign: "center",
428
- width: "40px",
429
- }}
430
- >
431
- <HolderOutlined
432
- style={{
433
- opacity: isDragging ? 0.5 : 1,
434
- transition: enableAnimation ? "opacity 0.2s" : "none",
435
- }}
436
- />
437
- </td>
438
- )}
439
- {item.cpn}
440
- </tr>
441
- );
442
- }
443
-
444
- const itemClass = clsx("enhanced-dragItem", {
445
- dragging: isDragging,
446
- dropping: isDropping,
447
- disabled: !item.isChecked,
448
- [`direction-${direction}`]: true,
449
- });
450
-
451
- return (
452
- <div
453
- className={itemClass}
454
- ref={(el) => {
455
- if (el) {
456
- itemRefs.current.set(item.id, el);
457
- }
458
- }}
459
- key={item.id}
460
- data-drag-id={item.id}
461
- data-drag-index={index}
462
- style={{
463
- transition: enableAnimation ? "all 0.3s ease" : "none",
464
- }}
465
- >
466
- <div className="enhanced-dragBody">{item.cpn}</div>
467
- <div className="enhanced-dragHandle">
468
- {item.isChecked &&
469
- (renderDragHandle
470
- ? renderDragHandle(item, isDragging)
471
- : renderDefaultDragHandle(item, isDragging))}
472
- </div>
473
- </div>
474
- );
475
- },
476
- [
477
- dragState,
478
- direction,
479
- enableAnimation,
480
- renderDragHandle,
481
- renderDefaultDragHandle,
482
- isTableRow,
483
- ]
484
- );
420
+ const itemIds = useMemo(() => items.map((item) => item.id), [items]);
485
421
 
486
422
  // 如果是表格行模式,直接渲染子元素而不是包装div
487
423
  if (isTableRow) {
488
- return <>{items.map(renderDragItem)}</>;
424
+ return (
425
+ <DndContext
426
+ sensors={sensors}
427
+ collisionDetection={closestCenter}
428
+ onDragStart={handleDragStart}
429
+ onDragEnd={handleDragEnd}
430
+ >
431
+ <SortableContext
432
+ items={itemIds}
433
+ strategy={sortingStrategy}
434
+ >
435
+ {items.map((item, index) => (
436
+ <SortableEnhancedItem
437
+ key={item.id}
438
+ item={item}
439
+ index={index}
440
+ enableAnimation={enableAnimation}
441
+ direction={direction}
442
+ renderDragHandle={renderDragHandle}
443
+ isTableRow={isTableRow}
444
+ />
445
+ ))}
446
+ </SortableContext>
447
+ <DragOverlay dropAnimation={null}>
448
+ {activeItem ? (
449
+ <div className="enhanced-sortDrag-overlay">
450
+ {renderDragPreview
451
+ ? renderDragPreview(activeItem)
452
+ : (
453
+ <div className="enhanced-sortDrag-overlay-row">
454
+ <span className="enhanced-sortDrag-overlay-label">
455
+ {activeItem.label || activeItem.id || '拖拽中...'}
456
+ </span>
457
+ </div>
458
+ )}
459
+ </div>
460
+ ) : null}
461
+ </DragOverlay>
462
+ </DndContext>
463
+ );
489
464
  }
490
465
 
491
466
  return (
492
- <div className={`enhanced-sortDrag direction-${direction}`}>
493
- {items.map(renderDragItem)}
494
- </div>
467
+ <DndContext
468
+ sensors={sensors}
469
+ collisionDetection={closestCenter}
470
+ onDragStart={handleDragStart}
471
+ onDragEnd={handleDragEnd}
472
+ >
473
+ <div className={`enhanced-sortDrag direction-${direction}`}>
474
+ <SortableContext
475
+ items={itemIds}
476
+ strategy={sortingStrategy}
477
+ >
478
+ {items.map((item, index) => (
479
+ <SortableEnhancedItem
480
+ key={item.id}
481
+ item={item}
482
+ index={index}
483
+ enableAnimation={enableAnimation}
484
+ direction={direction}
485
+ renderDragHandle={renderDragHandle}
486
+ isTableRow={isTableRow}
487
+ />
488
+ ))}
489
+ </SortableContext>
490
+ </div>
491
+ <DragOverlay dropAnimation={null}>
492
+ {activeItem ? (
493
+ <div className="enhanced-sortDrag-overlay">
494
+ {renderDragPreview
495
+ ? renderDragPreview(activeItem)
496
+ : (
497
+ <div className="enhanced-sortDrag-overlay-content">
498
+ <div className="enhanced-sortDrag-overlay-body">{activeItem.cpn}</div>
499
+ </div>
500
+ )}
501
+ </div>
502
+ ) : null}
503
+ </DragOverlay>
504
+ </DndContext>
495
505
  );
496
506
  });
497
507