deepsea-components 5.16.16 → 5.17.1

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