@dschz/solid-flow 0.3.0-next.4 → 1.0.0-next.6
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 +238 -224
- package/dist/index/index.d.ts +42 -8
- package/dist/index/index.js +69 -16
- package/dist/index/index.jsx +69 -16
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,100 +1,69 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
|
|
2
|
+
<!-- the %20%20 suffix works around a banner-generator bug that clips a trailing W -->
|
|
3
|
+
<img src="https://assets.solidjs.com/banner?project=solid-flow%20%20&type=Ecosystem&background=tiles" alt="@dschz/solid-flow banner" />
|
|
3
4
|
</p>
|
|
4
5
|
|
|
5
6
|
# Solid Flow
|
|
6
7
|
|
|
7
8
|
[](LICENSE)
|
|
8
9
|
[](https://www.npmjs.com/package/@dschz/solid-flow)
|
|
10
|
+
[](https://jsr.io/@dschz/solid-flow)
|
|
9
11
|
[](https://bundlephobia.com/package/@dschz/solid-flow)
|
|
10
12
|
[](https://github.com/dsnchz/solid-flow/actions/workflows/ci.yaml)
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
A SolidJS port of [React Flow](https://reactflow.dev/) and [Svelte Flow](https://svelteflow.dev/): build node-based editors, diagrams, and interactive graphs with the same battle-tested gesture system ([@xyflow/system](https://github.com/xyflow/xyflow)) and an API designed around Solid's fine-grained reactivity.
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
## Version pairing
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
| Solid Flow | SolidJS | Status |
|
|
19
|
+
| ---------- | --------------- | ------------------------------- |
|
|
20
|
+
| `1.x` | `solid-js` 2.x | Active development (`next` tag) |
|
|
21
|
+
| `0.2.x` | `solid-js` 1.9+ | Maintenance (fixes) |
|
|
17
22
|
|
|
18
|
-
-
|
|
19
|
-
- [Custom MiniMap Nodes](https://github.com/dsnchz/solid-flow/issues/12): define custom minimap jsx node elements.
|
|
20
|
-
- [Edge Reconnect Anchors](https://github.com/dsnchz/solid-flow/issues/13): the ability to re-connect already connected edges.
|
|
23
|
+
The 1.x line is built for SolidJS 2.0 and its deferred, fine-grained reactive graph; the stable 1.0.0 ships alongside SolidJS 2.0 stable (until then, install with the `next` tag). Keep `solid-js` and `@solidjs/web` on matching 2.0 versions — mixing them breaks at import time. Upgrading from 0.2.x? See [Migrating from 0.2.x](#migrating-from-02x).
|
|
21
24
|
|
|
22
25
|
## Key Features
|
|
23
26
|
|
|
24
|
-
- **Easy to use:**
|
|
25
|
-
- **Customizable:**
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **TypeScript First:** Fully typed API with generic type support and IntelliSense integration
|
|
27
|
+
- **Easy to use:** Zooming and panning, single- and multi-selection of graph elements, and keyboard shortcuts out of the box
|
|
28
|
+
- **Customizable:** Built-in node types (Input, Output, Default, Group) and edge types (Bezier, Straight, Step, SmoothStep) with full support for custom nodes and edges — including edge reconnection via `EdgeReconnectAnchor`
|
|
29
|
+
- **Fine-grained by construction:** A node drag is a handful of DOM attribute writes — no virtual DOM, no component re-renders, no memoization ceremony
|
|
30
|
+
- **Scales to large graphs:** Off-screen elements are viewport-culled by default, and an opt-in `onlyRenderVisibleElements` mode only mounts what's visible — stress-tested at 10,000 nodes
|
|
31
|
+
- **Type-guided data:** Your custom components are the schema — node/edge stores narrow each element's `data` by its `type` field, with autocomplete for type names
|
|
32
|
+
- **Rich plugins:** Background patterns, interactive MiniMap (custom minimap nodes, click handlers), zoom Controls, Node Toolbar, and Node Resizer
|
|
33
|
+
- **Accessible:** Keyboard navigation, screen reader support, ARIA labels, and focus management
|
|
34
|
+
- **SSR-ready:** Renders on the server (a dedicated SSR test lane keeps it that way)
|
|
33
35
|
|
|
34
36
|
## Installation
|
|
35
37
|
|
|
36
|
-
The easiest way to get the latest version of Solid Flow is to install it via npm, yarn or pnpm:
|
|
37
|
-
|
|
38
38
|
```sh
|
|
39
|
-
npm
|
|
40
|
-
pnpm
|
|
41
|
-
yarn
|
|
42
|
-
bun
|
|
39
|
+
npm install @dschz/solid-flow
|
|
40
|
+
pnpm add @dschz/solid-flow
|
|
41
|
+
yarn add @dschz/solid-flow
|
|
42
|
+
bun add @dschz/solid-flow
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
`solid-js` is a peer dependency. Also available on [JSR](https://jsr.io/@dschz/solid-flow).
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
## Quick Start
|
|
48
48
|
|
|
49
49
|
```tsx
|
|
50
50
|
import {
|
|
51
|
-
SolidFlow,
|
|
52
|
-
SolidFlowProvider,
|
|
53
|
-
Controls,
|
|
54
51
|
Background,
|
|
55
|
-
|
|
56
|
-
addEdge,
|
|
57
|
-
type EdgeConnection,
|
|
52
|
+
Controls,
|
|
58
53
|
createEdgeStore,
|
|
59
54
|
createNodeStore,
|
|
60
|
-
type
|
|
55
|
+
type EdgeConnection,
|
|
56
|
+
MiniMap,
|
|
57
|
+
Panel,
|
|
58
|
+
SolidFlow,
|
|
61
59
|
} from "@dschz/solid-flow";
|
|
62
|
-
import "@dschz/solid-flow/styles"; //
|
|
63
|
-
|
|
64
|
-
import { createStore, produce } from "solid-js/store";
|
|
65
|
-
|
|
66
|
-
export default function Page() {
|
|
67
|
-
return (
|
|
68
|
-
<SolidFlowProvider>
|
|
69
|
-
<Flow />
|
|
70
|
-
</SolidFlowProvider>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function Flow() {
|
|
75
|
-
// Can invoke useSolidFlow due to parent Page + SolidFlowProvider wrapper. Contains all helper APIs
|
|
76
|
-
const { .. } = useSolidFlow();
|
|
60
|
+
import "@dschz/solid-flow/styles"; // required styles, import once
|
|
77
61
|
|
|
78
|
-
|
|
62
|
+
export const Flow = () => {
|
|
79
63
|
const [nodes, setNodes] = createNodeStore([
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
data: { label: "Input Node" },
|
|
84
|
-
position: { x: 250, y: 0 },
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: "2",
|
|
88
|
-
type: "default",
|
|
89
|
-
data: { label: "Default Node" },
|
|
90
|
-
position: { x: 100, y: 100 },
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
id: "3",
|
|
94
|
-
type: "output",
|
|
95
|
-
data: { label: "Output Node" },
|
|
96
|
-
position: { x: 250, y: 200 },
|
|
97
|
-
},
|
|
64
|
+
{ id: "1", type: "input", data: { label: "Input" }, position: { x: 250, y: 0 } },
|
|
65
|
+
{ id: "2", type: "default", data: { label: "Default" }, position: { x: 100, y: 100 } },
|
|
66
|
+
{ id: "3", type: "output", data: { label: "Output" }, position: { x: 250, y: 200 } },
|
|
98
67
|
]);
|
|
99
68
|
|
|
100
69
|
const [edges, setEdges] = createEdgeStore([
|
|
@@ -102,234 +71,271 @@ function Flow() {
|
|
|
102
71
|
{ id: "e2-3", source: "2", target: "3" },
|
|
103
72
|
]);
|
|
104
73
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const updateViewport = () => {
|
|
112
|
-
setViewport("x", (prev) => prev + 10);
|
|
74
|
+
// Your stores own which elements exist — adopt new connections into
|
|
75
|
+
// your edge store (see "Who owns the data" below).
|
|
76
|
+
const onConnect = (connection: EdgeConnection) => {
|
|
77
|
+
setEdges((edges) => {
|
|
78
|
+
edges.push(connection);
|
|
79
|
+
});
|
|
113
80
|
};
|
|
114
81
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
(edge) => edge.id === connection.id,
|
|
121
|
-
produce((edge) => {
|
|
122
|
-
edge.animated = true;
|
|
123
|
-
}),
|
|
124
|
-
);
|
|
82
|
+
const nudge = () => {
|
|
83
|
+
// SolidJS 2.0 store writes are drafts: mutate what changed.
|
|
84
|
+
setNodes((nodes) => {
|
|
85
|
+
nodes[0]!.position.x += 20;
|
|
86
|
+
});
|
|
125
87
|
};
|
|
126
88
|
|
|
127
89
|
return (
|
|
128
90
|
<SolidFlow nodes={nodes} edges={edges} onConnect={onConnect} fitView>
|
|
129
91
|
<Controls />
|
|
130
92
|
<MiniMap />
|
|
93
|
+
<Background variant="dots" />
|
|
131
94
|
<Panel position="top-left">
|
|
132
|
-
<button onClick={
|
|
95
|
+
<button onClick={nudge}>Nudge first node</button>
|
|
133
96
|
</Panel>
|
|
134
|
-
<Background variant="dots" />
|
|
135
97
|
</SolidFlow>
|
|
136
98
|
);
|
|
137
|
-
}
|
|
99
|
+
};
|
|
138
100
|
```
|
|
139
101
|
|
|
140
|
-
##
|
|
141
|
-
|
|
142
|
-
### Built-in Node Types
|
|
102
|
+
## Your components are the schema
|
|
143
103
|
|
|
144
|
-
|
|
145
|
-
- **OutputNode** - Nodes with target handles only (ending points)
|
|
146
|
-
- **DefaultNode** - Standard nodes with both source and target handles
|
|
147
|
-
- **GroupNode** - Container nodes for organizing other nodes
|
|
104
|
+
`createNodeStore` / `createEdgeStore` derive each element's `data` type from the component registered for its `type` — the renderer map is the single source of truth, and the stores narrow against it:
|
|
148
105
|
|
|
149
|
-
|
|
106
|
+
```tsx
|
|
107
|
+
import {
|
|
108
|
+
createNodeStore,
|
|
109
|
+
Handle,
|
|
110
|
+
type NodeProps,
|
|
111
|
+
type NodeTypes,
|
|
112
|
+
Position,
|
|
113
|
+
} from "@dschz/solid-flow";
|
|
150
114
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
115
|
+
const CounterNode = (props: NodeProps<{ count: number }, "counter">) => (
|
|
116
|
+
<div style={{ padding: "10px", background: "white", border: "1px solid #333" }}>
|
|
117
|
+
<Handle type="target" position={Position.Top} />
|
|
118
|
+
<div>count: {props.data.count}</div>
|
|
119
|
+
<Handle type="source" position={Position.Bottom} />
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
155
122
|
|
|
156
|
-
|
|
123
|
+
const nodeTypes = { counter: CounterNode } satisfies NodeTypes;
|
|
157
124
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
125
|
+
const [nodes] = createNodeStore<typeof nodeTypes>([
|
|
126
|
+
// "counter" narrows data to { count: number }; built-ins still work
|
|
127
|
+
{ id: "a", type: "counter", data: { count: 1 }, position: { x: 0, y: 0 } },
|
|
128
|
+
{ id: "b", type: "default", data: { label: "hi" }, position: { x: 0, y: 100 } },
|
|
129
|
+
// { id: "c", type: "countr", ... } <-- compile error: unknown type name
|
|
130
|
+
]);
|
|
163
131
|
|
|
164
|
-
|
|
132
|
+
<SolidFlow nodes={nodes} nodeTypes={nodeTypes} />;
|
|
133
|
+
```
|
|
165
134
|
|
|
166
|
-
|
|
135
|
+
The same guided unions are exported as standalone types, so plain arrays, props, and vanilla stores get identical narrowing:
|
|
167
136
|
|
|
168
137
|
```tsx
|
|
169
|
-
|
|
170
|
-
const solidFlow = useSolidFlow();
|
|
138
|
+
import type { SolidFlowEdge, SolidFlowNode } from "@dschz/solid-flow";
|
|
171
139
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
140
|
+
const initialNodes = [
|
|
141
|
+
{ id: "a", type: "counter", data: { count: 1 }, position: { x: 0, y: 0 } },
|
|
142
|
+
] satisfies SolidFlowNode<typeof nodeTypes>[];
|
|
143
|
+
```
|
|
175
144
|
|
|
176
|
-
|
|
177
|
-
const viewport = useViewport();
|
|
145
|
+
## Who owns the data
|
|
178
146
|
|
|
179
|
-
|
|
180
|
-
const connection = useConnection();
|
|
147
|
+
The stores you pass as `nodes` / `edges` props are **controlled** — a deliberate contract:
|
|
181
148
|
|
|
182
|
-
|
|
183
|
-
|
|
149
|
+
- **Your store owns membership.** Which nodes and edges exist is decided by your store. Any write form works: draft mutations update in place (`O(changed)`), and wholesale replacement (`setNodes(() => next)`) re-seeds the flow — rows are keyed by `id`, so surviving elements keep their runtime state.
|
|
150
|
+
- **The flow writes runtime fields onto your rows.** Dragging updates `position`, selection updates `selected`, measurement fills `measured` — on the same objects you provided, so reading your store is always live.
|
|
151
|
+
- **Imperative commands don't write membership back.** `commands.addNodes(...)` and friends update the flow, not your store. To keep an element across a store replacement, adopt it — like the `onConnect` handler in the Quick Start pushing the new connection into the edge store.
|
|
184
152
|
|
|
185
|
-
|
|
186
|
-
const connections = useNodeConnections("node-1");
|
|
153
|
+
**Prefer letting the flow own the data?** Pass `defaultNodes` / `defaultEdges` instead of `nodes` / `edges` for an **uncontrolled** flow (React Flow parity): the arrays seed the flow once (later values are ignored), and membership belongs to the flow — commands like `addNodes` and completed connections persist with no adoption step. Read live state through `useSolidFlow()`'s `flow.nodes` / `flow.edges`. The two axes are independent, so you can control edges while leaving nodes uncontrolled (or vice versa); supplying both props on one axis is a mistake (`nodes` wins, with a dev warning).
|
|
187
154
|
|
|
188
|
-
|
|
189
|
-
// composable custom nodes and edge labels without prop drilling)
|
|
190
|
-
const nodeId = useNodeId();
|
|
191
|
-
const edgeId = useEdgeId();
|
|
192
|
-
```
|
|
155
|
+
## The flow API
|
|
193
156
|
|
|
194
|
-
|
|
157
|
+
`useSolidFlow()` returns `{ flow, commands }` (with `commands` also spread at the top level for React/Svelte Flow familiarity). Both are stable identities — destructuring is safe.
|
|
195
158
|
|
|
196
|
-
|
|
197
|
-
|
|
159
|
+
```tsx
|
|
160
|
+
import { SolidFlowProvider, useSolidFlow } from "@dschz/solid-flow";
|
|
198
161
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
subscription; wrap side effects in `createEffect` over those reads.
|
|
202
|
-
- `useNodesState` / `useEdgesState` — use `createNodeStore` /
|
|
203
|
-
`createEdgeStore` instead.
|
|
204
|
-
- `useStore` — the reactive `flow` struct is the supported read surface;
|
|
205
|
-
there is no public escape hatch into internals.
|
|
162
|
+
const Toolbar = () => {
|
|
163
|
+
const { flow, commands } = useSolidFlow();
|
|
206
164
|
|
|
207
|
-
|
|
165
|
+
// flow.* reads are reactive: use them in JSX or tracked scopes.
|
|
166
|
+
// flow.nodes, flow.edges, flow.viewport, flow.selection,
|
|
167
|
+
// flow.nodesInitialized, flow.viewportInitialized, ...
|
|
208
168
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
169
|
+
return (
|
|
170
|
+
<div>
|
|
171
|
+
<span>{flow.selection.nodes.length} selected</span>
|
|
172
|
+
<button onClick={() => commands.fitView()}>Fit</button>
|
|
173
|
+
<button onClick={() => commands.zoomIn()}>+</button>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Hooks used outside <SolidFlow> children need a provider:
|
|
179
|
+
export const App = () => (
|
|
180
|
+
<SolidFlowProvider>
|
|
181
|
+
<Toolbar />
|
|
182
|
+
<Flow />
|
|
183
|
+
</SolidFlowProvider>
|
|
221
184
|
);
|
|
185
|
+
```
|
|
222
186
|
|
|
223
|
-
|
|
224
|
-
setEdges(addEdge(connection, edges));
|
|
187
|
+
Commands include viewport control (`fitView`, `fitBounds`, `zoomIn`/`zoomOut`, `setZoom`, `setCenter`, `setViewport`, `panBy`), element updates (`updateNode`, `updateNodeData`, `updateEdge`, `addNodes`, `addEdges`, `deleteElements`), coordinate conversion (`screenToFlowPosition`, `flowToScreenPosition`), and geometry queries (`isNodeIntersecting`, `getIntersectingNodes`).
|
|
225
188
|
|
|
226
|
-
|
|
227
|
-
const { screenToFlowPosition, flowToScreenPosition } = useSolidFlow();
|
|
189
|
+
## Hooks
|
|
228
190
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
191
|
+
```tsx
|
|
192
|
+
const solidFlow = useSolidFlow(); // { flow, commands } — the main API
|
|
193
|
+
const nodes = useNodes(); // reactive readonly node array
|
|
194
|
+
const edges = useEdges(); // reactive readonly edge array
|
|
195
|
+
const viewport = useViewport(); // reactive viewport accessor
|
|
196
|
+
const connection = useConnection(); // in-progress connection state
|
|
197
|
+
|
|
198
|
+
// Hook parameters that feed reactive reads are accessors, so they can't
|
|
199
|
+
// silently go stale when derived from props:
|
|
200
|
+
const nodeData = useNodesData(() => ["node-1", "node-2"]);
|
|
201
|
+
const connections = useNodeConnections(() => ({ id: "node-1" }));
|
|
202
|
+
const internal = useInternalNode(() => props.nodeId); // measured/internal record
|
|
203
|
+
|
|
204
|
+
const updateInternals = useUpdateNodeInternals(); // re-measure after handle changes
|
|
205
|
+
|
|
206
|
+
// Which node/edge a nested component is rendered inside — for composable
|
|
207
|
+
// custom nodes and edge labels without prop drilling:
|
|
208
|
+
const nodeId = useNodeId();
|
|
209
|
+
const edgeId = useEdgeId();
|
|
232
210
|
```
|
|
233
211
|
|
|
234
|
-
|
|
212
|
+
### Coming from React Flow?
|
|
235
213
|
|
|
236
|
-
|
|
214
|
+
Some React Flow hooks intentionally have no Solid Flow equivalent, because Solid's fine-grained reactivity makes them unnecessary:
|
|
237
215
|
|
|
238
|
-
|
|
216
|
+
- `useOnSelectionChange` / `useOnViewportChange` — reading `flow.selection` or `flow.viewport` in a tracked scope IS the subscription; wrap side effects in `createEffect` over those reads.
|
|
217
|
+
- `useNodesState` / `useEdgesState` — use `createNodeStore` / `createEdgeStore` instead.
|
|
218
|
+
- `useStore` — the reactive `flow` struct is the supported read surface; there is no public escape hatch into internals.
|
|
219
|
+
|
|
220
|
+
## Custom edges and reconnection
|
|
239
221
|
|
|
240
222
|
```tsx
|
|
241
|
-
import {
|
|
223
|
+
import { BaseEdge, EdgeReconnectAnchor, type EdgeProps, getBezierPath } from "@dschz/solid-flow";
|
|
224
|
+
|
|
225
|
+
const ReconnectableEdge = (props: EdgeProps) => {
|
|
226
|
+
const path = () =>
|
|
227
|
+
getBezierPath({
|
|
228
|
+
sourceX: props.sourceX,
|
|
229
|
+
sourceY: props.sourceY,
|
|
230
|
+
sourcePosition: props.sourcePosition,
|
|
231
|
+
targetX: props.targetX,
|
|
232
|
+
targetY: props.targetY,
|
|
233
|
+
targetPosition: props.targetPosition,
|
|
234
|
+
})[0];
|
|
242
235
|
|
|
243
|
-
// Type-safe custom node component
|
|
244
|
-
function CustomNode(props: NodeProps<{ label: string }, "custom">) {
|
|
245
236
|
return (
|
|
246
|
-
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
<
|
|
250
|
-
<
|
|
251
|
-
|
|
237
|
+
<>
|
|
238
|
+
<BaseEdge path={path()} markerEnd={props.markerEnd} />
|
|
239
|
+
{/* drag either end of a selected edge onto another handle */}
|
|
240
|
+
<EdgeReconnectAnchor type="source" position={{ x: props.sourceX, y: props.sourceY }} />
|
|
241
|
+
<EdgeReconnectAnchor type="target" position={{ x: props.targetX, y: props.targetY }} />
|
|
242
|
+
</>
|
|
252
243
|
);
|
|
253
|
-
}
|
|
244
|
+
};
|
|
245
|
+
```
|
|
254
246
|
|
|
255
|
-
|
|
256
|
-
const nodeTypes = {
|
|
257
|
-
custom: CustomNode,
|
|
258
|
-
} satisfies NodeTypes;
|
|
247
|
+
Reconnection lifecycle callbacks (`onReconnectStart`, `onReconnect`, `onReconnectEnd`, `onBeforeReconnect`) are available on `<SolidFlow>`.
|
|
259
248
|
|
|
260
|
-
|
|
261
|
-
const [nodes] = createNodeStore<typeof nodeTypes>([...]);
|
|
249
|
+
## Built-in components
|
|
262
250
|
|
|
263
|
-
|
|
264
|
-
```
|
|
251
|
+
**Node types:** `input` (source handle only), `output` (target handle only), `default` (both), `group` (container for subflows)
|
|
265
252
|
|
|
266
|
-
|
|
253
|
+
**Edge types:** `default` (bezier), `straight`, `step`, `smoothstep`
|
|
267
254
|
|
|
268
|
-
|
|
269
|
-
import { type Connection } from "@dschz/solid-flow";
|
|
255
|
+
**Plugins:**
|
|
270
256
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
257
|
+
- **Background** — dots, lines, and cross patterns
|
|
258
|
+
- **Controls** — zoom in/out, fit view, lock interactions
|
|
259
|
+
- **MiniMap** — interactive overview with viewport indicator, custom node rendering via `nodeComponent`, and `onClick` / `onNodeClick` handlers
|
|
260
|
+
- **NodeToolbar** — context-sensitive toolbars for nodes
|
|
261
|
+
- **NodeResizer** — interactive node resizing with handles
|
|
275
262
|
|
|
276
|
-
|
|
277
|
-
console.log("New connection:", connection);
|
|
278
|
-
setEdges(addEdge(connection, edges));
|
|
279
|
-
};
|
|
263
|
+
## Performance
|
|
280
264
|
|
|
281
|
-
|
|
282
|
-
isValidConnection={isValidConnection}
|
|
283
|
-
onConnect={onConnect}
|
|
284
|
-
...
|
|
285
|
-
/>
|
|
286
|
-
```
|
|
265
|
+
Two complementary culling tiers keep large graphs fast:
|
|
287
266
|
|
|
288
|
-
|
|
267
|
+
- **CSS culling (always on):** Elements outside the (overscanned) viewport are hidden with `visibility: hidden` + `pointer-events: none`. Everything stays mounted, so component state, measurement, and accessibility semantics are untouched — this tier has no userland contract at all.
|
|
268
|
+
- **`onlyRenderVisibleElements` (opt-in):** Off-screen elements are **unmounted entirely** and remount as the viewport reaches them. At 10,000 nodes this cuts the DOM ~16x, roughly halves memory, and makes node drags several times faster. Positions, selection, and cached measurements live in the flow's data graph — outside your components — so elements come back exactly as they left. Component-_local_ state does not survive unmounting: keep state you care about in `node.data`. Selected elements, unmeasured nodes, and the node holding focus are never unmounted — and an element whose content must keep running off-screen (media, timers, embeds) can opt out of culling entirely with `cullable: false` on the node or edge.
|
|
289
269
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
onPaneClick={(event) => console.log("Pane clicked")}
|
|
296
|
-
onSelectionChange={(params) => console.log("Selection changed:", params)}
|
|
297
|
-
/>
|
|
298
|
-
```
|
|
270
|
+
The MiniMap always renders the full graph in either mode — it reads the data graph, not the DOM.
|
|
271
|
+
|
|
272
|
+
## Server-side rendering
|
|
273
|
+
|
|
274
|
+
`<SolidFlow>` renders with `renderToString` and hydrates cleanly. For meaningful server-rendered layout, give nodes explicit `width` / `height` (there is no DOM to measure on the server) — the same guidance as React/Svelte Flow.
|
|
299
275
|
|
|
300
276
|
## Accessibility
|
|
301
277
|
|
|
302
|
-
|
|
278
|
+
- Full keyboard navigation (arrow-key node movement, focus traversal, Escape to deselect)
|
|
279
|
+
- Screen reader support with ARIA labels and live announcements
|
|
280
|
+
- Focus management, including under viewport culling
|
|
281
|
+
- Color mode support (`light` / `dark` / `system`)
|
|
303
282
|
|
|
304
|
-
|
|
305
|
-
- Screen reader compatibility with ARIA labels
|
|
306
|
-
- Focus management and visual indicators
|
|
307
|
-
- High contrast and color mode support
|
|
308
|
-
- Customizable keyboard shortcuts
|
|
283
|
+
## Migrating from 0.2.x
|
|
309
284
|
|
|
310
|
-
|
|
285
|
+
The 1.x line targets SolidJS 2.0, which changes how you write to stores, and reworks the read API. The gestures, components, plugins, and commands are otherwise the same.
|
|
311
286
|
|
|
312
|
-
|
|
313
|
-
- **Viewport Optimization**: Option to render only visible elements (coming soon)
|
|
314
|
-
- **Memory Efficient**: Optimized data structures for large graphs
|
|
315
|
-
- **Stress Tested**: Handles hundreds of nodes smoothly
|
|
287
|
+
**1. Upgrade the peer dependencies.** `solid-js` and `@solidjs/web` move to matching 2.0 versions.
|
|
316
288
|
|
|
317
|
-
|
|
289
|
+
**2. Store writes: path setters are gone (SolidJS 2.0).** Every `setNodes` / `setEdges` call site using 1.x path syntax becomes a draft callback — the callback's argument is a mutable draft, so mutation is the API. Returning a value instead replaces wholesale.
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
// 0.2.x (SolidJS 1.x) — path syntax
|
|
293
|
+
setNodes(0, "position", "x", (x) => x + 20);
|
|
294
|
+
setEdges((edge) => edge.id === "e1", "animated", true);
|
|
295
|
+
|
|
296
|
+
// 1.x (SolidJS 2.0) — draft callback
|
|
297
|
+
setNodes((nodes) => {
|
|
298
|
+
nodes[0]!.position.x += 20;
|
|
299
|
+
});
|
|
300
|
+
setEdges((edges) => {
|
|
301
|
+
const edge = edges.find((e) => e.id === "e1");
|
|
302
|
+
if (edge) edge.animated = true;
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// wholesale replacement (re-seeds the flow; rows keyed by id are reused)
|
|
306
|
+
setNodes(() => nextNodes);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**3. `useSolidFlow` reads moved to the reactive `flow` struct.** The flat getters (`getNodes()`, `getEdges()`, `getNode(id)`, `getEdge(id)`, `getInternalNode(id)`, `getViewport()`, `getZoom()`) are removed:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
// 0.2.x // 1.x
|
|
313
|
+
solidFlow.getNodes();
|
|
314
|
+
flow.nodes;
|
|
315
|
+
solidFlow.getViewport();
|
|
316
|
+
flow.viewport;
|
|
317
|
+
solidFlow.getZoom();
|
|
318
|
+
flow.viewport.zoom;
|
|
319
|
+
solidFlow.getNode("a");
|
|
320
|
+
flow.nodes.find((n) => n.id === "a");
|
|
321
|
+
solidFlow.getInternalNode("a");
|
|
322
|
+
useInternalNode(() => "a");
|
|
323
|
+
```
|
|
318
324
|
|
|
319
|
-
|
|
325
|
+
`flow.*` reads are reactive — using them in JSX or a tracked scope subscribes. Commands (`fitView`, `setViewport`, `updateNode`, `deleteElements`, ...) are unchanged and now also available namespaced under `commands`.
|
|
320
326
|
|
|
321
|
-
|
|
322
|
-
- **Custom Nodes** - Creating specialized node types
|
|
323
|
-
- **Edge Types** - Different connection styles
|
|
324
|
-
- **Drag & Drop** - External elements and node creation
|
|
325
|
-
- **Validation** - Connection rules and constraints
|
|
326
|
-
- **Subflows** - Hierarchical node organization
|
|
327
|
-
- **Performance** - Large dataset handling
|
|
328
|
-
- **Accessibility** - Keyboard navigation and screen readers
|
|
327
|
+
**4. New connections are no longer written into your edge store.** In 0.2.x the flow inserted the connected edge into your store before `onConnect` fired. Under the 1.x ownership contract your store owns membership: adopt the connection yourself (see the Quick Start's `onConnect`). Unadopted connections still render, but won't survive a wholesale store replacement.
|
|
329
328
|
|
|
330
|
-
|
|
329
|
+
**5. `onlyRenderVisibleElements` now does what it says.** In 0.2.x the prop was accepted but inert. In 1.x it opts into unmount culling (off-screen elements are not mounted at all — see [Performance](#performance)), while the CSS culling tier is always on and needs no prop.
|
|
330
|
+
|
|
331
|
+
**6. Smaller signature changes.** `useNodes()` / `useEdges()` return `readonly` arrays; `useHandleEdgeSelect` is removed (it was internal plumbing — select edges through `commands`).
|
|
332
|
+
|
|
333
|
+
## Examples
|
|
334
|
+
|
|
335
|
+
The repository ships a playground with 25+ runnable examples — custom nodes and edges, edge reconnection, drag & drop, subflows, validation, minimap customization, a 10k-node stress test, accessibility, and more:
|
|
331
336
|
|
|
332
337
|
```bash
|
|
338
|
+
bun install
|
|
333
339
|
bun start
|
|
334
340
|
```
|
|
335
341
|
|
|
@@ -359,6 +365,14 @@ bun install
|
|
|
359
365
|
bun start
|
|
360
366
|
```
|
|
361
367
|
|
|
368
|
+
### Testing
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
bun run test # unit + component tests (vitest, jsdom)
|
|
372
|
+
bun run test:ssr # SSR lane (node environment, server builds)
|
|
373
|
+
bun run test:e2e # browser gesture harness (Playwright)
|
|
374
|
+
```
|
|
375
|
+
|
|
362
376
|
### Linting & Formatting
|
|
363
377
|
|
|
364
378
|
```bash
|
package/dist/index/index.d.ts
CHANGED
|
@@ -21,6 +21,14 @@ type Node<NodeData extends UnknownStruct = UnknownStruct, NodeType extends strin
|
|
|
21
21
|
class?: string;
|
|
22
22
|
style?: JSX.CSSProperties;
|
|
23
23
|
focusable?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* When `false`, the node is exempt from viewport culling on both tiers:
|
|
26
|
+
* the always-on CSS tier never hides it and `onlyRenderVisibleElements`
|
|
27
|
+
* never unmounts it. Use for nodes whose content must keep running while
|
|
28
|
+
* off-screen (media playback, timers, third-party embeds).
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
cullable?: boolean;
|
|
24
32
|
/**
|
|
25
33
|
* The ARIA role attribute for the node element, used for accessibility.
|
|
26
34
|
* @default "group"
|
|
@@ -72,6 +80,13 @@ type Edge<EdgeData extends UnknownStruct = UnknownStruct, EdgeType extends strin
|
|
|
72
80
|
style?: JSX.CSSProperties;
|
|
73
81
|
class?: string;
|
|
74
82
|
focusable?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* When `false`, the edge is exempt from viewport culling on both tiers:
|
|
85
|
+
* the always-on CSS tier never hides it and `onlyRenderVisibleElements`
|
|
86
|
+
* never unmounts it.
|
|
87
|
+
* @default true
|
|
88
|
+
*/
|
|
89
|
+
cullable?: boolean;
|
|
75
90
|
/**
|
|
76
91
|
* The ARIA role attribute for the edge, used for accessibility.
|
|
77
92
|
* @default "group"
|
|
@@ -612,6 +627,22 @@ type SolidFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge>
|
|
|
612
627
|
* ]);
|
|
613
628
|
*/
|
|
614
629
|
readonly edges?: Store<EdgeType[]>;
|
|
630
|
+
/**
|
|
631
|
+
* Initial nodes for an UNCONTROLLED flow. When `nodes` is not supplied,
|
|
632
|
+
* the flow owns element state: this array seeds it once (later values
|
|
633
|
+
* are ignored), and membership belongs to the flow — commands like
|
|
634
|
+
* `addNodes`/`deleteElements` and completed connections write through
|
|
635
|
+
* and persist, with no adoption step. Mutually exclusive with `nodes`
|
|
636
|
+
* (which wins, with a dev warning). Mode is fixed at mount, per axis:
|
|
637
|
+
* nodes and edges can each be controlled or uncontrolled independently.
|
|
638
|
+
*/
|
|
639
|
+
readonly defaultNodes?: readonly NodeType[];
|
|
640
|
+
/**
|
|
641
|
+
* Initial edges for an UNCONTROLLED flow — the edge-axis counterpart of
|
|
642
|
+
* `defaultNodes`: seeds once, flow owns membership, completed
|
|
643
|
+
* connections are kept automatically. Mutually exclusive with `edges`.
|
|
644
|
+
*/
|
|
645
|
+
readonly defaultEdges?: readonly EdgeType[];
|
|
615
646
|
/**
|
|
616
647
|
* Custom node types to be available in a flow.
|
|
617
648
|
* Solid Flow matches a node's type to a component in the nodeTypes object.
|
|
@@ -866,7 +897,10 @@ type SolidFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge>
|
|
|
866
897
|
* Component-LOCAL state does not survive — signals created inside a
|
|
867
898
|
* custom node, uncontrolled inputs, scroll positions of inner elements.
|
|
868
899
|
* Keep state you care about in `node.data`. Never unmounted: selected
|
|
869
|
-
* elements, unmeasured nodes, and the node holding DOM focus.
|
|
900
|
+
* elements, unmeasured nodes, and the node holding DOM focus. Elements
|
|
901
|
+
* whose component-local state (or side effects) must keep running while
|
|
902
|
+
* off-screen can opt out entirely with `cullable: false` on the node or
|
|
903
|
+
* edge — they are then exempt from both tiers.
|
|
870
904
|
*
|
|
871
905
|
* Independent of this prop, off-viewport elements are always CSS-culled
|
|
872
906
|
* (`visibility: hidden` + `pointer-events: none`, everything stays
|
|
@@ -1065,11 +1099,11 @@ type AllEdgeTypes<TUserEdgeTypes extends EdgeTypes> = TUserEdgeTypes extends Rec
|
|
|
1065
1099
|
* ```typescript
|
|
1066
1100
|
* const initialEdges = [
|
|
1067
1101
|
* { id: "e1", source: "1", target: "2", type: "labeled", data: { label: "hi" } },
|
|
1068
|
-
* ] satisfies
|
|
1102
|
+
* ] satisfies SolidFlowEdge<typeof edgeTypes>[];
|
|
1069
1103
|
* ```
|
|
1070
1104
|
*/
|
|
1071
|
-
type
|
|
1072
|
-
type EdgesInput<TUserEdgeTypes extends EdgeTypes> =
|
|
1105
|
+
type SolidFlowEdge<TUserEdgeTypes extends EdgeTypes = Record<string, never>> = { [K in keyof AllEdgeTypes<TUserEdgeTypes>]: Edge<EdgeDataOf<AllEdgeTypes<TUserEdgeTypes>[K]>, K & string>; }[keyof AllEdgeTypes<TUserEdgeTypes>];
|
|
1106
|
+
type EdgesInput<TUserEdgeTypes extends EdgeTypes> = SolidFlowEdge<TUserEdgeTypes>;
|
|
1073
1107
|
/**
|
|
1074
1108
|
* Creates a type-safe reactive store of edges for use in Solid Flow.
|
|
1075
1109
|
*
|
|
@@ -1314,11 +1348,11 @@ type AllNodeTypes<TUserNodeTypes extends NodeTypes> = TUserNodeTypes extends Rec
|
|
|
1314
1348
|
* ```typescript
|
|
1315
1349
|
* const initialNodes = [
|
|
1316
1350
|
* { id: "1", type: "custom", position: { x: 0, y: 0 }, data: { value: 1 } },
|
|
1317
|
-
* ] satisfies
|
|
1351
|
+
* ] satisfies SolidFlowNode<typeof nodeTypes>[];
|
|
1318
1352
|
* ```
|
|
1319
1353
|
*/
|
|
1320
|
-
type
|
|
1321
|
-
type NodesInput<TUserNodeTypes extends NodeTypes> =
|
|
1354
|
+
type SolidFlowNode<TUserNodeTypes extends NodeTypes = Record<string, never>> = { [K in keyof AllNodeTypes<TUserNodeTypes>]: Node<NodeDataOf<AllNodeTypes<TUserNodeTypes>[K]>, K & string>; }[keyof AllNodeTypes<TUserNodeTypes>];
|
|
1355
|
+
type NodesInput<TUserNodeTypes extends NodeTypes> = SolidFlowNode<TUserNodeTypes>;
|
|
1322
1356
|
/**
|
|
1323
1357
|
* Creates a type-safe reactive store of nodes for use in Solid Flow.
|
|
1324
1358
|
*
|
|
@@ -1911,4 +1945,4 @@ declare const getEdgeCenter: typeof getEdgeCenter$1;
|
|
|
1911
1945
|
/** Returns the label center and offsets for a bezier edge. */
|
|
1912
1946
|
declare const getBezierEdgeCenter: typeof getBezierEdgeCenter$1;
|
|
1913
1947
|
//#endregion
|
|
1914
|
-
export { Align, AriaLabelConfig, Background, type BackgroundProps, type BackgroundVariant, BaseEdge, BezierEdge, BezierEdgeInternal, type BezierEdgeProps, BezierPathOptions, Box, type BuiltInEdge, type BuiltInNode, type BuiltInNodeTypes, ColorMode, ColorModeClass, type Connection, ConnectionData, ConnectionLine, ConnectionLineComponentProps, ConnectionLineType, ConnectionMode, type ConnectionsRecord, ControlButton, type ControlLinePosition, type ControlPosition, Controls, type CoordinateExtent, type DefaultEdgeOptions, DefaultNode, DeleteEvents, Dimensions, type Edge, EdgeConnection, EdgeEvents, EdgeLabel, EdgeLabelRenderer, type EdgeMarker, EdgeMarkerType, type EdgeProps, EdgeReconnectAnchor, EdgeReconnectEvents, EdgeRenderer, EdgeToolbar, EdgeToolbarProps, type EdgeTypes, EdgeWrapper, type
|
|
1948
|
+
export { Align, AriaLabelConfig, Background, type BackgroundProps, type BackgroundVariant, BaseEdge, BezierEdge, BezierEdgeInternal, type BezierEdgeProps, BezierPathOptions, Box, type BuiltInEdge, type BuiltInNode, type BuiltInNodeTypes, ColorMode, ColorModeClass, type Connection, ConnectionData, ConnectionLine, ConnectionLineComponentProps, ConnectionLineType, ConnectionMode, type ConnectionsRecord, ControlButton, type ControlLinePosition, type ControlPosition, Controls, type CoordinateExtent, type DefaultEdgeOptions, DefaultNode, DeleteEvents, Dimensions, type Edge, EdgeConnection, EdgeEvents, EdgeLabel, EdgeLabelRenderer, type EdgeMarker, EdgeMarkerType, type EdgeProps, EdgeReconnectAnchor, EdgeReconnectEvents, EdgeRenderer, EdgeToolbar, EdgeToolbarProps, type EdgeTypes, EdgeWrapper, type FitBounds, FitBoundsOptions, FitViewOptions, type FlowCommands, type FlowSelection, type FlowState, GetBezierPathParams, type GetMiniMapNodeAttribute, GetSmoothStepPathParams, GetStraightPathParams, GroupNode, Handle, type HandleConnection, InputNode, type InternalNode, IsValidConnection, KeyDefinition, KeyDefinitionObject, KeyModifier, Marker, MarkerDefinition, MarkerType, MiniMap, MiniMapNode, type MiniMapNodeProps, type MiniMapProps, type Node, type NodeConnection, NodeEvents, NodeGraph, type NodeOrigin, type NodeProps, NodeRenderer, NodeResizer, NodeSelection, NodeSelectionEvents, NodeToolbar, NodeToolbarProps, type NodeTypes, NodeWrapper, OnBeforeDelete, OnBeforeEdgeConnect, OnBeforeReconnect, OnConnect, OnConnectEnd, OnConnectStart, OnConnectStartParams, OnDelete, OnEdgeConnect, OnEdgeCreate, OnError, type OnMove, OnMoveEnd, OnMoveStart, OnReconnect, OnReconnectEnd, OnReconnectStart, OnResize, OnResizeEnd, OnResizeStart, OnSelectionChange, OnSelectionDrag, OutputNode, PanOnScrollMode, Pane, PaneEvents, Panel, type PanelPosition, Position, ProOptions, Rect, ResizeControl, ResizeControlVariant, ResizeDragEvent, ResizeParams, ResizeParamsWithDirection, Selection, SelectionMode, SelectionRect, type SetCenter, SetCenterOptions, type SetViewport, ShortcutModifier, ShortcutModifierDefinition, type ShouldResize, SmoothStepEdge, SmoothStepEdgeInternal, type SmoothStepEdgeProps, SmoothStepPathOptions, SnapGrid, SolidFlow, type SolidFlowEdge, type SolidFlowInitialProps, type SolidFlowNode, type SolidFlowProps, SolidFlowProvider, StepEdge, StepEdgeInternal, type StepEdgeProps, StraightEdge, StraightEdgeInternal, type StraightEdgeProps, Transform, UseSolidFlowReturn, Viewport, ViewportHelperFunctionOptions, ViewportPortal, type XYPosition, XYZPosition, Zoom, addEdge, connectionKey, createEdgeStore, createNodeStore, getBezierEdgeCenter, getBezierPath, getConnectedEdges, getEdgeCenter, getIncomers, getNodesBounds, getOutgoers, getSmoothStepPath, getStraightPath, getViewportForBounds, useColorMode, useConnection, useEdgeId, useEdges, useInternalNode, useNodeConnections, useNodeId, useNodes, useNodesData, useNodesInitialized, useSolidFlow, useUpdateNodeInternals, useViewport, useViewportInitialized };
|
package/dist/index/index.js
CHANGED
|
@@ -430,9 +430,9 @@ const A11yDescriptions = () => {
|
|
|
430
430
|
height: 2 * halfHeight
|
|
431
431
|
};
|
|
432
432
|
}, { equals: rectsEqual }), isNodeCulled = (node, cullingViewport) => {
|
|
433
|
-
if (!cullingViewport || node.selected) return !1;
|
|
433
|
+
if (!cullingViewport || node.selected || node.cullable === !1) return !1;
|
|
434
434
|
let { width, height } = node.measured;
|
|
435
|
-
if (!width || !height) return !1;
|
|
435
|
+
if (!width || !height || !node.internals.handleBounds) return !1;
|
|
436
436
|
let { x, y } = node.internals.positionAbsolute;
|
|
437
437
|
return !rectsOverlap({
|
|
438
438
|
x,
|
|
@@ -441,7 +441,7 @@ const A11yDescriptions = () => {
|
|
|
441
441
|
height
|
|
442
442
|
}, cullingViewport);
|
|
443
443
|
}, isEdgeCulled = (row, cullingViewport) => {
|
|
444
|
-
if (!cullingViewport || row.selected) return !1;
|
|
444
|
+
if (!cullingViewport || row.selected || row.cullable === !1) return !1;
|
|
445
445
|
let x = Math.min(row.sourceX, row.targetX), y = Math.min(row.sourceY, row.targetY);
|
|
446
446
|
return !rectsOverlap({
|
|
447
447
|
x,
|
|
@@ -451,8 +451,6 @@ const A11yDescriptions = () => {
|
|
|
451
451
|
}, cullingViewport);
|
|
452
452
|
}, getDefaultFlowStateProps = () => ({
|
|
453
453
|
id: "1",
|
|
454
|
-
nodes: [],
|
|
455
|
-
edges: [],
|
|
456
454
|
nodeOrigin: [0, 0],
|
|
457
455
|
nodeExtent: infiniteExtent,
|
|
458
456
|
defaultEdgeOptions: {},
|
|
@@ -753,7 +751,9 @@ const createLayoutedEdges = (source) => {
|
|
|
753
751
|
zoom: 1
|
|
754
752
|
};
|
|
755
753
|
}, createFlowState = (props, injections = {}) => {
|
|
756
|
-
let _props = merge(getDefaultFlowStateProps(), props), initialNodeTypes = injections.initialNodeTypes ?? {}, initialEdgeTypes = injections.initialEdgeTypes ?? {}, prefersDark = injections.prefersDark ?? (() => _props.colorModeSSR === "dark"), [config, setConfig] = createSignal(_props), [ariaLabelConfig, setAriaLabelConfig] = createSignal(() => mergeAriaLabelConfig(config().ariaLabelConfig)), [ariaLiveMessage, setAriaLiveMessage] = createSignal(() => config().ariaLiveMessage), [clickConnectStartHandle, setClickConnectStartHandle] = createSignal(void 0), [connection, setConnection] = createSignal(initialConnection), [domNode, setDomNode] = createSignal(null), [dragging, setDragging] = createSignal(!1), [elementsSelectable, setElementsSelectable] = createSignal(() => config().elementsSelectable), [height, setHeight] = createSignal(() => config().height), [minZoom, _setMinZoom] = createSignal(() => config().minZoom), [maxZoom, _setMaxZoom] = createSignal(() => config().maxZoom), [nodesConnectable, setNodesConnectable] = createSignal(() => config().nodesDraggable), [nodesDraggable, setNodesDraggable] = createSignal(() => config().nodesDraggable), [panZoom, setPanZoom] = createSignal(null), [selectionRect, setSelectionRect] = createSignal(), [selectionRectMode, setSelectionRectMode] = createSignal(), [snapGrid, setSnapGrid] = createSignal(() => config().snapGrid), [translateExtent, _setTranslateExtent] = createSignal(() => config().translateExtent ?? infiniteExtent), [width, setWidth] = createSignal(() => config().width), [selectionKeyPressed, setSelectionKeyPressed] = createSignal(!1), [multiselectionKeyPressed, setMultiselectionKeyPressed] = createSignal(!1), [deleteKeyPressed, setDeleteKeyPressed] = createSignal(!1), [panActivationKeyPressed, setPanActivationKeyPressed] = createSignal(!1), [zoomActivationKeyPressed, setZoomActivationKeyPressed] = createSignal(!1)
|
|
754
|
+
let _props = merge(getDefaultFlowStateProps(), props), initialNodeTypes = injections.initialNodeTypes ?? {}, initialEdgeTypes = injections.initialEdgeTypes ?? {}, prefersDark = injections.prefersDark ?? (() => _props.colorModeSSR === "dark"), [config, setConfig] = createSignal(_props), [ariaLabelConfig, setAriaLabelConfig] = createSignal(() => mergeAriaLabelConfig(config().ariaLabelConfig)), [ariaLiveMessage, setAriaLiveMessage] = createSignal(() => config().ariaLiveMessage), [clickConnectStartHandle, setClickConnectStartHandle] = createSignal(void 0), [connection, setConnection] = createSignal(initialConnection), [domNode, setDomNode] = createSignal(null), [dragging, setDragging] = createSignal(!1), [elementsSelectable, setElementsSelectable] = createSignal(() => config().elementsSelectable), [height, setHeight] = createSignal(() => config().height), [minZoom, _setMinZoom] = createSignal(() => config().minZoom), [maxZoom, _setMaxZoom] = createSignal(() => config().maxZoom), [nodesConnectable, setNodesConnectable] = createSignal(() => config().nodesDraggable), [nodesDraggable, setNodesDraggable] = createSignal(() => config().nodesDraggable), [panZoom, setPanZoom] = createSignal(null), [selectionRect, setSelectionRect] = createSignal(), [selectionRectMode, setSelectionRectMode] = createSignal(), [snapGrid, setSnapGrid] = createSignal(() => config().snapGrid), [translateExtent, _setTranslateExtent] = createSignal(() => config().translateExtent ?? infiniteExtent), [width, setWidth] = createSignal(() => config().width), [selectionKeyPressed, setSelectionKeyPressed] = createSignal(!1), [multiselectionKeyPressed, setMultiselectionKeyPressed] = createSignal(!1), [deleteKeyPressed, setDeleteKeyPressed] = createSignal(!1), [panActivationKeyPressed, setPanActivationKeyPressed] = createSignal(!1), [zoomActivationKeyPressed, setZoomActivationKeyPressed] = createSignal(!1);
|
|
755
|
+
props.nodes !== void 0 && props.defaultNodes, props.edges !== void 0 && props.defaultEdges;
|
|
756
|
+
let [nodesStore, setNodesStore] = createStore(props.nodes ?? [...props.defaultNodes ?? []]), [edgesStore, setEdgesStore] = createStore(props.edges ?? [...props.defaultEdges ?? []]), nodeSeedAdopted = props.nodes !== void 0 || props.defaultNodes !== void 0, edgeSeedAdopted = props.edges !== void 0 || props.defaultEdges !== void 0, [measurementsStore, setMeasurementsStore] = createStore({}), internalNodes = createInternalNodes({
|
|
757
757
|
get nodes() {
|
|
758
758
|
return nodesStore;
|
|
759
759
|
},
|
|
@@ -775,16 +775,21 @@ const createLayoutedEdges = (source) => {
|
|
|
775
775
|
}), nodeLookup = new RecordMapFacade(internalNodes), initialViewport = getInitialViewport(_props.fitView, _props.initialViewport, _props.width ?? 0, _props.height ?? 0, nodeLookup), [viewportStore, setViewportStore] = createStore(_props.viewport ?? initialViewport);
|
|
776
776
|
createEffect(() => {
|
|
777
777
|
let next = config().nodes;
|
|
778
|
-
for (let node of next);
|
|
778
|
+
if (next) for (let node of next);
|
|
779
779
|
return { next };
|
|
780
780
|
}, ({ next }) => {
|
|
781
|
-
setNodesStore(() => next);
|
|
781
|
+
next && (nodeSeedAdopted = !0, setNodesStore(() => next));
|
|
782
782
|
}, { defer: !0 }), createEffect(() => {
|
|
783
783
|
let next = config().edges;
|
|
784
|
-
for (let edge of next);
|
|
784
|
+
if (next) for (let edge of next);
|
|
785
785
|
return { next };
|
|
786
786
|
}, ({ next }) => {
|
|
787
|
-
setEdgesStore(() => next);
|
|
787
|
+
next && (edgeSeedAdopted = !0, setEdgesStore(() => next));
|
|
788
|
+
}, { defer: !0 }), createEffect(() => ({
|
|
789
|
+
nodes: config().defaultNodes,
|
|
790
|
+
edges: config().defaultEdges
|
|
791
|
+
}), ({ nodes: defaultNodes, edges: defaultEdges }) => {
|
|
792
|
+
defaultNodes && !nodeSeedAdopted && config().nodes === void 0 && (nodeSeedAdopted = !0, setNodesStore(() => [...defaultNodes])), defaultEdges && !edgeSeedAdopted && config().edges === void 0 && (edgeSeedAdopted = !0, setEdgesStore(() => [...defaultEdges]));
|
|
788
793
|
}, { defer: !0 }), createEffect(() => config().viewport, (next) => {
|
|
789
794
|
next && setViewportStore(() => next);
|
|
790
795
|
}, { defer: !0 });
|
|
@@ -3278,6 +3283,34 @@ function matchesKey(event, keyDef) {
|
|
|
3278
3283
|
function matchesKeyArray(event, keyDefs) {
|
|
3279
3284
|
return keyDefs ? (Array.isArray(keyDefs) ? keyDefs : [keyDefs]).some((keyDef) => matchesKey(event, keyDef)) : !1;
|
|
3280
3285
|
}
|
|
3286
|
+
const MODIFIER_FLAG = {
|
|
3287
|
+
alt: "altKey",
|
|
3288
|
+
control: "ctrlKey",
|
|
3289
|
+
ctrl: "ctrlKey",
|
|
3290
|
+
meta: "metaKey",
|
|
3291
|
+
shift: "shiftKey"
|
|
3292
|
+
};
|
|
3293
|
+
/**
|
|
3294
|
+
* Whether an event's modifier flags PROVE this key definition cannot be held
|
|
3295
|
+
* right now: the key itself is a modifier whose flag is off, or one of its
|
|
3296
|
+
* required modifiers is off. Definitions built on non-modifier keys are never
|
|
3297
|
+
* contradicted (their state isn't derivable from flags).
|
|
3298
|
+
*/
|
|
3299
|
+
function isContradicted(flags, keyDef) {
|
|
3300
|
+
let keyFlag = MODIFIER_FLAG[getKeyString(keyDef).toLowerCase()];
|
|
3301
|
+
if (keyFlag && !flags[keyFlag]) return !0;
|
|
3302
|
+
let modifiers = getModifier(keyDef);
|
|
3303
|
+
return (Array.isArray(modifiers) ? modifiers.flatMap((mod) => mod) : [modifiers]).some((mod) => {
|
|
3304
|
+
let modFlag = MODIFIER_FLAG[mod.toLowerCase()];
|
|
3305
|
+
return !!modFlag && !flags[modFlag];
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
/** True only when EVERY definition for this key state is contradicted. */
|
|
3309
|
+
function allContradicted(flags, keyDefs) {
|
|
3310
|
+
if (!keyDefs) return !1;
|
|
3311
|
+
let keys = Array.isArray(keyDefs) ? keyDefs : [keyDefs];
|
|
3312
|
+
return keys.length > 0 && keys.every((keyDef) => isContradicted(flags, keyDef));
|
|
3313
|
+
}
|
|
3281
3314
|
const KeyHandler = (props) => {
|
|
3282
3315
|
let { store, actions } = useInternalSolidFlow(), { deleteElements } = useSolidFlow(), _props = {
|
|
3283
3316
|
get selectionKey() {
|
|
@@ -3297,6 +3330,20 @@ const KeyHandler = (props) => {
|
|
|
3297
3330
|
}
|
|
3298
3331
|
}, resetKeysAndSelection = () => {
|
|
3299
3332
|
actions.setSelectionRect(void 0), actions.setSelectionKeyPressed(!1), actions.setMultiselectionKeyPressed(!1), actions.setDeleteKeyPressed(!1), actions.setPanActivationKeyPressed(!1), actions.setZoomActivationKeyPressed(!1);
|
|
3333
|
+
}, reconcileModifiers = (event) => {
|
|
3334
|
+
let changed = !1;
|
|
3335
|
+
store.selectionKeyPressed && allContradicted(event, _props.selectionKey) && (actions.setSelectionKeyPressed(!1), changed = !0), store.multiselectionKeyPressed && allContradicted(event, _props.multiSelectionKey) && (actions.setMultiselectionKeyPressed(!1), changed = !0), store.panActivationKeyPressed && allContradicted(event, _props.panActivationKey) && (actions.setPanActivationKeyPressed(!1), changed = !0), store.zoomActivationKeyPressed && allContradicted(event, _props.zoomActivationKey) && (actions.setZoomActivationKeyPressed(!1), changed = !0), changed && flush();
|
|
3336
|
+
}, cancelPointerGestures = () => {
|
|
3337
|
+
let release = (Ctor, type) => {
|
|
3338
|
+
try {
|
|
3339
|
+
window.dispatchEvent(new Ctor(type, { view: window }));
|
|
3340
|
+
} catch {
|
|
3341
|
+
window.dispatchEvent(new Ctor(type));
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3344
|
+
release(MouseEvent, "mouseup"), typeof PointerEvent < "u" && release(PointerEvent, "pointerup");
|
|
3345
|
+
}, handleWindowBlur = () => {
|
|
3346
|
+
resetKeysAndSelection(), cancelPointerGestures();
|
|
3300
3347
|
}, handleDelete = async () => {
|
|
3301
3348
|
let selectedNodes = store.nodes.filter((node) => node.selected), selectedEdges = store.edges.filter((edge) => edge.selected), { deletedNodes, deletedEdges } = await deleteElements({
|
|
3302
3349
|
nodes: selectedNodes,
|
|
@@ -3307,16 +3354,22 @@ const KeyHandler = (props) => {
|
|
|
3307
3354
|
edges: deletedEdges
|
|
3308
3355
|
});
|
|
3309
3356
|
};
|
|
3310
|
-
return isServer || createEventListenerMap(window, {
|
|
3357
|
+
return isServer || (createEventListenerMap(window, {
|
|
3311
3358
|
keydown: (event) => {
|
|
3312
|
-
matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!0), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!0), matchesKeyArray(event, _props.deleteKey) && !isInputDOMNode(event) && (event.ctrlKey || event.metaKey || event.shiftKey || (actions.setDeleteKeyPressed(!0), handleDelete())), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!0), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!0), flush();
|
|
3359
|
+
reconcileModifiers(event), matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!0), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!0), matchesKeyArray(event, _props.deleteKey) && !isInputDOMNode(event) && (event.ctrlKey || event.metaKey || event.shiftKey || (actions.setDeleteKeyPressed(!0), handleDelete())), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!0), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!0), flush();
|
|
3313
3360
|
},
|
|
3314
3361
|
keyup: (event) => {
|
|
3315
|
-
matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!1), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!1), matchesKeyArray(event, _props.deleteKey) && actions.setDeleteKeyPressed(!1), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!1), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!1), flush();
|
|
3362
|
+
reconcileModifiers(event), matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!1), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!1), matchesKeyArray(event, _props.deleteKey) && actions.setDeleteKeyPressed(!1), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!1), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!1), flush();
|
|
3316
3363
|
},
|
|
3317
|
-
blur:
|
|
3364
|
+
blur: handleWindowBlur,
|
|
3318
3365
|
contextmenu: resetKeysAndSelection
|
|
3319
|
-
}),
|
|
3366
|
+
}), createEventListenerMap(window, {
|
|
3367
|
+
pointerdown: reconcileModifiers,
|
|
3368
|
+
wheel: reconcileModifiers
|
|
3369
|
+
}, {
|
|
3370
|
+
capture: !0,
|
|
3371
|
+
passive: !0
|
|
3372
|
+
})), null;
|
|
3320
3373
|
};
|
|
3321
3374
|
//#endregion
|
|
3322
3375
|
//#region src/components/SolidFlow.tsx
|
|
@@ -3349,7 +3402,7 @@ const SolidFlow = (props) => {
|
|
|
3349
3402
|
zoomOnPinch: !0,
|
|
3350
3403
|
zoomOnDoubleClick: !0,
|
|
3351
3404
|
zoomOnScroll: !0
|
|
3352
|
-
}, props), htmlProps = omit(_props, "nodes", "edges", "nodeTypes", "edgeTypes", "width", "height", "fitView", "fitViewOptions", "nodeOrigin", "nodeDragThreshold", "paneClickDistance", "nodeClickDistance", "minZoom", "maxZoom", "zIndexMode", "initialViewport", "viewport", "translateExtent", "nodeExtent", "selectionKey", "panActivationKey", "deleteKey", "multiSelectionKey", "zoomActivationKey", "panOnDrag", "panOnScroll", "panOnScrollMode", "panOnScrollSpeed", "selectionOnDrag", "selectNodesOnDrag", "preventScrolling", "zoomOnScroll", "zoomOnDoubleClick", "zoomOnPinch", "onlyRenderVisibleElements", "autoPanOnConnect", "autoPanOnNodeDrag", "autoPanOnNodeFocus", "autoPanOnSelection", "autoPanSpeed", "connectionRadius", "connectionMode", "connectionLineType", "connectionLineComponent", "connectionLineStyle", "connectionLineContainerStyle", "connectionDragThreshold", "isValidConnection", "clickConnect", "reconnectRadius", "selectionMode", "elementsSelectable", "nodesDraggable", "nodesConnectable", "nodesFocusable", "edgesFocusable", "disableKeyboardA11y", "ariaLabelConfig", "ariaLiveMessage", "colorMode", "colorModeSSR", "class", "style", "snapGrid", "defaultMarkerColor", "defaultEdgeOptions", "elevateNodesOnSelect", "elevateEdgesOnSelect", "noDragClass", "noPanClass", "noWheelClass", "attributionPosition", "proOptions", "onInit", "onMoveStart", "onMove", "onMoveEnd", "onFlowError", "onNodeClick", "onNodeContextMenu", "onNodeDrag", "onNodeDragStart", "onNodeDragStop", "onNodePointerEnter", "onNodePointerMove", "onNodePointerLeave", "onEdgeClick", "onEdgeContextMenu", "onEdgePointerEnter", "onEdgePointerLeave", "onPaneClick", "onPaneContextMenu", "onSelectionChange", "onSelectionClick", "onSelectionContextMenu", "onSelectionDrag", "onSelectionDragStart", "onSelectionDragStop", "onSelectionStart", "onSelectionEnd", "onConnect", "onConnectStart", "onConnectEnd", "onReconnect", "onReconnectStart", "onReconnectEnd", "onClickConnectStart", "onClickConnectEnd", "onBeforeConnect", "onBeforeReconnect", "onDelete", "onBeforeDelete", "deleteKeyCode", "selectionKeyCode", "panActivationKeyCode", "multiSelectionKeyCode", "zoomActivationKeyCode", "children"), TypedSolidFlowContext = SolidFlowContext, solidFlow = useContext(TypedSolidFlowContext) ?? createSolidFlow(_props), { store, actions } = solidFlow;
|
|
3405
|
+
}, props), htmlProps = omit(_props, "nodes", "edges", "defaultNodes", "defaultEdges", "nodeTypes", "edgeTypes", "width", "height", "fitView", "fitViewOptions", "nodeOrigin", "nodeDragThreshold", "paneClickDistance", "nodeClickDistance", "minZoom", "maxZoom", "zIndexMode", "initialViewport", "viewport", "translateExtent", "nodeExtent", "selectionKey", "panActivationKey", "deleteKey", "multiSelectionKey", "zoomActivationKey", "panOnDrag", "panOnScroll", "panOnScrollMode", "panOnScrollSpeed", "selectionOnDrag", "selectNodesOnDrag", "preventScrolling", "zoomOnScroll", "zoomOnDoubleClick", "zoomOnPinch", "onlyRenderVisibleElements", "autoPanOnConnect", "autoPanOnNodeDrag", "autoPanOnNodeFocus", "autoPanOnSelection", "autoPanSpeed", "connectionRadius", "connectionMode", "connectionLineType", "connectionLineComponent", "connectionLineStyle", "connectionLineContainerStyle", "connectionDragThreshold", "isValidConnection", "clickConnect", "reconnectRadius", "selectionMode", "elementsSelectable", "nodesDraggable", "nodesConnectable", "nodesFocusable", "edgesFocusable", "disableKeyboardA11y", "ariaLabelConfig", "ariaLiveMessage", "colorMode", "colorModeSSR", "class", "style", "snapGrid", "defaultMarkerColor", "defaultEdgeOptions", "elevateNodesOnSelect", "elevateEdgesOnSelect", "noDragClass", "noPanClass", "noWheelClass", "attributionPosition", "proOptions", "onInit", "onMoveStart", "onMove", "onMoveEnd", "onFlowError", "onNodeClick", "onNodeContextMenu", "onNodeDrag", "onNodeDragStart", "onNodeDragStop", "onNodePointerEnter", "onNodePointerMove", "onNodePointerLeave", "onEdgeClick", "onEdgeContextMenu", "onEdgePointerEnter", "onEdgePointerLeave", "onPaneClick", "onPaneContextMenu", "onSelectionChange", "onSelectionClick", "onSelectionContextMenu", "onSelectionDrag", "onSelectionDragStart", "onSelectionDragStop", "onSelectionStart", "onSelectionEnd", "onConnect", "onConnectStart", "onConnectEnd", "onReconnect", "onReconnectStart", "onReconnectEnd", "onClickConnectStart", "onClickConnectEnd", "onBeforeConnect", "onBeforeReconnect", "onDelete", "onBeforeDelete", "deleteKeyCode", "selectionKeyCode", "panActivationKeyCode", "multiSelectionKeyCode", "zoomActivationKeyCode", "children"), TypedSolidFlowContext = SolidFlowContext, solidFlow = useContext(TypedSolidFlowContext) ?? createSolidFlow(_props), { store, actions } = solidFlow;
|
|
3353
3406
|
onSettled(() => (actions.applyInitialFitView(_props.fitView), actions.setConfig(_props), actions.setDomNode(domNode), () => {
|
|
3354
3407
|
actions.reset();
|
|
3355
3408
|
})), createEffect(() => domNodeRef(), (el) => {
|
package/dist/index/index.jsx
CHANGED
|
@@ -250,9 +250,9 @@ const getEdgeId = (connection) => {
|
|
|
250
250
|
height: 2 * halfHeight
|
|
251
251
|
};
|
|
252
252
|
}, { equals: rectsEqual }), isNodeCulled = (node, cullingViewport) => {
|
|
253
|
-
if (!cullingViewport || node.selected) return !1;
|
|
253
|
+
if (!cullingViewport || node.selected || node.cullable === !1) return !1;
|
|
254
254
|
let { width, height } = node.measured;
|
|
255
|
-
if (!width || !height) return !1;
|
|
255
|
+
if (!width || !height || !node.internals.handleBounds) return !1;
|
|
256
256
|
let { x, y } = node.internals.positionAbsolute;
|
|
257
257
|
return !rectsOverlap({
|
|
258
258
|
x,
|
|
@@ -261,7 +261,7 @@ const getEdgeId = (connection) => {
|
|
|
261
261
|
height
|
|
262
262
|
}, cullingViewport);
|
|
263
263
|
}, isEdgeCulled = (row, cullingViewport) => {
|
|
264
|
-
if (!cullingViewport || row.selected) return !1;
|
|
264
|
+
if (!cullingViewport || row.selected || row.cullable === !1) return !1;
|
|
265
265
|
let x = Math.min(row.sourceX, row.targetX), y = Math.min(row.sourceY, row.targetY);
|
|
266
266
|
return !rectsOverlap({
|
|
267
267
|
x,
|
|
@@ -271,8 +271,6 @@ const getEdgeId = (connection) => {
|
|
|
271
271
|
}, cullingViewport);
|
|
272
272
|
}, getDefaultFlowStateProps = () => ({
|
|
273
273
|
id: "1",
|
|
274
|
-
nodes: [],
|
|
275
|
-
edges: [],
|
|
276
274
|
nodeOrigin: [0, 0],
|
|
277
275
|
nodeExtent: infiniteExtent,
|
|
278
276
|
defaultEdgeOptions: {},
|
|
@@ -573,7 +571,9 @@ const createLayoutedEdges = (source) => {
|
|
|
573
571
|
zoom: 1
|
|
574
572
|
};
|
|
575
573
|
}, createFlowState = (props, injections = {}) => {
|
|
576
|
-
let _props = merge(getDefaultFlowStateProps(), props), initialNodeTypes = injections.initialNodeTypes ?? {}, initialEdgeTypes = injections.initialEdgeTypes ?? {}, prefersDark = injections.prefersDark ?? (() => _props.colorModeSSR === "dark"), [config, setConfig] = createSignal(_props), [ariaLabelConfig, setAriaLabelConfig] = createSignal(() => mergeAriaLabelConfig(config().ariaLabelConfig)), [ariaLiveMessage, setAriaLiveMessage] = createSignal(() => config().ariaLiveMessage), [clickConnectStartHandle, setClickConnectStartHandle] = createSignal(void 0), [connection, setConnection] = createSignal(initialConnection), [domNode, setDomNode] = createSignal(null), [dragging, setDragging] = createSignal(!1), [elementsSelectable, setElementsSelectable] = createSignal(() => config().elementsSelectable), [height, setHeight] = createSignal(() => config().height), [minZoom, _setMinZoom] = createSignal(() => config().minZoom), [maxZoom, _setMaxZoom] = createSignal(() => config().maxZoom), [nodesConnectable, setNodesConnectable] = createSignal(() => config().nodesDraggable), [nodesDraggable, setNodesDraggable] = createSignal(() => config().nodesDraggable), [panZoom, setPanZoom] = createSignal(null), [selectionRect, setSelectionRect] = createSignal(), [selectionRectMode, setSelectionRectMode] = createSignal(), [snapGrid, setSnapGrid] = createSignal(() => config().snapGrid), [translateExtent, _setTranslateExtent] = createSignal(() => config().translateExtent ?? infiniteExtent), [width, setWidth] = createSignal(() => config().width), [selectionKeyPressed, setSelectionKeyPressed] = createSignal(!1), [multiselectionKeyPressed, setMultiselectionKeyPressed] = createSignal(!1), [deleteKeyPressed, setDeleteKeyPressed] = createSignal(!1), [panActivationKeyPressed, setPanActivationKeyPressed] = createSignal(!1), [zoomActivationKeyPressed, setZoomActivationKeyPressed] = createSignal(!1)
|
|
574
|
+
let _props = merge(getDefaultFlowStateProps(), props), initialNodeTypes = injections.initialNodeTypes ?? {}, initialEdgeTypes = injections.initialEdgeTypes ?? {}, prefersDark = injections.prefersDark ?? (() => _props.colorModeSSR === "dark"), [config, setConfig] = createSignal(_props), [ariaLabelConfig, setAriaLabelConfig] = createSignal(() => mergeAriaLabelConfig(config().ariaLabelConfig)), [ariaLiveMessage, setAriaLiveMessage] = createSignal(() => config().ariaLiveMessage), [clickConnectStartHandle, setClickConnectStartHandle] = createSignal(void 0), [connection, setConnection] = createSignal(initialConnection), [domNode, setDomNode] = createSignal(null), [dragging, setDragging] = createSignal(!1), [elementsSelectable, setElementsSelectable] = createSignal(() => config().elementsSelectable), [height, setHeight] = createSignal(() => config().height), [minZoom, _setMinZoom] = createSignal(() => config().minZoom), [maxZoom, _setMaxZoom] = createSignal(() => config().maxZoom), [nodesConnectable, setNodesConnectable] = createSignal(() => config().nodesDraggable), [nodesDraggable, setNodesDraggable] = createSignal(() => config().nodesDraggable), [panZoom, setPanZoom] = createSignal(null), [selectionRect, setSelectionRect] = createSignal(), [selectionRectMode, setSelectionRectMode] = createSignal(), [snapGrid, setSnapGrid] = createSignal(() => config().snapGrid), [translateExtent, _setTranslateExtent] = createSignal(() => config().translateExtent ?? infiniteExtent), [width, setWidth] = createSignal(() => config().width), [selectionKeyPressed, setSelectionKeyPressed] = createSignal(!1), [multiselectionKeyPressed, setMultiselectionKeyPressed] = createSignal(!1), [deleteKeyPressed, setDeleteKeyPressed] = createSignal(!1), [panActivationKeyPressed, setPanActivationKeyPressed] = createSignal(!1), [zoomActivationKeyPressed, setZoomActivationKeyPressed] = createSignal(!1);
|
|
575
|
+
props.nodes !== void 0 && props.defaultNodes, props.edges !== void 0 && props.defaultEdges;
|
|
576
|
+
let [nodesStore, setNodesStore] = createStore(props.nodes ?? [...props.defaultNodes ?? []]), [edgesStore, setEdgesStore] = createStore(props.edges ?? [...props.defaultEdges ?? []]), nodeSeedAdopted = props.nodes !== void 0 || props.defaultNodes !== void 0, edgeSeedAdopted = props.edges !== void 0 || props.defaultEdges !== void 0, [measurementsStore, setMeasurementsStore] = createStore({}), internalNodes = createInternalNodes({
|
|
577
577
|
get nodes() {
|
|
578
578
|
return nodesStore;
|
|
579
579
|
},
|
|
@@ -595,16 +595,21 @@ const createLayoutedEdges = (source) => {
|
|
|
595
595
|
}), nodeLookup = new RecordMapFacade(internalNodes), initialViewport = getInitialViewport(_props.fitView, _props.initialViewport, _props.width ?? 0, _props.height ?? 0, nodeLookup), [viewportStore, setViewportStore] = createStore(_props.viewport ?? initialViewport);
|
|
596
596
|
createEffect(() => {
|
|
597
597
|
let next = config().nodes;
|
|
598
|
-
for (let node of next);
|
|
598
|
+
if (next) for (let node of next);
|
|
599
599
|
return { next };
|
|
600
600
|
}, ({ next }) => {
|
|
601
|
-
setNodesStore(() => next);
|
|
601
|
+
next && (nodeSeedAdopted = !0, setNodesStore(() => next));
|
|
602
602
|
}, { defer: !0 }), createEffect(() => {
|
|
603
603
|
let next = config().edges;
|
|
604
|
-
for (let edge of next);
|
|
604
|
+
if (next) for (let edge of next);
|
|
605
605
|
return { next };
|
|
606
606
|
}, ({ next }) => {
|
|
607
|
-
setEdgesStore(() => next);
|
|
607
|
+
next && (edgeSeedAdopted = !0, setEdgesStore(() => next));
|
|
608
|
+
}, { defer: !0 }), createEffect(() => ({
|
|
609
|
+
nodes: config().defaultNodes,
|
|
610
|
+
edges: config().defaultEdges
|
|
611
|
+
}), ({ nodes: defaultNodes, edges: defaultEdges }) => {
|
|
612
|
+
defaultNodes && !nodeSeedAdopted && config().nodes === void 0 && (nodeSeedAdopted = !0, setNodesStore(() => [...defaultNodes])), defaultEdges && !edgeSeedAdopted && config().edges === void 0 && (edgeSeedAdopted = !0, setEdgesStore(() => [...defaultEdges]));
|
|
608
613
|
}, { defer: !0 }), createEffect(() => config().viewport, (next) => {
|
|
609
614
|
next && setViewportStore(() => next);
|
|
610
615
|
}, { defer: !0 });
|
|
@@ -2385,6 +2390,34 @@ function matchesKey(event, keyDef) {
|
|
|
2385
2390
|
function matchesKeyArray(event, keyDefs) {
|
|
2386
2391
|
return keyDefs ? (Array.isArray(keyDefs) ? keyDefs : [keyDefs]).some((keyDef) => matchesKey(event, keyDef)) : !1;
|
|
2387
2392
|
}
|
|
2393
|
+
const MODIFIER_FLAG = {
|
|
2394
|
+
alt: "altKey",
|
|
2395
|
+
control: "ctrlKey",
|
|
2396
|
+
ctrl: "ctrlKey",
|
|
2397
|
+
meta: "metaKey",
|
|
2398
|
+
shift: "shiftKey"
|
|
2399
|
+
};
|
|
2400
|
+
/**
|
|
2401
|
+
* Whether an event's modifier flags PROVE this key definition cannot be held
|
|
2402
|
+
* right now: the key itself is a modifier whose flag is off, or one of its
|
|
2403
|
+
* required modifiers is off. Definitions built on non-modifier keys are never
|
|
2404
|
+
* contradicted (their state isn't derivable from flags).
|
|
2405
|
+
*/
|
|
2406
|
+
function isContradicted(flags, keyDef) {
|
|
2407
|
+
let keyFlag = MODIFIER_FLAG[getKeyString(keyDef).toLowerCase()];
|
|
2408
|
+
if (keyFlag && !flags[keyFlag]) return !0;
|
|
2409
|
+
let modifiers = getModifier(keyDef);
|
|
2410
|
+
return (Array.isArray(modifiers) ? modifiers.flatMap((mod) => mod) : [modifiers]).some((mod) => {
|
|
2411
|
+
let modFlag = MODIFIER_FLAG[mod.toLowerCase()];
|
|
2412
|
+
return !!modFlag && !flags[modFlag];
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
/** True only when EVERY definition for this key state is contradicted. */
|
|
2416
|
+
function allContradicted(flags, keyDefs) {
|
|
2417
|
+
if (!keyDefs) return !1;
|
|
2418
|
+
let keys = Array.isArray(keyDefs) ? keyDefs : [keyDefs];
|
|
2419
|
+
return keys.length > 0 && keys.every((keyDef) => isContradicted(flags, keyDef));
|
|
2420
|
+
}
|
|
2388
2421
|
const KeyHandler = (props) => {
|
|
2389
2422
|
let { store, actions } = useInternalSolidFlow(), { deleteElements } = useSolidFlow(), _props = {
|
|
2390
2423
|
get selectionKey() {
|
|
@@ -2404,6 +2437,20 @@ const KeyHandler = (props) => {
|
|
|
2404
2437
|
}
|
|
2405
2438
|
}, resetKeysAndSelection = () => {
|
|
2406
2439
|
actions.setSelectionRect(void 0), actions.setSelectionKeyPressed(!1), actions.setMultiselectionKeyPressed(!1), actions.setDeleteKeyPressed(!1), actions.setPanActivationKeyPressed(!1), actions.setZoomActivationKeyPressed(!1);
|
|
2440
|
+
}, reconcileModifiers = (event) => {
|
|
2441
|
+
let changed = !1;
|
|
2442
|
+
store.selectionKeyPressed && allContradicted(event, _props.selectionKey) && (actions.setSelectionKeyPressed(!1), changed = !0), store.multiselectionKeyPressed && allContradicted(event, _props.multiSelectionKey) && (actions.setMultiselectionKeyPressed(!1), changed = !0), store.panActivationKeyPressed && allContradicted(event, _props.panActivationKey) && (actions.setPanActivationKeyPressed(!1), changed = !0), store.zoomActivationKeyPressed && allContradicted(event, _props.zoomActivationKey) && (actions.setZoomActivationKeyPressed(!1), changed = !0), changed && flush();
|
|
2443
|
+
}, cancelPointerGestures = () => {
|
|
2444
|
+
let release = (Ctor, type) => {
|
|
2445
|
+
try {
|
|
2446
|
+
window.dispatchEvent(new Ctor(type, { view: window }));
|
|
2447
|
+
} catch {
|
|
2448
|
+
window.dispatchEvent(new Ctor(type));
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
release(MouseEvent, "mouseup"), typeof PointerEvent < "u" && release(PointerEvent, "pointerup");
|
|
2452
|
+
}, handleWindowBlur = () => {
|
|
2453
|
+
resetKeysAndSelection(), cancelPointerGestures();
|
|
2407
2454
|
}, handleDelete = async () => {
|
|
2408
2455
|
let selectedNodes = store.nodes.filter((node) => node.selected), selectedEdges = store.edges.filter((edge) => edge.selected), { deletedNodes, deletedEdges } = await deleteElements({
|
|
2409
2456
|
nodes: selectedNodes,
|
|
@@ -2414,16 +2461,22 @@ const KeyHandler = (props) => {
|
|
|
2414
2461
|
edges: deletedEdges
|
|
2415
2462
|
});
|
|
2416
2463
|
};
|
|
2417
|
-
return isServer || createEventListenerMap(window, {
|
|
2464
|
+
return isServer || (createEventListenerMap(window, {
|
|
2418
2465
|
keydown: (event) => {
|
|
2419
|
-
matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!0), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!0), matchesKeyArray(event, _props.deleteKey) && !isInputDOMNode(event) && (event.ctrlKey || event.metaKey || event.shiftKey || (actions.setDeleteKeyPressed(!0), handleDelete())), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!0), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!0), flush();
|
|
2466
|
+
reconcileModifiers(event), matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!0), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!0), matchesKeyArray(event, _props.deleteKey) && !isInputDOMNode(event) && (event.ctrlKey || event.metaKey || event.shiftKey || (actions.setDeleteKeyPressed(!0), handleDelete())), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!0), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!0), flush();
|
|
2420
2467
|
},
|
|
2421
2468
|
keyup: (event) => {
|
|
2422
|
-
matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!1), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!1), matchesKeyArray(event, _props.deleteKey) && actions.setDeleteKeyPressed(!1), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!1), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!1), flush();
|
|
2469
|
+
reconcileModifiers(event), matchesKeyArray(event, _props.selectionKey) && actions.setSelectionKeyPressed(!1), matchesKeyArray(event, _props.multiSelectionKey) && actions.setMultiselectionKeyPressed(!1), matchesKeyArray(event, _props.deleteKey) && actions.setDeleteKeyPressed(!1), matchesKeyArray(event, _props.panActivationKey) && actions.setPanActivationKeyPressed(!1), matchesKeyArray(event, _props.zoomActivationKey) && actions.setZoomActivationKeyPressed(!1), flush();
|
|
2423
2470
|
},
|
|
2424
|
-
blur:
|
|
2471
|
+
blur: handleWindowBlur,
|
|
2425
2472
|
contextmenu: resetKeysAndSelection
|
|
2426
|
-
}),
|
|
2473
|
+
}), createEventListenerMap(window, {
|
|
2474
|
+
pointerdown: reconcileModifiers,
|
|
2475
|
+
wheel: reconcileModifiers
|
|
2476
|
+
}, {
|
|
2477
|
+
capture: !0,
|
|
2478
|
+
passive: !0
|
|
2479
|
+
})), null;
|
|
2427
2480
|
}, SolidFlow = (props) => {
|
|
2428
2481
|
let [domNodeRef, setDomNodeRef] = createSignal(), domNode, _props = merge({
|
|
2429
2482
|
...getDefaultFlowStateProps(),
|
|
@@ -2451,7 +2504,7 @@ const KeyHandler = (props) => {
|
|
|
2451
2504
|
zoomOnPinch: !0,
|
|
2452
2505
|
zoomOnDoubleClick: !0,
|
|
2453
2506
|
zoomOnScroll: !0
|
|
2454
|
-
}, props), htmlProps = omit(_props, "nodes", "edges", "nodeTypes", "edgeTypes", "width", "height", "fitView", "fitViewOptions", "nodeOrigin", "nodeDragThreshold", "paneClickDistance", "nodeClickDistance", "minZoom", "maxZoom", "zIndexMode", "initialViewport", "viewport", "translateExtent", "nodeExtent", "selectionKey", "panActivationKey", "deleteKey", "multiSelectionKey", "zoomActivationKey", "panOnDrag", "panOnScroll", "panOnScrollMode", "panOnScrollSpeed", "selectionOnDrag", "selectNodesOnDrag", "preventScrolling", "zoomOnScroll", "zoomOnDoubleClick", "zoomOnPinch", "onlyRenderVisibleElements", "autoPanOnConnect", "autoPanOnNodeDrag", "autoPanOnNodeFocus", "autoPanOnSelection", "autoPanSpeed", "connectionRadius", "connectionMode", "connectionLineType", "connectionLineComponent", "connectionLineStyle", "connectionLineContainerStyle", "connectionDragThreshold", "isValidConnection", "clickConnect", "reconnectRadius", "selectionMode", "elementsSelectable", "nodesDraggable", "nodesConnectable", "nodesFocusable", "edgesFocusable", "disableKeyboardA11y", "ariaLabelConfig", "ariaLiveMessage", "colorMode", "colorModeSSR", "class", "style", "snapGrid", "defaultMarkerColor", "defaultEdgeOptions", "elevateNodesOnSelect", "elevateEdgesOnSelect", "noDragClass", "noPanClass", "noWheelClass", "attributionPosition", "proOptions", "onInit", "onMoveStart", "onMove", "onMoveEnd", "onFlowError", "onNodeClick", "onNodeContextMenu", "onNodeDrag", "onNodeDragStart", "onNodeDragStop", "onNodePointerEnter", "onNodePointerMove", "onNodePointerLeave", "onEdgeClick", "onEdgeContextMenu", "onEdgePointerEnter", "onEdgePointerLeave", "onPaneClick", "onPaneContextMenu", "onSelectionChange", "onSelectionClick", "onSelectionContextMenu", "onSelectionDrag", "onSelectionDragStart", "onSelectionDragStop", "onSelectionStart", "onSelectionEnd", "onConnect", "onConnectStart", "onConnectEnd", "onReconnect", "onReconnectStart", "onReconnectEnd", "onClickConnectStart", "onClickConnectEnd", "onBeforeConnect", "onBeforeReconnect", "onDelete", "onBeforeDelete", "deleteKeyCode", "selectionKeyCode", "panActivationKeyCode", "multiSelectionKeyCode", "zoomActivationKeyCode", "children"), TypedSolidFlowContext = SolidFlowContext, solidFlow = useContext(TypedSolidFlowContext) ?? createSolidFlow(_props), { store, actions } = solidFlow;
|
|
2507
|
+
}, props), htmlProps = omit(_props, "nodes", "edges", "defaultNodes", "defaultEdges", "nodeTypes", "edgeTypes", "width", "height", "fitView", "fitViewOptions", "nodeOrigin", "nodeDragThreshold", "paneClickDistance", "nodeClickDistance", "minZoom", "maxZoom", "zIndexMode", "initialViewport", "viewport", "translateExtent", "nodeExtent", "selectionKey", "panActivationKey", "deleteKey", "multiSelectionKey", "zoomActivationKey", "panOnDrag", "panOnScroll", "panOnScrollMode", "panOnScrollSpeed", "selectionOnDrag", "selectNodesOnDrag", "preventScrolling", "zoomOnScroll", "zoomOnDoubleClick", "zoomOnPinch", "onlyRenderVisibleElements", "autoPanOnConnect", "autoPanOnNodeDrag", "autoPanOnNodeFocus", "autoPanOnSelection", "autoPanSpeed", "connectionRadius", "connectionMode", "connectionLineType", "connectionLineComponent", "connectionLineStyle", "connectionLineContainerStyle", "connectionDragThreshold", "isValidConnection", "clickConnect", "reconnectRadius", "selectionMode", "elementsSelectable", "nodesDraggable", "nodesConnectable", "nodesFocusable", "edgesFocusable", "disableKeyboardA11y", "ariaLabelConfig", "ariaLiveMessage", "colorMode", "colorModeSSR", "class", "style", "snapGrid", "defaultMarkerColor", "defaultEdgeOptions", "elevateNodesOnSelect", "elevateEdgesOnSelect", "noDragClass", "noPanClass", "noWheelClass", "attributionPosition", "proOptions", "onInit", "onMoveStart", "onMove", "onMoveEnd", "onFlowError", "onNodeClick", "onNodeContextMenu", "onNodeDrag", "onNodeDragStart", "onNodeDragStop", "onNodePointerEnter", "onNodePointerMove", "onNodePointerLeave", "onEdgeClick", "onEdgeContextMenu", "onEdgePointerEnter", "onEdgePointerLeave", "onPaneClick", "onPaneContextMenu", "onSelectionChange", "onSelectionClick", "onSelectionContextMenu", "onSelectionDrag", "onSelectionDragStart", "onSelectionDragStop", "onSelectionStart", "onSelectionEnd", "onConnect", "onConnectStart", "onConnectEnd", "onReconnect", "onReconnectStart", "onReconnectEnd", "onClickConnectStart", "onClickConnectEnd", "onBeforeConnect", "onBeforeReconnect", "onDelete", "onBeforeDelete", "deleteKeyCode", "selectionKeyCode", "panActivationKeyCode", "multiSelectionKeyCode", "zoomActivationKeyCode", "children"), TypedSolidFlowContext = SolidFlowContext, solidFlow = useContext(TypedSolidFlowContext) ?? createSolidFlow(_props), { store, actions } = solidFlow;
|
|
2455
2508
|
onSettled(() => (actions.applyInitialFitView(_props.fitView), actions.setConfig(_props), actions.setDomNode(domNode), () => {
|
|
2456
2509
|
actions.reset();
|
|
2457
2510
|
})), createEffect(() => domNodeRef(), (el) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dschz/solid-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-next.6",
|
|
4
4
|
"description": "Solid Flow - A highly customizable Solid library for building node-based editors, workflow systems, diagrams and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai workflows",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"start": "vite",
|
|
88
88
|
"test": "vitest run",
|
|
89
89
|
"test:cov": "vitest run --coverage",
|
|
90
|
+
"test:e2e": "playwright test",
|
|
90
91
|
"typecheck": "tsc --noEmit",
|
|
91
92
|
"test:ssr": "vitest run --config vite.config.ssr.ts"
|
|
92
93
|
},
|
|
@@ -98,8 +99,9 @@
|
|
|
98
99
|
},
|
|
99
100
|
"devDependencies": {
|
|
100
101
|
"@changesets/cli": "2.31.1",
|
|
101
|
-
"@dagrejs/dagre": "
|
|
102
|
+
"@dagrejs/dagre": "3.1.1",
|
|
102
103
|
"@dom-expressions/compiler": "0.50.0-next.43",
|
|
104
|
+
"@playwright/test": "1.62.1",
|
|
103
105
|
"@solidjs/testing-library": "1.0.0-beta.2",
|
|
104
106
|
"@solidjs/vite-plugin": "3.0.0-next.28",
|
|
105
107
|
"@testing-library/jest-dom": "7.0.0",
|