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