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