Compare commits

...

28 Commits

Author SHA1 Message Date
632b7dc078 docs: add some config for node-tool (#3416) 2023-03-23 09:37:55 +08:00
2b651274a3 chore: update contributors [skip ci] 2023-03-22 14:49:39 +00:00
8f20c66b1d fix: parseint zindex to get correct max zindex node (#3413) 2023-03-22 22:49:13 +08:00
7161d1432d chore: change workflow id names (#3406)
* chore: change workflow id names

* chore: upgrade codecov to v3

* chore: remove matrix in ci
2023-03-22 16:24:11 +08:00
423ddeda73 chore: update CONTRIBUTORS [skip ci] 2023-03-19 08:54:24 +00:00
2efa843e07 chore: update contributors [skip ci] 2023-03-19 08:47:28 +00:00
5d4c5991cc fix: x6 common deps (#3404) 2023-03-19 16:47:06 +08:00
2083c74bd5 chore: fix lint error (#3403)
* chore: fix lint error

* chore: fix style lint error

* chore: remove turbo token env for ci
2023-03-19 16:46:25 +08:00
44a5cfdbc3 chore: remove some workflows (#3400) 2023-03-19 09:24:04 +08:00
cbae927138 chore: change pnpm registry (#3401) 2023-03-19 09:19:09 +08:00
adc06ada96 fix: add timeout to wait target node is connected (#3392) 2023-03-17 10:38:50 +08:00
96ed42b5eb chore: update contributors [skip ci] 2023-03-16 08:37:56 +00:00
4b1a0f8b31 feat: createWidget and clearWidget (#3337)
* feat: createWidget and clearWidget

* feat: createWidget and clearWidget

* feat: 手动触发transofrom和clear

* chore: code optimize

* chore: code optimize
2023-03-16 16:37:31 +08:00
ac71e239cf chore: remove corejs deps (#3390)
* docs: update warnning for x6-react-shape

* chore: remove corejs deps
2023-03-16 11:19:07 +08:00
f4afde62d1 chore: update contributors [skip ci] 2023-03-16 03:18:31 +00:00
a277a8995d docs: 补充 view:mounted 和 view:unmounted 文档 (#3385) 2023-03-16 11:18:08 +08:00
e93021bfb7 docs: update warnning for x6-react-shape (#3388) 2023-03-16 11:06:21 +08:00
c1dedfa515 chore: update contributors [skip ci] 2023-03-16 01:29:05 +00:00
baabd328b6 chore: update contributors [skip ci] 2023-03-15 03:20:01 +00:00
37515c45c8 fix: add dependencies for x6-devtools (#3381)
* fix: add dependencies for x6-devtools

* docs: add tips for react-shape
2023-03-15 11:19:37 +08:00
efc45f6f01 chore: update CONTRIBUTORS [skip ci] 2023-03-15 03:13:38 +00:00
be8f734aa6 chore: release 3 packages (#3377)
@antv/x6-react-shape@2.1.1
@antv/x6-vue-shape@2.0.10
@antv/x62.5.5
2023-03-15 11:08:25 +08:00
68e3ef1535 feat: devtool show plugin (#3373)
* feat: add show plugin

* feat: add show plugin

* feat: add show plugin
2023-03-15 11:07:04 +08:00
6cb7a60e5d Feature devtool (#3363)
* feat: add x6-devtool

* feat: add x6-devtool

* feat: add x6-devtool

* chore: update prettierignore

* feat: calc inspect div location

* feat: change icon

* feat: modify value for node/edge
2023-03-09 22:47:49 +08:00
943c06b272 refactor: using Object.values instead of Object.keys (#3352)
* refactor: using Object.values instead of Object.keys

* refactor: using Object.values instead of Object.keys
2023-03-09 22:44:27 +08:00
8b1f661585 fix: confirm viewitem exist (#3357) 2023-03-07 16:06:37 +08:00
d72ff80be7 fix: hotfix #3351 error when call resetViews (#3356) 2023-03-07 15:47:49 +08:00
1a22248945 chore: release 1 packages (x6-angular-shape) (#3348) 2023-03-06 15:23:27 +08:00
91 changed files with 36870 additions and 31890 deletions

View File

@ -1,19 +0,0 @@
name: 🧑 Auto Assign
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: wow-actions/use-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- uses: wow-actions/auto-assign@v1
with:
GITHUB_TOKEN: ${{ env.BOT_TOKEN }}
CONFIG_FILE: .github/workflows/config/auto-assign.yml

View File

@ -1,48 +0,0 @@
name: 🚫 Delete Stale Releases
# on:
# repository_dispatch:
# types: [released]
# jobs:
# clean:
# runs-on: ubuntu-latest
# steps:
# - uses: wow-actions/use-app-token@v2
# with:
# app_id: ${{ secrets.APP_ID }}
# private_key: ${{ secrets.PRIVATE_KEY }}
#
# - uses: wow-actions/delete-stale-releases@v1
# with:
# GITHUB_TOKEN: ${{ env.BOT_TOKEN }}
# delete_tags: true
# keep_latest_count: 3
# group: '(?!^)@.*$'
# exclude: |
# @antv/x6@**
# @antv/x6-common@**
# @antv/x6-geometry@**
# @antv/x6-plugin-**@**
# @antv/x6-vue-shape@**
# @antv/x6-react-shape@**
# @antv/x6-angular-shape@**
# @antv/x6-react-components@**
# delete all releases and tag
on:
push:
branches:
- master
jobs:
clean:
runs-on: ubuntu-latest
steps:
- uses: wow-actions/use-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- uses: wow-actions/delete-stale-releases@v1
with:
GITHUB_TOKEN: ${{ env.BOT_TOKEN }}
delete_tags: true
keep_latest_count: -1

View File

@ -1,21 +0,0 @@
name: 🥤 GitLeaks
on:
pull_request:
push:
branches:
- master
- alpha
- beta
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: '1'
- name: wget
uses: wei/wget@v1
with:
args: -O .gitleaks.toml https://raw.githubusercontent.com/ycjcl868/gitleaks/master/.gitleaks.toml
- name: gitleaks-action
uses: zricethezav/gitleaks-action@master

View File

@ -10,16 +10,12 @@ on:
- beta
jobs:
ci:
strategy:
matrix:
codecov: [x6, x6-common, x6-geometry]
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
steps:
- name:  Checkout
uses: actions/checkout@v3
with:
persist-credentials: false
- name: 🎉 Setup nodejs
uses: actions/setup-node@v3
@ -63,11 +59,11 @@ jobs:
- name: 💡 Codecov
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/${{ matrix.codecov }}/test/coverage/lcov.info
flags: ${{ matrix.codecov }}
files: ./packages/x6/test/coverage/lcov.info
flags: x6
- name: 🔀 Dispatch(ci_passed)
uses: peter-evans/repository-dispatch@v2

View File

@ -1,42 +0,0 @@
name:  CodeQL
on:
push:
branches: [ "master", "bot", "gh-pages", "v1" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: "41 2 * * 1"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ javascript ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
config-file: ./.github/workflows/config/codeql.yml
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"

View File

@ -1,7 +0,0 @@
assignees:
- NewByVector
reviewers:
- bubkoo
- NewByVector
skipKeywords:
- wip

View File

@ -1,7 +0,0 @@
paths-ignore:
- sites/public
query-filters:
- exclude:
id: js/use-before-declaration
- exclude:
id: js/polynomial-redos

View File

@ -1,16 +0,0 @@
name: 🚧 Create Issue Branch
on:
issue_comment:
types: [created]
jobs:
cib:
runs-on: ubuntu-latest
steps:
- uses: wow-actions/use-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- uses: robvanderleek/create-issue-branch@main
env:
GITHUB_TOKEN: ${{ env.BOT_TOKEN }}

View File

@ -1,78 +0,0 @@
name: 🚀 Deploy Sites
on:
repository_dispatch:
types: [released]
jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- name:  Checkout
uses: actions/checkout@v3
- name: 🎉 Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: 🌱 Get Yarn Cache Directory
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: 🚸 Setup Cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
## cache webpack(babel-loader, eslint-loader)
- name: 💩 Setup Webpack Cache
uses: actions/cache@v2
with:
path: |
node_modules
sites/x6-sites-demos/packages/**/node_modules
key: ${{ runner.os }}-webpack-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-webpack-
## cache sites
- name: 💩 Setup Sites Cache
uses: actions/cache@v2
with:
path: sites/x6-sites/static/demos
key: ${{ runner.os }}-sites-${{ hashFiles('./packages/x6/package.json', './sites/x6-sites-demos/**/src') }}
restore-keys: |
${{ runner.os }}-sites-
- name: 🚧 Prepare
run: yarn global add lerna
- name: 🚀 Bootstrap
run: yarn bootstrap
- name: 🧲 Build Apps
run: yarn build:apps
- name: 📦 Build Demos
run: yarn build:demos
- name:  Build Sites
run: yarn build:sites
- name: 🔑 Generate Token
uses: wow-actions/use-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- name:  Deploy sites
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ env.BOT_TOKEN }}
publish_dir: ./sites/x6-sites/public
publish_branch: gh-pages

View File

@ -17,4 +17,4 @@ jobs:
- uses: wow-actions/pr-triage@v1
with:
GITHUB_TOKEN: ${{ env.BOT_TOKEN }}
WORKFLOW-ID: ${{ github.event.workflow_run.id }}
WORKFLOW_ID: ${{ github.event.workflow_run.id }}

View File

@ -1,85 +0,0 @@
name: 🔂 Surge PR Preview
on:
pull_request_target:
paths:
- sites/x6-sites/**
- sites/x6-sites-demos/**
- sites/x6-sites-demos-helper/**
- examples/x6-app-**
jobs:
surge:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- name: 🌱 Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: 🚸 Setup yarn cacha
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
## cache webpack(babel-loader, eslint-loader)
- name: 💩 Setup webpack cache
uses: actions/cache@v2
with:
path: |
node_modules
sites/x6-sites-demos/packages/**/node_modules
key: ${{ runner.os }}-webpack-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-webpack-
## cache sites
- name: 💩 Setup sites cache
uses: actions/cache@v2
with:
path: sites/x6-sites/static/demos
key: ${{ runner.os }}-sites-${{ hashFiles('./packages/x6/package.json', './sites/x6-sites-demos/**/src') }}
restore-keys: |
${{ runner.os }}-sites-
- name: 🎉 Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: 🚧 Prepare Environment
run: |
yarn global add lerna
- name: 🚀 Bootstrap
run: yarn bootstrap
- name: 📦 Build Demos
run: yarn build:demos
- name: 🧲 Build Apps
run: yarn build:apps
- name:  Build Sites
run: yarn build:sites
- name: 🔑 Generate Token
uses: wow-actions/use-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- name:  Deploy Sites
uses: afc163/surge-preview@v1
with:
surge_token: ${{ secrets.SURGE_TOKEN }}
github_token: ${{ env.BOT_TOKEN }}
build: |
echo Create sites preview
dist: sites/x6-sites/public

View File

@ -1,16 +0,0 @@
name: 📆 Monthly Report
on:
schedule:
- cron: '0 3 1 * *'
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: wow-actions/use-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- uses: wow-actions/activity-report@v1
with:
GITHUB_TOKEN: ${{ env.BOT_TOKEN }}

View File

@ -1,16 +0,0 @@
name: 📆 Weekly Report
on:
schedule:
- cron: '30 17 * * 5'
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: wow-actions/use-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- uses: wow-actions/activity-report@v1
with:
GITHUB_TOKEN: ${{ env.BOT_TOKEN }}

View File

@ -1,58 +0,0 @@
name: 👻 Stale
on:
schedule:
- cron: "0 0 * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: wow-actions/use-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- uses: actions/stale@v3
with:
repo-token: ${{ env.BOT_TOKEN }}
stale-issue-message: |
Hiya!
This issue has gone quiet. Spooky quiet. 👻
We get a lot of issues, so we currently close issues after 60 days of inactivity. Its been at least 20 days since the last update here. If we missed this issue or if you want to keep it open, please reply here. You can also add the label "not-stale" to keep this issue open!
As a friendly reminder: the best way to see this issue, or any other, fixed is to open a Pull Request. Check out [contribute](https://github.com/antvis/X6/blob/master/CONTRIBUTING.md) for more information about opening PRs, triaging issues, and contributing!
Thanks for being a part of the AntV community! 💪💯
close-issue-message: |
Hey again!
Its been 60 days since anything happened on this issue, so our friendly neighborhood robot (thats me!) is going to close it. Please keep in mind that Im only a robot 🤖, so if Ive closed this issue in error, Im `HUMAN_EMOTION_SORRY`. Please feel free to comment on this issue or create a new one if you need anything else.
As a friendly reminder: the best way to see this issue, or any other, fixed is to open a Pull Request. Check out [contribute](https://github.com/antvis/X6/blob/master/CONTRIBUTING.md) for more information about opening PRs, triaging issues, and contributing!
Thanks again for being part of the AntV community! 💪💯
stale-pr-message: |
Hiya!
This PR has gone quiet. Spooky quiet. 👻
We get a lot of PRs, so we currently close PRs after 60 days of inactivity. Its been at least 20 days since the last update here. If we missed this PR or if you want to keep it open, please reply here. You can also add the label "not-stale" to keep this PR open!
Thanks for being a part of the AntV community! 💪💯
close-pr-message: |
Hey again!
Its been 60 days since anything happened on this PR, so our friendly neighborhood robot (thats me!) is going to close it. Please keep in mind that Im only a robot 🤖, so if Ive closed this PR in error, Im `HUMAN_EMOTION_SORRY`. Please feel free to comment on this PR or create a new one if you need anything else.
Thanks again for being part of the AntV community! 💪💯
days-before-stale: 20
days-before-close: 40
stale-issue-label: 'stale'
exempt-issue-label: 'not-stale,awaiting-approval,work-in-progress'
stale-pr-label: 'stale'
exempt-pr-label: 'not-stale,awaiting-approval,work-in-progress'

View File

@ -1,21 +0,0 @@
name: 🔄 Sync Labels
on:
push:
branches:
- master
paths:
- .github/workflows/config/labels.yml
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: wow-actions/use-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- uses: micnncim/action-label-syncer@v1
env:
GITHUB_TOKEN: ${{ env.BOT_TOKEN }}
with:
manifest: .github/workflows/config/labels.yml

View File

@ -1,34 +0,0 @@
# https://github.com/marketplace/actions/gitee-pages-action
# 配置步骤如下
# 1. 在命令行终端或 Git Bash 使用命令 ssh-keygen -t rsa -C "youremail@example.com" 生成 SSH Key注意替换为自己的邮箱。生成的 id_rsa 是私钥id_rsa.pub 是公钥。(⚠️注意此处不要设置密码)
# 2. 在 GitHub 项目的「Settings -> Secrets」路径下配置好命名为 GITEE_RSA_PRIVATE_KEY 和 GITEE_PASSWORD 的两个密钥。其中GITEE_RSA_PRIVATE_KEY 存放 id_rsa 私钥GITEE_PASSWORD 存放 Gitee 帐号的密码。
# 3. 在 GitHub 的个人设置页面「Settings -> SSH and GPG keys」 配置 SSH 公钥id_rsa.pub命名随意。或者在仓库设置页面添加一个部署公钥。
# 4. 在 Gitee 的个人设置页面「安全设置 -> SSH 公钥」​ 配置 SSH 公钥id_rsa.pub命名随意。
name: 🔁 Sync to Gitee
on: [push]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: 🔁 Sync to Gitee
uses: wearerequired/git-mirror-action@master
env:
# 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY
SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}
with:
# 注意替换为你的 GitHub 源仓库地址
source-repo: 'git@github.com:antvis/X6.git'
# 注意替换为你的 Gitee 目标仓库地址
destination-repo: 'git@gitee.com:antv-x6/antv-x6.git'
- name: 📦 Build Gitee Pages
uses: yanglbme/gitee-pages-action@master
with:
# 注意替换为你的 Gitee 用户名
gitee-username: afc163
# 注意在 Settings->Secrets 配置 GITEE_PASSWORD
gitee-password: ${{ secrets.GITEE_PASSWORD }}
# 注意替换为你的 Gitee 仓库
gitee-repo: antv-x6/antv-x6
# 要部署的分支
branch: gh-pages

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
registry=https://registry.npm.taobao.org
strict-peer-dependencies=false

View File

@ -14,3 +14,4 @@ dist/
coverage/
sites/public
csstype.ts
ui.js

View File

@ -12,6 +12,8 @@ HQidea <HQidea@users.noreply.github.com>
ImgBotApp <ImgBotHelp@gmail.com>
Indomi <indomi126@gmail.com>
James Tsang <wtzeng1@gmail.com>
JasonSun <42314340+LolipopJ@users.noreply.github.com>
Jinxing Lin <172601673@qq.com>
Jógvan Olsen <jogvanolsen@hotmail.com>
Ken Geis <geis.ken@gmail.com>
Kent Wood <minzojian@hotmail.com>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 MiB

After

Width:  |  Height:  |  Size: 14 MiB

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Graph, Cell, Point, Timing, Interp } from '@antv/x6'
import { Graph, Cell, Point, Timing, Interp } from '@antv/x6'
import '../index.less'
export default class Example extends React.Component {

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Graph, Cell, Node } from '@antv/x6'
import { Graph, Node } from '@antv/x6'
import { connectors } from '../connector/xmind-definitions'
import Hierarchy from '@antv/hierarchy'
import { Selection } from '@antv/x6-plugin-selection'
@ -220,7 +220,7 @@ export default class Example extends React.Component {
const node = graph.getCellById(data.id)
node.prop('position', { x: hierarchyItem.x, y: hierarchyItem.y })
} else {
const node = graph.addNode({
graph.addNode({
id: data.id,
shape: data.type === 'topic-child' ? 'topic-child' : 'topic',
x: hierarchyItem.x,

View File

@ -1,4 +1,4 @@
import { Shape, Path, Point, NumberExt, JSONObject } from '@antv/x6'
import { Shape, Path, Point, NumberExt, JSONObject } from '@antv/x6'
interface KnobsAttrValue extends JSONObject {
round: boolean | string | number

View File

@ -1,3 +1,5 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable jsx-a11y/anchor-has-content */
import React from 'react'
import ReactDom from 'react-dom'
import { Menu, Dropdown } from 'antd'

View File

@ -14,14 +14,15 @@
"build:esm": "turbo run build:esm --filter=./packages/*",
"build:dev": "turbo run build:dev --filter=./packages/*",
"build:umd": "turbo run build:umd --filter=./packages/*",
"build:demos": "sh ./scripts/build-demos",
"clean:turbo": "pnpm -r --if-present --parallel --filter=./packages/* run clean:turbo",
"clean:build": "pnpm -r --if-present --parallel --filter=./packages/* run clean",
"clean:modules": "pnpm -r --parallel exec rimraf node_modules && rimraf node_modules",
"clean": "run-s clean:build clean:turbo clean:modules",
"update:deps": "pnpm update --interactive --latest --recursive",
"setup:husky": "husky install .husky",
"prepare": "is-ci || run-p setup:husky build:dev"
"prepare": "is-ci || run-p setup:husky build:dev",
"start:demo": "cd ./examples/x6-example-features && pnpm start",
"start:site": "cd ./sites/x6-sites && pnpm start"
},
"rss": {
"clean:turbo": "rimraf .turbo",
@ -159,7 +160,8 @@
"ts-node": "^10.2.1",
"tslib": "^2.4.1",
"turbo": "^1.6.3",
"typescript": "^4.9.3"
"typescript": "^4.9.3",
"cross-spawn": "^7.0.3"
},
"devDependencies": {
"@rollup/plugin-terser": "^0.2.0"

View File

@ -43,7 +43,8 @@
"@angular/core": ">= 14"
},
"devDependencies": {
"@antv/x6": "^2.x"
"@antv/x6": "^2.x",
"@angular/core": ">= 14"
},
"author": {
"name": "Eve-Sama",

7
packages/x6-devtool/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules
yarn.lock
.vscode
devtools.crx
devtools.pem
.DS_Store
.vscode

View File

@ -0,0 +1,49 @@
# X6 Devtool
> A devtool for @antv/x6 in chrome, it's still WIP, you can load it in unpack way;
## Quick Start
### Import unpacked plugin
![image](https://user-images.githubusercontent.com/15213473/150081309-61f9c451-c35e-4dab-a23c-ed5e425e7ec5.png)
1. Open the Extension Management page by navigating to chrome://extensions.
2. Enable Developer Mode by clicking the toggle switch next to Developer mode.
3. Click the Load unpacked button and select the 'devtool' directory.
### Connect with X6 Graph;
#### In X6
```javascript
// init window hook
window.__x6_instances__ = [];
var graph = new Graph({...blablabla});
window.__x6_instances__.push(graph);
```
### Using devtool
After these steps, the tab 'AntV X6' should show in devtools' tab, select it and choose a canvas
![image](https://user-images.githubusercontent.com/1826685/223881189-bf99f2ad-5158-4c39-8d16-018a287ac2fe.png)
## Features
### Inspect Element in Graph
![image](https://user-images.githubusercontent.com/1826685/223881189-bf99f2ad-5158-4c39-8d16-018a287ac2fe.png)
### View and Modify Attributes of Element
![image](https://user-images.githubusercontent.com/1826685/223881189-bf99f2ad-5158-4c39-8d16-018a287ac2fe.png)
### Using select element directly in graph
![image](https://user-images.githubusercontent.com/1826685/223881189-bf99f2ad-5158-4c39-8d16-018a287ac2fe.png)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,19 @@
{
"name": "AntV X6 Dev Tool",
"description": "devtool for x6 graph",
"version": "0.0.1",
"devtools_page": "x6_devtools.html",
"minimum_chrome_version": "49",
"manifest_version": 2,
"permissions": ["file:///*", "http://*/*", "https://*/*"],
"icons": {
"16": "icons/16.png",
"32": "icons/32.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"web_accessible_resources": ["main.html", "panel.html", "script/backend.js"],
"browser_action": {}
}

View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html style="display: flex">
<head>
<meta charset="utf8" />
<style>
html {
display: flex;
}
body {
margin: 0;
padding: 0;
flex: 1;
display: flex;
}
#container {
display: block;
flex: 1;
width: 100%;
position: fixed;
overflow: auto;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#desc {
width: 240px;
padding-left: 12px;
height: 100%;
position: fixed;
right: 0;
top: 0;
overflow: auto;
background: white;
border-left: 1px solid #eee;
}
.tag.selected {
background: rgba(135, 59, 244, 0.5);
}
.tag {
background: rgba(128, 128, 128, 0.5);
padding-left: 8px;
}
.tag.selected::after {
display: inline-block;
opacity: 0.4;
font-size: 12px;
content: '$gElement';
}
</style>
</head>
<body>
<div id="container"></div>
<script src="./ui/ui.js"></script>
<script src="./script/panel.js"></script>
</body>
</html>

View File

@ -0,0 +1,20 @@
chrome.runtime.onMessage.addListener((req, sender) => {
if (req.isAntVX6 && sender && sender.tab) {
if (req.disabled) {
chrome.browserAction.setIcon({
tabId: sender.tab.id,
path: 'icons/48-disabled.png',
})
} else {
chrome.browserAction.setIcon({
tabId: sender.tab.id,
path: {
16: 'icons/16.png',
32: 'icons/32.png',
48: 'icons/48.png',
128: 'icons/128.png',
},
})
}
}
})

View File

@ -0,0 +1,52 @@
/**
* global instance and flag
*/
var panelInstance
var itv
function createPanelInstance() {
if (panelInstance) {
return
}
chrome.devtools.inspectedWindow.eval(
`!!(window.__x6_instances__ && window.__x6_instances__.length)`,
function (connected, err) {
if (!connected) {
return
}
clearInterval(itv)
panelInstance = chrome.devtools.panels.create(
'AntV X6',
'icons/32.png',
'panel.html',
function (panel) {
panel.onHidden.addListener(function () {
chrome.devtools.inspectedWindow.eval(`(function() {
var elements = document.getElementsByClassName('g_devtool_rect');
[].forEach.apply(elements, [function (e) {
e.remove();
}])
})()`)
})
},
)
chrome.runtime.sendMessage({
isAntVX6: true,
disabled: false,
})
},
)
}
chrome.devtools.network.onNavigated.addListener(function () {
// createPanelIfReactLoaded();
})
createPanelInstance()
itv = setInterval(createPanelInstance, 1000)

View File

@ -0,0 +1,281 @@
//
// script execute function
//
// execute raw script in inspect window
var executeScriptInInspectWindow = function (script) {
return new Promise(function (resolve, reject) {
chrome.devtools.inspectedWindow.eval(script, function (result, exception) {
if (exception) {
console.error('eval error', script)
reject(exception)
} else {
resolve(result)
}
})
})
}
// execute function in anynomous code block
var executeFuntionInInspectWindow = function (func, args) {
return executeScriptInInspectWindow(
`(${func.toString()}).apply(window, ${JSON.stringify(args)})`,
)
}
//
// these are the function that execute in inspect window
//
function doFPSThings() {
if (window.__x6_fps) {
cancelAnimationFrame(window.__x6_fps)
}
let lastCalledTime
let fps
let delta
function requestAnimFrame() {
if (!lastCalledTime) {
lastCalledTime = performance.now()
fps = 0
} else {
delta = (performance.now() - lastCalledTime) / 1000
lastCalledTime = performance.now()
fps = 1 / delta
}
window.__x6_fps_value = Math.round(fps)
window.__x6_fps = requestAnimationFrame(requestAnimFrame)
}
requestAnimFrame()
}
// get global X6 Graph structure
function getGlobalInstances() {
var instances = window.__x6_instances__
var gmap = {}
var getGraphCells = function (graph) {
// 使用model中的引用直接获取元素
return graph.model.collection.cells
}
window.__x6_instances__.globalMap = gmap
var gInfo = []
function getX6Instance(instance) {
const ga = {}
// antv/g 实现了getChildren可以递归获取x6不行
// if (instance.getChildren && instance.getChildren()) {
// ga.children = instance.getChildren().map(function (p) {
// return getX6Instance(p);
// });
// }
if (!instance.__dev_hash) {
ga.hash = Math.random().toString(16).slice(-8)
instance.__dev_hash = ga.hash
} else {
ga.hash = instance.__dev_hash
}
gmap[ga.hash] = instance
ga.id = instance.id
ga.name = instance.name || instance.prop('shape') || instance.prop('label')
ga.type = instance.isEdge() ? 'edge' : 'node'
return ga
}
if (instances && instances.length) {
gInfo = instances.map(function (instance) {
const hash = instance.hash || Math.random().toString(16).slice(-8)
const cells = getGraphCells(instance).map((e) => getX6Instance(e))
const plugins = Array.from(instance.installedPlugins).map((plugin) => {
const hash = plugin.hash || Math.random().toString(16).slice(-8)
const ga = {
name: plugin.name,
hash,
type: 'plugin',
}
plugin.hash = ga.hash
gmap[ga.hash] = plugin
return ga
})
var ga = {
type: 'svg',
name: 'Graph',
nodeName: 'graph',
hash,
children: cells.concat(plugins),
memory: window.performance.memory.usedJSHeapSize,
fps: window.__x6_fps_value,
}
instance.hash = ga.hash
gmap[ga.hash] = instance
return ga
})
} else {
gInfo.length = 0
}
return gInfo
}
function checkGraphByHash(hash) {
return !!window.__x6_instances__.map((e) => e.hash).includes(hash)
}
function createBoxUsingId(bbox, id, color) {
var el = document.createElement('div')
window[id] = el
el.classList.add('x6_devtool_rect')
document.body.appendChild(el)
el.style.position = 'absolute'
el.style.width = `${bbox.width}px`
el.style.height = `${bbox.height}px`
el.style.top = `${bbox.top}px`
el.style.left = `${bbox.left}px`
el.style.background = color || 'rgba(135, 59, 244, 0.5)'
el.style.border = '2px dashed rgb(135, 59, 244)'
el.style.boxSizing = 'border-box'
}
function removeBoxUsingId(id) {
if (window[id]) {
window[id].remove()
}
}
function removeAllBox() {
var elements = document.getElementsByClassName('x6_devtool_rect')
;[].forEach.apply(elements, [
function (e) {
e.remove()
},
])
}
function getElemetBBoxByHash(hash) {
try {
var targetEl = window.__x6_instances__.globalMap[hash]
if (targetEl) {
const bbox = targetEl.getGraphArea
? targetEl.getGraphArea()
: targetEl.getBBox()
const pageBBox = targetEl.model.graph.localToPage(bbox)
const { width, height, x, y, left, top } = pageBBox
return { width, height, x, y, left, top }
}
} catch (e) {}
return {}
}
function getElementAttrByHash(hash) {
const instance = window.__x6_instances__.globalMap[hash]
if (instance) {
return instance.prop
? instance.prop()
: instance.options
? {
...instance.options,
container: undefined,
}
: {}
}
return {}
}
function setElementAttrByHash(hash, name, value) {
const instance = window.__x6_instances__.globalMap[hash]
// graph can not set prop
if (instance && instance.prop) {
instance.prop(name, value)
}
}
function setGElementByHash(hash) {
window.$gElemet = hash ? window.__x6_instances__.globalMap[hash] : undefined
}
function consoleElementByHash(hash, desc) {
window.console.log(
desc || '<Click To Expand>',
window.__x6_instances__.globalMap[hash],
)
}
//
// these are the functions that run in devtools panel
//
function setRect(bbox, id, color) {
executeFuntionInInspectWindow(removeBoxUsingId, [id]).finally(() => {
executeFuntionInInspectWindow(createBoxUsingId, [bbox, id, color])
})
}
function cleanRect(id) {
executeFuntionInInspectWindow(removeBoxUsingId, [id])
}
function showRect(hash, id, color) {
executeFuntionInInspectWindow(getElemetBBoxByHash, [hash]).then((bbox) => {
setRect(bbox, id, color)
})
}
function cleanAllRect() {
executeFuntionInInspectWindow(removeAllBox)
}
function getAttrs(hash) {
if (hash) {
executeFuntionInInspectWindow(setGElementByHash, [hash])
return executeFuntionInInspectWindow(getElementAttrByHash, [hash])
}
return executeFuntionInInspectWindow(setGElementByHash, [])
}
function updateAttrs(hash, name, attrs) {
return executeFuntionInInspectWindow(setElementAttrByHash, [
hash,
name,
attrs,
])
}
function consoleEl(hash, desc) {
return executeFuntionInInspectWindow(consoleElementByHash, [hash, desc])
}
function checkGraphAlive(hash) {
return executeFuntionInInspectWindow(checkGraphByHash, [hash]).then((res) => {
if (res) {
return true
} else {
return false
}
})
}
function getNowGraphData() {
return executeFuntionInInspectWindow(getGlobalInstances)
}
function startFPSMonitor() {
return executeFuntionInInspectWindow(doFPSThings)
}
getNowGraphData().then(function (data) {
const container = document.getElementById('container')
mount(data, container, {
showRect,
getAttrs,
cleanRect,
updateAttrs,
consoleEl,
checkGraphAlive,
getNowGraphData,
cleanAllRect,
startFPSMonitor,
})
})
startFPSMonitor()

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,49 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/** @license React v0.20.2
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="./script/main.js"></script>
</head>
<body></body>
</html>

View File

@ -0,0 +1,29 @@
{
"name": "@antv/x6-devtool",
"version": "0.0.1",
"private": true,
"description": "devtool for x6 in browser",
"license": "MIT",
"main": "index.js",
"scripts": {
"start": "webpack -w",
"build": "webpack"
},
"dependencies": {
"antd": "4.18.3",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-json-view": "1.21.3",
"@ant-design/icons": "^5.0.1"
},
"devDependencies": {
"@babel/core": "^7.16.7",
"@babel/preset-env": "^7.16.8",
"@babel/preset-react": "^7.16.7",
"babel-loader": "^8.2.3",
"css-loader": "^6.5.1",
"style-loader": "^3.3.1",
"webpack": "^5.66.0",
"webpack-cli": "4.9.1"
}
}

View File

@ -0,0 +1,189 @@
import React, { useState, useEffect } from 'react'
import { Row, Select, Col, Button, Tooltip, Tag, Empty } from 'antd'
import { CodeOutlined } from '@ant-design/icons'
import GTree from './GTree'
const countTree = (tree) => {
if (!tree) {
return 0
}
const count = tree?.children?.reduce((a, b) => a + countTree(b), 1) || 1
tree.count = count
return count
}
export function convertMemoryUnit(number, digits = 2) {
let value
let unit
if (number > 1000000000) {
unit = 'GB'
value = number / 1000000000
} else if (number > 1000000) {
unit = 'MB'
value = number / 1000000
} else if (number > 1000) {
unit = 'KB'
value = number / 1000
} else {
unit = 'B'
value = number
}
value = value && value % 1 !== 0 ? value.toFixed(digits) : value
return value + unit
}
const HeadBar = (props) => {
const {
data,
setSelectedHash,
actions,
selectedData,
setData,
selectedHash,
} = props
const [graphAlive, setGraphAlive] = useState(true)
useEffect(() => {
const itv = setInterval(() => {
actions.checkGraphAlive(selectedHash).then((res) => {
setGraphAlive(res)
if (res) {
actions.getNowGraphData().then((d) => {
if (d) {
setData(d)
}
})
}
})
}, 1000)
return () => {
clearInterval(itv)
}
}, [actions, setData, selectedData, selectedHash])
useEffect(() => {
if (!graphAlive) {
actions.getNowGraphData().then((d) => {
if (d) {
setData(d)
setSelectedHash(d[0].hash)
}
})
}
}, [actions, graphAlive, setData, setSelectedHash])
return (
<Row
align="middle"
style={{
padding: 2,
marginBottom: 6,
borderBottom: '1px solid #ddd',
background: 'rgba(0, 0, 0, 0.05)',
position: 'fixed',
width: '100%',
top: 0,
left: 0,
zIndex: 999,
}}
gutter={[12, 12]}
>
<Col>
<Select
bordered={false}
size="small"
defaultValue={0}
options={data.map((e, i) => ({
label: `Graph ${i}`,
value: e.hash,
info: e,
}))}
value={selectedHash}
onChange={(val) => {
setSelectedHash(val)
}}
placeholder="Choose a graph to inspect"
style={{ width: '100%' }}
/>
</Col>
{graphAlive ? (
<Col>
<Tag color="green">ALIVE</Tag>
</Col>
) : (
<Col>
<Tag color="red">DEAD</Tag>
<span>Trying to reconnect</span>
</Col>
)}
{selectedData && <Col>{selectedData?.count} Shapes</Col>}
{selectedData?.memory > 0 && (
<Col>HeapMemory:{convertMemoryUnit(selectedData.memory)}</Col>
)}
{selectedData?.fps > 0 && <Col>FPS: {selectedData?.fps}</Col>}
<Col flex={1} />
<Col>
<Button
size="small"
type="text"
onClick={() => {
actions.consoleEl(selectedData.hash)
}}
>
<Tooltip arrowPointAtCenter title="Console X6 Graph">
<CodeOutlined />
</Tooltip>
</Button>
</Col>
</Row>
)
}
const Devtool = (props) => {
const { data: initData = [], actions = {} } = props
const [selectedData, setSelectedData] = useState(initData[0])
const [selectedHash, setSelectedHash] = useState(initData[0].hash)
const [data, setData] = useState(initData)
useEffect(() => {
return () => {
actions.cleanAllRect()
actions.startFPSMonitor()
}
}, [actions, selectedHash])
useEffect(() => {
const target = data.find((e) => e.hash === selectedHash)
countTree(target)
setSelectedData(target)
}, [selectedHash, data])
return (
<div>
<HeadBar
data={data}
setSelectedHash={setSelectedHash}
selectedData={selectedData}
selectedHash={selectedHash}
actions={actions}
setData={setData}
/>
<div
style={{
marginTop: 48,
position: 'relative',
zIndex: 1,
}}
>
{selectedData ? (
<GTree actions={actions} data={selectedData} />
) : (
<Empty />
)}
</div>
</div>
)
}
export default Devtool

View File

@ -0,0 +1,179 @@
import { Drawer, Empty, Space, Tree, Typography } from 'antd'
import React, { useState, useEffect } from 'react'
import ReactJson from 'react-json-view'
import {
BlockOutlined,
PartitionOutlined,
AppstoreOutlined,
NodeIndexOutlined,
ToolOutlined,
} from '@ant-design/icons'
const iconMap = {
group: <BlockOutlined />,
svg: <PartitionOutlined />,
shape: <AppstoreOutlined />,
node: <AppstoreOutlined />,
edge: <NodeIndexOutlined />,
plugin: <ToolOutlined />,
}
const AttrsDrawer = ({ hash, getAttrs, onCancel, updateAttrs }) => {
const [val, setVal] = useState(hash)
const change = (all) => {
const { updated_src, namespace, name, existing_value, new_value } = all
const key = namespace[0] || name
const value = updated_src[key]
// 这个函数递归对新的value进行预处理。
// input框接收的new_value是字符串需要按照旧值做转换
const setNewValue = (value, paths, existing_value, new_value) => {
const key = paths.shift()
if (paths.length > 0) {
setNewValue(value[key], paths, existing_value, new_value)
} else {
if (new_value === undefined) {
// 删除
delete value[key]
} else if (typeof existing_value === 'undefined') {
// 新增
value[key] = new_value
} else if (typeof existing_value === 'boolean') {
value[key] = Boolean(new_value)
} else if (typeof existing_value === 'number') {
const val = Number(new_value)
if (!isNaN(val)) {
value[key] = val
}
} else if (typeof existing_value === 'string') {
value[key] = '' + new_value
}
}
}
setNewValue(
value,
namespace.slice(1).concat(name),
existing_value,
new_value,
)
updateAttrs(hash, key, value, all)
}
useEffect(() => {
if (hash && getAttrs) {
getAttrs(hash).then((res) => {
setVal(res)
})
} else {
onCancel()
getAttrs()
}
}, [getAttrs, hash, onCancel])
return (
<Drawer mask={false} onClose={onCancel} visible={hash}>
<ReactJson
style={{ fontSize: 12 }}
onAdd={change}
onEdit={change}
onDelete={change}
src={val}
/>
</Drawer>
)
}
const buildTreeData = (data = {}, isRoot) => {
const node = {
title: data.name || data.shape || data.type,
type: data.type,
key: data.hash,
name: data.name,
id: data.id,
hash: data.hash,
count: data.count,
num: data.children?.length || 0,
}
if (data.children) {
node.children = data.children.map((e) => buildTreeData(e))
}
if (isRoot) {
node.type = 'svg'
node.title = 'svg'
// node.key = node.hash || 'svg'
return node
}
return node
}
const GTree = (props) => {
const { data, actions = {} } = props
const [selectedKey, setSelected] = useState()
useEffect(() => {
actions.showRect(selectedKey, '__x6_select__', 'rgba(29, 57, 196, 0.5)')
return () => {
actions.cleanRect('__x6_select__')
}
}, [actions, selectedKey])
if (!data) {
return <Empty />
}
const treeData = buildTreeData(data, true)
return (
<div>
<Tree
selectedKeys={[selectedKey]}
onSelect={(keys) => setSelected(keys[0])}
showLine={{ showLeafIcon: false }}
height={document.body.clientHeight - 45}
titleRender={(node) => (
<div
onMouseEnter={() => {
actions.showRect(node.key, '__x6_hover__')
}}
onMouseLeave={() => {
actions.cleanRect('__x6_hover__')
}}
>
<Space>
{iconMap[node.type]}
{node.title}
{node.name && (
<Typography.Text type="secondary">
name:{node.name}
</Typography.Text>
)}
{node.id && (
<Typography.Text type="secondary">id:{node.id}</Typography.Text>
)}
{node.num > 0 && (
<Typography.Text type="secondary">
({node.num} children / {node.count || 0} descendants)
</Typography.Text>
)}
</Space>
</div>
)}
treeData={[treeData]}
/>
<AttrsDrawer
hash={selectedKey}
onCancel={() => setSelected()}
getAttrs={actions.getAttrs}
updateAttrs={actions.updateAttrs}
/>
</div>
)
}
export default GTree

View File

@ -0,0 +1,8 @@
import React from 'react'
import ReactDOM from 'react-dom'
import Devtool from './components/Devtool'
import 'antd/dist/antd.css'
window.mount = (data = [], container, actions = {}) => {
ReactDOM.render(<Devtool data={data} actions={actions} />, container)
}

View File

@ -0,0 +1,30 @@
const path = require('path')
module.exports = {
entry: './ui/index.js',
output: {
path: path.resolve(__dirname, 'devtools', 'ui'),
filename: 'ui.js',
},
optimization: {
usedExports: true,
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
}

View File

@ -74,17 +74,20 @@ Graph.prototype.unbindKey = function (
return this
}
Graph.prototype.clearKeys = function() {
Graph.prototype.clearKeys = function () {
const keyboard = this.getPlugin('keyboard') as Keyboard
if(keyboard) {
if (keyboard) {
keyboard.clear()
}
return this
}
Graph.prototype.triggerKey = function(key: string, action: KeyboardImpl.Action) {
Graph.prototype.triggerKey = function (
key: string,
action: KeyboardImpl.Action,
) {
const keyboard = this.getPlugin('keyboard') as Keyboard
if(keyboard) {
if (keyboard) {
keyboard.trigger(key, action)
}
return this

View File

@ -1,5 +1,30 @@
import { Graph, Node } from '@antv/x6'
import { TransformImpl } from './transform'
import { Transform } from './index'
declare module '@antv/x6/lib/graph/graph' {
interface Graph {
createTransformWidget: (node: Node) => Graph
clearTransformWidgets: () => Graph
}
}
declare module '@antv/x6/lib/graph/events' {
interface EventArgs extends TransformImpl.EventArgs {}
}
Graph.prototype.createTransformWidget = function (node) {
const transform = this.getPlugin('transform') as Transform
if (transform) {
transform.createWidget(node)
}
return this
}
Graph.prototype.clearTransformWidgets = function () {
const transform = this.getPlugin('transform') as Transform
if (transform) {
transform.clearWidgets()
}
return this
}

View File

@ -50,7 +50,7 @@ export class Transform extends Basecoat<Transform.EventArgs> {
return !this.disabled
}
protected onNodeClick({ node }: EventArgs['node:click']) {
createWidget(node: Node) {
this.clearWidgets()
const widget = this.createTransform(node)
if (widget) {
@ -62,6 +62,10 @@ export class Transform extends Basecoat<Transform.EventArgs> {
}
}
protected onNodeClick({ node }: EventArgs['node:click']) {
this.createWidget(node)
}
protected onBlankMouseDown() {
this.clearWidgets()
}
@ -135,7 +139,7 @@ export class Transform extends Basecoat<Transform.EventArgs> {
return options
}
protected clearWidgets() {
clearWidgets() {
this.widgets.forEach((widget, node) => {
if (this.graph.getCellById(node.id)) {
widget.dispose()

View File

@ -1,6 +1,6 @@
{
"name": "@antv/x6-react-shape",
"version": "2.1.0",
"version": "2.1.1",
"description": "X6 shape for rendering react components.",
"main": "lib/index.js",
"module": "es/index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@antv/x6-vue-shape",
"version": "2.0.9",
"version": "2.0.10",
"description": "X6 shape for rendering vue components.",
"main": "lib/index.js",
"module": "es/index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@antv/x6",
"version": "2.5.3",
"version": "2.5.6",
"description": "JavaScript diagramming library that uses SVG and HTML for rendering",
"main": "lib/index.js",
"module": "es/index.js",
@ -44,8 +44,8 @@
"pretest": "rss"
},
"dependencies": {
"@antv/x6-common": "^2.0.x",
"@antv/x6-geometry": "^2.0.x",
"@antv/x6-common": "^2.0.11",
"@antv/x6-geometry": "^2.0.4",
"utility-types": "^3.10.0"
},
"author": {

View File

@ -45,8 +45,7 @@ export class Cell<
} else if (typeof propHooks === 'function') {
this.propHooks.push(propHooks)
} else {
Object.keys(propHooks).forEach((name) => {
const hook = propHooks[name]
Object.values(propHooks).forEach((hook) => {
if (typeof hook === 'function') {
this.propHooks.push(hook)
}
@ -1292,8 +1291,7 @@ export class Cell<
const defaultAttrs = defaults.attrs || {}
const finalAttrs: Attr.CellAttrs = {}
Object.keys(props).forEach((key) => {
const val = props[key]
Object.entries(props).forEach(([key, val]) => {
if (
val != null &&
!Array.isArray(val) &&

View File

@ -315,8 +315,8 @@ export class Model extends Basecoat<Model.EventArgs> {
return this.batchUpdate(
'update',
() => {
Object.keys(prop).forEach((key) =>
existing.setProp(key, prop[key], options),
Object.entries(prop).forEach(([key, val]) =>
existing.setProp(key, val, options),
)
return true
},

View File

@ -116,7 +116,7 @@ export namespace Attr {
}
export namespace Attr {
export type Presets = typeof Attr['presets']
export type Presets = (typeof Attr)['presets']
export type NativeNames = keyof Presets
}

View File

@ -32,7 +32,7 @@ export namespace Background {
}
export namespace Background {
export type Presets = typeof Background['presets']
export type Presets = (typeof Background)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: Parameters<Presets[K]>[1] & {

View File

@ -38,7 +38,7 @@ export namespace ConnectionPoint {
}
export namespace ConnectionPoint {
export type Presets = typeof ConnectionPoint['presets']
export type Presets = (typeof ConnectionPoint)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: Parameters<Presets[K]>[3]

View File

@ -20,7 +20,7 @@ export namespace ConnectionStrategy {
}
export namespace ConnectionStrategy {
export type Presets = typeof ConnectionStrategy['presets']
export type Presets = (typeof ConnectionStrategy)['presets']
export type NativeNames = keyof Presets

View File

@ -20,7 +20,7 @@ export namespace Connector {
}
export namespace Connector {
export type Presets = typeof Connector['presets']
export type Presets = (typeof Connector)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: Parameters<Presets[K]>[3]

View File

@ -17,15 +17,15 @@ function setupUpdating(view: EdgeView) {
if (updateList == null) {
updateList = (view.graph as any)._jumpOverUpdateList = []
/**
* Handler for a batch:stop event to force
* update of all registered links with jump over connector
*/
view.graph.on('cell:mouseup', () => {
const list = (view.graph as any)._jumpOverUpdateList
for (let i = 0; i < list.length; i += 1) {
list[i].update()
}
// add timeout to wait for the target node to be connected
// fix https://github.com/antvis/X6/issues/3387
setTimeout(() => {
for (let i = 0; i < list.length; i += 1) {
list[i].update()
}
})
})
view.graph.on('model:reseted', () => {

View File

@ -27,7 +27,7 @@ export namespace EdgeAnchor {
}
export namespace EdgeAnchor {
export type Presets = typeof EdgeAnchor['presets']
export type Presets = (typeof EdgeAnchor)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: Parameters<Presets[K]>[3]

View File

@ -9,7 +9,7 @@ export namespace Filter {
}
export namespace Filter {
export type Presets = typeof Filter['presets']
export type Presets = (typeof Filter)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: NonUndefined<Parameters<Presets[K]>[0]>

View File

@ -79,7 +79,7 @@ export namespace Grid {
}
export namespace Grid {
export type Presets = typeof Grid['presets']
export type Presets = (typeof Grid)['presets']
export type OptionsMap = {
dot: patterns.DotOptions

View File

@ -32,7 +32,7 @@ export namespace Highlighter {
}
export namespace Highlighter {
export type Presets = typeof Highlighter['presets']
export type Presets = (typeof Highlighter)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: Parameters<Presets[K]['highlight']>[2]

View File

@ -23,7 +23,7 @@ export namespace Marker {
}
export namespace Marker {
export type Presets = typeof Marker['presets']
export type Presets = (typeof Marker)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: Parameters<Presets[K]>[0]

View File

@ -42,7 +42,7 @@ export namespace NodeAnchor {
}
export namespace NodeAnchor {
export type Presets = typeof NodeAnchor['presets']
export type Presets = (typeof NodeAnchor)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: Parameters<Presets[K]>[3]

View File

@ -28,7 +28,7 @@ export namespace PortLabelLayout {
}
export namespace PortLabelLayout {
export type Presets = typeof PortLabelLayout['presets']
export type Presets = (typeof PortLabelLayout)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: Parameters<Presets[K]>[2]

View File

@ -35,7 +35,7 @@ export namespace PortLayout {
}
export namespace PortLayout {
export type Presets = typeof PortLayout['presets']
export type Presets = (typeof PortLayout)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: Parameters<Presets[K]>[2]

View File

@ -35,8 +35,8 @@ export class Registry<
force = false,
) {
if (typeof name === 'object') {
Object.keys(name).forEach((key) => {
this.register(key, name[key], options)
Object.entries(name).forEach(([key, val]) => {
this.register(key, val, options)
})
return
}

View File

@ -15,7 +15,7 @@ export namespace Router {
}
export namespace Router {
export type Presets = typeof Router['presets']
export type Presets = (typeof Router)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: Parameters<Presets[K]>[1]

View File

@ -135,9 +135,9 @@ export class Button extends ToolsView.ToolItem<
export namespace Button {
export interface Options extends ToolsView.ToolItem.Options {
x?: number
y?: number
distance?: number
x?: number | string
y?: number | string
distance?: number | string
offset?: number | Point.PointLike
rotate?: boolean
useCellGeometry?: boolean

View File

@ -53,7 +53,7 @@ export namespace NodeTool {
}
export namespace NodeTool {
export type Presets = typeof NodeTool['presets']
export type Presets = (typeof NodeTool)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: ConstructorParameters<Presets[K]>[0]
@ -122,7 +122,7 @@ export namespace EdgeTool {
}
export namespace EdgeTool {
export type Presets = typeof EdgeTool['presets']
export type Presets = (typeof EdgeTool)['presets']
export type OptionsMap = {
readonly [K in keyof Presets]-?: ConstructorParameters<Presets[K]>[0]

View File

@ -259,9 +259,7 @@ export class Scheduler extends Disposable {
}
protected flushWaittingViews() {
const ids = Object.keys(this.views)
for (let i = 0, len = ids.length; i < len; i += 1) {
const viewItem = this.views[ids[i]]
Object.values(this.views).forEach((viewItem) => {
if (viewItem && viewItem.state === Scheduler.ViewState.WAITTING) {
const { view, flag, options } = viewItem
this.requestViewUpdate(
@ -272,7 +270,7 @@ export class Scheduler extends Disposable {
false,
)
}
}
})
this.flush()
}
@ -318,9 +316,8 @@ export class Scheduler extends Disposable {
}
protected resetViews() {
const willRemoveViews = { ...this.views, ...this.willRemoveViews }
Object.keys(willRemoveViews).forEach((id) => {
const viewItem = willRemoveViews[id]
this.willRemoveViews = { ...this.views, ...this.willRemoveViews }
Object.values(this.willRemoveViews).forEach((viewItem) => {
if (viewItem) {
this.removeView(viewItem.view)
}
@ -332,7 +329,7 @@ export class Scheduler extends Disposable {
protected removeView(view: CellView) {
const cell = view.cell
const viewItem = this.willRemoveViews[cell.id]
if (view) {
if (viewItem && view) {
viewItem.view.remove()
delete this.willRemoveViews[cell.id]
this.graph.trigger('view:unmounted', { view })
@ -403,8 +400,7 @@ export class Scheduler extends Disposable {
protected removeZPivots() {
if (this.zPivots) {
Object.keys(this.zPivots).forEach((z) => {
const elem = this.zPivots[z]
Object.values(this.zPivots).forEach((elem) => {
if (elem && elem.parentNode) {
elem.parentNode.removeChild(elem)
}

View File

@ -67,8 +67,7 @@ export class CellView<
}
if (actions) {
Object.keys(actions).forEach((key) => {
const val = actions[key]
Object.entries(actions).forEach(([key, val]) => {
const raw = ret.actions[key]
if (val && raw) {
ret.actions[key] = mergeActions(raw, val)

View File

@ -213,8 +213,7 @@ export class NodeView<
}
protected removePorts() {
Object.keys(this.portsCache).forEach((portId) => {
const cached = this.portsCache[portId]
Object.values(this.portsCache).forEach((cached) => {
Dom.remove(cached.portElement)
})
}
@ -710,7 +709,7 @@ export class NodeView<
if (options.frontOnly) {
if (candidates.length > 0) {
const zIndexMap = ArrayExt.groupBy(candidates, 'zIndex')
const maxZIndex = ArrayExt.max(Object.keys(zIndexMap))
const maxZIndex = ArrayExt.max(Object.keys(zIndexMap).map(parseInt))
if (maxZIndex) {
candidates = zIndexMap[maxZIndex]
}

66484
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -5,4 +5,4 @@
"previewer.actions.codepen": "在 CodePen 中打开(未实现)",
"previewer.actions.stackblitz": "在 StackBlitz 中打开",
"previewer.actions.separate": "在独立页面中打开"
}
}

View File

@ -207,7 +207,7 @@ graph.addEdge({
| 参数名 | 类型 | 默认值 | 说明 |
| -------- | -------------------------------------------------------------------- | ----------- | ------------------------------ |
| distance | number | `undefined` | 偏离起点的距离或比例。 |
| distance | number \| string | `undefined` | 偏离起点的距离或比例。 |
| offset | number \| Point.PointLike | `0` | 在 `distance` 基础上的偏移量。 |
| rotate | boolean | `undefined` | 是否跟随边旋转。 |
| markup | Markup.JSONMarkup | `undefined` | 渲染按钮的 Markup 定义。 |

View File

@ -9,7 +9,6 @@ redirect_from:
节点小工具是一些渲染在节点上的一些小组件,这些小工具通常都附带一些交互功能,如删除按钮,点击按钮时删除对应的节点。我们可以根据下面的一些场景来添加或删除小工具。
场景一:添加指定的小工具。
```ts
// 创建节点时添加小工具
@ -30,20 +29,8 @@ node.addTools([
args: { x: 10, y: 10 },
},
])
```
场景二:动态添加/删除小工具。
```ts
graph.on("node:mouseenter", ({ node }) => {
node.addTools([
{
name: "button-remove",
args: { x: 10, y: 10 },
},
]);
});
// 删除工具
graph.on("node:mouseleave", ({ node }) => {
if (node.hasTool("button-remove")) {
node.removeTool("button-remove");
@ -58,66 +45,41 @@ graph.on("node:mouseleave", ({ node }) => {
- [boundary](#boundary) 根据节点的包围盒渲染一个包围节点的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。
- [node-editor](#node-editor) 提供节点文本编辑功能。
## presets
## 节点工具
### button
在指定位置处渲染一个按钮,支持自定义按钮的点击交互。
在指定位置处渲染一个按钮,支持自定义按钮的点击交互。配置如下:
<span class="tag-param">参数<span>
| 参数名 | 类型 | 默认值 | 说明 |
|-----------------|----------------------------------------------------------------------|--------|--------------------------------------------------------------------------------------------------------|
| x | number \| string | `0` | 相对于节点的左上角 X 轴的坐标,小数和百分比表示相对位置。 |
| y | number \| string | `0` | 相对于节点的左上角 Y 轴的坐标,小数和百分比表示相对位置。 |
| offset | number \| { x: number, y: number } | `0` | 在 `x``y` 基础上的偏移量。 |
| rotate | boolean | - | 是否跟随节点旋转。 |
| useCellGeometry | boolean | `true` | 是否使用几何计算的方式来计算元素包围盒,开启后会有性能上的提升,如果出现计算准度问题,请将它设置为 `false`。 |
| markup | Markup.JSONMarkup | - | 渲染按钮的 Markup 定义。 |
| onClick | (args: {e: Dom.MouseDownEvent, cell: Cell, view: CellView }) => void | - | 点击按钮的回调函数。 |
| 参数名 | 类型 | 默认值 | 说明 |
| ------- | -------------------------------------------------------------------- | ----------- | --------------------------------------------------------- |
| x | number \| string | `0` | 相对于节点的左上角 X 轴的坐标,小数和百分比表示相对位置。 |
| y | number \| string | `0` | 相对于节点的左上角 Y 轴的坐标,小数和百分比表示相对位置。 |
| offset | number \| { x: number, y: number } | `0` | 在 `x``y` 基础上的偏移量。 |
| rotate | boolean | `undefined` | 是否跟随节点旋转。 |
| markup | Markup.JSONMarkup | `undefined` | 渲染按钮的 Markup 定义。 |
| onClick | (args: {e: Dom.MouseDownEvent, cell: Cell, view: CellView }) => void | `undefined` | 点击按钮的回调函数。 |
<span class="tag-example">使用</span>
```ts
const source = graph.addNode({
...,
// 添加一个始终显示的按钮
tools: [
{
name: 'button',
args: {
markup: ...,
x: '100%',
y: '100%',
offset: { x: -18, y: -18 },
onClick({ view }) { ... },
},
},
],
})
// ...
// 鼠标 Hover 时添加按钮
graph.on('node:mouseenter', ({ node }) => {
if (node === target) {
node.addTools({
name: 'button',
args: {
markup: ...,
x: 0,
y: 0,
offset: { x: 18, y: 18 },
onClick({ view }) { ... },
},
})
}
node.addTools({
name: 'button',
args: {
markup: ...,
x: 0,
y: 0,
offset: { x: 18, y: 18 },
onClick({ view }) { ... },
},
})
})
// 鼠标移开时删除按钮
graph.on('node:mouseleave', ({ node }) => {
if (node === target) {
node.removeTools()
}
node.removeTools() // 删除所有的工具
})
```
@ -125,20 +87,7 @@ graph.on('node:mouseleave', ({ node }) => {
### button-remove
在指定的位置处,渲染一个删除按钮,点击时删除对应的节点。
<span class="tag-param">参数<span>
| 参数名 | 类型 | 默认值 | 说明 |
| ------- | -------------------------------------------------------------------- | ----------- | --------------------------------------------------------- |
| x | number \| string | `0` | 相对于节点的左上角 X 轴的坐标,小数和百分比表示相对位置。 |
| y | number \| string | `0` | 相对于节点的左上角 Y 轴的坐标,小数和百分比表示相对位置。 |
| offset | number \| { x: number, y: number } | `0` | 在 `distance` 基础上的偏移量。 |
| rotate | boolean | `undefined` | 是否跟随边旋转。 |
| markup | Markup.JSONMarkup | `undefined` | 渲染按钮的 Markup 定义。 |
| onClick | (args: {e: Dom.MouseDownEvent, cell: Cell, view: CellView }) => void | `undefined` | 点击按钮的回调函数。 |
<span class="tag-example">使用</span>
在指定的位置处,渲染一个删除按钮,点击时删除对应的节点。它是上面 `button` 工具的一个特例,所以支持 `button` 的所有配置。
```ts
const source = graph.addNode({
@ -155,46 +104,26 @@ const source = graph.addNode({
},
],
})
// 鼠标 Hover 时添加删除按钮
graph.on('node:mouseenter', ({ node }) => {
if (node === target) {
node.addTools({
name: 'button-remove',
args: {
x: 0,
y: 0,
offset: { x: 10, y: 10 },
},
})
}
})
// 鼠标移开时删除删除按钮
graph.on('node:mouseleave', ({ node }) => {
if (node === target) {
node.removeTools()
}
})
```
<!-- <iframe src="/demos/api/registry/node-tool/button-remove"></iframe> -->
### boundary
根据节点的包围盒渲染一个包围节点的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。
根据节点的包围盒渲染一个包围节点的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。配置如下:
<span class="tag-param">参数<span>
| 参数名 | 类型 | 默认值 | 说明 |
| ------- | -------- | -------- | ------------------ |
| tagName | string | `rect` | 使用何种图形渲染。 |
| padding | number | `10` | 边距。 |
| attrs | KeyValue | `object` | 图形属性。 |
| 参数名 | 类型 | 默认值 | 说明 |
|-----------------|-------------|----------|--------------------------------------------------------------------------------------------------------|
| tagName | string | `rect` | 使用何种图形渲染。 |
| rotate | boolean | - | 图形是否跟随节点旋转。 |
| padding | SideOptions | `10` | 边距。 |
| attrs | KeyValue | `object` | 图形属性。 |
| useCellGeometry | boolean | `true` | 是否使用几何计算的方式来计算元素包围盒,开启后会有性能上的提升,如果出现计算准度问题,请将它设置为 `false`。 |
其中 `attrs` 的默认值(默认样式)为:
```js
```ts
{
fill: 'none',
stroke: '#333',
@ -204,7 +133,20 @@ graph.on('node:mouseleave', ({ node }) => {
}
```
<span class="tag-example">使用</span>
`SideOptions` 的类型定义如下:
```typescript
type SideOptions = number | {
vertical?: number;
horizontal?: number;
left?: number;
top?: number;
right?: number;
bottom?: number;
};
```
工具使用方式如下:
```ts
const source = graph.addNode({
@ -230,21 +172,19 @@ const source = graph.addNode({
### node-editor
提供节点上文本编辑功能。
提供节点上文本编辑功能。配置如下:
<span class="tag-param">参数<span>
| 参数名 | 类型 | 默认值 | 说明 |
| --------------------- | ----------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------- |
| event | Dom.EventObject | - | 触发文本编辑的事件参数 |
| attrs/fontSize | string | `14` | 编辑文本字体大小 |
| attrs/color | string | `#000` | 编辑文本字体颜色 |
| attrs/fontFamily | string | `Arial, helvetica, sans-serif` | 编辑文本的字体 |
| attrs/backgroundColor | string | `#fff` | 编辑区域的背景色 |
| 参数名 | 类型 | 默认值 | 说明 |
|-----------------------|-------------------------------------------------------------|--------------------------------|--------------------------------------------------------------|
| event | Dom.EventObject | - | 触发文本编辑的事件参数 |
| attrs/fontSize | string | `14` | 编辑文本字体大小 |
| attrs/color | string | `#000` | 编辑文本字体颜色 |
| attrs/fontFamily | string | `Arial, helvetica, sans-serif` | 编辑文本字体 |
| attrs/backgroundColor | string | `#fff` | 编辑区域的背景色 |
| getText | (this: CellView, args: {cell: Cell}) => string | - | 获取原文本方法,在自定义 `markup` 场景需要自定义 `getText` 方法 |
| setText | (this: CellView, args: {cell: Cell, value: string}) => void | - | 设置新文本,在自定义 `markup` 场景需要自定义 `setText` 方法 |
<span class="tag-example">使用</span>
工具使用方式如下:
```ts
// 双击进入编辑模式
@ -260,55 +200,19 @@ graph.on("node:dblclick", ({ node, e }) => {
<!-- <iframe src="/demos/api/registry/node-tool/editor"></iframe> -->
## registry
## 自定义工具
我们在 Registry.NodeTool.registry 对象上提供了注册和取消注册工具的方法,工具实际上是一个继承自 ToolItem 的[视图](/zh/docs/api/view/view)。
## 方式一
```ts
export type Definition =
| typeof ToolItem
| (new (options: ToolItem.Options) => ToolItem);
```
例如,上面提到的 `'button'` 工具的对应的定义为
```ts
export class Button extends ToolsView.ToolItem<EdgeView | NodeView, Button.Options> {
protected onRender() { ... }
protected onMouseDown() { ... }
}
```
创建工具类之后就可以使用下面的 `register` 方法来注册到系统中。
### register
```sign
register(entities: { [name: string]: Definition }, force?: boolean): void
register(name: string, entity: Definition, force?: boolean): Definition
```
注册工具。
### unregister
```sign
unregister(name: string): Definition | null
```
取消注册工具。
实际上,我们将 `registry` 对象的 `register``unregister` 方法分别挂载为 `Graph` 的两个静态方法 `Graph.registerNodeTool``Graph.unregisterNodeTool`,看下面使用示例。
### 自定义工具
场景一:继承 `ToolItem` 实现一个工具类,难度较高,要求对[视图基类](/zh/docs/api/view/view)和 `ToolItem` 类都有所了解,可以参考上述内置工具的源码,这里不展开叙述。
继承 `ToolItem` 实现一个工具类,难度较高,要求对 [ToolItem](https://github.com/antvis/X6/blob/master/packages/x6/src/view/tool.ts) 类都有所了解,可以参考上述内置工具的源码,这里不展开叙述。
```ts
Graph.registerNodeTool("button", Button);
```
场景二:继承已经注册的工具,为继承的工具指定默认选项或者默认样式。我们在 `ToolItem` 基类上提供了一个静态方法 `define` 来快速实现继承并配置默认选项。
## 方式二
继承已经注册的工具,在继承基础上修改配置。我们在 `ToolItem` 基类上提供了一个静态方法 `define` 来快速实现继承并修改配置。
```ts
const MyButton = Button.define<Button.Options>({
@ -324,7 +228,6 @@ Graph.registerNodeTool('my-btn', MyButton, true)
```ts
Graph.registerNodeTool('my-btn', {
'my-btn', // 工具名称,可省略,指定后其大驼峰形式同时作为继承的类的类名。
inherit:'button', // 基类名称,使用已经注册的工具名称。
markup: ...,
onClick: ...,

View File

@ -389,3 +389,18 @@ graph.on("edge:transition:complete", (args: Animation.CallbackArgs) => {});
graph.on("edge:transition:stop", (args: Animation.StopArgs) => {});
graph.on("edge:transition:finish", (args: Animation.CallbackArgs) => {});
```
## 视图
由于X6实现了异步的渲染调度算法所以节点的添加不一定意味着挂载到画布上。节点在被挂载到画布时以及从画布上卸载时会分别触发单独的事件。
| 事件名 | 回调参数 | 说明 |
| ---------------- | ----------------------- | ------------------------------ |
| `view:mounted` | `{ view: CellView }` | 节点被挂载到画布上时触发。 |
| `view:unmounted` | `{ view: CellView }` | 节点从画布上卸载时触发。 |
```ts
graph.on("view:mounted", ({ view }) => {});
graph.on("view:unmounted", ({ view }) => {});
```

View File

@ -18,6 +18,10 @@ redirect_from:
我们提供了一个独立的包 `@antv/x6-angular-shape` 以支持将 Angular 的组件/模板作为节点进行渲染。
:::warning{title=注意:}
需要注意的是x6 的版本要和 x6-angular-shape 的版本匹配,也就是两个包需要用同一个大版本。
:::
### Component 渲染
```ts

View File

@ -17,6 +17,11 @@ redirect_from:
我们提供了一个独立的包 `@antv/x6-react-shape` 来使用 React 渲染节点。
:::warning{title=注意:}
需要注意的是x6 的版本要和 x6-react-shape 的版本匹配,也就是两个包需要用同一个大版本。
:::
```ts
import { register } from "@antv/x6-react-shape";

View File

@ -17,6 +17,11 @@ redirect_from:
我们提供了一个独立的包 `@antv/x6-vue-shape` 来使用 Vue 组件渲染节点。
:::warning{title=注意:}
需要注意的是x6 的版本要和 x6-vue-shape 的版本匹配,也就是两个包需要用同一个大版本。
:::
```html
<template>
<div class="app-content">

View File

@ -55,6 +55,17 @@ graph.use(
## 配置
## API
### graph.createTransformWidget(node: Node)
给节点创建widget
### graph.clearTransformWidgets()
清除所有widget
### 调整尺寸
| 属性名 | 类型 | 默认值 | 必选 | 描述 |

View File

@ -48,11 +48,13 @@ const graph = new Graph({
connectionPoint: 'anchor',
},
})
graph.use(new Transform({
rotating: {
enabled: true,
}
}))
graph.use(
new Transform({
rotating: {
enabled: true,
},
}),
)
Graph.registerConnector(
'curve',

View File

@ -1,51 +1,51 @@
import { Graph, Shape } from "@antv/x6";
import { Graph, Shape } from '@antv/x6'
Shape.Rect.config({
attrs: {
body: {
fill: "#f5f5f5",
stroke: "#d9d9d9",
fill: '#f5f5f5',
stroke: '#d9d9d9',
strokeWidth: 1,
},
},
ports: {
groups: {
in: {
position: { name: "top" },
position: { name: 'top' },
},
out: {
position: { name: "bottom" },
position: { name: 'bottom' },
},
},
},
portMarkup: [
{
tagName: "circle",
selector: "portBody",
tagName: 'circle',
selector: 'portBody',
attrs: {
r: 5,
magnet: true,
stroke: "#31d0c6",
fill: "#fff",
stroke: '#31d0c6',
fill: '#fff',
strokeWidth: 2,
},
},
],
});
})
const magnetAvailabilityHighlighter = {
name: "stroke",
name: 'stroke',
args: {
padding: 3,
attrs: {
strokeWidth: 3,
stroke: "#52c41a",
stroke: '#52c41a',
},
},
};
}
const graph = new Graph({
container: document.getElementById("container"),
container: document.getElementById('container'),
grid: true,
highlighting: {
magnetAvailable: magnetAvailabilityHighlighter,
@ -57,24 +57,24 @@ const graph = new Graph({
allowNode: false,
highlight: true,
validateMagnet({ magnet }) {
return magnet.getAttribute("port-group") !== "in";
return magnet.getAttribute('port-group') !== 'in'
},
validateConnection({ sourceMagnet, targetMagnet }) {
// 只能从输出连接桩创建连接
if (!sourceMagnet || sourceMagnet.getAttribute("port-group") === "in") {
return false;
if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') {
return false
}
// 只能连接到输入连接桩
if (!targetMagnet || targetMagnet.getAttribute("port-group") !== "in") {
return false;
if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') {
return false
}
return true;
return true
},
},
});
})
const source = graph.addNode({
x: 40,
@ -82,12 +82,12 @@ const source = graph.addNode({
width: 100,
height: 40,
ports: [
{ id: "in-1", group: "in" },
{ id: "in-2", group: "in" },
{ id: "out-1", group: "out" },
{ id: "out-2", group: "out" },
{ id: 'in-1', group: 'in' },
{ id: 'in-2', group: 'in' },
{ id: 'out-1', group: 'out' },
{ id: 'out-2', group: 'out' },
],
});
})
const target = graph.addNode({
x: 140,
@ -95,12 +95,12 @@ const target = graph.addNode({
width: 100,
height: 40,
ports: [
{ id: "in-1", group: "in" },
{ id: "in-2", group: "in" },
{ id: "out-1", group: "out" },
{ id: "out-2", group: "out" },
{ id: 'in-1', group: 'in' },
{ id: 'in-2', group: 'in' },
{ id: 'out-1', group: 'out' },
{ id: 'out-2', group: 'out' },
],
});
})
graph.addNode({
x: 320,
@ -108,34 +108,34 @@ graph.addNode({
width: 100,
height: 40,
ports: [
{ id: "in-1", group: "in" },
{ id: "in-2", group: "in" },
{ id: "out-1", group: "out" },
{ id: "out-2", group: "out" },
{ id: 'in-1', group: 'in' },
{ id: 'in-2', group: 'in' },
{ id: 'out-1', group: 'out' },
{ id: 'out-2', group: 'out' },
],
});
})
graph.addEdge({
source: { cell: source.id, port: "out-2" },
target: { cell: target.id, port: "in-1" },
});
source: { cell: source.id, port: 'out-2' },
target: { cell: target.id, port: 'in-1' },
})
graph.on("edge:mouseenter", ({ cell }) => {
graph.on('edge:mouseenter', ({ cell }) => {
cell.addTools([
{
name: "source-arrowhead",
name: 'source-arrowhead',
},
{
name: "target-arrowhead",
name: 'target-arrowhead',
args: {
attrs: {
fill: "red",
fill: 'red',
},
},
},
]);
});
])
})
graph.on("edge:mouseleave", ({ cell }) => {
cell.removeTools();
});
graph.on('edge:mouseleave', ({ cell }) => {
cell.removeTools()
})

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Graph, Node, Color } from '@antv/x6'
import { Graph, Color } from '@antv/x6'
import { register } from '@antv/x6-react-shape'
const MyComponent = ({ node }) => {
@ -26,7 +26,7 @@ register({
width: 120,
height: 50,
effect: ['color'],
component: MyComponent
component: MyComponent,
})
const graph = new Graph({

View File

@ -1,9 +1,9 @@
import { Graph } from "@antv/x6";
import { Graph } from '@antv/x6'
const graph = new Graph({
container: document.getElementById("container"),
container: document.getElementById('container'),
grid: true,
});
})
// 文档https://x6.antv.vision/zh/docs/tutorial/basic/port
@ -14,8 +14,8 @@ const rect = graph.addNode({
height: 180,
attrs: {
body: {
fill: "#f5f5f5",
stroke: "#d9d9d9",
fill: '#f5f5f5',
stroke: '#d9d9d9',
strokeWidth: 1,
},
},
@ -26,50 +26,50 @@ const rect = graph.addNode({
circle: {
r: 6,
magnet: true,
stroke: "#31d0c6",
fill: "#fff",
stroke: '#31d0c6',
fill: '#fff',
strokeWidth: 2,
},
},
},
},
items: [
{ id: "port1", group: "group1" },
{ id: "port2", group: "group1" },
{ id: "port3", group: "group1" },
{ id: 'port1', group: 'group1' },
{ id: 'port2', group: 'group1' },
{ id: 'port3', group: 'group1' },
],
},
});
})
graph.addEdge({
source: { x: 40, y: 150 },
target: {
cell: rect,
port: "port1", // 连接桩 ID
port: 'port1', // 连接桩 ID
},
attrs: {
line: { stroke: "#d9d9d9" },
line: { stroke: '#d9d9d9' },
},
});
})
graph.addEdge({
source: { x: 40, y: 150 },
target: {
cell: rect,
port: "port2", // 连接桩 ID
port: 'port2', // 连接桩 ID
},
attrs: {
line: { stroke: "#d9d9d9" },
line: { stroke: '#d9d9d9' },
},
});
})
graph.addEdge({
source: { x: 40, y: 150 },
target: {
cell: rect,
port: "port3", // 连接桩 ID
port: 'port3', // 连接桩 ID
},
attrs: {
line: { stroke: "#d9d9d9" },
line: { stroke: '#d9d9d9' },
},
});
})

View File

@ -1,9 +1,9 @@
import { Graph } from "@antv/x6";
import { Graph } from '@antv/x6'
const graph = new Graph({
container: document.getElementById("container"),
container: document.getElementById('container'),
grid: true,
});
})
// 文档https://x6.antv.vision/zh/docs/tutorial/basic/port
@ -14,39 +14,39 @@ const rect = graph.addNode({
height: 180,
attrs: {
body: {
fill: "#f5f5f5",
stroke: "#d9d9d9",
fill: '#f5f5f5',
stroke: '#d9d9d9',
strokeWidth: 1,
},
},
ports: [
// 默认样式
{ id: "port1" },
{ id: 'port1' },
// 自定义连接桩样式
{
id: "port2",
id: 'port2',
attrs: {
circle: {
magnet: true,
r: 8,
stroke: "#31d0c6",
fill: "#fff",
stroke: '#31d0c6',
fill: '#fff',
strokeWidth: 2,
},
},
},
],
});
})
rect.addPort({
id: "port3",
id: 'port3',
attrs: {
circle: {
r: 6,
magnet: true,
stroke: "#31d0c6",
fill: "#fff",
stroke: '#31d0c6',
fill: '#fff',
strokeWidth: 2,
},
},
});
})

View File

@ -1,9 +1,9 @@
import { Graph } from "@antv/x6";
import { Graph } from '@antv/x6'
const graph = new Graph({
container: document.getElementById("container"),
container: document.getElementById('container'),
grid: true,
});
})
graph.addNode({
x: 100,
@ -12,8 +12,8 @@ graph.addNode({
height: 120,
attrs: {
body: {
fill: "#f5f5f5",
stroke: "#d9d9d9",
fill: '#f5f5f5',
stroke: '#d9d9d9',
strokeWidth: 1,
},
},
@ -24,37 +24,37 @@ graph.addNode({
circle: {
r: 6,
magnet: true,
stroke: "#31d0c6",
stroke: '#31d0c6',
strokeWidth: 2,
fill: "#fff",
fill: '#fff',
},
text: {
fontSize: 12,
fill: "#888",
fill: '#888',
},
},
// 文档https://x6.antv.vision/zh/docs/api/registry/port-layout#absolute
position: {
name: "absolute",
name: 'absolute',
},
},
},
items: [
{
id: "port1",
group: "group1",
id: 'port1',
group: 'group1',
// 通过 args 指定绝对位置
args: {
x: 0,
y: 60,
},
attrs: {
text: { text: "{ x: 0, y: 60 }" },
text: { text: '{ x: 0, y: 60 }' },
},
},
{
id: "port2",
group: "group1",
id: 'port2',
group: 'group1',
// 通过 args 指定绝对位置和连接桩的旋转角度
args: {
x: 0.6,
@ -64,40 +64,40 @@ graph.addNode({
// 自定义连接桩渲染的 SVG
markup: [
{
tagName: "path",
selector: "path",
tagName: 'path',
selector: 'path',
},
],
zIndex: 10,
attrs: {
path: {
d: "M -6 -8 L 0 8 L 6 -8 Z",
d: 'M -6 -8 L 0 8 L 6 -8 Z',
magnet: true,
fill: "red",
fill: 'red',
},
text: {
text: "{ x: 0.6, y: 32, angle: 45 }",
fill: "red",
text: '{ x: 0.6, y: 32, angle: 45 }',
fill: 'red',
},
},
},
{
id: "port3",
group: "group1",
id: 'port3',
group: 'group1',
// 通过 args 指定绝对位置
args: {
x: "100%",
y: "100%",
x: '100%',
y: '100%',
},
attrs: {
text: { text: "{ x: '100%', y: '100%' }" },
},
label: {
position: {
name: "right",
name: 'right',
},
},
},
],
},
});
})

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Graph, Node, Path, Cell } from '@antv/x6'
import { Graph, Path, Cell } from '@antv/x6'
import { Selection } from '@antv/x6-plugin-selection'
import insertCss from 'insert-css'
import { register } from '@antv/x6-react-shape'
@ -27,12 +27,12 @@ const AlgoNode = (props) => {
return (
<div className={`node ${status}`}>
<img src={image.logo} />
<img src={image.logo} alt="logo" />
<span className="label">{label}</span>
<span className="status">
{status === 'success' && <img src={image.success} />}
{status === 'failed' && <img src={image.failed} />}
{status === 'running' && <img src={image.running} />}
{status === 'success' && <img src={image.success} alt="success" />}
{status === 'failed' && <img src={image.failed} alt="failed" />}
{status === 'running' && <img src={image.running} alt="running" />}
</span>
</div>
)
@ -71,8 +71,7 @@ register({
},
},
},
},
)
})
Graph.registerEdge(
'dag-edge',
@ -234,14 +233,16 @@ const graph: Graph = new Graph({
},
},
})
graph.use(new Selection({
enabled: true,
multiple: true,
rubberEdge: true,
rubberNode: true,
modifiers: 'shift',
rubberband: true,
}))
graph.use(
new Selection({
enabled: true,
multiple: true,
rubberEdge: true,
rubberNode: true,
modifiers: 'shift',
rubberband: true,
}),
)
graph.on('edge:connected', ({ edge }) => {
edge.attr({
@ -287,7 +288,7 @@ const showNodeStatus = async (statusList: NodeStatus[][]) => {
const data = node.getData() as NodeStatus
node.setData({
...data,
status: status,
status,
})
})
setTimeout(() => {