Merge branch 'main' into middlewares

This commit is contained in:
Moritz Klack
2025-12-02 16:34:21 +01:00
committed by GitHub
115 changed files with 2360 additions and 426 deletions

View File

@@ -0,0 +1,5 @@
---
'@xyflow/react': patch
---
Prevent unnecessary re-render in `FlowRenderer`

View File

@@ -0,0 +1,5 @@
---
'@xyflow/system': patch
---
Allow custom `getEdgeId` function in `addEdge` and `reconnectEdge` options to enable custom edge ID schemes.

View File

@@ -0,0 +1,5 @@
---
'@xyflow/react': patch
---
Always create a new measured object in apply changes.

View File

@@ -0,0 +1,7 @@
---
'@xyflow/react': patch
'@xyflow/svelte': patch
'@xyflow/system': patch
---
Update an ongoing connection when user moves node with keyboard.

696
.coderabbit.yaml Normal file
View File

@@ -0,0 +1,696 @@
# Set the language for reviews by using the corresponding ISO language code.
language: 'en-US'
# Set the tone of reviews and chat. Example: 'You must use talk like Mr. T. I pity the fool who doesn't!'
tone_instructions: ''
# Enable early-access features.
early_access: false
# Enable free tier features for users not on a paid plan.
enable_free_tier: true
# =============================================================================
# REVIEWS
# Settings related to reviews.
# =============================================================================
# Settings related to reviews.
# Default: {}
reviews:
# Set the profile for reviews. Assertive profile yields more feedback, that may be considered nitpicky.
# Options: chill, assertive
# Default: "chill"
profile: 'chill'
# Approve the review once CodeRabbits comments are resolved and no pre-merge checks are in an error state. Note: In GitLab, all discussions must be resolved.
# Default: false
request_changes_workflow: false
# Generate a high level summary of the changes in the PR/MR description.
# Default: true
high_level_summary: false
# Placeholder in the PR/MR description that gets replaced with the high level summary.
# Default: "@coderabbitai summary"
high_level_summary_placeholder: '@coderabbitai summary'
# Include the high level summary in the walkthrough comment.
# Default: false
high_level_summary_in_walkthrough: false
# Add this keyword in the PR/MR title to auto-generate the title.
# Default: "@coderabbitai"
auto_title_placeholder: '@coderabbitai'
# Auto Title Instructions - Custom instructions for auto-generating the PR/MR title.
# Default: ""
auto_title_instructions: ''
# Post review details on each review. Additionally, post a review status when a review is skipped in certain cases.
# Default: true
review_status: false
# Set the commit status to 'pending' when the review is in progress and 'success' when it is complete.
# Default: true
commit_status: false
# Set the commit status to 'failure' when the PR cannot be reviewed by CodeRabbit for any reason.
# Default: false
fail_commit_status: false
# Generate walkthrough in a markdown collapsible section.
# Default: false
collapse_walkthrough: false
# Generate a summary of the changed files in the walkthrough.
# Default: true
changed_files_summary: false
# Generate sequence diagrams in the walkthrough.
# Default: true
sequence_diagrams: false
# Estimate the code review effort in the walkthrough.
# Default: true
estimate_code_review_effort: false
# Generate an assessment of how well the changes address the linked issues in the walkthrough.
# Default: true
assess_linked_issues: false
# Include possibly related issues in the walkthrough.
# Default: true
related_issues: false
# Related PRs - Include possibly related pull requests in the walkthrough.
# Default: true
related_prs: false
# Suggest labels based on the changes in the pull request in the walkthrough.
# Default: true
suggested_labels: false
# Labeling Instructions - Provide guidelines for suggesting labels for the PR/MR. When specific labels or instructions are provided, only those labels are considered, though previous examples are still used to inform the suggestions. If no such labels are provided, suggestions are based solely on previous PR/MRs.
# Default: []
labeling_instructions: []
# Automatically apply the suggested labels to the PR/MR.
# Default: false
auto_apply_labels: false
# Suggest reviewers based on the changes in the pull request in the walkthrough.
# Default: true
suggested_reviewers: false
# Automatically assign suggested reviewers to the pull request
# Default: false
auto_assign_reviewers: false
# Post an in-progress fortune message while the review is in progress.
# Default: true
in_progress_fortune: false
# Generate a poem in the walkthrough comment.
# Default: true
poem: false
# Specify file patterns to include or exclude in a review using glob patterns (e.g., !dist/**, src/**). These patterns also apply to 'git sparse-checkout', including specified patterns and ignoring excluded ones (starting with '!') when cloning the repository.
# Default: []
path_filters: []
# Path Instructions - Provide specific additional guidelines for code review based on file paths.
# Default: []
path_instructions: []
# Abort the in-progress review if the pull request is closed or merged.
# Default: true
abort_on_close: true
# Disable caching of code and dependencies. This will force CodeRabbit to download the code and dependencies fresh from the repository each time.
# Default: false
disable_cache: false
# Configuration for auto review
# Default: {}
auto_review:
# Automatic Review - Automatic code review
# Default: true
enabled: true
# Automatic Incremental Review - Automatic incremental code review on each push
# Default: true
auto_incremental_review: true
# Ignore reviewing if the title of the pull request contains any of these keywords (case-insensitive).
# Default: []
ignore_title_keywords: []
# List of labels to control which PRs/MRs to review. Labels starting with '!' are negative matches. Examples: ['bug', 'feature'] - reviews PRs with 'bug' OR 'feature' label. ['!wip'] - reviews all PRs except those with 'wip' label. ['bug', '!wip'] - reviews PRs with 'bug' label but not if they have 'wip' label.
# Default: []
labels: []
# Review draft PRs/MRs.
# Default: false
drafts: false
# Base branches (other than the default branch) to review. Accepts regex patterns. Use '.*' to match all branches.
# Default: []
base_branches: []
# Ignore reviewing pull requests by these usernames. These should match the Git platform usernames exactly, not the email addresses.
# Default: []
ignore_usernames: []
# Configuration for finishing touches
# Default: {}
finishing_touches:
# Docstrings - Options for generating Docstrings for your PRs/MRs.
# Default: {}
docstrings:
# Docstrings - Allow CodeRabbit to generate docstrings for PRs/MRs.
# Default: true
enabled: false
# Unit Tests - Options for generating unit tests for your PRs/MRs.
# Default: {}
unit_tests:
# Unit Tests - Allow CodeRabbit to generate unit tests for PRs/MRs.
# Default: true
enabled: false
# Configuration for pre merge checks
# Default: {}
pre_merge_checks:
# Docstring Coverage - Checks if the code has sufficient docstrings.
# Default: {}
docstrings:
# Mode - Determines how strictly the docstring coverage check is enforced. Warning will only generate a warning and does not require the user to resolve the check. Error requires the user to resolve issues before merging the pull request. If set to error and the request changes workflow is enabled, the pull request will be blocked until the issues are resolved.
# Options: off, warning, error
# Default: "warning"
mode: 'warning'
# Percentage threshold for docstring coverage check.
# Default: 80
threshold: 80
# Title Check - Checks if the pull request title is appropriate and follows best practices.
# Default: {}
title:
# Mode - Determines how strictly the title check is enforced. Warning will only generate a warning and does not require the user to resolve the check. Error requires the user to resolve issues before merging the pull request. If set to error and the request changes workflow is enabled, the pull request will be blocked until the issues are resolved.
# Options: off, warning, error
# Default: "warning"
mode: 'warning'
# Requirements - Requirements for the pull request title. Example: 'Title should be concise and descriptive, ideally under 50 characters.'
# Default: ""
requirements: ''
# Description Check - Checks if the pull request description is appropriate and follows best practices.
# Default: {}
description:
# Mode - Determines how strictly the description check is enforced. Warning will only generate a warning and does not require the user to resolve the check. Error requires the user to resolve issues before merging the pull request. If set to error and the request changes workflow is enabled, the pull request will be blocked until the issues are resolved.
# Options: off, warning, error
# Default: "warning"
mode: 'warning'
# Linked Issue Assessment - Checks if the pull request addresses the linked issues. Generate an assessment of how well the changes address the linked issues.
# Default: {}
issue_assessment:
# Mode - Determines how strictly the issue assessment check is enforced. Warning will only generate a warning and does not require the user to resolve the check. Error requires the user to resolve issues before merging the pull request. If set to error and the request changes workflow is enabled, the pull request will be blocked until the issues are resolved.
# Options: off, warning, error
# Default: "warning"
mode: 'warning'
# Custom Pre-merge Checks - Add unique checks to enforce your team's standards before merging a pull request. Each check must have a unique name (up to 50 characters) and clear instructions (up to 10000 characters). Use these to automatically verify coding, security, documentation, or business rules and maintain code quality.
# Default: []
custom_checks: []
# Tools that provide additional context to code reviews.
# Default: {}
tools:
# Enable ast-grep - ast-grep is a code analysis tool that helps you to find patterns in your codebase using abstract syntax trees patterns. - v0.39.6
# Default: {}
ast-grep:
# List of rules directories.
# Default: []
rule_dirs: []
# List of utils directories.
# Default: []
util_dirs: []
# Use ast-grep essentials package.
# Default: true
essential_rules: true
# Predefined packages to be used.
# Default: []
packages: []
# ShellCheck is a static analysis tool that finds bugs in your shell scripts.
# Default: {}
shellcheck:
# Enable ShellCheck - ShellCheck is a static analysis tool that finds bugs in your shell. - Enable ShellCheck integration. - v0.11.0
# Default: true
enabled: false
# Ruff is a Python linter and code formatter.
# Default: {}
ruff:
# Enable Ruff - Ruff is a Python linter and code formatter. - Enable Ruff integration. - v0.14.1
# Default: true
enabled: false
# markdownlint-cli2 is a static analysis tool to enforce standards and consistency for Markdown files.
# Default: {}
markdownlint:
# Enable markdownlint - markdownlint-cli2 is a static analysis tool to enforce standards and consistency for Markdown files. - Enable markdownlint integration. - v0.18.1
# Default: true
enabled: true
# GitHub Checks integration configuration.
# Default: {}
github-checks:
# Enable GitHub Checks - Enable integration, defaults to true - Enable GitHub Checks integration.
# Default: true
enabled: true
# Time in milliseconds to wait for all GitHub Checks to conclude. Default 90 seconds, max 15 minutes (900000ms).
# Default: 90000
timeout_ms: 90000
# LanguageTool is a style and grammar checker for 30+ languages.
# Default: {}
languagetool:
# Enable LanguageTool - Enable LanguageTool integration.
# Default: true
enabled: true
# IDs of rules to be enabled. The rule won't run unless 'level' is set to a level that activates the rule.
# Default: []
enabled_rules: []
# IDs of rules to be disabled. Note: EN_UNPAIRED_BRACKETS, and EN_UNPAIRED_QUOTES are always disabled.
# Default: []
disabled_rules: []
# IDs of categories to be enabled.
# Default: []
enabled_categories: []
# IDs of categories to be disabled. Note: TYPOS, TYPOGRAPHY, and CASING are always disabled.
# Default: []
disabled_categories: []
# Only the rules and categories whose IDs are specified with 'enabledRules' or 'enabledCategories' are enabled.
# Default: false
enabled_only: false
# If set to 'picky', additional rules will be activated, i.e. rules that you might only find useful when checking formal text.
# Options: default, picky
# Default: "default"
level: 'default'
# Biome is a fast formatter, linter, and analyzer for web projects.
# Default: {}
biome:
# Enable Biome - Biome is a fast formatter, linter, and analyzer for web projects. - Enable Biome integration. - v2.1.2
# Default: true
enabled: true
# Hadolint is a Dockerfile linter.
# Default: {}
hadolint:
# Enable Hadolint - Hadolint is a Dockerfile linter. - Enable Hadolint integration. - v2.14.0
# Default: true
enabled: false
# SwiftLint integration configuration object.
# Default: {}
swiftlint:
# Enable SwiftLint - SwiftLint is a Swift linter. - Enable SwiftLint integration. - v0.57.0
# Default: true
enabled: false
# Optional path to the SwiftLint configuration file relative to the repository. This is useful when the configuration file is named differently than the default '.swiftlint.yml' or '.swiftlint.yaml'.
config_file: 'example-value'
# PHPStan is a tool to analyze PHP code.
# Default: {}
phpstan:
# Enable PHPStan - PHPStan requires [config file](https://phpstan.org/config-reference#config-file) in your repository root. Please ensure that this file contains the `paths:` parameter. - v2.1.31
# Default: true
enabled: false
# Level - Specify the [rule level](https://phpstan.org/user-guide/rule-levels) to run. This setting is ignored if your configuration file already has a `level:` parameter.
# Options: default, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, max
# Default: "default"
level: 'default'
# PHPMD is a tool to find potential problems in PHP code.
# Default: {}
phpmd:
# Enable PHPMD - PHPMD is a tool to find potential problems in PHP code. - v2.15.0
# Default: true
enabled: false
# PHP CodeSniffer is a PHP linter and coding standard checker.
# Default: {}
phpcs:
# Enable PHP CodeSniffer - PHP CodeSniffer is a PHP linter and coding standard checker. - v3.7.2
# Default: true
enabled: false
# golangci-lint is a fast linters runner for Go.
# Default: {}
golangci-lint:
# Enable golangci-lint - golangci-lint is a fast linters runner for Go. - Enable golangci-lint integration. - v2.5.0
# Default: true
enabled: false
# Optional path to the golangci-lint configuration file relative to the repository. Useful when the configuration file is named differently than the default '.golangci.yml', '.golangci.yaml', '.golangci.toml', '.golangci.json'.
config_file: 'example-value'
# YAMLlint is a linter for YAML files.
# Default: {}
yamllint:
# Enable YAMLlint - YAMLlint is a linter for YAML files. - Enable YAMLlint integration. - v1.37.1
# Default: true
enabled: true
# Gitleaks is a secret scanner.
# Default: {}
gitleaks:
# Enable Gitleaks - Gitleaks is a secret scanner. - Enable Gitleaks integration. - v8.28.0
# Default: true
enabled: true
# Checkov is a static code analysis tool for infrastructure-as-code files.
# Default: {}
checkov:
# Enable Checkov - Checkov is a static code analysis tool for infrastructure-as-code files. - v3.2.334
# Default: true
enabled: false
# Detekt is a static code analysis tool for Kotlin files.
# Default: {}
detekt:
# Enable detekt - detekt is a static code analysis tool for Kotlin files. - v1.23.8
# Default: true
enabled: false
# Optional path to the detekt configuration file relative to the repository.
config_file: 'example-value'
# ESLint is a static code analysis tool for JavaScript files.
# Default: {}
eslint:
# Enable ESLint - ESLint is a static code analysis tool for JavaScript files.
# Default: true
enabled: true
# Flake8 is a Python linter that wraps PyFlakes, pycodestyle and Ned Batchelder's McCabe script.
# Default: {}
flake8:
# Enable Flake8 - Flake8 is a Python linter that wraps PyFlakes, pycodestyle and Ned Batchelder's McCabe script. - v7.3.0
# Default: true
enabled: false
# RuboCop is a Ruby static code analyzer (a.k.a. linter ) and code formatter.
# Default: {}
rubocop:
# Enable RuboCop - RuboCop is a Ruby static code analyzer (a.k.a. linter ) and code formatter. - v1.81.1
# Default: true
enabled: false
# Buf offers linting for Protobuf files.
# Default: {}
buf:
# Enable Buf - Buf offers linting for Protobuf files. - v1.58.0
# Default: true
enabled: false
# Regal is a linter and language server for Rego.
# Default: {}
regal:
# Enable Regal - Regal is a linter and language server for Rego. - v0.36.1
# Default: true
enabled: false
# actionlint is a static checker for GitHub Actions workflow files.
# Default: {}
actionlint:
# Enable actionlint - is a static checker for GitHub Actions workflow files. - v1.7.8
# Default: true
enabled: false
# PMD is an extensible multilanguage static code analyzer. Its mainly concerned with Java.
# Default: {}
pmd:
# Enable PMD - PMD is an extensible multilanguage static code analyzer. Its mainly concerned with Java. - v7.17.0
# Default: true
enabled: false
# Optional path to the PMD configuration file relative to the repository.
config_file: 'example-value'
# Configuration for Clang to perform static analysis on C and C++ code
# Default: {}
clang:
# Enable Clang for C/C++ static analysis and code quality checks - v14.0.6
# Default: true
enabled: false
# Cppcheck is a static code analysis tool for the C and C++ programming languages.
# Default: {}
cppcheck:
# Enable Cppcheck - Cppcheck is a static code analysis tool for the C and C++ programming languages. - v2.18.0
# Default: true
enabled: false
# Semgrep is a static analysis tool designed to scan code for security vulnerabilities and code quality issues.
# Default: {}
semgrep:
# Enable Semgrep - Semgrep is a static analysis tool designed to scan code for security vulnerabilities and code quality issues. - Enable Semgrep integration. - v1.140.0
# Default: true
enabled: false
# Optional path to the Semgrep configuration file relative to the repository.
config_file: 'example-value'
# CircleCI tool is a static checker for CircleCI config files.
# Default: {}
circleci:
# Enable CircleCI - CircleCI tool is a static checker for CircleCI config files. - v0.1.33494
# Default: true
enabled: false
# Clippy is a collection of lints to catch common mistakes and improve your Rust code.
# Default: {}
clippy:
# Enable Clippy - Clippy is a collection of lints to catch common mistakes and improve your Rust code. - Enable Clippy integration.
# Default: true
enabled: false
# SQLFluff is an open source, dialect-flexible and configurable SQL linter.
# Default: {}
sqlfluff:
# Enable SQLFluff - SQLFluff is an open source, dialect-flexible and configurable SQL linter. - v3.5.0
# Default: true
enabled: false
# Configuration for Prisma Schema linting to ensure schema file quality
# Default: {}
prismaLint:
# Enable Prisma Schema linting - Prisma Schema linting helps maintain consistent and error-free schema files - v0.10.3
# Default: true
enabled: false
# Pylint is a Python static code analysis tool.
# Default: {}
pylint:
# Enable Pylint - Pylint is a Python static code analysis tool. - v4.0.1
# Default: true
enabled: false
# Oxlint is a JavaScript/TypeScript linter for OXC written in Rust.
# Default: {}
oxc:
# Enable Oxlint - Oxlint is a JavaScript/TypeScript linter for OXC written in Rust. - v1.23.0
# Default: true
enabled: false
# Configuration for Shopify Theme Check to ensure theme quality and best practices
# Default: {}
shopifyThemeCheck:
# Enable Shopify Theme Check - A linter for Shopify themes that helps you follow Shopify theme & Liquid best practices - cli 3.84.2 - theme 3.58.2
# Default: true
enabled: false
# Configuration for Lua code linting to ensure code quality
# Default: {}
luacheck:
# Enable Lua code linting - Luacheck helps maintain consistent and error-free Lua code - v1.2.0
# Default: true
enabled: false
# Brakeman is a static analysis security vulnerability scanner for Ruby on Rails applications. - v7.1.0
# Default: {}
brakeman:
# Enable Brakeman - Brakeman is a static analysis security vulnerability scanner for Ruby on Rails applications. - v7.1.0
# Default: true
enabled: false
# dotenv-linter is a tool for checking and fixing .env files for problems and best practices
# Default: {}
dotenvLint:
# Enable dotenv-linter - dotenv-linter is a tool for checking and fixing .env files for problems and best practices - v4.0.0
# Default: true
enabled: false
# HTMLHint is a static code analysis tool for HTML files.
# Default: {}
htmlhint:
# Enable HTMLHint - HTMLHint is a static code analysis tool for HTML files. - Enable HTMLHint integration. - v1.7.1
# Default: true
enabled: false
# checkmake is a linter for Makefiles.
# Default: {}
checkmake:
# Enable checkmake - checkmake is a linter for Makefiles. - v0.2.2
# Default: true
enabled: false
# OSV Scanner is a tool for vulnerability package scanning.
# Default: {}
osvScanner:
# Enable OSV Scanner - OSV Scanner is a tool for vulnerability package scanning - v2.2.3
# Default: true
enabled: false
# =============================================================================
# CHAT
# Configuration for chat
# =============================================================================
# Configuration for chat
# Default: {}
chat:
# Generate art in response to chat messages. CodeRabbit expresses emotions as either ASCII or Emoji art.
# Default: true
art: true
# Enable the bot to reply automatically without requiring the user to tag it.
# Default: true
auto_reply: true
# Configuration for integrations
# Default: {}
integrations:
# Configuration for jira
# Default: {}
jira:
# Jira - Enable the Jira integration for opening issues, etc. 'auto' disables the integration for public repositories.
# Options: auto, enabled, disabled
# Default: "auto"
usage: 'disabled'
# Configuration for linear
# Default: {}
linear:
# Linear - Enable the Linear integration for opening issues, etc. 'auto' disables the integration for public repositories.
# Options: auto, enabled, disabled
# Default: "auto"
usage: 'disabled'
# =============================================================================
# KNOWLEDGE BASE
# Configuration for knowledge base
# =============================================================================
# Configuration for knowledge base
# Default: {}
knowledge_base:
# Opt Out - Disable all knowledge base features that require data retention. If you opt out after opting in, all of your existing knowledge base data will be removed from the system.
# Default: false
opt_out: false
# Configuration for web search
# Default: {}
web_search:
# Web Search - Enable the web search integration.
# Default: true
enabled: true
# CodeRabbit will analyse and learn from your organization's code guidelines, which you can mention in the file patterns section. These guidelines will then be used to conduct thorough code reviews.
# Default: {}
code_guidelines:
# Enabled - Enable CodeRabbit to enforce your organization's coding standards during reviews.
# Default: true
enabled: true
# File Patterns - Specify files for your coding guideline documents in this section. CodeRabbit will scan these files to understand your team's standards and apply them during code reviews. Multiple files supported. File names are case-sensitive. Common files like: (**/.cursorrules, .github/copilot-instructions.md, .github/instructions/*.instructions.md, **/CLAUDE.md, **/GEMINI.md, **/.cursor/rules/*, **/.windsurfrules, **/.clinerules/*, **/.rules/*, **/AGENT.md, **/AGENTS.md) are included by default.
# Default: []
filePatterns: []
# Configuration for learnings
# Default: {}
learnings:
# Learnings - Specify the scope of learnings to use for the knowledge base. 'local' uses the repository's learnings, 'global' uses the organization's learnings, and 'auto' uses repository's learnings for public repositories and organization's learnings for private repositories.
# Options: local, global, auto
# Default: "auto"
scope: 'auto'
# Configuration for issues
# Default: {}
issues:
# Issues - Specify the scope of git platform (GitHub/GitLab) issues to use for the knowledge base. 'local' uses the repository's issues, 'global' uses the organization's issues, and 'auto' uses repository's issues for public repositories and organization's issues for private repositories.
# Options: local, global, auto
# Default: "auto"
scope: 'auto'
# Configuration for jira
# Default: {}
jira:
# Jira - Enable the Jira knowledge base integration. 'auto' disables the integration for public repositories.
# Options: auto, enabled, disabled
# Default: "auto"
usage: 'disabled'
# Jira Project Keys - Specify the Jira project keys to use for the knowledge base.
# Default: []
project_keys: []
# Configuration for linear
# Default: {}
linear:
# Linear - Enable the Linear knowledge base integration. 'auto' disables the integration for public repositories.
# Options: auto, enabled, disabled
# Default: "auto"
usage: 'disabled'
# Linear Team Keys - Specify the Linear team keys (identifiers) to use for the knowledge base. E.g. 'ENG'
# Default: []
team_keys: []
# Configuration for pull requests
# Default: {}
pull_requests:
# Pull Requests - Specify the scope of pull requests to use for the knowledge base. 'local' uses the repository's pull requests, 'global' uses the organization's pull requests, and 'auto' uses repository's pull requests for public repositories and organization's pull requests for private repositories.
# Options: local, global, auto
# Default: "auto"
scope: 'auto'
# Configuration for mcp
# Default: {}
mcp:
# MCP - Enable the MCP knowledge base integration. 'auto' disables the integration for public repositories.
# Options: auto, enabled, disabled
# Default: "auto"
usage: 'auto'
# MCP Disabled Servers - Specify MCP server labels to disable (case-insensitive). These servers will be excluded from reviews and knowledge base queries.
# Default: []
disabled_servers: []

View File

@@ -3,7 +3,7 @@ run-name: Dispatch Website Update for ${{ github.event.release.tag_name }}
on:
release:
types: [published]
types: [published, edited]
jobs:
dispatch_website_update:

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019-2024 webkid GmbH
Copyright (c) 2019-2025 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

View File

@@ -156,7 +156,7 @@ For releasing packages we are using [changesets](https://github.com/changesets/c
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
3. changeset creates a PR that bumps all packages based on the changesets
4. merge changeset PR if you want to release to Github and npm
## Built by [xyflow](https://xyflow.com)

49
SECURITY.md Normal file
View File

@@ -0,0 +1,49 @@
# Security Policy
## Reporting a Vulnerability
To report a vulnerability, please privately report it via the Security tab on the correct GitHub repository ([see documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)). Do not open a public issue. Provide:
- A clear description of the issue
- Steps to reproduce
- Expected vs actual behavior
- Potential impact
- A proof of concept if possible
- Affected commit / version (if known)
## Acknowledgment Timeline
We aim to acknowledge receipt of a valid report within 1 week.
## Resolution Timeline
We aim to provide a remediation plan or decision within 4 weeks. Actual fix time may be shorter or longer depending on severity, complexity, and scope.
## Scope & Threat Model
In scope:
- Vulnerabilities introduced by code in this repository
- Supply-chain risks caused by how this repository consumes or distributes its own code (e.g. insecure GitHub Actions)
Out of scope:
- Issues only present in third-party dependencies (please report those upstream)
- Issues that can only be exploited when the underlying platform (browser, server runtime) is compromised
- Denial of service via excessive legitimate use
### Supported Versions
The following versions are currently supported with security updates:
| Package | Version | Supported |
| -------------- | ------- | --------------------- |
| @xyflow/react | 12.x | ✅ Fully supported |
| @xyflow/svelte | 1.x | ✅ Fully supported |
| @xyflow/system | 0.x | ✅ Fully supported |
| reactflow | > 11.x | ⚠ Depends on severity |
| reactflow | < 11.x | No longer |
## Disclosure
Please keep reports private until a fix is released and a Security Advisory is public.

8
dependabot.yml Normal file
View File

@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/packages/'
schedule:
interval: 'weekly'
ignore:
- dependency-type: 'development'

View File

@@ -17,9 +17,9 @@
"@types/react-dom": "^18.2.8",
"@xyflow/react": "workspace:^",
"@xyflow/svelte": "workspace:^",
"astro": "^5.9.2",
"astro": "^5.13.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"svelte": "^5.33.18"
"svelte": "^5.38.7"
}
}

View File

@@ -60,7 +60,7 @@ const initialNodes: Node[] = [
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
position: { x: 900, y: 100 },
...nodeSize,
handles: [
{
@@ -110,9 +110,10 @@ function Flow() {
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
// fitView
width={700}
height={400}
onlyRenderVisibleElements
>
<Background />
<Controls />

View File

@@ -54,7 +54,7 @@
},
{
id: 'C',
position: { x: 250, y: 300 },
position: { x: 950, y: 300 },
data: { label: 'C' },
sourcePosition: Position.Right,
targetPosition: Position.Left,
@@ -79,7 +79,7 @@
</script>
<div style="height: 400px; width: 700px;">
<SvelteFlow bind:nodes bind:edges fitView {defaultEdgeOptions} width={700} height={400}>
<SvelteFlow bind:nodes bind:edges onlyRenderVisibleElements {defaultEdgeOptions} width={700} height={400}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<ViewportPortal target="front">

View File

@@ -50,6 +50,7 @@ import CancelConnection from '../examples/CancelConnection';
import InteractiveMinimap from '../examples/InteractiveMinimap';
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
import NodeToolbar from '../examples/NodeToolbar';
import EdgeToolbar from '../examples/EdgeToolbar';
import UseConnection from '../examples/UseConnection';
import UseNodesInitialized from '../examples/UseNodesInit';
import UseNodesData from '../examples/UseNodesData';
@@ -193,6 +194,11 @@ const routes: IRoute[] = [
path: 'edge-routing',
component: EdgeRouting,
},
{
name: 'Edge Toolbar',
path: 'edge-toolbar',
component: EdgeToolbar,
},
{
name: 'Empty',
path: 'empty',

View File

@@ -1,4 +1,4 @@
import { MouseEvent, useCallback } from 'react';
import { MouseEvent, useCallback, useState } from 'react';
import {
ReactFlow,
MiniMap,
@@ -134,47 +134,57 @@ const BasicFlow = () => {
});
fitView();
};
const [isHidden, setIsHidden] = useState(false);
const toggleVisibility = () => {
setIsHidden(!isHidden);
};
return (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
onNodesChange={console.log}
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}
fitView
fitViewOptions={fitViewOptions}
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
elevateEdgesOnSelect
elevateNodesOnSelect={false}
nodeDragThreshold={0}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
<>
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
onNodesChange={console.log}
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"
style={{ display: isHidden ? 'none' : 'block' }}
minZoom={0.2}
maxZoom={4}
fitView
fitViewOptions={fitViewOptions}
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
elevateEdgesOnSelect
elevateNodesOnSelect={false}
nodeDragThreshold={0}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
<Panel position="top-right">
<button onClick={resetTransform}>reset transform</button>
<button onClick={updatePos}>change pos</button>
<button onClick={toggleClassnames}>toggle classnames</button>
<button onClick={logToObject}>toObject</button>
<Panel position="top-right">
<button onClick={resetTransform}>reset transform</button>
<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>
<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>
<button onClick={toggleVisibility} style={{ position: 'absolute', zIndex: 10, right: 10, top: 100 }}>
{isHidden ? 'Show' : 'Hide'} Flow
</button>
</>
);
};

View File

@@ -1,6 +1,7 @@
import { ConnectionLineComponentProps } from '@xyflow/react';
function ConnectionLine({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) {
function ConnectionLine({ fromX, fromY, toX, toY, pointer }: ConnectionLineComponentProps) {
console.log('pointer', pointer);
return (
<>
<path

View File

@@ -1,4 +1,4 @@
import { MouseEvent, CSSProperties, useCallback } from 'react';
import { MouseEvent, CSSProperties, useCallback, useState } from 'react';
import {
ReactFlow,
@@ -14,19 +14,13 @@ import {
ReactFlowInstance,
useEdgesState,
useNodesState,
Panel,
} from '@xyflow/react';
const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('flow loaded:', reactFlowInstance);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const buttonStyle: CSSProperties = {
position: 'absolute',
left: 10,
top: 10,
zIndex: 4,
};
const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => {
return <circle cx={x} cy={y} r={Math.max(width, height) / 2} fill="#ffcc00" />;
};
@@ -34,6 +28,7 @@ const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => {
const CustomMiniMapNodeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const [hideAllNodes, setHideAllNodes] = useState(false);
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
const addRandomNode = () => {
@@ -45,10 +40,20 @@ const CustomMiniMapNodeFlow = () => {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
hidden: hideAllNodes,
};
setNodes((nds) => nds.concat(newNode));
};
const toggleHideAllNodes = () => {
setHideAllNodes(prev => {
const next = !prev;
setNodes(nds => nds.map(n => ({ ...n, hidden: next })));
setEdges(eds => eds.map(e => ({ ...e, hidden: next })));
return next;
});
};
return (
<ReactFlow
nodes={nodes}
@@ -65,9 +70,14 @@ const CustomMiniMapNodeFlow = () => {
<Background variant={BackgroundVariant.Lines} />
<MiniMap nodeComponent={CustomMiniMapNode} />
<button type="button" onClick={addRandomNode} style={buttonStyle}>
add node
</button>
<Panel position="top-left">
<button type="button" onClick={addRandomNode}>
add node
</button>
<button type="button" onClick={toggleHideAllNodes}>
{hideAllNodes ? 'show all nodes' : 'hide all nodes'}
</button>
</Panel>
</ReactFlow>
);
};

View File

@@ -30,7 +30,7 @@ function ColorSelectorNode({ data, isConnectable }: NodeProps<ColorSelectorNode>
<div>
Custom Color Picker Node: <strong>{data.color}</strong>
</div>
<input className="nodrag" type="color" onChange={data.onChange} defaultValue={data.color} />
<input className="nodrag nokey" type="color" onChange={data.onChange} defaultValue={data.color} />
<Handle
type="source"
position={Position.Right}

View File

@@ -0,0 +1,40 @@
import { getBezierPath, BaseEdge, EdgeProps, useReactFlow, getStraightPath, getSmoothStepPath } from '@xyflow/react';
import { EdgeToolbar } from '@xyflow/react';
const getPath = (props: EdgeProps) => {
switch (props.data!.type) {
case 'smoothstep':
return getSmoothStepPath(props);
case 'straight':
return getStraightPath(props);
default:
return getBezierPath(props);
}
};
export function CustomEdge(props: EdgeProps) {
const [edgePath, centerX, centerY] = getPath(props);
const { setEdges } = useReactFlow();
const deleteEdge = () => {
setEdges((edges) => edges.filter((edge) => edge.id !== props.id));
};
return (
<>
<BaseEdge id={props.id} path={edgePath} />
<EdgeToolbar
edgeId={props.id}
x={centerX + 0}
y={centerY + 0}
alignX={props.data?.align?.[0] ?? 'center'}
alignY={props.data?.align?.[1] ?? 'center'}
isVisible
>
<button style={{}} onClick={deleteEdge}>
Delete
</button>
</EdgeToolbar>
</>
);
}

View File

@@ -0,0 +1,80 @@
import {
Background,
BackgroundVariant,
Controls,
Edge,
EdgeTypes,
MiniMap,
Node,
Position,
ReactFlow,
} from '@xyflow/react';
import { CustomEdge } from './CustomEdge';
const edgeTypes: EdgeTypes = {
custom: CustomEdge,
};
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1', toolbarPosition: Position.Top },
position: { x: 0, y: 0 },
className: 'react-flow__node-default',
},
{
id: '2',
data: { label: 'Node 2', toolbarPosition: Position.Top },
position: { x: 100, y: 150 },
className: 'react-flow__node-default',
},
{
id: '3',
data: { label: 'Node 3', toolbarPosition: Position.Top },
position: { x: 200, y: 0 },
className: 'react-flow__node-default',
},
];
const initialEdges: Edge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
type: 'custom',
data: { type: 'smoothstep', align: ['left', 'bottom'] },
},
{
id: 'e3-2',
source: '3',
target: '2',
type: 'custom',
data: { type: 'bezier', align: ['right', 'bottom'] },
},
{
id: 'e1-3',
source: '1',
target: '3',
type: 'custom',
data: { type: 'straight', align: ['center', 'center'] },
},
];
export default function EdgeToolbarExample() {
return (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
className="react-flow-edge-toolbar-example"
minZoom={0.2}
maxZoom={4}
fitView
edgeTypes={edgeTypes}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
</ReactFlow>
);
}

View File

@@ -30,10 +30,11 @@ const BasicFlow = () => {
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
selectionOnDrag
selectionOnDrag={true}
selectionMode={SelectionMode.Partial}
panOnDrag={panOnDrag}
panOnScroll
paneClickDistance={100}
zoomActivationKeyCode="Meta"
multiSelectionKeyCode={MULTI_SELECT_KEY}
onPaneContextMenu={onPaneContextMenu}
@@ -43,6 +44,12 @@ const BasicFlow = () => {
onMoveStart={onMoveStart}
onMove={onMove}
onMoveEnd={onMoveEnd}
onPaneClick={(e) => console.log('pane click', e)}
onSelectionStart={(e) => console.log('on selection start', e)}
onSelectionEnd={(e) => console.log('on selection end', e)}
onPointerDown={(e) => console.log('pointer down', e)}
onPointerUp={(e) => console.log('pointer up', e)}
onClick={(e) => console.log('click', e)}
>
<Background variant={BackgroundVariant.Cross} />
<Controls />

View File

@@ -1,7 +1,9 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps, NodeResizer } from '@xyflow/react';
import { Handle, Position, NodeProps, NodeResizer, useKeyPress } from '@xyflow/react';
const DefaultResizerNode: FC<NodeProps> = ({ data, selected }) => {
const keepAspectRatio = useKeyPress('k');
return (
<>
<NodeResizer
@@ -14,7 +16,7 @@ const DefaultResizerNode: FC<NodeProps> = ({ data, selected }) => {
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
keepAspectRatio={keepAspectRatio || (data.keepAspectRatio ?? undefined)}
/>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>

View File

@@ -239,6 +239,16 @@ const StressFlow = () => {
<button onClick={remount}>re-mount</button>
<button onClick={updatePos}>change pos</button>
<button onClick={updateElements}>update elements</button>
<button
onClick={() => {
setNodes((nds) => [
...nds,
{ id: (nds.length + 1).toString(), position: { x: 0, y: 0 }, data: { label: `Node ${nds.length + 1}` } },
]);
}}
>
Add element
</button>
</Panel>
</ReactFlow>
);

View File

@@ -27,15 +27,15 @@ const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const defaultViewport = { x: 0, y: 0, zoom: 1.5 };
const initialNodes: Node[] = [
{
id: 'extent',
position: { x: 0, y: 0 },
width: 1000,
height: 1000,
data: { label: 'Extent' },
origin: [0, 0],
zIndex: -1,
},
// {
// id: 'extent',
// position: { x: 0, y: 0 },
// width: 1000,
// height: 1000,
// data: { label: 'Extent' },
// origin: [0, 0],
// zIndex: -1,
// },
{
id: '1',
type: 'input',
@@ -96,12 +96,12 @@ const initialNodes: Node[] = [
},
{
id: '5',
type: 'group',
// type: 'group',
data: { label: 'Node 5' },
position: { x: 650, y: 250 },
className: 'light',
style: { width: 100, height: 100 },
zIndex: 1000,
// zIndex: 1000,
},
{
id: '5a',

View File

@@ -7,11 +7,13 @@
'add-node-on-drop',
'color-mode',
'custom-connection-line',
'custom-minimap',
'customnode',
'dagre',
'detached-handle',
'drag-n-drop',
'edges',
'edge-toolbar',
'figma',
'handle-connect',
'interaction',

View File

@@ -19,6 +19,8 @@
}
return null;
});
$inspect(connection.current.pointer);
</script>
{#if connection.current.inProgress}

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import {
SvelteFlow,
Background,
Controls,
MiniMap,
Panel,
type Node,
type Edge
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import CustomMiniMapNode from './CustomMiniMapNode.svelte';
let nodes = $state.raw<Node[]>([]);
let edges = $state.raw<Edge[]>([]);
let hideAllNodes = $state(false);
function addRandomNode() {
const nodeId = (nodes.length + 1).toString();
const newNode: Node = {
id: nodeId,
data: { label: `Node: ${nodeId}` },
position: {
x: Math.random() * (typeof window !== 'undefined' ? window.innerWidth : 800),
y: Math.random() * (typeof window !== 'undefined' ? window.innerHeight : 600)
},
type: 'default',
hidden: hideAllNodes
};
nodes = [...nodes, newNode];
}
function toggleHideAllNodes() {
hideAllNodes = !hideAllNodes;
nodes = nodes.map((node) => ({
...node,
hidden: hideAllNodes
}));
}
</script>
<SvelteFlow bind:nodes bind:edges onlyRenderVisibleElements={true}>
<Controls />
<Background />
<MiniMap nodeComponent={CustomMiniMapNode} />
<Panel position="top-left">
<button type="button" onclick={addRandomNode}> add node </button>
<button type="button" onclick={toggleHideAllNodes}>
{hideAllNodes ? 'show all nodes' : 'hide all nodes'}
</button>
</Panel>
</SvelteFlow>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import type { MiniMapNodeProps } from '@xyflow/svelte';
let { id, x, y, width, height, selected }: MiniMapNodeProps = $props();
</script>
<circle
cx={x + width / 2}
cy={y + height / 2}
r={Math.min(width, height) - 2}
fill={selected ? '#ff6b6b' : '#ffcc00'}
/>
<text
x={x + width / 2}
y={y + height / 2}
text-anchor="middle"
dominant-baseline="middle"
fill="blue"
stroke-width="0.5"
font-size="100"
font-family="Arial, sans-serif"
font-weight="bold"
>
{id}
</text>

View File

@@ -13,7 +13,7 @@
Custom Color Picker Node: <strong>{bg.color}</strong>
</div>
<input
class="nodrag"
class="nodrag nokey"
type="color"
oninput={(evt) => (bg.color = evt.currentTarget.value)}
value={bg.color}

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import { Background, Position, SvelteFlow, type Edge, type Node } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import CustomEdge from './CustomEdge.svelte';
const edgeTypes = {
custom: CustomEdge
};
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1', toolbarPosition: Position.Top },
position: { x: 0, y: 0 },
class: 'svelte-flow__node-default'
},
{
id: '2',
data: { label: 'Node 2', toolbarPosition: Position.Top },
position: { x: 100, y: 150 },
class: 'svelte-flow__node-default'
},
{
id: '3',
data: { label: 'Node 3', toolbarPosition: Position.Top },
position: { x: 200, y: 0 },
class: 'svelte-flow__node-default'
}
];
const initialEdges: Edge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
type: 'custom'
},
{
id: 'e3-2',
source: '3',
target: '2',
type: 'custom'
},
{
id: 'e1-3',
source: '1',
target: '3',
type: 'custom'
}
];
let nodes = $state.raw<Node[]>(initialNodes);
let edges = $state.raw<Edge[]>(initialEdges);
</script>
<SvelteFlow bind:nodes bind:edges {edgeTypes} fitView>
<Background />
</SvelteFlow>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { getBezierPath, BaseEdge, type EdgeProps, useSvelteFlow } from '@xyflow/svelte';
import { EdgeToolbar } from '@xyflow/svelte';
const { deleteElements } = useSvelteFlow();
let { id, sourceX, sourceY, targetX, targetY }: EdgeProps = $props();
let [edgePath, labelX, labelY] = $derived(
getBezierPath({
sourceX,
sourceY,
targetX,
targetY
})
);
const deleteEdge = () => {
deleteElements({ edges: [{ id }] });
};
</script>
<BaseEdge {id} path={edgePath} />
<EdgeToolbar edgeId={id} x={labelX} y={labelY} isVisible>
<button onclick={deleteEdge}>Delete</button>
</EdgeToolbar>

View File

@@ -9,11 +9,6 @@
import '@xyflow/svelte/dist/style.css';
const onPaneContextMenu = (e: any) => {
e.preventDefault();
console.log('context menu');
};
const panOnDrag = [1, 2];
const onmovestart = (e: any) => console.log('move start', e);
@@ -44,12 +39,17 @@
bind:edges
fitView
selectionMode={SelectionMode.Partial}
multiSelectionKey={['Meta', 'Shift']}
selectionOnDrag
panOnScroll
paneClickDistance={100}
{panOnDrag}
{onmovestart}
{onmove}
{onmoveend}
onpaneclick={(e) => console.log('on pane click', e)}
onselectionend={(e) => console.log('on selection end', e)}
onselectionstart={(e) => console.log('on selection start', e)}
>
<Controls />
<Background variant={BackgroundVariant.Dots} />

View File

@@ -5,15 +5,18 @@
import CustomResizer from './CustomResizer.svelte';
import VerticalResizer from './VerticalResizer.svelte';
import HorizontalResizer from './HorizontalResizer.svelte';
import BottomRightResizer from './BottomRightResizer.svelte';
import '@xyflow/svelte/dist/style.css';
import type { ResizeNode } from './types';
import { store } from './store.svelte';
const nodeTypes = {
defaultResizer: DefaultResizer,
customResizer: CustomResizer,
verticalResizer: VerticalResizer,
horizontalResizer: HorizontalResizer
horizontalResizer: HorizontalResizer,
bottomRightResizer: BottomRightResizer
};
const nodeStyle = 'border: 1px solid #222; font-size: 10px; background-color: #ddd;';
@@ -124,12 +127,28 @@
position: { x: 100, y: 100 },
parentId: '5',
style: nodeStyle
},
{
id: '6',
type: 'bottomRightResizer',
data: { label: 'bottom-right horizontal resizer' },
position: { x: 500, y: 0 },
style: nodeStyle
}
]);
let snapToGrid = $state(false);
</script>
<svelte:window
on:keydown={(e) => {
if (e.key === 'k') store.keepAspectRatio = true;
}}
on:keyup={(e) => {
if (e.key === 'k') store.keepAspectRatio = false;
}}
/>
<SvelteFlow
bind:nodes
bind:edges

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import {
Handle,
Position,
type NodeProps,
NodeResizeControl,
ResizeControlVariant
} from '@xyflow/svelte';
import type { ResizeNode } from './types';
let { data }: NodeProps<ResizeNode> = $props();
</script>
<NodeResizeControl
variant={ResizeControlVariant.Handle}
position="bottom-right"
resizeDirection="horizontal"
minWidth={100}
maxWidth={500}
color="orange"
autoScale={false}
/>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { Handle, Position, type NodeProps, NodeResizeControl, type Node } from '@xyflow/svelte';
import type { ResizeNode } from './types';
import { store } from './store.svelte';
let { data }: NodeProps<ResizeNode> = $props();
</script>
@@ -14,7 +15,7 @@
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
keepAspectRatio={store.keepAspectRatio || (data.keepAspectRatio ?? undefined)}
style="background: transparent; border: none;"
>
<svg

View File

@@ -2,6 +2,8 @@
import { Handle, NodeResizer, Position, type NodeProps } from '@xyflow/svelte';
import type { ResizeNode } from './types';
import { store } from './store.svelte';
let { data, selected }: NodeProps<ResizeNode> = $props();
</script>
@@ -15,7 +17,7 @@
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
keepAspectRatio={store.keepAspectRatio || (data.keepAspectRatio ?? undefined)}
/>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte';
import type { ResizeNode } from './types';
import { store } from './store.svelte';
let { data }: NodeProps<ResizeNode> = $props();
</script>
@@ -14,7 +15,7 @@
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
keepAspectRatio={store.keepAspectRatio || (data.keepAspectRatio ?? undefined)}
color="red"
position={Position.Left}
/>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte';
import type { ResizeNode } from './types';
import { store } from './store.svelte';
let { data }: NodeProps<ResizeNode> = $props();
</script>
@@ -14,7 +15,7 @@
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
keepAspectRatio={store.keepAspectRatio || (data.keepAspectRatio ?? undefined)}
color="red"
position={Position.Top}
/>

View File

@@ -0,0 +1,14 @@
function createStore() {
let keepAspectRatio = $state(false);
return {
get keepAspectRatio() {
return keepAspectRatio;
},
set keepAspectRatio(value: boolean) {
keepAspectRatio = value;
}
};
}
export const store = createStore();

View File

@@ -155,6 +155,8 @@
// console.log(edges.map((edge) => ({ id: edge.id, selected: edge.selected })));
edges.forEach((edge) => console.log({ id: edge.id, selected: edge.selected }));
});
let hidden = $state(false);
</script>
<!-- oninit={() => console.log('on init')}
@@ -197,6 +199,7 @@
}} -->
<SvelteFlow
style="display:{hidden ? 'none' : 'block'}"
bind:nodes
bind:edges
{nodeTypes}
@@ -247,7 +250,20 @@
<InitTracker />
</SvelteFlow>
<button
class="hide"
onclick={() => {
hidden = !hidden;
}}>Hide/Unhide</button
>
<style>
.hide {
position: absolute;
top: 80px;
right: 15px;
}
:root {
/* --background-color: #ffffdd; */
--background-pattern-color: #5050ff;

View File

@@ -1,5 +1,84 @@
# @xyflow/react
## 12.9.3
### Patch Changes
- [#5621](https://github.com/xyflow/xyflow/pull/5621) [`c1304dba7`](https://github.com/xyflow/xyflow/commit/c1304dba7a20bb8d74c7aceb23cd80b56e4c0482) Thanks [@moklick](https://github.com/moklick)! - Set `paneClickDistance` default value to `1`.
- [#5578](https://github.com/xyflow/xyflow/pull/5578) [`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773) Thanks [@peterkogo](https://github.com/peterkogo)! - Pass current pointer position to connection
- Updated dependencies [[`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773)]:
- @xyflow/system@0.0.73
## 12.9.2
### Patch Changes
- [#5593](https://github.com/xyflow/xyflow/pull/5593) [`a8ee089d7`](https://github.com/xyflow/xyflow/commit/a8ee089d7689d9a58113690c8e90e1c1e109602a) Thanks [@moklick](https://github.com/moklick)! - Reset selection box when user selects a node
## 12.9.1
### Patch Changes
- [#5572](https://github.com/xyflow/xyflow/pull/5572) [`5ec0cac7f`](https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix onPaneClick events being suppressed when selectionOnDrag=true
- Updated dependencies [[`5ec0cac7f`](https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9)]:
- @xyflow/system@0.0.72
## 12.9.0
### Minor Changes
- [#5544](https://github.com/xyflow/xyflow/pull/5544) [`c17b49f4c`](https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Add `EdgeToolbar` component
- [#5550](https://github.com/xyflow/xyflow/pull/5550) [`6ffb9f790`](https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent child nodes of different parents from overlapping
- [#5551](https://github.com/xyflow/xyflow/pull/5551) [`6bb64b3ed`](https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634) Thanks [@moklick](https://github.com/moklick)! - Allow to start a selection above a node
### Patch Changes
- [#5531](https://github.com/xyflow/xyflow/pull/5531) [`fe2a45ed2`](https://github.com/xyflow/xyflow/commit/fe2a45ed2aa0d96db8e1be0354038be7ce6f824d) Thanks [@Sec-ant](https://github.com/Sec-ant)! - Add `type` property to `EdgeProps` to match `NodeProps` behavior
- [#5528](https://github.com/xyflow/xyflow/pull/5528) [`d50a963cd`](https://github.com/xyflow/xyflow/commit/d50a963cd0f00cd31236d760dcd7995218d80553) Thanks [@peterkogo](https://github.com/peterkogo)! - Let `NodeResizer` props change during ongoing resize
- [#5530](https://github.com/xyflow/xyflow/pull/5530) [`3b9951e42`](https://github.com/xyflow/xyflow/commit/3b9951e42f2041276ac8957154598a7af41f81e9) Thanks [@peterkogo](https://github.com/peterkogo)! - Improve performance of adding nodes & edges
- [#5547](https://github.com/xyflow/xyflow/pull/5547) [`1c8961207`](https://github.com/xyflow/xyflow/commit/1c8961207e8ac326098af18489465b36cdd2d831) Thanks [@moklick](https://github.com/moklick)! - Always call `onMoveEnd` when `onMoveStart` was called
- Updated dependencies [[`c17b49f4c`](https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d), [`6ffb9f790`](https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32), [`d50a963cd`](https://github.com/xyflow/xyflow/commit/d50a963cd0f00cd31236d760dcd7995218d80553), [`6bb64b3ed`](https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634), [`8a9ee2d83`](https://github.com/xyflow/xyflow/commit/8a9ee2d836776da42b867c6ba90d302fbbc79b37), [`1c8961207`](https://github.com/xyflow/xyflow/commit/1c8961207e8ac326098af18489465b36cdd2d831)]:
- @xyflow/system@0.0.71
## 12.8.6
### Patch Changes
- [#5515](https://github.com/xyflow/xyflow/pull/5515) [`9b4e99029`](https://github.com/xyflow/xyflow/commit/9b4e99029ba87d44c71b33c06259a126db193b43) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix id of static handles not being parsed
- [#5512](https://github.com/xyflow/xyflow/pull/5512) [`70b672604`](https://github.com/xyflow/xyflow/commit/70b67260470738dd0d88ed8ab558d400097be785) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent native page zoom when pinch zooming on node with nowheel class
- Updated dependencies [[`9b4e99029`](https://github.com/xyflow/xyflow/commit/9b4e99029ba87d44c71b33c06259a126db193b43), [`70b672604`](https://github.com/xyflow/xyflow/commit/70b67260470738dd0d88ed8ab558d400097be785)]:
- @xyflow/system@0.0.70
## 12.8.5
### Patch Changes
- [#5480](https://github.com/xyflow/xyflow/pull/5480) [`f869808e3`](https://github.com/xyflow/xyflow/commit/f869808e3dfd3d289db034d21d505d62f84cdde3) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent multi-touch events while making a new connection
- [#5482](https://github.com/xyflow/xyflow/pull/5482) [`56381ae54`](https://github.com/xyflow/xyflow/commit/56381ae546f392b13e7c1390df60e48c9072f128) Thanks [@peterkogo](https://github.com/peterkogo)! - Make isNodeIntersecting behave the same as getIntersectingNodes
- [#5509](https://github.com/xyflow/xyflow/pull/5509) [`b6ae4cc11`](https://github.com/xyflow/xyflow/commit/b6ae4cc11f1456f843ace7e7f3de41c9196179e0) Thanks [@moklick](https://github.com/moklick)! - Prevent calling onResizeEnd if node was not resized
- [#5511](https://github.com/xyflow/xyflow/pull/5511) [`d891e5ff1`](https://github.com/xyflow/xyflow/commit/d891e5ff106c6202f69a9b14d261294cc29bbec7) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix regression: elevate edges if connected nodes are selected
- [#5497](https://github.com/xyflow/xyflow/pull/5497) [`eba087e8b`](https://github.com/xyflow/xyflow/commit/eba087e8bec74e6591e04ebf0e1e61799b94ecd8) Thanks [@peterkogo](https://github.com/peterkogo)! - Skip eagerly rendering nodes when node dimensions and handles are predefined
- [#5455](https://github.com/xyflow/xyflow/pull/5455) [`d2df1dced`](https://github.com/xyflow/xyflow/commit/d2df1dced7a7de4ed69b003c0069c7d7141bef05) Thanks [@Sec-ant](https://github.com/Sec-ant)! - Fix warning when display is set to none on the wrapper div
- Updated dependencies [[`f869808e3`](https://github.com/xyflow/xyflow/commit/f869808e3dfd3d289db034d21d505d62f84cdde3), [`2a35c65a0`](https://github.com/xyflow/xyflow/commit/2a35c65a02c6c8aec6b005ac3e72988f996669c4), [`b6ae4cc11`](https://github.com/xyflow/xyflow/commit/b6ae4cc11f1456f843ace7e7f3de41c9196179e0), [`d891e5ff1`](https://github.com/xyflow/xyflow/commit/d891e5ff106c6202f69a9b14d261294cc29bbec7), [`eba087e8b`](https://github.com/xyflow/xyflow/commit/eba087e8bec74e6591e04ebf0e1e61799b94ecd8)]:
- @xyflow/system@0.0.69
## 12.8.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/react",
"version": "12.8.4",
"version": "12.9.3",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [
"react",

View File

@@ -0,0 +1,79 @@
import { useCallback } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import { getEdgeToolbarTransform } from '@xyflow/system';
import { EdgeLabelRenderer } from '../../components/EdgeLabelRenderer';
import { useStore } from '../../hooks/useStore';
import { Edge, ReactFlowState } from '../../types';
import type { EdgeToolbarProps } from './types';
const zoomSelector = (state: ReactFlowState) => state.transform[2];
/**
* This component can render a toolbar or tooltip to one side of a custom edge. This
* toolbar doesn't scale with the viewport so that the content stays the same size.
*
* @public
* @example
* ```jsx
* import { EdgeToolbar, BaseEdge, getBezierPath, type EdgeProps } from "@xyflow/react";
*
* export function CustomEdge({ id, data, ...props }: EdgeProps) {
* const [edgePath, centerX, centerY] = getBezierPath(props);
*
* return (
* <>
* <BaseEdge id={id} path={edgePath} />
* <EdgeToolbar edgeId={id} x={centerX} y={centerY} isVisible>
* <button onClick={() => console.log('edge', id, 'click')}}>Click me</button>
* </EdgeToolbar>
* </>
* );
* }
* ```
*/
export function EdgeToolbar({
edgeId,
x,
y,
children,
className,
style,
isVisible,
alignX = 'center',
alignY = 'center',
...rest
}: EdgeToolbarProps) {
const edgeSelector = useCallback((state: ReactFlowState): Edge | undefined => state.edgeLookup.get(edgeId), [edgeId]);
const edge = useStore(edgeSelector, shallow);
const isActive = typeof isVisible === 'boolean' ? isVisible : edge?.selected;
const zoom = useStore(zoomSelector);
if (!isActive) {
return null;
}
const zIndex = (edge?.zIndex ?? 0) + 1;
const transform = getEdgeToolbarTransform(x, y, zoom, alignX, alignY);
return (
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform,
zIndex,
pointerEvents: 'all',
transformOrigin: '0 0',
...style,
}}
className={cc(['react-flow__edge-toolbar', className])}
data-id={edge?.id ?? ''}
{...rest}
>
{children}
</div>
</EdgeLabelRenderer>
);
}

View File

@@ -0,0 +1,2 @@
export { EdgeToolbar } from './EdgeToolbar';
export type { EdgeToolbarProps } from './types';

View File

@@ -0,0 +1,14 @@
import type { HTMLAttributes, ReactNode } from 'react';
import type { EdgeToolbarBaseProps } from '@xyflow/system';
/**
* @inline
*/
export type EdgeToolbarProps = EdgeToolbarBaseProps &
HTMLAttributes<HTMLDivElement> & {
/**
* An edge toolbar must be attached to an edge.
*/
edgeId: string;
children?: ReactNode;
};

View File

@@ -3,3 +3,4 @@ export * from './Controls';
export * from './MiniMap';
export * from './NodeResizer';
export * from './NodeToolbar';
export * from './EdgeToolbar';

View File

@@ -69,7 +69,7 @@ const ConnectionLine = <NodeType extends Node = Node>({
CustomComponent,
isValid,
}: ConnectionLineProps<NodeType>) => {
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } =
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition, pointer } =
useConnection<NodeType>();
if (!inProgress) {
@@ -92,6 +92,7 @@ const ConnectionLine = <NodeType extends Node = Node>({
connectionStatus={getConnectionStatus(isValid)}
toNode={toNode}
toHandle={toHandle}
pointer={pointer}
/>
);
}

View File

@@ -1,4 +1,4 @@
import { useState, useMemo, useRef, type KeyboardEvent, useCallback, JSX } from 'react';
import { useState, useMemo, useRef, type KeyboardEvent, useCallback, JSX, memo } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import {
@@ -15,7 +15,7 @@ import { builtinEdgeTypes, nullPosition } from './utils';
import { EdgeUpdateAnchors } from './EdgeUpdateAnchors';
import type { Edge, EdgeWrapperProps } from '../../types';
export function EdgeWrapper<EdgeType extends Edge = Edge>({
function EdgeWrapper<EdgeType extends Edge = Edge>({
id,
edgesFocusable,
edgesReconnectable,
@@ -263,3 +263,5 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
</svg>
);
}
export default memo(EdgeWrapper) as typeof EdgeWrapper;

View File

@@ -1,4 +1,4 @@
import { type MouseEvent, type KeyboardEvent } from 'react';
import { type MouseEvent, type KeyboardEvent, memo } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import {
@@ -20,7 +20,7 @@ import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from '.
import { useNodeObserver } from './useNodeObserver';
import type { InternalNode, Node, NodeWrapperProps } from '../../types';
export function NodeWrapper<NodeType extends Node>({
function NodeWrapper<NodeType extends Node>({
id,
onClick,
onMouseEnter,
@@ -249,3 +249,5 @@ export function NodeWrapper<NodeType extends Node>({
</div>
);
}
export default memo(NodeWrapper) as typeof NodeWrapper;

View File

@@ -69,7 +69,6 @@ const reactFlowFieldsToTrack = [
'onBeforeDelete',
'debug',
'autoPanSpeed',
'paneClickDistance',
'ariaLabelConfig',
] as const;
@@ -93,7 +92,6 @@ const selector = (s: ReactFlowState) => ({
setNodeExtent: s.setNodeExtent,
reset: s.reset,
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
setPaneClickDistance: s.setPaneClickDistance,
});
const initPrevValues = {
@@ -109,7 +107,6 @@ const initPrevValues = {
elementsSelectable: true,
noPanClassName: 'nopan',
rfId: '1',
paneClickDistance: 0,
};
export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
@@ -124,7 +121,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
setNodeExtent,
reset,
setDefaultNodesAndEdges,
setPaneClickDistance,
} = useStore(selector, shallow);
const store = useStoreApi<NodeType, EdgeType>();
@@ -155,7 +151,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
else if (fieldName === 'maxZoom') setMaxZoom(fieldValue as number);
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
else if (fieldName === 'ariaLabelConfig')
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
// Renamed fields

View File

@@ -5,7 +5,7 @@ import { useStore } from '../../hooks/useStore';
import { useVisibleEdgeIds } from '../../hooks/useVisibleEdgeIds';
import MarkerDefinitions from './MarkerDefinitions';
import { GraphViewProps } from '../GraphView';
import { EdgeWrapper } from '../../components/EdgeWrapper';
import EdgeWrapper from '../../components/EdgeWrapper';
import type { Edge, ReactFlowState, Node } from '../../types';
type EdgeRendererProps<EdgeType extends Edge = Edge> = Pick<

View File

@@ -1,4 +1,5 @@
import { memo, type ReactNode } from 'react';
import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore';
import { useGlobalKeyHandler } from '../../hooks/useGlobalKeyHandler';
@@ -72,7 +73,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
onViewportChange,
isControlledViewport,
}: FlowRendererProps<NodeType>) {
const { nodesSelectionActive, userSelectionActive } = useStore(selector);
const { nodesSelectionActive, userSelectionActive } = useStore(selector, shallow);
const selectionKeyPressed = useKeyPress(selectionKeyCode, { target: win });
const panActivationKeyPressed = useKeyPress(panActivationKeyCode, { target: win });
@@ -105,6 +106,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
onViewportChange={onViewportChange}
isControlledViewport={isControlledViewport}
paneClickDistance={paneClickDistance}
selectionOnDrag={_selectionOnDrag}
>
<Pane
onSelectionStart={onSelectionStart}
@@ -119,6 +121,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
isSelecting={!!isSelecting}
selectionMode={selectionMode}
selectionKeyPressed={selectionKeyPressed}
paneClickDistance={paneClickDistance}
selectionOnDrag={_selectionOnDrag}
>
{children}

View File

@@ -6,7 +6,7 @@ import { useStore } from '../../hooks/useStore';
import { containerStyle } from '../../styles/utils';
import { GraphViewProps } from '../GraphView';
import { useResizeObserver } from './useResizeObserver';
import { NodeWrapper } from '../../components/NodeWrapper';
import NodeWrapper from '../../components/NodeWrapper';
import type { Node, ReactFlowState } from '../../types';
export type NodeRendererProps<NodeType extends Node> = Pick<

View File

@@ -1,23 +1,15 @@
/**
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/
import {
useRef,
type MouseEventHandler,
type MutableRefObject,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
type WheelEvent as ReactWheelEvent,
type ReactNode,
} from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import {
getNodesInside,
getEventPosition,
SelectionMode,
areSetsEqual,
type NodeChange,
type EdgeChange,
} from '@xyflow/system';
import { getNodesInside, getEventPosition, SelectionMode, areSetsEqual } from '@xyflow/system';
import { UserSelection } from '../../components/UserSelection';
import { containerStyle } from '../../styles/utils';
@@ -29,6 +21,7 @@ type PaneProps = {
isSelecting: boolean;
selectionKeyPressed: boolean;
children: ReactNode;
paneClickDistance: number;
} & Partial<
Pick<
ReactFlowProps,
@@ -47,9 +40,9 @@ type PaneProps = {
>;
const wrapHandler = (
handler: React.MouseEventHandler | undefined,
containerRef: React.MutableRefObject<HTMLDivElement | null>
): React.MouseEventHandler => {
handler: MouseEventHandler | undefined,
containerRef: MutableRefObject<HTMLDivElement | null>
): MouseEventHandler => {
return (event: ReactMouseEvent) => {
if (event.target !== containerRef.current) {
return;
@@ -70,6 +63,7 @@ export function Pane({
selectionKeyPressed,
selectionMode = SelectionMode.Full,
panOnDrag,
paneClickDistance,
selectionOnDrag,
onSelectionStart,
onSelectionEnd,
@@ -83,17 +77,15 @@ export function Pane({
}: PaneProps) {
const store = useStoreApi();
const { userSelectionActive, elementsSelectable, dragging, connectionInProgress } = useStore(selector, shallow);
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
const isSelectionEnabled = elementsSelectable && (isSelecting || userSelectionActive);
const container = useRef<HTMLDivElement | null>(null);
const containerBounds = useRef<DOMRect>();
const selectedNodeIds = useRef<Set<string>>(new Set());
const selectedEdgeIds = useRef<Set<string>>(new Set());
// Used to prevent click events when the user lets go of the selectionKey during a selection
const selectionInProgress = useRef<boolean>(false);
const selectionStarted = useRef<boolean>(false);
const onClick = (event: ReactMouseEvent) => {
// We prevent click events when the user let go of the selectionKey during a selection
@@ -117,31 +109,37 @@ export function Pane({
onPaneContextMenu?.(event);
};
const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
const onWheel = onPaneScroll ? (event: ReactWheelEvent) => onPaneScroll(event) : undefined;
const onPointerDown = (event: ReactPointerEvent): void => {
const { resetSelectedElements, domNode } = store.getState();
const onClickCapture = (event: ReactMouseEvent) => {
if (selectionInProgress.current) {
event.stopPropagation();
selectionInProgress.current = false;
}
};
// We are using capture here in order to prevent other pointer events
// to be able to create a selection above a node or an edge
const onPointerDownCapture = (event: ReactPointerEvent): void => {
const { domNode } = store.getState();
containerBounds.current = domNode?.getBoundingClientRect();
if (!containerBounds.current) return;
if (
!elementsSelectable ||
!isSelecting ||
event.button !== 0 ||
event.target !== container.current ||
!containerBounds.current
) {
const eventTargetIsContainer = event.target === container.current;
// if a child element has the 'nokey' class, we don't want to swallow the event and don't start a selection
const isNoKeyEvent = !eventTargetIsContainer && !!(event.target as HTMLElement).closest('.nokey');
const isSelectionActive = (selectionOnDrag && eventTargetIsContainer) || selectionKeyPressed;
if (isNoKeyEvent || !isSelecting || !isSelectionActive || event.button !== 0 || !event.isPrimary) {
return;
}
(event.target as Partial<Element> | null)?.setPointerCapture?.(event.pointerId);
(event.target as Partial<Element>)?.setPointerCapture?.(event.pointerId);
selectionStarted.current = true;
selectionInProgress.current = false;
const { x, y } = getEventPosition(event.nativeEvent, containerBounds.current);
resetSelectedElements();
store.setState({
userSelectionRect: {
width: 0,
@@ -153,7 +151,10 @@ export function Pane({
},
});
onSelectionStart?.(event);
if (!eventTargetIsContainer) {
event.stopPropagation();
event.preventDefault();
}
};
const onPointerMove = (event: ReactPointerEvent): void => {
@@ -166,17 +167,28 @@ export function Pane({
triggerNodeChanges,
triggerEdgeChanges,
defaultEdgeOptions,
resetSelectedElements,
} = store.getState();
if (!containerBounds.current || !userSelectionRect) {
return;
}
selectionInProgress.current = true;
const { x: mouseX, y: mouseY } = getEventPosition(event.nativeEvent, containerBounds.current);
const { startX, startY } = userSelectionRect;
if (!selectionInProgress.current) {
const requiredDistance = selectionKeyPressed ? 0 : paneClickDistance;
const distance = Math.hypot(mouseX - startX, mouseY - startY);
if (distance <= requiredDistance) {
return;
}
resetSelectedElements();
onSelectionStart?.(event);
}
selectionInProgress.current = true;
const nextUserSelectRect = {
startX,
startY,
@@ -211,12 +223,12 @@ export function Pane({
}
if (!areSetsEqual(prevSelectedNodeIds, selectedNodeIds.current)) {
const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true) as NodeChange[];
const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true);
triggerNodeChanges(changes);
}
if (!areSetsEqual(prevSelectedEdgeIds, selectedEdgeIds.current)) {
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current) as EdgeChange[];
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current);
triggerEdgeChanges(changes);
}
@@ -228,37 +240,32 @@ export function Pane({
};
const onPointerUp = (event: ReactPointerEvent) => {
if (event.button !== 0 || !selectionStarted.current) {
if (event.button !== 0) {
return;
}
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
const { userSelectionRect } = store.getState();
/*
* We only want to trigger click functions when in selection mode if
* the user did not move the mouse.
*/
if (!userSelectionActive && userSelectionRect && event.target === container.current) {
if (!userSelectionActive && event.target === container.current && store.getState().userSelectionRect) {
onClick?.(event);
}
store.setState({
userSelectionActive: false,
userSelectionRect: null,
nodesSelectionActive: selectedNodeIds.current.size > 0,
});
onSelectionEnd?.(event);
/*
* If the user kept holding the selectionKey during the selection,
* we need to reset the selectionInProgress, so the next click event is not prevented
*/
if (selectionKeyPressed || selectionOnDrag) {
selectionInProgress.current = false;
if (selectionInProgress.current) {
onSelectionEnd?.(event);
store.setState({
nodesSelectionActive: selectedNodeIds.current.size > 0,
});
}
selectionStarted.current = false;
};
const draggable = panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0));
@@ -266,13 +273,14 @@ export function Pane({
return (
<div
className={cc(['react-flow__pane', { draggable, dragging, selection: isSelecting }])}
onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
onClick={isSelectionEnabled ? undefined : wrapHandler(onClick, container)}
onContextMenu={wrapHandler(onContextMenu, container)}
onWheel={wrapHandler(onWheel, container)}
onPointerEnter={hasActiveSelection ? undefined : onPaneMouseEnter}
onPointerDown={hasActiveSelection ? onPointerDown : onPaneMouseMove}
onPointerMove={hasActiveSelection ? onPointerMove : onPaneMouseMove}
onPointerUp={hasActiveSelection ? onPointerUp : undefined}
onPointerEnter={isSelectionEnabled ? undefined : onPaneMouseEnter}
onPointerMove={isSelectionEnabled ? onPointerMove : onPaneMouseMove}
onPointerUp={isSelectionEnabled ? onPointerUp : undefined}
onPointerDownCapture={isSelectionEnabled ? onPointerDownCapture : undefined}
onClickCapture={isSelectionEnabled ? onClickCapture : undefined}
onPointerLeave={onPaneMouseLeave}
ref={container}
style={containerStyle}

View File

@@ -104,7 +104,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onPaneMouseLeave,
onPaneScroll,
onPaneContextMenu,
paneClickDistance = 0,
paneClickDistance = 1,
nodeClickDistance = 0,
children,
onReconnect,
@@ -309,7 +309,6 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
nodeDragThreshold={nodeDragThreshold}
connectionDragThreshold={connectionDragThreshold}
onBeforeDelete={onBeforeDelete}
paneClickDistance={paneClickDistance}
debug={debug}
ariaLabelConfig={ariaLabelConfig}
/>

View File

@@ -12,12 +12,7 @@ import type { ReactFlowState } from '../../types';
type ZoomPaneProps = Omit<
FlowRendererProps,
| 'deleteKeyCode'
| 'selectionKeyCode'
| 'multiSelectionKeyCode'
| 'noDragClassName'
| 'disableKeyboardA11y'
| 'selectionOnDrag'
'deleteKeyCode' | 'selectionKeyCode' | 'multiSelectionKeyCode' | 'noDragClassName' | 'disableKeyboardA11y'
> & {
isControlledViewport: boolean;
};
@@ -25,6 +20,7 @@ type ZoomPaneProps = Omit<
const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive,
lib: s.lib,
connectionInProgress: s.connection.inProgress,
});
export function ZoomPane({
@@ -48,10 +44,11 @@ export function ZoomPane({
onViewportChange,
isControlledViewport,
paneClickDistance,
selectionOnDrag,
}: ZoomPaneProps) {
const store = useStoreApi();
const zoomPane = useRef<HTMLDivElement>(null);
const { userSelectionActive, lib } = useStore(selector, shallow);
const { userSelectionActive, lib, connectionInProgress } = useStore(selector, shallow);
const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode);
const panZoom = useRef<PanZoomInstance>();
@@ -76,7 +73,6 @@ export function ZoomPane({
maxZoom,
translateExtent,
viewport: defaultViewport,
paneClickDistance,
onDraggingChange: (paneDragging: boolean) => store.setState({ paneDragging }),
onPanZoomStart: (event, vp) => {
const { onViewportChangeStart, onMoveStart } = store.getState();
@@ -126,6 +122,9 @@ export function ZoomPane({
noWheelClassName,
lib,
onTransformChange,
connectionInProgress,
selectionOnDrag,
paneClickDistance,
});
}, [
onPaneContextMenu,
@@ -143,6 +142,9 @@ export function ZoomPane({
noWheelClassName,
lib,
onTransformChange,
connectionInProgress,
selectionOnDrag,
paneClickDistance,
]);
return (

View File

@@ -202,6 +202,11 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
return { deletedNodes: matchingNodes, deletedEdges: matchingEdges };
},
/**
* Partial is defined as "the 2 nodes/areas are intersecting partially".
* If a is contained in b or b is contained in a, they are both
* considered fully intersecting.
*/
getIntersectingNodes: (nodeOrRect, partially = true, nodes) => {
const isRect = isRectObject(nodeOrRect);
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
@@ -240,7 +245,11 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
const overlappingArea = getOverlappingArea(nodeRect, area);
const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
return (
partiallyVisible ||
overlappingArea >= area.width * area.height ||
overlappingArea >= nodeRect.width * nodeRect.height
);
},
updateNode,
updateNodeData: (id, dataUpdate, options = { replace: false }) => {

View File

@@ -13,7 +13,7 @@ export function useResizeHandler(domNode: MutableRefObject<HTMLDivElement | null
useEffect(() => {
const updateDimensions = () => {
if (!domNode.current) {
if (!domNode.current || !(domNode.current.checkVisibility?.() ?? true)) {
return false;
}
const size = getDimensions(domNode.current);

View File

@@ -14,6 +14,8 @@ import {
NodeOrigin,
CoordinateExtent,
fitViewport,
getHandlePosition,
Position,
} from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -176,7 +178,7 @@ const createStore = ({
updateNodePositions: (nodeDragItems, dragging = false) => {
const parentExpandChildren: ParentExpandChild[] = [];
let changes = [];
const { nodeLookup, triggerNodeChanges, onNodesChangeMiddlewareMap } = get();
const { nodeLookup, triggerNodeChanges, connection, updateConnection, onNodesChangeMiddlewareMap } = get();
for (const [id, dragItem] of nodeDragItems) {
// we are using the nodelookup to be sure to use the current expandParent and parentId value
@@ -195,6 +197,11 @@ const createStore = ({
dragging,
};
if (node && connection.inProgress && connection.fromNode.id === node.id) {
const updatedFrom = getHandlePosition(node, connection.fromHandle, Position.Left, true);
updateConnection({ ...connection, from: updatedFrom });
}
if (expandParent && node.parentId) {
parentExpandChildren.push({
id,
@@ -316,9 +323,6 @@ const createStore = ({
set({ translateExtent });
},
setPaneClickDistance: (clickDistance) => {
get().panZoom?.setClickDistance(clickDistance);
},
resetSelectedElements: () => {
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges, elementsSelectable } = get();

View File

@@ -79,8 +79,8 @@ const getInitialState = ({
return {
rfId: '1',
width: 0,
height: 0,
width: width ?? 0,
height: height ?? 0,
transform,
nodes: storeNodes,
nodesInitialized,

View File

@@ -612,6 +612,7 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
elevateNodesOnSelect?: boolean;
/**
* Enabling this option will raise the z-index of edges when they are selected.
* @default false
*/
elevateEdgesOnSelect?: boolean;
/**

View File

@@ -22,6 +22,7 @@ import type {
OnError,
OnReconnect,
FinalConnectionState,
XYPosition,
} from '@xyflow/system';
import { EdgeTypes, InternalNode, Node } from '.';
@@ -149,7 +150,7 @@ export type EdgeTextProps = Omit<SVGAttributes<SVGElement>, 'x' | 'y'> &
*/
export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
EdgeType,
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' | 'selectable' | 'deletable'
'id' | 'type' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' | 'selectable' | 'deletable'
> &
EdgePosition &
EdgeLabelOptions & {
@@ -279,6 +280,7 @@ export type ConnectionLineComponentProps<NodeType extends Node = Node> = {
connectionStatus: 'valid' | 'invalid' | null;
toNode: InternalNode<NodeType> | null;
toHandle: Handle | null;
pointer: XYPosition;
};
export type ConnectionLineComponent<NodeType extends Node = Node> = ComponentType<

View File

@@ -228,16 +228,16 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
nodeId: string;
handleId?: string | null;
}) => NodeConnection[];
// /**
// * Fits the view.
// *
// * @param options.padding - optional padding
// * @param options.includeHiddenNodes - optional includeHiddenNodes
// * @param options.minZoom - optional minZoom
// * @param options.maxZoom - optional maxZoom
// * @param options.duration - optional duration. If set, a transition will be applied
// * @param options.nodes - optional nodes to fit the view to
// */
/**
* Fits the view based on the passed params. By default it fits the view to all nodes.
*
* @param options.padding - optional padding
* @param options.nodes - optional nodes to fit the view to
* @param options.minZoom - optional minZoom
* @param options.maxZoom - optional maxZoom
* @param options.duration - optional duration. If set, a transition will be applied
* @param options.includeHiddenNodes - optional includeHiddenNodes
*/
fitView: FitView<NodeType>;
};
/**

View File

@@ -178,7 +178,6 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
panBy: PanBy;
setCenter: SetCenter;
setPaneClickDistance: (distance: number) => void;
};
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<

View File

@@ -125,9 +125,9 @@ function applyChange(change: any, element: any): any {
case 'dimensions': {
if (typeof change.dimensions !== 'undefined') {
element.measured ??= {};
element.measured.width = change.dimensions.width;
element.measured.height = change.dimensions.height;
element.measured = {
...change.dimensions,
};
if (change.setAttributes) {
if (change.setAttributes === true || change.setAttributes === 'width') {

View File

@@ -1,5 +1,98 @@
# @xyflow/svelte
## 1.4.2
### Patch Changes
- [#5603](https://github.com/xyflow/xyflow/pull/5603) [`17a175791`](https://github.com/xyflow/xyflow/commit/17a175791b2420b32d55a8fcbbb35649862b85cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix onPaneClick always firing when moving the viewport
- [#5615](https://github.com/xyflow/xyflow/pull/5615) [`8b35c77eb`](https://github.com/xyflow/xyflow/commit/8b35c77ebe5a4f6cba33c597d94eba1ead64fdfc) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix wrong positions when resizing nodes with a non-default node-origin
- [#5578](https://github.com/xyflow/xyflow/pull/5578) [`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773) Thanks [@peterkogo](https://github.com/peterkogo)! - Pass current pointer position to connection
- Updated dependencies [[`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773)]:
- @xyflow/system@0.0.73
## 1.4.1
### Patch Changes
- [#5572](https://github.com/xyflow/xyflow/pull/5572) [`2a401ed67`](https://github.com/xyflow/xyflow/commit/2a401ed675b85417da46fbca68832c58d0f9bf02) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix showing selection box when no nodes are selected
- [#5558](https://github.com/xyflow/xyflow/pull/5558) [`dca0a30f2`](https://github.com/xyflow/xyflow/commit/dca0a30f273a05d592d8b89b3f0f30d829f5bbd0) Thanks [@peterkogo](https://github.com/peterkogo)! - Remove `edgeId` from `EdgeToolbar` props
- [#5565](https://github.com/xyflow/xyflow/pull/5565) [`cbab3d8be`](https://github.com/xyflow/xyflow/commit/cbab3d8be72b4935ccaaa7a1f89011923b66c57b) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Pass nodeId to MiniMapNode
- [#5572](https://github.com/xyflow/xyflow/pull/5572) [`5ec0cac7f`](https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix onPaneClick events being suppressed when selectionOnDrag=true
- [#5572](https://github.com/xyflow/xyflow/pull/5572) [`9dccac89f`](https://github.com/xyflow/xyflow/commit/9dccac89fb85e51f34f96dc3c18f2f9c7ae5a86a) Thanks [@peterkogo](https://github.com/peterkogo)! - Remove focus outline from selection box
- Updated dependencies [[`5ec0cac7f`](https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9)]:
- @xyflow/system@0.0.72
## 1.4.0
### Minor Changes
- [#5544](https://github.com/xyflow/xyflow/pull/5544) [`c17b49f4c`](https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Add `EdgeToolbar` component
- [#5550](https://github.com/xyflow/xyflow/pull/5550) [`6ffb9f790`](https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent child nodes of different parents from overlapping
- [#5551](https://github.com/xyflow/xyflow/pull/5551) [`6bb64b3ed`](https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634) Thanks [@moklick](https://github.com/moklick)! - Allow to start a selection above a node
- [#5526](https://github.com/xyflow/xyflow/pull/5526) [`1b0bd9794`](https://github.com/xyflow/xyflow/commit/1b0bd9794f8d610468983a579e53b4633047b465) Thanks [@peterkogo](https://github.com/peterkogo)! - Add `resizeDirection` prop to `ResizeControls`
### Patch Changes
- [#5538](https://github.com/xyflow/xyflow/pull/5538) [`87a868958`](https://github.com/xyflow/xyflow/commit/87a8689584d9b29570d186628829324f3bec3b2e) Thanks [@peterkogo](https://github.com/peterkogo)! - Suppress `svelte/prefer-svelte-reactivity` warnings
- [#5529](https://github.com/xyflow/xyflow/pull/5529) [`da77772de`](https://github.com/xyflow/xyflow/commit/da77772debe6a0e730feacadf53210210178e7e2) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix selected set to false on every node and edge eagerly
- [#5528](https://github.com/xyflow/xyflow/pull/5528) [`d50a963cd`](https://github.com/xyflow/xyflow/commit/d50a963cd0f00cd31236d760dcd7995218d80553) Thanks [@peterkogo](https://github.com/peterkogo)! - Let `NodeResizer` props change during ongoing resize
- [#5546](https://github.com/xyflow/xyflow/pull/5546) [`8a9ee2d83`](https://github.com/xyflow/xyflow/commit/8a9ee2d836776da42b867c6ba90d302fbbc79b37) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Do not crash mini map if all nodes are hidden
- [#5547](https://github.com/xyflow/xyflow/pull/5547) [`1c8961207`](https://github.com/xyflow/xyflow/commit/1c8961207e8ac326098af18489465b36cdd2d831) Thanks [@moklick](https://github.com/moklick)! - Always call `onMoveEnd` when `onMoveStart` was called
- Updated dependencies [[`c17b49f4c`](https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d), [`6ffb9f790`](https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32), [`d50a963cd`](https://github.com/xyflow/xyflow/commit/d50a963cd0f00cd31236d760dcd7995218d80553), [`6bb64b3ed`](https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634), [`8a9ee2d83`](https://github.com/xyflow/xyflow/commit/8a9ee2d836776da42b867c6ba90d302fbbc79b37), [`1c8961207`](https://github.com/xyflow/xyflow/commit/1c8961207e8ac326098af18489465b36cdd2d831)]:
- @xyflow/system@0.0.71
## 1.3.1
### Patch Changes
- [#5515](https://github.com/xyflow/xyflow/pull/5515) [`9b4e99029`](https://github.com/xyflow/xyflow/commit/9b4e99029ba87d44c71b33c06259a126db193b43) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix id of static handles not being parsed
- [#5512](https://github.com/xyflow/xyflow/pull/5512) [`70b672604`](https://github.com/xyflow/xyflow/commit/70b67260470738dd0d88ed8ab558d400097be785) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent native page zoom when pinch zooming on node with nowheel class
- Updated dependencies [[`9b4e99029`](https://github.com/xyflow/xyflow/commit/9b4e99029ba87d44c71b33c06259a126db193b43), [`70b672604`](https://github.com/xyflow/xyflow/commit/70b67260470738dd0d88ed8ab558d400097be785)]:
- @xyflow/system@0.0.70
## 1.3.0
### Minor Changes
- [#5496](https://github.com/xyflow/xyflow/pull/5496) [`d23c3dfc5`](https://github.com/xyflow/xyflow/commit/d23c3dfc551216ea52183a24e9a9f24ca35cc447) Thanks [@printerscanner](https://github.com/printerscanner)! - Pass a custom `nodeComponent` to Svelteflow's Minimap
### Patch Changes
- [#5480](https://github.com/xyflow/xyflow/pull/5480) [`f869808e3`](https://github.com/xyflow/xyflow/commit/f869808e3dfd3d289db034d21d505d62f84cdde3) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent multi-touch events while making a new connection
- [#5510](https://github.com/xyflow/xyflow/pull/5510) [`cdd671699`](https://github.com/xyflow/xyflow/commit/cdd671699bfe21e0aa69bc5e8585a4d0ce189974) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing props autoPanSpeed and panOnScrollSpeed
- [#5482](https://github.com/xyflow/xyflow/pull/5482) [`56381ae54`](https://github.com/xyflow/xyflow/commit/56381ae546f392b13e7c1390df60e48c9072f128) Thanks [@peterkogo](https://github.com/peterkogo)! - Make isNodeIntersecting behave the same as getIntersectingNodes
- [#5509](https://github.com/xyflow/xyflow/pull/5509) [`b6ae4cc11`](https://github.com/xyflow/xyflow/commit/b6ae4cc11f1456f843ace7e7f3de41c9196179e0) Thanks [@moklick](https://github.com/moklick)! - Prevent calling onResizeEnd if node was not resized
- [#5511](https://github.com/xyflow/xyflow/pull/5511) [`d891e5ff1`](https://github.com/xyflow/xyflow/commit/d891e5ff106c6202f69a9b14d261294cc29bbec7) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix regression: elevate edges if connected nodes are selected
- [#5497](https://github.com/xyflow/xyflow/pull/5497) [`eba087e8b`](https://github.com/xyflow/xyflow/commit/eba087e8bec74e6591e04ebf0e1e61799b94ecd8) Thanks [@peterkogo](https://github.com/peterkogo)! - Skip eagerly rendering nodes when node dimensions and handles are predefined
- [#5455](https://github.com/xyflow/xyflow/pull/5455) [`86e8b4913`](https://github.com/xyflow/xyflow/commit/86e8b49133370b97deeed5269d040b01dc2d3097) Thanks [@Sec-ant](https://github.com/Sec-ant)! - Fix style attribute not being propagated to wrapper
- Updated dependencies [[`f869808e3`](https://github.com/xyflow/xyflow/commit/f869808e3dfd3d289db034d21d505d62f84cdde3), [`2a35c65a0`](https://github.com/xyflow/xyflow/commit/2a35c65a02c6c8aec6b005ac3e72988f996669c4), [`b6ae4cc11`](https://github.com/xyflow/xyflow/commit/b6ae4cc11f1456f843ace7e7f3de41c9196179e0), [`d891e5ff1`](https://github.com/xyflow/xyflow/commit/d891e5ff106c6202f69a9b14d261294cc29bbec7), [`eba087e8b`](https://github.com/xyflow/xyflow/commit/eba087e8bec74e6591e04ebf0e1e61799b94ecd8)]:
- @xyflow/system@0.0.69
## 1.2.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/svelte",
"version": "1.2.4",
"version": "1.4.2",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [
"svelte",

View File

@@ -35,8 +35,10 @@ type ZoomParams = {
userSelectionActive: boolean;
lib: string;
paneClickDistance: number;
selectionOnDrag?: boolean;
onTransformChange: (transform: Transform) => void;
onDraggingChange: (dragging: boolean) => void;
connectionInProgress: boolean;
};
export default function zoom(domNode: Element, params: ZoomParams) {
@@ -48,7 +50,6 @@ export default function zoom(domNode: Element, params: ZoomParams) {
onPanZoom,
onPanZoomEnd,
translateExtent,
paneClickDistance,
setPanZoomInstance,
onDraggingChange,
onTransformChange
@@ -60,7 +61,6 @@ export default function zoom(domNode: Element, params: ZoomParams) {
maxZoom,
translateExtent,
viewport: initialViewport,
paneClickDistance,
onPanZoom,
onPanZoomStart,
onPanZoomEnd,

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { getContext } from 'svelte';
import { hideOnSSR, portal } from '$lib/actions/portal';
import { hideOnSSR, portal } from '$lib/actions/portal';
import { useStore } from '$lib/store';
import type { EdgeLabelProps } from './types';
import { toPxString } from '$lib/utils';
@@ -19,12 +19,13 @@
}: EdgeLabelProps = $props();
const store = useStore();
const edgeId = getContext<string>('svelteflow__edge_id');
const id = getContext<string>('svelteflow__edge_id');
if (!edgeId) {
throw new Error('EdgeLabel must be used within an edge');
}
let z = $derived.by(() => {
return store.visible.edges.get(id)?.zIndex;
});
let z = $derived(store.visible.edges.get(edgeId)?.zIndex);
</script>
<div
@@ -39,7 +40,7 @@
style:z-index={z}
tabindex="-1"
onclick={() => {
if (selectEdgeOnClick && id) store.handleEdgeSelection(id);
if (selectEdgeOnClick) store.handleEdgeSelection(edgeId);
}}
{...rest}
>

View File

@@ -122,6 +122,7 @@
connectionMode: store.connectionMode,
lib: 'svelte',
autoPanOnConnect: store.autoPanOnConnect,
autoPanSpeed: store.autoPanSpeed,
flowId: store.flowId,
isValidConnection: isValidConnection ?? store.isValidConnection,
updateConnection: store.updateConnection,

View File

@@ -31,7 +31,12 @@
if (store.selectionRectMode === 'nodes') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
store.nodes;
return getInternalNodesBounds(store.nodeLookup, { filter: (node) => !!node.selected });
const nodeBounds = getInternalNodesBounds(store.nodeLookup, {
filter: (node) => !!node.selected
});
if (nodeBounds.width > 0 && nodeBounds.height > 0) {
return nodeBounds;
}
}
return null;
});
@@ -92,4 +97,9 @@
z-index: 2000;
pointer-events: all;
}
.svelte-flow__selection-wrapper:focus,
.svelte-flow__selection-wrapper:focus-visible {
outline: none;
}
</style>

View File

@@ -26,6 +26,7 @@
typeof ResizeObserver === 'undefined'
? null
: new ResizeObserver((entries: ResizeObserverEntry[]) => {
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const updates = new Map();
entries.forEach((entry: ResizeObserverEntry) => {

View File

@@ -47,6 +47,7 @@
let {
store = $bindable(),
panOnDrag = true,
paneClickDistance = 1,
selectionOnDrag,
onpaneclick,
onpanecontextmenu,
@@ -59,55 +60,53 @@
let container: HTMLDivElement;
let containerBounds: DOMRect | null = null;
/* eslint-disable svelte/prefer-svelte-reactivity */
let selectedNodeIds: Set<string> = new Set();
let selectedEdgeIds: Set<string> = new Set();
/* eslint-enable svelte/prefer-svelte-reactivity */
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
let isSelecting = $derived(
store.selectionKeyPressed ||
store.selectionRect ||
!!store.selectionRect ||
(selectionOnDrag && panOnDragActive !== true)
);
let hasActiveSelection = $derived(
let isSelectionEnabled = $derived(
store.elementsSelectable && (isSelecting || store.selectionRectMode === 'user')
);
// Used to prevent click events when the user lets go of the selectionKey during a selection
let selectionInProgress = false;
function onClick(event: MouseEvent) {
// We prevent click events when the user let go of the selectionKey during a selection
// We also prevent click events when a connection is in progress
if (selectionInProgress || store.connection.inProgress) {
selectionInProgress = false;
return;
}
onpaneclick?.({ event });
store.unselectNodesAndEdges();
store.selectionRectMode = null;
}
// We start the selection process when the user clicks down on the pane
function onPointerDown(event: PointerEvent) {
function onPointerDownCapture(event: PointerEvent) {
containerBounds = container?.getBoundingClientRect();
if (!containerBounds) return;
const eventTargetIsContainer = event.target === container;
const isNoKeyEvent =
!eventTargetIsContainer && !!(event.target as HTMLElement).closest('.nokey');
const isSelectionActive =
(selectionOnDrag && eventTargetIsContainer) || store.selectionKeyPressed;
if (
!store.elementsSelectable ||
isNoKeyEvent ||
!isSelecting ||
!isSelectionActive ||
event.button !== 0 ||
event.target !== container ||
!containerBounds
!event.isPrimary
) {
return;
}
(event.target as Partial<Element> | null)?.setPointerCapture?.(event.pointerId);
(event.target as Partial<Element>)?.setPointerCapture?.(event.pointerId);
selectionInProgress = false;
const { x, y } = getEventPosition(event, containerBounds);
store.unselectNodesAndEdges();
store.selectionRect = {
width: 0,
height: 0,
@@ -117,7 +116,10 @@
y
};
onselectionstart?.(event);
if (!eventTargetIsContainer) {
event.stopPropagation();
event.preventDefault();
}
}
function onPointerMove(event: PointerEvent) {
@@ -125,11 +127,21 @@
return;
}
selectionInProgress = true;
const mousePos = getEventPosition(event, containerBounds);
const { startX = 0, startY = 0 } = store.selectionRect;
if (!selectionInProgress) {
const requiredDistance = store.selectionKeyPressed ? 0 : paneClickDistance;
const distance = Math.hypot(mousePos.x - startX, mousePos.y - startY);
if (distance <= requiredDistance) {
return;
}
store.unselectNodesAndEdges();
onselectionstart?.(event);
}
selectionInProgress = true;
const nextUserSelectRect = {
...store.selectionRect,
x: mousePos.x < startX ? mousePos.x : startX,
@@ -184,26 +196,24 @@
return;
}
(event.target as Partial<Element> | null)?.releasePointerCapture?.(event.pointerId);
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
// We only want to trigger click functions when in selection mode if
// the user did not move the mouse.
if (!isSelecting && store.selectionRectMode === 'user' && event.target === container) {
if (!selectionInProgress && event.target === container) {
onClick?.(event);
}
store.selectionRect = null;
if (selectedNodeIds.size > 0) {
store.selectionRectMode = 'nodes';
if (selectionInProgress) {
store.selectionRectMode = selectedNodeIds.size > 0 ? 'nodes' : null;
}
// If the user kept holding the selectionKey during the selection,
// we need to reset the selectionInProgress, so the next click event is not prevented
if (store.selectionKeyPressed) {
selectionInProgress = false;
if (selectionInProgress) {
onselectionend?.(event);
}
onselectionend?.(event);
}
const onContextMenu = (event: MouseEvent) => {
@@ -214,6 +224,27 @@
onpanecontextmenu?.({ event });
};
const onClickCapture = (event: MouseEvent) => {
if (selectionInProgress) {
event.stopPropagation();
selectionInProgress = false;
}
};
function onClick(event: MouseEvent) {
// We prevent click events when the user let go of the selectionKey during a selection
// We also prevent click events when a connection is in progress
if (selectionInProgress || store.connection.inProgress) {
selectionInProgress = false;
return;
}
onpaneclick?.({ event });
store.unselectNodesAndEdges();
store.selectionRectMode = null;
store.selectionRect = null;
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -224,11 +255,12 @@
class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))}
class:dragging={store.dragging}
class:selection={isSelecting}
onclick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
onpointerdown={hasActiveSelection ? onPointerDown : undefined}
onpointermove={hasActiveSelection ? onPointerMove : undefined}
onpointerup={hasActiveSelection ? onPointerUp : undefined}
onclick={isSelectionEnabled ? undefined : wrapHandler(onClick, container)}
onpointerdowncapture={isSelectionEnabled ? onPointerDownCapture : undefined}
onpointermove={isSelectionEnabled ? onPointerMove : undefined}
onpointerup={isSelectionEnabled ? onPointerUp : undefined}
oncontextmenu={wrapHandler(onContextMenu, container)}
onclickcapture={isSelectionEnabled ? onClickCapture : undefined}
>
{@render children()}
</div>

View File

@@ -5,6 +5,7 @@ import type { SvelteFlowStore } from '$lib/store/types';
export type PaneProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
store: SvelteFlowStore<NodeType, EdgeType>;
panOnDrag?: boolean | number[];
paneClickDistance: number;
selectionOnDrag?: boolean;
onselectionstart?: (event: PointerEvent) => void;
onselectionend?: (event: PointerEvent) => void;

View File

@@ -59,8 +59,9 @@
zoomOnDoubleClick = true,
zoomOnPinch = true,
panOnScroll = false,
panOnScrollSpeed = 0.5,
panOnDrag = true,
selectionOnDrag = true,
selectionOnDrag = false,
connectionLineComponent,
connectionLineStyle,
connectionLineContainerStyle,
@@ -152,8 +153,10 @@
{zoomOnDoubleClick}
{zoomOnPinch}
{panOnScroll}
{panOnScrollSpeed}
{panOnDrag}
{paneClickDistance}
{selectionOnDrag}
{onmovestart}
{onmove}
{onmoveend}
@@ -166,6 +169,7 @@
{onselectionstart}
{onselectionend}
{panOnDrag}
{paneClickDistance}
{selectionOnDrag}
>
<ViewportComponent bind:store>

View File

@@ -80,7 +80,6 @@
autoPanOnConnect,
autoPanOnNodeDrag,
colorModeSSR,
style,
defaultEdgeOptions,
elevateNodesOnSelect,
elevateEdgesOnSelect,
@@ -95,6 +94,8 @@
noPanClass,
noWheelClass,
ariaLabelConfig,
autoPanSpeed,
panOnScrollSpeed,
...divAttributes
} = $derived(rest);
/* eslint-enable @typescript-eslint/no-unused-vars */

View File

@@ -239,6 +239,11 @@ export type SvelteFlowProps<
* @default true
*/
nodesDraggable?: boolean;
/**
* The speed at which the viewport pans while dragging a node or a selection box.
* @default 15
*/
autoPanSpeed?: number;
/**
* When `true`, the viewport will pan when a node is focused.
* @default true
@@ -305,6 +310,12 @@ export type SvelteFlowProps<
* @default false
*/
panOnScroll?: boolean;
/**
* Controls how fast viewport should be panned on scroll.
* Use together with `panOnScroll` prop.
* @default 0.5
*/
panOnScrollSpeed?: number;
/**
* This prop is used to limit the direction of panning when panOnScroll is enabled.
* The "free" option allows panning in any direction.

View File

@@ -1,5 +1,5 @@
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import { PanOnScrollMode, type PanZoomInstance, type Transform } from '@xyflow/system';
import { type PanZoomInstance, type Transform } from '@xyflow/system';
import zoom from '$lib/actions/zoom';
import type { ZoomProps } from './types';
@@ -7,14 +7,16 @@
let {
store = $bindable(),
panOnScrollMode = PanOnScrollMode.Free,
preventScrolling = true,
zoomOnScroll = true,
zoomOnDoubleClick = true,
zoomOnPinch = true,
panOnDrag = true,
panOnScroll = false,
paneClickDistance = 1,
panOnScrollMode,
preventScrolling,
zoomOnScroll,
zoomOnDoubleClick,
zoomOnPinch,
panOnDrag,
panOnScroll,
panOnScrollSpeed,
paneClickDistance,
selectionOnDrag,
onmovestart,
onmove,
onmoveend,
@@ -58,8 +60,8 @@
zoomOnPinch,
panOnScroll: panOnScrollActive,
panOnDrag: panOnDragActive,
panOnScrollSpeed: 0.5,
panOnScrollMode: panOnScrollMode || PanOnScrollMode.Free,
panOnScrollSpeed,
panOnScrollMode,
zoomActivationKeyPressed: store.zoomActivationKeyPressed,
preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true,
noPanClassName: store.noPanClass,
@@ -68,9 +70,11 @@
translateExtent: store.translateExtent,
lib: 'svelte',
paneClickDistance,
selectionOnDrag,
onTransformChange: (transform: Transform) => {
store.viewport = { x: transform[0], y: transform[1], zoom: transform[2] };
}
},
connectionInProgress: store.connection.inProgress
}}
>
{@render children()}

View File

@@ -6,6 +6,7 @@ import type { Snippet } from 'svelte';
export type ZoomProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
store: SvelteFlowStore<NodeType, EdgeType>;
panOnScrollMode: PanOnScrollMode;
panOnScrollSpeed: number;
preventScrolling: boolean;
zoomOnScroll: boolean;
zoomOnDoubleClick: boolean;
@@ -13,6 +14,7 @@ export type ZoomProps<NodeType extends Node = Node, EdgeType extends Edge = Edge
panOnScroll: boolean;
panOnDrag: boolean | number[];
paneClickDistance: number;
selectionOnDrag?: boolean;
onmove?: OnMove;
onmovestart?: OnMoveStart;
onmoveend?: OnMoveEnd;

View File

@@ -1,3 +1,4 @@
/* eslint-disable svelte/prefer-svelte-reactivity */
import {
areConnectionMapsEqual,
handleConnectionChange,
@@ -90,3 +91,5 @@ export function useNodeConnections({
}
};
}
/* eslint-enable svelte/prefer-svelte-reactivity */

View File

@@ -390,6 +390,11 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
return Promise.resolve(true);
},
/**
* Partial is defined as "the 2 nodes/areas are intersecting partially".
* If a is contained in b or b is contained in a, they are both
* considered fully intersecting.
*/
getIntersectingNodes: (
nodeOrRect: NodeType | { id: NodeType['id'] } | Rect,
partially = true,
@@ -434,7 +439,11 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
const overlappingArea = getOverlappingArea(nodeRect, area);
const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
return (
partiallyVisible ||
overlappingArea >= area.width * area.height ||
overlappingArea >= nodeRect.width * nodeRect.height
);
},
deleteElements: async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => {
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove<

View File

@@ -1,3 +1,4 @@
/* eslint-disable svelte/prefer-svelte-reactivity */
import { useStore } from '$lib/store';
import { getContext } from 'svelte';
@@ -38,3 +39,5 @@ export function useUpdateNodeInternals(): (nodeId?: string | string[]) => void {
return updateInternals;
}
/* eslint-enable svelte/prefer-svelte-reactivity */

View File

@@ -22,6 +22,7 @@ export * from '$lib/plugins/Controls';
export * from '$lib/plugins/Background';
export * from '$lib/plugins/Minimap';
export * from '$lib/plugins/NodeToolbar';
export * from '$lib/plugins/EdgeToolbar';
export * from '$lib/plugins/NodeResizer';
// store

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import { getEdgeToolbarTransform } from '@xyflow/system';
import { useStore } from '$lib/store';
import { EdgeLabel } from '$lib/components/EdgeLabel';
import type { EdgeToolbarProps } from './types';
import { getContext } from 'svelte';
let {
x,
y,
alignX = 'center',
alignY = 'center',
isVisible,
selectEdgeOnClick,
class: className,
children,
...rest
}: EdgeToolbarProps = $props();
const store = useStore();
const edgeId = getContext<string>('svelteflow__edge_id');
if (!edgeId) {
throw new Error('EdgeToolbar must be used within an edge');
}
const isActive = $derived(
typeof isVisible === 'boolean' ? isVisible : store.edgeLookup.get(edgeId)?.selected
);
const transform = $derived(getEdgeToolbarTransform(x, y, store.viewport.zoom, alignX, alignY));
</script>
{#if isActive}
<EdgeLabel {selectEdgeOnClick} transparent>
<div
style:position="absolute"
style:transform
style:transform-origin="0 0"
class={['svelte-flow__edge-toolbar', className]}
data-id={edgeId}
{...rest}
>
{@render children?.()}
</div>
</EdgeLabel>
{/if}

View File

@@ -0,0 +1,2 @@
export { default as EdgeToolbar } from './EdgeToolbar.svelte';
export * from './types';

View File

@@ -0,0 +1,8 @@
import type { EdgeToolbarBaseProps } from '@xyflow/system';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
export type EdgeToolbarProps = Omit<EdgeToolbarBaseProps, 'edgeId'> & {
selectEdgeOnClick?: boolean;
children?: Snippet;
} & HTMLAttributes<HTMLDivElement>;

View File

@@ -27,6 +27,7 @@
nodeClass = '',
nodeBorderRadius = 5,
nodeStrokeWidth = 2,
nodeComponent,
bgColor,
maskColor,
maskStrokeColor,
@@ -59,14 +60,11 @@
width: store.width / store.viewport.zoom,
height: store.height / store.viewport.zoom
});
let boundingRect = $derived(
store.nodeLookup.size > 0
? getBoundsOfRects(
getInternalNodesBounds(store.nodeLookup, { filter: (n) => !n.hidden }),
viewBB
)
: viewBB
getBoundsOfRects(getInternalNodesBounds(store.nodeLookup, { filter: (n) => !n.hidden }), viewBB)
);
let scaledWidth = $derived(boundingRect.width / width);
let scaledHeight = $derived(boundingRect.height / height);
let viewScale = $derived(Math.max(scaledWidth, scaledHeight));
@@ -120,13 +118,15 @@
{#each store.nodes as userNode (userNode.id)}
{@const node = store.nodeLookup.get(userNode.id)}
{#if node && nodeHasDimensions(node)}
{#if node && nodeHasDimensions(node) && !node.hidden}
{@const nodeDimesions = getNodeDimensions(node)}
<MinimapNode
id={node.id}
x={node.internals.positionAbsolute.x}
y={node.internals.positionAbsolute.y}
{...nodeDimesions}
selected={node.selected}
{nodeComponent}
color={nodeColorFunc?.(node)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}

View File

@@ -1,7 +1,10 @@
<script lang="ts">
import type { ClassValue } from 'svelte/elements';
import type { Component } from 'svelte';
import type { MiniMapNodeProps } from './types';
let {
id,
x,
y,
width,
@@ -12,8 +15,10 @@
strokeColor,
strokeWidth = 2,
selected,
class: className
class: className,
nodeComponent
}: {
id: string;
x: number;
y: number;
width: number;
@@ -25,20 +30,40 @@
strokeWidth?: number;
selected?: boolean;
class?: ClassValue;
nodeComponent?: Component<MiniMapNodeProps>;
} = $props();
</script>
<rect
class={['svelte-flow__minimap-node', className]}
class:selected
{x}
{y}
rx={borderRadius}
ry={borderRadius}
{width}
{height}
style:fill={color}
style:stroke={strokeColor}
style:stroke-width={strokeWidth}
shape-rendering={shapeRendering}
/>
{#if nodeComponent}
{@const CustomComponent = nodeComponent}
<CustomComponent
{id}
{x}
{y}
{width}
{height}
{borderRadius}
class={className}
{color}
{shapeRendering}
{strokeColor}
{strokeWidth}
{selected}
/>
{:else}
<rect
class={['svelte-flow__minimap-node', className]}
class:selected
{x}
{y}
rx={borderRadius}
ry={borderRadius}
{width}
{height}
style:fill={color}
style:stroke={strokeColor}
style:stroke-width={strokeWidth}
shape-rendering={shapeRendering}
/>
{/if}

View File

@@ -1,9 +1,30 @@
import type { PanelPosition } from '@xyflow/system';
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
import type { Component } from 'svelte';
import type { Node } from '$lib/types';
export type GetMiniMapNodeAttribute = (node: Node) => string;
/**
* The props that are passed to the MiniMapNode component
*
* @public
*/
export type MiniMapNodeProps = {
id: string;
x: number;
y: number;
width: number;
height: number;
borderRadius?: number;
class?: ClassValue;
color?: string;
shapeRendering?: string;
strokeColor?: string;
strokeWidth?: number;
selected?: boolean;
};
export type MiniMapProps = {
/** Background color of minimap */
bgColor?: string;
@@ -17,6 +38,8 @@ export type MiniMapProps = {
nodeBorderRadius?: number;
/** Stroke width of nodes on the minimap */
nodeStrokeWidth?: number;
/** A custom component to render the nodes in the minimap. This component must render an SVG element! */
nodeComponent?: Component<MiniMapNodeProps>;
/** Color of the mask representing viewport */
maskColor?: string;
/** Stroke color of the mask representing viewport */

View File

@@ -22,6 +22,7 @@
maxWidth = Number.MAX_VALUE,
maxHeight = Number.MAX_VALUE,
keepAspectRatio = false,
resizeDirection,
autoScale = true,
shouldResize,
onResizeStart,
@@ -66,27 +67,28 @@
};
},
onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => {
const changes = new Map<string, Partial<Node>>();
let position = change.x && change.y ? { x: change.x, y: change.y } : undefined;
changes.set(id, { ...change, position });
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const changes = new Map<string, XYResizerChange>();
changes.set(id, change);
for (const childChange of childChanges) {
changes.set(childChange.id, {
position: childChange.position
});
changes.set(childChange.id, {x: childChange.position.x, y: childChange.position.y });
}
store.nodes = store.nodes.map((node) => {
const change = changes.get(node.id);
const horizontal = !resizeDirection || resizeDirection === 'horizontal';
const vertical = !resizeDirection || resizeDirection === 'vertical';
if (change) {
return {
return {
...node,
position: {
x: change.position?.x ?? node.position.x,
y: change.position?.y ?? node.position.y
x: horizontal ? (change.x ?? node.position.x) : node.position.x,
y: vertical ? (change.y ?? node.position.y) : node.position.y
},
width: change.width ?? node.width,
height: change.height ?? node.height
width: horizontal ? (change.width ?? node.width) : node.width,
height: vertical ? (change.height ?? node.height) : node.height
};
}
return node;
@@ -109,6 +111,7 @@
maxHeight
},
keepAspectRatio: !!keepAspectRatio,
resizeDirection,
onResizeStart,
onResize,
onResizeEnd,

View File

@@ -3,6 +3,7 @@ import type { HTMLAttributes } from 'svelte/elements';
import type {
ControlPosition,
ResizeControlVariant,
ResizeControlDirection,
ShouldResize,
OnResizeStart,
OnResize,
@@ -46,6 +47,8 @@ export type NodeResizerProps = {
onResize?: OnResize;
/** Callback called when resizing ends */
onResizeEnd?: OnResizeEnd;
/** The direction the user can resize the node */
resizeDirection?: ResizeControlDirection;
} & HTMLAttributes<HTMLDivElement>;
export type ResizeControlProps = Pick<
@@ -62,6 +65,7 @@ export type ResizeControlProps = Pick<
| 'onResizeStart'
| 'onResize'
| 'onResizeEnd'
| 'resizeDirection'
> & {
/** Position of control
* @example ControlPosition.TopLeft, ControlPosition.TopRight,

View File

@@ -15,7 +15,9 @@ import {
updateAbsolutePositions,
snapPosition,
calculateNodePosition,
type SetCenterOptions
type SetCenterOptions,
getHandlePosition,
Position
} from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
@@ -51,6 +53,15 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
store.nodes = store.nodes.map((node) => {
if (store.connection.inProgress && store.connection.fromNode.id === node.id) {
const internalNode = store.nodeLookup.get(node.id);
if (internalNode) {
store.connection = {
...store.connection,
from: getHandlePosition(internalNode, store.connection.fromHandle, Position.Left, true)
};
}
}
const dragItem = nodeDragItems.get(node.id);
return dragItem ? { ...node, position: dragItem.position, dragging } : node;
});
@@ -191,10 +202,6 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
}
}
function setPaneClickDistance(distance: number) {
store.panZoom?.setClickDistance(distance);
}
function deselect<T extends Node | Edge>(
elements: T[],
elementsToDeselect: Set<string> | null = null
@@ -235,12 +242,8 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
const nodeWillBeSelected = ids.includes(node.id);
const selected = isMultiSelection ? node.selected || nodeWillBeSelected : nodeWillBeSelected;
if (node.selected !== selected) {
// we need to mutate the node here in order to have the correct selected state in the drag handler
const internalNode = store.nodeLookup.get(node.id);
if (internalNode) internalNode.selected = selected;
node.selected = selected;
return { ...node };
if (!!node.selected !== selected) {
return { ...node, selected };
}
return node;
});
@@ -257,7 +260,7 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
const edgeWillBeSelected = ids.includes(edge.id);
const selected = isMultiSelection ? edge.selected || edgeWillBeSelected : edgeWillBeSelected;
if (edge.selected !== selected) {
if (!!edge.selected !== selected) {
return { ...edge, selected };
}
return edge;
@@ -397,7 +400,6 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
setMinZoom,
setMaxZoom,
setTranslateExtent,
setPaneClickDistance,
unselectNodesAndEdges,
addSelectedNodes,
addSelectedEdges,

View File

@@ -1,3 +1,5 @@
/* eslint-disable svelte/prefer-svelte-reactivity */
import {
infiniteExtent,
SelectionMode,
@@ -286,6 +288,7 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
autoPanOnNodeFocus: boolean = $derived(signals.props.autoPanOnNodeFocus ?? true);
autoPanSpeed: number = $derived(signals.props.autoPanSpeed ?? 15);
connectionDragThreshold: number = $derived(signals.props.connectionDragThreshold ?? 1);
fitViewQueued: boolean = signals.props.fitView ?? false;
@@ -476,3 +479,5 @@ function warnIfDeeplyReactive(array: unknown[] | undefined, name: string) {
console.warn(`Use $state.raw for ${name} to prevent performance issues.`);
}
}
/* eslint-enable svelte/prefer-svelte-reactivity */

View File

@@ -23,7 +23,6 @@ export type SvelteFlowStoreActions<NodeType extends Node = Node, EdgeType extend
setMinZoom: (minZoom: number) => void;
setMaxZoom: (maxZoom: number) => void;
setTranslateExtent: (extent: CoordinateExtent) => void;
setPaneClickDistance: (distance: number) => void;
fitView: (options?: FitViewOptions) => Promise<boolean>;
setCenter: SetCenter;
updateNodePositions: UpdateNodePositions;

View File

@@ -1,5 +1,55 @@
# @xyflow/system
## 0.0.73
### Patch Changes
- [#5578](https://github.com/xyflow/xyflow/pull/5578) [`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773) Thanks [@peterkogo](https://github.com/peterkogo)! - Pass current pointer position to connection
## 0.0.72
### Patch Changes
- [#5572](https://github.com/xyflow/xyflow/pull/5572) [`5ec0cac7f`](https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix onPaneClick events being suppressed when selectionOnDrag=true
## 0.0.71
### Patch Changes
- [#5544](https://github.com/xyflow/xyflow/pull/5544) [`c17b49f4c`](https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Add `EdgeToolbar` component
- [#5550](https://github.com/xyflow/xyflow/pull/5550) [`6ffb9f790`](https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent child nodes of different parents from overlapping
- [#5528](https://github.com/xyflow/xyflow/pull/5528) [`d50a963cd`](https://github.com/xyflow/xyflow/commit/d50a963cd0f00cd31236d760dcd7995218d80553) Thanks [@peterkogo](https://github.com/peterkogo)! - Let `NodeResizer` props change during ongoing resize
- [#5551](https://github.com/xyflow/xyflow/pull/5551) [`6bb64b3ed`](https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634) Thanks [@moklick](https://github.com/moklick)! - Allow to start a selection above a node
- [#5546](https://github.com/xyflow/xyflow/pull/5546) [`8a9ee2d83`](https://github.com/xyflow/xyflow/commit/8a9ee2d836776da42b867c6ba90d302fbbc79b37) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Do not crash mini map if all nodes are hidden
- [#5547](https://github.com/xyflow/xyflow/pull/5547) [`1c8961207`](https://github.com/xyflow/xyflow/commit/1c8961207e8ac326098af18489465b36cdd2d831) Thanks [@moklick](https://github.com/moklick)! - Always call `onMoveEnd` when `onMoveStart` was called
## 0.0.70
### Patch Changes
- [#5515](https://github.com/xyflow/xyflow/pull/5515) [`9b4e99029`](https://github.com/xyflow/xyflow/commit/9b4e99029ba87d44c71b33c06259a126db193b43) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix id of static handles not being parsed
- [#5512](https://github.com/xyflow/xyflow/pull/5512) [`70b672604`](https://github.com/xyflow/xyflow/commit/70b67260470738dd0d88ed8ab558d400097be785) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent native page zoom when pinch zooming on node with nowheel class
## 0.0.69
### Patch Changes
- [#5480](https://github.com/xyflow/xyflow/pull/5480) [`f869808e3`](https://github.com/xyflow/xyflow/commit/f869808e3dfd3d289db034d21d505d62f84cdde3) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent multi-touch events while making a new connection
- [#5489](https://github.com/xyflow/xyflow/pull/5489) [`2a35c65a0`](https://github.com/xyflow/xyflow/commit/2a35c65a02c6c8aec6b005ac3e72988f996669c4) Thanks [@Sec-ant](https://github.com/Sec-ant)! - Fix nodeDragThreshold changing with zoom level
- [#5509](https://github.com/xyflow/xyflow/pull/5509) [`b6ae4cc11`](https://github.com/xyflow/xyflow/commit/b6ae4cc11f1456f843ace7e7f3de41c9196179e0) Thanks [@moklick](https://github.com/moklick)! - Prevent calling onResizeEnd if node was not resized
- [#5511](https://github.com/xyflow/xyflow/pull/5511) [`d891e5ff1`](https://github.com/xyflow/xyflow/commit/d891e5ff106c6202f69a9b14d261294cc29bbec7) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix regression: elevate edges if connected nodes are selected
- [#5497](https://github.com/xyflow/xyflow/pull/5497) [`eba087e8b`](https://github.com/xyflow/xyflow/commit/eba087e8bec74e6591e04ebf0e1e61799b94ecd8) Thanks [@peterkogo](https://github.com/peterkogo)! - Skip eagerly rendering nodes when node dimensions and handles are predefined
## 0.0.68
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/system",
"version": "0.0.68",
"version": "0.0.73",
"description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [
"node-based UI",

View File

@@ -129,3 +129,30 @@ export type EdgePosition = {
};
export type EdgeLookup<EdgeType extends EdgeBase = EdgeBase> = Map<string, EdgeType>;
export type EdgeToolbarBaseProps = {
/**
* The `x` position of the edge toolbar.
*/
x: number;
/**
* The `y` position of the edge toolbar.
*/
y: number;
/** If `true`, edge toolbar is visible even if edge is not selected.
* @default false
*/
isVisible?: boolean;
/**
* Align the vertical toolbar position relative to the passed x position.
* @default "center"
* @example 'left', 'center', 'right'
*/
alignX?: 'left' | 'center' | 'right';
/**
* Align the horizontal toolbar position relative to the passed y position.
* @default "center"
* @example 'top', 'center', 'bottom'
*/
alignY?: 'top' | 'center' | 'bottom';
};

View File

@@ -292,6 +292,7 @@ export const initialConnection: NoConnection = {
toHandle: null,
toPosition: null,
toNode: null,
pointer: null,
};
export type NoConnection = {
@@ -305,6 +306,7 @@ export type NoConnection = {
toHandle: null;
toPosition: null;
toNode: null;
pointer: null;
};
export type ConnectionInProgress<NodeType extends InternalNodeBase = InternalNodeBase> = {
/** Indicates whether a connection is currently in progress. */
@@ -330,6 +332,8 @@ export type ConnectionInProgress<NodeType extends InternalNodeBase = InternalNod
toPosition: Position;
/** Returns the end node or `null` if no connection is in progress. */
toNode: NodeType | null;
/** Returns the pointer position or `null` if no connection is in progress. */
pointer: XYPosition;
};
/**

View File

@@ -95,6 +95,7 @@ export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = Omit<NodeTy
internals: {
positionAbsolute: XYPosition;
z: number;
rootParentIndex?: number;
/**
* Holds a reference to the original node object provided by the user.
* Used as an optimization to avoid certain operations.

View File

@@ -9,7 +9,6 @@ export type PanZoomParams = {
domNode: Element;
minZoom: number;
maxZoom: number;
paneClickDistance: number;
viewport: Viewport;
translateExtent: CoordinateExtent;
onDraggingChange: OnDraggingChange;
@@ -42,6 +41,9 @@ export type PanZoomUpdateOptions = {
zoomActivationKeyPressed: boolean;
lib: string;
onTransformChange: OnTransformChange;
connectionInProgress: boolean;
paneClickDistance: number;
selectionOnDrag?: boolean;
};
export type PanZoomInstance = {

Some files were not shown because too many files have changed in this diff Show More