@wzyjs/uis 0.3.9 → 0.3.16
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/package.json +2 -2
- package/src/antd/form/FileUploader/index.tsx +163 -0
- package/src/antd/form/RadioButton/index.tsx +3 -1
- package/src/antd/form/index.ts +1 -0
- package/src/antd/index.ts +2 -0
- package/src/components/Crud/components/CardList/index.tsx +174 -0
- package/src/components/Crud/components/CreateUpdate/index.tsx +99 -0
- package/src/components/Crud/components/Provider/index.tsx +73 -0
- package/src/components/Crud/components/Remove/index.tsx +56 -0
- package/src/components/Crud/components/index.ts +4 -0
- package/src/components/Crud/hooks/index.ts +4 -0
- package/src/components/Crud/hooks/useColumns.tsx +169 -0
- package/src/components/Crud/hooks/useList.ts +54 -0
- package/src/components/Crud/hooks/useOrderable.tsx +107 -0
- package/src/components/Crud/hooks/useRequest.ts +41 -0
- package/src/components/Crud/index.tsx +91 -0
- package/src/components/Crud/types/index.ts +188 -0
- package/src/components/Crud/utils/index.ts +87 -0
- package/src/components/MindMap/context.tsx +29 -0
- package/src/components/MindMap/hooks/useAlignmentSnap.ts +220 -0
- package/src/components/MindMap/hooks/useCopyPaste.ts +272 -0
- package/src/components/MindMap/hooks/useDropToReparent.ts +288 -0
- package/src/components/MindMap/hooks/useExpandCollapse.ts +146 -0
- package/src/components/MindMap/hooks/useMoveDescendants.ts +136 -0
- package/src/components/MindMap/hooks/useUndoRedo.ts +232 -0
- package/src/components/MindMap/index.tsx +117 -0
- package/src/components/ProgressButton/index.module.scss +65 -0
- package/src/components/ProgressButton/index.tsx +96 -0
- package/src/components/TimelineBar/components/CurrentWeekHighlight/index.tsx +64 -0
- package/src/components/TimelineBar/components/Guides/index.tsx +61 -0
- package/src/components/TimelineBar/components/Ticks/index.tsx +56 -0
- package/src/components/TimelineBar/components/TodayIndicator/index.tsx +54 -0
- package/src/components/TimelineBar/components/index.ts +4 -0
- package/src/components/TimelineBar/const.ts +3 -0
- package/src/components/TimelineBar/hooks/index.ts +5 -0
- package/src/components/TimelineBar/hooks/useHighlightRange.ts +21 -0
- package/src/components/TimelineBar/hooks/useMonthGuides.ts +40 -0
- package/src/components/TimelineBar/hooks/useTickValues.ts +18 -0
- package/src/components/TimelineBar/hooks/useVisibleRange.ts +43 -0
- package/src/components/TimelineBar/hooks/useWeekGuides.ts +39 -0
- package/src/components/TimelineBar/index.tsx +63 -0
- package/src/components/TimelineBar/utils.ts +27 -0
- package/src/components/index.ts +4 -0
- package/src/rn.ts +1 -0
- package/src/rns/index.ts +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
4
|
+
import type { Edge, EdgeChange, Node, NodeChange } from '@xyflow/react'
|
|
5
|
+
|
|
6
|
+
interface UseDropToReparentParams<TNodeData extends Record<string, unknown>> {
|
|
7
|
+
enabled?: boolean
|
|
8
|
+
nodes: Array<Node<TNodeData>>
|
|
9
|
+
edges: Edge[]
|
|
10
|
+
dropHighlightClassName?: string
|
|
11
|
+
canDropOnNode?: (params: { draggingNode: Node<TNodeData>, targetNode: Node<TNodeData> }) => boolean
|
|
12
|
+
onNodesChange?: (changes: Array<NodeChange<Node<TNodeData>>>) => void
|
|
13
|
+
onEdgesChange?: (changes: Array<EdgeChange<Edge>>) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UseDropToReparentResult<TNodeData extends Record<string, unknown>> {
|
|
17
|
+
onNodeDrag: (_event: unknown, node: Node<TNodeData>) => void
|
|
18
|
+
onNodeDragStop: (_event: unknown, node: Node<TNodeData>) => void
|
|
19
|
+
draggingNodeId: string | null
|
|
20
|
+
dropTargetNodeId: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaultDropHighlightClassName: string = 'ring-2 ring-sky-500'
|
|
24
|
+
|
|
25
|
+
const normalizeClassName = (className?: string): string[] => {
|
|
26
|
+
return (className ?? '').split(/\s+/).map(item => item.trim()).filter(Boolean)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const withClassTokens = (className: string | undefined, tokens: string[], enabled: boolean): string | undefined => {
|
|
30
|
+
const list = normalizeClassName(className)
|
|
31
|
+
const tokenSet = new Set(list)
|
|
32
|
+
|
|
33
|
+
if (enabled) {
|
|
34
|
+
tokens.forEach(token => tokenSet.add(token))
|
|
35
|
+
} else {
|
|
36
|
+
tokens.forEach(token => tokenSet.delete(token))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const next = Array.from(tokenSet).join(' ')
|
|
40
|
+
return next.length > 0 ? next : undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const getNodeSize = <TNodeData extends Record<string, unknown>,>(node: Node<TNodeData>): { width: number, height: number } => {
|
|
44
|
+
const width = node.measured?.width ?? node.width ?? 0
|
|
45
|
+
const height = node.measured?.height ?? node.height ?? 0
|
|
46
|
+
return { width, height }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isPointInRect = (point: { x: number, y: number }, rect: { x: number, y: number, w: number, h: number }): boolean => {
|
|
50
|
+
return point.x >= rect.x && point.x <= rect.x + rect.w && point.y >= rect.y && point.y <= rect.y + rect.h
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getDescendants = (edges: Edge[], rootId: string): Set<string> => {
|
|
54
|
+
const sourceToTargets = new Map<string, string[]>()
|
|
55
|
+
edges.forEach(edge => {
|
|
56
|
+
const prev = sourceToTargets.get(edge.source) ?? []
|
|
57
|
+
sourceToTargets.set(edge.source, [...prev, edge.target])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const visited = new Set<string>([rootId])
|
|
61
|
+
const result = new Set<string>()
|
|
62
|
+
const queue: string[] = [rootId]
|
|
63
|
+
|
|
64
|
+
while (queue.length > 0) {
|
|
65
|
+
const currentId = queue.shift()
|
|
66
|
+
if (!currentId) {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
const targets = sourceToTargets.get(currentId) ?? []
|
|
70
|
+
for (const targetId of targets) {
|
|
71
|
+
if (visited.has(targetId)) {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
visited.add(targetId)
|
|
75
|
+
result.add(targetId)
|
|
76
|
+
queue.push(targetId)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pickDropTargetId = <TNodeData extends Record<string, unknown>,>(
|
|
84
|
+
nodes: Array<Node<TNodeData>>,
|
|
85
|
+
draggingNode: Node<TNodeData>,
|
|
86
|
+
edges: Edge[],
|
|
87
|
+
canDropOnNode?: (params: { draggingNode: Node<TNodeData>, targetNode: Node<TNodeData> }) => boolean,
|
|
88
|
+
): string | null => {
|
|
89
|
+
const draggingSize = getNodeSize(draggingNode)
|
|
90
|
+
if (!draggingSize.width || !draggingSize.height) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const descendants = getDescendants(edges, draggingNode.id)
|
|
95
|
+
|
|
96
|
+
const center = {
|
|
97
|
+
x: draggingNode.position.x + draggingSize.width / 2,
|
|
98
|
+
y: draggingNode.position.y + draggingSize.height / 2,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let bestId: string | null = null
|
|
102
|
+
let bestDist: number = Number.POSITIVE_INFINITY
|
|
103
|
+
|
|
104
|
+
nodes.forEach(node => {
|
|
105
|
+
if (node.id === draggingNode.id) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (descendants.has(node.id)) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if (canDropOnNode && !canDropOnNode({ draggingNode, targetNode: node })) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const size = getNodeSize(node)
|
|
116
|
+
if (!size.width || !size.height) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const rect = { x: node.position.x, y: node.position.y, w: size.width, h: size.height }
|
|
121
|
+
if (!isPointInRect(center, rect)) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const nodeCenterX = node.position.x + size.width / 2
|
|
126
|
+
const nodeCenterY = node.position.y + size.height / 2
|
|
127
|
+
const dx = center.x - nodeCenterX
|
|
128
|
+
const dy = center.y - nodeCenterY
|
|
129
|
+
const dist = (dx * dx) + (dy * dy)
|
|
130
|
+
if (dist < bestDist) {
|
|
131
|
+
bestDist = dist
|
|
132
|
+
bestId = node.id
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return bestId
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const useDropToReparent = <TNodeData extends Record<string, unknown>,>(
|
|
140
|
+
params: UseDropToReparentParams<TNodeData>,
|
|
141
|
+
): UseDropToReparentResult<TNodeData> => {
|
|
142
|
+
const {
|
|
143
|
+
enabled = true,
|
|
144
|
+
nodes,
|
|
145
|
+
edges,
|
|
146
|
+
dropHighlightClassName = defaultDropHighlightClassName,
|
|
147
|
+
canDropOnNode,
|
|
148
|
+
onNodesChange,
|
|
149
|
+
onEdgesChange,
|
|
150
|
+
} = params
|
|
151
|
+
|
|
152
|
+
const dropTargetIdRef = useRef<string | null>(null)
|
|
153
|
+
const [draggingNodeId, setDraggingNodeId] = useState<string | null>(null)
|
|
154
|
+
const [dropTargetNodeId, setDropTargetNodeId] = useState<string | null>(null)
|
|
155
|
+
const dropHighlightClassTokens = useMemo((): string[] => {
|
|
156
|
+
return normalizeClassName(dropHighlightClassName)
|
|
157
|
+
}, [dropHighlightClassName])
|
|
158
|
+
|
|
159
|
+
const nodeById = useMemo((): Map<string, Node<TNodeData>> => {
|
|
160
|
+
return new Map(nodes.map(node => [node.id, node]))
|
|
161
|
+
}, [nodes])
|
|
162
|
+
|
|
163
|
+
const setDropTargetId = useCallback((nextId: string | null): void => {
|
|
164
|
+
const prevId = dropTargetIdRef.current
|
|
165
|
+
if (prevId === nextId) {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
dropTargetIdRef.current = nextId
|
|
169
|
+
setDropTargetNodeId(nextId)
|
|
170
|
+
|
|
171
|
+
if (!onNodesChange) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const changes: Array<NodeChange<Node<TNodeData>>> = []
|
|
176
|
+
if (prevId) {
|
|
177
|
+
const prevNode = nodeById.get(prevId)
|
|
178
|
+
if (prevNode) {
|
|
179
|
+
changes.push({
|
|
180
|
+
type: 'replace',
|
|
181
|
+
id: prevId,
|
|
182
|
+
item: {
|
|
183
|
+
...prevNode,
|
|
184
|
+
className: withClassTokens(prevNode.className, dropHighlightClassTokens, false),
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (nextId) {
|
|
190
|
+
const nextNode = nodeById.get(nextId)
|
|
191
|
+
if (nextNode) {
|
|
192
|
+
changes.push({
|
|
193
|
+
type: 'replace',
|
|
194
|
+
id: nextId,
|
|
195
|
+
item: {
|
|
196
|
+
...nextNode,
|
|
197
|
+
className: withClassTokens(nextNode.className, dropHighlightClassTokens, true),
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (changes.length > 0) {
|
|
204
|
+
onNodesChange(changes)
|
|
205
|
+
}
|
|
206
|
+
}, [dropHighlightClassTokens, nodeById, onNodesChange])
|
|
207
|
+
|
|
208
|
+
const onNodeDrag = useCallback((_event: unknown, node: Node<TNodeData>): void => {
|
|
209
|
+
setDraggingNodeId(node.id)
|
|
210
|
+
if (!enabled) {
|
|
211
|
+
setDropTargetId(null)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
const targetId = pickDropTargetId(nodes, node, edges, canDropOnNode)
|
|
215
|
+
setDropTargetId(targetId)
|
|
216
|
+
}, [canDropOnNode, edges, enabled, nodes, setDropTargetId])
|
|
217
|
+
|
|
218
|
+
const onNodeDragStop = useCallback((_event: unknown, node: Node<TNodeData>): void => {
|
|
219
|
+
const targetId = dropTargetIdRef.current
|
|
220
|
+
setDropTargetId(null)
|
|
221
|
+
setDraggingNodeId(null)
|
|
222
|
+
|
|
223
|
+
if (!enabled) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
if (!targetId) {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
if (!onEdgesChange) {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const childId = node.id
|
|
234
|
+
if (targetId === childId) {
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const descendants = getDescendants(edges, childId)
|
|
239
|
+
if (descendants.has(targetId)) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const targetNode = nodeById.get(targetId)
|
|
244
|
+
if (!targetNode) {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if (canDropOnNode && !canDropOnNode({ draggingNode: node, targetNode })) {
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const nextEdgeId = `e-${targetId}-${childId}`
|
|
252
|
+
const hasSameEdge = edges.some(edge => edge.source === targetId && edge.target === childId)
|
|
253
|
+
const incomingEdges = edges.filter(edge => edge.target === childId)
|
|
254
|
+
|
|
255
|
+
const edgeChanges: Array<EdgeChange<Edge>> = []
|
|
256
|
+
incomingEdges.forEach(edge => {
|
|
257
|
+
if (edge.source === targetId && edge.target === childId) {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
edgeChanges.push({ type: 'remove', id: edge.id })
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
if (!hasSameEdge) {
|
|
264
|
+
edgeChanges.push({
|
|
265
|
+
type: 'add',
|
|
266
|
+
item: {
|
|
267
|
+
id: nextEdgeId,
|
|
268
|
+
source: targetId,
|
|
269
|
+
target: childId,
|
|
270
|
+
type: 'smoothstep',
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (edgeChanges.length === 0) {
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
onEdgesChange(edgeChanges)
|
|
280
|
+
}, [canDropOnNode, edges, enabled, nodeById, onEdgesChange, setDropTargetId])
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
onNodeDrag,
|
|
284
|
+
onNodeDragStop,
|
|
285
|
+
draggingNodeId,
|
|
286
|
+
dropTargetNodeId,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo } from 'react'
|
|
4
|
+
import type { Edge, EdgeChange, Node, NodeChange } from '@xyflow/react'
|
|
5
|
+
|
|
6
|
+
interface GetCountsResult {
|
|
7
|
+
childCount: number
|
|
8
|
+
descendantCount: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface UseExpandCollapseParams<TNodeData extends Record<string, unknown>> {
|
|
12
|
+
nodes: Array<Node<TNodeData>>
|
|
13
|
+
edges: Edge[]
|
|
14
|
+
onNodesChange?: (changes: Array<NodeChange<Node<TNodeData>>>) => void
|
|
15
|
+
onEdgesChange?: (changes: Array<EdgeChange<Edge>>) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UseExpandCollapseResult {
|
|
19
|
+
getCounts: (nodeId: string) => GetCountsResult
|
|
20
|
+
expand: (nodeId: string) => void
|
|
21
|
+
collapse: (nodeId: string) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const buildChildrenById = (edges: Edge[]): Map<string, string[]> => {
|
|
25
|
+
const map = new Map<string, string[]>()
|
|
26
|
+
edges.forEach(edge => {
|
|
27
|
+
const prev = map.get(edge.source) ?? []
|
|
28
|
+
map.set(edge.source, [...prev, edge.target])
|
|
29
|
+
})
|
|
30
|
+
return map
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const getDescendantIdSet = (childrenById: Map<string, string[]>, rootId: string): Set<string> => {
|
|
34
|
+
const visited = new Set<string>([rootId])
|
|
35
|
+
const result = new Set<string>()
|
|
36
|
+
const queue: string[] = [rootId]
|
|
37
|
+
|
|
38
|
+
while (queue.length > 0) {
|
|
39
|
+
const currentId = queue.shift()
|
|
40
|
+
if (!currentId) {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
const children = childrenById.get(currentId) ?? []
|
|
44
|
+
for (const childId of children) {
|
|
45
|
+
if (visited.has(childId)) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
visited.add(childId)
|
|
49
|
+
result.add(childId)
|
|
50
|
+
queue.push(childId)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const useExpandCollapse = <TNodeData extends Record<string, unknown>,>(
|
|
58
|
+
params: UseExpandCollapseParams<TNodeData>,
|
|
59
|
+
): UseExpandCollapseResult => {
|
|
60
|
+
const { nodes, edges, onNodesChange, onEdgesChange } = params
|
|
61
|
+
|
|
62
|
+
const childrenById = useMemo((): Map<string, string[]> => {
|
|
63
|
+
return buildChildrenById(edges)
|
|
64
|
+
}, [edges])
|
|
65
|
+
|
|
66
|
+
const nodeById = useMemo((): Map<string, Node<TNodeData>> => {
|
|
67
|
+
return new Map(nodes.map(n => [n.id, n]))
|
|
68
|
+
}, [nodes])
|
|
69
|
+
|
|
70
|
+
const edgeById = useMemo((): Map<string, Edge> => {
|
|
71
|
+
return new Map(edges.map(e => [e.id, e]))
|
|
72
|
+
}, [edges])
|
|
73
|
+
|
|
74
|
+
const getCounts = useCallback((nodeId: string): GetCountsResult => {
|
|
75
|
+
const childCount = (childrenById.get(nodeId) ?? []).length
|
|
76
|
+
const descendantCount = getDescendantIdSet(childrenById, nodeId).size
|
|
77
|
+
return { childCount, descendantCount }
|
|
78
|
+
}, [childrenById])
|
|
79
|
+
|
|
80
|
+
const setSubtreeHidden = useCallback((nodeId: string, hidden: boolean): void => {
|
|
81
|
+
const descendantIdSet = getDescendantIdSet(childrenById, nodeId)
|
|
82
|
+
if (descendantIdSet.size === 0) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (onNodesChange) {
|
|
87
|
+
const nodeChanges: Array<NodeChange<Node<TNodeData>>> = []
|
|
88
|
+
descendantIdSet.forEach(id => {
|
|
89
|
+
const node = nodeById.get(id)
|
|
90
|
+
if (!node) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
if ((node.hidden ?? false) === hidden) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
nodeChanges.push({
|
|
97
|
+
type: 'replace',
|
|
98
|
+
id,
|
|
99
|
+
item: {
|
|
100
|
+
...node,
|
|
101
|
+
hidden,
|
|
102
|
+
selected: false,
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
if (nodeChanges.length > 0) {
|
|
107
|
+
onNodesChange(nodeChanges)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (onEdgesChange) {
|
|
112
|
+
const idSet = new Set<string>([nodeId, ...Array.from(descendantIdSet)])
|
|
113
|
+
const edgeChanges: Array<EdgeChange<Edge>> = edges
|
|
114
|
+
.filter(edge => idSet.has(edge.source) && idSet.has(edge.target))
|
|
115
|
+
.map(edge => edgeById.get(edge.id))
|
|
116
|
+
.filter((edge): edge is Edge => !!edge)
|
|
117
|
+
.filter(edge => (edge.hidden ?? false) !== hidden)
|
|
118
|
+
.map(edge => ({
|
|
119
|
+
type: 'replace',
|
|
120
|
+
id: edge.id,
|
|
121
|
+
item: {
|
|
122
|
+
...edge,
|
|
123
|
+
hidden,
|
|
124
|
+
},
|
|
125
|
+
}))
|
|
126
|
+
|
|
127
|
+
if (edgeChanges.length > 0) {
|
|
128
|
+
onEdgesChange(edgeChanges)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, [childrenById, edgeById, edges, nodeById, onEdgesChange, onNodesChange])
|
|
132
|
+
|
|
133
|
+
const expand = useCallback((nodeId: string): void => {
|
|
134
|
+
setSubtreeHidden(nodeId, false)
|
|
135
|
+
}, [setSubtreeHidden])
|
|
136
|
+
|
|
137
|
+
const collapse = useCallback((nodeId: string): void => {
|
|
138
|
+
setSubtreeHidden(nodeId, true)
|
|
139
|
+
}, [setSubtreeHidden])
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
getCounts,
|
|
143
|
+
expand,
|
|
144
|
+
collapse,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { Edge, Node, NodeChange } from '@xyflow/react'
|
|
6
|
+
|
|
7
|
+
interface UseMoveDescendantsParams<TNodeData extends Record<string, unknown>> {
|
|
8
|
+
// 开关:关闭时只透传,不联动后代
|
|
9
|
+
enabled?: boolean
|
|
10
|
+
// 当前节点与边(用于计算父子关系)
|
|
11
|
+
nodes: Array<Node<TNodeData>>
|
|
12
|
+
edges: Edge[]
|
|
13
|
+
// 透传给 ReactFlow 的 onNodesChange(通常来自 useNodesState)
|
|
14
|
+
onNodesChange?: (changes: Array<NodeChange<Node<TNodeData>>>) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UseMoveDescendantsResult<TNodeData extends Record<string, unknown>> {
|
|
18
|
+
// 包装后的 onNodesChange:在 position change 上把位移叠加到后代节点
|
|
19
|
+
onNodesChange: (changes: Array<NodeChange<Node<TNodeData>>>) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const getDescendants = (sourceToTargets: Map<string, string[]>, rootId: string): Set<string> => {
|
|
23
|
+
const visited = new Set<string>([rootId])
|
|
24
|
+
const result = new Set<string>()
|
|
25
|
+
const queue: string[] = [rootId]
|
|
26
|
+
|
|
27
|
+
while (queue.length > 0) {
|
|
28
|
+
const currentId = queue.shift()
|
|
29
|
+
if (!currentId) {
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
const targets = sourceToTargets.get(currentId) ?? []
|
|
33
|
+
for (const targetId of targets) {
|
|
34
|
+
if (visited.has(targetId)) {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
visited.add(targetId)
|
|
38
|
+
result.add(targetId)
|
|
39
|
+
queue.push(targetId)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const useMoveDescendants = <TNodeData extends Record<string, unknown>,>(
|
|
47
|
+
params: UseMoveDescendantsParams<TNodeData>,
|
|
48
|
+
): UseMoveDescendantsResult<TNodeData> => {
|
|
49
|
+
const { enabled = true, nodes, edges, onNodesChange: onNodesChangeExternal } = params
|
|
50
|
+
|
|
51
|
+
// 用 edge.source -> edge.target 作为父子关系(默认把 source 当作父节点)
|
|
52
|
+
const sourceToTargets = useMemo((): Map<string, string[]> => {
|
|
53
|
+
const map = new Map<string, string[]>()
|
|
54
|
+
edges.forEach(edge => {
|
|
55
|
+
const prev = map.get(edge.source) ?? []
|
|
56
|
+
map.set(edge.source, [...prev, edge.target])
|
|
57
|
+
})
|
|
58
|
+
return map
|
|
59
|
+
}, [edges])
|
|
60
|
+
|
|
61
|
+
const onNodesChange = useCallback((changes: Array<NodeChange<Node<TNodeData>>>): void => {
|
|
62
|
+
if (!onNodesChangeExternal) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!enabled || edges.length === 0) {
|
|
67
|
+
onNodesChangeExternal(changes)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 用旧节点位置计算拖拽的 dx/dy,再把位移叠加到后代节点
|
|
72
|
+
const nodeMap = new Map<string, Node<TNodeData>>(nodes.map(node => [node.id, node]))
|
|
73
|
+
const changedIdSet = new Set<string>(
|
|
74
|
+
changes.filter(change => change.type === 'position').map(change => change.id),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const deltaMap = new Map<string, { dx: number, dy: number, dragging: boolean }>()
|
|
78
|
+
|
|
79
|
+
changes.forEach(change => {
|
|
80
|
+
if (change.type !== 'position' || !change.position) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
const prev = nodeMap.get(change.id)
|
|
84
|
+
if (!prev) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const dx = change.position.x - prev.position.x
|
|
89
|
+
const dy = change.position.y - prev.position.y
|
|
90
|
+
if (dx === 0 && dy === 0) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const dragging = change.dragging === true
|
|
95
|
+
// 只影响“未被本次 change 显式更新”的节点,避免和多选拖拽冲突
|
|
96
|
+
const descendants = getDescendants(sourceToTargets, change.id)
|
|
97
|
+
descendants.forEach(descId => {
|
|
98
|
+
if (changedIdSet.has(descId)) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const prevDelta = deltaMap.get(descId)
|
|
102
|
+
if (!prevDelta) {
|
|
103
|
+
deltaMap.set(descId, { dx, dy, dragging })
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
deltaMap.set(descId, { dx: prevDelta.dx + dx, dy: prevDelta.dy + dy, dragging: prevDelta.dragging || dragging })
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (deltaMap.size === 0) {
|
|
111
|
+
onNodesChangeExternal(changes)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const extraChanges: Array<NodeChange<Node<TNodeData>>> = []
|
|
116
|
+
deltaMap.forEach((delta, nodeId) => {
|
|
117
|
+
const node = nodeMap.get(nodeId)
|
|
118
|
+
if (!node) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
extraChanges.push({
|
|
122
|
+
type: 'position',
|
|
123
|
+
id: nodeId,
|
|
124
|
+
position: {
|
|
125
|
+
x: node.position.x + delta.dx,
|
|
126
|
+
y: node.position.y + delta.dy,
|
|
127
|
+
},
|
|
128
|
+
dragging: delta.dragging,
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
onNodesChangeExternal([...changes, ...extraChanges])
|
|
133
|
+
}, [edges.length, enabled, nodes, onNodesChangeExternal, sourceToTargets])
|
|
134
|
+
|
|
135
|
+
return { onNodesChange }
|
|
136
|
+
}
|