@dschz/solid-flow 0.3.0-next.4 → 0.3.0-next.5
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 +236 -224
- package/dist/index/index.d.ts +19 -1
- package/dist/index/index.js +56 -8
- package/dist/index/index.jsx +56 -8
- package/package.json +3 -1
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
|
+
| `0.3.x` | `solid-js` 2.x | Active development |
|
|
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 0.3 line is built for SolidJS 2.0 and its deferred, fine-grained reactive graph. 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,269 @@ 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 { EdgesFor, NodesFor } 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 NodesFor<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
|
+
## The flow API
|
|
187
154
|
|
|
188
|
-
|
|
189
|
-
// composable custom nodes and edge labels without prop drilling)
|
|
190
|
-
const nodeId = useNodeId();
|
|
191
|
-
const edgeId = useEdgeId();
|
|
192
|
-
```
|
|
155
|
+
`useSolidFlow()` returns `{ flow, commands }` (with `commands` also spread at the top level for React/Svelte Flow familiarity). Both are stable identities — destructuring is safe.
|
|
193
156
|
|
|
194
|
-
|
|
157
|
+
```tsx
|
|
158
|
+
import { SolidFlowProvider, useSolidFlow } from "@dschz/solid-flow";
|
|
195
159
|
|
|
196
|
-
|
|
197
|
-
|
|
160
|
+
const Toolbar = () => {
|
|
161
|
+
const { flow, commands } = useSolidFlow();
|
|
198
162
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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.
|
|
163
|
+
// flow.* reads are reactive: use them in JSX or tracked scopes.
|
|
164
|
+
// flow.nodes, flow.edges, flow.viewport, flow.selection,
|
|
165
|
+
// flow.nodesInitialized, flow.viewportInitialized, ...
|
|
206
166
|
|
|
207
|
-
|
|
167
|
+
return (
|
|
168
|
+
<div>
|
|
169
|
+
<span>{flow.selection.nodes.length} selected</span>
|
|
170
|
+
<button onClick={() => commands.fitView()}>Fit</button>
|
|
171
|
+
<button onClick={() => commands.zoomIn()}>+</button>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
208
175
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
import { produce } from "solid-js/store";
|
|
216
|
-
setNodes(
|
|
217
|
-
(node) => node.id === "1",
|
|
218
|
-
produce((node) => {
|
|
219
|
-
node.position.x += 20;
|
|
220
|
-
}),
|
|
176
|
+
// Hooks used outside <SolidFlow> children need a provider:
|
|
177
|
+
export const App = () => (
|
|
178
|
+
<SolidFlowProvider>
|
|
179
|
+
<Toolbar />
|
|
180
|
+
<Flow />
|
|
181
|
+
</SolidFlowProvider>
|
|
221
182
|
);
|
|
183
|
+
```
|
|
222
184
|
|
|
223
|
-
|
|
224
|
-
setEdges(addEdge(connection, edges));
|
|
185
|
+
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
186
|
|
|
226
|
-
|
|
227
|
-
const { screenToFlowPosition, flowToScreenPosition } = useSolidFlow();
|
|
187
|
+
## Hooks
|
|
228
188
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
189
|
+
```tsx
|
|
190
|
+
const solidFlow = useSolidFlow(); // { flow, commands } — the main API
|
|
191
|
+
const nodes = useNodes(); // reactive readonly node array
|
|
192
|
+
const edges = useEdges(); // reactive readonly edge array
|
|
193
|
+
const viewport = useViewport(); // reactive viewport accessor
|
|
194
|
+
const connection = useConnection(); // in-progress connection state
|
|
195
|
+
|
|
196
|
+
// Hook parameters that feed reactive reads are accessors, so they can't
|
|
197
|
+
// silently go stale when derived from props:
|
|
198
|
+
const nodeData = useNodesData(() => ["node-1", "node-2"]);
|
|
199
|
+
const connections = useNodeConnections(() => ({ id: "node-1" }));
|
|
200
|
+
const internal = useInternalNode(() => props.nodeId); // measured/internal record
|
|
201
|
+
|
|
202
|
+
const updateInternals = useUpdateNodeInternals(); // re-measure after handle changes
|
|
203
|
+
|
|
204
|
+
// Which node/edge a nested component is rendered inside — for composable
|
|
205
|
+
// custom nodes and edge labels without prop drilling:
|
|
206
|
+
const nodeId = useNodeId();
|
|
207
|
+
const edgeId = useEdgeId();
|
|
232
208
|
```
|
|
233
209
|
|
|
234
|
-
|
|
210
|
+
### Coming from React Flow?
|
|
235
211
|
|
|
236
|
-
|
|
212
|
+
Some React Flow hooks intentionally have no Solid Flow equivalent, because Solid's fine-grained reactivity makes them unnecessary:
|
|
237
213
|
|
|
238
|
-
|
|
214
|
+
- `useOnSelectionChange` / `useOnViewportChange` — reading `flow.selection` or `flow.viewport` in a tracked scope IS the subscription; wrap side effects in `createEffect` over those reads.
|
|
215
|
+
- `useNodesState` / `useEdgesState` — use `createNodeStore` / `createEdgeStore` instead.
|
|
216
|
+
- `useStore` — the reactive `flow` struct is the supported read surface; there is no public escape hatch into internals.
|
|
217
|
+
|
|
218
|
+
## Custom edges and reconnection
|
|
239
219
|
|
|
240
220
|
```tsx
|
|
241
|
-
import {
|
|
221
|
+
import { BaseEdge, EdgeReconnectAnchor, type EdgeProps, getBezierPath } from "@dschz/solid-flow";
|
|
222
|
+
|
|
223
|
+
const ReconnectableEdge = (props: EdgeProps) => {
|
|
224
|
+
const path = () =>
|
|
225
|
+
getBezierPath({
|
|
226
|
+
sourceX: props.sourceX,
|
|
227
|
+
sourceY: props.sourceY,
|
|
228
|
+
sourcePosition: props.sourcePosition,
|
|
229
|
+
targetX: props.targetX,
|
|
230
|
+
targetY: props.targetY,
|
|
231
|
+
targetPosition: props.targetPosition,
|
|
232
|
+
})[0];
|
|
242
233
|
|
|
243
|
-
// Type-safe custom node component
|
|
244
|
-
function CustomNode(props: NodeProps<{ label: string }, "custom">) {
|
|
245
234
|
return (
|
|
246
|
-
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
<
|
|
250
|
-
<
|
|
251
|
-
|
|
235
|
+
<>
|
|
236
|
+
<BaseEdge path={path()} markerEnd={props.markerEnd} />
|
|
237
|
+
{/* drag either end of a selected edge onto another handle */}
|
|
238
|
+
<EdgeReconnectAnchor type="source" position={{ x: props.sourceX, y: props.sourceY }} />
|
|
239
|
+
<EdgeReconnectAnchor type="target" position={{ x: props.targetX, y: props.targetY }} />
|
|
240
|
+
</>
|
|
252
241
|
);
|
|
253
|
-
}
|
|
242
|
+
};
|
|
243
|
+
```
|
|
254
244
|
|
|
255
|
-
|
|
256
|
-
const nodeTypes = {
|
|
257
|
-
custom: CustomNode,
|
|
258
|
-
} satisfies NodeTypes;
|
|
245
|
+
Reconnection lifecycle callbacks (`onReconnectStart`, `onReconnect`, `onReconnectEnd`, `onBeforeReconnect`) are available on `<SolidFlow>`.
|
|
259
246
|
|
|
260
|
-
|
|
261
|
-
const [nodes] = createNodeStore<typeof nodeTypes>([...]);
|
|
247
|
+
## Built-in components
|
|
262
248
|
|
|
263
|
-
|
|
264
|
-
```
|
|
249
|
+
**Node types:** `input` (source handle only), `output` (target handle only), `default` (both), `group` (container for subflows)
|
|
265
250
|
|
|
266
|
-
|
|
251
|
+
**Edge types:** `default` (bezier), `straight`, `step`, `smoothstep`
|
|
267
252
|
|
|
268
|
-
|
|
269
|
-
import { type Connection } from "@dschz/solid-flow";
|
|
253
|
+
**Plugins:**
|
|
270
254
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
255
|
+
- **Background** — dots, lines, and cross patterns
|
|
256
|
+
- **Controls** — zoom in/out, fit view, lock interactions
|
|
257
|
+
- **MiniMap** — interactive overview with viewport indicator, custom node rendering via `nodeComponent`, and `onClick` / `onNodeClick` handlers
|
|
258
|
+
- **NodeToolbar** — context-sensitive toolbars for nodes
|
|
259
|
+
- **NodeResizer** — interactive node resizing with handles
|
|
275
260
|
|
|
276
|
-
|
|
277
|
-
console.log("New connection:", connection);
|
|
278
|
-
setEdges(addEdge(connection, edges));
|
|
279
|
-
};
|
|
261
|
+
## Performance
|
|
280
262
|
|
|
281
|
-
|
|
282
|
-
isValidConnection={isValidConnection}
|
|
283
|
-
onConnect={onConnect}
|
|
284
|
-
...
|
|
285
|
-
/>
|
|
286
|
-
```
|
|
263
|
+
Two complementary culling tiers keep large graphs fast:
|
|
287
264
|
|
|
288
|
-
|
|
265
|
+
- **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.
|
|
266
|
+
- **`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
267
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
onPaneClick={(event) => console.log("Pane clicked")}
|
|
296
|
-
onSelectionChange={(params) => console.log("Selection changed:", params)}
|
|
297
|
-
/>
|
|
298
|
-
```
|
|
268
|
+
The MiniMap always renders the full graph in either mode — it reads the data graph, not the DOM.
|
|
269
|
+
|
|
270
|
+
## Server-side rendering
|
|
271
|
+
|
|
272
|
+
`<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
273
|
|
|
300
274
|
## Accessibility
|
|
301
275
|
|
|
302
|
-
|
|
276
|
+
- Full keyboard navigation (arrow-key node movement, focus traversal, Escape to deselect)
|
|
277
|
+
- Screen reader support with ARIA labels and live announcements
|
|
278
|
+
- Focus management, including under viewport culling
|
|
279
|
+
- Color mode support (`light` / `dark` / `system`)
|
|
303
280
|
|
|
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
|
|
281
|
+
## Migrating from 0.2.x
|
|
309
282
|
|
|
310
|
-
|
|
283
|
+
The 0.3 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
284
|
|
|
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
|
|
285
|
+
**1. Upgrade the peer dependencies.** `solid-js` and `@solidjs/web` move to matching 2.0 versions.
|
|
316
286
|
|
|
317
|
-
|
|
287
|
+
**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.
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
// 0.2.x (SolidJS 1.x) — path syntax
|
|
291
|
+
setNodes(0, "position", "x", (x) => x + 20);
|
|
292
|
+
setEdges((edge) => edge.id === "e1", "animated", true);
|
|
293
|
+
|
|
294
|
+
// 0.3.x (SolidJS 2.0) — draft callback
|
|
295
|
+
setNodes((nodes) => {
|
|
296
|
+
nodes[0]!.position.x += 20;
|
|
297
|
+
});
|
|
298
|
+
setEdges((edges) => {
|
|
299
|
+
const edge = edges.find((e) => e.id === "e1");
|
|
300
|
+
if (edge) edge.animated = true;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// wholesale replacement (re-seeds the flow; rows keyed by id are reused)
|
|
304
|
+
setNodes(() => nextNodes);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**3. `useSolidFlow` reads moved to the reactive `flow` struct.** The flat getters (`getNodes()`, `getEdges()`, `getNode(id)`, `getEdge(id)`, `getInternalNode(id)`, `getViewport()`, `getZoom()`) are removed:
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
// 0.2.x // 0.3.x
|
|
311
|
+
solidFlow.getNodes();
|
|
312
|
+
flow.nodes;
|
|
313
|
+
solidFlow.getViewport();
|
|
314
|
+
flow.viewport;
|
|
315
|
+
solidFlow.getZoom();
|
|
316
|
+
flow.viewport.zoom;
|
|
317
|
+
solidFlow.getNode("a");
|
|
318
|
+
flow.nodes.find((n) => n.id === "a");
|
|
319
|
+
solidFlow.getInternalNode("a");
|
|
320
|
+
useInternalNode(() => "a");
|
|
321
|
+
```
|
|
318
322
|
|
|
319
|
-
|
|
323
|
+
`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
324
|
|
|
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
|
|
325
|
+
**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 0.3's 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
326
|
|
|
330
|
-
|
|
327
|
+
**5. `onlyRenderVisibleElements` now does what it says.** In 0.2.x the prop was accepted but inert. In 0.3 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.
|
|
328
|
+
|
|
329
|
+
**6. Smaller signature changes.** `useNodes()` / `useEdges()` return `readonly` arrays; `useHandleEdgeSelect` is removed (it was internal plumbing — select edges through `commands`).
|
|
330
|
+
|
|
331
|
+
## Examples
|
|
332
|
+
|
|
333
|
+
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
334
|
|
|
332
335
|
```bash
|
|
336
|
+
bun install
|
|
333
337
|
bun start
|
|
334
338
|
```
|
|
335
339
|
|
|
@@ -359,6 +363,14 @@ bun install
|
|
|
359
363
|
bun start
|
|
360
364
|
```
|
|
361
365
|
|
|
366
|
+
### Testing
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
bun run test # unit + component tests (vitest, jsdom)
|
|
370
|
+
bun run test:ssr # SSR lane (node environment, server builds)
|
|
371
|
+
bun run test:e2e # browser gesture harness (Playwright)
|
|
372
|
+
```
|
|
373
|
+
|
|
362
374
|
### Linting & Formatting
|
|
363
375
|
|
|
364
376
|
```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"
|
|
@@ -866,7 +881,10 @@ type SolidFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge>
|
|
|
866
881
|
* Component-LOCAL state does not survive — signals created inside a
|
|
867
882
|
* custom node, uncontrolled inputs, scroll positions of inner elements.
|
|
868
883
|
* Keep state you care about in `node.data`. Never unmounted: selected
|
|
869
|
-
* elements, unmeasured nodes, and the node holding DOM focus.
|
|
884
|
+
* elements, unmeasured nodes, and the node holding DOM focus. Elements
|
|
885
|
+
* whose component-local state (or side effects) must keep running while
|
|
886
|
+
* off-screen can opt out entirely with `cullable: false` on the node or
|
|
887
|
+
* edge — they are then exempt from both tiers.
|
|
870
888
|
*
|
|
871
889
|
* Independent of this prop, off-viewport elements are always CSS-culled
|
|
872
890
|
* (`visibility: hidden` + `pointer-events: none`, everything stays
|
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,
|
|
@@ -3278,6 +3278,34 @@ function matchesKey(event, keyDef) {
|
|
|
3278
3278
|
function matchesKeyArray(event, keyDefs) {
|
|
3279
3279
|
return keyDefs ? (Array.isArray(keyDefs) ? keyDefs : [keyDefs]).some((keyDef) => matchesKey(event, keyDef)) : !1;
|
|
3280
3280
|
}
|
|
3281
|
+
const MODIFIER_FLAG = {
|
|
3282
|
+
alt: "altKey",
|
|
3283
|
+
control: "ctrlKey",
|
|
3284
|
+
ctrl: "ctrlKey",
|
|
3285
|
+
meta: "metaKey",
|
|
3286
|
+
shift: "shiftKey"
|
|
3287
|
+
};
|
|
3288
|
+
/**
|
|
3289
|
+
* Whether an event's modifier flags PROVE this key definition cannot be held
|
|
3290
|
+
* right now: the key itself is a modifier whose flag is off, or one of its
|
|
3291
|
+
* required modifiers is off. Definitions built on non-modifier keys are never
|
|
3292
|
+
* contradicted (their state isn't derivable from flags).
|
|
3293
|
+
*/
|
|
3294
|
+
function isContradicted(flags, keyDef) {
|
|
3295
|
+
let keyFlag = MODIFIER_FLAG[getKeyString(keyDef).toLowerCase()];
|
|
3296
|
+
if (keyFlag && !flags[keyFlag]) return !0;
|
|
3297
|
+
let modifiers = getModifier(keyDef);
|
|
3298
|
+
return (Array.isArray(modifiers) ? modifiers.flatMap((mod) => mod) : [modifiers]).some((mod) => {
|
|
3299
|
+
let modFlag = MODIFIER_FLAG[mod.toLowerCase()];
|
|
3300
|
+
return !!modFlag && !flags[modFlag];
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
/** True only when EVERY definition for this key state is contradicted. */
|
|
3304
|
+
function allContradicted(flags, keyDefs) {
|
|
3305
|
+
if (!keyDefs) return !1;
|
|
3306
|
+
let keys = Array.isArray(keyDefs) ? keyDefs : [keyDefs];
|
|
3307
|
+
return keys.length > 0 && keys.every((keyDef) => isContradicted(flags, keyDef));
|
|
3308
|
+
}
|
|
3281
3309
|
const KeyHandler = (props) => {
|
|
3282
3310
|
let { store, actions } = useInternalSolidFlow(), { deleteElements } = useSolidFlow(), _props = {
|
|
3283
3311
|
get selectionKey() {
|
|
@@ -3297,6 +3325,20 @@ const KeyHandler = (props) => {
|
|
|
3297
3325
|
}
|
|
3298
3326
|
}, resetKeysAndSelection = () => {
|
|
3299
3327
|
actions.setSelectionRect(void 0), actions.setSelectionKeyPressed(!1), actions.setMultiselectionKeyPressed(!1), actions.setDeleteKeyPressed(!1), actions.setPanActivationKeyPressed(!1), actions.setZoomActivationKeyPressed(!1);
|
|
3328
|
+
}, reconcileModifiers = (event) => {
|
|
3329
|
+
let changed = !1;
|
|
3330
|
+
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();
|
|
3331
|
+
}, cancelPointerGestures = () => {
|
|
3332
|
+
let release = (Ctor, type) => {
|
|
3333
|
+
try {
|
|
3334
|
+
window.dispatchEvent(new Ctor(type, { view: window }));
|
|
3335
|
+
} catch {
|
|
3336
|
+
window.dispatchEvent(new Ctor(type));
|
|
3337
|
+
}
|
|
3338
|
+
};
|
|
3339
|
+
release(MouseEvent, "mouseup"), typeof PointerEvent < "u" && release(PointerEvent, "pointerup");
|
|
3340
|
+
}, handleWindowBlur = () => {
|
|
3341
|
+
resetKeysAndSelection(), cancelPointerGestures();
|
|
3300
3342
|
}, handleDelete = async () => {
|
|
3301
3343
|
let selectedNodes = store.nodes.filter((node) => node.selected), selectedEdges = store.edges.filter((edge) => edge.selected), { deletedNodes, deletedEdges } = await deleteElements({
|
|
3302
3344
|
nodes: selectedNodes,
|
|
@@ -3307,16 +3349,22 @@ const KeyHandler = (props) => {
|
|
|
3307
3349
|
edges: deletedEdges
|
|
3308
3350
|
});
|
|
3309
3351
|
};
|
|
3310
|
-
return isServer || createEventListenerMap(window, {
|
|
3352
|
+
return isServer || (createEventListenerMap(window, {
|
|
3311
3353
|
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();
|
|
3354
|
+
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
3355
|
},
|
|
3314
3356
|
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();
|
|
3357
|
+
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
3358
|
},
|
|
3317
|
-
blur:
|
|
3359
|
+
blur: handleWindowBlur,
|
|
3318
3360
|
contextmenu: resetKeysAndSelection
|
|
3319
|
-
}),
|
|
3361
|
+
}), createEventListenerMap(window, {
|
|
3362
|
+
pointerdown: reconcileModifiers,
|
|
3363
|
+
wheel: reconcileModifiers
|
|
3364
|
+
}, {
|
|
3365
|
+
capture: !0,
|
|
3366
|
+
passive: !0
|
|
3367
|
+
})), null;
|
|
3320
3368
|
};
|
|
3321
3369
|
//#endregion
|
|
3322
3370
|
//#region src/components/SolidFlow.tsx
|
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,
|
|
@@ -2385,6 +2385,34 @@ function matchesKey(event, keyDef) {
|
|
|
2385
2385
|
function matchesKeyArray(event, keyDefs) {
|
|
2386
2386
|
return keyDefs ? (Array.isArray(keyDefs) ? keyDefs : [keyDefs]).some((keyDef) => matchesKey(event, keyDef)) : !1;
|
|
2387
2387
|
}
|
|
2388
|
+
const MODIFIER_FLAG = {
|
|
2389
|
+
alt: "altKey",
|
|
2390
|
+
control: "ctrlKey",
|
|
2391
|
+
ctrl: "ctrlKey",
|
|
2392
|
+
meta: "metaKey",
|
|
2393
|
+
shift: "shiftKey"
|
|
2394
|
+
};
|
|
2395
|
+
/**
|
|
2396
|
+
* Whether an event's modifier flags PROVE this key definition cannot be held
|
|
2397
|
+
* right now: the key itself is a modifier whose flag is off, or one of its
|
|
2398
|
+
* required modifiers is off. Definitions built on non-modifier keys are never
|
|
2399
|
+
* contradicted (their state isn't derivable from flags).
|
|
2400
|
+
*/
|
|
2401
|
+
function isContradicted(flags, keyDef) {
|
|
2402
|
+
let keyFlag = MODIFIER_FLAG[getKeyString(keyDef).toLowerCase()];
|
|
2403
|
+
if (keyFlag && !flags[keyFlag]) return !0;
|
|
2404
|
+
let modifiers = getModifier(keyDef);
|
|
2405
|
+
return (Array.isArray(modifiers) ? modifiers.flatMap((mod) => mod) : [modifiers]).some((mod) => {
|
|
2406
|
+
let modFlag = MODIFIER_FLAG[mod.toLowerCase()];
|
|
2407
|
+
return !!modFlag && !flags[modFlag];
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
/** True only when EVERY definition for this key state is contradicted. */
|
|
2411
|
+
function allContradicted(flags, keyDefs) {
|
|
2412
|
+
if (!keyDefs) return !1;
|
|
2413
|
+
let keys = Array.isArray(keyDefs) ? keyDefs : [keyDefs];
|
|
2414
|
+
return keys.length > 0 && keys.every((keyDef) => isContradicted(flags, keyDef));
|
|
2415
|
+
}
|
|
2388
2416
|
const KeyHandler = (props) => {
|
|
2389
2417
|
let { store, actions } = useInternalSolidFlow(), { deleteElements } = useSolidFlow(), _props = {
|
|
2390
2418
|
get selectionKey() {
|
|
@@ -2404,6 +2432,20 @@ const KeyHandler = (props) => {
|
|
|
2404
2432
|
}
|
|
2405
2433
|
}, resetKeysAndSelection = () => {
|
|
2406
2434
|
actions.setSelectionRect(void 0), actions.setSelectionKeyPressed(!1), actions.setMultiselectionKeyPressed(!1), actions.setDeleteKeyPressed(!1), actions.setPanActivationKeyPressed(!1), actions.setZoomActivationKeyPressed(!1);
|
|
2435
|
+
}, reconcileModifiers = (event) => {
|
|
2436
|
+
let changed = !1;
|
|
2437
|
+
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();
|
|
2438
|
+
}, cancelPointerGestures = () => {
|
|
2439
|
+
let release = (Ctor, type) => {
|
|
2440
|
+
try {
|
|
2441
|
+
window.dispatchEvent(new Ctor(type, { view: window }));
|
|
2442
|
+
} catch {
|
|
2443
|
+
window.dispatchEvent(new Ctor(type));
|
|
2444
|
+
}
|
|
2445
|
+
};
|
|
2446
|
+
release(MouseEvent, "mouseup"), typeof PointerEvent < "u" && release(PointerEvent, "pointerup");
|
|
2447
|
+
}, handleWindowBlur = () => {
|
|
2448
|
+
resetKeysAndSelection(), cancelPointerGestures();
|
|
2407
2449
|
}, handleDelete = async () => {
|
|
2408
2450
|
let selectedNodes = store.nodes.filter((node) => node.selected), selectedEdges = store.edges.filter((edge) => edge.selected), { deletedNodes, deletedEdges } = await deleteElements({
|
|
2409
2451
|
nodes: selectedNodes,
|
|
@@ -2414,16 +2456,22 @@ const KeyHandler = (props) => {
|
|
|
2414
2456
|
edges: deletedEdges
|
|
2415
2457
|
});
|
|
2416
2458
|
};
|
|
2417
|
-
return isServer || createEventListenerMap(window, {
|
|
2459
|
+
return isServer || (createEventListenerMap(window, {
|
|
2418
2460
|
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();
|
|
2461
|
+
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
2462
|
},
|
|
2421
2463
|
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();
|
|
2464
|
+
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
2465
|
},
|
|
2424
|
-
blur:
|
|
2466
|
+
blur: handleWindowBlur,
|
|
2425
2467
|
contextmenu: resetKeysAndSelection
|
|
2426
|
-
}),
|
|
2468
|
+
}), createEventListenerMap(window, {
|
|
2469
|
+
pointerdown: reconcileModifiers,
|
|
2470
|
+
wheel: reconcileModifiers
|
|
2471
|
+
}, {
|
|
2472
|
+
capture: !0,
|
|
2473
|
+
passive: !0
|
|
2474
|
+
})), null;
|
|
2427
2475
|
}, SolidFlow = (props) => {
|
|
2428
2476
|
let [domNodeRef, setDomNodeRef] = createSignal(), domNode, _props = merge({
|
|
2429
2477
|
...getDefaultFlowStateProps(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dschz/solid-flow",
|
|
3
|
-
"version": "0.3.0-next.
|
|
3
|
+
"version": "0.3.0-next.5",
|
|
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
|
},
|
|
@@ -100,6 +101,7 @@
|
|
|
100
101
|
"@changesets/cli": "2.31.1",
|
|
101
102
|
"@dagrejs/dagre": "^1.1.5",
|
|
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",
|