diff --git a/.changeset/curly-snails-do.md b/.changeset/curly-snails-do.md new file mode 100644 index 00000000..5560b33b --- /dev/null +++ b/.changeset/curly-snails-do.md @@ -0,0 +1,5 @@ +--- +'@xyflow/react': patch +--- + +Prevent unnecessary re-render in `FlowRenderer` diff --git a/.changeset/custom-edge-id-generator.md b/.changeset/custom-edge-id-generator.md new file mode 100644 index 00000000..439d729c --- /dev/null +++ b/.changeset/custom-edge-id-generator.md @@ -0,0 +1,5 @@ +--- +'@xyflow/system': patch +--- + +Allow custom `getEdgeId` function in `addEdge` and `reconnectEdge` options to enable custom edge ID schemes. diff --git a/.changeset/dry-seahorses-deliver.md b/.changeset/dry-seahorses-deliver.md new file mode 100644 index 00000000..1125ea3e --- /dev/null +++ b/.changeset/dry-seahorses-deliver.md @@ -0,0 +1,5 @@ +--- +'@xyflow/react': patch +--- + +Always create a new measured object in apply changes. diff --git a/.changeset/young-roses-shout.md b/.changeset/young-roses-shout.md new file mode 100644 index 00000000..f1f6a888 --- /dev/null +++ b/.changeset/young-roses-shout.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': patch +'@xyflow/svelte': patch +'@xyflow/system': patch +--- + +Update an ongoing connection when user moves node with keyboard. diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..1a97ca40 --- /dev/null +++ b/.coderabbit.yaml @@ -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 CodeRabbit’s 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. It’s mainly concerned with Java. + # Default: {} + pmd: + # Enable PMD - PMD is an extensible multilanguage static code analyzer. It’s 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: [] diff --git a/.github/workflows/dispatchWebsiteUpdate.yaml b/.github/workflows/dispatchWebsiteUpdate.yaml index 6acc97ac..b769e571 100644 --- a/.github/workflows/dispatchWebsiteUpdate.yaml +++ b/.github/workflows/dispatchWebsiteUpdate.yaml @@ -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: diff --git a/LICENSE b/LICENSE index 8540abfb..072ff46d 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index 3ed80c4b..116e99c7 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..52eadb52 --- /dev/null +++ b/SECURITY.md @@ -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. \ No newline at end of file diff --git a/dependabot.yml b/dependabot.yml new file mode 100644 index 00000000..8e3eba9e --- /dev/null +++ b/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: 'npm' + directory: '/packages/' + schedule: + interval: 'weekly' + ignore: + - dependency-type: 'development' diff --git a/examples/astro-xyflow/package.json b/examples/astro-xyflow/package.json index 313afc4b..4b952906 100644 --- a/examples/astro-xyflow/package.json +++ b/examples/astro-xyflow/package.json @@ -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" } } diff --git a/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx b/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx index 863458fb..c2957812 100644 --- a/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx +++ b/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx @@ -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 > diff --git a/examples/astro-xyflow/src/components/SvelteFlowExample/index.svelte b/examples/astro-xyflow/src/components/SvelteFlowExample/index.svelte index ff7e98c1..1b086c1f 100644 --- a/examples/astro-xyflow/src/components/SvelteFlowExample/index.svelte +++ b/examples/astro-xyflow/src/components/SvelteFlowExample/index.svelte @@ -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 @@
- + diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index d781ff4d..4cf4b57b 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -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', diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index 168bcff6..a24c908c 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -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 ( - - - - + <> + + + + - - - - - + + + + + - - - - - - - + + + + + + + + + ); }; diff --git a/examples/react/src/examples/CustomConnectionLine/ConnectionLine.tsx b/examples/react/src/examples/CustomConnectionLine/ConnectionLine.tsx index ddba960b..bf6a82d7 100644 --- a/examples/react/src/examples/CustomConnectionLine/ConnectionLine.tsx +++ b/examples/react/src/examples/CustomConnectionLine/ConnectionLine.tsx @@ -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 ( <> 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 ; }; @@ -34,6 +28,7 @@ const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => { const CustomMiniMapNodeFlow = () => { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); + 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 ( { - + + + + ); }; diff --git a/examples/react/src/examples/CustomNode/ColorSelectorNode.tsx b/examples/react/src/examples/CustomNode/ColorSelectorNode.tsx index 1265d408..77bce1c9 100644 --- a/examples/react/src/examples/CustomNode/ColorSelectorNode.tsx +++ b/examples/react/src/examples/CustomNode/ColorSelectorNode.tsx @@ -30,7 +30,7 @@ function ColorSelectorNode({ data, isConnectable }: NodeProps
Custom Color Picker Node: {data.color}
- + { + 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 ( + <> + + + + + + ); +} diff --git a/examples/react/src/examples/EdgeToolbar/index.tsx b/examples/react/src/examples/EdgeToolbar/index.tsx new file mode 100644 index 00000000..31d37ffa --- /dev/null +++ b/examples/react/src/examples/EdgeToolbar/index.tsx @@ -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 ( + + + + + + ); +} diff --git a/examples/react/src/examples/Figma/index.tsx b/examples/react/src/examples/Figma/index.tsx index 6780b6cb..2ac0d6b6 100644 --- a/examples/react/src/examples/Figma/index.tsx +++ b/examples/react/src/examples/Figma/index.tsx @@ -30,10 +30,11 @@ 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)} > diff --git a/examples/react/src/examples/NodeResizer/DefaultResizer.tsx b/examples/react/src/examples/NodeResizer/DefaultResizer.tsx index a3532a8f..d5d314ba 100644 --- a/examples/react/src/examples/NodeResizer/DefaultResizer.tsx +++ b/examples/react/src/examples/NodeResizer/DefaultResizer.tsx @@ -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 = ({ data, selected }) => { + const keepAspectRatio = useKeyPress('k'); + return ( <> = ({ data, selected }) => { onResizeStart={data.onResizeStart ?? undefined} onResize={data.onResize ?? undefined} onResizeEnd={data.onResizeEnd ?? undefined} - keepAspectRatio={data.keepAspectRatio ?? undefined} + keepAspectRatio={keepAspectRatio || (data.keepAspectRatio ?? undefined)} />
{data.label}
diff --git a/examples/react/src/examples/Stress/index.tsx b/examples/react/src/examples/Stress/index.tsx index 496fd500..537e533b 100644 --- a/examples/react/src/examples/Stress/index.tsx +++ b/examples/react/src/examples/Stress/index.tsx @@ -239,6 +239,16 @@ const StressFlow = () => { +
); diff --git a/examples/react/src/examples/Subflow/index.tsx b/examples/react/src/examples/Subflow/index.tsx index 82d0d413..ac806c59 100644 --- a/examples/react/src/examples/Subflow/index.tsx +++ b/examples/react/src/examples/Subflow/index.tsx @@ -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', diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index 1db10784..46ea8114 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -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', diff --git a/examples/svelte/src/routes/examples/custom-connection-line/ConnectionLine.svelte b/examples/svelte/src/routes/examples/custom-connection-line/ConnectionLine.svelte index 5c48f62d..129bf7d3 100644 --- a/examples/svelte/src/routes/examples/custom-connection-line/ConnectionLine.svelte +++ b/examples/svelte/src/routes/examples/custom-connection-line/ConnectionLine.svelte @@ -19,6 +19,8 @@ } return null; }); + + $inspect(connection.current.pointer); {#if connection.current.inProgress} diff --git a/examples/svelte/src/routes/examples/custom-minimap/+page.svelte b/examples/svelte/src/routes/examples/custom-minimap/+page.svelte new file mode 100644 index 00000000..0a0c734c --- /dev/null +++ b/examples/svelte/src/routes/examples/custom-minimap/+page.svelte @@ -0,0 +1,55 @@ + + + + + + + + + + + + + diff --git a/examples/svelte/src/routes/examples/custom-minimap/CustomMiniMapNode.svelte b/examples/svelte/src/routes/examples/custom-minimap/CustomMiniMapNode.svelte new file mode 100644 index 00000000..7cd33295 --- /dev/null +++ b/examples/svelte/src/routes/examples/custom-minimap/CustomMiniMapNode.svelte @@ -0,0 +1,25 @@ + + + + + {id} + diff --git a/examples/svelte/src/routes/examples/customnode/CustomNode.svelte b/examples/svelte/src/routes/examples/customnode/CustomNode.svelte index 6bc71e2e..30182418 100644 --- a/examples/svelte/src/routes/examples/customnode/CustomNode.svelte +++ b/examples/svelte/src/routes/examples/customnode/CustomNode.svelte @@ -13,7 +13,7 @@ Custom Color Picker Node: {bg.color}
(bg.color = evt.currentTarget.value)} value={bg.color} diff --git a/examples/svelte/src/routes/examples/edge-toolbar/+page.svelte b/examples/svelte/src/routes/examples/edge-toolbar/+page.svelte new file mode 100644 index 00000000..40240013 --- /dev/null +++ b/examples/svelte/src/routes/examples/edge-toolbar/+page.svelte @@ -0,0 +1,60 @@ + + + + + diff --git a/examples/svelte/src/routes/examples/edge-toolbar/CustomEdge.svelte b/examples/svelte/src/routes/examples/edge-toolbar/CustomEdge.svelte new file mode 100644 index 00000000..24876c64 --- /dev/null +++ b/examples/svelte/src/routes/examples/edge-toolbar/CustomEdge.svelte @@ -0,0 +1,26 @@ + + + + + + diff --git a/examples/svelte/src/routes/examples/figma/+page.svelte b/examples/svelte/src/routes/examples/figma/+page.svelte index 35a694ac..33de11ea 100644 --- a/examples/svelte/src/routes/examples/figma/+page.svelte +++ b/examples/svelte/src/routes/examples/figma/+page.svelte @@ -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)} > diff --git a/examples/svelte/src/routes/examples/node-resizer/+page.svelte b/examples/svelte/src/routes/examples/node-resizer/+page.svelte index 8a5ec2ae..61f4ca22 100644 --- a/examples/svelte/src/routes/examples/node-resizer/+page.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/+page.svelte @@ -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); + { + if (e.key === 'k') store.keepAspectRatio = true; + }} + on:keyup={(e) => { + if (e.key === 'k') store.keepAspectRatio = false; + }} +/> + + import { + Handle, + Position, + type NodeProps, + NodeResizeControl, + ResizeControlVariant + } from '@xyflow/svelte'; + import type { ResizeNode } from './types'; + + let { data }: NodeProps = $props(); + + + + +
{data.label}
+ diff --git a/examples/svelte/src/routes/examples/node-resizer/CustomResizer.svelte b/examples/svelte/src/routes/examples/node-resizer/CustomResizer.svelte index 464274ec..15cf8b22 100644 --- a/examples/svelte/src/routes/examples/node-resizer/CustomResizer.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/CustomResizer.svelte @@ -1,6 +1,7 @@ @@ -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;" > = $props(); @@ -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)} />
{data.label}
diff --git a/examples/svelte/src/routes/examples/node-resizer/HorizontalResizer.svelte b/examples/svelte/src/routes/examples/node-resizer/HorizontalResizer.svelte index d9ec28ff..2727e4c7 100644 --- a/examples/svelte/src/routes/examples/node-resizer/HorizontalResizer.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/HorizontalResizer.svelte @@ -1,6 +1,7 @@ @@ -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} /> diff --git a/examples/svelte/src/routes/examples/node-resizer/VerticalResizer.svelte b/examples/svelte/src/routes/examples/node-resizer/VerticalResizer.svelte index fedfff50..eec2578b 100644 --- a/examples/svelte/src/routes/examples/node-resizer/VerticalResizer.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/VerticalResizer.svelte @@ -1,6 +1,7 @@ @@ -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} /> diff --git a/examples/svelte/src/routes/examples/node-resizer/store.svelte.ts b/examples/svelte/src/routes/examples/node-resizer/store.svelte.ts new file mode 100644 index 00000000..076582b2 --- /dev/null +++ b/examples/svelte/src/routes/examples/node-resizer/store.svelte.ts @@ -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(); diff --git a/examples/svelte/src/routes/examples/overview/Flow.svelte b/examples/svelte/src/routes/examples/overview/Flow.svelte index 3075a0e8..e83548fd 100644 --- a/examples/svelte/src/routes/examples/overview/Flow.svelte +++ b/examples/svelte/src/routes/examples/overview/Flow.svelte @@ -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); + + diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index 2dc4c6aa..ef20b865 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -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) => { diff --git a/packages/svelte/src/lib/container/Pane/Pane.svelte b/packages/svelte/src/lib/container/Pane/Pane.svelte index db11bd7d..b7de8cfa 100644 --- a/packages/svelte/src/lib/container/Pane/Pane.svelte +++ b/packages/svelte/src/lib/container/Pane/Pane.svelte @@ -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 = new Set(); let selectedEdgeIds: Set = 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 | null)?.setPointerCapture?.(event.pointerId); + (event.target as Partial)?.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 | null)?.releasePointerCapture?.(event.pointerId); + (event.target as Partial)?.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; + } @@ -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()} diff --git a/packages/svelte/src/lib/container/Pane/types.ts b/packages/svelte/src/lib/container/Pane/types.ts index 8442834e..f0cef4df 100644 --- a/packages/svelte/src/lib/container/Pane/types.ts +++ b/packages/svelte/src/lib/container/Pane/types.ts @@ -5,6 +5,7 @@ import type { SvelteFlowStore } from '$lib/store/types'; export type PaneProps = { store: SvelteFlowStore; panOnDrag?: boolean | number[]; + paneClickDistance: number; selectionOnDrag?: boolean; onselectionstart?: (event: PointerEvent) => void; onselectionend?: (event: PointerEvent) => void; diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 9b039558..ab9378b8 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -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} > diff --git a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte index 847dc4e4..88ba9f1a 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte @@ -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 */ diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 5cd4b740..0fbd4b18 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -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. diff --git a/packages/svelte/src/lib/container/Zoom/Zoom.svelte b/packages/svelte/src/lib/container/Zoom/Zoom.svelte index 42a644a6..761cffef 100644 --- a/packages/svelte/src/lib/container/Zoom/Zoom.svelte +++ b/packages/svelte/src/lib/container/Zoom/Zoom.svelte @@ -1,5 +1,5 @@ + +{#if isActive} + +
+ {@render children?.()} +
+
+{/if} diff --git a/packages/svelte/src/lib/plugins/EdgeToolbar/index.ts b/packages/svelte/src/lib/plugins/EdgeToolbar/index.ts new file mode 100644 index 00000000..602e9a61 --- /dev/null +++ b/packages/svelte/src/lib/plugins/EdgeToolbar/index.ts @@ -0,0 +1,2 @@ +export { default as EdgeToolbar } from './EdgeToolbar.svelte'; +export * from './types'; diff --git a/packages/svelte/src/lib/plugins/EdgeToolbar/types.ts b/packages/svelte/src/lib/plugins/EdgeToolbar/types.ts new file mode 100644 index 00000000..12655490 --- /dev/null +++ b/packages/svelte/src/lib/plugins/EdgeToolbar/types.ts @@ -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 & { + selectEdgeOnClick?: boolean; + children?: Snippet; +} & HTMLAttributes; diff --git a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte index ac346066..eaca13a8 100644 --- a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte +++ b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte @@ -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)} 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; } = $props(); - +{#if nodeComponent} + {@const CustomComponent = nodeComponent} + + +{:else} + +{/if} diff --git a/packages/svelte/src/lib/plugins/Minimap/types.ts b/packages/svelte/src/lib/plugins/Minimap/types.ts index dd6903de..20ed49ff 100644 --- a/packages/svelte/src/lib/plugins/Minimap/types.ts +++ b/packages/svelte/src/lib/plugins/Minimap/types.ts @@ -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; /** Color of the mask representing viewport */ maskColor?: string; /** Stroke color of the mask representing viewport */ diff --git a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte index b9f38214..dc6eb47a 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte +++ b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte @@ -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>(); - 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(); + 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, diff --git a/packages/svelte/src/lib/plugins/NodeResizer/types.ts b/packages/svelte/src/lib/plugins/NodeResizer/types.ts index 5f9bb95e..cae8d7b8 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/types.ts +++ b/packages/svelte/src/lib/plugins/NodeResizer/types.ts @@ -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; export type ResizeControlProps = Pick< @@ -62,6 +65,7 @@ export type ResizeControlProps = Pick< | 'onResizeStart' | 'onResize' | 'onResizeEnd' + | 'resizeDirection' > & { /** Position of control * @example ControlPosition.TopLeft, ControlPosition.TopRight, diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index a1f220bb..30bcafc1 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -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 { 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( elements: T[], elementsToDeselect: Set | null = null @@ -235,12 +242,8 @@ export function createStore void; setMaxZoom: (maxZoom: number) => void; setTranslateExtent: (extent: CoordinateExtent) => void; - setPaneClickDistance: (distance: number) => void; fitView: (options?: FitViewOptions) => Promise; setCenter: SetCenter; updateNodePositions: UpdateNodePositions; diff --git a/packages/system/CHANGELOG.md b/packages/system/CHANGELOG.md index a3471662..5bbc621e 100644 --- a/packages/system/CHANGELOG.md +++ b/packages/system/CHANGELOG.md @@ -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 diff --git a/packages/system/package.json b/packages/system/package.json index 21aca406..6ee8e327 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -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", diff --git a/packages/system/src/types/edges.ts b/packages/system/src/types/edges.ts index 42678f8c..6f8f4bb6 100644 --- a/packages/system/src/types/edges.ts +++ b/packages/system/src/types/edges.ts @@ -129,3 +129,30 @@ export type EdgePosition = { }; export type EdgeLookup = Map; + +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'; +}; diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 15f64262..d43c50ee 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -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 = { /** Indicates whether a connection is currently in progress. */ @@ -330,6 +332,8 @@ export type ConnectionInProgress = Omit = { + left: 0, + center: 50, + right: 100, +}; + +const alignYToPercent: Record<'top' | 'center' | 'bottom', number> = { + top: 0, + center: 50, + bottom: 100, +}; + +export function getEdgeToolbarTransform( + x: number, + y: number, + zoom: number, + alignX: 'left' | 'center' | 'right' = 'center', + alignY: 'top' | 'center' | 'bottom' = 'center' +): string { + return `translate(${x}px, ${y}px) scale(${1 / zoom}) translate(${-(alignXToPercent[alignX] ?? 50)}%, ${-( + alignYToPercent[alignY] ?? 50 + )}%)`; +} diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index c88c7272..515a285a 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -45,12 +45,11 @@ export function getElevatedEdgeZIndex({ if (zIndex !== undefined) { return zIndex; } - const edgeZ = elevateOnSelect && selected ? 1000 : 0; const nodeZ = Math.max( - sourceNode.parentId ? sourceNode.internals.z : 0, - targetNode.parentId ? targetNode.internals.z : 0 + sourceNode.parentId || (elevateOnSelect && sourceNode.selected) ? sourceNode.internals.z : 0, + targetNode.parentId || (elevateOnSelect && targetNode.selected) ? targetNode.internals.z : 0 ); return edgeZ + nodeZ; @@ -85,7 +84,19 @@ export function isEdgeVisible({ sourceNode, targetNode, width, height, transform return getOverlappingArea(viewRect, boxToRect(edgeBox)) > 0; } -const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | EdgeBase): string => +/** + * Type for a custom edge ID generator function. + * @public + */ +export type GetEdgeId = (params: Connection | EdgeBase) => string; + +/** + * The default edge ID generator function. Generates an ID based on the source, target, and handles. + * @public + * @param params - The connection or edge to generate an ID for. + * @returns The generated edge ID. + */ +export const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | EdgeBase): string => `xy-edge__${source}${sourceHandle || ''}-${target}${targetHandle || ''}`; const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => { @@ -98,11 +109,19 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => { ); }; +export type AddEdgeOptions = { + /** + * Custom function to generate edge IDs. If not provided, the default `getEdgeId` function is used. + */ + getEdgeId?: GetEdgeId; +}; + /** * This util is a convenience function to add a new Edge to an array of edges. It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one. * @public * @param edgeParams - Either an `Edge` or a `Connection` you want to add. * @param edges - The array of all current edges. + * @param options - Optional configuration object. * @returns A new array of edges with the new edge added. * * @remarks If an edge with the same `target` and `source` already exists (and the same @@ -112,7 +131,8 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => { */ export const addEdge = ( edgeParams: EdgeType | Connection, - edges: EdgeType[] + edges: EdgeType[], + options: AddEdgeOptions = {} ): EdgeType[] => { if (!edgeParams.source || !edgeParams.target) { devWarn('006', errorMessages['error006']()); @@ -120,13 +140,15 @@ export const addEdge = ( return edges; } + const edgeIdGenerator = options.getEdgeId || getEdgeId; + let edge: EdgeType; if (isEdgeBase(edgeParams)) { edge = { ...edgeParams }; } else { edge = { ...edgeParams, - id: getEdgeId(edgeParams), + id: edgeIdGenerator(edgeParams), } as EdgeType; } @@ -151,6 +173,10 @@ export type ReconnectEdgeOptions = { * @default true */ shouldReplaceId?: boolean; + /** + * Custom function to generate edge IDs. If not provided, the default `getEdgeId` function is used. + */ + getEdgeId?: GetEdgeId; }; /** @@ -191,10 +217,12 @@ export const reconnectEdge = ( return edges; } + const edgeIdGenerator = options.getEdgeId || getEdgeId; + // Remove old edge and create the new edge with parameters of old edge. const edge = { ...rest, - id: options.shouldReplaceId ? getEdgeId(newConnection) : oldEdgeId, + id: options.shouldReplaceId ? edgeIdGenerator(newConnection) : oldEdgeId, source: newConnection.source, target: newConnection.target, sourceHandle: newConnection.sourceHandle, diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index d859265e..df32a165 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -241,20 +241,17 @@ export const getInternalNodesBounds = , params: GetInternalNodesBoundsParams = {} ): Rect => { - if (nodeLookup.size === 0) { - return { x: 0, y: 0, width: 0, height: 0 }; - } - let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }; + let hasVisibleNodes = false; nodeLookup.forEach((node) => { if (params.filter === undefined || params.filter(node)) { - const nodeBox = nodeToBox(node as InternalNodeBase); - box = getBoundsOfBoxes(box, nodeBox); + box = getBoundsOfBoxes(box, nodeToBox(node as InternalNodeBase)); + hasVisibleNodes = true; } }); - return boxToRect(box); + return hasVisibleNodes ? boxToRect(box) : { x: 0, y: 0, width: 0, height: 0 }; }; export const getNodesInside = ( diff --git a/packages/system/src/utils/index.ts b/packages/system/src/utils/index.ts index 2640195d..caf8b026 100644 --- a/packages/system/src/utils/index.ts +++ b/packages/system/src/utils/index.ts @@ -5,6 +5,7 @@ export * from './graph'; export * from './general'; export * from './marker'; export * from './node-toolbar'; +export * from './edge-toolbar'; export * from './store'; export * from './types'; export * from './shallow-node-data'; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 1bfd029d..88f4bb79 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -1,4 +1,4 @@ -import { HandleConnection, infiniteExtent } from '..'; +import { Handle, HandleConnection, infiniteExtent, NodeHandle, NodeHandleBounds } from '..'; import { NodeBase, CoordinateExtent, @@ -30,6 +30,9 @@ import { import { getNodePositionWithOrigin } from './graph'; import { ParentExpandChild } from './types'; +const SELECTED_NODE_Z = 1000; +const ROOT_PARENT_Z_INCREMENT = 10; + const defaultOptions = { nodeOrigin: [0, 0] as NodeOrigin, nodeExtent: infiniteExtent, @@ -72,6 +75,38 @@ export function updateAbsolutePositions( } } +function parseHandles(userNode: NodeBase, internalNode?: InternalNodeBase): NodeHandleBounds | undefined { + if (!userNode.handles) { + return !userNode.measured ? undefined : internalNode?.internals.handleBounds; + } + + const source: Handle[] = []; + const target: Handle[] = []; + + for (const handle of userNode.handles) { + const handleBounds = { + id: handle.id, + width: handle.width ?? 1, + height: handle.height ?? 1, + nodeId: userNode.id, + x: handle.x, + y: handle.y, + position: handle.position, + type: handle.type, + }; + if (handle.type === 'source') { + source.push(handleBounds); + } else if (handle.type === 'target') { + target.push(handleBounds); + } + } + + return { + source, + target, + }; +} + type UpdateNodesOptions = { nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; @@ -88,9 +123,10 @@ export function adoptUserNodes( ): boolean { const _options = mergeObjects(adoptUserNodesDefaultOptions, options); + let rootParentIndex = { i: -1 }; let nodesInitialized = nodes.length > 0; const tmpLookup = new Map(nodeLookup); - const selectedNodeZ: number = _options?.elevateNodesOnSelect ? 1000 : 0; + const selectedNodeZ: number = _options?.elevateNodesOnSelect ? SELECTED_NODE_Z : 0; nodeLookup.clear(); parentLookup.clear(); @@ -115,7 +151,7 @@ export function adoptUserNodes( internals: { positionAbsolute: clampedPosition, // if user re-initializes the node or removes `measured` for whatever reason, we reset the handleBounds so that the node gets re-measured - handleBounds: !userNode.measured ? undefined : internalNode?.internals.handleBounds, + handleBounds: parseHandles(userNode, internalNode), z: calculateZ(userNode, selectedNodeZ), userNode, }, @@ -134,7 +170,7 @@ export function adoptUserNodes( } if (userNode.parentId) { - updateChildNode(internalNode, nodeLookup, parentLookup, options); + updateChildNode(internalNode, nodeLookup, parentLookup, options, rootParentIndex); } } @@ -165,7 +201,8 @@ function updateChildNode( node: InternalNodeBase, nodeLookup: NodeLookup>, parentLookup: ParentLookup>, - options?: UpdateNodesOptions + options?: UpdateNodesOptions, + rootParentIndex?: { i: number } ) { const { elevateNodesOnSelect, nodeOrigin, nodeExtent } = mergeObjects(defaultOptions, options); const parentId = node.parentId!; @@ -180,7 +217,18 @@ function updateChildNode( updateParentLookup(node, parentLookup); - const selectedNodeZ = elevateNodesOnSelect ? 1000 : 0; + // We just want to set the rootParentIndex for the first child + if (rootParentIndex && !parentNode.parentId && parentNode.internals.rootParentIndex === undefined) { + parentNode.internals.rootParentIndex = ++rootParentIndex.i; + parentNode.internals.z = parentNode.internals.z + rootParentIndex.i * ROOT_PARENT_Z_INCREMENT; + } + + // But we need to update rootParentIndex.i also when parent has not been updated + if (rootParentIndex && parentNode.internals.rootParentIndex !== undefined) { + rootParentIndex.i = parentNode.internals.rootParentIndex; + } + + const selectedNodeZ = elevateNodesOnSelect ? SELECTED_NODE_Z : 0; const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ); const { positionAbsolute } = node.internals; const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y; diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 1da0720d..c78e1035 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -334,8 +334,10 @@ export function XYDrag voi } if (!dragStarted) { - const x = pointerPos.xSnapped - (lastPos.x ?? 0); - const y = pointerPos.ySnapped - (lastPos.y ?? 0); + // Calculate distance in client coordinates for consistent drag threshold behavior across zoom levels + const currentMousePosition = getEventPosition(event.sourceEvent, containerBounds!); + const x = currentMousePosition.x - mousePosition.x; + const y = currentMousePosition.y - mousePosition.y; const distance = Math.sqrt(x * x + y * y); if (distance > nodeDragThreshold) { diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index aa71d49d..b8edf767 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -93,8 +93,8 @@ function onPointerDown( position: fromHandleInternal.position, }; - const fromNodeInternal = nodeLookup.get(nodeId)!; - const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true); + const fromInternalNode = nodeLookup.get(nodeId)!; + const from = getHandlePosition(fromInternalNode, fromHandle, Position.Left, true); let previousConnection: ConnectionInProgress = { inProgress: true, @@ -103,12 +103,13 @@ function onPointerDown( from, fromHandle, fromPosition: fromHandle.position, - fromNode: fromNodeInternal, + fromNode: fromInternalNode, to: position, toHandle: null, toPosition: oppositePosition[fromHandle.position], toNode: null, + pointer: position, }; function startConnection() { @@ -171,9 +172,14 @@ function onPointerDown( connection = result.connection; isValid = isConnectionValid(!!closestHandle, result.isValid); + const fromInternalNode = nodeLookup.get(nodeId); + const from = fromInternalNode + ? getHandlePosition(fromInternalNode, fromHandle, Position.Left, true) + : previousConnection.from; + const newConnection: ConnectionInProgress = { - // from stays the same ...previousConnection, + from, isValid, to: result.toHandle && isValid @@ -182,31 +188,19 @@ function onPointerDown( toHandle: result.toHandle, toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position], toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)! : null, + pointer: position, }; - /* - * we don't want to trigger an update when the connection - * is snapped to the same handle as before - */ - if ( - isValid && - closestHandle && - previousConnection.toHandle && - newConnection.toHandle && - previousConnection.toHandle.type === newConnection.toHandle.type && - previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId && - previousConnection.toHandle.id === newConnection.toHandle.id && - previousConnection.to.x === newConnection.to.x && - previousConnection.to.y === newConnection.to.y - ) { - return; - } - updateConnection(newConnection); previousConnection = newConnection; } function onPointerUp(event: MouseEvent | TouchEvent) { + // Prevent multi-touch aborting connection + if ('touches' in event && event.touches.length > 0) { + return; + } + if (connectionStarted) { if ((closestHandle || resultHandleDomNode) && connection && isValid) { onConnect?.(connection); diff --git a/packages/system/src/xypanzoom/XYPanZoom.ts b/packages/system/src/xypanzoom/XYPanZoom.ts index 09d28dc2..ee1a763e 100644 --- a/packages/system/src/xypanzoom/XYPanZoom.ts +++ b/packages/system/src/xypanzoom/XYPanZoom.ts @@ -37,7 +37,6 @@ export function XYPanZoom({ domNode, minZoom, maxZoom, - paneClickDistance, translateExtent, viewport, onPanZoom, @@ -55,10 +54,7 @@ export function XYPanZoom({ isPanScrolling: false, }; const bbox = domNode.getBoundingClientRect(); - const d3ZoomInstance = zoom() - .clickDistance(!isNumeric(paneClickDistance) || paneClickDistance < 0 ? 0 : paneClickDistance) - .scaleExtent([minZoom, maxZoom]) - .translateExtent(translateExtent); + const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent); const d3Selection = select(domNode).call(d3ZoomInstance); setViewportConstrained( @@ -108,6 +104,9 @@ export function XYPanZoom({ zoomActivationKeyPressed, lib, onTransformChange, + connectionInProgress, + paneClickDistance, + selectionOnDrag, }: PanZoomUpdateOptions) { if (userSelectionActive && !zoomPanValues.isZoomingOrPanning) { destroy(); @@ -115,6 +114,10 @@ export function XYPanZoom({ const isPanOnScroll = panOnScroll && !zoomActivationKeyPressed && !userSelectionActive; + d3ZoomInstance.clickDistance( + selectionOnDrag ? Infinity : !isNumeric(paneClickDistance) || paneClickDistance < 0 ? 0 : paneClickDistance + ); + const wheelHandler = isPanOnScroll ? createPanOnScrollHandler({ zoomPanValues, @@ -178,6 +181,7 @@ export function XYPanZoom({ noPanClassName, noWheelClassName, lib, + connectionInProgress, }); d3ZoomInstance.filter(filter); diff --git a/packages/system/src/xypanzoom/eventhandler.ts b/packages/system/src/xypanzoom/eventhandler.ts index e6dda425..38f90aa5 100644 --- a/packages/system/src/xypanzoom/eventhandler.ts +++ b/packages/system/src/xypanzoom/eventhandler.ts @@ -12,7 +12,7 @@ import { type OnDraggingChange, type OnTransformChange, } from '../types'; -import { isRightClickPan, isWrappedWithClass, transformToViewport, viewChanged, wheelDelta } from './utils'; +import { isRightClickPan, isWrappedWithClass, transformToViewport, wheelDelta } from './utils'; import { isMacOs } from '../utils'; import { ZoomPanValues } from './XYPanZoom'; @@ -72,6 +72,9 @@ export function createPanOnScrollHandler({ }: PanOnScrollParams) { return (event: any) => { if (isWrappedWithClass(event, noWheelClassName)) { + if (event.ctrlKey) { + event.preventDefault(); // stop native page zoom for pinch zooming + } return false; } event.preventDefault(); @@ -125,9 +128,7 @@ export function createPanOnScrollHandler({ zoomPanValues.isPanScrolling = true; onPanZoomStart?.(event, nextViewport); - } - - if (zoomPanValues.isPanScrolling) { + } else { onPanZoom?.(event, nextViewport); zoomPanValues.panScrollTimeout = setTimeout(() => { @@ -233,7 +234,7 @@ export function createPanZoomEndHandler({ onDraggingChange(false); - if (onPanZoomEnd && viewChanged(zoomPanValues.prevViewport, event.transform)) { + if (onPanZoomEnd) { const viewport = transformToViewport(event.transform); zoomPanValues.prevViewport = viewport; diff --git a/packages/system/src/xypanzoom/filter.ts b/packages/system/src/xypanzoom/filter.ts index 57f92486..3822f7aa 100644 --- a/packages/system/src/xypanzoom/filter.ts +++ b/packages/system/src/xypanzoom/filter.ts @@ -12,6 +12,7 @@ export type FilterParams = { noWheelClassName: string; noPanClassName: string; lib: string; + connectionInProgress: boolean; }; export function createFilter({ @@ -25,10 +26,12 @@ export function createFilter({ noWheelClassName, noPanClassName, lib, + connectionInProgress, }: FilterParams) { return (event: any): boolean => { const zoomScroll = zoomActivationKeyPressed || zoomOnScroll; const pinchZoom = zoomOnPinch && event.ctrlKey; + const isWheelEvent = event.type === 'wheel'; if ( event.button === 1 && @@ -48,20 +51,25 @@ export function createFilter({ return false; } + // we want to disable pinch-zooming while making a connection + if (connectionInProgress && !isWheelEvent) { + return false; + } + // if the target element is inside an element with the nowheel class, we prevent zooming - if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') { + if (isWrappedWithClass(event, noWheelClassName) && isWheelEvent) { return false; } // if the target element is inside an element with the nopan class, we prevent panning if ( isWrappedWithClass(event, noPanClassName) && - (event.type !== 'wheel' || (panOnScroll && event.type === 'wheel' && !zoomActivationKeyPressed)) + (!isWheelEvent || (panOnScroll && isWheelEvent && !zoomActivationKeyPressed)) ) { return false; } - if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') { + if (!zoomOnPinch && event.ctrlKey && isWheelEvent) { return false; } @@ -71,7 +79,7 @@ export function createFilter({ } // when there is no scroll handling enabled, we prevent all wheel events - if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') { + if (!zoomScroll && !panOnScroll && !pinchZoom && isWheelEvent) { return false; } @@ -90,6 +98,6 @@ export function createFilter({ (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || !event.button || event.button <= 1; // default filter for d3-zoom - return (!event.ctrlKey || event.type === 'wheel') && buttonAllowed; + return (!event.ctrlKey || isWheelEvent) && buttonAllowed; }; } diff --git a/packages/system/src/xypanzoom/utils.ts b/packages/system/src/xypanzoom/utils.ts index e8c83694..27190aa9 100644 --- a/packages/system/src/xypanzoom/utils.ts +++ b/packages/system/src/xypanzoom/utils.ts @@ -6,9 +6,6 @@ import { transition } from 'd3-transition'; import { type D3SelectionInstance, type Viewport } from '../types'; import { isMacOs } from '../utils'; -export const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean => - prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k; - export const transformToViewport = (transform: ZoomTransform): Viewport => ({ x: transform.x, y: transform.y, diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index e6aace30..713d14e3 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -104,6 +104,18 @@ function nodeToChildExtent(child: NodeBase, parent: NodeBase, nodeOrigin: NodeOr export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: XYResizerParams): XYResizerInstance { const selection = select(domNode); + let params = { + controlDirection: getControlDirection('bottom-right'), + boundaries: { + minWidth: 0, + minHeight: 0, + maxWidth: Number.MAX_VALUE, + maxHeight: Number.MAX_VALUE, + }, + resizeDirection: undefined as ResizeControlDirection | undefined, + keepAspectRatio: false, + }; + function update({ controlPosition, boundaries, @@ -117,7 +129,12 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X let prevValues = { ...initPrevValues }; let startValues = { ...initStartValues }; - const controlDirection = getControlDirection(controlPosition); + params = { + boundaries, + resizeDirection, + keepAspectRatio, + controlDirection: getControlDirection(controlPosition), + }; let node: InternalNodeBase | undefined = undefined; let containerBounds: DOMRect | null = null; @@ -125,6 +142,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X let parentNode: InternalNodeBase | undefined = undefined; // Needed to fix expandParent let parentExtent: CoordinateExtent | undefined = undefined; let childExtent: CoordinateExtent | undefined = undefined; + // we only want to trigger onResizeEnd if onResize was actually called + let resizeDetected = false; const dragHandler = drag() .on('start', (event: ResizeDragEvent) => { @@ -210,16 +229,17 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X if (!node) { return; } + const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues; const change: XYResizerChange = {}; const nodeOrigin = node.origin ?? storeNodeOrigin; const { width, height, x, y } = getDimensionsAfterResize( startValues, - controlDirection, + params.controlDirection, pointerPosition, - boundaries, - keepAspectRatio, + params.boundaries, + params.keepAspectRatio, nodeOrigin, parentExtent, childExtent @@ -262,9 +282,13 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X if (isWidthChange || isHeightChange) { change.width = - isWidthChange && (!resizeDirection || resizeDirection === 'horizontal') ? width : prevValues.width; + isWidthChange && (!params.resizeDirection || params.resizeDirection === 'horizontal') + ? width + : prevValues.width; change.height = - isHeightChange && (!resizeDirection || resizeDirection === 'vertical') ? height : prevValues.height; + isHeightChange && (!params.resizeDirection || params.resizeDirection === 'vertical') + ? height + : prevValues.height; prevValues.width = change.width; prevValues.height = change.height; } @@ -289,8 +313,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X prevWidth, height: prevValues.height, prevHeight, - affectsX: controlDirection.affectsX, - affectsY: controlDirection.affectsY, + affectsX: params.controlDirection.affectsX, + affectsY: params.controlDirection.affectsY, }); const nextValues = { ...prevValues, direction }; @@ -300,13 +324,20 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X if (callResize === false) { return; } + resizeDetected = true; onResize?.(event, nextValues); onChange(change, childChanges); }) .on('end', (event: ResizeDragEvent) => { + if (!resizeDetected) { + return; + } + onResizeEnd?.(event, { ...prevValues }); onEnd?.({ ...prevValues }); + + resizeDetected = false; }); selection.call(dragHandler); } diff --git a/packages/system/src/xyresizer/types.ts b/packages/system/src/xyresizer/types.ts index 1d16eb3a..92bde6fd 100644 --- a/packages/system/src/xyresizer/types.ts +++ b/packages/system/src/xyresizer/types.ts @@ -1,5 +1,9 @@ import type { D3DragEvent, SubjectPosition } from 'd3-drag'; +/** + * @public + * @inline + */ export type ResizeParams = { x: number; y: number; @@ -15,6 +19,7 @@ export type ResizeParamsWithDirection = ResizeParams & { * Used to determine the control line position of the NodeResizer * * @public + * @inline */ export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right'; @@ -22,6 +27,7 @@ export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right'; * Used to determine the control position of the NodeResizer * * @public + * @inline */ export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; @@ -38,6 +44,7 @@ export enum ResizeControlVariant { /** * The direction the user can resize the node. * @public + * @inline */ export type ResizeControlDirection = 'horizontal' | 'vertical'; @@ -47,6 +54,12 @@ export const XY_RESIZER_LINE_POSITIONS: ControlLinePosition[] = ['top', 'right', type OnResizeHandler = (event: ResizeDragEvent, params: Params) => Result; export type ResizeDragEvent = D3DragEvent; +/** + * Callback to determine if node should resize + * + * @inline + * @public + */ export type ShouldResize = OnResizeHandler; export type OnResizeStart = OnResizeHandler; export type OnResize = OnResizeHandler; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41555ca1..0fed4399 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,7 +52,7 @@ importers: version: 4.3.0(@types/node@20.14.6)(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(terser@5.31.0) '@astrojs/svelte': specifier: ^7.1.0 - version: 7.1.0(@types/node@20.14.6)(astro@5.9.2(@types/node@20.14.6)(rollup@4.39.0)(terser@5.31.0)(typescript@5.8.3))(svelte@5.33.18)(terser@5.31.0)(typescript@5.8.3) + version: 7.1.0(@types/node@20.14.6)(astro@5.13.5(@types/node@20.14.6)(rollup@4.39.0)(terser@5.31.0)(typescript@5.8.3))(svelte@5.38.7)(terser@5.31.0)(typescript@5.8.3) '@types/react': specifier: ^18.2.24 version: 18.2.24 @@ -66,8 +66,8 @@ importers: specifier: workspace:^ version: link:../../packages/svelte astro: - specifier: ^5.9.2 - version: 5.9.2(@types/node@20.14.6)(rollup@4.39.0)(terser@5.31.0)(typescript@5.8.3) + specifier: ^5.13.5 + version: 5.13.5(@types/node@20.14.6)(rollup@4.39.0)(terser@5.31.0)(typescript@5.8.3) react: specifier: ^18.2.0 version: 18.2.0 @@ -75,8 +75,8 @@ importers: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) svelte: - specifier: ^5.33.18 - version: 5.33.18 + specifier: ^5.38.7 + version: 5.38.7 examples/react: dependencies: @@ -508,11 +508,11 @@ packages: '@astrojs/compiler@2.12.2': resolution: {integrity: sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw==} - '@astrojs/internal-helpers@0.6.1': - resolution: {integrity: sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A==} + '@astrojs/internal-helpers@0.7.2': + resolution: {integrity: sha512-KCkCqR3Goym79soqEtbtLzJfqhTWMyVaizUi35FLzgGSzBotSw8DB1qwsu7U96ihOJgYhDk2nVPz+3LnXPeX6g==} - '@astrojs/markdown-remark@6.3.2': - resolution: {integrity: sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q==} + '@astrojs/markdown-remark@6.3.6': + resolution: {integrity: sha512-bwylYktCTsLMVoCOEHbn2GSUA3c5KT/qilekBKA3CBng0bo1TYjNZPr761vxumRk9kJGqTOtU+fgCAp5Vwokug==} '@astrojs/prism@3.3.0': resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} @@ -1441,6 +1441,9 @@ packages: resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.1': resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} @@ -2319,8 +2322,8 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - astro@5.9.2: - resolution: {integrity: sha512-K/zZlQOWMpamfLDOls5jvG7lrsjH1gkk3ESRZyZDCkVBtKHMF4LbjwCicm/iBb3mX3V/PerqRYzLbOy3/4JLCQ==} + astro@5.13.5: + resolution: {integrity: sha512-XmBzkl13XU97+n/QiOM5uXQdAVe0yKt5gO+Wlgc8dHRwHR499qhMQ5sMFckLJweUINLzcNGjP3F5nG4wV8a2XA==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true @@ -3274,6 +3277,9 @@ packages: esrap@1.4.9: resolution: {integrity: sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==} + esrap@2.1.0: + resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -5959,14 +5965,14 @@ packages: resolution: {integrity: sha512-Uai13Ydt1ZE+bUHme6b9U38PCYVNCqBRoBMkUKbFbKiD7kHWjdUUrklYAQZJxyKK81qII4mrBwe/YmvEMSlC9w==} engines: {node: '>=18'} - svelte@5.33.18: - resolution: {integrity: sha512-GVAhi8vi8pGne/wlEdnfWIJvSR9eKvEknxjfL5Sr8gQALiyk8Ey+H0lhUYLpjW+MrqgH9h4dgh2NF6/BTFprRg==} - engines: {node: '>=18'} - svelte@5.33.19: resolution: {integrity: sha512-udmgc1nnGeAgnfVJjOMfSOAqPAKv8N65MWN2kDuxo6BZthTaUcsLh4vP8bdZC0bMXLGn69smSZXgQOeuZBOn4Q==} engines: {node: '>=18'} + svelte@5.38.7: + resolution: {integrity: sha512-1ld9TPZSdUS3EtYGQzisU2nhwXoIzNQcZ71IOU9fEmltaUofQnVfW5CQuhgM/zFsZ43arZXS1BRKi0MYgUV91w==} + engines: {node: '>=18'} + svgo@3.0.2: resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==} engines: {node: '>=14.0.0'} @@ -6713,11 +6719,11 @@ snapshots: '@astrojs/compiler@2.12.2': {} - '@astrojs/internal-helpers@0.6.1': {} + '@astrojs/internal-helpers@0.7.2': {} - '@astrojs/markdown-remark@6.3.2': + '@astrojs/markdown-remark@6.3.6': dependencies: - '@astrojs/internal-helpers': 0.6.1 + '@astrojs/internal-helpers': 0.7.2 '@astrojs/prism': 3.3.0 github-slugger: 2.0.0 hast-util-from-html: 2.0.3 @@ -6768,12 +6774,12 @@ snapshots: - tsx - yaml - '@astrojs/svelte@7.1.0(@types/node@20.14.6)(astro@5.9.2(@types/node@20.14.6)(rollup@4.39.0)(terser@5.31.0)(typescript@5.8.3))(svelte@5.33.18)(terser@5.31.0)(typescript@5.8.3)': + '@astrojs/svelte@7.1.0(@types/node@20.14.6)(astro@5.13.5(@types/node@20.14.6)(rollup@4.39.0)(terser@5.31.0)(typescript@5.8.3))(svelte@5.38.7)(terser@5.31.0)(typescript@5.8.3)': dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.33.18)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)) - astro: 5.9.2(@types/node@20.14.6)(rollup@4.39.0)(terser@5.31.0)(typescript@5.8.3) - svelte: 5.33.18 - svelte2tsx: 0.7.39(svelte@5.33.18)(typescript@5.8.3) + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.38.7)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)) + astro: 5.13.5(@types/node@20.14.6)(rollup@4.39.0)(terser@5.31.0)(typescript@5.8.3) + svelte: 5.38.7 + svelte2tsx: 0.7.39(svelte@5.38.7)(typescript@5.8.3) typescript: 5.8.3 vite: 6.3.5(@types/node@20.14.6)(terser@5.31.0) transitivePeerDependencies: @@ -6793,7 +6799,7 @@ snapshots: '@astrojs/telemetry@3.3.0': dependencies: ci-info: 4.2.0 - debug: 4.4.0 + debug: 4.4.1 dlv: 1.1.3 dset: 3.1.4 is-docker: 3.0.0 @@ -7803,6 +7809,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/resolve-uri@3.1.1': {} '@jridgewell/resolve-uri@3.1.2': {} @@ -8211,11 +8222,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.33.18)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)))(svelte@5.33.18)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.38.7)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)))(svelte@5.38.7)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.33.18)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)) + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.38.7)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)) debug: 4.4.0 - svelte: 5.33.18 + svelte: 5.38.7 vite: 6.3.5(@types/node@20.14.6)(terser@5.31.0) transitivePeerDependencies: - supports-color @@ -8242,14 +8253,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.33.18)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0))': + '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.38.7)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.33.18)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)))(svelte@5.33.18)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.38.7)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)))(svelte@5.38.7)(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 - svelte: 5.33.18 + svelte: 5.38.7 vite: 6.3.5(@types/node@20.14.6)(terser@5.31.0) vitefu: 1.0.4(vite@6.3.5(@types/node@20.14.6)(terser@5.31.0)) transitivePeerDependencies: @@ -8871,11 +8882,11 @@ snapshots: astral-regex@2.0.0: {} - astro@5.9.2(@types/node@20.14.6)(rollup@4.39.0)(terser@5.31.0)(typescript@5.8.3): + astro@5.13.5(@types/node@20.14.6)(rollup@4.39.0)(terser@5.31.0)(typescript@5.8.3): dependencies: '@astrojs/compiler': 2.12.2 - '@astrojs/internal-helpers': 0.6.1 - '@astrojs/markdown-remark': 6.3.2 + '@astrojs/internal-helpers': 0.7.2 + '@astrojs/markdown-remark': 6.3.6 '@astrojs/telemetry': 3.3.0 '@capsizecss/unpack': 2.4.0 '@oslojs/encoding': 1.1.0 @@ -8889,7 +8900,7 @@ snapshots: common-ancestor-path: 1.0.1 cookie: 1.0.2 cssesc: 3.0.0 - debug: 4.4.0 + debug: 4.4.1 deterministic-object-hash: 2.0.2 devalue: 5.1.1 diff: 5.2.0 @@ -8918,8 +8929,9 @@ snapshots: rehype: 13.0.2 semver: 7.7.2 shiki: 3.6.0 + smol-toml: 1.3.4 tinyexec: 0.3.2 - tinyglobby: 0.2.12 + tinyglobby: 0.2.14 tsconfck: 3.1.6(typescript@5.8.3) ultrahtml: 1.6.0 unifont: 0.5.0 @@ -10254,6 +10266,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + esrap@2.1.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -11617,7 +11633,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.10 - debug: 4.4.0 + debug: 4.4.1 decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -13335,11 +13351,11 @@ snapshots: svelte: 5.33.19 typescript: 5.8.3 - svelte2tsx@0.7.39(svelte@5.33.18)(typescript@5.8.3): + svelte2tsx@0.7.39(svelte@5.38.7)(typescript@5.8.3): dependencies: dedent-js: 1.0.1 pascal-case: 3.1.2 - svelte: 5.33.18 + svelte: 5.38.7 typescript: 5.8.3 svelte@5.27.0: @@ -13359,23 +13375,6 @@ snapshots: magic-string: 0.30.17 zimmerframe: 1.1.2 - svelte@5.33.18: - dependencies: - '@ampproject/remapping': 2.3.0 - '@jridgewell/sourcemap-codec': 1.5.0 - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.0) - '@types/estree': 1.0.7 - acorn: 8.14.0 - aria-query: 5.3.2 - axobject-query: 4.1.0 - clsx: 2.1.1 - esm-env: 1.2.2 - esrap: 1.4.9 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.17 - zimmerframe: 1.1.2 - svelte@5.33.19: dependencies: '@ampproject/remapping': 2.3.0 @@ -13393,6 +13392,23 @@ snapshots: magic-string: 0.30.17 zimmerframe: 1.1.2 + svelte@5.38.7: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.0 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@types/estree': 1.0.7 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + esm-env: 1.2.2 + esrap: 2.1.0 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.17 + zimmerframe: 1.1.2 + svgo@3.0.2: dependencies: '@trysound/sax': 0.2.0