Merge branch 'main' into refactor/promised-panzoom
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json",
|
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
|
||||||
"changelog": ["@changesets/changelog-github", { "repo": "wbkd/react-flow" }],
|
"changelog": ["@changesets/changelog-github", { "repo": "xyflow/xyflow" }],
|
||||||
"commit": false,
|
"commit": false,
|
||||||
"fixed": [],
|
"fixed": [],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
"access": "public",
|
"access": "restricted",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch",
|
"updateInternalDependencies": "patch",
|
||||||
"ignore": []
|
"ignore": ["react-examples", "svelte-examples", "astro-examples", "playwright"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,47 +4,57 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thank you for reporting an issue :pray:.
|
Hello! :sparkles: You found the place to submit a bug or issue that you discovered in the xyflow library, thank you for helping us out!
|
||||||
|
|
||||||
**Please only file issues for v11. Will will no longer fix issues for v10.**
|
Having trouble making something work in xyflow, but it’s not a bug with the library itself? You’ll get the fastest response by asking in [github discussions](https://github.com/xyflow/xyflow/discussions) or our [discord server](https://discord.gg/RVmnytFmGW).
|
||||||
|
|
||||||
This issue tracker is for reporting bugs found in react-flow (https://github.com/wbkd/react-flow)
|
**Search our [discussions](https://github.com/xyflow/xyflow/discussions?discussions_q=) and [issues](https://github.com/xyflow/xyflow/issues?q=)** to see if your problem has already been reported and you can add more context, or see if someone else has already solved your issue. And of course, check our [docs](https://reactflow.dev/learn) if you haven’t already.
|
||||||
If you have a question about how to achieve something and are struggling, please post a question
|
|
||||||
inside of react-flow's Discussion's tab: https://github.com/wbkd/react-flow/discussions
|
|
||||||
If it's an issue about the docs please go to the react-flow-docs (https://github.com/wbkd/react-flow-docs)
|
|
||||||
|
|
||||||
Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:
|
**Issue with the xyflow Docs?** Submit an issue in our [web repo](https://github.com/xyflow/web/issues) instead.
|
||||||
- react-flow's Open Issue's tab: https://github.com/wbkd/react-flow/issues?q=is%3Aissue+sort%3Aupdated-desc+position
|
|
||||||
- react-flow's Closed Issues tab: https://github.com/wbkd/react-flow/issues?q=is%3Aissue+sort%3Aupdated-desc+position+is%3Aclosed
|
|
||||||
- react-flow's Discussion's tab: https://github.com/wbkd/react-flow/discussions
|
|
||||||
|
|
||||||
The more information you fill in, the better the community can help you.
|
With that out of the way, let’s get started :bug:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: platform
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe the Bug
|
label: What platform were you using when you found the bug?
|
||||||
description: Provide a clear and concise description of the challenge you are running into.
|
description: We only address issues for the most recent xyflow major release or upcoming releases.
|
||||||
|
value: |
|
||||||
|
- React Flow / Svelte Flow version:
|
||||||
|
- Browser and version:
|
||||||
|
- OS and version:
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
id: link
|
id: link
|
||||||
attributes:
|
attributes:
|
||||||
label: Your Example Website or App
|
label: Live code example
|
||||||
description: |
|
description: |
|
||||||
Which website or app were you using when the bug happened?
|
This is the most important part of the bug report!
|
||||||
Note:
|
|
||||||
- Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the `react-flow-renderer` npm package.
|
React Flow codesandbox starters
|
||||||
- To create a shareable code example you can use Codesandbox (https://codesandbox.io/s/react-flow-starter-ou8or) or Stackblitz (https://stackblitz.com/).
|
- js: https://new.reactflow.dev/js
|
||||||
- Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve.
|
- ts: https://new.reactflow.dev/ts
|
||||||
placeholder: |
|
|
||||||
e.g. https://stackblitz.com/edit/...... OR Github Repo
|
Svelte Flow stackblitz starters
|
||||||
|
- js: https://new.svelteflow.dev/js
|
||||||
|
- ts: https://new.svelteflow.dev/ts
|
||||||
|
|
||||||
|
Avoid dependencies in your example for easier debugging.
|
||||||
|
placeholder: https://stackblitz.com/edit/... or Github Repo
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the Bug
|
||||||
|
description: What exactly is happening when you encounter the bug?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: steps
|
id: steps
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to Reproduce the Bug or Issue
|
label: Steps to reproduce the bug or issue
|
||||||
description: Describe the steps we have to take to reproduce the behavior.
|
description: How can we see the reported behavior in the code example you provided? The more detail, the better!
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
@@ -56,9 +66,8 @@ body:
|
|||||||
id: expected
|
id: expected
|
||||||
attributes:
|
attributes:
|
||||||
label: Expected behavior
|
label: Expected behavior
|
||||||
description: Provide a clear and concise description of what you expected to happen.
|
description: What would you expect to happen if there was no bug or issue?
|
||||||
placeholder: |
|
placeholder: As a user, I expected ___ behavior, but instead I am seeing ___
|
||||||
As a user, I expected ___ behavior but i am seeing ___
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -67,22 +76,9 @@ body:
|
|||||||
label: Screenshots or Videos
|
label: Screenshots or Videos
|
||||||
description: |
|
description: |
|
||||||
If applicable, add screenshots or a video to help explain your problem.
|
If applicable, add screenshots or a video to help explain your problem.
|
||||||
For more information on the supported file image/file types and the file size limits, please refer
|
Supported image/file types and file size limits- https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
|
||||||
to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
|
placeholder: Drag your video or image files inside of this editor
|
||||||
placeholder: |
|
|
||||||
You can drag your video or image files inside of this editor ↓
|
|
||||||
- type: textarea
|
|
||||||
id: platform
|
|
||||||
attributes:
|
|
||||||
label: Platform
|
|
||||||
value: |
|
|
||||||
- OS: [e.g. macOS, Windows, Linux]
|
|
||||||
- Browser: [e.g. Chrome, Safari, Firefox]
|
|
||||||
- Version: [e.g. 91.1]
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional context
|
label: Additional context
|
||||||
description: Add any other context about the problem here.
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name: 'CI checks'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
shell: bash
|
||||||
|
- name: Typecheck
|
||||||
|
run: pnpm typecheck
|
||||||
|
shell: bash
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
name: 'CI setup'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
- name: Setup node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
shell: bash
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Playwright Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
playwright:
|
||||||
|
name: 'Playwright Tests'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: mcr.microsoft.com/playwright:v1.45.0-jammy
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/ci-setup
|
||||||
|
- uses: ./.github/actions/ci-checks
|
||||||
|
- name: Run your tests
|
||||||
|
run: CI=true pnpm test:react
|
||||||
|
env:
|
||||||
|
HOME: /root
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
# push:
|
||||||
|
# branches:
|
||||||
|
# - main
|
||||||
|
|
||||||
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/ci-setup
|
||||||
|
- uses: ./.github/actions/ci-checks
|
||||||
|
|
||||||
|
- name: Create release PR or publish to npm
|
||||||
|
id: changesets
|
||||||
|
uses: xyflow/changeset-action@v1
|
||||||
|
with:
|
||||||
|
publish: pnpm release
|
||||||
|
title: Release packages
|
||||||
|
commit: 'chore(packages): bump'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
registry="https://registry.npmjs.com/"
|
|
||||||
legacy-peer-deps=true
|
legacy-peer-deps=true
|
||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2019-2023 webkid GmbH
|
Copyright (c) 2019-2024 webkid GmbH
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -17,22 +17,22 @@ Powerful open source libraries for building node-based UIs with React or Svelte.
|
|||||||
## The xyflow mono repo
|
## The xyflow mono repo
|
||||||
|
|
||||||
The xyflow repository is the home of four packages:
|
The xyflow repository is the home of four packages:
|
||||||
* React Flow v11 `reactflow` [v11 branch](https://github.com/xyflow/xyflow/tree/v11)
|
* React Flow 11 `reactflow` [v11 branch](https://github.com/xyflow/xyflow/tree/v11)
|
||||||
* React Flow v12 (not published yet) [packages/react](./packages/react)
|
* React Flow 12 (beta) `@xyflow/react` [packages/react](./packages/react)
|
||||||
* Svelte Flow `@xyflow/svelte` [packages/svelte](./packages/svelte)
|
* Svelte Flow `@xyflow/svelte` [packages/svelte](./packages/svelte)
|
||||||
* Shared helper library `@xyflow/system` [packages/system](./packages/system)
|
* Shared helper library `@xyflow/system` [packages/system](./packages/system)
|
||||||
|
|
||||||
**We just moved repositories** from the @wbkd org to this one. React Flow v11 will remain on the [v11 branch](https://github.com/xyflow/xyflow/tree/v11). When we have a stable v12, the package name of React Flow will change from `reactflow` to `@xyflow/react`.
|
**We just moved repositories** from the @wbkd org to this one. React Flow v11 will remain on the [v11 branch](https://github.com/xyflow/xyflow/tree/v11). When we have a stable v12, the package name of React Flow will change from `reactflow` to `@xyflow/react`.
|
||||||
|
|
||||||
## Commercial Usage
|
## Commercial usage
|
||||||
|
|
||||||
**Are you using React Flow or Svelte Flow for a personal project?** Great! No sponsorship needed, you can support us by reporting any bugs you find, sending us screenshots of your projects, and starring us on Github 🌟
|
**Are you using React Flow or Svelte Flow for a personal project?** Great! No sponsorship needed, you can support us by reporting any bugs you find, sending us screenshots of your projects, and starring us on Github 🌟
|
||||||
|
|
||||||
**Are you using React Flow or Svelte Flow at your organization and making money from it?** Awesome! We rely on your support to keep our libraries developed and maintained under an MIT License, just how we like it. For React Flow you can do that on the [React Flow Pro website](https://reactflow.dev/pro) and for both of our libraries you can do it through [Github Sponsors](https://github.com/sponsors/xyflow).
|
**Are you using React Flow or Svelte Flow at your organization and making money from it?** Awesome! We rely on your support to keep our libraries developed and maintained under an MIT License, just how we like it. For React Flow you can do that on the [React Flow Pro website](https://reactflow.dev/pro) and for both of our libraries you can do it through [Github Sponsors](https://github.com/sponsors/xyflow).
|
||||||
|
|
||||||
## Getting Started
|
## Getting started
|
||||||
|
|
||||||
The best way to get started is to check out the [React Flow](https://reactflow.dev/learn) or [Svelte Flow](https://svelteflow.dev/learn) learn section. However if you want to get a sneak peek of how to install the and use the libraries you can see it here:
|
The best way to get started is to check out the [React Flow](https://reactflow.dev/learn) or [Svelte Flow](https://svelteflow.dev/learn) learn section. However if you want to get a sneak peek of how to install and use the libraries you can see it here:
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>React Flow</strong> basic usage</summary>
|
<summary><strong>React Flow</strong> basic usage</summary>
|
||||||
@@ -151,6 +151,15 @@ npm install @xyflow/svelte
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
For releasing packages we are using [changesets](https://github.com/changesets/changesets) in combination with the [changeset Github action](https://github.com/changesets/action). The rough idea is:
|
||||||
|
|
||||||
|
1. create PRs for new features, updates and fixes (with a changeset if relevant for changelog)
|
||||||
|
2. merge into main
|
||||||
|
3. changset creates a PR that bumps all packages based on the changesets
|
||||||
|
4. merge changeset PR if you want to release to Github and npm
|
||||||
|
|
||||||
## The xyflow team
|
## The xyflow team
|
||||||
|
|
||||||
React Flow and Svelte Flow are maintained by the team behind [xyflow](https://xyflow.com). If you need help or want to talk to us about a collaboration, reach out through our [contact form](https://xyflow.com/contact) or by joining our [Discord Server](https://discord.gg/Bqt6xrs).
|
React Flow and Svelte Flow are maintained by the team behind [xyflow](https://xyflow.com). If you need help or want to talk to us about a collaboration, reach out through our [contact form](https://xyflow.com/contact) or by joining our [Discord Server](https://discord.gg/Bqt6xrs).
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Examples for Testing & Development
|
||||||
|
These examples are used for feature development and E2E testing.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
SvelteKit App with Svelte Flow examples at [`examples/svelte`](./svelte)
|
||||||
|
|
||||||
|
Vite App with React Flow examples at [`examples/react`](./react)
|
||||||
|
|
||||||
|
Astro App used for SSR testing at [`examples/astro-xyflow`](./astro-xyflow)
|
||||||
|
|
||||||
|
## E2E
|
||||||
|
|
||||||
|
For furhter documentation of E2E tests have a look at [`/tests/playwright`](/tests/playwright).
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "astro",
|
"name": "astro-examples",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const sourceHandleStyleB: CSSProperties = {
|
|||||||
left: 'auto',
|
left: 'auto',
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
|
const CustomNode: FC<NodeProps> = ({ data, positionAbsoluteX, positionAbsoluteY }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Handle type="target" position={Position.Top} />
|
<Handle type="target" position={Position.Top} />
|
||||||
@@ -18,7 +18,7 @@ const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
|
|||||||
<div>
|
<div>
|
||||||
Position:{' '}
|
Position:{' '}
|
||||||
<strong>
|
<strong>
|
||||||
{xPos.toFixed(2)},{yPos.toFixed(2)}
|
{positionAbsoluteX.toFixed(2)},{positionAbsoluteY.toFixed(2)}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const initialNodes: Node[] = [
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
data: { label: 'Node 1' },
|
data: { label: 'Node 1' },
|
||||||
position: { x: 250, y: 5 },
|
position: { x: 250, y: 5 },
|
||||||
size: nodeSize,
|
...nodeSize,
|
||||||
handles: [
|
handles: [
|
||||||
{
|
{
|
||||||
type: 'source',
|
type: 'source',
|
||||||
@@ -41,23 +41,19 @@ const initialNodes: Node[] = [
|
|||||||
id: '2',
|
id: '2',
|
||||||
data: { label: 'Node 2' },
|
data: { label: 'Node 2' },
|
||||||
position: { x: 100, y: 100 },
|
position: { x: 100, y: 100 },
|
||||||
size: nodeSize,
|
...nodeSize,
|
||||||
handles: [
|
handles: [
|
||||||
{
|
{
|
||||||
type: 'source',
|
type: 'source',
|
||||||
position: Position.Bottom,
|
position: Position.Bottom,
|
||||||
x: nodeSize.width * 0.5,
|
x: nodeSize.width * 0.5,
|
||||||
y: nodeSize.height,
|
y: nodeSize.height,
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'target',
|
type: 'target',
|
||||||
position: Position.Top,
|
position: Position.Top,
|
||||||
x: nodeSize.width * 0.5,
|
x: nodeSize.width * 0.5,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -65,23 +61,19 @@ const initialNodes: Node[] = [
|
|||||||
id: '3',
|
id: '3',
|
||||||
data: { label: 'Node 3' },
|
data: { label: 'Node 3' },
|
||||||
position: { x: 400, y: 100 },
|
position: { x: 400, y: 100 },
|
||||||
size: nodeSize,
|
...nodeSize,
|
||||||
handles: [
|
handles: [
|
||||||
{
|
{
|
||||||
type: 'source',
|
type: 'source',
|
||||||
position: Position.Bottom,
|
position: Position.Bottom,
|
||||||
x: nodeSize.width * 0.5,
|
x: nodeSize.width * 0.5,
|
||||||
y: nodeSize.height,
|
y: nodeSize.height,
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'target',
|
type: 'target',
|
||||||
position: Position.Top,
|
position: Position.Top,
|
||||||
x: nodeSize.width * 0.5,
|
x: nodeSize.width * 0.5,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -89,6 +81,8 @@ const initialNodes: Node[] = [
|
|||||||
id: '4',
|
id: '4',
|
||||||
data: { label: 'Node 4' },
|
data: { label: 'Node 4' },
|
||||||
position: { x: 400, y: 200 },
|
position: { x: 400, y: 200 },
|
||||||
|
width: 200,
|
||||||
|
height: 50,
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { Handle, Position } from '@xyflow/react';
|
||||||
|
|
||||||
|
function CustomNode() {
|
||||||
|
const [text, setText] = useState('this is a pretty long text');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Handle type="target" position={Position.Top} />
|
||||||
|
<div style={{ background: '#eee', padding: 10 }}>
|
||||||
|
<div>
|
||||||
|
<input value={text} onChange={(e) => setText(e.target.value)} />
|
||||||
|
<div>text: {text}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Handle type="source" position={Position.Bottom} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(CustomNode);
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
addEdge,
|
||||||
|
useEdgesState,
|
||||||
|
useNodesState,
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
type Connection,
|
||||||
|
type Edge,
|
||||||
|
type Node,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
import CustomNode from './CustomNode';
|
||||||
|
|
||||||
|
import '@xyflow/react/dist/style.css';
|
||||||
|
|
||||||
|
const initialNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
data: {},
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
initialWidth: 200,
|
||||||
|
initialHeight: 50,
|
||||||
|
type: 'custom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
data: {},
|
||||||
|
position: { x: 0, y: 200 },
|
||||||
|
width: 200,
|
||||||
|
initialHeight: 50,
|
||||||
|
type: 'custom',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
custom: CustomNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
function Flow() {
|
||||||
|
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: 700, height: 400 }}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
edges={edges}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
width={700}
|
||||||
|
height={400}
|
||||||
|
debug
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
<Controls />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Flow;
|
||||||
@@ -11,10 +11,8 @@
|
|||||||
data: { label: 'Node 0' },
|
data: { label: 'Node 0' },
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
size: {
|
width: 100,
|
||||||
width: 100,
|
height: 40,
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
handles: [
|
handles: [
|
||||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||||
@@ -26,10 +24,8 @@
|
|||||||
data: { label: 'A' },
|
data: { label: 'A' },
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
size: {
|
width: 100,
|
||||||
width: 100,
|
height: 40,
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
handles: [
|
handles: [
|
||||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||||
@@ -41,10 +37,8 @@
|
|||||||
data: { label: 'B' },
|
data: { label: 'B' },
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
size: {
|
width: 100,
|
||||||
width: 100,
|
height: 40,
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
handles: [
|
handles: [
|
||||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||||
@@ -56,10 +50,8 @@
|
|||||||
data: { label: 'C' },
|
data: { label: 'C' },
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
size: {
|
width: 100,
|
||||||
width: 100,
|
height: 40,
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
handles: [
|
handles: [
|
||||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
let text = 'some default text';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<div class="custom">
|
||||||
|
<div>
|
||||||
|
text: {text}
|
||||||
|
</div>
|
||||||
|
<input type="text" bind:value={text} />
|
||||||
|
</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom {
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #777;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { SvelteFlow, Controls, Background, BackgroundVariant, type Node, type Edge } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
import CustomNode from './CustomNode.svelte';
|
||||||
|
|
||||||
|
const nodes = writable<Node[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {},
|
||||||
|
initialWidth: 100,
|
||||||
|
initialHeight: 40,
|
||||||
|
type: 'input-node',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
position: { x: 0, y: 150 },
|
||||||
|
data: {},
|
||||||
|
initialWidth: 100,
|
||||||
|
initialHeight: 40,
|
||||||
|
type: 'input-node',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const edges = writable<Edge[]>([{ id: '1-2', source: '1', target: '2' }]);
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
'input-node': CustomNode,
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="height: 400px; width: 700px;">
|
||||||
|
<SvelteFlow {nodes} {edges} {nodeTypes} fitView width={700} height={400}>
|
||||||
|
<Controls />
|
||||||
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
</SvelteFlow>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
import ReactFlowApp from '../components/ReactFlowExample'
|
import ReactFlowApp from '../components/ReactFlowExample';
|
||||||
import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte'
|
import ReactFlowInitialApp from '../components/ReactFlowInitialExample';
|
||||||
|
import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte';
|
||||||
|
import SvelteFlowInitialApp from '../components/SvelteFlowInitialExample/index.svelte';
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -18,10 +20,24 @@ import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte'
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<h2>React Flow</h2>
|
||||||
|
<p>no client hydration</p>
|
||||||
|
<ReactFlowApp />
|
||||||
|
|
||||||
|
<p>client hydration on load (client:load)</p>
|
||||||
|
<ReactFlowApp client:load />
|
||||||
|
|
||||||
|
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
|
||||||
|
<ReactFlowInitialApp client:load />
|
||||||
|
|
||||||
<h2>Svelte Flow</h2>
|
<h2>Svelte Flow</h2>
|
||||||
<SvelteFlowApp />
|
<SvelteFlowApp />
|
||||||
|
|
||||||
<h2>React Flow</h2>
|
<p>client hydration on load (client:load)</p>
|
||||||
<ReactFlowApp />
|
<SvelteFlowApp client:load />
|
||||||
|
|
||||||
|
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
|
||||||
|
<SvelteFlowInitialApp client:load />
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# React Flow examples
|
# React Flow examples
|
||||||
|
|
||||||
This Vite app is used internally to test the library.
|
This Vite app is used internally to develop and test the library.
|
||||||
|
|
||||||
## Start local dev server
|
## Start local dev server
|
||||||
|
|
||||||
@@ -8,5 +8,15 @@ This Vite app is used internally to test the library.
|
|||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Adding new example
|
||||||
|
|
||||||
|
Development of the library is done against `src/examples`. Feel free to add new implementations for features that you develop.
|
||||||
|
|
||||||
|
1. Create a new folder & flow at `src/examples/`
|
||||||
|
2. Register the new route in `src/App/routes.ts`
|
||||||
|
|
||||||
|
## Adding new E2E implementation
|
||||||
|
E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under [`http://localhost:3000/tests/generic/$foldername/$filename`](http://localhost:5173/tests/generic/nodes/general). For further documentation visit [`xyflow/tests/playwright`](/tests/playwright).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineConfig } from 'cypress';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: 'http://localhost:3000',
|
baseUrl: 'http://localhost:3000/examples',
|
||||||
viewportWidth: 1280,
|
viewportWidth: 1280,
|
||||||
viewportHeight: 720,
|
viewportHeight: 720,
|
||||||
video: false,
|
video: false,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ describe('useEdges.cy.tsx', () => {
|
|||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get('@onChangeSpy').should('have.been.calledWith', []);
|
|
||||||
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
|
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ const initialNodes: Node[] = nodes.map((n) => ({
|
|||||||
|
|
||||||
const expectedNodes: Node[] = initialNodes.map((n) => ({
|
const expectedNodes: Node[] = initialNodes.map((n) => ({
|
||||||
...n,
|
...n,
|
||||||
positionAbsolute: n.position,
|
measured: {
|
||||||
...nodeDimensions,
|
...nodeDimensions,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('useNodes.cy.tsx', () => {
|
describe('useNodes.cy.tsx', () => {
|
||||||
@@ -29,7 +30,6 @@ describe('useNodes.cy.tsx', () => {
|
|||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get('@onChangeSpy').should('have.been.calledWith', []);
|
|
||||||
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
|
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import * as simpleflow from '../../fixtures/simpleflow';
|
|||||||
describe('<ReactFlow />: Basic Props', () => {
|
describe('<ReactFlow />: Basic Props', () => {
|
||||||
describe('uses defaultNodes and defaultEdges', () => {
|
describe('uses defaultNodes and defaultEdges', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} />);
|
cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} nodeDragThreshold={0} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mounts nodes and edges', () => {
|
it('mounts nodes and edges', () => {
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { OnNodesChange, Panel, ReactFlow, ReactFlowProps, applyNodeChanges, useReactFlow, Node } from '@xyflow/react';
|
||||||
|
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
data: { label: 'Node 1' },
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
style: { width: 100, height: 40 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
data: { label: 'Node 2' },
|
||||||
|
position: { x: 200, y: 200 },
|
||||||
|
style: { width: 80, height: 50 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('<ReactFlow />: onNodesChange', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const onNodesChange = cy.spy().as('onNodesChange');
|
||||||
|
cy.mount(<Comp nodes={nodes} onNodesChange={onNodesChange} />).wait(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receive initial dimension change', () => {
|
||||||
|
const expectedChanges = nodes.map((n) => ({
|
||||||
|
type: 'dimensions',
|
||||||
|
id: n.id,
|
||||||
|
dimensions: n.style,
|
||||||
|
}));
|
||||||
|
|
||||||
|
cy.get('@onNodesChange').should('have.callCount', 1);
|
||||||
|
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receive replace and dimension change', () => {
|
||||||
|
const expectedDimensionChanges = [
|
||||||
|
{
|
||||||
|
type: 'dimensions',
|
||||||
|
id: '1',
|
||||||
|
dimensions: { width: 200, height: 100 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expectedReplaceChanges = [
|
||||||
|
{
|
||||||
|
type: 'replace',
|
||||||
|
id: '1',
|
||||||
|
item: {
|
||||||
|
...nodes[0],
|
||||||
|
measured: { width: 200, height: 100 },
|
||||||
|
style: { width: 200, height: 100 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cy.get('[data-id=update-btn]').click();
|
||||||
|
|
||||||
|
cy.get('@onNodesChange').should('have.callCount', 3);
|
||||||
|
cy.get('@onNodesChange').then((s) => {
|
||||||
|
// dimension change (already checked above)
|
||||||
|
// const firstCall = s.getCall(0);
|
||||||
|
// replace change
|
||||||
|
// @ts-ignore
|
||||||
|
const secondChangeCallArgs = s.getCall(1).args[0];
|
||||||
|
// dimension change
|
||||||
|
// @ts-ignore
|
||||||
|
const thirdChangeCallArgs = s.getCall(2).args[0];
|
||||||
|
|
||||||
|
expect(secondChangeCallArgs).to.be.deep.eq(expectedReplaceChanges);
|
||||||
|
expect(thirdChangeCallArgs).to.be.deep.eq(expectedDimensionChanges);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receive select and unselect change', () => {
|
||||||
|
const expectedSelectChanges = [{ id: '1', type: 'select', selected: true }];
|
||||||
|
const expectedUnselectChanges = [{ id: '1', type: 'select', selected: false }];
|
||||||
|
|
||||||
|
cy.get('.react-flow__node').first().click();
|
||||||
|
cy.get('@onNodesChange').should('have.calledWith', expectedSelectChanges);
|
||||||
|
|
||||||
|
cy.get('.react-flow__pane').first().click({ force: true });
|
||||||
|
cy.get('@onNodesChange').should('have.calledWith', expectedUnselectChanges);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receive position change', () => {
|
||||||
|
const endPosition = { x: 200, y: 25 };
|
||||||
|
const expectedChanges = [
|
||||||
|
{ id: '1', type: 'position', dragging: false, position: endPosition, positionAbsolute: endPosition },
|
||||||
|
];
|
||||||
|
|
||||||
|
cy.drag('.react-flow__node:first', endPosition).then(() => {
|
||||||
|
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receive remove change', () => {
|
||||||
|
const expectedChanges = [{ id: '1', type: 'remove' }];
|
||||||
|
|
||||||
|
cy.get('.react-flow__node').first().click();
|
||||||
|
cy.realPress('Backspace');
|
||||||
|
|
||||||
|
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// test specific helpers
|
||||||
|
|
||||||
|
function UpdateButton() {
|
||||||
|
const { updateNode } = useReactFlow();
|
||||||
|
const updateNodeDimensions = () => {
|
||||||
|
updateNode('1', { style: { width: 200, height: 100 } });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel position="top-right">
|
||||||
|
<button data-id="update-btn" onClick={updateNodeDimensions} id="btn">
|
||||||
|
update
|
||||||
|
</button>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompProps = ReactFlowProps & { onNodesChange: (changes: any) => void };
|
||||||
|
|
||||||
|
function Comp(props: CompProps) {
|
||||||
|
const [nodes, setNodes] = useState(props.nodes || []);
|
||||||
|
const onNodesChange: OnNodesChange<Node> = useCallback((changes) => {
|
||||||
|
props.onNodesChange(changes);
|
||||||
|
setNodes((nds) => applyNodeChanges(changes, nds));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow nodes={nodes} onNodesChange={onNodesChange} nodeDragThreshold={0}>
|
||||||
|
<UpdateButton />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
applyNodeChanges,
|
||||||
|
applyEdgeChanges,
|
||||||
|
type Node,
|
||||||
|
type Edge,
|
||||||
|
NodeChange,
|
||||||
|
EdgeChange,
|
||||||
|
XYPosition,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
const nodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'input',
|
||||||
|
data: { label: 'Node 1' },
|
||||||
|
position: { x: 250, y: 5 },
|
||||||
|
},
|
||||||
|
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||||
|
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||||
|
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const edges: Edge[] = [
|
||||||
|
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||||
|
{ id: 'e1-3', source: '1', target: '3' },
|
||||||
|
{ id: 'e2-3', source: '2', target: '3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('applyChanges Testing', () => {
|
||||||
|
it('adds a node/edge', () => {
|
||||||
|
const currentNodes: Node[] = [];
|
||||||
|
const nodeChanges: NodeChange[] = [{ type: 'add', item: nodes[0] }];
|
||||||
|
const nextNodes = applyNodeChanges(nodeChanges, currentNodes);
|
||||||
|
expect(nextNodes.length).to.be.equal(nodeChanges.length);
|
||||||
|
|
||||||
|
const currentEdges: Edge[] = [];
|
||||||
|
const edgeChanges: EdgeChange[] = [{ type: 'add', item: edges[0] }];
|
||||||
|
const nextEdges = applyEdgeChanges(edgeChanges, currentEdges);
|
||||||
|
|
||||||
|
expect(nextEdges.length).to.be.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds multiple nodes/edges', () => {
|
||||||
|
const currentNodes: Node[] = [];
|
||||||
|
const nodeChanges: NodeChange[] = [
|
||||||
|
{ type: 'add', item: nodes[0] },
|
||||||
|
{ type: 'add', item: nodes[1] },
|
||||||
|
];
|
||||||
|
const nextNodes = applyNodeChanges(nodeChanges, currentNodes);
|
||||||
|
expect(nextNodes.length).to.be.equal(nodeChanges.length);
|
||||||
|
|
||||||
|
const currentEdges: Edge[] = [];
|
||||||
|
const edgeChanges: EdgeChange[] = [
|
||||||
|
{ type: 'add', item: edges[0] },
|
||||||
|
{ type: 'add', item: edges[1] },
|
||||||
|
];
|
||||||
|
const nextEdges = applyEdgeChanges(edgeChanges, currentEdges);
|
||||||
|
expect(nextEdges.length).to.be.equal(edgeChanges.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes a node/edge', () => {
|
||||||
|
const nodesLength = nodes.length;
|
||||||
|
const nodeChanges: NodeChange[] = [{ type: 'remove', id: '1' }];
|
||||||
|
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
||||||
|
expect(nextNodes.length).to.be.equal(nodes.length - nodeChanges.length);
|
||||||
|
expect(nodes.length).to.be.equal(nodesLength);
|
||||||
|
|
||||||
|
const edgesLength = edges.length;
|
||||||
|
const edgeChanges: EdgeChange[] = [{ type: 'remove', id: 'e1-2' }];
|
||||||
|
const nextEdges = applyEdgeChanges(edgeChanges, edges);
|
||||||
|
expect(nextEdges.length).to.be.equal(edges.length - 1);
|
||||||
|
expect(edges.length).to.be.equal(edgesLength);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes multiple node/edge', () => {
|
||||||
|
const nodesLength = nodes.length;
|
||||||
|
const nodeChanges: NodeChange[] = [
|
||||||
|
{ type: 'remove', id: '1' },
|
||||||
|
{ type: 'remove', id: '2' },
|
||||||
|
];
|
||||||
|
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
||||||
|
expect(nextNodes.length).to.be.equal(nodes.length - nodeChanges.length);
|
||||||
|
expect(nodes.length).to.be.equal(nodesLength);
|
||||||
|
|
||||||
|
const edgesLength = edges.length;
|
||||||
|
const edgeChanges: EdgeChange[] = [
|
||||||
|
{ type: 'remove', id: 'e1-2' },
|
||||||
|
{ type: 'remove', id: 'e1-3' },
|
||||||
|
];
|
||||||
|
const nextEdges = applyEdgeChanges(edgeChanges, edges);
|
||||||
|
expect(nextEdges.length).to.be.equal(edges.length - edgeChanges.length);
|
||||||
|
expect(edges.length).to.be.equal(edgesLength);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects a node/edge', () => {
|
||||||
|
const nodeChanges: NodeChange[] = [{ type: 'select', id: '1', selected: true }];
|
||||||
|
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
||||||
|
expect(nextNodes[0].selected).to.be.true;
|
||||||
|
|
||||||
|
const edgeChanges: EdgeChange[] = [{ type: 'select', id: 'e1-2', selected: true }];
|
||||||
|
const nextEdges = applyEdgeChanges(edgeChanges, edges);
|
||||||
|
expect(nextEdges[0].selected).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deselects a node/edge', () => {
|
||||||
|
const nodeChanges: NodeChange[] = [{ type: 'select', id: '1', selected: false }];
|
||||||
|
const nextNodes = applyNodeChanges(
|
||||||
|
nodeChanges,
|
||||||
|
nodes.map((n) => ({ ...n, selected: true }))
|
||||||
|
);
|
||||||
|
expect(nextNodes[0].selected).to.be.false;
|
||||||
|
expect(nextNodes[1].selected).to.be.true;
|
||||||
|
|
||||||
|
const edgeChanges: EdgeChange[] = [{ type: 'select', id: 'e1-2', selected: false }];
|
||||||
|
const nextEdges = applyEdgeChanges(
|
||||||
|
edgeChanges,
|
||||||
|
edges.map((e) => ({ ...e, selected: true }))
|
||||||
|
);
|
||||||
|
expect(nextEdges[0].selected).to.be.false;
|
||||||
|
expect(nextEdges[1].selected).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates node position', () => {
|
||||||
|
const newPosition: XYPosition = { x: 100, y: 100 };
|
||||||
|
const nodeChanges: NodeChange[] = [{ type: 'position', id: '1', position: newPosition }];
|
||||||
|
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
||||||
|
|
||||||
|
expect(nextNodes[0].position).to.be.deep.equal(newPosition);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates node dimensions', () => {
|
||||||
|
const newWidth = 200;
|
||||||
|
const newHeight = 200;
|
||||||
|
const nodeChanges: NodeChange[] = [
|
||||||
|
{ type: 'dimensions', id: '1', dimensions: { width: newWidth, height: newHeight } },
|
||||||
|
];
|
||||||
|
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
||||||
|
|
||||||
|
expect(nodes[0].measured).to.be.undefined;
|
||||||
|
expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
|
||||||
|
expect(nextNodes[0].width).to.be.undefined;
|
||||||
|
expect(nextNodes[0].height).to.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates node position and dimensions', () => {
|
||||||
|
const newPosition: XYPosition = { x: 100, y: 100 };
|
||||||
|
const newWidth = 200;
|
||||||
|
const newHeight = 200;
|
||||||
|
const nodeChanges: NodeChange[] = [
|
||||||
|
{ type: 'position', id: '1', position: newPosition },
|
||||||
|
{ type: 'dimensions', id: '1', dimensions: { width: newWidth, height: newHeight } },
|
||||||
|
];
|
||||||
|
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
||||||
|
|
||||||
|
expect(nextNodes[0].position).to.be.deep.equal(newPosition);
|
||||||
|
expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces nodes/edges', () => {
|
||||||
|
const nodesLength = nodes.length;
|
||||||
|
const nodeChanges: NodeChange[] = [{ type: 'replace', id: nodes[0].id, item: nodes[1] }];
|
||||||
|
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
||||||
|
|
||||||
|
expect(nodes.length).length.to.be.equal(nodesLength);
|
||||||
|
expect(nextNodes.length).to.be.equal(nodesLength);
|
||||||
|
expect(nextNodes[0]).to.be.deep.equal(nodes[1]);
|
||||||
|
|
||||||
|
const edgesLength = edges.length;
|
||||||
|
const edgeChange: EdgeChange[] = [{ type: 'replace', id: edges[0].id, item: edges[1] }];
|
||||||
|
const nextEdges = applyEdgeChanges(edgeChange, edges);
|
||||||
|
|
||||||
|
expect(edges.length).length.to.be.equal(edgesLength);
|
||||||
|
expect(nextEdges.length).to.be.equal(edgesLength);
|
||||||
|
expect(nextEdges[0]).to.be.deep.equal(edges[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -49,21 +49,16 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
|
|||||||
|
|
||||||
it('selects one node with a selection', () => {
|
it('selects one node with a selection', () => {
|
||||||
cy.get('body')
|
cy.get('body')
|
||||||
.type('{shift}', { release: false })
|
.type('{Shift}', { release: false })
|
||||||
.wait(50)
|
|
||||||
.get('.react-flow__pane')
|
.get('.react-flow__pane')
|
||||||
.trigger('mousedown', 1, 10, { button: 0, force: true })
|
.trigger('mousedown', 1, 10, { button: 0, force: true })
|
||||||
.trigger('mousemove', 1000, 450, { button: 0 })
|
.trigger('mousemove', 1000, 200, { button: 0 })
|
||||||
.wait(50)
|
.trigger('mouseup', 1000, 200, { button: 0 });
|
||||||
.trigger('mouseup', 1000, 450, { button: 0, force: true });
|
|
||||||
|
|
||||||
cy.wait(200);
|
cy.get('.react-flow__node').eq(0).should('have.class', 'selected');
|
||||||
|
|
||||||
cy.get('.react-flow__node').eq(1).should('have.class', 'selected');
|
|
||||||
cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected');
|
cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected');
|
||||||
|
|
||||||
cy.get('.react-flow__nodesselection-rect');
|
cy.get('.react-flow__nodesselection-rect');
|
||||||
|
|
||||||
cy.get('body').type('{shift}', { release: true, force: true });
|
cy.get('body').type('{shift}', { release: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,19 +92,19 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
|
|||||||
it('drags a node', () => {
|
it('drags a node', () => {
|
||||||
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
|
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
|
||||||
|
|
||||||
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el: any) => {
|
cy.drag('.react-flow__node:first', { x: 10, y: 10 }).then(($el: any) => {
|
||||||
const styleAfterDrag = $el.css('transform');
|
const styleAfterDrag = $el.css('transform');
|
||||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// @TODO: why does this fail since react18?
|
it('removes a node', () => {
|
||||||
// it('removes a node', () => {
|
cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
|
||||||
// cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
|
cy.get('html').realPress('Backspace');
|
||||||
// cy.get('html').type('{backspace}').wait(100);
|
|
||||||
// cy.get('.react-flow__node').should('have.length', 3);
|
cy.get('.react-flow__node').should('have.length', 3);
|
||||||
// cy.get('.react-flow__edge').should('have.length', 1);
|
cy.get('.react-flow__edge').should('have.length', 0);
|
||||||
// });
|
});
|
||||||
|
|
||||||
it('connects nodes', () => {
|
it('connects nodes', () => {
|
||||||
cy.get('.react-flow__node')
|
cy.get('.react-flow__node')
|
||||||
@@ -125,16 +120,15 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
|
|||||||
.trigger('mouseup', { force: true, button: 0 });
|
.trigger('mouseup', { force: true, button: 0 });
|
||||||
|
|
||||||
cy.get('.react-flow__edge').as('edge');
|
cy.get('.react-flow__edge').as('edge');
|
||||||
cy.get('@edge').should('have.length', 3);
|
cy.get('@edge').should('have.length', 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// @TODO: why does this fail since react18?
|
it('removes an edge', () => {
|
||||||
// it('removes an edge', () => {
|
cy.get('.react-flow__edge:first').click();
|
||||||
// cy.get('.react-flow__edge:first').click();
|
cy.get('html').realPress('Backspace');
|
||||||
// cy.get('body').type('{backspace}');
|
|
||||||
|
|
||||||
// cy.get('.react-flow__edge').should('have.length', 1);
|
cy.get('.react-flow__edge').should('have.length', 0);
|
||||||
// });
|
});
|
||||||
|
|
||||||
it('drags the pane', () => {
|
it('drags the pane', () => {
|
||||||
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
|
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function ControlledFlow({
|
|||||||
handlers.onConnect = onConnect;
|
handlers.onConnect = onConnect;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} />;
|
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} nodeDragThreshold={0} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ControlledFlow;
|
export default ControlledFlow;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Cypress.Commands.add('drag', (selector, { x, y }) =>
|
|||||||
const nextY: number = centerY + y;
|
const nextY: number = centerY + y;
|
||||||
|
|
||||||
return elementToDrag
|
return elementToDrag
|
||||||
.trigger('mousedown', { view: window })
|
.trigger('mousedown', { view: window, force: true })
|
||||||
.trigger('mousemove', nextX, nextY, { force: true })
|
.trigger('mousemove', nextX, nextY, { force: true })
|
||||||
.wait(50)
|
.wait(50)
|
||||||
.trigger('mouseup', { view: window, force: true });
|
.trigger('mouseup', { view: window, force: true });
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import './commands';
|
import './commands';
|
||||||
|
import 'cypress-real-events/support';
|
||||||
|
|
||||||
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
|
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>React Flow Examples</title>
|
<title>React Flow Examples</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+17
-14
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/react-examples",
|
"name": "react-examples",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -14,26 +14,29 @@
|
|||||||
"test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'"
|
"test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
"@xyflow/react": "workspace:*",
|
"@xyflow/react": "workspace:*",
|
||||||
"classcat": "^5.0.3",
|
"classcat": "^5.0.4",
|
||||||
"dagre": "^0.8.5",
|
"dagre": "^0.8.5",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-redux": "^9.1.1",
|
||||||
"zustand": "^4.4.1"
|
"react-router-dom": "^6.18.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"zustand": "^4.4.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/skip-test": "^2.6.1",
|
"@cypress/skip-test": "^2.6.1",
|
||||||
"@types/dagre": "^0.7.48",
|
"@types/dagre": "^0.7.52",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.2.36",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.2.14",
|
||||||
"@vitejs/plugin-react": "4.0.4",
|
"@vitejs/plugin-react": "4.1.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
"@vitejs/plugin-react-swc": "^3.4.1",
|
||||||
"cypress": "12.17.3",
|
"cypress": "13.6.6",
|
||||||
"cypress-real-events": "1.10.0",
|
"cypress-real-events": "1.12.0",
|
||||||
"start-server-and-test": "^1.14.0",
|
"start-server-and-test": "^2.0.2",
|
||||||
"typescript": "5.1.3",
|
"typescript": "5.2.2",
|
||||||
"vite": "4.4.9"
|
"vite": "4.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import routes from './routes';
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [currentPath, setCurrentPath] = useState(location.pathname);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const name = routes.find((route) => route.path === currentPath)?.name;
|
||||||
|
document.title = `React Flow Examples${name ? ' - ' + name : ''}`;
|
||||||
|
navigate(currentPath);
|
||||||
|
}, [currentPath]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header>
|
||||||
|
<a className="logo" href="https://github.com/xyflow/xyflow">
|
||||||
|
React Flow Dev
|
||||||
|
</a>
|
||||||
|
<select value={currentPath} onChange={(event) => setCurrentPath(event.target.value)}>
|
||||||
|
{routes.map((route) => (
|
||||||
|
<option value={route.path} key={route.path}>
|
||||||
|
{route.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</header>
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,326 +1,21 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
|
import routes from './routes';
|
||||||
|
import GenericTestRoutes from '../generic-tests';
|
||||||
import Basic from '../examples/Basic';
|
import Basic from '../examples/Basic';
|
||||||
import Backgrounds from '../examples/Backgrounds';
|
import Header from './header';
|
||||||
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
|
|
||||||
import ControlledViewport from '../examples/ControlledViewport';
|
|
||||||
import CustomConnectionLine from '../examples/CustomConnectionLine';
|
|
||||||
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
|
|
||||||
import CustomNode from '../examples/CustomNode';
|
|
||||||
import DefaultNodes from '../examples/DefaultNodes';
|
|
||||||
import DragHandle from '../examples/DragHandle';
|
|
||||||
import DragNDrop from '../examples/DragNDrop';
|
|
||||||
import EasyConnect from '../examples/EasyConnect';
|
|
||||||
import Edges from '../examples/Edges';
|
|
||||||
import EdgeRenderer from '../examples/EdgeRenderer';
|
|
||||||
import EdgeTypes from '../examples/EdgeTypes';
|
|
||||||
import Empty from '../examples/Empty';
|
|
||||||
import Figma from '../examples/Figma';
|
|
||||||
import FloatingEdges from '../examples/FloatingEdges';
|
|
||||||
import Hidden from '../examples/Hidden';
|
|
||||||
import Interaction from '../examples/Interaction';
|
|
||||||
import Intersection from '../examples/Intersection';
|
|
||||||
import Layouting from '../examples/Layouting';
|
|
||||||
import MultiFlows from '../examples/MultiFlows';
|
|
||||||
import NestedNodes from '../examples/NestedNodes';
|
|
||||||
import NodeResizer from '../examples/NodeResizer';
|
|
||||||
import NodeTypeChange from '../examples/NodeTypeChange';
|
|
||||||
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
|
|
||||||
import Overview from '../examples/Overview';
|
|
||||||
import Provider from '../examples/Provider';
|
|
||||||
import SaveRestore from '../examples/SaveRestore';
|
|
||||||
import Stress from '../examples/Stress';
|
|
||||||
import Subflow from '../examples/Subflow';
|
|
||||||
import SwitchFlow from '../examples/Switch';
|
|
||||||
import TouchDevice from '../examples/TouchDevice';
|
|
||||||
import Undirectional from '../examples/Undirectional';
|
|
||||||
import UpdatableEdge from '../examples/UpdatableEdge';
|
|
||||||
import UpdateNode from '../examples/UpdateNode';
|
|
||||||
import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals';
|
|
||||||
import UseReactFlow from '../examples/UseReactFlow';
|
|
||||||
import Validation from '../examples/Validation';
|
|
||||||
import UseKeyPress from '../examples/UseKeyPress';
|
|
||||||
import EdgeRouting from '../examples/EdgeRouting';
|
|
||||||
import CancelConnection from '../examples/CancelConnection';
|
|
||||||
import InteractiveMinimap from '../examples/InteractiveMinimap';
|
|
||||||
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
|
|
||||||
import NodeToolbar from '../examples/NodeToolbar';
|
|
||||||
import useNodesInitialized from '../examples/UseNodesInit';
|
|
||||||
|
|
||||||
interface IRoute {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
component: React.ComponentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes: IRoute[] = [
|
|
||||||
{
|
|
||||||
name: 'Basic',
|
|
||||||
path: '/',
|
|
||||||
component: Basic,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Backgrounds',
|
|
||||||
path: '/backgrounds',
|
|
||||||
component: Backgrounds,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Cancel Connection',
|
|
||||||
path: '/cancel-connection',
|
|
||||||
component: CancelConnection,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Controlled/Uncontrolled',
|
|
||||||
path: '/controlled-uncontrolled',
|
|
||||||
component: ControlledUncontrolled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Controlled Viewport',
|
|
||||||
path: '/controlled-viewport',
|
|
||||||
component: ControlledViewport,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Custom Connection Line',
|
|
||||||
path: '/custom-connectionline',
|
|
||||||
component: CustomConnectionLine,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Custom Minimap Node',
|
|
||||||
path: '/custom-minimap-node',
|
|
||||||
component: CustomMiniMapNode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Custom Node',
|
|
||||||
path: '/custom-node',
|
|
||||||
component: CustomNode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Default Nodes',
|
|
||||||
path: '/default-nodes',
|
|
||||||
component: DefaultNodes,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Drag Handle',
|
|
||||||
path: '/draghandle',
|
|
||||||
component: DragHandle,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Drag and Drop',
|
|
||||||
path: '/dragndrop',
|
|
||||||
component: DragNDrop,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'EasyConnect',
|
|
||||||
path: '/easy-connect',
|
|
||||||
component: EasyConnect,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Edges',
|
|
||||||
path: '/edges',
|
|
||||||
component: Edges,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Edge Renderer',
|
|
||||||
path: '/edge-renderer',
|
|
||||||
component: EdgeRenderer,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Edge Types',
|
|
||||||
path: '/edge-types',
|
|
||||||
component: EdgeTypes,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Edge Routing',
|
|
||||||
path: '/edge-routing',
|
|
||||||
component: EdgeRouting,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Empty',
|
|
||||||
path: '/empty',
|
|
||||||
component: Empty,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Figma',
|
|
||||||
path: '/figma',
|
|
||||||
component: Figma,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Floating Edges',
|
|
||||||
path: '/floating-edges',
|
|
||||||
component: FloatingEdges,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Hidden',
|
|
||||||
path: '/hidden',
|
|
||||||
component: Hidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Interaction',
|
|
||||||
path: '/interaction',
|
|
||||||
component: Interaction,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Intersection',
|
|
||||||
path: '/intersection',
|
|
||||||
component: Intersection,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Interactive Minimap',
|
|
||||||
path: '/interactive-minimap',
|
|
||||||
component: InteractiveMinimap,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Layouting',
|
|
||||||
path: '/layouting',
|
|
||||||
component: Layouting,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Multi Flows',
|
|
||||||
path: '/multiflows',
|
|
||||||
component: MultiFlows,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Nested Nodes',
|
|
||||||
path: '/nested-nodes',
|
|
||||||
component: NestedNodes,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Node Type Change',
|
|
||||||
path: '/nodetype-change',
|
|
||||||
component: NodeTypeChange,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'nodeTypes Object Change',
|
|
||||||
path: '/nodetypesobject-change',
|
|
||||||
component: NodeTypesObjectChange,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'NodeToolbar',
|
|
||||||
path: '/node-toolbar',
|
|
||||||
component: NodeToolbar,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'NodeResizer',
|
|
||||||
path: '/node-resizer',
|
|
||||||
component: NodeResizer,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Overview',
|
|
||||||
path: '/overview',
|
|
||||||
component: Overview,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Provider',
|
|
||||||
path: '/provider',
|
|
||||||
component: Provider,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Save/Restore',
|
|
||||||
path: '/save-restore',
|
|
||||||
component: SaveRestore,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Stress',
|
|
||||||
path: '/stress',
|
|
||||||
component: Stress,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Subflow',
|
|
||||||
path: '/subflow',
|
|
||||||
component: Subflow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Switch Flow',
|
|
||||||
path: '/switch',
|
|
||||||
component: SwitchFlow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Touch Device',
|
|
||||||
path: '/touch-device',
|
|
||||||
component: TouchDevice,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Undirectional',
|
|
||||||
path: '/undirectional',
|
|
||||||
component: Undirectional,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Updatable Edge',
|
|
||||||
path: '/updatable-edge',
|
|
||||||
component: UpdatableEdge,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Update Node',
|
|
||||||
path: '/update-node',
|
|
||||||
component: UpdateNode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'useNodesInitialized',
|
|
||||||
path: '/use-nodes-initialized',
|
|
||||||
component: useNodesInitialized,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'useOnSelectionChange',
|
|
||||||
path: '/use-on-selection-change',
|
|
||||||
component: UseOnSelectionChange,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'useReactFlow',
|
|
||||||
path: '/usereactflow',
|
|
||||||
component: UseReactFlow,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'useUpdateNodeInternals',
|
|
||||||
path: '/useupdatenodeinternals',
|
|
||||||
component: UseUpdateNodeInternals,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Validation',
|
|
||||||
path: '/validation',
|
|
||||||
component: Validation,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'useKeyPress',
|
|
||||||
path: '/use-key-press',
|
|
||||||
component: UseKeyPress,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const Header = () => {
|
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [currentPath, setCurrentPath] = useState(location.pathname);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const name = routes.find((route) => route.path === currentPath)?.name;
|
|
||||||
document.title = `React Flow Examples${name ? ' - ' + name : ''}`;
|
|
||||||
navigate(currentPath);
|
|
||||||
}, [currentPath]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
<a className="logo" href="https://github.com/wbkd/react-flow">
|
|
||||||
React Flow Dev
|
|
||||||
</a>
|
|
||||||
<select value={currentPath} onChange={(event) => setCurrentPath(event.target.value)}>
|
|
||||||
{routes.map((route) => (
|
|
||||||
<option value={route.path} key={route.path}>
|
|
||||||
{route.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Header />
|
|
||||||
<Routes>
|
<Routes>
|
||||||
{routes.map((route) => (
|
<Route path="/" element={<Navigate to="/examples" />} />
|
||||||
<Route path={route.path} key={route.path} element={<route.component />} />
|
<Route path="examples" element={<Header />}>
|
||||||
))}
|
<Route index element={<Basic />} />
|
||||||
|
{routes.map((route) => (
|
||||||
|
<Route path={route.path} key={route.path} element={<route.component />} />
|
||||||
|
))}
|
||||||
|
</Route>
|
||||||
|
<Route path="tests/generic/*" element={<GenericTestRoutes />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
import Basic from '../examples/Basic';
|
||||||
|
import Backgrounds from '../examples/Backgrounds';
|
||||||
|
import ColorMode from '../examples/ColorMode';
|
||||||
|
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
|
||||||
|
import ControlledViewport from '../examples/ControlledViewport';
|
||||||
|
import CustomConnectionLine from '../examples/CustomConnectionLine';
|
||||||
|
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
|
||||||
|
import CustomNode from '../examples/CustomNode';
|
||||||
|
import DefaultNodes from '../examples/DefaultNodes';
|
||||||
|
import DragHandle from '../examples/DragHandle';
|
||||||
|
import DragNDrop from '../examples/DragNDrop';
|
||||||
|
import EasyConnect from '../examples/EasyConnect';
|
||||||
|
import Edges from '../examples/Edges';
|
||||||
|
import EdgeRenderer from '../examples/EdgeRenderer';
|
||||||
|
import EdgeTypes from '../examples/EdgeTypes';
|
||||||
|
import Empty from '../examples/Empty';
|
||||||
|
import Figma from '../examples/Figma';
|
||||||
|
import FloatingEdges from '../examples/FloatingEdges';
|
||||||
|
import Hidden from '../examples/Hidden';
|
||||||
|
import Interaction from '../examples/Interaction';
|
||||||
|
import Intersection from '../examples/Intersection';
|
||||||
|
import Layouting from '../examples/Layouting';
|
||||||
|
import MultiFlows from '../examples/MultiFlows';
|
||||||
|
import MultiSetNodes from '../examples/MultiSetNodes';
|
||||||
|
import NodeResizer from '../examples/NodeResizer';
|
||||||
|
import NodeTypeChange from '../examples/NodeTypeChange';
|
||||||
|
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
|
||||||
|
import Overview from '../examples/Overview';
|
||||||
|
import Provider from '../examples/Provider';
|
||||||
|
import SaveRestore from '../examples/SaveRestore';
|
||||||
|
import SetNodesBatching from '../examples/SetNodesBatching';
|
||||||
|
import Stress from '../examples/Stress';
|
||||||
|
import Subflow from '../examples/Subflow';
|
||||||
|
import SwitchFlow from '../examples/Switch';
|
||||||
|
import TouchDevice from '../examples/TouchDevice';
|
||||||
|
import Undirectional from '../examples/Undirectional';
|
||||||
|
import ReconnectEdge from '../examples/ReconnectEdge';
|
||||||
|
import UpdateNode from '../examples/UpdateNode';
|
||||||
|
import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals';
|
||||||
|
import UseReactFlow from '../examples/UseReactFlow';
|
||||||
|
import Validation from '../examples/Validation';
|
||||||
|
import UseKeyPress from '../examples/UseKeyPress';
|
||||||
|
import EdgeRouting from '../examples/EdgeRouting';
|
||||||
|
import CancelConnection from '../examples/CancelConnection';
|
||||||
|
import InteractiveMinimap from '../examples/InteractiveMinimap';
|
||||||
|
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
|
||||||
|
import NodeToolbar from '../examples/NodeToolbar';
|
||||||
|
import UseConnection from '../examples/UseConnection';
|
||||||
|
import UseNodesInitialized from '../examples/UseNodesInit';
|
||||||
|
import UseNodesData from '../examples/UseNodesData';
|
||||||
|
import UseHandleConnections from '../examples/UseHandleConnections';
|
||||||
|
import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop';
|
||||||
|
import DevTools from '../examples/DevTools';
|
||||||
|
import Redux from '../examples/Redux';
|
||||||
|
|
||||||
|
export interface IRoute {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
component: React.ComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes: IRoute[] = [
|
||||||
|
{
|
||||||
|
name: 'Add Node on edge Drop',
|
||||||
|
path: 'add-node-edge-drop',
|
||||||
|
component: AddNodeOnEdgeDrop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Basic',
|
||||||
|
path: 'basic',
|
||||||
|
component: Basic,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Backgrounds',
|
||||||
|
path: 'backgrounds',
|
||||||
|
component: Backgrounds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Color Mode',
|
||||||
|
path: 'color-mode',
|
||||||
|
component: ColorMode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel Connection',
|
||||||
|
path: 'cancel-connection',
|
||||||
|
component: CancelConnection,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Controlled/Uncontrolled',
|
||||||
|
path: 'controlled-uncontrolled',
|
||||||
|
component: ControlledUncontrolled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Controlled Viewport',
|
||||||
|
path: 'controlled-viewport',
|
||||||
|
component: ControlledViewport,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Custom Connection Line',
|
||||||
|
path: 'custom-connectionline',
|
||||||
|
component: CustomConnectionLine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Custom Minimap Node',
|
||||||
|
path: 'custom-minimap-node',
|
||||||
|
component: CustomMiniMapNode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Custom Node',
|
||||||
|
path: 'custom-node',
|
||||||
|
component: CustomNode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Default Nodes',
|
||||||
|
path: 'default-nodes',
|
||||||
|
component: DefaultNodes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DevTools',
|
||||||
|
path: 'devtools',
|
||||||
|
component: DevTools,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Drag Handle',
|
||||||
|
path: 'draghandle',
|
||||||
|
component: DragHandle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Drag and Drop',
|
||||||
|
path: 'dragndrop',
|
||||||
|
component: DragNDrop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EasyConnect',
|
||||||
|
path: 'easy-connect',
|
||||||
|
component: EasyConnect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Edges',
|
||||||
|
path: 'edges',
|
||||||
|
component: Edges,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Edge Renderer',
|
||||||
|
path: 'edge-renderer',
|
||||||
|
component: EdgeRenderer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Edge Types',
|
||||||
|
path: 'edge-types',
|
||||||
|
component: EdgeTypes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Edge Routing',
|
||||||
|
path: 'edge-routing',
|
||||||
|
component: EdgeRouting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Empty',
|
||||||
|
path: 'empty',
|
||||||
|
component: Empty,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Figma',
|
||||||
|
path: 'figma',
|
||||||
|
component: Figma,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Floating Edges',
|
||||||
|
path: 'floating-edges',
|
||||||
|
component: FloatingEdges,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Hidden',
|
||||||
|
path: 'hidden',
|
||||||
|
component: Hidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Interaction',
|
||||||
|
path: 'interaction',
|
||||||
|
component: Interaction,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Intersection',
|
||||||
|
path: 'intersection',
|
||||||
|
component: Intersection,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Interactive Minimap',
|
||||||
|
path: 'interactive-minimap',
|
||||||
|
component: InteractiveMinimap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Layouting',
|
||||||
|
path: 'layouting',
|
||||||
|
component: Layouting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Multi setNodes',
|
||||||
|
path: 'multi-setnodes',
|
||||||
|
component: MultiSetNodes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Multi Flows',
|
||||||
|
path: 'multiflows',
|
||||||
|
component: MultiFlows,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Node Type Change',
|
||||||
|
path: 'nodetype-change',
|
||||||
|
component: NodeTypeChange,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nodeTypes Object Change',
|
||||||
|
path: 'nodetypesobject-change',
|
||||||
|
component: NodeTypesObjectChange,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'NodeToolbar',
|
||||||
|
path: 'node-toolbar',
|
||||||
|
component: NodeToolbar,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'NodeResizer',
|
||||||
|
path: 'node-resizer',
|
||||||
|
component: NodeResizer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Overview',
|
||||||
|
path: 'overview',
|
||||||
|
component: Overview,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Provider',
|
||||||
|
path: 'provider',
|
||||||
|
component: Provider,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Save/Restore',
|
||||||
|
path: 'save-restore',
|
||||||
|
component: SaveRestore,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SetNodes Batching',
|
||||||
|
path: 'setnodes-batching',
|
||||||
|
component: SetNodesBatching,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Stress',
|
||||||
|
path: 'stress',
|
||||||
|
component: Stress,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Subflow',
|
||||||
|
path: 'subflow',
|
||||||
|
component: Subflow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Switch Flow',
|
||||||
|
path: 'switch',
|
||||||
|
component: SwitchFlow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Touch Device',
|
||||||
|
path: 'touch-device',
|
||||||
|
component: TouchDevice,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Undirectional',
|
||||||
|
path: 'undirectional',
|
||||||
|
component: Undirectional,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Reconnect Edge',
|
||||||
|
path: 'reconnect-edge',
|
||||||
|
component: ReconnectEdge,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Update Node',
|
||||||
|
path: 'update-node',
|
||||||
|
component: UpdateNode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useConnection',
|
||||||
|
path: 'use-connection',
|
||||||
|
component: UseConnection,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useNodesInitialized',
|
||||||
|
path: 'use-nodes-initialized',
|
||||||
|
component: UseNodesInitialized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useOnSelectionChange',
|
||||||
|
path: 'use-on-selection-change',
|
||||||
|
component: UseOnSelectionChange,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useReactFlow',
|
||||||
|
path: 'usereactflow',
|
||||||
|
component: UseReactFlow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useHandleConnections',
|
||||||
|
path: 'usehandleconnections',
|
||||||
|
component: UseHandleConnections,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useNodesData',
|
||||||
|
path: 'usenodesdata',
|
||||||
|
component: UseNodesData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useUpdateNodeInternals',
|
||||||
|
path: 'useupdatenodeinternals',
|
||||||
|
component: UseUpdateNodeInternals,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'redux',
|
||||||
|
path: 'redux',
|
||||||
|
component: Redux,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Validation',
|
||||||
|
path: 'validation',
|
||||||
|
component: Validation,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useKeyPress',
|
||||||
|
path: 'use-key-press',
|
||||||
|
component: UseKeyPress,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { BackgroundProps, ControlProps, Edge, MiniMapProps, Node, PanelProps, ReactFlowProps } from '@xyflow/react';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface FlowConfig {
|
||||||
|
flowProps?: Omit<ReactFlowProps, 'nodes' | 'edges'> & { nodes: Node[]; edges: Edge[] };
|
||||||
|
panelProps?: PanelProps;
|
||||||
|
backgroundProps?: BackgroundProps;
|
||||||
|
controlsProps?: ControlProps;
|
||||||
|
minimapProps?: MiniMapProps;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
addEdge,
|
||||||
|
useReactFlow,
|
||||||
|
ReactFlowProvider,
|
||||||
|
OnConnect,
|
||||||
|
OnConnectStart,
|
||||||
|
OnConnectEnd,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
const initialNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '0',
|
||||||
|
type: 'input',
|
||||||
|
data: { label: 'Node' },
|
||||||
|
position: { x: 0, y: 50 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialEdges: Edge[] = [];
|
||||||
|
|
||||||
|
let id = 1;
|
||||||
|
const getId = () => `${id++}`;
|
||||||
|
|
||||||
|
const AddNodeOnEdgeDrop = () => {
|
||||||
|
const reactFlowWrapper = useRef(null);
|
||||||
|
const connectingNodeId = useRef<string | null>(null);
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
|
const onConnect: OnConnect = useCallback((params) => {
|
||||||
|
// reset the start node on connections
|
||||||
|
connectingNodeId.current = null;
|
||||||
|
setEdges((eds) => addEdge(params, eds));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
|
||||||
|
connectingNodeId.current = nodeId;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onConnectEnd: OnConnectEnd = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (!connectingNodeId.current) return;
|
||||||
|
|
||||||
|
const targetIsPane = (event.target as HTMLDivElement)?.classList.contains('react-flow__pane');
|
||||||
|
|
||||||
|
if (targetIsPane && 'clientX' in event && 'clientY' in event) {
|
||||||
|
// we need to remove the wrapper bounds, in order to get the correct position
|
||||||
|
const id = getId();
|
||||||
|
const newNode: Node = {
|
||||||
|
id,
|
||||||
|
position: screenToFlowPosition({
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
}),
|
||||||
|
data: { label: `Node ${id}` },
|
||||||
|
origin: [0.5, 0.0],
|
||||||
|
};
|
||||||
|
|
||||||
|
const newEdge: Edge = {
|
||||||
|
id,
|
||||||
|
source: connectingNodeId.current,
|
||||||
|
target: id,
|
||||||
|
};
|
||||||
|
|
||||||
|
setNodes((nds) => nds.concat(newNode));
|
||||||
|
setEdges((eds) => eds.concat(newEdge));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[screenToFlowPosition]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wrapper" ref={reactFlowWrapper} style={{ height: '100%' }}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
onConnectStart={onConnectStart}
|
||||||
|
onConnectEnd={onConnectEnd}
|
||||||
|
fitView
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<AddNodeOnEdgeDrop />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MouseEvent } from 'react';
|
import { MouseEvent, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
@@ -10,12 +10,16 @@ import {
|
|||||||
Edge,
|
Edge,
|
||||||
useReactFlow,
|
useReactFlow,
|
||||||
Panel,
|
Panel,
|
||||||
|
OnNodeDrag,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
|
const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
|
||||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
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 onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||||
|
|
||||||
|
const printSelectionEvent = (name: string) => (_: MouseEvent, nodes: Node[]) => console.log(name, nodes);
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -52,41 +56,80 @@ const initialEdges: Edge[] = [
|
|||||||
const defaultEdgeOptions = {};
|
const defaultEdgeOptions = {};
|
||||||
|
|
||||||
const BasicFlow = () => {
|
const BasicFlow = () => {
|
||||||
const instance = useReactFlow();
|
const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
|
||||||
|
useReactFlow();
|
||||||
|
|
||||||
const updatePos = () => {
|
const updatePos = () => {
|
||||||
instance.setNodes((nodes) =>
|
setNodes((nodes) =>
|
||||||
nodes.map((node) => {
|
nodes.map((node) => {
|
||||||
node.position = {
|
return {
|
||||||
x: Math.random() * 400,
|
...node,
|
||||||
y: Math.random() * 400,
|
position: {
|
||||||
|
x: Math.random() * 400,
|
||||||
|
y: Math.random() * 400,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return node;
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logToObject = () => console.log(instance.toObject());
|
const logToObject = () => console.log(toObject());
|
||||||
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
|
const resetTransform = () => setViewport({ x: 0, y: 0, zoom: 1 });
|
||||||
|
|
||||||
const toggleClassnames = () => {
|
const toggleClassnames = () => {
|
||||||
instance.setNodes((nodes) =>
|
setNodes((nodes) =>
|
||||||
nodes.map((node) => {
|
nodes.map((node) => {
|
||||||
node.className = node.className === 'light' ? 'dark' : 'light';
|
return {
|
||||||
|
...node,
|
||||||
return node;
|
className: node.className === 'light' ? 'dark' : 'light',
|
||||||
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteSelectedElements = useCallback(() => {
|
||||||
|
const selectedNodes = getNodes().filter((node) => node.selected);
|
||||||
|
const selectedEdges = getEdges().filter((edge) => edge.selected);
|
||||||
|
deleteElements({ nodes: selectedNodes, edges: selectedEdges });
|
||||||
|
}, [deleteElements]);
|
||||||
|
|
||||||
|
const deleteSomeElements = useCallback(() => {
|
||||||
|
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSetNodes = () => {
|
||||||
|
setNodes([
|
||||||
|
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node a' } },
|
||||||
|
{ id: 'b', position: { x: 0, y: 150 }, data: { label: 'Node b' } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateNode = () => {
|
||||||
|
updateNodeData('1', { label: 'update' });
|
||||||
|
updateNodeData('2', { label: 'update' });
|
||||||
|
};
|
||||||
|
const addNode = () => {
|
||||||
|
addNodes({
|
||||||
|
id: `${Math.random()}`,
|
||||||
|
data: { label: 'Node' },
|
||||||
|
position: { x: Math.random() * 300, y: Math.random() * 300 },
|
||||||
|
className: 'light',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
defaultNodes={initialNodes}
|
defaultNodes={initialNodes}
|
||||||
defaultEdges={initialEdges}
|
defaultEdges={initialEdges}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onNodeDragStop={onNodeDragStop}
|
onNodeDragStop={onNodeDragStop}
|
||||||
|
onNodeDragStart={onNodeDragStart}
|
||||||
onNodeDrag={onNodeDrag}
|
onNodeDrag={onNodeDrag}
|
||||||
|
onSelectionDragStart={printSelectionEvent('selection drag start')}
|
||||||
|
onSelectionDrag={printSelectionEvent('selection drag')}
|
||||||
|
onSelectionDragStop={printSelectionEvent('selection drag stop')}
|
||||||
className="react-flow-basic-example"
|
className="react-flow-basic-example"
|
||||||
minZoom={0.2}
|
minZoom={0.2}
|
||||||
maxZoom={4}
|
maxZoom={4}
|
||||||
@@ -95,6 +138,7 @@ const BasicFlow = () => {
|
|||||||
selectNodesOnDrag={false}
|
selectNodesOnDrag={false}
|
||||||
elevateEdgesOnSelect
|
elevateEdgesOnSelect
|
||||||
elevateNodesOnSelect={false}
|
elevateNodesOnSelect={false}
|
||||||
|
nodeDragThreshold={0}
|
||||||
>
|
>
|
||||||
<Background variant={BackgroundVariant.Dots} />
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
@@ -105,6 +149,12 @@ const BasicFlow = () => {
|
|||||||
<button onClick={updatePos}>change pos</button>
|
<button onClick={updatePos}>change pos</button>
|
||||||
<button onClick={toggleClassnames}>toggle classnames</button>
|
<button onClick={toggleClassnames}>toggle classnames</button>
|
||||||
<button onClick={logToObject}>toObject</button>
|
<button onClick={logToObject}>toObject</button>
|
||||||
|
|
||||||
|
<button onClick={deleteSelectedElements}>deleteSelectedElements</button>
|
||||||
|
<button onClick={deleteSomeElements}>deleteSomeElements</button>
|
||||||
|
<button onClick={onSetNodes}>setNodes</button>
|
||||||
|
<button onClick={onUpdateNode}>updateNode</button>
|
||||||
|
<button onClick={addNode}>addNode</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { ChangeEventHandler, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
addEdge,
|
||||||
|
Node,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
OnConnect,
|
||||||
|
Edge,
|
||||||
|
MiniMap,
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
Panel,
|
||||||
|
ColorMode,
|
||||||
|
Position,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
const nodeDefaults = {
|
||||||
|
sourcePosition: Position.Right,
|
||||||
|
targetPosition: Position.Left,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialNodes: Node[] = [
|
||||||
|
{ id: 'A', type: 'input', position: { x: 0, y: 150 }, data: { label: 'A' }, ...nodeDefaults },
|
||||||
|
{ id: 'B', position: { x: 250, y: 0 }, data: { label: 'B' }, ...nodeDefaults },
|
||||||
|
{ id: 'C', position: { x: 250, y: 150 }, data: { label: 'C' }, ...nodeDefaults },
|
||||||
|
{ id: 'D', position: { x: 250, y: 300 }, data: { label: 'D' }, ...nodeDefaults },
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialEdges: Edge[] = [
|
||||||
|
{
|
||||||
|
id: 'A-B',
|
||||||
|
source: 'A',
|
||||||
|
target: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A-C',
|
||||||
|
source: 'A',
|
||||||
|
target: 'C',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A-D',
|
||||||
|
source: 'A',
|
||||||
|
target: 'D',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ColorModeFlow = () => {
|
||||||
|
const [colorMode, setColorMode] = useState<ColorMode>('light');
|
||||||
|
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
|
const onConnect: OnConnect = useCallback(
|
||||||
|
(params) => {
|
||||||
|
console.log('on connect', params);
|
||||||
|
setEdges((eds) => addEdge(params, eds));
|
||||||
|
},
|
||||||
|
[setEdges]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChange: ChangeEventHandler<HTMLSelectElement> = (evt) => setColorMode(evt.target.value as ColorMode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
colorMode={colorMode}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<MiniMap />
|
||||||
|
<Background />
|
||||||
|
<Controls />
|
||||||
|
|
||||||
|
<Panel position="top-right">
|
||||||
|
<select onChange={onChange} data-testid="colormode-select">
|
||||||
|
<option value="light">light</option>
|
||||||
|
<option value="dark">dark</option>
|
||||||
|
<option value="system">system</option>
|
||||||
|
</select>
|
||||||
|
</Panel>
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorModeFlow;
|
||||||
@@ -60,12 +60,13 @@ const ControlledUncontrolled = () => {
|
|||||||
const updateNodePositions = () => {
|
const updateNodePositions = () => {
|
||||||
instance.setNodes((nodes) =>
|
instance.setNodes((nodes) =>
|
||||||
nodes.map((node) => {
|
nodes.map((node) => {
|
||||||
node.position = {
|
return {
|
||||||
x: Math.random() * 400,
|
...node,
|
||||||
y: Math.random() * 400,
|
position: {
|
||||||
|
x: Math.random() * 400,
|
||||||
|
y: Math.random() * 400,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return node;
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -73,11 +74,12 @@ const ControlledUncontrolled = () => {
|
|||||||
const updateEdgeColors = () => {
|
const updateEdgeColors = () => {
|
||||||
instance.setEdges((edges) =>
|
instance.setEdges((edges) =>
|
||||||
edges.map((edge) => {
|
edges.map((edge) => {
|
||||||
edge.style = {
|
return {
|
||||||
stroke: '#ff5050',
|
...edge,
|
||||||
|
style: {
|
||||||
|
stroke: '#ff5050',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return edge;
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ const buttonStyle: CSSProperties = {
|
|||||||
zIndex: 4,
|
zIndex: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomMiniMapNode = ({ x, y, width, height, color }: MiniMapNodeProps) => (
|
const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => {
|
||||||
<circle cx={x} cy={y} r={Math.max(width, height) / 2} fill={color} />
|
return <circle cx={x} cy={y} r={Math.max(width, height) / 2} fill="#ffcc00" />;
|
||||||
);
|
};
|
||||||
|
|
||||||
const CustomMiniMapNodeFlow = () => {
|
const CustomMiniMapNodeFlow = () => {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||||
|
|
||||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
||||||
const addRandomNode = () => {
|
const addRandomNode = () => {
|
||||||
@@ -59,7 +59,7 @@ const CustomMiniMapNodeFlow = () => {
|
|||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onConnect={(p) => onConnect(p)}
|
onConnect={(p) => onConnect(p)}
|
||||||
onNodeDragStop={onNodeDragStop}
|
onNodeDragStop={onNodeDragStop}
|
||||||
onlyRenderVisibleElements={false}
|
onlyRenderVisibleElements={true}
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background variant={BackgroundVariant.Lines} />
|
<Background variant={BackgroundVariant.Lines} />
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { memo, FC, CSSProperties, useCallback } from 'react';
|
import React, { memo, CSSProperties, useCallback } from 'react';
|
||||||
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react';
|
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react';
|
||||||
|
|
||||||
|
import type { ColorSelectorNode } from '.';
|
||||||
|
|
||||||
const targetHandleStyle: CSSProperties = { background: '#555' };
|
const targetHandleStyle: CSSProperties = { background: '#555' };
|
||||||
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
|
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
|
||||||
const sourceHandleStyleB: CSSProperties = {
|
const sourceHandleStyleB: CSSProperties = {
|
||||||
@@ -11,7 +13,7 @@ const sourceHandleStyleB: CSSProperties = {
|
|||||||
|
|
||||||
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
|
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
|
||||||
|
|
||||||
const ColorSelectorNode: FC<NodeProps> = ({ data, isConnectable }) => {
|
function ColorSelectorNode({ data, isConnectable }: NodeProps<ColorSelectorNode>) {
|
||||||
const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []);
|
const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []);
|
||||||
const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []);
|
const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []);
|
||||||
const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []);
|
const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []);
|
||||||
@@ -42,6 +44,6 @@ const ColorSelectorNode: FC<NodeProps> = ({ data, isConnectable }) => {
|
|||||||
<Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} />
|
<Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default memo(ColorSelectorNode);
|
export default memo(ColorSelectorNode);
|
||||||
|
|||||||
@@ -1,40 +1,64 @@
|
|||||||
import { useState, useEffect, MouseEvent, ChangeEvent, useCallback } from 'react';
|
import { useState, useEffect, MouseEvent, ChangeEvent, useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
Controls,
|
Controls,
|
||||||
addEdge,
|
addEdge,
|
||||||
Node,
|
Node,
|
||||||
ReactFlowInstance,
|
|
||||||
Position,
|
Position,
|
||||||
SnapGrid,
|
SnapGrid,
|
||||||
Connection,
|
|
||||||
useNodesState,
|
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
Background,
|
Background,
|
||||||
|
OnNodeDrag,
|
||||||
|
OnInit,
|
||||||
|
applyNodeChanges,
|
||||||
|
OnNodesChange,
|
||||||
|
OnConnect,
|
||||||
|
OnBeforeDelete,
|
||||||
|
BuiltInNode,
|
||||||
|
BuiltInEdge,
|
||||||
|
NodeTypes,
|
||||||
|
ReactFlowProvider,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
import ColorSelectorNode from './ColorSelectorNode';
|
import ColorSelectorNode from './ColorSelectorNode';
|
||||||
|
|
||||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
export type ColorSelectorNode = Node<
|
||||||
|
{ color: string; onChange: (event: ChangeEvent<HTMLInputElement>) => void },
|
||||||
|
'selectorNode'
|
||||||
|
>;
|
||||||
|
export type MyNode = BuiltInNode | ColorSelectorNode;
|
||||||
|
export type MyEdge = BuiltInEdge;
|
||||||
|
|
||||||
|
const onInit: OnInit<MyNode, MyEdge> = (reactFlowInstance) => {
|
||||||
console.log('flow loaded:', reactFlowInstance);
|
console.log('flow loaded:', reactFlowInstance);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
const onNodeDragStop: OnNodeDrag<MyNode> = (_, node) => console.log('drag stop', node);
|
||||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
const onNodeClick = (_: MouseEvent, node: MyNode) => console.log('click', node);
|
||||||
|
|
||||||
const initBgColor = '#1A192B';
|
const initBgColor = '#1A192B';
|
||||||
|
|
||||||
const connectionLineStyle = { stroke: '#fff' };
|
const connectionLineStyle = { stroke: '#fff' };
|
||||||
const snapGrid: SnapGrid = [16, 16];
|
const snapGrid: SnapGrid = [16, 16];
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes: NodeTypes = {
|
||||||
selectorNode: ColorSelectorNode,
|
selectorNode: ColorSelectorNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomNodeFlow = () => {
|
const CustomNodeFlow = () => {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const ref = useRef(null);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [nodes, setNodes] = useState<MyNode[]>([]);
|
||||||
|
const onNodesChange: OnNodesChange<MyNode> = useCallback(
|
||||||
|
(changes) =>
|
||||||
|
setNodes((nds) => {
|
||||||
|
const nextNodes = applyNodeChanges(changes, nds);
|
||||||
|
return nextNodes;
|
||||||
|
}),
|
||||||
|
[setNodes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState<MyEdge>([]);
|
||||||
|
|
||||||
const [bgColor, setBgColor] = useState<string>(initBgColor);
|
const [bgColor, setBgColor] = useState<string>(initBgColor);
|
||||||
|
|
||||||
@@ -42,7 +66,7 @@ const CustomNodeFlow = () => {
|
|||||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
setNodes((nds) =>
|
setNodes((nds) =>
|
||||||
nds.map((node) => {
|
nds.map((node) => {
|
||||||
if (node.id !== '2') {
|
if (node.id !== '2' || node.type !== 'selectorNode') {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,12 +143,13 @@ const CustomNodeFlow = () => {
|
|||||||
]);
|
]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onConnect = useCallback(
|
const onConnect: OnConnect = useCallback(
|
||||||
(connection: Connection) =>
|
(connection) => setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
|
||||||
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
|
|
||||||
[setEdges]
|
[setEdges]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onBeforeDelete: OnBeforeDelete<MyNode, MyEdge> = useCallback(async (params) => true, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -142,16 +167,18 @@ const CustomNodeFlow = () => {
|
|||||||
fitView
|
fitView
|
||||||
minZoom={0.3}
|
minZoom={0.3}
|
||||||
maxZoom={2}
|
maxZoom={2}
|
||||||
|
onBeforeDelete={onBeforeDelete}
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
<MiniMap
|
<MiniMap<MyNode>
|
||||||
nodeStrokeColor={(n: Node): string => {
|
nodeStrokeColor={(n: MyNode): string => {
|
||||||
if (n.type === 'input') return '#0041d0';
|
if (n.type === 'input') return '#0041d0';
|
||||||
if (n.type === 'selectorNode') return bgColor;
|
if (n.type === 'selectorNode') return bgColor;
|
||||||
if (n.type === 'output') return '#ff0072';
|
if (n.type === 'output') return '#ff0072';
|
||||||
|
|
||||||
return '#eee';
|
return '#eee';
|
||||||
}}
|
}}
|
||||||
nodeColor={(n: Node): string => {
|
nodeColor={(n: MyNode): string => {
|
||||||
if (n.type === 'selectorNode') return bgColor;
|
if (n.type === 'selectorNode') return bgColor;
|
||||||
|
|
||||||
return '#fff';
|
return '#fff';
|
||||||
@@ -163,4 +190,8 @@ const CustomNodeFlow = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomNodeFlow;
|
export default () => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<CustomNodeFlow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const defaultNodes: Node[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
|
type: 'output',
|
||||||
data: { label: 'Node 3' },
|
data: { label: 'Node 3' },
|
||||||
position: { x: 400, y: 100 },
|
position: { x: 400, y: 100 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
@@ -54,26 +55,24 @@ const DefaultNodes = () => {
|
|||||||
|
|
||||||
const updateNodePositions = () => {
|
const updateNodePositions = () => {
|
||||||
instance.setNodes((nodes) =>
|
instance.setNodes((nodes) =>
|
||||||
nodes.map((node) => {
|
nodes.map((node) => ({
|
||||||
node.position = {
|
...node,
|
||||||
|
position: {
|
||||||
x: Math.random() * 400,
|
x: Math.random() * 400,
|
||||||
y: Math.random() * 400,
|
y: Math.random() * 400,
|
||||||
};
|
},
|
||||||
|
}))
|
||||||
return node;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateEdgeColors = () => {
|
const updateEdgeColors = () => {
|
||||||
instance.setEdges((edges) =>
|
instance.setEdges((edges) =>
|
||||||
edges.map((edge) => {
|
edges.map((edge) => ({
|
||||||
edge.style = {
|
...edge,
|
||||||
|
style: {
|
||||||
stroke: '#ff5050',
|
stroke: '#ff5050',
|
||||||
};
|
},
|
||||||
|
}))
|
||||||
return edge;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { NodeChange, OnNodesChange, useStore, useStoreApi } from '@xyflow/react';
|
||||||
|
|
||||||
|
type ChangeLoggerProps = {
|
||||||
|
color?: string;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChangeInfoProps = {
|
||||||
|
change: NodeChange;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ChangeInfo({ change }: ChangeInfoProps) {
|
||||||
|
const id = 'id' in change ? change.id : '-';
|
||||||
|
const { type } = change;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<div>node id: {id}</div>
|
||||||
|
<div>
|
||||||
|
{type === 'add' ? JSON.stringify(change.item, null, 2) : null}
|
||||||
|
{type === 'dimensions' ? `${change.dimensions?.width} × ${change.dimensions?.height}` : null}
|
||||||
|
{type === 'position' ? `position: ${change.position?.x.toFixed(1)}, ${change.position?.y.toFixed(1)}` : null}
|
||||||
|
{type === 'remove' ? 'remove' : null}
|
||||||
|
{type === 'replace' ? JSON.stringify(change.item, null, 2) : null}
|
||||||
|
{type === 'select' ? (change.selected ? 'select' : 'unselect') : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChangeLogger({ limit = 20 }: ChangeLoggerProps) {
|
||||||
|
const [changes, setChanges] = useState<NodeChange[]>([]);
|
||||||
|
const onNodesChangeIntercepted = useRef(false);
|
||||||
|
const onNodesChange = useStore((s) => s.onNodesChange);
|
||||||
|
const store = useStoreApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onNodesChange || onNodesChangeIntercepted.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodesChangeIntercepted.current = true;
|
||||||
|
const userOnNodesChange = onNodesChange;
|
||||||
|
|
||||||
|
const onNodesChangeLogger: OnNodesChange = (changes) => {
|
||||||
|
userOnNodesChange(changes);
|
||||||
|
|
||||||
|
setChanges((c) => {
|
||||||
|
changes.forEach((change) => {
|
||||||
|
if (c.length >= limit) {
|
||||||
|
c.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
c = [change, ...c];
|
||||||
|
});
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
store.setState({ onNodesChange: onNodesChangeLogger });
|
||||||
|
}, [onNodesChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="react-flow__devtools-changelogger">
|
||||||
|
<div className="react-flow__devtools-title">Change Logger</div>
|
||||||
|
{changes.length === 0 ? (
|
||||||
|
<>no changes triggered</>
|
||||||
|
) : (
|
||||||
|
changes.map((change, index) => <ChangeInfo key={index} change={change} />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useNodes, ViewportPortal } from '@xyflow/react';
|
||||||
|
|
||||||
|
type NodeInfoProps = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
data: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) {
|
||||||
|
if (!width || !height) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="react-flow__devtools-nodeinfo"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(${x}px, ${y + height}px)`,
|
||||||
|
width: width * 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>id: {id}</div>
|
||||||
|
<div>type: {type}</div>
|
||||||
|
<div>
|
||||||
|
position: {x.toFixed(1)}, {y.toFixed(1)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
dimensions: {width} × {height}
|
||||||
|
</div>
|
||||||
|
<div>data: {JSON.stringify(data, null, 2)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NodeInspector() {
|
||||||
|
const nodes = useNodes();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewportPortal>
|
||||||
|
<div className="react-flow__devtools-nodeinspector">
|
||||||
|
{nodes.map((node) => {
|
||||||
|
const x = node?.position?.x || 0;
|
||||||
|
const y = node?.position?.y || 0;
|
||||||
|
const width = node.measured?.width || 0;
|
||||||
|
const height = node.measured?.height || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeInfo
|
||||||
|
key={node.id}
|
||||||
|
id={node.id}
|
||||||
|
type={node.type || 'default'}
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
data={node.data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ViewportPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useState, type Dispatch, type SetStateAction, type ReactNode, HTMLAttributes } from 'react';
|
||||||
|
import { Panel, PanelPosition } from '@xyflow/react';
|
||||||
|
|
||||||
|
import NodeInspector from './NodeInspector';
|
||||||
|
import ChangeLogger from './ChangeLogger';
|
||||||
|
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
export default function ReactFlowDevTools({ position = 'top-left' }: { position?: PanelPosition }) {
|
||||||
|
const [nodeInspectorActive, setNodeInspectorActive] = useState(false);
|
||||||
|
const [changeLoggerActive, setChangeLoggerActive] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="react-flow__devtools">
|
||||||
|
<Panel position={position}>
|
||||||
|
<DevToolButton setActive={setNodeInspectorActive} active={nodeInspectorActive} title="Toggle Node Inspector">
|
||||||
|
Node Inspector
|
||||||
|
</DevToolButton>
|
||||||
|
<DevToolButton setActive={setChangeLoggerActive} active={changeLoggerActive} title="Toggle Change Logger">
|
||||||
|
Change Logger
|
||||||
|
</DevToolButton>
|
||||||
|
</Panel>
|
||||||
|
{changeLoggerActive && <ChangeLogger />}
|
||||||
|
{nodeInspectorActive && <NodeInspector />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevToolButton({
|
||||||
|
active,
|
||||||
|
setActive,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
active: boolean;
|
||||||
|
setActive: Dispatch<SetStateAction<boolean>>;
|
||||||
|
children: ReactNode;
|
||||||
|
} & HTMLAttributes<HTMLButtonElement>) {
|
||||||
|
return (
|
||||||
|
<button onClick={() => setActive((a) => !a)} className={active ? 'active' : ''} {...rest}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
.react-flow__devtools {
|
||||||
|
--border-radius: 4px;
|
||||||
|
--highlight-color: rgba(238, 58, 115, 1);
|
||||||
|
--font: monospace, sans-serif;
|
||||||
|
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__devtools button {
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
padding: 5px 15px;
|
||||||
|
color: #222;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font);
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__devtools button:hover {
|
||||||
|
background: var(--highlight-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__devtools button.active {
|
||||||
|
background: var(--highlight-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__devtools button:first-child {
|
||||||
|
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__devtools button:last-child {
|
||||||
|
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__devtools-changelogger {
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
top: 50px;
|
||||||
|
left: 20px;
|
||||||
|
font-family: var(--font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__devtools-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__devtools-nodeinspector {
|
||||||
|
pointer-events: none;
|
||||||
|
font-family: monospace, sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__devtools-nodeinfo {
|
||||||
|
top: 5px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react';
|
||||||
|
|
||||||
|
import DevTools from './DevTools';
|
||||||
|
|
||||||
|
const initNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '1a',
|
||||||
|
type: 'input',
|
||||||
|
data: { label: 'Node 1' },
|
||||||
|
position: { x: 250, y: 5 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2a',
|
||||||
|
data: { label: 'Node 2' },
|
||||||
|
position: { x: 100, y: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3a',
|
||||||
|
data: { label: 'Node 3' },
|
||||||
|
position: { x: 400, y: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4a',
|
||||||
|
data: { label: 'Node 4' },
|
||||||
|
position: { x: 400, y: 200 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initEdges: Edge[] = [
|
||||||
|
{ id: 'e1-2', source: '1a', target: '2a' },
|
||||||
|
{ id: 'e1-3', source: '1a', target: '3a' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const BasicFlow = () => {
|
||||||
|
const [nodes, , onNodesChange] = useNodesState(initNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||||
|
|
||||||
|
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<DevTools />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BasicFlow;
|
||||||
@@ -14,7 +14,7 @@ const initialNodes: Node[] = [
|
|||||||
dragHandle: '.custom-drag-handle',
|
dragHandle: '.custom-drag-handle',
|
||||||
style: { border: '1px solid #ddd', padding: '20px 40px' },
|
style: { border: '1px solid #ddd', padding: '20px 40px' },
|
||||||
position: { x: 200, y: 200 },
|
position: { x: 200, y: 200 },
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ const DragHandleFlow = () => {
|
|||||||
edges={edges}
|
edges={edges}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
|
nodeDragThreshold={0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const nodeOrigin: NodeOrigin = [0.5, 0.5];
|
|||||||
const DnDFlow = () => {
|
const DnDFlow = () => {
|
||||||
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
|
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||||
|
|
||||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||||
const onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi);
|
const onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi);
|
||||||
@@ -49,7 +49,7 @@ const DnDFlow = () => {
|
|||||||
|
|
||||||
if (reactFlowInstance) {
|
if (reactFlowInstance) {
|
||||||
const type = event.dataTransfer.getData('application/reactflow');
|
const type = event.dataTransfer.getData('application/reactflow');
|
||||||
const position = reactFlowInstance.screenToFlowCoordinate({
|
const position = reactFlowInstance.screenToFlowPosition({
|
||||||
x: event.clientX,
|
x: event.clientX,
|
||||||
y: event.clientY,
|
y: event.clientY,
|
||||||
});
|
});
|
||||||
@@ -66,7 +66,7 @@ const DnDFlow = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.dndflow}>
|
<div className={styles.dndflow}>
|
||||||
<ReactFlowProvider nodes={initialNodes} edges={[]}>
|
<ReactFlowProvider initialNodes={initialNodes} initialEdges={[]}>
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -77,6 +77,7 @@ const DnDFlow = () => {
|
|||||||
onInit={onInit}
|
onInit={onInit}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
|
nodeOrigin={nodeOrigin}
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useStore, getStraightPath, EdgeProps } from '@xyflow/react';
|
import { useStore, getStraightPath, EdgeProps, useInternalNode } from '@xyflow/react';
|
||||||
|
|
||||||
import { getEdgeParams } from './utils.js';
|
import { getEdgeParams } from './utils.js';
|
||||||
|
|
||||||
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
|
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
|
||||||
const sourceNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === source), [source]));
|
const sourceNode = useInternalNode(source);
|
||||||
const targetNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === target), [target]));
|
const targetNode = useInternalNode(target);
|
||||||
|
|
||||||
if (!sourceNode || !targetNode) {
|
if (!sourceNode || !targetNode) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import CustomNode from './CustomNode';
|
|||||||
import FloatingEdge from './FloatingEdge';
|
import FloatingEdge from './FloatingEdge';
|
||||||
import CustomConnectionLine from './CustomConnectionLine';
|
import CustomConnectionLine from './CustomConnectionLine';
|
||||||
|
|
||||||
import '@xyflow/react/dist/style.css';
|
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { Node, Position, MarkerType, XYPosition } from '@xyflow/react';
|
import { Node, Position, MarkerType, XYPosition, InternalNode } from '@xyflow/react';
|
||||||
|
|
||||||
// this helper function returns the intersection point
|
// this helper function returns the intersection point
|
||||||
// of the line between the center of the intersectionNode and the target node
|
// of the line between the center of the intersectionNode and the target node
|
||||||
function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
|
function getNodeIntersection(intersectionNode: InternalNode, targetNode: InternalNode) {
|
||||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||||
const {
|
|
||||||
width: intersectionNodeWidth,
|
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured;
|
||||||
height: intersectionNodeHeight,
|
const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
|
||||||
positionAbsolute: intersectionNodePosition,
|
const targetPosition = targetNode.internals.positionAbsolute!;
|
||||||
} = intersectionNode;
|
|
||||||
const targetPosition = targetNode.positionAbsolute!;
|
|
||||||
|
|
||||||
const w = intersectionNodeWidth! / 2;
|
const w = intersectionNodeWidth! / 2;
|
||||||
const h = intersectionNodeHeight! / 2;
|
const h = intersectionNodeHeight! / 2;
|
||||||
|
|
||||||
const x2 = intersectionNodePosition!.x + w;
|
const x2 = intersectionNodePosition.x + w;
|
||||||
const y2 = intersectionNodePosition!.y + h;
|
const y2 = intersectionNodePosition.y + h;
|
||||||
const x1 = targetPosition.x + w;
|
const x1 = targetPosition.x + w;
|
||||||
const y1 = targetPosition.y + h;
|
const y1 = targetPosition.y + h;
|
||||||
|
|
||||||
@@ -31,8 +29,8 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
||||||
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
function getEdgePosition(node: InternalNode, intersectionPoint: XYPosition) {
|
||||||
const n = { ...node.positionAbsolute, ...node };
|
const n = { ...node.internals.positionAbsolute, ...node };
|
||||||
const nx = Math.round(n.x!);
|
const nx = Math.round(n.x!);
|
||||||
const ny = Math.round(n.y!);
|
const ny = Math.round(n.y!);
|
||||||
const px = Math.round(intersectionPoint.x);
|
const px = Math.round(intersectionPoint.x);
|
||||||
@@ -41,13 +39,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
|||||||
if (px <= nx + 1) {
|
if (px <= nx + 1) {
|
||||||
return Position.Left;
|
return Position.Left;
|
||||||
}
|
}
|
||||||
if (px >= nx + n.width! - 1) {
|
if (px >= nx + n.measured?.width! - 1) {
|
||||||
return Position.Right;
|
return Position.Right;
|
||||||
}
|
}
|
||||||
if (py <= ny + 1) {
|
if (py <= ny + 1) {
|
||||||
return Position.Top;
|
return Position.Top;
|
||||||
}
|
}
|
||||||
if (py >= n.y! + n.height! - 1) {
|
if (py >= n.y! + n.measured?.height! - 1) {
|
||||||
return Position.Bottom;
|
return Position.Bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +53,7 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
||||||
export function getEdgeParams(source: Node, target: Node) {
|
export function getEdgeParams(source: InternalNode, target: InternalNode) {
|
||||||
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
||||||
const targetIntersectionPoint = getNodeIntersection(target, source);
|
const targetIntersectionPoint = getNodeIntersection(target, source);
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,15 @@ const edgeTypes: EdgeTypes = {
|
|||||||
custom2: CustomEdge2,
|
custom2: CustomEdge2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultEdgeOptions = {
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
color: 'red',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const EdgesFlow = () => {
|
const EdgesFlow = () => {
|
||||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
@@ -196,6 +205,8 @@ const EdgesFlow = () => {
|
|||||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||||
onEdgeMouseMove={onEdgeMouseMove}
|
onEdgeMouseMove={onEdgeMouseMove}
|
||||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||||
|
onDelete={console.log}
|
||||||
|
defaultEdgeOptions={defaultEdgeOptions}
|
||||||
>
|
>
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
<Controls />
|
<Controls />
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ const buttonStyle: CSSProperties = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EmptyFlow = () => {
|
const EmptyFlow = () => {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||||
|
|
||||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
||||||
const addRandomNode = () => {
|
const addRandomNode = () => {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { getEdgeParams } from './utils';
|
|||||||
|
|
||||||
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
|
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
|
||||||
const { sourceNode, targetNode } = useStore((s) => {
|
const { sourceNode, targetNode } = useStore((s) => {
|
||||||
const sourceNode = s.nodes.find((n) => n.id === source);
|
const sourceNode = s.nodeLookup.get(source);
|
||||||
const targetNode = s.nodes.find((n) => n.id === target);
|
const targetNode = s.nodeLookup.get(target);
|
||||||
|
|
||||||
return { sourceNode, targetNode };
|
return { sourceNode, targetNode };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Position, XYPosition, Node, Edge } from '@xyflow/react';
|
import { Position, XYPosition, Node, Edge, InternalNode } from '@xyflow/react';
|
||||||
|
|
||||||
// this helper function returns the intersection point
|
// this helper function returns the intersection point
|
||||||
// of the line between the center of the intersectionNode and the target node
|
// of the line between the center of the intersectionNode and the target node
|
||||||
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
|
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
|
||||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||||
|
|
||||||
const {
|
const { position: intersectionNodePosition } = intersectionNode;
|
||||||
width: intersectionNodeWidth,
|
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured ?? {
|
||||||
height: intersectionNodeHeight,
|
width: 0,
|
||||||
position: intersectionNodePosition,
|
height: 0,
|
||||||
} = intersectionNode;
|
};
|
||||||
const targetPosition = targetNode.position;
|
const targetPosition = targetNode.position;
|
||||||
|
|
||||||
const w = (intersectionNodeWidth ?? 0) / 2;
|
const w = (intersectionNodeWidth ?? 0) / 2;
|
||||||
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
|||||||
if (px <= nx + 1) {
|
if (px <= nx + 1) {
|
||||||
return Position.Left;
|
return Position.Left;
|
||||||
}
|
}
|
||||||
if (px >= nx + (n.width ?? 0) - 1) {
|
if (px >= nx + (n.measured?.width ?? 0) - 1) {
|
||||||
return Position.Right;
|
return Position.Right;
|
||||||
}
|
}
|
||||||
if (py <= ny + 1) {
|
if (py <= ny + 1) {
|
||||||
return Position.Top;
|
return Position.Top;
|
||||||
}
|
}
|
||||||
if (py >= n.y + (n.height ?? 0) - 1) {
|
if (py >= n.y + (n.measured?.height ?? 0) - 1) {
|
||||||
return Position.Bottom;
|
return Position.Bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
||||||
export function getEdgeParams(source: Node, target: Node) {
|
export function getEdgeParams(source: InternalNode, target: InternalNode) {
|
||||||
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
||||||
const targetIntersectionPoint = getNodeIntersection(target, source);
|
const targetIntersectionPoint = getNodeIntersection(target, source);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
MiniMap,
|
MiniMap,
|
||||||
Controls,
|
Controls,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
import ReactFlowDevTools from '../DevTools/DevTools';
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
{
|
{
|
||||||
@@ -48,8 +49,10 @@ const initialEdges: Edge[] = [
|
|||||||
|
|
||||||
const setHidden = (hidden: boolean) => (els: any[]) =>
|
const setHidden = (hidden: boolean) => (els: any[]) =>
|
||||||
els.map((e: any) => {
|
els.map((e: any) => {
|
||||||
e.hidden = hidden;
|
return {
|
||||||
return e;
|
...e,
|
||||||
|
hidden,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const HiddenFlow = () => {
|
const HiddenFlow = () => {
|
||||||
@@ -70,6 +73,8 @@ const HiddenFlow = () => {
|
|||||||
setEdges(setHidden(isHidden));
|
setEdges(setHidden(isHidden));
|
||||||
}, [isHidden, setEdges, setNodes]);
|
}, [isHidden, setEdges, setNodes]);
|
||||||
|
|
||||||
|
console.log(nodes);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -80,6 +85,7 @@ const HiddenFlow = () => {
|
|||||||
>
|
>
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
<Controls />
|
<Controls />
|
||||||
|
<ReactFlowDevTools position="top-right" />
|
||||||
|
|
||||||
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
|
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', nod
|
|||||||
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
|
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
|
||||||
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event);
|
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event);
|
||||||
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
|
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
|
||||||
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('onPaneContextMenu', event);
|
const onPaneContextMenu = (event: ReactMouseEvent | MouseEvent) => console.log('onPaneContextMenu', event);
|
||||||
const onMoveEnd = (_: TouchEvent | MouseEvent | null, viewport: Viewport) => console.log('onMoveEnd', viewport);
|
const onMoveEnd = (_: TouchEvent | MouseEvent | null, viewport: Viewport) => console.log('onMoveEnd', viewport);
|
||||||
|
|
||||||
const InteractionFlow = () => {
|
const InteractionFlow = () => {
|
||||||
@@ -80,6 +80,7 @@ const InteractionFlow = () => {
|
|||||||
onPaneClick={captureZoomClick ? onPaneClick : undefined}
|
onPaneClick={captureZoomClick ? onPaneClick : undefined}
|
||||||
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
|
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
|
||||||
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
|
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
|
||||||
|
nodeDragThreshold={0}
|
||||||
onMoveEnd={onMoveEnd}
|
onMoveEnd={onMoveEnd}
|
||||||
>
|
>
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
|
|||||||
@@ -17,28 +17,34 @@ const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', n
|
|||||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '0',
|
||||||
|
data: { label: 'rectangle' },
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
draggable: false,
|
||||||
|
style: {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
data: { label: 'Node 1' },
|
data: { label: 'Node 1' },
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
className: 'light',
|
width: 200,
|
||||||
style: {
|
height: 100,
|
||||||
width: 200,
|
|
||||||
height: 100,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
data: { label: 'Node 2' },
|
data: { label: 'Node 2' },
|
||||||
position: { x: 0, y: 150 },
|
position: { x: 0, y: 150 },
|
||||||
className: 'light',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
data: { label: 'Node 3' },
|
data: { label: 'Node 3' },
|
||||||
position: { x: 250, y: 0 },
|
position: { x: 250, y: 0 },
|
||||||
className: 'light',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
@@ -48,7 +54,6 @@ const initialNodes: Node[] = [
|
|||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
},
|
},
|
||||||
className: 'light',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import initialItems from './initial-elements';
|
import initialItems from './initial-elements';
|
||||||
|
|
||||||
import styles from './layouting.module.css';
|
import styles from './layouting.module.css';
|
||||||
|
import ReactFlowDevTools from '../DevTools/DevTools';
|
||||||
|
|
||||||
const dagreGraph = new dagre.graphlib.Graph();
|
const dagreGraph = new dagre.graphlib.Graph();
|
||||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||||
@@ -56,16 +57,16 @@ const LayoutFlow = () => {
|
|||||||
|
|
||||||
const layoutedNodes = nodes.map((node) => {
|
const layoutedNodes = nodes.map((node) => {
|
||||||
const nodeWithPosition = dagreGraph.node(node.id);
|
const nodeWithPosition = dagreGraph.node(node.id);
|
||||||
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
|
|
||||||
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
|
|
||||||
// we need to pass a slightly different position in order to notify react flow about the change
|
|
||||||
// @TODO how can we change the position handling so that we dont need this hack?
|
|
||||||
node.position = {
|
|
||||||
x: nodeWithPosition.x + Math.random() / 1000,
|
|
||||||
y: nodeWithPosition.y,
|
|
||||||
};
|
|
||||||
|
|
||||||
return node;
|
return {
|
||||||
|
...node,
|
||||||
|
targetPosition: isHorizontal ? Position.Left : Position.Top,
|
||||||
|
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
|
||||||
|
position: {
|
||||||
|
x: nodeWithPosition.x,
|
||||||
|
y: nodeWithPosition.y,
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setNodes(layoutedNodes);
|
setNodes(layoutedNodes);
|
||||||
@@ -98,6 +99,7 @@ const LayoutFlow = () => {
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
|
<ReactFlowDevTools />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
<Panel position="top-right">
|
<Panel position="top-right">
|
||||||
<button onClick={() => onLayout('TB')}>vertical layout</button>
|
<button onClick={() => onLayout('TB')}>vertical layout</button>
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Controls,
|
||||||
|
addEdge,
|
||||||
|
Connection,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
Background,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
Panel,
|
||||||
|
useReactFlow,
|
||||||
|
ReactFlowProvider,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
const initNodes: Node[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
initNodes.push({
|
||||||
|
id: i.toString(),
|
||||||
|
data: {
|
||||||
|
label: `node ${i + 1}`,
|
||||||
|
},
|
||||||
|
position: { x: (i % 10) * 60, y: Math.floor(i / 10) * 60 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const initEdges: Edge[] = initNodes.reduce<Edge[]>((res, node, index) => {
|
||||||
|
if (index > 0) {
|
||||||
|
res.push({ id: `${index - 1}-${index}`, source: (index - 1).toString(), target: index.toString() });
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const CustomNodeFlow = () => {
|
||||||
|
const { setNodes, updateNodeData, updateEdge } = useReactFlow();
|
||||||
|
const [nodes, , onNodesChange] = useNodesState(initNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||||
|
|
||||||
|
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||||
|
|
||||||
|
const multiSetNodes = () => {
|
||||||
|
nodes.forEach((node) =>
|
||||||
|
setNodes((nds) =>
|
||||||
|
nds.map((n) => {
|
||||||
|
if (n.id === node.id) {
|
||||||
|
return { ...n, data: { label: 'node set' } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const multiUpdateNodes = () => {
|
||||||
|
nodes.forEach((node) => updateNodeData(node.id, { label: 'node update' }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const multiUpdateEdges = () => {
|
||||||
|
edges.forEach((edge) => updateEdge(edge.id, { label: 'edge update' }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
fitView
|
||||||
|
className="multiset"
|
||||||
|
>
|
||||||
|
<Controls />
|
||||||
|
<Background />
|
||||||
|
<Panel>
|
||||||
|
<button onClick={multiSetNodes}>set nodes</button>
|
||||||
|
<button onClick={multiUpdateNodes}>update nodes</button>
|
||||||
|
<button onClick={multiUpdateEdges}>update edges</button>
|
||||||
|
</Panel>
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<CustomNodeFlow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.multiset .react-flow__node {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
import { useState, MouseEvent, useCallback } from 'react';
|
|
||||||
import {
|
|
||||||
ReactFlow,
|
|
||||||
Controls,
|
|
||||||
MiniMap,
|
|
||||||
Background,
|
|
||||||
addEdge,
|
|
||||||
useNodesState,
|
|
||||||
useEdgesState,
|
|
||||||
Node,
|
|
||||||
Edge,
|
|
||||||
ReactFlowInstance,
|
|
||||||
Connection,
|
|
||||||
} from '@xyflow/react';
|
|
||||||
|
|
||||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
|
||||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
|
||||||
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
|
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
type: 'input',
|
|
||||||
data: { label: 'Node 1' },
|
|
||||||
position: { x: 250, y: 5 },
|
|
||||||
className: 'light',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
data: { label: 'Node 2' },
|
|
||||||
position: { x: 100, y: 100 },
|
|
||||||
className: 'light',
|
|
||||||
style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 200, height: 200 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2a',
|
|
||||||
data: { label: 'Node 2a' },
|
|
||||||
position: { x: 10, y: 50 },
|
|
||||||
parentNode: '2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
data: { label: 'Node 3' },
|
|
||||||
position: { x: 320, y: 100 },
|
|
||||||
className: 'light',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
data: { label: 'Node 4' },
|
|
||||||
position: { x: 320, y: 200 },
|
|
||||||
className: 'light',
|
|
||||||
style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: 300, height: 300 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4a',
|
|
||||||
data: { label: 'Node 4a' },
|
|
||||||
position: { x: 15, y: 65 },
|
|
||||||
className: 'light',
|
|
||||||
parentNode: '4',
|
|
||||||
extent: 'parent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4b',
|
|
||||||
data: { label: 'Node 4b' },
|
|
||||||
position: { x: 15, y: 120 },
|
|
||||||
className: 'light',
|
|
||||||
style: {
|
|
||||||
backgroundColor: 'rgba(255, 0, 255, 0.7)',
|
|
||||||
height: 150,
|
|
||||||
width: 270,
|
|
||||||
},
|
|
||||||
parentNode: '4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4b1',
|
|
||||||
data: { label: 'Node 4b1' },
|
|
||||||
position: { x: 20, y: 40 },
|
|
||||||
className: 'light',
|
|
||||||
parentNode: '4b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4b2',
|
|
||||||
data: { label: 'Node 4b2' },
|
|
||||||
position: { x: 100, y: 100 },
|
|
||||||
className: 'light',
|
|
||||||
parentNode: '4b',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const initialEdges: Edge[] = [
|
|
||||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
|
||||||
{ id: 'e1-3', source: '1', target: '3' },
|
|
||||||
{ id: 'e2a-4a', source: '2a', target: '4a' },
|
|
||||||
{ id: 'e3-4', source: '3', target: '4' },
|
|
||||||
{ id: 'e3-4b', source: '3', target: '4b' },
|
|
||||||
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
|
|
||||||
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
|
|
||||||
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const NestedFlow = () => {
|
|
||||||
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
||||||
|
|
||||||
const onConnect = useCallback(
|
|
||||||
(connection: Connection) => {
|
|
||||||
setEdges((eds) => addEdge(connection, eds));
|
|
||||||
},
|
|
||||||
[setEdges]
|
|
||||||
);
|
|
||||||
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
|
|
||||||
|
|
||||||
const updatePos = () => {
|
|
||||||
setNodes((nds) => {
|
|
||||||
return nds.map((n) => {
|
|
||||||
n.position = {
|
|
||||||
x: Math.random() * 400,
|
|
||||||
y: Math.random() * 400,
|
|
||||||
};
|
|
||||||
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const logToObject = () => console.log(rfInstance?.toObject());
|
|
||||||
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
|
|
||||||
|
|
||||||
const toggleClassnames = () => {
|
|
||||||
setNodes((nds) => {
|
|
||||||
return nds.map((n) => {
|
|
||||||
n.className = n.className === 'light' ? 'dark' : 'light';
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleChildNodes = () => {
|
|
||||||
setNodes((nds) => {
|
|
||||||
return nds.map((n) => {
|
|
||||||
n.hidden = !!n.parentNode && !n.hidden;
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReactFlow
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
onInit={onInit}
|
|
||||||
onNodesChange={onNodesChange}
|
|
||||||
onEdgesChange={onEdgesChange}
|
|
||||||
onNodeClick={onNodeClick}
|
|
||||||
onEdgeClick={onEdgeClick}
|
|
||||||
onConnect={onConnect}
|
|
||||||
onNodeDragStop={onNodeDragStop}
|
|
||||||
className="react-flow-basic-example"
|
|
||||||
minZoom={0.2}
|
|
||||||
maxZoom={4}
|
|
||||||
onlyRenderVisibleElements={false}
|
|
||||||
>
|
|
||||||
<MiniMap pannable />
|
|
||||||
<Controls />
|
|
||||||
<Background />
|
|
||||||
|
|
||||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
|
||||||
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
|
||||||
reset transform
|
|
||||||
</button>
|
|
||||||
<button onClick={updatePos} style={{ marginRight: 5 }}>
|
|
||||||
change pos
|
|
||||||
</button>
|
|
||||||
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
|
|
||||||
toggle classnames
|
|
||||||
</button>
|
|
||||||
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
|
|
||||||
toggleChildNodes
|
|
||||||
</button>
|
|
||||||
<button onClick={logToObject}>toObject</button>
|
|
||||||
</div>
|
|
||||||
</ReactFlow>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NestedFlow;
|
|
||||||
@@ -9,7 +9,7 @@ const DefaultResizerNode: FC<NodeProps> = ({ data, selected }) => {
|
|||||||
maxWidth={data.maxWidth ?? undefined}
|
maxWidth={data.maxWidth ?? undefined}
|
||||||
minHeight={data.minHeight ?? undefined}
|
minHeight={data.minHeight ?? undefined}
|
||||||
maxHeight={data.maxHeight ?? undefined}
|
maxHeight={data.maxHeight ?? undefined}
|
||||||
isVisible={data.isVisible ?? selected}
|
isVisible={data.isVisible ?? !!selected}
|
||||||
shouldResize={data.shouldResize ?? undefined}
|
shouldResize={data.shouldResize ?? undefined}
|
||||||
onResizeStart={data.onResizeStart ?? undefined}
|
onResizeStart={data.onResizeStart ?? undefined}
|
||||||
onResize={data.onResize ?? undefined}
|
onResize={data.onResize ?? undefined}
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import CustomResizer from './CustomResizer';
|
|||||||
import VerticalResizer from './VerticalResizer';
|
import VerticalResizer from './VerticalResizer';
|
||||||
import HorizontalResizer from './HorizontalResizer';
|
import HorizontalResizer from './HorizontalResizer';
|
||||||
|
|
||||||
import '@xyflow/react/dist/style.css';
|
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
defaultResizer: DefaultResizer,
|
defaultResizer: DefaultResizer,
|
||||||
customResizer: CustomResizer,
|
customResizer: CustomResizer,
|
||||||
@@ -39,6 +37,7 @@ const initialNodes: Node[] = [
|
|||||||
type: 'defaultResizer',
|
type: 'defaultResizer',
|
||||||
data: { label: 'default resizer' },
|
data: { label: 'default resizer' },
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
|
origin: [1, 1],
|
||||||
style: { ...nodeStyle },
|
style: { ...nodeStyle },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -52,7 +51,9 @@ const initialNodes: Node[] = [
|
|||||||
maxHeight: 200,
|
maxHeight: 200,
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 60 },
|
position: { x: 0, y: 60 },
|
||||||
style: { ...nodeStyle, width: 100, height: 80 },
|
width: 100,
|
||||||
|
height: 80,
|
||||||
|
style: { ...nodeStyle },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '1b',
|
id: '1b',
|
||||||
@@ -66,9 +67,9 @@ const initialNodes: Node[] = [
|
|||||||
maxHeight: 400,
|
maxHeight: 400,
|
||||||
},
|
},
|
||||||
position: { x: 250, y: 0 },
|
position: { x: 250, y: 0 },
|
||||||
|
width: 174,
|
||||||
|
height: 123,
|
||||||
style: {
|
style: {
|
||||||
width: 174,
|
|
||||||
height: 123,
|
|
||||||
...nodeStyle,
|
...nodeStyle,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -77,7 +78,9 @@ const initialNodes: Node[] = [
|
|||||||
type: 'customResizer',
|
type: 'customResizer',
|
||||||
data: { label: 'custom resize icon' },
|
data: { label: 'custom resize icon' },
|
||||||
position: { x: 0, y: 200 },
|
position: { x: 0, y: 200 },
|
||||||
style: { width: 100, height: 60, ...nodeStyle },
|
width: 100,
|
||||||
|
height: 60,
|
||||||
|
style: { ...nodeStyle },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
@@ -96,7 +99,8 @@ const initialNodes: Node[] = [
|
|||||||
keepAspectRatio: true,
|
keepAspectRatio: true,
|
||||||
},
|
},
|
||||||
position: { x: 400, y: 200 },
|
position: { x: 400, y: 200 },
|
||||||
style: { ...nodeStyle, height: 50 },
|
height: 50,
|
||||||
|
style: { ...nodeStyle },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
@@ -118,6 +122,50 @@ const initialNodes: Node[] = [
|
|||||||
position: { x: 250, y: 400 },
|
position: { x: 250, y: 400 },
|
||||||
style: { ...nodeStyle },
|
style: { ...nodeStyle },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: 'defaultResizer',
|
||||||
|
data: { label: 'Parent', keepAspectRatio: true },
|
||||||
|
position: { x: 700, y: 0 },
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
style: { ...nodeStyle },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5a',
|
||||||
|
type: 'defaultResizer',
|
||||||
|
data: {
|
||||||
|
label: 'Child with extent: parent',
|
||||||
|
},
|
||||||
|
position: { x: 50, y: 50 },
|
||||||
|
parentId: '5',
|
||||||
|
extent: 'parent',
|
||||||
|
width: 50,
|
||||||
|
height: 100,
|
||||||
|
style: { ...nodeStyle },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5b',
|
||||||
|
type: 'defaultResizer',
|
||||||
|
data: { label: 'Child with expandParent' },
|
||||||
|
position: { x: 100, y: 100 },
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
parentId: '5',
|
||||||
|
expandParent: true,
|
||||||
|
style: { ...nodeStyle },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5c',
|
||||||
|
type: 'defaultResizer',
|
||||||
|
data: { label: 'Child with expandParent & keepAspectRatio' },
|
||||||
|
position: { x: 250, y: 200 },
|
||||||
|
height: 100,
|
||||||
|
width: 100,
|
||||||
|
parentId: '5',
|
||||||
|
expandParent: true,
|
||||||
|
style: { ...nodeStyle },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const CustomNodeFlow = () => {
|
const CustomNodeFlow = () => {
|
||||||
@@ -142,6 +190,7 @@ const CustomNodeFlow = () => {
|
|||||||
maxZoom={5}
|
maxZoom={5}
|
||||||
snapToGrid={snapToGrid}
|
snapToGrid={snapToGrid}
|
||||||
fitView
|
fitView
|
||||||
|
onlyRenderVisibleElements
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
<Panel position="bottom-right">
|
<Panel position="bottom-right">
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ const initialNodes: Node[] = [
|
|||||||
data: { label: 'A Node' },
|
data: { label: 'A Node' },
|
||||||
position: { x: 250, y: 0 },
|
position: { x: 250, y: 0 },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'b',
|
||||||
|
sourcePosition: Position.Right,
|
||||||
|
targetPosition: Position.Left,
|
||||||
|
data: { label: 'B Node' },
|
||||||
|
position: { x: 350, y: 0 },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const buttonStyle: CSSProperties = {
|
const buttonStyle: CSSProperties = {
|
||||||
@@ -66,7 +74,7 @@ const nodeTypesObjects: NodeTypesObject = {
|
|||||||
const NodeTypeChangeFlow = () => {
|
const NodeTypeChangeFlow = () => {
|
||||||
const [nodeTypesId, setNodeTypesId] = useState<string>('a');
|
const [nodeTypesId, setNodeTypesId] = useState<string>('a');
|
||||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||||
const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
|
const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
|
||||||
|
|
||||||
@@ -78,6 +86,7 @@ const NodeTypeChangeFlow = () => {
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
nodeTypes={nodeTypesObjects[nodeTypesId]}
|
nodeTypes={nodeTypesObjects[nodeTypesId]}
|
||||||
|
fitView
|
||||||
>
|
>
|
||||||
<button onClick={changeType} style={buttonStyle}>
|
<button onClick={changeType} style={buttonStyle}>
|
||||||
change type
|
change type
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
Background,
|
Background,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
ConnectionMode,
|
ConnectionMode,
|
||||||
|
OnBeforeDelete,
|
||||||
|
OnDelete,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
|
const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
|
||||||
@@ -23,7 +25,7 @@ const onNodeDragStop = (_: ReactMouseEvent, node: Node, nodes: Node[]) => consol
|
|||||||
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
|
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
|
||||||
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
|
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
|
||||||
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
|
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
|
||||||
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('pane context menu', event);
|
const onPaneContextMenu = (event: ReactMouseEvent | MouseEvent) => console.log('pane context menu', event);
|
||||||
const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
|
const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
|
||||||
const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
|
const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
|
||||||
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
|
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
|
||||||
@@ -45,8 +47,12 @@ const onEdgeMouseEnter = (_: ReactMouseEvent, edge: Edge) => console.log('edge m
|
|||||||
const onEdgeMouseMove = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse move', edge);
|
const onEdgeMouseMove = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse move', edge);
|
||||||
const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
|
const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
|
||||||
const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge);
|
const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge);
|
||||||
const onNodesDelete = (nodes: Node[]) => console.log('nodes delete', nodes);
|
const onBeforeDelete: OnBeforeDelete = async ({ nodes, edges }) => {
|
||||||
const onEdgesDelete = (edges: Edge[]) => console.log('edges delete', edges);
|
console.log('on before delete', nodes, edges);
|
||||||
|
const deleteElements = confirm('Do you want to remove the selected elements?');
|
||||||
|
return deleteElements;
|
||||||
|
};
|
||||||
|
const onDelete: OnDelete = ({ nodes, edges }) => console.log('on delete', nodes, edges);
|
||||||
const onPaneMouseMove = (e: ReactMouseEvent) => console.log('pane move', e.clientX, e.clientY);
|
const onPaneMouseMove = (e: ReactMouseEvent) => console.log('pane move', e.clientX, e.clientY);
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
@@ -226,13 +232,13 @@ const OverviewFlow = () => {
|
|||||||
fitViewOptions={{ padding: 0.1 /*nodes: [{ id: '1' }]*/ }}
|
fitViewOptions={{ padding: 0.1 /*nodes: [{ id: '1' }]*/ }}
|
||||||
attributionPosition="top-right"
|
attributionPosition="top-right"
|
||||||
maxZoom={Infinity}
|
maxZoom={Infinity}
|
||||||
onNodesDelete={onNodesDelete}
|
onBeforeDelete={onBeforeDelete}
|
||||||
onEdgesDelete={onEdgesDelete}
|
onDelete={onDelete}
|
||||||
onPaneMouseMove={onPaneMouseMove}
|
onPaneMouseMove={onPaneMouseMove}
|
||||||
>
|
>
|
||||||
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
|
<MiniMap nodeBorderRadius={2} />
|
||||||
<Controls />
|
<Controls orientation="horizontal" />
|
||||||
<Background color="#aaa" gap={25} />
|
<Background gap={25} />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-16
@@ -2,11 +2,10 @@ import { useState, useCallback, MouseEvent as ReactMouseEvent } from 'react';
|
|||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Controls,
|
Controls,
|
||||||
updateEdge,
|
reconnectEdge,
|
||||||
addEdge,
|
addEdge,
|
||||||
applyNodeChanges,
|
applyNodeChanges,
|
||||||
applyEdgeChanges,
|
applyEdgeChanges,
|
||||||
ReactFlowInstance,
|
|
||||||
Connection,
|
Connection,
|
||||||
Edge,
|
Edge,
|
||||||
Node,
|
Node,
|
||||||
@@ -92,26 +91,24 @@ const initialNodes: Node[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const initialEdges: Edge[] = [
|
const initialEdges: Edge[] = [
|
||||||
{ id: 'e1-3', source: '1', target: '3', label: 'This edge can only be updated from source', updatable: 'source' },
|
{ id: 'e1-3', source: '1', target: '3', label: 'This edge can only be updated from source', reconnectable: 'source' },
|
||||||
{ id: 'e2-4', source: '2', target: '4', label: 'This edge can only be updated from target', updatable: 'target' },
|
{ id: 'e2-4', source: '2', target: '4', label: 'This edge can only be updated from target', reconnectable: 'target' },
|
||||||
{ id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' },
|
{ id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
|
const onReconnectStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
|
||||||
const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
|
|
||||||
console.log(`start update ${handleType} handle`, edge);
|
console.log(`start update ${handleType} handle`, edge);
|
||||||
const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) =>
|
const onReconnectEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) =>
|
||||||
console.log(`end update ${handleType} handle`, edge);
|
console.log(`end update ${handleType} handle`, edge);
|
||||||
|
|
||||||
const UpdatableEdge = () => {
|
const ReconnectEdge = () => {
|
||||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||||
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
|
const onReconnect = (oldEdge: Edge, newConnection: Connection) =>
|
||||||
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
|
||||||
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
|
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
|
||||||
|
|
||||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||||
console.log(changes);
|
|
||||||
setNodes((ns) => applyNodeChanges(changes, ns));
|
setNodes((ns) => applyNodeChanges(changes, ns));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -125,16 +122,16 @@ const UpdatableEdge = () => {
|
|||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onInit={onInit}
|
|
||||||
snapToGrid={true}
|
snapToGrid={true}
|
||||||
onEdgeUpdate={onEdgeUpdate}
|
onReconnect={onReconnect}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
onReconnectStart={onReconnectStart}
|
||||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
onReconnectEnd={onReconnectEnd}
|
||||||
|
fitView
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UpdatableEdge;
|
export default ReconnectEdge;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ReactFlow } from '@xyflow/react';
|
||||||
|
|
||||||
|
import { useDispatch, useSelector, Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { onNodesChange, onEdgesChange, setSelectedNodesAndEdges, store } from './state';
|
||||||
|
|
||||||
|
const OverviewFlow = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const nodes = useSelector((state) => state.myApplication.nodes);
|
||||||
|
const edges = useSelector((state) => state.myApplication.edges);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={(e) => dispatch(onNodesChange(e))}
|
||||||
|
onEdgesChange={(e) => dispatch(onEdgesChange(e))}
|
||||||
|
onSelectionChange={(e) => dispatch(setSelectedNodesAndEdges(e))}
|
||||||
|
fitView
|
||||||
|
attributionPosition="top-right"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<Provider store={store}>
|
||||||
|
<OverviewFlow />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { MarkerType, type Node, type Edge } from '@xyflow/react';
|
||||||
|
|
||||||
|
export const nodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'input',
|
||||||
|
data: {
|
||||||
|
label: 'hey',
|
||||||
|
},
|
||||||
|
position: { x: 250, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
data: {
|
||||||
|
label: 'default node',
|
||||||
|
},
|
||||||
|
position: { x: 100, y: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
data: {
|
||||||
|
label: 'custom style',
|
||||||
|
},
|
||||||
|
position: { x: 400, y: 100 },
|
||||||
|
style: {
|
||||||
|
background: '#D6D5E6',
|
||||||
|
color: '#333',
|
||||||
|
border: '1px solid #222138',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
position: { x: 250, y: 200 },
|
||||||
|
data: {
|
||||||
|
label: 'Another default node',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
data: {
|
||||||
|
label: 'Node id: 5',
|
||||||
|
},
|
||||||
|
position: { x: 250, y: 325 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
type: 'output',
|
||||||
|
data: {
|
||||||
|
label: 'output',
|
||||||
|
},
|
||||||
|
position: { x: 100, y: 480 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
type: 'output',
|
||||||
|
data: { label: 'Another output node' },
|
||||||
|
position: { x: 400, y: 450 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const edges: Edge[] = [
|
||||||
|
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
|
||||||
|
{ id: 'e1-3', source: '1', target: '3' },
|
||||||
|
{
|
||||||
|
id: 'e3-4',
|
||||||
|
source: '3',
|
||||||
|
target: '4',
|
||||||
|
animated: true,
|
||||||
|
label: 'animated edge',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e4-5',
|
||||||
|
source: '4',
|
||||||
|
target: '5',
|
||||||
|
label: 'edge with arrow head',
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e5-6',
|
||||||
|
source: '5',
|
||||||
|
target: '6',
|
||||||
|
type: 'smoothstep',
|
||||||
|
label: 'smooth step edge',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e5-7',
|
||||||
|
source: '5',
|
||||||
|
target: '7',
|
||||||
|
type: 'step',
|
||||||
|
style: { stroke: '#f6ab6c' },
|
||||||
|
label: 'a step edge',
|
||||||
|
animated: true,
|
||||||
|
labelStyle: { fill: '#f6ab6c', fontWeight: 700 },
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { createSlice, configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
|
||||||
|
import { nodes, edges } from './initial-elements';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
selectedNodes: [],
|
||||||
|
selectedEdges: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNodesReducer = (state, action) => {
|
||||||
|
state.nodes = action.payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setEdgesReducer = (state, action) => {
|
||||||
|
state.edges = action.payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNodesChangeReducer = (state, action) => {
|
||||||
|
const a = applyNodeChanges(action.payload, state.nodes);
|
||||||
|
state.nodes = a;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEdgesChangeReducer = (state, action) => {
|
||||||
|
const a = applyEdgeChanges(action.payload, state.edges);
|
||||||
|
state.edges = a;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSelectedNodesAndEdgesReducer = (state, action) => {
|
||||||
|
state.selectedNodes = action.payload.nodes;
|
||||||
|
state.selectedEdges = action.payload.edges;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSelectedNodesReducer = (state, action) => {
|
||||||
|
state.selectedNodes = action.payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MyApplicationSlice = createSlice({
|
||||||
|
name: 'MyApplication',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setNodes: setNodesReducer,
|
||||||
|
setEdges: setEdgesReducer,
|
||||||
|
onNodesChange: onNodesChangeReducer,
|
||||||
|
onEdgesChange: onEdgesChangeReducer,
|
||||||
|
setSelectedNodesAndEdges: setSelectedNodesAndEdgesReducer,
|
||||||
|
setSelectedNodes: setSelectedNodesReducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setNodes, setEdges, onNodesChange, onEdgesChange, setSelectedNodesAndEdges, setSelectedNodes } =
|
||||||
|
MyApplicationSlice.actions;
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
myApplication: MyApplicationSlice.reducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
MiniMap,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
Controls,
|
||||||
|
ReactFlowProvider,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
useReactFlow,
|
||||||
|
Panel,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
const a = { id: 'a', data: { label: 'A' }, position: { x: 250, y: 5 } };
|
||||||
|
const b = { id: 'b', data: { label: 'B' }, position: { x: 100, y: 100 } };
|
||||||
|
const c = { id: 'c', data: { label: 'C' }, position: { x: 400, y: 100 } };
|
||||||
|
|
||||||
|
const SetNotesBatchingFlow = () => {
|
||||||
|
const { setNodes, updateNode } = useReactFlow();
|
||||||
|
|
||||||
|
const triggerMultipleSetNodes = useCallback(() => {
|
||||||
|
setNodes([a]);
|
||||||
|
setNodes((nodes) => [...nodes, b]);
|
||||||
|
setNodes((nodes) => [...nodes, c]);
|
||||||
|
setNodes((nodes) =>
|
||||||
|
nodes.map((node) =>
|
||||||
|
node.id === 'a' ? { ...node, position: { x: node.position.x + 20, y: node.position.y + 20 } } : node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const triggerMultipleUpdateNodes = useCallback(() => {
|
||||||
|
triggerMultipleSetNodes();
|
||||||
|
updateNode('a', (a) => ({ position: { x: a.position.x + 20, y: a.position.y + 20 } }));
|
||||||
|
updateNode('b', (b) => ({ position: { x: b.position.x + 20, y: b.position.y + 20 } }));
|
||||||
|
updateNode('c', (c) => ({ position: { x: c.position.x + 20, y: c.position.y + 20 } }));
|
||||||
|
updateNode('a', (a) => ({ data: { ...a.data, label: `A ${Date.now()}` } }));
|
||||||
|
updateNode('b', (b) => ({ data: { ...b.data, label: `B ${Date.now()}` } }));
|
||||||
|
updateNode('c', (c) => ({ data: { ...c.data, label: `C ${Date.now()}` } }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
defaultNodes={[]}
|
||||||
|
defaultEdges={[]}
|
||||||
|
className="react-flow-basic-example"
|
||||||
|
minZoom={0.2}
|
||||||
|
maxZoom={4}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
<MiniMap />
|
||||||
|
<Controls />
|
||||||
|
|
||||||
|
<Panel position="top-right">
|
||||||
|
<button onClick={triggerMultipleSetNodes}>queue multiple setNodes calls</button>
|
||||||
|
<button onClick={triggerMultipleUpdateNodes}>queue multiple updateNode calls</button>
|
||||||
|
</Panel>
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<SetNotesBatchingFlow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
ReactFlowInstance,
|
|
||||||
Edge,
|
Edge,
|
||||||
Node,
|
Node,
|
||||||
NodeChange,
|
NodeChange,
|
||||||
@@ -12,16 +11,11 @@ import {
|
|||||||
EdgeChange,
|
EdgeChange,
|
||||||
Controls,
|
Controls,
|
||||||
Background,
|
Background,
|
||||||
MiniMap,
|
|
||||||
Panel,
|
Panel,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
import { getNodesAndEdges } from './utils';
|
import { getNodesAndEdges } from './utils';
|
||||||
|
import { FrameRecorder, generateMouseEventParamsTargetingNode, nextFrame } from './performanceUtils';
|
||||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
|
||||||
reactFlowInstance.fitView();
|
|
||||||
console.log(reactFlowInstance.getNodes());
|
|
||||||
};
|
|
||||||
|
|
||||||
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
|
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
|
||||||
|
|
||||||
@@ -31,6 +25,166 @@ const StressFlow = () => {
|
|||||||
const onConnect = useCallback((connection: Connection) => {
|
const onConnect = useCallback((connection: Connection) => {
|
||||||
setEdges((eds) => addEdge(connection, eds));
|
setEdges((eds) => addEdge(connection, eds));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const dragInViewport = async () => {
|
||||||
|
// Note: selecting specifically node 18, as it’s normally located in the right part of the viewport –
|
||||||
|
// which means dragging it left is safe to do without scrolling the viewport.
|
||||||
|
const nodeElement = document.querySelector('.react-flow__node[data-id="18"]');
|
||||||
|
if (!nodeElement) throw new Error('Node with id 18 not found');
|
||||||
|
|
||||||
|
const frameRecorder = new FrameRecorder();
|
||||||
|
|
||||||
|
// Hold down the mouse
|
||||||
|
frameRecorder.setStage('mousedown');
|
||||||
|
const mouseDownEvent = generateMouseEventParamsTargetingNode(nodeElement);
|
||||||
|
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseDownEvent));
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Start at the node position and move the mouse 5px to the left on every frame
|
||||||
|
frameRecorder.setStage('mousemove');
|
||||||
|
let currentXPosition = mouseDownEvent.clientX;
|
||||||
|
for (let iteration = 0; iteration < 20; ++iteration) {
|
||||||
|
const movementX = -5;
|
||||||
|
currentXPosition += movementX;
|
||||||
|
|
||||||
|
nodeElement.dispatchEvent(
|
||||||
|
new MouseEvent('mousemove', {
|
||||||
|
...mouseDownEvent,
|
||||||
|
clientX: currentXPosition,
|
||||||
|
screenX: currentXPosition,
|
||||||
|
movementX,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await nextFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release the mouse
|
||||||
|
frameRecorder.setStage('mouseup');
|
||||||
|
nodeElement.dispatchEvent(
|
||||||
|
new MouseEvent('mouseup', {
|
||||||
|
...mouseDownEvent,
|
||||||
|
clientX: currentXPosition,
|
||||||
|
screenX: currentXPosition,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Log the results
|
||||||
|
await frameRecorder.endRecordingAsync();
|
||||||
|
console.log('Frame durations:', frameRecorder.getFrames());
|
||||||
|
console.log(
|
||||||
|
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
|
||||||
|
frameRecorder.getFramesForObservable()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragOutsideViewport = async () => {
|
||||||
|
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
|
||||||
|
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodes[randomNodeIndex].id}"]`);
|
||||||
|
if (!nodeElement) throw new Error('Node not found');
|
||||||
|
|
||||||
|
const frameRecorder = new FrameRecorder();
|
||||||
|
|
||||||
|
// Hold down the mouse
|
||||||
|
frameRecorder.setStage('mousedown');
|
||||||
|
const mouseDownEvent = generateMouseEventParamsTargetingNode(nodeElement);
|
||||||
|
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseDownEvent));
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Move the mouse to the top of the viewport (so that the viewport starts
|
||||||
|
// scrolling up). Then, wiggle the mouse up and down to keep the viewport
|
||||||
|
// scrolling.
|
||||||
|
frameRecorder.setStage('mousemove');
|
||||||
|
let currentYPosition = 50;
|
||||||
|
for (let iteration = 0; iteration < 20; ++iteration) {
|
||||||
|
const movementY = Math.random() > 0.5 ? +2 : -2;
|
||||||
|
currentYPosition += movementY;
|
||||||
|
|
||||||
|
nodeElement.dispatchEvent(
|
||||||
|
new MouseEvent('mousemove', {
|
||||||
|
...mouseDownEvent,
|
||||||
|
clientY: currentYPosition,
|
||||||
|
screenY: currentYPosition,
|
||||||
|
movementY,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await nextFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release the mouse
|
||||||
|
frameRecorder.setStage('mouseup');
|
||||||
|
nodeElement.dispatchEvent(
|
||||||
|
new MouseEvent('mouseup', {
|
||||||
|
...mouseDownEvent,
|
||||||
|
clientY: currentYPosition,
|
||||||
|
screenY: currentYPosition,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Log the results
|
||||||
|
await frameRecorder.endRecordingAsync();
|
||||||
|
console.log('Frame durations:', frameRecorder.getFrames());
|
||||||
|
console.log(
|
||||||
|
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
|
||||||
|
frameRecorder.getFramesForObservable()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectNode = async () => {
|
||||||
|
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
|
||||||
|
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodes[randomNodeIndex].id}"]`);
|
||||||
|
if (!nodeElement) throw new Error('Node not found');
|
||||||
|
|
||||||
|
const frameRecorder = new FrameRecorder();
|
||||||
|
|
||||||
|
const mouseEvent = generateMouseEventParamsTargetingNode(nodeElement);
|
||||||
|
|
||||||
|
// mousedown
|
||||||
|
frameRecorder.setStage('mousedown');
|
||||||
|
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseEvent));
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// click
|
||||||
|
frameRecorder.setStage('click');
|
||||||
|
nodeElement.dispatchEvent(new MouseEvent('click', mouseEvent));
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// mouseup
|
||||||
|
frameRecorder.setStage('mouseup');
|
||||||
|
nodeElement.dispatchEvent(new MouseEvent('mouseup', mouseEvent));
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Log the results
|
||||||
|
await frameRecorder.endRecordingAsync();
|
||||||
|
console.log('Frame durations:', frameRecorder.getFrames());
|
||||||
|
console.log(
|
||||||
|
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
|
||||||
|
frameRecorder.getFramesForObservable()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
const frameRecorderRef = useRef<FrameRecorder | null>(null);
|
||||||
|
function remount() {
|
||||||
|
frameRecorderRef.current = new FrameRecorder();
|
||||||
|
setKey((k) => k + 1);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
const frameRecorder = frameRecorderRef.current;
|
||||||
|
if (!frameRecorder) return;
|
||||||
|
|
||||||
|
frameRecorder.endRecordingAsync().then(() => {
|
||||||
|
console.log('Frame durations:', frameRecorder.getFrames());
|
||||||
|
console.log(
|
||||||
|
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
|
||||||
|
frameRecorder.getFramesForObservable()
|
||||||
|
);
|
||||||
|
|
||||||
|
frameRecorderRef.current = null;
|
||||||
|
});
|
||||||
|
}, [key]);
|
||||||
|
|
||||||
const updatePos = () => {
|
const updatePos = () => {
|
||||||
setNodes((nds) => {
|
setNodes((nds) => {
|
||||||
return nds.map((n) => {
|
return nds.map((n) => {
|
||||||
@@ -62,19 +216,23 @@ const StressFlow = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
key={key}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onInit={onInit}
|
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgeChange}
|
onEdgesChange={onEdgeChange}
|
||||||
minZoom={0.2}
|
minZoom={0.2}
|
||||||
|
fitView
|
||||||
>
|
>
|
||||||
<MiniMap />
|
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background />
|
<Background />
|
||||||
|
|
||||||
<Panel position="top-right">
|
<Panel position="top-right">
|
||||||
|
<button onClick={selectNode}>select node</button>
|
||||||
|
<button onClick={dragInViewport}>drag node within the viewport</button>
|
||||||
|
<button onClick={dragOutsideViewport}>drag node outside of the viewport</button>
|
||||||
|
<button onClick={remount}>re-mount</button>
|
||||||
<button onClick={updatePos}>change pos</button>
|
<button onClick={updatePos}>change pos</button>
|
||||||
<button onClick={updateElements}>update elements</button>
|
<button onClick={updateElements}>update elements</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
type Frame = {
|
||||||
|
duration: number;
|
||||||
|
stage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures and outputs the duration of every frame that happens between the
|
||||||
|
* instance is created and `endRecording()` is called.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const recorder = new FrameRecorder();
|
||||||
|
*
|
||||||
|
* // Do some performance-intensive stuff
|
||||||
|
*
|
||||||
|
* await recorder.endRecordingAsync();
|
||||||
|
*
|
||||||
|
* console.log(recorder.getFrames());
|
||||||
|
* console.log(recorder.getFramesForObservable()); // → paste into https://observablehq.com/@iamakulov/long-frame-visualizer
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class FrameRecorder {
|
||||||
|
private frames: Frame[] = [];
|
||||||
|
private animationFrameId: number;
|
||||||
|
private stage: string = '<no stage>';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
let lastFrameTimestamp = performance.now();
|
||||||
|
|
||||||
|
const measureFrame = () => {
|
||||||
|
const timestamp = performance.now();
|
||||||
|
|
||||||
|
// Visualize the frames in the Performance pane (see the collapsed
|
||||||
|
// “Timings” section) – so it’s easier to see what exactly each frame
|
||||||
|
// captured
|
||||||
|
performance.measure(`frame (${this.stage})`, {
|
||||||
|
start: lastFrameTimestamp,
|
||||||
|
end: timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.frames.push({
|
||||||
|
duration: timestamp - lastFrameTimestamp,
|
||||||
|
stage: this.stage,
|
||||||
|
});
|
||||||
|
|
||||||
|
lastFrameTimestamp = timestamp;
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(measureFrame);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(measureFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The method is explicitly marked `async` in its name to make sure the caller
|
||||||
|
// doesn’t forget to `await` it. (Otherwise, some events might be lost.)
|
||||||
|
async endRecordingAsync() {
|
||||||
|
this.setStage('waiting for idle');
|
||||||
|
await new Promise((resolve) => requestIdleCallback(resolve));
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an optional annotation to all subsequent frames. Useful to
|
||||||
|
* differentiate frames from different events – e.g. you can call
|
||||||
|
* `setState("mousedown")` before dispatching a mousedown event, and then
|
||||||
|
* `setState("mouseup")` before a mouseup one.
|
||||||
|
*
|
||||||
|
* When used, will affect both `getFramesForObservable()` and `getFrames()`.
|
||||||
|
*/
|
||||||
|
setStage(stage: string) {
|
||||||
|
this.stage = stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFramesForObservable() {
|
||||||
|
return this.frames.map((frame, index) => ({ ...frame, index }));
|
||||||
|
}
|
||||||
|
|
||||||
|
getFrames() {
|
||||||
|
// Group frames by stage – so you could see which frames originated from `mousedown` vs `mousemove` vs `mouseup` events
|
||||||
|
const framesPerStage: Record<string, number[]> = {};
|
||||||
|
for (const frame of this.frames) {
|
||||||
|
const stage = frame.stage;
|
||||||
|
if (!framesPerStage[stage]) {
|
||||||
|
framesPerStage[stage] = [];
|
||||||
|
}
|
||||||
|
framesPerStage[stage].push(frame.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there’s only one stage, return the frames directly
|
||||||
|
return framesPerStage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a promise that resolves when the next frame starts, and everything
|
||||||
|
* that was already scheduled in the event queue has been processed.
|
||||||
|
*/
|
||||||
|
export function nextFrame() {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates params for a new MouseEvent() that will target the given node.
|
||||||
|
*/
|
||||||
|
export function generateMouseEventParamsTargetingNode(node: Element) {
|
||||||
|
const nodePosition = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Let’s make the event (eg click) happen 5px to the right and 5px to the
|
||||||
|
// bottom of the node’s top-left corner
|
||||||
|
const positionRelativeToNode = {
|
||||||
|
left: 5,
|
||||||
|
top: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientX: Math.round(nodePosition.left + positionRelativeToNode.left),
|
||||||
|
clientY: Math.round(nodePosition.top + positionRelativeToNode.top),
|
||||||
|
movementX: 0,
|
||||||
|
movementY: 0,
|
||||||
|
offsetX: positionRelativeToNode.left,
|
||||||
|
offsetY: positionRelativeToNode.top,
|
||||||
|
screenX: Math.round(nodePosition.left + positionRelativeToNode.left),
|
||||||
|
screenY: Math.round(nodePosition.top + positionRelativeToNode.top),
|
||||||
|
|
||||||
|
// Required boilerplate
|
||||||
|
altKey: false,
|
||||||
|
bubbles: true,
|
||||||
|
button: 0,
|
||||||
|
buttons: 1,
|
||||||
|
cancelBubble: false,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
ctrlKey: false,
|
||||||
|
currentTarget: null,
|
||||||
|
defaultPrevented: false,
|
||||||
|
detail: 1,
|
||||||
|
eventPhase: 0,
|
||||||
|
fromElement: null,
|
||||||
|
isTrusted: true,
|
||||||
|
metaKey: false,
|
||||||
|
relatedTarget: null,
|
||||||
|
returnValue: true,
|
||||||
|
shiftKey: false,
|
||||||
|
view: window,
|
||||||
|
which: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -20,8 +20,6 @@ export function getNodesAndEdges(xElements = 10, yElements = 10): ElementsCollec
|
|||||||
style: { width: 50, height: 30, fontSize: 11 },
|
style: { width: 50, height: 30, fontSize: 11 },
|
||||||
data,
|
data,
|
||||||
position,
|
position,
|
||||||
width: 50,
|
|
||||||
height: 30,
|
|
||||||
};
|
};
|
||||||
initialNodes.push(node);
|
initialNodes.push(node);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { memo, FC, CSSProperties } from 'react';
|
import React, { memo, FC, CSSProperties } from 'react';
|
||||||
|
|
||||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
import { Handle, NodeProps, Position, useInternalNode } from '@xyflow/react';
|
||||||
|
|
||||||
const infoStyle: CSSProperties = { fontSize: 11 };
|
const infoStyle: CSSProperties = { fontSize: 11 };
|
||||||
const idStyle: CSSProperties = {
|
const idStyle: CSSProperties = {
|
||||||
@@ -11,13 +11,18 @@ const idStyle: CSSProperties = {
|
|||||||
left: 2,
|
left: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DebugNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
|
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => {
|
||||||
|
const node = useInternalNode(id)!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Handle type="target" position={Position.Top} />
|
<Handle type="target" position={Position.Top} />
|
||||||
<div style={idStyle}>{id}</div>
|
<div style={idStyle}>{id}</div>
|
||||||
<div style={infoStyle}>
|
<div style={infoStyle}>
|
||||||
x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex}
|
x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex}
|
||||||
|
</div>
|
||||||
|
<div style={infoStyle}>
|
||||||
|
x:{Math.round(node.position.x)} y:{Math.round(node.position.y)}
|
||||||
</div>
|
</div>
|
||||||
<Handle type="source" position={Position.Bottom} />
|
<Handle type="source" position={Position.Bottom} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import {
|
|||||||
MiniMap,
|
MiniMap,
|
||||||
Background,
|
Background,
|
||||||
Panel,
|
Panel,
|
||||||
|
NodeOrigin,
|
||||||
|
useUpdateNodeInternals,
|
||||||
|
ReactFlowProvider,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
import DebugNode from './DebugNode';
|
import DebugNode from './DebugNode';
|
||||||
@@ -49,7 +52,7 @@ const initialNodes: Node[] = [
|
|||||||
data: { label: 'Node 4a' },
|
data: { label: 'Node 4a' },
|
||||||
position: { x: 15, y: 15 },
|
position: { x: 15, y: 15 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
parentNode: '4',
|
parentId: '4',
|
||||||
origin: [0.5, 0.5],
|
origin: [0.5, 0.5],
|
||||||
|
|
||||||
extent: [
|
extent: [
|
||||||
@@ -67,21 +70,21 @@ const initialNodes: Node[] = [
|
|||||||
height: 200,
|
height: 200,
|
||||||
width: 300,
|
width: 300,
|
||||||
},
|
},
|
||||||
parentNode: '4',
|
parentId: '4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4b1',
|
id: '4b1',
|
||||||
data: { label: 'Node 4b1' },
|
data: { label: 'Node 4b1' },
|
||||||
position: { x: 40, y: 20 },
|
position: { x: 40, y: 20 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
parentNode: '4b',
|
parentId: '4b',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4b2',
|
id: '4b2',
|
||||||
data: { label: 'Node 4b2' },
|
data: { label: 'Node 4b2' },
|
||||||
position: { x: 20, y: 100 },
|
position: { x: 20, y: 100 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
parentNode: '4b',
|
parentId: '4b',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '5',
|
id: '5',
|
||||||
@@ -89,7 +92,7 @@ const initialNodes: Node[] = [
|
|||||||
data: { label: 'Node 5' },
|
data: { label: 'Node 5' },
|
||||||
position: { x: 650, y: 250 },
|
position: { x: 650, y: 250 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
style: { width: 400, height: 150 },
|
style: { width: 100, height: 100 },
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -97,15 +100,15 @@ const initialNodes: Node[] = [
|
|||||||
data: { label: 'Node 5a' },
|
data: { label: 'Node 5a' },
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
parentNode: '5',
|
parentId: '5',
|
||||||
extent: 'parent',
|
extent: 'parent',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '5b',
|
id: '5b',
|
||||||
data: { label: 'Node 5b' },
|
data: { label: 'Node 5b' },
|
||||||
position: { x: 225, y: 50 },
|
position: { x: 200, y: 200 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
parentNode: '5',
|
parentId: '5',
|
||||||
expandParent: true,
|
expandParent: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -119,6 +122,7 @@ const initialNodes: Node[] = [
|
|||||||
data: { label: 'Node 3' },
|
data: { label: 'Node 3' },
|
||||||
position: { x: 400, y: 100 },
|
position: { x: 400, y: 100 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
extent: 'parent',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -149,6 +153,7 @@ const nodeTypes = {
|
|||||||
|
|
||||||
const Subflow = () => {
|
const Subflow = () => {
|
||||||
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
|
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
@@ -158,10 +163,13 @@ const Subflow = () => {
|
|||||||
const updatePos = () => {
|
const updatePos = () => {
|
||||||
setNodes((nds) => {
|
setNodes((nds) => {
|
||||||
return nds.map((n) => {
|
return nds.map((n) => {
|
||||||
if (!n.parentNode) {
|
if (!n.parentId) {
|
||||||
n.position = {
|
return {
|
||||||
x: Math.random() * 400,
|
...n,
|
||||||
y: Math.random() * 400,
|
position: {
|
||||||
|
x: Math.random() * 400,
|
||||||
|
y: Math.random() * 400,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,8 +184,10 @@ const Subflow = () => {
|
|||||||
const toggleClassnames = () => {
|
const toggleClassnames = () => {
|
||||||
setNodes((nds) => {
|
setNodes((nds) => {
|
||||||
return nds.map((n) => {
|
return nds.map((n) => {
|
||||||
n.className = n.className === 'light' ? 'dark' : 'light';
|
return {
|
||||||
return n;
|
...n,
|
||||||
|
className: n.className === 'light' ? 'dark' : 'light',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -185,8 +195,10 @@ const Subflow = () => {
|
|||||||
const toggleChildNodes = () => {
|
const toggleChildNodes = () => {
|
||||||
setNodes((nds) => {
|
setNodes((nds) => {
|
||||||
return nds.map((n) => {
|
return nds.map((n) => {
|
||||||
n.hidden = !!n.parentNode && !n.hidden;
|
return {
|
||||||
return n;
|
...n,
|
||||||
|
hidden: !!n.parentId && !n.hidden,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -207,28 +219,27 @@ const Subflow = () => {
|
|||||||
onlyRenderVisibleElements={false}
|
onlyRenderVisibleElements={false}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
fitView
|
fitView
|
||||||
|
nodeOrigin={[0, 0]}
|
||||||
>
|
>
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background />
|
<Background />
|
||||||
|
|
||||||
<Panel position="top-right">
|
<Panel position="top-right">
|
||||||
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
<button onClick={resetTransform}>reset transform</button>
|
||||||
reset transform
|
<button onClick={updatePos}>change pos</button>
|
||||||
</button>
|
<button onClick={toggleClassnames}>toggle classnames</button>
|
||||||
<button onClick={updatePos} style={{ marginRight: 5 }}>
|
<button onClick={toggleChildNodes}>toggleChildNodes</button>
|
||||||
change pos
|
|
||||||
</button>
|
|
||||||
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
|
|
||||||
toggle classnames
|
|
||||||
</button>
|
|
||||||
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
|
|
||||||
toggleChildNodes
|
|
||||||
</button>
|
|
||||||
<button onClick={logToObject}>toObject</button>
|
<button onClick={logToObject}>toObject</button>
|
||||||
|
<button onClick={() => setNodes(initialNodes)}>setNodes</button>
|
||||||
|
<button onClick={() => updateNodeInternals(nodes.map((node) => node.id))}>updateNodeInternals</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Subflow;
|
export default () => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<Subflow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { ReactFlow, Node, Edge, useNodesState, useEdgesState, Position, Connection, addEdge } from '@xyflow/react';
|
import { ReactFlow, Node, Edge, useNodesState, useEdgesState, Position, Connection, addEdge } from '@xyflow/react';
|
||||||
|
|
||||||
import styles from './touch-device.module.css';
|
import './touch-device.css';
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
{
|
{
|
||||||
@@ -42,7 +42,7 @@ const TouchDeviceFlow = () => {
|
|||||||
onConnectEnd={onConnectEnd}
|
onConnectEnd={onConnectEnd}
|
||||||
onClickConnectStart={onClickConnectStart}
|
onClickConnectStart={onClickConnectStart}
|
||||||
onClickConnectEnd={onClickConnectEnd}
|
onClickConnectEnd={onClickConnectEnd}
|
||||||
className={styles.flow}
|
className="touch-flow"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.react-flow.touch-flow .react-flow__handle {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #9f7aea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch-flow .react-flow__handle-right {
|
||||||
|
--translate: translate(50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch-flow .react-flow__handle-left {
|
||||||
|
--translate: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0% {
|
||||||
|
transform: var(--translate) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: var(--translate) scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow.touch-flow .react-flow__handle.clickconnecting {
|
||||||
|
animation: bounce 1600ms infinite ease-in;
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
.flow :global .react-flow__handle {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: #9f7aea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flow :global .react-flow__handle.connecting {
|
|
||||||
animation: bounce 1600ms infinite ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce {
|
|
||||||
0% {
|
|
||||||
transform: translate(0, -50%) scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(0, -50%) scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Edge,
|
Edge,
|
||||||
ConnectionLineType,
|
ConnectionLineType,
|
||||||
ConnectionMode,
|
ConnectionMode,
|
||||||
updateEdge,
|
reconnectEdge,
|
||||||
useNodesState,
|
useNodesState,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
@@ -22,55 +22,55 @@ const initialNodes: Node[] = [
|
|||||||
id: '00',
|
id: '00',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
position: { x: 300, y: 250 },
|
position: { x: 300, y: 250 },
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '01',
|
id: '01',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
position: { x: 100, y: 50 },
|
position: { x: 100, y: 50 },
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '02',
|
id: '02',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
position: { x: 500, y: 50 },
|
position: { x: 500, y: 50 },
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '03',
|
id: '03',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
position: { x: 500, y: 500 },
|
position: { x: 500, y: 500 },
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '04',
|
id: '04',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
position: { x: 100, y: 500 },
|
position: { x: 100, y: 500 },
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '10',
|
id: '10',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
position: { x: 300, y: 5 },
|
position: { x: 300, y: 5 },
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '20',
|
id: '20',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
position: { x: 600, y: 250 },
|
position: { x: 600, y: 250 },
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '30',
|
id: '30',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
position: { x: 300, y: 600 },
|
position: { x: 300, y: 600 },
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '40',
|
id: '40',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
position: { x: 5, y: 250 },
|
position: { x: 5, y: 250 },
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -183,11 +183,11 @@ const getId = () => `${id++}`;
|
|||||||
const UpdateNodeInternalsFlow = () => {
|
const UpdateNodeInternalsFlow = () => {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
const { project } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
|
|
||||||
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
||||||
const onEdgeUpdate = useCallback(
|
const onReconnect = useCallback(
|
||||||
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)),
|
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -196,12 +196,12 @@ const UpdateNodeInternalsFlow = () => {
|
|||||||
setNodes((nds) =>
|
setNodes((nds) =>
|
||||||
nds.concat({
|
nds.concat({
|
||||||
id: getId(),
|
id: getId(),
|
||||||
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
|
position: screenToFlowPosition({ x: evt.clientX, y: evt.clientY }),
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
[project, setNodes]
|
[screenToFlowPosition, setNodes]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -215,7 +215,7 @@ const UpdateNodeInternalsFlow = () => {
|
|||||||
onPaneClick={onPaneClick}
|
onPaneClick={onPaneClick}
|
||||||
connectionLineType={ConnectionLineType.Bezier}
|
connectionLineType={ConnectionLineType.Bezier}
|
||||||
connectionMode={ConnectionMode.Loose}
|
connectionMode={ConnectionMode.Loose}
|
||||||
onEdgeUpdate={onEdgeUpdate}
|
onReconnect={onReconnect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ReactFlow, Node, Edge, useNodesState, useEdgesState } from '@xyflow/react';
|
import { ReactFlow, Node, Edge, useNodesState, useEdgesState, ReactFlowProvider, useReactFlow } from '@xyflow/react';
|
||||||
|
|
||||||
import styles from './updatenode.module.css';
|
import styles from './updatenode.module.css';
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
|
|||||||
const UpdateNode = () => {
|
const UpdateNode = () => {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const { updateNode } = useReactFlow();
|
||||||
|
|
||||||
const [nodeName, setNodeName] = useState<string>('Node 1');
|
const [nodeName, setNodeName] = useState<string>('Node 1');
|
||||||
const [nodeBg, setNodeBg] = useState<string>('#eee');
|
const [nodeBg, setNodeBg] = useState<string>('#eee');
|
||||||
@@ -23,9 +24,12 @@ const UpdateNode = () => {
|
|||||||
nds.map((n) => {
|
nds.map((n) => {
|
||||||
if (n.id === '1') {
|
if (n.id === '1') {
|
||||||
// it's important that you create a new object here in order to notify react flow about the change
|
// it's important that you create a new object here in order to notify react flow about the change
|
||||||
n.data = {
|
return {
|
||||||
...n.data,
|
...n,
|
||||||
label: nodeName,
|
data: {
|
||||||
|
...n.data,
|
||||||
|
label: nodeName,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +43,10 @@ const UpdateNode = () => {
|
|||||||
nds.map((n) => {
|
nds.map((n) => {
|
||||||
if (n.id === '1') {
|
if (n.id === '1') {
|
||||||
// it's important that you create a new object here in order to notify react flow about the change
|
// it's important that you create a new object here in order to notify react flow about the change
|
||||||
n.style = { ...n.style, backgroundColor: nodeBg };
|
return {
|
||||||
|
...n,
|
||||||
|
style: { ...n.style, backgroundColor: nodeBg },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return n;
|
return n;
|
||||||
@@ -51,8 +58,10 @@ const UpdateNode = () => {
|
|||||||
setNodes((nds) =>
|
setNodes((nds) =>
|
||||||
nds.map((n) => {
|
nds.map((n) => {
|
||||||
if (n.id === '1' || n.id === 'e1-2') {
|
if (n.id === '1' || n.id === 'e1-2') {
|
||||||
// when you update a simple type you can just update the value
|
return {
|
||||||
n.hidden = nodeHidden;
|
...n,
|
||||||
|
hidden: nodeHidden,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return n;
|
return n;
|
||||||
@@ -80,9 +89,19 @@ const UpdateNode = () => {
|
|||||||
<label>hidden:</label>
|
<label>hidden:</label>
|
||||||
<input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} />
|
<input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => updateNode('1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }))}
|
||||||
|
>
|
||||||
|
update position
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UpdateNode;
|
export default () => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<UpdateNode />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Background,
|
||||||
|
MiniMap,
|
||||||
|
Node,
|
||||||
|
addEdge,
|
||||||
|
ReactFlowProvider,
|
||||||
|
Edge,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
OnConnect,
|
||||||
|
useConnection,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
const initialNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'input',
|
||||||
|
data: { label: 'Node 1' },
|
||||||
|
position: { x: 250, y: 5 },
|
||||||
|
className: 'light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
data: { label: 'Node 2' },
|
||||||
|
position: { x: 100, y: 100 },
|
||||||
|
className: 'light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
data: { label: 'Node 3' },
|
||||||
|
position: { x: 400, y: 100 },
|
||||||
|
className: 'light',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialEdges: Edge[] = [
|
||||||
|
{ id: 'e1-2', source: '1', target: '2' },
|
||||||
|
{ id: 'e1-3', source: '1', target: '3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const UseZoomPanHelperFlow = () => {
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
|
const onConnect: OnConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []);
|
||||||
|
const connection = useConnection();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('connection', connection);
|
||||||
|
}, [connection]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
<MiniMap />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const WrappedFlow = () => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<UseZoomPanHelperFlow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default WrappedFlow;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { memo, FC, useEffect, useCallback } from 'react';
|
||||||
|
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleProps } from '@xyflow/react';
|
||||||
|
|
||||||
|
function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }) {
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(connections: Connection[]) => console.log('onConnect handler, node id:', nodeId, connections),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDisconnect = useCallback(
|
||||||
|
(connections: Connection[]) => console.log('onDisconnect handler, node id:', nodeId, connections),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
const connections = useHandleConnections({
|
||||||
|
type: handleProps.type,
|
||||||
|
id: handleProps.id,
|
||||||
|
onConnect,
|
||||||
|
onDisconnect,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('useEffect, node id:', nodeId, handleProps.type, connections);
|
||||||
|
}, [connections]);
|
||||||
|
|
||||||
|
return <Handle {...handleProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomNode: FC<NodeProps> = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#333', color: '#fff', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<CustomHandle nodeId={id} type="target" position={Position.Left} />
|
||||||
|
<div>node {id}</div>
|
||||||
|
<CustomHandle nodeId={id} type="source" position={Position.Right} id="a" style={{ top: 10 }} />
|
||||||
|
<CustomHandle nodeId={id} type="source" position={Position.Right} id="b" style={{ top: 20 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CustomNode);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { memo, FC, useEffect, useCallback } from 'react';
|
||||||
|
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleProps } from '@xyflow/react';
|
||||||
|
|
||||||
|
function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }) {
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(connections: Connection[]) => {
|
||||||
|
console.log('onConnect handler, node id:', nodeId, connections);
|
||||||
|
},
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
const onDisconnect = useCallback(
|
||||||
|
(connections: Connection[]) => {
|
||||||
|
console.log('onDisconnect handler, node id:', nodeId, connections);
|
||||||
|
},
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
const connections = useHandleConnections({
|
||||||
|
type: handleProps.type,
|
||||||
|
id: handleProps.id,
|
||||||
|
onConnect,
|
||||||
|
onDisconnect,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('useEffect, node id:', nodeId, handleProps.type, connections);
|
||||||
|
}, [connections]);
|
||||||
|
|
||||||
|
return <Handle {...handleProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomNode: FC<NodeProps> = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#333', color: '#fff', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<CustomHandle nodeId={id} type="target" position={Position.Left} />
|
||||||
|
<div>node {id}</div>
|
||||||
|
<CustomHandle nodeId={id} type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CustomNode);
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
MiniMap,
|
||||||
|
Controls,
|
||||||
|
addEdge,
|
||||||
|
Connection,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
Background,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
import MultiHandleNode from './MultiHandleNode';
|
||||||
|
import SingleHandleNode from './SingleHandleNode';
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
multi: MultiHandleNode,
|
||||||
|
single: SingleHandleNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initNodes = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'single',
|
||||||
|
data: {},
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'single',
|
||||||
|
data: {},
|
||||||
|
position: { x: 200, y: -100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'single',
|
||||||
|
data: {},
|
||||||
|
position: { x: 200, y: 100 },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
type: 'multi',
|
||||||
|
data: {},
|
||||||
|
position: { x: 400, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: 'multi',
|
||||||
|
data: {},
|
||||||
|
position: { x: 600, y: -100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
type: 'multi',
|
||||||
|
data: {},
|
||||||
|
position: { x: 600, y: 100 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initEdges = [
|
||||||
|
{
|
||||||
|
id: 'e1-2',
|
||||||
|
source: '1',
|
||||||
|
target: '2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e1-3',
|
||||||
|
source: '1',
|
||||||
|
target: '3',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'e4a-5',
|
||||||
|
source: '4',
|
||||||
|
sourceHandle: 'a',
|
||||||
|
target: '5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e4b-5',
|
||||||
|
source: '4',
|
||||||
|
sourceHandle: 'b',
|
||||||
|
target: '6',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultEdgeOptions = {
|
||||||
|
animated: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomNodeFlow = () => {
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||||
|
|
||||||
|
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
minZoom={0.3}
|
||||||
|
maxZoom={2}
|
||||||
|
colorMode="dark"
|
||||||
|
defaultEdgeOptions={defaultEdgeOptions}
|
||||||
|
>
|
||||||
|
<MiniMap />
|
||||||
|
<Controls />
|
||||||
|
<Background />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomNodeFlow;
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { useKeyPress } from '@xyflow/react';
|
import { useKeyPress } from '@xyflow/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
const UseKeyPressComponent = () => {
|
const UseKeyPressComponent = () => {
|
||||||
const metaPressed = useKeyPress(['Meta']);
|
const metaPressed = useKeyPress(['Meta']);
|
||||||
|
|
||||||
console.log({ metaPressed });
|
useEffect(() => {
|
||||||
|
console.log({ metaPressed });
|
||||||
|
}, [metaPressed]);
|
||||||
|
|
||||||
return <div />;
|
return <div />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { Handle, Position, useHandleConnections, useNodesData } from '@xyflow/react';
|
||||||
|
import { isTextNode, type MyNode } from '.';
|
||||||
|
|
||||||
|
function ResultNode() {
|
||||||
|
const connections = useHandleConnections({
|
||||||
|
type: 'target',
|
||||||
|
});
|
||||||
|
const nodesData = useNodesData<MyNode>(connections.map((connection) => connection.source));
|
||||||
|
const textNodes = nodesData.filter(isTextNode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<div>incoming texts: {textNodes.map(({ data }, i) => <div key={i}>{data.text}</div>) || 'none'}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ResultNode);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { Position, NodeProps, Handle, useReactFlow } from '@xyflow/react';
|
||||||
|
|
||||||
|
function TextNode({ id, data }: NodeProps) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
const [text, setText] = useState(data.text);
|
||||||
|
const updateText = (text: string) => {
|
||||||
|
// avoid jumping caret with a synchronous update
|
||||||
|
setText(text);
|
||||||
|
// update actual node data
|
||||||
|
updateNodeData(id, { text });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<div>node {id}</div>
|
||||||
|
<div style={{ marginTop: 5 }}>
|
||||||
|
<label style={{ fontSize: 12 }}>useState + updateNodeData</label>
|
||||||
|
<input onChange={(evt) => updateText(evt.target.value)} value={text} style={{ display: 'block' }} />
|
||||||
|
|
||||||
|
<label style={{ fontSize: 12 }}>updateNodeData</label>
|
||||||
|
<input
|
||||||
|
onChange={(evt) => updateNodeData(id, { text: evt.target.value })}
|
||||||
|
value={data.text}
|
||||||
|
style={{ display: 'block' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(TextNode);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { memo, useEffect } from 'react';
|
||||||
|
import { Position, NodeProps, useReactFlow, Handle, useHandleConnections, useNodesData } from '@xyflow/react';
|
||||||
|
import { isTextNode, type TextNode, type MyNode } from '.';
|
||||||
|
|
||||||
|
function UppercaseNode({ id }: NodeProps) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
const connections = useHandleConnections({
|
||||||
|
type: 'target',
|
||||||
|
});
|
||||||
|
const nodesData = useNodesData<MyNode>(connections[0]?.source);
|
||||||
|
const textNode = isTextNode(nodesData) ? nodesData : null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateNodeData(id, { text: textNode?.data.text.toUpperCase() });
|
||||||
|
}, [textNode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<Handle type="target" position={Position.Left} isConnectable={connections.length === 0} />
|
||||||
|
<div>uppercase transform</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(UppercaseNode);
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Controls,
|
||||||
|
addEdge,
|
||||||
|
Connection,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
Background,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
import TextNode from './TextNode';
|
||||||
|
import ResultNode from './ResultNode';
|
||||||
|
import UppercaseNode from './UppercaseNode';
|
||||||
|
|
||||||
|
export type TextNode = Node<{ text: string }, 'text'>;
|
||||||
|
export type ResultNode = Node<{}, 'result'>;
|
||||||
|
export type UppercaseNode = Node<{ text: string }, 'uppercase'>;
|
||||||
|
export type MyNode = TextNode | ResultNode | UppercaseNode;
|
||||||
|
|
||||||
|
export function isTextNode(node: any): node is TextNode | UppercaseNode {
|
||||||
|
return node.type === 'text' || node.type === 'uppercase';
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
text: TextNode,
|
||||||
|
result: ResultNode,
|
||||||
|
uppercase: UppercaseNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initNodes: MyNode[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'text',
|
||||||
|
data: {
|
||||||
|
text: 'hello',
|
||||||
|
},
|
||||||
|
position: { x: -100, y: -50 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1a',
|
||||||
|
type: 'uppercase',
|
||||||
|
data: { text: '' },
|
||||||
|
position: { x: 100, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1b',
|
||||||
|
type: 'uppercase',
|
||||||
|
data: { text: '' },
|
||||||
|
position: { x: 100, y: -100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'text',
|
||||||
|
data: {
|
||||||
|
text: 'world',
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3a',
|
||||||
|
type: 'result',
|
||||||
|
data: {},
|
||||||
|
position: { x: 300, y: -75 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3b',
|
||||||
|
type: 'result',
|
||||||
|
data: {},
|
||||||
|
position: { x: 300, y: 50 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initEdges: Edge[] = [
|
||||||
|
{
|
||||||
|
id: 'e1-1a',
|
||||||
|
source: '1',
|
||||||
|
target: '1a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e1a-3a',
|
||||||
|
source: '1b',
|
||||||
|
target: '3a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e1-1b',
|
||||||
|
source: '1',
|
||||||
|
target: '1b',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e1a-3b',
|
||||||
|
source: '1a',
|
||||||
|
target: '3b',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e2-3b',
|
||||||
|
source: '2',
|
||||||
|
target: '3b',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CustomNodeFlow = () => {
|
||||||
|
const [nodes, , onNodesChange] = useNodesState(initNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||||
|
|
||||||
|
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<Controls />
|
||||||
|
<Background />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomNodeFlow;
|
||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
useEdgesState,
|
useEdgesState,
|
||||||
OnConnect,
|
OnConnect,
|
||||||
useNodesInitialized,
|
useNodesInitialized,
|
||||||
|
Panel,
|
||||||
|
useReactFlow,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
@@ -49,6 +51,7 @@ const initialEdges: Edge[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const UseZoomPanHelperFlow = () => {
|
const UseZoomPanHelperFlow = () => {
|
||||||
|
const { addNodes } = useReactFlow();
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
@@ -59,6 +62,13 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
console.log('initialized', initialized);
|
console.log('initialized', initialized);
|
||||||
}, [initialized]);
|
}, [initialized]);
|
||||||
|
|
||||||
|
const addNode = () =>
|
||||||
|
addNodes({
|
||||||
|
id: `${Math.random()}`,
|
||||||
|
data: { label: 'new node' },
|
||||||
|
position: { x: Math.random() * 400, y: Math.random() * 400 },
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -71,6 +81,9 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
>
|
>
|
||||||
<Background />
|
<Background />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
|
<Panel>
|
||||||
|
<button onClick={addNode}>add node</button>
|
||||||
|
</Panel>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
addEdge,
|
addEdge,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
useEdgesState,
|
useEdgesState,
|
||||||
useOnSelectionChange,
|
useOnSelectionChange,
|
||||||
OnSelectionChangeParams,
|
OnSelectionChangeParams,
|
||||||
|
OnSelectionChangeFunc,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
@@ -35,9 +36,9 @@ const initialEdges: Edge[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const SelectionLogger = () => {
|
const SelectionLogger = ({ id }: { id: string }) => {
|
||||||
const onChange = useCallback(({ nodes, edges }: OnSelectionChangeParams) => {
|
const onChange = useCallback(({ nodes, edges }: OnSelectionChangeParams) => {
|
||||||
console.log(nodes, edges);
|
console.log(id, nodes, edges);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useOnSelectionChange({
|
useOnSelectionChange({
|
||||||
@@ -63,14 +64,23 @@ const Flow = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const WrappedFlow = () => (
|
const WrappedFlow = () => {
|
||||||
<ReactFlowProvider>
|
const [secondLoggerActive, setSecondLoggerActive] = useState<boolean>(true);
|
||||||
<Flow />
|
|
||||||
<SelectionLogger />
|
const toggleSecondLogger = () => {
|
||||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
setSecondLoggerActive(!secondLoggerActive);
|
||||||
<input type={'text'} placeholder={'name'} />
|
};
|
||||||
</div>
|
|
||||||
</ReactFlowProvider>
|
return (
|
||||||
);
|
<ReactFlowProvider>
|
||||||
|
<Flow />
|
||||||
|
<SelectionLogger id="Logger 1" />
|
||||||
|
{secondLoggerActive && <SelectionLogger id="Logger 2" />}
|
||||||
|
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||||
|
<button onClick={toggleSecondLogger}>{secondLoggerActive ? 'Disable' : 'Enable'} Logger 2</button>
|
||||||
|
</div>
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default WrappedFlow;
|
export default WrappedFlow;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, MouseEvent, useEffect } from 'react';
|
import { useCallback, MouseEvent, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Background,
|
Background,
|
||||||
@@ -27,18 +27,21 @@ const initialNodes: Node[] = [
|
|||||||
data: { label: 'Node 2' },
|
data: { label: 'Node 2' },
|
||||||
position: { x: 100, y: 100 },
|
position: { x: 100, y: 100 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
type: 'default',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
data: { label: 'Node 3' },
|
data: { label: 'Node 3' },
|
||||||
position: { x: 400, y: 100 },
|
position: { x: 400, y: 100 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
type: 'default',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
data: { label: 'Node 4' },
|
data: { label: 'Node 4' },
|
||||||
position: { x: 400, y: 200 },
|
position: { x: 400, y: 200 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
|
type: 'default',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -57,7 +60,7 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||||
const {
|
const {
|
||||||
project,
|
screenToFlowPosition,
|
||||||
setCenter,
|
setCenter,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut,
|
zoomOut,
|
||||||
@@ -68,13 +71,14 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
getNodes,
|
getNodes,
|
||||||
getEdges,
|
getEdges,
|
||||||
deleteElements,
|
deleteElements,
|
||||||
|
updateNodeData,
|
||||||
} = useReactFlow();
|
} = useReactFlow();
|
||||||
|
|
||||||
const onPaneClick = useCallback(
|
const onPaneClick = useCallback(
|
||||||
(evt: MouseEvent) => {
|
(evt: MouseEvent) => {
|
||||||
const projectedPosition = project({
|
const projectedPosition = screenToFlowPosition({
|
||||||
x: evt.clientX,
|
x: evt.clientX,
|
||||||
y: evt.clientY - 40,
|
y: evt.clientY,
|
||||||
});
|
});
|
||||||
|
|
||||||
setNodes((nds) =>
|
setNodes((nds) =>
|
||||||
@@ -84,10 +88,11 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
data: {
|
data: {
|
||||||
label: `${projectedPosition.x}-${projectedPosition.y}`,
|
label: `${projectedPosition.x}-${projectedPosition.y}`,
|
||||||
},
|
},
|
||||||
|
type: 'default',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[project, setNodes]
|
[screenToFlowPosition, setNodes]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onNodeClick = useCallback(
|
const onNodeClick = useCallback(
|
||||||
@@ -105,6 +110,7 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
data: {
|
data: {
|
||||||
label: 'New Node',
|
label: 'New Node',
|
||||||
},
|
},
|
||||||
|
type: 'default',
|
||||||
};
|
};
|
||||||
|
|
||||||
addNodes(newNode);
|
addNodes(newNode);
|
||||||
@@ -125,12 +131,31 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
|
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const edgeAdded = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addEdges({ id: 'e3-4', source: '3', target: '4' });
|
if (!edgeAdded.current) {
|
||||||
|
addEdges({ id: 'e3-4', source: '3', target: '4' });
|
||||||
|
edgeAdded.current = true;
|
||||||
|
}
|
||||||
}, [addEdges]);
|
}, [addEdges]);
|
||||||
|
|
||||||
const onResetNodes = useCallback(() => setNodesHook(initialNodes), [setNodesHook]);
|
const onResetNodes = useCallback(() => setNodesHook(initialNodes), [setNodesHook]);
|
||||||
|
|
||||||
|
const onSetNodes = () => {
|
||||||
|
setNodes([
|
||||||
|
{ id: 'a', type: 'default', position: { x: 0, y: 0 }, data: { label: 'Node a' } },
|
||||||
|
{ id: 'b', type: 'default', position: { x: 0, y: 150 }, data: { label: 'Node b' } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateNode = () => {
|
||||||
|
updateNodeData('1', { label: 'update' });
|
||||||
|
updateNodeData('2', { label: 'update' });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -153,6 +178,8 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
<button onClick={logNodes}>useNodes</button>
|
<button onClick={logNodes}>useNodes</button>
|
||||||
<button onClick={deleteSelectedElements}>deleteSelectedElements</button>
|
<button onClick={deleteSelectedElements}>deleteSelectedElements</button>
|
||||||
<button onClick={deleteSomeElements}>deleteSomeElements</button>
|
<button onClick={deleteSomeElements}>deleteSomeElements</button>
|
||||||
|
<button onClick={onSetNodes}>setNodes</button>
|
||||||
|
<button onClick={onUpdateNode}>updateNode</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Background />
|
<Background />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
|
|||||||
@@ -33,23 +33,23 @@ const getId = (): string => `${id++}`;
|
|||||||
|
|
||||||
const UpdateNodeInternalsFlow = () => {
|
const UpdateNodeInternalsFlow = () => {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||||
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
||||||
|
|
||||||
const { project } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
|
|
||||||
const onPaneClick = useCallback(
|
const onPaneClick = useCallback(
|
||||||
(evt: MouseEvent) =>
|
(evt: MouseEvent) =>
|
||||||
setNodes((nds) =>
|
setNodes((nds) =>
|
||||||
nds.concat({
|
nds.concat({
|
||||||
id: getId(),
|
id: getId(),
|
||||||
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
|
position: screenToFlowPosition({ x: evt.clientX, y: evt.clientY }),
|
||||||
data: { label: 'new node' },
|
data: { label: 'new node' },
|
||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
[project, setNodes]
|
[screenToFlowPosition, setNodes]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user