deepsea-components 5.15.28 → 5.16.0

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