@dschz/solid-flow 0.3.0-next.3 → 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.
@@ -5,9 +5,18 @@ import { Dynamic, Portal, isServer } from "@solidjs/web";
5
5
  import { createEventListener, createEventListenerMap } from "@solid-primitives/event-listener";
6
6
  //#region src/contexts/edgeId.ts
7
7
  const EdgeIdContext = createContext(null);
8
+ /**
9
+ * Returns the id of the edge this component is rendered inside. Available
10
+ * anywhere in a custom edge's subtree (provided by the edge wrapper), so
11
+ * nested components — like a custom edge label — can learn their host edge
12
+ * without prop drilling.
13
+ *
14
+ * @public
15
+ * @returns a reactive accessor for the surrounding edge's id
16
+ */
8
17
  function useEdgeId() {
9
18
  let ctx = useContext(EdgeIdContext);
10
- if (!ctx) throw Error("solid-flow: Your application must be wrapped with <SolidFlow> in order to invoke useEdgeId");
19
+ if (!ctx) throw Error("solid-flow: useEdgeId must be called inside an edge component (anywhere under a custom edge's subtree)");
11
20
  return ctx;
12
21
  }
13
22
  //#endregion
@@ -141,11 +150,13 @@ const getEdgeId = (connection) => {
141
150
  size: 25,
142
151
  reconnecting: !1,
143
152
  style: {}
144
- }), rest = omit(_props, "type", "class", "style", "position", "size", "reconnecting", "children"), { store, nodeLookup, edgeLookup, actions } = useInternalSolidFlow(), edgeId = useEdgeId(), [reconnecting, setReconnecting] = createSignal(!1);
153
+ }), rest = omit(_props, "type", "class", "style", "position", "size", "reconnecting", "onReconnectingChange", "children"), { store, nodeLookup, edgeLookup, actions } = useInternalSolidFlow(), edgeId = useEdgeId(), [reconnecting, setReconnecting] = createSignal(!1);
145
154
  if (!edgeId()) throw Error("[solid-flow]: EdgeReconnectAnchor must be used within an Edge component");
146
- let edge = () => edgeLookup[edgeId()], onPointerDown = (event) => {
155
+ let edge = () => edgeLookup[edgeId()], isReconnecting = () => _props.reconnecting || reconnecting(), setReconnectingState = (next) => {
156
+ setReconnecting(next), _props.onReconnectingChange?.(next);
157
+ }, onPointerDown = (event) => {
147
158
  if (event.button !== 0) return;
148
- setReconnecting(!0), store.onReconnectStart?.(event, edge(), _props.type);
159
+ setReconnectingState(!0), store.onReconnectStart?.(event, edge(), _props.type);
149
160
  let opposite = _props.type === "target" ? {
150
161
  nodeId: edge().source,
151
162
  handleId: edge().sourceHandle ?? null,
@@ -169,7 +180,9 @@ const getEdgeId = (connection) => {
169
180
  edgeUpdaterType: opposite.type,
170
181
  cancelConnection: actions.cancelConnection,
171
182
  panBy: actions.panBy,
172
- updateConnection: actions.setConnection,
183
+ updateConnection: (connection) => {
184
+ actions.setConnection(connection), flush();
185
+ },
173
186
  isValidConnection: store.isValidConnection,
174
187
  onConnectStart: store.onConnectStart,
175
188
  onConnectEnd: store.onConnectEnd,
@@ -181,7 +194,7 @@ const getEdgeId = (connection) => {
181
194
  newEdge = store.onBeforeReconnect?.(newEdge, edge()) ?? newEdge, newEdge && actions.setEdges((edges) => edges.map((e) => e.id === edge().id ? newEdge : e)), store.onReconnect?.(edge(), connection);
182
195
  },
183
196
  onReconnectEnd: (event, connectionState) => {
184
- setReconnecting(!1), store.onReconnectEnd?.(event, edge(), opposite.type, connectionState);
197
+ setReconnectingState(!1), store.onReconnectEnd?.(event, edge(), opposite.type, connectionState);
185
198
  },
186
199
  getTransform: () => store.transform,
187
200
  getFromHandle: () => store.connection.fromHandle,
@@ -204,7 +217,7 @@ const getEdgeId = (connection) => {
204
217
  cursor: "move",
205
218
  ..._props.style
206
219
  }}>
207
- <Show when={!reconnecting()}>{_props.children}</Show>
220
+ <Show when={!isReconnecting()}>{_props.children}</Show>
208
221
  </div>
209
222
  </EdgeLabel>;
210
223
  }, ARIA_NODE_DESC_KEY = "solid-flow__node-desc", ARIA_EDGE_DESC_KEY = "solid-flow__edge-desc", A11yDescriptions = () => {
@@ -227,7 +240,6 @@ const getEdgeId = (connection) => {
227
240
  let [store, setStore] = createStore(edges);
228
241
  return [store, setStore];
229
242
  }, STEP = .5 / 2, rectsEqual = (a, b) => a === b || !!a && !!b && a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height, rectsOverlap = (a, b) => a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y, createCullingViewport = (source) => createMemo(() => {
230
- if (!source.onlyRenderVisibleElements) return null;
231
243
  let { width, height } = source;
232
244
  if (!width || !height) return null;
233
245
  let [tx, ty, zoom] = source.transform, zoomBucket = 2 ** Math.floor(Math.log2(zoom)), bucketWidth = width / zoomBucket, bucketHeight = height / zoomBucket, stepX = bucketWidth * STEP, stepY = bucketHeight * STEP, centerX = (width / 2 - tx) / zoom, centerY = (height / 2 - ty) / zoom, quantizedX = Math.round(centerX / stepX) * stepX, quantizedY = Math.round(centerY / stepY) * stepY, halfWidth = bucketWidth * 1, halfHeight = bucketHeight * 1;
@@ -238,9 +250,9 @@ const getEdgeId = (connection) => {
238
250
  height: 2 * halfHeight
239
251
  };
240
252
  }, { equals: rectsEqual }), isNodeCulled = (node, cullingViewport) => {
241
- if (!cullingViewport || node.selected) return !1;
253
+ if (!cullingViewport || node.selected || node.cullable === !1) return !1;
242
254
  let { width, height } = node.measured;
243
- if (!width || !height) return !1;
255
+ if (!width || !height || !node.internals.handleBounds) return !1;
244
256
  let { x, y } = node.internals.positionAbsolute;
245
257
  return !rectsOverlap({
246
258
  x,
@@ -249,7 +261,7 @@ const getEdgeId = (connection) => {
249
261
  height
250
262
  }, cullingViewport);
251
263
  }, isEdgeCulled = (row, cullingViewport) => {
252
- if (!cullingViewport || row.selected) return !1;
264
+ if (!cullingViewport || row.selected || row.cullable === !1) return !1;
253
265
  let x = Math.min(row.sourceX, row.targetX), y = Math.min(row.sourceY, row.targetY);
254
266
  return !rectsOverlap({
255
267
  x,
@@ -291,7 +303,7 @@ const getEdgeId = (connection) => {
291
303
  selectNodesOnDrag: !0,
292
304
  elevateNodesOnSelect: !0,
293
305
  zIndexMode: "basic",
294
- onlyRenderVisibleElements: !0,
306
+ onlyRenderVisibleElements: !1,
295
307
  disableKeyboardA11y: !1,
296
308
  defaultMarkerColor: "#b1b1b7",
297
309
  ariaLiveMessage: "",
@@ -581,9 +593,17 @@ const createLayoutedEdges = (source) => {
581
593
  return config().zIndexMode;
582
594
  }
583
595
  }), nodeLookup = new RecordMapFacade(internalNodes), initialViewport = getInitialViewport(_props.fitView, _props.initialViewport, _props.width ?? 0, _props.height ?? 0, nodeLookup), [viewportStore, setViewportStore] = createStore(_props.viewport ?? initialViewport);
584
- createEffect(() => config().nodes, (next) => {
596
+ createEffect(() => {
597
+ let next = config().nodes;
598
+ for (let node of next);
599
+ return { next };
600
+ }, ({ next }) => {
585
601
  setNodesStore(() => next);
586
- }, { defer: !0 }), createEffect(() => config().edges, (next) => {
602
+ }, { defer: !0 }), createEffect(() => {
603
+ let next = config().edges;
604
+ for (let edge of next);
605
+ return { next };
606
+ }, ({ next }) => {
587
607
  setEdgesStore(() => next);
588
608
  }, { defer: !0 }), createEffect(() => config().viewport, (next) => {
589
609
  next && setViewportStore(() => next);
@@ -744,9 +764,6 @@ const createLayoutedEdges = (source) => {
744
764
  return zoomActivationKeyPressed();
745
765
  }
746
766
  }), cullingViewport = createCullingViewport({
747
- get onlyRenderVisibleElements() {
748
- return store.onlyRenderVisibleElements;
749
- },
750
767
  get width() {
751
768
  return store.width;
752
769
  },
@@ -1572,16 +1589,30 @@ const Handle = (props) => {
1572
1589
  };
1573
1590
  }), dragging;
1574
1591
  }, NodeIdContext = createContext(null);
1592
+ /**
1593
+ * Returns the id of the node this component is rendered inside. Available
1594
+ * anywhere in a custom node's subtree (provided by the node wrapper), so
1595
+ * nested components — like a custom `Handle` — can learn their host node
1596
+ * without prop drilling.
1597
+ *
1598
+ * @public
1599
+ * @returns a reactive accessor for the surrounding node's id
1600
+ */
1575
1601
  function useNodeId() {
1576
1602
  let ctx = useContext(NodeIdContext);
1577
- if (!ctx) throw Error("solid-flow: Your application must be wrapped with <SolidFlow> in order to invoke useNodeId");
1603
+ if (!ctx) throw Error("solid-flow: useNodeId must be called inside a node component (anywhere under a custom node's subtree)");
1578
1604
  return ctx;
1579
1605
  }
1580
1606
  //#endregion
1581
1607
  //#region src/components/node/NodeWrapper.tsx
1582
1608
  /** Internal per-node wrapper: dragging, selection, a11y, measurement, viewport culling, and the dynamic node component. */
1583
1609
  const NodeWrapper = (props) => {
1584
- let { store, nodeLookup, parentIds, actions } = useInternalSolidFlow(), [nodeRef, setNodeRef] = createSignal(), node = () => nodeLookup.get(props.nodeId), nodeId = () => node().id, nodeType = () => node().type ?? "default", deletable = () => node().deletable ?? !0, selectable = () => node().selectable ?? store.elementsSelectable, focusable = () => node().focusable ?? store.nodesFocusable, draggable = () => node().draggable ?? store.nodesDraggable, connectable = () => node().connectable ?? store.nodesConnectable, userNode = () => node().internals.userNode, nodeTypeValid = () => nodeType() in store.nodeTypes, nodeComponent = () => store.nodeTypes[nodeType()], isParentNode = () => !!parentIds[node().id], transform = () => {
1610
+ let { store, nodeLookup, parentIds, actions } = useInternalSolidFlow(), [nodeRef, setNodeRef] = createSignal(), node = () => nodeLookup.get(props.nodeId);
1611
+ onCleanup(() => {
1612
+ let el = nodeRef();
1613
+ el && props.resizeObserver?.unobserve(el);
1614
+ });
1615
+ let nodeId = () => node().id, nodeType = () => node().type ?? "default", deletable = () => node().deletable ?? !0, selectable = () => node().selectable ?? store.elementsSelectable, focusable = () => node().focusable ?? store.nodesFocusable, draggable = () => node().draggable ?? store.nodesDraggable, connectable = () => node().connectable ?? store.nodesConnectable, userNode = () => node().internals.userNode, nodeTypeValid = () => nodeType() in store.nodeTypes, nodeComponent = () => store.nodeTypes[nodeType()], isParentNode = () => !!parentIds[node().id], transform = () => {
1585
1616
  let { x, y } = node().internals.positionAbsolute;
1586
1617
  return `translate(${x}px, ${y}px)`;
1587
1618
  }, sizeStyle = () => {
@@ -1950,16 +1981,28 @@ const ConnectionLine = (props) => {
1950
1981
  </svg>
1951
1982
  </Show>;
1952
1983
  }, EdgeRenderer = (props) => {
1953
- let { store } = useInternalSolidFlow();
1954
- return <div class="solid-flow__edges">
1984
+ let { store, actions } = useInternalSolidFlow(), [focusedEdgeId, setFocusedEdgeId] = createSignal(null), onFocusIn = (event) => {
1985
+ let edgeElement = event.target.closest("[data-id]");
1986
+ setFocusedEdgeId(edgeElement?.getAttribute("data-id") ?? null);
1987
+ };
1988
+ return <div class="solid-flow__edges" onFocusIn={onFocusIn} onFocusOut={() => setFocusedEdgeId(null)}>
1955
1989
  <MarkerDefinition />
1956
1990
 
1957
1991
  <For each={store.visibleEdgeIds}>
1958
- {(edgeId) => <EdgeWrapper edgeId={edgeId} onEdgeClick={props.onEdgeClick} onEdgeContextMenu={props.onEdgeContextMenu} onEdgePointerEnter={props.onEdgePointerEnter} onEdgePointerLeave={props.onEdgePointerLeave} />}
1992
+ {(edgeId) => {
1993
+ let unmounted = createMemo(() => {
1994
+ if (!store.onlyRenderVisibleElements || focusedEdgeId() === edgeId) return !1;
1995
+ let edge = actions.getEdge(edgeId);
1996
+ return !!edge && isEdgeCulled(edge, store.cullingViewport);
1997
+ });
1998
+ return <Show when={!unmounted()}>
1999
+ <EdgeWrapper edgeId={edgeId} onEdgeClick={props.onEdgeClick} onEdgeContextMenu={props.onEdgeContextMenu} onEdgePointerEnter={props.onEdgePointerEnter} onEdgePointerLeave={props.onEdgePointerLeave} />
2000
+ </Show>;
2001
+ }}
1959
2002
  </For>
1960
2003
  </div>;
1961
2004
  }, NodeRenderer = (props) => {
1962
- let { actions, store } = useInternalSolidFlow(), resizeObserver = isServer ? void 0 : new ResizeObserver((entries) => {
2005
+ let { actions, store, nodeLookup } = useInternalSolidFlow(), resizeObserver = isServer ? void 0 : new ResizeObserver((entries) => {
1963
2006
  actions.requestUpdateNodeInternals(entries.map((entry) => {
1964
2007
  let id = entry.target.getAttribute("data-id");
1965
2008
  return [id, {
@@ -1969,11 +2012,25 @@ const ConnectionLine = (props) => {
1969
2012
  }];
1970
2013
  }));
1971
2014
  });
1972
- return onCleanup(() => {
2015
+ onCleanup(() => {
1973
2016
  resizeObserver?.disconnect();
1974
- }), <div class="solid-flow__container solid-flow__nodes">
2017
+ });
2018
+ let [focusedNodeId, setFocusedNodeId] = createSignal(null), onFocusIn = (event) => {
2019
+ let nodeElement = event.target.closest("[data-id]");
2020
+ setFocusedNodeId(nodeElement?.getAttribute("data-id") ?? null);
2021
+ };
2022
+ return <div class="solid-flow__container solid-flow__nodes" onFocusIn={onFocusIn} onFocusOut={() => setFocusedNodeId(null)}>
1975
2023
  <For each={store.visibleNodeIds}>
1976
- {(nodeId) => <NodeWrapper nodeId={nodeId} resizeObserver={resizeObserver} nodeClickDistance={props.nodeClickDistance} onNodeClick={props.onNodeClick} onNodePointerEnter={props.onNodePointerEnter} onNodePointerMove={props.onNodePointerMove} onNodePointerLeave={props.onNodePointerLeave} onNodeDrag={props.onNodeDrag} onNodeDragStart={props.onNodeDragStart} onNodeDragStop={props.onNodeDragStop} onNodeContextMenu={props.onNodeContextMenu} />}
2024
+ {(nodeId) => {
2025
+ let unmounted = createMemo(() => {
2026
+ if (!store.onlyRenderVisibleElements || focusedNodeId() === nodeId) return !1;
2027
+ let node = nodeLookup.get(nodeId);
2028
+ return !!node && isNodeCulled(node, store.cullingViewport);
2029
+ });
2030
+ return <Show when={!unmounted()}>
2031
+ <NodeWrapper nodeId={nodeId} resizeObserver={resizeObserver} nodeClickDistance={props.nodeClickDistance} onNodeClick={props.onNodeClick} onNodePointerEnter={props.onNodePointerEnter} onNodePointerMove={props.onNodePointerMove} onNodePointerLeave={props.onNodePointerLeave} onNodeDrag={props.onNodeDrag} onNodeDragStart={props.onNodeDragStart} onNodeDragStop={props.onNodeDragStop} onNodeContextMenu={props.onNodeContextMenu} />
2032
+ </Show>;
2033
+ }}
1977
2034
  </For>
1978
2035
  </div>;
1979
2036
  }, isSetEqual = (a, b) => {
@@ -2285,25 +2342,14 @@ const ConnectionLine = (props) => {
2285
2342
  * `const { flow, commands } = useSolidFlow()`.
2286
2343
  *
2287
2344
  * @public
2288
- * @returns the flow's read struct, write surface, and deprecated aliases
2345
+ * @returns the flow's read struct and write surface
2289
2346
  */
2290
2347
  function useSolidFlow() {
2291
- let { flow, commands, store, edgeLookup } = useInternalSolidFlow(), getInternalNode = (id) => flow.internalNodes[id];
2348
+ let { flow, commands } = useInternalSolidFlow();
2292
2349
  return {
2293
2350
  ...commands,
2294
2351
  flow,
2295
- commands,
2296
- getInternalNode,
2297
- getNode: (id) => getInternalNode(id)?.internals.userNode,
2298
- getNodes: (ids) => ids ? ids.flatMap((id) => {
2299
- let userNode = flow.internalNodes[id]?.internals.userNode;
2300
- return userNode ? [userNode] : [];
2301
- }) : [...flow.nodes],
2302
- getEdge: (id) => edgeLookup[id],
2303
- getEdges: (ids) => ids ? ids.flatMap((id) => edgeLookup[id] ? [edgeLookup[id]] : []) : [...flow.edges],
2304
- getViewport: () => snapshot(store.viewport),
2305
- getZoom: () => flow.viewport.zoom,
2306
- getHandleConnections: ({ type, id, nodeId }) => Object.values(flow.connections[connectionKey(nodeId, type, id ?? null)] ?? {})
2352
+ commands
2307
2353
  };
2308
2354
  }
2309
2355
  //#endregion
@@ -2339,6 +2385,34 @@ function matchesKey(event, keyDef) {
2339
2385
  function matchesKeyArray(event, keyDefs) {
2340
2386
  return keyDefs ? (Array.isArray(keyDefs) ? keyDefs : [keyDefs]).some((keyDef) => matchesKey(event, keyDef)) : !1;
2341
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
+ }
2342
2416
  const KeyHandler = (props) => {
2343
2417
  let { store, actions } = useInternalSolidFlow(), { deleteElements } = useSolidFlow(), _props = {
2344
2418
  get selectionKey() {
@@ -2358,6 +2432,20 @@ const KeyHandler = (props) => {
2358
2432
  }
2359
2433
  }, resetKeysAndSelection = () => {
2360
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();
2361
2449
  }, handleDelete = async () => {
2362
2450
  let selectedNodes = store.nodes.filter((node) => node.selected), selectedEdges = store.edges.filter((edge) => edge.selected), { deletedNodes, deletedEdges } = await deleteElements({
2363
2451
  nodes: selectedNodes,
@@ -2368,16 +2456,22 @@ const KeyHandler = (props) => {
2368
2456
  edges: deletedEdges
2369
2457
  });
2370
2458
  };
2371
- return isServer || createEventListenerMap(window, {
2459
+ return isServer || (createEventListenerMap(window, {
2372
2460
  keydown: (event) => {
2373
- 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();
2374
2462
  },
2375
2463
  keyup: (event) => {
2376
- 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();
2377
2465
  },
2378
- blur: resetKeysAndSelection,
2466
+ blur: handleWindowBlur,
2379
2467
  contextmenu: resetKeysAndSelection
2380
- }), null;
2468
+ }), createEventListenerMap(window, {
2469
+ pointerdown: reconcileModifiers,
2470
+ wheel: reconcileModifiers
2471
+ }, {
2472
+ capture: !0,
2473
+ passive: !0
2474
+ })), null;
2381
2475
  }, SolidFlow = (props) => {
2382
2476
  let [domNodeRef, setDomNodeRef] = createSignal(), domNode, _props = merge({
2383
2477
  ...getDefaultFlowStateProps(),
@@ -2534,19 +2628,6 @@ function useViewport() {
2534
2628
  return () => flow.viewport;
2535
2629
  }
2536
2630
  //#endregion
2537
- //#region src/hooks/useHandleEdgeSelect.ts
2538
- /** Returns a callback applying edge-selection semantics for the given edge id. */
2539
- function useHandleEdgeSelect() {
2540
- let { store, edgeLookup, actions } = useInternalSolidFlow();
2541
- return (id) => {
2542
- let edge = edgeLookup[id];
2543
- edge && (edge.selectable || store.elementsSelectable && edge.selectable === void 0) && (actions.setSelectionRect(void 0), actions.setSelectionRectMode(void 0), edge.selected ? edge.selected && store.multiselectionKeyPressed && actions.unselectNodesAndEdges({
2544
- nodes: [],
2545
- edges: [edge]
2546
- }) : actions.addSelectedEdges([id]));
2547
- };
2548
- }
2549
- //#endregion
2550
2631
  //#region src/hooks/useInitialized.ts
2551
2632
  /**
2552
2633
  * Hook for seeing if all nodes have been measured.
@@ -2576,11 +2657,16 @@ function useViewportInitialized() {
2576
2657
  //#endregion
2577
2658
  //#region src/hooks/useInternalNode.ts
2578
2659
  /**
2579
- * Hook to get an internal node by id.
2660
+ * Hook to get an internal node (the node enriched with measured dimensions,
2661
+ * absolute position, and z-order) by id.
2662
+ *
2663
+ * The id is an accessor (not a plain string) on purpose: passing a raw
2664
+ * reactive read like `props.nodeId` would capture the value once and silently
2665
+ * lose reactivity — `useInternalNode(() => props.nodeId)` keeps it live.
2580
2666
  *
2581
2667
  * @public
2582
- * @param id - the node id
2583
- * @returns an accessor with an internal node or undefined
2668
+ * @param id - a reactive accessor for the node id
2669
+ * @returns an accessor with the internal node, or undefined while absent
2584
2670
  */
2585
2671
  function useInternalNode(id) {
2586
2672
  let { flow } = useInternalSolidFlow();
@@ -2667,7 +2753,6 @@ const DotPattern = (props) => {
2667
2753
  let _props = propDefaults(props, {
2668
2754
  variant: "dots",
2669
2755
  gap: 20,
2670
- size: 1,
2671
2756
  lineWidth: 1,
2672
2757
  style: {}
2673
2758
  }), { store } = useInternalSolidFlow(), isDots = () => _props.variant === "dots", isCross = () => _props.variant === "cross", patternId = () => `background-pattern-${store.id}-${_props.id ?? ""}`, scaledSize = () => (_props.size ?? DEFAULT_SIZE[_props.variant]) * store.viewport.zoom, gapXY = () => Array.isArray(_props.gap) ? _props.gap : [_props.gap, _props.gap], scaledGap = () => [gapXY()[0] * store.viewport.zoom || 1, gapXY()[1] * store.viewport.zoom || 1], patternDimensions = () => isCross() ? [scaledSize(), scaledSize()] : scaledGap(), patternOffset = () => isDots() ? [scaledSize() / 2, scaledSize() / 2] : [patternDimensions()[0] / 2, patternDimensions()[1] / 2];
@@ -2763,8 +2848,8 @@ const DotPattern = (props) => {
2763
2848
  borderRadius: 5,
2764
2849
  width: 0,
2765
2850
  height: 0
2766
- }), style = () => Object.entries({
2767
- fill: _props.color,
2851
+ }), fill = () => _props.color ?? _props.style?.background ?? _props.style?.["background-color"], style = () => Object.entries({
2852
+ fill: fill(),
2768
2853
  stroke: _props.strokeColor,
2769
2854
  "stroke-width": _props.strokeWidth
2770
2855
  }).filter(([_, value]) => value !== void 0).reduce((acc, [key, value]) => (acc[key] = value, acc), {});
@@ -2772,7 +2857,7 @@ const DotPattern = (props) => {
2772
2857
  "solid-flow__minimap-node",
2773
2858
  { selected: !!_props.selected },
2774
2859
  _props.class
2775
- ]} x={_props.x} y={_props.y} rx={_props.borderRadius} ry={_props.borderRadius} width={_props.width} height={_props.height} shape-rendering={_props.shapeRendering} style={style()} />;
2860
+ ]} x={_props.x} y={_props.y} rx={_props.borderRadius} ry={_props.borderRadius} width={_props.width} height={_props.height} shape-rendering={_props.shapeRendering} style={style()} onClick={_props.onClick ? (event) => _props.onClick(event, _props.id) : void 0} />;
2776
2861
  }, getAttrFunction = (func) => func instanceof Function ? func : () => func, MiniMap = (props) => {
2777
2862
  let { store, nodeLookup } = useInternalSolidFlow(), _props = propDefaults(props, {
2778
2863
  position: "bottom-right",
@@ -2785,7 +2870,7 @@ const DotPattern = (props) => {
2785
2870
  nodeBorderRadius: 5,
2786
2871
  nodeStrokeWidth: 2,
2787
2872
  style: {}
2788
- }), paneProps = omit(_props, "class", "style", "position", "nodeClass", "nodeStrokeColor", "nodeColor", "pannable", "zoomable", "inversePan", "zoomStep", "bgColor", "width", "height", "maskColor", "maskStrokeColor", "maskStrokeWidth", "nodeBorderRadius", "nodeStrokeWidth"), nodeColorFunc = () => _props.nodeColor === void 0 ? void 0 : getAttrFunction(_props.nodeColor), nodeStrokeColorFunc = () => getAttrFunction(_props.nodeStrokeColor), nodeClassFunc = () => getAttrFunction(_props.nodeClass), shapeRendering = () => typeof window > "u" || window.chrome ? "crispEdges" : "geometricPrecision", labelledBy = createMemo(() => `solid-flow__minimap-desc-${store.id}`), getViewBB = () => ({
2873
+ }), paneProps = omit(_props, "class", "style", "position", "nodeClass", "nodeStrokeColor", "nodeColor", "pannable", "zoomable", "inversePan", "zoomStep", "bgColor", "width", "height", "maskColor", "maskStrokeColor", "maskStrokeWidth", "nodeBorderRadius", "nodeStrokeWidth", "nodeComponent", "onClick", "onNodeClick"), nodeColorFunc = () => _props.nodeColor === void 0 ? void 0 : getAttrFunction(_props.nodeColor), nodeStrokeColorFunc = () => getAttrFunction(_props.nodeStrokeColor), nodeClassFunc = () => getAttrFunction(_props.nodeClass), shapeRendering = () => typeof window > "u" || window.chrome ? "crispEdges" : "geometricPrecision", labelledBy = createMemo(() => `solid-flow__minimap-desc-${store.id}`), getViewBB = () => ({
2789
2874
  x: -store.viewport.x / store.viewport.zoom,
2790
2875
  y: -store.viewport.y / store.viewport.zoom,
2791
2876
  width: store.width / store.viewport.zoom,
@@ -2810,7 +2895,7 @@ const DotPattern = (props) => {
2810
2895
  <Show when={store.panZoom}>
2811
2896
  {(panZoom) => {
2812
2897
  let [ref, setRef] = createSignal(), [minimap, setMinimap] = createSignal();
2813
- return createEffect(() => ({
2898
+ createEffect(() => ({
2814
2899
  el: ref(),
2815
2900
  panZoom: panZoom()
2816
2901
  }), ({ el, panZoom }) => {
@@ -2837,7 +2922,19 @@ const DotPattern = (props) => {
2837
2922
  }
2838
2923
  }), ({ instance, options }) => {
2839
2924
  instance?.update(options);
2840
- }), <svg ref={setRef} width={_props.width} height={_props.height} viewBox={`${getX()} ${getY()} ${getViewboxWidth()} ${getViewboxHeight()}`} class="solid-flow__minimap-svg" role="img" aria-labelledby={labelledBy()} style={{
2925
+ });
2926
+ let onSvgClick = (event) => {
2927
+ if (!_props.onClick) return;
2928
+ let [x, y] = minimap()?.pointer(event) ?? [0, 0];
2929
+ _props.onClick(event, {
2930
+ x,
2931
+ y
2932
+ });
2933
+ }, onSvgNodeClick = (event, nodeId) => {
2934
+ let node = nodeLookup.get(nodeId)?.internals.userNode;
2935
+ node && _props.onNodeClick?.(event, node);
2936
+ };
2937
+ return <svg ref={setRef} width={_props.width} height={_props.height} viewBox={`${getX()} ${getY()} ${getViewboxWidth()} ${getViewboxHeight()}`} class="solid-flow__minimap-svg" role="img" aria-labelledby={labelledBy()} onClick={_props.onClick ? onSvgClick : void 0} style={{
2841
2938
  "--xy-minimap-mask-background-color-props": _props.maskColor,
2842
2939
  "--xy-minimap-mask-stroke-color-props": _props.maskStrokeColor,
2843
2940
  "--xy-minimap-mask-stroke-width-props": strokeWidth()
@@ -2847,7 +2944,7 @@ const DotPattern = (props) => {
2847
2944
  {(nodeId) => {
2848
2945
  let node = createMemo(() => nodeLookup.get(nodeId())), nodeVisible = () => !!(node() && nodeHasDimensions(node()) && !node().hidden);
2849
2946
  return <Show when={nodeVisible() && getNodeDimensions(node())}>
2850
- {(nodeDimensions) => <MiniMapNode x={node().internals.positionAbsolute.x} y={node().internals.positionAbsolute.y} borderRadius={_props.nodeBorderRadius} strokeWidth={_props.nodeStrokeWidth} shapeRendering={shapeRendering()} width={nodeDimensions().width} height={nodeDimensions().height} selected={node().selected} color={nodeColorFunc()?.call(null, node())} strokeColor={nodeStrokeColorFunc().call(null, node())} class={nodeClassFunc().call(null, node())} />}
2947
+ {(nodeDimensions) => <Dynamic component={_props.nodeComponent ?? MiniMapNode} id={nodeId()} x={node().internals.positionAbsolute.x} y={node().internals.positionAbsolute.y} borderRadius={_props.nodeBorderRadius} strokeWidth={_props.nodeStrokeWidth} shapeRendering={shapeRendering()} width={nodeDimensions().width} height={nodeDimensions().height} selected={node().selected} color={nodeColorFunc()?.call(null, node())} strokeColor={nodeStrokeColorFunc().call(null, node())} class={nodeClassFunc().call(null, node())} style={node().style} onClick={_props.onNodeClick ? onSvgNodeClick : void 0} />}
2851
2948
  </Show>;
2852
2949
  }}
2853
2950
  </For>
@@ -2964,7 +3061,7 @@ const DotPattern = (props) => {
2964
3061
  </EdgeLabel>
2965
3062
  </Show>;
2966
3063
  }, NodeToolbar = (props) => {
2967
- let { store } = useInternalSolidFlow(), { getNodes, getNodesBounds, getInternalNode } = useSolidFlow(), _props = propDefaults(props, {
3064
+ let { store } = useInternalSolidFlow(), { flow, getNodesBounds } = useSolidFlow(), _props = propDefaults(props, {
2968
3065
  offset: 10,
2969
3066
  position: "top",
2970
3067
  align: "center",
@@ -2973,7 +3070,7 @@ const DotPattern = (props) => {
2973
3070
  let id = useContext(NodeIdContext);
2974
3071
  return id ? id() : "";
2975
3072
  }, toolbarNodes = () => (Array.isArray(_props.nodeId) ? _props.nodeId : [_props.nodeId ?? ctxNodeId()]).reduce((res, nodeId) => {
2976
- let node = getInternalNode(nodeId);
3073
+ let node = flow.internalNodes[nodeId];
2977
3074
  return node && res.push(node), res;
2978
3075
  }, []), transform = () => {
2979
3076
  let nodeRect = getNodesBounds(toolbarNodes());
@@ -2981,7 +3078,7 @@ const DotPattern = (props) => {
2981
3078
  }, zIndex = () => {
2982
3079
  let nodes = toolbarNodes();
2983
3080
  return nodes.length === 0 ? 1 : Math.max(...nodes.map((node) => (node.internals.z || 5) + 1));
2984
- }, selectedNodesCount = () => getNodes().filter((node) => node.selected).length, isActive = () => {
3081
+ }, selectedNodesCount = () => flow.selection.nodes.length, isActive = () => {
2985
3082
  let nodes = toolbarNodes();
2986
3083
  return typeof _props.isVisible == "boolean" ? _props.isVisible : nodes.length === 1 && !!nodes[0].selected && selectedNodesCount() === 1;
2987
3084
  }, showPortal = () => !!(store.domNode && isActive() && toolbarNodes().length > 0);
@@ -2999,4 +3096,4 @@ const DotPattern = (props) => {
2999
3096
  </Show>;
3000
3097
  }, SelectionMode = SelectionMode$1, getEdgeCenter = getEdgeCenter$1, getBezierEdgeCenter = getBezierEdgeCenter$1;
3001
3098
  //#endregion
3002
- export { Background, BaseEdge, BezierEdge, BezierEdgeInternal, ConnectionLine, ConnectionLineType, ConnectionMode, ControlButton, Controls, DefaultNode, EdgeLabel, EdgeLabelRenderer, EdgeReconnectAnchor, EdgeRenderer, EdgeToolbar, EdgeWrapper, GroupNode, Handle, InputNode, Marker, MarkerDefinition, MarkerType, MiniMap, NodeRenderer, NodeResizer, NodeSelection, NodeToolbar, NodeWrapper, OutputNode, PanOnScrollMode, Pane, Panel, Position, ResizeControl, ResizeControlVariant, Selection, SelectionMode, SmoothStepEdge, SmoothStepEdgeInternal, SolidFlow, SolidFlowProvider, StepEdge, StepEdgeInternal, StraightEdge, StraightEdgeInternal, Viewport, ViewportPortal, Zoom, addEdge, connectionKey, createEdgeStore, createNodeStore, getBezierEdgeCenter, getBezierPath, getConnectedEdges, getEdgeCenter, getIncomers, getNodesBounds, getOutgoers, getSmoothStepPath, getStraightPath, getViewportForBounds, useColorMode, useConnection, useEdges, useHandleEdgeSelect, useInternalNode, useNodeConnections, useNodes, useNodesData, useNodesInitialized, useSolidFlow, useUpdateNodeInternals, useViewport, useViewportInitialized };
3099
+ export { Background, BaseEdge, BezierEdge, BezierEdgeInternal, ConnectionLine, ConnectionLineType, ConnectionMode, ControlButton, Controls, DefaultNode, EdgeLabel, EdgeLabelRenderer, EdgeReconnectAnchor, EdgeRenderer, EdgeToolbar, EdgeWrapper, GroupNode, Handle, InputNode, Marker, MarkerDefinition, MarkerType, MiniMap, MiniMapNode, NodeRenderer, NodeResizer, NodeSelection, NodeToolbar, NodeWrapper, OutputNode, PanOnScrollMode, Pane, Panel, Position, ResizeControl, ResizeControlVariant, Selection, SelectionMode, SmoothStepEdge, SmoothStepEdgeInternal, SolidFlow, SolidFlowProvider, StepEdge, StepEdgeInternal, StraightEdge, StraightEdgeInternal, Viewport, ViewportPortal, 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 };
@@ -93,7 +93,7 @@
93
93
  --xy-connectionline-stroke-width-default: 1;
94
94
  }
95
95
 
96
- connectionline {
96
+ .solid-flow__connectionline {
97
97
  z-index: 1001;
98
98
  overflow: visible;
99
99
  position: absolute;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dschz/solid-flow",
3
- "version": "0.3.0-next.3",
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",