deepsea-components 5.16.4 → 5.16.6

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 (51) hide show
  1. package/dist/components/AutoFit.cjs +20 -15
  2. package/dist/components/AutoFit.js +20 -15
  3. package/dist/components/AutoScroll.cjs +12 -9
  4. package/dist/components/AutoScroll.js +12 -9
  5. package/dist/components/AutoSizeTextarea.cjs +81 -23
  6. package/dist/components/AutoSizeTextarea.js +81 -23
  7. package/dist/components/CircleText.cjs +120 -24
  8. package/dist/components/CircleText.js +120 -24
  9. package/dist/components/CopyButton.cjs +8 -2
  10. package/dist/components/CopyButton.js +8 -2
  11. package/dist/components/DraggableGrid.cjs +155 -104
  12. package/dist/components/DraggableGrid.d.ts +25 -23
  13. package/dist/components/DraggableGrid.js +155 -104
  14. package/dist/components/Echart.cjs +95 -21
  15. package/dist/components/Echart.js +95 -21
  16. package/dist/components/Flow.cjs +10 -10
  17. package/dist/components/Flow.js +10 -10
  18. package/dist/components/FormLabel.cjs +92 -27
  19. package/dist/components/FormLabel.js +92 -27
  20. package/dist/components/HlsPlayer.cjs +65 -24
  21. package/dist/components/HlsPlayer.js +65 -24
  22. package/dist/components/IconFileType.cjs +14 -4
  23. package/dist/components/IconFileType.js +14 -4
  24. package/dist/components/InfiniteScroll.cjs +17 -10
  25. package/dist/components/InfiniteScroll.js +17 -10
  26. package/dist/components/InputFile.cjs +5 -5
  27. package/dist/components/InputFile.js +5 -5
  28. package/dist/components/InputFileButton.cjs +14 -13
  29. package/dist/components/InputFileButton.js +14 -13
  30. package/dist/components/ReadSheet.cjs +32 -7
  31. package/dist/components/ReadSheet.js +32 -7
  32. package/dist/components/Ring.cjs +48 -10
  33. package/dist/components/Ring.js +48 -10
  34. package/dist/components/Scroll.cjs +10 -3
  35. package/dist/components/Scroll.js +10 -3
  36. package/dist/components/ScrollMask.cjs +97 -11
  37. package/dist/components/ScrollMask.js +97 -11
  38. package/dist/components/SectionRing.cjs +60 -15
  39. package/dist/components/SectionRing.js +60 -15
  40. package/dist/components/TransitionBox.cjs +120 -30
  41. package/dist/components/TransitionBox.js +120 -30
  42. package/dist/components/TransitionNum.cjs +8 -8
  43. package/dist/components/TransitionNum.js +8 -8
  44. package/dist/components/Trapezium.cjs +68 -9
  45. package/dist/components/Trapezium.js +68 -9
  46. package/dist/components/Unify.cjs +79 -19
  47. package/dist/components/Unify.js +79 -19
  48. package/dist/components/WriteSheet.cjs +1 -1
  49. package/dist/components/WriteSheet.js +1 -1
  50. package/package.json +3 -3
  51. package/src/components/DraggableGrid.tsx +510 -506
@@ -1,506 +1,510 @@
1
- import { CSSProperties, ComponentProps, Key, ReactNode, useEffect, useMemo, useRef, useState } from "react"
2
- import { clsx, getArray, isNullable } from "deepsea-tools"
3
- import { DragMoveEvent, DragMoveEvents, useDragMove } from "soda-hooks"
4
-
5
- import styles from "./DraggableGrid.module.css"
6
-
7
- export type MustBeReactNode<T> = false extends (T extends ReactNode ? true : false) ? false : true
8
-
9
- export type MustBeReactKey<T> = false extends (T extends Key ? true : false) ? false : true
10
-
11
- export type DraggableGridClassName = string | ((status: ContainerStatus) => string)
12
-
13
- export type DraggableGridItemClassName = string | ((status: DraggableGridItemStatus) => string)
14
-
15
- export type DraggableGridStyle = CSSProperties | ((status: ContainerStatus) => CSSProperties)
16
-
17
- export interface DraggableGridClassNames {
18
- /** 容器 */
19
- container?: DraggableGridClassName
20
- /** 元素 */
21
- item?: DraggableGridItemClassName
22
- }
23
-
24
- export interface ContainerStatus {
25
- /** 是否被拖拽 */
26
- isDragging: boolean
27
- }
28
-
29
- export interface DraggableGridItemStatus {
30
- /** 当前次序 */
31
- order: number
32
- /** 是否被拖拽 */
33
- isDragging: boolean
34
- }
35
-
36
- /** 元素的 key 到次序的映射 */
37
- export type DraggableGridKeyToOrder<K extends Key> = Map<K, number>
38
-
39
- /** 次序到元素的 key 的映射 */
40
- export type DraggableGridOrderToKey<K extends Key> = Map<number, K>
41
-
42
- function isTheSameArray<T>(a: T[], b: T[]) {
43
- if (a.length !== b.length) return false
44
- for (let i = 0; i < a.length; i++) {
45
- if (a[i] !== b[i]) return false
46
- }
47
- return true
48
- }
49
-
50
- function isTheSameIterable<T>(a: Iterable<T>, b: Iterable<T>) {
51
- const aSet = a instanceof Set ? a : new Set(a)
52
- const bSet = b instanceof Set ? b : new Set(b)
53
- if (aSet.size !== bSet.size) return false
54
- return aSet.difference(bSet).size === 0
55
- }
56
-
57
- interface GetOrderMapParams<K extends Key> {
58
- prev?: DraggableGridKeyToOrder<K>
59
- orders: number[]
60
- keys: K[]
61
- }
62
-
63
- function getOrderMap<K extends Key>({ prev, orders, keys }: GetOrderMapParams<K>) {
64
- const orderSet = new Set(orders)
65
- const orderMap = new Map<K, number>()
66
- const newKeys = keys.filter(key => {
67
- if (!prev) return true
68
- if (prev.has(key)) {
69
- const prevOrder = prev.get(key)!
70
- orderMap.set(key, prevOrder)
71
- orderSet.delete(prevOrder)
72
- return false
73
- }
74
- return true
75
- })
76
- orders = Array.from(orderSet)
77
- newKeys.forEach((key, index) => orderMap.set(key, orders[index]))
78
- return orderMap
79
- }
80
-
81
- interface GetPositionParams {
82
- order: number
83
- columns: number
84
- gapX: number
85
- gapY: number
86
- itemWidth: number
87
- itemHeight: number
88
- }
89
-
90
- interface Position {
91
- x: number
92
- y: number
93
- }
94
-
95
- function getPosition({ order, columns, gapX, gapY, itemWidth, itemHeight }: GetPositionParams): Position {
96
- const row = Math.floor(order / columns)
97
- const column = order % columns
98
- return {
99
- x: column * (itemWidth + gapX),
100
- y: row * (itemHeight + gapY),
101
- }
102
- }
103
-
104
- function getOrderToKey<K extends Key>(keyToOrder: DraggableGridKeyToOrder<K>) {
105
- return new Map(Array.from(keyToOrder.entries()).map(([key, order]) => [order, key]))
106
- }
107
-
108
- export type DraggableGridProps<T, K extends Key = T extends Key ? T : never> = Omit<ComponentProps<"div">, "className" | "children"> &
109
- DragMoveEvents<HTMLDivElement> & {
110
- /** 类名 */
111
- className?: DraggableGridClassName
112
- /** 类名 */
113
- classNames?: DraggableGridClassNames
114
- /** 样式 */
115
- style?: DraggableGridStyle
116
- /** 数据源 */
117
- items?: T[]
118
- /** 是否禁用拖拽 */
119
- disabled?: boolean
120
- /** 列数 */
121
- columns: number
122
- /** 行数 */
123
- rows: number
124
- /**
125
- * 间距
126
- * @default 0
127
- */
128
- gap?: number
129
- /**
130
- * 水平间距
131
- * @default 0
132
- */
133
- gapX?: number
134
- /**
135
- * 垂直间距
136
- * @default 0
137
- */
138
- gapY?: number
139
- /** 元素宽度 */
140
- itemWidth: number
141
- /** 元素高度 */
142
- itemHeight: number
143
- /** 元素的 key 到次序的映射 */
144
- orderMap?: DraggableGridKeyToOrder<K>
145
- /** 次序变化时回调 */
146
- onOrderMapChange?: (orderMap: DraggableGridKeyToOrder<K>) => void
147
- /** 禁用的元素 */
148
- isItemDisabled?: K[] | ((item: T, key: K) => boolean)
149
- /** 禁用的次序 */
150
- isOrderDisabled?: number[] | ((order: number) => boolean)
151
- /** 次序的优先级,可以通过此函数调整元素的优先放置的方向,左上,右上,左下,右下,中心,顺时针,逆时针等等 */
152
- orderPriority?: (a: number, b: number) => number
153
- /** 触发移动的元素 */
154
- handle?: string | HTMLElement | ((item: T, key: K, element: HTMLDivElement) => HTMLElement | undefined | null) | undefined | null
155
- } & (MustBeReactNode<T> extends true
156
- ? {
157
- /** 渲染函数,当 T 为 ReactNode 时,render 为可选 */
158
- render?: (item: T, status: DraggableGridItemStatus) => ReactNode
159
- }
160
- : {
161
- /** 渲染函数,当 T 为 ReactNode 时,render 为可选 */
162
- render: (item: T, status: DraggableGridItemStatus) => ReactNode
163
- }) &
164
- (MustBeReactKey<T> extends true
165
- ? {
166
- /** 获取 key 的函数,当 T 为 Key 时,keyExtractor 为可选 */
167
- keyExtractor?: (item: T) => K
168
- }
169
- : {
170
- /** 获取 key 的函数,当 T 为 Key 时,keyExtractor 为可选 */
171
- keyExtractor: (item: T) => K
172
- })
173
-
174
- interface DraggableGridItemProps<T, K extends Key = T extends Key ? T : never> extends ComponentProps<"div">, DragMoveEvents<HTMLDivElement> {
175
- item: T
176
- itemKey: K
177
- /** 触发移动的元素 */
178
- handle?: string | HTMLElement | ((item: T, key: K, element: HTMLDivElement) => HTMLElement | undefined | null) | undefined | null
179
- }
180
-
181
- function DraggableGridItem<T, K extends Key = T extends Key ? T : never>({
182
- item,
183
- itemKey,
184
- handle,
185
- onDragMoveStart,
186
- onDragMove,
187
- onDragMoveEnd,
188
- ...rest
189
- }: DraggableGridItemProps<T, K>) {
190
- const element = useRef<HTMLDivElement>(null)
191
-
192
- useDragMove({
193
- element: isNullable(handle)
194
- ? element
195
- : () =>
196
- element.current &&
197
- (typeof handle === "string"
198
- ? (element.current.querySelector(handle) as HTMLElement)
199
- : typeof handle === "function"
200
- ? handle(item, itemKey, element.current)
201
- : handle),
202
- onDragMoveStart,
203
- onDragMove,
204
- onDragMoveEnd,
205
- })
206
-
207
- return <div ref={element} {...rest} />
208
- }
209
-
210
- interface DraggingItem<K extends Key> {
211
- key: K
212
- startX: number
213
- startY: number
214
- deltaX: number
215
- deltaY: number
216
- orders: number[]
217
- columns: number
218
- rows: number
219
- gapX: number
220
- gapY: number
221
- itemWidth: number
222
- itemHeight: number
223
- keyToOrder: DraggableGridKeyToOrder<K>
224
- }
225
-
226
- export function DraggableGrid<T, K extends Key = T extends Key ? T : never>({
227
- className,
228
- classNames,
229
- style,
230
- items = [],
231
- disabled,
232
- columns,
233
- rows,
234
- gap = 0,
235
- gapX = gap,
236
- gapY = gap,
237
- itemWidth,
238
- itemHeight,
239
- orderMap,
240
- onOrderMapChange,
241
- render,
242
- keyExtractor,
243
- isItemDisabled: _isItemDisabled,
244
- isOrderDisabled,
245
- orderPriority,
246
- handle,
247
- onDragMoveStart: _onDragMoveStart,
248
- onDragMove: _onDragMove,
249
- onDragMoveEnd: _onDragMoveEnd,
250
- ...rest
251
- }: DraggableGridProps<T, K>) {
252
- const keyToItem = useMemo(() => {
253
- const keyToItem = new Map<K, T>()
254
- items.forEach(item => {
255
- const key = keyExtractor ? keyExtractor(item) : (item as unknown as K)
256
- keyToItem.set(key, item)
257
- })
258
- return keyToItem
259
- }, [items, keyExtractor])
260
-
261
- /** 检查 items 中是否有重复的 key */
262
- if (keyToItem.size !== items.length) throw new Error("there are duplicate keys in the items")
263
-
264
- /** 可用的次序 */
265
- const orders = useMemo(() => {
266
- let orders = getArray(columns * rows)
267
- if (isOrderDisabled) orders = orders.filter(order => (Array.isArray(isOrderDisabled) ? !isOrderDisabled.includes(order) : !isOrderDisabled(order)))
268
- if (orderPriority) orders = orders.toSorted(orderPriority)
269
- return orders
270
- }, [columns, rows, isOrderDisabled, orderPriority])
271
-
272
- /** 检查 items 的数量是否小于可用的次序的数量 */
273
- // if (items.length < orders.length) throw new Error("the number of items is less than the number of available orders")
274
-
275
- /** 元素的 key 到次序的映射 */
276
- const [keyToOrder, setKeyToOrder] = useState(() => {
277
- if (orderMap) return orderMap
278
- const newOrderMap = getOrderMap({ prev: undefined, orders, keys: Array.from(keyToItem.keys()) })
279
- onOrderMapChange?.(newOrderMap)
280
- return newOrderMap
281
- })
282
-
283
- /** 拖拽中的元素 */
284
- const [dragging, setDragging] = useState<DraggingItem<K> | undefined>(undefined)
285
-
286
- const { current: cache } = useRef({
287
- orders,
288
- keys: Array.from(keyToItem.keys()),
289
- })
290
-
291
- useEffect(() => {
292
- if (!isTheSameArray(dragging?.orders ?? cache.orders, orders) || !isTheSameIterable(dragging?.keyToOrder.keys() ?? cache.keys, keyToItem.keys())) {
293
- cache.orders = orders
294
- cache.keys = Array.from((dragging?.keyToOrder ?? keyToItem).keys())
295
- const newOrderMap = getOrderMap({ prev: dragging?.keyToOrder ?? keyToOrder, orders, keys: cache.keys })
296
- setKeyToOrder(newOrderMap)
297
- onOrderMapChange?.(newOrderMap)
298
- const newRenderKeys = new Set(newOrderMap.keys())
299
- setRenderKeys(prev => prev.intersection(newRenderKeys).union(newRenderKeys.difference(prev)))
300
- }
301
- }, [dragging, cache, orders, keyToItem, keyToOrder, onOrderMapChange])
302
-
303
- /** 渲染的元素的 key,不会随着 keyToOrder 的变化而变化 */
304
- const [renderKeys, setRenderKeys] = useState(() => new Set(keyToOrder.keys()))
305
-
306
- /** 用于记录最近被拖拽的元素,在元素返回位置时,仍旧能置顶显示 */
307
- const recent = useRef<Key | undefined>(undefined)
308
-
309
- useEffect(() => {
310
- if (
311
- !!dragging &&
312
- (!isTheSameArray(dragging.orders, orders) ||
313
- dragging.columns !== columns ||
314
- dragging.rows !== rows ||
315
- dragging.gapX !== gapX ||
316
- dragging.gapY !== gapY ||
317
- dragging.itemWidth !== itemWidth ||
318
- dragging.itemHeight !== itemHeight ||
319
- !isTheSameIterable(dragging.keyToOrder.keys(), keyToItem.keys()))
320
- ) {
321
- setDragging(undefined)
322
- if (isTheSameArray(dragging.orders, orders) && isTheSameIterable(dragging.keyToOrder.keys(), keyToItem.keys())) {
323
- setKeyToOrder(dragging.keyToOrder)
324
- onOrderMapChange?.(dragging.keyToOrder)
325
- }
326
- }
327
- }, [dragging, orders, columns, rows, gapX, gapY, itemWidth, itemHeight, keyToItem, keyToOrder, onOrderMapChange])
328
-
329
- /** 受控 */
330
- useEffect(() => {
331
- if (orderMap === keyToOrder) return
332
- if (!!orderMap) setKeyToOrder(orderMap)
333
- else onOrderMapChange?.(keyToOrder)
334
- }, [orderMap, keyToOrder, onOrderMapChange])
335
-
336
- function onDragMoveStart(key: K, event: DragMoveEvent<HTMLDivElement>) {
337
- _onDragMoveStart?.(event)
338
- const position = getPosition({ order: keyToOrder.get(key)!, columns, gapX, gapY, itemWidth, itemHeight })
339
- recent.current = key
340
- setDragging({
341
- key,
342
- startX: position.x,
343
- startY: position.y,
344
- deltaX: event.deltaX,
345
- deltaY: event.deltaY,
346
- orders,
347
- columns,
348
- rows,
349
- gapX,
350
- gapY,
351
- itemWidth,
352
- itemHeight,
353
- keyToOrder: keyToOrder,
354
- })
355
- }
356
-
357
- function onDragMove(event: DragMoveEvent<HTMLDivElement>) {
358
- _onDragMove?.(event)
359
- if (!dragging) return
360
-
361
- const { deltaX, deltaY } = event
362
- const { key, orders, keyToOrder, startX, startY, columns, gapX, gapY, itemWidth, itemHeight } = dragging!
363
- setDragging(prev => ({ ...prev!, deltaX, deltaY }))
364
- const x = startX + deltaX
365
- const y = startY + deltaY
366
- const currentOrder = keyToOrder.get(key)!
367
- const currentOrderIndex = orders.indexOf(currentOrder)
368
-
369
- /** 计算元素当前最近的位置 */
370
- const nearestOrder = orders.toSorted((a, b) => {
371
- const aPosition = getPosition({ order: a, columns, gapX, gapY, itemWidth, itemHeight })
372
- const bPosition = getPosition({ order: b, columns, gapX, gapY, itemWidth, itemHeight })
373
- const aDistance = (x - aPosition.x) ** 2 + (y - aPosition.y) ** 2
374
- const bDistance = (x - bPosition.x) ** 2 + (y - bPosition.y) ** 2
375
- if (aDistance - bDistance !== 0) return aDistance - bDistance
376
- const aIndex = orders.indexOf(a)
377
- const bIndex = orders.indexOf(b)
378
- return Math.abs(aIndex - currentOrderIndex) - Math.abs(bIndex - currentOrderIndex)
379
- })[0]
380
-
381
- /** 如果元素当前最近的位置就是开始移动前的位置,则直接恢复到移动前的次序 */
382
- if (nearestOrder === currentOrder) {
383
- setKeyToOrder(keyToOrder)
384
- onOrderMapChange?.(keyToOrder)
385
- return
386
- }
387
-
388
- /** 以元素开始移动前的次序为基准,计算元素当前最近的位置 */
389
- const newKeyToOrder = new Map(keyToOrder)
390
- newKeyToOrder.set(key, nearestOrder)
391
-
392
- const orderToKey = getOrderToKey(keyToOrder)
393
-
394
- /** 如果元素当前最近的位置没有被占用,则直接更新次序 */
395
- if (!orderToKey.has(nearestOrder)) {
396
- setKeyToOrder(newKeyToOrder)
397
- onOrderMapChange?.(newKeyToOrder)
398
- return
399
- }
400
-
401
- const nearestOrderIndex = orders.indexOf(nearestOrder)
402
-
403
- /**
404
- * 现在目标位置必定是有元素的
405
- * 我们需要确定元素在 orders 数组中移动的方向
406
- * 目标位置的元素方向应该尽量与移动元素移动的方向一致
407
- */
408
-
409
- const itemDirection = currentOrderIndex > nearestOrderIndex ? "decrease" : "increase"
410
-
411
- const targetDirection =
412
- (itemDirection === "decrease" && orders.slice(0, nearestOrderIndex).some(order => !orderToKey.has(order))) ||
413
- (itemDirection === "increase" && orders.slice(nearestOrderIndex + 1).every(order => orderToKey.has(order)))
414
- ? "decrease"
415
- : "increase"
416
-
417
- function moveTargetOrder(targetOrder: number) {
418
- const targetKey = orderToKey.get(targetOrder)!
419
- const targetOrderIndex = orders.indexOf(targetOrder)
420
- const nextOrderIndex = targetOrderIndex + (targetDirection === "decrease" ? -1 : 1)
421
- const nextOrder = orders[nextOrderIndex]
422
- newKeyToOrder.set(targetKey, nextOrder)
423
- if (nextOrder === currentOrder || !orderToKey.has(nextOrder)) return
424
- moveTargetOrder(nextOrder)
425
- }
426
-
427
- moveTargetOrder(nearestOrder)
428
- setKeyToOrder(newKeyToOrder)
429
- onOrderMapChange?.(newKeyToOrder)
430
- }
431
-
432
- function onDragMoveEnd(event: DragMoveEvent<HTMLDivElement>) {
433
- _onDragMoveEnd?.(event)
434
- if (!dragging) return
435
- setDragging(undefined)
436
- }
437
-
438
- function isItemDisabled(key: K) {
439
- if (!_isItemDisabled) return false
440
- if (Array.isArray(_isItemDisabled)) return _isItemDisabled.includes(key)
441
- return _isItemDisabled(keyToItem.get(key)!, key)
442
- }
443
-
444
- return (
445
- <div
446
- className={clsx(
447
- styles.draggableGrid,
448
- typeof className === "function" ? className({ isDragging: !!dragging }) : className,
449
- typeof classNames?.container === "function" ? classNames.container({ isDragging: !!dragging }) : classNames?.container,
450
- )}
451
- style={
452
- {
453
- "--width": `${columns * (itemWidth + gapX) - gapX}px`,
454
- "--height": `${rows * (itemHeight + gapY) - gapY}px`,
455
- ...(typeof style === "function" ? style({ isDragging: !!dragging }) : style),
456
- } as CSSProperties
457
- }
458
- {...rest}
459
- >
460
- {Array.from(renderKeys).map(key => {
461
- const item = keyToItem.get(key)!
462
- const order = keyToOrder.get(key)!
463
- const isDragging = dragging?.key === key
464
- const position = getPosition({ order, columns, gapX, gapY, itemWidth, itemHeight })
465
- const style = (
466
- isDragging
467
- ? {
468
- "--width": `${itemWidth}px`,
469
- "--height": `${itemHeight}px`,
470
- "--translate-x": `${dragging.startX + dragging.deltaX}px`,
471
- "--translate-y": `${dragging.startY + dragging.deltaY}px`,
472
- "--transition-property": "none",
473
- "--z-index": 999999,
474
- }
475
- : {
476
- "--width": `${itemWidth}px`,
477
- "--height": `${itemHeight}px`,
478
- "--translate-x": `${position.x}px`,
479
- "--translate-y": `${position.y}px`,
480
- "--transition-property": "transform",
481
- "--z-index": recent.current === key ? 999999 : keyToOrder.get(key),
482
- }
483
- ) as CSSProperties
484
- const children = render ? render(item, { order, isDragging: isDragging }) : (item as ReactNode)
485
-
486
- return (
487
- <DraggableGridItem
488
- key={key}
489
- item={item}
490
- itemKey={key}
491
- handle={handle}
492
- className={clsx(
493
- styles.draggableGridItem,
494
- typeof classNames?.item === "function" ? classNames.item({ order, isDragging: isDragging }) : classNames?.item,
495
- )}
496
- style={style}
497
- onDragMoveStart={!disabled && !isItemDisabled(key) ? event => onDragMoveStart(key, event) : undefined}
498
- onDragMove={!disabled && !isItemDisabled(key) ? onDragMove : undefined}
499
- onDragMoveEnd={!disabled && !isItemDisabled(key) ? onDragMoveEnd : undefined}
500
- children={children}
501
- />
502
- )
503
- })}
504
- </div>
505
- )
506
- }
1
+ import { CSSProperties, ComponentProps, Key, ReactNode, useEffect, useMemo, useRef, useState } from "react"
2
+ import { clsx, getArray, isNullable } from "deepsea-tools"
3
+ import { DragMoveEvent, DragMoveEvents, useDragMove } from "soda-hooks"
4
+
5
+ import styles from "./DraggableGrid.module.css"
6
+
7
+ export type MustBeReactNode<T> = false extends (T extends ReactNode ? true : false) ? false : true
8
+
9
+ export type MustBeReactKey<T> = false extends (T extends Key ? true : false) ? false : true
10
+
11
+ export type DraggableGridClassName = string | ((status: ContainerStatus) => string)
12
+
13
+ export type DraggableGridItemClassName = string | ((status: DraggableGridItemStatus) => string)
14
+
15
+ export type DraggableGridStyle = CSSProperties | ((status: ContainerStatus) => CSSProperties)
16
+
17
+ export interface DraggableGridClassNames {
18
+ /** 容器 */
19
+ container?: DraggableGridClassName
20
+ /** 元素 */
21
+ item?: DraggableGridItemClassName
22
+ }
23
+
24
+ export interface ContainerStatus {
25
+ /** 是否被拖拽 */
26
+ isDragging: boolean
27
+ }
28
+
29
+ export interface DraggableGridItemStatus {
30
+ /** 当前次序 */
31
+ order: number
32
+ /** 是否被拖拽 */
33
+ isDragging: boolean
34
+ }
35
+
36
+ /** 元素的 key 到次序的映射 */
37
+ export type DraggableGridKeyToOrder<K extends Key> = Map<K, number>
38
+
39
+ /** 次序到元素的 key 的映射 */
40
+ export type DraggableGridOrderToKey<K extends Key> = Map<number, K>
41
+
42
+ function isTheSameArray<T>(a: T[], b: T[]) {
43
+ if (a.length !== b.length) return false
44
+ for (let i = 0; i < a.length; i++) {
45
+ if (a[i] !== b[i]) return false
46
+ }
47
+ return true
48
+ }
49
+
50
+ function isTheSameIterable<T>(a: Iterable<T>, b: Iterable<T>) {
51
+ const aSet = a instanceof Set ? a : new Set(a)
52
+ const bSet = b instanceof Set ? b : new Set(b)
53
+ if (aSet.size !== bSet.size) return false
54
+ return aSet.difference(bSet).size === 0
55
+ }
56
+
57
+ interface GetOrderMapParams<K extends Key> {
58
+ prev?: DraggableGridKeyToOrder<K>
59
+ orders: number[]
60
+ keys: K[]
61
+ }
62
+
63
+ function getOrderMap<K extends Key>({ prev, orders, keys }: GetOrderMapParams<K>) {
64
+ const orderSet = new Set(orders)
65
+ const orderMap = new Map<K, number>()
66
+ const newKeys = keys.filter(key => {
67
+ if (!prev) return true
68
+ if (prev.has(key)) {
69
+ const prevOrder = prev.get(key)!
70
+ orderMap.set(key, prevOrder)
71
+ orderSet.delete(prevOrder)
72
+ return false
73
+ }
74
+ return true
75
+ })
76
+ orders = Array.from(orderSet)
77
+ newKeys.forEach((key, index) => orderMap.set(key, orders[index]))
78
+ return orderMap
79
+ }
80
+
81
+ interface GetPositionParams {
82
+ order: number
83
+ columns: number
84
+ gapX: number
85
+ gapY: number
86
+ itemWidth: number
87
+ itemHeight: number
88
+ }
89
+
90
+ interface Position {
91
+ x: number
92
+ y: number
93
+ }
94
+
95
+ function getPosition({ order, columns, gapX, gapY, itemWidth, itemHeight }: GetPositionParams): Position {
96
+ const row = Math.floor(order / columns)
97
+ const column = order % columns
98
+ return {
99
+ x: column * (itemWidth + gapX),
100
+ y: row * (itemHeight + gapY),
101
+ }
102
+ }
103
+
104
+ function getOrderToKey<K extends Key>(keyToOrder: DraggableGridKeyToOrder<K>) {
105
+ return new Map(Array.from(keyToOrder.entries()).map(([key, order]) => [order, key]))
106
+ }
107
+
108
+ export type DraggableGridPropsCore<T, K extends Key = T extends Key ? T : never> = {
109
+ /** 数据源 */
110
+ items?: T[]
111
+ /** 元素的 key 到次序的映射 */
112
+ orderMap?: DraggableGridKeyToOrder<K>
113
+ /** 次序变化时回调 */
114
+ onOrderMapChange?: (orderMap: DraggableGridKeyToOrder<K>) => void
115
+ /** 禁用的元素 */
116
+ isItemDisabled?: K[] | ((item: T, key: K) => boolean)
117
+ /** 触发移动的元素 */
118
+ handle?: string | HTMLElement | ((item: T, key: K, element: HTMLDivElement) => HTMLElement | undefined | null) | undefined | null
119
+ } & (MustBeReactNode<T> extends true
120
+ ? {
121
+ /** 渲染函数,当 T 为 ReactNode 时,render 为可选 */
122
+ render?: (item: T, status: DraggableGridItemStatus) => ReactNode
123
+ }
124
+ : {
125
+ /** 渲染函数,当 T 为 ReactNode 时,render 为可选 */
126
+ render: (item: T, status: DraggableGridItemStatus) => ReactNode
127
+ }) &
128
+ (MustBeReactKey<T> extends true
129
+ ? {
130
+ /** 获取 key 的函数,当 T 为 Key 时,keyExtractor 为可选 */
131
+ keyExtractor?: (item: T) => K
132
+ }
133
+ : {
134
+ /** 获取 key 的函数,当 T 为 Key 时,keyExtractor 为可选 */
135
+ keyExtractor: (item: T) => K
136
+ })
137
+
138
+ export type DraggableGridProps<T, K extends Key = T extends Key ? T : never> = Omit<ComponentProps<"div">, "className" | "children"> &
139
+ DragMoveEvents<HTMLDivElement> & {
140
+ /** 类名 */
141
+ className?: DraggableGridClassName
142
+ /** 类名 */
143
+ classNames?: DraggableGridClassNames
144
+ /** 样式 */
145
+ style?: DraggableGridStyle
146
+
147
+ /** 是否禁用拖拽 */
148
+ disabled?: boolean
149
+ /** 列数 */
150
+ columns: number
151
+ /** 行数 */
152
+ rows: number
153
+ /**
154
+ * 间距
155
+ * @default 0
156
+ */
157
+ gap?: number
158
+ /**
159
+ * 水平间距
160
+ * @default 0
161
+ */
162
+ gapX?: number
163
+ /**
164
+ * 垂直间距
165
+ * @default 0
166
+ */
167
+ gapY?: number
168
+ /** 元素宽度 */
169
+ itemWidth: number
170
+ /** 元素高度 */
171
+ itemHeight: number
172
+ /** 禁用的次序 */
173
+ isOrderDisabled?: number[] | ((order: number) => boolean)
174
+ /** 次序的优先级,可以通过此函数调整元素的优先放置的方向,左上,右上,左下,右下,中心,顺时针,逆时针等等 */
175
+ orderPriority?: (a: number, b: number) => number
176
+ } & DraggableGridPropsCore<T, K>
177
+
178
+ interface DraggableGridItemProps<T, K extends Key = T extends Key ? T : never> extends ComponentProps<"div">, DragMoveEvents<HTMLDivElement> {
179
+ item: T
180
+ itemKey: K
181
+ /** 触发移动的元素 */
182
+ handle?: string | HTMLElement | ((item: T, key: K, element: HTMLDivElement) => HTMLElement | undefined | null) | undefined | null
183
+ }
184
+
185
+ function DraggableGridItem<T, K extends Key = T extends Key ? T : never>({
186
+ item,
187
+ itemKey,
188
+ handle,
189
+ onDragMoveStart,
190
+ onDragMove,
191
+ onDragMoveEnd,
192
+ ...rest
193
+ }: DraggableGridItemProps<T, K>) {
194
+ const element = useRef<HTMLDivElement>(null)
195
+
196
+ useDragMove({
197
+ element: isNullable(handle)
198
+ ? element
199
+ : () =>
200
+ element.current &&
201
+ (typeof handle === "string"
202
+ ? (element.current.querySelector(handle) as HTMLElement)
203
+ : typeof handle === "function"
204
+ ? handle(item, itemKey, element.current)
205
+ : handle),
206
+ onDragMoveStart,
207
+ onDragMove,
208
+ onDragMoveEnd,
209
+ })
210
+
211
+ return <div ref={element} {...rest} />
212
+ }
213
+
214
+ interface DraggingItem<K extends Key> {
215
+ key: K
216
+ startX: number
217
+ startY: number
218
+ deltaX: number
219
+ deltaY: number
220
+ orders: number[]
221
+ columns: number
222
+ rows: number
223
+ gapX: number
224
+ gapY: number
225
+ itemWidth: number
226
+ itemHeight: number
227
+ keyToOrder: DraggableGridKeyToOrder<K>
228
+ }
229
+
230
+ export function DraggableGrid<T, K extends Key = T extends Key ? T : never>({
231
+ className,
232
+ classNames,
233
+ style,
234
+ items = [],
235
+ disabled,
236
+ columns,
237
+ rows,
238
+ gap = 0,
239
+ gapX = gap,
240
+ gapY = gap,
241
+ itemWidth,
242
+ itemHeight,
243
+ orderMap,
244
+ onOrderMapChange,
245
+ render,
246
+ keyExtractor,
247
+ isItemDisabled: _isItemDisabled,
248
+ isOrderDisabled,
249
+ orderPriority,
250
+ handle,
251
+ onDragMoveStart: _onDragMoveStart,
252
+ onDragMove: _onDragMove,
253
+ onDragMoveEnd: _onDragMoveEnd,
254
+ ...rest
255
+ }: DraggableGridProps<T, K>) {
256
+ const keyToItem = useMemo(() => {
257
+ const keyToItem = new Map<K, T>()
258
+ items.forEach(item => {
259
+ const key = keyExtractor ? keyExtractor(item) : (item as unknown as K)
260
+ keyToItem.set(key, item)
261
+ })
262
+ return keyToItem
263
+ }, [items, keyExtractor])
264
+
265
+ /** 检查 items 中是否有重复的 key */
266
+ if (keyToItem.size !== items.length) throw new Error("there are duplicate keys in the items")
267
+
268
+ /** 可用的次序 */
269
+ const orders = useMemo(() => {
270
+ let orders = getArray(columns * rows)
271
+ if (isOrderDisabled) orders = orders.filter(order => (Array.isArray(isOrderDisabled) ? !isOrderDisabled.includes(order) : !isOrderDisabled(order)))
272
+ if (orderPriority) orders = orders.toSorted(orderPriority)
273
+ return orders
274
+ }, [columns, rows, isOrderDisabled, orderPriority])
275
+
276
+ /** 检查 items 的数量是否小于可用的次序的数量 */
277
+ // if (items.length < orders.length) throw new Error("the number of items is less than the number of available orders")
278
+
279
+ /** 元素的 key 到次序的映射 */
280
+ const [keyToOrder, setKeyToOrder] = useState(() => {
281
+ if (orderMap) return orderMap
282
+ const newOrderMap = getOrderMap({ prev: undefined, orders, keys: Array.from(keyToItem.keys()) })
283
+ onOrderMapChange?.(newOrderMap)
284
+ return newOrderMap
285
+ })
286
+
287
+ /** 拖拽中的元素 */
288
+ const [dragging, setDragging] = useState<DraggingItem<K> | undefined>(undefined)
289
+
290
+ const { current: cache } = useRef({
291
+ orders,
292
+ keys: Array.from(keyToItem.keys()),
293
+ })
294
+
295
+ useEffect(() => {
296
+ if (!isTheSameArray(dragging?.orders ?? cache.orders, orders) || !isTheSameIterable(dragging?.keyToOrder.keys() ?? cache.keys, keyToItem.keys())) {
297
+ cache.orders = orders
298
+ cache.keys = Array.from((dragging?.keyToOrder ?? keyToItem).keys())
299
+ const newOrderMap = getOrderMap({ prev: dragging?.keyToOrder ?? keyToOrder, orders, keys: cache.keys })
300
+ setKeyToOrder(newOrderMap)
301
+ onOrderMapChange?.(newOrderMap)
302
+ const newRenderKeys = new Set(newOrderMap.keys())
303
+ setRenderKeys(prev => prev.intersection(newRenderKeys).union(newRenderKeys.difference(prev)))
304
+ }
305
+ }, [dragging, cache, orders, keyToItem, keyToOrder, onOrderMapChange])
306
+
307
+ /** 渲染的元素的 key,不会随着 keyToOrder 的变化而变化 */
308
+ const [renderKeys, setRenderKeys] = useState(() => new Set(keyToOrder.keys()))
309
+
310
+ /** 用于记录最近被拖拽的元素,在元素返回位置时,仍旧能置顶显示 */
311
+ const recent = useRef<Key | undefined>(undefined)
312
+
313
+ useEffect(() => {
314
+ if (
315
+ !!dragging &&
316
+ (!isTheSameArray(dragging.orders, orders) ||
317
+ dragging.columns !== columns ||
318
+ dragging.rows !== rows ||
319
+ dragging.gapX !== gapX ||
320
+ dragging.gapY !== gapY ||
321
+ dragging.itemWidth !== itemWidth ||
322
+ dragging.itemHeight !== itemHeight ||
323
+ !isTheSameIterable(dragging.keyToOrder.keys(), keyToItem.keys()))
324
+ ) {
325
+ setDragging(undefined)
326
+ if (isTheSameArray(dragging.orders, orders) && isTheSameIterable(dragging.keyToOrder.keys(), keyToItem.keys())) {
327
+ setKeyToOrder(dragging.keyToOrder)
328
+ onOrderMapChange?.(dragging.keyToOrder)
329
+ }
330
+ }
331
+ }, [dragging, orders, columns, rows, gapX, gapY, itemWidth, itemHeight, keyToItem, keyToOrder, onOrderMapChange])
332
+
333
+ /** 受控 */
334
+ useEffect(() => {
335
+ if (orderMap === keyToOrder) return
336
+ if (!!orderMap) setKeyToOrder(orderMap)
337
+ else onOrderMapChange?.(keyToOrder)
338
+ }, [orderMap, keyToOrder, onOrderMapChange])
339
+
340
+ function onDragMoveStart(key: K, event: DragMoveEvent<HTMLDivElement>) {
341
+ _onDragMoveStart?.(event)
342
+ const position = getPosition({ order: keyToOrder.get(key)!, columns, gapX, gapY, itemWidth, itemHeight })
343
+ recent.current = key
344
+ setDragging({
345
+ key,
346
+ startX: position.x,
347
+ startY: position.y,
348
+ deltaX: event.deltaX,
349
+ deltaY: event.deltaY,
350
+ orders,
351
+ columns,
352
+ rows,
353
+ gapX,
354
+ gapY,
355
+ itemWidth,
356
+ itemHeight,
357
+ keyToOrder: keyToOrder,
358
+ })
359
+ }
360
+
361
+ function onDragMove(event: DragMoveEvent<HTMLDivElement>) {
362
+ _onDragMove?.(event)
363
+ if (!dragging) return
364
+
365
+ const { deltaX, deltaY } = event
366
+ const { key, orders, keyToOrder, startX, startY, columns, gapX, gapY, itemWidth, itemHeight } = dragging!
367
+ setDragging(prev => ({ ...prev!, deltaX, deltaY }))
368
+ const x = startX + deltaX
369
+ const y = startY + deltaY
370
+ const currentOrder = keyToOrder.get(key)!
371
+ const currentOrderIndex = orders.indexOf(currentOrder)
372
+
373
+ /** 计算元素当前最近的位置 */
374
+ const nearestOrder = orders.toSorted((a, b) => {
375
+ const aPosition = getPosition({ order: a, columns, gapX, gapY, itemWidth, itemHeight })
376
+ const bPosition = getPosition({ order: b, columns, gapX, gapY, itemWidth, itemHeight })
377
+ const aDistance = (x - aPosition.x) ** 2 + (y - aPosition.y) ** 2
378
+ const bDistance = (x - bPosition.x) ** 2 + (y - bPosition.y) ** 2
379
+ if (aDistance - bDistance !== 0) return aDistance - bDistance
380
+ const aIndex = orders.indexOf(a)
381
+ const bIndex = orders.indexOf(b)
382
+ return Math.abs(aIndex - currentOrderIndex) - Math.abs(bIndex - currentOrderIndex)
383
+ })[0]
384
+
385
+ /** 如果元素当前最近的位置就是开始移动前的位置,则直接恢复到移动前的次序 */
386
+ if (nearestOrder === currentOrder) {
387
+ setKeyToOrder(keyToOrder)
388
+ onOrderMapChange?.(keyToOrder)
389
+ return
390
+ }
391
+
392
+ /** 以元素开始移动前的次序为基准,计算元素当前最近的位置 */
393
+ const newKeyToOrder = new Map(keyToOrder)
394
+ newKeyToOrder.set(key, nearestOrder)
395
+
396
+ const orderToKey = getOrderToKey(keyToOrder)
397
+
398
+ /** 如果元素当前最近的位置没有被占用,则直接更新次序 */
399
+ if (!orderToKey.has(nearestOrder)) {
400
+ setKeyToOrder(newKeyToOrder)
401
+ onOrderMapChange?.(newKeyToOrder)
402
+ return
403
+ }
404
+
405
+ const nearestOrderIndex = orders.indexOf(nearestOrder)
406
+
407
+ /**
408
+ * 现在目标位置必定是有元素的
409
+ * 我们需要确定元素在 orders 数组中移动的方向
410
+ * 目标位置的元素方向应该尽量与移动元素移动的方向一致
411
+ */
412
+
413
+ const itemDirection = currentOrderIndex > nearestOrderIndex ? "decrease" : "increase"
414
+
415
+ const targetDirection =
416
+ (itemDirection === "decrease" && orders.slice(0, nearestOrderIndex).some(order => !orderToKey.has(order))) ||
417
+ (itemDirection === "increase" && orders.slice(nearestOrderIndex + 1).every(order => orderToKey.has(order)))
418
+ ? "decrease"
419
+ : "increase"
420
+
421
+ function moveTargetOrder(targetOrder: number) {
422
+ const targetKey = orderToKey.get(targetOrder)!
423
+ const targetOrderIndex = orders.indexOf(targetOrder)
424
+ const nextOrderIndex = targetOrderIndex + (targetDirection === "decrease" ? -1 : 1)
425
+ const nextOrder = orders[nextOrderIndex]
426
+ newKeyToOrder.set(targetKey, nextOrder)
427
+ if (nextOrder === currentOrder || !orderToKey.has(nextOrder)) return
428
+ moveTargetOrder(nextOrder)
429
+ }
430
+
431
+ moveTargetOrder(nearestOrder)
432
+ setKeyToOrder(newKeyToOrder)
433
+ onOrderMapChange?.(newKeyToOrder)
434
+ }
435
+
436
+ function onDragMoveEnd(event: DragMoveEvent<HTMLDivElement>) {
437
+ _onDragMoveEnd?.(event)
438
+ if (!dragging) return
439
+ setDragging(undefined)
440
+ }
441
+
442
+ function isItemDisabled(key: K) {
443
+ if (!_isItemDisabled) return false
444
+ if (Array.isArray(_isItemDisabled)) return _isItemDisabled.includes(key)
445
+ return _isItemDisabled(keyToItem.get(key)!, key)
446
+ }
447
+
448
+ return (
449
+ <div
450
+ className={clsx(
451
+ styles.draggableGrid,
452
+ typeof className === "function" ? className({ isDragging: !!dragging }) : className,
453
+ typeof classNames?.container === "function" ? classNames.container({ isDragging: !!dragging }) : classNames?.container,
454
+ )}
455
+ style={
456
+ {
457
+ "--width": `${columns * (itemWidth + gapX) - gapX}px`,
458
+ "--height": `${rows * (itemHeight + gapY) - gapY}px`,
459
+ ...(typeof style === "function" ? style({ isDragging: !!dragging }) : style),
460
+ } as CSSProperties
461
+ }
462
+ {...rest}
463
+ >
464
+ {Array.from(renderKeys).map(key => {
465
+ const item = keyToItem.get(key)!
466
+ const order = keyToOrder.get(key)!
467
+ const isDragging = dragging?.key === key
468
+ const position = getPosition({ order, columns, gapX, gapY, itemWidth, itemHeight })
469
+ const style = (
470
+ isDragging
471
+ ? {
472
+ "--width": `${itemWidth}px`,
473
+ "--height": `${itemHeight}px`,
474
+ "--translate-x": `${dragging.startX + dragging.deltaX}px`,
475
+ "--translate-y": `${dragging.startY + dragging.deltaY}px`,
476
+ "--transition-property": "none",
477
+ "--z-index": 999999,
478
+ }
479
+ : {
480
+ "--width": `${itemWidth}px`,
481
+ "--height": `${itemHeight}px`,
482
+ "--translate-x": `${position.x}px`,
483
+ "--translate-y": `${position.y}px`,
484
+ "--transition-property": "transform",
485
+ "--z-index": recent.current === key ? 999999 : keyToOrder.get(key),
486
+ }
487
+ ) as CSSProperties
488
+ const children = render ? render(item, { order, isDragging: isDragging }) : (item as ReactNode)
489
+
490
+ return (
491
+ <DraggableGridItem
492
+ key={key}
493
+ item={item}
494
+ itemKey={key}
495
+ handle={handle}
496
+ className={clsx(
497
+ styles.draggableGridItem,
498
+ typeof classNames?.item === "function" ? classNames.item({ order, isDragging: isDragging }) : classNames?.item,
499
+ )}
500
+ style={style}
501
+ onDragMoveStart={!disabled && !isItemDisabled(key) ? event => onDragMoveStart(key, event) : undefined}
502
+ onDragMove={!disabled && !isItemDisabled(key) ? onDragMove : undefined}
503
+ onDragMoveEnd={!disabled && !isItemDisabled(key) ? onDragMoveEnd : undefined}
504
+ children={children}
505
+ />
506
+ )
507
+ })}
508
+ </div>
509
+ )
510
+ }