@statelyai/flow-react 0.5.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 +56 -11
- 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 +207 -36
- package/dist/esm/index.js +1110 -221
- package/package.json +17 -13
- package/dist/esm/FlowInstanceContext.js +0 -20
package/dist/esm/index.js
CHANGED
|
@@ -1,44 +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";
|
|
8
|
+
export * from "@statelyai/flow-dom";
|
|
6
9
|
//#region src/createFlowInstance.ts
|
|
7
10
|
function createFlowInstance(options) {
|
|
11
|
+
const { keymap, ...createFlowSettings } = options.settings ?? {};
|
|
8
12
|
const flow = createFlow({
|
|
9
|
-
graph: options.
|
|
10
|
-
...
|
|
13
|
+
graph: options.initialGraph,
|
|
14
|
+
...createFlowSettings
|
|
11
15
|
});
|
|
16
|
+
const inputSettings = { keymap };
|
|
12
17
|
const cleanup = /* @__PURE__ */ new Set();
|
|
13
18
|
function getSnapshot() {
|
|
14
|
-
return flow.
|
|
19
|
+
return flow.getSnapshot();
|
|
15
20
|
}
|
|
16
|
-
function clientToCanvas(point) {
|
|
17
|
-
|
|
18
|
-
return flow.screenToCanvas({
|
|
19
|
-
x: point.x - (rect?.left ?? 0),
|
|
20
|
-
y: point.y - (rect?.top ?? 0)
|
|
21
|
-
});
|
|
21
|
+
function clientToCanvas$1(point) {
|
|
22
|
+
return clientToCanvas(flow.internals.store, point);
|
|
22
23
|
}
|
|
23
24
|
function listen(selector, listener, listenerOptions = {}) {
|
|
24
25
|
const isEqual = listenerOptions.isEqual ?? Object.is;
|
|
25
26
|
let previousValue = selector(getSnapshot());
|
|
26
27
|
if (listenerOptions.fireImmediately) listener(previousValue, previousValue, getSnapshot());
|
|
27
|
-
const unsubscribe = flow.subscribe((
|
|
28
|
-
const nextValue = selector(context);
|
|
29
|
-
if (isEqual(previousValue, nextValue)) return;
|
|
30
|
-
const currentPreviousValue = previousValue;
|
|
28
|
+
const unsubscribe = flow.subscribe(selector, (nextValue, currentPreviousValue) => {
|
|
31
29
|
previousValue = nextValue;
|
|
32
|
-
listener(nextValue, currentPreviousValue,
|
|
33
|
-
});
|
|
30
|
+
listener(nextValue, currentPreviousValue, getSnapshot());
|
|
31
|
+
}, { equals: isEqual });
|
|
34
32
|
cleanup.add(unsubscribe);
|
|
35
33
|
return () => {
|
|
36
34
|
cleanup.delete(unsubscribe);
|
|
37
35
|
unsubscribe();
|
|
38
36
|
};
|
|
39
37
|
}
|
|
38
|
+
const configure = (next) => {
|
|
39
|
+
flow.configure(next);
|
|
40
|
+
};
|
|
40
41
|
function subscribeEvent(event, listener) {
|
|
41
|
-
const subscription = flow.on(event, listener);
|
|
42
|
+
const subscription = event === "commit" ? flow.on("commit", listener) : flow.on(event, listener);
|
|
42
43
|
const unsubscribe = () => subscription.unsubscribe();
|
|
43
44
|
cleanup.add(unsubscribe);
|
|
44
45
|
return () => {
|
|
@@ -48,12 +49,16 @@ function createFlowInstance(options) {
|
|
|
48
49
|
}
|
|
49
50
|
if (options.events?.connectionEnded) subscribeEvent("connectionEnded", options.events.connectionEnded);
|
|
50
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);
|
|
51
54
|
for (const listener of options.listeners ?? []) listen(listener.selector, listener.onChange, listener);
|
|
52
55
|
return {
|
|
53
56
|
...flow,
|
|
54
57
|
getSnapshot,
|
|
58
|
+
configure,
|
|
55
59
|
listen,
|
|
56
|
-
clientToCanvas,
|
|
60
|
+
clientToCanvas: clientToCanvas$1,
|
|
61
|
+
inputSettings,
|
|
57
62
|
destroy() {
|
|
58
63
|
for (const unsubscribe of cleanup) unsubscribe();
|
|
59
64
|
cleanup.clear();
|
|
@@ -66,25 +71,19 @@ function createFlowInstance(options) {
|
|
|
66
71
|
function useFlow(options) {
|
|
67
72
|
return useCreateFlowInstance(options);
|
|
68
73
|
}
|
|
74
|
+
/** @deprecated Use `useFlow(options)`. */
|
|
69
75
|
function useCreateFlowInstance(options) {
|
|
70
76
|
const instanceRef = useRef(null);
|
|
71
77
|
const listenersRef = useRef(options.listeners);
|
|
72
78
|
const eventsRef = useRef(options.events);
|
|
73
|
-
const
|
|
79
|
+
const lifecycleRef = useRef(0);
|
|
74
80
|
listenersRef.current = options.listeners;
|
|
75
81
|
eventsRef.current = options.events;
|
|
76
82
|
if (!instanceRef.current) instanceRef.current = createFlowInstance({
|
|
77
|
-
|
|
83
|
+
initialGraph: options.initialGraph,
|
|
78
84
|
settings: options.settings
|
|
79
85
|
});
|
|
80
86
|
const flow = instanceRef.current;
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (!didMountRef.current) {
|
|
83
|
-
didMountRef.current = true;
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
flow.trigger.setGraph({ graph: options.graph });
|
|
87
|
-
}, [flow, options.graph]);
|
|
88
87
|
useEffect(() => {
|
|
89
88
|
const cleanup = [];
|
|
90
89
|
if (eventsRef.current?.connectionEnded) {
|
|
@@ -95,6 +94,14 @@ function useCreateFlowInstance(options) {
|
|
|
95
94
|
const subscription = flow.on("layoutRequested", eventsRef.current.layoutRequested);
|
|
96
95
|
cleanup.push(() => subscription.unsubscribe());
|
|
97
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
|
+
}
|
|
98
105
|
for (const listener of listenersRef.current ?? []) cleanup.push(flow.listen(listener.selector, listener.onChange, listener));
|
|
99
106
|
return () => {
|
|
100
107
|
for (const dispose of cleanup) dispose();
|
|
@@ -102,40 +109,70 @@ function useCreateFlowInstance(options) {
|
|
|
102
109
|
}, [
|
|
103
110
|
flow,
|
|
104
111
|
options.listeners,
|
|
105
|
-
options.events
|
|
112
|
+
options.events,
|
|
113
|
+
options.onCommit
|
|
106
114
|
]);
|
|
107
|
-
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]);
|
|
108
126
|
return flow;
|
|
109
127
|
}
|
|
110
128
|
function useFlowSelector(selector, options = {}) {
|
|
111
129
|
const contextFlow = useOptionalFlowInstance();
|
|
112
130
|
const flow = options.flow ?? contextFlow;
|
|
113
131
|
if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
|
|
114
|
-
|
|
115
|
-
const isEqualRef = useRef(options.isEqual ?? Object.is);
|
|
116
|
-
selectorRef.current = selector;
|
|
117
|
-
isEqualRef.current = options.isEqual ?? Object.is;
|
|
118
|
-
const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
setSelected((previous) => {
|
|
121
|
-
const next = selectorRef.current(flow.getSnapshot());
|
|
122
|
-
return isEqualRef.current(previous, next) ? previous : next;
|
|
123
|
-
});
|
|
124
|
-
return flow.listen((context) => selectorRef.current(context), (value) => {
|
|
125
|
-
setSelected((previous) => isEqualRef.current(previous, value) ? previous : value);
|
|
126
|
-
});
|
|
127
|
-
}, [flow]);
|
|
128
|
-
return selected;
|
|
132
|
+
return useSyncExternalStoreWithSelector(useCallback((listener) => flow.subscribe(() => listener()), [flow]), flow.getSnapshot, flow.getSnapshot, selector, options.isEqual);
|
|
129
133
|
}
|
|
130
134
|
function useFlowContext() {
|
|
131
135
|
return useFlowSelector((context) => context);
|
|
132
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
|
+
}
|
|
133
170
|
const entityNotifiers = /* @__PURE__ */ new WeakMap();
|
|
134
171
|
function getEntityNotifier(flow) {
|
|
135
|
-
let notifier = entityNotifiers.get(flow.store);
|
|
172
|
+
let notifier = entityNotifiers.get(flow.internals.store);
|
|
136
173
|
if (!notifier) {
|
|
137
|
-
notifier = createEntityChangeNotifier(flow.store);
|
|
138
|
-
entityNotifiers.set(flow.store, notifier);
|
|
174
|
+
notifier = createEntityChangeNotifier(flow.internals.store, { getEdgeDependents: flow.internals.engine.getEdgeSourceDependents });
|
|
175
|
+
entityNotifiers.set(flow.internals.store, notifier);
|
|
139
176
|
}
|
|
140
177
|
return notifier;
|
|
141
178
|
}
|
|
@@ -151,15 +188,23 @@ function useFlowEntity(id, selector, options = {}) {
|
|
|
151
188
|
if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
|
|
152
189
|
const selectorRef = useRef(selector);
|
|
153
190
|
const isEqualRef = useRef(options.isEqual ?? Object.is);
|
|
191
|
+
const selectedRef = useRef(void 0);
|
|
192
|
+
const selectedInitializedRef = useRef(false);
|
|
154
193
|
selectorRef.current = selector;
|
|
155
194
|
isEqualRef.current = options.isEqual ?? Object.is;
|
|
156
|
-
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
|
+
});
|
|
157
201
|
useEffect(() => {
|
|
158
202
|
const update = () => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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);
|
|
163
208
|
};
|
|
164
209
|
update();
|
|
165
210
|
return getEntityNotifier(flow).subscribe(id, update);
|
|
@@ -204,7 +249,7 @@ function useFlowEdge(id, selector, isEqual) {
|
|
|
204
249
|
function useFlowPort(id, selector, isEqual) {
|
|
205
250
|
const flow = useFlowInstance();
|
|
206
251
|
return useFlowSelector((context) => {
|
|
207
|
-
const addressable = flow?.
|
|
252
|
+
const addressable = flow?.getEntityRef(id);
|
|
208
253
|
if (!addressable || addressable.kind !== "port") throw new Error(`Flow port "${id}" was not found.`);
|
|
209
254
|
const binding = {
|
|
210
255
|
...addressable.port,
|
|
@@ -243,24 +288,128 @@ function getDerivedFields(context, id, props) {
|
|
|
243
288
|
* and applied to the flow store. This replaces hand-written gesture handlers.
|
|
244
289
|
*/
|
|
245
290
|
function useFlowInput(flow, containerRef, options = {}) {
|
|
246
|
-
const
|
|
247
|
-
|
|
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;
|
|
248
296
|
useEffect(() => {
|
|
249
297
|
const container = containerRef.current;
|
|
250
298
|
if (!container) return;
|
|
251
|
-
const
|
|
299
|
+
const activeKeymap = keymapRef.current;
|
|
252
300
|
const runtime = createFlowInputRuntime({
|
|
253
|
-
store: flow.store,
|
|
254
|
-
engine: flow.engine,
|
|
255
|
-
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 } : {}
|
|
256
306
|
});
|
|
257
307
|
return attachInput({
|
|
258
308
|
container,
|
|
259
|
-
store: flow.store,
|
|
309
|
+
store: flow.internals.store,
|
|
260
310
|
runtime,
|
|
261
|
-
|
|
311
|
+
keyboardTarget,
|
|
312
|
+
autoPan: (context, screenPoint) => getAutoPanDelta(context, screenPoint, flow.internals.engine)
|
|
262
313
|
});
|
|
263
|
-
}, [
|
|
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
|
+
}, []);
|
|
264
413
|
}
|
|
265
414
|
//#endregion
|
|
266
415
|
//#region src/NodeContext.tsx
|
|
@@ -272,20 +421,56 @@ function useNodeRenderContext() {
|
|
|
272
421
|
return context;
|
|
273
422
|
}
|
|
274
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
|
|
275
448
|
//#region src/StatelyFlow.tsx
|
|
276
449
|
function stringArraysEqual(a, b) {
|
|
277
450
|
if (a.length !== b.length) return false;
|
|
278
451
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
279
452
|
return true;
|
|
280
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
|
+
}
|
|
281
466
|
function useRenderableIds(instance, enabled) {
|
|
282
|
-
return useFlowSelector((context) => computeRenderableIds(instance.engine, context, enabled), { isEqual: renderableIdsEqual });
|
|
467
|
+
return useFlowSelector((context) => computeRenderableIds(instance.internals.engine, context, enabled), { isEqual: renderableIdsEqual });
|
|
283
468
|
}
|
|
284
469
|
function useViewportTransform(ref, instance) {
|
|
285
470
|
useLayoutEffect(() => {
|
|
286
471
|
const el = ref.current;
|
|
287
472
|
if (!el) return;
|
|
288
|
-
return attachViewportTransform(instance.store, el);
|
|
473
|
+
return attachViewportTransform(instance.internals.store, el);
|
|
289
474
|
}, [ref, instance]);
|
|
290
475
|
}
|
|
291
476
|
function StatelyFlow(props) {
|
|
@@ -296,11 +481,12 @@ function StatelyFlow(props) {
|
|
|
296
481
|
return /* @__PURE__ */ jsx(OwnedStatelyFlow, { ...props });
|
|
297
482
|
}
|
|
298
483
|
function OwnedStatelyFlow(props) {
|
|
299
|
-
const flow =
|
|
300
|
-
|
|
484
|
+
const flow = useFlow({
|
|
485
|
+
initialGraph: props.initialGraph,
|
|
301
486
|
settings: props.settings,
|
|
302
487
|
listeners: props.listeners,
|
|
303
|
-
events: props.events
|
|
488
|
+
events: props.events,
|
|
489
|
+
onCommit: props.onCommit
|
|
304
490
|
});
|
|
305
491
|
return /* @__PURE__ */ jsx(StatelyFlowView, {
|
|
306
492
|
...props,
|
|
@@ -309,61 +495,155 @@ function OwnedStatelyFlow(props) {
|
|
|
309
495
|
}
|
|
310
496
|
function StatelyFlowView(props) {
|
|
311
497
|
const containerRef = useRef(null);
|
|
498
|
+
const setContainerRef = useCallback((element) => {
|
|
499
|
+
containerRef.current = element;
|
|
500
|
+
assignRef(props.rootRef, element);
|
|
501
|
+
}, [props.rootRef]);
|
|
312
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]);
|
|
313
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 ?? {};
|
|
314
519
|
useLayoutEffect(() => {
|
|
315
520
|
const element = containerRef.current;
|
|
316
521
|
if (!element) return;
|
|
317
|
-
return attachContainer(props.flow.store, element);
|
|
522
|
+
return attachContainer(props.flow.internals.store, element);
|
|
318
523
|
}, [props.flow]);
|
|
319
524
|
useFlowInput(props.flow, containerRef, {
|
|
320
525
|
modes: props.inputModes,
|
|
321
|
-
profile: props.settings?.store?.profile
|
|
526
|
+
profile: props.settings?.store?.profile,
|
|
527
|
+
keyboardTarget: props.keyboardTarget,
|
|
528
|
+
keymap: props.settings?.keymap
|
|
322
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
|
+
]);
|
|
323
549
|
return /* @__PURE__ */ jsx(FlowInstanceProvider, {
|
|
324
550
|
flow: props.flow,
|
|
325
|
-
children: /* @__PURE__ */
|
|
326
|
-
|
|
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
|
-
|
|
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
|
+
})
|
|
359
635
|
})
|
|
360
|
-
|
|
636
|
+
})
|
|
361
637
|
})
|
|
362
638
|
});
|
|
363
639
|
}
|
|
640
|
+
function useActiveProjections(flow, projections) {
|
|
641
|
+
const flowProjections = useSyncExternalStore(flow.projections.subscribe, flow.projections.get, flow.projections.get);
|
|
642
|
+
return projections ?? flowProjections;
|
|
643
|
+
}
|
|
364
644
|
function SnapLinesLayer() {
|
|
365
645
|
const instance = useFlowInstance();
|
|
366
|
-
const snapResult = useFlowSelector(() => instance.engine.getSnapResult());
|
|
646
|
+
const snapResult = useFlowSelector(() => instance.internals.engine.getSnapResult());
|
|
367
647
|
const viewport = useFlowSelector((context) => context.viewport);
|
|
368
648
|
const viewportSize = useFlowSelector((context) => context.viewportSize);
|
|
369
649
|
if (!snapResult) return null;
|
|
@@ -373,11 +653,16 @@ function SnapLinesLayer() {
|
|
|
373
653
|
width: viewportSize.width / viewport.zoom,
|
|
374
654
|
height: viewportSize.height / viewport.zoom
|
|
375
655
|
};
|
|
376
|
-
const strokeWidth = 1
|
|
656
|
+
const strokeWidth = 1 / viewport.zoom;
|
|
377
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))";
|
|
378
662
|
return /* @__PURE__ */ jsx("svg", {
|
|
379
663
|
"aria-hidden": "true",
|
|
380
664
|
"data-flow-canvas-layer": true,
|
|
665
|
+
"data-flow-snap-guides": "",
|
|
381
666
|
style: {
|
|
382
667
|
position: "absolute",
|
|
383
668
|
inset: 0,
|
|
@@ -387,89 +672,128 @@ function SnapLinesLayer() {
|
|
|
387
672
|
pointerEvents: "none"
|
|
388
673
|
},
|
|
389
674
|
children: /* @__PURE__ */ jsxs("g", { children: [snapResult.snapLines.map((line, index) => line.axis === "x" ? /* @__PURE__ */ jsx("line", {
|
|
675
|
+
"data-flow-snap-line": "x",
|
|
390
676
|
x1: line.value,
|
|
391
677
|
y1: visibleBounds.y,
|
|
392
678
|
x2: line.value,
|
|
393
679
|
y2: visibleBounds.y + visibleBounds.height,
|
|
394
|
-
stroke:
|
|
680
|
+
stroke: snapColor,
|
|
395
681
|
strokeWidth,
|
|
396
|
-
strokeDasharray: `${
|
|
682
|
+
strokeDasharray: `${4 / viewport.zoom}`
|
|
397
683
|
}, `snap-x-${index}`) : /* @__PURE__ */ jsx("line", {
|
|
684
|
+
"data-flow-snap-line": "y",
|
|
398
685
|
x1: visibleBounds.x,
|
|
399
686
|
y1: line.value,
|
|
400
687
|
x2: visibleBounds.x + visibleBounds.width,
|
|
401
688
|
y2: line.value,
|
|
402
|
-
stroke:
|
|
689
|
+
stroke: snapColor,
|
|
403
690
|
strokeWidth,
|
|
404
|
-
strokeDasharray: `${
|
|
691
|
+
strokeDasharray: `${4 / viewport.zoom}`
|
|
405
692
|
}, `snap-y-${index}`)), snapResult.spacingGuides?.flatMap((guide, guideIndex) => guide.ranges.flatMap((range, rangeIndex) => {
|
|
406
693
|
const key = `spacing-${guideIndex}-${rangeIndex}`;
|
|
407
694
|
if (guide.axis === "x") return [
|
|
408
695
|
/* @__PURE__ */ jsx("line", {
|
|
696
|
+
"data-flow-spacing-guide": "x",
|
|
409
697
|
x1: range.start,
|
|
410
698
|
y1: guide.position,
|
|
411
699
|
x2: range.end,
|
|
412
700
|
y2: guide.position,
|
|
413
|
-
stroke:
|
|
701
|
+
stroke: spacingColor,
|
|
414
702
|
strokeWidth
|
|
415
703
|
}, `${key}-line`),
|
|
416
704
|
/* @__PURE__ */ jsx("line", {
|
|
417
705
|
x1: range.start,
|
|
418
|
-
y1: guide.position
|
|
706
|
+
y1: guide.position,
|
|
419
707
|
x2: range.start,
|
|
420
|
-
y2: guide.position
|
|
421
|
-
stroke:
|
|
422
|
-
strokeWidth,
|
|
708
|
+
y2: guide.position,
|
|
709
|
+
stroke: spacingColor,
|
|
710
|
+
strokeWidth: capSize,
|
|
423
711
|
strokeLinecap: "round"
|
|
424
712
|
}, `${key}-start`),
|
|
425
713
|
/* @__PURE__ */ jsx("line", {
|
|
426
714
|
x1: range.end,
|
|
427
|
-
y1: guide.position
|
|
715
|
+
y1: guide.position,
|
|
428
716
|
x2: range.end,
|
|
429
|
-
y2: guide.position
|
|
430
|
-
stroke:
|
|
431
|
-
strokeWidth,
|
|
717
|
+
y2: guide.position,
|
|
718
|
+
stroke: spacingColor,
|
|
719
|
+
strokeWidth: capSize,
|
|
432
720
|
strokeLinecap: "round"
|
|
433
|
-
}, `${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`)
|
|
434
731
|
];
|
|
435
732
|
return [
|
|
436
733
|
/* @__PURE__ */ jsx("line", {
|
|
734
|
+
"data-flow-spacing-guide": "y",
|
|
437
735
|
x1: guide.position,
|
|
438
736
|
y1: range.start,
|
|
439
737
|
x2: guide.position,
|
|
440
738
|
y2: range.end,
|
|
441
|
-
stroke:
|
|
739
|
+
stroke: spacingColor,
|
|
442
740
|
strokeWidth
|
|
443
741
|
}, `${key}-line`),
|
|
444
742
|
/* @__PURE__ */ jsx("line", {
|
|
445
|
-
x1: guide.position
|
|
743
|
+
x1: guide.position,
|
|
446
744
|
y1: range.start,
|
|
447
|
-
x2: guide.position
|
|
745
|
+
x2: guide.position,
|
|
448
746
|
y2: range.start,
|
|
449
|
-
stroke:
|
|
450
|
-
strokeWidth,
|
|
747
|
+
stroke: spacingColor,
|
|
748
|
+
strokeWidth: capSize,
|
|
451
749
|
strokeLinecap: "round"
|
|
452
750
|
}, `${key}-start`),
|
|
453
751
|
/* @__PURE__ */ jsx("line", {
|
|
454
|
-
x1: guide.position
|
|
752
|
+
x1: guide.position,
|
|
455
753
|
y1: range.end,
|
|
456
|
-
x2: guide.position
|
|
754
|
+
x2: guide.position,
|
|
457
755
|
y2: range.end,
|
|
458
|
-
stroke:
|
|
459
|
-
strokeWidth,
|
|
756
|
+
stroke: spacingColor,
|
|
757
|
+
strokeWidth: capSize,
|
|
460
758
|
strokeLinecap: "round"
|
|
461
|
-
}, `${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`)
|
|
462
770
|
];
|
|
463
771
|
}))] })
|
|
464
772
|
});
|
|
465
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
|
+
}
|
|
466
789
|
function EdgeLayer(props) {
|
|
467
790
|
const instance = useFlowInstance();
|
|
468
791
|
const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
|
|
469
|
-
const edgeIds = useFlowSelector((context) => context.
|
|
792
|
+
const edgeIds = useFlowSelector((context) => getOrderedEntities(instance, context).edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
|
|
470
793
|
const renderEdge = props.renderEdge;
|
|
471
794
|
return /* @__PURE__ */ jsx("svg", {
|
|
472
795
|
"aria-label": "edges",
|
|
796
|
+
className: "stately-flow__edges",
|
|
473
797
|
"data-flow-canvas-layer": true,
|
|
474
798
|
style: {
|
|
475
799
|
position: "absolute",
|
|
@@ -477,34 +801,77 @@ function EdgeLayer(props) {
|
|
|
477
801
|
width: "100%",
|
|
478
802
|
height: "100%",
|
|
479
803
|
overflow: "visible",
|
|
480
|
-
pointerEvents: "
|
|
804
|
+
pointerEvents: "none"
|
|
481
805
|
},
|
|
482
806
|
children: /* @__PURE__ */ jsxs("g", { children: [edgeIds.map((id) => {
|
|
483
807
|
if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
|
|
484
808
|
return /* @__PURE__ */ jsx(EdgeView, {
|
|
485
809
|
flow: instance,
|
|
810
|
+
geometryObserver: props.geometryObserver,
|
|
486
811
|
id,
|
|
812
|
+
projections: props.projections,
|
|
487
813
|
renderEdge
|
|
488
814
|
}, id);
|
|
489
815
|
}), /* @__PURE__ */ jsx(ConnectionPreviewEdge, {
|
|
490
816
|
flow: instance,
|
|
491
|
-
renderEdge
|
|
817
|
+
renderEdge,
|
|
818
|
+
renderConnectionLine: props.renderConnectionLine
|
|
492
819
|
})] })
|
|
493
820
|
});
|
|
494
821
|
}
|
|
495
822
|
const EdgeView = memo(function EdgeView(props) {
|
|
496
|
-
const
|
|
823
|
+
const ref = useRef(null);
|
|
824
|
+
const { graph, edge, pathData, selected, focused, hovered, highlights } = useFlowEntity(props.id, (context) => ({
|
|
825
|
+
graph: context.graph,
|
|
497
826
|
edge: props.flow.getEdge(props.id) ?? null,
|
|
498
827
|
pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
|
|
828
|
+
geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
|
|
499
829
|
selected: context.selection.has(props.id),
|
|
830
|
+
focused: context.focusedEntityId === props.id,
|
|
500
831
|
hovered: context.hoveredEntityIds.has(props.id),
|
|
501
|
-
highlights: props.flow.engine.getHighlights(props.id)
|
|
502
|
-
}), { 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
|
+
]);
|
|
503
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
|
+
};
|
|
504
865
|
if (props.renderEdge) return /* @__PURE__ */ jsxs("g", {
|
|
866
|
+
ref,
|
|
867
|
+
...focusProps,
|
|
505
868
|
"data-entity-id": props.id,
|
|
506
869
|
"data-flow-selected": selected ? "true" : void 0,
|
|
507
|
-
|
|
870
|
+
className: classNames$1("stately-flow__edge", attrs.className),
|
|
871
|
+
style: {
|
|
872
|
+
pointerEvents: "auto",
|
|
873
|
+
outline: focused ? "2px solid #2563eb" : "none"
|
|
874
|
+
},
|
|
508
875
|
children: [/* @__PURE__ */ jsx(EdgeHitTarget, {
|
|
509
876
|
flow: props.flow,
|
|
510
877
|
edge,
|
|
@@ -514,19 +881,28 @@ const EdgeView = memo(function EdgeView(props) {
|
|
|
514
881
|
flow: props.flow,
|
|
515
882
|
edge,
|
|
516
883
|
pathData,
|
|
884
|
+
attrs,
|
|
517
885
|
selected,
|
|
518
886
|
hovered,
|
|
519
887
|
highlights
|
|
520
888
|
})]
|
|
521
889
|
});
|
|
522
890
|
return /* @__PURE__ */ jsx("g", {
|
|
891
|
+
ref,
|
|
892
|
+
...focusProps,
|
|
523
893
|
"data-entity-id": props.id,
|
|
524
894
|
"data-flow-selected": selected ? "true" : void 0,
|
|
525
|
-
|
|
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
|
+
},
|
|
526
901
|
children: /* @__PURE__ */ jsx(DefaultEdge, {
|
|
527
902
|
flow: props.flow,
|
|
528
903
|
edge,
|
|
529
904
|
pathData,
|
|
905
|
+
attrs,
|
|
530
906
|
selected
|
|
531
907
|
})
|
|
532
908
|
});
|
|
@@ -534,17 +910,22 @@ const EdgeView = memo(function EdgeView(props) {
|
|
|
534
910
|
/** The in-progress connection gesture's preview edge (renders only mid-gesture). */
|
|
535
911
|
function ConnectionPreviewEdge(props) {
|
|
536
912
|
const connection = useFlowSelector((context) => context.connection);
|
|
537
|
-
if (!connection?.active) return null;
|
|
913
|
+
if (!connection?.active || connection.preview === false) return null;
|
|
538
914
|
const previewGraph = createConnectionPreviewGraph(props.flow.getSnapshot().graph, connection);
|
|
539
915
|
const pathData = props.flow.getAllEdgePaths({ graph: previewGraph }).get(CONNECTION_PREVIEW_EDGE_ID);
|
|
540
916
|
const edge = previewGraph.edges.find((candidate) => candidate.id === CONNECTION_PREVIEW_EDGE_ID);
|
|
541
917
|
if (!edge || !pathData) return null;
|
|
542
918
|
return /* @__PURE__ */ jsx("g", {
|
|
543
919
|
style: { pointerEvents: "none" },
|
|
544
|
-
children: props.
|
|
920
|
+
children: props.renderConnectionLine ? props.renderConnectionLine({
|
|
921
|
+
connection,
|
|
922
|
+
pathData,
|
|
923
|
+
isValid: connection.isValid
|
|
924
|
+
}) : props.renderEdge ? props.renderEdge({
|
|
545
925
|
flow: props.flow,
|
|
546
926
|
edge,
|
|
547
927
|
pathData,
|
|
928
|
+
attrs: {},
|
|
548
929
|
selected: false,
|
|
549
930
|
hovered: false,
|
|
550
931
|
highlights: []
|
|
@@ -552,22 +933,26 @@ function ConnectionPreviewEdge(props) {
|
|
|
552
933
|
flow: props.flow,
|
|
553
934
|
edge,
|
|
554
935
|
pathData,
|
|
936
|
+
attrs: {},
|
|
555
937
|
selected: false,
|
|
556
938
|
preview: true
|
|
557
939
|
})
|
|
558
940
|
});
|
|
559
941
|
}
|
|
560
942
|
function DefaultEdge(props) {
|
|
561
|
-
return /* @__PURE__ */
|
|
943
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [props.pathData.segments.map((segment, index) => {
|
|
562
944
|
const d = pathToSVG(segment);
|
|
563
945
|
return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
|
|
564
946
|
d,
|
|
947
|
+
"data-flow-edge-segment": index,
|
|
565
948
|
fill: "none",
|
|
566
|
-
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"),
|
|
567
950
|
strokeDasharray: props.preview ? "6 6" : void 0,
|
|
568
|
-
strokeWidth: props.selected ? 3 : 2
|
|
951
|
+
strokeWidth: props.attrs.strokeWidth ?? (props.selected ? 3 : 2),
|
|
952
|
+
style: props.attrs.style
|
|
569
953
|
}), props.preview ? null : /* @__PURE__ */ jsx("path", {
|
|
570
954
|
d,
|
|
955
|
+
"data-flow-edge-segment": index,
|
|
571
956
|
"data-entity-id": props.edge.id,
|
|
572
957
|
fill: "none",
|
|
573
958
|
stroke: "#000",
|
|
@@ -575,12 +960,18 @@ function DefaultEdge(props) {
|
|
|
575
960
|
strokeWidth: 18,
|
|
576
961
|
pointerEvents: "stroke"
|
|
577
962
|
})] }, index);
|
|
578
|
-
})
|
|
963
|
+
}), props.preview ? null : /* @__PURE__ */ jsx(EdgeEndpointHandles, {
|
|
964
|
+
flow: props.flow,
|
|
965
|
+
edge: props.edge,
|
|
966
|
+
pathData: props.pathData,
|
|
967
|
+
selected: props.selected
|
|
968
|
+
})] });
|
|
579
969
|
}
|
|
580
970
|
function EdgeHitTarget(props) {
|
|
581
|
-
return /* @__PURE__ */
|
|
971
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [props.pathData.segments.map((segment, index) => {
|
|
582
972
|
return /* @__PURE__ */ jsx("path", {
|
|
583
973
|
d: pathToSVG(segment),
|
|
974
|
+
"data-flow-edge-segment": index,
|
|
584
975
|
"data-entity-id": props.edge.id,
|
|
585
976
|
fill: "none",
|
|
586
977
|
stroke: "#000",
|
|
@@ -588,88 +979,225 @@ function EdgeHitTarget(props) {
|
|
|
588
979
|
strokeWidth: 18,
|
|
589
980
|
pointerEvents: "stroke"
|
|
590
981
|
}, index);
|
|
591
|
-
}) });
|
|
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)) });
|
|
592
1015
|
}
|
|
593
1016
|
function EdgeLabelLayer(props) {
|
|
594
1017
|
const instance = useFlowInstance();
|
|
595
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 });
|
|
596
1022
|
return /* @__PURE__ */ jsx("div", {
|
|
597
1023
|
"aria-label": "edge labels",
|
|
1024
|
+
className: "stately-flow__edge-labels",
|
|
598
1025
|
style: {
|
|
599
1026
|
position: "absolute",
|
|
600
1027
|
inset: 0,
|
|
601
1028
|
pointerEvents: "none"
|
|
602
1029
|
},
|
|
603
|
-
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) => {
|
|
604
1040
|
if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
|
|
605
1041
|
return /* @__PURE__ */ jsx(EdgeLabelItem, {
|
|
606
1042
|
flow: instance,
|
|
607
|
-
|
|
1043
|
+
geometryObserver: props.geometryObserver,
|
|
1044
|
+
id,
|
|
1045
|
+
projections: props.projections,
|
|
1046
|
+
renderEdgeLabel
|
|
608
1047
|
}, id);
|
|
609
1048
|
})
|
|
610
1049
|
});
|
|
611
1050
|
}
|
|
612
1051
|
const EdgeLabelItem = memo(function EdgeLabelItem(props) {
|
|
613
|
-
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,
|
|
614
1054
|
edge: props.flow.getEdge(props.id) ?? null,
|
|
615
1055
|
pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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;
|
|
619
1074
|
return /* @__PURE__ */ jsx(EdgeLabelView, {
|
|
620
1075
|
flow: props.flow,
|
|
1076
|
+
geometryObserver: props.geometryObserver,
|
|
621
1077
|
edge,
|
|
622
|
-
|
|
623
|
-
|
|
1078
|
+
attrs,
|
|
1079
|
+
bounds,
|
|
1080
|
+
pathData,
|
|
1081
|
+
selected,
|
|
1082
|
+
dragging,
|
|
1083
|
+
hovered,
|
|
1084
|
+
highlights,
|
|
1085
|
+
projections: props.projections,
|
|
1086
|
+
renderEdgeLabel: props.renderEdgeLabel
|
|
624
1087
|
});
|
|
625
1088
|
});
|
|
626
1089
|
function EdgeLabelView(props) {
|
|
627
1090
|
const ref = useRef(null);
|
|
628
|
-
const observer =
|
|
1091
|
+
const observer = useMeasurementObserver();
|
|
629
1092
|
useLayoutEffect(() => {
|
|
630
1093
|
const element = ref.current;
|
|
631
1094
|
if (!element) return;
|
|
632
1095
|
observer.observe(element, { edgeId: props.edge.id });
|
|
633
|
-
return () => observer.
|
|
1096
|
+
return () => observer.unobserve(element);
|
|
634
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
|
+
]);
|
|
635
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
|
+
};
|
|
636
1148
|
return /* @__PURE__ */ jsx("div", {
|
|
637
1149
|
ref,
|
|
638
1150
|
"data-entity-id": props.edge.id,
|
|
639
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),
|
|
640
1155
|
style: {
|
|
641
1156
|
position: "absolute",
|
|
642
|
-
left:
|
|
643
|
-
top:
|
|
644
|
-
|
|
645
|
-
|
|
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,
|
|
646
1162
|
boxSizing: "border-box",
|
|
647
|
-
|
|
648
|
-
border: `1px solid ${props.selected ? "#2563eb" : "#cbd5e1"}`,
|
|
649
|
-
borderRadius: 999,
|
|
650
|
-
background: "#fff",
|
|
651
|
-
color: "#334155",
|
|
652
|
-
display: "grid",
|
|
653
|
-
placeItems: "center",
|
|
654
|
-
cursor: "grab",
|
|
655
|
-
font: "600 12px/1 system-ui, sans-serif",
|
|
1163
|
+
...defaultStyle,
|
|
656
1164
|
pointerEvents: "auto",
|
|
657
|
-
userSelect: "none"
|
|
1165
|
+
userSelect: "none",
|
|
1166
|
+
opacity: props.attrs.opacity,
|
|
1167
|
+
...props.attrs.style
|
|
658
1168
|
},
|
|
659
|
-
children:
|
|
1169
|
+
children: props.renderEdgeLabel ? customLabel : label
|
|
660
1170
|
});
|
|
661
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
|
+
}
|
|
662
1184
|
function NodeLayer(props) {
|
|
663
1185
|
const instance = useFlowInstance();
|
|
664
1186
|
const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
|
|
665
|
-
const
|
|
666
|
-
const
|
|
667
|
-
return context.graph.nodes.
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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();
|
|
671
1198
|
return /* @__PURE__ */ jsx("div", {
|
|
672
1199
|
"aria-label": "nodes",
|
|
1200
|
+
className: "stately-flow__nodes",
|
|
673
1201
|
"data-flow-canvas-layer": true,
|
|
674
1202
|
style: {
|
|
675
1203
|
position: "absolute",
|
|
@@ -677,35 +1205,100 @@ function NodeLayer(props) {
|
|
|
677
1205
|
transformOrigin: "0 0",
|
|
678
1206
|
pointerEvents: "none"
|
|
679
1207
|
},
|
|
680
|
-
children:
|
|
1208
|
+
children: nodePaintEntries.map(({ id, orderIndex }) => {
|
|
681
1209
|
if (renderableIds && !renderableIds.nodeIds.has(id)) return null;
|
|
682
1210
|
return /* @__PURE__ */ jsx(NodeView, {
|
|
683
1211
|
flow: instance,
|
|
1212
|
+
geometryObserver: props.geometryObserver,
|
|
684
1213
|
id,
|
|
1214
|
+
orderIndex,
|
|
685
1215
|
observer,
|
|
1216
|
+
projections: props.projections,
|
|
686
1217
|
renderNode: props.renderNode
|
|
687
1218
|
}, id);
|
|
688
1219
|
})
|
|
689
1220
|
});
|
|
690
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
|
+
}
|
|
691
1250
|
function rectsShallowEqual(a, b) {
|
|
692
1251
|
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
693
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
|
+
}
|
|
694
1280
|
const NodeView = memo(function NodeView(props) {
|
|
695
1281
|
const ref = useRef(null);
|
|
696
|
-
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) => {
|
|
697
1283
|
const current = props.flow.getNode(props.id) ?? null;
|
|
698
1284
|
return {
|
|
1285
|
+
graph: context.graph,
|
|
699
1286
|
node: current,
|
|
700
|
-
|
|
1287
|
+
geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
|
|
1288
|
+
hidden: !current || props.flow.internals.engine.resolveEntityConfig(current).hidden,
|
|
701
1289
|
selected: context.selection.has(props.id),
|
|
702
|
-
dragging: context.
|
|
1290
|
+
dragging: context.canvasState === "dragging" && context.selection.has(props.id),
|
|
703
1291
|
focused: context.focusedEntityId === props.id,
|
|
704
1292
|
hovered: context.hoveredEntityIds.has(props.id),
|
|
705
|
-
highlights: props.flow.engine.getHighlights(props.id),
|
|
706
|
-
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)
|
|
707
1295
|
};
|
|
708
|
-
}, { 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);
|
|
709
1302
|
useLayoutEffect(() => {
|
|
710
1303
|
const element = ref.current;
|
|
711
1304
|
if (!element) return;
|
|
@@ -719,7 +1312,30 @@ const NodeView = memo(function NodeView(props) {
|
|
|
719
1312
|
props.flow,
|
|
720
1313
|
hidden
|
|
721
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
|
+
]);
|
|
722
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;
|
|
723
1339
|
return /* @__PURE__ */ jsx(NodeRenderContextProvider, {
|
|
724
1340
|
value: {
|
|
725
1341
|
node,
|
|
@@ -729,26 +1345,31 @@ const NodeView = memo(function NodeView(props) {
|
|
|
729
1345
|
ref,
|
|
730
1346
|
"data-entity-id": props.id,
|
|
731
1347
|
"data-flow-selected": selected ? "true" : void 0,
|
|
1348
|
+
className: classNames$1("stately-flow__node", attrs.className),
|
|
732
1349
|
role: "group",
|
|
733
1350
|
"aria-roledescription": "flow node",
|
|
734
|
-
"aria-
|
|
735
|
-
|
|
736
|
-
|
|
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 }),
|
|
737
1355
|
style: {
|
|
738
1356
|
...nodePositionStyle(bounds, {
|
|
739
|
-
autoSize:
|
|
1357
|
+
autoSize: contentOwnsSize,
|
|
740
1358
|
dragging
|
|
741
1359
|
}),
|
|
742
1360
|
cursor: dragging ? "grabbing" : "grab",
|
|
743
1361
|
pointerEvents: "auto",
|
|
744
1362
|
userSelect: "none",
|
|
1363
|
+
zIndex: props.orderIndex,
|
|
745
1364
|
outline: focused ? "2px solid #2563eb" : "none",
|
|
746
|
-
outlineOffset: 2
|
|
1365
|
+
outlineOffset: 2,
|
|
1366
|
+
opacity: attrs.opacity
|
|
747
1367
|
},
|
|
748
1368
|
children: props.renderNode ? props.renderNode({
|
|
749
1369
|
flow: props.flow,
|
|
750
1370
|
node,
|
|
751
1371
|
bounds,
|
|
1372
|
+
attrs,
|
|
752
1373
|
selected,
|
|
753
1374
|
dragging,
|
|
754
1375
|
hovered,
|
|
@@ -757,6 +1378,7 @@ const NodeView = memo(function NodeView(props) {
|
|
|
757
1378
|
flow: props.flow,
|
|
758
1379
|
node,
|
|
759
1380
|
bounds,
|
|
1381
|
+
attrs,
|
|
760
1382
|
selected,
|
|
761
1383
|
dragging,
|
|
762
1384
|
hovered,
|
|
@@ -765,6 +1387,7 @@ const NodeView = memo(function NodeView(props) {
|
|
|
765
1387
|
flow: props.flow,
|
|
766
1388
|
node,
|
|
767
1389
|
bounds,
|
|
1390
|
+
attrs,
|
|
768
1391
|
selected,
|
|
769
1392
|
dragging,
|
|
770
1393
|
hovered,
|
|
@@ -779,8 +1402,10 @@ const NodeView = memo(function NodeView(props) {
|
|
|
779
1402
|
* `engine.getGroupGeometry`; custom renderers can do the same.
|
|
780
1403
|
*/
|
|
781
1404
|
function DefaultGroupNode(props) {
|
|
782
|
-
const geometry = props.flow.engine.getGroupGeometry(props.node.id);
|
|
1405
|
+
const geometry = props.flow.internals.engine.getGroupGeometry(props.node.id);
|
|
783
1406
|
const label = props.node.data?.label;
|
|
1407
|
+
const badges = decorationsToArray(props.attrs.badge);
|
|
1408
|
+
const title = decorationTitle(props.attrs.tooltip);
|
|
784
1409
|
if (!geometry) return /* @__PURE__ */ jsx(DefaultNode, { ...props });
|
|
785
1410
|
const relative = (rect) => ({
|
|
786
1411
|
left: rect.x - props.bounds.x,
|
|
@@ -793,13 +1418,14 @@ function DefaultGroupNode(props) {
|
|
|
793
1418
|
position: "absolute",
|
|
794
1419
|
...relative(geometry.childArea),
|
|
795
1420
|
boxSizing: "border-box",
|
|
796
|
-
border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : "#cbd5e1"}`,
|
|
1421
|
+
border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : props.attrs.color ?? "#cbd5e1"}`,
|
|
797
1422
|
...dropTarget ? { background: "rgba(22, 163, 74, 0.08)" } : null,
|
|
798
1423
|
borderRadius: 10,
|
|
799
1424
|
background: "rgba(148, 163, 184, 0.07)",
|
|
800
1425
|
pointerEvents: "none"
|
|
801
|
-
} }), /* @__PURE__ */
|
|
1426
|
+
} }), /* @__PURE__ */ jsxs("div", {
|
|
802
1427
|
"data-flow-drag-handle": true,
|
|
1428
|
+
title,
|
|
803
1429
|
style: {
|
|
804
1430
|
position: "absolute",
|
|
805
1431
|
...relative(geometry.contentBounds),
|
|
@@ -807,37 +1433,65 @@ function DefaultGroupNode(props) {
|
|
|
807
1433
|
display: "flex",
|
|
808
1434
|
alignItems: "center",
|
|
809
1435
|
padding: "0 12px",
|
|
810
|
-
border: `2px solid ${props.selected ? "#2563eb" : "#94a3b8"}`,
|
|
1436
|
+
border: `2px solid ${props.selected ? "#2563eb" : props.attrs.color ?? "#94a3b8"}`,
|
|
811
1437
|
borderRadius: 8,
|
|
812
1438
|
background: props.selected ? "#eff6ff" : "#f8fafc",
|
|
813
1439
|
color: "#0f172a",
|
|
814
1440
|
font: "600 13px/1.2 system-ui, sans-serif",
|
|
815
1441
|
cursor: props.dragging ? "grabbing" : "grab",
|
|
816
|
-
userSelect: "none"
|
|
1442
|
+
userSelect: "none",
|
|
1443
|
+
...props.attrs.style
|
|
817
1444
|
},
|
|
818
|
-
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]
|
|
819
1449
|
})] });
|
|
820
1450
|
}
|
|
821
1451
|
function DefaultNode(props) {
|
|
822
1452
|
const label = props.node.data?.label;
|
|
823
|
-
|
|
1453
|
+
const badges = decorationsToArray(props.attrs.badge);
|
|
1454
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1455
|
+
title: decorationTitle(props.attrs.tooltip),
|
|
824
1456
|
style: {
|
|
1457
|
+
position: "relative",
|
|
825
1458
|
display: "grid",
|
|
826
1459
|
placeItems: "center",
|
|
827
1460
|
width: "100%",
|
|
828
1461
|
height: "100%",
|
|
829
1462
|
boxSizing: "border-box",
|
|
830
1463
|
padding: "10px 14px",
|
|
831
|
-
border: `2px solid ${props.selected ? "#2563eb" : props.hovered ? "#94a3b8" : "#cbd5e1"}`,
|
|
1464
|
+
border: `2px solid ${props.selected ? "#2563eb" : props.attrs.color ?? (props.hovered ? "#94a3b8" : "#cbd5e1")}`,
|
|
832
1465
|
borderRadius: 8,
|
|
833
1466
|
background: props.selected ? "#eff6ff" : "#ffffff",
|
|
834
1467
|
color: "#0f172a",
|
|
835
1468
|
font: "600 14px/1.2 system-ui, sans-serif",
|
|
836
|
-
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
|
|
837
1471
|
},
|
|
838
|
-
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]
|
|
839
1476
|
});
|
|
840
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
|
+
}
|
|
841
1495
|
//#endregion
|
|
842
1496
|
//#region src/FlowToolbar.tsx
|
|
843
1497
|
/** Border-box size of an element, kept current via ResizeObserver. */
|
|
@@ -877,7 +1531,7 @@ function useFlowToolbar(options = {}) {
|
|
|
877
1531
|
const anchor = useFlowSelector((context) => {
|
|
878
1532
|
const ids = options.ids ?? [...context.selection];
|
|
879
1533
|
if (ids.length === 0) return null;
|
|
880
|
-
const canvasRect = flow.engine.getToolbarAnchor(ids);
|
|
1534
|
+
const canvasRect = flow.internals.engine.getToolbarAnchor(ids);
|
|
881
1535
|
if (!canvasRect) return null;
|
|
882
1536
|
const zoom = context.viewport.zoom;
|
|
883
1537
|
return {
|
|
@@ -972,7 +1626,7 @@ function MiniMap(props) {
|
|
|
972
1626
|
x: event.clientX - rect.left,
|
|
973
1627
|
y: event.clientY - rect.top
|
|
974
1628
|
});
|
|
975
|
-
flow.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
|
|
1629
|
+
flow.internals.store.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
|
|
976
1630
|
};
|
|
977
1631
|
return /* @__PURE__ */ jsxs("svg", {
|
|
978
1632
|
"aria-label": "minimap",
|
|
@@ -1006,7 +1660,7 @@ function MiniMap(props) {
|
|
|
1006
1660
|
...props.style
|
|
1007
1661
|
},
|
|
1008
1662
|
children: [nodes.map((node) => {
|
|
1009
|
-
const rect = projection.rectToMinimap(flow.engine.
|
|
1663
|
+
const rect = projection.rectToMinimap(flow.internals.engine.visualBounds(node.id));
|
|
1010
1664
|
return props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
|
|
1011
1665
|
x: rect.x,
|
|
1012
1666
|
y: rect.y,
|
|
@@ -1027,6 +1681,60 @@ function MiniMap(props) {
|
|
|
1027
1681
|
});
|
|
1028
1682
|
}
|
|
1029
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
|
|
1030
1738
|
//#region src/Controls.tsx
|
|
1031
1739
|
const buttonStyle = {
|
|
1032
1740
|
width: 28,
|
|
@@ -1046,18 +1754,15 @@ const buttonStyle = {
|
|
|
1046
1754
|
*/
|
|
1047
1755
|
function Controls(props) {
|
|
1048
1756
|
const flow = useFlowInstance();
|
|
1049
|
-
return /* @__PURE__ */ jsxs(
|
|
1757
|
+
return /* @__PURE__ */ jsxs(Panel, {
|
|
1050
1758
|
role: "toolbar",
|
|
1051
1759
|
"aria-label": "canvas controls",
|
|
1760
|
+
position: props.position ?? "bottom-left",
|
|
1052
1761
|
className: props.className,
|
|
1053
1762
|
style: {
|
|
1054
|
-
position: "absolute",
|
|
1055
|
-
left: 12,
|
|
1056
|
-
bottom: 12,
|
|
1057
1763
|
display: "flex",
|
|
1058
1764
|
flexDirection: "column",
|
|
1059
1765
|
gap: 4,
|
|
1060
|
-
pointerEvents: "auto",
|
|
1061
1766
|
...props.style
|
|
1062
1767
|
},
|
|
1063
1768
|
children: [
|
|
@@ -1065,21 +1770,21 @@ function Controls(props) {
|
|
|
1065
1770
|
type: "button",
|
|
1066
1771
|
"aria-label": "zoom in",
|
|
1067
1772
|
style: buttonStyle,
|
|
1068
|
-
onClick: () => flow.trigger.zoomBy({ factor: 1.2 }),
|
|
1773
|
+
onClick: () => flow.internals.store.trigger.zoomBy({ factor: 1.2 }),
|
|
1069
1774
|
children: "+"
|
|
1070
1775
|
}),
|
|
1071
1776
|
/* @__PURE__ */ jsx("button", {
|
|
1072
1777
|
type: "button",
|
|
1073
1778
|
"aria-label": "zoom out",
|
|
1074
1779
|
style: buttonStyle,
|
|
1075
|
-
onClick: () => flow.trigger.zoomBy({ factor: 1 / 1.2 }),
|
|
1780
|
+
onClick: () => flow.internals.store.trigger.zoomBy({ factor: 1 / 1.2 }),
|
|
1076
1781
|
children: "−"
|
|
1077
1782
|
}),
|
|
1078
1783
|
/* @__PURE__ */ jsx("button", {
|
|
1079
1784
|
type: "button",
|
|
1080
1785
|
"aria-label": "fit view",
|
|
1081
1786
|
style: buttonStyle,
|
|
1082
|
-
onClick: () => flow.trigger.fitView({ padding: .1 }),
|
|
1787
|
+
onClick: () => flow.internals.store.trigger.fitView({ padding: .1 }),
|
|
1083
1788
|
children: "⛶"
|
|
1084
1789
|
}),
|
|
1085
1790
|
props.children
|
|
@@ -1089,22 +1794,84 @@ function Controls(props) {
|
|
|
1089
1794
|
//#endregion
|
|
1090
1795
|
//#region src/EntityAnchor.tsx
|
|
1091
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) {
|
|
1092
1802
|
const viewport = useFlowSelector((context) => context.viewport);
|
|
1093
1803
|
const transform = getAnchorTransform(props.bounds, viewport, props.anchor);
|
|
1804
|
+
const { bounds: _bounds, anchor: _anchor, inverseScale, isVisible: _isVisible, children, className, style, ...divProps } = props;
|
|
1094
1805
|
return /* @__PURE__ */ jsx("div", {
|
|
1095
|
-
|
|
1806
|
+
...divProps,
|
|
1807
|
+
"data-flow-entity-anchor": "",
|
|
1808
|
+
className: classNames("stately-flow__entity-anchor", className),
|
|
1096
1809
|
style: {
|
|
1097
1810
|
position: "absolute",
|
|
1098
1811
|
left: transform.x,
|
|
1099
1812
|
top: transform.y,
|
|
1100
|
-
transform:
|
|
1813
|
+
transform: inverseScale ? `translate(-50%, -50%) scale(${transform.scale})` : "translate(-50%, -50%)",
|
|
1101
1814
|
transformOrigin: "center",
|
|
1102
1815
|
pointerEvents: "auto",
|
|
1103
|
-
...
|
|
1816
|
+
...style
|
|
1104
1817
|
},
|
|
1105
|
-
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
|
|
1106
1838
|
});
|
|
1107
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
|
+
}
|
|
1108
1875
|
//#endregion
|
|
1109
1876
|
//#region src/Port.tsx
|
|
1110
1877
|
/**
|
|
@@ -1122,11 +1889,11 @@ function Port(props) {
|
|
|
1122
1889
|
const { node } = useNodeRenderContext();
|
|
1123
1890
|
const ref = useRef(null);
|
|
1124
1891
|
const observerRef = useRef(null);
|
|
1892
|
+
const observer = useMeasurementObserver();
|
|
1125
1893
|
const measured = props.measured ?? true;
|
|
1126
1894
|
useLayoutEffect(() => {
|
|
1127
1895
|
const element = ref.current;
|
|
1128
1896
|
if (!element || !measured) return;
|
|
1129
|
-
const observer = createMeasurementObserver(flow.store);
|
|
1130
1897
|
observerRef.current = observer;
|
|
1131
1898
|
observer.observe(element, {
|
|
1132
1899
|
nodeId: node.id,
|
|
@@ -1134,10 +1901,10 @@ function Port(props) {
|
|
|
1134
1901
|
});
|
|
1135
1902
|
return () => {
|
|
1136
1903
|
observerRef.current = null;
|
|
1137
|
-
observer.
|
|
1904
|
+
observer.unobserve(element);
|
|
1138
1905
|
};
|
|
1139
1906
|
}, [
|
|
1140
|
-
|
|
1907
|
+
observer,
|
|
1141
1908
|
node.id,
|
|
1142
1909
|
props.name,
|
|
1143
1910
|
measured
|
|
@@ -1165,6 +1932,128 @@ function Port(props) {
|
|
|
1165
1932
|
});
|
|
1166
1933
|
}
|
|
1167
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
|
|
1168
2057
|
//#region src/ColorMode.tsx
|
|
1169
2058
|
function FlowColorModeScope(props) {
|
|
1170
2059
|
const { colorMode, resolvedColorMode } = useFlowColorMode();
|
|
@@ -1209,4 +2098,4 @@ function useApplyFlowColorMode(element) {
|
|
|
1209
2098
|
]);
|
|
1210
2099
|
}
|
|
1211
2100
|
//#endregion
|
|
1212
|
-
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 };
|