Merge branch 'main' into bug-fix-node-positions
This commit is contained in:
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
"@xyflow/react": minor
|
|
||||||
"@xyflow/svelte": minor
|
|
||||||
"@xyflow/system": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Add `ariaRole` prop to nodes and edges
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@xyflow/react': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix node fallback to respect custom default node type when unknown node type is encountered
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'@xyflow/svelte': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix data in `EdgeProps` that was not typed correctly
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
'@xyflow/react': minor
|
|
||||||
'@xyflow/svelte': minor
|
|
||||||
'@xyflow/system': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Improve typing for Nodes
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'@xyflow/svelte': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix setting nodesInitialized multiple times
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
'@xyflow/react': minor
|
|
||||||
'@xyflow/svelte': minor
|
|
||||||
'@xyflow/system': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Add an `ease` and `interpolate` option to all function that alter the viewport
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'@xyflow/svelte': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Change a11y description inline styles to classes
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'@xyflow/react': patch
|
||||||
|
'@xyflow/svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Return intersections correctly of passed node is bigger than intersecting nodes
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@xyflow/react': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix edge fallback to respect custom default edge type when unknown edge type is encountered.
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
"@xyflow/react": minor
|
|
||||||
"@xyflow/svelte": minor
|
|
||||||
"@xyflow/system": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'@xyflow/svelte': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Export missing callback types
|
|
||||||
+37
-1
@@ -41,7 +41,7 @@ Talking to us first about the enhancement you want to build will be the most lik
|
|||||||
To ask about a possible enhancement, email us at info@reactflow.dev
|
To ask about a possible enhancement, email us at info@reactflow.dev
|
||||||
|
|
||||||
|
|
||||||
### 💫 Pull Requests
|
## 💫 Pull Requests
|
||||||
|
|
||||||
If you want to contribute improvements or new features we are happy to review your PR :)
|
If you want to contribute improvements or new features we are happy to review your PR :)
|
||||||
Please use a meaningful commit message and add a little description of your changes.
|
Please use a meaningful commit message and add a little description of your changes.
|
||||||
@@ -50,3 +50,39 @@ Please use a meaningful commit message and add a little description of your chan
|
|||||||
2. Start dev server `pnpm dev`
|
2. Start dev server `pnpm dev`
|
||||||
3. Test your changes with the existing examples or add a new one if it's needed for your changes
|
3. Test your changes with the existing examples or add a new one if it's needed for your changes
|
||||||
4. Run tests `pnpm test` and add new new tests if you are introducing a new feature
|
4. Run tests `pnpm test` and add new new tests if you are introducing a new feature
|
||||||
|
|
||||||
|
## Changeset Style Guide
|
||||||
|
|
||||||
|
*Inspired and taken from [Common Changelogs](https://github.com/vweevers/common-changelog?tab=readme-ov-file) and [Warp by Broad Institute](https://broadinstitute.github.io/warp/docs/contribution/contribute_to_warp/changelog_style/)*
|
||||||
|
|
||||||
|
If you are writing a changeset for a PR, here are some helpful tips:
|
||||||
|
|
||||||
|
## TLDR
|
||||||
|
|
||||||
|
- Changelogs are for humans
|
||||||
|
- Communicate the impact of changes
|
||||||
|
- Use active voice *and* presence tense *(”Fix …” instead of “… was fixed”)*
|
||||||
|
- Omit redundant verbs
|
||||||
|
- *“Document” instead of “Add documentation”*
|
||||||
|
- Omit personal pronouns
|
||||||
|
- Use backticks for function or component names (`getNodesBounds`, `<ReactFlow />`, etc) for a better syntax highlighting
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**🛑 Bad:**
|
||||||
|
“minimap: use latest node dimensions”
|
||||||
|
|
||||||
|
**✅ Good:**
|
||||||
|
“Display minimap nodes even if onNodesChange is not implemented”
|
||||||
|
|
||||||
|
**🛑 Bad:**
|
||||||
|
“fix(handles): reconnect for connectionMode=loose”
|
||||||
|
|
||||||
|
**✅ Good:**
|
||||||
|
“Fix reconnections when connectionMode is set to loose”
|
||||||
|
|
||||||
|
**🛑 Bad:**
|
||||||
|
“use correct index when using setNodes for inserting”
|
||||||
|
|
||||||
|
**✅ Good:**
|
||||||
|
“Fix incorrect order of nodes added with setNodes from useReactFlow”
|
||||||
@@ -11,15 +11,15 @@
|
|||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/react": "^3.0.2",
|
"@astrojs/react": "^4.3.0",
|
||||||
"@astrojs/svelte": "^4.0.2",
|
"@astrojs/svelte": "^7.1.0",
|
||||||
"@types/react": "^18.2.24",
|
"@types/react": "^18.2.24",
|
||||||
"@types/react-dom": "^18.2.8",
|
"@types/react-dom": "^18.2.8",
|
||||||
"@xyflow/react": "workspace:^",
|
"@xyflow/react": "workspace:^",
|
||||||
"@xyflow/svelte": "workspace:^",
|
"@xyflow/svelte": "workspace:^",
|
||||||
"astro": "^3.2.2",
|
"astro": "^5.9.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"svelte": "^4.2.1"
|
"svelte": "^5.33.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { writable } from 'svelte/store';
|
import {
|
||||||
import { SvelteFlow, Controls, Background, BackgroundVariant, Position, type Node, type Edge } from '@xyflow/svelte';
|
SvelteFlow,
|
||||||
|
Controls,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
Position,
|
||||||
|
ViewportPortal,
|
||||||
|
type Node,
|
||||||
|
type Edge,
|
||||||
|
} from '@xyflow/svelte';
|
||||||
|
|
||||||
import '@xyflow/svelte/dist/style.css';
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
|
||||||
const nodes = writable<Node[]>([
|
let nodes = $state.raw<Node[]>([
|
||||||
{
|
{
|
||||||
id: '0',
|
id: '0',
|
||||||
position: { x: 0, y: 150 },
|
position: { x: 0, y: 150 },
|
||||||
@@ -59,7 +67,7 @@
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const edges = writable<Edge[]>([
|
let edges = $state.raw<Edge[]>([
|
||||||
{ id: '0A', source: '0', target: 'A', animated: true },
|
{ id: '0A', source: '0', target: 'A', animated: true },
|
||||||
{ id: '0B', source: '0', target: 'B', animated: true },
|
{ id: '0B', source: '0', target: 'B', animated: true },
|
||||||
{ id: '0C', source: '0', target: 'C', animated: true },
|
{ id: '0C', source: '0', target: 'C', animated: true },
|
||||||
@@ -71,8 +79,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="height: 400px; width: 700px;">
|
<div style="height: 400px; width: 700px;">
|
||||||
<SvelteFlow {nodes} {edges} fitView {defaultEdgeOptions} width={700} height={400}>
|
<SvelteFlow bind:nodes bind:edges fitView {defaultEdgeOptions} width={700} height={400}>
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background variant={BackgroundVariant.Dots} />
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
<ViewportPortal target="front">
|
||||||
|
<div style:transform="translate(100px, 100px)" style:position="absolute">[100, 100] inside the flow.</div>
|
||||||
|
</ViewportPortal>
|
||||||
</SvelteFlow>
|
</SvelteFlow>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
import { SvelteFlow, Controls, Background, BackgroundVariant, type Node, type Edge } from '@xyflow/svelte';
|
import { SvelteFlow, Controls, Background, BackgroundVariant, type Node, type Edge } from '@xyflow/svelte';
|
||||||
|
|
||||||
import '@xyflow/svelte/dist/style.css';
|
import '@xyflow/svelte/dist/style.css';
|
||||||
import CustomNode from './CustomNode.svelte';
|
import CustomNode from './CustomNode.svelte';
|
||||||
|
|
||||||
const nodes = writable<Node[]>([
|
let nodes = $state.raw<Node[]>([
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
@@ -24,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const edges = writable<Edge[]>([{ id: '1-2', source: '1', target: '2' }]);
|
let edges = $state.raw<Edge[]>([{ id: '1-2', source: '1', target: '2' }]);
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
'input-node': CustomNode,
|
'input-node': CustomNode,
|
||||||
@@ -32,7 +31,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="height: 400px; width: 700px;">
|
<div style="height: 400px; width: 700px;">
|
||||||
<SvelteFlow {nodes} {edges} {nodeTypes} fitView width={700} height={400}>
|
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView width={700} height={400}>
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background variant={BackgroundVariant.Dots} />
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
</SvelteFlow>
|
</SvelteFlow>
|
||||||
|
|||||||
@@ -6,38 +6,38 @@ import SvelteFlowInitialApp from '../components/SvelteFlowInitialExample/index.s
|
|||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<title>Astro example for React Flow and Svelte Flow</title>
|
<title>Astro example for React Flow and Svelte Flow</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html,
|
||||||
margin:0;
|
body {
|
||||||
font-family: sans-serif;
|
margin: 0;
|
||||||
}
|
font-family: sans-serif;
|
||||||
</style>
|
}
|
||||||
</head>
|
</style>
|
||||||
<body>
|
</head>
|
||||||
<h2>React Flow</h2>
|
<body>
|
||||||
<p>no client hydration</p>
|
<h2>React Flow</h2>
|
||||||
<ReactFlowApp />
|
<p>no client hydration</p>
|
||||||
|
<ReactFlowApp />
|
||||||
|
|
||||||
<p>client hydration on load (client:load)</p>
|
<p>client hydration on load (client:load)</p>
|
||||||
<ReactFlowApp client:load />
|
<ReactFlowApp client:load />
|
||||||
|
|
||||||
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
|
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
|
||||||
<ReactFlowInitialApp client:load />
|
<ReactFlowInitialApp client:load />
|
||||||
|
|
||||||
<h2>Svelte Flow</h2>
|
<h2>Svelte Flow</h2>
|
||||||
<SvelteFlowApp />
|
<SvelteFlowApp />
|
||||||
|
|
||||||
<p>client hydration on load (client:load)</p>
|
<p>client hydration on load (client:load)</p>
|
||||||
<SvelteFlowApp client:load />
|
<SvelteFlowApp client:load />
|
||||||
|
|
||||||
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
|
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
|
||||||
<SvelteFlowInitialApp client:load />
|
<SvelteFlowInitialApp client:load />
|
||||||
|
</body>
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import ControlledViewport from '../examples/ControlledViewport';
|
|||||||
import CustomConnectionLine from '../examples/CustomConnectionLine';
|
import CustomConnectionLine from '../examples/CustomConnectionLine';
|
||||||
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
|
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
|
||||||
import CustomNode from '../examples/CustomNode';
|
import CustomNode from '../examples/CustomNode';
|
||||||
|
import DefaultEdgeOverwrite from '../examples/DefaultEdgeOverwrite';
|
||||||
|
import DefaultNodeOverwrite from '../examples/DefaultNodeOverwrite';
|
||||||
import DefaultNodes from '../examples/DefaultNodes';
|
import DefaultNodes from '../examples/DefaultNodes';
|
||||||
import DragHandle from '../examples/DragHandle';
|
import DragHandle from '../examples/DragHandle';
|
||||||
import DragNDrop from '../examples/DragNDrop';
|
import DragNDrop from '../examples/DragNDrop';
|
||||||
@@ -129,6 +131,16 @@ const routes: IRoute[] = [
|
|||||||
path: 'custom-node',
|
path: 'custom-node',
|
||||||
component: CustomNode,
|
component: CustomNode,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Default Node Overwrite',
|
||||||
|
path: 'default-node-overwrite',
|
||||||
|
component: DefaultNodeOverwrite,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Default Edge Overwrite',
|
||||||
|
path: 'default-edge-overwrite',
|
||||||
|
component: DefaultEdgeOverwrite,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Default Nodes',
|
name: 'Default Nodes',
|
||||||
path: 'default-nodes',
|
path: 'default-nodes',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MouseEvent } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
@@ -8,15 +8,10 @@ import {
|
|||||||
ReactFlowProvider,
|
ReactFlowProvider,
|
||||||
Node,
|
Node,
|
||||||
Edge,
|
Edge,
|
||||||
OnNodeDrag,
|
|
||||||
AriaLabelConfig,
|
AriaLabelConfig,
|
||||||
|
Panel,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
|
|
||||||
const onNodeDragStart = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
|
|
||||||
const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
|
|
||||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -24,24 +19,46 @@ const initialNodes: Node[] = [
|
|||||||
data: { label: 'A11y Node 1' },
|
data: { label: 'A11y Node 1' },
|
||||||
position: { x: 250, y: 5 },
|
position: { x: 250, y: 5 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
domAttributes: {
|
||||||
|
tabIndex: 10,
|
||||||
|
'aria-roledescription': 'A11y Node',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
data: { label: 'Node 2' },
|
data: { label: 'Node 2' },
|
||||||
position: { x: 100, y: 100 },
|
position: { x: 1000, y: 100 },
|
||||||
className: 'light',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
data: { label: 'Node 3' },
|
data: { label: 'Node 3' },
|
||||||
position: { x: 400, y: 100 },
|
position: { x: 100, y: 100 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
ariaRole: 'button',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
data: { label: 'Node 4' },
|
||||||
|
position: { x: 300, y: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
data: { label: 'Node 5' },
|
||||||
|
position: { x: 400, y: 200 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
data: { label: 'Node 6' },
|
||||||
|
position: { x: -1000, y: 200 },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const initialEdges: Edge[] = [
|
const initialEdges: Edge[] = [
|
||||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||||
{ id: 'e1-3', source: '1', target: '3' },
|
{ id: 'e1-3', source: '1', target: '3' },
|
||||||
|
{ id: 'e1-4', source: '1', target: '4' },
|
||||||
|
{ id: 'e1-5', source: '4', target: '5' },
|
||||||
|
{ id: 'e1-6', source: '3', target: '6' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ariaLabelConfig: Partial<AriaLabelConfig> = {
|
const ariaLabelConfig: Partial<AriaLabelConfig> = {
|
||||||
@@ -59,19 +76,13 @@ const ariaLabelConfig: Partial<AriaLabelConfig> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const A11y = () => {
|
const A11y = () => {
|
||||||
|
const [autoPanOnNodeFocus, setAutoPanOnNodeFocus] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
defaultNodes={initialNodes}
|
defaultNodes={initialNodes}
|
||||||
defaultEdges={initialEdges}
|
defaultEdges={initialEdges}
|
||||||
onNodesChange={console.log}
|
autoPanOnNodeFocus={autoPanOnNodeFocus}
|
||||||
onNodeClick={onNodeClick}
|
|
||||||
onNodeDragStop={onNodeDragStop}
|
|
||||||
onNodeDragStart={onNodeDragStart}
|
|
||||||
onNodeDrag={onNodeDrag}
|
|
||||||
className="react-flow-basic-example"
|
|
||||||
minZoom={0.2}
|
|
||||||
maxZoom={4}
|
|
||||||
fitView
|
|
||||||
selectNodesOnDrag={false}
|
selectNodesOnDrag={false}
|
||||||
elevateEdgesOnSelect
|
elevateEdgesOnSelect
|
||||||
elevateNodesOnSelect={false}
|
elevateNodesOnSelect={false}
|
||||||
@@ -81,6 +92,20 @@ const A11y = () => {
|
|||||||
<Background variant={BackgroundVariant.Dots} />
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
<Controls />
|
<Controls />
|
||||||
|
<Panel position="top-right">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="focusPannable">
|
||||||
|
<input
|
||||||
|
id="focusPannable"
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoPanOnNodeFocus}
|
||||||
|
onChange={(event) => setAutoPanOnNodeFocus(event.target.checked)}
|
||||||
|
className="xy-theme__checkbox"
|
||||||
|
/>
|
||||||
|
autoPanOnNodeFocus
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const ConnectionLineFlow = () => {
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
connectionLineComponent={ConnectionLine}
|
connectionLineComponent={ConnectionLine}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
|
connectionDragThreshold={25}
|
||||||
>
|
>
|
||||||
<Background variant={BackgroundVariant.Lines} />
|
<Background variant={BackgroundVariant.Lines} />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
ReactFlowProvider,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
EdgeProps,
|
||||||
|
getBezierPath,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
const initialNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
data: { label: 'Node 1' },
|
||||||
|
position: { x: 250, y: 5 },
|
||||||
|
className: 'light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
data: { label: 'Node 2' },
|
||||||
|
position: { x: 100, y: 100 },
|
||||||
|
className: 'light',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialEdges: Edge[] = [
|
||||||
|
{
|
||||||
|
id: 'e1-2',
|
||||||
|
source: '1',
|
||||||
|
target: '2',
|
||||||
|
type: 'unregistered', // This will fallback to custom default
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CustomEdge = ({ sourceX, sourceY, targetX, targetY }: EdgeProps) => {
|
||||||
|
const [edgePath] = getBezierPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<path d={edgePath} stroke="red" strokeWidth={3} fill="none" strokeDasharray="5,5" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const edgeTypes = {
|
||||||
|
default: CustomEdge,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultEdgeOverwrite = () => {
|
||||||
|
return (
|
||||||
|
<ReactFlow defaultNodes={initialNodes} defaultEdges={initialEdges} edgeTypes={edgeTypes} fitView>
|
||||||
|
<Background variant={BackgroundVariant.Lines} />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<DefaultEdgeOverwrite />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { ReactFlow, Node, ReactFlowProvider, Background, BackgroundVariant, NodeProps } from '@xyflow/react';
|
||||||
|
|
||||||
|
const initialNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
data: { label: 'Node 1' },
|
||||||
|
position: { x: 250, y: 5 },
|
||||||
|
className: 'light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
data: { label: 'Node 2' },
|
||||||
|
type: 'unregistered',
|
||||||
|
position: { x: 100, y: 100 },
|
||||||
|
className: 'light',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CustomNode = (_: NodeProps) => {
|
||||||
|
return <div>Custom node</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
default: CustomNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultNodeOverwrite = () => {
|
||||||
|
return (
|
||||||
|
<ReactFlow defaultNodes={initialNodes} nodeTypes={nodeTypes} fitView>
|
||||||
|
<Background variant={BackgroundVariant.Lines} />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<DefaultNodeOverwrite />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ const CustomResizerNode: FC<NodeProps> = ({ data }) => {
|
|||||||
resizeDirection="horizontal"
|
resizeDirection="horizontal"
|
||||||
minWidth={100}
|
minWidth={100}
|
||||||
maxWidth={500}
|
maxWidth={500}
|
||||||
|
color="orange"
|
||||||
|
autoScale={false}
|
||||||
/>
|
/>
|
||||||
<Handle type="target" position={Position.Left} />
|
<Handle type="target" position={Position.Left} />
|
||||||
<div>{data.label}</div>
|
<div>{data.label}</div>
|
||||||
|
|||||||
@@ -152,10 +152,10 @@ const initialEdges: Edge[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ id: 'e1-3', source: '1', target: '3' },
|
{ id: 'e1-3', source: '1', target: '3' },
|
||||||
{ id: 'e3-4', source: '3', target: '4', zIndex: 100 },
|
{ id: 'e3-4', source: '3', target: '4' },
|
||||||
{ id: 'e3-4b', source: '3', target: '4b' },
|
{ id: 'e3-4b', source: '3', target: '4b' },
|
||||||
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
|
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
|
||||||
{ id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 },
|
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
|
||||||
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
|
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,29 @@ export default {
|
|||||||
data: { label: '11' },
|
data: { label: '11' },
|
||||||
position: { x: 100, y: 500 },
|
position: { x: 100, y: 500 },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '12',
|
||||||
|
data: { label: '12' },
|
||||||
|
position: { x: 100, y: 600 },
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '12-a',
|
||||||
|
parentId: '12',
|
||||||
|
data: { label: '12-a' },
|
||||||
|
position: { x: 10, y: 20 },
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '12-b',
|
||||||
|
parentId: '12',
|
||||||
|
data: { label: '12-b' },
|
||||||
|
position: { x: 140, y: 20 },
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// id: '12',
|
// id: '12',
|
||||||
// data: { label: '12' },
|
// data: { label: '12' },
|
||||||
@@ -145,6 +168,16 @@ export default {
|
|||||||
markerEnd: { type: MarkerType.Arrow },
|
markerEnd: { type: MarkerType.Arrow },
|
||||||
markerStart: { type: MarkerType.ArrowClosed },
|
markerStart: { type: MarkerType.ArrowClosed },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'subflow-edge',
|
||||||
|
source: '11',
|
||||||
|
target: '12-a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subflow-edge-2',
|
||||||
|
source: '12-a',
|
||||||
|
target: '12-b',
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// id: 'updatable',
|
// id: 'updatable',
|
||||||
// source: '9',
|
// source: '9',
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
"@sveltejs/kit": "^2.20.7",
|
"@sveltejs/kit": "^2.20.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||||
"@typescript-eslint/parser": "^8.30.1",
|
"@typescript-eslint/parser": "^8.34.0",
|
||||||
"eslint": "^9.24.0",
|
"eslint": "^9.24.0",
|
||||||
"eslint-config-prettier": "^10.1.2",
|
"eslint-config-prettier": "^10.1.2",
|
||||||
"eslint-plugin-svelte": "^3.5.1",
|
"eslint-plugin-svelte": "^3.5.1",
|
||||||
|
|||||||
@@ -61,6 +61,29 @@ export default {
|
|||||||
id: '11',
|
id: '11',
|
||||||
data: { label: '11' },
|
data: { label: '11' },
|
||||||
position: { x: 100, y: 500 }
|
position: { x: 100, y: 500 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '12',
|
||||||
|
data: { label: '12' },
|
||||||
|
position: { x: 100, y: 600 },
|
||||||
|
width: 200,
|
||||||
|
height: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '12-a',
|
||||||
|
parentId: '12',
|
||||||
|
data: { label: '12-a' },
|
||||||
|
position: { x: 10, y: 20 },
|
||||||
|
width: 50,
|
||||||
|
height: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '12-b',
|
||||||
|
parentId: '12',
|
||||||
|
data: { label: '12-b' },
|
||||||
|
position: { x: 140, y: 20 },
|
||||||
|
width: 50,
|
||||||
|
height: 50
|
||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
// id: '12',
|
// id: '12',
|
||||||
@@ -142,6 +165,16 @@ export default {
|
|||||||
label: 'markers',
|
label: 'markers',
|
||||||
markerEnd: { type: MarkerType.Arrow },
|
markerEnd: { type: MarkerType.Arrow },
|
||||||
markerStart: { type: MarkerType.ArrowClosed }
|
markerStart: { type: MarkerType.ArrowClosed }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subflow-edge',
|
||||||
|
source: '11',
|
||||||
|
target: '12-a'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subflow-edge-2',
|
||||||
|
source: '12-a',
|
||||||
|
target: '12-b'
|
||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
// id: 'updatable',
|
// id: 'updatable',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { SvelteFlow, Controls, Background, MiniMap } from '@xyflow/svelte';
|
import { SvelteFlow, Controls, Background, MiniMap, Panel } from '@xyflow/svelte';
|
||||||
|
|
||||||
import '@xyflow/svelte/dist/style.css';
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
data: { label: 'A' }
|
data: { label: 'A' }
|
||||||
},
|
},
|
||||||
{ id: 'B', position: { x: -100, y: 150 }, data: { label: 'B' } },
|
{ id: 'B', position: { x: -100, y: 150 }, data: { label: 'B' } },
|
||||||
{ id: 'C', position: { x: 100, y: 150 }, data: { label: 'C' } },
|
{ id: 'C', position: { x: 1000, y: 150 }, data: { label: 'C' } },
|
||||||
{ id: 'D', position: { x: 0, y: 260 }, data: { label: 'D' } }
|
{ id: 'D', position: { x: 0, y: 260 }, data: { label: 'D' } }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -19,17 +19,19 @@
|
|||||||
{ id: 'A-C', source: 'A', target: 'C' },
|
{ id: 'A-C', source: 'A', target: 'C' },
|
||||||
{ id: 'A-D', source: 'A', target: 'D' }
|
{ id: 'A-D', source: 'A', target: 'D' }
|
||||||
]);
|
]);
|
||||||
</script>
|
let autoPanOnNodeFocus = $state(true);
|
||||||
|
const ariaLabelConfig = $state({
|
||||||
<SvelteFlow
|
|
||||||
bind:nodes
|
|
||||||
bind:edges
|
|
||||||
fitView
|
|
||||||
ariaLabelConfig={{
|
|
||||||
'node.a11yDescription.default': 'Svelte Custom Node Desc.',
|
'node.a11yDescription.default': 'Svelte Custom Node Desc.',
|
||||||
'node.a11yDescription.keyboardDisabled': 'Svelte Custom Keyboard Desc.',
|
'node.a11yDescription.keyboardDisabled': 'Svelte Custom Keyboard Desc.',
|
||||||
'node.a11yDescription.ariaLiveMessage': ({ direction, x, y }) =>
|
'node.a11yDescription.ariaLiveMessage': ({
|
||||||
`Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`,
|
direction,
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
}: {
|
||||||
|
direction: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}) => `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`,
|
||||||
'edge.a11yDescription.default': 'Svelte Custom Edge Desc.',
|
'edge.a11yDescription.default': 'Svelte Custom Edge Desc.',
|
||||||
'controls.ariaLabel': 'Svelte Custom Control Aria Label',
|
'controls.ariaLabel': 'Svelte Custom Control Aria Label',
|
||||||
'controls.zoomIn.ariaLabel': 'Svelte Custom Zoom in',
|
'controls.zoomIn.ariaLabel': 'Svelte Custom Zoom in',
|
||||||
@@ -37,9 +39,24 @@
|
|||||||
// 'controls.fitView.ariaLabel': 'Svelte Custom Fit View',
|
// 'controls.fitView.ariaLabel': 'Svelte Custom Fit View',
|
||||||
'controls.interactive.ariaLabel': 'Svelte Custom Toggle Interactivity',
|
'controls.interactive.ariaLabel': 'Svelte Custom Toggle Interactivity',
|
||||||
'minimap.ariaLabel': 'Svelte Custom Minimap'
|
'minimap.ariaLabel': 'Svelte Custom Minimap'
|
||||||
}}
|
});
|
||||||
>
|
</script>
|
||||||
|
|
||||||
|
<SvelteFlow bind:nodes bind:edges {autoPanOnNodeFocus} {ariaLabelConfig}>
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background />
|
<Background />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
|
<Panel class="panel top-right">
|
||||||
|
<div>
|
||||||
|
<label for="focusPannable">
|
||||||
|
Enable Pan on Focus
|
||||||
|
<input
|
||||||
|
id="focusPannable"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={autoPanOnNodeFocus}
|
||||||
|
class="svelte-flow__zoomonscroll"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
</SvelteFlow>
|
</SvelteFlow>
|
||||||
|
|||||||
@@ -24,7 +24,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="height:100vh;">
|
<div style="height:100vh;">
|
||||||
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView connectionLineComponent={ConnectionLine}>
|
<SvelteFlow
|
||||||
|
bind:nodes
|
||||||
|
bind:edges
|
||||||
|
{nodeTypes}
|
||||||
|
fitView
|
||||||
|
connectionLineComponent={ConnectionLine}
|
||||||
|
connectionDragThreshold={100}
|
||||||
|
>
|
||||||
<Background variant={BackgroundVariant.Lines} />
|
<Background variant={BackgroundVariant.Lines} />
|
||||||
</SvelteFlow>
|
</SvelteFlow>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,15 @@
|
|||||||
handleType: 'target'
|
handleType: 'target'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useNodeConnections({
|
||||||
|
onConnect: (connection) => {
|
||||||
|
console.log('Connection made:', connection);
|
||||||
|
},
|
||||||
|
onDisconnect: (connection) => {
|
||||||
|
console.log('Connection disconnected:', connection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let nodeData = $derived(
|
let nodeData = $derived(
|
||||||
useNodesData<MyNode>(connections.current.map((connection) => connection.source))
|
useNodesData<MyNode>(connections.current.map((connection) => connection.source))
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@
|
|||||||
"@changesets/cli": "^2.25.0",
|
"@changesets/cli": "^2.25.0",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
"eslint": "^8.22.0",
|
"eslint": "^8.57.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@@ -1,5 +1,65 @@
|
|||||||
# @xyflow/react
|
# @xyflow/react
|
||||||
|
|
||||||
|
## 12.8.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`26f2cdd7`](https://github.com/xyflow/xyflow/commit/26f2cdd720fc2c8fb337d3af13b82dab6a90fb60)]:
|
||||||
|
- @xyflow/system@0.0.65
|
||||||
|
|
||||||
|
## 12.8.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#5361](https://github.com/xyflow/xyflow/pull/5361) [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c) Thanks [@peterkogo](https://github.com/peterkogo)! - Render edges above nodes when they are within a subflow
|
||||||
|
|
||||||
|
- [#5344](https://github.com/xyflow/xyflow/pull/5344) [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde) Thanks [@moklick](https://github.com/moklick)! - Add connectionDragThreshold prop
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5368](https://github.com/xyflow/xyflow/pull/5368) [`445017ed`](https://github.com/xyflow/xyflow/commit/445017ed6fdedee4b39a0eaeb1cbbe0155f9a037) Thanks [@moklick](https://github.com/moklick)! - Cleanup store updater
|
||||||
|
|
||||||
|
- [#5362](https://github.com/xyflow/xyflow/pull/5362) [`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc) Thanks [@moklick](https://github.com/moklick)! - Remove pointer events from Panel via CSS while a selection gets dragged
|
||||||
|
|
||||||
|
- Updated dependencies [[`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc), [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c), [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde)]:
|
||||||
|
- @xyflow/system@0.0.64
|
||||||
|
|
||||||
|
## 12.7.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5354](https://github.com/xyflow/xyflow/pull/5354) [`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations
|
||||||
|
|
||||||
|
- [#5333](https://github.com/xyflow/xyflow/pull/5333) [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing type exports
|
||||||
|
|
||||||
|
- [#5353](https://github.com/xyflow/xyflow/pull/5353) [`4dd3ae66`](https://github.com/xyflow/xyflow/commit/4dd3ae6684fa3c62ee8cabd64e7631a52173c8f4) Thanks [@aidanbarrett](https://github.com/aidanbarrett)! - Add missing `FinalConnectionState` parameter to `onReconnectEnd` prop types
|
||||||
|
|
||||||
|
- Updated dependencies [[`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950), [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276), [`9c61000c`](https://github.com/xyflow/xyflow/commit/9c61000cac6277ce97274cc626fa7266f82dec27)]:
|
||||||
|
- @xyflow/system@0.0.63
|
||||||
|
|
||||||
|
## 12.7.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#5299](https://github.com/xyflow/xyflow/pull/5299) [`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaRole` prop to nodes and edges
|
||||||
|
|
||||||
|
- [#5280](https://github.com/xyflow/xyflow/pull/5280) [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac) Thanks [@moklick](https://github.com/moklick)! - Improve typing for Nodes
|
||||||
|
|
||||||
|
- [#5276](https://github.com/xyflow/xyflow/pull/5276) [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48) Thanks [@moklick](https://github.com/moklick)! - Add an `ease` and `interpolate` option to all function that alter the viewport
|
||||||
|
|
||||||
|
- [#5277](https://github.com/xyflow/xyflow/pull/5277) [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
|
||||||
|
|
||||||
|
- [#5317](https://github.com/xyflow/xyflow/pull/5317) [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850) Thanks [@moklick](https://github.com/moklick)! - Add `domAttributes` option for nodes and edges
|
||||||
|
|
||||||
|
- [#5326](https://github.com/xyflow/xyflow/pull/5326) [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent NodeResizer controls to become too small when zooming out
|
||||||
|
|
||||||
|
- [#5308](https://github.com/xyflow/xyflow/pull/5308) [`09fab679`](https://github.com/xyflow/xyflow/commit/09fab6794031410c9e9465281d038c3520afe783) Thanks [@printerscanner](https://github.com/printerscanner)! - Focus nodes on tab if not within the viewport and add a new prop `autoPanOnNodeFocus`
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1), [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac), [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48), [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef), [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850), [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf)]:
|
||||||
|
- @xyflow/system@0.0.62
|
||||||
|
|
||||||
## 12.6.4
|
## 12.6.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/react",
|
"name": "@xyflow/react",
|
||||||
"version": "12.6.4",
|
"version": "12.8.1",
|
||||||
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
|
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useRef, useEffect, memo } from 'react';
|
import { useRef, useEffect, memo, useCallback } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
|
import { shallow } from 'zustand/shallow';
|
||||||
import {
|
import {
|
||||||
XYResizer,
|
XYResizer,
|
||||||
ResizeControlVariant,
|
ResizeControlVariant,
|
||||||
@@ -13,18 +14,28 @@ import {
|
|||||||
evaluateAbsolutePosition,
|
evaluateAbsolutePosition,
|
||||||
ParentExpandChild,
|
ParentExpandChild,
|
||||||
XYPosition,
|
XYPosition,
|
||||||
|
ControlPosition,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import { useStoreApi } from '../../hooks/useStore';
|
import { useStoreApi, useStore } from '../../hooks/useStore';
|
||||||
import { useNodeId } from '../../contexts/NodeIdContext';
|
import { useNodeId } from '../../contexts/NodeIdContext';
|
||||||
import type { ResizeControlProps, ResizeControlLineProps } from './types';
|
import type { ResizeControlProps, ResizeControlLineProps } from './types';
|
||||||
|
import { ReactFlowState } from '../../types';
|
||||||
|
|
||||||
|
const scaleSelector = (calculateScale: boolean) => (store: ReactFlowState) =>
|
||||||
|
calculateScale ? `${Math.max(1 / store.transform[2], 1)}` : undefined;
|
||||||
|
|
||||||
|
const defaultPositions: Record<ResizeControlVariant, ControlPosition> = {
|
||||||
|
[ResizeControlVariant.Line]: 'right',
|
||||||
|
[ResizeControlVariant.Handle]: 'bottom-right',
|
||||||
|
};
|
||||||
|
|
||||||
function ResizeControl({
|
function ResizeControl({
|
||||||
nodeId,
|
nodeId,
|
||||||
position,
|
position,
|
||||||
variant = ResizeControlVariant.Handle,
|
variant = ResizeControlVariant.Handle,
|
||||||
className,
|
className,
|
||||||
style = {},
|
style = undefined,
|
||||||
children,
|
children,
|
||||||
color,
|
color,
|
||||||
minWidth = 10,
|
minWidth = 10,
|
||||||
@@ -33,6 +44,7 @@ function ResizeControl({
|
|||||||
maxHeight = Number.MAX_VALUE,
|
maxHeight = Number.MAX_VALUE,
|
||||||
keepAspectRatio = false,
|
keepAspectRatio = false,
|
||||||
resizeDirection,
|
resizeDirection,
|
||||||
|
autoScale = true,
|
||||||
shouldResize,
|
shouldResize,
|
||||||
onResizeStart,
|
onResizeStart,
|
||||||
onResize,
|
onResize,
|
||||||
@@ -42,10 +54,13 @@ function ResizeControl({
|
|||||||
const id = typeof nodeId === 'string' ? nodeId : contextNodeId;
|
const id = typeof nodeId === 'string' ? nodeId : contextNodeId;
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const resizeControlRef = useRef<HTMLDivElement>(null);
|
const resizeControlRef = useRef<HTMLDivElement>(null);
|
||||||
const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right';
|
const isHandleControl = variant === ResizeControlVariant.Handle;
|
||||||
const controlPosition = position ?? defaultPosition;
|
const scale = useStore(
|
||||||
|
useCallback(scaleSelector(isHandleControl && autoScale), [isHandleControl, autoScale]),
|
||||||
|
shallow
|
||||||
|
);
|
||||||
const resizer = useRef<XYResizerInstance | null>(null);
|
const resizer = useRef<XYResizerInstance | null>(null);
|
||||||
|
const controlPosition = position ?? defaultPositions[variant];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resizeControlRef.current || !id) {
|
if (!resizeControlRef.current || !id) {
|
||||||
@@ -192,14 +207,16 @@ function ResizeControl({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const positionClassNames = controlPosition.split('-');
|
const positionClassNames = controlPosition.split('-');
|
||||||
const colorStyleProp = variant === ResizeControlVariant.Line ? 'borderColor' : 'backgroundColor';
|
|
||||||
const controlStyle = color ? { ...style, [colorStyleProp]: color } : style;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cc(['react-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])}
|
className={cc(['react-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])}
|
||||||
ref={resizeControlRef}
|
ref={resizeControlRef}
|
||||||
style={controlStyle}
|
style={{
|
||||||
|
...style,
|
||||||
|
scale,
|
||||||
|
...(color && { [isHandleControl ? 'backgroundColor' : 'borderColor']: color }),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export function NodeResizer({
|
|||||||
maxWidth = Number.MAX_VALUE,
|
maxWidth = Number.MAX_VALUE,
|
||||||
maxHeight = Number.MAX_VALUE,
|
maxHeight = Number.MAX_VALUE,
|
||||||
keepAspectRatio = false,
|
keepAspectRatio = false,
|
||||||
|
autoScale = true,
|
||||||
shouldResize,
|
shouldResize,
|
||||||
onResizeStart,
|
onResizeStart,
|
||||||
onResize,
|
onResize,
|
||||||
@@ -66,6 +67,7 @@ export function NodeResizer({
|
|||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
onResizeStart={onResizeStart}
|
onResizeStart={onResizeStart}
|
||||||
keepAspectRatio={keepAspectRatio}
|
keepAspectRatio={keepAspectRatio}
|
||||||
|
autoScale={autoScale}
|
||||||
shouldResize={shouldResize}
|
shouldResize={shouldResize}
|
||||||
onResize={onResize}
|
onResize={onResize}
|
||||||
onResizeEnd={onResizeEnd}
|
onResizeEnd={onResizeEnd}
|
||||||
@@ -85,6 +87,7 @@ export function NodeResizer({
|
|||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
onResizeStart={onResizeStart}
|
onResizeStart={onResizeStart}
|
||||||
keepAspectRatio={keepAspectRatio}
|
keepAspectRatio={keepAspectRatio}
|
||||||
|
autoScale={autoScale}
|
||||||
shouldResize={shouldResize}
|
shouldResize={shouldResize}
|
||||||
onResize={onResize}
|
onResize={onResize}
|
||||||
onResizeEnd={onResizeEnd}
|
onResizeEnd={onResizeEnd}
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ export type NodeResizerProps = {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
keepAspectRatio?: boolean;
|
keepAspectRatio?: boolean;
|
||||||
|
/**
|
||||||
|
* Scale the controls with the zoom level.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoScale?: boolean;
|
||||||
/** Callback to determine if node should resize. */
|
/** Callback to determine if node should resize. */
|
||||||
shouldResize?: ShouldResize;
|
shouldResize?: ShouldResize;
|
||||||
/** Callback called when resizing starts. */
|
/** Callback called when resizing starts. */
|
||||||
@@ -82,6 +87,7 @@ export type ResizeControlProps = Pick<
|
|||||||
| 'maxHeight'
|
| 'maxHeight'
|
||||||
| 'keepAspectRatio'
|
| 'keepAspectRatio'
|
||||||
| 'shouldResize'
|
| 'shouldResize'
|
||||||
|
| 'autoScale'
|
||||||
| 'onResizeStart'
|
| 'onResizeStart'
|
||||||
| 'onResize'
|
| 'onResize'
|
||||||
| 'onResizeEnd'
|
| 'onResizeEnd'
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
// Reconnectable edges have a anchors around their handles to reconnect the edge.
|
// Reconnectable edges have a anchors around their handles to reconnect the edge.
|
||||||
import { XYHandle, type Connection, EdgePosition, FinalConnectionState, HandleType } from '@xyflow/system';
|
import {
|
||||||
|
XYHandle,
|
||||||
|
type Connection,
|
||||||
|
EdgePosition,
|
||||||
|
FinalConnectionState,
|
||||||
|
HandleType,
|
||||||
|
OnConnectStart,
|
||||||
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import { EdgeAnchor } from '../Edges/EdgeAnchor';
|
import { EdgeAnchor } from '../Edges/EdgeAnchor';
|
||||||
import type { EdgeWrapperProps, Edge } from '../../types/edges';
|
import type { EdgeWrapperProps, Edge } from '../../types/edges';
|
||||||
@@ -60,15 +67,17 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
|||||||
} = store.getState();
|
} = store.getState();
|
||||||
const isTarget = oppositeHandle.type === 'target';
|
const isTarget = oppositeHandle.type === 'target';
|
||||||
|
|
||||||
setReconnecting(true);
|
|
||||||
onReconnectStart?.(event, edge, oppositeHandle.type);
|
|
||||||
|
|
||||||
const _onReconnectEnd = (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => {
|
const _onReconnectEnd = (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => {
|
||||||
setReconnecting(false);
|
setReconnecting(false);
|
||||||
onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState);
|
onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
|
const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
|
||||||
|
const _onConnectStart: OnConnectStart = (_event, params) => {
|
||||||
|
setReconnecting(true);
|
||||||
|
onReconnectStart?.(event, edge, oppositeHandle.type);
|
||||||
|
onConnectStart?.(_event, params);
|
||||||
|
};
|
||||||
|
|
||||||
XYHandle.onPointerDown(event.nativeEvent, {
|
XYHandle.onPointerDown(event.nativeEvent, {
|
||||||
autoPanOnConnect,
|
autoPanOnConnect,
|
||||||
@@ -86,12 +95,13 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
|||||||
panBy,
|
panBy,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
onConnect: onConnectEdge,
|
onConnect: onConnectEdge,
|
||||||
onConnectStart,
|
onConnectStart: _onConnectStart,
|
||||||
onConnectEnd,
|
onConnectEnd,
|
||||||
onReconnectEnd: _onReconnectEnd,
|
onReconnectEnd: _onReconnectEnd,
|
||||||
updateConnection,
|
updateConnection,
|
||||||
getTransform: () => store.getState().transform,
|
getTransform: () => store.getState().transform,
|
||||||
getFromHandle: () => store.getState().connection.fromHandle,
|
getFromHandle: () => store.getState().connection.fromHandle,
|
||||||
|
dragThreshold: store.getState().connectionDragThreshold,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
|||||||
if (EdgeComponent === undefined) {
|
if (EdgeComponent === undefined) {
|
||||||
onError?.('011', errorMessages['error011'](edgeType));
|
onError?.('011', errorMessages['error011'](edgeType));
|
||||||
edgeType = 'default';
|
edgeType = 'default';
|
||||||
EdgeComponent = builtinEdgeTypes.default;
|
EdgeComponent = edgeTypes?.['default'] || builtinEdgeTypes.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
|
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
|
||||||
@@ -199,7 +199,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
|||||||
onKeyDown={isFocusable ? onKeyDown : undefined}
|
onKeyDown={isFocusable ? onKeyDown : undefined}
|
||||||
tabIndex={isFocusable ? 0 : undefined}
|
tabIndex={isFocusable ? 0 : undefined}
|
||||||
role={edge.ariaRole ?? (isFocusable ? 'group' : 'img')}
|
role={edge.ariaRole ?? (isFocusable ? 'group' : 'img')}
|
||||||
aria-roledescription={edge.ariaRoleDescription || 'edge'}
|
aria-roledescription="edge"
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-testid={`rf__edge-${id}`}
|
data-testid={`rf__edge-${id}`}
|
||||||
aria-label={
|
aria-label={
|
||||||
@@ -207,6 +207,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
|||||||
}
|
}
|
||||||
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
|
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
|
||||||
ref={edgeRef}
|
ref={edgeRef}
|
||||||
|
{...edge.domAttributes}
|
||||||
>
|
>
|
||||||
{!reconnecting && (
|
{!reconnecting && (
|
||||||
<EdgeComponent
|
<EdgeComponent
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ function HandleComponent(
|
|||||||
getTransform: () => store.getState().transform,
|
getTransform: () => store.getState().transform,
|
||||||
getFromHandle: () => store.getState().connection.fromHandle,
|
getFromHandle: () => store.getState().connection.fromHandle,
|
||||||
autoPanSpeed: currentStore.autoPanSpeed,
|
autoPanSpeed: currentStore.autoPanSpeed,
|
||||||
|
dragThreshold: currentStore.connectionDragThreshold,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getNodeDimensions,
|
getNodeDimensions,
|
||||||
isInputDOMNode,
|
isInputDOMNode,
|
||||||
nodeHasDimensions,
|
nodeHasDimensions,
|
||||||
|
getNodesInside,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||||
@@ -57,7 +58,7 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
if (NodeComponent === undefined) {
|
if (NodeComponent === undefined) {
|
||||||
onError?.('003', errorMessages['error003'](nodeType));
|
onError?.('003', errorMessages['error003'](nodeType));
|
||||||
nodeType = 'default';
|
nodeType = 'default';
|
||||||
NodeComponent = builtinNodeTypes.default;
|
NodeComponent = nodeTypes?.['default'] || builtinNodeTypes.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
|
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
|
||||||
@@ -158,6 +159,28 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
if (disableKeyboardA11y || !nodeRef.current?.matches(':focus-visible')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { transform, width, height, autoPanOnNodeFocus, setCenter } = store.getState();
|
||||||
|
|
||||||
|
if (!autoPanOnNodeFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withinViewport =
|
||||||
|
getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, transform, true).length > 0;
|
||||||
|
|
||||||
|
if (!withinViewport) {
|
||||||
|
setCenter(node.position.x + nodeDimensions.width / 2, node.position.y + nodeDimensions.height / 2, {
|
||||||
|
zoom: transform[2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cc([
|
className={cc([
|
||||||
@@ -195,10 +218,12 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
onDoubleClick={onDoubleClickHandler}
|
onDoubleClick={onDoubleClickHandler}
|
||||||
onKeyDown={isFocusable ? onKeyDown : undefined}
|
onKeyDown={isFocusable ? onKeyDown : undefined}
|
||||||
tabIndex={isFocusable ? 0 : undefined}
|
tabIndex={isFocusable ? 0 : undefined}
|
||||||
|
onFocus={isFocusable ? onFocus : undefined}
|
||||||
role={node.ariaRole ?? (isFocusable ? 'group' : undefined)}
|
role={node.ariaRole ?? (isFocusable ? 'group' : undefined)}
|
||||||
aria-roledescription={node.ariaRoleDescription || 'node'}
|
aria-roledescription="node"
|
||||||
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
|
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
|
||||||
aria-label={node.ariaLabel}
|
aria-label={node.ariaLabel}
|
||||||
|
{...node.domAttributes}
|
||||||
>
|
>
|
||||||
<Provider value={id}>
|
<Provider value={id}>
|
||||||
<NodeComponent
|
<NodeComponent
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { HTMLAttributes, forwardRef } from 'react';
|
|||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import type { PanelPosition } from '@xyflow/system';
|
import type { PanelPosition } from '@xyflow/system';
|
||||||
|
|
||||||
import { useStore } from '../../hooks/useStore';
|
|
||||||
import type { ReactFlowState } from '../../types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @expand
|
* @expand
|
||||||
*/
|
*/
|
||||||
@@ -16,8 +13,6 @@ export type PanelProps = HTMLAttributes<HTMLDivElement> & {
|
|||||||
position?: PanelPosition;
|
position?: PanelPosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `<Panel />` component helps you position content above the viewport.
|
* The `<Panel />` component helps you position content above the viewport.
|
||||||
* It is used internally by the [`<MiniMap />`](/api-reference/components/minimap)
|
* It is used internally by the [`<MiniMap />`](/api-reference/components/minimap)
|
||||||
@@ -45,16 +40,10 @@ const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all')
|
|||||||
*/
|
*/
|
||||||
export const Panel = forwardRef<HTMLDivElement, PanelProps>(
|
export const Panel = forwardRef<HTMLDivElement, PanelProps>(
|
||||||
({ position = 'top-left', children, className, style, ...rest }, ref) => {
|
({ position = 'top-left', children, className, style, ...rest }, ref) => {
|
||||||
const pointerEvents = useStore(selector);
|
|
||||||
const positionClasses = `${position}`.split('-');
|
const positionClasses = `${position}`.split('-');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cc(['react-flow__panel', className, ...positionClasses])} style={style} ref={ref} {...rest}>
|
||||||
className={cc(['react-flow__panel', className, ...positionClasses])}
|
|
||||||
style={{ ...style, pointerEvents }}
|
|
||||||
ref={ref}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const reactFlowFieldsToTrack = [
|
|||||||
'onClickConnectStart',
|
'onClickConnectStart',
|
||||||
'onClickConnectEnd',
|
'onClickConnectEnd',
|
||||||
'nodesDraggable',
|
'nodesDraggable',
|
||||||
|
'autoPanOnNodeFocus',
|
||||||
'nodesConnectable',
|
'nodesConnectable',
|
||||||
'nodesFocusable',
|
'nodesFocusable',
|
||||||
'edgesFocusable',
|
'edgesFocusable',
|
||||||
@@ -64,6 +65,7 @@ const reactFlowFieldsToTrack = [
|
|||||||
'isValidConnection',
|
'isValidConnection',
|
||||||
'selectNodesOnDrag',
|
'selectNodesOnDrag',
|
||||||
'nodeDragThreshold',
|
'nodeDragThreshold',
|
||||||
|
'connectionDragThreshold',
|
||||||
'onBeforeDelete',
|
'onBeforeDelete',
|
||||||
'debug',
|
'debug',
|
||||||
'autoPanSpeed',
|
'autoPanSpeed',
|
||||||
@@ -154,13 +156,11 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
|
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
|
||||||
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
|
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
|
||||||
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
|
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
|
||||||
|
else if (fieldName === 'ariaLabelConfig')
|
||||||
|
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
|
||||||
// Renamed fields
|
// Renamed fields
|
||||||
else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
|
else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
|
||||||
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
|
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
|
||||||
|
|
||||||
if (fieldName === 'ariaLabelConfig') {
|
|
||||||
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
|
|
||||||
}
|
|
||||||
// General case
|
// General case
|
||||||
else store.setState({ [fieldName]: fieldValue });
|
else store.setState({ [fieldName]: fieldValue });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
|||||||
onlyRenderVisibleElements = false,
|
onlyRenderVisibleElements = false,
|
||||||
selectNodesOnDrag,
|
selectNodesOnDrag,
|
||||||
nodesDraggable,
|
nodesDraggable,
|
||||||
|
autoPanOnNodeFocus,
|
||||||
nodesConnectable,
|
nodesConnectable,
|
||||||
nodesFocusable,
|
nodesFocusable,
|
||||||
nodeOrigin = defaultNodeOrigin,
|
nodeOrigin = defaultNodeOrigin,
|
||||||
@@ -138,6 +139,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
|||||||
style,
|
style,
|
||||||
id,
|
id,
|
||||||
nodeDragThreshold,
|
nodeDragThreshold,
|
||||||
|
connectionDragThreshold,
|
||||||
viewport,
|
viewport,
|
||||||
onViewportChange,
|
onViewportChange,
|
||||||
width,
|
width,
|
||||||
@@ -261,6 +263,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
|||||||
onClickConnectStart={onClickConnectStart}
|
onClickConnectStart={onClickConnectStart}
|
||||||
onClickConnectEnd={onClickConnectEnd}
|
onClickConnectEnd={onClickConnectEnd}
|
||||||
nodesDraggable={nodesDraggable}
|
nodesDraggable={nodesDraggable}
|
||||||
|
autoPanOnNodeFocus={autoPanOnNodeFocus}
|
||||||
nodesConnectable={nodesConnectable}
|
nodesConnectable={nodesConnectable}
|
||||||
nodesFocusable={nodesFocusable}
|
nodesFocusable={nodesFocusable}
|
||||||
edgesFocusable={edgesFocusable}
|
edgesFocusable={edgesFocusable}
|
||||||
@@ -304,6 +307,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
|||||||
isValidConnection={isValidConnection}
|
isValidConnection={isValidConnection}
|
||||||
selectNodesOnDrag={selectNodesOnDrag}
|
selectNodesOnDrag={selectNodesOnDrag}
|
||||||
nodeDragThreshold={nodeDragThreshold}
|
nodeDragThreshold={nodeDragThreshold}
|
||||||
|
connectionDragThreshold={connectionDragThreshold}
|
||||||
onBeforeDelete={onBeforeDelete}
|
onBeforeDelete={onBeforeDelete}
|
||||||
paneClickDistance={paneClickDistance}
|
paneClickDistance={paneClickDistance}
|
||||||
debug={debug}
|
debug={debug}
|
||||||
|
|||||||
@@ -222,7 +222,11 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
|
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
|
||||||
const partiallyVisible = partially && overlappingArea > 0;
|
const partiallyVisible = partially && overlappingArea > 0;
|
||||||
|
|
||||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
return (
|
||||||
|
partiallyVisible ||
|
||||||
|
overlappingArea >= currNodeRect.width * currNodeRect.height ||
|
||||||
|
overlappingArea >= nodeRect.width * nodeRect.height
|
||||||
|
);
|
||||||
}) as NodeType[];
|
}) as NodeType[];
|
||||||
},
|
},
|
||||||
isNodeIntersecting: (nodeOrRect, area, partially = true) => {
|
isNodeIntersecting: (nodeOrRect, area, partially = true) => {
|
||||||
|
|||||||
@@ -63,25 +63,7 @@ const useViewportHelper = (): ViewportHelperFunctions => {
|
|||||||
return { x, y, zoom };
|
return { x, y, zoom };
|
||||||
},
|
},
|
||||||
setCenter: async (x, y, options) => {
|
setCenter: async (x, y, options) => {
|
||||||
const { width, height, maxZoom, panZoom } = store.getState();
|
return store.getState().setCenter(x, y, options);
|
||||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
|
|
||||||
const centerX = width / 2 - x * nextZoom;
|
|
||||||
const centerY = height / 2 - y * nextZoom;
|
|
||||||
|
|
||||||
if (!panZoom) {
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
await panZoom.setViewport(
|
|
||||||
{
|
|
||||||
x: centerX,
|
|
||||||
y: centerY,
|
|
||||||
zoom: nextZoom,
|
|
||||||
},
|
|
||||||
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
|
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.resolve(true);
|
|
||||||
},
|
},
|
||||||
fitBounds: async (bounds, options) => {
|
fitBounds: async (bounds, options) => {
|
||||||
const { width, height, minZoom, maxZoom, panZoom } = store.getState();
|
const { width, height, minZoom, maxZoom, panZoom } = store.getState();
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ export {
|
|||||||
type NodeConnection,
|
type NodeConnection,
|
||||||
type OnReconnect,
|
type OnReconnect,
|
||||||
type AriaLabelConfig,
|
type AriaLabelConfig,
|
||||||
|
type SetCenter,
|
||||||
|
type SetViewport,
|
||||||
|
type FitBounds,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
// we need this workaround to prevent a duplicate identifier error
|
// we need this workaround to prevent a duplicate identifier error
|
||||||
|
|||||||
@@ -360,6 +360,26 @@ const createStore = ({
|
|||||||
|
|
||||||
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
|
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
|
||||||
},
|
},
|
||||||
|
setCenter: async (x, y, options) => {
|
||||||
|
const { width, height, maxZoom, panZoom } = get();
|
||||||
|
|
||||||
|
if (!panZoom) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
|
||||||
|
|
||||||
|
await panZoom.setViewport(
|
||||||
|
{
|
||||||
|
x: width / 2 - x * nextZoom,
|
||||||
|
y: height / 2 - y * nextZoom,
|
||||||
|
zoom: nextZoom,
|
||||||
|
},
|
||||||
|
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.resolve(true);
|
||||||
|
},
|
||||||
cancelConnection: () => {
|
cancelConnection: () => {
|
||||||
set({
|
set({
|
||||||
connection: { ...initialConnection },
|
connection: { ...initialConnection },
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ const getInitialState = ({
|
|||||||
noPanClassName: 'nopan',
|
noPanClassName: 'nopan',
|
||||||
nodeOrigin: storeNodeOrigin,
|
nodeOrigin: storeNodeOrigin,
|
||||||
nodeDragThreshold: 1,
|
nodeDragThreshold: 1,
|
||||||
|
connectionDragThreshold: 1,
|
||||||
|
|
||||||
snapGrid: [15, 15],
|
snapGrid: [15, 15],
|
||||||
snapToGrid: false,
|
snapToGrid: false,
|
||||||
@@ -134,7 +135,9 @@ const getInitialState = ({
|
|||||||
ariaLiveMessage: '',
|
ariaLiveMessage: '',
|
||||||
autoPanOnConnect: true,
|
autoPanOnConnect: true,
|
||||||
autoPanOnNodeDrag: true,
|
autoPanOnNodeDrag: true,
|
||||||
|
autoPanOnNodeFocus: true,
|
||||||
autoPanSpeed: 15,
|
autoPanSpeed: 15,
|
||||||
|
|
||||||
connectionRadius: 20,
|
connectionRadius: 20,
|
||||||
onError: devWarn,
|
onError: devWarn,
|
||||||
isValidConnection: undefined,
|
isValidConnection: undefined,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
SnapGrid,
|
SnapGrid,
|
||||||
OnReconnect,
|
OnReconnect,
|
||||||
AriaLabelConfig,
|
AriaLabelConfig,
|
||||||
|
FinalConnectionState,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -138,7 +139,7 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
|
|||||||
* This event fires when the user releases the source or target of an editable edge. It is called
|
* This event fires when the user releases the source or target of an editable edge. It is called
|
||||||
* even if an edge update does not occur.
|
* even if an edge update does not occur.
|
||||||
*/
|
*/
|
||||||
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
|
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType, connectionState: FinalConnectionState) => void;
|
||||||
/**
|
/**
|
||||||
* Use this event handler to add interactivity to a controlled flow.
|
* Use this event handler to add interactivity to a controlled flow.
|
||||||
* It is called on node drag, select, and move.
|
* It is called on node drag, select, and move.
|
||||||
@@ -383,6 +384,11 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
nodesDraggable?: boolean;
|
nodesDraggable?: boolean;
|
||||||
|
/**
|
||||||
|
* When `true`, the viewport will pan when a node is focused.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoPanOnNodeFocus?: boolean;
|
||||||
/**
|
/**
|
||||||
* Controls whether all nodes should be connectable or not. Individual nodes can override this
|
* Controls whether all nodes should be connectable or not. Individual nodes can override this
|
||||||
* setting by setting their `connectable` prop.
|
* setting by setting their `connectable` prop.
|
||||||
@@ -652,6 +658,12 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
|
|||||||
* @default 1
|
* @default 1
|
||||||
*/
|
*/
|
||||||
nodeDragThreshold?: number;
|
nodeDragThreshold?: number;
|
||||||
|
/**
|
||||||
|
* The threshold in pixels that the mouse must move before a connection line starts to drag.
|
||||||
|
* This is useful to prevent accidental connections when clicking on a handle.
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
connectionDragThreshold?: number;
|
||||||
/** Sets a fixed width for the flow. */
|
/** Sets a fixed width for the flow. */
|
||||||
width?: number;
|
width?: number;
|
||||||
/** Sets a fixed height for the flow. */
|
/** Sets a fixed height for the flow. */
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ export type Edge<
|
|||||||
* @default "group"
|
* @default "group"
|
||||||
*/
|
*/
|
||||||
ariaRole?: AriaRole;
|
ariaRole?: AriaRole;
|
||||||
|
/**
|
||||||
|
* General escape hatch for adding custom attributes to the edge's DOM element.
|
||||||
|
*/
|
||||||
|
domAttributes?: Omit<SVGAttributes<SVGGElement>, 'id' | 'style' | 'className' | 'role' | 'aria-label'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SmoothStepEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>> = Edge<
|
type SmoothStepEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>> = Edge<
|
||||||
@@ -180,12 +184,12 @@ export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd' | 'path' | '
|
|||||||
path: string;
|
path: string;
|
||||||
/**
|
/**
|
||||||
* The id of the SVG marker to use at the start of the edge. This should be defined in a
|
* The id of the SVG marker to use at the start of the edge. This should be defined in a
|
||||||
* `<defs>` element in a separate SVG document or element.
|
* `<defs>` element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
|
||||||
*/
|
*/
|
||||||
markerStart?: string;
|
markerStart?: string;
|
||||||
/**
|
/**
|
||||||
* The id of the SVG marker to use at the end of the edge. This should be defined in a `<defs>`
|
* The id of the SVG marker to use at the end of the edge. This should be defined in a `<defs>`
|
||||||
* element in a separate SVG document or element.
|
* element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
|
||||||
*/
|
*/
|
||||||
markerEnd?: string;
|
markerEnd?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ export type OnSelectionChangeFunc<NodeType extends Node = Node, EdgeType extends
|
|||||||
params: OnSelectionChangeParams<NodeType, EdgeType>
|
params: OnSelectionChangeParams<NodeType, EdgeType>
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inline
|
||||||
|
*/
|
||||||
export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>;
|
export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,9 +110,18 @@ export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<Node
|
|||||||
* transform the viewport smoothly over a given amount of time.
|
* transform the viewport smoothly over a given amount of time.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
|
* @inline
|
||||||
*/
|
*/
|
||||||
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
|
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inline
|
||||||
|
*/
|
||||||
export type FitView<NodeType extends Node = Node> = (fitViewOptions?: FitViewOptions<NodeType>) => Promise<boolean>;
|
export type FitView<NodeType extends Node = Node> = (fitViewOptions?: FitViewOptions<NodeType>) => Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inline
|
||||||
|
*/
|
||||||
export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
|
export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
|
||||||
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
|
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
|
||||||
) => void;
|
) => void;
|
||||||
@@ -209,4 +221,8 @@ export type OnBeforeDelete<NodeType extends Node = Node, EdgeType extends Edge =
|
|||||||
EdgeType
|
EdgeType
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This type can be used to type the `isValidConnection` function.
|
||||||
|
* If the function returns `true`, the connection is valid and can be created.
|
||||||
|
*/
|
||||||
export type IsValidConnection<EdgeType extends Edge = Edge> = (edge: EdgeType | Connection) => boolean;
|
export type IsValidConnection<EdgeType extends Edge = Edge> = (edge: EdgeType | Connection) => boolean;
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
|
|||||||
* parameter can be set to `true` to include nodes that are only partially intersecting.
|
* parameter can be set to `true` to include nodes that are only partially intersecting.
|
||||||
*
|
*
|
||||||
* @param node - the node or rect to check for intersections
|
* @param node - the node or rect to check for intersections
|
||||||
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
|
* @param partially - true by default, if set to false, only nodes that are fully intersecting will be returned
|
||||||
* @param nodes - optional nodes array to check for intersections
|
* @param nodes - optional nodes array to check for intersections
|
||||||
*
|
*
|
||||||
* @returns an array of intersecting nodes
|
* @returns an array of intersecting nodes
|
||||||
@@ -252,10 +252,10 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
|
|||||||
NodeType,
|
NodeType,
|
||||||
EdgeType
|
EdgeType
|
||||||
> &
|
> &
|
||||||
ViewportHelperFunctions & {
|
ViewportHelperFunctions & {
|
||||||
/**
|
/**
|
||||||
* React Flow needs to mount the viewport to the DOM and initialize its zoom and pan behavior.
|
* React Flow needs to mount the viewport to the DOM and initialize its zoom and pan behavior.
|
||||||
* This property tells you when viewport is initialized.
|
* This property tells you when viewport is initialized.
|
||||||
*/
|
*/
|
||||||
viewportInitialized: boolean;
|
viewportInitialized: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CSSProperties, MouseEvent as ReactMouseEvent, AriaRole } from 'react';
|
import type { CSSProperties, MouseEvent as ReactMouseEvent, AriaRole, HTMLAttributes, DOMAttributes } from 'react';
|
||||||
import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, InternalNodeBase } from '@xyflow/system';
|
import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, InternalNodeBase } from '@xyflow/system';
|
||||||
|
|
||||||
import { NodeTypes } from './general';
|
import { NodeTypes } from './general';
|
||||||
@@ -22,8 +22,15 @@ export type Node<
|
|||||||
* The ARIA role attribute for the node element, used for accessibility.
|
* The ARIA role attribute for the node element, used for accessibility.
|
||||||
* @default "group"
|
* @default "group"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ariaRole?: AriaRole;
|
ariaRole?: AriaRole;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General escape hatch for adding custom attributes to the node's DOM element.
|
||||||
|
*/
|
||||||
|
domAttributes?: Omit<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
'id' | 'style' | 'className' | 'draggable' | 'role' | 'aria-label' | keyof DOMAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
type EdgeChange,
|
type EdgeChange,
|
||||||
type ParentLookup,
|
type ParentLookup,
|
||||||
type AriaLabelConfig,
|
type AriaLabelConfig,
|
||||||
|
SetCenter,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -75,6 +76,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
|
|||||||
nodeExtent: CoordinateExtent;
|
nodeExtent: CoordinateExtent;
|
||||||
nodeOrigin: NodeOrigin;
|
nodeOrigin: NodeOrigin;
|
||||||
nodeDragThreshold: number;
|
nodeDragThreshold: number;
|
||||||
|
connectionDragThreshold: number;
|
||||||
|
|
||||||
nodesSelectionActive: boolean;
|
nodesSelectionActive: boolean;
|
||||||
userSelectionActive: boolean;
|
userSelectionActive: boolean;
|
||||||
@@ -88,6 +90,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
|
|||||||
snapGrid: SnapGrid;
|
snapGrid: SnapGrid;
|
||||||
|
|
||||||
nodesDraggable: boolean;
|
nodesDraggable: boolean;
|
||||||
|
autoPanOnNodeFocus: boolean;
|
||||||
nodesConnectable: boolean;
|
nodesConnectable: boolean;
|
||||||
nodesFocusable: boolean;
|
nodesFocusable: boolean;
|
||||||
edgesFocusable: boolean;
|
edgesFocusable: boolean;
|
||||||
@@ -171,6 +174,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
|||||||
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
|
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
|
||||||
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
|
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
|
||||||
panBy: PanBy;
|
panBy: PanBy;
|
||||||
|
setCenter: SetCenter;
|
||||||
setPaneClickDistance: (distance: number) => void;
|
setPaneClickDistance: (distance: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,83 @@
|
|||||||
# @xyflow/svelte
|
# @xyflow/svelte
|
||||||
|
|
||||||
|
## 1.2.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`26f2cdd7`](https://github.com/xyflow/xyflow/commit/26f2cdd720fc2c8fb337d3af13b82dab6a90fb60)]:
|
||||||
|
- @xyflow/system@0.0.65
|
||||||
|
|
||||||
|
## 1.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#5361](https://github.com/xyflow/xyflow/pull/5361) [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c) Thanks [@peterkogo](https://github.com/peterkogo)! - Render edges above nodes when they are within a subflow
|
||||||
|
|
||||||
|
- [#5344](https://github.com/xyflow/xyflow/pull/5344) [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde) Thanks [@moklick](https://github.com/moklick)! - Add connectionDragThreshold prop
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5362](https://github.com/xyflow/xyflow/pull/5362) [`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc) Thanks [@moklick](https://github.com/moklick)! - Remove pointer events from Panel via CSS while a selection gets dragged
|
||||||
|
|
||||||
|
- Updated dependencies [[`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc), [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c), [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde)]:
|
||||||
|
- @xyflow/system@0.0.64
|
||||||
|
|
||||||
|
## 1.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5339](https://github.com/xyflow/xyflow/pull/5339) [`56ebde81`](https://github.com/xyflow/xyflow/commit/56ebde811555775a0a9e2b2f88d35d9511f90c95) Thanks [@jrmajor](https://github.com/jrmajor)! - Fix `onmove`, `onmovestart` and `onmoveend` events not firing
|
||||||
|
|
||||||
|
- [#5354](https://github.com/xyflow/xyflow/pull/5354) [`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations
|
||||||
|
|
||||||
|
- [#5335](https://github.com/xyflow/xyflow/pull/5335) [`8474ba49`](https://github.com/xyflow/xyflow/commit/8474ba49cc1f48b6758a728fe4371f38260952e5) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent proxying objects in the store
|
||||||
|
|
||||||
|
- [#5336](https://github.com/xyflow/xyflow/pull/5336) [`d6db97c5`](https://github.com/xyflow/xyflow/commit/d6db97c53597db0eee8edd19beaf20b83f89fabf) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix `useNodeConnections` callbacks firing only when returned signal is used
|
||||||
|
|
||||||
|
- [#5333](https://github.com/xyflow/xyflow/pull/5333) [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing type exports
|
||||||
|
|
||||||
|
- Updated dependencies [[`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950), [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276), [`9c61000c`](https://github.com/xyflow/xyflow/commit/9c61000cac6277ce97274cc626fa7266f82dec27)]:
|
||||||
|
- @xyflow/system@0.0.63
|
||||||
|
|
||||||
|
## 1.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#5299](https://github.com/xyflow/xyflow/pull/5299) [`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaRole` prop to nodes and edges
|
||||||
|
|
||||||
|
- [#5280](https://github.com/xyflow/xyflow/pull/5280) [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac) Thanks [@moklick](https://github.com/moklick)! - Improve typing for Nodes
|
||||||
|
|
||||||
|
- [#5276](https://github.com/xyflow/xyflow/pull/5276) [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48) Thanks [@moklick](https://github.com/moklick)! - Add an `ease` and `interpolate` option to all function that alter the viewport
|
||||||
|
|
||||||
|
- [#5277](https://github.com/xyflow/xyflow/pull/5277) [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
|
||||||
|
|
||||||
|
- [#5317](https://github.com/xyflow/xyflow/pull/5317) [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850) Thanks [@moklick](https://github.com/moklick)! - Add `domAttributes` option for nodes and edges
|
||||||
|
|
||||||
|
- [#5326](https://github.com/xyflow/xyflow/pull/5326) [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent NodeResizer controls to become too small when zooming out
|
||||||
|
|
||||||
|
- [#5308](https://github.com/xyflow/xyflow/pull/5308) [`09fab679`](https://github.com/xyflow/xyflow/commit/09fab6794031410c9e9465281d038c3520afe783) Thanks [@printerscanner](https://github.com/printerscanner)! - Focus nodes on tab if not within the viewport and add a new prop `autoPanOnNodeFocus`
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`75ed6dec`](https://github.com/xyflow/xyflow/commit/75ed6decfb3ff408f6136bc1bc712fc4eb3737ef) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent selecting of edges when spacebar is pressed
|
||||||
|
|
||||||
|
- [#5294](https://github.com/xyflow/xyflow/pull/5294) [`4a582e23`](https://github.com/xyflow/xyflow/commit/4a582e2371066170b12f3df947f24fe233b542cd) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix data in `EdgeProps` that was not typed correctly
|
||||||
|
|
||||||
|
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`d0c36fdb`](https://github.com/xyflow/xyflow/commit/d0c36fdb708b784cbbd87711512b813eb68757ac) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix initial fitView for SSR
|
||||||
|
|
||||||
|
- [#5275](https://github.com/xyflow/xyflow/pull/5275) [`a67bfc09`](https://github.com/xyflow/xyflow/commit/a67bfc09d5b352b5ff3b896c9a9e7138f2c4b20c) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix setting nodesInitialized multiple times
|
||||||
|
|
||||||
|
- [#5271](https://github.com/xyflow/xyflow/pull/5271) [`5224a1a2`](https://github.com/xyflow/xyflow/commit/5224a1a252eee59ffc48fffd74872206e19207f3) Thanks [@leejuyuu](https://github.com/leejuyuu)! - Change a11y description inline styles to classes
|
||||||
|
|
||||||
|
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`d0c36fdb`](https://github.com/xyflow/xyflow/commit/d0c36fdb708b784cbbd87711512b813eb68757ac) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix `ViewportPortal` not working when used outside of `SvelteFlow` component
|
||||||
|
|
||||||
|
- [#5295](https://github.com/xyflow/xyflow/pull/5295) [`93aefe71`](https://github.com/xyflow/xyflow/commit/93aefe71fb199d0e3463aca38a4564252ab8cbf4) Thanks [@peterkogo](https://github.com/peterkogo)! - Export missing callback types
|
||||||
|
|
||||||
|
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`d0c36fdb`](https://github.com/xyflow/xyflow/commit/d0c36fdb708b784cbbd87711512b813eb68757ac) Thanks [@peterkogo](https://github.com/peterkogo)! - Display nodes correctly in SSR output
|
||||||
|
|
||||||
|
- Updated dependencies [[`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1), [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac), [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48), [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef), [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850), [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf)]:
|
||||||
|
- @xyflow/system@0.0.62
|
||||||
|
|
||||||
## 1.0.2
|
## 1.0.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/svelte",
|
"name": "@xyflow/svelte",
|
||||||
"version": "1.0.2",
|
"version": "1.2.1",
|
||||||
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte",
|
"svelte",
|
||||||
@@ -55,35 +55,35 @@
|
|||||||
"@xyflow/system": "workspace:*"
|
"@xyflow/system": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.24.0",
|
"@eslint/js": "^9.28.0",
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.1",
|
||||||
"@sveltejs/kit": "^2.20.7",
|
"@sveltejs/kit": "^2.21.4",
|
||||||
"@sveltejs/package": "^2.3.11",
|
"@sveltejs/package": "^2.3.11",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||||
"@typescript-eslint/parser": "^8.30.1",
|
"@typescript-eslint/parser": "^8.34.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"cssnano": "^7.0.6",
|
"cssnano": "^7.0.7",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"eslint": "^9.24.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-config-prettier": "^10.1.2",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-svelte": "^3.5.1",
|
"eslint-plugin-svelte": "^3.9.2",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.2.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.4",
|
||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
"postcss-combine-duplicated-selectors": "^10.0.3",
|
"postcss-combine-duplicated-selectors": "^10.0.3",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"postcss-nested": "^7.0.2",
|
"postcss-nested": "^7.0.2",
|
||||||
"postcss-rename": "^0.6.1",
|
"postcss-rename": "^0.8.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"svelte": "^5.27.0",
|
"svelte": "^5.33.19",
|
||||||
"svelte-check": "^4.1.6",
|
"svelte-check": "^4.2.1",
|
||||||
"svelte-eslint-parser": "^1.1.2",
|
"svelte-eslint-parser": "^1.2.0",
|
||||||
"svelte-preprocess": "^6.0.3",
|
"svelte-preprocess": "^6.0.3",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.30.1"
|
"typescript-eslint": "^8.34.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^5.25.0"
|
"svelte": "^5.25.0"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { portal } from './portal.svelte';
|
export { portal } from './portal.svelte';
|
||||||
|
export { hideOnSSR } from './utils.svelte';
|
||||||
|
|||||||
@@ -15,25 +15,32 @@ function tryToMount(node: Element, domNode: Element | null, target: Portal | und
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function portal(node: Element, target: Portal | undefined) {
|
export function portal(node: Element, target: Portal | undefined) {
|
||||||
// TODO: does this work if called outside of SvelteFlow
|
const { domNode } = $derived(useStore());
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
let previousTarget: Portal | undefined = target;
|
let destroyEffect: (() => void) | undefined;
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
tryToMount(node, store.domNode, target);
|
if (domNode) {
|
||||||
|
// if the domNode is already mounted, we can directly try to mount the node
|
||||||
|
tryToMount(node, domNode, target);
|
||||||
|
} else {
|
||||||
|
// if the domNode is not mounted yet, we need to wait for it to be ready
|
||||||
|
destroyEffect = $effect.root(() => {
|
||||||
|
$effect(() => {
|
||||||
|
tryToMount(node, domNode, target);
|
||||||
|
destroyEffect?.();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async update(target: Portal) {
|
async update(target: Portal | undefined) {
|
||||||
if (target !== previousTarget) {
|
tryToMount(node, domNode, target);
|
||||||
node.parentNode?.removeChild(node);
|
|
||||||
previousTarget = target;
|
|
||||||
}
|
|
||||||
tryToMount(node, store.domNode, target);
|
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
if (node.parentNode) {
|
if (node.parentNode) {
|
||||||
node.parentNode.removeChild(node);
|
node.parentNode.removeChild(node);
|
||||||
}
|
}
|
||||||
|
destroyEffect?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export function hideOnSSR(): { value: boolean } {
|
||||||
|
let hide = $state(typeof window === 'undefined');
|
||||||
|
if (hide) {
|
||||||
|
const destroyEffect = $effect.root(() => {
|
||||||
|
$effect(() => {
|
||||||
|
hide = false;
|
||||||
|
destroyEffect?.();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get value() {
|
||||||
|
return hide;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -44,6 +44,9 @@ export default function zoom(domNode: Element, params: ZoomParams) {
|
|||||||
minZoom,
|
minZoom,
|
||||||
maxZoom,
|
maxZoom,
|
||||||
initialViewport,
|
initialViewport,
|
||||||
|
onPanZoomStart,
|
||||||
|
onPanZoom,
|
||||||
|
onPanZoomEnd,
|
||||||
translateExtent,
|
translateExtent,
|
||||||
paneClickDistance,
|
paneClickDistance,
|
||||||
setPanZoomInstance,
|
setPanZoomInstance,
|
||||||
@@ -58,6 +61,9 @@ export default function zoom(domNode: Element, params: ZoomParams) {
|
|||||||
translateExtent,
|
translateExtent,
|
||||||
viewport: initialViewport,
|
viewport: initialViewport,
|
||||||
paneClickDistance,
|
paneClickDistance,
|
||||||
|
onPanZoom,
|
||||||
|
onPanZoomStart,
|
||||||
|
onPanZoomEnd,
|
||||||
onDraggingChange
|
onDraggingChange
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { portal } from '$lib/actions/portal';
|
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||||
|
|
||||||
import { useStore } from '$lib/store';
|
import { useStore } from '$lib/store';
|
||||||
import type { EdgeLabelProps } from './types';
|
import type { EdgeLabelProps } from './types';
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
use:portal={'edge-labels'}
|
use:portal={'edge-labels'}
|
||||||
|
style:display={hideOnSSR().value ? 'none' : undefined}
|
||||||
class={['svelte-flow__edge-label', { transparent }, className]}
|
class={['svelte-flow__edge-label', { transparent }, className]}
|
||||||
style:cursor={selectEdgeOnClick ? 'pointer' : undefined}
|
style:cursor={selectEdgeOnClick ? 'pointer' : undefined}
|
||||||
style:transform="translate(-50%, -50%) translate({x}px,{y}px)"
|
style:transform="translate(-50%, -50%) translate({x}px,{y}px)"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useStore } from '$lib/store';
|
import { useStore } from '$lib/store';
|
||||||
import type { Edge } from '$lib/types';
|
import type { Edge } from '$lib/types';
|
||||||
import { XYHandle, type HandleType } from '@xyflow/system';
|
import { XYHandle, type HandleType, type OnConnectStart } from '@xyflow/system';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { EdgeLabel } from '../EdgeLabel';
|
import { EdgeLabel } from '../EdgeLabel';
|
||||||
import type { EdgeReconnectAnchorProps } from './types';
|
import type { EdgeReconnectAnchorProps } from './types';
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
position,
|
position,
|
||||||
class: className,
|
class: className,
|
||||||
size = 25,
|
size = 25,
|
||||||
|
dragThreshold = 1,
|
||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
}: EdgeReconnectAnchorProps = $props();
|
}: EdgeReconnectAnchorProps = $props();
|
||||||
@@ -52,8 +53,11 @@
|
|||||||
let newEdge: Edge | undefined;
|
let newEdge: Edge | undefined;
|
||||||
let edge = edgeLookup.get(edgeId)!;
|
let edge = edgeLookup.get(edgeId)!;
|
||||||
|
|
||||||
reconnecting = true;
|
const _onConnectStart: OnConnectStart = (evt, params) => {
|
||||||
onreconnectstart?.(event, edge, type);
|
reconnecting = true;
|
||||||
|
onreconnectstart?.(event, edge, type);
|
||||||
|
onconnectstart?.(evt, params);
|
||||||
|
};
|
||||||
|
|
||||||
const opposite =
|
const opposite =
|
||||||
type === 'target'
|
type === 'target'
|
||||||
@@ -79,7 +83,7 @@
|
|||||||
cancelConnection,
|
cancelConnection,
|
||||||
panBy,
|
panBy,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
onConnectStart: onconnectstart,
|
onConnectStart: _onConnectStart,
|
||||||
onConnectEnd: onconnectend,
|
onConnectEnd: onconnectend,
|
||||||
onConnect: (connection) => {
|
onConnect: (connection) => {
|
||||||
newEdge = { ...edge, ...connection };
|
newEdge = { ...edge, ...connection };
|
||||||
@@ -97,7 +101,8 @@
|
|||||||
},
|
},
|
||||||
updateConnection,
|
updateConnection,
|
||||||
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
||||||
getFromHandle: () => store.connection.fromHandle
|
getFromHandle: () => store.connection.fromHandle,
|
||||||
|
dragThreshold: dragThreshold ?? store.connectionDragThreshold
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ export type EdgeReconnectAnchorProps = {
|
|||||||
position?: XYPosition;
|
position?: XYPosition;
|
||||||
size?: number;
|
size?: number;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
|
dragThreshold?: number;
|
||||||
} & HTMLAttributes<HTMLDivElement>;
|
} & HTMLAttributes<HTMLDivElement>;
|
||||||
|
|||||||
@@ -89,8 +89,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onkeydown = (event: KeyboardEvent) => {
|
function onkeydown(event: KeyboardEvent) {
|
||||||
// TODO: Possible Svelte Bug? onkeydown is always firing for the last edge
|
|
||||||
if (!store.disableKeyboardA11y && elementSelectionKeys.includes(event.key) && selectable) {
|
if (!store.disableKeyboardA11y && elementSelectionKeys.includes(event.key) && selectable) {
|
||||||
const { unselectNodesAndEdges, addSelectedEdges } = store;
|
const { unselectNodesAndEdges, addSelectedEdges } = store;
|
||||||
const unselect = event.key === 'Escape';
|
const unselect = event.key === 'Escape';
|
||||||
@@ -102,7 +101,7 @@
|
|||||||
addSelectedEdges([id]);
|
addSelectedEdges([id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
@@ -138,9 +137,10 @@
|
|||||||
: `Edge from ${source} to ${target}`}
|
: `Edge from ${source} to ${target}`}
|
||||||
aria-describedby={focusable ? `${ARIA_EDGE_DESC_KEY}-${store.flowId}` : undefined}
|
aria-describedby={focusable ? `${ARIA_EDGE_DESC_KEY}-${store.flowId}` : undefined}
|
||||||
role={edge.ariaRole ?? (focusable ? 'group' : 'img')}
|
role={edge.ariaRole ?? (focusable ? 'group' : 'img')}
|
||||||
aria-roledescription={edge.ariaRoleDescription || 'edge'}
|
aria-roledescription="edge"
|
||||||
onkeydown={focusable ? onkeydown : undefined}
|
onkeydown={focusable ? onkeydown : undefined}
|
||||||
tabindex={focusable ? 0 : undefined}
|
tabindex={focusable ? 0 : undefined}
|
||||||
|
{...edge.domAttributes}
|
||||||
>
|
>
|
||||||
<EdgeComponent
|
<EdgeComponent
|
||||||
{id}
|
{id}
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
let store = useStore();
|
let store = useStore();
|
||||||
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
||||||
|
|
||||||
|
|
||||||
let prevConnections: Map<string, HandleConnection> | null = null;
|
let prevConnections: Map<string, HandleConnection> | null = null;
|
||||||
$effect.pre(() => {
|
$effect.pre(() => {
|
||||||
if (onconnect || ondisconnect) {
|
if (onconnect || ondisconnect) {
|
||||||
@@ -140,7 +139,8 @@
|
|||||||
store.onconnectend?.(event, connectionState);
|
store.onconnectend?.(event, connectionState);
|
||||||
},
|
},
|
||||||
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
||||||
getFromHandle: () => store.connection.fromHandle
|
getFromHandle: () => store.connection.fromHandle,
|
||||||
|
dragThreshold: store.connectionDragThreshold
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
errorMessages,
|
errorMessages,
|
||||||
isInputDOMNode,
|
isInputDOMNode,
|
||||||
nodeHasDimensions,
|
nodeHasDimensions,
|
||||||
Position
|
Position,
|
||||||
|
getNodesInside
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import drag from '$lib/actions/drag';
|
import drag from '$lib/actions/drag';
|
||||||
@@ -65,7 +66,9 @@
|
|||||||
let draggable = $derived(_draggable ?? store.nodesDraggable);
|
let draggable = $derived(_draggable ?? store.nodesDraggable);
|
||||||
let selectable = $derived(_selectable ?? store.elementsSelectable);
|
let selectable = $derived(_selectable ?? store.elementsSelectable);
|
||||||
let connectable = $derived(_connectable ?? store.nodesConnectable);
|
let connectable = $derived(_connectable ?? store.nodesConnectable);
|
||||||
let initialized = $derived(nodeHasDimensions(node) && !!node.internals.handleBounds);
|
let hasDimensions = $derived(nodeHasDimensions(node));
|
||||||
|
let hasHandleBounds = $derived(!!node.internals.handleBounds);
|
||||||
|
let isInitialized = $derived(hasDimensions && hasHandleBounds);
|
||||||
let focusable = $derived(_focusable ?? store.nodesFocusable);
|
let focusable = $derived(_focusable ?? store.nodesFocusable);
|
||||||
|
|
||||||
function isInParentLookup(id: string) {
|
function isInParentLookup(id: string) {
|
||||||
@@ -149,7 +152,7 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||||
if (resizeObserver && (!initialized || nodeRef !== prevNodeRef)) {
|
if (resizeObserver && (!isInitialized || nodeRef !== prevNodeRef)) {
|
||||||
prevNodeRef && resizeObserver.unobserve(prevNodeRef);
|
prevNodeRef && resizeObserver.unobserve(prevNodeRef);
|
||||||
nodeRef && resizeObserver.observe(nodeRef);
|
nodeRef && resizeObserver.observe(nodeRef);
|
||||||
prevNodeRef = nodeRef;
|
prevNodeRef = nodeRef;
|
||||||
@@ -197,6 +200,34 @@
|
|||||||
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
|
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
if (
|
||||||
|
store.disableKeyboardA11y ||
|
||||||
|
!store.autoPanOnNodeFocus ||
|
||||||
|
!nodeRef?.matches(':focus-visible')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width, height, viewport } = store;
|
||||||
|
|
||||||
|
const withinViewport =
|
||||||
|
getNodesInside(
|
||||||
|
new Map([[id, node]]),
|
||||||
|
{ x: 0, y: 0, width, height },
|
||||||
|
[viewport.x, viewport.y, viewport.zoom],
|
||||||
|
true
|
||||||
|
).length > 0;
|
||||||
|
|
||||||
|
if (!withinViewport) {
|
||||||
|
store.setCenter(
|
||||||
|
node.position.x + (node.measured.width ?? 0) / 2,
|
||||||
|
node.position.y + (node.measured.height ?? 0) / 2,
|
||||||
|
{ zoom: viewport.zoom }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !hidden}
|
{#if !hidden}
|
||||||
@@ -236,7 +267,7 @@
|
|||||||
class:parent={isParent}
|
class:parent={isParent}
|
||||||
style:z-index={zIndex}
|
style:z-index={zIndex}
|
||||||
style:transform="translate({positionX}px, {positionY}px)"
|
style:transform="translate({positionX}px, {positionY}px)"
|
||||||
style:visibility={initialized ? 'visible' : 'hidden'}
|
style:visibility={hasDimensions ? 'visible' : 'hidden'}
|
||||||
style={nodeStyle}
|
style={nodeStyle}
|
||||||
onclick={onSelectNodeHandler}
|
onclick={onSelectNodeHandler}
|
||||||
onpointerenter={onnodepointerenter
|
onpointerenter={onnodepointerenter
|
||||||
@@ -252,12 +283,14 @@
|
|||||||
? (event) => onnodecontextmenu({ node: userNode, event })
|
? (event) => onnodecontextmenu({ node: userNode, event })
|
||||||
: undefined}
|
: undefined}
|
||||||
onkeydown={focusable ? onKeyDown : undefined}
|
onkeydown={focusable ? onKeyDown : undefined}
|
||||||
|
onfocus={focusable ? onFocus : undefined}
|
||||||
tabIndex={focusable ? 0 : undefined}
|
tabIndex={focusable ? 0 : undefined}
|
||||||
role={node.ariaRole ?? (focusable ? 'group' : undefined)}
|
role={node.ariaRole ?? (focusable ? 'group' : undefined)}
|
||||||
aria-roledescription={node.ariaRoleDescription || 'node'}
|
aria-roledescription="node"
|
||||||
aria-describedby={store.disableKeyboardA11y
|
aria-describedby={store.disableKeyboardA11y
|
||||||
? undefined
|
? undefined
|
||||||
: `${ARIA_NODE_DESC_KEY}-${store.flowId}`}
|
: `${ARIA_NODE_DESC_KEY}-${store.flowId}`}
|
||||||
|
{...node.domAttributes}
|
||||||
>
|
>
|
||||||
<NodeComponent
|
<NodeComponent
|
||||||
{data}
|
{data}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { portal } from '$lib/actions/portal';
|
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||||
import type { ViewportPortalProps } from './types';
|
import type { ViewportPortalProps } from './types';
|
||||||
|
|
||||||
let { target = 'front', children, ...rest }: ViewportPortalProps = $props();
|
let { target = 'front', children, ...rest }: ViewportPortalProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:portal={`viewport-${target}`} {...rest}>
|
<div
|
||||||
|
use:portal={`viewport-${target}`}
|
||||||
|
style:display={hideOnSSR().value ? 'none' : undefined}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PanelProps } from './types';
|
import type { PanelProps } from './types';
|
||||||
import { useStore } from '$lib/store';
|
|
||||||
|
|
||||||
let { position = 'top-right', style, class: className, children, ...rest }: PanelProps = $props();
|
let { position = 'top-right', style, class: className, children, ...rest }: PanelProps = $props();
|
||||||
|
|
||||||
let store = $derived(useStore());
|
|
||||||
|
|
||||||
let positionClasses = $derived(`${position}`.split('-'));
|
let positionClasses = $derived(`${position}`.split('-'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class={['svelte-flow__panel', className, ...positionClasses]} {style} {...rest}>
|
||||||
class={['svelte-flow__panel', className, ...positionClasses]}
|
|
||||||
{style}
|
|
||||||
style:pointer-events={store.selectionRectMode ? 'none' : ''}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
fitViewOptions,
|
fitViewOptions,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
nodeDragThreshold,
|
nodeDragThreshold,
|
||||||
|
connectionDragThreshold,
|
||||||
minZoom,
|
minZoom,
|
||||||
maxZoom,
|
maxZoom,
|
||||||
initialViewport,
|
initialViewport,
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
elevateNodesOnSelect,
|
elevateNodesOnSelect,
|
||||||
elevateEdgesOnSelect,
|
elevateEdgesOnSelect,
|
||||||
nodesDraggable,
|
nodesDraggable,
|
||||||
|
autoPanOnNodeFocus,
|
||||||
nodesConnectable,
|
nodesConnectable,
|
||||||
elementsSelectable,
|
elementsSelectable,
|
||||||
nodesFocusable,
|
nodesFocusable,
|
||||||
@@ -100,6 +102,16 @@
|
|||||||
type OnlyDivAttributes<T> = {
|
type OnlyDivAttributes<T> = {
|
||||||
[K in keyof T]: K extends keyof HTMLAttributes<HTMLDivElement> ? T[K] : never;
|
[K in keyof T]: K extends keyof HTMLAttributes<HTMLDivElement> ? T[K] : never;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Undo scroll events, preventing viewport from shifting when nodes outside of it are focused
|
||||||
|
function wrapperOnScroll(e: UIEvent & { currentTarget: EventTarget & HTMLDivElement }) {
|
||||||
|
e.currentTarget.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
||||||
|
|
||||||
|
// Forward the event to any existing onscroll handler if needed
|
||||||
|
if (rest.onscroll) {
|
||||||
|
rest.onscroll(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -111,6 +123,7 @@
|
|||||||
class={['svelte-flow', 'svelte-flow__container', className, colorMode]}
|
class={['svelte-flow', 'svelte-flow__container', className, colorMode]}
|
||||||
data-testid="svelte-flow__wrapper"
|
data-testid="svelte-flow__wrapper"
|
||||||
role="application"
|
role="application"
|
||||||
|
onscroll={wrapperOnScroll}
|
||||||
{...divAttributes satisfies OnlyDivAttributes<typeof divAttributes>}
|
{...divAttributes satisfies OnlyDivAttributes<typeof divAttributes>}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -170,6 +170,12 @@ export type SvelteFlowProps<
|
|||||||
* @default 0
|
* @default 0
|
||||||
*/
|
*/
|
||||||
nodeClickDistance?: number;
|
nodeClickDistance?: number;
|
||||||
|
/**
|
||||||
|
* The threshold in pixels that the mouse must move before a connection line starts to drag.
|
||||||
|
* This is useful to prevent accidental connections when clicking on a handle.
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
connectionDragThreshold?: number;
|
||||||
/** Minimum zoom level
|
/** Minimum zoom level
|
||||||
* @default 0.5
|
* @default 0.5
|
||||||
*/
|
*/
|
||||||
@@ -232,6 +238,11 @@ export type SvelteFlowProps<
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
nodesDraggable?: boolean;
|
nodesDraggable?: boolean;
|
||||||
|
/**
|
||||||
|
* When `true`, the viewport will pan when a node is focused.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoPanOnNodeFocus?: boolean;
|
||||||
/**
|
/**
|
||||||
* Controls if all nodes should be connectable to each other
|
* Controls if all nodes should be connectable to each other
|
||||||
* @default true
|
* @default true
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ type UseNodeConnectionsParams = {
|
|||||||
onDisconnect?: (connections: Connection[]) => void;
|
onDisconnect?: (connections: Connection[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ConnectionMap = Map<string, NodeConnection>;
|
||||||
|
|
||||||
const initialConnections: NodeConnection[] = [];
|
const initialConnections: NodeConnection[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,26 +44,46 @@ export function useNodeConnections({
|
|||||||
const contextNodeId = getContext<string>('svelteflow__node_id');
|
const contextNodeId = getContext<string>('svelteflow__node_id');
|
||||||
const nodeId = id ?? contextNodeId;
|
const nodeId = id ?? contextNodeId;
|
||||||
|
|
||||||
let prevConnections: Map<string, NodeConnection> = new Map();
|
let connectionMaps: { previous: ConnectionMap; next: ConnectionMap } = {
|
||||||
|
previous: new Map(),
|
||||||
|
next: new Map()
|
||||||
|
};
|
||||||
let connectionsArray: NodeConnection[] = initialConnections;
|
let connectionsArray: NodeConnection[] = initialConnections;
|
||||||
|
|
||||||
const connections = $derived.by(() => {
|
const connections = $derived.by(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
edges;
|
edges;
|
||||||
|
|
||||||
|
const prevConnections = connectionMaps.next;
|
||||||
const nextConnections =
|
const nextConnections =
|
||||||
connectionLookup.get(
|
connectionLookup.get(
|
||||||
`${nodeId}${handleType ? (handleId ? `-${handleType}-${handleId}` : `-${handleType}`) : ''}`
|
`${nodeId}${handleType ? (handleId ? `-${handleType}-${handleId}` : `-${handleType}`) : ''}`
|
||||||
) ?? new Map();
|
) ?? new Map();
|
||||||
if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
|
if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
|
||||||
if (onConnect) handleConnectionChange(nextConnections, prevConnections, onConnect);
|
connectionMaps = {
|
||||||
if (onDisconnect) handleConnectionChange(prevConnections, nextConnections, onDisconnect);
|
previous: prevConnections,
|
||||||
|
next: nextConnections
|
||||||
prevConnections = nextConnections;
|
};
|
||||||
connectionsArray = Array.from(nextConnections.values() || initialConnections);
|
connectionsArray = Array.from(nextConnections.values() || initialConnections);
|
||||||
}
|
}
|
||||||
return connectionsArray;
|
return connectionsArray;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// We subscribe to changes to the connections only when onConnect/onDisconnect are provided
|
||||||
|
if (onConnect) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
|
connections;
|
||||||
|
handleConnectionChange(connectionMaps.next, connectionMaps.previous, onConnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDisconnect) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
|
connections;
|
||||||
|
handleConnectionChange(connectionMaps.previous, connectionMaps.next, onDisconnect);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get current() {
|
get current() {
|
||||||
return connections;
|
return connections;
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
|
|||||||
* Returns all nodes that intersect with the given node or rect.
|
* Returns all nodes that intersect with the given node or rect.
|
||||||
*
|
*
|
||||||
* @param node - the node or rect to check for intersections
|
* @param node - the node or rect to check for intersections
|
||||||
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
|
* @param partially - true by default, if set to false, only nodes that are fully intersecting will be returned
|
||||||
* @param nodes - optional nodes array to check for intersections
|
* @param nodes - optional nodes array to check for intersections
|
||||||
*
|
*
|
||||||
* @returns an array of intersecting nodes
|
* @returns an array of intersecting nodes
|
||||||
@@ -366,28 +366,8 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
},
|
},
|
||||||
getViewport: () => $state.snapshot(store.viewport),
|
getViewport: () => $state.snapshot(store.viewport),
|
||||||
setCenter: async (x, y, options) => {
|
setCenter: async (x, y, options) => store.setCenter(x, y, options),
|
||||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
|
fitView: (options?: FitViewOptions) => store.fitView(options),
|
||||||
const currentPanZoom = store.panZoom;
|
|
||||||
|
|
||||||
if (!currentPanZoom) {
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
await currentPanZoom.setViewport(
|
|
||||||
{
|
|
||||||
x: store.width / 2 - x * nextZoom,
|
|
||||||
y: store.height / 2 - y * nextZoom,
|
|
||||||
zoom: nextZoom
|
|
||||||
},
|
|
||||||
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
|
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.resolve(true);
|
|
||||||
},
|
|
||||||
fitView: (options?: FitViewOptions) => {
|
|
||||||
return store.fitView(options);
|
|
||||||
},
|
|
||||||
fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => {
|
fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => {
|
||||||
if (!store.panZoom) {
|
if (!store.panZoom) {
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
@@ -432,7 +412,11 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
|
|||||||
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
|
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
|
||||||
const partiallyVisible = partially && overlappingArea > 0;
|
const partiallyVisible = partially && overlappingArea > 0;
|
||||||
|
|
||||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
return (
|
||||||
|
partiallyVisible ||
|
||||||
|
overlappingArea >= currNodeRect.width * currNodeRect.height ||
|
||||||
|
overlappingArea >= nodeRect.width * nodeRect.height
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isNodeIntersecting: (
|
isNodeIntersecting: (
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ export {
|
|||||||
type OnResizeStart,
|
type OnResizeStart,
|
||||||
type OnResize,
|
type OnResize,
|
||||||
type OnResizeEnd,
|
type OnResizeEnd,
|
||||||
|
type OnReconnect,
|
||||||
|
type OnReconnectStart,
|
||||||
|
type OnReconnectEnd,
|
||||||
type ControlPosition,
|
type ControlPosition,
|
||||||
type ControlLinePosition,
|
type ControlLinePosition,
|
||||||
ResizeControlVariant,
|
ResizeControlVariant,
|
||||||
@@ -113,7 +116,10 @@ export {
|
|||||||
type ResizeDragEvent,
|
type ResizeDragEvent,
|
||||||
type IsValidConnection,
|
type IsValidConnection,
|
||||||
type NodeConnection,
|
type NodeConnection,
|
||||||
type AriaLabelConfig
|
type AriaLabelConfig,
|
||||||
|
type SetCenter,
|
||||||
|
type SetViewport,
|
||||||
|
type FitBounds
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
// system utils
|
// system utils
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
buttonColor,
|
buttonColor,
|
||||||
buttonColorHover,
|
buttonColorHover,
|
||||||
buttonBorderColor,
|
buttonBorderColor,
|
||||||
'aria-label': ariaLabel,
|
|
||||||
fitViewOptions,
|
fitViewOptions,
|
||||||
children,
|
children,
|
||||||
before,
|
before,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
handleStyle,
|
handleStyle,
|
||||||
lineClass,
|
lineClass,
|
||||||
lineStyle,
|
lineStyle,
|
||||||
|
autoScale = true,
|
||||||
...rest
|
...rest
|
||||||
}: NodeResizerProps = $props();
|
}: NodeResizerProps = $props();
|
||||||
</script>
|
</script>
|
||||||
@@ -25,11 +26,19 @@
|
|||||||
style={lineStyle}
|
style={lineStyle}
|
||||||
{nodeId}
|
{nodeId}
|
||||||
{position}
|
{position}
|
||||||
|
{autoScale}
|
||||||
variant={ResizeControlVariant.Line}
|
variant={ResizeControlVariant.Line}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{#each XY_RESIZER_HANDLE_POSITIONS as position (position)}
|
{#each XY_RESIZER_HANDLE_POSITIONS as position (position)}
|
||||||
<ResizeControl class={handleClass} style={handleStyle} {nodeId} {position} {...rest} />
|
<ResizeControl
|
||||||
|
class={handleClass}
|
||||||
|
style={handleStyle}
|
||||||
|
{nodeId}
|
||||||
|
{position}
|
||||||
|
{autoScale}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
maxWidth = Number.MAX_VALUE,
|
maxWidth = Number.MAX_VALUE,
|
||||||
maxHeight = Number.MAX_VALUE,
|
maxHeight = Number.MAX_VALUE,
|
||||||
keepAspectRatio = false,
|
keepAspectRatio = false,
|
||||||
|
autoScale = true,
|
||||||
shouldResize,
|
shouldResize,
|
||||||
onResizeStart,
|
onResizeStart,
|
||||||
onResize,
|
onResize,
|
||||||
@@ -40,10 +41,10 @@
|
|||||||
let resizeControlRef: HTMLDivElement;
|
let resizeControlRef: HTMLDivElement;
|
||||||
let resizer: XYResizerInstance | null = $state(null);
|
let resizer: XYResizerInstance | null = $state(null);
|
||||||
|
|
||||||
|
let isLineVariant = $derived(variant === ResizeControlVariant.Line);
|
||||||
|
|
||||||
let controlPosition = $derived.by(() => {
|
let controlPosition = $derived.by(() => {
|
||||||
let defaultPosition = (
|
let defaultPosition = (isLineVariant ? 'right' : 'bottom-right') as ControlPosition;
|
||||||
variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'
|
|
||||||
) as ControlPosition;
|
|
||||||
return position ?? defaultPosition;
|
return position ?? defaultPosition;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,8 +120,9 @@
|
|||||||
<div
|
<div
|
||||||
class={['svelte-flow__resize-control', store.noDragClass, ...positionClasses, variant, className]}
|
class={['svelte-flow__resize-control', store.noDragClass, ...positionClasses, variant, className]}
|
||||||
bind:this={resizeControlRef}
|
bind:this={resizeControlRef}
|
||||||
style:border-color={variant === ResizeControlVariant.Line ? color : undefined}
|
style:border-color={isLineVariant ? color : undefined}
|
||||||
style:background-color={variant === ResizeControlVariant.Line ? undefined : color}
|
style:background-color={isLineVariant ? undefined : color}
|
||||||
|
style:scale={isLineVariant || !autoScale ? undefined : Math.max(1 / store.viewport.zoom, 1)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export type NodeResizerProps = {
|
|||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
/** Keep aspect ratio when resizing */
|
/** Keep aspect ratio when resizing */
|
||||||
keepAspectRatio?: boolean;
|
keepAspectRatio?: boolean;
|
||||||
|
/** Automatically scale the node when resizing */
|
||||||
|
autoScale?: boolean;
|
||||||
/** Callback to determine if node should resize */
|
/** Callback to determine if node should resize */
|
||||||
shouldResize?: ShouldResize;
|
shouldResize?: ShouldResize;
|
||||||
/** Callback called when resizing starts */
|
/** Callback called when resizing starts */
|
||||||
@@ -55,6 +57,7 @@ export type ResizeControlProps = Pick<
|
|||||||
| 'maxWidth'
|
| 'maxWidth'
|
||||||
| 'maxHeight'
|
| 'maxHeight'
|
||||||
| 'keepAspectRatio'
|
| 'keepAspectRatio'
|
||||||
|
| 'autoScale'
|
||||||
| 'shouldResize'
|
| 'shouldResize'
|
||||||
| 'onResizeStart'
|
| 'onResizeStart'
|
||||||
| 'onResize'
|
| 'onResize'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { Position, getNodeToolbarTransform } from '@xyflow/system';
|
import { Position, getNodeToolbarTransform } from '@xyflow/system';
|
||||||
|
|
||||||
import { portal } from '$lib/actions/portal';
|
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||||
import { useStore } from '$lib/store';
|
import { useStore } from '$lib/store';
|
||||||
import { useSvelteFlow } from '$lib/hooks/useSvelteFlow.svelte';
|
import { useSvelteFlow } from '$lib/hooks/useSvelteFlow.svelte';
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
{#if store.domNode && isActive && toolbarNodes}
|
{#if store.domNode && isActive && toolbarNodes}
|
||||||
<div
|
<div
|
||||||
use:portal={'root'}
|
use:portal={'root'}
|
||||||
|
style:display={hideOnSSR().value ? 'none' : undefined}
|
||||||
class="svelte-flow__node-toolbar"
|
class="svelte-flow__node-toolbar"
|
||||||
data-id={toolbarNodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
|
data-id={toolbarNodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
|
||||||
style:position="absolute"
|
style:position="absolute"
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import {
|
|||||||
type ConnectionState,
|
type ConnectionState,
|
||||||
updateAbsolutePositions,
|
updateAbsolutePositions,
|
||||||
snapPosition,
|
snapPosition,
|
||||||
calculateNodePosition
|
calculateNodePosition,
|
||||||
|
type SetCenterOptions
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
|
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
|
||||||
@@ -126,6 +127,26 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
return fitViewResolver.promise;
|
return fitViewResolver.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setCenter(x: number, y: number, options?: SetCenterOptions) {
|
||||||
|
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
|
||||||
|
const currentPanZoom = store.panZoom;
|
||||||
|
|
||||||
|
if (!currentPanZoom) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await currentPanZoom.setViewport(
|
||||||
|
{
|
||||||
|
x: store.width / 2 - x * nextZoom,
|
||||||
|
y: store.height / 2 - y * nextZoom,
|
||||||
|
zoom: nextZoom
|
||||||
|
},
|
||||||
|
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
|
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
|
||||||
const panZoom = store.panZoom;
|
const panZoom = store.panZoom;
|
||||||
if (!panZoom) {
|
if (!panZoom) {
|
||||||
@@ -372,6 +393,7 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut,
|
zoomOut,
|
||||||
fitView,
|
fitView,
|
||||||
|
setCenter,
|
||||||
setMinZoom,
|
setMinZoom,
|
||||||
setMaxZoom,
|
setMaxZoom,
|
||||||
setTranslateExtent,
|
setTranslateExtent,
|
||||||
|
|||||||
@@ -84,6 +84,25 @@ export const initialEdgeTypes = {
|
|||||||
step: StepEdgeInternal
|
step: StepEdgeInternal
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getInitialViewport(
|
||||||
|
// This is just used to make sure adoptUserNodes is called before we calculate the viewport
|
||||||
|
_nodesInitialized: boolean,
|
||||||
|
fitView: boolean | undefined,
|
||||||
|
initialViewport: Viewport | undefined,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
nodeLookup: NodeLookup
|
||||||
|
) {
|
||||||
|
if (fitView && !initialViewport && width && height) {
|
||||||
|
const bounds = getInternalNodesBounds(nodeLookup, {
|
||||||
|
filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight))
|
||||||
|
});
|
||||||
|
return getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
|
||||||
|
} else {
|
||||||
|
return initialViewport ?? { x: 0, y: 0, zoom: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getInitialStore<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
export function getInitialStore<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||||
signals: StoreSignals<NodeType, EdgeType>
|
signals: StoreSignals<NodeType, EdgeType>
|
||||||
) {
|
) {
|
||||||
@@ -91,10 +110,10 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
|||||||
// Inline classes have some performance implications but we just call it once (max twice).
|
// Inline classes have some performance implications but we just call it once (max twice).
|
||||||
class SvelteFlowStore {
|
class SvelteFlowStore {
|
||||||
flowId: string = $derived(signals.props.id ?? '1');
|
flowId: string = $derived(signals.props.id ?? '1');
|
||||||
domNode = $state<HTMLDivElement | null>(null);
|
domNode = $state.raw<HTMLDivElement | null>(null);
|
||||||
panZoom: PanZoomInstance | null = $state(null);
|
panZoom: PanZoomInstance | null = $state.raw(null);
|
||||||
width = $state<number>(signals.width ?? 0);
|
width = $state.raw<number>(signals.width ?? 0);
|
||||||
height = $state<number>(signals.height ?? 0);
|
height = $state.raw<number>(signals.height ?? 0);
|
||||||
|
|
||||||
nodesInitialized: boolean = $derived.by(() => {
|
nodesInitialized: boolean = $derived.by(() => {
|
||||||
const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, {
|
const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, {
|
||||||
@@ -266,6 +285,8 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
|||||||
nodeDragThreshold: number = $derived(signals.props.nodeDragThreshold ?? 1);
|
nodeDragThreshold: number = $derived(signals.props.nodeDragThreshold ?? 1);
|
||||||
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
|
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
|
||||||
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
|
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
|
||||||
|
autoPanOnNodeFocus: boolean = $derived(signals.props.autoPanOnNodeFocus ?? true);
|
||||||
|
connectionDragThreshold: number = $derived(signals.props.connectionDragThreshold ?? 1);
|
||||||
|
|
||||||
fitViewQueued: boolean = signals.props.fitView ?? false;
|
fitViewQueued: boolean = signals.props.fitView ?? false;
|
||||||
fitViewOptions: FitViewOptions | undefined = signals.props.fitViewOptions;
|
fitViewOptions: FitViewOptions | undefined = signals.props.fitViewOptions;
|
||||||
@@ -273,16 +294,16 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
|||||||
|
|
||||||
snapGrid: SnapGrid | null = $derived(signals.props.snapGrid ?? null);
|
snapGrid: SnapGrid | null = $derived(signals.props.snapGrid ?? null);
|
||||||
|
|
||||||
dragging: boolean = $state(false);
|
dragging: boolean = $state.raw(false);
|
||||||
selectionRect: SelectionRect | null = $state(null);
|
selectionRect: SelectionRect | null = $state.raw(null);
|
||||||
|
|
||||||
selectionKeyPressed: boolean = $state(false);
|
selectionKeyPressed: boolean = $state.raw(false);
|
||||||
multiselectionKeyPressed: boolean = $state(false);
|
multiselectionKeyPressed: boolean = $state.raw(false);
|
||||||
deleteKeyPressed: boolean = $state(false);
|
deleteKeyPressed: boolean = $state.raw(false);
|
||||||
panActivationKeyPressed: boolean = $state(false);
|
panActivationKeyPressed: boolean = $state.raw(false);
|
||||||
zoomActivationKeyPressed: boolean = $state(false);
|
zoomActivationKeyPressed: boolean = $state.raw(false);
|
||||||
selectionRectMode: string | null = $state(null);
|
selectionRectMode: string | null = $state.raw(null);
|
||||||
ariaLiveMessage = $state<string>('');
|
ariaLiveMessage = $state.raw<string>('');
|
||||||
selectionMode: SelectionMode = $derived(signals.props.selectionMode ?? SelectionMode.Partial);
|
selectionMode: SelectionMode = $derived(signals.props.selectionMode ?? SelectionMode.Partial);
|
||||||
|
|
||||||
nodeTypes: NodeTypes = $derived({ ...initialNodeTypes, ...signals.props.nodeTypes });
|
nodeTypes: NodeTypes = $derived({ ...initialNodeTypes, ...signals.props.nodeTypes });
|
||||||
@@ -297,7 +318,16 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
|||||||
|
|
||||||
// _viewport is the internal viewport.
|
// _viewport is the internal viewport.
|
||||||
// when binding to viewport, we operate on signals.viewport instead
|
// when binding to viewport, we operate on signals.viewport instead
|
||||||
_viewport: Viewport = $state(signals.props.initialViewport ?? { x: 0, y: 0, zoom: 1 });
|
_viewport: Viewport = $state.raw(
|
||||||
|
getInitialViewport(
|
||||||
|
this.nodesInitialized,
|
||||||
|
signals.props.fitView,
|
||||||
|
signals.props.initialViewport,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.nodeLookup
|
||||||
|
)
|
||||||
|
);
|
||||||
get viewport() {
|
get viewport() {
|
||||||
return signals.viewport ?? this._viewport;
|
return signals.viewport ?? this._viewport;
|
||||||
}
|
}
|
||||||
@@ -309,21 +339,21 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
|||||||
}
|
}
|
||||||
|
|
||||||
// _connection is viewport independent and originating from XYHandle
|
// _connection is viewport independent and originating from XYHandle
|
||||||
_connection: ConnectionState = $state(initialConnection);
|
_connection: ConnectionState = $state.raw(initialConnection);
|
||||||
// We derive a viewport dependent connection here
|
// We derive a viewport dependent connection here
|
||||||
connection: ConnectionState = $derived.by(() => {
|
connection: ConnectionState = $derived.by(() => {
|
||||||
if (this._connection.inProgress) {
|
if (!this._connection.inProgress) {
|
||||||
return {
|
|
||||||
...this._connection,
|
|
||||||
to: pointToRendererPoint(this._connection.to, [
|
|
||||||
this.viewport.x,
|
|
||||||
this.viewport.y,
|
|
||||||
this.viewport.zoom
|
|
||||||
])
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return this._connection;
|
return this._connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...this._connection,
|
||||||
|
to: pointToRendererPoint(this._connection.to, [
|
||||||
|
this.viewport.x,
|
||||||
|
this.viewport.y,
|
||||||
|
this.viewport.zoom
|
||||||
|
])
|
||||||
|
};
|
||||||
});
|
});
|
||||||
connectionMode: ConnectionMode = $derived(
|
connectionMode: ConnectionMode = $derived(
|
||||||
signals.props.connectionMode ?? ConnectionMode.Strict
|
signals.props.connectionMode ?? ConnectionMode.Strict
|
||||||
@@ -363,7 +393,7 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
|||||||
clickConnect?: boolean = $derived(signals.props.clickConnect ?? true);
|
clickConnect?: boolean = $derived(signals.props.clickConnect ?? true);
|
||||||
onclickconnectstart?: OnConnectStart = $derived(signals.props.onclickconnectstart);
|
onclickconnectstart?: OnConnectStart = $derived(signals.props.onclickconnectstart);
|
||||||
onclickconnectend?: OnConnectEnd = $derived(signals.props.onclickconnectend);
|
onclickconnectend?: OnConnectEnd = $derived(signals.props.onclickconnectend);
|
||||||
clickConnectStartHandle: Pick<Handle, 'id' | 'nodeId' | 'type'> | null = $state(null);
|
clickConnectStartHandle: Pick<Handle, 'id' | 'nodeId' | 'type'> | null = $state.raw(null);
|
||||||
|
|
||||||
onselectiondrag?: OnSelectionDrag<NodeType> = $derived(signals.props.onselectiondrag);
|
onselectiondrag?: OnSelectionDrag<NodeType> = $derived(signals.props.onselectiondrag);
|
||||||
onselectiondragstart?: OnSelectionDrag<NodeType> = $derived(signals.props.onselectiondragstart);
|
onselectiondragstart?: OnSelectionDrag<NodeType> = $derived(signals.props.onselectiondragstart);
|
||||||
@@ -409,15 +439,6 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
|||||||
);
|
);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Process intial fitView here
|
|
||||||
if (signals.props.fitView && !signals.props.initialViewport && this.width && this.height) {
|
|
||||||
const bounds = getInternalNodesBounds(this.nodeLookup, {
|
|
||||||
filter: (node) =>
|
|
||||||
!!((node.width || node.initialWidth) && (node.height || node.initialHeight))
|
|
||||||
});
|
|
||||||
this.viewport = getViewportForBounds(bounds, this.width, this.height, 0.5, 2, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
warnIfDeeplyReactive(signals.nodes, 'nodes');
|
warnIfDeeplyReactive(signals.nodes, 'nodes');
|
||||||
warnIfDeeplyReactive(signals.edges, 'edges');
|
warnIfDeeplyReactive(signals.edges, 'edges');
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import type {
|
|||||||
UpdateNodePositions,
|
UpdateNodePositions,
|
||||||
CoordinateExtent,
|
CoordinateExtent,
|
||||||
UpdateConnection,
|
UpdateConnection,
|
||||||
Viewport
|
Viewport,
|
||||||
|
SetCenter
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type { getInitialStore } from './initial-store.svelte';
|
import type { getInitialStore } from './initial-store.svelte';
|
||||||
@@ -24,6 +25,7 @@ export type SvelteFlowStoreActions<NodeType extends Node = Node, EdgeType extend
|
|||||||
setTranslateExtent: (extent: CoordinateExtent) => void;
|
setTranslateExtent: (extent: CoordinateExtent) => void;
|
||||||
setPaneClickDistance: (distance: number) => void;
|
setPaneClickDistance: (distance: number) => void;
|
||||||
fitView: (options?: FitViewOptions) => Promise<boolean>;
|
fitView: (options?: FitViewOptions) => Promise<boolean>;
|
||||||
|
setCenter: SetCenter;
|
||||||
updateNodePositions: UpdateNodePositions;
|
updateNodePositions: UpdateNodePositions;
|
||||||
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void;
|
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void;
|
||||||
unselectNodesAndEdges: (params?: { nodes?: NodeType[]; edges?: EdgeType[] }) => void;
|
unselectNodesAndEdges: (params?: { nodes?: NodeType[]; edges?: EdgeType[] }) => void;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Component } from 'svelte';
|
import type { Component } from 'svelte';
|
||||||
|
import type { ClassValue, HTMLAttributes, SVGAttributes } from 'svelte/elements';
|
||||||
import type {
|
import type {
|
||||||
EdgeBase,
|
EdgeBase,
|
||||||
BezierPathOptions,
|
BezierPathOptions,
|
||||||
@@ -9,7 +10,6 @@ import type {
|
|||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type { Node } from '$lib/types';
|
import type { Node } from '$lib/types';
|
||||||
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An `Edge` is the complete description with everything Svelte Flow needs to know in order to
|
* An `Edge` is the complete description with everything Svelte Flow needs to know in order to
|
||||||
@@ -30,6 +30,13 @@ export type Edge<
|
|||||||
* @default "group"
|
* @default "group"
|
||||||
*/
|
*/
|
||||||
ariaRole?: HTMLAttributes<HTMLElement>['role'];
|
ariaRole?: HTMLAttributes<HTMLElement>['role'];
|
||||||
|
/**
|
||||||
|
* General escape hatch for adding custom attributes to the edge's DOM element.
|
||||||
|
*/
|
||||||
|
domAttributes?: Omit<
|
||||||
|
SVGAttributes<SVGGElement>,
|
||||||
|
'id' | 'style' | 'class' | 'role' | 'aria-label'
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BaseEdgeProps = Pick<
|
export type BaseEdgeProps = Pick<
|
||||||
@@ -43,11 +50,15 @@ export type BaseEdgeProps = Pick<
|
|||||||
labelX?: number;
|
labelX?: number;
|
||||||
/** The y coordinate of the label */
|
/** The y coordinate of the label */
|
||||||
labelY?: number;
|
labelY?: number;
|
||||||
/** Marker at start of edge
|
/**
|
||||||
|
* The id of the SVG marker to use at the start of the edge. This should be defined in a
|
||||||
|
* `<defs>` element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
|
||||||
* @example 'url(#arrow)'
|
* @example 'url(#arrow)'
|
||||||
*/
|
*/
|
||||||
markerStart?: string;
|
markerStart?: string;
|
||||||
/** Marker at end of edge
|
/**
|
||||||
|
* The id of the SVG marker to use at the end of the edge. This should be defined in a `<defs>`
|
||||||
|
* element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
|
||||||
* @example 'url(#arrow)'
|
* @example 'url(#arrow)'
|
||||||
*/
|
*/
|
||||||
markerEnd?: string;
|
markerEnd?: string;
|
||||||
|
|||||||
@@ -21,8 +21,16 @@ export type ConnectionData = {
|
|||||||
connectionStatus: string | null;
|
connectionStatus: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inline
|
||||||
|
*/
|
||||||
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
|
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This type can be used to type the `onDelete` function with a custom node and edge type.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
export type OnDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (params: {
|
export type OnDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (params: {
|
||||||
nodes: NodeType[];
|
nodes: NodeType[];
|
||||||
edges: EdgeType[];
|
edges: EdgeType[];
|
||||||
@@ -40,6 +48,10 @@ export type OnBeforeDelete<
|
|||||||
EdgeType extends Edge = Edge
|
EdgeType extends Edge = Edge
|
||||||
> = OnBeforeDeleteBase<NodeType, EdgeType>;
|
> = OnBeforeDeleteBase<NodeType, EdgeType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This type can be used to type the `isValidConnection` function.
|
||||||
|
* If the function returns `true`, the connection is valid and can be created.
|
||||||
|
*/
|
||||||
export type IsValidConnection<EdgeType extends Edge = Edge> = (
|
export type IsValidConnection<EdgeType extends Edge = Edge> = (
|
||||||
edge: EdgeType | Connection
|
edge: EdgeType | Connection
|
||||||
) => boolean;
|
) => boolean;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Component } from 'svelte';
|
import type { Component } from 'svelte';
|
||||||
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
|
import type { ClassValue, HTMLAttributes, DOMAttributes } from 'svelte/elements';
|
||||||
import type { InternalNodeBase, NodeBase, NodeProps as NodePropsBase } from '@xyflow/system';
|
import type { InternalNodeBase, NodeBase, NodeProps as NodePropsBase } from '@xyflow/system';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +25,21 @@ export type Node<
|
|||||||
* The ARIA role attribute for the node element, used for accessibility.
|
* The ARIA role attribute for the node element, used for accessibility.
|
||||||
* @default "group"
|
* @default "group"
|
||||||
*/
|
*/
|
||||||
ariaRole?: HTMLAttributes<HTMLElement>['role'];
|
ariaRole?: HTMLAttributes<HTMLDivElement>['role'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General escape hatch for adding custom attributes to the node's DOM element.
|
||||||
|
*/
|
||||||
|
domAttributes?: Omit<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
| 'id'
|
||||||
|
| 'style'
|
||||||
|
| 'class'
|
||||||
|
| 'draggable'
|
||||||
|
| 'role'
|
||||||
|
| 'aria-label'
|
||||||
|
| keyof DOMAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @todo: currently generics for nodes are not really supported
|
// @todo: currently generics for nodes are not really supported
|
||||||
|
|||||||
@@ -1,5 +1,47 @@
|
|||||||
# @xyflow/system
|
# @xyflow/system
|
||||||
|
|
||||||
|
## 0.0.65
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5370](https://github.com/xyflow/xyflow/pull/5370) [`26f2cdd7`](https://github.com/xyflow/xyflow/commit/26f2cdd720fc2c8fb337d3af13b82dab6a90fb60) Thanks [@moklick](https://github.com/moklick)! - Only fire connection end events if connection was started
|
||||||
|
|
||||||
|
## 0.0.64
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5362](https://github.com/xyflow/xyflow/pull/5362) [`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc) Thanks [@moklick](https://github.com/moklick)! - Remove pointer events from Panel via CSS while a selection gets dragged
|
||||||
|
|
||||||
|
- [#5361](https://github.com/xyflow/xyflow/pull/5361) [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c) Thanks [@peterkogo](https://github.com/peterkogo)! - Render edges above nodes when they are within a subflow
|
||||||
|
|
||||||
|
- [#5344](https://github.com/xyflow/xyflow/pull/5344) [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde) Thanks [@moklick](https://github.com/moklick)! - Add connectionDragThreshold prop
|
||||||
|
|
||||||
|
## 0.0.63
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5354](https://github.com/xyflow/xyflow/pull/5354) [`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations
|
||||||
|
|
||||||
|
- [#5333](https://github.com/xyflow/xyflow/pull/5333) [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing type exports
|
||||||
|
|
||||||
|
- [#5350](https://github.com/xyflow/xyflow/pull/5350) [`9c61000c`](https://github.com/xyflow/xyflow/commit/9c61000cac6277ce97274cc626fa7266f82dec27) Thanks [@moklick](https://github.com/moklick)! - Only send node position updates if positions changed
|
||||||
|
|
||||||
|
## 0.0.62
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#5299](https://github.com/xyflow/xyflow/pull/5299) [`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaRole` prop to nodes and edges
|
||||||
|
|
||||||
|
- [#5280](https://github.com/xyflow/xyflow/pull/5280) [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac) Thanks [@moklick](https://github.com/moklick)! - Improve typing for Nodes
|
||||||
|
|
||||||
|
- [#5276](https://github.com/xyflow/xyflow/pull/5276) [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48) Thanks [@moklick](https://github.com/moklick)! - Add an `ease` and `interpolate` option to all function that alter the viewport
|
||||||
|
|
||||||
|
- [#5277](https://github.com/xyflow/xyflow/pull/5277) [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
|
||||||
|
|
||||||
|
- [#5317](https://github.com/xyflow/xyflow/pull/5317) [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850) Thanks [@moklick](https://github.com/moklick)! - Add `domAttributes` option for nodes and edges
|
||||||
|
|
||||||
|
- [#5326](https://github.com/xyflow/xyflow/pull/5326) [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent NodeResizer controls to become too small when zooming out
|
||||||
|
|
||||||
## 0.0.61
|
## 0.0.61
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
+39
-24
@@ -1,47 +1,62 @@
|
|||||||
# @xyflow/system
|
# @xyflow/system
|
||||||
|
|
||||||
Core system that powers React Flow and Svelte Flow.
|
Core system utilities powering [React Flow](https://reactflow.dev) and [Svelte Flow](https://svelteflow.dev).
|
||||||
|
|
||||||
|
> **Note:** This package is designed as a shared vanilla utility layer for React Flow and Svelte Flow. It is not intended for use with unrelated libraries.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install @xyflow/system
|
pnpm add @xyflow/system
|
||||||
```
|
```
|
||||||
|
|
||||||
## What is this package about?
|
## What is this package?
|
||||||
|
|
||||||
The @xyflow/system package was created to have a place for vanilla utils for React Flow and Svelte Flow. The package exports helpers for edge creation, pan and zoom, dragging of nodes, general utils and lots of types. All the helpers are specifically built for React Flow and Svelte Flow so it's probably not too interesting to use them with other libraries.
|
`@xyflow/system` provides core, framework-agnostic helpers and types for building node-based editors and flow diagrams. It contains the logic and utilities that are shared between React Flow and Svelte Flow, such as edge path calculations, pan/zoom, node dragging, and more.
|
||||||
|
|
||||||
### XYPanZoom
|
## Features
|
||||||
|
|
||||||
Adds zoom and pan for the pane.
|
- **Pan & Zoom (`XYPanZoom`)**: Utilities for adding interactive pan and zoom to your canvas.
|
||||||
|
- **Dragging (`XYDrag`)**: Helpers for node and selection dragging.
|
||||||
|
- **Handles/Connections (`XYHandle`)**: Logic for drawing and managing connection lines between nodes.
|
||||||
|
- **Minimap (`XYMiniMap`)**: Interactive minimap utilities for overview and navigation.
|
||||||
|
- **Edge Utilities**: Functions for SVG edge path creation (bezier, straight, step, smoothstep, etc.).
|
||||||
|
- **Store Utilities**: Helpers for managing and updating flow state.
|
||||||
|
- **DOM Utilities**: Functions for DOM measurements and interactions.
|
||||||
|
- **Marker Utilities**: Helpers for SVG markers on edges.
|
||||||
|
- **Graph Utilities**: Functions for working with nodes, edges, and graph structure.
|
||||||
|
- **General Utilities**: Miscellaneous helpers used throughout the system.
|
||||||
|
- **Types & Constants**: Shared types, enums, and constants for consistent data structures.
|
||||||
|
|
||||||
### XYDrag
|
## Usage
|
||||||
|
|
||||||
Adds drag for nodes and selection.
|
You can import any utility, type, or helper directly from the package:
|
||||||
|
|
||||||
### XYHandle
|
```ts
|
||||||
|
import { getBezierPath, getConnectedEdges, Position, XYPanZoom } from '@xyflow/system';
|
||||||
|
```
|
||||||
|
|
||||||
Adds connection line drawing.
|
### Example: Bezier Edge Path Creation
|
||||||
|
|
||||||
### XYMinimap
|
```ts
|
||||||
|
import { getBezierPath, Position } from '@xyflow/system';
|
||||||
|
|
||||||
Adds interactive mini map (zoom and pan).
|
const [path, labelX, labelY] = getBezierPath({
|
||||||
|
sourceX: 0,
|
||||||
|
sourceY: 20,
|
||||||
|
sourcePosition: Position.Right,
|
||||||
|
targetX: 150,
|
||||||
|
targetY: 100,
|
||||||
|
targetPosition: Position.Left,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### Edge utils
|
## API Reference
|
||||||
|
|
||||||
Util function for SVG edge path creating.
|
There is currently no dedicated API documentation for this package. For details on available utilities, types, and helpers, please refer to the [source code](./src) or check out the [React Flow documentation](https://reactflow.dev/api-reference/) where we are documenting a lot of stuff from this package.
|
||||||
|
|
||||||
### Store utils
|
## Contributing
|
||||||
|
|
||||||
Helpers for store functions.
|
See the main [xyflow repository](https://github.com/xyflow/xyflow) for contribution guidelines.
|
||||||
|
|
||||||
### Dom utils
|
|
||||||
|
|
||||||
### Marker utils
|
|
||||||
|
|
||||||
### Graph utils
|
|
||||||
|
|
||||||
### General utils
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/system",
|
"name": "@xyflow/system",
|
||||||
"version": "0.0.61",
|
"version": "0.0.65",
|
||||||
"description": "xyflow core system that powers React Flow and Svelte Flow.",
|
"description": "xyflow core system that powers React Flow and Svelte Flow.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"node-based UI",
|
"node-based UI",
|
||||||
|
|||||||
@@ -265,6 +265,10 @@ svg.xy-flow__connectionline {
|
|||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xy-flow__pane.selection .xy-flow__panel {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.xy-flow__panel {
|
.xy-flow__panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
|||||||
@@ -28,12 +28,12 @@
|
|||||||
|
|
||||||
/* handle styles */
|
/* handle styles */
|
||||||
.xy-flow__resize-control.handle {
|
.xy-flow__resize-control.handle {
|
||||||
width: 4px;
|
width: 5px;
|
||||||
height: 4px;
|
height: 5px;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
|
background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
|
||||||
transform: translate(-50%, -50%);
|
translate: -50% -50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xy-flow__resize-control.handle.left {
|
.xy-flow__resize-control.handle.left {
|
||||||
|
|||||||
@@ -40,11 +40,6 @@ export type EdgeBase<
|
|||||||
* This property sets the width of that invisible path.
|
* This property sets the width of that invisible path.
|
||||||
*/
|
*/
|
||||||
interactionWidth?: number;
|
interactionWidth?: number;
|
||||||
/**
|
|
||||||
* A description of the edge's, used for accessibility.
|
|
||||||
* @default "edge"
|
|
||||||
*/
|
|
||||||
ariaRoleDescription?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SmoothStepPathOptions = {
|
export type SmoothStepPathOptions = {
|
||||||
|
|||||||
@@ -13,16 +13,56 @@ import { EdgeBase } from '..';
|
|||||||
|
|
||||||
export type Project = (position: XYPosition) => XYPosition;
|
export type Project = (position: XYPosition) => XYPosition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This type is used to define the `onMove` handler.
|
||||||
|
*/
|
||||||
export type OnMove = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void;
|
export type OnMove = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void;
|
||||||
export type OnMoveStart = OnMove;
|
export type OnMoveStart = OnMove;
|
||||||
export type OnMoveEnd = OnMove;
|
export type OnMoveEnd = OnMove;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inline
|
||||||
|
*/
|
||||||
export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => Promise<boolean>;
|
export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* @inline
|
||||||
|
*/
|
||||||
export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
|
export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* @inline
|
||||||
|
*/
|
||||||
export type GetZoom = () => number;
|
export type GetZoom = () => number;
|
||||||
|
/**
|
||||||
|
* @inline
|
||||||
|
*/
|
||||||
export type GetViewport = () => Viewport;
|
export type GetViewport = () => Viewport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `SetViewport` function is used to set the viewport of the flow.
|
||||||
|
*
|
||||||
|
* @inline
|
||||||
|
* @param viewport - The viewport to set.
|
||||||
|
* @param options - Optional parameters to control the animation and easing of the viewport change.
|
||||||
|
*/
|
||||||
export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
|
export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `SetCenter` function is used to set the center of the flow viewport to a specific position
|
||||||
|
*
|
||||||
|
* @inline
|
||||||
|
* @param x - x coordinate
|
||||||
|
* @param y - y coordinate
|
||||||
|
* @param options - Optional parameters to control the animation and easing of the viewport change.
|
||||||
|
*/
|
||||||
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise<boolean>;
|
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `FitBounds` function is used to fit the flow viewport to the bounds of the nodes.
|
||||||
|
*
|
||||||
|
* @inline
|
||||||
|
* @param bounds - The bounds to fit the viewport to.
|
||||||
|
* @param options - Optional parameters to control the animation and easing of the viewport change.
|
||||||
|
*/
|
||||||
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise<boolean>;
|
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,10 +219,16 @@ export type ViewportHelperFunctionOptions = {
|
|||||||
interpolate?: 'smooth' | 'linear';
|
interpolate?: 'smooth' | 'linear';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inline
|
||||||
|
*/
|
||||||
export type SetCenterOptions = ViewportHelperFunctionOptions & {
|
export type SetCenterOptions = ViewportHelperFunctionOptions & {
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inline
|
||||||
|
*/
|
||||||
export type FitBoundsOptions = ViewportHelperFunctionOptions & {
|
export type FitBoundsOptions = ViewportHelperFunctionOptions & {
|
||||||
padding?: number;
|
padding?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,11 +73,6 @@ export type NodeBase<
|
|||||||
*/
|
*/
|
||||||
origin?: NodeOrigin;
|
origin?: NodeOrigin;
|
||||||
handles?: NodeHandle[];
|
handles?: NodeHandle[];
|
||||||
/**
|
|
||||||
* A description of the node's role, used for accessibility.
|
|
||||||
* @default "node"
|
|
||||||
*/
|
|
||||||
ariaRoleDescription?: string;
|
|
||||||
measured?: {
|
measured?: {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
|||||||
@@ -30,21 +30,30 @@ export type GetEdgeZIndexParams = {
|
|||||||
elevateOnSelect?: boolean;
|
elevateOnSelect?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the z-index for an edge based on the node it connects and whether it is selected.
|
||||||
|
* By default, edges are rendered below nodes. This behaviour is different for edges that are
|
||||||
|
* connected to nodes with a parent, as they are rendered above the parent node.
|
||||||
|
*/
|
||||||
export function getElevatedEdgeZIndex({
|
export function getElevatedEdgeZIndex({
|
||||||
sourceNode,
|
sourceNode,
|
||||||
targetNode,
|
targetNode,
|
||||||
selected = false,
|
selected = false,
|
||||||
zIndex = 0,
|
zIndex,
|
||||||
elevateOnSelect = false,
|
elevateOnSelect = false,
|
||||||
}: GetEdgeZIndexParams): number {
|
}: GetEdgeZIndexParams): number {
|
||||||
if (!elevateOnSelect) {
|
if (zIndex !== undefined) {
|
||||||
return zIndex;
|
return zIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
|
const edgeZ = elevateOnSelect && selected ? 1000 : 0;
|
||||||
const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000);
|
|
||||||
|
|
||||||
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
|
const nodeZ = Math.max(
|
||||||
|
sourceNode.parentId ? sourceNode.internals.z : 0,
|
||||||
|
targetNode.parentId ? targetNode.internals.z : 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return edgeZ + nodeZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IsEdgeVisibleParams = {
|
type IsEdgeVisibleParams = {
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ function calculateChildXYZ<NodeType extends NodeBase>(
|
|||||||
return {
|
return {
|
||||||
x: absolutePosition.x,
|
x: absolutePosition.x,
|
||||||
y: absolutePosition.y,
|
y: absolutePosition.y,
|
||||||
z: parentZ > childZ ? parentZ : childZ,
|
z: parentZ >= childZ ? parentZ + 1 : childZ,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
|||||||
let dragStarted = false;
|
let dragStarted = false;
|
||||||
let d3Selection: Selection<Element, unknown, null, undefined> | null = null;
|
let d3Selection: Selection<Element, unknown, null, undefined> | null = null;
|
||||||
let abortDrag = false; // prevents unintentional dragging on multitouch
|
let abortDrag = false; // prevents unintentional dragging on multitouch
|
||||||
|
let nodePositionsChanged = false;
|
||||||
|
|
||||||
// public functions
|
// public functions
|
||||||
function update({
|
function update({
|
||||||
@@ -191,6 +192,8 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
|||||||
dragItem.internals.positionAbsolute = positionAbsolute;
|
dragItem.internals.positionAbsolute = positionAbsolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodePositionsChanged = nodePositionsChanged || hasChange;
|
||||||
|
|
||||||
if (!hasChange) {
|
if (!hasChange) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -294,6 +297,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
|||||||
containerBounds = domNode?.getBoundingClientRect() || null;
|
containerBounds = domNode?.getBoundingClientRect() || null;
|
||||||
|
|
||||||
abortDrag = false;
|
abortDrag = false;
|
||||||
|
nodePositionsChanged = false;
|
||||||
|
|
||||||
if (nodeDragThreshold === 0) {
|
if (nodeDragThreshold === 0) {
|
||||||
startDrag(event);
|
startDrag(event);
|
||||||
@@ -354,7 +358,10 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
|||||||
if (dragItems.size > 0) {
|
if (dragItems.size > 0) {
|
||||||
const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
|
const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
|
||||||
|
|
||||||
updateNodePositions(dragItems, false);
|
if (nodePositionsChanged) {
|
||||||
|
updateNodePositions(dragItems, false);
|
||||||
|
nodePositionsChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (onDragStop || onNodeDragStop || (!nodeId && onSelectionDragStop)) {
|
if (onDragStop || onNodeDragStop || (!nodeId && onSelectionDragStop)) {
|
||||||
const [currentNode, currentNodes] = getEventHandlerParams({
|
const [currentNode, currentNodes] = getEventHandlerParams({
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ function onPointerDown(
|
|||||||
getTransform,
|
getTransform,
|
||||||
getFromHandle,
|
getFromHandle,
|
||||||
autoPanSpeed,
|
autoPanSpeed,
|
||||||
|
dragThreshold = 1,
|
||||||
}: OnPointerDownParams
|
}: OnPointerDownParams
|
||||||
) {
|
) {
|
||||||
// when xyflow is used inside a shadow root we can't use document
|
// when xyflow is used inside a shadow root we can't use document
|
||||||
@@ -56,6 +57,7 @@ function onPointerDown(
|
|||||||
const clickedHandle = doc?.elementFromPoint(x, y);
|
const clickedHandle = doc?.elementFromPoint(x, y);
|
||||||
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
|
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
|
||||||
const containerBounds = domNode?.getBoundingClientRect();
|
const containerBounds = domNode?.getBoundingClientRect();
|
||||||
|
let connectionStarted = false;
|
||||||
|
|
||||||
if (!containerBounds || !handleType) {
|
if (!containerBounds || !handleType) {
|
||||||
return;
|
return;
|
||||||
@@ -92,10 +94,9 @@ function onPointerDown(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fromNodeInternal = nodeLookup.get(nodeId)!;
|
const fromNodeInternal = nodeLookup.get(nodeId)!;
|
||||||
|
|
||||||
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
|
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
|
||||||
|
|
||||||
const newConnection: ConnectionInProgress = {
|
let previousConnection: ConnectionInProgress = {
|
||||||
inProgress: true,
|
inProgress: true,
|
||||||
isValid: null,
|
isValid: null,
|
||||||
|
|
||||||
@@ -110,12 +111,30 @@ function onPointerDown(
|
|||||||
toNode: null,
|
toNode: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
updateConnection(newConnection);
|
function startConnection() {
|
||||||
let previousConnection: ConnectionInProgress = newConnection;
|
connectionStarted = true;
|
||||||
|
updateConnection(previousConnection);
|
||||||
|
onConnectStart?.(event, { nodeId, handleId, handleType });
|
||||||
|
}
|
||||||
|
|
||||||
onConnectStart?.(event, { nodeId, handleId, handleType });
|
if (dragThreshold === 0) {
|
||||||
|
startConnection();
|
||||||
|
}
|
||||||
|
|
||||||
function onPointerMove(event: MouseEvent | TouchEvent) {
|
function onPointerMove(event: MouseEvent | TouchEvent) {
|
||||||
|
if (!connectionStarted) {
|
||||||
|
const { x: evtX, y: evtY } = getEventPosition(event);
|
||||||
|
const dx = evtX - x;
|
||||||
|
const dy = evtY - y;
|
||||||
|
const nextConnectionStarted = dx * dx + dy * dy > dragThreshold * dragThreshold;
|
||||||
|
|
||||||
|
if (!nextConnectionStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startConnection();
|
||||||
|
}
|
||||||
|
|
||||||
if (!getFromHandle() || !fromHandle) {
|
if (!getFromHandle() || !fromHandle) {
|
||||||
onPointerUp(event);
|
onPointerUp(event);
|
||||||
return;
|
return;
|
||||||
@@ -188,24 +207,27 @@ function onPointerDown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onPointerUp(event: MouseEvent | TouchEvent) {
|
function onPointerUp(event: MouseEvent | TouchEvent) {
|
||||||
if ((closestHandle || handleDomNode) && connection && isValid) {
|
if (connectionStarted) {
|
||||||
onConnect?.(connection);
|
if ((closestHandle || handleDomNode) && connection && isValid) {
|
||||||
}
|
onConnect?.(connection);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* it's important to get a fresh reference from the store here
|
* it's important to get a fresh reference from the store here
|
||||||
* in order to get the latest state of onConnectEnd
|
* in order to get the latest state of onConnectEnd
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { inProgress, ...connectionState } = previousConnection;
|
const { inProgress, ...connectionState } = previousConnection;
|
||||||
const finalConnectionState = {
|
const finalConnectionState = {
|
||||||
...connectionState,
|
...connectionState,
|
||||||
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null,
|
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null,
|
||||||
};
|
};
|
||||||
onConnectEnd?.(event, finalConnectionState);
|
|
||||||
|
|
||||||
if (edgeUpdaterType) {
|
onConnectEnd?.(event, finalConnectionState);
|
||||||
onReconnectEnd?.(event, finalConnectionState);
|
|
||||||
|
if (edgeUpdaterType) {
|
||||||
|
onReconnectEnd?.(event, finalConnectionState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelConnection();
|
cancelConnection();
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export type OnPointerDownParams = {
|
|||||||
getTransform: () => Transform;
|
getTransform: () => Transform;
|
||||||
getFromHandle: () => Handle | null;
|
getFromHandle: () => Handle | null;
|
||||||
autoPanSpeed?: number;
|
autoPanSpeed?: number;
|
||||||
|
dragThreshold?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IsValidParams = {
|
export type IsValidParams = {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
createZoomOnScrollHandler,
|
createZoomOnScrollHandler,
|
||||||
} from './eventhandler';
|
} from './eventhandler';
|
||||||
import { createFilter } from './filter';
|
import { createFilter } from './filter';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
import { transition } from 'd3-transition';
|
import { transition } from 'd3-transition';
|
||||||
|
|
||||||
export type ZoomPanValues = {
|
export type ZoomPanValues = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
|
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
import { transition } from 'd3-transition';
|
import { transition } from 'd3-transition';
|
||||||
|
|
||||||
import { type D3SelectionInstance, type Viewport } from '../types';
|
import { type D3SelectionInstance, type Viewport } from '../types';
|
||||||
|
|||||||
Generated
+2834
-2737
File diff suppressed because it is too large
Load Diff
@@ -153,5 +153,26 @@ test.describe('Edges', () => {
|
|||||||
await expect(edge).toHaveAttribute('marker-start', "url('#1__type=arrowclosed')");
|
await expect(edge).toHaveAttribute('marker-start', "url('#1__type=arrowclosed')");
|
||||||
await expect(edge).toHaveAttribute('marker-end', "url('#1__type=arrow')");
|
await expect(edge).toHaveAttribute('marker-end', "url('#1__type=arrow')");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('z-index', async ({ page }) => {
|
||||||
|
const svg = page.locator('svg', { has: page.locator('[data-id="edge-with-class"]') });
|
||||||
|
|
||||||
|
await expect(svg).toBeAttached();
|
||||||
|
await expect(svg).toHaveCSS('z-index', '0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sub flow: normal node to child node, z-index', async ({ page }) => {
|
||||||
|
const svg = page.locator('svg', { has: page.locator('[data-id="subflow-edge"]') });
|
||||||
|
|
||||||
|
await expect(svg).toBeAttached();
|
||||||
|
await expect(svg).toHaveCSS('z-index', '1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sub flow: child node to child node, z-index', async ({ page }) => {
|
||||||
|
const svg = page.locator('svg', { has: page.locator('[data-id="subflow-edge-2"]') });
|
||||||
|
|
||||||
|
await expect(svg).toBeAttached();
|
||||||
|
await expect(svg).toHaveCSS('z-index', '1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user