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",
|
||||
"changelog": ["@changesets/changelog-github", { "repo": "wbkd/react-flow" }],
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
|
||||
"changelog": ["@changesets/changelog-github", { "repo": "xyflow/xyflow" }],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
"ignore": ["react-examples", "svelte-examples", "astro-examples", "playwright"]
|
||||
}
|
||||
|
||||
@@ -4,47 +4,57 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
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)
|
||||
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)
|
||||
**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.
|
||||
|
||||
Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:
|
||||
- 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
|
||||
**Issue with the xyflow Docs?** Submit an issue in our [web repo](https://github.com/xyflow/web/issues) instead.
|
||||
|
||||
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
|
||||
id: description
|
||||
id: platform
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
description: Provide a clear and concise description of the challenge you are running into.
|
||||
label: What platform were you using when you found the bug?
|
||||
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:
|
||||
required: true
|
||||
- type: input
|
||||
id: link
|
||||
attributes:
|
||||
label: Your Example Website or App
|
||||
label: Live code example
|
||||
description: |
|
||||
Which website or app were you using when the bug happened?
|
||||
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.
|
||||
- To create a shareable code example you can use Codesandbox (https://codesandbox.io/s/react-flow-starter-ou8or) or Stackblitz (https://stackblitz.com/).
|
||||
- Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve.
|
||||
placeholder: |
|
||||
e.g. https://stackblitz.com/edit/...... OR Github Repo
|
||||
This is the most important part of the bug report!
|
||||
|
||||
React Flow codesandbox starters
|
||||
- js: https://new.reactflow.dev/js
|
||||
- ts: https://new.reactflow.dev/ts
|
||||
|
||||
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:
|
||||
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
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to Reproduce the Bug or Issue
|
||||
description: Describe the steps we have to take to reproduce the behavior.
|
||||
label: Steps to reproduce the bug or issue
|
||||
description: How can we see the reported behavior in the code example you provided? The more detail, the better!
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
@@ -56,9 +66,8 @@ body:
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Provide a clear and concise description of what you expected to happen.
|
||||
placeholder: |
|
||||
As a user, I expected ___ behavior but i am seeing ___
|
||||
description: What would you expect to happen if there was no bug or issue?
|
||||
placeholder: As a user, I expected ___ behavior, but instead I am seeing ___
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -67,22 +76,9 @@ body:
|
||||
label: Screenshots or Videos
|
||||
description: |
|
||||
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
|
||||
to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
|
||||
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
|
||||
Supported image/file types and file size limits- 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
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
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
|
||||
strict-peer-dependencies=false
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
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 repository is the home of four packages:
|
||||
* React Flow v11 `reactflow` [v11 branch](https://github.com/xyflow/xyflow/tree/v11)
|
||||
* React Flow v12 (not published yet) [packages/react](./packages/react)
|
||||
* React Flow 11 `reactflow` [v11 branch](https://github.com/xyflow/xyflow/tree/v11)
|
||||
* React Flow 12 (beta) `@xyflow/react` [packages/react](./packages/react)
|
||||
* Svelte Flow `@xyflow/svelte` [packages/svelte](./packages/svelte)
|
||||
* 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`.
|
||||
|
||||
## 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 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>
|
||||
<summary><strong>React Flow</strong> basic usage</summary>
|
||||
@@ -151,6 +151,15 @@ npm install @xyflow/svelte
|
||||
```
|
||||
</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
|
||||
|
||||
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",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
|
||||
@@ -7,7 +7,7 @@ const sourceHandleStyleB: CSSProperties = {
|
||||
left: 'auto',
|
||||
};
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
|
||||
const CustomNode: FC<NodeProps> = ({ data, positionAbsoluteX, positionAbsoluteY }) => {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Top} />
|
||||
@@ -18,7 +18,7 @@ const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
|
||||
<div>
|
||||
Position:{' '}
|
||||
<strong>
|
||||
{xPos.toFixed(2)},{yPos.toFixed(2)}
|
||||
{positionAbsoluteX.toFixed(2)},{positionAbsoluteY.toFixed(2)}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ const initialNodes: Node[] = [
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
size: nodeSize,
|
||||
...nodeSize,
|
||||
handles: [
|
||||
{
|
||||
type: 'source',
|
||||
@@ -41,23 +41,19 @@ const initialNodes: Node[] = [
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
size: nodeSize,
|
||||
...nodeSize,
|
||||
handles: [
|
||||
{
|
||||
type: 'source',
|
||||
position: Position.Bottom,
|
||||
x: nodeSize.width * 0.5,
|
||||
y: nodeSize.height,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
type: 'target',
|
||||
position: Position.Top,
|
||||
x: nodeSize.width * 0.5,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -65,23 +61,19 @@ const initialNodes: Node[] = [
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
size: nodeSize,
|
||||
...nodeSize,
|
||||
handles: [
|
||||
{
|
||||
type: 'source',
|
||||
position: Position.Bottom,
|
||||
x: nodeSize.width * 0.5,
|
||||
y: nodeSize.height,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
type: 'target',
|
||||
position: Position.Top,
|
||||
x: nodeSize.width * 0.5,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -89,6 +81,8 @@ const initialNodes: Node[] = [
|
||||
id: '4',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
width: 200,
|
||||
height: 50,
|
||||
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' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
size: {
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
width: 100,
|
||||
height: 40,
|
||||
handles: [
|
||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||
@@ -26,10 +24,8 @@
|
||||
data: { label: 'A' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
size: {
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
width: 100,
|
||||
height: 40,
|
||||
handles: [
|
||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||
@@ -41,10 +37,8 @@
|
||||
data: { label: 'B' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
size: {
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
width: 100,
|
||||
height: 40,
|
||||
handles: [
|
||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||
@@ -56,10 +50,8 @@
|
||||
data: { label: 'C' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
size: {
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
width: 100,
|
||||
height: 40,
|
||||
handles: [
|
||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||
{ 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 SvelteFlowApp from '../components/SvelteFlowExample/index.svelte'
|
||||
import ReactFlowApp from '../components/ReactFlowExample';
|
||||
import ReactFlowInitialApp from '../components/ReactFlowInitialExample';
|
||||
import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte';
|
||||
import SvelteFlowInitialApp from '../components/SvelteFlowInitialExample/index.svelte';
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
@@ -18,10 +20,24 @@ import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte'
|
||||
</style>
|
||||
</head>
|
||||
<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>
|
||||
<SvelteFlowApp />
|
||||
|
||||
<h2>React Flow</h2>
|
||||
<ReactFlowApp />
|
||||
<p>client hydration on load (client:load)</p>
|
||||
<SvelteFlowApp client:load />
|
||||
|
||||
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
|
||||
<SvelteFlowInitialApp client:load />
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -8,5 +8,15 @@ This Vite app is used internally to test the library.
|
||||
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({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
baseUrl: 'http://localhost:3000/examples',
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 720,
|
||||
video: false,
|
||||
|
||||
@@ -13,7 +13,6 @@ describe('useEdges.cy.tsx', () => {
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', []);
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ const initialNodes: Node[] = nodes.map((n) => ({
|
||||
|
||||
const expectedNodes: Node[] = initialNodes.map((n) => ({
|
||||
...n,
|
||||
positionAbsolute: n.position,
|
||||
...nodeDimensions,
|
||||
measured: {
|
||||
...nodeDimensions,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useNodes.cy.tsx', () => {
|
||||
@@ -29,7 +30,6 @@ describe('useNodes.cy.tsx', () => {
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', []);
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as simpleflow from '../../fixtures/simpleflow';
|
||||
describe('<ReactFlow />: Basic Props', () => {
|
||||
describe('uses defaultNodes and defaultEdges', () => {
|
||||
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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
cy.get('body')
|
||||
.type('{shift}', { release: false })
|
||||
.wait(50)
|
||||
.type('{Shift}', { release: false })
|
||||
.get('.react-flow__pane')
|
||||
.trigger('mousedown', 1, 10, { button: 0, force: true })
|
||||
.trigger('mousemove', 1000, 450, { button: 0 })
|
||||
.wait(50)
|
||||
.trigger('mouseup', 1000, 450, { button: 0, force: true });
|
||||
.trigger('mousemove', 1000, 200, { button: 0 })
|
||||
.trigger('mouseup', 1000, 200, { button: 0 });
|
||||
|
||||
cy.wait(200);
|
||||
|
||||
cy.get('.react-flow__node').eq(1).should('have.class', 'selected');
|
||||
cy.get('.react-flow__node').eq(0).should('have.class', 'selected');
|
||||
cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected');
|
||||
|
||||
cy.get('.react-flow__nodesselection-rect');
|
||||
|
||||
cy.get('body').type('{shift}', { release: true, force: true });
|
||||
});
|
||||
|
||||
@@ -97,19 +92,19 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
|
||||
it('drags a node', () => {
|
||||
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');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
|
||||
// @TODO: why does this fail since react18?
|
||||
// it('removes a node', () => {
|
||||
// cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
|
||||
// cy.get('html').type('{backspace}').wait(100);
|
||||
// cy.get('.react-flow__node').should('have.length', 3);
|
||||
// cy.get('.react-flow__edge').should('have.length', 1);
|
||||
// });
|
||||
it('removes a node', () => {
|
||||
cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
|
||||
cy.get('html').realPress('Backspace');
|
||||
|
||||
cy.get('.react-flow__node').should('have.length', 3);
|
||||
cy.get('.react-flow__edge').should('have.length', 0);
|
||||
});
|
||||
|
||||
it('connects nodes', () => {
|
||||
cy.get('.react-flow__node')
|
||||
@@ -125,16 +120,15 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
|
||||
.trigger('mouseup', { force: true, button: 0 });
|
||||
|
||||
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', () => {
|
||||
// cy.get('.react-flow__edge:first').click();
|
||||
// cy.get('body').type('{backspace}');
|
||||
it('removes an edge', () => {
|
||||
cy.get('.react-flow__edge:first').click();
|
||||
cy.get('html').realPress('Backspace');
|
||||
|
||||
// cy.get('.react-flow__edge').should('have.length', 1);
|
||||
// });
|
||||
cy.get('.react-flow__edge').should('have.length', 0);
|
||||
});
|
||||
|
||||
it('drags the pane', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
@@ -59,7 +59,7 @@ function ControlledFlow({
|
||||
handlers.onConnect = onConnect;
|
||||
}
|
||||
|
||||
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} />;
|
||||
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} nodeDragThreshold={0} />;
|
||||
}
|
||||
|
||||
export default ControlledFlow;
|
||||
|
||||
@@ -9,7 +9,7 @@ Cypress.Commands.add('drag', (selector, { x, y }) =>
|
||||
const nextY: number = centerY + y;
|
||||
|
||||
return elementToDrag
|
||||
.trigger('mousedown', { view: window })
|
||||
.trigger('mousedown', { view: window, force: true })
|
||||
.trigger('mousemove', nextX, nextY, { force: true })
|
||||
.wait(50)
|
||||
.trigger('mouseup', { view: window, force: true });
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
import 'cypress-real-events/support';
|
||||
|
||||
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<title>React Flow Examples</title>
|
||||
</head>
|
||||
|
||||
+17
-14
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@xyflow/react-examples",
|
||||
"name": "react-examples",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
@@ -14,26 +14,29 @@
|
||||
"test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.2.3",
|
||||
"@xyflow/react": "workspace:*",
|
||||
"classcat": "^5.0.3",
|
||||
"classcat": "^5.0.4",
|
||||
"dagre": "^0.8.5",
|
||||
"localforage": "^1.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"zustand": "^4.4.1"
|
||||
"react-redux": "^9.1.1",
|
||||
"react-router-dom": "^6.18.0",
|
||||
"redux": "^5.0.1",
|
||||
"zustand": "^4.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/skip-test": "^2.6.1",
|
||||
"@types/dagre": "^0.7.48",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "4.0.4",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"cypress": "12.17.3",
|
||||
"cypress-real-events": "1.10.0",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"typescript": "5.1.3",
|
||||
"vite": "4.4.9"
|
||||
"@types/dagre": "^0.7.52",
|
||||
"@types/react": "^18.2.36",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"@vitejs/plugin-react": "4.1.1",
|
||||
"@vitejs/plugin-react-swc": "^3.4.1",
|
||||
"cypress": "13.6.6",
|
||||
"cypress-real-events": "1.12.0",
|
||||
"start-server-and-test": "^2.0.2",
|
||||
"typescript": "5.2.2",
|
||||
"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, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import routes from './routes';
|
||||
import GenericTestRoutes from '../generic-tests';
|
||||
import Basic from '../examples/Basic';
|
||||
import Backgrounds from '../examples/Backgrounds';
|
||||
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>
|
||||
);
|
||||
};
|
||||
import Header from './header';
|
||||
|
||||
export default () => (
|
||||
<BrowserRouter>
|
||||
<Header />
|
||||
<Routes>
|
||||
{routes.map((route) => (
|
||||
<Route path={route.path} key={route.path} element={<route.component />} />
|
||||
))}
|
||||
<Route path="/" element={<Navigate to="/examples" />} />
|
||||
<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>
|
||||
</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 {
|
||||
ReactFlow,
|
||||
MiniMap,
|
||||
@@ -10,12 +10,16 @@ import {
|
||||
Edge,
|
||||
useReactFlow,
|
||||
Panel,
|
||||
OnNodeDrag,
|
||||
} from '@xyflow/react';
|
||||
|
||||
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
|
||||
const onNodeDragStart = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
|
||||
const printSelectionEvent = (name: string) => (_: MouseEvent, nodes: Node[]) => console.log(name, nodes);
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
@@ -52,41 +56,80 @@ const initialEdges: Edge[] = [
|
||||
const defaultEdgeOptions = {};
|
||||
|
||||
const BasicFlow = () => {
|
||||
const instance = useReactFlow();
|
||||
const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
|
||||
useReactFlow();
|
||||
|
||||
const updatePos = () => {
|
||||
instance.setNodes((nodes) =>
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
},
|
||||
};
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const logToObject = () => console.log(instance.toObject());
|
||||
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
|
||||
const logToObject = () => console.log(toObject());
|
||||
const resetTransform = () => setViewport({ x: 0, y: 0, zoom: 1 });
|
||||
|
||||
const toggleClassnames = () => {
|
||||
instance.setNodes((nodes) =>
|
||||
setNodes((nodes) =>
|
||||
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 (
|
||||
<ReactFlow
|
||||
defaultNodes={initialNodes}
|
||||
defaultEdges={initialEdges}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDrag={onNodeDrag}
|
||||
onSelectionDragStart={printSelectionEvent('selection drag start')}
|
||||
onSelectionDrag={printSelectionEvent('selection drag')}
|
||||
onSelectionDragStop={printSelectionEvent('selection drag stop')}
|
||||
className="react-flow-basic-example"
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
@@ -95,6 +138,7 @@ const BasicFlow = () => {
|
||||
selectNodesOnDrag={false}
|
||||
elevateEdgesOnSelect
|
||||
elevateNodesOnSelect={false}
|
||||
nodeDragThreshold={0}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<MiniMap />
|
||||
@@ -105,6 +149,12 @@ const BasicFlow = () => {
|
||||
<button onClick={updatePos}>change pos</button>
|
||||
<button onClick={toggleClassnames}>toggle classnames</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>
|
||||
</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 = () => {
|
||||
instance.setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
},
|
||||
};
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -73,11 +74,12 @@ const ControlledUncontrolled = () => {
|
||||
const updateEdgeColors = () => {
|
||||
instance.setEdges((edges) =>
|
||||
edges.map((edge) => {
|
||||
edge.style = {
|
||||
stroke: '#ff5050',
|
||||
return {
|
||||
...edge,
|
||||
style: {
|
||||
stroke: '#ff5050',
|
||||
},
|
||||
};
|
||||
|
||||
return edge;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,13 +27,13 @@ const buttonStyle: CSSProperties = {
|
||||
zIndex: 4,
|
||||
};
|
||||
|
||||
const CustomMiniMapNode = ({ x, y, width, height, color }: MiniMapNodeProps) => (
|
||||
<circle cx={x} cy={y} r={Math.max(width, height) / 2} fill={color} />
|
||||
);
|
||||
const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => {
|
||||
return <circle cx={x} cy={y} r={Math.max(width, height) / 2} fill="#ffcc00" />;
|
||||
};
|
||||
|
||||
const CustomMiniMapNodeFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
||||
const addRandomNode = () => {
|
||||
@@ -59,7 +59,7 @@ const CustomMiniMapNodeFlow = () => {
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={(p) => onConnect(p)}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onlyRenderVisibleElements={false}
|
||||
onlyRenderVisibleElements={true}
|
||||
>
|
||||
<Controls />
|
||||
<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 type { ColorSelectorNode } from '.';
|
||||
|
||||
const targetHandleStyle: CSSProperties = { background: '#555' };
|
||||
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
|
||||
const sourceHandleStyleB: CSSProperties = {
|
||||
@@ -11,7 +13,7 @@ const sourceHandleStyleB: CSSProperties = {
|
||||
|
||||
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 onChange = useCallback((viewport: Viewport) => console.log('onChange', 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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
ReactFlow,
|
||||
MiniMap,
|
||||
Controls,
|
||||
addEdge,
|
||||
Node,
|
||||
ReactFlowInstance,
|
||||
Position,
|
||||
SnapGrid,
|
||||
Connection,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Background,
|
||||
OnNodeDrag,
|
||||
OnInit,
|
||||
applyNodeChanges,
|
||||
OnNodesChange,
|
||||
OnConnect,
|
||||
OnBeforeDelete,
|
||||
BuiltInNode,
|
||||
BuiltInEdge,
|
||||
NodeTypes,
|
||||
ReactFlowProvider,
|
||||
} from '@xyflow/react';
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
const onNodeDragStop: OnNodeDrag<MyNode> = (_, node) => console.log('drag stop', node);
|
||||
const onNodeClick = (_: MouseEvent, node: MyNode) => console.log('click', node);
|
||||
|
||||
const initBgColor = '#1A192B';
|
||||
|
||||
const connectionLineStyle = { stroke: '#fff' };
|
||||
const snapGrid: SnapGrid = [16, 16];
|
||||
|
||||
const nodeTypes = {
|
||||
const nodeTypes: NodeTypes = {
|
||||
selectorNode: ColorSelectorNode,
|
||||
};
|
||||
|
||||
const CustomNodeFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const ref = useRef(null);
|
||||
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);
|
||||
|
||||
@@ -42,7 +66,7 @@ const CustomNodeFlow = () => {
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id !== '2') {
|
||||
if (node.id !== '2' || node.type !== 'selectorNode') {
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -119,12 +143,13 @@ const CustomNodeFlow = () => {
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) =>
|
||||
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
|
||||
const onConnect: OnConnect = useCallback(
|
||||
(connection) => setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onBeforeDelete: OnBeforeDelete<MyNode, MyEdge> = useCallback(async (params) => true, []);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
@@ -142,16 +167,18 @@ const CustomNodeFlow = () => {
|
||||
fitView
|
||||
minZoom={0.3}
|
||||
maxZoom={2}
|
||||
onBeforeDelete={onBeforeDelete}
|
||||
ref={ref}
|
||||
>
|
||||
<MiniMap
|
||||
nodeStrokeColor={(n: Node): string => {
|
||||
<MiniMap<MyNode>
|
||||
nodeStrokeColor={(n: MyNode): string => {
|
||||
if (n.type === 'input') return '#0041d0';
|
||||
if (n.type === 'selectorNode') return bgColor;
|
||||
if (n.type === 'output') return '#ff0072';
|
||||
|
||||
return '#eee';
|
||||
}}
|
||||
nodeColor={(n: Node): string => {
|
||||
nodeColor={(n: MyNode): string => {
|
||||
if (n.type === 'selectorNode') return bgColor;
|
||||
|
||||
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',
|
||||
type: 'output',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
@@ -54,26 +55,24 @@ const DefaultNodes = () => {
|
||||
|
||||
const updateNodePositions = () => {
|
||||
instance.setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.position = {
|
||||
nodes.map((node) => ({
|
||||
...node,
|
||||
position: {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
};
|
||||
|
||||
return node;
|
||||
})
|
||||
},
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const updateEdgeColors = () => {
|
||||
instance.setEdges((edges) =>
|
||||
edges.map((edge) => {
|
||||
edge.style = {
|
||||
edges.map((edge) => ({
|
||||
...edge,
|
||||
style: {
|
||||
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',
|
||||
style: { border: '1px solid #ddd', padding: '20px 40px' },
|
||||
position: { x: 200, y: 200 },
|
||||
data: null,
|
||||
data: {},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -33,6 +33,7 @@ const DragHandleFlow = () => {
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onNodeClick}
|
||||
nodeDragThreshold={0}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ const nodeOrigin: NodeOrigin = [0.5, 0.5];
|
||||
const DnDFlow = () => {
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
|
||||
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 onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi);
|
||||
@@ -49,7 +49,7 @@ const DnDFlow = () => {
|
||||
|
||||
if (reactFlowInstance) {
|
||||
const type = event.dataTransfer.getData('application/reactflow');
|
||||
const position = reactFlowInstance.screenToFlowCoordinate({
|
||||
const position = reactFlowInstance.screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
@@ -66,7 +66,7 @@ const DnDFlow = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.dndflow}>
|
||||
<ReactFlowProvider nodes={initialNodes} edges={[]}>
|
||||
<ReactFlowProvider initialNodes={initialNodes} initialEdges={[]}>
|
||||
<div className={styles.wrapper}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
@@ -77,6 +77,7 @@ const DnDFlow = () => {
|
||||
onInit={onInit}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
nodeOrigin={nodeOrigin}
|
||||
>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useStore, getStraightPath, EdgeProps } from '@xyflow/react';
|
||||
import { useStore, getStraightPath, EdgeProps, useInternalNode } from '@xyflow/react';
|
||||
|
||||
import { getEdgeParams } from './utils.js';
|
||||
|
||||
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
|
||||
const sourceNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === source), [source]));
|
||||
const targetNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === target), [target]));
|
||||
const sourceNode = useInternalNode(source);
|
||||
const targetNode = useInternalNode(target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
|
||||
@@ -5,7 +5,6 @@ import CustomNode from './CustomNode';
|
||||
import FloatingEdge from './FloatingEdge';
|
||||
import CustomConnectionLine from './CustomConnectionLine';
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import './style.css';
|
||||
|
||||
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
|
||||
// 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
|
||||
const {
|
||||
width: intersectionNodeWidth,
|
||||
height: intersectionNodeHeight,
|
||||
positionAbsolute: intersectionNodePosition,
|
||||
} = intersectionNode;
|
||||
const targetPosition = targetNode.positionAbsolute!;
|
||||
|
||||
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured;
|
||||
const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
|
||||
const targetPosition = targetNode.internals.positionAbsolute!;
|
||||
|
||||
const w = intersectionNodeWidth! / 2;
|
||||
const h = intersectionNodeHeight! / 2;
|
||||
|
||||
const x2 = intersectionNodePosition!.x + w;
|
||||
const y2 = intersectionNodePosition!.y + h;
|
||||
const x2 = intersectionNodePosition.x + w;
|
||||
const y2 = intersectionNodePosition.y + h;
|
||||
const x1 = targetPosition.x + w;
|
||||
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
|
||||
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
const n = { ...node.positionAbsolute, ...node };
|
||||
function getEdgePosition(node: InternalNode, intersectionPoint: XYPosition) {
|
||||
const n = { ...node.internals.positionAbsolute, ...node };
|
||||
const nx = Math.round(n.x!);
|
||||
const ny = Math.round(n.y!);
|
||||
const px = Math.round(intersectionPoint.x);
|
||||
@@ -41,13 +39,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
if (px <= nx + 1) {
|
||||
return Position.Left;
|
||||
}
|
||||
if (px >= nx + n.width! - 1) {
|
||||
if (px >= nx + n.measured?.width! - 1) {
|
||||
return Position.Right;
|
||||
}
|
||||
if (py <= ny + 1) {
|
||||
return Position.Top;
|
||||
}
|
||||
if (py >= n.y! + n.height! - 1) {
|
||||
if (py >= n.y! + n.measured?.height! - 1) {
|
||||
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
|
||||
export function getEdgeParams(source: Node, target: Node) {
|
||||
export function getEdgeParams(source: InternalNode, target: InternalNode) {
|
||||
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
||||
const targetIntersectionPoint = getNodeIntersection(target, source);
|
||||
|
||||
|
||||
@@ -175,6 +175,15 @@ const edgeTypes: EdgeTypes = {
|
||||
custom2: CustomEdge2,
|
||||
};
|
||||
|
||||
const defaultEdgeOptions = {
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: 'red',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
};
|
||||
|
||||
const EdgesFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
@@ -196,6 +205,8 @@ const EdgesFlow = () => {
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseMove={onEdgeMouseMove}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
onDelete={console.log}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
@@ -26,8 +26,8 @@ const buttonStyle: CSSProperties = {
|
||||
};
|
||||
|
||||
const EmptyFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
||||
const addRandomNode = () => {
|
||||
|
||||
@@ -5,8 +5,8 @@ import { getEdgeParams } from './utils';
|
||||
|
||||
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
|
||||
const { sourceNode, targetNode } = useStore((s) => {
|
||||
const sourceNode = s.nodes.find((n) => n.id === source);
|
||||
const targetNode = s.nodes.find((n) => n.id === target);
|
||||
const sourceNode = s.nodeLookup.get(source);
|
||||
const targetNode = s.nodeLookup.get(target);
|
||||
|
||||
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
|
||||
// of the line between the center of the intersectionNode and the target node
|
||||
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
|
||||
|
||||
const {
|
||||
width: intersectionNodeWidth,
|
||||
height: intersectionNodeHeight,
|
||||
position: intersectionNodePosition,
|
||||
} = intersectionNode;
|
||||
const { position: intersectionNodePosition } = intersectionNode;
|
||||
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured ?? {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
const targetPosition = targetNode.position;
|
||||
|
||||
const w = (intersectionNodeWidth ?? 0) / 2;
|
||||
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
if (px <= nx + 1) {
|
||||
return Position.Left;
|
||||
}
|
||||
if (px >= nx + (n.width ?? 0) - 1) {
|
||||
if (px >= nx + (n.measured?.width ?? 0) - 1) {
|
||||
return Position.Right;
|
||||
}
|
||||
if (py <= ny + 1) {
|
||||
return Position.Top;
|
||||
}
|
||||
if (py >= n.y + (n.height ?? 0) - 1) {
|
||||
if (py >= n.y + (n.measured?.height ?? 0) - 1) {
|
||||
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
|
||||
export function getEdgeParams(source: Node, target: Node) {
|
||||
export function getEdgeParams(source: InternalNode, target: InternalNode) {
|
||||
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
||||
const targetIntersectionPoint = getNodeIntersection(target, source);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MiniMap,
|
||||
Controls,
|
||||
} from '@xyflow/react';
|
||||
import ReactFlowDevTools from '../DevTools/DevTools';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
@@ -48,8 +49,10 @@ const initialEdges: Edge[] = [
|
||||
|
||||
const setHidden = (hidden: boolean) => (els: any[]) =>
|
||||
els.map((e: any) => {
|
||||
e.hidden = hidden;
|
||||
return e;
|
||||
return {
|
||||
...e,
|
||||
hidden,
|
||||
};
|
||||
});
|
||||
|
||||
const HiddenFlow = () => {
|
||||
@@ -70,6 +73,8 @@ const HiddenFlow = () => {
|
||||
setEdges(setHidden(isHidden));
|
||||
}, [isHidden, setEdges, setNodes]);
|
||||
|
||||
console.log(nodes);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
@@ -80,6 +85,7 @@ const HiddenFlow = () => {
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<ReactFlowDevTools position="top-right" />
|
||||
|
||||
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
|
||||
<div>
|
||||
|
||||
@@ -36,7 +36,7 @@ const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', nod
|
||||
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
|
||||
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', 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 InteractionFlow = () => {
|
||||
@@ -80,6 +80,7 @@ const InteractionFlow = () => {
|
||||
onPaneClick={captureZoomClick ? onPaneClick : undefined}
|
||||
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
|
||||
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
|
||||
nodeDragThreshold={0}
|
||||
onMoveEnd={onMoveEnd}
|
||||
>
|
||||
<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 initialNodes: Node[] = [
|
||||
{
|
||||
id: '0',
|
||||
data: { label: 'rectangle' },
|
||||
position: { x: 0, y: 0 },
|
||||
width: 100,
|
||||
height: 100,
|
||||
draggable: false,
|
||||
style: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 0, y: 0 },
|
||||
className: 'light',
|
||||
style: {
|
||||
width: 200,
|
||||
height: 100,
|
||||
},
|
||||
width: 200,
|
||||
height: 100,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 0, y: 150 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 250, y: 0 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
@@ -48,7 +54,6 @@ const initialNodes: Node[] = [
|
||||
width: 50,
|
||||
height: 50,
|
||||
},
|
||||
className: 'light',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import initialItems from './initial-elements';
|
||||
|
||||
import styles from './layouting.module.css';
|
||||
import ReactFlowDevTools from '../DevTools/DevTools';
|
||||
|
||||
const dagreGraph = new dagre.graphlib.Graph();
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||
@@ -56,16 +57,16 @@ const LayoutFlow = () => {
|
||||
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
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);
|
||||
@@ -98,6 +99,7 @@ const LayoutFlow = () => {
|
||||
onEdgesChange={onEdgesChange}
|
||||
>
|
||||
<Controls />
|
||||
<ReactFlowDevTools />
|
||||
</ReactFlow>
|
||||
<Panel position="top-right">
|
||||
<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}
|
||||
minHeight={data.minHeight ?? undefined}
|
||||
maxHeight={data.maxHeight ?? undefined}
|
||||
isVisible={data.isVisible ?? selected}
|
||||
isVisible={data.isVisible ?? !!selected}
|
||||
shouldResize={data.shouldResize ?? undefined}
|
||||
onResizeStart={data.onResizeStart ?? undefined}
|
||||
onResize={data.onResize ?? undefined}
|
||||
|
||||
@@ -16,8 +16,6 @@ import CustomResizer from './CustomResizer';
|
||||
import VerticalResizer from './VerticalResizer';
|
||||
import HorizontalResizer from './HorizontalResizer';
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
|
||||
const nodeTypes = {
|
||||
defaultResizer: DefaultResizer,
|
||||
customResizer: CustomResizer,
|
||||
@@ -39,6 +37,7 @@ const initialNodes: Node[] = [
|
||||
type: 'defaultResizer',
|
||||
data: { label: 'default resizer' },
|
||||
position: { x: 0, y: 0 },
|
||||
origin: [1, 1],
|
||||
style: { ...nodeStyle },
|
||||
},
|
||||
{
|
||||
@@ -52,7 +51,9 @@ const initialNodes: Node[] = [
|
||||
maxHeight: 200,
|
||||
},
|
||||
position: { x: 0, y: 60 },
|
||||
style: { ...nodeStyle, width: 100, height: 80 },
|
||||
width: 100,
|
||||
height: 80,
|
||||
style: { ...nodeStyle },
|
||||
},
|
||||
{
|
||||
id: '1b',
|
||||
@@ -66,9 +67,9 @@ const initialNodes: Node[] = [
|
||||
maxHeight: 400,
|
||||
},
|
||||
position: { x: 250, y: 0 },
|
||||
width: 174,
|
||||
height: 123,
|
||||
style: {
|
||||
width: 174,
|
||||
height: 123,
|
||||
...nodeStyle,
|
||||
},
|
||||
},
|
||||
@@ -77,7 +78,9 @@ const initialNodes: Node[] = [
|
||||
type: 'customResizer',
|
||||
data: { label: 'custom resize icon' },
|
||||
position: { x: 0, y: 200 },
|
||||
style: { width: 100, height: 60, ...nodeStyle },
|
||||
width: 100,
|
||||
height: 60,
|
||||
style: { ...nodeStyle },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
@@ -96,7 +99,8 @@ const initialNodes: Node[] = [
|
||||
keepAspectRatio: true,
|
||||
},
|
||||
position: { x: 400, y: 200 },
|
||||
style: { ...nodeStyle, height: 50 },
|
||||
height: 50,
|
||||
style: { ...nodeStyle },
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
@@ -118,6 +122,50 @@ const initialNodes: Node[] = [
|
||||
position: { x: 250, y: 400 },
|
||||
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 = () => {
|
||||
@@ -142,6 +190,7 @@ const CustomNodeFlow = () => {
|
||||
maxZoom={5}
|
||||
snapToGrid={snapToGrid}
|
||||
fitView
|
||||
onlyRenderVisibleElements
|
||||
>
|
||||
<Controls />
|
||||
<Panel position="bottom-right">
|
||||
|
||||
@@ -28,6 +28,14 @@ const initialNodes: Node[] = [
|
||||
data: { label: 'A Node' },
|
||||
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 = {
|
||||
@@ -66,7 +74,7 @@ const nodeTypesObjects: NodeTypesObject = {
|
||||
const NodeTypeChangeFlow = () => {
|
||||
const [nodeTypesId, setNodeTypesId] = useState<string>('a');
|
||||
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 changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
|
||||
|
||||
@@ -78,6 +86,7 @@ const NodeTypeChangeFlow = () => {
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypesObjects[nodeTypesId]}
|
||||
fitView
|
||||
>
|
||||
<button onClick={changeType} style={buttonStyle}>
|
||||
change type
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
Background,
|
||||
MiniMap,
|
||||
ConnectionMode,
|
||||
OnBeforeDelete,
|
||||
OnDelete,
|
||||
} from '@xyflow/react';
|
||||
|
||||
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 onPaneClick = (event: ReactMouseEvent) => console.log('pane click', 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 onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', 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 onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
|
||||
const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge);
|
||||
const onNodesDelete = (nodes: Node[]) => console.log('nodes delete', nodes);
|
||||
const onEdgesDelete = (edges: Edge[]) => console.log('edges delete', edges);
|
||||
const onBeforeDelete: OnBeforeDelete = async ({ nodes, 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 initialNodes: Node[] = [
|
||||
@@ -226,13 +232,13 @@ const OverviewFlow = () => {
|
||||
fitViewOptions={{ padding: 0.1 /*nodes: [{ id: '1' }]*/ }}
|
||||
attributionPosition="top-right"
|
||||
maxZoom={Infinity}
|
||||
onNodesDelete={onNodesDelete}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
onBeforeDelete={onBeforeDelete}
|
||||
onDelete={onDelete}
|
||||
onPaneMouseMove={onPaneMouseMove}
|
||||
>
|
||||
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
|
||||
<Controls />
|
||||
<Background color="#aaa" gap={25} />
|
||||
<MiniMap nodeBorderRadius={2} />
|
||||
<Controls orientation="horizontal" />
|
||||
<Background gap={25} />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
+13
-16
@@ -2,11 +2,10 @@ import { useState, useCallback, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Controls,
|
||||
updateEdge,
|
||||
reconnectEdge,
|
||||
addEdge,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
@@ -92,26 +91,24 @@ const initialNodes: Node[] = [
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-3', source: '1', target: '3', label: 'This edge can only be updated from source', updatable: 'source' },
|
||||
{ id: 'e2-4', source: '2', target: '4', label: 'This edge can only be updated from target', updatable: 'target' },
|
||||
{ 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', reconnectable: 'target' },
|
||||
{ id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' },
|
||||
];
|
||||
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
|
||||
const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
|
||||
const onReconnectStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
|
||||
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);
|
||||
|
||||
const UpdatableEdge = () => {
|
||||
const ReconnectEdge = () => {
|
||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
|
||||
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
||||
const onReconnect = (oldEdge: Edge, newConnection: Connection) =>
|
||||
setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
|
||||
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
|
||||
|
||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||
console.log(changes);
|
||||
setNodes((ns) => applyNodeChanges(changes, ns));
|
||||
}, []);
|
||||
|
||||
@@ -125,16 +122,16 @@ const UpdatableEdge = () => {
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onInit={onInit}
|
||||
snapToGrid={true}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onReconnect={onReconnect}
|
||||
onConnect={onConnect}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
onReconnectStart={onReconnectStart}
|
||||
onReconnectEnd={onReconnectEnd}
|
||||
fitView
|
||||
>
|
||||
<Controls />
|
||||
</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 {
|
||||
ReactFlow,
|
||||
ReactFlowInstance,
|
||||
Edge,
|
||||
Node,
|
||||
NodeChange,
|
||||
@@ -12,16 +11,11 @@ import {
|
||||
EdgeChange,
|
||||
Controls,
|
||||
Background,
|
||||
MiniMap,
|
||||
Panel,
|
||||
} from '@xyflow/react';
|
||||
|
||||
import { getNodesAndEdges } from './utils';
|
||||
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
||||
reactFlowInstance.fitView();
|
||||
console.log(reactFlowInstance.getNodes());
|
||||
};
|
||||
import { FrameRecorder, generateMouseEventParamsTargetingNode, nextFrame } from './performanceUtils';
|
||||
|
||||
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
|
||||
|
||||
@@ -31,6 +25,166 @@ const StressFlow = () => {
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
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 = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
@@ -62,19 +216,23 @@ const StressFlow = () => {
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
key={key}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={onInit}
|
||||
onConnect={onConnect}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgeChange}
|
||||
minZoom={0.2}
|
||||
fitView
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
|
||||
<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={updateElements}>update elements</button>
|
||||
</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 },
|
||||
data,
|
||||
position,
|
||||
width: 50,
|
||||
height: 30,
|
||||
};
|
||||
initialNodes.push(node);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 idStyle: CSSProperties = {
|
||||
@@ -11,13 +11,18 @@ const idStyle: CSSProperties = {
|
||||
left: 2,
|
||||
};
|
||||
|
||||
const DebugNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
|
||||
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => {
|
||||
const node = useInternalNode(id)!;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Top} />
|
||||
<div style={idStyle}>{id}</div>
|
||||
<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>
|
||||
<Handle type="source" position={Position.Bottom} />
|
||||
</>
|
||||
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
MiniMap,
|
||||
Background,
|
||||
Panel,
|
||||
NodeOrigin,
|
||||
useUpdateNodeInternals,
|
||||
ReactFlowProvider,
|
||||
} from '@xyflow/react';
|
||||
|
||||
import DebugNode from './DebugNode';
|
||||
@@ -49,7 +52,7 @@ const initialNodes: Node[] = [
|
||||
data: { label: 'Node 4a' },
|
||||
position: { x: 15, y: 15 },
|
||||
className: 'light',
|
||||
parentNode: '4',
|
||||
parentId: '4',
|
||||
origin: [0.5, 0.5],
|
||||
|
||||
extent: [
|
||||
@@ -67,21 +70,21 @@ const initialNodes: Node[] = [
|
||||
height: 200,
|
||||
width: 300,
|
||||
},
|
||||
parentNode: '4',
|
||||
parentId: '4',
|
||||
},
|
||||
{
|
||||
id: '4b1',
|
||||
data: { label: 'Node 4b1' },
|
||||
position: { x: 40, y: 20 },
|
||||
className: 'light',
|
||||
parentNode: '4b',
|
||||
parentId: '4b',
|
||||
},
|
||||
{
|
||||
id: '4b2',
|
||||
data: { label: 'Node 4b2' },
|
||||
position: { x: 20, y: 100 },
|
||||
className: 'light',
|
||||
parentNode: '4b',
|
||||
parentId: '4b',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
@@ -89,7 +92,7 @@ const initialNodes: Node[] = [
|
||||
data: { label: 'Node 5' },
|
||||
position: { x: 650, y: 250 },
|
||||
className: 'light',
|
||||
style: { width: 400, height: 150 },
|
||||
style: { width: 100, height: 100 },
|
||||
zIndex: 1000,
|
||||
},
|
||||
{
|
||||
@@ -97,15 +100,15 @@ const initialNodes: Node[] = [
|
||||
data: { label: 'Node 5a' },
|
||||
position: { x: 0, y: 0 },
|
||||
className: 'light',
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
extent: 'parent',
|
||||
},
|
||||
{
|
||||
id: '5b',
|
||||
data: { label: 'Node 5b' },
|
||||
position: { x: 225, y: 50 },
|
||||
position: { x: 200, y: 200 },
|
||||
className: 'light',
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
expandParent: true,
|
||||
},
|
||||
{
|
||||
@@ -119,6 +122,7 @@ const initialNodes: Node[] = [
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
extent: 'parent',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -149,6 +153,7 @@ const nodeTypes = {
|
||||
|
||||
const Subflow = () => {
|
||||
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
@@ -158,10 +163,13 @@ const Subflow = () => {
|
||||
const updatePos = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
if (!n.parentNode) {
|
||||
n.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
if (!n.parentId) {
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,8 +184,10 @@ const Subflow = () => {
|
||||
const toggleClassnames = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
n.className = n.className === 'light' ? 'dark' : 'light';
|
||||
return n;
|
||||
return {
|
||||
...n,
|
||||
className: n.className === 'light' ? 'dark' : 'light',
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -185,8 +195,10 @@ const Subflow = () => {
|
||||
const toggleChildNodes = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
n.hidden = !!n.parentNode && !n.hidden;
|
||||
return n;
|
||||
return {
|
||||
...n,
|
||||
hidden: !!n.parentId && !n.hidden,
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -207,28 +219,27 @@ const Subflow = () => {
|
||||
onlyRenderVisibleElements={false}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
nodeOrigin={[0, 0]}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
|
||||
<Panel position="top-right">
|
||||
<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={resetTransform}>reset transform</button>
|
||||
<button onClick={updatePos}>change pos</button>
|
||||
<button onClick={toggleClassnames}>toggle classnames</button>
|
||||
<button onClick={toggleChildNodes}>toggleChildNodes</button>
|
||||
<button onClick={logToObject}>toObject</button>
|
||||
<button onClick={() => setNodes(initialNodes)}>setNodes</button>
|
||||
<button onClick={() => updateNodeInternals(nodes.map((node) => node.id))}>updateNodeInternals</button>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default Subflow;
|
||||
export default () => (
|
||||
<ReactFlowProvider>
|
||||
<Subflow />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from '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[] = [
|
||||
{
|
||||
@@ -42,7 +42,7 @@ const TouchDeviceFlow = () => {
|
||||
onConnectEnd={onConnectEnd}
|
||||
onClickConnectStart={onClickConnectStart}
|
||||
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,
|
||||
ConnectionLineType,
|
||||
ConnectionMode,
|
||||
updateEdge,
|
||||
reconnectEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from '@xyflow/react';
|
||||
@@ -22,55 +22,55 @@ const initialNodes: Node[] = [
|
||||
id: '00',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 250 },
|
||||
data: null,
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 50 },
|
||||
data: null,
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
type: 'custom',
|
||||
position: { x: 500, y: 50 },
|
||||
data: null,
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
type: 'custom',
|
||||
position: { x: 500, y: 500 },
|
||||
data: null,
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '04',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 500 },
|
||||
data: null,
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 5 },
|
||||
data: null,
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
type: 'custom',
|
||||
position: { x: 600, y: 250 },
|
||||
data: null,
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '30',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 600 },
|
||||
data: null,
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '40',
|
||||
type: 'custom',
|
||||
position: { x: 5, y: 250 },
|
||||
data: null,
|
||||
data: {},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -183,11 +183,11 @@ const getId = () => `${id++}`;
|
||||
const UpdateNodeInternalsFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
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 onEdgeUpdate = useCallback(
|
||||
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)),
|
||||
const onReconnect = useCallback(
|
||||
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -196,12 +196,12 @@ const UpdateNodeInternalsFlow = () => {
|
||||
setNodes((nds) =>
|
||||
nds.concat({
|
||||
id: getId(),
|
||||
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
|
||||
position: screenToFlowPosition({ x: evt.clientX, y: evt.clientY }),
|
||||
type: 'custom',
|
||||
data: null,
|
||||
})
|
||||
),
|
||||
[project, setNodes]
|
||||
[screenToFlowPosition, setNodes]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -215,7 +215,7 @@ const UpdateNodeInternalsFlow = () => {
|
||||
onPaneClick={onPaneClick}
|
||||
connectionLineType={ConnectionLineType.Bezier}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onReconnect={onReconnect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
@@ -13,6 +13,7 @@ const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
|
||||
const UpdateNode = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const { updateNode } = useReactFlow();
|
||||
|
||||
const [nodeName, setNodeName] = useState<string>('Node 1');
|
||||
const [nodeBg, setNodeBg] = useState<string>('#eee');
|
||||
@@ -23,9 +24,12 @@ const UpdateNode = () => {
|
||||
nds.map((n) => {
|
||||
if (n.id === '1') {
|
||||
// it's important that you create a new object here in order to notify react flow about the change
|
||||
n.data = {
|
||||
...n.data,
|
||||
label: nodeName,
|
||||
return {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
label: nodeName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +43,10 @@ const UpdateNode = () => {
|
||||
nds.map((n) => {
|
||||
if (n.id === '1') {
|
||||
// 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;
|
||||
@@ -51,8 +58,10 @@ const UpdateNode = () => {
|
||||
setNodes((nds) =>
|
||||
nds.map((n) => {
|
||||
if (n.id === '1' || n.id === 'e1-2') {
|
||||
// when you update a simple type you can just update the value
|
||||
n.hidden = nodeHidden;
|
||||
return {
|
||||
...n,
|
||||
hidden: nodeHidden,
|
||||
};
|
||||
}
|
||||
|
||||
return n;
|
||||
@@ -80,9 +89,19 @@ const UpdateNode = () => {
|
||||
<label>hidden:</label>
|
||||
<input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} />
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => updateNode('1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }))}
|
||||
>
|
||||
update position
|
||||
</button>
|
||||
</div>
|
||||
</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 { useEffect } from 'react';
|
||||
|
||||
const UseKeyPressComponent = () => {
|
||||
const metaPressed = useKeyPress(['Meta']);
|
||||
|
||||
console.log({ metaPressed });
|
||||
useEffect(() => {
|
||||
console.log({ metaPressed });
|
||||
}, [metaPressed]);
|
||||
|
||||
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,
|
||||
OnConnect,
|
||||
useNodesInitialized,
|
||||
Panel,
|
||||
useReactFlow,
|
||||
} from '@xyflow/react';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
@@ -49,6 +51,7 @@ const initialEdges: Edge[] = [
|
||||
];
|
||||
|
||||
const UseZoomPanHelperFlow = () => {
|
||||
const { addNodes } = useReactFlow();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
@@ -59,6 +62,13 @@ const UseZoomPanHelperFlow = () => {
|
||||
console.log('initialized', initialized);
|
||||
}, [initialized]);
|
||||
|
||||
const addNode = () =>
|
||||
addNodes({
|
||||
id: `${Math.random()}`,
|
||||
data: { label: 'new node' },
|
||||
position: { x: Math.random() * 400, y: Math.random() * 400 },
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
@@ -71,6 +81,9 @@ const UseZoomPanHelperFlow = () => {
|
||||
>
|
||||
<Background />
|
||||
<MiniMap />
|
||||
<Panel>
|
||||
<button onClick={addNode}>add node</button>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
addEdge,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useEdgesState,
|
||||
useOnSelectionChange,
|
||||
OnSelectionChangeParams,
|
||||
OnSelectionChangeFunc,
|
||||
} from '@xyflow/react';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
@@ -35,9 +36,9 @@ const initialEdges: Edge[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const SelectionLogger = () => {
|
||||
const SelectionLogger = ({ id }: { id: string }) => {
|
||||
const onChange = useCallback(({ nodes, edges }: OnSelectionChangeParams) => {
|
||||
console.log(nodes, edges);
|
||||
console.log(id, nodes, edges);
|
||||
}, []);
|
||||
|
||||
useOnSelectionChange({
|
||||
@@ -63,14 +64,23 @@ const Flow = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const WrappedFlow = () => (
|
||||
<ReactFlowProvider>
|
||||
<Flow />
|
||||
<SelectionLogger />
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<input type={'text'} placeholder={'name'} />
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
const WrappedFlow = () => {
|
||||
const [secondLoggerActive, setSecondLoggerActive] = useState<boolean>(true);
|
||||
|
||||
const toggleSecondLogger = () => {
|
||||
setSecondLoggerActive(!secondLoggerActive);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, MouseEvent, useEffect } from 'react';
|
||||
import { useCallback, MouseEvent, useEffect, useRef } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
@@ -27,18 +27,21 @@ const initialNodes: Node[] = [
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
className: 'light',
|
||||
type: 'default',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
type: 'default',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
className: 'light',
|
||||
type: 'default',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -57,7 +60,7 @@ const UseZoomPanHelperFlow = () => {
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
const {
|
||||
project,
|
||||
screenToFlowPosition,
|
||||
setCenter,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
@@ -68,13 +71,14 @@ const UseZoomPanHelperFlow = () => {
|
||||
getNodes,
|
||||
getEdges,
|
||||
deleteElements,
|
||||
updateNodeData,
|
||||
} = useReactFlow();
|
||||
|
||||
const onPaneClick = useCallback(
|
||||
(evt: MouseEvent) => {
|
||||
const projectedPosition = project({
|
||||
const projectedPosition = screenToFlowPosition({
|
||||
x: evt.clientX,
|
||||
y: evt.clientY - 40,
|
||||
y: evt.clientY,
|
||||
});
|
||||
|
||||
setNodes((nds) =>
|
||||
@@ -84,10 +88,11 @@ const UseZoomPanHelperFlow = () => {
|
||||
data: {
|
||||
label: `${projectedPosition.x}-${projectedPosition.y}`,
|
||||
},
|
||||
type: 'default',
|
||||
})
|
||||
);
|
||||
},
|
||||
[project, setNodes]
|
||||
[screenToFlowPosition, setNodes]
|
||||
);
|
||||
|
||||
const onNodeClick = useCallback(
|
||||
@@ -105,6 +110,7 @@ const UseZoomPanHelperFlow = () => {
|
||||
data: {
|
||||
label: 'New Node',
|
||||
},
|
||||
type: 'default',
|
||||
};
|
||||
|
||||
addNodes(newNode);
|
||||
@@ -125,12 +131,31 @@ const UseZoomPanHelperFlow = () => {
|
||||
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
|
||||
}, []);
|
||||
|
||||
const edgeAdded = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
addEdges({ id: 'e3-4', source: '3', target: '4' });
|
||||
if (!edgeAdded.current) {
|
||||
addEdges({ id: 'e3-4', source: '3', target: '4' });
|
||||
edgeAdded.current = true;
|
||||
}
|
||||
}, [addEdges]);
|
||||
|
||||
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 (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
@@ -153,6 +178,8 @@ const UseZoomPanHelperFlow = () => {
|
||||
<button onClick={logNodes}>useNodes</button>
|
||||
<button onClick={deleteSelectedElements}>deleteSelectedElements</button>
|
||||
<button onClick={deleteSomeElements}>deleteSomeElements</button>
|
||||
<button onClick={onSetNodes}>setNodes</button>
|
||||
<button onClick={onUpdateNode}>updateNode</button>
|
||||
</Panel>
|
||||
<Background />
|
||||
<MiniMap />
|
||||
|
||||
@@ -33,23 +33,23 @@ const getId = (): string => `${id++}`;
|
||||
|
||||
const UpdateNodeInternalsFlow = () => {
|
||||
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 { project } = useReactFlow();
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
|
||||
const onPaneClick = useCallback(
|
||||
(evt: MouseEvent) =>
|
||||
setNodes((nds) =>
|
||||
nds.concat({
|
||||
id: getId(),
|
||||
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
|
||||
position: screenToFlowPosition({ x: evt.clientX, y: evt.clientY }),
|
||||
data: { label: 'new node' },
|
||||
targetPosition: Position.Left,
|
||||
sourcePosition: Position.Right,
|
||||
})
|
||||
),
|
||||
[project, setNodes]
|
||||
[screenToFlowPosition, setNodes]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user