@statelyai/flow-react 0.6.0 → 0.7.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 +51 -8
- package/dist/esm/backgrounds.d.ts +13 -7
- package/dist/esm/backgrounds.js +1 -95
- package/dist/esm/backgrounds2.js +124 -0
- package/dist/esm/index.d.ts +206 -36
- package/dist/esm/index.js +1109 -221
- package/package.json +16 -12
- package/dist/esm/FlowInstanceContext.js +0 -20
package/dist/esm/index.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import { n as useFlowInstance, r as useOptionalFlowInstance, t as
|
|
2
|
-
import {
|
|
1
|
+
import { a as FlowInstanceProvider, i as LinesBackground, n as CrossBackground, o as useFlowInstance, r as DotsBackground, s as useOptionalFlowInstance, t as Background } from "./backgrounds2.js";
|
|
2
|
+
import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, clientToCanvas, computeRenderableIds, createEntityGeometryObserver, createFlowView, createMeasurementObserver, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
|
|
3
|
+
import { CONNECTION_PREVIEW_EDGE_ID, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, nodeDepths, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
|
|
4
|
+
import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
+
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
|
|
7
|
+
import { createPortal } from "react-dom";
|
|
6
8
|
export * from "@statelyai/flow-dom";
|
|
7
9
|
//#region src/createFlowInstance.ts
|
|
8
10
|
function createFlowInstance(options) {
|
|
11
|
+
const { keymap, ...createFlowSettings } = options.settings ?? {};
|
|
9
12
|
const flow = createFlow({
|
|
10
|
-
graph: options.
|
|
11
|
-
...
|
|
13
|
+
graph: options.initialGraph,
|
|
14
|
+
...createFlowSettings
|
|
12
15
|
});
|
|
16
|
+
const inputSettings = { keymap };
|
|
13
17
|
const cleanup = /* @__PURE__ */ new Set();
|
|
14
18
|
function getSnapshot() {
|
|
15
|
-
return flow.
|
|
19
|
+
return flow.getSnapshot();
|
|
16
20
|
}
|
|
17
|
-
function clientToCanvas(point) {
|
|
18
|
-
|
|
19
|
-
return flow.screenToCanvas({
|
|
20
|
-
x: point.x - (rect?.left ?? 0),
|
|
21
|
-
y: point.y - (rect?.top ?? 0)
|
|
22
|
-
});
|
|
21
|
+
function clientToCanvas$1(point) {
|
|
22
|
+
return clientToCanvas(flow.internals.store, point);
|
|
23
23
|
}
|
|
24
24
|
function listen(selector, listener, listenerOptions = {}) {
|
|
25
25
|
const isEqual = listenerOptions.isEqual ?? Object.is;
|
|
26
26
|
let previousValue = selector(getSnapshot());
|
|
27
27
|
if (listenerOptions.fireImmediately) listener(previousValue, previousValue, getSnapshot());
|
|
28
|
-
const unsubscribe = flow.subscribe((
|
|
29
|
-
const nextValue = selector(context);
|
|
30
|
-
if (isEqual(previousValue, nextValue)) return;
|
|
31
|
-
const currentPreviousValue = previousValue;
|
|
28
|
+
const unsubscribe = flow.subscribe(selector, (nextValue, currentPreviousValue) => {
|
|
32
29
|
previousValue = nextValue;
|
|
33
|
-
listener(nextValue, currentPreviousValue,
|
|
34
|
-
});
|
|
30
|
+
listener(nextValue, currentPreviousValue, getSnapshot());
|
|
31
|
+
}, { equals: isEqual });
|
|
35
32
|
cleanup.add(unsubscribe);
|
|
36
33
|
return () => {
|
|
37
34
|
cleanup.delete(unsubscribe);
|
|
38
35
|
unsubscribe();
|
|
39
36
|
};
|
|
40
37
|
}
|
|
38
|
+
const configure = (next) => {
|
|
39
|
+
flow.configure(next);
|
|
40
|
+
};
|
|
41
41
|
function subscribeEvent(event, listener) {
|
|
42
|
-
const subscription = flow.on(event, listener);
|
|
42
|
+
const subscription = event === "commit" ? flow.on("commit", listener) : flow.on(event, listener);
|
|
43
43
|
const unsubscribe = () => subscription.unsubscribe();
|
|
44
44
|
cleanup.add(unsubscribe);
|
|
45
45
|
return () => {
|
|
@@ -49,12 +49,16 @@ function createFlowInstance(options) {
|
|
|
49
49
|
}
|
|
50
50
|
if (options.events?.connectionEnded) subscribeEvent("connectionEnded", options.events.connectionEnded);
|
|
51
51
|
if (options.events?.layoutRequested) subscribeEvent("layoutRequested", options.events.layoutRequested);
|
|
52
|
+
if (options.events?.commit) subscribeEvent("commit", options.events.commit);
|
|
53
|
+
if (options.onCommit) subscribeEvent("commit", options.onCommit);
|
|
52
54
|
for (const listener of options.listeners ?? []) listen(listener.selector, listener.onChange, listener);
|
|
53
55
|
return {
|
|
54
56
|
...flow,
|
|
55
57
|
getSnapshot,
|
|
58
|
+
configure,
|
|
56
59
|
listen,
|
|
57
|
-
clientToCanvas,
|
|
60
|
+
clientToCanvas: clientToCanvas$1,
|
|
61
|
+
inputSettings,
|
|
58
62
|
destroy() {
|
|
59
63
|
for (const unsubscribe of cleanup) unsubscribe();
|
|
60
64
|
cleanup.clear();
|
|
@@ -67,25 +71,19 @@ function createFlowInstance(options) {
|
|
|
67
71
|
function useFlow(options) {
|
|
68
72
|
return useCreateFlowInstance(options);
|
|
69
73
|
}
|
|
74
|
+
/** @deprecated Use `useFlow(options)`. */
|
|
70
75
|
function useCreateFlowInstance(options) {
|
|
71
76
|
const instanceRef = useRef(null);
|
|
72
77
|
const listenersRef = useRef(options.listeners);
|
|
73
78
|
const eventsRef = useRef(options.events);
|
|
74
|
-
const
|
|
79
|
+
const lifecycleRef = useRef(0);
|
|
75
80
|
listenersRef.current = options.listeners;
|
|
76
81
|
eventsRef.current = options.events;
|
|
77
82
|
if (!instanceRef.current) instanceRef.current = createFlowInstance({
|
|
78
|
-
|
|
83
|
+
initialGraph: options.initialGraph,
|
|
79
84
|
settings: options.settings
|
|
80
85
|
});
|
|
81
86
|
const flow = instanceRef.current;
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (!didMountRef.current) {
|
|
84
|
-
didMountRef.current = true;
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
flow.trigger.setGraph({ graph: options.graph });
|
|
88
|
-
}, [flow, options.graph]);
|
|
89
87
|
useEffect(() => {
|
|
90
88
|
const cleanup = [];
|
|
91
89
|
if (eventsRef.current?.connectionEnded) {
|
|
@@ -96,6 +94,14 @@ function useCreateFlowInstance(options) {
|
|
|
96
94
|
const subscription = flow.on("layoutRequested", eventsRef.current.layoutRequested);
|
|
97
95
|
cleanup.push(() => subscription.unsubscribe());
|
|
98
96
|
}
|
|
97
|
+
if (eventsRef.current?.commit) {
|
|
98
|
+
const subscription = flow.on("commit", eventsRef.current.commit);
|
|
99
|
+
cleanup.push(() => subscription.unsubscribe());
|
|
100
|
+
}
|
|
101
|
+
if (options.onCommit) {
|
|
102
|
+
const subscription = flow.on("commit", options.onCommit);
|
|
103
|
+
cleanup.push(() => subscription.unsubscribe());
|
|
104
|
+
}
|
|
99
105
|
for (const listener of listenersRef.current ?? []) cleanup.push(flow.listen(listener.selector, listener.onChange, listener));
|
|
100
106
|
return () => {
|
|
101
107
|
for (const dispose of cleanup) dispose();
|
|
@@ -103,40 +109,70 @@ function useCreateFlowInstance(options) {
|
|
|
103
109
|
}, [
|
|
104
110
|
flow,
|
|
105
111
|
options.listeners,
|
|
106
|
-
options.events
|
|
112
|
+
options.events,
|
|
113
|
+
options.onCommit
|
|
107
114
|
]);
|
|
108
|
-
useEffect(() =>
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const lifecycle = lifecycleRef;
|
|
117
|
+
const generation = ++lifecycle.current;
|
|
118
|
+
return () => {
|
|
119
|
+
queueMicrotask(() => {
|
|
120
|
+
if (lifecycle.current !== generation) return;
|
|
121
|
+
lifecycle.current++;
|
|
122
|
+
flow.destroy();
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
}, [flow]);
|
|
109
126
|
return flow;
|
|
110
127
|
}
|
|
111
128
|
function useFlowSelector(selector, options = {}) {
|
|
112
129
|
const contextFlow = useOptionalFlowInstance();
|
|
113
130
|
const flow = options.flow ?? contextFlow;
|
|
114
131
|
if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
|
|
115
|
-
|
|
116
|
-
const isEqualRef = useRef(options.isEqual ?? Object.is);
|
|
117
|
-
selectorRef.current = selector;
|
|
118
|
-
isEqualRef.current = options.isEqual ?? Object.is;
|
|
119
|
-
const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
setSelected((previous) => {
|
|
122
|
-
const next = selectorRef.current(flow.getSnapshot());
|
|
123
|
-
return isEqualRef.current(previous, next) ? previous : next;
|
|
124
|
-
});
|
|
125
|
-
return flow.listen((context) => selectorRef.current(context), (value) => {
|
|
126
|
-
setSelected((previous) => isEqualRef.current(previous, value) ? previous : value);
|
|
127
|
-
});
|
|
128
|
-
}, [flow]);
|
|
129
|
-
return selected;
|
|
132
|
+
return useSyncExternalStoreWithSelector(useCallback((listener) => flow.subscribe(() => listener()), [flow]), flow.getSnapshot, flow.getSnapshot, selector, options.isEqual);
|
|
130
133
|
}
|
|
131
134
|
function useFlowContext() {
|
|
132
135
|
return useFlowSelector((context) => context);
|
|
133
136
|
}
|
|
137
|
+
function useConnection() {
|
|
138
|
+
return useFlowSelector((context) => context.connection);
|
|
139
|
+
}
|
|
140
|
+
function useFlowConnections(input) {
|
|
141
|
+
const flow = useFlowInstance();
|
|
142
|
+
return useFlowSelector(() => flow.connections.get(input), { isEqual: (current, next) => current.length === next.length && current.every((connection, index) => {
|
|
143
|
+
const candidate = next[index];
|
|
144
|
+
return connection.edge === candidate.edge && connection.source.nodeId === candidate.source.nodeId && connection.source.port === candidate.source.port && connection.target.nodeId === candidate.target.nodeId && connection.target.port === candidate.target.port;
|
|
145
|
+
}) });
|
|
146
|
+
}
|
|
147
|
+
function useNodesInitialized() {
|
|
148
|
+
return useFlowSelector(areNodesMeasured);
|
|
149
|
+
}
|
|
150
|
+
function useKeyPress(keys) {
|
|
151
|
+
const keySet = useMemo(() => new Set(Array.isArray(keys) ? keys : [keys]), [keys]);
|
|
152
|
+
const [pressed, setPressed] = useState(false);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
function update(event, value) {
|
|
155
|
+
const target = event.target;
|
|
156
|
+
if (target && (target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT")) return;
|
|
157
|
+
if (keySet.has(event.key)) setPressed(value);
|
|
158
|
+
}
|
|
159
|
+
const down = (event) => update(event, true);
|
|
160
|
+
const up = (event) => update(event, false);
|
|
161
|
+
window.addEventListener("keydown", down);
|
|
162
|
+
window.addEventListener("keyup", up);
|
|
163
|
+
return () => {
|
|
164
|
+
window.removeEventListener("keydown", down);
|
|
165
|
+
window.removeEventListener("keyup", up);
|
|
166
|
+
};
|
|
167
|
+
}, [keySet]);
|
|
168
|
+
return pressed;
|
|
169
|
+
}
|
|
134
170
|
const entityNotifiers = /* @__PURE__ */ new WeakMap();
|
|
135
171
|
function getEntityNotifier(flow) {
|
|
136
|
-
let notifier = entityNotifiers.get(flow.store);
|
|
172
|
+
let notifier = entityNotifiers.get(flow.internals.store);
|
|
137
173
|
if (!notifier) {
|
|
138
|
-
notifier = createEntityChangeNotifier(flow.store);
|
|
139
|
-
entityNotifiers.set(flow.store, notifier);
|
|
174
|
+
notifier = createEntityChangeNotifier(flow.internals.store, { getEdgeDependents: flow.internals.engine.getEdgeSourceDependents });
|
|
175
|
+
entityNotifiers.set(flow.internals.store, notifier);
|
|
140
176
|
}
|
|
141
177
|
return notifier;
|
|
142
178
|
}
|
|
@@ -152,15 +188,23 @@ function useFlowEntity(id, selector, options = {}) {
|
|
|
152
188
|
if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
|
|
153
189
|
const selectorRef = useRef(selector);
|
|
154
190
|
const isEqualRef = useRef(options.isEqual ?? Object.is);
|
|
191
|
+
const selectedRef = useRef(void 0);
|
|
192
|
+
const selectedInitializedRef = useRef(false);
|
|
155
193
|
selectorRef.current = selector;
|
|
156
194
|
isEqualRef.current = options.isEqual ?? Object.is;
|
|
157
|
-
const [selected, setSelected] = useState(() =>
|
|
195
|
+
const [selected, setSelected] = useState(() => {
|
|
196
|
+
const initial = selector(flow.getSnapshot());
|
|
197
|
+
selectedRef.current = initial;
|
|
198
|
+
selectedInitializedRef.current = true;
|
|
199
|
+
return initial;
|
|
200
|
+
});
|
|
158
201
|
useEffect(() => {
|
|
159
202
|
const update = () => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
203
|
+
const next = selectorRef.current(flow.getSnapshot());
|
|
204
|
+
if (selectedInitializedRef.current && isEqualRef.current(selectedRef.current, next)) return;
|
|
205
|
+
selectedRef.current = next;
|
|
206
|
+
selectedInitializedRef.current = true;
|
|
207
|
+
setSelected(next);
|
|
164
208
|
};
|
|
165
209
|
update();
|
|
166
210
|
return getEntityNotifier(flow).subscribe(id, update);
|
|
@@ -205,7 +249,7 @@ function useFlowEdge(id, selector, isEqual) {
|
|
|
205
249
|
function useFlowPort(id, selector, isEqual) {
|
|
206
250
|
const flow = useFlowInstance();
|
|
207
251
|
return useFlowSelector((context) => {
|
|
208
|
-
const addressable = flow?.
|
|
252
|
+
const addressable = flow?.getEntityRef(id);
|
|
209
253
|
if (!addressable || addressable.kind !== "port") throw new Error(`Flow port "${id}" was not found.`);
|
|
210
254
|
const binding = {
|
|
211
255
|
...addressable.port,
|
|
@@ -244,24 +288,128 @@ function getDerivedFields(context, id, props) {
|
|
|
244
288
|
* and applied to the flow store. This replaces hand-written gesture handlers.
|
|
245
289
|
*/
|
|
246
290
|
function useFlowInput(flow, containerRef, options = {}) {
|
|
247
|
-
const
|
|
248
|
-
|
|
291
|
+
const { modes, profile, keyboardTarget, keymap } = options;
|
|
292
|
+
const effectiveKeymap = keymap ?? flow.inputSettings?.keymap;
|
|
293
|
+
const keymapKey = effectiveKeymap ? JSON.stringify(effectiveKeymap) : void 0;
|
|
294
|
+
const keymapRef = useRef(effectiveKeymap);
|
|
295
|
+
keymapRef.current = effectiveKeymap;
|
|
249
296
|
useEffect(() => {
|
|
250
297
|
const container = containerRef.current;
|
|
251
298
|
if (!container) return;
|
|
252
|
-
const
|
|
299
|
+
const activeKeymap = keymapRef.current;
|
|
253
300
|
const runtime = createFlowInputRuntime({
|
|
254
|
-
store: flow.store,
|
|
255
|
-
engine: flow.engine,
|
|
256
|
-
modes: modes ?? (profile ? profileModesForContext(profile) : void 0)
|
|
301
|
+
store: flow.internals.store,
|
|
302
|
+
engine: flow.internals.engine,
|
|
303
|
+
modes: modes ?? (profile ? profileModesForContext(profile) : void 0),
|
|
304
|
+
policies: profile ? profilePoliciesForContext(profile) : void 0,
|
|
305
|
+
...activeKeymap ? { keymap: activeKeymap } : {}
|
|
257
306
|
});
|
|
258
307
|
return attachInput({
|
|
259
308
|
container,
|
|
260
|
-
store: flow.store,
|
|
309
|
+
store: flow.internals.store,
|
|
261
310
|
runtime,
|
|
262
|
-
|
|
311
|
+
keyboardTarget,
|
|
312
|
+
autoPan: (context, screenPoint) => getAutoPanDelta(context, screenPoint, flow.internals.engine)
|
|
263
313
|
});
|
|
264
|
-
}, [
|
|
314
|
+
}, [
|
|
315
|
+
flow,
|
|
316
|
+
containerRef,
|
|
317
|
+
modes,
|
|
318
|
+
profile,
|
|
319
|
+
keyboardTarget,
|
|
320
|
+
keymapKey
|
|
321
|
+
]);
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/MeasurementContext.tsx
|
|
325
|
+
const MeasurementContext = createContext(null);
|
|
326
|
+
function MeasurementProvider(props) {
|
|
327
|
+
return /* @__PURE__ */ jsx(MeasurementContext.Provider, {
|
|
328
|
+
value: props.observer,
|
|
329
|
+
children: props.children
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function useMeasurementObserver() {
|
|
333
|
+
const observer = useContext(MeasurementContext);
|
|
334
|
+
if (!observer) throw new Error("Measurement observer is missing.");
|
|
335
|
+
return observer;
|
|
336
|
+
}
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/useFlowView.tsx
|
|
339
|
+
const FlowViewContext = createContext(null);
|
|
340
|
+
function FlowViewProvider(props) {
|
|
341
|
+
const measurements = useMeasurementObserver();
|
|
342
|
+
const view = useMemo(() => ({
|
|
343
|
+
exportImage: (options) => {
|
|
344
|
+
const container = props.containerRef.current;
|
|
345
|
+
if (!container) return Promise.reject(/* @__PURE__ */ new Error("Flow view is not mounted."));
|
|
346
|
+
return createFlowView(container, { measurements }).exportImage(options);
|
|
347
|
+
},
|
|
348
|
+
refreshMeasurements: (targets) => measurements.refresh(targets)
|
|
349
|
+
}), [measurements, props.containerRef]);
|
|
350
|
+
return /* @__PURE__ */ jsx(FlowViewContext.Provider, {
|
|
351
|
+
value: view,
|
|
352
|
+
children: props.children
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function useFlowView() {
|
|
356
|
+
const view = useContext(FlowViewContext);
|
|
357
|
+
if (!view) throw new Error("useFlowView() must be used within <StatelyFlow>.");
|
|
358
|
+
return view;
|
|
359
|
+
}
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/baseStyles.ts
|
|
362
|
+
/**
|
|
363
|
+
* Minimal base stylesheet for the flow canvas. Intentionally tiny and
|
|
364
|
+
* inert-by-default: it only styles elements that opt in via `data-flow-*`
|
|
365
|
+
* attributes, and every visual value is a namespaced `--flow-*` CSS variable
|
|
366
|
+
* so consumers can theme it (set the variable on the canvas root or any
|
|
367
|
+
* ancestor) without overriding rules.
|
|
368
|
+
*
|
|
369
|
+
* Attributes:
|
|
370
|
+
* - `data-flow-interactive-when-selected` — the element ignores all pointer /
|
|
371
|
+
* wheel input (so canvas pan/zoom and box-select pass straight through it,
|
|
372
|
+
* e.g. over an embedded `<iframe>`) until its node/edge is selected, at which
|
|
373
|
+
* point it becomes fully interactive. Mark the embedded content, not the
|
|
374
|
+
* whole node, so ports and the selecting click still work. Don't also set an
|
|
375
|
+
* inline `pointer-events` on the same element — inline wins over this rule.
|
|
376
|
+
* - `data-flow-selection-box` — the box-select marquee (styled here; geometry
|
|
377
|
+
* is set inline by the renderer).
|
|
378
|
+
*/
|
|
379
|
+
const FLOW_BASE_STYLES_ID = "flow-base-styles";
|
|
380
|
+
const FLOW_BASE_CSS = `
|
|
381
|
+
[data-flow-root] {
|
|
382
|
+
--flow-selection-color: #2563eb;
|
|
383
|
+
--flow-selection-bg: rgba(37, 99, 235, 0.12);
|
|
384
|
+
touch-action: none;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
[data-flow-interactive-when-selected] {
|
|
388
|
+
pointer-events: none;
|
|
389
|
+
}
|
|
390
|
+
[data-flow-selected="true"] [data-flow-interactive-when-selected] {
|
|
391
|
+
pointer-events: auto;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
[data-flow-selection-box] {
|
|
395
|
+
position: absolute;
|
|
396
|
+
box-sizing: border-box;
|
|
397
|
+
border: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);
|
|
398
|
+
border-radius: calc(2px / var(--flow-zoom, 1));
|
|
399
|
+
background: var(--flow-selection-bg);
|
|
400
|
+
pointer-events: none;
|
|
401
|
+
}
|
|
402
|
+
`;
|
|
403
|
+
/** Inject the base stylesheet once per document (deduped by element id). */
|
|
404
|
+
function useFlowBaseStyles() {
|
|
405
|
+
useLayoutEffect(() => {
|
|
406
|
+
if (typeof document === "undefined") return;
|
|
407
|
+
if (document.getElementById("flow-base-styles")) return;
|
|
408
|
+
const style = document.createElement("style");
|
|
409
|
+
style.id = FLOW_BASE_STYLES_ID;
|
|
410
|
+
style.textContent = FLOW_BASE_CSS;
|
|
411
|
+
document.head.appendChild(style);
|
|
412
|
+
}, []);
|
|
265
413
|
}
|
|
266
414
|
//#endregion
|
|
267
415
|
//#region src/NodeContext.tsx
|
|
@@ -273,20 +421,56 @@ function useNodeRenderContext() {
|
|
|
273
421
|
return context;
|
|
274
422
|
}
|
|
275
423
|
//#endregion
|
|
424
|
+
//#region src/LayerPortals.tsx
|
|
425
|
+
const FlowLayerTargetsContext = createContext(null);
|
|
426
|
+
function FlowLayerTargetsProvider(props) {
|
|
427
|
+
return /* @__PURE__ */ jsx(FlowLayerTargetsContext.Provider, {
|
|
428
|
+
value: props.targets,
|
|
429
|
+
children: props.children
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function useFlowLayerTargets() {
|
|
433
|
+
const targets = useContext(FlowLayerTargetsContext);
|
|
434
|
+
if (!targets) throw new Error("Flow layer portals must be rendered within <StatelyFlow>.");
|
|
435
|
+
return targets;
|
|
436
|
+
}
|
|
437
|
+
/** Render canvas-coordinate content in the transformed viewport layer. */
|
|
438
|
+
function ViewportPortal(props) {
|
|
439
|
+
const target = useFlowLayerTargets().viewport;
|
|
440
|
+
return target ? createPortal(props.children, target) : null;
|
|
441
|
+
}
|
|
442
|
+
/** Render screen-coordinate content in the untransformed pane layer. */
|
|
443
|
+
function PanePortal(props) {
|
|
444
|
+
const target = useFlowLayerTargets().pane;
|
|
445
|
+
return target ? createPortal(props.children, target) : null;
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
276
448
|
//#region src/StatelyFlow.tsx
|
|
277
449
|
function stringArraysEqual(a, b) {
|
|
278
450
|
if (a.length !== b.length) return false;
|
|
279
451
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
280
452
|
return true;
|
|
281
453
|
}
|
|
454
|
+
function nodePaintEntriesEqual(a, b) {
|
|
455
|
+
if (a.length !== b.length) return false;
|
|
456
|
+
for (let index = 0; index < a.length; index++) if (a[index]?.id !== b[index]?.id || a[index]?.orderIndex !== b[index]?.orderIndex) return false;
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
function classNames$1(...values) {
|
|
460
|
+
return values.filter(Boolean).join(" ");
|
|
461
|
+
}
|
|
462
|
+
function assignRef(ref, value) {
|
|
463
|
+
if (typeof ref === "function") ref(value);
|
|
464
|
+
else if (ref) ref.current = value;
|
|
465
|
+
}
|
|
282
466
|
function useRenderableIds(instance, enabled) {
|
|
283
|
-
return useFlowSelector((context) => computeRenderableIds(instance.engine, context, enabled), { isEqual: renderableIdsEqual });
|
|
467
|
+
return useFlowSelector((context) => computeRenderableIds(instance.internals.engine, context, enabled), { isEqual: renderableIdsEqual });
|
|
284
468
|
}
|
|
285
469
|
function useViewportTransform(ref, instance) {
|
|
286
470
|
useLayoutEffect(() => {
|
|
287
471
|
const el = ref.current;
|
|
288
472
|
if (!el) return;
|
|
289
|
-
return attachViewportTransform(instance.store, el);
|
|
473
|
+
return attachViewportTransform(instance.internals.store, el);
|
|
290
474
|
}, [ref, instance]);
|
|
291
475
|
}
|
|
292
476
|
function StatelyFlow(props) {
|
|
@@ -297,11 +481,12 @@ function StatelyFlow(props) {
|
|
|
297
481
|
return /* @__PURE__ */ jsx(OwnedStatelyFlow, { ...props });
|
|
298
482
|
}
|
|
299
483
|
function OwnedStatelyFlow(props) {
|
|
300
|
-
const flow =
|
|
301
|
-
|
|
484
|
+
const flow = useFlow({
|
|
485
|
+
initialGraph: props.initialGraph,
|
|
302
486
|
settings: props.settings,
|
|
303
487
|
listeners: props.listeners,
|
|
304
|
-
events: props.events
|
|
488
|
+
events: props.events,
|
|
489
|
+
onCommit: props.onCommit
|
|
305
490
|
});
|
|
306
491
|
return /* @__PURE__ */ jsx(StatelyFlowView, {
|
|
307
492
|
...props,
|
|
@@ -310,61 +495,155 @@ function OwnedStatelyFlow(props) {
|
|
|
310
495
|
}
|
|
311
496
|
function StatelyFlowView(props) {
|
|
312
497
|
const containerRef = useRef(null);
|
|
498
|
+
const setContainerRef = useCallback((element) => {
|
|
499
|
+
containerRef.current = element;
|
|
500
|
+
assignRef(props.rootRef, element);
|
|
501
|
+
}, [props.rootRef]);
|
|
313
502
|
const viewportRef = useRef(null);
|
|
503
|
+
const [viewportLayer, setViewportLayer] = useState(null);
|
|
504
|
+
const [paneLayer, setPaneLayer] = useState(null);
|
|
505
|
+
const layerTargets = useMemo(() => ({
|
|
506
|
+
viewport: viewportLayer,
|
|
507
|
+
pane: paneLayer
|
|
508
|
+
}), [viewportLayer, paneLayer]);
|
|
509
|
+
const measurementObserver = useMemo(() => createMeasurementObserver(props.flow.internals.store, { container: () => containerRef.current }), [props.flow]);
|
|
510
|
+
const geometryObserver = useMemo(() => createEntityGeometryObserver(props.flow.internals.store, { getEdgeDependents: props.flow.internals.engine.getEdgeSourceDependents }), [props.flow]);
|
|
511
|
+
useLayoutEffect(() => () => {
|
|
512
|
+
measurementObserver.disconnect();
|
|
513
|
+
geometryObserver.disconnect();
|
|
514
|
+
}, [geometryObserver, measurementObserver]);
|
|
314
515
|
useViewportTransform(viewportRef, props.flow);
|
|
516
|
+
useFlowBaseStyles();
|
|
517
|
+
const projections = useActiveProjections(props.flow, props.projections);
|
|
518
|
+
const { className: rootClassName, style: rootStyle, tabIndex: rootTabIndex, ...rootProps } = props.rootProps ?? {};
|
|
315
519
|
useLayoutEffect(() => {
|
|
316
520
|
const element = containerRef.current;
|
|
317
521
|
if (!element) return;
|
|
318
|
-
return attachContainer(props.flow.store, element);
|
|
522
|
+
return attachContainer(props.flow.internals.store, element);
|
|
319
523
|
}, [props.flow]);
|
|
320
524
|
useFlowInput(props.flow, containerRef, {
|
|
321
525
|
modes: props.inputModes,
|
|
322
|
-
profile: props.settings?.store?.profile
|
|
526
|
+
profile: props.settings?.store?.profile,
|
|
527
|
+
keyboardTarget: props.keyboardTarget,
|
|
528
|
+
keymap: props.settings?.keymap
|
|
323
529
|
});
|
|
530
|
+
useEffect(() => {
|
|
531
|
+
props.flow.configure({
|
|
532
|
+
engine: props.settings?.engine,
|
|
533
|
+
store: props.settings?.store,
|
|
534
|
+
defaultEdgeOptions: props.settings?.defaultEdgeOptions,
|
|
535
|
+
createEdgeOnConnect: props.settings?.createEdgeOnConnect,
|
|
536
|
+
layout: props.settings?.layout,
|
|
537
|
+
historyLimit: props.settings?.store?.historyLimit
|
|
538
|
+
});
|
|
539
|
+
if (props.settings?.projections) props.flow.projections.set(props.settings.projections);
|
|
540
|
+
}, [
|
|
541
|
+
props.flow,
|
|
542
|
+
props.settings?.engine,
|
|
543
|
+
props.settings?.store,
|
|
544
|
+
props.settings?.defaultEdgeOptions,
|
|
545
|
+
props.settings?.createEdgeOnConnect,
|
|
546
|
+
props.settings?.layout,
|
|
547
|
+
props.settings?.projections
|
|
548
|
+
]);
|
|
324
549
|
return /* @__PURE__ */ jsx(FlowInstanceProvider, {
|
|
325
550
|
flow: props.flow,
|
|
326
|
-
children: /* @__PURE__ */
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
551
|
+
children: /* @__PURE__ */ jsx(MeasurementProvider, {
|
|
552
|
+
observer: measurementObserver,
|
|
553
|
+
children: /* @__PURE__ */ jsx(FlowViewProvider, {
|
|
554
|
+
containerRef,
|
|
555
|
+
children: /* @__PURE__ */ jsx(FlowLayerTargetsProvider, {
|
|
556
|
+
targets: layerTargets,
|
|
557
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
558
|
+
...rootProps,
|
|
559
|
+
ref: setContainerRef,
|
|
560
|
+
className: classNames$1("stately-flow", "stately-flow__pane", props.className, rootClassName),
|
|
561
|
+
"data-flow-root": "",
|
|
562
|
+
tabIndex: rootTabIndex ?? 0,
|
|
563
|
+
style: {
|
|
564
|
+
position: "relative",
|
|
565
|
+
overflow: "hidden",
|
|
566
|
+
width: "100%",
|
|
567
|
+
height: "100%",
|
|
568
|
+
minHeight: 320,
|
|
569
|
+
touchAction: "none",
|
|
570
|
+
...props.style,
|
|
571
|
+
...rootStyle
|
|
572
|
+
},
|
|
573
|
+
children: [
|
|
574
|
+
props.background,
|
|
575
|
+
/* @__PURE__ */ jsxs("div", {
|
|
576
|
+
ref: viewportRef,
|
|
577
|
+
className: "stately-flow__viewport",
|
|
578
|
+
"data-flow-viewport": true,
|
|
579
|
+
style: viewportStyle(),
|
|
580
|
+
children: [
|
|
581
|
+
/* @__PURE__ */ jsx(EdgeLayer, {
|
|
582
|
+
geometryObserver,
|
|
583
|
+
renderEdge: props.renderEdge,
|
|
584
|
+
renderConnectionLine: props.renderConnectionLine,
|
|
585
|
+
projections,
|
|
586
|
+
onlyRenderVisible: props.onlyRenderVisible
|
|
587
|
+
}),
|
|
588
|
+
/* @__PURE__ */ jsx(NodeLayer, {
|
|
589
|
+
geometryObserver,
|
|
590
|
+
renderNode: props.renderNode,
|
|
591
|
+
projections,
|
|
592
|
+
onlyRenderVisible: props.onlyRenderVisible
|
|
593
|
+
}),
|
|
594
|
+
/* @__PURE__ */ jsx(SnapLinesLayer, {}),
|
|
595
|
+
/* @__PURE__ */ jsx(EdgeLabelLayer, {
|
|
596
|
+
geometryObserver,
|
|
597
|
+
renderEdgeLabel: props.renderEdgeLabel,
|
|
598
|
+
projections,
|
|
599
|
+
onlyRenderVisible: props.onlyRenderVisible
|
|
600
|
+
}),
|
|
601
|
+
/* @__PURE__ */ jsx(SelectionBoxLayer, {}),
|
|
602
|
+
/* @__PURE__ */ jsx("div", {
|
|
603
|
+
ref: setViewportLayer,
|
|
604
|
+
className: "stately-flow__viewport-layer",
|
|
605
|
+
"data-flow-canvas-layer": "",
|
|
606
|
+
"data-flow-layer": "viewport",
|
|
607
|
+
style: {
|
|
608
|
+
position: "absolute",
|
|
609
|
+
inset: 0,
|
|
610
|
+
pointerEvents: "none"
|
|
611
|
+
},
|
|
612
|
+
children: props.layers?.viewport
|
|
613
|
+
})
|
|
614
|
+
]
|
|
615
|
+
}),
|
|
616
|
+
/* @__PURE__ */ jsx("div", {
|
|
617
|
+
ref: setPaneLayer,
|
|
618
|
+
className: "stately-flow__pane-layer",
|
|
619
|
+
"data-flow-canvas-layer": "",
|
|
620
|
+
"data-flow-layer": "pane",
|
|
621
|
+
style: {
|
|
622
|
+
position: "absolute",
|
|
623
|
+
inset: 0,
|
|
624
|
+
pointerEvents: "none"
|
|
625
|
+
},
|
|
626
|
+
children: props.layers?.pane
|
|
627
|
+
}),
|
|
628
|
+
/* @__PURE__ */ jsx("div", {
|
|
629
|
+
className: "stately-flow__overlays",
|
|
630
|
+
"data-flow-overlay": true,
|
|
631
|
+
children: props.children
|
|
632
|
+
})
|
|
633
|
+
]
|
|
634
|
+
})
|
|
360
635
|
})
|
|
361
|
-
|
|
636
|
+
})
|
|
362
637
|
})
|
|
363
638
|
});
|
|
364
639
|
}
|
|
640
|
+
function useActiveProjections(flow, projections) {
|
|
641
|
+
const flowProjections = useSyncExternalStore(flow.projections.subscribe, flow.projections.get, flow.projections.get);
|
|
642
|
+
return projections ?? flowProjections;
|
|
643
|
+
}
|
|
365
644
|
function SnapLinesLayer() {
|
|
366
645
|
const instance = useFlowInstance();
|
|
367
|
-
const snapResult = useFlowSelector(() => instance.engine.getSnapResult());
|
|
646
|
+
const snapResult = useFlowSelector(() => instance.internals.engine.getSnapResult());
|
|
368
647
|
const viewport = useFlowSelector((context) => context.viewport);
|
|
369
648
|
const viewportSize = useFlowSelector((context) => context.viewportSize);
|
|
370
649
|
if (!snapResult) return null;
|
|
@@ -374,11 +653,16 @@ function SnapLinesLayer() {
|
|
|
374
653
|
width: viewportSize.width / viewport.zoom,
|
|
375
654
|
height: viewportSize.height / viewport.zoom
|
|
376
655
|
};
|
|
377
|
-
const strokeWidth = 1
|
|
656
|
+
const strokeWidth = 1 / viewport.zoom;
|
|
378
657
|
const capSize = 4 / viewport.zoom;
|
|
658
|
+
const fontSize = 10 / viewport.zoom;
|
|
659
|
+
const labelOffset = 6 / viewport.zoom;
|
|
660
|
+
const snapColor = "var(--flow-snap-line-color, #ef4444)";
|
|
661
|
+
const spacingColor = "var(--flow-spacing-guide-color, var(--flow-snap-line-color, #ef4444))";
|
|
379
662
|
return /* @__PURE__ */ jsx("svg", {
|
|
380
663
|
"aria-hidden": "true",
|
|
381
664
|
"data-flow-canvas-layer": true,
|
|
665
|
+
"data-flow-snap-guides": "",
|
|
382
666
|
style: {
|
|
383
667
|
position: "absolute",
|
|
384
668
|
inset: 0,
|
|
@@ -388,89 +672,128 @@ function SnapLinesLayer() {
|
|
|
388
672
|
pointerEvents: "none"
|
|
389
673
|
},
|
|
390
674
|
children: /* @__PURE__ */ jsxs("g", { children: [snapResult.snapLines.map((line, index) => line.axis === "x" ? /* @__PURE__ */ jsx("line", {
|
|
675
|
+
"data-flow-snap-line": "x",
|
|
391
676
|
x1: line.value,
|
|
392
677
|
y1: visibleBounds.y,
|
|
393
678
|
x2: line.value,
|
|
394
679
|
y2: visibleBounds.y + visibleBounds.height,
|
|
395
|
-
stroke:
|
|
680
|
+
stroke: snapColor,
|
|
396
681
|
strokeWidth,
|
|
397
|
-
strokeDasharray: `${
|
|
682
|
+
strokeDasharray: `${4 / viewport.zoom}`
|
|
398
683
|
}, `snap-x-${index}`) : /* @__PURE__ */ jsx("line", {
|
|
684
|
+
"data-flow-snap-line": "y",
|
|
399
685
|
x1: visibleBounds.x,
|
|
400
686
|
y1: line.value,
|
|
401
687
|
x2: visibleBounds.x + visibleBounds.width,
|
|
402
688
|
y2: line.value,
|
|
403
|
-
stroke:
|
|
689
|
+
stroke: snapColor,
|
|
404
690
|
strokeWidth,
|
|
405
|
-
strokeDasharray: `${
|
|
691
|
+
strokeDasharray: `${4 / viewport.zoom}`
|
|
406
692
|
}, `snap-y-${index}`)), snapResult.spacingGuides?.flatMap((guide, guideIndex) => guide.ranges.flatMap((range, rangeIndex) => {
|
|
407
693
|
const key = `spacing-${guideIndex}-${rangeIndex}`;
|
|
408
694
|
if (guide.axis === "x") return [
|
|
409
695
|
/* @__PURE__ */ jsx("line", {
|
|
696
|
+
"data-flow-spacing-guide": "x",
|
|
410
697
|
x1: range.start,
|
|
411
698
|
y1: guide.position,
|
|
412
699
|
x2: range.end,
|
|
413
700
|
y2: guide.position,
|
|
414
|
-
stroke:
|
|
701
|
+
stroke: spacingColor,
|
|
415
702
|
strokeWidth
|
|
416
703
|
}, `${key}-line`),
|
|
417
704
|
/* @__PURE__ */ jsx("line", {
|
|
418
705
|
x1: range.start,
|
|
419
|
-
y1: guide.position
|
|
706
|
+
y1: guide.position,
|
|
420
707
|
x2: range.start,
|
|
421
|
-
y2: guide.position
|
|
422
|
-
stroke:
|
|
423
|
-
strokeWidth,
|
|
708
|
+
y2: guide.position,
|
|
709
|
+
stroke: spacingColor,
|
|
710
|
+
strokeWidth: capSize,
|
|
424
711
|
strokeLinecap: "round"
|
|
425
712
|
}, `${key}-start`),
|
|
426
713
|
/* @__PURE__ */ jsx("line", {
|
|
427
714
|
x1: range.end,
|
|
428
|
-
y1: guide.position
|
|
715
|
+
y1: guide.position,
|
|
429
716
|
x2: range.end,
|
|
430
|
-
y2: guide.position
|
|
431
|
-
stroke:
|
|
432
|
-
strokeWidth,
|
|
717
|
+
y2: guide.position,
|
|
718
|
+
stroke: spacingColor,
|
|
719
|
+
strokeWidth: capSize,
|
|
433
720
|
strokeLinecap: "round"
|
|
434
|
-
}, `${key}-end`)
|
|
721
|
+
}, `${key}-end`),
|
|
722
|
+
/* @__PURE__ */ jsx("text", {
|
|
723
|
+
"data-flow-spacing-label": "x",
|
|
724
|
+
x: (range.start + range.end) / 2,
|
|
725
|
+
y: guide.position - labelOffset,
|
|
726
|
+
fontSize,
|
|
727
|
+
fill: spacingColor,
|
|
728
|
+
textAnchor: "middle",
|
|
729
|
+
children: Math.round(guide.spacing)
|
|
730
|
+
}, `${key}-label`)
|
|
435
731
|
];
|
|
436
732
|
return [
|
|
437
733
|
/* @__PURE__ */ jsx("line", {
|
|
734
|
+
"data-flow-spacing-guide": "y",
|
|
438
735
|
x1: guide.position,
|
|
439
736
|
y1: range.start,
|
|
440
737
|
x2: guide.position,
|
|
441
738
|
y2: range.end,
|
|
442
|
-
stroke:
|
|
739
|
+
stroke: spacingColor,
|
|
443
740
|
strokeWidth
|
|
444
741
|
}, `${key}-line`),
|
|
445
742
|
/* @__PURE__ */ jsx("line", {
|
|
446
|
-
x1: guide.position
|
|
743
|
+
x1: guide.position,
|
|
447
744
|
y1: range.start,
|
|
448
|
-
x2: guide.position
|
|
745
|
+
x2: guide.position,
|
|
449
746
|
y2: range.start,
|
|
450
|
-
stroke:
|
|
451
|
-
strokeWidth,
|
|
747
|
+
stroke: spacingColor,
|
|
748
|
+
strokeWidth: capSize,
|
|
452
749
|
strokeLinecap: "round"
|
|
453
750
|
}, `${key}-start`),
|
|
454
751
|
/* @__PURE__ */ jsx("line", {
|
|
455
|
-
x1: guide.position
|
|
752
|
+
x1: guide.position,
|
|
456
753
|
y1: range.end,
|
|
457
|
-
x2: guide.position
|
|
754
|
+
x2: guide.position,
|
|
458
755
|
y2: range.end,
|
|
459
|
-
stroke:
|
|
460
|
-
strokeWidth,
|
|
756
|
+
stroke: spacingColor,
|
|
757
|
+
strokeWidth: capSize,
|
|
461
758
|
strokeLinecap: "round"
|
|
462
|
-
}, `${key}-end`)
|
|
759
|
+
}, `${key}-end`),
|
|
760
|
+
/* @__PURE__ */ jsx("text", {
|
|
761
|
+
"data-flow-spacing-label": "y",
|
|
762
|
+
x: guide.position + labelOffset,
|
|
763
|
+
y: (range.start + range.end) / 2,
|
|
764
|
+
fontSize,
|
|
765
|
+
fill: spacingColor,
|
|
766
|
+
textAnchor: "start",
|
|
767
|
+
dominantBaseline: "middle",
|
|
768
|
+
children: Math.round(guide.spacing)
|
|
769
|
+
}, `${key}-label`)
|
|
463
770
|
];
|
|
464
771
|
}))] })
|
|
465
772
|
});
|
|
466
773
|
}
|
|
774
|
+
function SelectionBoxLayer() {
|
|
775
|
+
const box = useFlowSelector((context) => context.selectionBox);
|
|
776
|
+
if (!box) return null;
|
|
777
|
+
const [a, b] = box;
|
|
778
|
+
return /* @__PURE__ */ jsx("div", {
|
|
779
|
+
"aria-hidden": "true",
|
|
780
|
+
"data-flow-selection-box": "",
|
|
781
|
+
style: {
|
|
782
|
+
left: Math.min(a.x, b.x),
|
|
783
|
+
top: Math.min(a.y, b.y),
|
|
784
|
+
width: Math.abs(b.x - a.x),
|
|
785
|
+
height: Math.abs(b.y - a.y)
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
467
789
|
function EdgeLayer(props) {
|
|
468
790
|
const instance = useFlowInstance();
|
|
469
791
|
const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
|
|
470
|
-
const edgeIds = useFlowSelector((context) => context.
|
|
792
|
+
const edgeIds = useFlowSelector((context) => getOrderedEntities(instance, context).edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
|
|
471
793
|
const renderEdge = props.renderEdge;
|
|
472
794
|
return /* @__PURE__ */ jsx("svg", {
|
|
473
795
|
"aria-label": "edges",
|
|
796
|
+
className: "stately-flow__edges",
|
|
474
797
|
"data-flow-canvas-layer": true,
|
|
475
798
|
style: {
|
|
476
799
|
position: "absolute",
|
|
@@ -478,34 +801,77 @@ function EdgeLayer(props) {
|
|
|
478
801
|
width: "100%",
|
|
479
802
|
height: "100%",
|
|
480
803
|
overflow: "visible",
|
|
481
|
-
pointerEvents: "
|
|
804
|
+
pointerEvents: "none"
|
|
482
805
|
},
|
|
483
806
|
children: /* @__PURE__ */ jsxs("g", { children: [edgeIds.map((id) => {
|
|
484
807
|
if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
|
|
485
808
|
return /* @__PURE__ */ jsx(EdgeView, {
|
|
486
809
|
flow: instance,
|
|
810
|
+
geometryObserver: props.geometryObserver,
|
|
487
811
|
id,
|
|
812
|
+
projections: props.projections,
|
|
488
813
|
renderEdge
|
|
489
814
|
}, id);
|
|
490
815
|
}), /* @__PURE__ */ jsx(ConnectionPreviewEdge, {
|
|
491
816
|
flow: instance,
|
|
492
|
-
renderEdge
|
|
817
|
+
renderEdge,
|
|
818
|
+
renderConnectionLine: props.renderConnectionLine
|
|
493
819
|
})] })
|
|
494
820
|
});
|
|
495
821
|
}
|
|
496
822
|
const EdgeView = memo(function EdgeView(props) {
|
|
497
|
-
const
|
|
823
|
+
const ref = useRef(null);
|
|
824
|
+
const { graph, edge, pathData, selected, focused, hovered, highlights } = useFlowEntity(props.id, (context) => ({
|
|
825
|
+
graph: context.graph,
|
|
498
826
|
edge: props.flow.getEdge(props.id) ?? null,
|
|
499
827
|
pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
|
|
828
|
+
geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
|
|
500
829
|
selected: context.selection.has(props.id),
|
|
830
|
+
focused: context.focusedEntityId === props.id,
|
|
501
831
|
hovered: context.hoveredEntityIds.has(props.id),
|
|
502
|
-
highlights: props.flow.engine.getHighlights(props.id)
|
|
503
|
-
}), { isEqual: (a, b) =>
|
|
832
|
+
highlights: props.flow.internals.engine.getHighlights(props.id)
|
|
833
|
+
}), { isEqual: (a, b) => {
|
|
834
|
+
if (!(a.selected === b.selected && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights) || a.geometryActive !== b.geometryActive) return false;
|
|
835
|
+
if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
|
|
836
|
+
return a.graph === b.graph && a.edge === b.edge && a.pathData === b.pathData;
|
|
837
|
+
} });
|
|
838
|
+
useLayoutEffect(() => {
|
|
839
|
+
const element = ref.current;
|
|
840
|
+
if (!element) return;
|
|
841
|
+
return props.geometryObserver.observeEdge(element, {
|
|
842
|
+
id: props.id,
|
|
843
|
+
getPathData: () => props.flow.getAllEdgePaths().get(props.id) ?? null
|
|
844
|
+
});
|
|
845
|
+
}, [
|
|
846
|
+
props.flow,
|
|
847
|
+
props.geometryObserver,
|
|
848
|
+
props.id
|
|
849
|
+
]);
|
|
504
850
|
if (!edge || !pathData) return null;
|
|
851
|
+
const attrs = getProjectedEdgeAttrs({
|
|
852
|
+
graph,
|
|
853
|
+
edge,
|
|
854
|
+
projections: props.projections
|
|
855
|
+
});
|
|
856
|
+
const focusable = props.flow.internals.engine.resolveEntityConfig(edge).focusable;
|
|
857
|
+
const focusProps = {
|
|
858
|
+
role: "group",
|
|
859
|
+
"aria-roledescription": "flow edge",
|
|
860
|
+
"aria-label": props.flow.internals.engine.getAriaLabel(props.id),
|
|
861
|
+
"aria-selected": selected,
|
|
862
|
+
tabIndex: focusable ? 0 : void 0,
|
|
863
|
+
onFocus: () => props.flow.internals.store.trigger.focus({ entityId: props.id })
|
|
864
|
+
};
|
|
505
865
|
if (props.renderEdge) return /* @__PURE__ */ jsxs("g", {
|
|
866
|
+
ref,
|
|
867
|
+
...focusProps,
|
|
506
868
|
"data-entity-id": props.id,
|
|
507
869
|
"data-flow-selected": selected ? "true" : void 0,
|
|
508
|
-
|
|
870
|
+
className: classNames$1("stately-flow__edge", attrs.className),
|
|
871
|
+
style: {
|
|
872
|
+
pointerEvents: "auto",
|
|
873
|
+
outline: focused ? "2px solid #2563eb" : "none"
|
|
874
|
+
},
|
|
509
875
|
children: [/* @__PURE__ */ jsx(EdgeHitTarget, {
|
|
510
876
|
flow: props.flow,
|
|
511
877
|
edge,
|
|
@@ -515,19 +881,28 @@ const EdgeView = memo(function EdgeView(props) {
|
|
|
515
881
|
flow: props.flow,
|
|
516
882
|
edge,
|
|
517
883
|
pathData,
|
|
884
|
+
attrs,
|
|
518
885
|
selected,
|
|
519
886
|
hovered,
|
|
520
887
|
highlights
|
|
521
888
|
})]
|
|
522
889
|
});
|
|
523
890
|
return /* @__PURE__ */ jsx("g", {
|
|
891
|
+
ref,
|
|
892
|
+
...focusProps,
|
|
524
893
|
"data-entity-id": props.id,
|
|
525
894
|
"data-flow-selected": selected ? "true" : void 0,
|
|
526
|
-
|
|
895
|
+
className: classNames$1("stately-flow__edge", attrs.className),
|
|
896
|
+
style: {
|
|
897
|
+
pointerEvents: "auto",
|
|
898
|
+
opacity: attrs.opacity,
|
|
899
|
+
outline: focused ? "2px solid #2563eb" : "none"
|
|
900
|
+
},
|
|
527
901
|
children: /* @__PURE__ */ jsx(DefaultEdge, {
|
|
528
902
|
flow: props.flow,
|
|
529
903
|
edge,
|
|
530
904
|
pathData,
|
|
905
|
+
attrs,
|
|
531
906
|
selected
|
|
532
907
|
})
|
|
533
908
|
});
|
|
@@ -535,17 +910,22 @@ const EdgeView = memo(function EdgeView(props) {
|
|
|
535
910
|
/** The in-progress connection gesture's preview edge (renders only mid-gesture). */
|
|
536
911
|
function ConnectionPreviewEdge(props) {
|
|
537
912
|
const connection = useFlowSelector((context) => context.connection);
|
|
538
|
-
if (!connection?.active) return null;
|
|
913
|
+
if (!connection?.active || connection.preview === false) return null;
|
|
539
914
|
const previewGraph = createConnectionPreviewGraph(props.flow.getSnapshot().graph, connection);
|
|
540
915
|
const pathData = props.flow.getAllEdgePaths({ graph: previewGraph }).get(CONNECTION_PREVIEW_EDGE_ID);
|
|
541
916
|
const edge = previewGraph.edges.find((candidate) => candidate.id === CONNECTION_PREVIEW_EDGE_ID);
|
|
542
917
|
if (!edge || !pathData) return null;
|
|
543
918
|
return /* @__PURE__ */ jsx("g", {
|
|
544
919
|
style: { pointerEvents: "none" },
|
|
545
|
-
children: props.
|
|
920
|
+
children: props.renderConnectionLine ? props.renderConnectionLine({
|
|
921
|
+
connection,
|
|
922
|
+
pathData,
|
|
923
|
+
isValid: connection.isValid
|
|
924
|
+
}) : props.renderEdge ? props.renderEdge({
|
|
546
925
|
flow: props.flow,
|
|
547
926
|
edge,
|
|
548
927
|
pathData,
|
|
928
|
+
attrs: {},
|
|
549
929
|
selected: false,
|
|
550
930
|
hovered: false,
|
|
551
931
|
highlights: []
|
|
@@ -553,22 +933,26 @@ function ConnectionPreviewEdge(props) {
|
|
|
553
933
|
flow: props.flow,
|
|
554
934
|
edge,
|
|
555
935
|
pathData,
|
|
936
|
+
attrs: {},
|
|
556
937
|
selected: false,
|
|
557
938
|
preview: true
|
|
558
939
|
})
|
|
559
940
|
});
|
|
560
941
|
}
|
|
561
942
|
function DefaultEdge(props) {
|
|
562
|
-
return /* @__PURE__ */
|
|
943
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [props.pathData.segments.map((segment, index) => {
|
|
563
944
|
const d = pathToSVG(segment);
|
|
564
945
|
return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
|
|
565
946
|
d,
|
|
947
|
+
"data-flow-edge-segment": index,
|
|
566
948
|
fill: "none",
|
|
567
|
-
stroke: props.preview ? props.edge.data?.isValid === false ? "#dc2626" : props.edge.data?.isValid === true ? "#16a34a" : "#0f766e" : props.selected ? "#2563eb" : "#94a3b8",
|
|
949
|
+
stroke: props.attrs.color ?? (props.preview ? props.edge.data?.isValid === false ? "#dc2626" : props.edge.data?.isValid === true ? "#16a34a" : "#0f766e" : props.selected ? "#2563eb" : "#94a3b8"),
|
|
568
950
|
strokeDasharray: props.preview ? "6 6" : void 0,
|
|
569
|
-
strokeWidth: props.selected ? 3 : 2
|
|
951
|
+
strokeWidth: props.attrs.strokeWidth ?? (props.selected ? 3 : 2),
|
|
952
|
+
style: props.attrs.style
|
|
570
953
|
}), props.preview ? null : /* @__PURE__ */ jsx("path", {
|
|
571
954
|
d,
|
|
955
|
+
"data-flow-edge-segment": index,
|
|
572
956
|
"data-entity-id": props.edge.id,
|
|
573
957
|
fill: "none",
|
|
574
958
|
stroke: "#000",
|
|
@@ -576,12 +960,18 @@ function DefaultEdge(props) {
|
|
|
576
960
|
strokeWidth: 18,
|
|
577
961
|
pointerEvents: "stroke"
|
|
578
962
|
})] }, index);
|
|
579
|
-
})
|
|
963
|
+
}), props.preview ? null : /* @__PURE__ */ jsx(EdgeEndpointHandles, {
|
|
964
|
+
flow: props.flow,
|
|
965
|
+
edge: props.edge,
|
|
966
|
+
pathData: props.pathData,
|
|
967
|
+
selected: props.selected
|
|
968
|
+
})] });
|
|
580
969
|
}
|
|
581
970
|
function EdgeHitTarget(props) {
|
|
582
|
-
return /* @__PURE__ */
|
|
971
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [props.pathData.segments.map((segment, index) => {
|
|
583
972
|
return /* @__PURE__ */ jsx("path", {
|
|
584
973
|
d: pathToSVG(segment),
|
|
974
|
+
"data-flow-edge-segment": index,
|
|
585
975
|
"data-entity-id": props.edge.id,
|
|
586
976
|
fill: "none",
|
|
587
977
|
stroke: "#000",
|
|
@@ -589,88 +979,225 @@ function EdgeHitTarget(props) {
|
|
|
589
979
|
strokeWidth: 18,
|
|
590
980
|
pointerEvents: "stroke"
|
|
591
981
|
}, index);
|
|
592
|
-
}) });
|
|
982
|
+
}), /* @__PURE__ */ jsx(EdgeEndpointHandles, { ...props })] });
|
|
983
|
+
}
|
|
984
|
+
function EdgeEndpointHandles(props) {
|
|
985
|
+
if (!props.selected) return null;
|
|
986
|
+
if (!props.flow.internals.engine.resolveEntityConfig(props.edge).reconnectable) return null;
|
|
987
|
+
const first = props.pathData.segments[0]?.points[0];
|
|
988
|
+
const last = props.pathData.segments.at(-1)?.points.at(-1);
|
|
989
|
+
if (!first || !last) return null;
|
|
990
|
+
const radius = 6 / props.flow.getContext().viewport.zoom;
|
|
991
|
+
const strokeWidth = 2 / props.flow.getContext().viewport.zoom;
|
|
992
|
+
return /* @__PURE__ */ jsx(Fragment, { children: [{
|
|
993
|
+
end: "source",
|
|
994
|
+
point: first,
|
|
995
|
+
cursor: "crosshair"
|
|
996
|
+
}, {
|
|
997
|
+
end: "target",
|
|
998
|
+
point: last,
|
|
999
|
+
cursor: "crosshair"
|
|
1000
|
+
}].map(({ end, point, cursor }) => /* @__PURE__ */ jsx("circle", {
|
|
1001
|
+
"data-entity-id": props.edge.id,
|
|
1002
|
+
"data-flow-part": `edge-endpoint:${end}`,
|
|
1003
|
+
"data-flow-edge-endpoint": end,
|
|
1004
|
+
cx: point.x,
|
|
1005
|
+
cy: point.y,
|
|
1006
|
+
r: radius,
|
|
1007
|
+
fill: "#ffffff",
|
|
1008
|
+
stroke: "#2563eb",
|
|
1009
|
+
strokeWidth,
|
|
1010
|
+
style: {
|
|
1011
|
+
cursor,
|
|
1012
|
+
pointerEvents: "auto"
|
|
1013
|
+
}
|
|
1014
|
+
}, end)) });
|
|
593
1015
|
}
|
|
594
1016
|
function EdgeLabelLayer(props) {
|
|
595
1017
|
const instance = useFlowInstance();
|
|
596
1018
|
const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
|
|
1019
|
+
const renderEdgeLabel = props.renderEdgeLabel;
|
|
1020
|
+
const graphEdgeIds = useFlowSelector((context) => context.graph.edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
|
|
1021
|
+
const durableLabelIds = useFlowSelector((context) => context.graph.edges.filter((edge) => edge.label).map((edge) => edge.id), { isEqual: stringArraysEqual });
|
|
597
1022
|
return /* @__PURE__ */ jsx("div", {
|
|
598
1023
|
"aria-label": "edge labels",
|
|
1024
|
+
className: "stately-flow__edge-labels",
|
|
599
1025
|
style: {
|
|
600
1026
|
position: "absolute",
|
|
601
1027
|
inset: 0,
|
|
602
1028
|
pointerEvents: "none"
|
|
603
1029
|
},
|
|
604
|
-
children:
|
|
1030
|
+
children: useMemo(() => {
|
|
1031
|
+
const ids = new Set(durableLabelIds);
|
|
1032
|
+
const existing = new Set(graphEdgeIds);
|
|
1033
|
+
for (const entry of props.projections ?? []) for (const id of Object.keys(entry.projection.edges ?? {})) if (existing.has(id)) ids.add(id);
|
|
1034
|
+
return [...ids];
|
|
1035
|
+
}, [
|
|
1036
|
+
durableLabelIds,
|
|
1037
|
+
graphEdgeIds,
|
|
1038
|
+
props.projections
|
|
1039
|
+
]).map((id) => {
|
|
605
1040
|
if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
|
|
606
1041
|
return /* @__PURE__ */ jsx(EdgeLabelItem, {
|
|
607
1042
|
flow: instance,
|
|
608
|
-
|
|
1043
|
+
geometryObserver: props.geometryObserver,
|
|
1044
|
+
id,
|
|
1045
|
+
projections: props.projections,
|
|
1046
|
+
renderEdgeLabel
|
|
609
1047
|
}, id);
|
|
610
1048
|
})
|
|
611
1049
|
});
|
|
612
1050
|
}
|
|
613
1051
|
const EdgeLabelItem = memo(function EdgeLabelItem(props) {
|
|
614
|
-
const { edge, pathData, selected } = useFlowEntity(props.id, (context) => ({
|
|
1052
|
+
const { graph, edge, pathData, selected, dragging, hovered, highlights } = useFlowEntity(props.id, (context) => ({
|
|
1053
|
+
graph: context.graph,
|
|
615
1054
|
edge: props.flow.getEdge(props.id) ?? null,
|
|
616
1055
|
pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1056
|
+
geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
|
|
1057
|
+
selected: context.selection.has(props.id),
|
|
1058
|
+
dragging: context.draggedEntityId === props.id,
|
|
1059
|
+
hovered: context.hoveredEntityIds.has(props.id),
|
|
1060
|
+
highlights: props.flow.internals.engine.getHighlights(props.id)
|
|
1061
|
+
}), { isEqual: (a, b) => {
|
|
1062
|
+
if (!(a.selected === b.selected && a.dragging === b.dragging && a.hovered === b.hovered && a.highlights === b.highlights) || a.geometryActive !== b.geometryActive) return false;
|
|
1063
|
+
if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
|
|
1064
|
+
return a.graph === b.graph && a.edge === b.edge && a.pathData === b.pathData;
|
|
1065
|
+
} });
|
|
1066
|
+
if (!edge || !pathData) return null;
|
|
1067
|
+
const attrs = getProjectedEdgeAttrs({
|
|
1068
|
+
graph,
|
|
1069
|
+
edge,
|
|
1070
|
+
projections: props.projections
|
|
1071
|
+
});
|
|
1072
|
+
const bounds = pathData.labelBounds ?? projectedEdgeLabelBounds(pathData, attrs.label);
|
|
1073
|
+
if (!edge.label && attrs.label === void 0 || !bounds) return null;
|
|
620
1074
|
return /* @__PURE__ */ jsx(EdgeLabelView, {
|
|
621
1075
|
flow: props.flow,
|
|
1076
|
+
geometryObserver: props.geometryObserver,
|
|
622
1077
|
edge,
|
|
623
|
-
|
|
624
|
-
|
|
1078
|
+
attrs,
|
|
1079
|
+
bounds,
|
|
1080
|
+
pathData,
|
|
1081
|
+
selected,
|
|
1082
|
+
dragging,
|
|
1083
|
+
hovered,
|
|
1084
|
+
highlights,
|
|
1085
|
+
projections: props.projections,
|
|
1086
|
+
renderEdgeLabel: props.renderEdgeLabel
|
|
625
1087
|
});
|
|
626
1088
|
});
|
|
627
1089
|
function EdgeLabelView(props) {
|
|
628
1090
|
const ref = useRef(null);
|
|
629
|
-
const observer =
|
|
1091
|
+
const observer = useMeasurementObserver();
|
|
630
1092
|
useLayoutEffect(() => {
|
|
631
1093
|
const element = ref.current;
|
|
632
1094
|
if (!element) return;
|
|
633
1095
|
observer.observe(element, { edgeId: props.edge.id });
|
|
634
|
-
return () => observer.
|
|
1096
|
+
return () => observer.unobserve(element);
|
|
635
1097
|
}, [observer, props.edge.id]);
|
|
1098
|
+
useLayoutEffect(() => {
|
|
1099
|
+
const element = ref.current;
|
|
1100
|
+
if (!element) return;
|
|
1101
|
+
return props.geometryObserver.observeRect(element, {
|
|
1102
|
+
id: props.edge.id,
|
|
1103
|
+
autoSize: props.edge.label?.autoSize,
|
|
1104
|
+
getRect: () => {
|
|
1105
|
+
const pathData = props.flow.getAllEdgePaths().get(props.edge.id);
|
|
1106
|
+
if (!pathData) return null;
|
|
1107
|
+
const edge = props.flow.getEdge(props.edge.id);
|
|
1108
|
+
if (!edge) return null;
|
|
1109
|
+
const attrs = getProjectedEdgeAttrs({
|
|
1110
|
+
graph: props.flow.getSnapshot().graph,
|
|
1111
|
+
edge,
|
|
1112
|
+
projections: props.projections
|
|
1113
|
+
});
|
|
1114
|
+
return pathData.labelBounds ?? projectedEdgeLabelBounds(pathData, attrs.label);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
}, [
|
|
1118
|
+
props.edge.id,
|
|
1119
|
+
props.edge.label?.autoSize,
|
|
1120
|
+
props.flow,
|
|
1121
|
+
props.geometryObserver,
|
|
1122
|
+
props.projections
|
|
1123
|
+
]);
|
|
636
1124
|
const text = (props.edge.label?.data)?.text;
|
|
1125
|
+
const label = props.attrs.label ?? String(text ?? props.edge.id);
|
|
1126
|
+
const customLabel = props.renderEdgeLabel?.({
|
|
1127
|
+
flow: props.flow,
|
|
1128
|
+
edge: props.edge,
|
|
1129
|
+
pathData: props.pathData,
|
|
1130
|
+
attrs: props.attrs,
|
|
1131
|
+
bounds: props.bounds,
|
|
1132
|
+
selected: props.selected,
|
|
1133
|
+
dragging: props.dragging,
|
|
1134
|
+
hovered: props.hovered,
|
|
1135
|
+
highlights: props.highlights
|
|
1136
|
+
});
|
|
1137
|
+
const defaultStyle = props.renderEdgeLabel ? void 0 : {
|
|
1138
|
+
padding: "4px 8px",
|
|
1139
|
+
border: `1px solid ${props.selected ? "#2563eb" : props.attrs.color ?? "#cbd5e1"}`,
|
|
1140
|
+
borderRadius: 999,
|
|
1141
|
+
background: "#fff",
|
|
1142
|
+
color: "#334155",
|
|
1143
|
+
display: "grid",
|
|
1144
|
+
placeItems: "center",
|
|
1145
|
+
cursor: "grab",
|
|
1146
|
+
font: "600 12px/1 system-ui, sans-serif"
|
|
1147
|
+
};
|
|
637
1148
|
return /* @__PURE__ */ jsx("div", {
|
|
638
1149
|
ref,
|
|
639
1150
|
"data-entity-id": props.edge.id,
|
|
640
1151
|
"data-flow-edge-label": "",
|
|
1152
|
+
"data-flow-selected": props.selected ? "true" : void 0,
|
|
1153
|
+
"data-flow-dragging": props.dragging ? "true" : void 0,
|
|
1154
|
+
className: classNames$1("stately-flow__edge-label", props.attrs.className),
|
|
641
1155
|
style: {
|
|
642
1156
|
position: "absolute",
|
|
643
|
-
left:
|
|
644
|
-
top:
|
|
645
|
-
|
|
646
|
-
|
|
1157
|
+
left: 0,
|
|
1158
|
+
top: 0,
|
|
1159
|
+
transform: `translate(${props.bounds.x}px, ${props.bounds.y}px)`,
|
|
1160
|
+
width: props.edge.label?.autoSize ? void 0 : props.bounds.width,
|
|
1161
|
+
height: props.edge.label?.autoSize ? void 0 : props.bounds.height,
|
|
647
1162
|
boxSizing: "border-box",
|
|
648
|
-
|
|
649
|
-
border: `1px solid ${props.selected ? "#2563eb" : "#cbd5e1"}`,
|
|
650
|
-
borderRadius: 999,
|
|
651
|
-
background: "#fff",
|
|
652
|
-
color: "#334155",
|
|
653
|
-
display: "grid",
|
|
654
|
-
placeItems: "center",
|
|
655
|
-
cursor: "grab",
|
|
656
|
-
font: "600 12px/1 system-ui, sans-serif",
|
|
1163
|
+
...defaultStyle,
|
|
657
1164
|
pointerEvents: "auto",
|
|
658
|
-
userSelect: "none"
|
|
1165
|
+
userSelect: "none",
|
|
1166
|
+
opacity: props.attrs.opacity,
|
|
1167
|
+
...props.attrs.style
|
|
659
1168
|
},
|
|
660
|
-
children:
|
|
1169
|
+
children: props.renderEdgeLabel ? customLabel : label
|
|
661
1170
|
});
|
|
662
1171
|
}
|
|
1172
|
+
function projectedEdgeLabelBounds(pathData, label) {
|
|
1173
|
+
if (label === void 0) return null;
|
|
1174
|
+
const points = pathData.segments.flatMap((segment) => segment.points);
|
|
1175
|
+
if (!points.length) return null;
|
|
1176
|
+
const middle = points[Math.floor(points.length / 2)];
|
|
1177
|
+
return {
|
|
1178
|
+
x: middle.x - 40,
|
|
1179
|
+
y: middle.y - 12,
|
|
1180
|
+
width: 80,
|
|
1181
|
+
height: 24
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
663
1184
|
function NodeLayer(props) {
|
|
664
1185
|
const instance = useFlowInstance();
|
|
665
1186
|
const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
|
|
666
|
-
const
|
|
667
|
-
const
|
|
668
|
-
return context.graph.nodes.
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1187
|
+
const nodePaintEntries = useFlowSelector((context) => {
|
|
1188
|
+
const paintOrder = new Map(getOrderedEntities(instance, context).nodes.map((node, orderIndex) => [node.id, orderIndex]));
|
|
1189
|
+
return context.graph.nodes.flatMap((node) => {
|
|
1190
|
+
const orderIndex = paintOrder.get(node.id);
|
|
1191
|
+
return orderIndex === void 0 ? [] : [{
|
|
1192
|
+
id: node.id,
|
|
1193
|
+
orderIndex
|
|
1194
|
+
}];
|
|
1195
|
+
});
|
|
1196
|
+
}, { isEqual: nodePaintEntriesEqual });
|
|
1197
|
+
const observer = useMeasurementObserver();
|
|
672
1198
|
return /* @__PURE__ */ jsx("div", {
|
|
673
1199
|
"aria-label": "nodes",
|
|
1200
|
+
className: "stately-flow__nodes",
|
|
674
1201
|
"data-flow-canvas-layer": true,
|
|
675
1202
|
style: {
|
|
676
1203
|
position: "absolute",
|
|
@@ -678,35 +1205,100 @@ function NodeLayer(props) {
|
|
|
678
1205
|
transformOrigin: "0 0",
|
|
679
1206
|
pointerEvents: "none"
|
|
680
1207
|
},
|
|
681
|
-
children:
|
|
1208
|
+
children: nodePaintEntries.map(({ id, orderIndex }) => {
|
|
682
1209
|
if (renderableIds && !renderableIds.nodeIds.has(id)) return null;
|
|
683
1210
|
return /* @__PURE__ */ jsx(NodeView, {
|
|
684
1211
|
flow: instance,
|
|
1212
|
+
geometryObserver: props.geometryObserver,
|
|
685
1213
|
id,
|
|
1214
|
+
orderIndex,
|
|
686
1215
|
observer,
|
|
1216
|
+
projections: props.projections,
|
|
687
1217
|
renderNode: props.renderNode
|
|
688
1218
|
}, id);
|
|
689
1219
|
})
|
|
690
1220
|
});
|
|
691
1221
|
}
|
|
1222
|
+
function getOrderedEntities(flow, context) {
|
|
1223
|
+
const maybeEngine = flow.internals.engine;
|
|
1224
|
+
const visibleOrdered = maybeEngine.getVisibleEntities?.();
|
|
1225
|
+
if (visibleOrdered) return visibleOrdered;
|
|
1226
|
+
const ordered = maybeEngine.getEntityOrder?.();
|
|
1227
|
+
if (ordered) return ordered;
|
|
1228
|
+
const depths = nodeDepths(context.graph.nodes);
|
|
1229
|
+
const nodeOrder = new Map(context.graph.nodes.map((node, index) => [node.id, index]));
|
|
1230
|
+
const edgeOrder = new Map(context.graph.edges.map((edge, index) => [edge.id, index]));
|
|
1231
|
+
return {
|
|
1232
|
+
nodes: [...context.graph.nodes].sort((a, b) => {
|
|
1233
|
+
const depthDelta = (depths.get(a.id) ?? 0) - (depths.get(b.id) ?? 0);
|
|
1234
|
+
if (depthDelta !== 0) return depthDelta;
|
|
1235
|
+
const zDelta = (a.zIndex ?? 0) - (b.zIndex ?? 0);
|
|
1236
|
+
if (zDelta !== 0) return zDelta;
|
|
1237
|
+
const selectedDelta = Number(context.selection.has(a.id)) - Number(context.selection.has(b.id));
|
|
1238
|
+
if (selectedDelta !== 0) return selectedDelta;
|
|
1239
|
+
return (nodeOrder.get(a.id) ?? 0) - (nodeOrder.get(b.id) ?? 0);
|
|
1240
|
+
}),
|
|
1241
|
+
edges: [...context.graph.edges].sort((a, b) => {
|
|
1242
|
+
const depthDelta = Math.max(depths.get(a.sourceId) ?? 0, depths.get(a.targetId) ?? 0) - Math.max(depths.get(b.sourceId) ?? 0, depths.get(b.targetId) ?? 0);
|
|
1243
|
+
if (depthDelta !== 0) return depthDelta;
|
|
1244
|
+
const zDelta = (a.zIndex ?? 0) - (b.zIndex ?? 0);
|
|
1245
|
+
if (zDelta !== 0) return zDelta;
|
|
1246
|
+
return (edgeOrder.get(a.id) ?? 0) - (edgeOrder.get(b.id) ?? 0);
|
|
1247
|
+
})
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
692
1250
|
function rectsShallowEqual(a, b) {
|
|
693
1251
|
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
694
1252
|
}
|
|
1253
|
+
const ENTITY_GEOMETRY_KEYS = /* @__PURE__ */ new Set([
|
|
1254
|
+
"x",
|
|
1255
|
+
"y",
|
|
1256
|
+
"dx",
|
|
1257
|
+
"dy",
|
|
1258
|
+
"width",
|
|
1259
|
+
"height",
|
|
1260
|
+
"measured"
|
|
1261
|
+
]);
|
|
1262
|
+
const EDGE_GEOMETRY_KEYS = /* @__PURE__ */ new Set([...ENTITY_GEOMETRY_KEYS, "label"]);
|
|
1263
|
+
function shallowObjectEqualExcept(a, b, ignored) {
|
|
1264
|
+
if (a === b) return true;
|
|
1265
|
+
if (!a || !b) return false;
|
|
1266
|
+
const left = a;
|
|
1267
|
+
const right = b;
|
|
1268
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
1269
|
+
for (const key of keys) if (!ignored.has(key) && left[key] !== right[key]) return false;
|
|
1270
|
+
return true;
|
|
1271
|
+
}
|
|
1272
|
+
function nodeRenderContentEqual(a, b) {
|
|
1273
|
+
return shallowObjectEqualExcept(a, b, ENTITY_GEOMETRY_KEYS);
|
|
1274
|
+
}
|
|
1275
|
+
function edgeRenderContentEqual(a, b) {
|
|
1276
|
+
if (a === b) return true;
|
|
1277
|
+
if (!a || !b) return false;
|
|
1278
|
+
return shallowObjectEqualExcept(a, b, EDGE_GEOMETRY_KEYS) && shallowObjectEqualExcept(a.label, b.label, ENTITY_GEOMETRY_KEYS);
|
|
1279
|
+
}
|
|
695
1280
|
const NodeView = memo(function NodeView(props) {
|
|
696
1281
|
const ref = useRef(null);
|
|
697
|
-
const { node, hidden, selected, dragging, focused, hovered, highlights, bounds } = useFlowEntity(props.id, (context) => {
|
|
1282
|
+
const { graph, node, hidden, selected, dragging, focused, hovered, highlights, bounds } = useFlowEntity(props.id, (context) => {
|
|
698
1283
|
const current = props.flow.getNode(props.id) ?? null;
|
|
699
1284
|
return {
|
|
1285
|
+
graph: context.graph,
|
|
700
1286
|
node: current,
|
|
701
|
-
|
|
1287
|
+
geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
|
|
1288
|
+
hidden: !current || props.flow.internals.engine.resolveEntityConfig(current).hidden,
|
|
702
1289
|
selected: context.selection.has(props.id),
|
|
703
|
-
dragging: context.
|
|
1290
|
+
dragging: context.canvasState === "dragging" && context.selection.has(props.id),
|
|
704
1291
|
focused: context.focusedEntityId === props.id,
|
|
705
1292
|
hovered: context.hoveredEntityIds.has(props.id),
|
|
706
|
-
highlights: props.flow.engine.getHighlights(props.id),
|
|
707
|
-
bounds: context.resizingEntityId === props.id && context.resizeCorners ? props.flow.engine.getResizeBounds() ?? props.flow.engine.
|
|
1293
|
+
highlights: props.flow.internals.engine.getHighlights(props.id),
|
|
1294
|
+
bounds: context.resizingEntityId === props.id && context.resizeCorners ? props.flow.internals.engine.getResizeBounds() ?? props.flow.internals.engine.visualBounds(props.id) : props.flow.internals.engine.visualBounds(props.id)
|
|
708
1295
|
};
|
|
709
|
-
}, { isEqual: (a, b) =>
|
|
1296
|
+
}, { isEqual: (a, b) => {
|
|
1297
|
+
if (!(a.hidden === b.hidden && a.selected === b.selected && a.dragging === b.dragging && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights) || a.geometryActive !== b.geometryActive) return false;
|
|
1298
|
+
if (a.geometryActive) return nodeRenderContentEqual(a.node, b.node);
|
|
1299
|
+
return a.graph === b.graph && a.node === b.node && rectsShallowEqual(a.bounds, b.bounds);
|
|
1300
|
+
} });
|
|
1301
|
+
const contentOwnsSize = Boolean(node?.autoSize && node.style?.customWidth === void 0 && node.style?.customHeight === void 0);
|
|
710
1302
|
useLayoutEffect(() => {
|
|
711
1303
|
const element = ref.current;
|
|
712
1304
|
if (!element) return;
|
|
@@ -720,7 +1312,30 @@ const NodeView = memo(function NodeView(props) {
|
|
|
720
1312
|
props.flow,
|
|
721
1313
|
hidden
|
|
722
1314
|
]);
|
|
1315
|
+
useLayoutEffect(() => {
|
|
1316
|
+
const element = ref.current;
|
|
1317
|
+
if (!element) return;
|
|
1318
|
+
return props.geometryObserver.observeRect(element, {
|
|
1319
|
+
id: props.id,
|
|
1320
|
+
autoSize: contentOwnsSize,
|
|
1321
|
+
getRect: () => {
|
|
1322
|
+
const context = props.flow.getSnapshot();
|
|
1323
|
+
return context.resizingEntityId === props.id && context.resizeCorners ? props.flow.internals.engine.getResizeBounds() ?? props.flow.internals.engine.visualBounds(props.id) : props.flow.internals.engine.visualBounds(props.id);
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
}, [
|
|
1327
|
+
contentOwnsSize,
|
|
1328
|
+
props.flow,
|
|
1329
|
+
props.geometryObserver,
|
|
1330
|
+
props.id
|
|
1331
|
+
]);
|
|
723
1332
|
if (!node || hidden) return null;
|
|
1333
|
+
const attrs = getProjectedNodeAttrs({
|
|
1334
|
+
graph,
|
|
1335
|
+
node,
|
|
1336
|
+
projections: props.projections
|
|
1337
|
+
});
|
|
1338
|
+
const focusable = props.flow.internals.engine.resolveEntityConfig(node).focusable;
|
|
724
1339
|
return /* @__PURE__ */ jsx(NodeRenderContextProvider, {
|
|
725
1340
|
value: {
|
|
726
1341
|
node,
|
|
@@ -730,26 +1345,31 @@ const NodeView = memo(function NodeView(props) {
|
|
|
730
1345
|
ref,
|
|
731
1346
|
"data-entity-id": props.id,
|
|
732
1347
|
"data-flow-selected": selected ? "true" : void 0,
|
|
1348
|
+
className: classNames$1("stately-flow__node", attrs.className),
|
|
733
1349
|
role: "group",
|
|
734
1350
|
"aria-roledescription": "flow node",
|
|
735
|
-
"aria-
|
|
736
|
-
|
|
737
|
-
|
|
1351
|
+
"aria-selected": selected,
|
|
1352
|
+
"aria-label": props.flow.internals.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
|
|
1353
|
+
tabIndex: focusable ? 0 : void 0,
|
|
1354
|
+
onFocus: () => props.flow.internals.store.trigger.focus({ entityId: props.id }),
|
|
738
1355
|
style: {
|
|
739
1356
|
...nodePositionStyle(bounds, {
|
|
740
|
-
autoSize:
|
|
1357
|
+
autoSize: contentOwnsSize,
|
|
741
1358
|
dragging
|
|
742
1359
|
}),
|
|
743
1360
|
cursor: dragging ? "grabbing" : "grab",
|
|
744
1361
|
pointerEvents: "auto",
|
|
745
1362
|
userSelect: "none",
|
|
1363
|
+
zIndex: props.orderIndex,
|
|
746
1364
|
outline: focused ? "2px solid #2563eb" : "none",
|
|
747
|
-
outlineOffset: 2
|
|
1365
|
+
outlineOffset: 2,
|
|
1366
|
+
opacity: attrs.opacity
|
|
748
1367
|
},
|
|
749
1368
|
children: props.renderNode ? props.renderNode({
|
|
750
1369
|
flow: props.flow,
|
|
751
1370
|
node,
|
|
752
1371
|
bounds,
|
|
1372
|
+
attrs,
|
|
753
1373
|
selected,
|
|
754
1374
|
dragging,
|
|
755
1375
|
hovered,
|
|
@@ -758,6 +1378,7 @@ const NodeView = memo(function NodeView(props) {
|
|
|
758
1378
|
flow: props.flow,
|
|
759
1379
|
node,
|
|
760
1380
|
bounds,
|
|
1381
|
+
attrs,
|
|
761
1382
|
selected,
|
|
762
1383
|
dragging,
|
|
763
1384
|
hovered,
|
|
@@ -766,6 +1387,7 @@ const NodeView = memo(function NodeView(props) {
|
|
|
766
1387
|
flow: props.flow,
|
|
767
1388
|
node,
|
|
768
1389
|
bounds,
|
|
1390
|
+
attrs,
|
|
769
1391
|
selected,
|
|
770
1392
|
dragging,
|
|
771
1393
|
hovered,
|
|
@@ -780,8 +1402,10 @@ const NodeView = memo(function NodeView(props) {
|
|
|
780
1402
|
* `engine.getGroupGeometry`; custom renderers can do the same.
|
|
781
1403
|
*/
|
|
782
1404
|
function DefaultGroupNode(props) {
|
|
783
|
-
const geometry = props.flow.engine.getGroupGeometry(props.node.id);
|
|
1405
|
+
const geometry = props.flow.internals.engine.getGroupGeometry(props.node.id);
|
|
784
1406
|
const label = props.node.data?.label;
|
|
1407
|
+
const badges = decorationsToArray(props.attrs.badge);
|
|
1408
|
+
const title = decorationTitle(props.attrs.tooltip);
|
|
785
1409
|
if (!geometry) return /* @__PURE__ */ jsx(DefaultNode, { ...props });
|
|
786
1410
|
const relative = (rect) => ({
|
|
787
1411
|
left: rect.x - props.bounds.x,
|
|
@@ -794,13 +1418,14 @@ function DefaultGroupNode(props) {
|
|
|
794
1418
|
position: "absolute",
|
|
795
1419
|
...relative(geometry.childArea),
|
|
796
1420
|
boxSizing: "border-box",
|
|
797
|
-
border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : "#cbd5e1"}`,
|
|
1421
|
+
border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : props.attrs.color ?? "#cbd5e1"}`,
|
|
798
1422
|
...dropTarget ? { background: "rgba(22, 163, 74, 0.08)" } : null,
|
|
799
1423
|
borderRadius: 10,
|
|
800
1424
|
background: "rgba(148, 163, 184, 0.07)",
|
|
801
1425
|
pointerEvents: "none"
|
|
802
|
-
} }), /* @__PURE__ */
|
|
1426
|
+
} }), /* @__PURE__ */ jsxs("div", {
|
|
803
1427
|
"data-flow-drag-handle": true,
|
|
1428
|
+
title,
|
|
804
1429
|
style: {
|
|
805
1430
|
position: "absolute",
|
|
806
1431
|
...relative(geometry.contentBounds),
|
|
@@ -808,37 +1433,65 @@ function DefaultGroupNode(props) {
|
|
|
808
1433
|
display: "flex",
|
|
809
1434
|
alignItems: "center",
|
|
810
1435
|
padding: "0 12px",
|
|
811
|
-
border: `2px solid ${props.selected ? "#2563eb" : "#94a3b8"}`,
|
|
1436
|
+
border: `2px solid ${props.selected ? "#2563eb" : props.attrs.color ?? "#94a3b8"}`,
|
|
812
1437
|
borderRadius: 8,
|
|
813
1438
|
background: props.selected ? "#eff6ff" : "#f8fafc",
|
|
814
1439
|
color: "#0f172a",
|
|
815
1440
|
font: "600 13px/1.2 system-ui, sans-serif",
|
|
816
1441
|
cursor: props.dragging ? "grabbing" : "grab",
|
|
817
|
-
userSelect: "none"
|
|
1442
|
+
userSelect: "none",
|
|
1443
|
+
...props.attrs.style
|
|
818
1444
|
},
|
|
819
|
-
children: String(label ?? props.node.id)
|
|
1445
|
+
children: [/* @__PURE__ */ jsx("span", { children: String(label ?? props.node.id) }), badges.length ? /* @__PURE__ */ jsx("span", {
|
|
1446
|
+
style: defaultBadgeListStyle,
|
|
1447
|
+
children: badges
|
|
1448
|
+
}) : null]
|
|
820
1449
|
})] });
|
|
821
1450
|
}
|
|
822
1451
|
function DefaultNode(props) {
|
|
823
1452
|
const label = props.node.data?.label;
|
|
824
|
-
|
|
1453
|
+
const badges = decorationsToArray(props.attrs.badge);
|
|
1454
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1455
|
+
title: decorationTitle(props.attrs.tooltip),
|
|
825
1456
|
style: {
|
|
1457
|
+
position: "relative",
|
|
826
1458
|
display: "grid",
|
|
827
1459
|
placeItems: "center",
|
|
828
1460
|
width: "100%",
|
|
829
1461
|
height: "100%",
|
|
830
1462
|
boxSizing: "border-box",
|
|
831
1463
|
padding: "10px 14px",
|
|
832
|
-
border: `2px solid ${props.selected ? "#2563eb" : props.hovered ? "#94a3b8" : "#cbd5e1"}`,
|
|
1464
|
+
border: `2px solid ${props.selected ? "#2563eb" : props.attrs.color ?? (props.hovered ? "#94a3b8" : "#cbd5e1")}`,
|
|
833
1465
|
borderRadius: 8,
|
|
834
1466
|
background: props.selected ? "#eff6ff" : "#ffffff",
|
|
835
1467
|
color: "#0f172a",
|
|
836
1468
|
font: "600 14px/1.2 system-ui, sans-serif",
|
|
837
|
-
boxShadow: props.selected ? "0 0 0 4px rgba(37, 99, 235, 0.16)" : "0 1px 2px rgba(15, 23, 42, 0.1)"
|
|
1469
|
+
boxShadow: props.selected ? "0 0 0 4px rgba(37, 99, 235, 0.16)" : "0 1px 2px rgba(15, 23, 42, 0.1)",
|
|
1470
|
+
...props.attrs.style
|
|
838
1471
|
},
|
|
839
|
-
children: String(label ?? props.node.id)
|
|
1472
|
+
children: [/* @__PURE__ */ jsx("span", { children: String(label ?? props.node.id) }), badges.length ? /* @__PURE__ */ jsx("span", {
|
|
1473
|
+
style: defaultBadgeListStyle,
|
|
1474
|
+
children: badges
|
|
1475
|
+
}) : null]
|
|
840
1476
|
});
|
|
841
1477
|
}
|
|
1478
|
+
const defaultBadgeListStyle = {
|
|
1479
|
+
position: "absolute",
|
|
1480
|
+
top: -8,
|
|
1481
|
+
right: -8,
|
|
1482
|
+
display: "flex",
|
|
1483
|
+
gap: 4,
|
|
1484
|
+
alignItems: "center"
|
|
1485
|
+
};
|
|
1486
|
+
function decorationsToArray(value) {
|
|
1487
|
+
if (value === void 0) return [];
|
|
1488
|
+
return Array.isArray(value) ? value : [value];
|
|
1489
|
+
}
|
|
1490
|
+
function decorationTitle(value) {
|
|
1491
|
+
if (value === void 0) return void 0;
|
|
1492
|
+
const text = (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string" || typeof item === "number").map(String);
|
|
1493
|
+
return text.length ? text.join("\n") : void 0;
|
|
1494
|
+
}
|
|
842
1495
|
//#endregion
|
|
843
1496
|
//#region src/FlowToolbar.tsx
|
|
844
1497
|
/** Border-box size of an element, kept current via ResizeObserver. */
|
|
@@ -878,7 +1531,7 @@ function useFlowToolbar(options = {}) {
|
|
|
878
1531
|
const anchor = useFlowSelector((context) => {
|
|
879
1532
|
const ids = options.ids ?? [...context.selection];
|
|
880
1533
|
if (ids.length === 0) return null;
|
|
881
|
-
const canvasRect = flow.engine.getToolbarAnchor(ids);
|
|
1534
|
+
const canvasRect = flow.internals.engine.getToolbarAnchor(ids);
|
|
882
1535
|
if (!canvasRect) return null;
|
|
883
1536
|
const zoom = context.viewport.zoom;
|
|
884
1537
|
return {
|
|
@@ -973,7 +1626,7 @@ function MiniMap(props) {
|
|
|
973
1626
|
x: event.clientX - rect.left,
|
|
974
1627
|
y: event.clientY - rect.top
|
|
975
1628
|
});
|
|
976
|
-
flow.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
|
|
1629
|
+
flow.internals.store.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
|
|
977
1630
|
};
|
|
978
1631
|
return /* @__PURE__ */ jsxs("svg", {
|
|
979
1632
|
"aria-label": "minimap",
|
|
@@ -1007,7 +1660,7 @@ function MiniMap(props) {
|
|
|
1007
1660
|
...props.style
|
|
1008
1661
|
},
|
|
1009
1662
|
children: [nodes.map((node) => {
|
|
1010
|
-
const rect = projection.rectToMinimap(flow.engine.
|
|
1663
|
+
const rect = projection.rectToMinimap(flow.internals.engine.visualBounds(node.id));
|
|
1011
1664
|
return props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
|
|
1012
1665
|
x: rect.x,
|
|
1013
1666
|
y: rect.y,
|
|
@@ -1028,6 +1681,60 @@ function MiniMap(props) {
|
|
|
1028
1681
|
});
|
|
1029
1682
|
}
|
|
1030
1683
|
//#endregion
|
|
1684
|
+
//#region src/Panel.tsx
|
|
1685
|
+
const placement = {
|
|
1686
|
+
"top-left": {
|
|
1687
|
+
top: 12,
|
|
1688
|
+
left: 12
|
|
1689
|
+
},
|
|
1690
|
+
"top-center": {
|
|
1691
|
+
top: 12,
|
|
1692
|
+
left: "50%",
|
|
1693
|
+
transform: "translateX(-50%)"
|
|
1694
|
+
},
|
|
1695
|
+
"top-right": {
|
|
1696
|
+
top: 12,
|
|
1697
|
+
right: 12
|
|
1698
|
+
},
|
|
1699
|
+
"center-left": {
|
|
1700
|
+
top: "50%",
|
|
1701
|
+
left: 12,
|
|
1702
|
+
transform: "translateY(-50%)"
|
|
1703
|
+
},
|
|
1704
|
+
"center-right": {
|
|
1705
|
+
top: "50%",
|
|
1706
|
+
right: 12,
|
|
1707
|
+
transform: "translateY(-50%)"
|
|
1708
|
+
},
|
|
1709
|
+
"bottom-left": {
|
|
1710
|
+
bottom: 12,
|
|
1711
|
+
left: 12
|
|
1712
|
+
},
|
|
1713
|
+
"bottom-center": {
|
|
1714
|
+
bottom: 12,
|
|
1715
|
+
left: "50%",
|
|
1716
|
+
transform: "translateX(-50%)"
|
|
1717
|
+
},
|
|
1718
|
+
"bottom-right": {
|
|
1719
|
+
bottom: 12,
|
|
1720
|
+
right: 12
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
function Panel(props) {
|
|
1724
|
+
const { position = "top-left", style, ...rest } = props;
|
|
1725
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1726
|
+
"data-flow-panel": position,
|
|
1727
|
+
...rest,
|
|
1728
|
+
style: {
|
|
1729
|
+
position: "absolute",
|
|
1730
|
+
pointerEvents: "auto",
|
|
1731
|
+
...placement[position],
|
|
1732
|
+
...style
|
|
1733
|
+
},
|
|
1734
|
+
children: props.children
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
//#endregion
|
|
1031
1738
|
//#region src/Controls.tsx
|
|
1032
1739
|
const buttonStyle = {
|
|
1033
1740
|
width: 28,
|
|
@@ -1047,18 +1754,15 @@ const buttonStyle = {
|
|
|
1047
1754
|
*/
|
|
1048
1755
|
function Controls(props) {
|
|
1049
1756
|
const flow = useFlowInstance();
|
|
1050
|
-
return /* @__PURE__ */ jsxs(
|
|
1757
|
+
return /* @__PURE__ */ jsxs(Panel, {
|
|
1051
1758
|
role: "toolbar",
|
|
1052
1759
|
"aria-label": "canvas controls",
|
|
1760
|
+
position: props.position ?? "bottom-left",
|
|
1053
1761
|
className: props.className,
|
|
1054
1762
|
style: {
|
|
1055
|
-
position: "absolute",
|
|
1056
|
-
left: 12,
|
|
1057
|
-
bottom: 12,
|
|
1058
1763
|
display: "flex",
|
|
1059
1764
|
flexDirection: "column",
|
|
1060
1765
|
gap: 4,
|
|
1061
|
-
pointerEvents: "auto",
|
|
1062
1766
|
...props.style
|
|
1063
1767
|
},
|
|
1064
1768
|
children: [
|
|
@@ -1066,21 +1770,21 @@ function Controls(props) {
|
|
|
1066
1770
|
type: "button",
|
|
1067
1771
|
"aria-label": "zoom in",
|
|
1068
1772
|
style: buttonStyle,
|
|
1069
|
-
onClick: () => flow.trigger.zoomBy({ factor: 1.2 }),
|
|
1773
|
+
onClick: () => flow.internals.store.trigger.zoomBy({ factor: 1.2 }),
|
|
1070
1774
|
children: "+"
|
|
1071
1775
|
}),
|
|
1072
1776
|
/* @__PURE__ */ jsx("button", {
|
|
1073
1777
|
type: "button",
|
|
1074
1778
|
"aria-label": "zoom out",
|
|
1075
1779
|
style: buttonStyle,
|
|
1076
|
-
onClick: () => flow.trigger.zoomBy({ factor: 1 / 1.2 }),
|
|
1780
|
+
onClick: () => flow.internals.store.trigger.zoomBy({ factor: 1 / 1.2 }),
|
|
1077
1781
|
children: "−"
|
|
1078
1782
|
}),
|
|
1079
1783
|
/* @__PURE__ */ jsx("button", {
|
|
1080
1784
|
type: "button",
|
|
1081
1785
|
"aria-label": "fit view",
|
|
1082
1786
|
style: buttonStyle,
|
|
1083
|
-
onClick: () => flow.trigger.fitView({ padding: .1 }),
|
|
1787
|
+
onClick: () => flow.internals.store.trigger.fitView({ padding: .1 }),
|
|
1084
1788
|
children: "⛶"
|
|
1085
1789
|
}),
|
|
1086
1790
|
props.children
|
|
@@ -1090,22 +1794,84 @@ function Controls(props) {
|
|
|
1090
1794
|
//#endregion
|
|
1091
1795
|
//#region src/EntityAnchor.tsx
|
|
1092
1796
|
function EntityAnchor(props) {
|
|
1797
|
+
if (props.isVisible === false) return null;
|
|
1798
|
+
if ("bounds" in props && props.bounds) return /* @__PURE__ */ jsx(CanvasEntityAnchor, { ...props });
|
|
1799
|
+
return /* @__PURE__ */ jsx(RelativeEntityAnchor, { ...props });
|
|
1800
|
+
}
|
|
1801
|
+
function CanvasEntityAnchor(props) {
|
|
1093
1802
|
const viewport = useFlowSelector((context) => context.viewport);
|
|
1094
1803
|
const transform = getAnchorTransform(props.bounds, viewport, props.anchor);
|
|
1804
|
+
const { bounds: _bounds, anchor: _anchor, inverseScale, isVisible: _isVisible, children, className, style, ...divProps } = props;
|
|
1095
1805
|
return /* @__PURE__ */ jsx("div", {
|
|
1096
|
-
|
|
1806
|
+
...divProps,
|
|
1807
|
+
"data-flow-entity-anchor": "",
|
|
1808
|
+
className: classNames("stately-flow__entity-anchor", className),
|
|
1097
1809
|
style: {
|
|
1098
1810
|
position: "absolute",
|
|
1099
1811
|
left: transform.x,
|
|
1100
1812
|
top: transform.y,
|
|
1101
|
-
transform:
|
|
1813
|
+
transform: inverseScale ? `translate(-50%, -50%) scale(${transform.scale})` : "translate(-50%, -50%)",
|
|
1102
1814
|
transformOrigin: "center",
|
|
1103
1815
|
pointerEvents: "auto",
|
|
1104
|
-
...
|
|
1816
|
+
...style
|
|
1105
1817
|
},
|
|
1106
|
-
children
|
|
1818
|
+
children
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
function RelativeEntityAnchor(props) {
|
|
1822
|
+
const { side, anchor = defaultAnchorPoint(props.side), align = "center", offset = 0, inverseScale, isVisible: _isVisible, children, className, style, ...divProps } = props;
|
|
1823
|
+
const positionStyle = relativePositionStyle(side, align, anchor, offset, !!inverseScale);
|
|
1824
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1825
|
+
...divProps,
|
|
1826
|
+
"data-flow-entity-anchor": "",
|
|
1827
|
+
"data-flow-entity-anchor-side": side,
|
|
1828
|
+
className: classNames("stately-flow__entity-anchor", `stately-flow__entity-anchor-${side}`, className),
|
|
1829
|
+
style: {
|
|
1830
|
+
position: "absolute",
|
|
1831
|
+
zIndex: 20,
|
|
1832
|
+
pointerEvents: "auto",
|
|
1833
|
+
...positionStyle,
|
|
1834
|
+
transformOrigin: inverseScale ? anchorTransformOrigin(anchor) : void 0,
|
|
1835
|
+
...style
|
|
1836
|
+
},
|
|
1837
|
+
children
|
|
1107
1838
|
});
|
|
1108
1839
|
}
|
|
1840
|
+
function relativePositionStyle(side, align, anchor, offset, inverseScale) {
|
|
1841
|
+
const crossAxis = align === "start" ? "0%" : align === "end" ? "100%" : "50%";
|
|
1842
|
+
const translate = anchorTranslate(anchor);
|
|
1843
|
+
const signedOffset = side === "top" || side === "left" ? -offset : offset;
|
|
1844
|
+
const offsetValue = inverseScale ? `calc(${signedOffset}px * var(--flow-inverse-zoom, 1))` : `${signedOffset}px`;
|
|
1845
|
+
const offsetTransform = side === "top" || side === "bottom" ? `translateY(${offsetValue})` : `translateX(${offsetValue})`;
|
|
1846
|
+
const scaleTransform = inverseScale ? " scale(var(--flow-inverse-zoom, 1))" : "";
|
|
1847
|
+
return {
|
|
1848
|
+
left: side === "left" ? 0 : side === "right" ? "100%" : crossAxis,
|
|
1849
|
+
top: side === "top" ? 0 : side === "bottom" ? "100%" : crossAxis,
|
|
1850
|
+
transform: `translate(${translate.x}, ${translate.y}) ${offsetTransform}${scaleTransform}`
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
function defaultAnchorPoint(side) {
|
|
1854
|
+
switch (side) {
|
|
1855
|
+
case "top": return "bottom-center";
|
|
1856
|
+
case "bottom": return "top-center";
|
|
1857
|
+
case "left": return "center-right";
|
|
1858
|
+
case "right": return "center-left";
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function anchorTranslate(anchor) {
|
|
1862
|
+
const [vertical, horizontal] = anchor === "center" ? ["center", "center"] : anchor.split("-");
|
|
1863
|
+
return {
|
|
1864
|
+
x: horizontal === "left" ? "0%" : horizontal === "right" ? "-100%" : "-50%",
|
|
1865
|
+
y: vertical === "top" ? "0%" : vertical === "bottom" ? "-100%" : "-50%"
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
function anchorTransformOrigin(anchor) {
|
|
1869
|
+
const [vertical, horizontal] = anchor === "center" ? ["center", "center"] : anchor.split("-");
|
|
1870
|
+
return `${horizontal} ${vertical}`;
|
|
1871
|
+
}
|
|
1872
|
+
function classNames(...values) {
|
|
1873
|
+
return values.filter(Boolean).join(" ");
|
|
1874
|
+
}
|
|
1109
1875
|
//#endregion
|
|
1110
1876
|
//#region src/Port.tsx
|
|
1111
1877
|
/**
|
|
@@ -1123,11 +1889,11 @@ function Port(props) {
|
|
|
1123
1889
|
const { node } = useNodeRenderContext();
|
|
1124
1890
|
const ref = useRef(null);
|
|
1125
1891
|
const observerRef = useRef(null);
|
|
1892
|
+
const observer = useMeasurementObserver();
|
|
1126
1893
|
const measured = props.measured ?? true;
|
|
1127
1894
|
useLayoutEffect(() => {
|
|
1128
1895
|
const element = ref.current;
|
|
1129
1896
|
if (!element || !measured) return;
|
|
1130
|
-
const observer = createMeasurementObserver(flow.store);
|
|
1131
1897
|
observerRef.current = observer;
|
|
1132
1898
|
observer.observe(element, {
|
|
1133
1899
|
nodeId: node.id,
|
|
@@ -1135,10 +1901,10 @@ function Port(props) {
|
|
|
1135
1901
|
});
|
|
1136
1902
|
return () => {
|
|
1137
1903
|
observerRef.current = null;
|
|
1138
|
-
observer.
|
|
1904
|
+
observer.unobserve(element);
|
|
1139
1905
|
};
|
|
1140
1906
|
}, [
|
|
1141
|
-
|
|
1907
|
+
observer,
|
|
1142
1908
|
node.id,
|
|
1143
1909
|
props.name,
|
|
1144
1910
|
measured
|
|
@@ -1166,6 +1932,128 @@ function Port(props) {
|
|
|
1166
1932
|
});
|
|
1167
1933
|
}
|
|
1168
1934
|
//#endregion
|
|
1935
|
+
//#region src/DragHandle.tsx
|
|
1936
|
+
/**
|
|
1937
|
+
* Marks a region inside a custom node as the *only* draggable area. When a node
|
|
1938
|
+
* renders a `<DragHandle>`, pointer presses elsewhere on the node no longer
|
|
1939
|
+
* start a drag (they select/pass through) — the input runtime only begins a
|
|
1940
|
+
* move when the press lands inside an element carrying `data-flow-drag-handle`.
|
|
1941
|
+
*
|
|
1942
|
+
* Render it anywhere inside your node's markup:
|
|
1943
|
+
*
|
|
1944
|
+
* ```tsx
|
|
1945
|
+
* function MyNode(props) {
|
|
1946
|
+
* return (
|
|
1947
|
+
* <div>
|
|
1948
|
+
* <DragHandle>⠿ title bar</DragHandle>
|
|
1949
|
+
* <div>…non-draggable body…</div>
|
|
1950
|
+
* </div>
|
|
1951
|
+
* );
|
|
1952
|
+
* }
|
|
1953
|
+
* ```
|
|
1954
|
+
*/
|
|
1955
|
+
function DragHandle({ children, style, ...rest }) {
|
|
1956
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1957
|
+
"data-flow-drag-handle": "",
|
|
1958
|
+
style: {
|
|
1959
|
+
cursor: "grab",
|
|
1960
|
+
...style
|
|
1961
|
+
},
|
|
1962
|
+
...rest,
|
|
1963
|
+
children
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
//#endregion
|
|
1967
|
+
//#region src/NodeResizer.tsx
|
|
1968
|
+
const DEFAULT_HANDLES = [
|
|
1969
|
+
"top",
|
|
1970
|
+
"right",
|
|
1971
|
+
"bottom",
|
|
1972
|
+
"left",
|
|
1973
|
+
"top-left",
|
|
1974
|
+
"top-right",
|
|
1975
|
+
"bottom-left",
|
|
1976
|
+
"bottom-right"
|
|
1977
|
+
];
|
|
1978
|
+
const HANDLE_STYLE = {
|
|
1979
|
+
top: {
|
|
1980
|
+
left: "50%",
|
|
1981
|
+
top: 0,
|
|
1982
|
+
cursor: "ns-resize"
|
|
1983
|
+
},
|
|
1984
|
+
right: {
|
|
1985
|
+
left: "100%",
|
|
1986
|
+
top: "50%",
|
|
1987
|
+
cursor: "ew-resize"
|
|
1988
|
+
},
|
|
1989
|
+
bottom: {
|
|
1990
|
+
left: "50%",
|
|
1991
|
+
top: "100%",
|
|
1992
|
+
cursor: "ns-resize"
|
|
1993
|
+
},
|
|
1994
|
+
left: {
|
|
1995
|
+
left: 0,
|
|
1996
|
+
top: "50%",
|
|
1997
|
+
cursor: "ew-resize"
|
|
1998
|
+
},
|
|
1999
|
+
"top-left": {
|
|
2000
|
+
left: 0,
|
|
2001
|
+
top: 0,
|
|
2002
|
+
cursor: "nwse-resize"
|
|
2003
|
+
},
|
|
2004
|
+
"top-right": {
|
|
2005
|
+
left: "100%",
|
|
2006
|
+
top: 0,
|
|
2007
|
+
cursor: "nesw-resize"
|
|
2008
|
+
},
|
|
2009
|
+
"bottom-left": {
|
|
2010
|
+
left: 0,
|
|
2011
|
+
top: "100%",
|
|
2012
|
+
cursor: "nesw-resize"
|
|
2013
|
+
},
|
|
2014
|
+
"bottom-right": {
|
|
2015
|
+
left: "100%",
|
|
2016
|
+
top: "100%",
|
|
2017
|
+
cursor: "nwse-resize"
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
function NodeResizer(props) {
|
|
2021
|
+
const { node } = useNodeRenderContext();
|
|
2022
|
+
const selected = useFlowSelector((context) => context.selection.has(node.id));
|
|
2023
|
+
if (!(props.isVisible ?? selected)) return null;
|
|
2024
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2025
|
+
"aria-hidden": "true",
|
|
2026
|
+
className: props.className,
|
|
2027
|
+
"data-flow-node-resizer": "",
|
|
2028
|
+
style: {
|
|
2029
|
+
position: "absolute",
|
|
2030
|
+
inset: 0,
|
|
2031
|
+
pointerEvents: "none",
|
|
2032
|
+
...props.style
|
|
2033
|
+
},
|
|
2034
|
+
children: (props.handles ?? DEFAULT_HANDLES).map((handle) => /* @__PURE__ */ jsx("div", {
|
|
2035
|
+
"data-entity-id": node.id,
|
|
2036
|
+
"data-flow-part": `resize-handle:${handle}`,
|
|
2037
|
+
"data-flow-resize-handle": handle,
|
|
2038
|
+
style: {
|
|
2039
|
+
position: "absolute",
|
|
2040
|
+
width: "calc(10px / var(--flow-zoom, 1))",
|
|
2041
|
+
height: "calc(10px / var(--flow-zoom, 1))",
|
|
2042
|
+
minWidth: 6,
|
|
2043
|
+
minHeight: 6,
|
|
2044
|
+
boxSizing: "border-box",
|
|
2045
|
+
border: "calc(1px / var(--flow-zoom, 1)) solid #ffffff",
|
|
2046
|
+
borderRadius: "calc(2px / var(--flow-zoom, 1))",
|
|
2047
|
+
background: "var(--flow-selection-color, #2563eb)",
|
|
2048
|
+
boxShadow: "0 0 0 calc(1px / var(--flow-zoom, 1)) rgba(15, 23, 42, 0.8)",
|
|
2049
|
+
pointerEvents: "auto",
|
|
2050
|
+
transform: "translate(-50%, -50%)",
|
|
2051
|
+
...HANDLE_STYLE[handle]
|
|
2052
|
+
}
|
|
2053
|
+
}, handle))
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
//#endregion
|
|
1169
2057
|
//#region src/ColorMode.tsx
|
|
1170
2058
|
function FlowColorModeScope(props) {
|
|
1171
2059
|
const { colorMode, resolvedColorMode } = useFlowColorMode();
|
|
@@ -1210,4 +2098,4 @@ function useApplyFlowColorMode(element) {
|
|
|
1210
2098
|
]);
|
|
1211
2099
|
}
|
|
1212
2100
|
//#endregion
|
|
1213
|
-
export { Controls, EntityAnchor, FlowColorModeScope, FlowInstanceProvider, FlowToolbar, MiniMap, Port, StatelyFlow, createFlowInstance, useApplyFlowColorMode, useCreateFlowInstance, useFlow, useFlowColorMode, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowViewport, useMeasuredSize, useOptionalFlowInstance };
|
|
2101
|
+
export { Background, Controls, CrossBackground, DotsBackground, DragHandle, EntityAnchor, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, FlowInstanceProvider, FlowToolbar, LinesBackground, MiniMap, NodeResizer, PanePortal, Panel, Port, StatelyFlow, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };
|