deepsea-components 5.17.0 → 5.17.2
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/components/AutoFit.cjs +1 -1
- package/dist/components/AutoFit.js +1 -1
- package/dist/components/DraggableGrid.cjs +1 -1
- package/dist/components/DraggableGrid.js +1 -1
- package/dist/components/FormLabel.cjs +1 -1
- package/dist/components/FormLabel.js +1 -1
- package/dist/components/TransitionNum.cjs +1 -1
- package/dist/components/TransitionNum.js +1 -1
- package/package.json +6 -6
- package/src/components/DraggableGrid.module.css +19 -19
- package/src/components/DraggableGrid.tsx +547 -547
|
@@ -84,7 +84,7 @@ const AutoFit = /*#__PURE__*/ (0, external_react_namespaceObject.forwardRef)((pr
|
|
|
84
84
|
const parent = null == (_ele_current = ele.current) ? void 0 : _ele_current.parentElement;
|
|
85
85
|
if (!parent) return;
|
|
86
86
|
const style = getComputedStyle(parent);
|
|
87
|
-
if ("static" === style.position) console.warn("AutoFit
|
|
87
|
+
if ("static" === style.position) console.warn("AutoFit 组件的父元素的 position 属性不应该是 static");
|
|
88
88
|
}, [
|
|
89
89
|
null == (_ele_current1 = ele.current) ? void 0 : _ele_current1.parentElement
|
|
90
90
|
]);
|
|
@@ -56,7 +56,7 @@ const AutoFit = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
|
56
56
|
const parent = null == (_ele_current = ele.current) ? void 0 : _ele_current.parentElement;
|
|
57
57
|
if (!parent) return;
|
|
58
58
|
const style = getComputedStyle(parent);
|
|
59
|
-
if ("static" === style.position) console.warn("AutoFit
|
|
59
|
+
if ("static" === style.position) console.warn("AutoFit 组件的父元素的 position 属性不应该是 static");
|
|
60
60
|
}, [
|
|
61
61
|
null == (_ele_current1 = ele.current) ? void 0 : _ele_current1.parentElement
|
|
62
62
|
]);
|
|
@@ -36,8 +36,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
36
36
|
DraggableGrid: ()=>DraggableGrid
|
|
37
37
|
});
|
|
38
38
|
const jsx_runtime_namespaceObject = require("react/jsx-runtime");
|
|
39
|
-
const external_react_namespaceObject = require("react");
|
|
40
39
|
const external_deepsea_tools_namespaceObject = require("deepsea-tools");
|
|
40
|
+
const external_react_namespaceObject = require("react");
|
|
41
41
|
const external_soda_hooks_namespaceObject = require("soda-hooks");
|
|
42
42
|
const external_DraggableGrid_module_cjs_namespaceObject = require("./DraggableGrid.module.cjs");
|
|
43
43
|
var external_DraggableGrid_module_cjs_default = /*#__PURE__*/ __webpack_require__.n(external_DraggableGrid_module_cjs_namespaceObject);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
2
|
import { clsx, getArray, isNullable } from "deepsea-tools";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useDragMove } from "soda-hooks";
|
|
5
5
|
import DraggableGrid_module from "./DraggableGrid.module.js";
|
|
6
6
|
function isTheSameArray(a, b) {
|
|
@@ -32,7 +32,7 @@ const external_deepsea_tools_namespaceObject = require("deepsea-tools");
|
|
|
32
32
|
const external_react_namespaceObject = require("react");
|
|
33
33
|
const TransitionNum = /*#__PURE__*/ (0, external_react_namespaceObject.forwardRef)((props, ref)=>{
|
|
34
34
|
const { children: num, period, numToStr, ins, ...rest } = props;
|
|
35
|
-
if (!Number.isInteger(num) || !Number.isInteger(period) || period <= 0) throw new RangeError("
|
|
35
|
+
if (!Number.isInteger(num) || !Number.isInteger(period) || period <= 0) throw new RangeError("目标数字必须是整数,周期必须是正整数");
|
|
36
36
|
const ele = (0, external_react_namespaceObject.useRef)(null);
|
|
37
37
|
const cache = (0, external_react_namespaceObject.useRef)({
|
|
38
38
|
num,
|
|
@@ -4,7 +4,7 @@ import { setFrameInterval } from "deepsea-tools";
|
|
|
4
4
|
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
|
5
5
|
const TransitionNum = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
6
6
|
const { children: num, period, numToStr, ins, ...rest } = props;
|
|
7
|
-
if (!Number.isInteger(num) || !Number.isInteger(period) || period <= 0) throw new RangeError("
|
|
7
|
+
if (!Number.isInteger(num) || !Number.isInteger(period) || period <= 0) throw new RangeError("目标数字必须是整数,周期必须是正整数");
|
|
8
8
|
const ele = useRef(null);
|
|
9
9
|
const cache = useRef({
|
|
10
10
|
num,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepsea-components",
|
|
3
|
-
"version": "5.17.
|
|
3
|
+
"version": "5.17.2",
|
|
4
4
|
"description": "格数科技自用组件库",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@emotion/css": "^11.13.5",
|
|
39
39
|
"clipboard": "^2.0.11",
|
|
40
|
-
"echarts": "^
|
|
41
|
-
"hls.js": "^1.6.
|
|
40
|
+
"echarts": "^6.0.0",
|
|
41
|
+
"hls.js": "^1.6.13",
|
|
42
42
|
"smooth-scrollbar": "^8.8.4",
|
|
43
|
-
"deepsea-tools": "5.42.
|
|
44
|
-
"soda-hooks": "6.15.
|
|
43
|
+
"deepsea-tools": "5.42.4",
|
|
44
|
+
"soda-hooks": "6.15.2"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"typescript": "^5.9.
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@types/node": ">=20",
|
|
@@ -1,19 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,547 +1,547 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
+
}
|