@barefootjs/xyflow 0.1.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/README.md +117 -0
- package/dist/classes.d.ts +31 -0
- package/dist/classes.d.ts.map +1 -0
- package/dist/compat.d.ts +67 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/connection.d.ts +20 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/edge-path.d.ts +14 -0
- package/dist/edge-path.d.ts.map +1 -0
- package/dist/flow-subsystems.d.ts +28 -0
- package/dist/flow-subsystems.d.ts.map +1 -0
- package/dist/hooks.d.ts +34 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6572 -0
- package/dist/node-resizer.d.ts +52 -0
- package/dist/node-resizer.d.ts.map +1 -0
- package/dist/node-type-dispatch.d.ts +34 -0
- package/dist/node-type-dispatch.d.ts.map +1 -0
- package/dist/selection.d.ts +32 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/store.d.ts +8 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/xyflow.browser.min.js +95 -0
- package/dist/xyflow.browser.min.js.map +129 -0
- package/package.json +58 -0
- package/src/__tests__/clamp-drag-position.test.ts +111 -0
- package/src/__tests__/compat.test.ts +157 -0
- package/src/__tests__/host-element-store.test.ts +33 -0
- package/src/__tests__/jsx-smoke.test.ts +33 -0
- package/src/__tests__/jsx-smoke.tsx +23 -0
- package/src/__tests__/node-type-dispatch.test.ts +104 -0
- package/src/__tests__/store.test.ts +399 -0
- package/src/__tests__/tsconfig.json +23 -0
- package/src/classes.ts +41 -0
- package/src/compat.ts +237 -0
- package/src/connection.ts +459 -0
- package/src/constants.ts +8 -0
- package/src/context.ts +8 -0
- package/src/edge-path.ts +89 -0
- package/src/flow-subsystems.ts +506 -0
- package/src/hooks.ts +72 -0
- package/src/index.ts +134 -0
- package/src/node-resizer.ts +276 -0
- package/src/node-type-dispatch.ts +46 -0
- package/src/selection.ts +407 -0
- package/src/store.ts +526 -0
- package/src/types.ts +329 -0
- package/src/utils.ts +13 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSignal,
|
|
3
|
+
createEffect,
|
|
4
|
+
createMemo,
|
|
5
|
+
untrack,
|
|
6
|
+
} from '@barefootjs/client'
|
|
7
|
+
import {
|
|
8
|
+
adoptUserNodes,
|
|
9
|
+
updateAbsolutePositions,
|
|
10
|
+
updateConnectionLookup,
|
|
11
|
+
fitViewport,
|
|
12
|
+
panBy as panByUtil,
|
|
13
|
+
} from '@xyflow/system'
|
|
14
|
+
import type {
|
|
15
|
+
NodeBase,
|
|
16
|
+
EdgeBase,
|
|
17
|
+
InternalNodeBase,
|
|
18
|
+
Viewport,
|
|
19
|
+
NodeLookup,
|
|
20
|
+
ParentLookup,
|
|
21
|
+
EdgeLookup,
|
|
22
|
+
ConnectionLookup,
|
|
23
|
+
SnapGrid,
|
|
24
|
+
NodeOrigin,
|
|
25
|
+
Transform,
|
|
26
|
+
PanZoomInstance,
|
|
27
|
+
NodeDragItem,
|
|
28
|
+
XYPosition,
|
|
29
|
+
} from '@xyflow/system'
|
|
30
|
+
import type { FlowStoreOptions, InternalFlowStore, FitViewOptions } from './types'
|
|
31
|
+
import { INFINITE_EXTENT } from './constants'
|
|
32
|
+
|
|
33
|
+
const DEFAULT_VIEWPORT: Viewport = { x: 0, y: 0, zoom: 1 }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a signal-based reactive store that bridges @xyflow/system
|
|
37
|
+
* with BarefootJS reactivity.
|
|
38
|
+
*/
|
|
39
|
+
export function createFlowStore<
|
|
40
|
+
NodeType extends NodeBase = NodeBase,
|
|
41
|
+
EdgeType extends EdgeBase = EdgeBase,
|
|
42
|
+
>(options: FlowStoreOptions<NodeType, EdgeType> = {}): InternalFlowStore<NodeType, EdgeType> {
|
|
43
|
+
// --- Configuration ---
|
|
44
|
+
const minZoom = options.minZoom ?? 0.5
|
|
45
|
+
const maxZoom = options.maxZoom ?? 2
|
|
46
|
+
const nodeOrigin: NodeOrigin = options.nodeOrigin ?? [0, 0]
|
|
47
|
+
const nodeExtent = options.nodeExtent ?? INFINITE_EXTENT
|
|
48
|
+
const snapToGrid = options.snapToGrid ?? false
|
|
49
|
+
const snapGrid: SnapGrid = options.snapGrid ?? [15, 15]
|
|
50
|
+
const edgesReconnectable = options.edgesReconnectable ?? false
|
|
51
|
+
|
|
52
|
+
// --- Core state signals ---
|
|
53
|
+
const [nodes, setNodes] = createSignal<NodeType[]>(options.nodes ?? [])
|
|
54
|
+
const [edges, setEdges] = createSignal<EdgeType[]>(options.edges ?? [])
|
|
55
|
+
const [viewport, setViewport] = createSignal<Viewport>(
|
|
56
|
+
options.defaultViewport ?? DEFAULT_VIEWPORT
|
|
57
|
+
)
|
|
58
|
+
const [width, setWidth] = createSignal(0)
|
|
59
|
+
const [height, setHeight] = createSignal(0)
|
|
60
|
+
const [dragging, setDragging] = createSignal(false)
|
|
61
|
+
|
|
62
|
+
// --- Internal refs ---
|
|
63
|
+
const [panZoom, setPanZoom] = createSignal<PanZoomInstance | null>(null)
|
|
64
|
+
const [domNode, setDomNode] = createSignal<HTMLElement | null>(null)
|
|
65
|
+
|
|
66
|
+
// --- Lookups ---
|
|
67
|
+
//
|
|
68
|
+
// The outer signals hold the lookup `Map` for iteration-style
|
|
69
|
+
// consumers (edge-path effect, adapters that walk every node). They
|
|
70
|
+
// emit a fresh `Map` reference on each change so barefoot's
|
|
71
|
+
// identity-based dedupe does not suppress notification.
|
|
72
|
+
//
|
|
73
|
+
// The internal map of per-node signals (`nodeSignals`) is what
|
|
74
|
+
// delivers the fine-grained channel: per-node consumers subscribe
|
|
75
|
+
// through `nodeSignal(id)` and only wake up when *that* node's entry
|
|
76
|
+
// changes. This removes the recurring "read `nodes()` first to wake
|
|
77
|
+
// up the lookup" workaround at its source (#1270).
|
|
78
|
+
const [nodeLookup, setNodeLookup] = createSignal<NodeLookup<InternalNodeBase<NodeType>>>(
|
|
79
|
+
new Map()
|
|
80
|
+
)
|
|
81
|
+
const [parentLookup, setParentLookup] = createSignal<ParentLookup<InternalNodeBase<NodeType>>>(
|
|
82
|
+
new Map()
|
|
83
|
+
)
|
|
84
|
+
const [edgeLookup, setEdgeLookup] = createSignal<EdgeLookup<EdgeType>>(new Map())
|
|
85
|
+
const [connectionLookup, setConnectionLookup] = createSignal<ConnectionLookup>(new Map())
|
|
86
|
+
|
|
87
|
+
type NodeSignalSlot = {
|
|
88
|
+
get: () => InternalNodeBase<NodeType> | undefined
|
|
89
|
+
set: (
|
|
90
|
+
value:
|
|
91
|
+
| InternalNodeBase<NodeType>
|
|
92
|
+
| undefined
|
|
93
|
+
| ((prev: InternalNodeBase<NodeType> | undefined) => InternalNodeBase<NodeType> | undefined),
|
|
94
|
+
) => void
|
|
95
|
+
}
|
|
96
|
+
const nodeSignals = new Map<string, NodeSignalSlot>()
|
|
97
|
+
|
|
98
|
+
// Slots are retained across remove → re-add so a consumer that
|
|
99
|
+
// called `nodeSignal(id)` before the node was added (or after it was
|
|
100
|
+
// removed) keeps subscribing to the same slot and is notified when
|
|
101
|
+
// the node appears.
|
|
102
|
+
function getOrCreateNodeSlot(id: string): NodeSignalSlot {
|
|
103
|
+
let slot = nodeSignals.get(id)
|
|
104
|
+
if (!slot) {
|
|
105
|
+
const [get, set] = createSignal<InternalNodeBase<NodeType> | undefined>(undefined)
|
|
106
|
+
slot = { get, set: set as NodeSignalSlot['set'] }
|
|
107
|
+
nodeSignals.set(id, slot)
|
|
108
|
+
}
|
|
109
|
+
return slot
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function nodeSignal(id: string): InternalNodeBase<NodeType> | undefined {
|
|
113
|
+
return getOrCreateNodeSlot(id).get()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Lightweight counter for notifying position-dependent subscribers (edges,
|
|
117
|
+
// per-node position effects) without triggering the full adoptUserNodes
|
|
118
|
+
// pipeline. Bumped by the drag handler after mutating nodeLookup in-place.
|
|
119
|
+
const [positionEpoch, setPositionEpoch] = createSignal(0)
|
|
120
|
+
|
|
121
|
+
// --- Derived state ---
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Process user nodes through @xyflow/system's adoptUserNodes.
|
|
125
|
+
* This populates nodeLookup/parentLookup and calculates internals.
|
|
126
|
+
* Returns whether all nodes have measured dimensions.
|
|
127
|
+
*/
|
|
128
|
+
const nodesInitialized = createMemo(() => {
|
|
129
|
+
const currentNodes = nodes()
|
|
130
|
+
const lookup = untrack(nodeLookup)
|
|
131
|
+
const parents = untrack(parentLookup)
|
|
132
|
+
|
|
133
|
+
// Preserve measured dimensions from existing internal nodes.
|
|
134
|
+
// adoptUserNodes reads userNode.measured but setNodes callers
|
|
135
|
+
// may not include it. Inject from existing lookup before rebuild.
|
|
136
|
+
for (const userNode of currentNodes) {
|
|
137
|
+
if (userNode.measured?.width) continue
|
|
138
|
+
const existing = lookup.get(userNode.id)
|
|
139
|
+
if (existing?.measured.width) {
|
|
140
|
+
;(userNode as NodeBase).measured = {
|
|
141
|
+
width: existing.measured.width,
|
|
142
|
+
height: existing.measured.height,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const result = adoptUserNodes(currentNodes, lookup, parents, {
|
|
148
|
+
nodeOrigin,
|
|
149
|
+
nodeExtent,
|
|
150
|
+
checkEquality: false,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
updateAbsolutePositions(lookup, parents, {
|
|
154
|
+
nodeOrigin,
|
|
155
|
+
nodeExtent,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Reconcile the per-node signal cache with the freshly-rebuilt
|
|
159
|
+
// lookup. `adoptUserNodes(..., checkEquality: false)` constructs
|
|
160
|
+
// a new InternalNode wrapper for every entry on every call, so
|
|
161
|
+
// entry identity is **not** a reliable change signal. The
|
|
162
|
+
// underlying `internals.userNode` reference, however, only flips
|
|
163
|
+
// when `setNodes` actually produced a new node identity (callers
|
|
164
|
+
// use `prev.map(n => n.id === target ? { ...n, ... } : n)`), so
|
|
165
|
+
// we gate on that — sibling ids whose user node is unchanged stay
|
|
166
|
+
// silent. Removed ids see `undefined`.
|
|
167
|
+
for (const [id, entry] of lookup) {
|
|
168
|
+
const slot = getOrCreateNodeSlot(id)
|
|
169
|
+
const current = untrack(slot.get)
|
|
170
|
+
if (current?.internals.userNode !== entry.internals.userNode) {
|
|
171
|
+
slot.set(entry)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const [id, slot] of nodeSignals) {
|
|
175
|
+
if (!lookup.has(id) && untrack(slot.get) !== undefined) {
|
|
176
|
+
slot.set(undefined)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Emit fresh outer Map references so coarse consumers (iterators
|
|
181
|
+
// that walk every node) re-run. `adoptUserNodes` mutates the
|
|
182
|
+
// existing `Map` in place, so re-emitting the same reference
|
|
183
|
+
// would be a no-op under barefoot's Object.is dedupe (#1270).
|
|
184
|
+
setNodeLookup(() => new Map(lookup))
|
|
185
|
+
setParentLookup(() => new Map(parents))
|
|
186
|
+
|
|
187
|
+
return result.nodesInitialized
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// Process edges into lookup when edges change
|
|
191
|
+
createEffect(() => {
|
|
192
|
+
const currentEdges = edges()
|
|
193
|
+
const eLookup = new Map<string, EdgeType>()
|
|
194
|
+
for (const edge of currentEdges) {
|
|
195
|
+
eLookup.set(edge.id, edge)
|
|
196
|
+
}
|
|
197
|
+
setEdgeLookup(() => eLookup)
|
|
198
|
+
|
|
199
|
+
const connLookup = untrack(connectionLookup)
|
|
200
|
+
updateConnectionLookup(connLookup, eLookup, currentEdges)
|
|
201
|
+
// `updateConnectionLookup` mutates `connLookup` in place; emit a
|
|
202
|
+
// fresh `Map` reference so subscribers actually see the change.
|
|
203
|
+
setConnectionLookup(() => new Map(connLookup))
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// --- Selection state ---
|
|
207
|
+
const [multiSelectionActive, setMultiSelectionActive] = createSignal(false)
|
|
208
|
+
|
|
209
|
+
// --- Interactivity ---
|
|
210
|
+
const [nodesDraggable, setNodesDraggable] = createSignal(options.nodesDraggable ?? true)
|
|
211
|
+
const [nodesConnectable, setNodesConnectable] = createSignal(options.nodesConnectable ?? true)
|
|
212
|
+
const [elementsSelectable, setElementsSelectable] = createSignal(options.elementsSelectable ?? true)
|
|
213
|
+
|
|
214
|
+
// --- Pan/zoom config (reactive signals for dynamic changes) ---
|
|
215
|
+
const [panOnDrag, setPanOnDrag] = createSignal(options.panOnDrag ?? true)
|
|
216
|
+
const [panOnScroll, setPanOnScroll] = createSignal(options.panOnScroll ?? false)
|
|
217
|
+
const [zoomOnScroll, setZoomOnScroll] = createSignal(options.zoomOnScroll ?? true)
|
|
218
|
+
|
|
219
|
+
// --- Static config ---
|
|
220
|
+
const deleteKeyCode = options.deleteKeyCode !== undefined ? options.deleteKeyCode : ['Delete', 'Backspace']
|
|
221
|
+
const selectionKeyCode = options.selectionKeyCode !== undefined ? options.selectionKeyCode : 'Shift'
|
|
222
|
+
const connectionLineStyle = options.connectionLineStyle
|
|
223
|
+
const defaultEdgeOptions = options.defaultEdgeOptions
|
|
224
|
+
const elevateNodesOnSelect = options.elevateNodesOnSelect ?? false
|
|
225
|
+
const reconnectRadius = options.reconnectRadius ?? 20
|
|
226
|
+
const zoomOnDoubleClick = options.zoomOnDoubleClick ?? true
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Update pan/zoom instance configuration.
|
|
230
|
+
* Called by initFlow on setup and reactively when settings change.
|
|
231
|
+
*/
|
|
232
|
+
function updatePanZoomConfig() {
|
|
233
|
+
const pz = untrack(panZoom)
|
|
234
|
+
if (!pz) return
|
|
235
|
+
pz.update({
|
|
236
|
+
noWheelClassName: 'nowheel',
|
|
237
|
+
noPanClassName: 'nopan',
|
|
238
|
+
preventScrolling: true,
|
|
239
|
+
panOnScroll: panOnScroll(),
|
|
240
|
+
panOnDrag: panOnDrag(),
|
|
241
|
+
panOnScrollMode: 'free' as any,
|
|
242
|
+
panOnScrollSpeed: 0.5,
|
|
243
|
+
userSelectionActive: false,
|
|
244
|
+
zoomOnPinch: true,
|
|
245
|
+
zoomOnScroll: zoomOnScroll(),
|
|
246
|
+
zoomOnDoubleClick,
|
|
247
|
+
zoomActivationKeyPressed: false,
|
|
248
|
+
lib: 'bf',
|
|
249
|
+
onTransformChange: (transform: Transform) => {
|
|
250
|
+
setViewport({ x: transform[0], y: transform[1], zoom: transform[2] })
|
|
251
|
+
},
|
|
252
|
+
connectionInProgress: false,
|
|
253
|
+
paneClickDistance: 0,
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// --- Actions ---
|
|
258
|
+
|
|
259
|
+
function getTransform(): Transform {
|
|
260
|
+
const vp = untrack(viewport)
|
|
261
|
+
return [vp.x, vp.y, vp.zoom]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Bump the position epoch counter to notify position-dependent effects
|
|
266
|
+
* (edges, per-node position) without triggering adoptUserNodes.
|
|
267
|
+
*/
|
|
268
|
+
function triggerPositionUpdate(): void {
|
|
269
|
+
setPositionEpoch((n) => n + 1)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Update node positions during drag operations.
|
|
274
|
+
* Called by XYDrag with the current drag items.
|
|
275
|
+
*/
|
|
276
|
+
function updateNodePositions(
|
|
277
|
+
dragItems: Map<string, NodeDragItem | InternalNodeBase>,
|
|
278
|
+
isDragging = true,
|
|
279
|
+
) {
|
|
280
|
+
const lookup = untrack(nodeLookup)
|
|
281
|
+
let mutated = false
|
|
282
|
+
|
|
283
|
+
for (const [id, item] of dragItems) {
|
|
284
|
+
const internalNode = lookup.get(id)
|
|
285
|
+
if (!internalNode) continue
|
|
286
|
+
|
|
287
|
+
internalNode.internals.positionAbsolute = item.internals
|
|
288
|
+
? (item as InternalNodeBase).internals.positionAbsolute
|
|
289
|
+
: { x: (item as NodeDragItem).position.x, y: (item as NodeDragItem).position.y }
|
|
290
|
+
|
|
291
|
+
internalNode.internals.userNode.position = item.position
|
|
292
|
+
internalNode.internals.userNode.dragging = isDragging
|
|
293
|
+
|
|
294
|
+
// Per-node fine-grained emit: shallow-clone so the entry identity
|
|
295
|
+
// flips (barefoot's Object.is dedupe would otherwise swallow the
|
|
296
|
+
// notification — the mutations above don't change the entry
|
|
297
|
+
// reference). The inner `internals` object stays shared, so
|
|
298
|
+
// consumers reading `.internals.positionAbsolute` still see the
|
|
299
|
+
// updated position.
|
|
300
|
+
const fresh = { ...internalNode } as InternalNodeBase<NodeType>
|
|
301
|
+
lookup.set(id, fresh)
|
|
302
|
+
getOrCreateNodeSlot(id).set(fresh)
|
|
303
|
+
mutated = true
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (mutated) {
|
|
307
|
+
// Coarse emit so iteration-style consumers re-run too. Per-node
|
|
308
|
+
// bridges that switched to `nodeSignal(id)` only wake up for
|
|
309
|
+
// their own id and skip this channel.
|
|
310
|
+
setNodeLookup(() => new Map(lookup))
|
|
311
|
+
}
|
|
312
|
+
// Keep positionEpoch as the dedicated drag-rate channel for
|
|
313
|
+
// existing subscribers (edge-path effect).
|
|
314
|
+
triggerPositionUpdate()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Deselect all nodes and edges, or specific ones.
|
|
319
|
+
*/
|
|
320
|
+
function unselectNodesAndEdges(params?: {
|
|
321
|
+
nodes?: NodeBase[]
|
|
322
|
+
edges?: EdgeBase[]
|
|
323
|
+
}) {
|
|
324
|
+
const currentNodes = untrack(nodes)
|
|
325
|
+
const currentEdges = untrack(edges)
|
|
326
|
+
|
|
327
|
+
if (params?.nodes) {
|
|
328
|
+
const idsToDeselect = new Set(params.nodes.map((n) => n.id))
|
|
329
|
+
setNodes(
|
|
330
|
+
currentNodes.map((n) =>
|
|
331
|
+
idsToDeselect.has(n.id) ? { ...n, selected: false } : n,
|
|
332
|
+
) as NodeType[],
|
|
333
|
+
)
|
|
334
|
+
} else {
|
|
335
|
+
setNodes(
|
|
336
|
+
currentNodes.map((n) =>
|
|
337
|
+
n.selected ? { ...n, selected: false } : n,
|
|
338
|
+
) as NodeType[],
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (params?.edges) {
|
|
343
|
+
const idsToDeselect = new Set(params.edges.map((e) => e.id))
|
|
344
|
+
setEdges(
|
|
345
|
+
currentEdges.map((e) =>
|
|
346
|
+
idsToDeselect.has(e.id) ? { ...e, selected: false } : e,
|
|
347
|
+
) as EdgeType[],
|
|
348
|
+
)
|
|
349
|
+
} else {
|
|
350
|
+
setEdges(
|
|
351
|
+
currentEdges.map((e) =>
|
|
352
|
+
e.selected ? { ...e, selected: false } : e,
|
|
353
|
+
) as EdgeType[],
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Pan the viewport by a delta amount.
|
|
360
|
+
*/
|
|
361
|
+
async function panByDelta(delta: XYPosition): Promise<boolean> {
|
|
362
|
+
return panByUtil({
|
|
363
|
+
delta,
|
|
364
|
+
panZoom: untrack(panZoom),
|
|
365
|
+
transform: getTransform(),
|
|
366
|
+
translateExtent: INFINITE_EXTENT,
|
|
367
|
+
width: untrack(width),
|
|
368
|
+
height: untrack(height),
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Add a new edge to the store.
|
|
374
|
+
*/
|
|
375
|
+
function addEdge(edge: EdgeType) {
|
|
376
|
+
setEdges((prev) => [...prev, edge])
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Delete nodes and edges from the store.
|
|
381
|
+
*/
|
|
382
|
+
function deleteElements(params: {
|
|
383
|
+
nodes?: NodeType[]
|
|
384
|
+
edges?: EdgeType[]
|
|
385
|
+
}) {
|
|
386
|
+
if (params.nodes?.length) {
|
|
387
|
+
const idsToRemove = new Set(params.nodes.map((n) => n.id))
|
|
388
|
+
setNodes((prev) => prev.filter((n) => !idsToRemove.has(n.id)))
|
|
389
|
+
// Also remove connected edges
|
|
390
|
+
setEdges((prev) =>
|
|
391
|
+
prev.filter(
|
|
392
|
+
(e) => !idsToRemove.has(e.source) && !idsToRemove.has(e.target),
|
|
393
|
+
),
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
if (params.edges?.length) {
|
|
397
|
+
const idsToRemove = new Set(params.edges.map((e) => e.id))
|
|
398
|
+
setEdges((prev) => prev.filter((e) => !idsToRemove.has(e.id)))
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function fitView(fitViewOptions?: FitViewOptions) {
|
|
403
|
+
const pz = untrack(panZoom)
|
|
404
|
+
if (!pz) return
|
|
405
|
+
|
|
406
|
+
const lookup = untrack(nodeLookup)
|
|
407
|
+
const w = untrack(width)
|
|
408
|
+
const h = untrack(height)
|
|
409
|
+
|
|
410
|
+
fitViewport(
|
|
411
|
+
{
|
|
412
|
+
nodes: lookup,
|
|
413
|
+
width: w,
|
|
414
|
+
height: h,
|
|
415
|
+
panZoom: pz,
|
|
416
|
+
minZoom,
|
|
417
|
+
maxZoom,
|
|
418
|
+
},
|
|
419
|
+
{ padding: 0.1, ...fitViewOptions }
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
// Signal getters
|
|
425
|
+
nodes,
|
|
426
|
+
edges,
|
|
427
|
+
viewport,
|
|
428
|
+
width,
|
|
429
|
+
height,
|
|
430
|
+
dragging,
|
|
431
|
+
nodesInitialized,
|
|
432
|
+
|
|
433
|
+
// Lookups
|
|
434
|
+
nodeLookup,
|
|
435
|
+
parentLookup,
|
|
436
|
+
edgeLookup,
|
|
437
|
+
connectionLookup,
|
|
438
|
+
nodeSignal,
|
|
439
|
+
|
|
440
|
+
// Internal refs
|
|
441
|
+
panZoom,
|
|
442
|
+
domNode,
|
|
443
|
+
|
|
444
|
+
// Setters
|
|
445
|
+
setNodes,
|
|
446
|
+
setEdges,
|
|
447
|
+
setViewport,
|
|
448
|
+
setWidth,
|
|
449
|
+
setHeight,
|
|
450
|
+
|
|
451
|
+
// Selection state
|
|
452
|
+
multiSelectionActive,
|
|
453
|
+
|
|
454
|
+
// Interactivity
|
|
455
|
+
nodesDraggable,
|
|
456
|
+
setNodesDraggable,
|
|
457
|
+
nodesConnectable,
|
|
458
|
+
setNodesConnectable,
|
|
459
|
+
elementsSelectable,
|
|
460
|
+
setElementsSelectable,
|
|
461
|
+
panOnDrag,
|
|
462
|
+
setPanOnDrag,
|
|
463
|
+
panOnScroll,
|
|
464
|
+
setPanOnScroll,
|
|
465
|
+
zoomOnScroll,
|
|
466
|
+
setZoomOnScroll,
|
|
467
|
+
|
|
468
|
+
// Static config
|
|
469
|
+
deleteKeyCode,
|
|
470
|
+
selectionKeyCode,
|
|
471
|
+
connectionLineStyle,
|
|
472
|
+
defaultEdgeOptions,
|
|
473
|
+
elevateNodesOnSelect,
|
|
474
|
+
reconnectRadius,
|
|
475
|
+
|
|
476
|
+
// Lightweight position change notification (avoids full adoptUserNodes)
|
|
477
|
+
positionEpoch,
|
|
478
|
+
triggerPositionUpdate,
|
|
479
|
+
|
|
480
|
+
setDragging,
|
|
481
|
+
setPanZoom,
|
|
482
|
+
setDomNode,
|
|
483
|
+
setMultiSelectionActive,
|
|
484
|
+
updatePanZoomConfig,
|
|
485
|
+
|
|
486
|
+
// Actions
|
|
487
|
+
fitView,
|
|
488
|
+
updateNodePositions,
|
|
489
|
+
unselectNodesAndEdges,
|
|
490
|
+
panByDelta,
|
|
491
|
+
addEdge,
|
|
492
|
+
deleteElements,
|
|
493
|
+
|
|
494
|
+
// Configuration
|
|
495
|
+
minZoom,
|
|
496
|
+
maxZoom,
|
|
497
|
+
nodeOrigin,
|
|
498
|
+
nodeExtent,
|
|
499
|
+
snapToGrid,
|
|
500
|
+
snapGrid,
|
|
501
|
+
edgesReconnectable,
|
|
502
|
+
|
|
503
|
+
getTransform,
|
|
504
|
+
|
|
505
|
+
// Custom types
|
|
506
|
+
nodeTypes: options.nodeTypes,
|
|
507
|
+
edgeTypes: options.edgeTypes,
|
|
508
|
+
|
|
509
|
+
// Connection callbacks
|
|
510
|
+
onConnect: options.onConnect,
|
|
511
|
+
onConnectStart: options.onConnectStart,
|
|
512
|
+
onConnectEnd: options.onConnectEnd,
|
|
513
|
+
isValidConnection: options.isValidConnection,
|
|
514
|
+
onReconnect: options.onReconnect,
|
|
515
|
+
|
|
516
|
+
// Lifecycle callbacks
|
|
517
|
+
onInit: options.onInit,
|
|
518
|
+
onNodeDragStart: options.onNodeDragStart,
|
|
519
|
+
onNodeDragStop: options.onNodeDragStop,
|
|
520
|
+
onMoveEnd: options.onMoveEnd,
|
|
521
|
+
onPaneClick: options.onPaneClick,
|
|
522
|
+
onPaneMouseMove: options.onPaneMouseMove,
|
|
523
|
+
onNodesDelete: options.onNodesDelete,
|
|
524
|
+
onEdgesDelete: options.onEdgesDelete,
|
|
525
|
+
}
|
|
526
|
+
}
|