Merge pull request #4878 from xyflow/migrate/svelte5

Svelte Flow 1.0.0
This commit is contained in:
Peter Kogo
2025-05-14 12:30:18 +02:00
committed by GitHub
208 changed files with 7697 additions and 6787 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
name: 'Playwright Tests' name: 'Playwright Tests'
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: mcr.microsoft.com/playwright:v1.45.0-jammy image: mcr.microsoft.com/playwright:v1.51.1-jammy
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v4 uses: actions/checkout@v4
+1
View File
@@ -1 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" /> /// <reference types="astro/client" />
+1
View File
@@ -7,6 +7,7 @@
"dev": "vite --port 3000 --open --host", "dev": "vite --port 3000 --open --host",
"serve": "vite serve --port 3000", "serve": "vite serve --port 3000",
"build": "vite build", "build": "vite build",
"preview": "vite preview",
"test:dev": "cypress open", "test:dev": "cypress open",
"test": "pnpm test-component && pnpm test-e2e", "test": "pnpm test-component && pnpm test-e2e",
"test-component": "cypress run --component", "test-component": "cypress run --component",
@@ -4,6 +4,7 @@ export default {
flowProps: { flowProps: {
fitView: true, fitView: true,
multiSelectionKeyCode: 's', multiSelectionKeyCode: 's',
deleteKeyCode: 'd',
nodes: [ nodes: [
{ {
id: '1', id: '1',
@@ -3,6 +3,8 @@ import DragHandleNode from './components/DragHandleNode';
export default { export default {
flowProps: { flowProps: {
fitView: true, fitView: true,
deleteKeyCode: 'd',
multiSelectionKeyCode: 's',
nodeTypes: { nodeTypes: {
DragHandleNode, DragHandleNode,
}, },
+16 -16
View File
@@ -12,25 +12,25 @@
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.2.1", "@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.5.10", "@sveltejs/kit": "^2.20.7",
"@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/eslint-plugin": "^8.30.1",
"@typescript-eslint/parser": "^6.10.0", "@typescript-eslint/parser": "^8.30.1",
"eslint": "^8.53.0", "eslint": "^9.24.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^10.1.2",
"eslint-plugin-svelte": "^2.35.0", "eslint-plugin-svelte": "^3.5.1",
"prettier": "^3.0.3", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.0.3", "prettier-plugin-svelte": "^3.3.3",
"svelte": "^4.2.12", "svelte": "^5.27.0",
"svelte-check": "^3.6.6", "svelte-check": "^4.1.6",
"tslib": "^2.6.2", "tslib": "^2.8.1",
"typescript": "^5.4.5", "typescript": "^5.8.3",
"vite": "^5.2.12" "vite": "^6.3.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^1.0.4", "@dagrejs/dagre": "^1.1.4",
"@sveltejs/vite-plugin-svelte": "^3.1.1", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@xyflow/svelte": "workspace:^" "@xyflow/svelte": "workspace:^"
} }
} }
@@ -35,7 +35,7 @@
<header> <header>
<div class="logo">Svelte Flow</div> <div class="logo">Svelte Flow</div>
<select on:change={onChange} value={$page.route.id}> <select onchange={onChange} value={$page.route.id}>
{#each routes as route} {#each routes as route}
<option value={`/examples/${route}`}>{route}</option> <option value={`/examples/${route}`}>{route}</option>
{/each} {/each}
@@ -3,6 +3,8 @@ import { MarkerType } from '@xyflow/svelte';
export default { export default {
flowProps: { flowProps: {
fitView: true, fitView: true,
multiSelectionKey: ['Meta', 's'],
deleteKey: 'd',
nodes: [ nodes: [
{ {
id: '1', id: '1',
@@ -1,9 +1,18 @@
<script lang="ts"> <script lang="ts">
import { NodeToolbar, type NodeProps, Handle, Position } from '@xyflow/svelte'; import {
NodeToolbar,
type NodeProps,
type Node,
Handle,
Position,
type Align
} from '@xyflow/svelte';
type $$Props = NodeProps; let {
data
export let data: $$Props['data']; }: NodeProps<
Node<{ label: string; toolbarPosition: Position; toolbarAlign: Align; toolbarVisible: boolean }>
> = $props();
</script> </script>
<NodeToolbar <NodeToolbar
@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { NodeProps } from '@xyflow/svelte'; import type { NodeProps } from '@xyflow/svelte';
type $$Props = NodeProps; let {}: NodeProps = $props();
</script> </script>
<div class="container"> <div class="container">
<div class="drag-handle custom-drag-handle" /> <div class="drag-handle custom-drag-handle"></div>
</div> </div>
<style> <style>
@@ -4,6 +4,9 @@ export default {
flowProps: { flowProps: {
fitView: true, fitView: true,
nodeDragThreshold: 0, nodeDragThreshold: 0,
autoPanOnConnect: false,
autoPanOnNodeDrag: false,
deleteKey: 'd',
nodeTypes: { nodeTypes: {
DragHandleNode DragHandleNode
}, },
+2
View File
@@ -0,0 +1,2 @@
export const ssr = false;
export const prerender = false;
@@ -1,11 +1,16 @@
<script lang="ts"> <script lang="ts">
import { Header } from '$components/Header'; import { Header } from '$components/Header';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script> </script>
<div class="app"> <div class="app">
<Header /> <Header />
<div class="flow"> <div class="flow">
<slot /> {@render children?.()}
</div> </div>
</div> </div>
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
useNodeConnections,
useSvelteFlow, useSvelteFlow,
type Edge, type Edge,
type Node, type Node,
@@ -19,21 +19,27 @@
} }
]; ];
const nodes = writable<Node[]>(initialNodes); let nodes = $state.raw<Node[]>(initialNodes);
const edges = writable<Edge[]>([]); let edges = $state.raw<Edge[]>([]);
let connectingNodeId: string | null = '0'; let connectingNodeId: string | null = $state('0');
let rect: DOMRectReadOnly; let rect = $state<DOMRectReadOnly>();
let id = 1; let id = 1;
const getId = () => `${id++}`; const getId = () => `${id++}`;
const { screenToFlowPosition, flowToScreenPosition } = useSvelteFlow(); const { screenToFlowPosition, flowToScreenPosition } = useSvelteFlow();
const connections = useNodeConnections({ id: '0', handleType: 'source' });
$inspect(connections.current);
const handleConnectEnd: OnConnectEnd = (event) => { const handleConnectEnd: OnConnectEnd = (event) => {
if (!connectingNodeId) return; if (!connectingNodeId) return;
// See of connection landed inside the flow pane // See of connection landed inside the flow pane
const targetIsPane = (event.target as Partial<Element> | null)?.classList?.contains('svelte-flow__pane'); const targetIsPane = (event.target as Partial<Element> | null)?.classList?.contains(
'svelte-flow__pane'
);
if (targetIsPane && 'clientX' in event && 'clientY' in event) { if (targetIsPane && 'clientX' in event && 'clientY' in event) {
const id = getId(); const id = getId();
const position = { const position = {
@@ -56,15 +62,14 @@
origin: [0.5, 0.0] origin: [0.5, 0.0]
}; };
$nodes.push(newNode); nodes = [...nodes, newNode];
$edges.push({
const newEdge = {
source: connectingNodeId, source: connectingNodeId,
target: id, target: id,
id: `${connectingNodeId}--${id}` id: `${connectingNodeId}--${id}`
}); };
edges = [...edges, newEdge];
$nodes = $nodes;
$edges = $edges;
} }
}; };
</script> </script>
@@ -73,8 +78,8 @@
<div class="wrapper" bind:contentRect={rect}> <div class="wrapper" bind:contentRect={rect}>
<SvelteFlow <SvelteFlow
{nodes} bind:nodes
{edges} bind:edges
fitView fitView
fitViewOptions={{ padding: 2 }} fitViewOptions={{ padding: 2 }}
onconnectstart={(_, { nodeId }) => { onconnectstart={(_, { nodeId }) => {
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -18,7 +17,7 @@
targetPosition: Position.Left targetPosition: Position.Left
}; };
const nodes = writable([ let nodes = $state.raw([
{ {
id: 'A', id: 'A',
position: { x: 0, y: 150 }, position: { x: 0, y: 150 },
@@ -30,16 +29,16 @@
{ id: 'D', position: { x: 250, y: 300 }, data: { label: 'D' }, ...nodeDefaults } { id: 'D', position: { x: 250, y: 300 }, data: { label: 'D' }, ...nodeDefaults }
]); ]);
const edges = writable([ let edges = $state.raw([
{ id: 'A-B', source: 'A', target: 'B' }, { id: 'A-B', source: 'A', target: 'B' },
{ id: 'A-C', source: 'A', target: 'C' }, { id: 'A-C', source: 'A', target: 'C' },
{ id: 'A-D', source: 'A', target: 'D' } { id: 'A-D', source: 'A', target: 'D' }
]); ]);
let colorMode: ColorMode = 'light'; let colorMode: ColorMode = $state('light');
</script> </script>
<SvelteFlow {nodes} {edges} {colorMode} fitView> <SvelteFlow bind:nodes bind:edges {colorMode} colorModeSSR={'light'} fitView>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { SvelteFlow } from '@xyflow/svelte'; import { SvelteFlow } from '@xyflow/svelte';
import { Background, BackgroundVariant, type Edge, type Node } from '@xyflow/svelte'; import { Background, BackgroundVariant, type Edge, type Node } from '@xyflow/svelte';
import { writable } from 'svelte/store';
import CustomNode from './CustomNode.svelte'; import CustomNode from './CustomNode.svelte';
import ConnectionLine from './ConnectionLine.svelte'; import ConnectionLine from './ConnectionLine.svelte';
@@ -12,7 +11,7 @@
custom: CustomNode custom: CustomNode
}; };
const nodes = writable<Node[]>([ let nodes = $state.raw<Node[]>([
{ {
id: 'connectionline-1', id: 'connectionline-1',
type: 'custom', type: 'custom',
@@ -21,12 +20,11 @@
} }
]); ]);
const edges = writable<Edge[]>([]); let edges = $state.raw<Edge[]>([]);
</script> </script>
<div style="height:100vh;"> <div style="height:100vh;">
<SvelteFlow {nodeTypes} {nodes} {edges} fitView> <SvelteFlow bind:nodes bind:edges {nodeTypes} fitView connectionLineComponent={ConnectionLine}>
<ConnectionLine slot="connectionLine" />
<Background variant={BackgroundVariant.Lines} /> <Background variant={BackgroundVariant.Lines} />
</SvelteFlow> </SvelteFlow>
</div> </div>
@@ -3,22 +3,24 @@
const connection = useConnection(); const connection = useConnection();
let path: string | null = null; let path: string | null = $derived.by(() => {
if (connection.current.inProgress) {
$: if ($connection.inProgress) { const { from, to, fromPosition, toPosition } = connection.current;
const { from, to, fromPosition, toPosition } = $connection; const pathParams = {
const pathParams = { sourceX: from.x,
sourceX: from.x, sourceY: from.y,
sourceY: from.y, sourcePosition: fromPosition,
sourcePosition: fromPosition, targetX: to.x,
targetX: to.x, targetY: to.y,
targetY: to.y, targetPosition: toPosition
targetPosition: toPosition };
}; const [path] = getBezierPath(pathParams);
[path] = getBezierPath(pathParams); return path;
} }
return null;
});
</script> </script>
{#if $connection.inProgress} {#if connection.current.inProgress}
<path d={path} fill="none" stroke={$connection.fromHandle.id} /> <path d={path} fill="none" stroke={connection.current.fromHandle.id} />
{/if} {/if}
@@ -1,10 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Handle, Position, type NodeProps } from '@xyflow/svelte'; import { Handle, Position, type NodeProps } from '@xyflow/svelte';
type $$Props = NodeProps; let { isConnectable }: NodeProps = $props();
export let isConnectable: $$Props['isConnectable'];
$$restProps;
const handleStyle = 'width: 10px; height: 10px; bottom: -5px;'; const handleStyle = 'width: 10px; height: 10px; bottom: -5px;';
</script> </script>
@@ -1,5 +1,13 @@
<script lang="ts" module>
// TODO: Is this the best way?
class background {
color: string = $state('#1A192B');
}
export const bg = new background();
</script>
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -20,9 +28,7 @@
colorNode: CustomNode colorNode: CustomNode
}; };
const bgColor = writable('#1A192B'); let nodes = $state.raw<Node[]>([
const nodes = writable<Node[]>([
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -33,7 +39,7 @@
{ {
id: '2', id: '2',
type: 'colorNode', type: 'colorNode',
data: { colorStore: bgColor }, data: {},
position: { x: 250, y: 50 } position: { x: 250, y: 50 }
}, },
{ {
@@ -52,7 +58,7 @@
} }
]); ]);
const edges = writable<Edge[]>([ let edges = $state.raw<Edge[]>([
{ {
id: 'e1-2', id: 'e1-2',
source: '1', source: '1',
@@ -81,10 +87,10 @@
</script> </script>
<SvelteFlow <SvelteFlow
{nodes} bind:nodes
{edges} bind:edges
{nodeTypes} {nodeTypes}
style="--xy-background-color: {$bgColor}" style="--xy-background-color: {bg.color}"
fitView fitView
onconnect={onConnect} onconnect={onConnect}
> >
@@ -1,12 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { Writable } from 'svelte/store';
import { Handle, Position, type NodeProps, type Node } from '@xyflow/svelte'; import { Handle, Position, type NodeProps, type Node } from '@xyflow/svelte';
import { bg } from './+page.svelte';
type $$Props = NodeProps<Node<{ colorStore: Writable<string> }>>; let { data }: NodeProps<Node> = $props();
export let data: $$Props['data'];
$$restProps;
const { colorStore } = data; const { colorStore } = data;
</script> </script>
@@ -14,13 +10,13 @@
<div class="custom"> <div class="custom">
<Handle type="target" position={Position.Left} /> <Handle type="target" position={Position.Left} />
<div> <div>
Custom Color Picker Node: <strong>{$colorStore}</strong> Custom Color Picker Node: <strong>{bg.color}</strong>
</div> </div>
<input <input
class="nodrag" class="nodrag"
type="color" type="color"
on:input={(evt) => colorStore.set(evt.currentTarget.value)} oninput={(evt) => (bg.color = evt.currentTarget.value)}
value={$colorStore} value={bg.color}
/> />
<Handle type="source" position={Position.Right} id="a" style="top: 20px;" /> <Handle type="source" position={Position.Right} id="a" style="top: 20px;" />
<Handle type="source" position={Position.Right} id="b" style="top: auto; bottom: 10px;" /> <Handle type="source" position={Position.Right} id="b" style="top: auto; bottom: 10px;" />
@@ -1,79 +1,77 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store'; import { SvelteFlow, Background, Position, ConnectionLineType, Panel } from '@xyflow/svelte';
import { SvelteFlow, Background, Position, ConnectionLineType, Panel } from '@xyflow/svelte'; import type { Edge, Node } from '@xyflow/svelte';
import type { Edge, Node } from '@xyflow/svelte'; import dagre from '@dagrejs/dagre';
import dagre from '@dagrejs/dagre';
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
import { initialNodes, initialEdges } from './nodes-and-edges'; import { initialNodes, initialEdges } from './nodes-and-edges';
const dagreGraph = new dagre.graphlib.Graph(); const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({})); dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 172; const nodeWidth = 172;
const nodeHeight = 36; const nodeHeight = 36;
function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') { function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') {
const isHorizontal = direction === 'LR'; const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction }); dagreGraph.setGraph({ rankdir: direction });
nodes.forEach((node) => { nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
}); });
edges.forEach((edge) => { edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target); dagreGraph.setEdge(edge.source, edge.target);
}); });
dagre.layout(dagreGraph); dagre.layout(dagreGraph);
nodes.forEach((node) => { const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id); const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// We are shifting the dagre node position (anchor=center center) to the top left return {
// so it matches the React Flow node anchor point (top left). ...node,
node.position = { targetPosition: isHorizontal ? Position.Left : Position.Top,
x: nodeWithPosition.x - nodeWidth / 2, sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
y: nodeWithPosition.y - nodeHeight / 2 position: {
}; x: nodeWithPosition.x - nodeWidth / 2,
}); y: nodeWithPosition.y - nodeHeight / 2
}
};
});
return { nodes, edges }; return { nodes: layoutedNodes, edges };
} }
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
initialNodes, initialNodes,
initialEdges initialEdges
); );
const nodes = writable<Node[]>(layoutedNodes); let nodes = $state.raw<Node[]>(layoutedNodes);
const edges = writable<Edge[]>(layoutedEdges); let edges = $state.raw<Edge[]>(layoutedEdges);
function onLayout(direction: string) { function onLayout(direction: string) {
const layoutedElements = getLayoutedElements($nodes, $edges, direction); const layoutedElements = getLayoutedElements(nodes, edges, direction);
$nodes = layoutedElements.nodes; nodes = layoutedElements.nodes;
$edges = layoutedElements.edges; edges = layoutedElements.edges;
// nodes.set(layoutedElements.nodes); }
// edges.set(layoutedElements.edges);
}
</script> </script>
<div style="height:100vh;"> <div style="height:100vh;">
<SvelteFlow <SvelteFlow
{nodes} bind:nodes
{edges} bind:edges
fitView fitView
connectionLineType={ConnectionLineType.SmoothStep} connectionLineType={ConnectionLineType.SmoothStep}
defaultEdgeOptions={{ type: 'smoothstep', animated: true }} defaultEdgeOptions={{ type: 'smoothstep', animated: true }}
> >
<Panel position="top-right"> <Panel position="top-right">
<button on:click={() => onLayout('TB')}>vertical layout</button> <button onclick={() => onLayout('TB')}>vertical layout</button>
<button on:click={() => onLayout('LR')}>horizontal layout</button> <button onclick={() => onLayout('LR')}>horizontal layout</button>
</Panel> </Panel>
<Background /> <Background />
</SvelteFlow> </SvelteFlow>
</div> </div>
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -13,7 +12,7 @@
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
const nodes = writable<Node[]>([ const initialNodes = [
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -32,9 +31,11 @@
data: { label: 'Output Node' }, data: { label: 'Output Node' },
position: { x: 300, y: 150 } position: { x: 300, y: 150 }
} }
]); ];
const edges = writable([ let nodes = $state.raw<Node[]>(initialNodes);
let edges = $state.raw([
{ {
id: '1-2', id: '1-2',
type: 'default', type: 'default',
@@ -50,7 +51,7 @@
} }
]); ]);
const svelteFlow = useSvelteFlow(); const { screenToFlowPosition } = useSvelteFlow();
const onDragOver = (event: DragEvent) => { const onDragOver = (event: DragEvent) => {
event.preventDefault(); event.preventDefault();
@@ -68,7 +69,7 @@
} }
const type = event.dataTransfer.getData('application/svelteflow') || 'default'; const type = event.dataTransfer.getData('application/svelteflow') || 'default';
const position = svelteFlow.screenToFlowPosition({ const position = screenToFlowPosition({
x: event.clientX, x: event.clientX,
y: event.clientY y: event.clientY
}); });
@@ -79,17 +80,14 @@
data: { label: `${type} node` } data: { label: `${type} node` }
}; };
$nodes.push(newNode); nodes = [...nodes, newNode];
$nodes = $nodes;
}; };
$: { $inspect(nodes);
console.log($nodes);
}
</script> </script>
<main> <main>
<SvelteFlow {nodes} {edges} fitView on:dragover={onDragOver} on:drop={onDrop}> <SvelteFlow bind:nodes bind:edges fitView ondragover={onDragOver} ondrop={onDrop}>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import { useSvelteFlow } from '@xyflow/svelte';
const onDragStart = (event: DragEvent, nodeType: string) => { const onDragStart = (event: DragEvent, nodeType: string) => {
if (!event.dataTransfer) { if (!event.dataTransfer) {
return null; return null;
@@ -9,29 +7,28 @@
event.dataTransfer.setData('application/svelteflow', nodeType); event.dataTransfer.setData('application/svelteflow', nodeType);
event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.effectAllowed = 'move';
}; };
const { zoomIn, zoomOut, fitView, viewport, nodes } = useSvelteFlow();
</script> </script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<aside> <aside>
<div class="label">You can drag these nodes to the pane on the left.</div> <div class="label">You can drag these nodes to the pane on the left.</div>
<div <div
class="input-node node" class="input-node node"
on:dragstart={(event) => onDragStart(event, 'input')} ondragstart={(event) => onDragStart(event, 'input')}
draggable={true} draggable={true}
> >
Input Node Input Node
</div> </div>
<div <div
class="default-node node" class="default-node node"
on:dragstart={(event) => onDragStart(event, 'default')} ondragstart={(event) => onDragStart(event, 'default')}
draggable={true} draggable={true}
> >
Default Node Default Node
</div> </div>
<div <div
class="output-node node" class="output-node node"
on:dragstart={(event) => onDragStart(event, 'output')} ondragstart={(event) => onDragStart(event, 'output')}
draggable={true} draggable={true}
> >
Output Node Output Node
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -7,14 +6,15 @@
BackgroundVariant, BackgroundVariant,
MiniMap, MiniMap,
MarkerType, MarkerType,
type Connection type Connection,
type EdgeTypes
} from '@xyflow/svelte'; } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
import ButtonEdge from './ButtonEdge.svelte'; import ButtonEdge from './ButtonEdge.svelte';
import CustomBezierEdge from './CustomBezierEdge.svelte'; import CustomBezierEdge from './CustomBezierEdge.svelte';
const nodes = writable([ let nodes = $state.raw([
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -54,7 +54,7 @@
} }
]); ]);
const edges = writable([ let edges = $state.raw([
{ {
id: 'e1-2', id: 'e1-2',
source: '1', source: '1',
@@ -128,45 +128,29 @@
target: '6', target: '6',
label: 'hi', label: 'hi',
labelStyle: 'background: red; font-weight: 700; padding: 5px;', labelStyle: 'background: red; font-weight: 700; padding: 5px;',
style: 'stroke: #ffcc0', style: 'stroke: #ffcc0'
markerEnd: {
type: MarkerType.Arrow,
color: '#FFCC00',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20,
strokeWidth: 2
},
markerStart: {
type: MarkerType.ArrowClosed,
color: '#FFCC00',
orient: 'auto-start-reverse',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20
}
} }
]); ]);
const edgeTypes = { const edgeTypes: EdgeTypes = {
button: ButtonEdge, button: ButtonEdge,
customBezier: CustomBezierEdge customBezier: CustomBezierEdge
}; };
function getEdgeId(connection: Connection) { function getEdgeId(connection: Connection) {
return `edge-${connection.source}-${connection.target}}`; return `edge-${connection.source}-${connection.target}}`;
} }
$: console.log('edges', $edges); $inspect(edges);
</script> </script>
<SvelteFlow <SvelteFlow
{nodes} bind:nodes
{edges} bind:edges
{edgeTypes} {edgeTypes}
fitView fitView
nodeDragThreshold={2} nodeDragThreshold={2}
onedgecreate={(connection) => { onbeforeconnect={(connection) => {
console.log('on edge create', connection); console.log('on edge create', connection);
return { return {
@@ -1,59 +1,80 @@
<script lang="ts"> <script lang="ts">
import { getBezierPath, BaseEdge, type EdgeProps, EdgeLabelRenderer } from '@xyflow/svelte'; import {
getBezierPath,
BaseEdge,
type EdgeProps,
useSvelteFlow,
MarkerType,
EdgeReconnectAnchor,
EdgeLabel
} from '@xyflow/svelte';
type $$Props = EdgeProps; let {
style,
export let id: $$Props['id'] = ''; markerEnd,
export let style: $$Props['style'] = undefined;
export let markerEnd: $$Props['markerEnd'] = undefined;
export let sourceX: $$Props['sourceX'];
export let sourceY: $$Props['sourceY'];
export let sourcePosition: $$Props['sourcePosition'];
export let targetX: $$Props['targetX'];
export let targetY: $$Props['targetY'];
export let targetPosition: $$Props['targetPosition'];
$$restProps;
$: [edgePath, labelX, labelY] = getBezierPath({
sourceX, sourceX,
sourceY, sourceY,
sourcePosition, sourcePosition,
targetX, targetX,
targetY, targetY,
targetPosition targetPosition,
}); selected
}: EdgeProps = $props();
let [edgePath, labelX, labelY] = $derived(
getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
})
);
const { updateEdge } = useSvelteFlow();
let reconnecting = $state(false);
</script> </script>
<BaseEdge path={edgePath} {markerEnd} {style} /> {#if !reconnecting}
<EdgeLabelRenderer> <BaseEdge path={edgePath} {markerEnd} {style} />
<div <EdgeLabel x={labelX} y={labelY}>
class="edgeButtonContainer nodrag nopan"
style:transform="translate(-50%, -50%) translate({labelX}px,{labelY}px)"
>
<button <button
class="edgeButton" class="edgeButton"
on:click={(event) => { onclick={(event) => {
event.stopPropagation(); event.stopPropagation();
alert(`remove ${id}`); updateEdge('e5-6', {
markerEnd: {
type: MarkerType.Arrow,
color: '#FFCC00',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20,
strokeWidth: 2
},
markerStart: {
type: MarkerType.ArrowClosed,
color: '#FFCC00',
orient: 'auto-start-reverse',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20
}
});
}} }}
> >
× ×
</button> </button>
</div> </EdgeLabel>
</EdgeLabelRenderer> {/if}
{#if selected}
<EdgeReconnectAnchor bind:reconnecting type="source" position={{ x: sourceX, y: sourceY }} />
<EdgeReconnectAnchor bind:reconnecting type="target" position={{ x: targetX, y: targetY }} />
{/if}
<style> <style>
.edgeButtonContainer {
position: absolute;
font-size: 12pt;
/* everything inside EdgeLabelRenderer has no pointer events by default */
/* if you have an interactive element, set pointer-events: all */
pointer-events: all;
}
.edgeButton { .edgeButton {
width: 20px; width: 20px;
height: 20px; height: 20px;
@@ -61,8 +82,7 @@
border: 1px solid #fff; border: 1px solid #fff;
cursor: pointer; cursor: pointer;
border-radius: 50%; border-radius: 50%;
font-size: 12px; font-size: 10px;
line-height: 1;
} }
.edgeButton:hover { .edgeButton:hover {
@@ -1,38 +1,7 @@
<script lang="ts"> <script lang="ts">
import { BezierEdge, type EdgeProps } from '@xyflow/svelte'; import { BezierEdge, type EdgeProps } from '@xyflow/svelte';
type $$Props = EdgeProps; let { ...props }: EdgeProps = $props();
export let label: $$Props['label'] = undefined;
export let labelStyle: $$Props['labelStyle'] = undefined;
export let style: $$Props['style'] = undefined;
export let markerStart: $$Props['markerStart'] = undefined;
export let markerEnd: $$Props['markerEnd'] = undefined;
export let interactionWidth: $$Props['interactionWidth'] = undefined;
export let sourceX: $$Props['sourceX'];
export let sourceY: $$Props['sourceY'];
export let sourcePosition: $$Props['sourcePosition'];
export let targetX: $$Props['targetX'];
export let targetY: $$Props['targetY'];
export let targetPosition: $$Props['targetPosition'];
$$restProps;
</script> </script>
<BezierEdge <BezierEdge {...props} pathOptions={{ curvature: 1 }} />
{sourceX}
{sourceY}
{sourcePosition}
{targetX}
{targetY}
{targetPosition}
{label}
{labelStyle}
{markerStart}
{markerEnd}
{style}
{interactionWidth}
pathOptions={{ curvature: 1 }}
/>
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -17,11 +16,11 @@
const panOnDrag = [1, 2]; const panOnDrag = [1, 2];
const onMoveStart = (e: any) => console.log('move start', e); const onmovestart = (e: any) => console.log('move start', e);
const onMove = (e: any) => console.log('move', e); const onmove = (e: any) => console.log('move', e);
const onMoveEnd = (e: any) => console.log('move end', e); const onmoveend = (e: any) => console.log('move end', e);
const nodes = writable([ let nodes = $state.raw([
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -34,23 +33,23 @@
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' } { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }
]); ]);
const edges = writable([ let edges = $state.raw([
{ id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' } { id: 'e1-3', source: '1', target: '3' }
]); ]);
</script> </script>
<SvelteFlow <SvelteFlow
{nodes} bind:nodes
{edges} bind:edges
fitView fitView
selectionMode={SelectionMode.Partial} selectionMode={SelectionMode.Partial}
selectionOnDrag selectionOnDrag
panOnScroll panOnScroll
{panOnDrag} {panOnDrag}
{onMoveStart} {onmovestart}
{onMove} {onmove}
{onMoveEnd} {onmoveend}
> >
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -20,7 +19,7 @@
multi: MultiHandleNode multi: MultiHandleNode
}; };
const nodes = writable<Node[]>([ let nodes = $state.raw<Node[]>([
{ {
id: '1', id: '1',
type: 'single', type: 'single',
@@ -60,7 +59,7 @@
} }
]); ]);
const edges = writable<Edge[]>([ let edges = $state.raw<Edge[]>([
{ {
id: 'e1-2', id: 'e1-2',
source: '1', source: '1',
@@ -87,7 +86,7 @@
]); ]);
</script> </script>
<SvelteFlow {nodes} {edges} {nodeTypes} fitView colorMode="dark"> <SvelteFlow bind:nodes bind:edges {nodeTypes} fitView colorMode="dark">
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -4,12 +4,10 @@
Position, Position,
type NodeProps, type NodeProps,
type Connection, type Connection,
useHandleConnections useNodeConnections
} from '@xyflow/svelte'; } from '@xyflow/svelte';
type $$Props = NodeProps; let { id }: NodeProps = $props();
export let id: $$Props['id'];
function onConnectTarget(connection: Connection[]) { function onConnectTarget(connection: Connection[]) {
console.log('connect target', connection); console.log('connect target', connection);
@@ -27,13 +25,18 @@
console.log('disconnect source', handleId, connection); console.log('disconnect source', handleId, connection);
} }
const connections = useHandleConnections({ nodeId: id, type: 'target' }); const connections = useNodeConnections({
id: id,
handleType: 'target',
onConnect: (connections) => {
console.log('onConnect', connections);
},
onDisconnect: (connections) => {
console.log('onDisconnect', connections);
}
});
$: { $inspect(connections.current);
console.log('connections', id, $connections);
}
$$restProps;
</script> </script>
<div class="custom"> <div class="custom">
@@ -1,9 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Handle, Position, type NodeProps, type Connection } from '@xyflow/svelte'; import { Handle, Position, type NodeProps, type Connection } from '@xyflow/svelte';
type $$Props = NodeProps; let { id }: NodeProps = $props();
export let id: $$Props['id'];
function onConnectTarget(connection: Connection[]) { function onConnectTarget(connection: Connection[]) {
console.log('connect target', connection); console.log('connect target', connection);
@@ -20,8 +18,6 @@
function onDisconnectSource(connection: Connection[]) { function onDisconnectSource(connection: Connection[]) {
console.log('disconnect source', connection); console.log('disconnect source', connection);
} }
$$restProps;
</script> </script>
<div class="custom"> <div class="custom">
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -13,7 +12,7 @@
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
const nodes = writable([ let nodes = $state.raw([
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -25,37 +24,37 @@
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } } { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }
]); ]);
const edges = writable([ let edges = $state.raw([
{ id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' } { id: 'e1-3', source: '1', target: '3' }
]); ]);
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node); const onnodedragstart = (_: MouseEvent, node: Node) => console.log('drag start', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onnodedragstop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onnodeclick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const onedgeclick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const onPaneClick = (event: MouseEvent) => console.log('onPaneClick', event); const onpaneclick = (event: MouseEvent) => console.log('onPaneClick', event);
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event); const onpanescroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
const onPaneContextMenu = (event: MouseEvent) => console.log('onPaneContextMenu', event); const onpanecontextmenu = (event: MouseEvent) => console.log('onPaneContextMenu', event);
const onMoveEnd: OnMoveEnd = (_, viewport) => console.log('onMoveEnd', viewport); const onmoveend: OnMoveEnd = (_, viewport) => console.log('onMoveEnd', viewport);
let isSelectable = false; let isSelectable = $state(false);
let isDraggable = false; let isDraggable = $state(false);
let isConnectable = false; let isConnectable = $state(false);
let zoomOnScroll = false; let zoomOnScroll = $state(false);
let zoomOnPinch = false; let zoomOnPinch = $state(false);
let panOnScroll = false; let panOnScroll = $state(false);
let panOnScrollMode = PanOnScrollMode.Free; let panOnScrollMode = $state(PanOnScrollMode.Free);
let zoomOnDoubleClick = false; let zoomOnDoubleClick = $state(false);
let panOnDrag = true; let panOnDrag = $state(true);
let captureZoomClick = false; let captureZoomClick = $state(false);
let captureZoomScroll = false; let captureZoomScroll = $state(false);
let captureElementClick = false; let captureElementClick = $state(false);
</script> </script>
<SvelteFlow <SvelteFlow
{nodes} bind:nodes
{edges} bind:edges
elementsSelectable={isSelectable} elementsSelectable={isSelectable}
nodesConnectable={isConnectable} nodesConnectable={isConnectable}
nodesDraggable={isDraggable} nodesDraggable={isDraggable}
@@ -65,7 +64,8 @@
{panOnScrollMode} {panOnScrollMode}
{zoomOnDoubleClick} {zoomOnDoubleClick}
{panOnDrag} {panOnDrag}
{onMoveEnd} {onmoveend}
fitView
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
@@ -143,7 +143,7 @@
<select <select
id="panonscrollmode" id="panonscrollmode"
bind:value={panOnScrollMode} bind:value={panOnScrollMode}
on:change={(event) => { onchange={(event) => {
panOnScrollMode = PanOnScrollMode.Free; panOnScrollMode = PanOnScrollMode.Free;
}} }}
class="svelte-flow__panonscrollmode" class="svelte-flow__panonscrollmode"
@@ -1,31 +1,34 @@
<script lang="ts"> <script lang="ts">
import { SvelteFlow, Background, Controls, useSvelteFlow } from '@xyflow/svelte'; import { SvelteFlow, Background, Controls, useSvelteFlow } from '@xyflow/svelte';
import type { Edge, Node } from '@xyflow/svelte'; import type { Edge, Node } from '@xyflow/svelte';
import { writable } from 'svelte/store';
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
import { initialNodes, initialEdges } from './nodes-and-edges'; import { initialNodes, initialEdges } from './nodes-and-edges';
const nodes = writable<Node[]>(initialNodes); let nodes = $state.raw<Node[]>(initialNodes);
const edges = writable<Edge[]>(initialEdges); let edges = $state.raw<Edge[]>(initialEdges);
const { getIntersectingNodes } = useSvelteFlow(); const { getIntersectingNodes } = useSvelteFlow();
function onNodeDrag({ detail: { targetNode } }) {
if (targetNode) {
const intersections = getIntersectingNodes(targetNode).map((n) => n.id);
$nodes.forEach((n) => {
n.class = intersections.includes(n.id) ? 'highlight' : '';
});
$nodes = $nodes;
}
}
</script> </script>
<div style="height:100vh;"> <div style="height:100vh;">
<SvelteFlow {nodes} {edges} fitView class="intersection-flow" on:nodedrag={onNodeDrag}> <SvelteFlow
bind:nodes
bind:edges
fitView
class="intersection-flow"
onnodedrag={({ targetNode }) => {
if (targetNode) {
const intersections = getIntersectingNodes(targetNode).map((n) => n.id);
nodes = nodes.map((node) => ({
...node,
class: intersections.includes(node.id) ? 'highlight' : ''
}));
}
}}
>
<Background /> <Background />
<Controls /> <Controls />
</SvelteFlow> </SvelteFlow>
@@ -48,10 +51,10 @@
} }
:global( :global(
.intersection-flow .svelte-flow__node.selected, .intersection-flow .svelte-flow__node.selected,
.intersection-flow .svelte-flow__node:hover, .intersection-flow .svelte-flow__node:hover,
.intersection-flow .svelte-flow__node:focus .intersection-flow .svelte-flow__node:focus
) { ) {
box-shadow: 6px 6px 0 1px rgba(0, 0, 0, 0.7); box-shadow: 6px 6px 0 1px rgba(0, 0, 0, 0.7);
background-color: #eee; background-color: #eee;
} }
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Controls, Panel, type Edge } from '@xyflow/svelte'; import { SvelteFlow, Controls, Panel, type Edge } from '@xyflow/svelte';
import DefaultResizer from './DefaultResizer.svelte'; import DefaultResizer from './DefaultResizer.svelte';
@@ -19,9 +18,9 @@
const nodeStyle = 'border: 1px solid #222; font-size: 10px; background-color: #ddd;'; const nodeStyle = 'border: 1px solid #222; font-size: 10px; background-color: #ddd;';
const edges = writable<Edge[]>([]); let edges = $state.raw<Edge[]>([]);
const nodes = writable<ResizeNode[]>([ let nodes = $state.raw<ResizeNode[]>([
{ {
id: '1', id: '1',
type: 'defaultResizer', type: 'defaultResizer',
@@ -128,12 +127,12 @@
} }
]); ]);
let snapToGrid = false; let snapToGrid = $state(false);
</script> </script>
<SvelteFlow <SvelteFlow
{nodes} bind:nodes
{edges} bind:edges
{nodeTypes} {nodeTypes}
minZoom={0.2} minZoom={0.2}
maxZoom={5} maxZoom={5}
@@ -144,7 +143,7 @@
<Controls /> <Controls />
<Panel position="bottom-right"> <Panel position="bottom-right">
<button <button
on:click={() => { onclick={() => {
snapToGrid = !snapToGrid; snapToGrid = !snapToGrid;
}} }}
> >
@@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Handle, Position, type NodeProps, NodeResizeControl } from '@xyflow/svelte'; import { Handle, Position, type NodeProps, NodeResizeControl, type Node } from '@xyflow/svelte';
import type { ResizeNode } from './types'; import type { ResizeNode } from './types';
type $$Props = NodeProps<ResizeNode>; let { data }: NodeProps<ResizeNode> = $props();
export let data: $$Props['data'];
</script> </script>
<NodeResizeControl <NodeResizeControl
@@ -2,10 +2,7 @@
import { Handle, NodeResizer, Position, type NodeProps } from '@xyflow/svelte'; import { Handle, NodeResizer, Position, type NodeProps } from '@xyflow/svelte';
import type { ResizeNode } from './types'; import type { ResizeNode } from './types';
type $$Props = NodeProps<ResizeNode>; let { data, selected }: NodeProps<ResizeNode> = $props();
export let data: $$Props['data'];
export let selected: $$Props['selected'] = undefined;
</script> </script>
<NodeResizer <NodeResizer
@@ -2,9 +2,7 @@
import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte'; import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte';
import type { ResizeNode } from './types'; import type { ResizeNode } from './types';
type $$Props = NodeProps<ResizeNode>; let { data }: NodeProps<ResizeNode> = $props();
export let data: $$Props['data'];
</script> </script>
<NodeResizeControl <NodeResizeControl
@@ -2,9 +2,7 @@
import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte'; import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte';
import type { ResizeNode } from './types'; import type { ResizeNode } from './types';
type $$Props = NodeProps<ResizeNode>; let { data }: NodeProps<ResizeNode> = $props();
export let data: $$Props['data'];
</script> </script>
<NodeResizeControl <NodeResizeControl
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Background, Background,
@@ -11,7 +10,7 @@
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
import CustomNode from './CustomNode.svelte'; import CustomNode, { type CustomNodeType } from './CustomNode.svelte';
import SelectedNodesToolbar from './SelectedNodesToolbar.svelte'; import SelectedNodesToolbar from './SelectedNodesToolbar.svelte';
const nodeTypes: NodeTypes = { const nodeTypes: NodeTypes = {
@@ -19,9 +18,9 @@
}; };
const positions = ['top', 'right', 'bottom', 'left']; const positions = ['top', 'right', 'bottom', 'left'];
const alignments = ['start', 'center', 'end']; const alignments: ('start' | 'center' | 'end')[] = ['start', 'center', 'end'];
const initialNodes: Node[] = [ const initialNodes: CustomNodeType[] = [
{ {
id: 'default-node', id: 'default-node',
type: 'custom', type: 'custom',
@@ -51,12 +50,12 @@
const initialEdges: Edge[] = []; const initialEdges: Edge[] = [];
const nodes = writable<Node[]>(initialNodes); let nodes = $state.raw<Node[]>(initialNodes);
const edges = writable<Edge[]>(initialEdges); let edges = $state.raw<Edge[]>(initialEdges);
</script> </script>
<div style="height: 100vh;"> <div style="height: 100vh;">
<SvelteFlow {nodes} {edges} {nodeTypes} fitView> <SvelteFlow bind:nodes bind:edges {nodeTypes} fitView>
<Background /> <Background />
<SelectedNodesToolbar /> <SelectedNodesToolbar />
</SvelteFlow> </SvelteFlow>
@@ -1,10 +1,18 @@
<script lang="ts" module>
import { type Node } from '@xyflow/svelte';
export type CustomNodeType = Node<{
toolbarVisible?: boolean;
toolbarPosition: Position;
toolbarAlign?: 'start' | 'center' | 'end';
label: string;
}>;
</script>
<script lang="ts"> <script lang="ts">
import { NodeToolbar, type NodeProps, Handle, Position } from '@xyflow/svelte'; import { NodeToolbar, type NodeProps, Handle, Position } from '@xyflow/svelte';
type $$Props = NodeProps; let { data }: NodeProps<CustomNodeType> = $props();
$$restProps;
export let data: $$Props['data'];
</script> </script>
<NodeToolbar <NodeToolbar
@@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import { NodeToolbar, useNodes } from '@xyflow/svelte'; import { NodeToolbar, useNodes } from '@xyflow/svelte';
const nodes = useNodes(); let nodes = useNodes();
$: selectedNodeIds = $nodes.filter((node) => node.selected).map((node) => node.id); let selectedNodeIds = $derived(
$: isVisible = selectedNodeIds.length > 1; nodes.current.filter((node) => node.selected).map((node) => node.id)
);
let isVisible = $derived(selectedNodeIds.length > 1);
</script> </script>
<NodeToolbar nodeId={selectedNodeIds} {isVisible}> <NodeToolbar nodeId={selectedNodeIds} {isVisible}>
@@ -1,240 +1,8 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store'; import { SvelteFlowProvider } from '@xyflow/svelte';
import { import Flow from './Flow.svelte';
SvelteFlow,
Controls,
Background,
BackgroundVariant,
MiniMap,
Panel,
SelectionMode,
type NodeTypes,
type EdgeTypes,
type Node,
type Edge,
ConnectionMode,
ControlButton,
type FitViewOptions
} from '@xyflow/svelte';
import CustomNode from './CustomNode.svelte';
import CustomNodeDragHandle from './CustomNodeDragHandle.svelte';
import CustomEdge from './CustomEdge.svelte';
import '@xyflow/svelte/dist/style.css';
import InitTracker from './InitTracker.svelte';
const nodeTypes: NodeTypes = {
custom: CustomNode,
dragHandle: CustomNodeDragHandle
};
const edgeTypes: EdgeTypes = {
custom: CustomEdge
};
const fitViewOptions: FitViewOptions = {
padding: 0.2,
nodes: [{ id: '1' }, { id: '2' }]
};
const nodes = writable<Node[]>([
{
id: '1',
type: 'input',
data: { label: 'Input Node' },
position: { x: 150, y: 5 }
},
{
id: '2',
type: 'default',
data: { label: 'Node' },
position: { x: 0, y: 150 },
selectable: false
},
{
id: 'A',
type: 'default',
data: { label: 'Styled with class' },
class: 'custom-style',
position: { x: 150, y: 150 }
},
{
id: 'D',
type: 'default',
data: { label: 'Not draggable' },
position: { x: 150, y: 200 },
draggable: false
},
{
id: '3',
type: 'output',
data: { label: 'Output Node' },
position: { x: 300, y: 150 }
},
{
id: 'B',
type: 'default',
data: { label: 'Styled with style' },
style: 'border: 2px solid #ff5050;',
position: { x: 450, y: 150 }
},
{
id: 'C',
type: 'dragHandle',
data: { label: 'custom drag handle' },
dragHandle: '.custom-drag-handle',
position: { x: 450, y: 250 }
},
{
id: '4',
type: 'custom',
data: { label: 'Custom Node' },
position: { x: 150, y: 300 }
},
{
id: 'hideunhide',
data: { label: 'HIDE ME' },
position: { x: 300, y: 75 },
hidden: true
}
]);
const edges = writable<Edge[]>([
{
id: '1-2',
type: 'default',
source: '1',
target: '2',
label: 'Edge Text'
},
{
id: '1-3',
type: 'smoothstep',
source: '1',
target: '3',
selectable: false
},
{
id: '2-4',
type: 'custom',
source: '2',
target: '4',
animated: true
}
]);
function updateNode() {
$nodes[0].position.x += 20;
$nodes = $nodes;
}
function updateEdge() {
$edges[0].type = $edges[0].type === 'default' ? 'smoothstep' : 'default';
$edges = $edges;
}
$: {
console.log('nodes changed', $nodes);
}
</script> </script>
<SvelteFlow <SvelteFlowProvider>
{nodes} <Flow />
{edges} </SvelteFlowProvider>
{nodeTypes}
{edgeTypes}
fitView
fitViewOptions={{
padding: 0.1,
nodes: [{ id: '1' }, { id: '2' }, { id: '3' }]
}}
minZoom={0}
maxZoom={Infinity}
selectionMode={SelectionMode.Full}
initialViewport={{ x: 100, y: 100, zoom: 2 }}
snapGrid={[25, 25]}
oninit={() => console.log('on init')}
on:nodeclick={(event) => console.log('on node click', event)}
on:nodemouseenter={(event) => console.log('on node enter', event)}
on:nodemouseleave={(event) => console.log('on node leave', event)}
on:edgeclick={(event) => console.log('edge click', event)}
on:edgemouseenter={(event) => console.log('edge enter', event)}
on:edgemouseleave={(event) => console.log('edge leave', event)}
onconnectstart={(event) => console.log('on connect start', event)}
onconnect={(event) => console.log('on connect', event)}
onconnectend={(event) => console.log('on connect end', event)}
on:paneclick={(event) => console.log('on pane click', event)}
on:panecontextmenu={(event) => {
console.log('on pane contextmenu', event);
}}
on:nodedrag={(event) => {
console.log('on node drag', event);
}}
on:nodedragstart={(event) => {
console.log('on node drag start', event);
}}
on:nodedragstop={(event) => {
console.log('on node drag stop', event);
}}
on:nodecontextmenu={(event) => {
event.detail.event.preventDefault();
console.log('on node contextmenu', event);
}}
on:edgecontextmenu={({ detail: { event, edge } }) => {
event.preventDefault();
console.log('on edge contextmenu', edge);
}}
on:selectionclick={(event) => console.log('on selection click', event)}
on:selectioncontextmenu={(event) => console.log('on selection contextmenu', event)}
onbeforedelete={async ({ nodes, edges }) => {
console.log('on before delete', nodes, edges);
const deleteElements = confirm('Are you sure you want to delete the selected elements?');
return deleteElements;
}}
autoPanOnConnect
autoPanOnNodeDrag
connectionMode={ConnectionMode.Strict}
attributionPosition={'top-center'}
deleteKey={['Backspace', 'd']}
>
<Controls orientation="horizontal">
<ControlButton slot="before">xy</ControlButton>
<ControlButton aria-label="log" on:click={() => console.log('control button')}
>log</ControlButton
>
</Controls>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Panel position="top-right">
<button on:click={updateNode}>update node pos</button>
<button on:click={updateEdge}>update edge type</button>
<button
on:click={() => {
console.log($nodes, $nodes.length);
$nodes[$nodes.length - 1].hidden = !$nodes[$nodes.length - 1].hidden;
$nodes = $nodes;
}}>hide/unhide</button
>
</Panel>
<InitTracker />
</SvelteFlow>
<style>
:global(.svelte-flow .custom-style) {
background: #ff5050;
color: white;
}
:root {
--background-color: #ffffdd;
--background-pattern-color: #5050ff;
--minimap-background-color: #f5f6f7;
--minimap-mask-color: rgb(255, 255, 240, 0.6);
--controls-button-background-color: #ddd;
--controls-button-background-color-hover: #ccc;
}
</style>
@@ -1,46 +1,35 @@
<script lang="ts"> <script lang="ts">
import { import { BaseEdge, useEdges, getBezierPath, type EdgeProps, EdgeLabel } from '@xyflow/svelte';
BaseEdge,
EdgeLabelRenderer,
useEdges,
type EdgeProps,
getBezierPath
} from '@xyflow/svelte';
type $$Props = EdgeProps; let { ...props }: EdgeProps = $props();
$: [path, labelX, labelY] = getBezierPath({ let [path, labelX, labelY] = $derived(
sourceX: $$props.sourceX, getBezierPath({
sourceY: $$props.sourceY, sourceX: props.sourceX,
targetX: $$props.targetX, sourceY: props.sourceY,
targetY: $$props.targetY, targetX: props.targetX,
sourcePosition: $$props.sourcePosition, targetY: props.targetY,
targetPosition: $$props.targetPosition sourcePosition: props.sourcePosition,
}); targetPosition: props.targetPosition
})
);
const edges = useEdges(); const edges = useEdges();
function onClick() { function onClick() {
edges.update((eds) => eds.filter((e) => e.id !== $$props.id)); edges.current = edges.current.filter((e) => e.id !== props.id);
} }
</script> </script>
<BaseEdge {path} {labelX} {labelY} {...$$props} /> <BaseEdge {path} {labelX} {labelY} {...props} />
<EdgeLabelRenderer> <EdgeLabel x={labelX} y={labelY} selectEdgeOnClick>
<button <button class="edge-button" onclick={onClick}> </button>
style:transform={`translate(-50%,-50%) translate(${labelX}px,${labelY}px)`} </EdgeLabel>
class="edge-button"
on:click={onClick}
>
</button>
</EdgeLabelRenderer>
<style> <style>
.edge-button { .edge-button {
border-radius: 50%; border-radius: 50%;
position: absolute;
border: none; border: none;
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
@@ -48,7 +37,6 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 8px; font-size: 8px;
pointer-events: all;
cursor: pointer; cursor: pointer;
background-color: #eee; background-color: #eee;
} }
@@ -1,12 +1,21 @@
<script lang="ts"> <script lang="ts">
import { Handle, Position, type BuiltInNode, type NodeProps } from '@xyflow/svelte'; import {
Handle,
Position,
useOnSelectionChange,
type BuiltInNode,
type NodeProps
} from '@xyflow/svelte';
type $$Props = NodeProps<BuiltInNode>; let {
$$restProps; data = { label: 'Node' },
positionAbsoluteX = 0,
positionAbsoluteY = 0
}: NodeProps<BuiltInNode> = $props();
export let data: { label: string } = { label: 'Node' }; useOnSelectionChange(({ nodes, edges }) => {
export let positionAbsoluteX: number = 0; console.log('on selection changed via hook', { nodes, edges });
export let positionAbsoluteY: number = 0; });
</script> </script>
<div class="custom"> <div class="custom">
@@ -1,15 +1,12 @@
<script lang="ts"> <script lang="ts">
import type { BuiltInNode, NodeProps } from '@xyflow/svelte'; import type { BuiltInNode, NodeProps } from '@xyflow/svelte';
type $$Props = NodeProps<BuiltInNode>; let { data = { label: 'Node' } }: NodeProps<BuiltInNode> = $props();
$$restProps;
export let data: { label: string } = { label: 'Node' };
</script> </script>
<div class="custom"> <div class="custom">
<div>{data.label}</div> <div>{data.label}</div>
<div class="custom-drag-handle" style="width:15px;height:15px;background:#222;" /> <div class="custom-drag-handle" style="width:15px;height:15px;background:#222;"></div>
</div> </div>
<style> <style>
@@ -0,0 +1,261 @@
<script lang="ts">
import {
SvelteFlow,
Controls,
Background,
BackgroundVariant,
MiniMap,
Panel,
SelectionMode,
type NodeTypes,
type EdgeTypes,
type Node,
type Edge,
ConnectionMode,
ControlButton,
type FitViewOptions,
useSvelteFlow,
Position
} from '@xyflow/svelte';
import CustomNode from './CustomNode.svelte';
import CustomNodeDragHandle from './CustomNodeDragHandle.svelte';
import CustomEdge from './CustomEdge.svelte';
import '@xyflow/svelte/dist/style.css';
import InitTracker from './InitTracker.svelte';
const nodeTypes: NodeTypes = {
custom: CustomNode,
dragHandle: CustomNodeDragHandle
};
const edgeTypes: EdgeTypes = {
custom: CustomEdge
};
const fitViewOptions: FitViewOptions = {
padding: 0.2,
nodes: [{ id: '1' }, { id: '2' }]
};
let nodes = $state.raw<Node[]>([
{
id: '1',
type: 'input',
data: { label: 'Input Node' },
position: { x: 150, y: 5 }
},
{
id: '2',
type: 'default',
data: { label: 'Node' },
position: { x: 0, y: 150 },
selectable: false
},
{
id: 'A',
type: 'default',
data: { label: 'Styled with class' },
class: 'custom-style',
position: { x: 150, y: 150 }
},
{
id: 'D',
type: 'default',
data: { label: 'Not draggable' },
position: { x: 150, y: 200 },
draggable: false
},
{
id: '3',
type: 'output',
data: { label: 'Output Node' },
position: { x: 300, y: 150 }
},
{
id: 'B',
type: 'default',
data: { label: 'Styled with style' },
style: 'border: 2px solid #ff5050;',
height: 55,
position: { x: 450, y: 150 }
},
{
id: 'C',
type: 'dragHandle',
data: { label: 'custom drag handle' },
dragHandle: '.custom-drag-handle',
position: { x: 450, y: 250 }
},
{
id: '4',
type: 'custom',
data: { label: 'Custom Node' },
position: { x: 150, y: 300 }
},
{
id: 'hideunhide',
data: { label: 'HIDE ME' },
position: { x: 300, y: 75 },
hidden: true
}
]);
let edges = $state.raw<Edge[]>([
{
id: '1-2',
type: 'default',
source: '1',
target: '2',
label: 'Edge Text'
},
{
id: '1-3',
type: 'smoothstep',
source: '1',
target: '3',
selectable: false
},
{
id: '2-4',
type: 'custom',
source: '2',
target: '4',
animated: true
}
]);
function moveNode() {
// We might as well just use updateNode() here
// this is just to show what is required to update a node
const newNode = { ...nodes[0] };
newNode.position.x += 20;
nodes[0] = newNode;
nodes = [...nodes];
}
function changeEdgeType() {
// We might as well just use updateEdge() here
// this is just to show what is required to update an edge
const newEdge = { ...edges[0] };
newEdge.type = newEdge.type === 'default' ? 'smoothstep' : 'default';
edges[0] = newEdge;
edges = [...edges];
}
let { updateNode } = useSvelteFlow();
function hideUnhide() {
updateNode('hideunhide', (node) => ({ hidden: !node.hidden }));
}
// $inspect(edges);
$effect(() => {
// console.log(edges.map((edge) => ({ id: edge.id, selected: edge.selected })));
edges.forEach((edge) => console.log({ id: edge.id, selected: edge.selected }));
});
</script>
<!-- oninit={() => console.log('on init')}
onnodeclick={(event) => console.log('on node click', event)}
onnodepointerenter={(event) => console.log('on node enter', event)}
onnodepointerleave={(event) => console.log('on node leave', event)}
onedgeclick={(event) => console.log('edge click', event)}
onedgepointerenter={(event) => console.log('edge enter', event)}
onedgepointerleave={(event) => console.log('edge leave', event)}
onconnectstart={(event) => console.log('on connect start', event)}
onconnect={(event) => console.log('on connect', event)}
onconnectend={(event) => console.log('on connect end', event)}
onpaneclick={(event) => console.log('on pane click', event)}
onpanecontextmenu={(event) => {
console.log('on pane contextmenu', event);
}}
onnodedrag={(event) => {
console.log('on node drag', event);
}}
onnodedragstart={(event) => {
console.log('on node drag start', event);
}}
onnodedragstop={({ event }) => {
console.log('on node drag stop', event);
}}
onnodecontextmenu={({ event }) => {
event.preventDefault();
console.log('on node contextmenu', event);
}}
onedgecontextmenu={({ event, edge }) => {
event.preventDefault();
console.log('on edge contextmenu', edge);
}}
onselectionclick={(event) => console.log('on selection click', event)}
onselectioncontextmenu={(event) => console.log('on selection contextmenu', event)}
onbeforedelete={async ({ nodes, edges }) => {
console.log('on before delete', nodes, edges);
const deleteElements = confirm('Are you sure you want to delete the selected elements?');
return deleteElements;
}} -->
<SvelteFlow
bind:nodes
bind:edges
{nodeTypes}
{edgeTypes}
fitView
fitViewOptions={{
padding: 0.1,
nodes: [{ id: '1' }, { id: '2' }, { id: '3' }]
}}
minZoom={0}
maxZoom={Infinity}
selectionMode={SelectionMode.Full}
initialViewport={{ x: 100, y: 100, zoom: 2 }}
snapGrid={[25, 25]}
autoPanOnConnect
autoPanOnNodeDrag
connectionMode={ConnectionMode.Strict}
attributionPosition={'top-center'}
deleteKey={['Backspace', 'd']}
onselectionchange={({ nodes, edges }) => {
console.log('on selection changed via prop', { nodes, edges });
}}
selectNodesOnDrag
>
<Controls orientation="horizontal" {fitViewOptions}>
{#snippet before()}
<ControlButton>xy</ControlButton>
{/snippet}
<ControlButton aria-label="log" onclick={() => console.log('control button')}>log</ControlButton
>
</Controls>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Panel position="top-right">
<button onclick={moveNode}>update node pos</button>
<button onclick={changeEdgeType}>update edge type</button>
<button onclick={hideUnhide}>hide/unhide</button>
<button
onclick={() => {
nodes = nodes.map((node) => ({
...node,
sourcePosition: Position.Right,
targetPosition: Position.Left
}));
}}>left-right</button
>
</Panel>
<InitTracker />
</SvelteFlow>
<style>
:root {
/* --background-color: #ffffdd; */
--background-pattern-color: #5050ff;
--xy-minimap-background-color: red;
--xy-minimap-mask-color: rgb(255, 255, 240, 0.6);
--controls-button-background-color: #ddd;
--controls-button-background-color-hover: #ccc;
}
</style>
@@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import { useNodesInitialized, useInitialized } from '@xyflow/svelte'; import { useNodesInitialized, useViewportInitialized } from '@xyflow/svelte';
const nodesInitialized = useNodesInitialized(); const nodesInitialized = useNodesInitialized();
const initialized = useInitialized(); const viewportInitialized = useViewportInitialized();
$: { $effect(() => {
if (nodesInitialized) { if (viewportInitialized.current) {
console.log('viewportInitialized');
}
});
$effect(() => {
if (nodesInitialized.current) {
console.log('nodes initialized'); console.log('nodes initialized');
} }
} });
$: {
if (initialized) {
console.log('initialized');
}
}
</script> </script>
@@ -1,16 +1,17 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Controls, Background, MiniMap, type Node, type Edge } from '@xyflow/svelte'; import { SvelteFlow, Controls, Background, MiniMap, type Node, type Edge } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
const nodes = writable<Node[]>([]); let nodes = $state.raw<Node[]>([]);
const edges = writable<Edge[]>([]); let edges = $state.raw<Edge[]>([]);
const resetNodesArray = () => { const resetNodesArray = () => {
const node1: Node = { const node1: Node = {
id: 'c1', id: 'c1',
data: { label: '1' }, data: { label: '1' },
width: 100,
height: 100,
position: { x: 0, y: 0 } position: { x: 0, y: 0 }
}; };
@@ -20,14 +21,14 @@
position: { x: 100, y: 0 } position: { x: 100, y: 0 }
}; };
$nodes = [node1, node2]; nodes = [node1, node2];
}; };
</script> </script>
<button on:click={resetNodesArray}> Reset </button> <button onclick={resetNodesArray}> Reset </button>
<div style:height="100vh"> <div style:height="100vh">
<SvelteFlow {nodes} {edges} fitView> <SvelteFlow bind:nodes bind:edges fitView>
<Controls /> <Controls />
<Background /> <Background />
<MiniMap /> <MiniMap />
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -29,7 +28,8 @@
id, id,
data, data,
position, position,
type: 'default' type: 'default',
selected: true
}; };
nodeItems.push(node); nodeItems.push(node);
@@ -46,11 +46,11 @@
} }
} }
const nodes = writable(nodeItems); let nodes = $state.raw(nodeItems);
const edges = writable(edgeItems); let edges = $state.raw(edgeItems);
</script> </script>
<SvelteFlow {nodes} {edges} fitView minZoom={0.2}> <SvelteFlow bind:nodes bind:edges fitView minZoom={0.2} onlyRenderVisibleElements>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Lines} /> <Background variant={BackgroundVariant.Lines} />
<MiniMap /> <MiniMap />
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -7,7 +6,8 @@
BackgroundVariant, BackgroundVariant,
MiniMap, MiniMap,
type NodeTypes, type NodeTypes,
type Node type Node,
type Edge
} from '@xyflow/svelte'; } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
@@ -18,7 +18,7 @@
default: DebugNode default: DebugNode
}; };
const nodes = writable<Node[]>([ let nodes = $state.raw<Node[]>([
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -95,7 +95,7 @@
} }
]); ]);
const edges = writable([ let edges = $state.raw<Edge[]>([
{ {
id: 'e1-2', id: 'e1-2',
source: '1', source: '1',
@@ -115,13 +115,9 @@
{ id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 }, { id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' } { id: 'e4b1-4b2', source: '4b1', target: '4b2' }
]); ]);
$: {
console.log($nodes);
}
</script> </script>
<SvelteFlow {nodes} {edges} {nodeTypes} fitView minZoom={0.1} maxZoom={2.5}> <SvelteFlow bind:nodes bind:edges {nodeTypes} fitView minZoom={0.1} maxZoom={2.5}>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -3,12 +3,23 @@
type $$Props = NodeProps; type $$Props = NodeProps;
export let id: string; interface Props {
export let positionAbsoluteX: number = 0; id: string;
export let positionAbsoluteY: number = 0; positionAbsoluteX?: number;
export let zIndex: number = 0; positionAbsoluteY?: number;
zIndex?: number;
[key: string]: any
}
$$restProps; let {
id,
positionAbsoluteX = 0,
positionAbsoluteY = 0,
zIndex = 0,
...rest
}: Props = $props();
rest;
</script> </script>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -12,7 +11,7 @@
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
const nodes = writable([ let nodes = $state.raw([
{ {
id: 'A', id: 'A',
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
@@ -20,27 +19,25 @@
}, },
{ id: 'B', position: { x: 0, y: 100 }, data: { label: 'B' } } { id: 'B', position: { x: 0, y: 100 }, data: { label: 'B' } }
]); ]);
const edges = writable([{ id: 'ab', source: 'A', target: 'B' }]);
const viewport = writable<Viewport>({ x: 100, y: 100, zoom: 1.25 }); let edges = $state.raw([{ id: 'ab', source: 'A', target: 'B' }]);
let viewport = $state<Viewport>({ x: 100, y: 100, zoom: 5 });
const { fitView } = useSvelteFlow(); const { fitView } = useSvelteFlow();
const updateViewport = () => { const updateViewport = () => {
$viewport.x += 10; viewport.x += 10;
$viewport = $viewport;
}; };
viewport.subscribe((vp) => { $inspect(viewport);
console.log('viewport update', vp);
});
</script> </script>
<SvelteFlow {nodes} {edges} {viewport}> <SvelteFlow bind:nodes bind:edges bind:viewport>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<Panel> <Panel>
<button on:click={updateViewport}>update viewport</button> <button onclick={updateViewport}>update viewport</button>
<button on:click={() => fitView()}>fitView</button> <button onclick={() => fitView()}>fitView</button>
</Panel> </Panel>
</SvelteFlow> </SvelteFlow>
@@ -1,11 +1,11 @@
<script context="module" lang="ts"> <script module lang="ts">
import type { Node } from '@xyflow/svelte'; import type { Node } from '@xyflow/svelte';
type TextNodeType = Node<{ text: string }, 'text'>; type TextNodeType = Node<{ text: string }, 'text'>;
type UppercaseNodeType = Node<{ text: string }, 'uppercase'>; type UppercaseNodeType = Node<{ text: string }, 'uppercase'>;
type ResultNodeType = Node<{}, 'result'>; type ResultNodeType = Node<{}, 'result'>;
export function isTextNode(node: any): node is TextNodeType | UppercaseNode { export function isTextNode(node: any): node is TextNodeType | UppercaseNodeType {
return !node || !node.type ? false : node.type === 'text' || node.type === 'uppercase'; return !node || !node.type ? false : node.type === 'text' || node.type === 'uppercase';
} }
@@ -13,7 +13,6 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -35,7 +34,7 @@
result: ResultNode result: ResultNode
}; };
const nodes = writable<MyNode[]>([ let nodes = $state.raw<MyNode[]>([
{ {
id: '1', id: '1',
type: 'text', type: 'text',
@@ -60,7 +59,6 @@
}, },
position: { x: 0, y: 100 } position: { x: 0, y: 100 }
}, },
{ {
id: '3', id: '3',
type: 'result', type: 'result',
@@ -69,7 +67,7 @@
} }
]); ]);
const edges = writable<Edge[]>([ let edges = $state.raw<Edge[]>([
{ {
id: 'e1-1a', id: 'e1-1a',
source: '1', source: '1',
@@ -88,7 +86,7 @@
]); ]);
</script> </script>
<SvelteFlow {nodes} {edges} {nodeTypes} fitView> <SvelteFlow bind:nodes bind:edges {nodeTypes} fitView>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -4,22 +4,22 @@
Position, Position,
useNodeConnections, useNodeConnections,
useNodesData, useNodesData,
type NodeProps type NodeProps,
type Node
} from '@xyflow/svelte'; } from '@xyflow/svelte';
import { isTextNode, type MyNode } from './+page.svelte'; import { isTextNode, type MyNode } from './+page.svelte';
type $$Props = NodeProps; let { id }: NodeProps = $props();
export let id: $$Props['id'];
$$restProps;
const connections = useNodeConnections({ const connections = useNodeConnections({
id: id, id: id,
handleType: 'target' handleType: 'target'
}); });
$: nodeData = useNodesData<MyNode>($connections.map((connection) => connection.source)); let nodeData = $derived(
$: textNodes = $nodeData.filter(isTextNode); useNodesData<MyNode>(connections.current.map((connection) => connection.source))
);
let textNodes = $derived(nodeData.current.filter(isTextNode));
</script> </script>
<div class="custom"> <div class="custom">
@@ -1,13 +1,9 @@
<script lang="ts"> <script lang="ts">
import { Handle, Position, type NodeProps, useSvelteFlow } from '@xyflow/svelte'; import { Handle, Position, type NodeProps, useSvelteFlow } from '@xyflow/svelte';
type $$Props = NodeProps; let { id, data }: NodeProps = $props();
export let id: $$Props['id'];
export let data: $$Props['data'];
const { updateNodeData } = useSvelteFlow(); const { updateNodeData } = useSvelteFlow();
$$restProps;
</script> </script>
<div class="custom"> <div class="custom">
@@ -15,7 +11,7 @@
<div> <div>
<input <input
value={data.text} value={data.text}
on:input={(evt) => updateNodeData(id, { text: evt.currentTarget.value })} oninput={(evt) => updateNodeData(id, { text: evt.currentTarget.value })}
/> />
</div> </div>
<Handle type="source" position={Position.Right} /> <Handle type="source" position={Position.Right} />
@@ -5,15 +5,12 @@
useNodeConnections, useNodeConnections,
useNodesData, useNodesData,
useSvelteFlow, useSvelteFlow,
type Node,
type NodeProps type NodeProps
} from '@xyflow/svelte'; } from '@xyflow/svelte';
import { isTextNode, type MyNode } from './+page.svelte'; import { isTextNode, type MyNode } from './+page.svelte';
type $$Props = NodeProps; let { id }: NodeProps<Node<{ text: string }>> = $props();
export let id: $$Props['id'];
export let data: $$Props['data'];
$$restProps;
const { updateNodeData } = useSvelteFlow(); const { updateNodeData } = useSvelteFlow();
const connections = useNodeConnections({ const connections = useNodeConnections({
@@ -21,20 +18,17 @@
handleType: 'target' handleType: 'target'
}); });
$: nodeData = useNodesData<MyNode>($connections[0]?.source); let nodeData = $derived(useNodesData<MyNode>(connections.current[0]?.source));
$: textNode = isTextNode($nodeData) ? $nodeData : null; let textNodeData = $derived(isTextNode(nodeData.current) ? nodeData.current.data.text : null);
$: console.log(textNode?.data, data); $effect.pre(() => {
const input = textNodeData?.toUpperCase() ?? '';
$: {
const input = textNode?.data.text.toUpperCase() ?? '';
updateNodeData(id, { text: input }); updateNodeData(id, { text: input });
console.log('updatedNodeData with', input); });
}
</script> </script>
<div class="custom"> <div class="custom">
<Handle type="target" position={Position.Left} isConnectable={$connections.length === 0} /> <Handle type="target" position={Position.Left} isConnectable={connections.current.length === 0} />
<div>uppercase transform</div> <div>uppercase transform</div>
<Handle type="source" position={Position.Right} /> <Handle type="source" position={Position.Right} />
</div> </div>
@@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Controls, Background, BackgroundVariant, MiniMap } from '@xyflow/svelte'; import { SvelteFlow, Controls, Background, BackgroundVariant, MiniMap } from '@xyflow/svelte';
import Sidebar from './Sidebar.svelte'; import Sidebar from './Sidebar.svelte';
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
const nodes = writable([ let nodes = $state.raw([
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -27,7 +26,7 @@
} }
]); ]);
const edges = writable([ let edges = $state.raw([
{ {
id: '1-2', id: '1-2',
type: 'default', type: 'default',
@@ -45,7 +44,7 @@
</script> </script>
<main> <main>
<SvelteFlow {nodes} {edges} fitView> <SvelteFlow bind:nodes bind:edges fitView>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { useEdges, useNodes, useSvelteFlow } from '@xyflow/svelte'; import { useEdges, useNodes, useSvelteFlow, useViewport } from '@xyflow/svelte';
const { const {
zoomIn, zoomIn,
@@ -9,53 +9,59 @@
setCenter, setCenter,
setViewport, setViewport,
getViewport, getViewport,
viewport,
toObject, toObject,
deleteElements deleteElements
} = useSvelteFlow(); } = useSvelteFlow();
const nodes = useNodes(); let nodes = useNodes();
const edges = useEdges(); let edges = useEdges();
let viewport = useViewport();
const deleteNode = () => { const deleteNode = () => {
$nodes.shift(); //TODO: do we really want to allow this?
$nodes = $nodes; // nodes.shift();
nodes.current.shift();
nodes.current = [...nodes.current];
}; };
</script> </script>
<aside> <aside>
<div class="label">Functions:</div> <div class="label">Functions:</div>
<button on:click={() => zoomIn()}>zoom in</button> <button onclick={() => zoomIn()}>zoom in</button>
<button on:click={() => zoomOut({ duration: 1000 })}>zoom out transition</button> <button onclick={() => zoomOut({ duration: 1000 })}>zoom out transition</button>
<button on:click={() => setZoom(2)}>set zoom</button> <button onclick={() => setZoom(2)}>set zoom</button>
<button on:click={() => fitView()}>fitView</button> <button onclick={() => fitView()}>fitView</button>
<button on:click={() => setCenter(0, 0)}>setCenter 0, 0</button> <button onclick={() => setCenter(0, 0)}>setCenter 0, 0</button>
<button on:click={() => setViewport({ x: 100, y: 100, zoom: 2 })}>setViewport</button> <button onclick={() => setViewport({ x: 100, y: 100, zoom: 2 })}>setViewport</button>
<button on:click={() => console.log(getViewport())}>getViewport</button> <button onclick={() => console.log(getViewport())}>getViewport</button>
<button on:click={() => deleteElements({ edges: $edges.map((edge) => ({ id: edge.id })) })} <button onclick={() => deleteElements({ edges: edges.current.map((edge) => ({ id: edge.id })) })}
>delete edges</button >delete edges</button
> >
<button on:click={() => deleteElements({ nodes: [{ id: $nodes[0].id }] })}>delete node</button> <button onclick={() => deleteElements({ nodes: [{ id: nodes.current[0].id }] })}
<button on:click={() => deleteElements({ nodes: $nodes.map((node) => ({ id: node.id })) })} >delete node</button
>
<button onclick={() => deleteElements({ nodes: nodes.current.map((node) => ({ id: node.id })) })}
>deleteElements</button >deleteElements</button
> >
<button on:click={() => deleteNode()}>delete via store</button> <button onclick={() => deleteNode()}>delete via store</button>
<button <button
on:click={() => { onclick={() => {
const { nodes, edges, viewport } = toObject(); const { nodes, edges, viewport } = toObject();
console.log(nodes, edges, viewport); console.log(nodes, edges, viewport);
}}>toObject</button }}>toObject</button
> >
<div class="label">Nodes:</div> <div class="label">Nodes:</div>
{#each $nodes as node (node.id)} {#each nodes.current as node (node.id)}
<div>id: {node.id} | x: {node.position.x.toFixed(1)} y: {node.position.y.toFixed(1)}</div> <div>id: {node.id} | x: {node.position.x.toFixed(1)} y: {node.position.y.toFixed(1)}</div>
{/each} {/each}
<div class="label">Viewport:</div> <div class="label">Viewport:</div>
<div> <div>
x: {$viewport.x.toFixed(1)} y: {$viewport.y.toFixed(1)} zoom: {$viewport.zoom.toFixed(1)} x: {viewport.current.x.toFixed(1)} y: {viewport.current.y.toFixed(1)} zoom: {viewport.current.zoom.toFixed(
1
)}
</div> </div>
</aside> </aside>
@@ -1,23 +1,20 @@
<script lang="ts"> <script lang="ts">
import { Handle, Position, type NodeProps, useUpdateNodeInternals } from '@xyflow/svelte'; import { Handle, Position, type NodeProps, useUpdateNodeInternals } from '@xyflow/svelte';
type $$Props = NodeProps; let { id }: NodeProps = $props();
export let id: $$Props['id'];
$$restProps;
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
$: handleCount = 1; let handleCount = $state(1);
const onClick = () => { const onClick = () => {
handleCount += 1; handleCount += 1;
updateNodeInternals(id); updateNodeInternals();
}; };
</script> </script>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
<button on:click={onClick}>add handle</button> <button onclick={onClick}>add handle</button>
{#each Array.from({ length: handleCount }) as handle, i} {#each Array.from({ length: handleCount }) as handle, i}
<Handle type="source" position={Position.Bottom} id={`${i}`} style={`left: ${i * 10}px;`} /> <Handle type="source" position={Position.Bottom} id={`${i}`} style={`left: ${i * 10}px;`} />
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -19,7 +18,7 @@
custom: CustomNode custom: CustomNode
}; };
const nodes = writable([ let nodes = $state.raw([
{ {
id: '1', id: '1',
type: 'custom', type: 'custom',
@@ -40,7 +39,7 @@
} }
]); ]);
const edges = writable([ let edges = $state.raw([
{ {
id: '1-2', id: '1-2',
source: '1', source: '1',
@@ -56,17 +55,19 @@
const { updateNode } = useSvelteFlow(); const { updateNode } = useSvelteFlow();
const updateNodePosition = () => { const updateNodePosition = () => {
updateNode('1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } })); updateNode('1', (node) => ({
position: { x: node.position.x + 10, y: node.position.y }
}));
}; };
</script> </script>
<main> <main>
<SvelteFlow {nodes} {edges} {nodeTypes} fitView> <SvelteFlow bind:nodes bind:edges {nodeTypes} fitView>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
<Panel><button on:click={updateNodePosition}>update node</button></Panel> <Panel><button onclick={updateNodePosition}>update node</button></Panel>
</SvelteFlow> </SvelteFlow>
</main> </main>
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store';
import { import {
SvelteFlow, SvelteFlow,
Controls, Controls,
@@ -17,7 +16,7 @@
targetPosition: Position.Left targetPosition: Position.Left
}; };
const nodes = writable([ let nodes = $state.raw([
{ {
id: '0', id: '0',
position: { x: 0, y: 150 }, position: { x: 0, y: 150 },
@@ -29,7 +28,7 @@
{ id: 'C', position: { x: 250, y: 300 }, data: { label: 'C' }, ...nodeDefaults } { id: 'C', position: { x: 250, y: 300 }, data: { label: 'C' }, ...nodeDefaults }
]); ]);
const edges = writable([]); let edges = $state.raw([]);
const isValidConnection: IsValidConnection = (connection) => connection.target === 'B'; const isValidConnection: IsValidConnection = (connection) => connection.target === 'B';
@@ -39,8 +38,8 @@
</script> </script>
<SvelteFlow <SvelteFlow
{nodes} bind:nodes
{edges} bind:edges
fitView fitView
minZoom={0.1} minZoom={0.1}
maxZoom={2.5} maxZoom={2.5}
@@ -3,7 +3,11 @@
import Flow from './Flow.svelte'; import Flow from './Flow.svelte';
export let data: { flowConfig: FlowConfig }; let {
data
}: {
data: { flowConfig: FlowConfig };
} = $props();
</script> </script>
<SvelteFlowProvider> <SvelteFlowProvider>
@@ -1,19 +1,25 @@
<script lang="ts"> <script lang="ts">
import { writable } from 'svelte/store'; import {
import { Background, Controls, MiniMap, Panel, SvelteFlow } from '@xyflow/svelte'; Background,
Controls,
MiniMap,
Panel,
SvelteFlow,
type SvelteFlowProps
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css'; import '@xyflow/svelte/dist/style.css';
export let flowConfig: FlowConfig; let { flowConfig }: { flowConfig: FlowConfig } = $props();
// Create writables here so it is easier to create test cases // Create writables here so it is easier to create test cases
const nodes = writable(flowConfig.flowProps?.nodes); let nodes = $state.raw(flowConfig.flowProps?.nodes ?? []);
const edges = writable(flowConfig.flowProps?.edges); let edges = $state.raw(flowConfig.flowProps?.edges ?? []);
const flowProps = { ...flowConfig.flowProps, nodes, edges }; const flowProps: SvelteFlowProps = { ...flowConfig.flowProps };
</script> </script>
<SvelteFlow {...flowProps}> <SvelteFlow {...flowProps} bind:nodes bind:edges>
{#if flowConfig.panelProps} {#if flowConfig.panelProps}
<Panel {...flowConfig.panelProps} /> <Panel {...flowConfig.panelProps} />
{/if} {/if}
+4 -2
View File
@@ -5,8 +5,10 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = { const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors // Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess({ script: true }),
compilerOptions: {
runes: true
},
kit: { kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter. // If your environment is not supported or you settled on a specific environment, switch out the adapter.
+4 -1
View File
@@ -8,7 +8,10 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true "strict": true,
"target": "ES2022",
"verbatimModuleSyntax": true,
"isolatedModules": true
} }
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// //
+4
View File
@@ -3,6 +3,10 @@ import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()], plugins: [sveltekit()],
build: {
sourcemap: true,
minify: false
},
server: { server: {
fs: { fs: {
// Allow serving files from one level up to the project root // Allow serving files from one level up to the project root
@@ -23,9 +23,9 @@ export const builtinNodeTypes: NodeTypes = {
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>( export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
node: InternalNode<NodeType> node: InternalNode<NodeType>
): { ): {
width: number | string | undefined; width: number | string | undefined;
height: number | string | undefined; height: number | string | undefined;
} { } {
if (node.internals.handleBounds === undefined) { if (node.internals.handleBounds === undefined) {
return { return {
width: node.width ?? node.initialWidth ?? node.style?.width, width: node.width ?? node.initialWidth ?? node.style?.width,
@@ -61,9 +61,9 @@ export function NodesSelection<NodeType extends Node>({
const onContextMenu = onSelectionContextMenu const onContextMenu = onSelectionContextMenu
? (event: MouseEvent) => { ? (event: MouseEvent) => {
const selectedNodes = store.getState().nodes.filter((n) => n.selected); const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes); onSelectionContextMenu(event, selectedNodes);
} }
: undefined; : undefined;
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
-14
View File
@@ -1,14 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
dist
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
-30
View File
@@ -1,30 +0,0 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};
+16
View File
@@ -1,5 +1,21 @@
# @xyflow/svelte # @xyflow/svelte
## 1.0.0
Yeah! Svelte Flow 1.0. is here 🚀 It is based on Svelte 5 and introduces a lot of new features and improvements. Please refer to the [migration guide](https://svelteflow.dev/learn/troubleshooting/migrate-to-v1) to see how to upgrade.
### New features
- **[Reconnect edges](https://svelteflow.dev/examples/edges/reconnect-edge)**: You can reconnect your edges by using the new `<EdgeReconnectAnchor />` component. It can be used to add custom reconnection points on custom edges.
- **Keyboard navigation & A11y**: We added support for keyboard navigation and improved accessibility for screen readers. You can now tab through nodes and edges and move nodes with the arrow keys. Can be disabled via [**disableKeyboardA11y**](https://svelteflow.dev/api-reference/svelte-flow#disablekeyboarda11y)
- **[Click connect](https://svelteflow.dev/examples/edges/click-connect)**: You can now create a new connection by clicking on a handle one by one.
- **[Enhanced ViewportPortal](https://svelteflow.dev/api-reference/components/viewport-portal)**: You can now decide if you want to render something below or above the nodes & edges in the viewport.
- **Improved [fitView](https://svelteflow.dev/api-reference/hooks/use-svelte-flow#fitview)**: We finetuned the `fitView` function to better work with dynamically added nodes.
- **colorModeSSR** prop: You can pass a fallback color mode for server side rendering when colorMode is set to 'system'.
- [**elevateNodesOnSelect**](https://svelteflow.dev/api-reference/svelte-flow#elevateNodesOnSelect) & [**elevateEdgesOnSelect**](https://svelteflow.dev/api-reference/svelte-flow#elevateEdgesOnSelect): Control if nodes & edges should be elevated via z-index when selected.
- [**noDragClass, noWheelClass, noPanClass**](https://svelteflow.dev/api-reference/svelte-flow#style-props): You can now modify the class name used to disable dragging, panning and zooming.
- [**onselectionchange**](https://svelteflow.dev/api-reference/svelte-flow#onselectionchange) & [**useOnSelectionChange**](https://svelteflow.dev/api-reference/hooks/use-on-selection-change): You can now listen to selection changes via a callback
## 0.1.39 ## 0.1.39
### Patch Changes ### Patch Changes
+39
View File
@@ -0,0 +1,39 @@
import globals from 'globals';
import js from '@eslint/js';
import ts from 'typescript-eslint';
import svelte from 'eslint-plugin-svelte';
import eslintConfigPrettier from 'eslint-config-prettier';
import svelteConfig from './svelte.config.js';
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
eslintConfigPrettier,
{
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module',
globals: {
...globals.browser,
...globals.es2017,
...globals.node
}
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
ignores: ['eslint.config.js', 'svelte.config.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
},
{
ignores: ['.svelte-kit/*', 'build/*', 'dist/*'] // Add common ignore patterns
}
);
+32 -29
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/svelte", "name": "@xyflow/svelte",
"version": "0.1.39", "version": "1.0.0",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.", "description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [ "keywords": [
"svelte", "svelte",
@@ -51,45 +51,48 @@
"access": "public" "access": "public"
}, },
"dependencies": { "dependencies": {
"@svelte-put/shortcut": "3.1.1", "@svelte-put/shortcut": "^4.1.0",
"@xyflow/system": "workspace:*", "@xyflow/system": "workspace:*"
"classcat": "^5.0.4"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1", "@eslint/js": "^9.24.0",
"@sveltejs/kit": "^2.5.4", "@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/package": "^2.3.0", "@sveltejs/kit": "^2.20.7",
"@typescript-eslint/eslint-plugin": "^7.2.0", "@sveltejs/package": "^2.3.11",
"@typescript-eslint/parser": "^7.2.0", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"autoprefixer": "^10.4.18", "@typescript-eslint/eslint-plugin": "^8.30.1",
"cssnano": "^6.1.0", "@typescript-eslint/parser": "^8.30.1",
"dotenv": "^16.4.5", "autoprefixer": "^10.4.21",
"eslint": "^8.57.0", "cssnano": "^7.0.6",
"eslint-config-prettier": "^9.1.0", "dotenv": "^16.5.0",
"eslint-plugin-svelte": "^2.35.1", "eslint": "^9.24.0",
"postcss": "^8.4.35", "eslint-config-prettier": "^10.1.2",
"postcss-cli": "^11.0.0", "eslint-plugin-svelte": "^3.5.1",
"globals": "^16.0.0",
"postcss": "^8.5.3",
"postcss-cli": "^11.0.1",
"postcss-combine-duplicated-selectors": "^10.0.3", "postcss-combine-duplicated-selectors": "^10.0.3",
"postcss-import": "^16.0.1", "postcss-import": "^16.1.0",
"postcss-nested": "^6.0.1", "postcss-nested": "^7.0.2",
"postcss-rename": "^0.6.1", "postcss-rename": "^0.6.1",
"prettier": "^3.2.5", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.2.2", "prettier-plugin-svelte": "^3.3.3",
"svelte": "^4.2.12", "svelte": "^5.27.0",
"svelte-check": "^3.6.7", "svelte-check": "^4.1.6",
"svelte-eslint-parser": "^0.33.1", "svelte-eslint-parser": "^1.1.2",
"svelte-preprocess": "^5.1.3", "svelte-preprocess": "^6.0.3",
"tslib": "^2.6.2", "tslib": "^2.8.1",
"typescript": "5.4.5" "typescript": "^5.8.3",
"typescript-eslint": "^8.30.1"
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" "svelte": "^5.25.0"
}, },
"files": [ "files": [
"dist" "dist"
], ],
"typesVersions": { "typesVersions": {
">4.0": { ">5.0": {
"index": [ "index": [
"./dist/lib/index.d.ts" "./dist/lib/index.d.ts"
] ]
+27 -22
View File
@@ -1,10 +1,10 @@
import { get } from 'svelte/store'; import { XYDrag, type NodeBase, type OnDrag, type XYDragParams } from '@xyflow/system';
import { XYDrag, type OnDrag } from '@xyflow/system';
import type { SvelteFlowStore } from '$lib/store/types'; import type { SvelteFlowStore } from '$lib/store/types';
import type { Node, Edge, NodeTargetEventWithPointer } from '$lib/types';
export type UseDragParams = { export type UseDragParams<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
store: SvelteFlowStore; store: SvelteFlowStore<NodeType, EdgeType>;
disabled?: boolean; disabled?: boolean;
noDragClass?: string; noDragClass?: string;
handleSelector?: string; handleSelector?: string;
@@ -17,7 +17,10 @@ export type UseDragParams = {
onNodeMouseDown?: (id: string) => void; onNodeMouseDown?: (id: string) => void;
}; };
export default function drag(domNode: Element, params: UseDragParams) { export default function drag<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
domNode: Element,
params: UseDragParams<NodeType, EdgeType>
) {
const { store, onDrag, onDragStart, onDragStop, onNodeMouseDown } = params; const { store, onDrag, onDragStart, onDragStop, onNodeMouseDown } = params;
const dragInstance = XYDrag({ const dragInstance = XYDrag({
onDrag, onDrag,
@@ -25,32 +28,34 @@ export default function drag(domNode: Element, params: UseDragParams) {
onDragStop, onDragStop,
onNodeMouseDown, onNodeMouseDown,
getStoreItems: () => { getStoreItems: () => {
const snapGrid = get(store.snapGrid); const { snapGrid, viewport } = store;
const vp = get(store.viewport);
return { return {
nodes: get(store.nodes), nodes: store.nodes satisfies NodeBase[],
nodeLookup: get(store.nodeLookup), nodeLookup: store.nodeLookup,
edges: get(store.edges), edges: store.edges,
nodeExtent: get(store.nodeExtent), nodeExtent: store.nodeExtent,
snapGrid: snapGrid ? snapGrid : [0, 0], snapGrid: snapGrid ? snapGrid : [0, 0],
snapToGrid: !!snapGrid, snapToGrid: !!snapGrid,
nodeOrigin: get(store.nodeOrigin), nodeOrigin: store.nodeOrigin,
multiSelectionActive: get(store.multiselectionKeyPressed), multiSelectionActive: store.multiselectionKeyPressed,
domNode: get(store.domNode), domNode: store.domNode,
transform: [vp.x, vp.y, vp.zoom], transform: [viewport.x, viewport.y, viewport.zoom],
autoPanOnNodeDrag: get(store.autoPanOnNodeDrag), autoPanOnNodeDrag: store.autoPanOnNodeDrag,
nodesDraggable: get(store.nodesDraggable), nodesDraggable: store.nodesDraggable,
selectNodesOnDrag: get(store.selectNodesOnDrag), selectNodesOnDrag: store.selectNodesOnDrag,
nodeDragThreshold: get(store.nodeDragThreshold), nodeDragThreshold: store.nodeDragThreshold,
unselectNodesAndEdges: store.unselectNodesAndEdges, unselectNodesAndEdges: store.unselectNodesAndEdges,
updateNodePositions: store.updateNodePositions, updateNodePositions: store.updateNodePositions,
onSelectionDrag: store.onselectiondrag,
onSelectionDragStart: store.onselectiondragstart,
onSelectionDragStop: store.onselectiondragstop,
panBy: store.panBy panBy: store.panBy
}; };
} }
}); } as XYDragParams<NodeTargetEventWithPointer<MouseEvent | TouchEvent, NodeType>>);
function updateDrag(domNode: Element, params: UseDragParams) { function updateDrag(domNode: Element, params: UseDragParams<NodeType, EdgeType>) {
if (params.disabled) { if (params.disabled) {
dragInstance.destroy(); dragInstance.destroy();
return; return;
@@ -69,7 +74,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
updateDrag(domNode, params); updateDrag(domNode, params);
return { return {
update(params: UseDragParams) { update(params: UseDragParams<NodeType, EdgeType>) {
updateDrag(domNode, params); updateDrag(domNode, params);
}, },
destroy() { destroy() {
@@ -1,31 +1 @@
type PortalOptions = { export { portal } from './portal.svelte';
target?: string;
domNode: Element | null;
};
function tryToMount(node: Element, domNode: Element | null, target: string | undefined) {
if (!domNode) {
return;
}
const targetEl = target ? domNode.querySelector(target) : domNode;
if (targetEl) {
targetEl.appendChild(node);
}
}
export default function (node: Element, { target, domNode }: PortalOptions) {
tryToMount(node, domNode, target);
return {
async update({ target, domNode }: PortalOptions) {
tryToMount(node, domNode, target);
},
destroy() {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}
};
}
@@ -0,0 +1,38 @@
import { useStore } from '$lib/store';
type Portal = 'viewport-back' | 'viewport-front' | 'root' | 'edge-labels';
function tryToMount(node: Element, domNode: Element | null, target: Portal | undefined) {
if (!target || !domNode) {
return;
}
const targetEl = target === 'root' ? domNode : domNode.querySelector(`.svelte-flow__${target}`);
if (targetEl) {
targetEl.appendChild(node);
}
}
export function portal(node: Element, target: Portal | undefined) {
const store = useStore();
let previousTarget: Portal | undefined = target;
tryToMount(node, store.domNode, target);
return {
async update(target: Portal) {
if (target !== previousTarget) {
node.parentNode?.removeChild(node);
previousTarget = target;
}
tryToMount(node, store.domNode, target);
},
destroy() {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}
};
}
+19 -12
View File
@@ -1,4 +1,3 @@
import type { Writable } from 'svelte/store';
import { import {
PanOnScrollMode, PanOnScrollMode,
XYPanZoom, XYPanZoom,
@@ -10,17 +9,16 @@ import {
} from '@xyflow/system'; } from '@xyflow/system';
type ZoomParams = { type ZoomParams = {
viewport: Writable<Viewport>; viewport: Viewport;
initialViewport: Viewport; initialViewport: Viewport;
minZoom: number; minZoom: number;
maxZoom: number; maxZoom: number;
dragging: Writable<boolean>; setPanZoomInstance: (panZoomInstance: PanZoomInstance) => void;
onPanZoomStart?: OnPanZoom; onPanZoomStart?: OnPanZoom;
onPanZoom?: OnPanZoom; onPanZoom?: OnPanZoom;
onPanZoomEnd?: OnPanZoom; onPanZoomEnd?: OnPanZoom;
onPaneContextMenu?: (event: MouseEvent) => void; onPaneContextMenu?: (event: MouseEvent) => void;
translateExtent: CoordinateExtent; translateExtent: CoordinateExtent;
panZoom: Writable<PanZoomInstance | null>;
zoomOnScroll: boolean; zoomOnScroll: boolean;
zoomOnPinch: boolean; zoomOnPinch: boolean;
zoomOnDoubleClick: boolean; zoomOnDoubleClick: boolean;
@@ -38,18 +36,19 @@ type ZoomParams = {
lib: string; lib: string;
paneClickDistance: number; paneClickDistance: number;
onTransformChange: (transform: Transform) => void; onTransformChange: (transform: Transform) => void;
onDraggingChange: (dragging: boolean) => void;
}; };
export default function zoom(domNode: Element, params: ZoomParams) { export default function zoom(domNode: Element, params: ZoomParams) {
const { const {
panZoom,
minZoom, minZoom,
maxZoom, maxZoom,
initialViewport, initialViewport,
viewport,
dragging,
translateExtent, translateExtent,
paneClickDistance paneClickDistance,
setPanZoomInstance,
onDraggingChange,
onTransformChange
} = params; } = params;
const panZoomInstance = XYPanZoom({ const panZoomInstance = XYPanZoom({
@@ -59,11 +58,19 @@ export default function zoom(domNode: Element, params: ZoomParams) {
translateExtent, translateExtent,
viewport: initialViewport, viewport: initialViewport,
paneClickDistance, paneClickDistance,
onDraggingChange: dragging.set onDraggingChange
}); });
const currentViewport = panZoomInstance.getViewport();
viewport.set(currentViewport); const viewport = panZoomInstance.getViewport();
panZoom.set(panZoomInstance); if (
initialViewport.x !== viewport.x ||
initialViewport.y !== viewport.y ||
initialViewport.zoom !== viewport.zoom
) {
onTransformChange([viewport.x, viewport.y, viewport.zoom]);
}
setPanZoomInstance(panZoomInstance);
panZoomInstance.update(params); panZoomInstance.update(params);
@@ -0,0 +1,30 @@
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import type { SvelteFlowStore } from '$lib/store/types';
import type { Node, Edge } from '$lib/types';
import { ARIA_EDGE_DESC_KEY, ARIA_LIVE_MESSAGE, ARIA_NODE_DESC_KEY } from '.';
let { store }: { store: SvelteFlowStore<NodeType, EdgeType> } = $props();
</script>
<div id={`${ARIA_NODE_DESC_KEY}-${store.flowId}`} style="display: none;">
Press enter or space to select a node.
{#if !store.disableKeyboardA11y}
You can then use the arrow keys to move the node around.
{/if}
Press delete to remove it and escape to cancel.
</div>
<div id={`${ARIA_EDGE_DESC_KEY}-${store.flowId}`} style="display: none;">
Press enter or space to select an edge. You can then press delete to remove it or escape to
cancel.
</div>
{#if !store.disableKeyboardA11y}
<div
id={`${ARIA_LIVE_MESSAGE}-${store.flowId}`}
aria-live="assertive"
aria-atomic="true"
style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0; padding: 0; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); clip-path: inset(100%);"
>
{store.ariaLiveMessage}
</div>
{/if}
@@ -0,0 +1,5 @@
export { default as A11yDescriptions } from './A11yDescriptions.svelte';
export const ARIA_NODE_DESC_KEY = 'svelte-flow__node-desc';
export const ARIA_EDGE_DESC_KEY = 'svelte-flow__edge-desc';
export const ARIA_LIVE_MESSAGE = 'svelte-flow__aria-live';
@@ -2,10 +2,7 @@
import { Panel } from '$lib/container/Panel'; import { Panel } from '$lib/container/Panel';
import type { AttributionProps } from './types'; import type { AttributionProps } from './types';
type $$Props = AttributionProps; let { proOptions, position = 'bottom-right' }: AttributionProps = $props();
export let proOptions: $$Props['proOptions'] = undefined;
export let position: $$Props['position'] = 'bottom-right';
</script> </script>
{#if !proOptions?.hideAttribution} {#if !proOptions?.hideAttribution}
@@ -1,48 +0,0 @@
<script lang="ts">
import cc from 'classcat';
import type { BaseEdgeProps } from './types';
import EdgeLabel from '../EdgeLabel/EdgeLabel.svelte';
type $$Props = BaseEdgeProps;
export let id: $$Props['id'] = undefined;
export let path: $$Props['path'];
export let label: $$Props['label'] = undefined;
export let labelX: $$Props['labelX'] = undefined;
export let labelY: $$Props['labelY'] = undefined;
export let labelStyle: $$Props['labelStyle'] = undefined;
export let markerStart: $$Props['markerStart'] = undefined;
export let markerEnd: $$Props['markerEnd'] = undefined;
export let style: $$Props['style'] = undefined;
export let interactionWidth: $$Props['interactionWidth'] = 20;
let className: $$Props['class'] = undefined;
export { className as class };
let interactionWidthValue = interactionWidth === undefined ? 20 : interactionWidth;
</script>
<path
{id}
d={path}
class={cc(['svelte-flow__edge-path', className])}
marker-start={markerStart}
marker-end={markerEnd}
fill="none"
{style}
/>
{#if interactionWidthValue}
<path
d={path}
stroke-opacity={0}
stroke-width={interactionWidthValue}
fill="none"
class="svelte-flow__edge-interaction"
/>
{/if}
{#if label}
<EdgeLabel x={labelX} y={labelY} style={labelStyle}>
{label}
</EdgeLabel>
{/if}
@@ -1 +0,0 @@
export { default as BaseEdge } from './BaseEdge.svelte';
@@ -1,23 +0,0 @@
import type { EdgeProps } from '$lib/types';
export type BaseEdgeProps = Pick<
EdgeProps,
'interactionWidth' | 'label' | 'labelStyle' | 'style'
> & {
id?: string;
/** SVG path of the edge */
path: string;
/** The x coordinate of the label */
labelX?: number;
/** The y coordinate of the label */
labelY?: number;
/** Marker at start of edge
* @example 'url(#arrow)'
*/
markerStart?: string;
/** Marker at end of edge
* @example 'url(#arrow)'
*/
markerEnd?: string;
class?: string;
};
@@ -1,14 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
let _onMount: (() => void) | undefined = undefined;
export { _onMount as onMount };
let _onDestroy: (() => void) | undefined = undefined;
export { _onDestroy as onDestroy };
onMount(() => {
_onMount?.();
return _onDestroy;
});
</script>
@@ -1 +0,0 @@
export { default as CallOnMount } from './CallOnMount.svelte';
@@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import cc from 'classcat'; import type { Component } from 'svelte';
import { useStore } from '$lib/store';
import { import {
ConnectionLineType, ConnectionLineType,
getBezierPath, getBezierPath,
@@ -10,50 +8,69 @@
getStraightPath getStraightPath
} from '@xyflow/system'; } from '@xyflow/system';
export let containerStyle: string = ''; import type { SvelteFlowStore } from '$lib/store/types';
export let style: string = ''; import type { Node, Edge } from '$lib/types';
export let isCustomComponent: boolean = false;
const { width, height, connection, connectionLineType } = useStore(); let {
store = $bindable(),
type,
containerStyle,
style,
LineComponent
}: {
store: SvelteFlowStore<NodeType, EdgeType>;
type: ConnectionLineType;
containerStyle?: string;
style?: string;
LineComponent?: Component;
} = $props();
let path: string | null = null; let path = $derived.by(() => {
if (!store.connection.inProgress) {
return '';
}
$: if ($connection.inProgress && !isCustomComponent) {
const { from, to, fromPosition, toPosition } = $connection;
const pathParams = { const pathParams = {
sourceX: from.x, sourceX: store.connection.from.x,
sourceY: from.y, sourceY: store.connection.from.y,
sourcePosition: fromPosition, sourcePosition: store.connection.fromPosition,
targetX: to.x, targetX: store.connection.to.x,
targetY: to.y, targetY: store.connection.to.y,
targetPosition: toPosition targetPosition: store.connection.toPosition
}; };
switch ($connectionLineType) { switch (type) {
case ConnectionLineType.Bezier: case ConnectionLineType.Bezier: {
[path] = getBezierPath(pathParams); const [path] = getBezierPath(pathParams);
break; return path;
}
case ConnectionLineType.Straight: {
const [path] = getStraightPath(pathParams);
return path;
}
case ConnectionLineType.Step: case ConnectionLineType.Step:
[path] = getSmoothStepPath({ case ConnectionLineType.SmoothStep: {
const [path] = getSmoothStepPath({
...pathParams, ...pathParams,
borderRadius: 0 borderRadius: type === ConnectionLineType.Step ? 0 : undefined
}); });
break; return path;
case ConnectionLineType.SmoothStep: }
[path] = getSmoothStepPath(pathParams);
break;
default:
[path] = getStraightPath(pathParams);
} }
} });
</script> </script>
{#if $connection.inProgress} {#if store.connection.inProgress}
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}> <svg
<g class={cc(['svelte-flow__connection', getConnectionStatus($connection.isValid)])}> width={store.width}
<slot name="connectionLine" /> height={store.height}
<!-- slot fallbacks do not work if slots are forwarded in parent --> class="svelte-flow__connectionline"
{#if !isCustomComponent} style={containerStyle}
>
<g class={['svelte-flow__connection', getConnectionStatus(store.connection.isValid)]}>
{#if LineComponent}
<LineComponent></LineComponent>
{:else}
<path d={path} {style} fill="none" class="svelte-flow__connection-path" /> <path d={path} {style} fill="none" class="svelte-flow__connection-path" />
{/if} {/if}
</g> </g>
@@ -1,31 +1,53 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { portal } from '$lib/actions/portal';
import { EdgeLabelRenderer } from '$lib/components/EdgeLabelRenderer'; import { useStore } from '$lib/store';
import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect'; import type { EdgeLabelProps } from './types';
import type { BaseEdgeProps } from '$lib/components/BaseEdge/types'; import { toPxString } from '$lib/utils';
export let style: BaseEdgeProps['labelStyle'] = undefined; let {
export let x: BaseEdgeProps['labelX'] = undefined; x = 0,
export let y: BaseEdgeProps['labelY'] = undefined; y = 0,
width,
height,
selectEdgeOnClick = false,
transparent = false,
class: className,
children,
...rest
}: EdgeLabelProps = $props();
const handleEdgeSelect = useHandleEdgeSelect(); const store = useStore();
const id = getContext<string>('svelteflow__edge_id'); const id = getContext<string>('svelteflow__edge_id');
let z = $derived.by(() => {
return store.visible.edges.get(id)?.zIndex;
});
</script> </script>
<EdgeLabelRenderer> <div
<div use:portal={'edge-labels'}
class="svelte-flow__edge-label" class={['svelte-flow__edge-label', { transparent }, className]}
style:transform="translate(-50%, -50%) translate({x}px,{y}px)" style:cursor={selectEdgeOnClick ? 'pointer' : undefined}
style={'pointer-events: all;' + style} style:transform="translate(-50%, -50%) translate({x}px,{y}px)"
role="button" style:pointer-events="all"
tabindex="-1" style:width={toPxString(width)}
on:keyup={() => {}} style:height={toPxString(height)}
on:click={() => { style:z-index={z}
if (id) handleEdgeSelect(id); role="button"
}} tabindex="-1"
> onclick={() => {
<slot /> if (selectEdgeOnClick && id) store.handleEdgeSelection(id);
</div> }}
</EdgeLabelRenderer> {...rest}
>
{@render children?.()}
</div>
<style>
.transparent {
background: transparent;
}
</style>
@@ -1 +1,2 @@
export { default as EdgeLabel } from './EdgeLabel.svelte'; export { default as EdgeLabel } from './EdgeLabel.svelte';
export * from './types';
@@ -0,0 +1,12 @@
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
export type EdgeLabelProps = {
x?: number;
y?: number;
width?: number;
height?: number;
selectEdgeOnClick?: boolean;
transparent?: boolean;
children?: Snippet;
} & HTMLAttributes<HTMLDivElement>;
@@ -1,10 +0,0 @@
<script lang="ts">
import portal from '$lib/actions/portal';
import { useStore } from '$lib/store';
const { domNode } = useStore();
</script>
<div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: $domNode }}>
<slot />
</div>
@@ -1 +0,0 @@
export { default as EdgeLabelRenderer } from './EdgeLabelRenderer.svelte';
@@ -0,0 +1,123 @@
<script lang="ts">
import { useStore } from '$lib/store';
import type { Edge } from '$lib/types';
import { XYHandle, type HandleType } from '@xyflow/system';
import { getContext } from 'svelte';
import { EdgeLabel } from '../EdgeLabel';
import type { EdgeReconnectAnchorProps } from './types';
let {
type,
reconnecting = $bindable(false),
position,
class: className,
size = 25,
children,
...rest
}: EdgeReconnectAnchorProps = $props();
const store = useStore();
let edgeId: string | undefined = getContext('svelteflow__edge_id');
if (!edgeId) {
throw new Error('EdgeReconnectAnchor must be used within an Edge component');
}
const onPointerDown = (event: PointerEvent) => {
if (event.button !== 0) {
return;
}
const {
autoPanOnConnect,
domNode,
isValidConnection,
connectionMode,
connectionRadius,
onconnectstart,
onconnectend,
onreconnect,
onreconnectstart,
onreconnectend,
onbeforereconnect,
cancelConnection,
nodeLookup,
flowId,
panBy,
updateConnection,
edgeLookup
} = store;
let newEdge: Edge | undefined;
let edge = edgeLookup.get(edgeId)!;
reconnecting = true;
onreconnectstart?.(event, edge, type);
const opposite =
type === 'target'
? { nodeId: edge.source, handleId: edge.sourceHandle ?? null, type: 'source' as HandleType }
: {
nodeId: edge.target,
handleId: edge.targetHandle ?? null,
type: 'target' as HandleType
};
XYHandle.onPointerDown(event, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId: opposite.handleId,
nodeId: opposite.nodeId,
nodeLookup,
isTarget: opposite.type === 'target',
edgeUpdaterType: opposite.type,
lib: 'svelte',
flowId,
cancelConnection,
panBy,
isValidConnection,
onConnectStart: onconnectstart,
onConnectEnd: onconnectend,
onConnect: (connection) => {
newEdge = { ...edge, ...connection };
newEdge = onbeforereconnect ? (onbeforereconnect(newEdge, edge) ?? undefined) : newEdge;
if (newEdge) {
store.edges = store.edges.map((e) => (e.id === edge.id ? (newEdge as Edge) : e));
}
onreconnect?.(edge, connection);
},
onReconnectEnd: (event, connectionState) => {
reconnecting = false;
onreconnectend?.(event, edge, opposite.type, connectionState);
},
updateConnection,
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
getFromHandle: () => store.connection.fromHandle
});
};
</script>
<EdgeLabel
x={position?.x}
y={position?.y}
width={size}
height={size}
class={[
'svelte-flow__edgeupdater',
`svelte-flow__edgeupdater-${type}`,
store.noPanClass,
className
]}
onpointerdown={onPointerDown}
transparent
{...rest}
>
{#if !reconnecting && children}
{@render children()}
{/if}
</EdgeLabel>
@@ -0,0 +1,2 @@
export { default as EdgeReconnectAnchor } from './EdgeReconnectAnchor.svelte';
export * from './types';
@@ -0,0 +1,13 @@
import type { HandleType, XYPosition } from '@xyflow/system';
import type { Snippet } from 'svelte';
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
export type EdgeReconnectAnchorProps = {
type: HandleType;
reconnecting?: boolean;
style?: string;
class?: ClassValue;
position?: XYPosition;
size?: number;
children?: Snippet;
} & HTMLAttributes<HTMLDivElement>;
@@ -1,115 +1,147 @@
<svelte:options immutable /> <script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import { setContext } from 'svelte';
<script lang="ts"> import { elementSelectionKeys, getMarkerId } from '@xyflow/system';
import { createEventDispatcher, setContext } from 'svelte';
import cc from 'classcat';
import { getMarkerId } from '@xyflow/system';
import { useStore } from '$lib/store';
import { BezierEdgeInternal } from '$lib/components/edges'; import { BezierEdgeInternal } from '$lib/components/edges';
import type { EdgeLayouted, Edge } from '$lib/types';
import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect';
type $$Props = EdgeLayouted; import type { Node, EdgeLayouted, Edge, EdgeEvents } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
export let id: $$Props['id']; const {
export let type: $$Props['type'] = 'default'; edge,
export let source: $$Props['source'] = ''; store = $bindable(),
export let target: $$Props['target'] = ''; onedgeclick,
export let data: $$Props['data'] = {}; onedgecontextmenu,
export let style: $$Props['style'] = undefined; onedgepointerenter,
export let zIndex: $$Props['zIndex'] = undefined; onedgepointerleave
}: {
store: SvelteFlowStore<NodeType, EdgeType>;
edge: EdgeLayouted<EdgeType>;
} & EdgeEvents<EdgeType> = $props();
export let animated: $$Props['animated'] = false; let {
export let selected: $$Props['selected'] = false; source,
export let selectable: $$Props['selectable'] = undefined; target,
export let deletable: $$Props['deletable'] = undefined; sourceX,
export let hidden: $$Props['hidden'] = false; sourceY,
export let label: $$Props['label'] = undefined; targetX,
export let labelStyle: $$Props['labelStyle'] = undefined; targetY,
export let markerStart: $$Props['markerStart'] = undefined; sourcePosition,
export let markerEnd: $$Props['markerEnd'] = undefined; targetPosition,
export let sourceHandle: $$Props['sourceHandle'] = undefined; animated = false,
export let targetHandle: $$Props['targetHandle'] = undefined; selected = false,
export let sourceX: $$Props['sourceX']; label,
export let sourceY: $$Props['sourceY']; labelStyle,
export let targetX: $$Props['targetX']; data = {},
export let targetY: $$Props['targetY']; style,
export let sourcePosition: $$Props['sourcePosition']; interactionWidth,
export let targetPosition: $$Props['targetPosition']; type = 'default',
export let ariaLabel: $$Props['ariaLabel'] = undefined; sourceHandle,
export let interactionWidth: $$Props['interactionWidth'] = undefined; targetHandle,
markerStart,
markerEnd,
selectable: _selectable,
focusable: _focusable,
deletable = true,
hidden,
zIndex,
class: className,
ariaLabel
} = $derived(edge);
// @ todo: support edge updates // svelte-ignore non_reactive_update
let className: string = ''; let edgeRef: SVGGElement | null = null;
export { className as class };
const { id } = edge;
setContext('svelteflow__edge_id', id); setContext('svelteflow__edge_id', id);
const { edgeLookup, edgeTypes, flowId, elementsSelectable } = useStore(); let selectable = $derived(_selectable ?? store.elementsSelectable);
const dispatch = createEventDispatcher<{ let focusable = $derived(_focusable ?? store.edgesFocusable);
edgeclick: { edge: Edge; event: MouseEvent | TouchEvent };
edgecontextmenu: { edge: Edge; event: MouseEvent };
edgemouseenter: { edge: Edge; event: MouseEvent };
edgemouseleave: { edge: Edge; event: MouseEvent };
}>();
$: edgeType = type || 'default'; let EdgeComponent = $derived(store.edgeTypes[type] ?? BezierEdgeInternal);
$: edgeComponent = $edgeTypes[edgeType] || BezierEdgeInternal;
$: markerStartUrl = markerStart ? `url('#${getMarkerId(markerStart, $flowId)}')` : undefined;
$: markerEndUrl = markerEnd ? `url('#${getMarkerId(markerEnd, $flowId)}')` : undefined;
$: isSelectable = selectable ?? $elementsSelectable;
const handleEdgeSelect = useHandleEdgeSelect(); let markerStartUrl = $derived(
markerStart ? `url('#${getMarkerId(markerStart, store.flowId)}')` : undefined
);
let markerEndUrl = $derived(
markerEnd ? `url('#${getMarkerId(markerEnd, store.flowId)}')` : undefined
);
function onClick(event: MouseEvent | TouchEvent) { function onclick(event: MouseEvent) {
const edge = $edgeLookup.get(id); const edge = store.edgeLookup.get(id);
if (edge) { if (edge) {
handleEdgeSelect(id); if (selectable) store.handleEdgeSelection(id);
dispatch('edgeclick', { event, edge }); onedgeclick?.({ event, edge });
} }
} }
type EdgeMouseEvent = 'edgecontextmenu' | 'edgemouseenter' | 'edgemouseleave'; function onmouseevent<T = MouseEvent>(
function onMouseEvent(event: MouseEvent, type: EdgeMouseEvent) { event: T,
const edge = $edgeLookup.get(id); callback: ({ edge, event }: { edge: EdgeType; event: T }) => void
) {
const edge = store.edgeLookup.get(id);
if (edge) { if (edge) {
dispatch(type, { event, edge }); callback({ event, edge });
} }
} }
onkeydown = (event: KeyboardEvent) => {
// TODO: Possible Svelte Bug? onkeydown is always firing for the last edge
if (!store.disableKeyboardA11y && elementSelectionKeys.includes(event.key) && selectable) {
const { unselectNodesAndEdges, addSelectedEdges } = store;
const unselect = event.key === 'Escape';
if (unselect) {
edgeRef?.blur();
unselectNodesAndEdges({ edges: [edge] });
} else {
addSelectedEdges([id]);
}
}
};
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
{#if !hidden} {#if !hidden}
<svg style:z-index={zIndex}> <svg style:z-index={zIndex} class="svelte-flow__edge-wrapper">
<g <g
class={cc(['svelte-flow__edge', className])} bind:this={edgeRef}
class={['svelte-flow__edge', className]}
class:animated class:animated
class:selected class:selected
class:selectable={isSelectable} class:selectable
data-id={id} data-id={id}
on:click={onClick} {onclick}
on:contextmenu={(e) => { oncontextmenu={onedgecontextmenu
onMouseEvent(e, 'edgecontextmenu'); ? (e) => {
}} onmouseevent(e, onedgecontextmenu);
on:mouseenter={(e) => { }
onMouseEvent(e, 'edgemouseenter'); : undefined}
}} onpointerenter={onedgepointerenter
on:mouseleave={(e) => { ? (e) => {
onMouseEvent(e, 'edgemouseleave'); onmouseevent(e, onedgepointerenter);
}} }
: undefined}
onpointerleave={onedgepointerleave
? (e) => {
onmouseevent(e, onedgepointerleave);
}
: undefined}
aria-label={ariaLabel === null aria-label={ariaLabel === null
? undefined ? undefined
: ariaLabel : ariaLabel
? ariaLabel ? ariaLabel
: `Edge from ${source} to ${target}`} : `Edge from ${source} to ${target}`}
role="img" aria-describedby={focusable ? `${ARIA_EDGE_DESC_KEY}-${store.flowId}` : undefined}
role={focusable ? 'button' : 'img'}
onkeydown={focusable ? onkeydown : undefined}
tabindex={focusable ? 0 : undefined}
> >
<svelte:component <EdgeComponent
this={edgeComponent}
{id} {id}
{source} {source}
{target} {target}
@@ -126,9 +158,9 @@
{data} {data}
{style} {style}
{interactionWidth} {interactionWidth}
selectable={isSelectable} {selectable}
deletable={deletable ?? true} {deletable}
type={edgeType} {type}
sourceHandleId={sourceHandle} sourceHandleId={sourceHandle}
targetHandleId={targetHandle} targetHandleId={targetHandle}
markerStart={markerStartUrl} markerStart={markerStartUrl}
@@ -1,71 +1,113 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import type { Writable } from 'svelte/store';
import cc from 'classcat';
import { import {
Position, Position,
XYHandle, XYHandle,
isMouseEvent, isMouseEvent,
type HandleConnection,
areConnectionMapsEqual, areConnectionMapsEqual,
handleConnectionChange, handleConnectionChange,
ConnectionMode ConnectionMode,
getHostForElement,
type HandleConnection,
type Optional,
type ConnectionState,
type Connection
} from '@xyflow/system'; } from '@xyflow/system';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { HandleProps } from '$lib/types';
type $$Props = HandleProps; import type { ConnectableContext } from '../NodeWrapper/types';
import type { HandleProps } from './types';
export let id: $$Props['id'] = undefined; let {
export let type: $$Props['type'] = 'source'; id: handleId = null,
export let position: $$Props['position'] = Position.Top; type = 'source',
export let style: $$Props['style'] = undefined; position = Position.Top,
export let isValidConnection: $$Props['isValidConnection'] = undefined; style,
export let onconnect: $$Props['onconnect'] = undefined; class: className,
export let ondisconnect: $$Props['ondisconnect'] = undefined; isConnectable: isConnectableProp,
// @todo implement connectablestart, connectableend isConnectableStart = true,
// export let isConnectableStart: $$Props['isConnectableStart'] = undefined; isConnectableEnd = true,
// export let isConnectableEnd: $$Props['isConnectableEnd'] = undefined; isValidConnection,
onconnect,
ondisconnect,
children,
...rest
}: HandleProps = $props();
let isConnectableProp: $$Props['isConnectable'] = undefined;
export { isConnectableProp as isConnectable };
let className: $$Props['class'] = undefined;
export { className as class };
$: isTarget = type === 'target';
const nodeId = getContext<string>('svelteflow__node_id'); const nodeId = getContext<string>('svelteflow__node_id');
const connectable = getContext<Writable<boolean>>('svelteflow__node_connectable'); const isConnectableContext = getContext<ConnectableContext>('svelteflow__node_connectable');
$: isConnectable = isConnectableProp !== undefined ? isConnectableProp : $connectable;
$: handleId = id || null; let isTarget = $derived(type === 'target');
let isConnectable = $derived(
isConnectableProp !== undefined ? isConnectableProp : isConnectableContext.value
);
const store = useStore(); let store = useStore();
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
} = store;
function onPointerDown(event: MouseEvent | TouchEvent) { let prevConnections: Map<string, HandleConnection> | null = null;
$effect.pre(() => {
if (onconnect || ondisconnect) {
// connectionLookup is not reactive, so we use edges to get notified about updates
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
store.edges;
let connections = store.connectionLookup.get(
`${nodeId}-${type}${handleId ? `-${handleId}` : ''}`
);
if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
const _connections = connections ?? new Map();
handleConnectionChange(prevConnections, _connections, ondisconnect);
handleConnectionChange(_connections, prevConnections, onconnect);
}
prevConnections = new Map(connections);
}
});
let [connectionInProgress, connectingFrom, connectingTo, isPossibleTargetHandle, valid] =
$derived.by(() => {
if (!store.connection.inProgress) {
return [false, false, false, false, null];
}
const { fromHandle, toHandle, isValid } = store.connection;
const connectingFrom =
fromHandle &&
fromHandle.nodeId === nodeId &&
fromHandle.type === type &&
fromHandle.id === handleId;
const connectingTo =
toHandle &&
toHandle.nodeId === nodeId &&
toHandle.type === type &&
toHandle.id === handleId;
const isPossibleTargetHandle =
store.connectionMode === ConnectionMode.Strict
? fromHandle?.type !== type
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id;
const valid = connectingTo && isValid;
return [true, connectingFrom, connectingTo, isPossibleTargetHandle, valid];
});
function onConnectExtended(connection: Connection) {
const edge = store.onbeforeconnect?.(connection) ?? connection;
if (!edge) {
return;
}
store.addEdge(edge);
store.onconnect?.(connection);
}
function onpointerdown(event: MouseEvent | TouchEvent) {
const isMouseTriggered = isMouseEvent(event); const isMouseTriggered = isMouseEvent(event);
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) { if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
@@ -73,77 +115,82 @@
handleId, handleId,
nodeId, nodeId,
isTarget, isTarget,
connectionRadius: $connectionRadius, connectionRadius: store.connectionRadius,
domNode: $domNode, domNode: store.domNode,
nodeLookup: $nodeLookup, nodeLookup: store.nodeLookup,
connectionMode: $connectionMode, connectionMode: store.connectionMode,
lib: $lib, lib: 'svelte',
autoPanOnConnect: $autoPanOnConnect, autoPanOnConnect: store.autoPanOnConnect,
flowId: $flowId, flowId: store.flowId,
isValidConnection: isValidConnection ?? $isValidConnectionStore, isValidConnection: isValidConnection ?? store.isValidConnection,
updateConnection, updateConnection: store.updateConnection,
cancelConnection, cancelConnection: store.cancelConnection,
panBy, panBy: store.panBy,
onConnect: (connection) => { onConnect: onConnectExtended,
const edge = $onedgecreate ? $onedgecreate(connection) : connection;
if (!edge) {
return;
}
addEdge(edge);
$onConnectAction?.(connection);
},
onConnectStart: (event, startParams) => { onConnectStart: (event, startParams) => {
$onConnectStartAction?.(event, { store.onconnectstart?.(event, {
nodeId: startParams.nodeId, nodeId: startParams.nodeId,
handleId: startParams.handleId, handleId: startParams.handleId,
handleType: startParams.handleType handleType: startParams.handleType
}); });
}, },
onConnectEnd: (event, connectionState) => { onConnectEnd: (event, connectionState) => {
$onConnectEndAction?.(event, connectionState); store.onconnectend?.(event, connectionState);
}, },
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom], getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
getFromHandle: () => $connection.fromHandle getFromHandle: () => store.connection.fromHandle
}); });
} }
} }
let prevConnections: Map<string, HandleConnection> | null = null; function onclick(event: MouseEvent) {
let connections: Map<string, HandleConnection> | undefined; if (!nodeId || (!store.clickConnectStartHandle && !isConnectableStart)) {
return;
$: if (onconnect || ondisconnect) {
// connectionLookup is not reactive, so we use edges to get notified about updates
$edges;
connections = $connectionLookup.get(`${nodeId}-${type}${id ? `-${id}` : ''}`);
}
$: {
if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
const _connections = connections ?? new Map();
handleConnectionChange(prevConnections, _connections, ondisconnect);
handleConnectionChange(_connections, prevConnections, onconnect);
} }
prevConnections = connections ?? new Map(); if (!store.clickConnectStartHandle) {
} store.onclickconnectstart?.(event, { nodeId, handleId, handleType: type });
store.clickConnectStartHandle = { nodeId, type, id: handleId };
return;
}
$: connectionInProcess = !!$connection.fromHandle; const doc = getHostForElement(event.target);
$: connectingFrom = const isValidConnectionHandler = isValidConnection ?? store.isValidConnection;
$connection.fromHandle?.nodeId === nodeId &&
$connection.fromHandle?.type === type && const { connectionMode, clickConnectStartHandle, flowId, nodeLookup } = store;
$connection.fromHandle?.id === handleId; const { connection, isValid } = XYHandle.isValid(event, {
$: connectingTo = handle: {
$connection.toHandle?.nodeId === nodeId && nodeId,
$connection.toHandle?.type === type && id: handleId,
$connection.toHandle?.id === handleId; type
$: isPossibleEndHandle = },
$connectionMode === ConnectionMode.Strict connectionMode,
? $connection.fromHandle?.type !== type fromNodeId: clickConnectStartHandle.nodeId,
: nodeId !== $connection.fromHandle?.nodeId || handleId !== $connection.fromHandle?.id; fromHandleId: clickConnectStartHandle.id ?? null,
$: valid = connectingTo && $connection.isValid; fromType: clickConnectStartHandle.type,
isValidConnection: isValidConnectionHandler,
flowId,
doc,
lib: 'svelte',
nodeLookup
});
if (isValid && connection) {
onConnectExtended(connection);
}
const connectionClone = structuredClone($state.snapshot(store.connection)) as Optional<
ConnectionState,
'inProgress'
>;
delete connectionClone.inProgress;
connectionClone.toPosition = connectionClone.toHandle
? connectionClone.toHandle.position
: null;
store.onclickconnectend?.(event, connectionClone);
store.clickConnectStartHandle = null;
}
</script> </script>
<!-- <!--
@@ -154,29 +201,34 @@ The Handle component is the part of a node that can be used to connect nodes.
data-handleid={handleId} data-handleid={handleId}
data-nodeid={nodeId} data-nodeid={nodeId}
data-handlepos={position} data-handlepos={position}
data-id="{$flowId}-{nodeId}-{id || null}-{type}" data-id="{store.flowId}-{nodeId}-{handleId ?? 'null'}-{type}"
class={cc([ class={[
'svelte-flow__handle', 'svelte-flow__handle',
`svelte-flow__handle-${position}`, `svelte-flow__handle-${position}`,
'nodrag', store.noDragClass,
'nopan', store.noPanClass,
position, position,
className className
])} ]}
class:valid class:valid
class:connectingto={connectingTo} class:connectingto={connectingTo}
class:connectingfrom={connectingFrom} class:connectingfrom={connectingFrom}
class:source={!isTarget} class:source={!isTarget}
class:target={isTarget} class:target={isTarget}
class:connectablestart={isConnectable} class:connectablestart={isConnectableStart}
class:connectableend={isConnectable} class:connectableend={isConnectableEnd}
class:connectable={isConnectable} class:connectable={isConnectable}
class:connectionindicator={isConnectable && (!connectionInProcess || isPossibleEndHandle)} class:connectionindicator={isConnectable &&
on:mousedown={onPointerDown} (!connectionInProgress || isPossibleTargetHandle) &&
on:touchstart={onPointerDown} (connectionInProgress || store.clickConnectStartHandle ? isConnectableEnd : isConnectableStart)}
onmousedown={onpointerdown}
ontouchstart={onpointerdown}
onclick={store.clickConnect ? onclick : undefined}
onkeypress={() => {}}
{style} {style}
role="button" role="button"
tabindex="-1" tabindex="-1"
{...rest}
> >
<slot /> {@render children?.()}
</div> </div>
@@ -1 +1,2 @@
export { default as Handle } from './Handle.svelte'; export { default as Handle } from './Handle.svelte';
export * from './types';
@@ -0,0 +1,10 @@
import type { Connection, HandleProps as HandlePropsSystem } from '@xyflow/system';
import type { Snippet } from 'svelte';
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
export type HandleProps = HandlePropsSystem & {
class?: ClassValue;
onconnect?: (connections: Connection[]) => void;
ondisconnect?: (connections: Connection[]) => void;
children?: Snippet;
} & HTMLAttributes<HTMLDivElement>;
@@ -1,4 +1,4 @@
<script lang="ts"> <script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import { import {
shortcut, shortcut,
type ShortcutEventDetail, type ShortcutEventDetail,
@@ -6,26 +6,20 @@
} from '@svelte-put/shortcut'; } from '@svelte-put/shortcut';
import { isInputDOMNode, isMacOs } from '@xyflow/system'; import { isInputDOMNode, isMacOs } from '@xyflow/system';
import { useStore } from '$lib/store';
import type { KeyHandlerProps } from './types'; import type { KeyHandlerProps } from './types';
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types'; import type { Node, Edge, KeyDefinition, KeyDefinitionObject } from '$lib/types';
import { useSvelteFlow } from '$lib/hooks/useSvelteFlow.svelte';
type $$Props = KeyHandlerProps; let {
store = $bindable(),
selectionKey = 'Shift',
multiSelectionKey = isMacOs() ? 'Meta' : 'Control',
deleteKey = 'Backspace',
panActivationKey = ' ',
zoomActivationKey = isMacOs() ? 'Meta' : 'Control'
}: KeyHandlerProps<NodeType, EdgeType> = $props();
export let selectionKey: $$Props['selectionKey'] = 'Shift'; let { deleteElements } = useSvelteFlow<NodeType, EdgeType>();
export let multiSelectionKey: $$Props['multiSelectionKey'] = isMacOs() ? 'Meta' : 'Control';
export let deleteKey: $$Props['deleteKey'] = 'Backspace';
export let panActivationKey: $$Props['panActivationKey'] = ' ';
export let zoomActivationKey: $$Props['zoomActivationKey'] = isMacOs() ? 'Meta' : 'Control';
const {
selectionKeyPressed,
multiselectionKeyPressed,
deleteKeyPressed,
panActivationKeyPressed,
zoomActivationKeyPressed,
selectionRect
} = useStore();
function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject { function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject {
return key !== null && typeof key === 'object'; return key !== null && typeof key === 'object';
@@ -62,12 +56,29 @@
} }
function resetKeysAndSelection() { function resetKeysAndSelection() {
selectionRect.set(null); store.selectionRect = null;
selectionKeyPressed.set(false); store.selectionKeyPressed = false;
multiselectionKeyPressed.set(false); store.multiselectionKeyPressed = false;
deleteKeyPressed.set(false); store.deleteKeyPressed = false;
panActivationKeyPressed.set(false); store.panActivationKeyPressed = false;
zoomActivationKeyPressed.set(false); store.zoomActivationKeyPressed = false;
}
async function handleDelete() {
const selectedNodes = store.nodes.filter((node) => node.selected);
const selectedEdges = store.edges.filter((edge) => edge.selected);
const { deletedNodes, deletedEdges } = await deleteElements({
nodes: selectedNodes,
edges: selectedEdges
});
if (deletedNodes.length > 0 || deletedEdges.length > 0) {
store.ondelete?.({
nodes: deletedNodes,
edges: deletedEdges
});
}
} }
</script> </script>
@@ -75,19 +86,21 @@
on:blur={resetKeysAndSelection} on:blur={resetKeysAndSelection}
on:contextmenu={resetKeysAndSelection} on:contextmenu={resetKeysAndSelection}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(true)), trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = true)),
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(false)), trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = false)),
type: 'keyup' type: 'keyup'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(true)), trigger: getShortcutTrigger(multiSelectionKey, () => {
store.multiselectionKeyPressed = true;
}),
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(false)), trigger: getShortcutTrigger(multiSelectionKey, () => (store.multiselectionKeyPressed = false)),
type: 'keyup' type: 'keyup'
}} }}
use:shortcut={{ use:shortcut={{
@@ -97,29 +110,30 @@
detail.originalEvent.metaKey || detail.originalEvent.metaKey ||
detail.originalEvent.shiftKey; detail.originalEvent.shiftKey;
if (!isModifierKey && !isInputDOMNode(detail.originalEvent)) { if (!isModifierKey && !isInputDOMNode(detail.originalEvent)) {
deleteKeyPressed.set(true); store.deleteKeyPressed = true;
handleDelete();
} }
}), }),
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(deleteKey, () => deleteKeyPressed.set(false)), trigger: getShortcutTrigger(deleteKey, () => (store.deleteKeyPressed = false)),
type: 'keyup' type: 'keyup'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(true)), trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = true)),
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(false)), trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = false)),
type: 'keyup' type: 'keyup'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(true)), trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = true)),
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(false)), trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = false)),
type: 'keyup' type: 'keyup'
}} }}
/> />
@@ -1,6 +1,8 @@
import type { KeyDefinition } from '$lib/types'; import type { SvelteFlowStore } from '$lib/store/types';
import type { Node, Edge, KeyDefinition } from '$lib/types';
export type KeyHandlerProps = { export type KeyHandlerProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
store: SvelteFlowStore<NodeType, EdgeType>;
selectionKey?: KeyDefinition | KeyDefinition[] | null; selectionKey?: KeyDefinition | KeyDefinition[] | null;
multiSelectionKey?: KeyDefinition | KeyDefinition[] | null; multiSelectionKey?: KeyDefinition | KeyDefinition[] | null;
deleteKey?: KeyDefinition | KeyDefinition[] | null; deleteKey?: KeyDefinition | KeyDefinition[] | null;
@@ -1,73 +1,95 @@
<script lang="ts"> <script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import { createEventDispatcher } from 'svelte';
import { getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system'; import { getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system';
import { useStore } from '$lib/store';
import { Selection } from '$lib/components/Selection'; import { Selection } from '$lib/components/Selection';
import drag from '$lib/actions/drag'; import drag from '$lib/actions/drag';
import type { Node, NodeEventMap } from '$lib/types';
const store = useStore(); import type { NodeSelectionProps } from './types';
const { selectionRectMode, nodes, nodeLookup } = store; import { arrowKeyDiffs, toPxString } from '$lib/utils';
import type { Node, Edge } from '$lib/types';
const dispatch = createEventDispatcher< let {
NodeEventMap & { store = $bindable(),
selectioncontextmenu: { nodes: Node[]; event: MouseEvent | TouchEvent }; onnodedrag,
selectionclick: { nodes: Node[]; event: MouseEvent | TouchEvent }; onnodedragstart,
onnodedragstop,
onselectionclick,
onselectioncontextmenu
}: NodeSelectionProps<NodeType, EdgeType> = $props();
let ref = $state<HTMLDivElement>();
$effect(() => {
if (!store.disableKeyboardA11y) {
ref?.focus({
preventScroll: true
});
} }
>(); });
let bounds: Rect | null = null; let bounds: Rect | null = $derived.by(() => {
if (store.selectionRectMode === 'nodes') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
store.nodes;
return getInternalNodesBounds(store.nodeLookup, { filter: (node) => !!node.selected });
}
return null;
});
$: if ($selectionRectMode === 'nodes') { function oncontextmenu(event: MouseEvent) {
bounds = getInternalNodesBounds($nodeLookup, { filter: (node) => !!node.selected }); const selectedNodes = store.nodes.filter((n) => n.selected);
$nodes; onselectioncontextmenu?.({ nodes: selectedNodes, event });
} }
function onContextMenu(event: MouseEvent | TouchEvent) { function onclick(event: MouseEvent) {
const selectedNodes = $nodes.filter((n) => n.selected); const selectedNodes = store.nodes.filter((n) => n.selected);
dispatch('selectioncontextmenu', { nodes: selectedNodes, event }); onselectionclick?.({ nodes: selectedNodes, event });
} }
function onClick(event: MouseEvent | TouchEvent) { function onkeydown(event: KeyboardEvent) {
const selectedNodes = $nodes.filter((n) => n.selected); if (Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
dispatch('selectionclick', { nodes: selectedNodes, event }); event.preventDefault();
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
}
} }
</script> </script>
{#if $selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)} {#if store.selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
<div <div
class="selection-wrapper nopan" class={['svelte-flow__selection-wrapper', store.noPanClass]}
style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)" style:width={toPxString(bounds.width)}
style:height={toPxString(bounds.height)}
style:transform="translate({bounds.x}px, {bounds.y}px)"
use:drag={{ use:drag={{
disabled: false, disabled: false,
store, store,
onDrag: (event, _, __, nodes) => { onDrag: (event, _, __, nodes) => {
dispatch('nodedrag', { event, targetNode: null, nodes }); onnodedrag?.({ event, targetNode: null, nodes: nodes as NodeType[] });
}, },
onDragStart: (event, _, __, nodes) => { onDragStart: (event, _, __, nodes) => {
dispatch('nodedragstart', { event, targetNode: null, nodes }); onnodedragstart?.({ event, targetNode: null, nodes: nodes as NodeType[] });
}, },
onDragStop: (event, _, __, nodes) => { onDragStop: (event, _, __, nodes) => {
dispatch('nodedragstop', { event, targetNode: null, nodes }); onnodedragstop?.({ event, targetNode: null, nodes: nodes as NodeType[] });
} }
}} }}
on:contextmenu={onContextMenu} {oncontextmenu}
on:click={onClick} {onclick}
role="button" role={store.disableKeyboardA11y ? undefined : 'button'}
tabindex="-1" tabIndex={store.disableKeyboardA11y ? undefined : -1}
on:keyup={() => {}} onkeydown={store.disableKeyboardA11y ? undefined : onkeydown}
bind:this={ref}
> >
<Selection width="100%" height="100%" x={0} y={0} /> <Selection width="100%" height="100%" x={0} y={0} />
</div> </div>
{/if} {/if}
<style> <style>
.selection-wrapper { .svelte-flow__selection-wrapper {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 7; z-index: 2000;
pointer-events: all; pointer-events: all;
} }
</style> </style>

Some files were not shown because too many files have changed in this diff Show More