WIP: migrated store and useSvelteFlow hook to signals & solved state propagation for provider

This commit is contained in:
peterkogo
2024-12-06 13:53:48 +01:00
parent ed1ead7b5a
commit f29898f0bd
49 changed files with 1616 additions and 1137 deletions
+1
View File
@@ -1 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" /> /// <reference types="astro/client" />
+10 -10
View File
@@ -13,24 +13,24 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.3.1", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.8.0", "@sveltejs/kit": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^8.14.0", "@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.14.0", "@typescript-eslint/parser": "^8.17.0",
"eslint": "^8.53.0", "eslint": "^8.53.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.0", "eslint-plugin-svelte": "^2.46.1",
"prettier": "^3.3.3", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.2.8", "prettier-plugin-svelte": "^3.3.2",
"svelte": "^5.1.16", "svelte": "^5.7.1",
"svelte-check": "^4.0.7", "svelte-check": "^4.1.1",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"vite": "^5.4.11" "vite": "^6.0.3"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.1",
"@xyflow/svelte": "workspace:^" "@xyflow/svelte": "workspace:^"
} }
} }
@@ -129,23 +129,7 @@
target: '6', target: '6',
label: 'hi', label: 'hi',
labelStyle: 'background: red; font-weight: 700; padding: 5px;', labelStyle: 'background: red; font-weight: 700; padding: 5px;',
style: 'stroke: #ffcc0', style: 'stroke: #ffcc0'
markerEnd: {
type: MarkerType.Arrow,
color: '#FFCC00',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20,
strokeWidth: 2
},
markerStart: {
type: MarkerType.ArrowClosed,
color: '#FFCC00',
orient: 'auto-start-reverse',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20
}
} }
]); ]);
@@ -1,10 +1,15 @@
<script lang="ts"> <script lang="ts">
import { getBezierPath, BaseEdge, type EdgeProps, EdgeLabelRenderer } from '@xyflow/svelte'; import {
getBezierPath,
BaseEdge,
type EdgeProps,
EdgeLabelRenderer,
useSvelteFlow,
MarkerType
} from '@xyflow/svelte';
type $$Props = EdgeProps; type $$Props = EdgeProps;
interface Props { interface Props {
id?: $$Props['id']; id?: $$Props['id'];
style?: $$Props['style']; style?: $$Props['style'];
@@ -15,7 +20,7 @@
targetX: $$Props['targetX']; targetX: $$Props['targetX'];
targetY: $$Props['targetY']; targetY: $$Props['targetY'];
targetPosition: $$Props['targetPosition']; targetPosition: $$Props['targetPosition'];
[key: string]: any [key: string]: any;
} }
let { let {
@@ -33,14 +38,18 @@
rest; rest;
let [edgePath, labelX, labelY] = $derived(getBezierPath({ let [edgePath, labelX, labelY] = $derived(
sourceX, getBezierPath({
sourceY, sourceX,
sourcePosition, sourceY,
targetX, sourcePosition,
targetY, targetX,
targetPosition targetY,
})); targetPosition
})
);
const { updateEdge } = useSvelteFlow();
</script> </script>
<BaseEdge path={edgePath} {markerEnd} {style} /> <BaseEdge path={edgePath} {markerEnd} {style} />
@@ -53,7 +62,24 @@
class="edgeButton" class="edgeButton"
onclick={(event) => { onclick={(event) => {
event.stopPropagation(); event.stopPropagation();
alert(`remove ${id}`); updateEdge('e5-6', {
markerEnd: {
type: MarkerType.Arrow,
color: '#FFCC00',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20,
strokeWidth: 2
},
markerStart: {
type: MarkerType.ArrowClosed,
color: '#FFCC00',
orient: 'auto-start-reverse',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20
}
});
}} }}
> >
× ×
@@ -10,9 +10,9 @@
const nodes = writable<Node[]>(initialNodes); const nodes = writable<Node[]>(initialNodes);
const edges = writable<Edge[]>(initialEdges); const edges = writable<Edge[]>(initialEdges);
const { getIntersectingNodes } = useSvelteFlow(); const { getIntersectingNodes } = $derived(useSvelteFlow());
function onNodeDrag({ detail: { targetNode } }) { function onNodeDrag({ targetNode }) {
if (targetNode) { if (targetNode) {
const intersections = getIntersectingNodes(targetNode).map((n) => n.id); const intersections = getIntersectingNodes(targetNode).map((n) => n.id);
@@ -25,7 +25,7 @@
</script> </script>
<div style="height:100vh;"> <div style="height:100vh;">
<SvelteFlow {nodes} {edges} fitView class="intersection-flow" on:nodedrag={onNodeDrag}> <SvelteFlow {nodes} {edges} fitView class="intersection-flow" onnodedrag={onNodeDrag}>
<Background /> <Background />
<Controls /> <Controls />
</SvelteFlow> </SvelteFlow>
+9 -9
View File
@@ -56,26 +56,26 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.3.1", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.8.0", "@sveltejs/kit": "^2.9.0",
"@sveltejs/package": "^2.3.7", "@sveltejs/package": "^2.3.7",
"@typescript-eslint/eslint-plugin": "^8.14.0", "@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.14.0", "@typescript-eslint/parser": "^8.17.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
"dotenv": "^16.4.5", "dotenv": "^16.4.7",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.0", "eslint-plugin-svelte": "^2.46.1",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"postcss-cli": "^11.0.0", "postcss-cli": "^11.0.0",
"postcss-combine-duplicated-selectors": "^10.0.3", "postcss-combine-duplicated-selectors": "^10.0.3",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-nested": "^7.0.2", "postcss-nested": "^7.0.2",
"postcss-rename": "^0.6.1", "postcss-rename": "^0.6.1",
"prettier": "^3.3.3", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.2.8", "prettier-plugin-svelte": "^3.3.2",
"svelte": "^5.1.16", "svelte": "^5.7.1",
"svelte-check": "^4.0.7", "svelte-check": "^4.1.1",
"svelte-eslint-parser": "^0.43.0", "svelte-eslint-parser": "^0.43.0",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1", "tslib": "^2.8.1",
+10 -10
View File
@@ -25,24 +25,24 @@ export default function drag(domNode: Element, params: UseDragParams) {
onDragStop, onDragStop,
onNodeMouseDown, onNodeMouseDown,
getStoreItems: () => { getStoreItems: () => {
const snapGrid = get(store.snapGrid); const snapGrid = store.snapGrid;
const vp = get(store.viewport); const vp = get(store.viewport);
return { return {
nodes: get(store.nodes), nodes: get(store.nodes),
nodeLookup: get(store.nodeLookup), nodeLookup: store.nodeLookup,
edges: get(store.edges), edges: get(store.edges),
nodeExtent: get(store.nodeExtent), nodeExtent: store.nodeExtent,
snapGrid: snapGrid ? snapGrid : [0, 0], snapGrid: snapGrid ? snapGrid : [0, 0],
snapToGrid: !!snapGrid, snapToGrid: !!snapGrid,
nodeOrigin: get(store.nodeOrigin), nodeOrigin: store.nodeOrigin,
multiSelectionActive: get(store.multiselectionKeyPressed), multiSelectionActive: store.multiselectionKeyPressed,
domNode: get(store.domNode), domNode: store.domNode,
transform: [vp.x, vp.y, vp.zoom], transform: [vp.x, vp.y, vp.zoom],
autoPanOnNodeDrag: get(store.autoPanOnNodeDrag), autoPanOnNodeDrag: store.autoPanOnNodeDrag,
nodesDraggable: get(store.nodesDraggable), nodesDraggable: store.nodesDraggable,
selectNodesOnDrag: get(store.selectNodesOnDrag), selectNodesOnDrag: store.selectNodesOnDrag,
nodeDragThreshold: get(store.nodeDragThreshold), nodeDragThreshold: store.nodeDragThreshold,
unselectNodesAndEdges: store.unselectNodesAndEdges, unselectNodesAndEdges: store.unselectNodesAndEdges,
updateNodePositions: store.updateNodePositions, updateNodePositions: store.updateNodePositions,
panBy: store.panBy panBy: store.panBy
@@ -14,13 +14,12 @@ type ZoomParams = {
initialViewport: Viewport; initialViewport: Viewport;
minZoom: number; minZoom: number;
maxZoom: number; maxZoom: number;
dragging: Writable<boolean>; setPanZoomInstance: (panZoomInstance: PanZoomInstance) => void;
onPanZoomStart?: OnPanZoom; onPanZoomStart?: OnPanZoom;
onPanZoom?: OnPanZoom; onPanZoom?: OnPanZoom;
onPanZoomEnd?: OnPanZoom; onPanZoomEnd?: OnPanZoom;
onPaneContextMenu?: (event: MouseEvent) => void; onPaneContextMenu?: (event: MouseEvent) => void;
translateExtent: CoordinateExtent; translateExtent: CoordinateExtent;
panZoom: Writable<PanZoomInstance | null>;
zoomOnScroll: boolean; zoomOnScroll: boolean;
zoomOnPinch: boolean; zoomOnPinch: boolean;
zoomOnDoubleClick: boolean; zoomOnDoubleClick: boolean;
@@ -38,18 +37,19 @@ type ZoomParams = {
lib: string; lib: string;
paneClickDistance: number; paneClickDistance: number;
onTransformChange: (transform: Transform) => void; onTransformChange: (transform: Transform) => void;
onDraggingChange: (dragging: boolean) => void;
}; };
export default function zoom(domNode: Element, params: ZoomParams) { export default function zoom(domNode: Element, params: ZoomParams) {
const { const {
panZoom,
minZoom, minZoom,
maxZoom, maxZoom,
initialViewport, initialViewport,
viewport, viewport,
dragging,
translateExtent, translateExtent,
paneClickDistance paneClickDistance,
setPanZoomInstance,
onDraggingChange
} = params; } = params;
const panZoomInstance = XYPanZoom({ const panZoomInstance = XYPanZoom({
@@ -59,11 +59,11 @@ export default function zoom(domNode: Element, params: ZoomParams) {
translateExtent, translateExtent,
viewport: initialViewport, viewport: initialViewport,
paneClickDistance, paneClickDistance,
onDraggingChange: dragging.set onDraggingChange
}); });
const currentViewport = panZoomInstance.getViewport(); const currentViewport = panZoomInstance.getViewport();
viewport.set(currentViewport); viewport.set(currentViewport);
panZoom.set(panZoomInstance); setPanZoomInstance(panZoomInstance);
panZoomInstance.update(params); panZoomInstance.update(params);
@@ -9,35 +9,37 @@
getStraightPath getStraightPath
} from '@xyflow/system'; } from '@xyflow/system';
import { useStore } from '$lib/store'; import type { SvelteFlowStore } from '$lib/store/types';
let { let {
store,
containerStyle = '', containerStyle = '',
style = '', style = '',
connectionLine connectionLine
}: { }: {
store: SvelteFlowStore;
containerStyle: string; containerStyle: string;
style: string; style: string;
connectionLine?: Snippet; connectionLine?: Snippet;
} = $props(); } = $props();
const { width, height, connection, connectionLineType } = useStore(); // $inspect(store.connection);
let path = $derived.by(() => { let path = $derived.by(() => {
if (!$connection.inProgress) { if (!store.connection.inProgress) {
return ''; return '';
} }
const pathParams = { const pathParams = {
sourceX: $connection.from.x, sourceX: store.connection.from.x,
sourceY: $connection.from.y, sourceY: store.connection.from.y,
sourcePosition: $connection.fromPosition, sourcePosition: store.connection.fromPosition,
targetX: $connection.to.x, targetX: store.connection.to.x,
targetY: $connection.to.y, targetY: store.connection.to.y,
targetPosition: $connection.toPosition targetPosition: store.connection.toPosition
}; };
switch ($connectionLineType) { switch (store.connectionLineType) {
case ConnectionLineType.Bezier: { case ConnectionLineType.Bezier: {
const [path] = getBezierPath(pathParams); const [path] = getBezierPath(pathParams);
return path; return path;
@@ -50,7 +52,7 @@
case ConnectionLineType.SmoothStep: { case ConnectionLineType.SmoothStep: {
const [path] = getSmoothStepPath({ const [path] = getSmoothStepPath({
...pathParams, ...pathParams,
borderRadius: $connectionLineType === ConnectionLineType.Step ? 0 : undefined borderRadius: store.connectionLineType === ConnectionLineType.Step ? 0 : undefined
}); });
return path; return path;
} }
@@ -58,9 +60,14 @@
}); });
</script> </script>
{#if $connection.inProgress} {#if store.connection.inProgress}
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}> <svg
<g class={cc(['svelte-flow__connection', getConnectionStatus($connection.isValid)])}> width={store.width}
height={store.height}
class="svelte-flow__connectionline"
style={containerStyle}
>
<g class={cc(['svelte-flow__connection', getConnectionStatus(store.connection.isValid)])}>
{#if connectionLine} {#if connectionLine}
{@render connectionLine()} {@render connectionLine()}
{:else} {:else}
@@ -3,11 +3,11 @@
import portal from '$lib/actions/portal'; import portal from '$lib/actions/portal';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
const { domNode } = useStore(); const store = useStore();
let { children }: { children?: Snippet } = $props(); let { children }: { children?: Snippet } = $props();
</script> </script>
<div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: $domNode }}> <div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: store.domNode }}>
{@render children?.()} {@render children?.()}
</div> </div>
@@ -9,10 +9,12 @@
import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect'; import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect';
import type { EdgeLayouted, Edge, EdgeEvents } from '$lib/types'; import type { EdgeLayouted, Edge, EdgeEvents } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types';
const { const {
id, id,
type = 'default', type = 'default',
store,
source, source,
target, target,
data = {}, data = {},
@@ -42,24 +44,24 @@
onedgecontextmenu, onedgecontextmenu,
onedgemouseenter, onedgemouseenter,
onedgemouseleave onedgemouseleave
}: EdgeLayouted & EdgeEvents = $props(); }: { store: SvelteFlowStore } & EdgeLayouted & EdgeEvents = $props();
setContext('svelteflow__edge_id', id); setContext('svelteflow__edge_id', id);
const { edgeLookup, edgeTypes, flowId, elementsSelectable } = useStore();
let edgeType = $derived(type ?? 'default'); let edgeType = $derived(type ?? 'default');
let EdgeComponent = $derived($edgeTypes[edgeType] ?? BezierEdgeInternal); let EdgeComponent = $derived(store.edgeTypes[edgeType] ?? BezierEdgeInternal);
let markerStartUrl = $derived( let markerStartUrl = $derived(
markerStart ? `url('#${getMarkerId(markerStart, $flowId)}')` : undefined markerStart ? `url('#${getMarkerId(markerStart, store.flowId)}')` : undefined
); );
let markerEndUrl = $derived(markerEnd ? `url('#${getMarkerId(markerEnd, $flowId)}')` : undefined); let markerEndUrl = $derived(
let isSelectable = $derived(selectable ?? $elementsSelectable); markerEnd ? `url('#${getMarkerId(markerEnd, store.flowId)}')` : undefined
);
let isSelectable = $derived(selectable ?? store.elementsSelectable);
const handleEdgeSelect = useHandleEdgeSelect(); const handleEdgeSelect = useHandleEdgeSelect();
function onclick(event: MouseEvent | TouchEvent) { function onclick(event: MouseEvent | TouchEvent) {
const edge = $edgeLookup.get(id); const edge = store.edgeLookup.get(id);
if (edge) { if (edge) {
handleEdgeSelect(id); handleEdgeSelect(id);
@@ -71,7 +73,7 @@
event: MouseEvent, event: MouseEvent,
callback: ({ edge, event }: { edge: Edge; event: MouseEvent }) => void callback: ({ edge, event }: { edge: Edge; event: MouseEvent }) => void
) { ) {
const edge = $edgeLookup.get(id); const edge = store.edgeLookup.get(id);
if (edge) { if (edge) {
callback({ event, edge }); callback({ event, edge });
@@ -39,28 +39,8 @@
isConnectableProp !== undefined ? isConnectableProp : isConnectableContext.value isConnectableProp !== undefined ? isConnectableProp : isConnectableContext.value
); );
const { let store = useStore();
connectionMode, let { viewport, edges } = store;
domNode,
nodeLookup,
connectionRadius,
viewport,
isValidConnection: isValidConnectionStore,
lib,
addEdge,
onedgecreate,
panBy,
cancelConnection,
updateConnection,
autoPanOnConnect,
edges,
connectionLookup,
onconnect: onConnectAction,
onconnectstart: onConnectStartAction,
onconnectend: onConnectEndAction,
flowId,
connection
} = useStore();
function onPointerDown(event: MouseEvent | TouchEvent) { function onPointerDown(event: MouseEvent | TouchEvent) {
const isMouseTriggered = isMouseEvent(event); const isMouseTriggered = isMouseEvent(event);
@@ -70,39 +50,39 @@
handleId, handleId,
nodeId, nodeId,
isTarget, isTarget,
connectionRadius: $connectionRadius, connectionRadius: store.connectionRadius,
domNode: $domNode, domNode: store.domNode,
nodeLookup: $nodeLookup, nodeLookup: store.nodeLookup,
connectionMode: $connectionMode, connectionMode: store.connectionMode,
lib: $lib, lib: 'svelte',
autoPanOnConnect: $autoPanOnConnect, autoPanOnConnect: store.autoPanOnConnect,
flowId: $flowId, flowId: store.flowId,
isValidConnection: isValidConnectionProp ?? $isValidConnectionStore, isValidConnection: isValidConnectionProp ?? store.isValidConnection,
updateConnection, updateConnection: store.updateConnection,
cancelConnection, cancelConnection: store.cancelConnection,
panBy, panBy: store.panBy,
onConnect: (connection) => { onConnect: (connection) => {
const edge = $onedgecreate ? $onedgecreate(connection) : connection; const edge = store.onedgecreate ? store.onedgecreate(connection) : connection;
if (!edge) { if (!edge) {
return; return;
} }
addEdge(edge); store.addEdge(edge);
$onConnectAction?.(connection); store.onconnect?.(connection);
}, },
onConnectStart: (event, startParams) => { onConnectStart: (event, startParams) => {
$onConnectStartAction?.(event, { store.onconnectstart?.(event, {
nodeId: startParams.nodeId, nodeId: startParams.nodeId,
handleId: startParams.handleId, handleId: startParams.handleId,
handleType: startParams.handleType handleType: startParams.handleType
}); });
}, },
onConnectEnd: (event, connectionState) => { onConnectEnd: (event, connectionState) => {
$onConnectEndAction?.(event, connectionState); store.onconnectend?.(event, connectionState);
}, },
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom], getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
getFromHandle: () => $connection.fromHandle getFromHandle: () => store.connection.fromHandle
}); });
} }
} }
@@ -113,7 +93,7 @@
// eslint-disable-next-line @typescript-eslint/no-unused-expressions // eslint-disable-next-line @typescript-eslint/no-unused-expressions
$edges; $edges;
if (onconnect || ondisconnect) { if (onconnect || ondisconnect) {
let connections = $connectionLookup.get(`${nodeId}-${type}-${handleId}`); let connections = store.connectionLookup.get(`${nodeId}-${type}-${handleId}`);
if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) { if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
const _connections = connections ?? new Map(); const _connections = connections ?? new Map();
@@ -128,7 +108,7 @@
let [connectionInProcess, connectingFrom, connectingTo, isPossibleEndHandle, valid] = $derived.by( let [connectionInProcess, connectingFrom, connectingTo, isPossibleEndHandle, valid] = $derived.by(
() => { () => {
const { fromHandle, toHandle, isValid } = $connection; const { fromHandle, toHandle, isValid } = store.connection;
const connectionInProcess = !!fromHandle; const connectionInProcess = !!fromHandle;
@@ -139,7 +119,7 @@
toHandle?.nodeId === nodeId && toHandle?.type === type && toHandle?.id === handleId; toHandle?.nodeId === nodeId && toHandle?.type === type && toHandle?.id === handleId;
const isPossibleEndHandle = const isPossibleEndHandle =
$connectionMode === ConnectionMode.Strict store.connectionMode === ConnectionMode.Strict
? fromHandle?.type !== type ? fromHandle?.type !== type
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id; : nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id;
@@ -158,7 +138,7 @@ The Handle component is the part of a node that can be used to connect nodes.
data-handleid={handleId} data-handleid={handleId}
data-nodeid={nodeId} data-nodeid={nodeId}
data-handlepos={position} data-handlepos={position}
data-id="{$flowId}-{nodeId}-{handleId}-{type}" data-id="{store.flowId}-{nodeId}-{handleId}-{type}"
class={cc([ class={cc([
'svelte-flow__handle', 'svelte-flow__handle',
`svelte-flow__handle-${position}`, `svelte-flow__handle-${position}`,
@@ -12,6 +12,7 @@
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types'; import type { KeyDefinition, KeyDefinitionObject } from '$lib/types';
let { let {
store,
selectionKey = 'Shift', selectionKey = 'Shift',
multiSelectionKey = isMacOs() ? 'Meta' : 'Control', multiSelectionKey = isMacOs() ? 'Meta' : 'Control',
deleteKey = 'Backspace', deleteKey = 'Backspace',
@@ -19,18 +20,7 @@
zoomActivationKey = isMacOs() ? 'Meta' : 'Control' zoomActivationKey = isMacOs() ? 'Meta' : 'Control'
}: KeyHandlerProps = $props(); }: KeyHandlerProps = $props();
const { const { nodes: _nodes, edges: _edges } = store;
selectionKeyPressed,
multiselectionKeyPressed,
deleteKeyPressed,
panActivationKeyPressed,
zoomActivationKeyPressed,
selectionRect,
onbeforedelete,
ondelete,
nodes: _nodes,
edges: _edges
} = useStore();
function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject { function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject {
return key !== null && typeof key === 'object'; return key !== null && typeof key === 'object';
@@ -67,12 +57,12 @@
} }
function resetKeysAndSelection() { function resetKeysAndSelection() {
selectionRect.set(null); store.selectionRect = null;
selectionKeyPressed.set(false); store.selectionKeyPressed = false;
multiselectionKeyPressed.set(false); store.multiselectionKeyPressed = false;
deleteKeyPressed.set(false); store.deleteKeyPressed = false;
panActivationKeyPressed.set(false); store.panActivationKeyPressed = false;
zoomActivationKeyPressed.set(false); store.zoomActivationKeyPressed = false;
} }
async function handleDelete() { async function handleDelete() {
@@ -86,14 +76,14 @@
edgesToRemove: selectedEdges, edgesToRemove: selectedEdges,
nodes, nodes,
edges, edges,
onBeforeDelete: get(onbeforedelete) onBeforeDelete: store.onbeforedelete
}); });
if (matchingNodes.length || matchingEdges.length) { if (matchingNodes.length || matchingEdges.length) {
_nodes.update((nds) => nds.filter((node) => !matchingNodes.some((mN) => mN.id === node.id))); _nodes.update((nds) => nds.filter((node) => !matchingNodes.some((mN) => mN.id === node.id)));
_edges.update((eds) => eds.filter((edge) => !matchingEdges.some((mE) => mE.id === edge.id))); _edges.update((eds) => eds.filter((edge) => !matchingEdges.some((mE) => mE.id === edge.id)));
get(ondelete)?.({ store.ondelete?.({
nodes: matchingNodes, nodes: matchingNodes,
edges: matchingEdges edges: matchingEdges
}); });
@@ -105,19 +95,19 @@
on:blur={resetKeysAndSelection} on:blur={resetKeysAndSelection}
on:contextmenu={resetKeysAndSelection} on:contextmenu={resetKeysAndSelection}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(true)), trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = true)),
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(false)), trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = false)),
type: 'keyup' type: 'keyup'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(true)), trigger: getShortcutTrigger(multiSelectionKey, () => (store.multiselectionKeyPressed = true)),
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(false)), trigger: getShortcutTrigger(multiSelectionKey, () => (store.multiselectionKeyPressed = false)),
type: 'keyup' type: 'keyup'
}} }}
use:shortcut={{ use:shortcut={{
@@ -127,30 +117,30 @@
detail.originalEvent.metaKey || detail.originalEvent.metaKey ||
detail.originalEvent.shiftKey; detail.originalEvent.shiftKey;
if (!isModifierKey && !isInputDOMNode(detail.originalEvent)) { if (!isModifierKey && !isInputDOMNode(detail.originalEvent)) {
deleteKeyPressed.set(true); store.deleteKeyPressed = true;
handleDelete(); handleDelete();
} }
}), }),
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(deleteKey, () => deleteKeyPressed.set(false)), trigger: getShortcutTrigger(deleteKey, () => (store.deleteKeyPressed = false)),
type: 'keyup' type: 'keyup'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(true)), trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = true)),
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(false)), trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = false)),
type: 'keyup' type: 'keyup'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(true)), trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = true)),
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(false)), trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = false)),
type: 'keyup' type: 'keyup'
}} }}
/> />
@@ -1,6 +1,8 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { KeyDefinition } from '$lib/types'; import type { KeyDefinition } from '$lib/types';
export type KeyHandlerProps = { export type KeyHandlerProps = {
store: SvelteFlowStore;
selectionKey?: KeyDefinition | KeyDefinition[] | null; selectionKey?: KeyDefinition | KeyDefinition[] | null;
multiSelectionKey?: KeyDefinition | KeyDefinition[] | null; multiSelectionKey?: KeyDefinition | KeyDefinition[] | null;
deleteKey?: KeyDefinition | KeyDefinition[] | null; deleteKey?: KeyDefinition | KeyDefinition[] | null;
@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system'; import { getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system';
import { useStore } from '$lib/store';
import { Selection } from '$lib/components/Selection'; import { Selection } from '$lib/components/Selection';
import drag from '$lib/actions/drag'; import drag from '$lib/actions/drag';
import type { NodeSelectionProps } from './types'; import type { NodeSelectionProps } from './types';
let { let {
store,
onnodedrag, onnodedrag,
onnodedragstart, onnodedragstart,
onnodedragstop, onnodedragstop,
@@ -15,14 +15,13 @@
onselectioncontextmenu onselectioncontextmenu
}: NodeSelectionProps = $props(); }: NodeSelectionProps = $props();
const store = useStore(); const { nodes } = store;
const { selectionRectMode, nodes, nodeLookup } = store;
let bounds: Rect | null = $derived.by(() => { let bounds: Rect | null = $derived.by(() => {
if ($selectionRectMode === 'nodes') { if (store.selectionRectMode === 'nodes') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions // eslint-disable-next-line @typescript-eslint/no-unused-expressions
$nodes; $nodes;
return getInternalNodesBounds($nodeLookup, { filter: (node) => !!node.selected }); return getInternalNodesBounds(store.nodeLookup, { filter: (node) => !!node.selected });
} }
return null; return null;
}); });
@@ -38,7 +37,7 @@
} }
</script> </script>
{#if $selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)} {#if store.selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
<div <div
class="selection-wrapper nopan" class="selection-wrapper nopan"
style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)" style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)"
@@ -1,4 +1,5 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { NodeEvents, NodeSelectionEvents } from '$lib/types'; import type { NodeEvents, NodeSelectionEvents } from '$lib/types';
export type NodeSelectionProps = NodeSelectionEvents & export type NodeSelectionProps = { store: SvelteFlowStore } & NodeSelectionEvents &
Pick<NodeEvents, 'onnodedrag' | 'onnodedragstart' | 'onnodedragstop'>; Pick<NodeEvents, 'onnodedrag' | 'onnodedragstart' | 'onnodedragstop'>;
@@ -1,17 +1,16 @@
<script lang="ts"> <script lang="ts">
import { setContext, onDestroy } from 'svelte'; import { setContext, onDestroy } from 'svelte';
import { get } from 'svelte/store';
import cc from 'classcat'; import cc from 'classcat';
import { errorMessages, Position } from '@xyflow/system'; import { errorMessages, Position } from '@xyflow/system';
import drag from '$lib/actions/drag'; import drag from '$lib/actions/drag';
import { useStore } from '$lib/store';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import type { ConnectableContext, NodeWrapperProps } from './types'; import type { ConnectableContext, NodeWrapperProps } from './types';
import { getNodeInlineStyleDimensions } from './utils'; import { getNodeInlineStyleDimensions } from './utils';
import type { NodeEvents } from '$lib/types'; import type { NodeEvents } from '$lib/types';
let { let {
store,
node, node,
id, id,
data = {}, data = {},
@@ -52,15 +51,6 @@
onnodecontextmenu onnodecontextmenu
}: NodeWrapperProps & NodeEvents = $props(); }: NodeWrapperProps & NodeEvents = $props();
const store = useStore();
const {
nodeTypes,
nodeDragThreshold,
selectNodesOnDrag,
handleNodeSelection,
updateNodeInternals
} = store;
let nodeRef: HTMLDivElement | null = $state(null); let nodeRef: HTMLDivElement | null = $state(null);
let prevNodeRef: HTMLDivElement | null = null; let prevNodeRef: HTMLDivElement | null = null;
@@ -68,7 +58,7 @@
let prevSourcePosition: Position | undefined; let prevSourcePosition: Position | undefined;
let prevTargetPosition: Position | undefined; let prevTargetPosition: Position | undefined;
let NodeComponent = $derived($nodeTypes[type] ?? DefaultNode); let NodeComponent = $derived(store.nodeTypes[type] ?? DefaultNode);
let connectableContext: ConnectableContext = { let connectableContext: ConnectableContext = {
get value() { get value() {
@@ -80,7 +70,7 @@
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
$effect(() => { $effect(() => {
const valid = !!$nodeTypes[type]; const valid = !!store.nodeTypes[type];
if (!valid) { if (!valid) {
console.warn('003', errorMessages['error003'](type!)); console.warn('003', errorMessages['error003'](type!));
} }
@@ -109,7 +99,7 @@
if (doUpdate && nodeRef !== null) { if (doUpdate && nodeRef !== null) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (nodeRef !== null) { if (nodeRef !== null) {
updateNodeInternals( store.updateNodeInternals(
new Map([ new Map([
[ [
id, id,
@@ -148,10 +138,10 @@
}); });
function onSelectNodeHandler(event: MouseEvent | TouchEvent) { function onSelectNodeHandler(event: MouseEvent | TouchEvent) {
if (selectable && (!get(selectNodesOnDrag) || !draggable || get(nodeDragThreshold) > 0)) { if (selectable && (!store.selectNodesOnDrag || !draggable || store.nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true // this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
// here we only need to call it when selectNodesOnDrag=false // here we only need to call it when selectNodesOnDrag=false
handleNodeSelection(id); store.handleNodeSelection(id);
} }
onnodeclick?.({ node: node.internals.userNode, event }); onnodeclick?.({ node: node.internals.userNode, event });
@@ -169,7 +159,7 @@
handleSelector: dragHandle, handleSelector: dragHandle,
noDragClass: 'nodrag', noDragClass: 'nodrag',
nodeClickDistance, nodeClickDistance,
onNodeMouseDown: handleNodeSelection, onNodeMouseDown: store.handleNodeSelection,
onDrag: (event, _, targetNode, nodes) => { onDrag: (event, _, targetNode, nodes) => {
onnodedrag?.({ event, targetNode, nodes }); onnodedrag?.({ event, targetNode, nodes });
}, },
@@ -1,3 +1,4 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { InternalNode, Node } from '$lib/types'; import type { InternalNode, Node } from '$lib/types';
export type ConnectableContext = { export type ConnectableContext = {
@@ -39,4 +40,5 @@ export type NodeWrapperProps = Pick<
node: InternalNode; node: InternalNode;
initialized: boolean; initialized: boolean;
nodeClickDistance?: number; nodeClickDistance?: number;
store: SvelteFlowStore;
}; };
@@ -3,29 +3,21 @@
import { createStore, key } from '$lib/store'; import { createStore, key } from '$lib/store';
import type { SvelteFlowProviderProps } from './types'; import type { SvelteFlowProviderProps } from './types';
import type { ProviderContext, SvelteFlowStore } from '$lib/store/types';
let { let { children }: SvelteFlowProviderProps = $props();
initialNodes,
initialEdges,
initialWidth,
initialHeight,
nodeOrigin,
fitView,
children
}: SvelteFlowProviderProps = $props();
const store = createStore({ let store = $state.raw(createStore({ props: {} }));
nodes: initialNodes,
edges: initialEdges,
width: initialWidth,
height: initialHeight,
nodeOrigin,
fitView
});
setContext(key, { setContext(key, {
getStore: () => store provider: true,
}); getStore() {
return store;
},
setStore: (newStore: SvelteFlowStore) => {
store = newStore;
}
} satisfies ProviderContext);
onDestroy(() => { onDestroy(() => {
store.reset(); store.reset();
@@ -2,13 +2,13 @@
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import { Selection } from '$lib/components/Selection'; import { Selection } from '$lib/components/Selection';
const { selectionRect, selectionRectMode } = useStore(); const store = useStore();
</script> </script>
<Selection <Selection
isVisible={!!($selectionRect && $selectionRectMode === 'user')} isVisible={!!(store.selectionRect && store.selectionRectMode === 'user')}
width={$selectionRect?.width} width={store.selectionRect?.width}
height={$selectionRect?.height} height={store.selectionRect?.height}
x={$selectionRect?.x} x={store.selectionRect?.x}
y={$selectionRect?.y} y={store.selectionRect?.y}
/> />
@@ -6,9 +6,9 @@
let { children }: { children?: Snippet } = $props(); let { children }: { children?: Snippet } = $props();
const { domNode } = useStore(); const store = useStore();
</script> </script>
<div use:portal={{ target: '.svelte-flow__viewport-portal', domNode: $domNode }}> <div use:portal={{ target: '.svelte-flow__viewport-portal', domNode: store.domNode }}>
{@render children?.()} {@render children?.()}
</div> </div>
@@ -1,29 +1,59 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { EdgeWrapper } from '$lib/components/EdgeWrapper'; import { EdgeWrapper } from '$lib/components/EdgeWrapper';
import { CallOnMount } from '$lib/components/CallOnMount'; import { CallOnMount } from '$lib/components/CallOnMount';
import { MarkerDefinition } from '$lib/container/EdgeRenderer/MarkerDefinition'; import { MarkerDefinition } from '$lib/container/EdgeRenderer/MarkerDefinition';
import { useStore } from '$lib/store'; import type { EdgeEvents, EdgeLayouted } from '$lib/types';
import type { DefaultEdgeOptions, EdgeEvents } from '$lib/types'; import { getEdgePosition, getElevatedEdgeZIndex } from '@xyflow/system';
import type { SvelteFlowStore } from '$lib/store/types';
let { let {
defaultEdgeOptions, store,
onedgeclick, onedgeclick,
onedgecontextmenu, onedgecontextmenu,
onedgemouseenter, onedgemouseenter,
onedgemouseleave onedgemouseleave
}: { defaultEdgeOptions?: DefaultEdgeOptions } & EdgeEvents = $props(); }: { store: SvelteFlowStore } & EdgeEvents = $props();
const { const { edges, nodes } = store;
visibleEdges,
edgesInitialized,
edges: { setDefaultOptions },
elementsSelectable
} = useStore();
onMount(() => { let layoutedEdges = $derived.by(() => {
// TODO: Remove this when we have a better way to set default options $nodes;
if (defaultEdgeOptions) setDefaultOptions(defaultEdgeOptions); const layoutedEdges = $edges.reduce<EdgeLayouted[]>((res, edge) => {
const sourceNode = store.nodeLookup.get(edge.source);
const targetNode = store.nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) {
return res;
}
const edgePosition = getEdgePosition({
id: edge.id,
sourceNode,
targetNode,
sourceHandle: edge.sourceHandle || null,
targetHandle: edge.targetHandle || null,
connectionMode: store.connectionMode,
onError: store.onerror
});
if (edgePosition) {
res.push({
...edge,
zIndex: getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: false
}),
...edgePosition
});
}
return res;
}, []);
return layoutedEdges;
}); });
</script> </script>
@@ -32,8 +62,9 @@
<MarkerDefinition /> <MarkerDefinition />
</svg> </svg>
{#each $visibleEdges as edge (edge.id)} {#each layoutedEdges as edge (edge.id)}
<EdgeWrapper <EdgeWrapper
{store}
id={edge.id} id={edge.id}
source={edge.source} source={edge.source}
target={edge.target} target={edge.target}
@@ -41,7 +72,7 @@
style={edge.style} style={edge.style}
animated={edge.animated} animated={edge.animated}
selected={edge.selected} selected={edge.selected}
selectable={edge.selectable ?? $elementsSelectable} selectable={edge.selectable ?? store.elementsSelectable}
deletable={edge.deletable} deletable={edge.deletable}
hidden={edge.hidden} hidden={edge.hidden}
label={edge.label} label={edge.label}
@@ -68,13 +99,13 @@
/> />
{/each} {/each}
{#if $visibleEdges.length > 0} {#if layoutedEdges.length > 0}
<CallOnMount <CallOnMount
onMount={() => { onMount={() => {
$edgesInitialized = true; store.edgesInitialized = true;
}} }}
onDestroy={() => { onDestroy={() => {
$edgesInitialized = false; store.edgesInitialized = false;
}} }}
/> />
{/if} {/if}
@@ -2,11 +2,11 @@
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import { Marker } from '$lib/container/EdgeRenderer/MarkerDefinition'; import { Marker } from '$lib/container/EdgeRenderer/MarkerDefinition';
const { markers } = useStore(); const store = useStore();
</script> </script>
<defs> <defs>
{#each $markers as marker (marker.id)} {#each store.markers as marker (marker.id)}
<Marker {...marker} /> <Marker {...marker} />
{/each} {/each}
</defs> </defs>
@@ -4,9 +4,10 @@
import type { NodeEvents } from '$lib/types'; import type { NodeEvents } from '$lib/types';
import { NodeWrapper } from '$lib/components/NodeWrapper'; import { NodeWrapper } from '$lib/components/NodeWrapper';
import { useStore } from '$lib/store'; import type { SvelteFlowStore } from '$lib/store/types';
let { let {
store,
nodeClickDistance, nodeClickDistance,
onnodeclick, onnodeclick,
onnodecontextmenu, onnodecontextmenu,
@@ -16,16 +17,9 @@
onnodedrag, onnodedrag,
onnodedragstart, onnodedragstart,
onnodedragstop onnodedragstop
}: { nodeClickDistance?: number } & NodeEvents = $props(); }: { store: SvelteFlowStore; nodeClickDistance?: number } & NodeEvents = $props();
const { const { nodes } = store;
visibleNodes,
nodesDraggable,
nodesConnectable,
elementsSelectable,
updateNodeInternals,
parentLookup
} = useStore();
const resizeObserver: ResizeObserver | null = const resizeObserver: ResizeObserver | null =
typeof ResizeObserver === 'undefined' typeof ResizeObserver === 'undefined'
@@ -43,7 +37,7 @@
}); });
}); });
updateNodeInternals(updates); store.updateNodeInternals(updates);
}); });
onDestroy(() => { onDestroy(() => {
@@ -52,41 +46,46 @@
</script> </script>
<div class="svelte-flow__nodes"> <div class="svelte-flow__nodes">
{#each $visibleNodes as node (node.id)} {#each $nodes as node (node.id)}
{@const internalNode = store.nodeLookup.get(node.id)}
<NodeWrapper <NodeWrapper
{node} {store}
node={internalNode!}
id={node.id} id={node.id}
data={node.data} data={node.data}
selected={!!node.selected} selected={!!node.selected}
hidden={!!node.hidden} hidden={!!node.hidden}
draggable={!!(node.draggable || ($nodesDraggable && typeof node.draggable === 'undefined'))} draggable={!!(
node.draggable ||
(store.nodesDraggable && typeof node.draggable === 'undefined')
)}
selectable={!!( selectable={!!(
node.selectable || node.selectable ||
($elementsSelectable && typeof node.selectable === 'undefined') (store.elementsSelectable && typeof node.selectable === 'undefined')
)} )}
connectable={!!( connectable={!!(
node.connectable || node.connectable ||
($nodesConnectable && typeof node.connectable === 'undefined') (store.nodesConnectable && typeof node.connectable === 'undefined')
)} )}
deletable={node.deletable ?? true} deletable={node.deletable ?? true}
positionX={node.internals.positionAbsolute.x} positionX={internalNode!.internals.positionAbsolute.x}
positionY={node.internals.positionAbsolute.y} positionY={internalNode!.internals.positionAbsolute.y}
isParent={$parentLookup.has(node.id)} isParent={store.parentLookup.has(node.id)}
style={node.style} style={node.style}
class={node.class} class={node.class}
type={node.type ?? 'default'} type={node.type ?? 'default'}
sourcePosition={node.sourcePosition} sourcePosition={node.sourcePosition}
targetPosition={node.targetPosition} targetPosition={node.targetPosition}
dragging={node.dragging} dragging={node.dragging}
zIndex={node.internals.z ?? 0} zIndex={internalNode!.internals.z ?? 0}
dragHandle={node.dragHandle} dragHandle={node.dragHandle}
initialized={nodeHasDimensions(node)} initialized={nodeHasDimensions(node)}
width={node.width} width={node.width}
height={node.height} height={node.height}
initialWidth={node.initialWidth} initialWidth={node.initialWidth}
initialHeight={node.initialHeight} initialHeight={node.initialHeight}
measuredWidth={node.measured.width} measuredWidth={node.measured?.width ?? 0}
measuredHeight={node.measured.height} measuredHeight={node.measured?.height ?? 0}
parentId={node.parentId} parentId={node.parentId}
{resizeObserver} {resizeObserver}
{nodeClickDistance} {nodeClickDistance}
@@ -37,39 +37,29 @@
import type { Node, Edge, InternalNode } from '$lib/types'; import type { Node, Edge, InternalNode } from '$lib/types';
import type { PaneProps } from './types'; import type { PaneProps } from './types';
let { let {
store,
panOnDrag = true, panOnDrag = true,
selectionOnDrag, selectionOnDrag,
onpaneclick, onpaneclick,
onpanecontextmenu, onpanecontextmenu,
nodes,
edges,
children children
}: PaneProps = $props(); }: PaneProps = $props();
const { const { viewport, nodes, edges } = store;
nodeLookup,
viewport,
dragging,
elementsSelectable,
selectionRect,
selectionRectMode,
selectionKeyPressed,
selectionMode,
panActivationKeyPressed,
unselectNodesAndEdges
} = useStore();
// svelte-ignore non_reactive_update // svelte-ignore non_reactive_update
let container: HTMLDivElement; let container: HTMLDivElement;
let containerBounds: DOMRect | null = null; let containerBounds: DOMRect | null = null;
let selectedNodes: InternalNode[] = []; let selectedNodes: InternalNode[] = [];
let panOnDragActive = $derived($panActivationKeyPressed || panOnDrag); let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
let isSelecting = $derived( let isSelecting = $derived(
$selectionKeyPressed || $selectionRect || (selectionOnDrag && panOnDragActive !== true) store.selectionKeyPressed ||
store.selectionRect ||
(selectionOnDrag && panOnDragActive !== true)
); );
let hasActiveSelection = $derived( let hasActiveSelection = $derived(
$elementsSelectable && (isSelecting || $selectionRectMode === 'user') store.elementsSelectable && (isSelecting || store.selectionRectMode === 'user')
); );
// Used to prevent click events when the user lets go of the selectionKey during a selection // Used to prevent click events when the user lets go of the selectionKey during a selection
@@ -83,15 +73,15 @@
} }
onpaneclick?.({ event }); onpaneclick?.({ event });
unselectNodesAndEdges(); store.unselectNodesAndEdges();
selectionRectMode.set(null); store.selectionRectMode = null;
} }
function onPointerDown(event: PointerEvent) { function onPointerDown(event: PointerEvent) {
containerBounds = container.getBoundingClientRect(); containerBounds = container.getBoundingClientRect();
if ( if (
!elementsSelectable || !store.elementsSelectable ||
!isSelecting || !isSelecting ||
event.button !== 0 || event.button !== 0 ||
event.target !== container || event.target !== container ||
@@ -104,32 +94,32 @@
const { x, y } = getEventPosition(event, containerBounds); const { x, y } = getEventPosition(event, containerBounds);
unselectNodesAndEdges(); store.unselectNodesAndEdges();
selectionRect.set({ store.selectionRect = {
width: 0, width: 0,
height: 0, height: 0,
startX: x, startX: x,
startY: y, startY: y,
x, x,
y y
}); };
// onSelectionStart?.(event); // onSelectionStart?.(event);
} }
function onPointerMove(event: PointerEvent) { function onPointerMove(event: PointerEvent) {
if (!isSelecting || !containerBounds || !$selectionRect) { if (!isSelecting || !containerBounds || !store.selectionRect) {
return; return;
} }
selectionInProgress = true; selectionInProgress = true;
const mousePos = getEventPosition(event, containerBounds); const mousePos = getEventPosition(event, containerBounds);
const startX = $selectionRect.startX ?? 0; const startX = store.selectionRect.startX ?? 0;
const startY = $selectionRect.startY ?? 0; const startY = store.selectionRect.startY ?? 0;
const nextUserSelectRect = { const nextUserSelectRect = {
...$selectionRect, ...store.selectionRect,
x: mousePos.x < startX ? mousePos.x : startX, x: mousePos.x < startX ? mousePos.x : startX,
y: mousePos.y < startY ? mousePos.y : startY, y: mousePos.y < startY ? mousePos.y : startY,
width: Math.abs(mousePos.x - startX), width: Math.abs(mousePos.x - startX),
@@ -139,10 +129,10 @@
const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id); const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
selectedNodes = getNodesInside( selectedNodes = getNodesInside(
$nodeLookup, store.nodeLookup,
nextUserSelectRect, nextUserSelectRect,
[$viewport.x, $viewport.y, $viewport.zoom], [$viewport.x, $viewport.y, $viewport.zoom],
$selectionMode === SelectionMode.Partial, store.selectionMode === SelectionMode.Partial,
true true
); );
const selectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id); const selectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
@@ -163,8 +153,8 @@
edges.update((edges) => edges.map(toggleSelected(selectedEdgeIds))); edges.update((edges) => edges.map(toggleSelected(selectedEdgeIds)));
} }
selectionRectMode.set('user'); store.selectionRectMode = 'user';
selectionRect.set(nextUserSelectRect); store.selectionRect = nextUserSelectRect;
} }
function onPointerUp(event: PointerEvent) { function onPointerUp(event: PointerEvent) {
@@ -176,18 +166,18 @@
// We only want to trigger click functions when in selection mode if // We only want to trigger click functions when in selection mode if
// the user did not move the mouse. // the user did not move the mouse.
if (!isSelecting && $selectionRectMode === 'user' && event.target === container) { if (!isSelecting && store.selectionRectMode === 'user' && event.target === container) {
onClick?.(event); onClick?.(event);
} }
selectionRect.set(null); store.selectionRect = null;
if (selectedNodes.length > 0) { if (selectedNodes.length > 0) {
$selectionRectMode = 'nodes'; store.selectionRectMode = 'nodes';
} }
// If the user kept holding the selectionKey during the selection, // If the user kept holding the selectionKey during the selection,
// we need to reset the selectionInProgress, so the next click event is not prevented // we need to reset the selectionInProgress, so the next click event is not prevented
if ($selectionKeyPressed) { if (store.selectionKeyPressed) {
selectionInProgress = false; selectionInProgress = false;
} }
@@ -210,7 +200,7 @@
bind:this={container} bind:this={container}
class="svelte-flow__pane" class="svelte-flow__pane"
class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))} class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))}
class:dragging={$dragging} class:dragging={store.dragging}
class:selection={isSelecting} class:selection={isSelecting}
onclick={hasActiveSelection ? undefined : wrapHandler(onClick, container)} onclick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
onpointerdown={hasActiveSelection ? onPointerDown : undefined} onpointerdown={hasActiveSelection ? onPointerDown : undefined}
@@ -1,11 +1,10 @@
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import type { Writable } from 'svelte/store'; import type { PaneEvents } from '$lib/types';
import type { Edge, Node, PaneEvents } from '$lib/types'; import type { SvelteFlowStore } from '$lib/store/types';
export type PaneProps = { export type PaneProps = {
store: SvelteFlowStore;
panOnDrag?: boolean | number[]; panOnDrag?: boolean | number[];
selectionOnDrag?: boolean; selectionOnDrag?: boolean;
nodes: Writable<Node[]>;
edges: Writable<Edge[]>;
children: Snippet; children: Snippet;
} & PaneEvents; } & PaneEvents;
@@ -11,7 +11,7 @@
...restProps ...restProps
}: PanelProps = $props(); }: PanelProps = $props();
const { selectionRectMode } = useStore(); const store = useStore();
let positionClasses = $derived(`${position}`.split('-')); let positionClasses = $derived(`${position}`.split('-'));
</script> </script>
@@ -19,7 +19,7 @@
<div <div
class={cc(['svelte-flow__panel', className, ...positionClasses])} class={cc(['svelte-flow__panel', className, ...positionClasses])}
{style} {style}
style:pointer-events={$selectionRectMode ? 'none' : ''} style:pointer-events={store.selectionRectMode ? 'none' : ''}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount, hasContext } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { onMount, getContext, setContext } from 'svelte';
import cc from 'classcat'; import cc from 'classcat';
import { ConnectionMode, PanOnScrollMode } from '@xyflow/system'; import { PanOnScrollMode } from '@xyflow/system';
import { Zoom } from '$lib/container/Zoom'; import { Zoom } from '$lib/container/Zoom';
import { Pane } from '$lib/container/Pane'; import { Pane } from '$lib/container/Pane';
@@ -14,78 +14,28 @@
import { KeyHandler } from '$lib/components/KeyHandler'; import { KeyHandler } from '$lib/components/KeyHandler';
import { ConnectionLine } from '$lib/components/ConnectionLine'; import { ConnectionLine } from '$lib/components/ConnectionLine';
import { Attribution } from '$lib/components/Attribution'; import { Attribution } from '$lib/components/Attribution';
import { key, useStore, createStoreContext } from '$lib/store'; import { key, createStore } from '$lib/store';
import type { SvelteFlowProps } from './types'; import type { SvelteFlowProps } from './types';
import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
import { useColorModeClass } from '$lib/hooks/useColorModeClass'; import { useColorModeClass } from '$lib/hooks/useColorModeClass';
import { type ProviderContext, type ContainerSignals, type StoreContext } from '$lib/store/types';
let { let {
id = '1', style,
nodes, class: className,
edges, width,
fitView, height,
fitViewOptions, proOptions,
minZoom,
maxZoom,
initialViewport,
viewport,
nodeTypes,
edgeTypes,
selectionKey, selectionKey,
selectionMode, deleteKey,
panActivationKey, panActivationKey,
multiSelectionKey, multiSelectionKey,
zoomActivationKey, zoomActivationKey,
nodesDraggable,
nodesConnectable,
nodeDragThreshold,
elementsSelectable,
snapGrid,
deleteKey,
connectionRadius,
connectionLineType,
connectionMode = ConnectionMode.Strict,
connectionLineStyle = '',
connectionLineContainerStyle = '',
onMoveStart,
onMove,
onMoveEnd,
isValidConnection,
translateExtent,
nodeExtent,
onlyRenderVisibleElements,
panOnScrollMode = PanOnScrollMode.Free,
preventScrolling = true,
zoomOnScroll = true,
zoomOnDoubleClick = true,
zoomOnPinch = true,
panOnScroll = false,
panOnDrag = true,
selectionOnDrag,
autoPanOnConnect = true,
autoPanOnNodeDrag = true,
onerror,
ondelete,
onedgecreate,
attributionPosition,
proOptions,
defaultEdgeOptions,
width,
height,
colorMode = 'light',
onconnect,
onconnectstart,
onconnectend,
onbeforedelete,
oninit,
nodeOrigin,
paneClickDistance = 1, paneClickDistance = 1,
nodeClickDistance = 1, nodeClickDistance = 1,
defaultMarkerColor = '#b1b1b7', onMoveStart,
style, onMoveEnd,
class: className, onMove,
connectionLine, connectionLine,
children,
onnodeclick, onnodeclick,
onnodecontextmenu, onnodecontextmenu,
onnodedrag, onnodedrag,
@@ -102,135 +52,74 @@
onedgemouseleave, onedgemouseleave,
onpaneclick, onpaneclick,
onpanecontextmenu, onpanecontextmenu,
...rest panOnScrollMode = PanOnScrollMode.Free,
preventScrolling = true,
zoomOnScroll = true,
zoomOnDoubleClick = true,
zoomOnPinch = true,
panOnScroll = false,
panOnDrag = true,
selectionOnDrag = true,
defaultEdgeOptions,
connectionLineContainerStyle = '',
connectionLineStyle = '',
attributionPosition,
children,
...props
}: SvelteFlowProps = $props(); }: SvelteFlowProps = $props();
let domNode = $state<HTMLDivElement>(); // svelte-ignore perf_avoid_inline_class
let clientWidth = $state<number>(); const container: ContainerSignals = new (class {
let clientHeight = $state<number>(); domNode = $state<HTMLDivElement>();
width = $state<number | undefined>(width);
height = $state<number | undefined>(height);
})();
const initViewport = $viewport || initialViewport; const store = createStore({
props,
container
});
const { viewport } = store;
const initialViewport = props.initialViewport ?? get(viewport);
//TODO SVELTE5 store.syncNodeStores(props.nodes);
store.syncEdgeStores(props.edges);
store.syncViewport(props.viewport);
const store = hasContext(key) const providerContext = getContext<ProviderContext>(key);
? useStore() if (providerContext) {
: createStoreContext({ providerContext.setStore(store);
nodes: get(nodes), }
edges: get(edges), setContext(key, {
width, provider: false,
height, getStore() {
fitView, return store;
nodeOrigin, }
nodeExtent } satisfies StoreContext);
});
onMount(() => { onMount(() => {
store.width.set(clientWidth!);
store.height.set(clientHeight!);
store.domNode.set(domNode!);
store.syncNodeStores(nodes);
store.syncEdgeStores(edges);
store.syncViewport(viewport);
if (fitView !== undefined) {
store.fitViewOnInit.set(fitView);
}
if (fitViewOptions) {
store.fitViewOptions.set(fitViewOptions);
}
updateStore(store, {
nodeTypes,
edgeTypes,
minZoom,
maxZoom,
translateExtent,
paneClickDistance
});
return () => { return () => {
store.reset(); store.reset();
}; };
}); });
// Update width & height on resize let colorModeClass = $derived(useColorModeClass(store.colorMode));
$effect.pre(() => {
if (clientWidth !== undefined && clientHeight !== undefined) {
store.width.set(clientWidth);
store.height.set(clientHeight);
}
});
// Call oninit once when flow is intialized
const { initialized } = store;
let onInitCalled = $state(false);
$effect.pre(() => {
if (!onInitCalled && $initialized) {
oninit?.();
onInitCalled = true;
}
});
// this updates the store for simple changes
// where the prop names equals the store name
$effect(() => {
const updatableProps: UpdatableStoreProps = {
flowId: id,
connectionLineType,
connectionRadius,
selectionMode,
snapGrid,
defaultMarkerColor,
nodesDraggable,
nodesConnectable,
elementsSelectable,
onlyRenderVisibleElements,
isValidConnection,
autoPanOnConnect,
autoPanOnNodeDrag,
onerror,
ondelete,
onedgecreate,
connectionMode,
nodeDragThreshold,
onconnect,
onconnectstart,
onconnectend,
onbeforedelete,
nodeOrigin
};
updateStoreByKeys(store, updatableProps);
});
$effect(() => {
updateStore(store, {
nodeTypes,
edgeTypes,
minZoom,
maxZoom,
translateExtent,
paneClickDistance
});
});
let colorModeClass = $derived(useColorModeClass(colorMode));
</script> </script>
<div <div
bind:this={domNode} bind:this={container.domNode}
bind:clientWidth bind:clientWidth={container.width}
bind:clientHeight bind:clientHeight={container.height}
style:width
style:height
{style} {style}
class={cc(['svelte-flow', className, $colorModeClass])} class={cc(['svelte-flow', className, $colorModeClass])}
data-testid="svelte-flow__wrapper" data-testid="svelte-flow__wrapper"
{...rest} {...props}
role="application" role="application"
> >
<KeyHandler <KeyHandler
{store}
{selectionKey} {selectionKey}
{deleteKey} {deleteKey}
{panActivationKey} {panActivationKey}
@@ -238,7 +127,8 @@
{zoomActivationKey} {zoomActivationKey}
/> />
<Zoom <Zoom
initialViewport={initViewport} {store}
{initialViewport}
{onMoveStart} {onMoveStart}
{onMove} {onMove}
{onMoveEnd} {onMoveEnd}
@@ -251,16 +141,17 @@
{panOnDrag} {panOnDrag}
{paneClickDistance} {paneClickDistance}
> >
<Pane {nodes} {edges} {onpaneclick} {onpanecontextmenu} {panOnDrag} {selectionOnDrag}> <Pane {store} {onpaneclick} {onpanecontextmenu} {panOnDrag} {selectionOnDrag}>
<ViewportComponent> <ViewportComponent viewport={$viewport}>
<EdgeRenderer <EdgeRenderer
{store}
{onedgeclick} {onedgeclick}
{onedgecontextmenu} {onedgecontextmenu}
{onedgemouseenter} {onedgemouseenter}
{onedgemouseleave} {onedgemouseleave}
{defaultEdgeOptions}
/> />
<ConnectionLine <ConnectionLine
{store}
containerStyle={connectionLineContainerStyle} containerStyle={connectionLineContainerStyle}
style={connectionLineStyle} style={connectionLineStyle}
{connectionLine} {connectionLine}
@@ -268,6 +159,7 @@
<div class="svelte-flow__edgelabel-renderer"></div> <div class="svelte-flow__edgelabel-renderer"></div>
<div class="svelte-flow__viewport-portal"></div> <div class="svelte-flow__viewport-portal"></div>
<NodeRenderer <NodeRenderer
{store}
{nodeClickDistance} {nodeClickDistance}
{onnodeclick} {onnodeclick}
{onnodecontextmenu} {onnodecontextmenu}
@@ -279,6 +171,7 @@
{onnodedragstop} {onnodedragstop}
/> />
<NodeSelection <NodeSelection
{store}
{onselectionclick} {onselectionclick}
{onselectioncontextmenu} {onselectioncontextmenu}
{onnodedrag} {onnodedrag}
@@ -188,6 +188,10 @@ export type SvelteFlowProps = NodeEvents &
* @default 'full' * @default 'full'
*/ */
selectionMode?: SelectionMode; selectionMode?: SelectionMode;
/**
* Controls if nodes should be automatically selected when being dragged
*/
selectNodesOnDrag: boolean;
/** Grid all nodes will snap to /** Grid all nodes will snap to
* @example [20, 20] * @example [20, 20]
*/ */
@@ -1,91 +0,0 @@
import type { Writable } from 'svelte/store';
import type { CoordinateExtent } from '@xyflow/system';
import type { SvelteFlowStore } from '$lib/store/types';
import type { EdgeTypes, NodeTypes } from '$lib/types';
// this is helper function for updating the store
// for props where we need to call a specific store action
export function updateStore(
store: SvelteFlowStore,
{
nodeTypes,
edgeTypes,
minZoom,
maxZoom,
translateExtent,
paneClickDistance
}: {
nodeTypes?: NodeTypes;
edgeTypes?: EdgeTypes;
minZoom?: number;
maxZoom?: number;
translateExtent?: CoordinateExtent;
paneClickDistance?: number;
}
) {
if (nodeTypes !== undefined) {
store.setNodeTypes(nodeTypes);
}
if (edgeTypes !== undefined) {
store.setEdgeTypes(edgeTypes);
}
if (minZoom !== undefined) {
store.setMinZoom(minZoom);
}
if (maxZoom !== undefined) {
store.setMaxZoom(maxZoom);
}
if (translateExtent !== undefined) {
store.setTranslateExtent(translateExtent);
}
if (paneClickDistance !== undefined) {
store.setPaneClickDistance(paneClickDistance);
}
}
const getKeys = <T extends object>(obj: T) => Object.keys(obj) as Array<keyof T>;
type UnwrapWritable<T> = T extends Writable<infer U> ? U : T;
// @todo there must be a better way to define the types here..
export type UpdatableStoreProps = {
flowId?: UnwrapWritable<SvelteFlowStore['flowId']>;
connectionLineType?: UnwrapWritable<SvelteFlowStore['connectionLineType']>;
connectionRadius?: UnwrapWritable<SvelteFlowStore['connectionRadius']>;
selectionMode?: UnwrapWritable<SvelteFlowStore['selectionMode']>;
snapGrid?: UnwrapWritable<SvelteFlowStore['snapGrid']>;
defaultMarkerColor?: UnwrapWritable<SvelteFlowStore['defaultMarkerColor']>;
nodesDraggable?: UnwrapWritable<SvelteFlowStore['nodesDraggable']>;
nodesConnectable?: UnwrapWritable<SvelteFlowStore['nodesConnectable']>;
elementsSelectable?: UnwrapWritable<SvelteFlowStore['elementsSelectable']>;
onlyRenderVisibleElements?: UnwrapWritable<SvelteFlowStore['onlyRenderVisibleElements']>;
isValidConnection?: UnwrapWritable<SvelteFlowStore['isValidConnection']>;
autoPanOnConnect?: UnwrapWritable<SvelteFlowStore['autoPanOnConnect']>;
autoPanOnNodeDrag?: UnwrapWritable<SvelteFlowStore['autoPanOnNodeDrag']>;
connectionMode?: UnwrapWritable<SvelteFlowStore['connectionMode']>;
onerror?: UnwrapWritable<SvelteFlowStore['onerror']>;
ondelete?: UnwrapWritable<SvelteFlowStore['ondelete']>;
onedgecreate?: UnwrapWritable<SvelteFlowStore['onedgecreate']>;
nodeDragThreshold?: UnwrapWritable<SvelteFlowStore['nodeDragThreshold']>;
onconnect?: UnwrapWritable<SvelteFlowStore['onconnect']>;
onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>;
onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>;
onbeforedelete?: UnwrapWritable<SvelteFlowStore['onbeforedelete']>;
nodeOrigin?: UnwrapWritable<SvelteFlowStore['nodeOrigin']>;
};
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {
getKeys(keys).forEach((prop) => {
const update = keys[prop];
if (update !== undefined) {
// @ts-expect-error @todo: how to fix this TS error?
store[prop].set(update);
}
});
}
@@ -1,15 +1,13 @@
<script lang="ts"> <script lang="ts">
import { useStore } from '$lib/store'; import type { Viewport } from '@xyflow/system';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
const { viewport } = useStore(); let { viewport, children }: { viewport: Viewport; children: Snippet } = $props();
let { children }: { children: Snippet } = $props();
</script> </script>
<div <div
class="svelte-flow__viewport xyflow__viewport" class="svelte-flow__viewport xyflow__viewport"
style="transform: translate({$viewport.x}px, {$viewport.y}px) scale({$viewport.zoom})" style="transform: translate({viewport.x}px, {viewport.y}px) scale({viewport.zoom})"
> >
{@render children()} {@render children()}
</div> </div>
@@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { PanOnScrollMode, type Transform } from '@xyflow/system'; import { PanOnScrollMode, type PanZoomInstance, type Transform } from '@xyflow/system';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import zoom from '$lib/actions/zoom'; import zoom from '$lib/actions/zoom';
import type { ZoomProps } from './types'; import type { ZoomProps } from './types';
let { let {
store,
initialViewport = { x: 0, y: 0, zoom: 1 }, initialViewport = { x: 0, y: 0, zoom: 1 },
onMoveStart, onMoveStart,
onMove, onMove,
@@ -22,40 +23,30 @@
children children
}: ZoomProps = $props(); }: ZoomProps = $props();
const { let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
viewport, let panOnScrollActive = $derived(store.panActivationKeyPressed || panOnScroll);
panZoom,
selectionRect,
minZoom,
maxZoom,
dragging,
translateExtent,
lib,
panActivationKeyPressed,
zoomActivationKeyPressed,
viewportInitialized
} = useStore();
let panOnDragActive = $derived($panActivationKeyPressed || panOnDrag);
let panOnScrollActive = $derived($panActivationKeyPressed || panOnScroll);
const onTransformChange = (transform: Transform) => const onTransformChange = (transform: Transform) =>
viewport.set({ x: transform[0], y: transform[1], zoom: transform[2] }); store.viewport.set({ x: transform[0], y: transform[1], zoom: transform[2] });
onMount(() => { onMount(() => {
$viewportInitialized = true; store.viewportInitialized = true;
}); });
</script> </script>
<div <div
class="svelte-flow__zoom" class="svelte-flow__zoom"
use:zoom={{ use:zoom={{
viewport, viewport: store.viewport,
minZoom: $minZoom, minZoom: store.minZoom,
maxZoom: $maxZoom, maxZoom: store.maxZoom,
initialViewport, initialViewport,
dragging, onDraggingChange: (dragging: boolean) => {
panZoom, store.dragging = dragging;
},
setPanZoomInstance: (instance: PanZoomInstance) => {
store.panZoom = instance;
},
onPanZoomStart: onMoveStart, onPanZoomStart: onMoveStart,
onPanZoom: onMove, onPanZoom: onMove,
onPanZoomEnd: onMoveEnd, onPanZoomEnd: onMoveEnd,
@@ -66,13 +57,13 @@
panOnDrag: panOnDragActive, panOnDrag: panOnDragActive,
panOnScrollSpeed: 0.5, panOnScrollSpeed: 0.5,
panOnScrollMode: panOnScrollMode || PanOnScrollMode.Free, panOnScrollMode: panOnScrollMode || PanOnScrollMode.Free,
zoomActivationKeyPressed: $zoomActivationKeyPressed, zoomActivationKeyPressed: store.zoomActivationKeyPressed,
preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true, preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true,
noPanClassName: 'nopan', noPanClassName: 'nopan',
noWheelClassName: 'nowheel', noWheelClassName: 'nowheel',
userSelectionActive: !!$selectionRect, userSelectionActive: !!store.selectionRect,
translateExtent: $translateExtent, translateExtent: store.translateExtent,
lib: $lib, lib: 'svelte',
paneClickDistance, paneClickDistance,
onTransformChange onTransformChange
}} }}
@@ -1,7 +1,9 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode, Viewport } from '@xyflow/system'; import type { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode, Viewport } from '@xyflow/system';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
export type ZoomProps = { export type ZoomProps = {
store: SvelteFlowStore;
initialViewport?: Viewport; initialViewport?: Viewport;
panOnScrollMode: PanOnScrollMode; panOnScrollMode: PanOnScrollMode;
onMove?: OnMove; onMove?: OnMove;
@@ -0,0 +1,19 @@
import { key } from '$lib/store';
import type { StoreContext } from '$lib/store/types';
import { getContext } from 'svelte';
export function derivedWarning(functionName: string) {
const storeContext = getContext<StoreContext>(key);
if (!storeContext) {
throw new Error(
`In order to use ${functionName}() you need to wrap your component in a <SvelteFlowProvider />`
);
}
if (storeContext.provider && !$effect.tracking()) {
console.warn(
`Use $derived(${functionName}()), when not calling inside a child of the <SvelteFlow /> component.`
);
}
}
+1
View File
@@ -0,0 +1 @@
export * from './useSvelteFlow.svelte';
+21
View File
@@ -0,0 +1,21 @@
import { getContext } from 'svelte';
import type { StoreContext, SvelteFlowStore } from '../store/types';
import { key } from '../store';
import { derivedWarning } from './derivedWarning.svelte';
export function useStore(): SvelteFlowStore {
const storeContext = getContext<StoreContext>(key);
if (!storeContext) {
throw new Error(
'In order to use useStore you need to wrap your component in a <SvelteFlowProvider />'
);
}
if (process.env.NODE_ENV === 'development') {
derivedWarning('useStore');
}
return storeContext.getStore();
}
@@ -22,7 +22,8 @@ import {
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { Edge, FitViewOptions, InternalNode, Node } from '$lib/types'; import type { Edge, FitViewOptions, InternalNode, Node } from '$lib/types';
import { isNode } from '$lib/utils'; import { isEdge, isNode } from '$lib/utils';
import { derivedWarning } from './derivedWarning.svelte';
/** /**
* Hook for accessing the ReactFlow instance. * Hook for accessing the ReactFlow instance.
@@ -232,6 +233,21 @@ export function useSvelteFlow(): {
* *
* @returns the nodes, edges and the viewport as a JSON object * @returns the nodes, edges and the viewport as a JSON object
*/ */
/**
* Updates an edge.
*
* @param id - id of the edge to update
* @param edgeUpdate - the edge update as an object or a function that receives the current edge and returns the edge update
* @param options.replace - if true, the edge is replaced with the edge update, otherwise the changes get merged
*
* @example
* updateNode('node-1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }));
*/
updateEdge: (
id: string,
edgeUpdate: Partial<Edge> | ((edge: Edge) => Partial<Edge>),
options?: { replace: boolean }
) => void;
toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport }; toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
/** /**
* Returns the bounds of the given nodes or node ids. * Returns the bounds of the given nodes or node ids.
@@ -258,37 +274,22 @@ export function useSvelteFlow(): {
id?: string | null; id?: string | null;
}) => HandleConnection[]; }) => HandleConnection[];
} { } {
const { if (process.env.NODE_ENV === 'development') {
zoomIn, derivedWarning('useSvelteFlow');
zoomOut, }
fitView,
onbeforedelete, const store = useStore();
snapGrid, const { viewport, nodes, edges } = store;
viewport,
width,
height,
minZoom,
maxZoom,
panZoom,
nodes,
edges,
domNode,
nodeLookup,
nodeOrigin,
edgeLookup,
connectionLookup
} = useStore();
const getNodeRect = (node: Node | { id: Node['id'] }): Rect | null => { const getNodeRect = (node: Node | { id: Node['id'] }): Rect | null => {
const $nodeLookup = get(nodeLookup); const nodeToUse = isNode(node) ? node : store.nodeLookup.get(node.id)!;
const nodeToUse = isNode(node) ? node : $nodeLookup.get(node.id)!;
const position = nodeToUse.parentId const position = nodeToUse.parentId
? evaluateAbsolutePosition( ? evaluateAbsolutePosition(
nodeToUse.position, nodeToUse.position,
nodeToUse.measured, nodeToUse.measured,
nodeToUse.parentId, nodeToUse.parentId,
$nodeLookup, store.nodeLookup,
get(nodeOrigin) store.nodeOrigin
) )
: nodeToUse.position; : nodeToUse.position;
@@ -308,7 +309,7 @@ export function useSvelteFlow(): {
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>), nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
options: { replace: boolean } = { replace: false } options: { replace: boolean } = { replace: false }
) => { ) => {
const node = get(nodeLookup).get(id)?.internals.userNode; const node = store.nodeLookup.get(id)?.internals.userNode;
if (!node) { if (!node) {
return; return;
@@ -328,36 +329,64 @@ export function useSvelteFlow(): {
); );
} else { } else {
Object.assign(node, nextNode); Object.assign(node, nextNode);
nodes.update((nds) => nds); nodes.set(get(nodes));
} }
}; };
const getInternalNode = (id: string) => get(nodeLookup).get(id); const updateEdge = (
id: string,
edgeUpdate: Partial<Edge> | ((edge: Edge) => Partial<Edge>),
options: { replace: boolean } = { replace: false }
) => {
const edge = store.edgeLookup.get(id);
if (!edge) {
return;
}
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge as Edge) : edgeUpdate;
if (options.replace) {
edges.update((edgs) =>
edgs.map((edge) => {
if (edge.id === id) {
return isEdge(nextEdge) ? nextEdge : { ...edge, ...nextEdge };
}
return edge;
})
);
} else {
Object.assign(edge, nextEdge);
edges.set(get(edges));
}
};
const getInternalNode = (id: string) => store.nodeLookup.get(id);
return { return {
zoomIn, zoomIn: store.zoomIn,
zoomOut, zoomOut: store.zoomOut,
getInternalNode, getInternalNode,
getNode: (id) => getInternalNode(id)?.internals.userNode, getNode: (id) => getInternalNode(id)?.internals.userNode,
getNodes: (ids) => (ids === undefined ? get(nodes) : getElements(get(nodeLookup), ids)), getNodes: (ids) => (ids === undefined ? get(nodes) : getElements(store.nodeLookup, ids)),
getEdge: (id) => get(edgeLookup).get(id), getEdge: (id) => store.edgeLookup.get(id),
getEdges: (ids) => (ids === undefined ? get(edges) : getElements(get(edgeLookup), ids)), getEdges: (ids) => (ids === undefined ? get(edges) : getElements(store.edgeLookup, ids)),
setZoom: (zoomLevel, options) => { setZoom: (zoomLevel, options) => {
const currentPanZoom = get(panZoom); const panZoom = store.panZoom;
return currentPanZoom return panZoom
? currentPanZoom.scaleTo(zoomLevel, { duration: options?.duration }) ? panZoom.scaleTo(zoomLevel, { duration: options?.duration })
: Promise.resolve(false); : Promise.resolve(false);
}, },
getZoom: () => get(viewport).zoom, getZoom: () => get(viewport).zoom,
setViewport: async (nextViewport, options) => { setViewport: async (nextViewport, options) => {
const currentViewport = get(viewport); const currentViewport = get(viewport);
const currentPanZoom = get(panZoom);
if (!currentPanZoom) { if (!store.panZoom) {
return Promise.resolve(false); return Promise.resolve(false);
} }
await currentPanZoom.setViewport( await store.panZoom.setViewport(
{ {
x: nextViewport.x ?? currentViewport.x, x: nextViewport.x ?? currentViewport.x,
y: nextViewport.y ?? currentViewport.y, y: nextViewport.y ?? currentViewport.y,
@@ -370,8 +399,8 @@ export function useSvelteFlow(): {
}, },
getViewport: () => get(viewport), getViewport: () => get(viewport),
setCenter: async (x, y, options) => { setCenter: async (x, y, options) => {
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : get(maxZoom); const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
const currentPanZoom = get(panZoom); const currentPanZoom = store.panZoom;
if (!currentPanZoom) { if (!currentPanZoom) {
return Promise.resolve(false); return Promise.resolve(false);
@@ -379,8 +408,8 @@ export function useSvelteFlow(): {
await currentPanZoom.setViewport( await currentPanZoom.setViewport(
{ {
x: get(width) / 2 - x * nextZoom, x: store.width / 2 - x * nextZoom,
y: get(height) / 2 - y * nextZoom, y: store.height / 2 - y * nextZoom,
zoom: nextZoom zoom: nextZoom
}, },
{ duration: options?.duration } { duration: options?.duration }
@@ -388,24 +417,22 @@ export function useSvelteFlow(): {
return Promise.resolve(true); return Promise.resolve(true);
}, },
fitView, fitView: store.fitView,
fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => { fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => {
const currentPanZoom = get(panZoom); if (!store.panZoom) {
if (!currentPanZoom) {
return Promise.resolve(false); return Promise.resolve(false);
} }
const viewport = getViewportForBounds( const viewport = getViewportForBounds(
bounds, bounds,
get(width), store.width,
get(height), store.height,
get(minZoom), store.minZoom,
get(maxZoom), store.maxZoom,
options?.padding ?? 0.1 options?.padding ?? 0.1
); );
await currentPanZoom.setViewport(viewport, { duration: options?.duration }); await store.panZoom.setViewport(viewport, { duration: options?.duration });
return Promise.resolve(true); return Promise.resolve(true);
}, },
@@ -422,7 +449,7 @@ export function useSvelteFlow(): {
} }
return (nodesToIntersect || get(nodes)).filter((n) => { return (nodesToIntersect || get(nodes)).filter((n) => {
const internalNode = get(nodeLookup).get(n.id); const internalNode = store.nodeLookup.get(n.id);
if (!internalNode || (!isRect && n.id === nodeOrRect.id)) { if (!internalNode || (!isRect && n.id === nodeOrRect.id)) {
return false; return false;
} }
@@ -457,7 +484,7 @@ export function useSvelteFlow(): {
edgesToRemove, edgesToRemove,
nodes: get(nodes), nodes: get(nodes),
edges: get(edges), edges: get(edges),
onBeforeDelete: get(onbeforedelete) onBeforeDelete: store.onbeforedelete
}); });
if (matchingNodes) { if (matchingNodes) {
@@ -481,15 +508,13 @@ export function useSvelteFlow(): {
position: XYPosition, position: XYPosition,
options: { snapToGrid: boolean } = { snapToGrid: true } options: { snapToGrid: boolean } = { snapToGrid: true }
) => { ) => {
const _domNode = get(domNode); if (!store.domNode) {
if (!_domNode) {
return position; return position;
} }
const _snapGrid = options.snapToGrid ? get(snapGrid) : false; const _snapGrid = options.snapToGrid ? store.snapGrid : false;
const { x, y, zoom } = get(viewport); const { x, y, zoom } = get(viewport);
const { x: domX, y: domY } = _domNode.getBoundingClientRect(); const { x: domX, y: domY } = store.domNode.getBoundingClientRect();
const correctedPosition = { const correctedPosition = {
x: position.x - domX, x: position.x - domX,
y: position.y - domY y: position.y - domY
@@ -508,14 +533,12 @@ export function useSvelteFlow(): {
* @returns * @returns
*/ */
flowToScreenPosition: (position: XYPosition) => { flowToScreenPosition: (position: XYPosition) => {
const _domNode = get(domNode); if (!store.domNode) {
if (!_domNode) {
return position; return position;
} }
const { x, y, zoom } = get(viewport); const { x, y, zoom } = get(viewport);
const { x: domX, y: domY } = _domNode.getBoundingClientRect(); const { x: domX, y: domY } = store.domNode.getBoundingClientRect();
const rendererPosition = rendererPointToPoint(position, [x, y, zoom]); const rendererPosition = rendererPointToPoint(position, [x, y, zoom]);
return { return {
@@ -539,7 +562,7 @@ export function useSvelteFlow(): {
}, },
updateNode, updateNode,
updateNodeData: (id, dataUpdate, options) => { updateNodeData: (id, dataUpdate, options) => {
const node = get(nodeLookup).get(id)?.internals.userNode; const node = store.nodeLookup.get(id)?.internals.userNode;
if (!node) { if (!node) {
return; return;
@@ -551,18 +574,12 @@ export function useSvelteFlow(): {
nodes.update((nds) => nds); nodes.update((nds) => nds);
}, },
updateEdge,
getNodesBounds: (nodes) => { getNodesBounds: (nodes) => {
const _nodeLookup = get(nodeLookup); return getNodesBounds(nodes, { nodeLookup: store.nodeLookup, nodeOrigin: store.nodeOrigin });
const _nodeOrigin = get(nodeOrigin);
return getNodesBounds(nodes, { nodeLookup: _nodeLookup, nodeOrigin: _nodeOrigin });
}, },
getHandleConnections: ({ type, id, nodeId }) => getHandleConnections: ({ type, id, nodeId }) =>
Array.from( Array.from(store.connectionLookup.get(`${nodeId}-${type}-${id ?? null}`)?.values() ?? []),
get(connectionLookup)
.get(`${nodeId}-${type}-${id ?? null}`)
?.values() ?? []
),
viewport viewport
}; };
} }
+1 -1
View File
@@ -26,7 +26,7 @@ export { useStore } from '$lib/store';
export * from '$lib/utils'; export * from '$lib/utils';
//hooks //hooks
export * from '$lib/hooks/useSvelteFlow'; export * from '$lib/hooks/useSvelteFlow.svelte';
export * from '$lib/hooks/useUpdateNodeInternals'; export * from '$lib/hooks/useUpdateNodeInternals';
export * from '$lib/hooks/useConnection'; export * from '$lib/hooks/useConnection';
export * from '$lib/hooks/useNodesEdges'; export * from '$lib/hooks/useNodesEdges';
@@ -27,13 +27,17 @@
class: className class: className
}: BackgroundProps = $props(); }: BackgroundProps = $props();
const { viewport, flowId } = useStore(); const store = useStore();
const { viewport } = store;
$inspect($viewport);
const patternSize = size ?? defaultSize[variant!]; const patternSize = size ?? defaultSize[variant!];
const isDots = variant === BackgroundVariant.Dots; const isDots = variant === BackgroundVariant.Dots;
const isCross = variant === BackgroundVariant.Cross; const isCross = variant === BackgroundVariant.Cross;
const gapXY: number[] = Array.isArray(gap!) ? gap! : [gap!, gap!]; const gapXY: number[] = Array.isArray(gap!) ? gap! : [gap!, gap!];
let patternId = $derived(`background-pattern-${$flowId}-${id ?? ''}`); let patternId = $derived(`background-pattern-${store.flowId}-${id ?? ''}`);
let scaledGap = $derived([gapXY[0] * $viewport.zoom || 1, gapXY[1] * $viewport.zoom || 1]); let scaledGap = $derived([gapXY[0] * $viewport.zoom || 1, gapXY[1] * $viewport.zoom || 1]);
let scaledSize = $derived(patternSize * $viewport.zoom); let scaledSize = $derived(patternSize * $viewport.zoom);
let patternDimensions = $derived( let patternDimensions = $derived(
@@ -32,17 +32,8 @@
after after
}: ControlsProps = $props(); }: ControlsProps = $props();
const { const store = useStore();
viewport, const { viewport } = store;
minZoom,
maxZoom,
nodesDraggable,
nodesConnectable,
elementsSelectable,
zoomIn,
zoomOut,
fitView
} = useStore();
const buttonProps = { const buttonProps = {
bgColor: buttonBgColor, bgColor: buttonBgColor,
@@ -52,29 +43,31 @@
borderColor: buttonBorderColor borderColor: buttonBorderColor
}; };
let isInteractive = $state($nodesDraggable || $nodesConnectable || $elementsSelectable); let isInteractive = $state(
let minZoomReached = $derived($viewport.zoom <= $minZoom); store.nodesDraggable || store.nodesConnectable || store.elementsSelectable
let maxZoomReached = $derived($viewport.zoom >= $maxZoom); );
let minZoomReached = $derived($viewport.zoom <= store.minZoom);
let maxZoomReached = $derived($viewport.zoom >= store.maxZoom);
let orientationClass = $derived(orientation === 'horizontal' ? 'horizontal' : 'vertical'); let orientationClass = $derived(orientation === 'horizontal' ? 'horizontal' : 'vertical');
const onZoomInHandler = () => { const onZoomInHandler = () => {
zoomIn(); store.zoomIn();
}; };
const onZoomOutHandler = () => { const onZoomOutHandler = () => {
zoomOut(); store.zoomOut();
}; };
const onFitViewHandler = () => { const onFitViewHandler = () => {
fitView(fitViewOptions); store.fitView(fitViewOptions);
}; };
const onToggleInteractivity = () => { const onToggleInteractivity = () => {
isInteractive = !isInteractive; isInteractive = !isInteractive;
nodesDraggable.set(isInteractive); store.nodesDraggable = isInteractive;
nodesConnectable.set(isInteractive); store.nodesConnectable = isInteractive;
elementsSelectable.set(isInteractive); store.elementsSelectable = isInteractive;
}; };
</script> </script>
@@ -42,16 +42,10 @@
class: className class: className
}: MiniMapProps = $props(); }: MiniMapProps = $props();
const { let store = useStore();
nodes, const { viewport } = store;
nodeLookup,
viewport, let nodes = $derived(store.nodes);
width: containerWidth,
height: containerHeight,
flowId,
panZoom,
translateExtent
} = useStore();
const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor); const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
@@ -59,17 +53,20 @@
const shapeRendering = const shapeRendering =
// @ts-expect-error - TS doesn't know about chrome // @ts-expect-error - TS doesn't know about chrome
typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
const labelledBy = `svelte-flow__minimap-desc-${$flowId}`;
let labelledBy = $derived(`svelte-flow__minimap-desc-${store.flowId}`);
// TODO: simplify this // TODO: simplify this
let viewBB = $derived({ let viewBB = $derived({
x: -$viewport.x / $viewport.zoom, x: -$viewport.x / $viewport.zoom,
y: -$viewport.y / $viewport.zoom, y: -$viewport.y / $viewport.zoom,
width: $containerWidth / $viewport.zoom, width: store.width / $viewport.zoom,
height: $containerHeight / $viewport.zoom height: store.height / $viewport.zoom
}); });
let boundingRect = $derived( let boundingRect = $derived(
$nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds($nodeLookup), viewBB) : viewBB store.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(store.nodeLookup), viewBB)
: viewBB
); );
let scaledWidth = $derived(boundingRect.width / width); let scaledWidth = $derived(boundingRect.width / width);
let scaledHeight = $derived(boundingRect.height / height); let scaledHeight = $derived(boundingRect.height / height);
@@ -91,7 +88,7 @@
class={cc(['svelte-flow__minimap', className])} class={cc(['svelte-flow__minimap', className])}
data-testid="svelte-flow__minimap" data-testid="svelte-flow__minimap"
> >
{#if $panZoom} {#if store.panZoom}
<svg <svg
{width} {width}
{height} {height}
@@ -105,12 +102,12 @@
? maskStrokeWidth * viewScale ? maskStrokeWidth * viewScale
: undefined} : undefined}
use:interactive={{ use:interactive={{
panZoom: $panZoom, panZoom: store.panZoom,
viewport, viewport: store.viewport,
getViewScale, getViewScale,
translateExtent: $translateExtent, translateExtent: store.translateExtent,
width: $containerWidth, width: store.width,
height: $containerHeight, height: store.height,
inversePan, inversePan,
zoomStep, zoomStep,
pannable, pannable,
@@ -120,7 +117,7 @@
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if} {#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
{#each $nodes as userNode (userNode.id)} {#each $nodes as userNode (userNode.id)}
{@const node = $nodeLookup.get(userNode.id)} {@const node = store.nodeLookup.get(userNode.id)}
{#if node && nodeHasDimensions(node)} {#if node && nodeHasDimensions(node)}
{@const nodeDimesions = getNodeDimensions(node)} {@const nodeDimesions = getNodeDimensions(node)}
<MinimapNode <MinimapNode
@@ -4,7 +4,7 @@
import portal from '$lib/actions/portal'; import portal from '$lib/actions/portal';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import { useSvelteFlow } from '$lib/hooks/useSvelteFlow'; import { useSvelteFlow } from '$lib/hooks';
import type { InternalNode } from '$lib/types'; import type { InternalNode } from '$lib/types';
import type { NodeToolbarProps } from './types'; import type { NodeToolbarProps } from './types';
@@ -0,0 +1,24 @@
import { getContext } from 'svelte';
import type { StoreContext, SvelteFlowStore } from './types';
import { key } from './';
export function useStore(): SvelteFlowStore {
const storeContext = getContext<StoreContext>(key);
if (!storeContext) {
throw new Error(
'In order to use useStore you need to wrap your component in a <SvelteFlowProvider />'
);
}
if (process.env.NODE_ENV === 'development') {
if (storeContext.provider && !$effect.tracking()) {
console.warn(
'Use $derived(useStore()), when not calling inside a child of the <SvelteFlow /> component.'
);
}
}
return storeContext.getStore();
}
+132 -203
View File
@@ -1,14 +1,11 @@
import { getContext, setContext } from 'svelte'; import { get } from 'svelte/store';
import { derived, get, writable } from 'svelte/store';
import { import {
createMarkerIds,
fitView as fitViewSystem, fitView as fitViewSystem,
panBy as panBySystem, panBy as panBySystem,
updateNodeInternals as updateNodeInternalsSystem, updateNodeInternals as updateNodeInternalsSystem,
addEdge as addEdgeUtil, addEdge as addEdgeUtil,
initialConnection, initialConnection,
errorMessages, errorMessages,
pointToRendererPoint,
type UpdateNodePositions, type UpdateNodePositions,
type InternalNodeUpdate, type InternalNodeUpdate,
type ViewportHelperFunctionOptions, type ViewportHelperFunctionOptions,
@@ -17,60 +14,37 @@ import {
type CoordinateExtent, type CoordinateExtent,
type UpdateConnection, type UpdateConnection,
type ConnectionState, type ConnectionState,
type NodeOrigin,
getFitViewNodes, getFitViewNodes,
updateAbsolutePositions, updateAbsolutePositions,
getDimensions getDimensions
} from '@xyflow/system'; } from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store'; import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store.svelte';
import type { SvelteFlowStore } from './types'; import { type StoreSignals, type SvelteFlowStore, type SvelteFlowStoreActions } from './types';
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils'; import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
import { getVisibleEdges } from './visible-edges'; // import { getVisibleEdges } from './visible-edges';
import { getVisibleNodes } from './visible-nodes'; // import { getVisibleNodes } from './visible-nodes';
export const key = Symbol(); export const key = Symbol();
export function createStore({ export { useStore } from './hook.svelte';
nodes,
edges, export function createStore(signals: StoreSignals): SvelteFlowStore {
width, const store = getInitialStore(signals);
height,
fitView: fitViewOnCreate,
nodeOrigin,
nodeExtent
}: {
nodes?: Node[];
edges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent;
}): SvelteFlowStore {
const store = getInitialStore({
nodes,
edges,
width,
height,
fitView: fitViewOnCreate,
nodeOrigin,
nodeExtent
});
function setNodeTypes(nodeTypes: NodeTypes) { function setNodeTypes(nodeTypes: NodeTypes) {
store.nodeTypes.set({ store.nodeTypes = {
...initialNodeTypes, ...initialNodeTypes,
...nodeTypes ...nodeTypes
}); };
} }
function setEdgeTypes(edgeTypes: EdgeTypes) { function setEdgeTypes(edgeTypes: EdgeTypes) {
store.edgeTypes.set({ store.edgeTypes = {
...initialEdgeTypes, ...initialEdgeTypes,
...edgeTypes ...edgeTypes
}); };
} }
function addEdge(edgeParams: Edge | Connection) { function addEdge(edgeParams: Edge | Connection) {
@@ -79,10 +53,8 @@ export function createStore({
} }
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => { const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
const nodeLookup = get(store.nodeLookup);
for (const [id, dragItem] of nodeDragItems) { for (const [id, dragItem] of nodeDragItems) {
const node = nodeLookup.get(id)?.internals.userNode; const node = store.nodeLookup.get(id)?.internals.userNode;
if (!node) { if (!node) {
continue; continue;
@@ -96,33 +68,33 @@ export function createStore({
}; };
function updateNodeInternals(updates: Map<string, InternalNodeUpdate>) { function updateNodeInternals(updates: Map<string, InternalNodeUpdate>) {
const nodeLookup = get(store.nodeLookup);
const parentLookup = get(store.parentLookup);
const { changes, updatedInternals } = updateNodeInternalsSystem( const { changes, updatedInternals } = updateNodeInternalsSystem(
updates, updates,
nodeLookup, store.nodeLookup,
get(store.parentLookup), store.parentLookup,
get(store.domNode), store.domNode,
get(store.nodeOrigin) store.nodeOrigin
); );
if (!updatedInternals) { if (!updatedInternals) {
return; return;
} }
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent }); updateAbsolutePositions(store.nodeLookup, store.parentLookup, {
nodeOrigin: store.nodeOrigin,
nodeExtent: store.nodeExtent
});
if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) { if (!store.fitViewOnInitDone && store.fitViewOnInit) {
const fitViewOptions = get(store.fitViewOptions);
const fitViewOnInitDone = fitViewSync({ const fitViewOnInitDone = fitViewSync({
...fitViewOptions, ...store.fitViewOptions,
nodes: fitViewOptions?.nodes nodes: store.fitViewOptions?.nodes
}); });
store.fitViewOnInitDone.set(fitViewOnInitDone); store.fitViewOnInitDone = fitViewOnInitDone;
} }
for (const change of changes) { for (const change of changes) {
const node = nodeLookup.get(change.id)?.internals.userNode; const node = store.nodeLookup.get(change.id)?.internals.userNode;
if (!node) { if (!node) {
continue; continue;
@@ -149,14 +121,14 @@ export function createStore({
store.nodes.update((nds) => nds); store.nodes.update((nds) => nds);
if (!get(store.nodesInitialized)) { if (!store.nodesInitialized) {
store.nodesInitialized.set(true); store.nodesInitialized = true;
} }
} }
function fitView(options?: FitViewOptions) { function fitView(options?: FitViewOptions) {
const panZoom = get(store.panZoom); const panZoom = store.panZoom;
const domNode = get(store.domNode); const domNode = store.domNode;
if (!panZoom || !domNode) { if (!panZoom || !domNode) {
return Promise.resolve(false); return Promise.resolve(false);
@@ -164,15 +136,15 @@ export function createStore({
const { width, height } = getDimensions(domNode); const { width, height } = getDimensions(domNode);
const fitViewNodes = getFitViewNodes(get(store.nodeLookup), options); const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
return fitViewSystem( return fitViewSystem(
{ {
nodes: fitViewNodes, nodes: fitViewNodes,
width, width,
height, height,
minZoom: get(store.minZoom), minZoom: store.minZoom,
maxZoom: get(store.maxZoom), maxZoom: store.maxZoom,
panZoom panZoom
}, },
options options
@@ -180,21 +152,21 @@ export function createStore({
} }
function fitViewSync(options?: FitViewOptions) { function fitViewSync(options?: FitViewOptions) {
const panZoom = get(store.panZoom); const panZoom = store.panZoom;
if (!panZoom) { if (!panZoom) {
return false; return false;
} }
const fitViewNodes = getFitViewNodes(get(store.nodeLookup), options); const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
fitViewSystem( fitViewSystem(
{ {
nodes: fitViewNodes, nodes: fitViewNodes,
width: get(store.width), width: store.width,
height: get(store.height), height: store.height,
minZoom: get(store.minZoom), minZoom: store.minZoom,
maxZoom: get(store.maxZoom), maxZoom: store.maxZoom,
panZoom panZoom
}, },
options options
@@ -204,7 +176,7 @@ export function createStore({
} }
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) { function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
const panZoom = get(store.panZoom); const panZoom = store.panZoom;
if (!panZoom) { if (!panZoom) {
return Promise.resolve(false); return Promise.resolve(false);
} }
@@ -221,29 +193,29 @@ export function createStore({
} }
function setMinZoom(minZoom: number) { function setMinZoom(minZoom: number) {
const panZoom = get(store.panZoom); const panZoom = store.panZoom;
if (panZoom) { if (panZoom) {
panZoom.setScaleExtent([minZoom, get(store.maxZoom)]); panZoom.setScaleExtent([minZoom, store.maxZoom]);
store.minZoom.set(minZoom); store.minZoom = minZoom;
} }
} }
function setMaxZoom(maxZoom: number) { function setMaxZoom(maxZoom: number) {
const panZoom = get(store.panZoom); const panZoom = store.panZoom;
if (panZoom) { if (panZoom) {
panZoom.setScaleExtent([get(store.minZoom), maxZoom]); panZoom.setScaleExtent([store.minZoom, maxZoom]);
store.maxZoom.set(maxZoom); store.maxZoom = maxZoom;
} }
} }
function setTranslateExtent(extent: CoordinateExtent) { function setTranslateExtent(extent: CoordinateExtent) {
const panZoom = get(store.panZoom); const panZoom = store.panZoom;
if (panZoom) { if (panZoom) {
panZoom.setTranslateExtent(extent); panZoom.setTranslateExtent(extent);
store.translateExtent.set(extent); store.translateExtent = extent;
} }
} }
@@ -259,7 +231,7 @@ export function createStore({
} }
function setPaneClickDistance(distance: number) { function setPaneClickDistance(distance: number) {
get(store.panZoom)?.setClickDistance(distance); store.panZoom?.setClickDistance(distance);
} }
function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) { function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) {
@@ -270,39 +242,8 @@ export function createStore({
if (resetEdges) store.edges.set(get(store.edges)); if (resetEdges) store.edges.set(get(store.edges));
} }
// store.deleteKeyPressed.subscribe(async (deleteKeyPressed) => {
// if (deleteKeyPressed) {
// const nodes = get(store.nodes);
// const edges = get(store.edges);
// const selectedNodes = nodes.filter((node) => node.selected);
// const selectedEdges = edges.filter((edge) => edge.selected);
// const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
// nodesToRemove: selectedNodes,
// edgesToRemove: selectedEdges,
// nodes,
// edges,
// onBeforeDelete: get(store.onbeforedelete)
// });
// if (matchingNodes.length || matchingEdges.length) {
// store.nodes.update((nds) =>
// nds.filter((node) => !matchingNodes.some((mN) => mN.id === node.id))
// );
// store.edges.update((eds) =>
// eds.filter((edge) => !matchingEdges.some((mE) => mE.id === edge.id))
// );
// get(store.ondelete)?.({
// nodes: matchingNodes,
// edges: matchingEdges
// });
// }
// }
// });
function addSelectedNodes(ids: string[]) { function addSelectedNodes(ids: string[]) {
const isMultiSelection = get(store.multiselectionKeyPressed); const isMultiSelection = store.multiselectionKeyPressed;
store.nodes.update((ns) => store.nodes.update((ns) =>
ns.map((node) => { ns.map((node) => {
@@ -329,7 +270,7 @@ export function createStore({
} }
function addSelectedEdges(ids: string[]) { function addSelectedEdges(ids: string[]) {
const isMultiSelection = get(store.multiselectionKeyPressed); const isMultiSelection = store.multiselectionKeyPressed;
store.edges.update((edges) => store.edges.update((edges) =>
edges.map((edge) => { edges.map((edge) => {
@@ -362,12 +303,12 @@ export function createStore({
return; return;
} }
store.selectionRect.set(null); store.selectionRect = null;
store.selectionRectMode.set(null); store.selectionRectMode = null;
if (!node.selected) { if (!node.selected) {
addSelectedNodes([id]); addSelectedNodes([id]);
} else if (node.selected && get(store.multiselectionKeyPressed)) { } else if (node.selected && store.multiselectionKeyPressed) {
unselectNodesAndEdges({ nodes: [node], edges: [] }); unselectNodesAndEdges({ nodes: [node], edges: [] });
} }
} }
@@ -376,78 +317,33 @@ export function createStore({
const viewport = get(store.viewport); const viewport = get(store.viewport);
return panBySystem({ return panBySystem({
delta, delta,
panZoom: get(store.panZoom), panZoom: store.panZoom,
transform: [viewport.x, viewport.y, viewport.zoom], transform: [viewport.x, viewport.y, viewport.zoom],
translateExtent: get(store.translateExtent), translateExtent: store.translateExtent,
width: get(store.width), width: store.width,
height: get(store.height) height: store.height
}); });
} }
const _connection = writable<ConnectionState>(initialConnection); // const _connection = writable<ConnectionState>(initialConnection);
const updateConnection: UpdateConnection = (newConnection: ConnectionState) => { const updateConnection: UpdateConnection = (newConnection: ConnectionState) => {
_connection.set({ ...newConnection }); store.rawConnection = { ...newConnection };
}; };
function cancelConnection() { function cancelConnection() {
_connection.set(initialConnection); store.rawConnection = initialConnection;
} }
function reset() { function reset() {
store.fitViewOnInitDone.set(false); store.fitViewOnInitDone = false;
store.selectionRect.set(null); store.selectionRect = null;
store.selectionRectMode.set(null); store.selectionRectMode = null;
store.snapGrid.set(null);
store.isValidConnection.set(() => true);
unselectNodesAndEdges(); unselectNodesAndEdges();
cancelConnection(); cancelConnection();
} }
return { const storeWithActions = Object.assign(store, {
// state
...store,
// derived state
visibleEdges: getVisibleEdges(store),
visibleNodes: getVisibleNodes(store),
connection: derived([_connection, store.viewport], ([connection, viewport]) => {
return connection.inProgress
? {
...connection,
to: pointToRendererPoint(connection.to, [viewport.x, viewport.y, viewport.zoom])
}
: { ...connection };
}),
markers: derived(
[store.edges, store.defaultMarkerColor, store.flowId],
([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id })
),
initialized: (() => {
let initialized = false;
const initialNodesLength = get(store.nodes).length;
const initialEdgesLength = get(store.edges).length;
return derived(
[store.nodesInitialized, store.edgesInitialized, store.viewportInitialized],
([nodesInitialized, edgesInitialized, viewportInitialized]) => {
// If it was already initialized, return true from then on
if (initialized) return initialized;
// if it hasn't been initialised check if it's now
if (initialNodesLength === 0) {
initialized = viewportInitialized;
} else if (initialEdgesLength === 0) {
initialized = viewportInitialized && nodesInitialized;
} else {
initialized = viewportInitialized && nodesInitialized && edgesInitialized;
}
return initialized;
}
);
})(),
// actions
syncNodeStores: (nodes) => syncNodeStores(store.nodes, nodes), syncNodeStores: (nodes) => syncNodeStores(store.nodes, nodes),
syncEdgeStores: (edges) => syncEdgeStores(store.edges, edges), syncEdgeStores: (edges) => syncEdgeStores(store.edges, edges),
syncViewport: (viewport) => syncViewportStores(store.panZoom, store.viewport, viewport), syncViewport: (viewport) => syncViewportStores(store.panZoom, store.viewport, viewport),
@@ -471,43 +367,76 @@ export function createStore({
updateConnection, updateConnection,
cancelConnection, cancelConnection,
reset reset
}; } satisfies SvelteFlowStoreActions);
}
export function useStore(): SvelteFlowStore { return storeWithActions;
const store = getContext<{ getStore: () => SvelteFlowStore }>(key);
if (!store) { // return {
throw new Error( // // state
'In order to use useStore you need to wrap your component in a <SvelteFlowProvider />' // ...store,
);
}
return store.getStore(); // // derived state
} // // visibleEdges: getVisibleEdges(store),
// // visibleNodes: getVisibleNodes(store),
// // connection: derived([_connection, store.viewport], ([connection, viewport]) => {
// // return connection.inProgress
// // ? {
// // ...connection,
// // to: pointToRendererPoint(connection.to, [viewport.x, viewport.y, viewport.zoom])
// // }
// // : { ...connection };
// // }),
// // markers: derived(
// // [store.edges, store.defaultMarkerColor, store.flowId],
// // ([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id })
// // ),
// // initialized: (() => {
// // let initialized = false;
// // const initialNodesLength = get(store.nodes).length;
// // const initialEdgesLength = get(store.edges).length;
// // return derived(
// // [store.nodesInitialized, store.edgesInitialized, store.viewportInitialized],
// // ([nodesInitialized, edgesInitialized, viewportInitialized]) => {
// // // If it was already initialized, return true from then on
// // if (initialized) return initialized;
export function createStoreContext({ // // // if it hasn't been initialised check if it's now
nodes, // // if (initialNodesLength === 0) {
edges, // // initialized = viewportInitialized;
width, // // } else if (initialEdgesLength === 0) {
height, // // initialized = viewportInitialized && nodesInitialized;
fitView, // // } else {
nodeOrigin, // // initialized = viewportInitialized && nodesInitialized && edgesInitialized;
nodeExtent // // }
}: {
nodes?: Node[];
edges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent;
}) {
const store = createStore({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent });
setContext(key, { // // return initialized;
getStore: () => store // // }
}); // // );
// // })(),
return store; // // actions
// syncNodeStores: (nodes) => syncNodeStores(store.nodes, nodes),
// syncEdgeStores: (edges) => syncEdgeStores(store.edges, edges),
// // syncViewport: (viewport) => syncViewportStores(store.panZoom, store.viewport, viewport),
// setNodeTypes,
// setEdgeTypes,
// addEdge,
// updateNodePositions,
// updateNodeInternals,
// zoomIn,
// zoomOut,
// fitView: (options?: FitViewOptions) => fitView(options),
// setMinZoom,
// setMaxZoom,
// setTranslateExtent,
// setPaneClickDistance,
// unselectNodesAndEdges,
// addSelectedNodes,
// addSelectedEdges,
// handleNodeSelection,
// panBy,
// updateConnection,
// cancelConnection,
// reset
// };
} }
@@ -0,0 +1,318 @@
import { get, writable, type Writable } from 'svelte/store';
import {
infiniteExtent,
SelectionMode,
ConnectionMode,
ConnectionLineType,
devWarn,
adoptUserNodes,
getViewportForBounds,
updateConnectionLookup,
initialConnection,
type SelectionRect,
type SnapGrid,
type MarkerProps,
type PanZoomInstance,
type CoordinateExtent,
type NodeOrigin,
type OnError,
type Viewport,
type OnConnect,
type OnConnectStart,
type OnConnectEnd,
type NodeLookup,
type ConnectionState,
getInternalNodesBounds,
type ColorMode,
createMarkerIds,
type EdgeLookup,
type ConnectionLookup,
type ParentLookup,
pointToRendererPoint
} from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import InputNode from '$lib/components/nodes/InputNode.svelte';
import OutputNode from '$lib/components/nodes/OutputNode.svelte';
import GroupNode from '$lib/components/nodes/GroupNode.svelte';
import {
BezierEdgeInternal,
SmoothStepEdgeInternal,
StraightEdgeInternal,
StepEdgeInternal
} from '$lib/components/edges';
import type {
NodeTypes,
EdgeTypes,
FitViewOptions,
OnDelete,
OnEdgeCreate,
OnBeforeDelete,
IsValidConnection
} from '$lib/types';
import { createNodesStore, createEdgesStore } from './utils';
import type { StoreSignals } from './types';
export const initialNodeTypes = {
input: InputNode,
output: OutputNode,
default: DefaultNode,
group: GroupNode
};
export const initialEdgeTypes = {
straight: StraightEdgeInternal,
smoothstep: SmoothStepEdgeInternal,
default: BezierEdgeInternal,
step: StepEdgeInternal
};
export const getInitialStore = (signals: StoreSignals) => {
class SvelteFlowStore {
nodes: ReturnType<typeof createNodesStore>;
edges: ReturnType<typeof createEdgesStore>;
nodeLookup: NodeLookup = new Map();
parentLookup: ParentLookup = new Map();
connectionLookup: ConnectionLookup = new Map();
edgeLookup: EdgeLookup = new Map();
domNode: HTMLDivElement | null = $derived(
signals.container?.domNode ?? signals.props.domNode ?? null
);
width: number = $derived(signals.container?.width ?? signals.props.width ?? 0);
height: number = $derived(signals.container?.height ?? signals.props.height ?? 0);
flowId: string = $derived(signals.props.id ?? '1');
minZoom: number = $derived(signals.props.minZoom ?? 0.5);
maxZoom: number = $derived(signals.props.maxZoom ?? 2);
nodeOrigin: NodeOrigin = $derived(signals.props.nodeOrigin ?? [0, 0]);
nodeExtent: CoordinateExtent = $derived(signals.props.nodeExtent ?? infiniteExtent);
translateExtent: CoordinateExtent = $derived(signals.props.translateExtent ?? infiniteExtent);
nodeDragThreshold: number = $derived(signals.props.nodeDragThreshold ?? 1);
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
fitViewOnInitDone: boolean = $state(false);
fitViewOnInit: boolean = $derived(signals.props.fitView ?? false);
fitViewOptions: FitViewOptions | undefined = $derived(signals.props.fitViewOptions);
panZoom: PanZoomInstance | null = $state(null);
snapGrid: SnapGrid | null = $derived(signals.props.snapGrid ?? null);
dragging: boolean = $state(false);
selectionRect: SelectionRect | null = $state(null);
selectionKeyPressed: boolean = $state(false);
multiselectionKeyPressed: boolean = $state(false);
deleteKeyPressed: boolean = $state(false);
panActivationKeyPressed: boolean = $state(false);
zoomActivationKeyPressed: boolean = $state(false);
selectionRectMode: string | null = $state(null);
selectionMode: SelectionMode = $derived(signals.props.selectionMode ?? SelectionMode.Partial);
nodeTypes: NodeTypes = $derived({ ...initialNodeTypes, ...signals.props.nodeTypes });
edgeTypes: EdgeTypes = $derived({ ...initialEdgeTypes, ...signals.props.edgeTypes });
viewport: Writable<Viewport> = writable(
signals.props.initialViewport ?? { x: 0, y: 0, zoom: 1 }
);
connectionMode: ConnectionMode = $derived(
signals.props.connectionMode ?? ConnectionMode.Strict
);
rawConnection: ConnectionState = $state(initialConnection);
connection: ConnectionState = $derived.by(() => {
if (this.rawConnection.inProgress) {
const viewport = get(this.viewport);
return {
...this.rawConnection,
to: pointToRendererPoint(this.rawConnection.to, [viewport.x, viewport.y, viewport.zoom])
};
} else {
return { ...this.rawConnection };
}
});
connectionLineType: ConnectionLineType = $derived(
signals.props.connectionLineType ?? ConnectionLineType.Bezier
);
connectionRadius: number = $derived(signals.props.connectionRadius ?? 20);
isValidConnection: IsValidConnection = $derived(
signals.props.isValidConnection ?? (() => true)
);
nodesDraggable: boolean = $derived(signals.props.nodesDraggable ?? true);
nodesConnectable: boolean = $derived(signals.props.nodesConnectable ?? true);
elementsSelectable: boolean = $derived(signals.props.elementsSelectable ?? true);
selectNodesOnDrag: boolean = $derived(signals.props.selectNodesOnDrag ?? true);
// // [store.edges, store.defaultMarkerColor, store.flowId],
// // ([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id })
defaultMarkerColor: string = $derived(signals.props.defaultMarkerColor ?? '#b1b1b7');
markers: MarkerProps[] = $derived.by(() => {
const edges = signals.props.edges ?? writable([]);
return createMarkerIds(get(edges), {
defaultColor: this.defaultMarkerColor,
id: this.flowId
});
});
onlyRenderVisibleElements: boolean = $derived(signals.props.onlyRenderVisibleElements ?? false);
onerror: OnError = $derived(signals.props.onerror ?? devWarn);
ondelete?: OnDelete = $derived(signals.props.ondelete ?? undefined);
onedgecreate?: OnEdgeCreate = $derived(signals.props.onedgecreate);
onconnect?: OnConnect = $derived(signals.props.onconnect);
onconnectstart?: OnConnectStart = $derived(signals.props.onconnectstart);
onconnectend?: OnConnectEnd = $derived(signals.props.onconnectend);
onbeforedelete?: OnBeforeDelete = $derived(signals.props.onbeforedelete);
nodesInitialized: boolean = $state(false);
edgesInitialized: boolean = $state(false);
viewportInitialized: boolean = $state(false);
initialNodesLength: number = signals.props.nodes ? get(signals.props.nodes)?.length : 0;
initialEdgesLength: number = signals.props.edges ? get(signals.props.edges)?.length : 0;
initialized: boolean = $derived.by(() => {
let initialized = false;
// if it hasn't been initialised check if it's now
if (this.initialNodesLength === 0) {
initialized = this.viewportInitialized;
} else if (this.initialEdgesLength === 0) {
initialized = this.viewportInitialized && this.nodesInitialized;
} else {
initialized = this.viewportInitialized && this.nodesInitialized && this.edgesInitialized;
}
return initialized;
});
colorMode: ColorMode = $derived(signals.props.colorMode ?? 'light');
constructor() {
const nodes = signals.props.nodes ? get(signals.props.nodes) : [];
const edges = signals.props.edges ? get(signals.props.edges) : [];
this.nodes = createNodesStore(
nodes,
this.nodeLookup,
this.parentLookup,
this.nodeOrigin,
this.nodeExtent
);
adoptUserNodes(nodes, this.nodeLookup, this.parentLookup, {
nodeExtent: this.nodeExtent,
nodeOrigin: this.nodeOrigin,
elevateNodesOnSelect: false,
checkEquality: false
});
this.edges = createEdgesStore(edges, this.connectionLookup, this.edgeLookup);
updateConnectionLookup(this.connectionLookup, this.edgeLookup, edges);
if (signals.props.fitView && this.width && this.height) {
const bounds = getInternalNodesBounds(this.nodeLookup, {
filter: (node) =>
!!((node.width || node.initialWidth) && (node.height || node.initialHeight))
});
this.viewport.set(getViewportForBounds(bounds, this.width, this.height, 0.5, 2, 0.1));
}
}
resetStoreValues() {
// TODO: reset all store values here
}
}
return new SvelteFlowStore();
// return new Store();
// const nodeLookup: NodeLookup = new Map();
// const parentLookup = new Map();
// const connectionLookup = new Map();
// const edgeLookup = new Map();
// const storeNodeOrigin = nodeOrigin ?? [0, 0];
// const storeNodeExtent = nodeExtent ?? infiniteExtent;
// adoptUserNodes(nodes, nodeLookup, parentLookup, {
// nodeExtent: storeNodeExtent,
// nodeOrigin: storeNodeOrigin,
// elevateNodesOnSelect: false,
// checkEquality: false
// });
// updateConnectionLookup(connectionLookup, edgeLookup, edges);
// let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
// if (fitView && width && height) {
// const bounds = getInternalNodesBounds(nodeLookup, {
// filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight))
// });
// viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
// }
// return {
// flowId: writable<string | null>(null),
// nodes: createNodesStore(nodes, nodeLookup, parentLookup, storeNodeOrigin, storeNodeExtent),
// nodeLookup: readable<NodeLookup<InternalNode>>(nodeLookup),
// parentLookup: readable<ParentLookup<InternalNode>>(parentLookup),
// edgeLookup: readable<EdgeLookup<Edge>>(edgeLookup),
// visibleNodes: readable<InternalNode[]>([]),
// edges: createEdgesStore(edges, connectionLookup, edgeLookup),
// visibleEdges: readable<EdgeLayouted[]>([]),
// connectionLookup: readable<ConnectionLookup>(connectionLookup),
// height: writable<number>(500),
// width: writable<number>(500),
// minZoom: writable<number>(0.5),
// maxZoom: writable<number>(2),
// nodeOrigin: writable<NodeOrigin>(storeNodeOrigin),
// nodeDragThreshold: writable<number>(1),
// nodeExtent: writable<CoordinateExtent>(storeNodeExtent),
// translateExtent: writable<CoordinateExtent>(infiniteExtent),
// autoPanOnNodeDrag: writable<boolean>(true),
// autoPanOnConnect: writable<boolean>(true),
// fitViewOnInit: writable<boolean>(false),
// fitViewOnInitDone: writable<boolean>(false),
// fitViewOptions: writable<FitViewOptions>(undefined),
// panZoom: writable<PanZoomInstance | null>(null),
// snapGrid: writable<SnapGrid | null>(null),
// dragging: writable<boolean>(false),
// selectionRect: writable<SelectionRect | null>(null),
// selectionKeyPressed: writable<boolean>(false),
// multiselectionKeyPressed: writable<boolean>(false),
// deleteKeyPressed: writable<boolean>(false),
// panActivationKeyPressed: writable<boolean>(false),
// zoomActivationKeyPressed: writable<boolean>(false),
// selectionRectMode: writable<string | null>(null),
// selectionMode: writable<SelectionMode>(SelectionMode.Partial),
// nodeTypes: writable<NodeTypes>(initialNodeTypes),
// edgeTypes: writable<EdgeTypes>(initialEdgeTypes),
// viewport: writable<Viewport>(viewport),
// connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
// domNode: writable<HTMLDivElement | null>(null),
// connection: readable<ConnectionState>(initialConnection),
// connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier),
// connectionRadius: writable<number>(20),
// isValidConnection: writable<IsValidConnection>(() => true),
// nodesDraggable: writable<boolean>(true),
// nodesConnectable: writable<boolean>(true),
// elementsSelectable: writable<boolean>(true),
// selectNodesOnDrag: writable<boolean>(true),
// markers: readable<MarkerProps[]>([]),
// defaultMarkerColor: writable<string>('#b1b1b7'),
// lib: readable<string>('svelte'),
// onlyRenderVisibleElements: writable<boolean>(false),
// onerror: writable<OnError>(devWarn),
// ondelete: writable<OnDelete>(undefined),
// onedgecreate: writable<OnEdgeCreate>(undefined),
// onconnect: writable<OnConnect>(undefined),
// onconnectstart: writable<OnConnectStart>(undefined),
// onconnectend: writable<OnConnectEnd>(undefined),
// onbeforedelete: writable<OnBeforeDelete>(undefined),
// nodesInitialized: writable<boolean>(false),
// edgesInitialized: writable<boolean>(false),
// viewportInitialized: writable<boolean>(false),
// initialized: readable<boolean>(false)
// };
};
+17 -1
View File
@@ -10,8 +10,9 @@ import type {
Viewport Viewport
} from '@xyflow/system'; } from '@xyflow/system';
import type { getInitialStore } from './initial-store'; import type { getInitialStore } from './initial-store.svelte';
import type { Node, Edge, NodeTypes, EdgeTypes, FitViewOptions } from '$lib/types'; import type { Node, Edge, NodeTypes, EdgeTypes, FitViewOptions } from '$lib/types';
import type { SvelteFlowProps } from '$lib/container/SvelteFlow';
export type SvelteFlowStoreActions = { export type SvelteFlowStoreActions = {
syncNodeStores: (nodesStore: Writable<Node[]>) => void; syncNodeStores: (nodesStore: Writable<Node[]>) => void;
@@ -39,6 +40,21 @@ export type SvelteFlowStoreActions = {
reset(): void; reset(): void;
}; };
export type ContainerSignals = { domNode?: HTMLDivElement | null; width?: number; height?: number };
export type StoreSignals = {
props: Partial<SvelteFlowProps> & ContainerSignals;
container?: ContainerSignals;
};
export type SvelteFlowStoreState = ReturnType<typeof getInitialStore>; export type SvelteFlowStoreState = ReturnType<typeof getInitialStore>;
export type SvelteFlowStore = SvelteFlowStoreState & SvelteFlowStoreActions; export type SvelteFlowStore = SvelteFlowStoreState & SvelteFlowStoreActions;
export type StoreContext = {
getStore: () => SvelteFlowStore;
provider: boolean;
};
export type ProviderContext = StoreContext & {
setStore: (store: SvelteFlowStore) => void;
};
+2 -2
View File
@@ -78,7 +78,7 @@ export function syncEdgeStores(
// it is possible to pass a viewport store to SvelteFlow for having more control // it is possible to pass a viewport store to SvelteFlow for having more control
// if that's the case we need to sync the internal viewport with the user viewport // if that's the case we need to sync the internal viewport with the user viewport
export const syncViewportStores = ( export const syncViewportStores = (
panZoomStore: Writable<PanZoomInstance | null>, panZoomStore: PanZoomInstance | null,
viewportStore: Writable<Viewport>, viewportStore: Writable<Viewport>,
userViewportStore?: Writable<Viewport> userViewportStore?: Writable<Viewport>
) => { ) => {
@@ -86,7 +86,7 @@ export const syncViewportStores = (
return; return;
} }
const panZoom = get(panZoomStore); const panZoom = panZoomStore;
const viewportStoreSetter = viewportStore.set; const viewportStoreSetter = viewportStore.set;
const userViewportStoreSetter = userViewportStore.set; const userViewportStoreSetter = userViewportStore.set;
+23
View File
@@ -0,0 +1,23 @@
/** @typedef {typeof __propDef.props} PageProps */
/** @typedef {typeof __propDef.events} PageEvents */
/** @typedef {typeof __propDef.slots} PageSlots */
export default class Page extends SvelteComponentTyped<{
[x: string]: never;
}, {
[evt: string]: CustomEvent<any>;
}, {}> {
}
export type PageProps = typeof __propDef.props;
export type PageEvents = typeof __propDef.events;
export type PageSlots = typeof __propDef.slots;
import { SvelteComponentTyped } from "svelte";
declare const __propDef: {
props: {
[x: string]: never;
};
events: {
[evt: string]: CustomEvent<any>;
};
slots: {};
};
export {};
+536 -211
View File
File diff suppressed because it is too large Load Diff