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'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.45.0-jammy
image: mcr.microsoft.com/playwright:v1.51.1-jammy
steps:
- name: Checkout repo
uses: actions/checkout@v4
+1
View File
@@ -1 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
+1
View File
@@ -7,6 +7,7 @@
"dev": "vite --port 3000 --open --host",
"serve": "vite serve --port 3000",
"build": "vite build",
"preview": "vite preview",
"test:dev": "cypress open",
"test": "pnpm test-component && pnpm test-e2e",
"test-component": "cypress run --component",
@@ -4,6 +4,7 @@ export default {
flowProps: {
fitView: true,
multiSelectionKeyCode: 's',
deleteKeyCode: 'd',
nodes: [
{
id: '1',
@@ -3,6 +3,8 @@ import DragHandleNode from './components/DragHandleNode';
export default {
flowProps: {
fitView: true,
deleteKeyCode: 'd',
multiSelectionKeyCode: 's',
nodeTypes: {
DragHandleNode,
},
+16 -16
View File
@@ -12,25 +12,25 @@
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.2.1",
"@sveltejs/kit": "^2.5.10",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-svelte": "^2.35.0",
"prettier": "^3.0.3",
"prettier-plugin-svelte": "^3.0.3",
"svelte": "^4.2.12",
"svelte-check": "^3.6.6",
"tslib": "^2.6.2",
"typescript": "^5.4.5",
"vite": "^5.2.12"
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.20.7",
"@typescript-eslint/eslint-plugin": "^8.30.1",
"@typescript-eslint/parser": "^8.30.1",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-svelte": "^3.5.1",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.27.0",
"svelte-check": "^4.1.6",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"vite": "^6.3.0"
},
"type": "module",
"dependencies": {
"@dagrejs/dagre": "^1.0.4",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@dagrejs/dagre": "^1.1.4",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@xyflow/svelte": "workspace:^"
}
}
@@ -35,7 +35,7 @@
<header>
<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}
<option value={`/examples/${route}`}>{route}</option>
{/each}
@@ -3,6 +3,8 @@ import { MarkerType } from '@xyflow/svelte';
export default {
flowProps: {
fitView: true,
multiSelectionKey: ['Meta', 's'],
deleteKey: 'd',
nodes: [
{
id: '1',
@@ -1,9 +1,18 @@
<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;
export let data: $$Props['data'];
let {
data
}: NodeProps<
Node<{ label: string; toolbarPosition: Position; toolbarAlign: Align; toolbarVisible: boolean }>
> = $props();
</script>
<NodeToolbar
@@ -1,11 +1,11 @@
<script lang="ts">
import type { NodeProps } from '@xyflow/svelte';
type $$Props = NodeProps;
let {}: NodeProps = $props();
</script>
<div class="container">
<div class="drag-handle custom-drag-handle" />
<div class="drag-handle custom-drag-handle"></div>
</div>
<style>
@@ -4,6 +4,9 @@ export default {
flowProps: {
fitView: true,
nodeDragThreshold: 0,
autoPanOnConnect: false,
autoPanOnNodeDrag: false,
deleteKey: 'd',
nodeTypes: {
DragHandleNode
},
+2
View File
@@ -0,0 +1,2 @@
export const ssr = false;
export const prerender = false;
@@ -1,11 +1,16 @@
<script lang="ts">
import { Header } from '$components/Header';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="app">
<Header />
<div class="flow">
<slot />
{@render children?.()}
</div>
</div>
@@ -1,7 +1,7 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
useNodeConnections,
useSvelteFlow,
type Edge,
type Node,
@@ -19,21 +19,27 @@
}
];
const nodes = writable<Node[]>(initialNodes);
const edges = writable<Edge[]>([]);
let nodes = $state.raw<Node[]>(initialNodes);
let edges = $state.raw<Edge[]>([]);
let connectingNodeId: string | null = '0';
let rect: DOMRectReadOnly;
let connectingNodeId: string | null = $state('0');
let rect = $state<DOMRectReadOnly>();
let id = 1;
const getId = () => `${id++}`;
const { screenToFlowPosition, flowToScreenPosition } = useSvelteFlow();
const connections = useNodeConnections({ id: '0', handleType: 'source' });
$inspect(connections.current);
const handleConnectEnd: OnConnectEnd = (event) => {
if (!connectingNodeId) return;
// 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) {
const id = getId();
const position = {
@@ -56,15 +62,14 @@
origin: [0.5, 0.0]
};
$nodes.push(newNode);
$edges.push({
nodes = [...nodes, newNode];
const newEdge = {
source: connectingNodeId,
target: id,
id: `${connectingNodeId}--${id}`
});
$nodes = $nodes;
$edges = $edges;
};
edges = [...edges, newEdge];
}
};
</script>
@@ -73,8 +78,8 @@
<div class="wrapper" bind:contentRect={rect}>
<SvelteFlow
{nodes}
{edges}
bind:nodes
bind:edges
fitView
fitViewOptions={{ padding: 2 }}
onconnectstart={(_, { nodeId }) => {
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -18,7 +17,7 @@
targetPosition: Position.Left
};
const nodes = writable([
let nodes = $state.raw([
{
id: 'A',
position: { x: 0, y: 150 },
@@ -30,16 +29,16 @@
{ 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-C', source: 'A', target: 'C' },
{ id: 'A-D', source: 'A', target: 'D' }
]);
let colorMode: ColorMode = 'light';
let colorMode: ColorMode = $state('light');
</script>
<SvelteFlow {nodes} {edges} {colorMode} fitView>
<SvelteFlow bind:nodes bind:edges {colorMode} colorModeSSR={'light'} fitView>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
@@ -1,7 +1,6 @@
<script lang="ts">
import { SvelteFlow } from '@xyflow/svelte';
import { Background, BackgroundVariant, type Edge, type Node } from '@xyflow/svelte';
import { writable } from 'svelte/store';
import CustomNode from './CustomNode.svelte';
import ConnectionLine from './ConnectionLine.svelte';
@@ -12,7 +11,7 @@
custom: CustomNode
};
const nodes = writable<Node[]>([
let nodes = $state.raw<Node[]>([
{
id: 'connectionline-1',
type: 'custom',
@@ -21,12 +20,11 @@
}
]);
const edges = writable<Edge[]>([]);
let edges = $state.raw<Edge[]>([]);
</script>
<div style="height:100vh;">
<SvelteFlow {nodeTypes} {nodes} {edges} fitView>
<ConnectionLine slot="connectionLine" />
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView connectionLineComponent={ConnectionLine}>
<Background variant={BackgroundVariant.Lines} />
</SvelteFlow>
</div>
@@ -3,22 +3,24 @@
const connection = useConnection();
let path: string | null = null;
$: if ($connection.inProgress) {
const { from, to, fromPosition, toPosition } = $connection;
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition
};
[path] = getBezierPath(pathParams);
}
let path: string | null = $derived.by(() => {
if (connection.current.inProgress) {
const { from, to, fromPosition, toPosition } = connection.current;
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition
};
const [path] = getBezierPath(pathParams);
return path;
}
return null;
});
</script>
{#if $connection.inProgress}
<path d={path} fill="none" stroke={$connection.fromHandle.id} />
{#if connection.current.inProgress}
<path d={path} fill="none" stroke={connection.current.fromHandle.id} />
{/if}
@@ -1,10 +1,7 @@
<script lang="ts">
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
type $$Props = NodeProps;
export let isConnectable: $$Props['isConnectable'];
$$restProps;
let { isConnectable }: NodeProps = $props();
const handleStyle = 'width: 10px; height: 10px; bottom: -5px;';
</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">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -20,9 +28,7 @@
colorNode: CustomNode
};
const bgColor = writable('#1A192B');
const nodes = writable<Node[]>([
let nodes = $state.raw<Node[]>([
{
id: '1',
type: 'input',
@@ -33,7 +39,7 @@
{
id: '2',
type: 'colorNode',
data: { colorStore: bgColor },
data: {},
position: { x: 250, y: 50 }
},
{
@@ -52,7 +58,7 @@
}
]);
const edges = writable<Edge[]>([
let edges = $state.raw<Edge[]>([
{
id: 'e1-2',
source: '1',
@@ -81,10 +87,10 @@
</script>
<SvelteFlow
{nodes}
{edges}
bind:nodes
bind:edges
{nodeTypes}
style="--xy-background-color: {$bgColor}"
style="--xy-background-color: {bg.color}"
fitView
onconnect={onConnect}
>
@@ -1,12 +1,8 @@
<script lang="ts">
import type { Writable } from 'svelte/store';
import { Handle, Position, type NodeProps, type Node } from '@xyflow/svelte';
import { bg } from './+page.svelte';
type $$Props = NodeProps<Node<{ colorStore: Writable<string> }>>;
export let data: $$Props['data'];
$$restProps;
let { data }: NodeProps<Node> = $props();
const { colorStore } = data;
</script>
@@ -14,13 +10,13 @@
<div class="custom">
<Handle type="target" position={Position.Left} />
<div>
Custom Color Picker Node: <strong>{$colorStore}</strong>
Custom Color Picker Node: <strong>{bg.color}</strong>
</div>
<input
class="nodrag"
type="color"
on:input={(evt) => colorStore.set(evt.currentTarget.value)}
value={$colorStore}
oninput={(evt) => (bg.color = evt.currentTarget.value)}
value={bg.color}
/>
<Handle type="source" position={Position.Right} id="a" style="top: 20px;" />
<Handle type="source" position={Position.Right} id="b" style="top: auto; bottom: 10px;" />
@@ -1,79 +1,77 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Background, Position, ConnectionLineType, Panel } from '@xyflow/svelte';
import type { Edge, Node } from '@xyflow/svelte';
import dagre from '@dagrejs/dagre';
import { SvelteFlow, Background, Position, ConnectionLineType, Panel } from '@xyflow/svelte';
import type { Edge, Node } from '@xyflow/svelte';
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();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 172;
const nodeHeight = 36;
const nodeWidth = 172;
const nodeHeight = 36;
function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') {
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction });
function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') {
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction });
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
dagre.layout(dagreGraph);
nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2
};
});
return {
...node,
targetPosition: isHorizontal ? Position.Left : Position.Top,
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
position: {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2
}
};
});
return { nodes, edges };
}
return { nodes: layoutedNodes, edges };
}
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
initialNodes,
initialEdges
);
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
initialNodes,
initialEdges
);
const nodes = writable<Node[]>(layoutedNodes);
const edges = writable<Edge[]>(layoutedEdges);
let nodes = $state.raw<Node[]>(layoutedNodes);
let edges = $state.raw<Edge[]>(layoutedEdges);
function onLayout(direction: string) {
const layoutedElements = getLayoutedElements($nodes, $edges, direction);
function onLayout(direction: string) {
const layoutedElements = getLayoutedElements(nodes, edges, direction);
$nodes = layoutedElements.nodes;
$edges = layoutedElements.edges;
// nodes.set(layoutedElements.nodes);
// edges.set(layoutedElements.edges);
}
nodes = layoutedElements.nodes;
edges = layoutedElements.edges;
}
</script>
<div style="height:100vh;">
<SvelteFlow
{nodes}
{edges}
fitView
connectionLineType={ConnectionLineType.SmoothStep}
defaultEdgeOptions={{ type: 'smoothstep', animated: true }}
>
<Panel position="top-right">
<button on:click={() => onLayout('TB')}>vertical layout</button>
<button on:click={() => onLayout('LR')}>horizontal layout</button>
</Panel>
<Background />
</SvelteFlow>
<SvelteFlow
bind:nodes
bind:edges
fitView
connectionLineType={ConnectionLineType.SmoothStep}
defaultEdgeOptions={{ type: 'smoothstep', animated: true }}
>
<Panel position="top-right">
<button onclick={() => onLayout('TB')}>vertical layout</button>
<button onclick={() => onLayout('LR')}>horizontal layout</button>
</Panel>
<Background />
</SvelteFlow>
</div>
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -13,7 +12,7 @@
import '@xyflow/svelte/dist/style.css';
const nodes = writable<Node[]>([
const initialNodes = [
{
id: '1',
type: 'input',
@@ -32,9 +31,11 @@
data: { label: 'Output Node' },
position: { x: 300, y: 150 }
}
]);
];
const edges = writable([
let nodes = $state.raw<Node[]>(initialNodes);
let edges = $state.raw([
{
id: '1-2',
type: 'default',
@@ -50,7 +51,7 @@
}
]);
const svelteFlow = useSvelteFlow();
const { screenToFlowPosition } = useSvelteFlow();
const onDragOver = (event: DragEvent) => {
event.preventDefault();
@@ -68,7 +69,7 @@
}
const type = event.dataTransfer.getData('application/svelteflow') || 'default';
const position = svelteFlow.screenToFlowPosition({
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY
});
@@ -79,17 +80,14 @@
data: { label: `${type} node` }
};
$nodes.push(newNode);
$nodes = $nodes;
nodes = [...nodes, newNode];
};
$: {
console.log($nodes);
}
$inspect(nodes);
</script>
<main>
<SvelteFlow {nodes} {edges} fitView on:dragover={onDragOver} on:drop={onDrop}>
<SvelteFlow bind:nodes bind:edges fitView ondragover={onDragOver} ondrop={onDrop}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
@@ -1,6 +1,4 @@
<script lang="ts">
import { useSvelteFlow } from '@xyflow/svelte';
const onDragStart = (event: DragEvent, nodeType: string) => {
if (!event.dataTransfer) {
return null;
@@ -9,29 +7,28 @@
event.dataTransfer.setData('application/svelteflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
const { zoomIn, zoomOut, fitView, viewport, nodes } = useSvelteFlow();
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<aside>
<div class="label">You can drag these nodes to the pane on the left.</div>
<div
class="input-node node"
on:dragstart={(event) => onDragStart(event, 'input')}
ondragstart={(event) => onDragStart(event, 'input')}
draggable={true}
>
Input Node
</div>
<div
class="default-node node"
on:dragstart={(event) => onDragStart(event, 'default')}
ondragstart={(event) => onDragStart(event, 'default')}
draggable={true}
>
Default Node
</div>
<div
class="output-node node"
on:dragstart={(event) => onDragStart(event, 'output')}
ondragstart={(event) => onDragStart(event, 'output')}
draggable={true}
>
Output Node
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -7,14 +6,15 @@
BackgroundVariant,
MiniMap,
MarkerType,
type Connection
type Connection,
type EdgeTypes
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import ButtonEdge from './ButtonEdge.svelte';
import CustomBezierEdge from './CustomBezierEdge.svelte';
const nodes = writable([
let nodes = $state.raw([
{
id: '1',
type: 'input',
@@ -54,7 +54,7 @@
}
]);
const edges = writable([
let edges = $state.raw([
{
id: 'e1-2',
source: '1',
@@ -128,45 +128,29 @@
target: '6',
label: 'hi',
labelStyle: 'background: red; font-weight: 700; padding: 5px;',
style: 'stroke: #ffcc0',
markerEnd: {
type: MarkerType.Arrow,
color: '#FFCC00',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20,
strokeWidth: 2
},
markerStart: {
type: MarkerType.ArrowClosed,
color: '#FFCC00',
orient: 'auto-start-reverse',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20
}
style: 'stroke: #ffcc0'
}
]);
const edgeTypes = {
const edgeTypes: EdgeTypes = {
button: ButtonEdge,
customBezier: CustomBezierEdge
};
function getEdgeId(connection: Connection) {
function getEdgeId(connection: Connection) {
return `edge-${connection.source}-${connection.target}}`;
}
$: console.log('edges', $edges);
$inspect(edges);
</script>
<SvelteFlow
{nodes}
{edges}
{edgeTypes}
bind:nodes
bind:edges
{edgeTypes}
fitView
nodeDragThreshold={2}
onedgecreate={(connection) => {
onbeforeconnect={(connection) => {
console.log('on edge create', connection);
return {
@@ -1,59 +1,80 @@
<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;
export let id: $$Props['id'] = '';
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({
let {
style,
markerEnd,
sourceX,
sourceY,
sourcePosition,
targetX,
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>
<BaseEdge path={edgePath} {markerEnd} {style} />
<EdgeLabelRenderer>
<div
class="edgeButtonContainer nodrag nopan"
style:transform="translate(-50%, -50%) translate({labelX}px,{labelY}px)"
>
{#if !reconnecting}
<BaseEdge path={edgePath} {markerEnd} {style} />
<EdgeLabel x={labelX} y={labelY}>
<button
class="edgeButton"
on:click={(event) => {
onclick={(event) => {
event.stopPropagation();
alert(`remove ${id}`);
updateEdge('e5-6', {
markerEnd: {
type: MarkerType.Arrow,
color: '#FFCC00',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20,
strokeWidth: 2
},
markerStart: {
type: MarkerType.ArrowClosed,
color: '#FFCC00',
orient: 'auto-start-reverse',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20
}
});
}}
>
×
</button>
</div>
</EdgeLabelRenderer>
</EdgeLabel>
{/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>
.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 {
width: 20px;
height: 20px;
@@ -61,8 +82,7 @@
border: 1px solid #fff;
cursor: pointer;
border-radius: 50%;
font-size: 12px;
line-height: 1;
font-size: 10px;
}
.edgeButton:hover {
@@ -1,38 +1,7 @@
<script lang="ts">
import { BezierEdge, type EdgeProps } from '@xyflow/svelte';
type $$Props = EdgeProps;
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;
let { ...props }: EdgeProps = $props();
</script>
<BezierEdge
{sourceX}
{sourceY}
{sourcePosition}
{targetX}
{targetY}
{targetPosition}
{label}
{labelStyle}
{markerStart}
{markerEnd}
{style}
{interactionWidth}
pathOptions={{ curvature: 1 }}
/>
<BezierEdge {...props} pathOptions={{ curvature: 1 }} />
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -17,11 +16,11 @@
const panOnDrag = [1, 2];
const onMoveStart = (e: any) => console.log('move start', e);
const onMove = (e: any) => console.log('move', e);
const onMoveEnd = (e: any) => console.log('move end', e);
const onmovestart = (e: any) => console.log('move start', e);
const onmove = (e: any) => console.log('move', e);
const onmoveend = (e: any) => console.log('move end', e);
const nodes = writable([
let nodes = $state.raw([
{
id: '1',
type: 'input',
@@ -34,23 +33,23 @@
{ 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-3', source: '1', target: '3' }
]);
</script>
<SvelteFlow
{nodes}
{edges}
bind:nodes
bind:edges
fitView
selectionMode={SelectionMode.Partial}
selectionOnDrag
panOnScroll
{panOnDrag}
{onMoveStart}
{onMove}
{onMoveEnd}
{onmovestart}
{onmove}
{onmoveend}
>
<Controls />
<Background variant={BackgroundVariant.Dots} />
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -20,7 +19,7 @@
multi: MultiHandleNode
};
const nodes = writable<Node[]>([
let nodes = $state.raw<Node[]>([
{
id: '1',
type: 'single',
@@ -60,7 +59,7 @@
}
]);
const edges = writable<Edge[]>([
let edges = $state.raw<Edge[]>([
{
id: 'e1-2',
source: '1',
@@ -87,7 +86,7 @@
]);
</script>
<SvelteFlow {nodes} {edges} {nodeTypes} fitView colorMode="dark">
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView colorMode="dark">
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
@@ -4,12 +4,10 @@
Position,
type NodeProps,
type Connection,
useHandleConnections
useNodeConnections
} from '@xyflow/svelte';
type $$Props = NodeProps;
export let id: $$Props['id'];
let { id }: NodeProps = $props();
function onConnectTarget(connection: Connection[]) {
console.log('connect target', connection);
@@ -27,13 +25,18 @@
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);
}
});
$: {
console.log('connections', id, $connections);
}
$$restProps;
$inspect(connections.current);
</script>
<div class="custom">
@@ -1,9 +1,7 @@
<script lang="ts">
import { Handle, Position, type NodeProps, type Connection } from '@xyflow/svelte';
type $$Props = NodeProps;
export let id: $$Props['id'];
let { id }: NodeProps = $props();
function onConnectTarget(connection: Connection[]) {
console.log('connect target', connection);
@@ -20,8 +18,6 @@
function onDisconnectSource(connection: Connection[]) {
console.log('disconnect source', connection);
}
$$restProps;
</script>
<div class="custom">
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -13,7 +12,7 @@
import '@xyflow/svelte/dist/style.css';
const nodes = writable([
let nodes = $state.raw([
{
id: '1',
type: 'input',
@@ -25,37 +24,37 @@
{ 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-3', source: '1', target: '3' }
]);
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const onPaneClick = (event: MouseEvent) => console.log('onPaneClick', event);
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
const onPaneContextMenu = (event: MouseEvent) => console.log('onPaneContextMenu', event);
const onMoveEnd: OnMoveEnd = (_, viewport) => console.log('onMoveEnd', viewport);
const onnodedragstart = (_: MouseEvent, node: Node) => console.log('drag start', node);
const onnodedragstop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onnodeclick = (_: MouseEvent, node: Node) => console.log('click', node);
const onedgeclick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const onpaneclick = (event: MouseEvent) => console.log('onPaneClick', event);
const onpanescroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
const onpanecontextmenu = (event: MouseEvent) => console.log('onPaneContextMenu', event);
const onmoveend: OnMoveEnd = (_, viewport) => console.log('onMoveEnd', viewport);
let isSelectable = false;
let isDraggable = false;
let isConnectable = false;
let zoomOnScroll = false;
let zoomOnPinch = false;
let panOnScroll = false;
let panOnScrollMode = PanOnScrollMode.Free;
let zoomOnDoubleClick = false;
let panOnDrag = true;
let captureZoomClick = false;
let captureZoomScroll = false;
let captureElementClick = false;
let isSelectable = $state(false);
let isDraggable = $state(false);
let isConnectable = $state(false);
let zoomOnScroll = $state(false);
let zoomOnPinch = $state(false);
let panOnScroll = $state(false);
let panOnScrollMode = $state(PanOnScrollMode.Free);
let zoomOnDoubleClick = $state(false);
let panOnDrag = $state(true);
let captureZoomClick = $state(false);
let captureZoomScroll = $state(false);
let captureElementClick = $state(false);
</script>
<SvelteFlow
{nodes}
{edges}
bind:nodes
bind:edges
elementsSelectable={isSelectable}
nodesConnectable={isConnectable}
nodesDraggable={isDraggable}
@@ -65,7 +64,8 @@
{panOnScrollMode}
{zoomOnDoubleClick}
{panOnDrag}
{onMoveEnd}
{onmoveend}
fitView
>
<MiniMap />
<Controls />
@@ -143,7 +143,7 @@
<select
id="panonscrollmode"
bind:value={panOnScrollMode}
on:change={(event) => {
onchange={(event) => {
panOnScrollMode = PanOnScrollMode.Free;
}}
class="svelte-flow__panonscrollmode"
@@ -1,31 +1,34 @@
<script lang="ts">
import { SvelteFlow, Background, Controls, useSvelteFlow } from '@xyflow/svelte';
import type { Edge, Node } from '@xyflow/svelte';
import { writable } from 'svelte/store';
import '@xyflow/svelte/dist/style.css';
import { initialNodes, initialEdges } from './nodes-and-edges';
const nodes = writable<Node[]>(initialNodes);
const edges = writable<Edge[]>(initialEdges);
let nodes = $state.raw<Node[]>(initialNodes);
let edges = $state.raw<Edge[]>(initialEdges);
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>
<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 />
<Controls />
</SvelteFlow>
@@ -48,10 +51,10 @@
}
:global(
.intersection-flow .svelte-flow__node.selected,
.intersection-flow .svelte-flow__node:hover,
.intersection-flow .svelte-flow__node:focus
) {
.intersection-flow .svelte-flow__node.selected,
.intersection-flow .svelte-flow__node:hover,
.intersection-flow .svelte-flow__node:focus
) {
box-shadow: 6px 6px 0 1px rgba(0, 0, 0, 0.7);
background-color: #eee;
}
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Controls, Panel, type Edge } from '@xyflow/svelte';
import DefaultResizer from './DefaultResizer.svelte';
@@ -19,9 +18,9 @@
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',
type: 'defaultResizer',
@@ -128,12 +127,12 @@
}
]);
let snapToGrid = false;
let snapToGrid = $state(false);
</script>
<SvelteFlow
{nodes}
{edges}
bind:nodes
bind:edges
{nodeTypes}
minZoom={0.2}
maxZoom={5}
@@ -144,7 +143,7 @@
<Controls />
<Panel position="bottom-right">
<button
on:click={() => {
onclick={() => {
snapToGrid = !snapToGrid;
}}
>
@@ -1,10 +1,8 @@
<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';
type $$Props = NodeProps<ResizeNode>;
export let data: $$Props['data'];
let { data }: NodeProps<ResizeNode> = $props();
</script>
<NodeResizeControl
@@ -2,10 +2,7 @@
import { Handle, NodeResizer, Position, type NodeProps } from '@xyflow/svelte';
import type { ResizeNode } from './types';
type $$Props = NodeProps<ResizeNode>;
export let data: $$Props['data'];
export let selected: $$Props['selected'] = undefined;
let { data, selected }: NodeProps<ResizeNode> = $props();
</script>
<NodeResizer
@@ -2,9 +2,7 @@
import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte';
import type { ResizeNode } from './types';
type $$Props = NodeProps<ResizeNode>;
export let data: $$Props['data'];
let { data }: NodeProps<ResizeNode> = $props();
</script>
<NodeResizeControl
@@ -2,9 +2,7 @@
import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte';
import type { ResizeNode } from './types';
type $$Props = NodeProps<ResizeNode>;
export let data: $$Props['data'];
let { data }: NodeProps<ResizeNode> = $props();
</script>
<NodeResizeControl
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Background,
@@ -11,7 +10,7 @@
import '@xyflow/svelte/dist/style.css';
import CustomNode from './CustomNode.svelte';
import CustomNode, { type CustomNodeType } from './CustomNode.svelte';
import SelectedNodesToolbar from './SelectedNodesToolbar.svelte';
const nodeTypes: NodeTypes = {
@@ -19,9 +18,9 @@
};
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',
type: 'custom',
@@ -51,12 +50,12 @@
const initialEdges: Edge[] = [];
const nodes = writable<Node[]>(initialNodes);
const edges = writable<Edge[]>(initialEdges);
let nodes = $state.raw<Node[]>(initialNodes);
let edges = $state.raw<Edge[]>(initialEdges);
</script>
<div style="height: 100vh;">
<SvelteFlow {nodes} {edges} {nodeTypes} fitView>
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView>
<Background />
<SelectedNodesToolbar />
</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">
import { NodeToolbar, type NodeProps, Handle, Position } from '@xyflow/svelte';
type $$Props = NodeProps;
$$restProps;
export let data: $$Props['data'];
let { data }: NodeProps<CustomNodeType> = $props();
</script>
<NodeToolbar
@@ -1,10 +1,12 @@
<script lang="ts">
import { NodeToolbar, useNodes } from '@xyflow/svelte';
const nodes = useNodes();
let nodes = useNodes();
$: selectedNodeIds = $nodes.filter((node) => node.selected).map((node) => node.id);
$: isVisible = selectedNodeIds.length > 1;
let selectedNodeIds = $derived(
nodes.current.filter((node) => node.selected).map((node) => node.id)
);
let isVisible = $derived(selectedNodeIds.length > 1);
</script>
<NodeToolbar nodeId={selectedNodeIds} {isVisible}>
@@ -1,240 +1,8 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
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);
}
import { SvelteFlowProvider } from '@xyflow/svelte';
import Flow from './Flow.svelte';
</script>
<SvelteFlow
{nodes}
{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]}
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>
<SvelteFlowProvider>
<Flow />
</SvelteFlowProvider>
@@ -1,46 +1,35 @@
<script lang="ts">
import {
BaseEdge,
EdgeLabelRenderer,
useEdges,
type EdgeProps,
getBezierPath
} from '@xyflow/svelte';
import { BaseEdge, useEdges, getBezierPath, type EdgeProps, EdgeLabel } from '@xyflow/svelte';
type $$Props = EdgeProps;
let { ...props }: EdgeProps = $props();
$: [path, labelX, labelY] = getBezierPath({
sourceX: $$props.sourceX,
sourceY: $$props.sourceY,
targetX: $$props.targetX,
targetY: $$props.targetY,
sourcePosition: $$props.sourcePosition,
targetPosition: $$props.targetPosition
});
let [path, labelX, labelY] = $derived(
getBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition
})
);
const edges = useEdges();
function onClick() {
edges.update((eds) => eds.filter((e) => e.id !== $$props.id));
edges.current = edges.current.filter((e) => e.id !== props.id);
}
</script>
<BaseEdge {path} {labelX} {labelY} {...$$props} />
<BaseEdge {path} {labelX} {labelY} {...props} />
<EdgeLabelRenderer>
<button
style:transform={`translate(-50%,-50%) translate(${labelX}px,${labelY}px)`}
class="edge-button"
on:click={onClick}
>
</button>
</EdgeLabelRenderer>
<EdgeLabel x={labelX} y={labelY} selectEdgeOnClick>
<button class="edge-button" onclick={onClick}> </button>
</EdgeLabel>
<style>
.edge-button {
border-radius: 50%;
position: absolute;
border: none;
width: 1rem;
height: 1rem;
@@ -48,7 +37,6 @@
justify-content: center;
align-items: center;
font-size: 8px;
pointer-events: all;
cursor: pointer;
background-color: #eee;
}
@@ -1,12 +1,21 @@
<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>;
$$restProps;
let {
data = { label: 'Node' },
positionAbsoluteX = 0,
positionAbsoluteY = 0
}: NodeProps<BuiltInNode> = $props();
export let data: { label: string } = { label: 'Node' };
export let positionAbsoluteX: number = 0;
export let positionAbsoluteY: number = 0;
useOnSelectionChange(({ nodes, edges }) => {
console.log('on selection changed via hook', { nodes, edges });
});
</script>
<div class="custom">
@@ -1,15 +1,12 @@
<script lang="ts">
import type { BuiltInNode, NodeProps } from '@xyflow/svelte';
type $$Props = NodeProps<BuiltInNode>;
$$restProps;
export let data: { label: string } = { label: 'Node' };
let { data = { label: 'Node' } }: NodeProps<BuiltInNode> = $props();
</script>
<div class="custom">
<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>
<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">
import { useNodesInitialized, useInitialized } from '@xyflow/svelte';
import { useNodesInitialized, useViewportInitialized } from '@xyflow/svelte';
const nodesInitialized = useNodesInitialized();
const initialized = useInitialized();
const viewportInitialized = useViewportInitialized();
$: {
if (nodesInitialized) {
$effect(() => {
if (viewportInitialized.current) {
console.log('viewportInitialized');
}
});
$effect(() => {
if (nodesInitialized.current) {
console.log('nodes initialized');
}
}
$: {
if (initialized) {
console.log('initialized');
}
}
});
</script>
@@ -1,16 +1,17 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Controls, Background, MiniMap, type Node, type Edge } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
const nodes = writable<Node[]>([]);
const edges = writable<Edge[]>([]);
let nodes = $state.raw<Node[]>([]);
let edges = $state.raw<Edge[]>([]);
const resetNodesArray = () => {
const node1: Node = {
id: 'c1',
data: { label: '1' },
width: 100,
height: 100,
position: { x: 0, y: 0 }
};
@@ -20,14 +21,14 @@
position: { x: 100, y: 0 }
};
$nodes = [node1, node2];
nodes = [node1, node2];
};
</script>
<button on:click={resetNodesArray}> Reset </button>
<button onclick={resetNodesArray}> Reset </button>
<div style:height="100vh">
<SvelteFlow {nodes} {edges} fitView>
<SvelteFlow bind:nodes bind:edges fitView>
<Controls />
<Background />
<MiniMap />
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -29,7 +28,8 @@
id,
data,
position,
type: 'default'
type: 'default',
selected: true
};
nodeItems.push(node);
@@ -46,11 +46,11 @@
}
}
const nodes = writable(nodeItems);
const edges = writable(edgeItems);
let nodes = $state.raw(nodeItems);
let edges = $state.raw(edgeItems);
</script>
<SvelteFlow {nodes} {edges} fitView minZoom={0.2}>
<SvelteFlow bind:nodes bind:edges fitView minZoom={0.2} onlyRenderVisibleElements>
<Controls />
<Background variant={BackgroundVariant.Lines} />
<MiniMap />
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -7,7 +6,8 @@
BackgroundVariant,
MiniMap,
type NodeTypes,
type Node
type Node,
type Edge
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
@@ -18,7 +18,7 @@
default: DebugNode
};
const nodes = writable<Node[]>([
let nodes = $state.raw<Node[]>([
{
id: '1',
type: 'input',
@@ -95,7 +95,7 @@
}
]);
const edges = writable([
let edges = $state.raw<Edge[]>([
{
id: 'e1-2',
source: '1',
@@ -115,13 +115,9 @@
{ id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' }
]);
$: {
console.log($nodes);
}
</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 />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
@@ -3,12 +3,23 @@
type $$Props = NodeProps;
export let id: string;
export let positionAbsoluteX: number = 0;
export let positionAbsoluteY: number = 0;
export let zIndex: number = 0;
interface Props {
id: string;
positionAbsoluteX?: number;
positionAbsoluteY?: number;
zIndex?: number;
[key: string]: any
}
$$restProps;
let {
id,
positionAbsoluteX = 0,
positionAbsoluteY = 0,
zIndex = 0,
...rest
}: Props = $props();
rest;
</script>
<Handle type="target" position={Position.Top} />
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -12,7 +11,7 @@
import '@xyflow/svelte/dist/style.css';
const nodes = writable([
let nodes = $state.raw([
{
id: 'A',
position: { x: 0, y: 0 },
@@ -20,27 +19,25 @@
},
{ 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 updateViewport = () => {
$viewport.x += 10;
$viewport = $viewport;
viewport.x += 10;
};
viewport.subscribe((vp) => {
console.log('viewport update', vp);
});
$inspect(viewport);
</script>
<SvelteFlow {nodes} {edges} {viewport}>
<SvelteFlow bind:nodes bind:edges bind:viewport>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<Panel>
<button on:click={updateViewport}>update viewport</button>
<button on:click={() => fitView()}>fitView</button>
<button onclick={updateViewport}>update viewport</button>
<button onclick={() => fitView()}>fitView</button>
</Panel>
</SvelteFlow>
@@ -1,11 +1,11 @@
<script context="module" lang="ts">
<script module lang="ts">
import type { Node } from '@xyflow/svelte';
type TextNodeType = Node<{ text: string }, 'text'>;
type UppercaseNodeType = Node<{ text: string }, 'uppercase'>;
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';
}
@@ -13,7 +13,6 @@
</script>
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -35,7 +34,7 @@
result: ResultNode
};
const nodes = writable<MyNode[]>([
let nodes = $state.raw<MyNode[]>([
{
id: '1',
type: 'text',
@@ -60,7 +59,6 @@
},
position: { x: 0, y: 100 }
},
{
id: '3',
type: 'result',
@@ -69,7 +67,7 @@
}
]);
const edges = writable<Edge[]>([
let edges = $state.raw<Edge[]>([
{
id: 'e1-1a',
source: '1',
@@ -88,7 +86,7 @@
]);
</script>
<SvelteFlow {nodes} {edges} {nodeTypes} fitView>
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
@@ -4,22 +4,22 @@
Position,
useNodeConnections,
useNodesData,
type NodeProps
type NodeProps,
type Node
} from '@xyflow/svelte';
import { isTextNode, type MyNode } from './+page.svelte';
type $$Props = NodeProps;
export let id: $$Props['id'];
$$restProps;
let { id }: NodeProps = $props();
const connections = useNodeConnections({
id: id,
handleType: 'target'
});
$: nodeData = useNodesData<MyNode>($connections.map((connection) => connection.source));
$: textNodes = $nodeData.filter(isTextNode);
let nodeData = $derived(
useNodesData<MyNode>(connections.current.map((connection) => connection.source))
);
let textNodes = $derived(nodeData.current.filter(isTextNode));
</script>
<div class="custom">
@@ -1,13 +1,9 @@
<script lang="ts">
import { Handle, Position, type NodeProps, useSvelteFlow } from '@xyflow/svelte';
type $$Props = NodeProps;
export let id: $$Props['id'];
export let data: $$Props['data'];
let { id, data }: NodeProps = $props();
const { updateNodeData } = useSvelteFlow();
$$restProps;
</script>
<div class="custom">
@@ -15,7 +11,7 @@
<div>
<input
value={data.text}
on:input={(evt) => updateNodeData(id, { text: evt.currentTarget.value })}
oninput={(evt) => updateNodeData(id, { text: evt.currentTarget.value })}
/>
</div>
<Handle type="source" position={Position.Right} />
@@ -5,15 +5,12 @@
useNodeConnections,
useNodesData,
useSvelteFlow,
type Node,
type NodeProps
} from '@xyflow/svelte';
import { isTextNode, type MyNode } from './+page.svelte';
type $$Props = NodeProps;
export let id: $$Props['id'];
export let data: $$Props['data'];
$$restProps;
let { id }: NodeProps<Node<{ text: string }>> = $props();
const { updateNodeData } = useSvelteFlow();
const connections = useNodeConnections({
@@ -21,20 +18,17 @@
handleType: 'target'
});
$: nodeData = useNodesData<MyNode>($connections[0]?.source);
$: textNode = isTextNode($nodeData) ? $nodeData : null;
let nodeData = $derived(useNodesData<MyNode>(connections.current[0]?.source));
let textNodeData = $derived(isTextNode(nodeData.current) ? nodeData.current.data.text : null);
$: console.log(textNode?.data, data);
$: {
const input = textNode?.data.text.toUpperCase() ?? '';
$effect.pre(() => {
const input = textNodeData?.toUpperCase() ?? '';
updateNodeData(id, { text: input });
console.log('updatedNodeData with', input);
}
});
</script>
<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>
<Handle type="source" position={Position.Right} />
</div>
@@ -1,12 +1,11 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Controls, Background, BackgroundVariant, MiniMap } from '@xyflow/svelte';
import Sidebar from './Sidebar.svelte';
import '@xyflow/svelte/dist/style.css';
const nodes = writable([
let nodes = $state.raw([
{
id: '1',
type: 'input',
@@ -27,7 +26,7 @@
}
]);
const edges = writable([
let edges = $state.raw([
{
id: '1-2',
type: 'default',
@@ -45,7 +44,7 @@
</script>
<main>
<SvelteFlow {nodes} {edges} fitView>
<SvelteFlow bind:nodes bind:edges fitView>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
@@ -1,5 +1,5 @@
<script lang="ts">
import { useEdges, useNodes, useSvelteFlow } from '@xyflow/svelte';
import { useEdges, useNodes, useSvelteFlow, useViewport } from '@xyflow/svelte';
const {
zoomIn,
@@ -9,53 +9,59 @@
setCenter,
setViewport,
getViewport,
viewport,
toObject,
deleteElements
} = useSvelteFlow();
const nodes = useNodes();
const edges = useEdges();
let nodes = useNodes();
let edges = useEdges();
let viewport = useViewport();
const deleteNode = () => {
$nodes.shift();
$nodes = $nodes;
//TODO: do we really want to allow this?
// nodes.shift();
nodes.current.shift();
nodes.current = [...nodes.current];
};
</script>
<aside>
<div class="label">Functions:</div>
<button on:click={() => zoomIn()}>zoom in</button>
<button on:click={() => zoomOut({ duration: 1000 })}>zoom out transition</button>
<button on:click={() => setZoom(2)}>set zoom</button>
<button on:click={() => fitView()}>fitView</button>
<button on:click={() => setCenter(0, 0)}>setCenter 0, 0</button>
<button on:click={() => setViewport({ x: 100, y: 100, zoom: 2 })}>setViewport</button>
<button on:click={() => console.log(getViewport())}>getViewport</button>
<button onclick={() => zoomIn()}>zoom in</button>
<button onclick={() => zoomOut({ duration: 1000 })}>zoom out transition</button>
<button onclick={() => setZoom(2)}>set zoom</button>
<button onclick={() => fitView()}>fitView</button>
<button onclick={() => setCenter(0, 0)}>setCenter 0, 0</button>
<button onclick={() => setViewport({ x: 100, y: 100, zoom: 2 })}>setViewport</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
>
<button on:click={() => deleteElements({ nodes: [{ id: $nodes[0].id }] })}>delete node</button>
<button on:click={() => deleteElements({ nodes: $nodes.map((node) => ({ id: node.id })) })}
<button onclick={() => deleteElements({ nodes: [{ id: nodes.current[0].id }] })}
>delete node</button
>
<button onclick={() => deleteElements({ nodes: nodes.current.map((node) => ({ id: node.id })) })}
>deleteElements</button
>
<button on:click={() => deleteNode()}>delete via store</button>
<button onclick={() => deleteNode()}>delete via store</button>
<button
on:click={() => {
onclick={() => {
const { nodes, edges, viewport } = toObject();
console.log(nodes, edges, viewport);
}}>toObject</button
>
<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>
{/each}
<div class="label">Viewport:</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>
</aside>
@@ -1,23 +1,20 @@
<script lang="ts">
import { Handle, Position, type NodeProps, useUpdateNodeInternals } from '@xyflow/svelte';
type $$Props = NodeProps;
export let id: $$Props['id'];
$$restProps;
let { id }: NodeProps = $props();
const updateNodeInternals = useUpdateNodeInternals();
$: handleCount = 1;
let handleCount = $state(1);
const onClick = () => {
handleCount += 1;
updateNodeInternals(id);
updateNodeInternals();
};
</script>
<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}
<Handle type="source" position={Position.Bottom} id={`${i}`} style={`left: ${i * 10}px;`} />
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -19,7 +18,7 @@
custom: CustomNode
};
const nodes = writable([
let nodes = $state.raw([
{
id: '1',
type: 'custom',
@@ -40,7 +39,7 @@
}
]);
const edges = writable([
let edges = $state.raw([
{
id: '1-2',
source: '1',
@@ -56,17 +55,19 @@
const { updateNode } = useSvelteFlow();
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>
<main>
<SvelteFlow {nodes} {edges} {nodeTypes} fitView>
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Panel><button on:click={updateNodePosition}>update node</button></Panel>
<Panel><button onclick={updateNodePosition}>update node</button></Panel>
</SvelteFlow>
</main>
@@ -1,5 +1,4 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
@@ -17,7 +16,7 @@
targetPosition: Position.Left
};
const nodes = writable([
let nodes = $state.raw([
{
id: '0',
position: { x: 0, y: 150 },
@@ -29,7 +28,7 @@
{ 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';
@@ -39,8 +38,8 @@
</script>
<SvelteFlow
{nodes}
{edges}
bind:nodes
bind:edges
fitView
minZoom={0.1}
maxZoom={2.5}
@@ -3,7 +3,11 @@
import Flow from './Flow.svelte';
export let data: { flowConfig: FlowConfig };
let {
data
}: {
data: { flowConfig: FlowConfig };
} = $props();
</script>
<SvelteFlowProvider>
@@ -1,19 +1,25 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { Background, Controls, MiniMap, Panel, SvelteFlow } from '@xyflow/svelte';
import {
Background,
Controls,
MiniMap,
Panel,
SvelteFlow,
type SvelteFlowProps
} from '@xyflow/svelte';
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
const nodes = writable(flowConfig.flowProps?.nodes);
const edges = writable(flowConfig.flowProps?.edges);
let nodes = $state.raw(flowConfig.flowProps?.nodes ?? []);
let edges = $state.raw(flowConfig.flowProps?.edges ?? []);
const flowProps = { ...flowConfig.flowProps, nodes, edges };
const flowProps: SvelteFlowProps = { ...flowConfig.flowProps };
</script>
<SvelteFlow {...flowProps}>
<SvelteFlow {...flowProps} bind:nodes bind:edges>
{#if flowConfig.panelProps}
<Panel {...flowConfig.panelProps} />
{/if}
+4 -2
View File
@@ -5,8 +5,10 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
preprocess: vitePreprocess({ script: true }),
compilerOptions: {
runes: true
},
kit: {
// 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.
+4 -1
View File
@@ -8,7 +8,10 @@
"resolveJsonModule": true,
"skipLibCheck": 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
//
+4
View File
@@ -3,6 +3,10 @@ import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
build: {
sourcemap: true,
minify: false
},
server: {
fs: {
// 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>(
node: InternalNode<NodeType>
): {
width: number | string | undefined;
height: number | string | undefined;
} {
width: number | string | undefined;
height: number | string | undefined;
} {
if (node.internals.handleBounds === undefined) {
return {
width: node.width ?? node.initialWidth ?? node.style?.width,
@@ -61,9 +61,9 @@ export function NodesSelection<NodeType extends Node>({
const onContextMenu = onSelectionContextMenu
? (event: MouseEvent) => {
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
: undefined;
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
## 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
### 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",
"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.",
"keywords": [
"svelte",
@@ -51,45 +51,48 @@
"access": "public"
},
"dependencies": {
"@svelte-put/shortcut": "3.1.1",
"@xyflow/system": "workspace:*",
"classcat": "^5.0.4"
"@svelte-put/shortcut": "^4.1.0",
"@xyflow/system": "workspace:*"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^2.5.4",
"@sveltejs/package": "^2.3.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"autoprefixer": "^10.4.18",
"cssnano": "^6.1.0",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"postcss": "^8.4.35",
"postcss-cli": "^11.0.0",
"@eslint/js": "^9.24.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.20.7",
"@sveltejs/package": "^2.3.11",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@typescript-eslint/eslint-plugin": "^8.30.1",
"@typescript-eslint/parser": "^8.30.1",
"autoprefixer": "^10.4.21",
"cssnano": "^7.0.6",
"dotenv": "^16.5.0",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"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-import": "^16.0.1",
"postcss-nested": "^6.0.1",
"postcss-import": "^16.1.0",
"postcss-nested": "^7.0.2",
"postcss-rename": "^0.6.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"svelte": "^4.2.12",
"svelte-check": "^3.6.7",
"svelte-eslint-parser": "^0.33.1",
"svelte-preprocess": "^5.1.3",
"tslib": "^2.6.2",
"typescript": "5.4.5"
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.27.0",
"svelte-check": "^4.1.6",
"svelte-eslint-parser": "^1.1.2",
"svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.30.1"
},
"peerDependencies": {
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
"svelte": "^5.25.0"
},
"files": [
"dist"
],
"typesVersions": {
">4.0": {
">5.0": {
"index": [
"./dist/lib/index.d.ts"
]
+27 -22
View File
@@ -1,10 +1,10 @@
import { get } from 'svelte/store';
import { XYDrag, type OnDrag } from '@xyflow/system';
import { XYDrag, type NodeBase, type OnDrag, type XYDragParams } from '@xyflow/system';
import type { SvelteFlowStore } from '$lib/store/types';
import type { Node, Edge, NodeTargetEventWithPointer } from '$lib/types';
export type UseDragParams = {
store: SvelteFlowStore;
export type UseDragParams<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
store: SvelteFlowStore<NodeType, EdgeType>;
disabled?: boolean;
noDragClass?: string;
handleSelector?: string;
@@ -17,7 +17,10 @@ export type UseDragParams = {
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 dragInstance = XYDrag({
onDrag,
@@ -25,32 +28,34 @@ export default function drag(domNode: Element, params: UseDragParams) {
onDragStop,
onNodeMouseDown,
getStoreItems: () => {
const snapGrid = get(store.snapGrid);
const vp = get(store.viewport);
const { snapGrid, viewport } = store;
return {
nodes: get(store.nodes),
nodeLookup: get(store.nodeLookup),
edges: get(store.edges),
nodeExtent: get(store.nodeExtent),
nodes: store.nodes satisfies NodeBase[],
nodeLookup: store.nodeLookup,
edges: store.edges,
nodeExtent: store.nodeExtent,
snapGrid: snapGrid ? snapGrid : [0, 0],
snapToGrid: !!snapGrid,
nodeOrigin: get(store.nodeOrigin),
multiSelectionActive: get(store.multiselectionKeyPressed),
domNode: get(store.domNode),
transform: [vp.x, vp.y, vp.zoom],
autoPanOnNodeDrag: get(store.autoPanOnNodeDrag),
nodesDraggable: get(store.nodesDraggable),
selectNodesOnDrag: get(store.selectNodesOnDrag),
nodeDragThreshold: get(store.nodeDragThreshold),
nodeOrigin: store.nodeOrigin,
multiSelectionActive: store.multiselectionKeyPressed,
domNode: store.domNode,
transform: [viewport.x, viewport.y, viewport.zoom],
autoPanOnNodeDrag: store.autoPanOnNodeDrag,
nodesDraggable: store.nodesDraggable,
selectNodesOnDrag: store.selectNodesOnDrag,
nodeDragThreshold: store.nodeDragThreshold,
unselectNodesAndEdges: store.unselectNodesAndEdges,
updateNodePositions: store.updateNodePositions,
onSelectionDrag: store.onselectiondrag,
onSelectionDragStart: store.onselectiondragstart,
onSelectionDragStop: store.onselectiondragstop,
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) {
dragInstance.destroy();
return;
@@ -69,7 +74,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
updateDrag(domNode, params);
return {
update(params: UseDragParams) {
update(params: UseDragParams<NodeType, EdgeType>) {
updateDrag(domNode, params);
},
destroy() {
@@ -1,31 +1 @@
type PortalOptions = {
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);
}
}
};
}
export { portal } from './portal.svelte';
@@ -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 {
PanOnScrollMode,
XYPanZoom,
@@ -10,17 +9,16 @@ import {
} from '@xyflow/system';
type ZoomParams = {
viewport: Writable<Viewport>;
viewport: Viewport;
initialViewport: Viewport;
minZoom: number;
maxZoom: number;
dragging: Writable<boolean>;
setPanZoomInstance: (panZoomInstance: PanZoomInstance) => void;
onPanZoomStart?: OnPanZoom;
onPanZoom?: OnPanZoom;
onPanZoomEnd?: OnPanZoom;
onPaneContextMenu?: (event: MouseEvent) => void;
translateExtent: CoordinateExtent;
panZoom: Writable<PanZoomInstance | null>;
zoomOnScroll: boolean;
zoomOnPinch: boolean;
zoomOnDoubleClick: boolean;
@@ -38,18 +36,19 @@ type ZoomParams = {
lib: string;
paneClickDistance: number;
onTransformChange: (transform: Transform) => void;
onDraggingChange: (dragging: boolean) => void;
};
export default function zoom(domNode: Element, params: ZoomParams) {
const {
panZoom,
minZoom,
maxZoom,
initialViewport,
viewport,
dragging,
translateExtent,
paneClickDistance
paneClickDistance,
setPanZoomInstance,
onDraggingChange,
onTransformChange
} = params;
const panZoomInstance = XYPanZoom({
@@ -59,11 +58,19 @@ export default function zoom(domNode: Element, params: ZoomParams) {
translateExtent,
viewport: initialViewport,
paneClickDistance,
onDraggingChange: dragging.set
onDraggingChange
});
const currentViewport = panZoomInstance.getViewport();
viewport.set(currentViewport);
panZoom.set(panZoomInstance);
const viewport = panZoomInstance.getViewport();
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);
@@ -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 type { AttributionProps } from './types';
type $$Props = AttributionProps;
export let proOptions: $$Props['proOptions'] = undefined;
export let position: $$Props['position'] = 'bottom-right';
let { proOptions, position = 'bottom-right' }: AttributionProps = $props();
</script>
{#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">
import cc from 'classcat';
import { useStore } from '$lib/store';
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import type { Component } from 'svelte';
import {
ConnectionLineType,
getBezierPath,
@@ -10,50 +8,69 @@
getStraightPath
} from '@xyflow/system';
export let containerStyle: string = '';
export let style: string = '';
export let isCustomComponent: boolean = false;
import type { SvelteFlowStore } from '$lib/store/types';
import type { Node, Edge } from '$lib/types';
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 = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition
sourceX: store.connection.from.x,
sourceY: store.connection.from.y,
sourcePosition: store.connection.fromPosition,
targetX: store.connection.to.x,
targetY: store.connection.to.y,
targetPosition: store.connection.toPosition
};
switch ($connectionLineType) {
case ConnectionLineType.Bezier:
[path] = getBezierPath(pathParams);
break;
switch (type) {
case ConnectionLineType.Bezier: {
const [path] = getBezierPath(pathParams);
return path;
}
case ConnectionLineType.Straight: {
const [path] = getStraightPath(pathParams);
return path;
}
case ConnectionLineType.Step:
[path] = getSmoothStepPath({
case ConnectionLineType.SmoothStep: {
const [path] = getSmoothStepPath({
...pathParams,
borderRadius: 0
borderRadius: type === ConnectionLineType.Step ? 0 : undefined
});
break;
case ConnectionLineType.SmoothStep:
[path] = getSmoothStepPath(pathParams);
break;
default:
[path] = getStraightPath(pathParams);
return path;
}
}
}
});
</script>
{#if $connection.inProgress}
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}>
<g class={cc(['svelte-flow__connection', getConnectionStatus($connection.isValid)])}>
<slot name="connectionLine" />
<!-- slot fallbacks do not work if slots are forwarded in parent -->
{#if !isCustomComponent}
{#if store.connection.inProgress}
<svg
width={store.width}
height={store.height}
class="svelte-flow__connectionline"
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" />
{/if}
</g>
@@ -1,31 +1,53 @@
<script lang="ts">
import { getContext } from 'svelte';
import { portal } from '$lib/actions/portal';
import { EdgeLabelRenderer } from '$lib/components/EdgeLabelRenderer';
import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect';
import type { BaseEdgeProps } from '$lib/components/BaseEdge/types';
import { useStore } from '$lib/store';
import type { EdgeLabelProps } from './types';
import { toPxString } from '$lib/utils';
export let style: BaseEdgeProps['labelStyle'] = undefined;
export let x: BaseEdgeProps['labelX'] = undefined;
export let y: BaseEdgeProps['labelY'] = undefined;
let {
x = 0,
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');
let z = $derived.by(() => {
return store.visible.edges.get(id)?.zIndex;
});
</script>
<EdgeLabelRenderer>
<div
class="svelte-flow__edge-label"
style:transform="translate(-50%, -50%) translate({x}px,{y}px)"
style={'pointer-events: all;' + style}
role="button"
tabindex="-1"
on:keyup={() => {}}
on:click={() => {
if (id) handleEdgeSelect(id);
}}
>
<slot />
</div>
</EdgeLabelRenderer>
<div
use:portal={'edge-labels'}
class={['svelte-flow__edge-label', { transparent }, className]}
style:cursor={selectEdgeOnClick ? 'pointer' : undefined}
style:transform="translate(-50%, -50%) translate({x}px,{y}px)"
style:pointer-events="all"
style:width={toPxString(width)}
style:height={toPxString(height)}
style:z-index={z}
role="button"
tabindex="-1"
onclick={() => {
if (selectEdgeOnClick && id) store.handleEdgeSelection(id);
}}
{...rest}
>
{@render children?.()}
</div>
<style>
.transparent {
background: transparent;
}
</style>
@@ -1 +1,2 @@
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 { createEventDispatcher, setContext } from 'svelte';
import cc from 'classcat';
import { getMarkerId } from '@xyflow/system';
import { elementSelectionKeys, getMarkerId } from '@xyflow/system';
import { useStore } from '$lib/store';
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'];
export let type: $$Props['type'] = 'default';
export let source: $$Props['source'] = '';
export let target: $$Props['target'] = '';
export let data: $$Props['data'] = {};
export let style: $$Props['style'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
const {
edge,
store = $bindable(),
onedgeclick,
onedgecontextmenu,
onedgepointerenter,
onedgepointerleave
}: {
store: SvelteFlowStore<NodeType, EdgeType>;
edge: EdgeLayouted<EdgeType>;
} & EdgeEvents<EdgeType> = $props();
export let animated: $$Props['animated'] = false;
export let selected: $$Props['selected'] = false;
export let selectable: $$Props['selectable'] = undefined;
export let deletable: $$Props['deletable'] = undefined;
export let hidden: $$Props['hidden'] = false;
export let label: $$Props['label'] = undefined;
export let labelStyle: $$Props['labelStyle'] = undefined;
export let markerStart: $$Props['markerStart'] = undefined;
export let markerEnd: $$Props['markerEnd'] = undefined;
export let sourceHandle: $$Props['sourceHandle'] = undefined;
export let targetHandle: $$Props['targetHandle'] = undefined;
export let sourceX: $$Props['sourceX'];
export let sourceY: $$Props['sourceY'];
export let targetX: $$Props['targetX'];
export let targetY: $$Props['targetY'];
export let sourcePosition: $$Props['sourcePosition'];
export let targetPosition: $$Props['targetPosition'];
export let ariaLabel: $$Props['ariaLabel'] = undefined;
export let interactionWidth: $$Props['interactionWidth'] = undefined;
let {
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
animated = false,
selected = false,
label,
labelStyle,
data = {},
style,
interactionWidth,
type = 'default',
sourceHandle,
targetHandle,
markerStart,
markerEnd,
selectable: _selectable,
focusable: _focusable,
deletable = true,
hidden,
zIndex,
class: className,
ariaLabel
} = $derived(edge);
// @ todo: support edge updates
let className: string = '';
export { className as class };
// svelte-ignore non_reactive_update
let edgeRef: SVGGElement | null = null;
const { id } = edge;
setContext('svelteflow__edge_id', id);
const { edgeLookup, edgeTypes, flowId, elementsSelectable } = useStore();
const dispatch = createEventDispatcher<{
edgeclick: { edge: Edge; event: MouseEvent | TouchEvent };
edgecontextmenu: { edge: Edge; event: MouseEvent };
edgemouseenter: { edge: Edge; event: MouseEvent };
edgemouseleave: { edge: Edge; event: MouseEvent };
}>();
let selectable = $derived(_selectable ?? store.elementsSelectable);
let focusable = $derived(_focusable ?? store.edgesFocusable);
$: edgeType = type || 'default';
$: edgeComponent = $edgeTypes[edgeType] || BezierEdgeInternal;
$: markerStartUrl = markerStart ? `url('#${getMarkerId(markerStart, $flowId)}')` : undefined;
$: markerEndUrl = markerEnd ? `url('#${getMarkerId(markerEnd, $flowId)}')` : undefined;
$: isSelectable = selectable ?? $elementsSelectable;
let EdgeComponent = $derived(store.edgeTypes[type] ?? BezierEdgeInternal);
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) {
const edge = $edgeLookup.get(id);
function onclick(event: MouseEvent) {
const edge = store.edgeLookup.get(id);
if (edge) {
handleEdgeSelect(id);
dispatch('edgeclick', { event, edge });
if (selectable) store.handleEdgeSelection(id);
onedgeclick?.({ event, edge });
}
}
type EdgeMouseEvent = 'edgecontextmenu' | 'edgemouseenter' | 'edgemouseleave';
function onMouseEvent(event: MouseEvent, type: EdgeMouseEvent) {
const edge = $edgeLookup.get(id);
function onmouseevent<T = MouseEvent>(
event: T,
callback: ({ edge, event }: { edge: EdgeType; event: T }) => void
) {
const edge = store.edgeLookup.get(id);
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>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
{#if !hidden}
<svg style:z-index={zIndex}>
<svg style:z-index={zIndex} class="svelte-flow__edge-wrapper">
<g
class={cc(['svelte-flow__edge', className])}
bind:this={edgeRef}
class={['svelte-flow__edge', className]}
class:animated
class:selected
class:selectable={isSelectable}
class:selectable
data-id={id}
on:click={onClick}
on:contextmenu={(e) => {
onMouseEvent(e, 'edgecontextmenu');
}}
on:mouseenter={(e) => {
onMouseEvent(e, 'edgemouseenter');
}}
on:mouseleave={(e) => {
onMouseEvent(e, 'edgemouseleave');
}}
{onclick}
oncontextmenu={onedgecontextmenu
? (e) => {
onmouseevent(e, onedgecontextmenu);
}
: undefined}
onpointerenter={onedgepointerenter
? (e) => {
onmouseevent(e, onedgepointerenter);
}
: undefined}
onpointerleave={onedgepointerleave
? (e) => {
onmouseevent(e, onedgepointerleave);
}
: undefined}
aria-label={ariaLabel === null
? undefined
: ariaLabel
? ariaLabel
: `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
this={edgeComponent}
<EdgeComponent
{id}
{source}
{target}
@@ -126,9 +158,9 @@
{data}
{style}
{interactionWidth}
selectable={isSelectable}
deletable={deletable ?? true}
type={edgeType}
{selectable}
{deletable}
{type}
sourceHandleId={sourceHandle}
targetHandleId={targetHandle}
markerStart={markerStartUrl}
@@ -1,71 +1,113 @@
<script lang="ts">
import { getContext } from 'svelte';
import type { Writable } from 'svelte/store';
import cc from 'classcat';
import {
Position,
XYHandle,
isMouseEvent,
type HandleConnection,
areConnectionMapsEqual,
handleConnectionChange,
ConnectionMode
ConnectionMode,
getHostForElement,
type HandleConnection,
type Optional,
type ConnectionState,
type Connection
} from '@xyflow/system';
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;
export let type: $$Props['type'] = 'source';
export let position: $$Props['position'] = Position.Top;
export let style: $$Props['style'] = undefined;
export let isValidConnection: $$Props['isValidConnection'] = undefined;
export let onconnect: $$Props['onconnect'] = undefined;
export let ondisconnect: $$Props['ondisconnect'] = undefined;
// @todo implement connectablestart, connectableend
// export let isConnectableStart: $$Props['isConnectableStart'] = undefined;
// export let isConnectableEnd: $$Props['isConnectableEnd'] = undefined;
let {
id: handleId = null,
type = 'source',
position = Position.Top,
style,
class: className,
isConnectable: isConnectableProp,
isConnectableStart = true,
isConnectableEnd = true,
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 connectable = getContext<Writable<boolean>>('svelteflow__node_connectable');
$: isConnectable = isConnectableProp !== undefined ? isConnectableProp : $connectable;
const isConnectableContext = getContext<ConnectableContext>('svelteflow__node_connectable');
$: handleId = id || null;
let isTarget = $derived(type === 'target');
let isConnectable = $derived(
isConnectableProp !== undefined ? isConnectableProp : isConnectableContext.value
);
const 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;
let store = useStore();
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);
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
@@ -73,77 +115,82 @@
handleId,
nodeId,
isTarget,
connectionRadius: $connectionRadius,
domNode: $domNode,
nodeLookup: $nodeLookup,
connectionMode: $connectionMode,
lib: $lib,
autoPanOnConnect: $autoPanOnConnect,
flowId: $flowId,
isValidConnection: isValidConnection ?? $isValidConnectionStore,
updateConnection,
cancelConnection,
panBy,
onConnect: (connection) => {
const edge = $onedgecreate ? $onedgecreate(connection) : connection;
if (!edge) {
return;
}
addEdge(edge);
$onConnectAction?.(connection);
},
connectionRadius: store.connectionRadius,
domNode: store.domNode,
nodeLookup: store.nodeLookup,
connectionMode: store.connectionMode,
lib: 'svelte',
autoPanOnConnect: store.autoPanOnConnect,
flowId: store.flowId,
isValidConnection: isValidConnection ?? store.isValidConnection,
updateConnection: store.updateConnection,
cancelConnection: store.cancelConnection,
panBy: store.panBy,
onConnect: onConnectExtended,
onConnectStart: (event, startParams) => {
$onConnectStartAction?.(event, {
store.onconnectstart?.(event, {
nodeId: startParams.nodeId,
handleId: startParams.handleId,
handleType: startParams.handleType
});
},
onConnectEnd: (event, connectionState) => {
$onConnectEndAction?.(event, connectionState);
store.onconnectend?.(event, connectionState);
},
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
getFromHandle: () => $connection.fromHandle
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
getFromHandle: () => store.connection.fromHandle
});
}
}
let prevConnections: Map<string, HandleConnection> | null = null;
let connections: Map<string, HandleConnection> | undefined;
$: 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);
function onclick(event: MouseEvent) {
if (!nodeId || (!store.clickConnectStartHandle && !isConnectableStart)) {
return;
}
prevConnections = connections ?? new Map();
}
if (!store.clickConnectStartHandle) {
store.onclickconnectstart?.(event, { nodeId, handleId, handleType: type });
store.clickConnectStartHandle = { nodeId, type, id: handleId };
return;
}
$: connectionInProcess = !!$connection.fromHandle;
$: connectingFrom =
$connection.fromHandle?.nodeId === nodeId &&
$connection.fromHandle?.type === type &&
$connection.fromHandle?.id === handleId;
$: connectingTo =
$connection.toHandle?.nodeId === nodeId &&
$connection.toHandle?.type === type &&
$connection.toHandle?.id === handleId;
$: isPossibleEndHandle =
$connectionMode === ConnectionMode.Strict
? $connection.fromHandle?.type !== type
: nodeId !== $connection.fromHandle?.nodeId || handleId !== $connection.fromHandle?.id;
$: valid = connectingTo && $connection.isValid;
const doc = getHostForElement(event.target);
const isValidConnectionHandler = isValidConnection ?? store.isValidConnection;
const { connectionMode, clickConnectStartHandle, flowId, nodeLookup } = store;
const { connection, isValid } = XYHandle.isValid(event, {
handle: {
nodeId,
id: handleId,
type
},
connectionMode,
fromNodeId: clickConnectStartHandle.nodeId,
fromHandleId: clickConnectStartHandle.id ?? null,
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>
<!--
@@ -154,29 +201,34 @@ The Handle component is the part of a node that can be used to connect nodes.
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
data-id="{$flowId}-{nodeId}-{id || null}-{type}"
class={cc([
data-id="{store.flowId}-{nodeId}-{handleId ?? 'null'}-{type}"
class={[
'svelte-flow__handle',
`svelte-flow__handle-${position}`,
'nodrag',
'nopan',
store.noDragClass,
store.noPanClass,
position,
className
])}
]}
class:valid
class:connectingto={connectingTo}
class:connectingfrom={connectingFrom}
class:source={!isTarget}
class:target={isTarget}
class:connectablestart={isConnectable}
class:connectableend={isConnectable}
class:connectablestart={isConnectableStart}
class:connectableend={isConnectableEnd}
class:connectable={isConnectable}
class:connectionindicator={isConnectable && (!connectionInProcess || isPossibleEndHandle)}
on:mousedown={onPointerDown}
on:touchstart={onPointerDown}
class:connectionindicator={isConnectable &&
(!connectionInProgress || isPossibleTargetHandle) &&
(connectionInProgress || store.clickConnectStartHandle ? isConnectableEnd : isConnectableStart)}
onmousedown={onpointerdown}
ontouchstart={onpointerdown}
onclick={store.clickConnect ? onclick : undefined}
onkeypress={() => {}}
{style}
role="button"
tabindex="-1"
{...rest}
>
<slot />
{@render children?.()}
</div>
@@ -1 +1,2 @@
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 {
shortcut,
type ShortcutEventDetail,
@@ -6,26 +6,20 @@
} from '@svelte-put/shortcut';
import { isInputDOMNode, isMacOs } from '@xyflow/system';
import { useStore } from '$lib/store';
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';
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();
let { deleteElements } = useSvelteFlow<NodeType, EdgeType>();
function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject {
return key !== null && typeof key === 'object';
@@ -62,12 +56,29 @@
}
function resetKeysAndSelection() {
selectionRect.set(null);
selectionKeyPressed.set(false);
multiselectionKeyPressed.set(false);
deleteKeyPressed.set(false);
panActivationKeyPressed.set(false);
zoomActivationKeyPressed.set(false);
store.selectionRect = null;
store.selectionKeyPressed = false;
store.multiselectionKeyPressed = false;
store.deleteKeyPressed = false;
store.panActivationKeyPressed = false;
store.zoomActivationKeyPressed = false;
}
async function handleDelete() {
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>
@@ -75,19 +86,21 @@
on:blur={resetKeysAndSelection}
on:contextmenu={resetKeysAndSelection}
use:shortcut={{
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(true)),
trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(false)),
trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(true)),
trigger: getShortcutTrigger(multiSelectionKey, () => {
store.multiselectionKeyPressed = true;
}),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(false)),
trigger: getShortcutTrigger(multiSelectionKey, () => (store.multiselectionKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
@@ -97,29 +110,30 @@
detail.originalEvent.metaKey ||
detail.originalEvent.shiftKey;
if (!isModifierKey && !isInputDOMNode(detail.originalEvent)) {
deleteKeyPressed.set(true);
store.deleteKeyPressed = true;
handleDelete();
}
}),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(deleteKey, () => deleteKeyPressed.set(false)),
trigger: getShortcutTrigger(deleteKey, () => (store.deleteKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(true)),
trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(false)),
trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(true)),
trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(false)),
trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = false)),
type: 'keyup'
}}
/>
@@ -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;
multiSelectionKey?: KeyDefinition | KeyDefinition[] | null;
deleteKey?: KeyDefinition | KeyDefinition[] | null;
@@ -1,73 +1,95 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import { getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system';
import { useStore } from '$lib/store';
import { Selection } from '$lib/components/Selection';
import drag from '$lib/actions/drag';
import type { Node, NodeEventMap } from '$lib/types';
const store = useStore();
const { selectionRectMode, nodes, nodeLookup } = store;
import type { NodeSelectionProps } from './types';
import { arrowKeyDiffs, toPxString } from '$lib/utils';
import type { Node, Edge } from '$lib/types';
const dispatch = createEventDispatcher<
NodeEventMap & {
selectioncontextmenu: { nodes: Node[]; event: MouseEvent | TouchEvent };
selectionclick: { nodes: Node[]; event: MouseEvent | TouchEvent };
let {
store = $bindable(),
onnodedrag,
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') {
bounds = getInternalNodesBounds($nodeLookup, { filter: (node) => !!node.selected });
$nodes;
function oncontextmenu(event: MouseEvent) {
const selectedNodes = store.nodes.filter((n) => n.selected);
onselectioncontextmenu?.({ nodes: selectedNodes, event });
}
function onContextMenu(event: MouseEvent | TouchEvent) {
const selectedNodes = $nodes.filter((n) => n.selected);
dispatch('selectioncontextmenu', { nodes: selectedNodes, event });
function onclick(event: MouseEvent) {
const selectedNodes = store.nodes.filter((n) => n.selected);
onselectionclick?.({ nodes: selectedNodes, event });
}
function onClick(event: MouseEvent | TouchEvent) {
const selectedNodes = $nodes.filter((n) => n.selected);
dispatch('selectionclick', { nodes: selectedNodes, event });
function onkeydown(event: KeyboardEvent) {
if (Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
event.preventDefault();
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
}
}
</script>
{#if $selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
{#if store.selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
<div
class="selection-wrapper nopan"
style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)"
class={['svelte-flow__selection-wrapper', store.noPanClass]}
style:width={toPxString(bounds.width)}
style:height={toPxString(bounds.height)}
style:transform="translate({bounds.x}px, {bounds.y}px)"
use:drag={{
disabled: false,
store,
onDrag: (event, _, __, nodes) => {
dispatch('nodedrag', { event, targetNode: null, nodes });
onnodedrag?.({ event, targetNode: null, nodes: nodes as NodeType[] });
},
onDragStart: (event, _, __, nodes) => {
dispatch('nodedragstart', { event, targetNode: null, nodes });
onnodedragstart?.({ event, targetNode: null, nodes: nodes as NodeType[] });
},
onDragStop: (event, _, __, nodes) => {
dispatch('nodedragstop', { event, targetNode: null, nodes });
onnodedragstop?.({ event, targetNode: null, nodes: nodes as NodeType[] });
}
}}
on:contextmenu={onContextMenu}
on:click={onClick}
role="button"
tabindex="-1"
on:keyup={() => {}}
{oncontextmenu}
{onclick}
role={store.disableKeyboardA11y ? undefined : 'button'}
tabIndex={store.disableKeyboardA11y ? undefined : -1}
onkeydown={store.disableKeyboardA11y ? undefined : onkeydown}
bind:this={ref}
>
<Selection width="100%" height="100%" x={0} y={0} />
</div>
{/if}
<style>
.selection-wrapper {
.svelte-flow__selection-wrapper {
position: absolute;
top: 0;
left: 0;
z-index: 7;
z-index: 2000;
pointer-events: all;
}
</style>

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