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 1622 additions and 1143 deletions

View File

@@ -1 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

View File

@@ -13,24 +13,24 @@
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.8.0",
"@typescript-eslint/eslint-plugin": "^8.14.0",
"@typescript-eslint/parser": "^8.14.0",
"@sveltejs/kit": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.17.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.0",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.8",
"svelte": "^5.1.16",
"svelte-check": "^4.0.7",
"eslint-plugin-svelte": "^2.46.1",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"svelte": "^5.7.1",
"svelte-check": "^4.1.1",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"vite": "^5.4.11"
"vite": "^6.0.3"
},
"type": "module",
"dependencies": {
"@dagrejs/dagre": "^1.1.4",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@sveltejs/vite-plugin-svelte": "^5.0.1",
"@xyflow/svelte": "workspace:^"
}
}

View File

@@ -129,23 +129,7 @@
target: '6',
label: 'hi',
labelStyle: 'background: red; font-weight: 700; padding: 5px;',
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
}
style: 'stroke: #ffcc0'
}
]);

View File

@@ -1,10 +1,15 @@
<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;
interface Props {
id?: $$Props['id'];
style?: $$Props['style'];
@@ -15,7 +20,7 @@
targetX: $$Props['targetX'];
targetY: $$Props['targetY'];
targetPosition: $$Props['targetPosition'];
[key: string]: any
[key: string]: any;
}
let {
@@ -33,14 +38,18 @@
rest;
let [edgePath, labelX, labelY] = $derived(getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
}));
let [edgePath, labelX, labelY] = $derived(
getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
})
);
const { updateEdge } = useSvelteFlow();
</script>
<BaseEdge path={edgePath} {markerEnd} {style} />
@@ -53,7 +62,24 @@
class="edgeButton"
onclick={(event) => {
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
}
});
}}
>
×

View File

@@ -10,9 +10,9 @@
const nodes = writable<Node[]>(initialNodes);
const edges = writable<Edge[]>(initialEdges);
const { getIntersectingNodes } = useSvelteFlow();
const { getIntersectingNodes } = $derived(useSvelteFlow());
function onNodeDrag({ detail: { targetNode } }) {
function onNodeDrag({ targetNode }) {
if (targetNode) {
const intersections = getIntersectingNodes(targetNode).map((n) => n.id);
@@ -25,7 +25,7 @@
</script>
<div style="height:100vh;">
<SvelteFlow {nodes} {edges} fitView class="intersection-flow" on:nodedrag={onNodeDrag}>
<SvelteFlow {nodes} {edges} fitView class="intersection-flow" onnodedrag={onNodeDrag}>
<Background />
<Controls />
</SvelteFlow>

View File

@@ -56,26 +56,26 @@
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.8.0",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/package": "^2.3.7",
"@typescript-eslint/eslint-plugin": "^8.14.0",
"@typescript-eslint/parser": "^8.14.0",
"@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.17.0",
"autoprefixer": "^10.4.20",
"cssnano": "^7.0.6",
"dotenv": "^16.4.5",
"dotenv": "^16.4.7",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.0",
"eslint-plugin-svelte": "^2.46.1",
"postcss": "^8.4.49",
"postcss-cli": "^11.0.0",
"postcss-combine-duplicated-selectors": "^10.0.3",
"postcss-import": "^16.1.0",
"postcss-nested": "^7.0.2",
"postcss-rename": "^0.6.1",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.8",
"svelte": "^5.1.16",
"svelte-check": "^4.0.7",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"svelte": "^5.7.1",
"svelte-check": "^4.1.1",
"svelte-eslint-parser": "^0.43.0",
"svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1",

View File

@@ -25,24 +25,24 @@ export default function drag(domNode: Element, params: UseDragParams) {
onDragStop,
onNodeMouseDown,
getStoreItems: () => {
const snapGrid = get(store.snapGrid);
const snapGrid = store.snapGrid;
const vp = get(store.viewport);
return {
nodes: get(store.nodes),
nodeLookup: get(store.nodeLookup),
nodeLookup: store.nodeLookup,
edges: get(store.edges),
nodeExtent: get(store.nodeExtent),
nodeExtent: store.nodeExtent,
snapGrid: snapGrid ? snapGrid : [0, 0],
snapToGrid: !!snapGrid,
nodeOrigin: get(store.nodeOrigin),
multiSelectionActive: get(store.multiselectionKeyPressed),
domNode: get(store.domNode),
nodeOrigin: store.nodeOrigin,
multiSelectionActive: store.multiselectionKeyPressed,
domNode: store.domNode,
transform: [vp.x, vp.y, vp.zoom],
autoPanOnNodeDrag: get(store.autoPanOnNodeDrag),
nodesDraggable: get(store.nodesDraggable),
selectNodesOnDrag: get(store.selectNodesOnDrag),
nodeDragThreshold: get(store.nodeDragThreshold),
autoPanOnNodeDrag: store.autoPanOnNodeDrag,
nodesDraggable: store.nodesDraggable,
selectNodesOnDrag: store.selectNodesOnDrag,
nodeDragThreshold: store.nodeDragThreshold,
unselectNodesAndEdges: store.unselectNodesAndEdges,
updateNodePositions: store.updateNodePositions,
panBy: store.panBy

View File

@@ -14,13 +14,12 @@ type ZoomParams = {
initialViewport: Viewport;
minZoom: number;
maxZoom: number;
dragging: Writable<boolean>;
setPanZoomInstance: (panZoomInstance: PanZoomInstance) => void;
onPanZoomStart?: OnPanZoom;
onPanZoom?: OnPanZoom;
onPanZoomEnd?: OnPanZoom;
onPaneContextMenu?: (event: MouseEvent) => void;
translateExtent: CoordinateExtent;
panZoom: Writable<PanZoomInstance | null>;
zoomOnScroll: boolean;
zoomOnPinch: boolean;
zoomOnDoubleClick: boolean;
@@ -38,18 +37,19 @@ type ZoomParams = {
lib: string;
paneClickDistance: number;
onTransformChange: (transform: Transform) => void;
onDraggingChange: (dragging: boolean) => void;
};
export default function zoom(domNode: Element, params: ZoomParams) {
const {
panZoom,
minZoom,
maxZoom,
initialViewport,
viewport,
dragging,
translateExtent,
paneClickDistance
paneClickDistance,
setPanZoomInstance,
onDraggingChange
} = params;
const panZoomInstance = XYPanZoom({
@@ -59,11 +59,11 @@ export default function zoom(domNode: Element, params: ZoomParams) {
translateExtent,
viewport: initialViewport,
paneClickDistance,
onDraggingChange: dragging.set
onDraggingChange
});
const currentViewport = panZoomInstance.getViewport();
viewport.set(currentViewport);
panZoom.set(panZoomInstance);
setPanZoomInstance(panZoomInstance);
panZoomInstance.update(params);

View File

@@ -9,35 +9,37 @@
getStraightPath
} from '@xyflow/system';
import { useStore } from '$lib/store';
import type { SvelteFlowStore } from '$lib/store/types';
let {
store,
containerStyle = '',
style = '',
connectionLine
}: {
store: SvelteFlowStore;
containerStyle: string;
style: string;
connectionLine?: Snippet;
} = $props();
const { width, height, connection, connectionLineType } = useStore();
// $inspect(store.connection);
let path = $derived.by(() => {
if (!$connection.inProgress) {
if (!store.connection.inProgress) {
return '';
}
const pathParams = {
sourceX: $connection.from.x,
sourceY: $connection.from.y,
sourcePosition: $connection.fromPosition,
targetX: $connection.to.x,
targetY: $connection.to.y,
targetPosition: $connection.toPosition
sourceX: store.connection.from.x,
sourceY: store.connection.from.y,
sourcePosition: store.connection.fromPosition,
targetX: store.connection.to.x,
targetY: store.connection.to.y,
targetPosition: store.connection.toPosition
};
switch ($connectionLineType) {
switch (store.connectionLineType) {
case ConnectionLineType.Bezier: {
const [path] = getBezierPath(pathParams);
return path;
@@ -50,7 +52,7 @@
case ConnectionLineType.SmoothStep: {
const [path] = getSmoothStepPath({
...pathParams,
borderRadius: $connectionLineType === ConnectionLineType.Step ? 0 : undefined
borderRadius: store.connectionLineType === ConnectionLineType.Step ? 0 : undefined
});
return path;
}
@@ -58,9 +60,14 @@
});
</script>
{#if $connection.inProgress}
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}>
<g class={cc(['svelte-flow__connection', getConnectionStatus($connection.isValid)])}>
{#if store.connection.inProgress}
<svg
width={store.width}
height={store.height}
class="svelte-flow__connectionline"
style={containerStyle}
>
<g class={cc(['svelte-flow__connection', getConnectionStatus(store.connection.isValid)])}>
{#if connectionLine}
{@render connectionLine()}
{:else}

View File

@@ -3,11 +3,11 @@
import portal from '$lib/actions/portal';
import { useStore } from '$lib/store';
const { domNode } = useStore();
const store = useStore();
let { children }: { children?: Snippet } = $props();
</script>
<div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: $domNode }}>
<div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: store.domNode }}>
{@render children?.()}
</div>

View File

@@ -9,10 +9,12 @@
import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect';
import type { EdgeLayouted, Edge, EdgeEvents } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types';
const {
id,
type = 'default',
store,
source,
target,
data = {},
@@ -42,24 +44,24 @@
onedgecontextmenu,
onedgemouseenter,
onedgemouseleave
}: EdgeLayouted & EdgeEvents = $props();
}: { store: SvelteFlowStore } & EdgeLayouted & EdgeEvents = $props();
setContext('svelteflow__edge_id', id);
const { edgeLookup, edgeTypes, flowId, elementsSelectable } = useStore();
let edgeType = $derived(type ?? 'default');
let EdgeComponent = $derived($edgeTypes[edgeType] ?? BezierEdgeInternal);
let EdgeComponent = $derived(store.edgeTypes[edgeType] ?? BezierEdgeInternal);
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 isSelectable = $derived(selectable ?? $elementsSelectable);
let markerEndUrl = $derived(
markerEnd ? `url('#${getMarkerId(markerEnd, store.flowId)}')` : undefined
);
let isSelectable = $derived(selectable ?? store.elementsSelectable);
const handleEdgeSelect = useHandleEdgeSelect();
function onclick(event: MouseEvent | TouchEvent) {
const edge = $edgeLookup.get(id);
const edge = store.edgeLookup.get(id);
if (edge) {
handleEdgeSelect(id);
@@ -71,7 +73,7 @@
event: MouseEvent,
callback: ({ edge, event }: { edge: Edge; event: MouseEvent }) => void
) {
const edge = $edgeLookup.get(id);
const edge = store.edgeLookup.get(id);
if (edge) {
callback({ event, edge });

View File

@@ -39,28 +39,8 @@
isConnectableProp !== undefined ? isConnectableProp : isConnectableContext.value
);
const {
connectionMode,
domNode,
nodeLookup,
connectionRadius,
viewport,
isValidConnection: isValidConnectionStore,
lib,
addEdge,
onedgecreate,
panBy,
cancelConnection,
updateConnection,
autoPanOnConnect,
edges,
connectionLookup,
onconnect: onConnectAction,
onconnectstart: onConnectStartAction,
onconnectend: onConnectEndAction,
flowId,
connection
} = useStore();
let store = useStore();
let { viewport, edges } = store;
function onPointerDown(event: MouseEvent | TouchEvent) {
const isMouseTriggered = isMouseEvent(event);
@@ -70,39 +50,39 @@
handleId,
nodeId,
isTarget,
connectionRadius: $connectionRadius,
domNode: $domNode,
nodeLookup: $nodeLookup,
connectionMode: $connectionMode,
lib: $lib,
autoPanOnConnect: $autoPanOnConnect,
flowId: $flowId,
isValidConnection: isValidConnectionProp ?? $isValidConnectionStore,
updateConnection,
cancelConnection,
panBy,
connectionRadius: store.connectionRadius,
domNode: store.domNode,
nodeLookup: store.nodeLookup,
connectionMode: store.connectionMode,
lib: 'svelte',
autoPanOnConnect: store.autoPanOnConnect,
flowId: store.flowId,
isValidConnection: isValidConnectionProp ?? store.isValidConnection,
updateConnection: store.updateConnection,
cancelConnection: store.cancelConnection,
panBy: store.panBy,
onConnect: (connection) => {
const edge = $onedgecreate ? $onedgecreate(connection) : connection;
const edge = store.onedgecreate ? store.onedgecreate(connection) : connection;
if (!edge) {
return;
}
addEdge(edge);
$onConnectAction?.(connection);
store.addEdge(edge);
store.onconnect?.(connection);
},
onConnectStart: (event, startParams) => {
$onConnectStartAction?.(event, {
store.onconnectstart?.(event, {
nodeId: startParams.nodeId,
handleId: startParams.handleId,
handleType: startParams.handleType
});
},
onConnectEnd: (event, connectionState) => {
$onConnectEndAction?.(event, connectionState);
store.onconnectend?.(event, connectionState);
},
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
$edges;
if (onconnect || ondisconnect) {
let connections = $connectionLookup.get(`${nodeId}-${type}-${handleId}`);
let connections = store.connectionLookup.get(`${nodeId}-${type}-${handleId}`);
if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
const _connections = connections ?? new Map();
@@ -128,7 +108,7 @@
let [connectionInProcess, connectingFrom, connectingTo, isPossibleEndHandle, valid] = $derived.by(
() => {
const { fromHandle, toHandle, isValid } = $connection;
const { fromHandle, toHandle, isValid } = store.connection;
const connectionInProcess = !!fromHandle;
@@ -139,7 +119,7 @@
toHandle?.nodeId === nodeId && toHandle?.type === type && toHandle?.id === handleId;
const isPossibleEndHandle =
$connectionMode === ConnectionMode.Strict
store.connectionMode === ConnectionMode.Strict
? fromHandle?.type !== type
: 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-nodeid={nodeId}
data-handlepos={position}
data-id="{$flowId}-{nodeId}-{handleId}-{type}"
data-id="{store.flowId}-{nodeId}-{handleId}-{type}"
class={cc([
'svelte-flow__handle',
`svelte-flow__handle-${position}`,

View File

@@ -12,6 +12,7 @@
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types';
let {
store,
selectionKey = 'Shift',
multiSelectionKey = isMacOs() ? 'Meta' : 'Control',
deleteKey = 'Backspace',
@@ -19,18 +20,7 @@
zoomActivationKey = isMacOs() ? 'Meta' : 'Control'
}: KeyHandlerProps = $props();
const {
selectionKeyPressed,
multiselectionKeyPressed,
deleteKeyPressed,
panActivationKeyPressed,
zoomActivationKeyPressed,
selectionRect,
onbeforedelete,
ondelete,
nodes: _nodes,
edges: _edges
} = useStore();
const { nodes: _nodes, edges: _edges } = store;
function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject {
return key !== null && typeof key === 'object';
@@ -67,12 +57,12 @@
}
function resetKeysAndSelection() {
selectionRect.set(null);
selectionKeyPressed.set(false);
multiselectionKeyPressed.set(false);
deleteKeyPressed.set(false);
panActivationKeyPressed.set(false);
zoomActivationKeyPressed.set(false);
store.selectionRect = null;
store.selectionKeyPressed = false;
store.multiselectionKeyPressed = false;
store.deleteKeyPressed = false;
store.panActivationKeyPressed = false;
store.zoomActivationKeyPressed = false;
}
async function handleDelete() {
@@ -86,14 +76,14 @@
edgesToRemove: selectedEdges,
nodes,
edges,
onBeforeDelete: get(onbeforedelete)
onBeforeDelete: store.onbeforedelete
});
if (matchingNodes.length || matchingEdges.length) {
_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)));
get(ondelete)?.({
store.ondelete?.({
nodes: matchingNodes,
edges: matchingEdges
});
@@ -105,19 +95,19 @@
on:blur={resetKeysAndSelection}
on:contextmenu={resetKeysAndSelection}
use:shortcut={{
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(true)),
trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(false)),
trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(true)),
trigger: getShortcutTrigger(multiSelectionKey, () => (store.multiselectionKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(false)),
trigger: getShortcutTrigger(multiSelectionKey, () => (store.multiselectionKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
@@ -127,30 +117,30 @@
detail.originalEvent.metaKey ||
detail.originalEvent.shiftKey;
if (!isModifierKey && !isInputDOMNode(detail.originalEvent)) {
deleteKeyPressed.set(true);
store.deleteKeyPressed = true;
handleDelete();
}
}),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(deleteKey, () => deleteKeyPressed.set(false)),
trigger: getShortcutTrigger(deleteKey, () => (store.deleteKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(true)),
trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(false)),
trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(true)),
trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(false)),
trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = false)),
type: 'keyup'
}}
/>

View File

@@ -1,6 +1,8 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { KeyDefinition } from '$lib/types';
export type KeyHandlerProps = {
store: SvelteFlowStore;
selectionKey?: KeyDefinition | KeyDefinition[] | null;
multiSelectionKey?: KeyDefinition | KeyDefinition[] | null;
deleteKey?: KeyDefinition | KeyDefinition[] | null;

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import { getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system';
import { useStore } from '$lib/store';
import { Selection } from '$lib/components/Selection';
import drag from '$lib/actions/drag';
import type { NodeSelectionProps } from './types';
let {
store,
onnodedrag,
onnodedragstart,
onnodedragstop,
@@ -15,14 +15,13 @@
onselectioncontextmenu
}: NodeSelectionProps = $props();
const store = useStore();
const { selectionRectMode, nodes, nodeLookup } = store;
const { nodes } = store;
let bounds: Rect | null = $derived.by(() => {
if ($selectionRectMode === 'nodes') {
if (store.selectionRectMode === 'nodes') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
$nodes;
return getInternalNodesBounds($nodeLookup, { filter: (node) => !!node.selected });
return getInternalNodesBounds(store.nodeLookup, { filter: (node) => !!node.selected });
}
return null;
});
@@ -38,7 +37,7 @@
}
</script>
{#if $selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
{#if store.selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
<div
class="selection-wrapper nopan"
style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)"

View File

@@ -1,4 +1,5 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { NodeEvents, NodeSelectionEvents } from '$lib/types';
export type NodeSelectionProps = NodeSelectionEvents &
export type NodeSelectionProps = { store: SvelteFlowStore } & NodeSelectionEvents &
Pick<NodeEvents, 'onnodedrag' | 'onnodedragstart' | 'onnodedragstop'>;

View File

@@ -1,17 +1,16 @@
<script lang="ts">
import { setContext, onDestroy } from 'svelte';
import { get } from 'svelte/store';
import cc from 'classcat';
import { errorMessages, Position } from '@xyflow/system';
import drag from '$lib/actions/drag';
import { useStore } from '$lib/store';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import type { ConnectableContext, NodeWrapperProps } from './types';
import { getNodeInlineStyleDimensions } from './utils';
import type { NodeEvents } from '$lib/types';
let {
store,
node,
id,
data = {},
@@ -52,15 +51,6 @@
onnodecontextmenu
}: NodeWrapperProps & NodeEvents = $props();
const store = useStore();
const {
nodeTypes,
nodeDragThreshold,
selectNodesOnDrag,
handleNodeSelection,
updateNodeInternals
} = store;
let nodeRef: HTMLDivElement | null = $state(null);
let prevNodeRef: HTMLDivElement | null = null;
@@ -68,7 +58,7 @@
let prevSourcePosition: Position | undefined;
let prevTargetPosition: Position | undefined;
let NodeComponent = $derived($nodeTypes[type] ?? DefaultNode);
let NodeComponent = $derived(store.nodeTypes[type] ?? DefaultNode);
let connectableContext: ConnectableContext = {
get value() {
@@ -80,7 +70,7 @@
if (process.env.NODE_ENV === 'development') {
$effect(() => {
const valid = !!$nodeTypes[type];
const valid = !!store.nodeTypes[type];
if (!valid) {
console.warn('003', errorMessages['error003'](type!));
}
@@ -109,7 +99,7 @@
if (doUpdate && nodeRef !== null) {
requestAnimationFrame(() => {
if (nodeRef !== null) {
updateNodeInternals(
store.updateNodeInternals(
new Map([
[
id,
@@ -148,10 +138,10 @@
});
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
// here we only need to call it when selectNodesOnDrag=false
handleNodeSelection(id);
store.handleNodeSelection(id);
}
onnodeclick?.({ node: node.internals.userNode, event });
@@ -169,7 +159,7 @@
handleSelector: dragHandle,
noDragClass: 'nodrag',
nodeClickDistance,
onNodeMouseDown: handleNodeSelection,
onNodeMouseDown: store.handleNodeSelection,
onDrag: (event, _, targetNode, nodes) => {
onnodedrag?.({ event, targetNode, nodes });
},

View File

@@ -1,3 +1,4 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { InternalNode, Node } from '$lib/types';
export type ConnectableContext = {
@@ -39,4 +40,5 @@ export type NodeWrapperProps = Pick<
node: InternalNode;
initialized: boolean;
nodeClickDistance?: number;
store: SvelteFlowStore;
};

View File

@@ -3,29 +3,21 @@
import { createStore, key } from '$lib/store';
import type { SvelteFlowProviderProps } from './types';
import type { ProviderContext, SvelteFlowStore } from '$lib/store/types';
let {
initialNodes,
initialEdges,
initialWidth,
initialHeight,
nodeOrigin,
fitView,
children
}: SvelteFlowProviderProps = $props();
let { children }: SvelteFlowProviderProps = $props();
const store = createStore({
nodes: initialNodes,
edges: initialEdges,
width: initialWidth,
height: initialHeight,
nodeOrigin,
fitView
});
let store = $state.raw(createStore({ props: {} }));
setContext(key, {
getStore: () => store
});
provider: true,
getStore() {
return store;
},
setStore: (newStore: SvelteFlowStore) => {
store = newStore;
}
} satisfies ProviderContext);
onDestroy(() => {
store.reset();

View File

@@ -2,13 +2,13 @@
import { useStore } from '$lib/store';
import { Selection } from '$lib/components/Selection';
const { selectionRect, selectionRectMode } = useStore();
const store = useStore();
</script>
<Selection
isVisible={!!($selectionRect && $selectionRectMode === 'user')}
width={$selectionRect?.width}
height={$selectionRect?.height}
x={$selectionRect?.x}
y={$selectionRect?.y}
isVisible={!!(store.selectionRect && store.selectionRectMode === 'user')}
width={store.selectionRect?.width}
height={store.selectionRect?.height}
x={store.selectionRect?.x}
y={store.selectionRect?.y}
/>

View File

@@ -6,9 +6,9 @@
let { children }: { children?: Snippet } = $props();
const { domNode } = useStore();
const store = useStore();
</script>
<div use:portal={{ target: '.svelte-flow__viewport-portal', domNode: $domNode }}>
<div use:portal={{ target: '.svelte-flow__viewport-portal', domNode: store.domNode }}>
{@render children?.()}
</div>

View File

@@ -1,29 +1,59 @@
<script lang="ts">
import { onMount } from 'svelte';
import { EdgeWrapper } from '$lib/components/EdgeWrapper';
import { CallOnMount } from '$lib/components/CallOnMount';
import { MarkerDefinition } from '$lib/container/EdgeRenderer/MarkerDefinition';
import { useStore } from '$lib/store';
import type { DefaultEdgeOptions, EdgeEvents } from '$lib/types';
import type { EdgeEvents, EdgeLayouted } from '$lib/types';
import { getEdgePosition, getElevatedEdgeZIndex } from '@xyflow/system';
import type { SvelteFlowStore } from '$lib/store/types';
let {
defaultEdgeOptions,
store,
onedgeclick,
onedgecontextmenu,
onedgemouseenter,
onedgemouseleave
}: { defaultEdgeOptions?: DefaultEdgeOptions } & EdgeEvents = $props();
}: { store: SvelteFlowStore } & EdgeEvents = $props();
const {
visibleEdges,
edgesInitialized,
edges: { setDefaultOptions },
elementsSelectable
} = useStore();
const { edges, nodes } = store;
onMount(() => {
// TODO: Remove this when we have a better way to set default options
if (defaultEdgeOptions) setDefaultOptions(defaultEdgeOptions);
let layoutedEdges = $derived.by(() => {
$nodes;
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>
@@ -32,8 +62,9 @@
<MarkerDefinition />
</svg>
{#each $visibleEdges as edge (edge.id)}
{#each layoutedEdges as edge (edge.id)}
<EdgeWrapper
{store}
id={edge.id}
source={edge.source}
target={edge.target}
@@ -41,7 +72,7 @@
style={edge.style}
animated={edge.animated}
selected={edge.selected}
selectable={edge.selectable ?? $elementsSelectable}
selectable={edge.selectable ?? store.elementsSelectable}
deletable={edge.deletable}
hidden={edge.hidden}
label={edge.label}
@@ -68,13 +99,13 @@
/>
{/each}
{#if $visibleEdges.length > 0}
{#if layoutedEdges.length > 0}
<CallOnMount
onMount={() => {
$edgesInitialized = true;
store.edgesInitialized = true;
}}
onDestroy={() => {
$edgesInitialized = false;
store.edgesInitialized = false;
}}
/>
{/if}

View File

@@ -2,11 +2,11 @@
import { useStore } from '$lib/store';
import { Marker } from '$lib/container/EdgeRenderer/MarkerDefinition';
const { markers } = useStore();
const store = useStore();
</script>
<defs>
{#each $markers as marker (marker.id)}
{#each store.markers as marker (marker.id)}
<Marker {...marker} />
{/each}
</defs>

View File

@@ -4,9 +4,10 @@
import type { NodeEvents } from '$lib/types';
import { NodeWrapper } from '$lib/components/NodeWrapper';
import { useStore } from '$lib/store';
import type { SvelteFlowStore } from '$lib/store/types';
let {
store,
nodeClickDistance,
onnodeclick,
onnodecontextmenu,
@@ -16,16 +17,9 @@
onnodedrag,
onnodedragstart,
onnodedragstop
}: { nodeClickDistance?: number } & NodeEvents = $props();
}: { store: SvelteFlowStore; nodeClickDistance?: number } & NodeEvents = $props();
const {
visibleNodes,
nodesDraggable,
nodesConnectable,
elementsSelectable,
updateNodeInternals,
parentLookup
} = useStore();
const { nodes } = store;
const resizeObserver: ResizeObserver | null =
typeof ResizeObserver === 'undefined'
@@ -43,7 +37,7 @@
});
});
updateNodeInternals(updates);
store.updateNodeInternals(updates);
});
onDestroy(() => {
@@ -52,41 +46,46 @@
</script>
<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
{node}
{store}
node={internalNode!}
id={node.id}
data={node.data}
selected={!!node.selected}
hidden={!!node.hidden}
draggable={!!(node.draggable || ($nodesDraggable && typeof node.draggable === 'undefined'))}
draggable={!!(
node.draggable ||
(store.nodesDraggable && typeof node.draggable === 'undefined')
)}
selectable={!!(
node.selectable ||
($elementsSelectable && typeof node.selectable === 'undefined')
(store.elementsSelectable && typeof node.selectable === 'undefined')
)}
connectable={!!(
node.connectable ||
($nodesConnectable && typeof node.connectable === 'undefined')
(store.nodesConnectable && typeof node.connectable === 'undefined')
)}
deletable={node.deletable ?? true}
positionX={node.internals.positionAbsolute.x}
positionY={node.internals.positionAbsolute.y}
isParent={$parentLookup.has(node.id)}
positionX={internalNode!.internals.positionAbsolute.x}
positionY={internalNode!.internals.positionAbsolute.y}
isParent={store.parentLookup.has(node.id)}
style={node.style}
class={node.class}
type={node.type ?? 'default'}
sourcePosition={node.sourcePosition}
targetPosition={node.targetPosition}
dragging={node.dragging}
zIndex={node.internals.z ?? 0}
zIndex={internalNode!.internals.z ?? 0}
dragHandle={node.dragHandle}
initialized={nodeHasDimensions(node)}
width={node.width}
height={node.height}
initialWidth={node.initialWidth}
initialHeight={node.initialHeight}
measuredWidth={node.measured.width}
measuredHeight={node.measured.height}
measuredWidth={node.measured?.width ?? 0}
measuredHeight={node.measured?.height ?? 0}
parentId={node.parentId}
{resizeObserver}
{nodeClickDistance}

View File

@@ -37,39 +37,29 @@
import type { Node, Edge, InternalNode } from '$lib/types';
import type { PaneProps } from './types';
let {
store,
panOnDrag = true,
selectionOnDrag,
onpaneclick,
onpanecontextmenu,
nodes,
edges,
children
}: PaneProps = $props();
const {
nodeLookup,
viewport,
dragging,
elementsSelectable,
selectionRect,
selectionRectMode,
selectionKeyPressed,
selectionMode,
panActivationKeyPressed,
unselectNodesAndEdges
} = useStore();
const { viewport, nodes, edges } = store;
// svelte-ignore non_reactive_update
let container: HTMLDivElement;
let containerBounds: DOMRect | null = null;
let selectedNodes: InternalNode[] = [];
let panOnDragActive = $derived($panActivationKeyPressed || panOnDrag);
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
let isSelecting = $derived(
$selectionKeyPressed || $selectionRect || (selectionOnDrag && panOnDragActive !== true)
store.selectionKeyPressed ||
store.selectionRect ||
(selectionOnDrag && panOnDragActive !== true)
);
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
@@ -83,15 +73,15 @@
}
onpaneclick?.({ event });
unselectNodesAndEdges();
selectionRectMode.set(null);
store.unselectNodesAndEdges();
store.selectionRectMode = null;
}
function onPointerDown(event: PointerEvent) {
containerBounds = container.getBoundingClientRect();
if (
!elementsSelectable ||
!store.elementsSelectable ||
!isSelecting ||
event.button !== 0 ||
event.target !== container ||
@@ -104,32 +94,32 @@
const { x, y } = getEventPosition(event, containerBounds);
unselectNodesAndEdges();
store.unselectNodesAndEdges();
selectionRect.set({
store.selectionRect = {
width: 0,
height: 0,
startX: x,
startY: y,
x,
y
});
};
// onSelectionStart?.(event);
}
function onPointerMove(event: PointerEvent) {
if (!isSelecting || !containerBounds || !$selectionRect) {
if (!isSelecting || !containerBounds || !store.selectionRect) {
return;
}
selectionInProgress = true;
const mousePos = getEventPosition(event, containerBounds);
const startX = $selectionRect.startX ?? 0;
const startY = $selectionRect.startY ?? 0;
const startX = store.selectionRect.startX ?? 0;
const startY = store.selectionRect.startY ?? 0;
const nextUserSelectRect = {
...$selectionRect,
...store.selectionRect,
x: mousePos.x < startX ? mousePos.x : startX,
y: mousePos.y < startY ? mousePos.y : startY,
width: Math.abs(mousePos.x - startX),
@@ -139,10 +129,10 @@
const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
selectedNodes = getNodesInside(
$nodeLookup,
store.nodeLookup,
nextUserSelectRect,
[$viewport.x, $viewport.y, $viewport.zoom],
$selectionMode === SelectionMode.Partial,
store.selectionMode === SelectionMode.Partial,
true
);
const selectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
@@ -163,8 +153,8 @@
edges.update((edges) => edges.map(toggleSelected(selectedEdgeIds)));
}
selectionRectMode.set('user');
selectionRect.set(nextUserSelectRect);
store.selectionRectMode = 'user';
store.selectionRect = nextUserSelectRect;
}
function onPointerUp(event: PointerEvent) {
@@ -176,18 +166,18 @@
// We only want to trigger click functions when in selection mode if
// the user did not move the mouse.
if (!isSelecting && $selectionRectMode === 'user' && event.target === container) {
if (!isSelecting && store.selectionRectMode === 'user' && event.target === container) {
onClick?.(event);
}
selectionRect.set(null);
store.selectionRect = null;
if (selectedNodes.length > 0) {
$selectionRectMode = 'nodes';
store.selectionRectMode = 'nodes';
}
// If the user kept holding the selectionKey during the selection,
// we need to reset the selectionInProgress, so the next click event is not prevented
if ($selectionKeyPressed) {
if (store.selectionKeyPressed) {
selectionInProgress = false;
}
@@ -210,7 +200,7 @@
bind:this={container}
class="svelte-flow__pane"
class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))}
class:dragging={$dragging}
class:dragging={store.dragging}
class:selection={isSelecting}
onclick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
onpointerdown={hasActiveSelection ? onPointerDown : undefined}

View File

@@ -1,11 +1,10 @@
import type { Snippet } from 'svelte';
import type { Writable } from 'svelte/store';
import type { Edge, Node, PaneEvents } from '$lib/types';
import type { PaneEvents } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types';
export type PaneProps = {
store: SvelteFlowStore;
panOnDrag?: boolean | number[];
selectionOnDrag?: boolean;
nodes: Writable<Node[]>;
edges: Writable<Edge[]>;
children: Snippet;
} & PaneEvents;

View File

@@ -11,7 +11,7 @@
...restProps
}: PanelProps = $props();
const { selectionRectMode } = useStore();
const store = useStore();
let positionClasses = $derived(`${position}`.split('-'));
</script>
@@ -19,7 +19,7 @@
<div
class={cc(['svelte-flow__panel', className, ...positionClasses])}
{style}
style:pointer-events={$selectionRectMode ? 'none' : ''}
style:pointer-events={store.selectionRectMode ? 'none' : ''}
{...restProps}
>
{@render children?.()}

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { onMount, hasContext } from 'svelte';
import { get } from 'svelte/store';
import { onMount, getContext, setContext } from 'svelte';
import cc from 'classcat';
import { ConnectionMode, PanOnScrollMode } from '@xyflow/system';
import { PanOnScrollMode } from '@xyflow/system';
import { Zoom } from '$lib/container/Zoom';
import { Pane } from '$lib/container/Pane';
@@ -14,78 +14,28 @@
import { KeyHandler } from '$lib/components/KeyHandler';
import { ConnectionLine } from '$lib/components/ConnectionLine';
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 { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
import { useColorModeClass } from '$lib/hooks/useColorModeClass';
import { type ProviderContext, type ContainerSignals, type StoreContext } from '$lib/store/types';
let {
id = '1',
nodes,
edges,
fitView,
fitViewOptions,
minZoom,
maxZoom,
initialViewport,
viewport,
nodeTypes,
edgeTypes,
style,
class: className,
width,
height,
proOptions,
selectionKey,
selectionMode,
deleteKey,
panActivationKey,
multiSelectionKey,
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,
nodeClickDistance = 1,
defaultMarkerColor = '#b1b1b7',
style,
class: className,
onMoveStart,
onMoveEnd,
onMove,
connectionLine,
children,
onnodeclick,
onnodecontextmenu,
onnodedrag,
@@ -102,135 +52,74 @@
onedgemouseleave,
onpaneclick,
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();
let domNode = $state<HTMLDivElement>();
let clientWidth = $state<number>();
let clientHeight = $state<number>();
// svelte-ignore perf_avoid_inline_class
const container: ContainerSignals = new (class {
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)
? useStore()
: createStoreContext({
nodes: get(nodes),
edges: get(edges),
width,
height,
fitView,
nodeOrigin,
nodeExtent
});
const providerContext = getContext<ProviderContext>(key);
if (providerContext) {
providerContext.setStore(store);
}
setContext(key, {
provider: false,
getStore() {
return store;
}
} satisfies StoreContext);
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 () => {
store.reset();
};
});
// Update width & height on resize
$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));
let colorModeClass = $derived(useColorModeClass(store.colorMode));
</script>
<div
bind:this={domNode}
bind:clientWidth
bind:clientHeight
bind:this={container.domNode}
bind:clientWidth={container.width}
bind:clientHeight={container.height}
style:width
style:height
{style}
class={cc(['svelte-flow', className, $colorModeClass])}
data-testid="svelte-flow__wrapper"
{...rest}
{...props}
role="application"
>
<KeyHandler
{store}
{selectionKey}
{deleteKey}
{panActivationKey}
@@ -238,7 +127,8 @@
{zoomActivationKey}
/>
<Zoom
initialViewport={initViewport}
{store}
{initialViewport}
{onMoveStart}
{onMove}
{onMoveEnd}
@@ -251,16 +141,17 @@
{panOnDrag}
{paneClickDistance}
>
<Pane {nodes} {edges} {onpaneclick} {onpanecontextmenu} {panOnDrag} {selectionOnDrag}>
<ViewportComponent>
<Pane {store} {onpaneclick} {onpanecontextmenu} {panOnDrag} {selectionOnDrag}>
<ViewportComponent viewport={$viewport}>
<EdgeRenderer
{store}
{onedgeclick}
{onedgecontextmenu}
{onedgemouseenter}
{onedgemouseleave}
{defaultEdgeOptions}
/>
<ConnectionLine
{store}
containerStyle={connectionLineContainerStyle}
style={connectionLineStyle}
{connectionLine}
@@ -268,6 +159,7 @@
<div class="svelte-flow__edgelabel-renderer"></div>
<div class="svelte-flow__viewport-portal"></div>
<NodeRenderer
{store}
{nodeClickDistance}
{onnodeclick}
{onnodecontextmenu}
@@ -279,6 +171,7 @@
{onnodedragstop}
/>
<NodeSelection
{store}
{onselectionclick}
{onselectioncontextmenu}
{onnodedrag}

View File

@@ -188,6 +188,10 @@ export type SvelteFlowProps = NodeEvents &
* @default 'full'
*/
selectionMode?: SelectionMode;
/**
* Controls if nodes should be automatically selected when being dragged
*/
selectNodesOnDrag: boolean;
/** Grid all nodes will snap to
* @example [20, 20]
*/

View File

@@ -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);
}
});
}

View File

@@ -1,15 +1,13 @@
<script lang="ts">
import { useStore } from '$lib/store';
import type { Viewport } from '@xyflow/system';
import type { Snippet } from 'svelte';
const { viewport } = useStore();
let { children }: { children: Snippet } = $props();
let { viewport, children }: { viewport: Viewport; children: Snippet } = $props();
</script>
<div
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()}
</div>

View File

@@ -1,12 +1,13 @@
<script lang="ts">
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 zoom from '$lib/actions/zoom';
import type { ZoomProps } from './types';
let {
store,
initialViewport = { x: 0, y: 0, zoom: 1 },
onMoveStart,
onMove,
@@ -22,40 +23,30 @@
children
}: ZoomProps = $props();
const {
viewport,
panZoom,
selectionRect,
minZoom,
maxZoom,
dragging,
translateExtent,
lib,
panActivationKeyPressed,
zoomActivationKeyPressed,
viewportInitialized
} = useStore();
let panOnDragActive = $derived($panActivationKeyPressed || panOnDrag);
let panOnScrollActive = $derived($panActivationKeyPressed || panOnScroll);
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
let panOnScrollActive = $derived(store.panActivationKeyPressed || panOnScroll);
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(() => {
$viewportInitialized = true;
store.viewportInitialized = true;
});
</script>
<div
class="svelte-flow__zoom"
use:zoom={{
viewport,
minZoom: $minZoom,
maxZoom: $maxZoom,
viewport: store.viewport,
minZoom: store.minZoom,
maxZoom: store.maxZoom,
initialViewport,
dragging,
panZoom,
onDraggingChange: (dragging: boolean) => {
store.dragging = dragging;
},
setPanZoomInstance: (instance: PanZoomInstance) => {
store.panZoom = instance;
},
onPanZoomStart: onMoveStart,
onPanZoom: onMove,
onPanZoomEnd: onMoveEnd,
@@ -66,13 +57,13 @@
panOnDrag: panOnDragActive,
panOnScrollSpeed: 0.5,
panOnScrollMode: panOnScrollMode || PanOnScrollMode.Free,
zoomActivationKeyPressed: $zoomActivationKeyPressed,
zoomActivationKeyPressed: store.zoomActivationKeyPressed,
preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true,
noPanClassName: 'nopan',
noWheelClassName: 'nowheel',
userSelectionActive: !!$selectionRect,
translateExtent: $translateExtent,
lib: $lib,
userSelectionActive: !!store.selectionRect,
translateExtent: store.translateExtent,
lib: 'svelte',
paneClickDistance,
onTransformChange
}}

View File

@@ -1,7 +1,9 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode, Viewport } from '@xyflow/system';
import type { Snippet } from 'svelte';
export type ZoomProps = {
store: SvelteFlowStore;
initialViewport?: Viewport;
panOnScrollMode: PanOnScrollMode;
onMove?: OnMove;

View File

@@ -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.`
);
}
}

View File

@@ -0,0 +1 @@
export * from './useSvelteFlow.svelte';

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();
}

View File

@@ -22,7 +22,8 @@ import {
import { useStore } from '$lib/store';
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.
@@ -232,6 +233,21 @@ export function useSvelteFlow(): {
*
* @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 };
/**
* Returns the bounds of the given nodes or node ids.
@@ -258,37 +274,22 @@ export function useSvelteFlow(): {
id?: string | null;
}) => HandleConnection[];
} {
const {
zoomIn,
zoomOut,
fitView,
onbeforedelete,
snapGrid,
viewport,
width,
height,
minZoom,
maxZoom,
panZoom,
nodes,
edges,
domNode,
nodeLookup,
nodeOrigin,
edgeLookup,
connectionLookup
} = useStore();
if (process.env.NODE_ENV === 'development') {
derivedWarning('useSvelteFlow');
}
const store = useStore();
const { viewport, nodes, edges } = store;
const getNodeRect = (node: Node | { id: Node['id'] }): Rect | null => {
const $nodeLookup = get(nodeLookup);
const nodeToUse = isNode(node) ? node : $nodeLookup.get(node.id)!;
const nodeToUse = isNode(node) ? node : store.nodeLookup.get(node.id)!;
const position = nodeToUse.parentId
? evaluateAbsolutePosition(
nodeToUse.position,
nodeToUse.measured,
nodeToUse.parentId,
$nodeLookup,
get(nodeOrigin)
store.nodeLookup,
store.nodeOrigin
)
: nodeToUse.position;
@@ -308,7 +309,7 @@ export function useSvelteFlow(): {
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
options: { replace: boolean } = { replace: false }
) => {
const node = get(nodeLookup).get(id)?.internals.userNode;
const node = store.nodeLookup.get(id)?.internals.userNode;
if (!node) {
return;
@@ -328,36 +329,64 @@ export function useSvelteFlow(): {
);
} else {
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 {
zoomIn,
zoomOut,
zoomIn: store.zoomIn,
zoomOut: store.zoomOut,
getInternalNode,
getNode: (id) => getInternalNode(id)?.internals.userNode,
getNodes: (ids) => (ids === undefined ? get(nodes) : getElements(get(nodeLookup), ids)),
getEdge: (id) => get(edgeLookup).get(id),
getEdges: (ids) => (ids === undefined ? get(edges) : getElements(get(edgeLookup), ids)),
getNodes: (ids) => (ids === undefined ? get(nodes) : getElements(store.nodeLookup, ids)),
getEdge: (id) => store.edgeLookup.get(id),
getEdges: (ids) => (ids === undefined ? get(edges) : getElements(store.edgeLookup, ids)),
setZoom: (zoomLevel, options) => {
const currentPanZoom = get(panZoom);
return currentPanZoom
? currentPanZoom.scaleTo(zoomLevel, { duration: options?.duration })
const panZoom = store.panZoom;
return panZoom
? panZoom.scaleTo(zoomLevel, { duration: options?.duration })
: Promise.resolve(false);
},
getZoom: () => get(viewport).zoom,
setViewport: async (nextViewport, options) => {
const currentViewport = get(viewport);
const currentPanZoom = get(panZoom);
if (!currentPanZoom) {
if (!store.panZoom) {
return Promise.resolve(false);
}
await currentPanZoom.setViewport(
await store.panZoom.setViewport(
{
x: nextViewport.x ?? currentViewport.x,
y: nextViewport.y ?? currentViewport.y,
@@ -370,8 +399,8 @@ export function useSvelteFlow(): {
},
getViewport: () => get(viewport),
setCenter: async (x, y, options) => {
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : get(maxZoom);
const currentPanZoom = get(panZoom);
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
const currentPanZoom = store.panZoom;
if (!currentPanZoom) {
return Promise.resolve(false);
@@ -379,8 +408,8 @@ export function useSvelteFlow(): {
await currentPanZoom.setViewport(
{
x: get(width) / 2 - x * nextZoom,
y: get(height) / 2 - y * nextZoom,
x: store.width / 2 - x * nextZoom,
y: store.height / 2 - y * nextZoom,
zoom: nextZoom
},
{ duration: options?.duration }
@@ -388,24 +417,22 @@ export function useSvelteFlow(): {
return Promise.resolve(true);
},
fitView,
fitView: store.fitView,
fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => {
const currentPanZoom = get(panZoom);
if (!currentPanZoom) {
if (!store.panZoom) {
return Promise.resolve(false);
}
const viewport = getViewportForBounds(
bounds,
get(width),
get(height),
get(minZoom),
get(maxZoom),
store.width,
store.height,
store.minZoom,
store.maxZoom,
options?.padding ?? 0.1
);
await currentPanZoom.setViewport(viewport, { duration: options?.duration });
await store.panZoom.setViewport(viewport, { duration: options?.duration });
return Promise.resolve(true);
},
@@ -422,7 +449,7 @@ export function useSvelteFlow(): {
}
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)) {
return false;
}
@@ -457,7 +484,7 @@ export function useSvelteFlow(): {
edgesToRemove,
nodes: get(nodes),
edges: get(edges),
onBeforeDelete: get(onbeforedelete)
onBeforeDelete: store.onbeforedelete
});
if (matchingNodes) {
@@ -481,15 +508,13 @@ export function useSvelteFlow(): {
position: XYPosition,
options: { snapToGrid: boolean } = { snapToGrid: true }
) => {
const _domNode = get(domNode);
if (!_domNode) {
if (!store.domNode) {
return position;
}
const _snapGrid = options.snapToGrid ? get(snapGrid) : false;
const _snapGrid = options.snapToGrid ? store.snapGrid : false;
const { x, y, zoom } = get(viewport);
const { x: domX, y: domY } = _domNode.getBoundingClientRect();
const { x: domX, y: domY } = store.domNode.getBoundingClientRect();
const correctedPosition = {
x: position.x - domX,
y: position.y - domY
@@ -508,14 +533,12 @@ export function useSvelteFlow(): {
* @returns
*/
flowToScreenPosition: (position: XYPosition) => {
const _domNode = get(domNode);
if (!_domNode) {
if (!store.domNode) {
return position;
}
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]);
return {
@@ -539,7 +562,7 @@ export function useSvelteFlow(): {
},
updateNode,
updateNodeData: (id, dataUpdate, options) => {
const node = get(nodeLookup).get(id)?.internals.userNode;
const node = store.nodeLookup.get(id)?.internals.userNode;
if (!node) {
return;
@@ -551,18 +574,12 @@ export function useSvelteFlow(): {
nodes.update((nds) => nds);
},
updateEdge,
getNodesBounds: (nodes) => {
const _nodeLookup = get(nodeLookup);
const _nodeOrigin = get(nodeOrigin);
return getNodesBounds(nodes, { nodeLookup: _nodeLookup, nodeOrigin: _nodeOrigin });
return getNodesBounds(nodes, { nodeLookup: store.nodeLookup, nodeOrigin: store.nodeOrigin });
},
getHandleConnections: ({ type, id, nodeId }) =>
Array.from(
get(connectionLookup)
.get(`${nodeId}-${type}-${id ?? null}`)
?.values() ?? []
),
Array.from(store.connectionLookup.get(`${nodeId}-${type}-${id ?? null}`)?.values() ?? []),
viewport
};
}

View File

@@ -26,7 +26,7 @@ export { useStore } from '$lib/store';
export * from '$lib/utils';
//hooks
export * from '$lib/hooks/useSvelteFlow';
export * from '$lib/hooks/useSvelteFlow.svelte';
export * from '$lib/hooks/useUpdateNodeInternals';
export * from '$lib/hooks/useConnection';
export * from '$lib/hooks/useNodesEdges';

View File

@@ -27,13 +27,17 @@
class: className
}: BackgroundProps = $props();
const { viewport, flowId } = useStore();
const store = useStore();
const { viewport } = store;
$inspect($viewport);
const patternSize = size ?? defaultSize[variant!];
const isDots = variant === BackgroundVariant.Dots;
const isCross = variant === BackgroundVariant.Cross;
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 scaledSize = $derived(patternSize * $viewport.zoom);
let patternDimensions = $derived(

View File

@@ -32,17 +32,8 @@
after
}: ControlsProps = $props();
const {
viewport,
minZoom,
maxZoom,
nodesDraggable,
nodesConnectable,
elementsSelectable,
zoomIn,
zoomOut,
fitView
} = useStore();
const store = useStore();
const { viewport } = store;
const buttonProps = {
bgColor: buttonBgColor,
@@ -52,29 +43,31 @@
borderColor: buttonBorderColor
};
let isInteractive = $state($nodesDraggable || $nodesConnectable || $elementsSelectable);
let minZoomReached = $derived($viewport.zoom <= $minZoom);
let maxZoomReached = $derived($viewport.zoom >= $maxZoom);
let isInteractive = $state(
store.nodesDraggable || store.nodesConnectable || store.elementsSelectable
);
let minZoomReached = $derived($viewport.zoom <= store.minZoom);
let maxZoomReached = $derived($viewport.zoom >= store.maxZoom);
let orientationClass = $derived(orientation === 'horizontal' ? 'horizontal' : 'vertical');
const onZoomInHandler = () => {
zoomIn();
store.zoomIn();
};
const onZoomOutHandler = () => {
zoomOut();
store.zoomOut();
};
const onFitViewHandler = () => {
fitView(fitViewOptions);
store.fitView(fitViewOptions);
};
const onToggleInteractivity = () => {
isInteractive = !isInteractive;
nodesDraggable.set(isInteractive);
nodesConnectable.set(isInteractive);
elementsSelectable.set(isInteractive);
store.nodesDraggable = isInteractive;
store.nodesConnectable = isInteractive;
store.elementsSelectable = isInteractive;
};
</script>

View File

@@ -42,16 +42,10 @@
class: className
}: MiniMapProps = $props();
const {
nodes,
nodeLookup,
viewport,
width: containerWidth,
height: containerHeight,
flowId,
panZoom,
translateExtent
} = useStore();
let store = useStore();
const { viewport } = store;
let nodes = $derived(store.nodes);
const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
@@ -59,17 +53,20 @@
const shapeRendering =
// @ts-expect-error - TS doesn't know about chrome
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
let viewBB = $derived({
x: -$viewport.x / $viewport.zoom,
y: -$viewport.y / $viewport.zoom,
width: $containerWidth / $viewport.zoom,
height: $containerHeight / $viewport.zoom
width: store.width / $viewport.zoom,
height: store.height / $viewport.zoom
});
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 scaledHeight = $derived(boundingRect.height / height);
@@ -91,7 +88,7 @@
class={cc(['svelte-flow__minimap', className])}
data-testid="svelte-flow__minimap"
>
{#if $panZoom}
{#if store.panZoom}
<svg
{width}
{height}
@@ -105,12 +102,12 @@
? maskStrokeWidth * viewScale
: undefined}
use:interactive={{
panZoom: $panZoom,
viewport,
panZoom: store.panZoom,
viewport: store.viewport,
getViewScale,
translateExtent: $translateExtent,
width: $containerWidth,
height: $containerHeight,
translateExtent: store.translateExtent,
width: store.width,
height: store.height,
inversePan,
zoomStep,
pannable,
@@ -120,7 +117,7 @@
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
{#each $nodes as userNode (userNode.id)}
{@const node = $nodeLookup.get(userNode.id)}
{@const node = store.nodeLookup.get(userNode.id)}
{#if node && nodeHasDimensions(node)}
{@const nodeDimesions = getNodeDimensions(node)}
<MinimapNode

View File

@@ -4,7 +4,7 @@
import portal from '$lib/actions/portal';
import { useStore } from '$lib/store';
import { useSvelteFlow } from '$lib/hooks/useSvelteFlow';
import { useSvelteFlow } from '$lib/hooks';
import type { InternalNode } from '$lib/types';
import type { NodeToolbarProps } from './types';

View File

@@ -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();
}

View File

@@ -1,14 +1,11 @@
import { getContext, setContext } from 'svelte';
import { derived, get, writable } from 'svelte/store';
import { get } from 'svelte/store';
import {
createMarkerIds,
fitView as fitViewSystem,
panBy as panBySystem,
updateNodeInternals as updateNodeInternalsSystem,
addEdge as addEdgeUtil,
initialConnection,
errorMessages,
pointToRendererPoint,
type UpdateNodePositions,
type InternalNodeUpdate,
type ViewportHelperFunctionOptions,
@@ -17,60 +14,37 @@ import {
type CoordinateExtent,
type UpdateConnection,
type ConnectionState,
type NodeOrigin,
getFitViewNodes,
updateAbsolutePositions,
getDimensions
} from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
import type { SvelteFlowStore } from './types';
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store.svelte';
import { type StoreSignals, type SvelteFlowStore, type SvelteFlowStoreActions } from './types';
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
import { getVisibleEdges } from './visible-edges';
import { getVisibleNodes } from './visible-nodes';
// import { getVisibleEdges } from './visible-edges';
// import { getVisibleNodes } from './visible-nodes';
export const key = Symbol();
export function createStore({
nodes,
edges,
width,
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
});
export { useStore } from './hook.svelte';
export function createStore(signals: StoreSignals): SvelteFlowStore {
const store = getInitialStore(signals);
function setNodeTypes(nodeTypes: NodeTypes) {
store.nodeTypes.set({
store.nodeTypes = {
...initialNodeTypes,
...nodeTypes
});
};
}
function setEdgeTypes(edgeTypes: EdgeTypes) {
store.edgeTypes.set({
store.edgeTypes = {
...initialEdgeTypes,
...edgeTypes
});
};
}
function addEdge(edgeParams: Edge | Connection) {
@@ -79,10 +53,8 @@ export function createStore({
}
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
const nodeLookup = get(store.nodeLookup);
for (const [id, dragItem] of nodeDragItems) {
const node = nodeLookup.get(id)?.internals.userNode;
const node = store.nodeLookup.get(id)?.internals.userNode;
if (!node) {
continue;
@@ -96,33 +68,33 @@ export function createStore({
};
function updateNodeInternals(updates: Map<string, InternalNodeUpdate>) {
const nodeLookup = get(store.nodeLookup);
const parentLookup = get(store.parentLookup);
const { changes, updatedInternals } = updateNodeInternalsSystem(
updates,
nodeLookup,
get(store.parentLookup),
get(store.domNode),
get(store.nodeOrigin)
store.nodeLookup,
store.parentLookup,
store.domNode,
store.nodeOrigin
);
if (!updatedInternals) {
return;
}
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent });
updateAbsolutePositions(store.nodeLookup, store.parentLookup, {
nodeOrigin: store.nodeOrigin,
nodeExtent: store.nodeExtent
});
if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) {
const fitViewOptions = get(store.fitViewOptions);
if (!store.fitViewOnInitDone && store.fitViewOnInit) {
const fitViewOnInitDone = fitViewSync({
...fitViewOptions,
nodes: fitViewOptions?.nodes
...store.fitViewOptions,
nodes: store.fitViewOptions?.nodes
});
store.fitViewOnInitDone.set(fitViewOnInitDone);
store.fitViewOnInitDone = fitViewOnInitDone;
}
for (const change of changes) {
const node = nodeLookup.get(change.id)?.internals.userNode;
const node = store.nodeLookup.get(change.id)?.internals.userNode;
if (!node) {
continue;
@@ -149,14 +121,14 @@ export function createStore({
store.nodes.update((nds) => nds);
if (!get(store.nodesInitialized)) {
store.nodesInitialized.set(true);
if (!store.nodesInitialized) {
store.nodesInitialized = true;
}
}
function fitView(options?: FitViewOptions) {
const panZoom = get(store.panZoom);
const domNode = get(store.domNode);
const panZoom = store.panZoom;
const domNode = store.domNode;
if (!panZoom || !domNode) {
return Promise.resolve(false);
@@ -164,15 +136,15 @@ export function createStore({
const { width, height } = getDimensions(domNode);
const fitViewNodes = getFitViewNodes(get(store.nodeLookup), options);
const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
return fitViewSystem(
{
nodes: fitViewNodes,
width,
height,
minZoom: get(store.minZoom),
maxZoom: get(store.maxZoom),
minZoom: store.minZoom,
maxZoom: store.maxZoom,
panZoom
},
options
@@ -180,21 +152,21 @@ export function createStore({
}
function fitViewSync(options?: FitViewOptions) {
const panZoom = get(store.panZoom);
const panZoom = store.panZoom;
if (!panZoom) {
return false;
}
const fitViewNodes = getFitViewNodes(get(store.nodeLookup), options);
const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
fitViewSystem(
{
nodes: fitViewNodes,
width: get(store.width),
height: get(store.height),
minZoom: get(store.minZoom),
maxZoom: get(store.maxZoom),
width: store.width,
height: store.height,
minZoom: store.minZoom,
maxZoom: store.maxZoom,
panZoom
},
options
@@ -204,7 +176,7 @@ export function createStore({
}
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
const panZoom = get(store.panZoom);
const panZoom = store.panZoom;
if (!panZoom) {
return Promise.resolve(false);
}
@@ -221,29 +193,29 @@ export function createStore({
}
function setMinZoom(minZoom: number) {
const panZoom = get(store.panZoom);
const panZoom = store.panZoom;
if (panZoom) {
panZoom.setScaleExtent([minZoom, get(store.maxZoom)]);
store.minZoom.set(minZoom);
panZoom.setScaleExtent([minZoom, store.maxZoom]);
store.minZoom = minZoom;
}
}
function setMaxZoom(maxZoom: number) {
const panZoom = get(store.panZoom);
const panZoom = store.panZoom;
if (panZoom) {
panZoom.setScaleExtent([get(store.minZoom), maxZoom]);
store.maxZoom.set(maxZoom);
panZoom.setScaleExtent([store.minZoom, maxZoom]);
store.maxZoom = maxZoom;
}
}
function setTranslateExtent(extent: CoordinateExtent) {
const panZoom = get(store.panZoom);
const panZoom = store.panZoom;
if (panZoom) {
panZoom.setTranslateExtent(extent);
store.translateExtent.set(extent);
store.translateExtent = extent;
}
}
@@ -259,7 +231,7 @@ export function createStore({
}
function setPaneClickDistance(distance: number) {
get(store.panZoom)?.setClickDistance(distance);
store.panZoom?.setClickDistance(distance);
}
function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) {
@@ -270,39 +242,8 @@ export function createStore({
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[]) {
const isMultiSelection = get(store.multiselectionKeyPressed);
const isMultiSelection = store.multiselectionKeyPressed;
store.nodes.update((ns) =>
ns.map((node) => {
@@ -329,7 +270,7 @@ export function createStore({
}
function addSelectedEdges(ids: string[]) {
const isMultiSelection = get(store.multiselectionKeyPressed);
const isMultiSelection = store.multiselectionKeyPressed;
store.edges.update((edges) =>
edges.map((edge) => {
@@ -362,12 +303,12 @@ export function createStore({
return;
}
store.selectionRect.set(null);
store.selectionRectMode.set(null);
store.selectionRect = null;
store.selectionRectMode = null;
if (!node.selected) {
addSelectedNodes([id]);
} else if (node.selected && get(store.multiselectionKeyPressed)) {
} else if (node.selected && store.multiselectionKeyPressed) {
unselectNodesAndEdges({ nodes: [node], edges: [] });
}
}
@@ -376,78 +317,33 @@ export function createStore({
const viewport = get(store.viewport);
return panBySystem({
delta,
panZoom: get(store.panZoom),
panZoom: store.panZoom,
transform: [viewport.x, viewport.y, viewport.zoom],
translateExtent: get(store.translateExtent),
width: get(store.width),
height: get(store.height)
translateExtent: store.translateExtent,
width: store.width,
height: store.height
});
}
const _connection = writable<ConnectionState>(initialConnection);
// const _connection = writable<ConnectionState>(initialConnection);
const updateConnection: UpdateConnection = (newConnection: ConnectionState) => {
_connection.set({ ...newConnection });
store.rawConnection = { ...newConnection };
};
function cancelConnection() {
_connection.set(initialConnection);
store.rawConnection = initialConnection;
}
function reset() {
store.fitViewOnInitDone.set(false);
store.selectionRect.set(null);
store.selectionRectMode.set(null);
store.snapGrid.set(null);
store.isValidConnection.set(() => true);
store.fitViewOnInitDone = false;
store.selectionRect = null;
store.selectionRectMode = null;
unselectNodesAndEdges();
cancelConnection();
}
return {
// 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
const storeWithActions = Object.assign(store, {
syncNodeStores: (nodes) => syncNodeStores(store.nodes, nodes),
syncEdgeStores: (edges) => syncEdgeStores(store.edges, edges),
syncViewport: (viewport) => syncViewportStores(store.panZoom, store.viewport, viewport),
@@ -471,43 +367,76 @@ export function createStore({
updateConnection,
cancelConnection,
reset
};
}
export function useStore(): SvelteFlowStore {
const store = getContext<{ getStore: () => SvelteFlowStore }>(key);
if (!store) {
throw new Error(
'In order to use useStore you need to wrap your component in a <SvelteFlowProvider />'
);
}
return store.getStore();
}
export function createStoreContext({
nodes,
edges,
width,
height,
fitView,
nodeOrigin,
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, {
getStore: () => store
});
return store;
} satisfies SvelteFlowStoreActions);
return storeWithActions;
// return {
// // 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),
// 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
// };
}

View File

@@ -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)
// };
};

View File

@@ -10,8 +10,9 @@ import type {
Viewport
} 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 { SvelteFlowProps } from '$lib/container/SvelteFlow';
export type SvelteFlowStoreActions = {
syncNodeStores: (nodesStore: Writable<Node[]>) => void;
@@ -39,6 +40,21 @@ export type SvelteFlowStoreActions = {
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 SvelteFlowStore = SvelteFlowStoreState & SvelteFlowStoreActions;
export type StoreContext = {
getStore: () => SvelteFlowStore;
provider: boolean;
};
export type ProviderContext = StoreContext & {
setStore: (store: SvelteFlowStore) => void;
};

View File

@@ -78,7 +78,7 @@ export function syncEdgeStores(
// 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
export const syncViewportStores = (
panZoomStore: Writable<PanZoomInstance | null>,
panZoomStore: PanZoomInstance | null,
viewportStore: Writable<Viewport>,
userViewportStore?: Writable<Viewport>
) => {
@@ -86,7 +86,7 @@ export const syncViewportStores = (
return;
}
const panZoom = get(panZoomStore);
const panZoom = panZoomStore;
const viewportStoreSetter = viewportStore.set;
const userViewportStoreSetter = userViewportStore.set;

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 {};

747
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff