File size: 3,927 Bytes
793e953
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/**
 * useDragSort Hook
 *
 * 列表项拖拽排序,使用 HTML5 Drag and Drop API。
 * 触摸设备通过 emulating touch events 支持拖拽(简单模拟,不支持复杂的多点触控排序)。
 *
 * 用法:
 *   const { draggedIndex, dropTargetIndex, dragHandlers } = useDragSort({
 *     items,
 *     onReorder: (fromIndex, toIndex) => reorder(fromIndex, toIndex),
 *   })
 *
 *   return items.map((item, index) => (
 *     <div key={item.id} {...dragHandlers(index)}>
 *       {item.name}
 *     </div>
 *   ))
 */

import { useState, useCallback, useRef } from 'react'

// ================================================================
// 类型定义
// ================================================================

interface UseDragSortOptions {
  /** 重排序回调 */
  onReorder: (fromIndex: number, toIndex: number) => void
  /** 拖拽句柄的类名(可选),如果设置则仅在该元素上触发拖拽 */
  dragHandleClass?: string
  /** 是否启用,默认 true */
  enabled?: boolean
}

interface DragHandlers {
  /** 拖拽元素的 ref */
  draggable: boolean
  onDragStart: (e: React.DragEvent) => void
  onDragOver: (e: React.DragEvent) => void
  onDragEnter: (e: React.DragEvent) => void
  onDragEnd: (e: React.DragEvent) => void
  /** 元素在当前列表中的样式(拖拽中时显示占位效果) */
  style: React.CSSProperties
}

interface UseDragSortReturn {
  /** 当前被拖拽项的索引 */
  draggedIndex: number | null
  /** 拖拽目标位置的索引 */
  dropTargetIndex: number | null
  /** 获取指定索引项的拖拽事件处理器 */
  dragHandlers: (index: number) => DragHandlers
}

// ================================================================
// Hook
// ================================================================

export function useDragSort(options: UseDragSortOptions): UseDragSortReturn {
  const { onReorder, enabled = true } = options

  const [draggedIndex, setDraggedIndex] = useState<number | null>(null)
  const [dropTargetIndex, setDropTargetIndex] = useState<number | null>(null)
  const onReorderRef = useRef(onReorder)
  onReorderRef.current = onReorder

  const handleDragStart = useCallback(
    (index: number) => (_e: React.DragEvent) => {
      if (!enabled) return
      setDraggedIndex(index)
      setDropTargetIndex(null)
    },
    [enabled]
  )

  const handleDragOver = useCallback(
    (_index: number) => (e: React.DragEvent) => {
      if (!enabled) return
      e.preventDefault()
      e.dataTransfer.dropEffect = 'move'
    },
    [enabled]
  )

  const handleDragEnter = useCallback(
    (index: number) => (_e: React.DragEvent) => {
      if (!enabled) return
      setDropTargetIndex(index)
    },
    [enabled]
  )

  const handleDragEnd = useCallback(() => {
    if (!enabled) return
    if (draggedIndex !== null && dropTargetIndex !== null && draggedIndex !== dropTargetIndex) {
      onReorderRef.current(draggedIndex, dropTargetIndex)
    }
    setDraggedIndex(null)
    setDropTargetIndex(null)
  }, [enabled, draggedIndex, dropTargetIndex])

  const dragHandlers = useCallback(
    (index: number): DragHandlers => {
      const isDragging = draggedIndex === index
      const isOver = dropTargetIndex === index && draggedIndex !== null && draggedIndex !== index

      return {
        draggable: enabled,
        onDragStart: handleDragStart(index),
        onDragOver: handleDragOver(index),
        onDragEnter: handleDragEnter(index),
        onDragEnd: handleDragEnd,
        style: {
          opacity: isDragging ? 0.4 : 1,
          ...(isOver ? { borderTop: '2px solid #3b82f6' } : {}),
          cursor: enabled ? 'grab' : 'default',
          transition: 'opacity 0.15s ease',
        },
      }
    },
    [enabled, draggedIndex, dropTargetIndex, handleDragStart, handleDragOver, handleDragEnter, handleDragEnd]
  )

  return { draggedIndex, dropTargetIndex, dragHandlers }
}