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