├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature.md └── workflows │ └── gh-pages.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .travis.yml ├── .umirc.ts ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.en-US.md ├── README.md ├── babel.config.js ├── docs ├── examples │ ├── component │ │ ├── command.md │ │ ├── command.zh-CN.md │ │ ├── detail-panel.md │ │ ├── detail-panel.zh-CN.md │ │ ├── item-panel.md │ │ └── item-panel.zh-CN.md │ ├── editor │ │ ├── context.md │ │ └── context.zh-CN.md │ ├── graph │ │ ├── flow.md │ │ ├── flow.zh-CN.md │ │ ├── mind.md │ │ └── mind.zh-CN.md │ ├── plugin │ │ ├── context-menu.md │ │ ├── context-menu.zh-CN.md │ │ ├── editable-label.md │ │ ├── editable-label.zh-CN.md │ │ ├── item-popover.md │ │ └── item-popover.zh-CN.md │ └── register │ │ ├── dom-node.md │ │ ├── dom-node.zh-CN.md │ │ ├── node.md │ │ └── node.zh-CN.md ├── guide │ ├── getting-started.md │ └── getting-started.zh-CN.md ├── index.md └── index.zh-CN.md ├── examples ├── component │ ├── command │ │ ├── index.less │ │ └── index.tsx │ ├── detail-panel │ │ ├── Panel │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ └── item-panel │ │ ├── index.less │ │ └── index.tsx ├── editor │ └── context │ │ ├── WrappedClassComponent.tsx │ │ ├── WrappedFunctionComponent.tsx │ │ ├── index.less │ │ └── index.tsx ├── graph │ ├── flow │ │ ├── index.less │ │ └── index.tsx │ └── mind │ │ ├── index.less │ │ └── index.tsx ├── index.d.ts ├── plugin │ ├── context-menu │ │ ├── index.less │ │ └── index.tsx │ ├── editable-label │ │ ├── index.less │ │ └── index.tsx │ └── item-popover │ │ ├── index.less │ │ └── index.tsx ├── register │ ├── dom-node │ │ ├── index.less │ │ └── index.tsx │ └── node │ │ ├── index.less │ │ └── index.tsx └── tsconfig.json ├── package.json ├── public └── CNAME ├── scripts └── build.js ├── src ├── common │ ├── CommandManager │ │ └── index.ts │ ├── behaviorManager │ │ └── index.ts │ ├── commandManager │ │ └── index.ts │ ├── constants │ │ └── index.ts │ ├── global │ │ └── index.ts │ ├── interfaces │ │ └── index.ts │ └── withContext │ │ └── index.tsx ├── components │ ├── Command │ │ └── index.tsx │ ├── DetailPanel │ │ └── index.tsx │ ├── Editor │ │ └── index.tsx │ ├── EditorContext │ │ └── index.tsx │ ├── Flow │ │ ├── behavior │ │ │ ├── dragAddEdge.ts │ │ │ ├── dragAddNode.ts │ │ │ └── index.ts │ │ └── index.tsx │ ├── Graph │ │ ├── behavior │ │ │ ├── clickItem.ts │ │ │ ├── dragCanvas.ts │ │ │ ├── hoverItem.ts │ │ │ ├── index.ts │ │ │ └── recallEdge.ts │ │ ├── command │ │ │ ├── add.ts │ │ │ ├── base.ts │ │ │ ├── copy.ts │ │ │ ├── index.ts │ │ │ ├── paste.ts │ │ │ ├── pasteHere.ts │ │ │ ├── redo.ts │ │ │ ├── remove.ts │ │ │ ├── undo.ts │ │ │ ├── update.ts │ │ │ ├── zoomIn.ts │ │ │ └── zoomOut.ts │ │ └── index.tsx │ ├── ItemPanel │ │ ├── Item.tsx │ │ └── index.tsx │ ├── Mind │ │ ├── command │ │ │ ├── fold.ts │ │ │ ├── index.ts │ │ │ ├── subtopic.ts │ │ │ ├── topic.ts │ │ │ └── unfold.ts │ │ └── index.tsx │ └── Register │ │ └── index.ts ├── helpers │ └── index.ts ├── index.tsx ├── plugins │ ├── ContextMenu │ │ └── index.tsx │ ├── EditableLabel │ │ └── index.tsx │ └── ItemPopover │ │ └── index.tsx ├── shape │ ├── common │ │ └── anchor.ts │ ├── edges │ │ ├── bizFlowEdge.ts │ │ └── bizMindEdge.ts │ ├── index.ts │ ├── nodes │ │ ├── bizFlowNode.ts │ │ ├── bizMindNode.ts │ │ └── bizNode.ts │ └── utils │ │ └── index.ts └── utils │ └── index.ts ├── tsconfig.cjs.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /lib 3 | /es 4 | 5 | /examples/dist 6 | /node_modules 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | }, 5 | parser: '@typescript-eslint/parser', 6 | extends: [ 7 | 'plugin:@typescript-eslint/recommended', 8 | 'plugin:react/recommended', 9 | 'plugin:prettier/recommended', 10 | 'prettier/@typescript-eslint', 11 | ], 12 | plugins: ['@typescript-eslint', 'react', 'prettier'], 13 | rules: { 14 | '@typescript-eslint/explicit-function-return-type': 0, 15 | '@typescript-eslint/no-empty-function': 0, 16 | '@typescript-eslint/no-empty-interface': 0, 17 | '@typescript-eslint/no-explicit-any': 0, 18 | 'react/prop-types': 0, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 错误报告 3 | about: 报告一个可以复现的错误 4 | --- 5 | 6 | ## 版本 7 | 8 | > 请检查问题是否存在于最新版本中 9 | 10 | ## 环境 11 | 12 | > 系统、浏览器、React 等版本信息 13 | 14 | ## 重现链接 15 | 16 | > 最小化重现错误以便精准定位问题,例如:[Flow](https://codesandbox.io/s/ggeditor-flow-hq64m) / [Mind](https://codesandbox.io/s/ggeditor-mind-2262q) 17 | 18 | ## 重现步骤 19 | 20 | > 清晰的重现步骤以便迅速定位问题 21 | 22 | ## 期望的结果是什么 23 | 24 | ## 实际的结果是什么 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ 功能要求 3 | about: 提交一个要求实现的功能 4 | --- 5 | 6 | ## 这个功能解决了什么问题 7 | 8 | ## 你所期望的 API 是怎样的 9 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - run: npm install 14 | - run: npm run docs:build 15 | - name: Deploy 16 | uses: peaceiris/actions-gh-pages@v3 17 | with: 18 | github_token: ${{ secrets.GITHUB_TOKEN }} 19 | publish_dir: ./dist 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /node_modules 4 | 5 | /dist 6 | /lib 7 | /es 8 | 9 | .umi 10 | .umi-production 11 | .env.local 12 | 13 | .eslintcache 14 | /examples/dist 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /examples/**/dist 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 11 5 | 6 | script: 7 | - npm run lint 8 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | mode: 'site', 5 | logo: 'https://img.alicdn.com/tfs/TB1FFA1CFP7gK0jSZFjXXc5aXXa-214-200.png', 6 | title: 'GGEditor', 7 | favicon: 'https://img.alicdn.com/tfs/TB1Des3CNn1gK0jSZKPXXXvUXXa-16-16.ico', 8 | navs: { 9 | 'en-US': [ 10 | null, 11 | { 12 | title: 'GitHub', 13 | path: 'https://github.com/alibaba/GGEditor', 14 | }, 15 | { 16 | title: 'Changelog', 17 | path: 'https://github.com/alibaba/GGEditor/blob/master/CHANGELOG.md', 18 | }, 19 | ], 20 | 'zh-CN': [ 21 | null, 22 | { 23 | title: 'GitHub', 24 | path: 'https://github.com/alibaba/GGEditor', 25 | }, 26 | { 27 | title: '更新日志', 28 | path: 'https://github.com/alibaba/GGEditor/blob/master/CHANGELOG.md', 29 | }, 30 | ], 31 | }, 32 | exportStatic: {}, 33 | styles: ['https://g.alicdn.com/code/lib/antd/3.23.6/antd.min.css'], 34 | scripts: [ 35 | 'https://g.alicdn.com/code/lib/react/16.8.6/umd/react.production.min.js', 36 | 'https://g.alicdn.com/code/lib/react-dom/16.8.6/umd/react-dom.production.min.js', 37 | 'https://g.alicdn.com/code/lib/moment.js/2.24.0/moment.min.js', 38 | 'https://g.alicdn.com/code/lib/antd/3.23.6/antd.min.js', 39 | ], 40 | externals: { 41 | react: 'React', 42 | 'react-dom': 'ReactDOM', 43 | antd: 'antd', 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.1.3](https://github.com/alibaba/GGEditor/compare/3.1.2...3.1.3) (2020-07-10) 2 | 3 | ### Bug Fixes 4 | 5 | - fix `onAfterConnect` event ([a704aec](https://github.com/alibaba/GGEditor/commit/a704aec5a115020980f88231b1dd3c6442380fdb)) 6 | 7 | ## [3.1.2](https://github.com/alibaba/GGEditor/compare/3.1.1...3.1.2) (2020-06-11) 8 | 9 | ### Bug Fixes 10 | 11 | - fix for getting of recall edges ([bb7005c](https://github.com/alibaba/GGEditor/commit/bb7005cea51c7faef500e124fe4f5bfa84416910)), closes [#489](https://github.com/alibaba/GGEditor/issues/489) 12 | - update type assertion ([f2eb776](https://github.com/alibaba/GGEditor/commit/f2eb776c30d69d2a3e76734433ae1e838087ed19)) 13 | 14 | ## [3.1.1](https://github.com/alibaba/GGEditor/compare/3.1.0...3.1.1) (2020-04-22) 15 | 16 | ### Bug Fixes 17 | 18 | - distinguish class component and function component ([565159f](https://github.com/alibaba/GGEditor/commit/565159ffa6acc13ccb41425a084be7984c03d528)) 19 | - suppress content editable warning ([5f4135c](https://github.com/alibaba/GGEditor/commit/5f4135c2ddddb16a116d00ed95bef208d82144f2)) 20 | 21 | # [3.1.0](https://github.com/alibaba/GGEditor/compare/3.0.6...3.1.0) (2020-04-07) 22 | 23 | ### Bug Fixes 24 | 25 | - copying a model should be a deep copy ([03f9875](https://github.com/alibaba/GGEditor/commit/03f9875ba8997565e0a15df1ff7dcdcc44de70e6)) 26 | - the `GraphEvent` parameter may be empty ([d9476dd](https://github.com/alibaba/GGEditor/commit/d9476dd036a715afb0dfeccd615f7834447a926e)) 27 | 28 | ### Features 29 | 30 | - `withEditorContext` supports functional component ([e7e3512](https://github.com/alibaba/GGEditor/commit/e7e3512488dd46726b202f8061854ec28a2f6b7f)) 31 | - refactor the anchor point of the node ([9edbf9a](https://github.com/alibaba/GGEditor/commit/9edbf9a1ff43dcb213b05e4ba48d779d0453fee3)) 32 | - upgrade to G6 3.3.x ([104c6c1](https://github.com/alibaba/GGEditor/commit/104c6c1bf2ce600b011b6012a5b9c39fafa178aa)) 33 | 34 | ## [3.0.6](https://github.com/alibaba/GGEditor/compare/3.0.5...3.0.6) (2020-03-24) 35 | 36 | ### Features 37 | 38 | - command manager is not singleton ([962779a](https://github.com/alibaba/GGEditor/commit/962779a573570db46529615416ca5c295a8c868f)) 39 | 40 | ## [3.0.5](https://github.com/alibaba/GGEditor/compare/3.0.4...3.0.5) (2020-02-11) 41 | 42 | ### Bug Fixes 43 | 44 | - maybe parent node is undefined ([3d3c3a4](https://github.com/alibaba/GGEditor/commit/3d3c3a42bd99944bd68b140c2d657ce428e76536)) 45 | 46 | ## [3.0.4](https://github.com/alibaba/GGEditor/compare/3.0.3...3.0.4) (2020-02-11) 47 | 48 | ### Bug Fixes 49 | 50 | - optimize the `shouldTriggerShortcut` method ([9fd1db9](https://github.com/alibaba/GGEditor/commit/9fd1db9a8370b713219d280786760de43d29310f)) 51 | - the edges may have been removed ([63045b5](https://github.com/alibaba/GGEditor/commit/63045b5e2c4c0f7b926c8bb521c664f0ac10e2c0)) 52 | 53 | ### Features 54 | 55 | - export the `global` instance ([c1ff342](https://github.com/alibaba/GGEditor/commit/c1ff3426c3b34f27e7ecf467d581faae806bc4c7)) 56 | - focus on the root node of the mind graph ([3025a2d](https://github.com/alibaba/GGEditor/commit/3025a2d7d149b45185b5e5d29b2dca88a81cbeab)) 57 | 58 | ## [3.0.3](https://github.com/alibaba/GGEditor/compare/3.0.2...3.0.3) (2020-02-06) 59 | 60 | ### Bug Fixes 61 | 62 | - add `shouldTriggerShortcut` method ([3880958](https://github.com/alibaba/GGEditor/commit/3880958396900ec44a6b6dfe4209b33ba4f93ffa)) 63 | 64 | ### Features 65 | 66 | - export `G6` and `baseCommand` ([eb103c7](https://github.com/alibaba/GGEditor/commit/eb103c722b2a075ecb04c302507fcbb9e0d27a57)) 67 | 68 | ## [3.0.2](https://github.com/alibaba/GGEditor/compare/3.0.1...3.0.2) (2020-02-04) 69 | 70 | ### Features 71 | 72 | - export `commandManager` and `behaviorManager` ([a67fb0a](https://github.com/alibaba/GGEditor/commit/a67fb0a110e6ea49fad9aec5ae41879e137d67f0)) 73 | 74 | ## [3.0.1](https://github.com/alibaba/GGEditor/compare/3.0.0...3.0.1) (2020-01-15) 75 | 76 | ### Bug Fixes 77 | 78 | - check if edge is destroyed ([e6c2e06](https://github.com/alibaba/GGEditor/commit/e6c2e06e2d20486c5708de5d9b4416debe55a357)) 79 | 80 | ### Features 81 | 82 | - add `fold` and `unfold` buttons for mind graph ([86a9067](https://github.com/alibaba/GGEditor/commit/86a90673d05831d07987c2bec69913663dc35ac3)) 83 | 84 | # [3.0.0](https://github.com/alibaba/GGEditor/compare/2.0.4...3.0.0) (2020-01-09) 85 | 86 | ### Features 87 | 88 | - upgrade to G6 3.x ([a5e574d](https://github.com/alibaba/GGEditor/commit/a5e574dd4dc04a64801fe877b8df172707132f01)) 89 | 90 | ## [2.0.4](https://github.com/alibaba/GGEditor/compare/2.0.3...2.0.4) (2019-04-24) 91 | 92 | ### Bug Fixes 93 | 94 | - Fix the position of the edges of the group ([8ea67e4](https://github.com/alibaba/GGEditor/commit/8ea67e4231d16037269a291f3c4492162b3bcb6e)) 95 | 96 | ## [2.0.3](https://github.com/alibaba/GGEditor/compare/2.0.2...2.0.3) (2019-04-08) 97 | 98 | ### Features 99 | 100 | - Add the type definitions for the project ([b70ff04](https://github.com/alibaba/GGEditor/commit/b70ff043de0378509ca72699f5f48f19ccb44cb0)) 101 | 102 | ## [2.0.2](https://github.com/alibaba/GGEditor/compare/2.0.1...2.0.2) (2019-03-20) 103 | 104 | ### Bug Fixes 105 | 106 | - Add the setting of `corejs` option ([a635c64](https://github.com/alibaba/GGEditor/commit/a635c64e691d463cd5754404f8cbd2991df3b8cb)) 107 | 108 | ## [2.0.1](https://github.com/alibaba/GGEditor/compare/2.0.0...2.0.1) (2019-01-15) 109 | 110 | ### Bug Fixes 111 | 112 | - The root node of mind can not be selected ([735da00](https://github.com/alibaba/GGEditor/commit/735da00ad852cdbd0102cc232882a8fb32154ec6)) 113 | 114 | ### Features 115 | 116 | - **Minimap:** Import the `Minimap` component directly from `G6` ([4bba85e](https://github.com/alibaba/GGEditor/commit/4bba85e78f20e9ace1fc6368ede81faa6b35ca68)) 117 | 118 | # [2.0.0](https://github.com/alibaba/GGEditor/compare/1.5.2...2.0.0) (2019-01-02) 119 | 120 | ### Features 121 | 122 | - Replace `@antv/g6-editor` with `gg-editor-core` ([44c0e92](https://github.com/alibaba/GGEditor/commit/44c0e920dbcfcb8696519358f3200792c645fcc9)) 123 | 124 | ## [1.5.2](https://github.com/alibaba/GGEditor/compare/1.5.1...1.5.2) (2018-12-29) 125 | 126 | ### Features 127 | 128 | - Get the project version from the environment variables ([57bf784](https://github.com/alibaba/GGEditor/commit/57bf7840bd708cd8c8f8288c4b9df33fb5bd7fd6)) 129 | 130 | ## [1.5.1](https://github.com/alibaba/GGEditor/compare/1.5.0...1.5.1) (2018-12-28) 131 | 132 | ### Bug Fixes 133 | 134 | - Can't get the version of the project ([abc59fc](https://github.com/alibaba/GGEditor/commit/abc59fc70c4c7b321e92a8e50ec928cca44ff515)) 135 | 136 | # [1.5.0](https://github.com/alibaba/GGEditor/compare/1.4.0...1.5.0) (2018-12-28) 137 | 138 | ### Bug Fixes 139 | 140 | - Can't get the `pageId` property of the child class ([2e2b1fe](https://github.com/alibaba/GGEditor/commit/2e2b1fe6dd5e53a85cd08e73b092b7000003efef)) 141 | 142 | # [1.4.0](https://github.com/alibaba/GGEditor/compare/1.3.5...1.4.0) (2018-12-26) 143 | 144 | ### Features 145 | 146 | - Add the `Koni` component ([5fbbc8e](https://github.com/alibaba/GGEditor/commit/5fbbc8ee11729b8a7d1600e1c7d5cdf25c13fd13)) 147 | 148 | ## [1.3.5](https://github.com/alibaba/GGEditor/compare/1.3.4...1.3.5) (2018-12-14) 149 | 150 | ### Features 151 | 152 | - Record the version of the project ([8de4469](https://github.com/alibaba/GGEditor/commit/8de446943933f2d5cb1db2772962d9de1fc126b0)) 153 | - Upgrade the `track` method ([d2d41b8](https://github.com/alibaba/GGEditor/commit/d2d41b8a74be36acfe66cf55592e92dd3fd48721)) 154 | - **track:** Delay sending track messages ([f29aab6](https://github.com/alibaba/GGEditor/commit/f29aab6220ee0da5618fd2ceabc03766ca719795)) 155 | - **utils:** Add the `toQueryString` method ([2681afc](https://github.com/alibaba/GGEditor/commit/2681afc28ea485ff93dee8c5fbdf183fc29421c9)) 156 | - **utils:** Add the `uniqueId` method ([03d56e7](https://github.com/alibaba/GGEditor/commit/03d56e788e5de649b535c6ccefad58d3a2e9bddd)) 157 | 158 | ## [1.3.4](https://github.com/alibaba/GGEditor/compare/1.3.3...1.3.4) (2018-11-22) 159 | 160 | ## [1.3.3](https://github.com/alibaba/GGEditor/compare/1.3.2...1.3.3) (2018-11-06) 161 | 162 | ## [1.3.2](https://github.com/alibaba/GGEditor/compare/1.3.1...1.3.2) (2018-11-06) 163 | 164 | ### Features 165 | 166 | - Complement the mouse synthetic event ([4c3f0cf](https://github.com/alibaba/GGEditor/commit/4c3f0cf8bb024c7b8598ffd9ad7f2c1121dd4cba)) 167 | 168 | ## [1.3.1](https://github.com/alibaba/GGEditor/compare/1.3.0...1.3.1) (2018-11-02) 169 | 170 | # [1.3.0](https://github.com/alibaba/GGEditor/compare/1.2.5...1.3.0) (2018-10-24) 171 | 172 | ### Features 173 | 174 | - Replace the track method with the track method of G6Editor ([8356eee](https://github.com/alibaba/GGEditor/commit/8356eeee14561867f0ba8802f215a0d3fe9a9351)) 175 | 176 | ## [1.2.5](https://github.com/alibaba/GGEditor/compare/1.2.4...1.2.5) (2018-10-21) 177 | 178 | ### Features 179 | 180 | - **Context:** Forward the `ref` property ([c370409](https://github.com/alibaba/GGEditor/commit/c370409bc362bf668392d40db1fda4e682ad835f)) 181 | 182 | ## [1.2.4](https://github.com/alibaba/GGEditor/compare/1.2.3...1.2.4) (2018-10-21) 183 | 184 | ### Bug Fixes 185 | 186 | - **Register:** Pass the context down to the `Register` component ([2a32a9d](https://github.com/alibaba/GGEditor/commit/2a32a9d5d3395af7f28ece91a9d8f18f57af0a1d)) 187 | 188 | ## [1.2.3](https://github.com/alibaba/GGEditor/compare/1.2.2...1.2.3) (2018-10-19) 189 | 190 | ## [1.2.2](https://github.com/alibaba/GGEditor/compare/1.2.1...1.2.2) (2018-09-30) 191 | 192 | ### Bug Fixes 193 | 194 | - **Toolbar:** Register the `Toolbar` after the page added ([252662a](https://github.com/alibaba/GGEditor/commit/252662a028216fb0daf56fcd1de58c49c658dcf3)) 195 | 196 | ## [1.2.1](https://github.com/alibaba/GGEditor/compare/1.2.0...1.2.1) (2018-09-29) 197 | 198 | ### Features 199 | 200 | - **propsAPI:** Pass the `executeCommand` method ([dff1c1b](https://github.com/alibaba/GGEditor/commit/dff1c1b4a3294775278c40e898f11e26cd9cf792)) 201 | 202 | # [1.2.0](https://github.com/alibaba/GGEditor/compare/1.1.3...1.2.0) (2018-09-20) 203 | 204 | ### Bug Fixes 205 | 206 | - **Page:** Fix the propType of pageId ([3b6259b](https://github.com/alibaba/GGEditor/commit/3b6259b79e434b7a92e3e598b8bdc3e6f5891588)) 207 | - Update the components reference ([d748568](https://github.com/alibaba/GGEditor/commit/d748568a16d1c1d59f081a8b73764cdcc9be60c4)) 208 | 209 | ### Features 210 | 211 | - **GGEditor:** Add the `withPropsAPI` static method for the GGEditor ([d5602c6](https://github.com/alibaba/GGEditor/commit/d5602c6dcc4b31915ddb33aec7edd9d1210e29e7)) 212 | - **propsAPI:** Pass the `read` and `save` method of the current page ([a08fca8](https://github.com/alibaba/GGEditor/commit/a08fca86783b9035ebe9b3ed30ea1b801d374175)) 213 | 214 | ## [1.1.3](https://github.com/alibaba/GGEditor/compare/1.1.2...1.1.3) (2018-09-11) 215 | 216 | ### Bug Fixes 217 | 218 | - Listener support the components async render ([a27a1fa](https://github.com/alibaba/GGEditor/commit/a27a1fa7deebda471a3d724cff7bc0c5369d2880)) 219 | 220 | ## [1.1.2](https://github.com/alibaba/GGEditor/compare/1.1.1...1.1.2) (2018-09-10) 221 | 222 | ## [1.1.1](https://github.com/alibaba/GGEditor/compare/33ea4e005600c4d3a5aa10824b90a2281a1b18fa...1.1.1) (2018-09-06) 223 | 224 | ### Bug Fixes 225 | 226 | - **constants.js:** Rename event onBeforeItemUnactived to onBeforeItemInactivated as unactived is not ([9a15675](https://github.com/alibaba/GGEditor/commit/9a15675464e945eb43f5886cfafea655ec186b61)) 227 | - **DetailPannel:** Fix repeat render ([d1bfb78](https://github.com/alibaba/GGEditor/commit/d1bfb789f831c8d65a9557227b311a751966f7c7)) 228 | - Fix onKeyUpEditLabel event ([a4cafda](https://github.com/alibaba/GGEditor/commit/a4cafdafaa617bb41a870ab05d073417a6df8ed9)) 229 | - Fix read data after component props changed ([56bb985](https://github.com/alibaba/GGEditor/commit/56bb98559df152573a94a1868cb4372c7ee6e83a)) 230 | - Fix register command ([3022645](https://github.com/alibaba/GGEditor/commit/3022645956911990c459dfe747aaa6a631a056a1)) 231 | - Rename events name ([ed3a301](https://github.com/alibaba/GGEditor/commit/ed3a301d16992b7f3660ac5eaafa07ad83043e2d)) 232 | - **Flow:** Add the default data for the Flow component ([a73394c](https://github.com/alibaba/GGEditor/commit/a73394c126680b732c41ab822eae2050c53cb0b5)) 233 | - **Flow:** Add the default props for the Flow component ([b091bb9](https://github.com/alibaba/GGEditor/commit/b091bb93f6ac9c727b931676476413e53ca5bdee)) 234 | - **ItemPanel:** To separate the typo Itempannel from the code base. ([c54f6d7](https://github.com/alibaba/GGEditor/commit/c54f6d700d0c0ee346db2f4a576406aadeedfe47)) 235 | - **track.js:** Update the format of the `page` param ([2594366](https://github.com/alibaba/GGEditor/commit/25943660a0f5b1da11875cfe094341717b2ef52f)) 236 | 237 | ### Features 238 | 239 | - Execute `read` method after page added ([25e41d1](https://github.com/alibaba/GGEditor/commit/25e41d13b9d375bc3f696e21dc7a54f56a155255)) 240 | - **ContextMenu:** The menu container can toggle display status based on the page status ([8c294c9](https://github.com/alibaba/GGEditor/commit/8c294c9f07aa576f4800e07f01d006ef7559207f)) 241 | - Init ([33ea4e0](https://github.com/alibaba/GGEditor/commit/33ea4e005600c4d3a5aa10824b90a2281a1b18fa)) 242 | - **Base/Editor:** Call track funciton when the either Mind or Flow is added on the page ([e0d974c](https://github.com/alibaba/GGEditor/commit/e0d974c3b98a3d07aa516dfab6ddac2e025ee9fe)) 243 | - **BaseComponent:** Rename Base/index into Base/BaseComponent ([e1af153](https://github.com/alibaba/GGEditor/commit/e1af1531b5cece14b106be32542824d86df4abfe)) 244 | - **ContextMenu:** Pass the `className` and `style` props to the ContextMenu component ([74a32ef](https://github.com/alibaba/GGEditor/commit/74a32ef4393af91f8d517941cda67fc4d327a4a8)) 245 | - **Global:** Add Global for storing global variables ([2703203](https://github.com/alibaba/GGEditor/commit/27032038f42ab3f5ab355bf6168d3b6f517f9ee7)) 246 | - **Minimap:** Resize automatically ([c125f94](https://github.com/alibaba/GGEditor/commit/c125f948b7c0f58f03cd61b6eebad781761da8df)) 247 | - **PropsAPI:** Add PropsAPI class for exposing the API as the props for the child components in con ([51b199d](https://github.com/alibaba/GGEditor/commit/51b199da5100def86dd53dedb3b3585f39c6762d)) 248 | - **utils:** Add toQueryString into utils ([b8d0981](https://github.com/alibaba/GGEditor/commit/b8d0981dc1ffbdfd92751877a17fe1a0886b607a)) 249 | - **webpack.config.base:** Add GG_EDITOR_VERSION, G6_VERSION, G6_EDITOR_VERSION from package.json as ([2440d0d](https://github.com/alibaba/GGEditor/commit/2440d0d4b62ffbbe5cf66057b4e021dc59b5a3b1)) 250 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 gaoli <3071730@qq.com> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.en-US.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](README.md) 2 | 3 |

GGEditor

4 | 5 |
6 | 7 | A visual graph editor based on [G6](https://github.com/antvis/g6) and [React](https://github.com/facebook/react). 8 | 9 | [![GitHub](https://img.shields.io/github/license/alibaba/GGEditor)](/LICENSE) 10 | [![npm](https://img.shields.io/npm/v/gg-editor)](https://www.npmjs.com/package/gg-editor) 11 | [![npm](https://img.shields.io/npm/dm/gg-editor)](https://www.npmjs.com/package/gg-editor) 12 | 13 |
14 | 15 | ## Installation 16 | 17 | ### npm 18 | 19 | ```bash 20 | npm install gg-editor --save 21 | ``` 22 | 23 | ### umd 24 | 25 | ```html 26 | 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Flow 32 | 33 | [![Edit GGEditor - Flow](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/ggeditor-flow-hq64m?fontsize=14&hidenavigation=1&theme=dark) 34 | 35 | ```jsx 36 | import GGEditor, { Flow } from 'gg-editor'; 37 | 38 | const data = { 39 | nodes: [ 40 | { 41 | id: '0', 42 | label: 'Node', 43 | x: 55, 44 | y: 55, 45 | }, 46 | { 47 | id: '1', 48 | label: 'Node', 49 | x: 55, 50 | y: 255, 51 | }, 52 | ], 53 | edges: [ 54 | { 55 | label: 'Label', 56 | source: '0', 57 | target: '1', 58 | }, 59 | ], 60 | }; 61 | 62 | 63 | 64 | ; 65 | ``` 66 | 67 | ### Mind 68 | 69 | [![Edit GGEditor - Mind](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/ggeditor-mind-2262q?fontsize=14&hidenavigation=1&theme=dark) 70 | 71 | ```jsx 72 | import GGEditor, { Mind } from 'gg-editor'; 73 | 74 | const data = { 75 | label: 'Central Topic', 76 | children: [ 77 | { 78 | label: 'Main Topic 1', 79 | }, 80 | { 81 | label: 'Main Topic 2', 82 | }, 83 | { 84 | label: 'Main Topic 3', 85 | }, 86 | ], 87 | }; 88 | 89 | 90 | 91 | ; 92 | ``` 93 | 94 | ## Examples 95 | 96 | ```bash 97 | # Clone the repository 98 | $ git clone https://github.com/alibaba/GGEditor.git 99 | 100 | # Change directory 101 | $ cd gg-editor 102 | 103 | # Install dependencies 104 | $ npm install 105 | 106 | # Run examples 107 | $ npm start 108 | ``` 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English](README.en-US.md) | 简体中文 2 | 3 |

GGEditor

4 | 5 |
6 | 7 | 基于 [G6](https://github.com/antvis/g6) 和 [React](https://github.com/facebook/react) 的可视化图编辑器 8 | 9 | [![GitHub](https://img.shields.io/github/license/alibaba/GGEditor)](LICENSE) 10 | [![npm](https://img.shields.io/npm/v/gg-editor)](https://www.npmjs.com/package/gg-editor) 11 | [![npm](https://img.shields.io/npm/dm/gg-editor)](https://www.npmjs.com/package/gg-editor) 12 | 13 |
14 | 15 | ## 安装 16 | 17 | ### npm 18 | 19 | ```bash 20 | npm install gg-editor --save 21 | ``` 22 | 23 | ### umd 24 | 25 | ```html 26 | 27 | ``` 28 | 29 | ## 使用 30 | 31 | ### 流程图 32 | 33 | [![Edit GGEditor - Flow](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/ggeditor-flow-hq64m?fontsize=14&hidenavigation=1&theme=dark) 34 | 35 | ```jsx 36 | import GGEditor, { Flow } from 'gg-editor'; 37 | 38 | const data = { 39 | nodes: [ 40 | { 41 | id: '0', 42 | label: 'Node', 43 | x: 55, 44 | y: 55, 45 | }, 46 | { 47 | id: '1', 48 | label: 'Node', 49 | x: 55, 50 | y: 255, 51 | }, 52 | ], 53 | edges: [ 54 | { 55 | label: 'Label', 56 | source: '0', 57 | target: '1', 58 | }, 59 | ], 60 | }; 61 | 62 | 63 | 64 | ; 65 | ``` 66 | 67 | ### 脑图 68 | 69 | [![Edit GGEditor - Mind](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/ggeditor-mind-2262q?fontsize=14&hidenavigation=1&theme=dark) 70 | 71 | ```jsx 72 | import GGEditor, { Mind } from 'gg-editor'; 73 | 74 | const data = { 75 | label: 'Central Topic', 76 | children: [ 77 | { 78 | label: 'Main Topic 1', 79 | }, 80 | { 81 | label: 'Main Topic 2', 82 | }, 83 | { 84 | label: 'Main Topic 3', 85 | }, 86 | ], 87 | }; 88 | 89 | 90 | 91 | ; 92 | ``` 93 | 94 | ## 示例 95 | 96 | ```bash 97 | # 克隆仓库 98 | $ git clone https://github.com/alibaba/GGEditor.git 99 | 100 | # 切换目录 101 | $ cd gg-editor 102 | 103 | # 安装依赖 104 | $ npm install 105 | 106 | # 运行示例 107 | $ npm start 108 | ``` 109 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | 4 | const presets = [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | loose: true, 9 | modules: false, 10 | }, 11 | ], 12 | '@babel/preset-react', 13 | ]; 14 | 15 | const plugins = ['lodash']; 16 | 17 | return { 18 | presets, 19 | plugins, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /docs/examples/component/command.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Command 3 | group: 4 | title: Component 5 | order: 2 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/component/command.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 命令 3 | group: 4 | title: 组件 5 | order: 2 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/component/detail-panel.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DetailPanel 3 | group: 4 | title: Component 5 | order: 2 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/component/detail-panel.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 详情面板 3 | group: 4 | title: 组件 5 | order: 2 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/component/item-panel.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ItemPanel 3 | group: 4 | title: Component 5 | order: 2 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/component/item-panel.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 元素面板 3 | group: 4 | title: 组件 5 | order: 2 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/editor/context.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Context 3 | group: 4 | title: Editor 5 | order: 0 6 | nav: 7 | title: Examples 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/examples/editor/context.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 上下文 3 | group: 4 | title: 编辑器 5 | order: 0 6 | nav: 7 | title: 演示 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/examples/graph/flow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Flow 3 | group: 4 | title: Graph 5 | order: 1 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/graph/flow.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 流程图 3 | group: 4 | title: 图表 5 | order: 1 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/graph/mind.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mind 3 | group: 4 | title: Graph 5 | order: 1 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/graph/mind.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 脑图 3 | group: 4 | title: 图表 5 | order: 1 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/plugin/context-menu.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ContextMenu 3 | group: 4 | title: Plugin 5 | order: 3 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/plugin/context-menu.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 右键菜单 3 | group: 4 | title: 插件 5 | order: 3 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/plugin/editable-label.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: EditableLabel 3 | group: 4 | title: Plugin 5 | order: 3 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/plugin/editable-label.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 标签编辑 3 | group: 4 | title: 插件 5 | order: 3 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/plugin/item-popover.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ItemPopover 3 | group: 4 | title: Plugin 5 | order: 3 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/plugin/item-popover.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 元素浮层 3 | group: 4 | title: 插件 5 | order: 3 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/register/dom-node.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DOMNode 3 | group: 4 | title: Register 5 | order: 4 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/register/dom-node.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DOM 节点 3 | group: 4 | title: 注册 5 | order: 4 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/register/node.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Node 3 | group: 4 | title: Register 5 | order: 4 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/examples/register/node.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 节点 3 | group: 4 | title: 注册 5 | order: 4 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | group: 4 | title: Guide 5 | order: 0 6 | nav: 7 | title: Guide 8 | --- 9 | 10 | ## Installation 11 | 12 | ### npm 13 | 14 | ```bash | pure 15 | npm install gg-editor --save 16 | ``` 17 | 18 | ### umd 19 | 20 | ```html | pure 21 | 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Flow 27 | 28 | [![Edit GGEditor - Flow](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/ggeditor-flow-hq64m?fontsize=14&hidenavigation=1&theme=dark) 29 | 30 | ```jsx | pure 31 | import GGEditor, { Flow } from 'gg-editor'; 32 | 33 | const data = { 34 | nodes: [ 35 | { 36 | id: '0', 37 | label: 'Node', 38 | x: 55, 39 | y: 55, 40 | }, 41 | { 42 | id: '1', 43 | label: 'Node', 44 | x: 55, 45 | y: 255, 46 | }, 47 | ], 48 | edges: [ 49 | { 50 | label: 'Label', 51 | source: '0', 52 | target: '1', 53 | }, 54 | ], 55 | }; 56 | 57 | 58 | 59 | ; 60 | ``` 61 | 62 | ### Mind 63 | 64 | [![Edit GGEditor - Mind](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/ggeditor-mind-2262q?fontsize=14&hidenavigation=1&theme=dark) 65 | 66 | ```jsx | pure 67 | import GGEditor, { Mind } from 'gg-editor'; 68 | 69 | const data = { 70 | label: 'Central Topic', 71 | children: [ 72 | { 73 | label: 'Main Topic 1', 74 | }, 75 | { 76 | label: 'Main Topic 2', 77 | }, 78 | { 79 | label: 'Main Topic 3', 80 | }, 81 | ], 82 | }; 83 | 84 | 85 | 86 | ; 87 | ``` 88 | 89 | ## Examples 90 | 91 | ```bash | pure 92 | # Clone the repository 93 | $ git clone https://github.com/alibaba/GGEditor.git 94 | 95 | # Change directory 96 | $ cd gg-editor 97 | 98 | # Install dependencies 99 | $ npm install 100 | 101 | # Run examples 102 | $ npm start 103 | ``` 104 | -------------------------------------------------------------------------------- /docs/guide/getting-started.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速上手 3 | group: 4 | title: 指南 5 | order: 0 6 | nav: 7 | title: 指南 8 | --- 9 | 10 | ## 安装 11 | 12 | ### npm 13 | 14 | ```bash | pure 15 | npm install gg-editor --save 16 | ``` 17 | 18 | ### umd 19 | 20 | ```html | pure 21 | 22 | ``` 23 | 24 | ## 使用 25 | 26 | ### 流程图 27 | 28 | [![Edit GGEditor - Flow](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/ggeditor-flow-hq64m?fontsize=14&hidenavigation=1&theme=dark) 29 | 30 | ```jsx | pure 31 | import GGEditor, { Flow } from 'gg-editor'; 32 | 33 | const data = { 34 | nodes: [ 35 | { 36 | id: '0', 37 | label: 'Node', 38 | x: 55, 39 | y: 55, 40 | }, 41 | { 42 | id: '1', 43 | label: 'Node', 44 | x: 55, 45 | y: 255, 46 | }, 47 | ], 48 | edges: [ 49 | { 50 | label: 'Label', 51 | source: '0', 52 | target: '1', 53 | }, 54 | ], 55 | }; 56 | 57 | 58 | 59 | ; 60 | ``` 61 | 62 | ### 脑图 63 | 64 | [![Edit GGEditor - Mind](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/ggeditor-mind-2262q?fontsize=14&hidenavigation=1&theme=dark) 65 | 66 | ```jsx | pure 67 | import GGEditor, { Mind } from 'gg-editor'; 68 | 69 | const data = { 70 | label: 'Central Topic', 71 | children: [ 72 | { 73 | label: 'Main Topic 1', 74 | }, 75 | { 76 | label: 'Main Topic 2', 77 | }, 78 | { 79 | label: 'Main Topic 3', 80 | }, 81 | ], 82 | }; 83 | 84 | 85 | 86 | ; 87 | ``` 88 | 89 | ## 示例 90 | 91 | ```bash | pure 92 | # 克隆仓库 93 | $ git clone https://github.com/alibaba/GGEditor.git 94 | 95 | # 切换目录 96 | $ cd gg-editor 97 | 98 | # 安装依赖 99 | $ npm install 100 | 101 | # 运行示例 102 | $ npm start 103 | ``` 104 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GGEditor - A visual graph editor based on G6 and React 3 | hero: 4 | title: GGEditor 5 | desc: A visual graph editor based on G6 and React 6 | actions: 7 | - text: Getting Started 8 | link: /guide/getting-started 9 | footer: Open-source MIT Licensed | Copyright © 2020
Powered by [dumi](https://d.umijs.org) 10 | --- 11 | -------------------------------------------------------------------------------- /docs/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GGEditor - 基于 G6 和 React 的可视化图编辑器 3 | hero: 4 | title: GGEditor 5 | desc: 基于 G6 和 React 的可视化图编辑器 6 | actions: 7 | - text: 快速上手 8 | link: /zh-CN/guide/getting-started 9 | footer: Open-source MIT Licensed | Copyright © 2020
Powered by [dumi](https://d.umijs.org) 10 | --- 11 | -------------------------------------------------------------------------------- /examples/component/command/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | } 5 | 6 | .toolbar { 7 | display: flex; 8 | align-items: center; 9 | padding: 8px; 10 | border-bottom: 1px solid #e8e8e8; 11 | background: #ffffff; 12 | 13 | .command i { 14 | display: inline-block; 15 | width: 27px; 16 | height: 27px; 17 | margin: 0 6px; 18 | padding-top: 6px; 19 | text-align: center; 20 | border: 1px solid #fff; 21 | cursor: pointer; 22 | 23 | &:hover { 24 | border: 1px solid #e8e8e8; 25 | } 26 | } 27 | 28 | .commandDisabled i { 29 | color: rgba(0, 0, 0, 0.25); 30 | cursor: auto; 31 | 32 | &:hover { 33 | border: 1px solid #fff; 34 | } 35 | } 36 | } 37 | 38 | .contextMenu { 39 | background: #ffffff; 40 | border-radius: 4px; 41 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 42 | overflow: hidden; 43 | 44 | .command { 45 | cursor: pointer; 46 | display: flex; 47 | align-items: center; 48 | padding: 5px 12px; 49 | transition: all 0.3s; 50 | user-select: none; 51 | 52 | &:hover { 53 | background: #e6f7ff; 54 | } 55 | 56 | i { 57 | margin-right: 8px; 58 | } 59 | } 60 | 61 | .commandDisabled { 62 | cursor: auto; 63 | color: rgba(0, 0, 0, 0.25); 64 | 65 | &:hover { 66 | background: #fff; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/component/command/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import upperFirst from 'lodash/upperFirst'; 3 | import { Icon, Divider, Tooltip } from 'antd'; 4 | import GGEditor, { Flow, Mind, Command, ContextMenu, constants } from 'gg-editor'; 5 | import { MindData } from 'gg-editor/lib/common/interfaces'; 6 | import styles from './index.less'; 7 | 8 | const { EditorCommand } = constants; 9 | 10 | const IconFont = Icon.createFromIconfontCN({ 11 | scriptUrl: 'https://at.alicdn.com/t/font_1518433_oa5sw7ezue.js', 12 | }); 13 | 14 | const FLOW_COMMAND_LIST = [ 15 | EditorCommand.Undo, 16 | EditorCommand.Redo, 17 | '|', 18 | EditorCommand.Copy, 19 | EditorCommand.Paste, 20 | EditorCommand.Remove, 21 | '|', 22 | EditorCommand.ZoomIn, 23 | EditorCommand.ZoomOut, 24 | ]; 25 | 26 | const MIND_COMMAND_LIST = [ 27 | EditorCommand.Undo, 28 | EditorCommand.Redo, 29 | '|', 30 | EditorCommand.Copy, 31 | EditorCommand.Paste, 32 | EditorCommand.Remove, 33 | '|', 34 | EditorCommand.Topic, 35 | EditorCommand.Subtopic, 36 | '|', 37 | EditorCommand.Fold, 38 | EditorCommand.Unfold, 39 | '|', 40 | EditorCommand.ZoomIn, 41 | EditorCommand.ZoomOut, 42 | ]; 43 | 44 | const flowData = { 45 | nodes: [ 46 | { 47 | id: '0', 48 | label: 'Node', 49 | x: 50, 50 | y: 50, 51 | }, 52 | { 53 | id: '1', 54 | label: 'Node', 55 | x: 50, 56 | y: 200, 57 | }, 58 | ], 59 | edges: [ 60 | { 61 | label: 'Label', 62 | source: '0', 63 | sourceAnchor: 1, 64 | target: '1', 65 | targetAnchor: 0, 66 | }, 67 | ], 68 | }; 69 | 70 | const mindData: MindData = { 71 | id: '0', 72 | label: 'Central Topic', 73 | children: [ 74 | { 75 | id: '1', 76 | side: 'left', 77 | label: 'Main Topic 1', 78 | }, 79 | { 80 | id: '2', 81 | side: 'right', 82 | label: 'Main Topic 2', 83 | }, 84 | { 85 | id: '3', 86 | side: 'right', 87 | label: 'Main Topic 3', 88 | }, 89 | ], 90 | }; 91 | 92 | function App() { 93 | return ( 94 | 95 |
96 | {FLOW_COMMAND_LIST.map((name, index) => { 97 | if (name === '|') { 98 | return ; 99 | } 100 | 101 | return ( 102 | 103 | 104 | 105 | 106 | 107 | ); 108 | })} 109 |
110 | 111 | {/* */} 112 | { 114 | const { x: left, y: top } = position; 115 | 116 | return ( 117 |
118 | {[EditorCommand.Undo, EditorCommand.Redo, EditorCommand.PasteHere].map(name => { 119 | return ( 120 | 121 |
122 | 123 | {upperFirst(name)} 124 |
125 |
126 | ); 127 | })} 128 |
129 | ); 130 | }} 131 | /> 132 |
133 | ); 134 | } 135 | 136 | export default App; 137 | -------------------------------------------------------------------------------- /examples/component/detail-panel/Panel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import upperFirst from 'lodash/upperFirst'; 3 | import { Card, Form, Input } from 'antd'; 4 | import { FormComponentProps } from 'antd/lib/form'; 5 | import { DetailPanel, withEditorContext } from 'gg-editor'; 6 | import { EditorContextProps } from 'gg-editor/lib/components/EditorContext'; 7 | import { DetailPanelComponentProps } from 'gg-editor/lib/components/DetailPanel'; 8 | 9 | const { Item } = Form; 10 | 11 | const formItemLayout = { 12 | labelCol: { 13 | span: 5, 14 | }, 15 | wrapperCol: { 16 | span: 19, 17 | }, 18 | }; 19 | 20 | interface PanelProps extends FormComponentProps, EditorContextProps, DetailPanelComponentProps {} 21 | 22 | interface PanelState {} 23 | 24 | class Panel extends React.Component { 25 | handleSubmit = (e: React.FocusEvent) => { 26 | if (e && e.preventDefault) { 27 | e.preventDefault(); 28 | } 29 | 30 | const { form } = this.props; 31 | 32 | form.validateFieldsAndScroll((err, values) => { 33 | if (err) { 34 | return; 35 | } 36 | 37 | const { type, nodes, edges, executeCommand } = this.props; 38 | 39 | const item = type === 'node' ? nodes[0] : edges[0]; 40 | 41 | if (!item) { 42 | return; 43 | } 44 | 45 | executeCommand('update', { 46 | id: item.get('id'), 47 | updateModel: { 48 | ...values, 49 | }, 50 | }); 51 | }); 52 | }; 53 | 54 | renderNodeDetail = () => { 55 | const { form } = this.props; 56 | 57 | return ( 58 |
59 | 60 | {form.getFieldDecorator('label', { 61 | initialValue: '', 62 | })()} 63 | 64 |
65 | ); 66 | }; 67 | 68 | renderEdgeDetail = () => { 69 | const { form } = this.props; 70 | 71 | return ( 72 |
73 | 74 | {form.getFieldDecorator('label', { 75 | initialValue: '', 76 | })()} 77 | 78 |
79 | ); 80 | }; 81 | 82 | renderMultiDetail = () => { 83 | return null; 84 | }; 85 | 86 | renderCanvasDetail = () => { 87 | return

Select a node or edge :)

; 88 | }; 89 | 90 | render() { 91 | const { type } = this.props; 92 | 93 | return ( 94 | 95 | {type === 'node' && this.renderNodeDetail()} 96 | {type === 'edge' && this.renderEdgeDetail()} 97 | {type === 'multi' && this.renderMultiDetail()} 98 | {type === 'canvas' && this.renderCanvasDetail()} 99 | 100 | ); 101 | } 102 | } 103 | 104 | const WrappedPanel = Form.create({ 105 | mapPropsToFields(props) { 106 | const { type, nodes, edges } = props; 107 | 108 | let label = ''; 109 | 110 | if (type === 'node') { 111 | label = nodes[0].getModel().label; 112 | } 113 | 114 | if (type === 'edge') { 115 | label = edges[0].getModel().label; 116 | } 117 | 118 | return { 119 | label: Form.createFormField({ 120 | value: label, 121 | }), 122 | }; 123 | }, 124 | })(withEditorContext(Panel)); 125 | 126 | type WrappedPanelProps = Omit; 127 | 128 | export const NodePanel = DetailPanel.create('node')(WrappedPanel); 129 | export const EdgePanel = DetailPanel.create('edge')(WrappedPanel); 130 | export const MultiPanel = DetailPanel.create('multi')(WrappedPanel); 131 | export const CanvasPanel = DetailPanel.create('canvas')(WrappedPanel); 132 | -------------------------------------------------------------------------------- /examples/component/detail-panel/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | overflow: hidden; 5 | } 6 | 7 | .detailPanel { 8 | float: right; 9 | width: 300px; 10 | height: 500px; 11 | background-color: #ffffff; 12 | border-left: 1px solid #e8e8e8; 13 | } 14 | -------------------------------------------------------------------------------- /examples/component/detail-panel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GGEditor, { Flow, EditableLabel } from 'gg-editor'; 3 | import { NodePanel, EdgePanel, MultiPanel, CanvasPanel } from './Panel'; 4 | import styles from './index.less'; 5 | 6 | const data = { 7 | nodes: [ 8 | { 9 | id: '0', 10 | label: 'Node', 11 | x: 50, 12 | y: 50, 13 | }, 14 | { 15 | id: '1', 16 | label: 'Node', 17 | x: 50, 18 | y: 200, 19 | }, 20 | ], 21 | edges: [ 22 | { 23 | label: 'Label', 24 | source: '0', 25 | sourceAnchor: 1, 26 | target: '1', 27 | targetAnchor: 0, 28 | }, 29 | ], 30 | }; 31 | 32 | function App() { 33 | return ( 34 | 35 |
36 | 37 | 38 | 39 | 40 |
41 | 42 | 43 |
44 | ); 45 | } 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /examples/component/item-panel/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | overflow: hidden; 5 | } 6 | 7 | .itemPanel { 8 | float: left; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | padding: 24px; 13 | height: 500px; 14 | background: #ffffff; 15 | border-right: 1px solid #e8e8e8; 16 | 17 | .item { 18 | margin-bottom: 24px; 19 | user-select: none; 20 | 21 | img { 22 | display: block; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/component/item-panel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GGEditor, { Flow, Item, ItemPanel } from 'gg-editor'; 3 | import styles from './index.less'; 4 | 5 | const data = { 6 | nodes: [], 7 | edges: [], 8 | }; 9 | 10 | function App() { 11 | return ( 12 | 13 | 14 | 22 | 28 | 29 | 37 | 43 | 44 | 52 | 58 | 59 | 67 | 73 | 74 | 82 | 88 | 89 | 90 | 91 | 92 | ); 93 | } 94 | 95 | export default App; 96 | -------------------------------------------------------------------------------- /examples/editor/context/WrappedClassComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withEditorContext } from 'gg-editor'; 3 | import { EditorContextProps } from 'gg-editor/lib/components/EditorContext'; 4 | 5 | interface WrappedClassComponentProps extends EditorContextProps {} 6 | 7 | class WrappedClassComponent extends React.Component { 8 | componentDidMount() { 9 | console.log('wrappedClassComponentProps:', this.props); 10 | } 11 | 12 | render() { 13 | return
; 14 | } 15 | } 16 | 17 | export default withEditorContext(WrappedClassComponent); 18 | -------------------------------------------------------------------------------- /examples/editor/context/WrappedFunctionComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withEditorContext } from 'gg-editor'; 3 | import { EditorContextProps } from 'gg-editor/lib/components/EditorContext'; 4 | 5 | interface WrappedFunctionComponentProps extends EditorContextProps { 6 | forwardRef?: React.Ref; 7 | } 8 | 9 | const WrappedFunctionComponent: React.FC = props => { 10 | console.log('wrappedFunctionComponentProps:', props); 11 | 12 | return
; 13 | }; 14 | 15 | export default withEditorContext(WrappedFunctionComponent); 16 | -------------------------------------------------------------------------------- /examples/editor/context/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | } 5 | -------------------------------------------------------------------------------- /examples/editor/context/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GGEditor, { Flow } from 'gg-editor'; 3 | import WrappedClassComponent from './WrappedClassComponent'; 4 | import WrappedFunctionComponent from './WrappedFunctionComponent'; 5 | import styles from './index.less'; 6 | 7 | const data = { 8 | nodes: [ 9 | { 10 | id: '0', 11 | label: 'Node', 12 | x: 50, 13 | y: 50, 14 | }, 15 | { 16 | id: '1', 17 | label: 'Node', 18 | x: 50, 19 | y: 200, 20 | }, 21 | ], 22 | edges: [ 23 | { 24 | label: 'Label', 25 | source: '0', 26 | sourceAnchor: 1, 27 | target: '1', 28 | targetAnchor: 0, 29 | }, 30 | ], 31 | }; 32 | 33 | function App() { 34 | return ( 35 | 36 | { 39 | if (component) { 40 | console.log('graph:', component.graph); 41 | } 42 | }} 43 | data={data} 44 | /> 45 | { 47 | console.log('wrappedClassComponentRef:', component); 48 | }} 49 | /> 50 | { 52 | console.log('wrappedFunctionComponentRef:', el); 53 | }} 54 | /> 55 | 56 | ); 57 | } 58 | 59 | export default App; 60 | -------------------------------------------------------------------------------- /examples/graph/flow/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | } 5 | -------------------------------------------------------------------------------- /examples/graph/flow/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GGEditor, { Flow } from 'gg-editor'; 3 | import styles from './index.less'; 4 | 5 | const data = { 6 | nodes: [ 7 | { 8 | id: '0', 9 | label: 'Node', 10 | x: 50, 11 | y: 50, 12 | }, 13 | { 14 | id: '1', 15 | label: 'Node', 16 | x: 50, 17 | y: 200, 18 | }, 19 | ], 20 | edges: [ 21 | { 22 | label: 'Label', 23 | source: '0', 24 | sourceAnchor: 1, 25 | target: '1', 26 | targetAnchor: 0, 27 | }, 28 | ], 29 | }; 30 | 31 | function App() { 32 | return ( 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /examples/graph/mind/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | } 5 | -------------------------------------------------------------------------------- /examples/graph/mind/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GGEditor, { Mind } from 'gg-editor'; 3 | import { MindData } from 'gg-editor/lib/common/interfaces'; 4 | import styles from './index.less'; 5 | 6 | const data: MindData = { 7 | id: '0', 8 | label: 'Central Topic', 9 | children: [ 10 | { 11 | id: '1', 12 | side: 'left', 13 | label: 'Main Topic 1', 14 | }, 15 | { 16 | id: '2', 17 | side: 'right', 18 | label: 'Main Topic 2', 19 | }, 20 | { 21 | id: '3', 22 | side: 'right', 23 | label: 'Main Topic 3', 24 | }, 25 | ], 26 | }; 27 | 28 | function App() { 29 | return ( 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /examples/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.less'; 2 | -------------------------------------------------------------------------------- /examples/plugin/context-menu/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | } 5 | -------------------------------------------------------------------------------- /examples/plugin/context-menu/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu } from 'antd'; 3 | import GGEditor, { Flow, ContextMenu } from 'gg-editor'; 4 | import styles from './index.less'; 5 | 6 | const data = { 7 | nodes: [ 8 | { 9 | id: '0', 10 | label: 'Node', 11 | x: 50, 12 | y: 50, 13 | }, 14 | { 15 | id: '1', 16 | label: 'Node', 17 | x: 50, 18 | y: 200, 19 | }, 20 | ], 21 | edges: [ 22 | { 23 | label: 'Label', 24 | source: '0', 25 | sourceAnchor: 1, 26 | target: '1', 27 | targetAnchor: 0, 28 | }, 29 | ], 30 | }; 31 | 32 | function App() { 33 | return ( 34 | 35 | 36 | { 38 | const { x: left, y: top } = position; 39 | 40 | return ( 41 |
42 | 43 | Option 1 44 | Option 2 45 | Option 3 46 | 47 |
48 | ); 49 | }} 50 | /> 51 |
52 | ); 53 | } 54 | 55 | export default App; 56 | -------------------------------------------------------------------------------- /examples/plugin/editable-label/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | } 5 | -------------------------------------------------------------------------------- /examples/plugin/editable-label/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GGEditor, { Flow, EditableLabel } from 'gg-editor'; 3 | import styles from './index.less'; 4 | 5 | const data = { 6 | nodes: [ 7 | { 8 | id: '0', 9 | label: 'Node', 10 | x: 50, 11 | y: 50, 12 | }, 13 | { 14 | id: '1', 15 | label: 'Node', 16 | x: 50, 17 | y: 200, 18 | }, 19 | ], 20 | edges: [ 21 | { 22 | label: 'Label', 23 | source: '0', 24 | sourceAnchor: 1, 25 | target: '1', 26 | targetAnchor: 0, 27 | }, 28 | ], 29 | }; 30 | 31 | function App() { 32 | return ( 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /examples/plugin/item-popover/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | } 5 | -------------------------------------------------------------------------------- /examples/plugin/item-popover/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Popover } from 'antd'; 3 | import GGEditor, { Flow, ItemPopover } from 'gg-editor'; 4 | import styles from './index.less'; 5 | 6 | const data = { 7 | nodes: [ 8 | { 9 | id: '0', 10 | label: 'Node', 11 | x: 50, 12 | y: 50, 13 | }, 14 | { 15 | id: '1', 16 | label: 'Node', 17 | x: 50, 18 | y: 200, 19 | }, 20 | ], 21 | edges: [ 22 | { 23 | label: 'Label', 24 | source: '0', 25 | sourceAnchor: 1, 26 | target: '1', 27 | targetAnchor: 0, 28 | }, 29 | ], 30 | }; 31 | 32 | function App() { 33 | return ( 34 | 35 | 36 | { 38 | const { minY: top, centerX: left } = position; 39 | 40 | return ( 41 | 42 |
43 | 44 | ); 45 | }} 46 | /> 47 | 48 | ); 49 | } 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /examples/register/dom-node/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | } 5 | -------------------------------------------------------------------------------- /examples/register/dom-node/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GGEditor, { Flow, RegisterNode } from 'gg-editor'; 3 | import styles from './index.less'; 4 | 5 | const data = { 6 | nodes: [ 7 | { 8 | id: '0', 9 | label: 'Node', 10 | x: 50, 11 | y: 50, 12 | }, 13 | { 14 | id: '1', 15 | label: 'Node', 16 | x: 50, 17 | y: 200, 18 | }, 19 | ], 20 | edges: [ 21 | { 22 | label: 'Label', 23 | source: '0', 24 | sourceAnchor: 1, 25 | target: '1', 26 | targetAnchor: 0, 27 | }, 28 | ], 29 | }; 30 | 31 | function App() { 32 | return ( 33 | 34 | 39 | ${label}`, 65 | }, 66 | }); 67 | 68 | return keyShape; 69 | }, 70 | getAnchorPoints() { 71 | return [ 72 | [0.5, 0], 73 | [0.5, 1], 74 | [0, 0.5], 75 | [1, 0.5], 76 | ]; 77 | }, 78 | }} 79 | extend="single-shape" 80 | /> 81 | 82 | ); 83 | } 84 | 85 | export default App; 86 | -------------------------------------------------------------------------------- /examples/register/node/index.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | height: 500px; 4 | } 5 | -------------------------------------------------------------------------------- /examples/register/node/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GGEditor, { Flow, RegisterNode, setAnchorPointsState } from 'gg-editor'; 3 | import styles from './index.less'; 4 | 5 | const data = { 6 | nodes: [ 7 | { 8 | id: '0', 9 | label: 'Node', 10 | x: 50, 11 | y: 50, 12 | }, 13 | { 14 | id: '1', 15 | label: 'Node', 16 | x: 50, 17 | y: 200, 18 | }, 19 | ], 20 | edges: [ 21 | { 22 | label: 'Label', 23 | source: '0', 24 | sourceAnchor: 1, 25 | target: '1', 26 | targetAnchor: 0, 27 | }, 28 | ], 29 | }; 30 | 31 | function App() { 32 | return ( 33 | 34 | 41 | 54 | 82 | { 92 | const { width, height } = item.getKeyShape().getBBox(); 93 | 94 | const [x, y] = anchorPoint; 95 | 96 | return { 97 | x: width * x - width / 2, 98 | y: height * y - height / 2, 99 | }; 100 | }, 101 | (item, anchorPoint) => { 102 | const { width, height } = item.getKeyShape().getBBox(); 103 | 104 | const [x, y] = anchorPoint; 105 | 106 | return { 107 | x: width * x - width / 2, 108 | y: height * y - height / 2, 109 | }; 110 | }, 111 | ); 112 | }, 113 | getAnchorPoints() { 114 | return [ 115 | [0.5, 0], 116 | [0.5, 1], 117 | [0, 0.5], 118 | [1, 0.5], 119 | ]; 120 | }, 121 | }} 122 | extend="circle" 123 | /> 124 | 125 | ); 126 | } 127 | 128 | export default App; 129 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "target": "esnext", 7 | "jsx": "react", 8 | "noImplicitThis": true, 9 | "strictBindCallApply": true, 10 | "baseUrl": ".", 11 | "sourceMap": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gg-editor", 3 | "version": "3.1.3", 4 | "description": "A visual graph editor based on G6 and React", 5 | "keywords": [ 6 | "react", 7 | "graph", 8 | "editor", 9 | "flow", 10 | "mind" 11 | ], 12 | "main": "lib/index.js", 13 | "unpkg": "dist/index.js", 14 | "module": "es/index.js", 15 | "types": "lib/index.d.ts", 16 | "files": [ 17 | "dist", 18 | "lib", 19 | "es", 20 | "src" 21 | ], 22 | "scripts": { 23 | "start": "npm run docs:start", 24 | "build": "node ./scripts/build.js", 25 | "docs:start": "dumi dev", 26 | "docs:build": "dumi build", 27 | "lint": "eslint --cache --ext .ts,.tsx ./src", 28 | "lint:fix": "npm run lint -- --fix", 29 | "pretty-quick": "pretty-quick", 30 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0" 31 | }, 32 | "husky": { 33 | "hooks": { 34 | "pre-commit": "pretty-quick --staged" 35 | } 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/alibaba/GGEditor.git" 40 | }, 41 | "author": { 42 | "name": "高力", 43 | "email": "3071730@qq.com" 44 | }, 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/alibaba/GGEditor/issues" 48 | }, 49 | "homepage": "https://github.com/alibaba/GGEditor#readme", 50 | "peerDependencies": { 51 | "react": "^16.8.0", 52 | "react-dom": "^16.8.0" 53 | }, 54 | "dependencies": { 55 | "@antv/g6": "3.5.2", 56 | "@babel/runtime": "^7.7.6", 57 | "lodash": "^4.17.15" 58 | }, 59 | "devDependencies": { 60 | "@antv/g6": "^3.5.0", 61 | "@babel/cli": "^7.7.5", 62 | "@babel/core": "^7.7.5", 63 | "@babel/plugin-transform-runtime": "^7.7.6", 64 | "@babel/preset-env": "^7.7.6", 65 | "@babel/preset-react": "^7.7.4", 66 | "@rollup/plugin-commonjs": "^11.0.2", 67 | "@rollup/plugin-node-resolve": "^7.1.1", 68 | "@rollup/plugin-replace": "^2.3.1", 69 | "@types/lodash": "^4.14.149", 70 | "@types/react": "^16.9.9", 71 | "@types/react-dom": "^16.9.2", 72 | "@typescript-eslint/eslint-plugin": "^2.4.0", 73 | "@typescript-eslint/parser": "^2.4.0", 74 | "antd": "^3.24.2", 75 | "babel-plugin-lodash": "^3.3.4", 76 | "conventional-changelog-cli": "^2.0.31", 77 | "cz-conventional-changelog": "^3.0.2", 78 | "dumi": "^1.0.17", 79 | "eslint": "^6.5.1", 80 | "eslint-config-prettier": "^6.4.0", 81 | "eslint-plugin-prettier": "^3.1.1", 82 | "eslint-plugin-react": "^7.16.0", 83 | "husky": "^3.0.9", 84 | "less": "^3.10.3", 85 | "prettier": "^1.18.2", 86 | "pretty-quick": "^2.0.0", 87 | "react": "^16.11.0", 88 | "react-dom": "^16.11.0", 89 | "rimraf": "^3.0.0", 90 | "rollup": "^1.24.0", 91 | "rollup-plugin-babel": "^4.3.3", 92 | "rollup-plugin-terser": "^5.1.2", 93 | "rollup-plugin-typescript2": "^0.24.3", 94 | "signale": "^1.4.0", 95 | "tscpaths": "^0.0.9", 96 | "typescript": "^3.7.2" 97 | }, 98 | "config": { 99 | "commitizen": { 100 | "path": "./node_modules/cz-conventional-changelog" 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | ggeditor.com -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable */ 4 | const signale = require('signale'); 5 | const rimraf = require('rimraf'); 6 | const rollup = require('rollup'); 7 | const resolve = require('@rollup/plugin-node-resolve'); 8 | const replace = require('@rollup/plugin-replace'); 9 | const commonjs = require('@rollup/plugin-commonjs'); 10 | const typescript = require('rollup-plugin-typescript2'); 11 | const babel = require('rollup-plugin-babel'); 12 | const { terser } = require('rollup-plugin-terser'); 13 | const { exec } = require('child_process'); 14 | const { version, dependencies = {}, peerDependencies = {} } = require('../package.json'); 15 | /* eslint-enable */ 16 | 17 | const makeExternalPredicate = externalArray => { 18 | if (!externalArray.length) { 19 | return () => false; 20 | } 21 | 22 | const pattern = new RegExp(`^(${externalArray.join('|')})($|/)`); 23 | 24 | return id => pattern.test(id); 25 | }; 26 | 27 | async function build() { 28 | // Clean 29 | rimraf.sync('dist'); 30 | rimraf.sync('lib'); 31 | rimraf.sync('es'); 32 | 33 | signale.success('Clean success'); 34 | 35 | // Build umd 36 | try { 37 | const umdBundle = await rollup.rollup({ 38 | input: 'src/index.tsx', 39 | plugins: [ 40 | resolve({ 41 | browser: true, 42 | }), 43 | replace({ 44 | 'process.env.GG_EDITOR_VERSION': JSON.stringify(version), 45 | }), 46 | commonjs(), 47 | typescript({ 48 | tsconfigOverride: { 49 | compilerOptions: { 50 | declaration: false, 51 | }, 52 | }, 53 | }), 54 | babel({ 55 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 56 | }), 57 | terser(), 58 | ], 59 | external: makeExternalPredicate([...Object.keys(peerDependencies)]), 60 | }); 61 | 62 | await umdBundle.write({ 63 | name: 'GGEditor', 64 | file: 'dist/index.js', 65 | format: 'umd', 66 | globals: { 67 | react: 'React', 68 | 'react-dom': 'ReactDOM', 69 | }, 70 | exports: 'named', 71 | }); 72 | 73 | signale.success('Build umd success'); 74 | } catch (error) { 75 | signale.error(error); 76 | } 77 | 78 | // Build cjs 79 | try { 80 | const cjsBundle = await rollup.rollup({ 81 | input: 'src/index.tsx', 82 | plugins: [ 83 | resolve(), 84 | replace({ 85 | 'process.env.GG_EDITOR_VERSION': JSON.stringify(version), 86 | }), 87 | commonjs(), 88 | typescript(), 89 | babel({ 90 | exclude: 'node_modules/**', 91 | extensions: ['.ts', '.tsx'], 92 | plugins: [['@babel/plugin-transform-runtime', { useESModules: false }]], 93 | runtimeHelpers: true, 94 | }), 95 | ], 96 | external: makeExternalPredicate([...Object.keys(dependencies), ...Object.keys(peerDependencies)]), 97 | }); 98 | 99 | await cjsBundle.write({ 100 | file: 'lib/index.js', 101 | format: 'cjs', 102 | exports: 'named', 103 | }); 104 | signale.success('Build cjs success'); 105 | } catch (error) { 106 | signale.error(error); 107 | } 108 | 109 | // Build esm 110 | try { 111 | const esmBundle = await rollup.rollup({ 112 | input: 'src/index.tsx', 113 | plugins: [ 114 | resolve(), 115 | replace({ 116 | 'process.env.GG_EDITOR_VERSION': JSON.stringify(version), 117 | }), 118 | commonjs(), 119 | typescript({ 120 | tsconfigOverride: { 121 | compilerOptions: { 122 | declaration: false, 123 | }, 124 | }, 125 | }), 126 | babel({ 127 | exclude: 'node_modules/**', 128 | extensions: ['.ts', '.tsx'], 129 | plugins: [['@babel/plugin-transform-runtime', { useESModules: true }]], 130 | runtimeHelpers: true, 131 | }), 132 | ], 133 | external: makeExternalPredicate([...Object.keys(dependencies), ...Object.keys(peerDependencies)]), 134 | }); 135 | 136 | await esmBundle.write({ 137 | file: 'es/index.js', 138 | format: 'esm', 139 | }); 140 | 141 | signale.success('Build esm success'); 142 | } catch (error) { 143 | signale.error(error); 144 | } 145 | 146 | // Replace absolute paths to relative paths 147 | exec(`tscpaths -p ./tsconfig.cjs.json -s ./lib`, error => { 148 | if (error) { 149 | signale.error(error); 150 | } 151 | }); 152 | } 153 | 154 | build(); 155 | -------------------------------------------------------------------------------- /src/common/CommandManager/index.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep'; 2 | import { getGraphState } from '@/utils'; 3 | import { EditorEvent } from '@/common/constants'; 4 | import { Graph, Command } from '@/common/interfaces'; 5 | 6 | class CommandManager { 7 | command: { 8 | [propName: string]: Command; 9 | }; 10 | commandQueue: Command[]; 11 | commandIndex: number; 12 | 13 | constructor() { 14 | this.command = {}; 15 | this.commandQueue = []; 16 | this.commandIndex = 0; 17 | } 18 | 19 | /** 注册命令 */ 20 | register(name: string, command: Command) { 21 | this.command[name] = { 22 | ...command, 23 | name, 24 | }; 25 | } 26 | 27 | /** 执行命令 */ 28 | execute(graph: Graph, name: string, params?: object) { 29 | const Command = this.command[name]; 30 | 31 | if (!Command) { 32 | return; 33 | } 34 | 35 | const command = Object.create(Command); 36 | 37 | command.params = cloneDeep(Command.params); 38 | 39 | if (params) { 40 | command.params = { 41 | ...command.params, 42 | ...params, 43 | }; 44 | } 45 | 46 | if (!command.canExecute(graph)) { 47 | return; 48 | } 49 | 50 | if (!command.shouldExecute(graph)) { 51 | return; 52 | } 53 | 54 | command.init(graph); 55 | 56 | graph.emit(EditorEvent.onBeforeExecuteCommand, { 57 | name: command.name, 58 | params: command.params, 59 | }); 60 | 61 | command.execute(graph); 62 | 63 | graph.emit(EditorEvent.onAfterExecuteCommand, { 64 | name: command.name, 65 | params: command.params, 66 | }); 67 | 68 | if (command.canUndo(graph)) { 69 | const { commandQueue, commandIndex } = this; 70 | 71 | commandQueue.splice(commandIndex, commandQueue.length - commandIndex, command); 72 | 73 | this.commandIndex += 1; 74 | } 75 | 76 | graph.emit(EditorEvent.onGraphStateChange, { 77 | graphState: getGraphState(graph), 78 | }); 79 | } 80 | 81 | /** 判断是否可以执行 */ 82 | canExecute(graph: Graph, name: string) { 83 | return this.command[name].canExecute(graph); 84 | } 85 | 86 | /** 注入是否应该执行 */ 87 | injectShouldExecute(name: string, shouldExecute: (graph: Graph) => boolean) { 88 | this.command[name].shouldExecute = shouldExecute; 89 | } 90 | } 91 | 92 | export default CommandManager; 93 | -------------------------------------------------------------------------------- /src/common/behaviorManager/index.ts: -------------------------------------------------------------------------------- 1 | import G6 from '@antv/g6'; 2 | import { isMind } from '@/utils'; 3 | import { GraphType } from '@/common/constants'; 4 | import { Graph, Behavior } from '@/common/interfaces'; 5 | 6 | class BehaviorManager { 7 | behaviors: { 8 | [propName: string]: Behavior; 9 | }; 10 | 11 | constructor() { 12 | this.behaviors = {}; 13 | } 14 | 15 | getRegisteredBehaviors(type: GraphType) { 16 | const registeredBehaviors = {}; 17 | 18 | Object.keys(this.behaviors).forEach(name => { 19 | const behavior = this.behaviors[name]; 20 | 21 | const { graphType } = behavior; 22 | 23 | if (graphType && graphType !== type) { 24 | return; 25 | } 26 | 27 | const { graphMode = 'default' } = behavior; 28 | 29 | if (!registeredBehaviors[graphMode]) { 30 | registeredBehaviors[graphMode] = {}; 31 | } 32 | 33 | registeredBehaviors[graphMode][name] = name; 34 | }); 35 | 36 | return registeredBehaviors; 37 | } 38 | 39 | wrapEventHandler(type: GraphType, behavior: Behavior): Behavior { 40 | const events = behavior.getEvents(); 41 | 42 | Object.keys(events).forEach(event => { 43 | const handlerName = events[event]; 44 | const handler = behavior[handlerName]; 45 | 46 | behavior[handlerName] = function(...params: any[]) { 47 | const { graph } = this; 48 | 49 | if ( 50 | (type === GraphType.Flow && isMind(graph as Graph) === false) || 51 | (type === GraphType.Mind && isMind(graph as Graph)) 52 | ) { 53 | handler.apply(this, params); 54 | } 55 | }; 56 | }); 57 | 58 | return behavior; 59 | } 60 | 61 | register(name: string, behavior: Behavior) { 62 | const { graphType } = behavior; 63 | 64 | this.behaviors[name] = behavior; 65 | 66 | switch (graphType) { 67 | case GraphType.Flow: 68 | G6.registerBehavior(name, this.wrapEventHandler(GraphType.Flow, behavior)); 69 | break; 70 | 71 | case GraphType.Mind: 72 | G6.registerBehavior(name, this.wrapEventHandler(GraphType.Mind, behavior)); 73 | break; 74 | 75 | default: 76 | G6.registerBehavior(name, behavior); 77 | break; 78 | } 79 | } 80 | } 81 | 82 | export default new BehaviorManager(); 83 | -------------------------------------------------------------------------------- /src/common/commandManager/index.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep'; 2 | import { getGraphState } from '@/utils'; 3 | import { EditorEvent } from '@/common/constants'; 4 | import { Graph, Command } from '@/common/interfaces'; 5 | 6 | class CommandManager { 7 | command: { 8 | [propName: string]: Command; 9 | }; 10 | commandQueue: Command[]; 11 | commandIndex: number; 12 | 13 | constructor() { 14 | this.command = {}; 15 | this.commandQueue = []; 16 | this.commandIndex = 0; 17 | } 18 | 19 | /** 注册命令 */ 20 | register(name: string, command: Command) { 21 | this.command[name] = { 22 | ...command, 23 | name, 24 | }; 25 | } 26 | 27 | /** 执行命令 */ 28 | execute(graph: Graph, name: string, params?: object) { 29 | const Command = this.command[name]; 30 | 31 | if (!Command) { 32 | return; 33 | } 34 | 35 | const command = Object.create(Command); 36 | 37 | command.params = cloneDeep(Command.params); 38 | 39 | if (params) { 40 | command.params = { 41 | ...command.params, 42 | ...params, 43 | }; 44 | } 45 | 46 | if (!command.canExecute(graph)) { 47 | return; 48 | } 49 | 50 | if (!command.shouldExecute(graph)) { 51 | return; 52 | } 53 | 54 | command.init(graph); 55 | 56 | graph.emit(EditorEvent.onBeforeExecuteCommand, { 57 | name: command.name, 58 | params: command.params, 59 | }); 60 | 61 | command.execute(graph); 62 | 63 | graph.emit(EditorEvent.onAfterExecuteCommand, { 64 | name: command.name, 65 | params: command.params, 66 | }); 67 | 68 | if (command.canUndo(graph)) { 69 | const { commandQueue, commandIndex } = this; 70 | 71 | commandQueue.splice(commandIndex, commandQueue.length - commandIndex, command); 72 | 73 | this.commandIndex += 1; 74 | } 75 | 76 | graph.emit(EditorEvent.onGraphStateChange, { 77 | graphState: getGraphState(graph), 78 | }); 79 | } 80 | 81 | /** 判断是否可以执行 */ 82 | canExecute(graph: Graph, name: string) { 83 | return this.command[name].canExecute(graph); 84 | } 85 | 86 | /** 注入是否应该执行 */ 87 | injectShouldExecute(name: string, shouldExecute: (graph: Graph) => boolean) { 88 | this.command[name].shouldExecute = shouldExecute; 89 | } 90 | } 91 | 92 | export default CommandManager; 93 | -------------------------------------------------------------------------------- /src/common/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const FLOW_CONTAINER_ID = 'J_FlowContainer'; 2 | export const MIND_CONTAINER_ID = 'J_MindContainer'; 3 | 4 | export const LABEL_DEFAULT_TEXT = '新建节点'; 5 | 6 | export enum RendererType { 7 | Canvas = 'canvas', 8 | Svg = 'svg', 9 | } 10 | 11 | export enum ItemType { 12 | Node = 'node', 13 | Edge = 'edge', 14 | } 15 | 16 | export enum ItemState { 17 | Active = 'active', 18 | ActiveAnchorPoints = 'activeAnchorPoints', 19 | Selected = 'selected', 20 | HighLight = 'highLight', 21 | Error = 'error', 22 | } 23 | 24 | export enum GraphType { 25 | Flow = 'flow', 26 | Mind = 'mind', 27 | } 28 | 29 | export enum GraphMode { 30 | Default = 'default', 31 | AddNode = 'addNode', 32 | Readonly = 'readonly', 33 | } 34 | 35 | export enum GraphState { 36 | NodeSelected = 'nodeSelected', 37 | EdgeSelected = 'edgeSelected', 38 | MultiSelected = 'multiSelected', 39 | CanvasSelected = 'canvasSelected', 40 | } 41 | 42 | export enum LabelState { 43 | Hide = 'hide', 44 | Show = 'show', 45 | } 46 | 47 | export enum AnchorPointState { 48 | Enabled = 'enabled', 49 | Disabled = 'disabled', 50 | } 51 | 52 | export enum EditorEvent { 53 | /** 调用命令之前触发 */ 54 | onBeforeExecuteCommand = 'onBeforeExecuteCommand', 55 | /** 调用命令之后触发 */ 56 | onAfterExecuteCommand = 'onAfterExecuteCommand', 57 | /** 改变画面状态触发 */ 58 | onGraphStateChange = 'onGraphStateChange', 59 | /** 改变标签状态触发 */ 60 | onLabelStateChange = 'onLabelStateChange', 61 | } 62 | 63 | export enum EditorCommand { 64 | /** 撤销 */ 65 | Undo = 'undo', 66 | /** 重做 */ 67 | Redo = 'redo', 68 | /** 添加 */ 69 | Add = 'add', 70 | /** 更新 */ 71 | Update = 'update', 72 | /** 删除 */ 73 | Remove = 'remove', 74 | /** 复制 */ 75 | Copy = 'copy', 76 | /** 粘贴 */ 77 | Paste = 'paste', 78 | /** 粘贴到这里 */ 79 | PasteHere = 'pasteHere', 80 | /** 放大 */ 81 | ZoomIn = 'zoomIn', 82 | /** 缩小 */ 83 | ZoomOut = 'zoomOut', 84 | /** 插入主题 */ 85 | Topic = 'topic', 86 | /** 插入子主题 */ 87 | Subtopic = 'subtopic', 88 | /** 收起 */ 89 | Fold = 'fold', 90 | /** 展开 */ 91 | Unfold = 'unfold', 92 | } 93 | 94 | export enum GraphCommonEvent { 95 | /** 单击鼠标左键或者按下回车键时触发 */ 96 | onClick = 'click', 97 | /** 双击鼠标左键时触发 */ 98 | onDoubleClick = 'dblclick', 99 | /** 鼠标移入元素范围内触发,该事件不冒泡,即鼠标移到其后代元素上时不会触发 */ 100 | onMouseEnter = 'mouseenter', 101 | /** 鼠标在元素内部移到时不断触发,不能通过键盘触发 */ 102 | onMouseMove = 'mousemove', 103 | /** 鼠标移出目标元素后触发 */ 104 | onMouseOut = 'mouseout', 105 | /** 鼠标移入目标元素上方,鼠标移到其后代元素上时会触发 */ 106 | onMouseOver = 'mouseover', 107 | /** 鼠标移出元素范围时触发,该事件不冒泡,即鼠标移到其后代元素时不会触发 */ 108 | onMouseLeave = 'mouseleave', 109 | /** 鼠标按钮被按下(左键或者右键)时触发,不能通过键盘触发 */ 110 | onMouseDown = 'mousedown', 111 | /** 鼠标按钮被释放弹起时触发,不能通过键盘触发 */ 112 | onMouseUp = 'mouseup', 113 | /** 用户右击鼠标时触发并打开上下文菜单 */ 114 | onContextMenu = 'contextmenu', 115 | /** 当拖拽元素开始被拖拽的时候触发的事件,此事件作用在被拖曳元素上 */ 116 | onDragStart = 'dragstart', 117 | /** 当拖拽元素在拖动过程中时触发的事件,此事件作用于被拖拽元素上 */ 118 | onDrag = 'drag', 119 | /** 当拖拽完成后触发的事件,此事件作用在被拖曳元素上 */ 120 | onDragEnd = 'dragend', 121 | /** 当拖曳元素进入目标元素的时候触发的事件,此事件作用在目标元素上 */ 122 | onDragEnter = 'dragenter', 123 | /** 当拖曳元素离开目标元素的时候触发的事件,此事件作用在目标元素上 */ 124 | onDragLeave = 'dragleave', 125 | /** 被拖拽的元素在目标元素上同时鼠标放开触发的事件,此事件作用在目标元素上 */ 126 | onDrop = 'drop', 127 | /** 按下键盘键触发该事件 */ 128 | onKeyDown = 'keydown', 129 | /** 释放键盘键触发该事件 */ 130 | onKeyUp = 'keyup', 131 | /** 当手指触摸屏幕时候触发,即使已经有一个手指放在屏幕上也会触发 */ 132 | onTouchStart = 'touchstart', 133 | /** 当手指在屏幕上滑动的时候连续地触发。在这个事件发生期间,调用 preventDefault() 事件可以阻止滚动。 */ 134 | onTouchMove = 'touchmove', 135 | /** 当手指从屏幕上离开的时候触发 */ 136 | onTouchEnd = 'touchend', 137 | } 138 | 139 | export enum GraphNodeEvent { 140 | /** 鼠标左键单击节点时触发 */ 141 | onNodeClick = 'node:click', 142 | /** 鼠标双击左键节点时触发 */ 143 | onNodeDoubleClick = 'node:dblclick', 144 | /** 鼠标移入节点时触发 */ 145 | onNodeMouseEnter = 'node:mouseenter', 146 | /** 鼠标在节点内部移到时不断触发,不能通过键盘触发 */ 147 | onNodeMouseMove = 'node:mousemove', 148 | /** 鼠标移出节点后触发 */ 149 | onNodeMouseOut = 'node:mouseout', 150 | /** 鼠标移入节点上方时触发 */ 151 | onNodeMouseOver = 'node:mouseover', 152 | /** 鼠标移出节点时触发 */ 153 | onNodeMouseLeave = 'node:mouseleave', 154 | /** 鼠标按钮在节点上按下(左键或者右键)时触发,不能通过键盘触发 */ 155 | onNodeMouseDown = 'node:mousedown', 156 | /** 节点上按下的鼠标按钮被释放弹起时触发,不能通过键盘触发 */ 157 | onNodeMouseUp = 'node:mouseup', 158 | /** 用户在节点上右击鼠标时触发并打开右键菜单 */ 159 | onNodeContextMenu = 'node:contextmenu', 160 | /** 当节点开始被拖拽的时候触发的事件,此事件作用在被拖曳节点上 */ 161 | onNodeDragStart = 'node:dragstart', 162 | /** 当节点在拖动过程中时触发的事件,此事件作用于被拖拽节点上 */ 163 | onNodeDrag = 'node:drag', 164 | /** 当拖拽完成后触发的事件,此事件作用在被拖曳节点上 */ 165 | onNodeDragEnd = 'node:dragend', 166 | /** 当拖曳节点进入目标元素的时候触发的事件,此事件作用在目标元素上 */ 167 | onNodeDragEnter = 'node:dragenter', 168 | /** 当拖曳节点离开目标元素的时候触发的事件,此事件作用在目标元素上 */ 169 | onNodeDragLeave = 'node:dragleave', 170 | /** 被拖拽的节点在目标元素上同时鼠标放开触发的事件,此事件作用在目标元素上 */ 171 | onNodeDrop = 'node:drop', 172 | } 173 | 174 | export enum GraphEdgeEvent { 175 | /** 鼠标左键单击边时触发 */ 176 | onEdgeClick = 'edge:click', 177 | /** 鼠标双击左键边时触发 */ 178 | onEdgeDoubleClick = 'edge:dblclick', 179 | /** 鼠标移入边时触发 */ 180 | onEdgeMouseEnter = 'edge:mouseenter', 181 | /** 鼠标在边上移到时不断触发,不能通过键盘触发 */ 182 | onEdgeMouseMove = 'edge:mousemove', 183 | /** 鼠标移出边后触发 */ 184 | onEdgeMouseOut = 'edge:mouseout', 185 | /** 鼠标移入边上方时触发 */ 186 | onEdgeMouseOver = 'edge:mouseover', 187 | /** 鼠标移出边时触发 */ 188 | onEdgeMouseLeave = 'edge:mouseleave', 189 | /** 鼠标按钮在边上按下(左键或者右键)时触发,不能通过键盘触发 */ 190 | onEdgeMouseDown = 'edge:mousedown', 191 | /** 边上按下的鼠标按钮被释放弹起时触发,不能通过键盘触发 */ 192 | onEdgeMouseUp = 'edge:mouseup', 193 | /** 用户在边上右击鼠标时触发并打开右键菜单 */ 194 | onEdgeContextMenu = 'edge:contextmenu', 195 | } 196 | 197 | export enum GraphCanvasEvent { 198 | /** 鼠标左键单击画布时触发 */ 199 | onCanvasClick = 'canvas:click', 200 | /** 鼠标双击左键画布时触发 */ 201 | onCanvasDoubleClick = 'canvas:dblclick', 202 | /** 鼠标移入画布时触发 */ 203 | onCanvasMouseEnter = 'canvas:mouseenter', 204 | /** 鼠标在画布内部移到时不断触发,不能通过键盘触发 */ 205 | onCanvasMouseMove = 'canvas:mousemove', 206 | /** 鼠标移出画布后触发 */ 207 | onCanvasMouseOut = 'canvas:mouseout', 208 | /** 鼠标移入画布上方时触发 */ 209 | onCanvasMouseOver = 'canvas:mouseover', 210 | /** 鼠标移出画布时触发 */ 211 | onCanvasMouseLeave = 'canvas:mouseleave', 212 | /** 鼠标按钮在画布上按下(左键或者右键)时触发,不能通过键盘触发 */ 213 | onCanvasMouseDown = 'canvas:mousedown', 214 | /** 画布上按下的鼠标按钮被释放弹起时触发,不能通过键盘触发 */ 215 | onCanvasMouseUp = 'canvas:mouseup', 216 | /** 用户在画布上右击鼠标时触发并打开右键菜单 */ 217 | onCanvasContextMenu = 'canvas:contextmenu', 218 | /** 当画布开始被拖拽的时候触发的事件,此事件作用在被拖曳画布上 */ 219 | onCanvasDragStart = 'canvas:dragstart', 220 | /** 当画布在拖动过程中时触发的事件,此事件作用于被拖拽画布上 */ 221 | onCanvasDrag = 'canvas:drag', 222 | /** 当拖拽完成后触发的事件,此事件作用在被拖曳画布上 */ 223 | onCanvasDragEnd = 'canvas:dragend', 224 | /** 当拖曳画布进入目标元素的时候触发的事件,此事件作用在目标元素上 */ 225 | onCanvasDragEnter = 'canvas:dragenter', 226 | /** 当拖曳画布离开目标元素的时候触发的事件,此事件作用在目标元素上 */ 227 | onCanvasDragLeave = 'canvas:dragleave', 228 | } 229 | 230 | export enum GraphCustomEvent { 231 | /** 调用 add / addItem 方法之前触发 */ 232 | onBeforeAddItem = 'beforeadditem', 233 | /** 调用 add / addItem 方法之后触发 */ 234 | onAfterAddItem = 'afteradditem', 235 | /** 调用 remove / removeItem 方法之前触发 */ 236 | onBeforeRemoveItem = 'beforeremoveitem', 237 | /** 调用 remove / removeItem 方法之后触发 */ 238 | onAfterRemoveItem = 'afterremoveitem', 239 | /** 调用 update / updateItem 方法之前触发 */ 240 | onBeforeUpdateItem = 'beforeupdateitem', 241 | /** 调用 update / updateItem 方法之后触发 */ 242 | onAfterUpdateItem = 'afterupdateitem', 243 | /** 调用 showItem / hideItem 方法之前触发 */ 244 | onBeforeItemVisibilityChange = 'beforeitemvisibilitychange', 245 | /** 调用 showItem / hideItem 方法之后触发 */ 246 | onAfterItemVisibilityChange = 'afteritemvisibilitychange', 247 | /** 调用 setItemState 方法之前触发 */ 248 | onBeforeItemStateChange = 'beforeitemstatechange', 249 | /** 调用 setItemState 方法之后触发 */ 250 | onAfterItemStateChange = 'afteritemstatechange', 251 | /** 调用 refreshItem 方法之前触发 */ 252 | onBeforeRefreshItem = 'beforerefreshitem', 253 | /** 调用 refreshItem 方法之后触发 */ 254 | onAfterRefreshItem = 'afterrefreshitem', 255 | /** 调用 clearItemStates 方法之前触发 */ 256 | onBeforeItemStatesClear = 'beforeitemstatesclear', 257 | /** 调用 clearItemStates 方法之后触发 */ 258 | onAfterItemStatesClear = 'afteritemstatesclear', 259 | /** 布局前触发。调用 render 时会进行布局,因此 render 时会触发。或用户主动调用图的 layout 时触发 */ 260 | onBeforeLayout = 'beforelayout', 261 | /** 布局完成后触发。调用 render 时会进行布局,因此 render 时布局完成后会触发。或用户主动调用图的 layout 时布局完成后触发 */ 262 | onAfterLayout = 'afterlayout', 263 | /** 连线完成之后触发 */ 264 | onAfterConnect = 'afterconnect', 265 | } 266 | -------------------------------------------------------------------------------- /src/common/global/index.ts: -------------------------------------------------------------------------------- 1 | import { guid } from '@/utils'; 2 | import { NodeModel } from '@/common/interfaces'; 3 | 4 | class Global { 5 | /** 当前版本 */ 6 | version: string = process.env.GG_EDITOR_VERSION; 7 | 8 | /** 埋点开关 */ 9 | trackable = true; 10 | 11 | /** 剪贴板 */ 12 | clipboard: { 13 | point: { 14 | x: number; 15 | y: number; 16 | }; 17 | models: NodeModel[]; 18 | } = { 19 | point: { 20 | x: 0, 21 | y: 0, 22 | }, 23 | models: [], 24 | }; 25 | 26 | /** 组件数据 */ 27 | component: { 28 | itemPanel: { 29 | model: Partial; 30 | delegateShapeClassName: string; 31 | }; 32 | } = { 33 | itemPanel: { 34 | model: null, 35 | delegateShapeClassName: `delegateShape_${guid()}`, 36 | }, 37 | }; 38 | 39 | /** 插件数据 */ 40 | plugin: { 41 | itemPopover: { 42 | state: 'show' | 'hide'; 43 | }; 44 | contextMenu: { 45 | state: 'show' | 'hide'; 46 | }; 47 | editableLabel: { 48 | state: 'show' | 'hide'; 49 | }; 50 | } = { 51 | itemPopover: { 52 | state: 'hide', 53 | }, 54 | contextMenu: { 55 | state: 'hide', 56 | }, 57 | editableLabel: { 58 | state: 'hide', 59 | }, 60 | }; 61 | } 62 | 63 | export default new Global(); 64 | -------------------------------------------------------------------------------- /src/common/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphType, 3 | GraphState, 4 | LabelState, 5 | EditorCommand, 6 | GraphCommonEvent, 7 | GraphNodeEvent, 8 | GraphEdgeEvent, 9 | GraphCanvasEvent, 10 | GraphCustomEvent, 11 | } from '@/common/constants'; 12 | import IGGroup from '@antv/g-canvas/lib/group'; 13 | import { IShape as IGShape } from '@antv/g-canvas/lib/interfaces'; 14 | import { Graph as IGraph, TreeGraph as ITreeGraph } from '@antv/g6'; 15 | import { 16 | IPoint, 17 | ShapeStyle as IShapeStyle, 18 | GraphOptions as IGraphOptions, 19 | GraphData as IGraphData, 20 | TreeGraphData as ITreeGraphData, 21 | NodeConfig as INodeConfig, 22 | EdgeConfig as IEdgeConfig, 23 | BehaviorOption as IBehaviorOption, 24 | IG6GraphEvent as IGraphEvent, 25 | } from '@antv/g6/lib/types'; 26 | import { ShapeOptions as IShapeOptions } from '@antv/g6/lib/interface/shape'; 27 | import { INode, IEdge } from '@antv/g6/lib/interface/item'; 28 | 29 | export interface GShape extends IGShape {} 30 | export interface GGroup extends IGGroup {} 31 | 32 | export interface Graph extends IGraph {} 33 | export interface TreeGraph extends ITreeGraph {} 34 | 35 | export interface AnchorPoint extends IPoint { 36 | index: number; 37 | } 38 | 39 | export interface ShapeStyle extends IShapeStyle {} 40 | 41 | export interface FlowData extends IGraphData {} 42 | export interface MindData extends ITreeGraphData {} 43 | 44 | export interface NodeModel extends INodeConfig {} 45 | export interface EdgeModel extends IEdgeConfig {} 46 | export interface GraphEvent extends IGraphEvent {} 47 | 48 | export interface GraphOptions extends IGraphOptions {} 49 | export interface CustomShape extends IShapeOptions {} 50 | export interface CustomNode extends CustomShape {} 51 | export interface CustomEdge extends CustomShape {} 52 | 53 | export type Item = Node | Edge; 54 | export interface Node extends INode {} 55 | export interface Edge extends IEdge {} 56 | 57 | export interface Behavior extends IBehaviorOption { 58 | graph?: Graph; 59 | graphType?: GraphType; 60 | graphMode?: string; 61 | [propName: string]: any; 62 | } 63 | 64 | export interface Command

{ 65 | /** 命令名称 */ 66 | name: string; 67 | /** 命令参数 */ 68 | params: P; 69 | /** 是否可以执行 */ 70 | canExecute(graph: G): boolean; 71 | /** 是否应该执行 */ 72 | shouldExecute(graph: G): boolean; 73 | /** 是否可以撤销 */ 74 | canUndo(graph: G): boolean; 75 | /** 初始命令 */ 76 | init(graph: G): void; 77 | /** 执行命令 */ 78 | execute(graph: G): void; 79 | /** 撤销命令 */ 80 | undo(graph: G): void; 81 | /** 命令快捷键 */ 82 | shortcuts: string[] | string[][]; 83 | } 84 | 85 | export interface CommandEvent { 86 | name: EditorCommand; 87 | params: object; 88 | } 89 | 90 | export interface GraphStateEvent { 91 | graphState: GraphState; 92 | } 93 | 94 | export interface LabelStateEvent { 95 | labelState: LabelState; 96 | } 97 | 98 | export type GraphNativeEvent = GraphCommonEvent | GraphNodeEvent | GraphEdgeEvent | GraphCanvasEvent | GraphCustomEvent; 99 | 100 | export type GraphReactEvent = 101 | | keyof typeof GraphCommonEvent 102 | | keyof typeof GraphNodeEvent 103 | | keyof typeof GraphEdgeEvent 104 | | keyof typeof GraphCanvasEvent 105 | | keyof typeof GraphCustomEvent; 106 | 107 | export type GraphReactEventProps = Record void>; 108 | -------------------------------------------------------------------------------- /src/common/withContext/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function(Context: React.Context, shouldRender: (context: CP) => boolean = () => true) { 4 | return function

(WrappedComponent: React.ComponentType

) { 5 | type WrappedComponentProps = Omit, keyof CP>; 6 | type WrappedComponentPropsWithForwardRef = WrappedComponentProps & { 7 | forwardRef: React.Ref; 8 | }; 9 | 10 | const InjectContext: React.FC = props => { 11 | const { forwardRef, ...rest } = props; 12 | 13 | let refProp = {}; 14 | 15 | if (WrappedComponent.prototype.isReactComponent) { 16 | refProp = { 17 | ref: forwardRef, 18 | }; 19 | } else { 20 | refProp = { 21 | forwardRef, 22 | }; 23 | } 24 | 25 | return ( 26 | 27 | {context => 28 | shouldRender(context) ? : null 29 | } 30 | 31 | ); 32 | }; 33 | 34 | return React.forwardRef((props, ref) => ); 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Command/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EditorEvent } from '@/common/constants'; 3 | import CommandManager from '@/common/CommandManager'; 4 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext'; 5 | 6 | interface CommandProps extends EditorContextProps { 7 | name: string; 8 | className?: string; 9 | disabledClassName?: string; 10 | } 11 | 12 | interface CommandState {} 13 | 14 | class Command extends React.Component { 15 | static defaultProps = { 16 | className: 'command', 17 | disabledClassName: 'command-disabled', 18 | }; 19 | 20 | state = { 21 | disabled: false, 22 | }; 23 | 24 | componentDidMount() { 25 | const { graph, name } = this.props; 26 | 27 | const commandManager: CommandManager = graph.get('commandManager'); 28 | 29 | this.setState({ 30 | disabled: !commandManager.canExecute(graph, name), 31 | }); 32 | 33 | graph.on(EditorEvent.onGraphStateChange, () => { 34 | this.setState({ 35 | disabled: !commandManager.canExecute(graph, name), 36 | }); 37 | }); 38 | } 39 | 40 | handleClick = () => { 41 | const { name, executeCommand } = this.props; 42 | 43 | executeCommand(name); 44 | }; 45 | 46 | render() { 47 | const { graph } = this.props; 48 | 49 | if (!graph) { 50 | return null; 51 | } 52 | 53 | const { className, disabledClassName, children } = this.props; 54 | const { disabled } = this.state; 55 | 56 | return ( 57 |

58 | {children} 59 |
60 | ); 61 | } 62 | } 63 | 64 | export default withEditorContext(Command); 65 | -------------------------------------------------------------------------------- /src/components/DetailPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getSelectedNodes, getSelectedEdges } from '@/utils'; 3 | import { GraphState, EditorEvent } from '@/common/constants'; 4 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext'; 5 | import { Node, Edge, GraphStateEvent } from '@/common/interfaces'; 6 | 7 | type DetailPanelType = 'node' | 'edge' | 'multi' | 'canvas'; 8 | 9 | export interface DetailPanelComponentProps { 10 | type: DetailPanelType; 11 | nodes: Node[]; 12 | edges: Edge[]; 13 | } 14 | 15 | class DetailPanel { 16 | static create = function

(type: DetailPanelType) { 17 | return function(WrappedComponent: React.ComponentType

) { 18 | type TypedPanelProps = EditorContextProps & Omit; 19 | type TypedPanelState = { graphState: GraphState }; 20 | 21 | class TypedPanel extends React.Component { 22 | state = { 23 | graphState: GraphState.CanvasSelected, 24 | }; 25 | 26 | componentDidMount() { 27 | const { graph } = this.props; 28 | 29 | graph.on(EditorEvent.onGraphStateChange, ({ graphState }: GraphStateEvent) => { 30 | this.setState({ 31 | graphState, 32 | }); 33 | }); 34 | } 35 | 36 | render() { 37 | const { graph } = this.props; 38 | const { graphState } = this.state; 39 | 40 | if (graphState !== `${type}Selected`) { 41 | return null; 42 | } 43 | 44 | const nodes = getSelectedNodes(graph); 45 | const edges = getSelectedEdges(graph); 46 | 47 | return ; 48 | } 49 | } 50 | 51 | return withEditorContext(TypedPanel); 52 | }; 53 | }; 54 | } 55 | 56 | export default DetailPanel; 57 | -------------------------------------------------------------------------------- /src/components/Editor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isArray from 'lodash/isArray'; 3 | import pick from 'lodash/pick'; 4 | import global from '@/common/global'; 5 | import { RendererType, EditorEvent, GraphCommonEvent } from '@/common/constants'; 6 | import { Graph, CommandEvent } from '@/common/interfaces'; 7 | import CommandManager from '@/common/CommandManager'; 8 | import { 9 | EditorContext, 10 | EditorPrivateContext, 11 | EditorContextProps, 12 | EditorPrivateContextProps, 13 | } from '@/components/EditorContext'; 14 | 15 | interface EditorProps { 16 | style?: React.CSSProperties; 17 | className?: string; 18 | [EditorEvent.onBeforeExecuteCommand]?: (e: CommandEvent) => void; 19 | [EditorEvent.onAfterExecuteCommand]?: (e: CommandEvent) => void; 20 | } 21 | 22 | interface EditorState extends EditorContextProps, EditorPrivateContextProps {} 23 | 24 | class Editor extends React.Component { 25 | static setTrackable(trackable: boolean) { 26 | global.trackable = trackable; 27 | } 28 | 29 | static defaultProps = { 30 | [EditorEvent.onBeforeExecuteCommand]: () => {}, 31 | [EditorEvent.onAfterExecuteCommand]: () => {}, 32 | }; 33 | 34 | lastMousedownTarget: HTMLElement | null = null; 35 | 36 | constructor(props: EditorProps) { 37 | super(props); 38 | 39 | this.state = { 40 | graph: null, 41 | setGraph: this.setGraph, 42 | executeCommand: this.executeCommand, 43 | commandManager: new CommandManager(), 44 | }; 45 | 46 | this.lastMousedownTarget = null; 47 | } 48 | 49 | shouldTriggerShortcut(graph: Graph, target: HTMLElement | null) { 50 | const renderer: RendererType = graph.get('renderer'); 51 | const canvasElement = graph.get('canvas').get('el'); 52 | 53 | if (!target) { 54 | return false; 55 | } 56 | 57 | if (target === canvasElement) { 58 | return true; 59 | } 60 | 61 | if (renderer === RendererType.Svg) { 62 | if (target.nodeName === 'svg') { 63 | return true; 64 | } 65 | 66 | let parentNode = target.parentNode; 67 | 68 | while (parentNode && parentNode.nodeName !== 'BODY') { 69 | if (parentNode.nodeName === 'svg') { 70 | return true; 71 | } else { 72 | parentNode = parentNode.parentNode; 73 | } 74 | } 75 | 76 | return false; 77 | } 78 | } 79 | 80 | bindEvent(graph: Graph) { 81 | const { props } = this; 82 | 83 | graph.on(EditorEvent.onBeforeExecuteCommand, props[EditorEvent.onBeforeExecuteCommand]); 84 | graph.on(EditorEvent.onAfterExecuteCommand, props[EditorEvent.onAfterExecuteCommand]); 85 | } 86 | 87 | bindShortcut(graph: Graph) { 88 | const { commandManager } = this.state; 89 | 90 | window.addEventListener(GraphCommonEvent.onMouseDown, e => { 91 | this.lastMousedownTarget = e.target as HTMLElement; 92 | }); 93 | 94 | graph.on(GraphCommonEvent.onKeyDown, (e: any) => { 95 | if (!this.shouldTriggerShortcut(graph, this.lastMousedownTarget)) { 96 | return; 97 | } 98 | 99 | Object.values(commandManager.command).some(command => { 100 | const { name, shortcuts } = command; 101 | 102 | const flag = shortcuts.some((shortcut: string | string[]) => { 103 | const { key } = e; 104 | 105 | if (!isArray(shortcut)) { 106 | return shortcut === key; 107 | } 108 | 109 | return shortcut.every((item, index) => { 110 | if (index === shortcut.length - 1) { 111 | return item === key; 112 | } 113 | 114 | return e[item]; 115 | }); 116 | }); 117 | 118 | if (flag) { 119 | if (commandManager.canExecute(graph, name)) { 120 | // Prevent default 121 | e.preventDefault(); 122 | 123 | // Execute command 124 | this.executeCommand(name); 125 | 126 | return true; 127 | } 128 | } 129 | 130 | return false; 131 | }); 132 | }); 133 | } 134 | 135 | setGraph = (graph: Graph) => { 136 | this.setState({ 137 | graph, 138 | }); 139 | 140 | this.bindEvent(graph); 141 | this.bindShortcut(graph); 142 | }; 143 | 144 | executeCommand = (name: string, params?: object) => { 145 | const { graph, commandManager } = this.state; 146 | 147 | if (graph) { 148 | commandManager.execute(graph, name, params); 149 | } 150 | }; 151 | 152 | render() { 153 | const { children } = this.props; 154 | const { graph, setGraph, executeCommand, commandManager } = this.state; 155 | 156 | return ( 157 | 164 | 170 |

{children}
171 | 172 | 173 | ); 174 | } 175 | } 176 | 177 | export default Editor; 178 | -------------------------------------------------------------------------------- /src/components/EditorContext/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Graph } from '@/common/interfaces'; 3 | import withContext from '@/common/withContext'; 4 | import CommandManager from '@/common/CommandManager'; 5 | 6 | export interface EditorContextProps { 7 | graph: Graph | null; 8 | executeCommand: (name: string, params?: object) => void; 9 | commandManager: CommandManager; 10 | } 11 | 12 | export interface EditorPrivateContextProps { 13 | setGraph: (graph: Graph) => void; 14 | commandManager: CommandManager; 15 | } 16 | 17 | export const EditorContext = React.createContext({} as EditorContextProps); 18 | export const EditorPrivateContext = React.createContext({} as EditorPrivateContextProps); 19 | 20 | export const withEditorContext = withContext(EditorContext, context => !!context.graph); 21 | export const withEditorPrivateContext = withContext(EditorPrivateContext); 22 | -------------------------------------------------------------------------------- /src/components/Flow/behavior/dragAddEdge.ts: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'lodash/isPlainObject'; 2 | import { guid } from '@/utils'; 3 | import { ItemType, ItemState, GraphType, AnchorPointState, GraphCustomEvent } from '@/common/constants'; 4 | import { Node, Edge, Behavior, GraphEvent, EdgeModel, AnchorPoint } from '@/common/interfaces'; 5 | import behaviorManager from '@/common/behaviorManager'; 6 | 7 | interface DragAddEdgeBehavior extends Behavior { 8 | edge: Edge | null; 9 | isEnabledAnchorPoint(e: GraphEvent): boolean; 10 | isNotSelf(e: GraphEvent): boolean; 11 | canFindTargetAnchorPoint(e: GraphEvent): boolean; 12 | shouldAddDelegateEdge(e: GraphEvent): boolean; 13 | shouldAddRealEdge(): boolean; 14 | handleNodeMouseEnter(e: GraphEvent): void; 15 | handleNodeMouseLeave(e: GraphEvent): void; 16 | handleNodeMouseDown(e: GraphEvent): void; 17 | handleMouseMove(e: GraphEvent): void; 18 | handleMouseUp(e: GraphEvent): void; 19 | } 20 | 21 | interface DefaultConfig { 22 | /** 边线类型 */ 23 | edgeType: string; 24 | /** 获取来源节点锚点状态 */ 25 | getAnchorPointStateOfSourceNode(sourceNode: Node, sourceAnchorPoint: AnchorPoint): AnchorPointState; 26 | /** 获取目标节点锚点状态 */ 27 | getAnchorPointStateOfTargetNode( 28 | sourceNode: Node, 29 | sourceAnchorPoint: AnchorPoint, 30 | targetNode: Node, 31 | targetAnchorPoint: AnchorPoint, 32 | ): AnchorPointState; 33 | } 34 | 35 | const dragAddEdgeBehavior: DragAddEdgeBehavior & ThisType = { 36 | edge: null, 37 | 38 | graphType: GraphType.Flow, 39 | 40 | getDefaultCfg(): DefaultConfig { 41 | return { 42 | edgeType: 'bizFlowEdge', 43 | getAnchorPointStateOfSourceNode: () => AnchorPointState.Enabled, 44 | getAnchorPointStateOfTargetNode: () => AnchorPointState.Enabled, 45 | }; 46 | }, 47 | 48 | getEvents() { 49 | return { 50 | 'node:mouseenter': 'handleNodeMouseEnter', 51 | 'node:mouseleave': 'handleNodeMouseLeave', 52 | 'node:mousedown': 'handleNodeMouseDown', 53 | mousemove: 'handleMouseMove', 54 | mouseup: 'handleMouseUp', 55 | }; 56 | }, 57 | 58 | isEnabledAnchorPoint(e) { 59 | const { target } = e; 60 | 61 | return !!target.get('isAnchorPoint') && target.get('anchorPointState') === AnchorPointState.Enabled; 62 | }, 63 | 64 | isNotSelf(e) { 65 | const { edge } = this; 66 | const { item } = e; 67 | 68 | return item.getModel().id !== edge.getSource().getModel().id; 69 | }, 70 | 71 | getTargetNodes(sourceId: string) { 72 | const { graph } = this; 73 | 74 | const nodes = graph.getNodes(); 75 | 76 | return nodes.filter(node => node.getModel().id !== sourceId); 77 | }, 78 | 79 | canFindTargetAnchorPoint(e) { 80 | return this.isEnabledAnchorPoint(e) && this.isNotSelf(e); 81 | }, 82 | 83 | shouldAddDelegateEdge(e) { 84 | return this.isEnabledAnchorPoint(e); 85 | }, 86 | 87 | shouldAddRealEdge() { 88 | const { edge } = this; 89 | 90 | const target = edge.getTarget(); 91 | 92 | return !isPlainObject(target); 93 | }, 94 | 95 | handleNodeMouseEnter(e) { 96 | const { graph, getAnchorPointStateOfSourceNode } = this; 97 | 98 | const sourceNode = e.item as Node; 99 | const sourceAnchorPoints = sourceNode.getAnchorPoints() as AnchorPoint[]; 100 | const sourceAnchorPointsState = []; 101 | 102 | sourceAnchorPoints.forEach(sourceAnchorPoint => { 103 | sourceAnchorPointsState.push(getAnchorPointStateOfSourceNode(sourceNode, sourceAnchorPoint)); 104 | }); 105 | 106 | sourceNode.set('anchorPointsState', sourceAnchorPointsState); 107 | 108 | graph.setItemState(sourceNode, ItemState.ActiveAnchorPoints, true); 109 | }, 110 | 111 | handleNodeMouseLeave(e) { 112 | const { graph, edge } = this; 113 | const { item } = e; 114 | 115 | if (!edge) { 116 | item.set('anchorPointsState', []); 117 | graph.setItemState(item, ItemState.ActiveAnchorPoints, false); 118 | } 119 | }, 120 | 121 | handleNodeMouseDown(e) { 122 | if (!this.shouldBegin(e) || !this.shouldAddDelegateEdge(e)) { 123 | return; 124 | } 125 | 126 | const { graph, edgeType, getAnchorPointStateOfTargetNode } = this; 127 | const { target } = e; 128 | 129 | const sourceNode = e.item as Node; 130 | const sourceNodeId = sourceNode.getModel().id; 131 | const sourceAnchorPointIndex = target.get('anchorPointIndex'); 132 | const sourceAnchorPoint = sourceNode.getAnchorPoints()[sourceAnchorPointIndex] as AnchorPoint; 133 | 134 | const model: EdgeModel = { 135 | id: guid(), 136 | type: edgeType, 137 | source: sourceNodeId, 138 | sourceAnchor: sourceAnchorPointIndex, 139 | target: { 140 | x: e.x, 141 | y: e.y, 142 | } as any, 143 | }; 144 | 145 | this.edge = graph.addItem(ItemType.Edge, model); 146 | 147 | graph.getNodes().forEach(targetNode => { 148 | if (targetNode.getModel().id === sourceNodeId) { 149 | return; 150 | } 151 | 152 | const targetAnchorPoints = targetNode.getAnchorPoints() as AnchorPoint[]; 153 | const targetAnchorPointsState = []; 154 | 155 | targetAnchorPoints.forEach(targetAnchorPoint => { 156 | targetAnchorPointsState.push( 157 | getAnchorPointStateOfTargetNode(sourceNode, sourceAnchorPoint, targetNode, targetAnchorPoint), 158 | ); 159 | }); 160 | 161 | targetNode.set('anchorPointsState', targetAnchorPointsState); 162 | 163 | graph.setItemState(targetNode, ItemState.ActiveAnchorPoints, true); 164 | }); 165 | }, 166 | 167 | handleMouseMove(e) { 168 | const { graph, edge } = this; 169 | 170 | if (!edge) { 171 | return; 172 | } 173 | 174 | if (this.canFindTargetAnchorPoint(e)) { 175 | const { item, target } = e; 176 | 177 | const targetId = item.getModel().id; 178 | const targetAnchor = target.get('anchorPointIndex'); 179 | 180 | graph.updateItem(edge, { 181 | target: targetId, 182 | targetAnchor, 183 | }); 184 | } else { 185 | graph.updateItem(edge, { 186 | target: { 187 | x: e.x, 188 | y: e.y, 189 | } as any, 190 | targetAnchor: undefined, 191 | }); 192 | } 193 | }, 194 | 195 | handleMouseUp() { 196 | const { graph, edge } = this; 197 | 198 | if (!edge) { 199 | return; 200 | } 201 | 202 | if (!this.shouldAddRealEdge()) { 203 | graph.removeItem(this.edge); 204 | } 205 | 206 | graph.emit(GraphCustomEvent.onAfterConnect, { 207 | edge: this.edge, 208 | }); 209 | 210 | this.edge = null; 211 | 212 | graph.getNodes().forEach(node => { 213 | node.set('anchorPointsState', []); 214 | graph.setItemState(node, ItemState.ActiveAnchorPoints, false); 215 | }); 216 | }, 217 | }; 218 | 219 | behaviorManager.register('drag-add-edge', dragAddEdgeBehavior); 220 | -------------------------------------------------------------------------------- /src/components/Flow/behavior/dragAddNode.ts: -------------------------------------------------------------------------------- 1 | import isArray from 'lodash/isArray'; 2 | import { guid } from '@/utils'; 3 | import global from '@/common/global'; 4 | import { ItemType, GraphType, GraphMode, EditorCommand } from '@/common/constants'; 5 | import { GShape, GGroup, NodeModel, Behavior, GraphEvent } from '@/common/interfaces'; 6 | import CommandManager from '@/common/CommandManager'; 7 | import behaviorManager from '@/common/behaviorManager'; 8 | 9 | interface DragAddNodeBehavior extends Behavior { 10 | shape: GShape | null; 11 | handleCanvasMouseEnter(e: GraphEvent): void; 12 | handleMouseMove(e: GraphEvent): void; 13 | handleMouseUp(e: GraphEvent): void; 14 | } 15 | 16 | const dragAddNodeBehavior: DragAddNodeBehavior = { 17 | shape: null, 18 | 19 | graphType: GraphType.Flow, 20 | 21 | graphMode: GraphMode.AddNode, 22 | 23 | getEvents() { 24 | return { 25 | 'canvas:mouseenter': 'handleCanvasMouseEnter', 26 | mousemove: 'handleMouseMove', 27 | mouseup: 'handleMouseUp', 28 | }; 29 | }, 30 | 31 | handleCanvasMouseEnter(e) { 32 | const { graph, shape } = this; 33 | 34 | if (shape) { 35 | return; 36 | } 37 | 38 | const group: GGroup = graph.get('group'); 39 | const model: Partial = global.component.itemPanel.model; 40 | 41 | const { size = 100 } = model; 42 | 43 | let width = 0; 44 | let height = 0; 45 | 46 | if (isArray(size)) { 47 | width = size[0]; 48 | height = size[1]; 49 | } else { 50 | width = size; 51 | height = size; 52 | } 53 | 54 | const x = e.x - width / 2; 55 | const y = e.y - height / 2; 56 | 57 | this.shape = group.addShape('rect', { 58 | className: global.component.itemPanel.delegateShapeClassName, 59 | attrs: { 60 | x, 61 | y, 62 | width, 63 | height, 64 | fill: '#f3f9ff', 65 | fillOpacity: 0.5, 66 | stroke: '#1890ff', 67 | strokeOpacity: 0.9, 68 | lineDash: [5, 5], 69 | }, 70 | }); 71 | 72 | graph.paint(); 73 | }, 74 | 75 | handleMouseMove(e) { 76 | const { graph } = this; 77 | const { width, height } = this.shape.getBBox(); 78 | 79 | const x = e.x - width / 2; 80 | const y = e.y - height / 2; 81 | 82 | this.shape.attr({ 83 | x, 84 | y, 85 | }); 86 | 87 | graph.paint(); 88 | }, 89 | 90 | handleMouseUp(e) { 91 | const { graph } = this; 92 | const { width, height } = this.shape.getBBox(); 93 | 94 | let x = e.x; 95 | let y = e.y; 96 | 97 | const model: Partial = global.component.itemPanel.model; 98 | 99 | if (model.center === 'topLeft') { 100 | x -= width / 2; 101 | y -= height / 2; 102 | } 103 | 104 | this.shape.remove(true); 105 | 106 | const commandManager: CommandManager = graph.get('commandManager'); 107 | 108 | commandManager.execute(graph, EditorCommand.Add, { 109 | type: ItemType.Node, 110 | model: { 111 | id: guid(), 112 | x, 113 | y, 114 | ...model, 115 | }, 116 | }); 117 | }, 118 | }; 119 | 120 | behaviorManager.register('drag-add-node', dragAddNodeBehavior); 121 | -------------------------------------------------------------------------------- /src/components/Flow/behavior/index.ts: -------------------------------------------------------------------------------- 1 | import './dragAddNode'; 2 | import './dragAddEdge'; 3 | -------------------------------------------------------------------------------- /src/components/Flow/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import omit from 'lodash/omit'; 3 | import merge from 'lodash/merge'; 4 | import G6 from '@antv/g6'; 5 | import { guid } from '@/utils'; 6 | import global from '@/common/global'; 7 | import { FLOW_CONTAINER_ID, GraphType } from '@/common/constants'; 8 | import { Graph, GraphOptions, FlowData, GraphEvent, GraphReactEventProps } from '@/common/interfaces'; 9 | import behaviorManager from '@/common/behaviorManager'; 10 | import GraphComponent from '@/components/Graph'; 11 | 12 | import './behavior'; 13 | 14 | interface FlowProps extends Partial { 15 | style?: React.CSSProperties; 16 | className?: string; 17 | data: FlowData; 18 | graphConfig?: Partial; 19 | customModes?: (mode: string, behaviors: any) => object; 20 | } 21 | 22 | interface FlowState {} 23 | 24 | class Flow extends React.Component { 25 | static defaultProps = { 26 | graphConfig: {}, 27 | }; 28 | 29 | graph: Graph | null = null; 30 | 31 | containerId = `${FLOW_CONTAINER_ID}_${guid()}`; 32 | 33 | canDragNode = (e: GraphEvent) => { 34 | return !['anchor', 'banAnchor'].some(item => item === e.target.get('className')); 35 | }; 36 | 37 | canDragOrZoomCanvas = () => { 38 | const { graph } = this; 39 | 40 | if (!graph) { 41 | return false; 42 | } 43 | 44 | return ( 45 | global.plugin.itemPopover.state === 'hide' && 46 | global.plugin.contextMenu.state === 'hide' && 47 | global.plugin.editableLabel.state === 'hide' 48 | ); 49 | }; 50 | 51 | parseData = data => { 52 | const { nodes, edges } = data; 53 | 54 | [...nodes, ...edges].forEach(item => { 55 | const { id } = item; 56 | 57 | if (id) { 58 | return; 59 | } 60 | 61 | item.id = guid(); 62 | }); 63 | }; 64 | 65 | initGraph = (width: number, height: number) => { 66 | const { containerId } = this; 67 | const { graphConfig, customModes } = this.props; 68 | 69 | const modes: any = merge(behaviorManager.getRegisteredBehaviors(GraphType.Flow), { 70 | default: { 71 | 'drag-node': { 72 | type: 'drag-node', 73 | enableDelegate: true, 74 | shouldBegin: this.canDragNode, 75 | }, 76 | 'drag-canvas': { 77 | type: 'drag-canvas', 78 | shouldBegin: this.canDragOrZoomCanvas, 79 | shouldUpdate: this.canDragOrZoomCanvas, 80 | }, 81 | 'zoom-canvas': { 82 | type: 'zoom-canvas', 83 | shouldUpdate: this.canDragOrZoomCanvas, 84 | }, 85 | 'recall-edge': 'recall-edge', 86 | 'brush-select': 'brush-select', 87 | }, 88 | }); 89 | 90 | Object.keys(modes).forEach(mode => { 91 | const behaviors = modes[mode]; 92 | 93 | modes[mode] = Object.values(customModes ? customModes(mode, behaviors) : behaviors); 94 | }); 95 | 96 | this.graph = new G6.Graph({ 97 | container: containerId, 98 | width, 99 | height, 100 | modes, 101 | defaultNode: { 102 | type: 'bizFlowNode', 103 | }, 104 | defaultEdge: { 105 | type: 'bizFlowEdge', 106 | }, 107 | ...graphConfig, 108 | }); 109 | 110 | return this.graph; 111 | }; 112 | 113 | render() { 114 | const { containerId, parseData, initGraph } = this; 115 | 116 | return ( 117 | 123 | ); 124 | } 125 | } 126 | 127 | export default Flow; 128 | -------------------------------------------------------------------------------- /src/components/Graph/behavior/clickItem.ts: -------------------------------------------------------------------------------- 1 | import { isMind, isEdge, getGraphState, clearSelectedState } from '@/utils'; 2 | import { ItemState, GraphState, EditorEvent } from '@/common/constants'; 3 | import { Item, Behavior } from '@/common/interfaces'; 4 | import behaviorManager from '@/common/behaviorManager'; 5 | 6 | interface ClickItemBehavior extends Behavior { 7 | /** 处理点击事件 */ 8 | handleItemClick({ item }: { item: Item }): void; 9 | /** 处理画布点击 */ 10 | handleCanvasClick(): void; 11 | /** 处理按键按下 */ 12 | handleKeyDown(e: KeyboardEvent): void; 13 | /** 处理按键抬起 */ 14 | handleKeyUp(e: KeyboardEvent): void; 15 | } 16 | 17 | interface DefaultConfig { 18 | /** 是否支持多选 */ 19 | multiple: boolean; 20 | /** 是否按下多选 */ 21 | keydown: boolean; 22 | /** 多选按键码值 */ 23 | keyCode: number; 24 | } 25 | 26 | const clickItemBehavior: ClickItemBehavior & ThisType = { 27 | getDefaultCfg(): DefaultConfig { 28 | return { 29 | multiple: true, 30 | keydown: false, 31 | keyCode: 17, 32 | }; 33 | }, 34 | 35 | getEvents() { 36 | return { 37 | 'node:click': 'handleItemClick', 38 | 'edge:click': 'handleItemClick', 39 | 'canvas:click': 'handleCanvasClick', 40 | keydown: 'handleKeyDown', 41 | keyup: 'handleKeyUp', 42 | }; 43 | }, 44 | 45 | handleItemClick({ item }) { 46 | const { graph } = this; 47 | 48 | if (isMind(graph) && isEdge(item)) { 49 | return; 50 | } 51 | 52 | const isSelected = item.hasState(ItemState.Selected); 53 | 54 | if (this.multiple && this.keydown) { 55 | graph.setItemState(item, ItemState.Selected, !isSelected); 56 | } else { 57 | clearSelectedState(graph, selectedItem => { 58 | return selectedItem !== item; 59 | }); 60 | 61 | if (!isSelected) { 62 | graph.setItemState(item, ItemState.Selected, true); 63 | } 64 | } 65 | 66 | graph.emit(EditorEvent.onGraphStateChange, { 67 | graphState: getGraphState(graph), 68 | }); 69 | }, 70 | 71 | handleCanvasClick() { 72 | const { graph } = this; 73 | 74 | clearSelectedState(graph); 75 | 76 | graph.emit(EditorEvent.onGraphStateChange, { 77 | graphState: GraphState.CanvasSelected, 78 | }); 79 | }, 80 | 81 | handleKeyDown(e) { 82 | this.keydown = (e.keyCode || e.which) === this.keyCode; 83 | }, 84 | 85 | handleKeyUp() { 86 | this.keydown = false; 87 | }, 88 | }; 89 | 90 | behaviorManager.register('click-item', clickItemBehavior); 91 | -------------------------------------------------------------------------------- /src/components/Graph/behavior/dragCanvas.ts: -------------------------------------------------------------------------------- 1 | import { Behavior, GraphEvent } from '@/common/interfaces'; 2 | import behaviorManager from '@/common/behaviorManager'; 3 | 4 | interface DragCanvasBehavior extends Behavior { 5 | /** 开始拖拽坐标 */ 6 | origin: { 7 | x: number; 8 | y: number; 9 | } | null; 10 | /** 当前按键码值 */ 11 | keyCode: number | null; 12 | /** 正在拖拽标识 */ 13 | dragging: boolean; 14 | /** 是否能够拖拽 */ 15 | canDrag(): boolean; 16 | /** 更新当前窗口 */ 17 | updateViewport(e: GraphEvent): void; 18 | /** 处理画布拖拽开始 */ 19 | handleCanvasDragStart(e: GraphEvent): void; 20 | /** 处理画布拖拽 */ 21 | handleCanvasDrag(e: GraphEvent): void; 22 | /** 处理画布拖拽结束 */ 23 | handleCanvasDragEnd(e?: GraphEvent): void; 24 | /** 处理窗口鼠标弹起 */ 25 | handleWindowMouseUp: (e: MouseEvent) => void | null; 26 | /** 处理鼠标移出画布 */ 27 | handleCanvasMouseLeave(e: GraphEvent): void; 28 | /** 处理画布鼠标右键 */ 29 | handleCanvasContextMenu(e: GraphEvent): void; 30 | /** 处理按键按下 */ 31 | handleKeyDown(e: KeyboardEvent): void; 32 | /** 处理按键抬起 */ 33 | handleKeyUp(e: KeyboardEvent): void; 34 | } 35 | 36 | interface DefaultConfig { 37 | /** 允许拖拽 KeyCode */ 38 | allowKeyCode: number[]; 39 | /** 禁止拖拽 KeyCode */ 40 | notAllowKeyCode: number[]; 41 | } 42 | 43 | const dragCanvasBehavior: DragCanvasBehavior & ThisType = { 44 | origin: null, 45 | 46 | keyCode: null, 47 | 48 | dragging: false, 49 | 50 | handleWindowMouseUp: null, 51 | 52 | getDefaultCfg(): DefaultConfig { 53 | return { 54 | allowKeyCode: [], 55 | notAllowKeyCode: [16], 56 | }; 57 | }, 58 | 59 | getEvents() { 60 | return { 61 | 'canvas:dragstart': 'handleCanvasDragStart', 62 | 'canvas:drag': 'handleCanvasDrag', 63 | 'canvas:dragend': 'handleCanvasDragEnd', 64 | 'canvas:mouseleave': 'handleCanvasMouseLeave', 65 | 'canvas:contextmenu': 'handleCanvasContextMenu', 66 | keydown: 'handleKeyDown', 67 | keyup: 'handleKeyUp', 68 | }; 69 | }, 70 | 71 | canDrag() { 72 | const { keyCode, allowKeyCode, notAllowKeyCode } = this; 73 | 74 | let isAllow = !!!allowKeyCode.length; 75 | 76 | if (!keyCode) { 77 | return isAllow; 78 | } 79 | 80 | if (allowKeyCode.length && allowKeyCode.includes(keyCode)) { 81 | isAllow = true; 82 | } 83 | 84 | if (notAllowKeyCode.includes(keyCode)) { 85 | isAllow = false; 86 | } 87 | 88 | return isAllow; 89 | }, 90 | 91 | updateViewport(e) { 92 | const { clientX, clientY } = e; 93 | 94 | const dx = clientX - this.origin.x; 95 | const dy = clientY - this.origin.y; 96 | 97 | this.origin = { 98 | x: clientX, 99 | y: clientY, 100 | }; 101 | 102 | this.graph.translate(dx, dy); 103 | this.graph.paint(); 104 | }, 105 | 106 | handleCanvasDragStart(e) { 107 | if (!this.shouldBegin.call(this, e)) { 108 | return; 109 | } 110 | 111 | if (!this.canDrag()) { 112 | return; 113 | } 114 | 115 | this.origin = { 116 | x: e.clientX, 117 | y: e.clientY, 118 | }; 119 | 120 | this.dragging = false; 121 | }, 122 | 123 | handleCanvasDrag(e) { 124 | if (!this.shouldUpdate.call(this, e)) { 125 | return; 126 | } 127 | 128 | if (!this.canDrag()) { 129 | return; 130 | } 131 | 132 | if (!this.origin) { 133 | return; 134 | } 135 | 136 | if (!this.dragging) { 137 | this.dragging = true; 138 | } else { 139 | this.updateViewport(e); 140 | } 141 | }, 142 | 143 | handleCanvasDragEnd(e) { 144 | if (!this.shouldEnd.call(this, e)) { 145 | return; 146 | } 147 | 148 | if (!this.canDrag()) { 149 | return; 150 | } 151 | 152 | this.origin = null; 153 | this.dragging = false; 154 | 155 | if (this.handleWindowMouseUp) { 156 | document.body.removeEventListener('mouseup', this.handleWindowMouseUp, false); 157 | this.handleWindowMouseUp = null; 158 | } 159 | }, 160 | 161 | handleCanvasMouseLeave() { 162 | const canvasElement = this.graph.get('canvas').get('el'); 163 | 164 | if (this.handleWindowMouseUp) { 165 | return; 166 | } 167 | 168 | this.handleWindowMouseUp = e => { 169 | if (e.target !== canvasElement) { 170 | this.handleCanvasDragEnd(); 171 | } 172 | }; 173 | 174 | document.body.addEventListener('mouseup', this.handleWindowMouseUp, false); 175 | }, 176 | 177 | handleCanvasContextMenu() { 178 | this.origin = null; 179 | this.dragging = false; 180 | }, 181 | 182 | handleKeyDown(e) { 183 | this.keyCode = e.keyCode || e.which; 184 | }, 185 | 186 | handleKeyUp() { 187 | this.keyCode = null; 188 | }, 189 | }; 190 | 191 | behaviorManager.register('drag-canvas', dragCanvasBehavior); 192 | -------------------------------------------------------------------------------- /src/components/Graph/behavior/hoverItem.ts: -------------------------------------------------------------------------------- 1 | import { ItemState } from '@/common/constants'; 2 | import { Item, Behavior } from '@/common/interfaces'; 3 | import behaviorManager from '@/common/behaviorManager'; 4 | 5 | interface HoverItemBehavior extends Behavior { 6 | /** 处理鼠标进入 */ 7 | handleItemMouseenter({ item }: { item: Item }): void; 8 | /** 处理鼠标移出 */ 9 | handleItemMouseleave({ item }: { item: Item }): void; 10 | } 11 | 12 | const hoverItemBehavior: HoverItemBehavior = { 13 | getEvents() { 14 | return { 15 | 'node:mouseenter': 'handleItemMouseenter', 16 | 'edge:mouseenter': 'handleItemMouseenter', 17 | 'node:mouseleave': 'handleItemMouseleave', 18 | 'edge:mouseleave': 'handleItemMouseleave', 19 | }; 20 | }, 21 | 22 | handleItemMouseenter({ item }) { 23 | const { graph } = this; 24 | 25 | graph.setItemState(item, ItemState.Active, true); 26 | }, 27 | 28 | handleItemMouseleave({ item }) { 29 | const { graph } = this; 30 | 31 | graph.setItemState(item, ItemState.Active, false); 32 | }, 33 | }; 34 | 35 | behaviorManager.register('hover-item', hoverItemBehavior); 36 | -------------------------------------------------------------------------------- /src/components/Graph/behavior/index.ts: -------------------------------------------------------------------------------- 1 | import './clickItem'; 2 | import './hoverItem'; 3 | import './dragCanvas'; 4 | import './recallEdge'; 5 | -------------------------------------------------------------------------------- /src/components/Graph/behavior/recallEdge.ts: -------------------------------------------------------------------------------- 1 | import { isFlow, isMind, getFlowRecallEdges, getMindRecallEdges, executeBatch } from '@/utils'; 2 | import { ItemState } from '@/common/constants'; 3 | import { TreeGraph, Node, Edge, Behavior, GraphEvent } from '@/common/interfaces'; 4 | import behaviorManager from '@/common/behaviorManager'; 5 | 6 | interface RecallEdgeBehavior extends Behavior { 7 | /** 当前高亮边线 Id */ 8 | edgeIds: string[]; 9 | /** 设置高亮状态 */ 10 | setHighLightState(edges: Edge[]): void; 11 | /** 清除高亮状态 */ 12 | clearHighLightState(): void; 13 | /** 处理节点点击 */ 14 | handleNodeClick(e: GraphEvent): void; 15 | /** 处理边线点击 */ 16 | handleEdgeClick(e: GraphEvent): void; 17 | /** 处理画布点击 */ 18 | handleCanvasClick(e: GraphEvent): void; 19 | } 20 | 21 | const recallEdgeBehavior: RecallEdgeBehavior = { 22 | edgeIds: [], 23 | 24 | getEvents() { 25 | return { 26 | 'node:click': 'handleNodeClick', 27 | 'edge:click': 'handleEdgeClick', 28 | 'canvas:click': 'handleCanvasClick', 29 | }; 30 | }, 31 | 32 | setHighLightState(edges: Edge[]) { 33 | const { graph } = this; 34 | 35 | this.clearHighLightState(); 36 | 37 | executeBatch(graph, () => { 38 | edges.forEach(item => { 39 | graph.setItemState(item, ItemState.HighLight, true); 40 | }); 41 | }); 42 | 43 | this.edgeIds = edges.map(edge => edge.get('id')); 44 | }, 45 | 46 | clearHighLightState() { 47 | const { graph } = this; 48 | 49 | executeBatch(graph, () => { 50 | this.edgeIds.forEach(id => { 51 | const item = graph.findById(id); 52 | 53 | if (item && !item.destroyed) { 54 | graph.setItemState(item, ItemState.HighLight, false); 55 | } 56 | }); 57 | }); 58 | 59 | this.edgeIds = []; 60 | }, 61 | 62 | handleNodeClick({ item }) { 63 | const { graph } = this; 64 | 65 | let edges: Edge[] = []; 66 | 67 | if (isFlow(graph)) { 68 | edges = getFlowRecallEdges(graph, item as Node); 69 | } 70 | 71 | if (isMind(graph)) { 72 | edges = getMindRecallEdges(graph as TreeGraph, item as Node); 73 | } 74 | 75 | this.setHighLightState(edges); 76 | }, 77 | 78 | handleEdgeClick() { 79 | this.clearHighLightState(); 80 | }, 81 | 82 | handleCanvasClick() { 83 | this.clearHighLightState(); 84 | }, 85 | }; 86 | 87 | behaviorManager.register('recall-edge', recallEdgeBehavior); 88 | -------------------------------------------------------------------------------- /src/components/Graph/command/add.ts: -------------------------------------------------------------------------------- 1 | import { guid } from '@/utils'; 2 | import { ItemType } from '@/common/constants'; 3 | import { NodeModel, EdgeModel } from '@/common/interfaces'; 4 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base'; 5 | 6 | export interface AddCommandParams { 7 | type: ItemType; 8 | model: NodeModel | EdgeModel; 9 | } 10 | 11 | const addCommand: BaseCommand = { 12 | ...baseCommand, 13 | 14 | params: { 15 | type: ItemType.Node, 16 | model: { 17 | id: '', 18 | }, 19 | }, 20 | 21 | init() { 22 | const { model } = this.params; 23 | 24 | if (model.id) { 25 | return; 26 | } 27 | 28 | model.id = guid(); 29 | }, 30 | 31 | execute(graph) { 32 | const { type, model } = this.params; 33 | 34 | graph.add(type, model); 35 | 36 | this.setSelectedItems(graph, [model.id]); 37 | }, 38 | 39 | undo(graph) { 40 | const { model } = this.params; 41 | 42 | graph.remove(model.id); 43 | }, 44 | }; 45 | 46 | export default addCommand; 47 | -------------------------------------------------------------------------------- /src/components/Graph/command/base.ts: -------------------------------------------------------------------------------- 1 | import { isMind, getSelectedNodes, getSelectedEdges, setSelectedItems } from '@/utils'; 2 | import { LabelState, EditorEvent } from '@/common/constants'; 3 | import { Graph, Item, Node, Edge, Command } from '@/common/interfaces'; 4 | 5 | export interface BaseCommand

extends Command { 6 | /** 判断是否脑图 */ 7 | isMind(graph: G): boolean; 8 | /** 获取选中节点 */ 9 | getSelectedNodes(graph: G): Node[]; 10 | /** 获取选中连线 */ 11 | getSelectedEdges(graph: G): Edge[]; 12 | /** 设置选中元素 */ 13 | setSelectedItems(graph: G, items: Item[] | string[]): void; 14 | /** 编辑选中节点 */ 15 | editSelectedNode(graph: G): void; 16 | } 17 | 18 | export const baseCommand: BaseCommand = { 19 | name: '', 20 | 21 | params: {}, 22 | 23 | canExecute() { 24 | return true; 25 | }, 26 | 27 | shouldExecute() { 28 | return true; 29 | }, 30 | 31 | canUndo() { 32 | return true; 33 | }, 34 | 35 | init() {}, 36 | 37 | execute() {}, 38 | 39 | undo() {}, 40 | 41 | shortcuts: [], 42 | 43 | isMind, 44 | 45 | getSelectedNodes, 46 | 47 | getSelectedEdges, 48 | 49 | setSelectedItems, 50 | 51 | editSelectedNode(graph) { 52 | graph.emit(EditorEvent.onLabelStateChange, { 53 | labelState: LabelState.Show, 54 | }); 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/Graph/command/copy.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep'; 2 | import global from '@/common/global'; 3 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base'; 4 | import { NodeModel } from '@/common/interfaces'; 5 | 6 | const copyCommand: BaseCommand = { 7 | ...baseCommand, 8 | 9 | canExecute(graph) { 10 | return !!this.getSelectedNodes(graph).length; 11 | }, 12 | 13 | canUndo() { 14 | return false; 15 | }, 16 | 17 | execute(graph) { 18 | const selectedNodes = this.getSelectedNodes(graph); 19 | 20 | global.clipboard.models = cloneDeep(selectedNodes.map(node => node.getModel() as NodeModel)); 21 | }, 22 | 23 | shortcuts: [ 24 | ['metaKey', 'c'], 25 | ['ctrlKey', 'c'], 26 | ], 27 | }; 28 | 29 | export default copyCommand; 30 | -------------------------------------------------------------------------------- /src/components/Graph/command/index.ts: -------------------------------------------------------------------------------- 1 | import redo from './redo'; 2 | import undo from './undo'; 3 | import add from './add'; 4 | import remove from './remove'; 5 | import update from './update'; 6 | import copy from './copy'; 7 | import paste from './paste'; 8 | import pasteHere from './pasteHere'; 9 | import zoomIn from './zoomIn'; 10 | import zoomOut from './zoomOut'; 11 | 12 | export default { redo, undo, add, remove, update, copy, paste, pasteHere, zoomIn, zoomOut }; 13 | -------------------------------------------------------------------------------- /src/components/Graph/command/paste.ts: -------------------------------------------------------------------------------- 1 | import { guid, executeBatch } from '@/utils'; 2 | import global from '@/common/global'; 3 | import { ItemType } from '@/common/constants'; 4 | import { NodeModel } from '@/common/interfaces'; 5 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base'; 6 | 7 | export interface PasteCommandParams { 8 | models: NodeModel[]; 9 | } 10 | 11 | const pasteCommand: BaseCommand = { 12 | ...baseCommand, 13 | 14 | params: { 15 | models: [], 16 | }, 17 | 18 | canExecute() { 19 | return !!global.clipboard.models.length; 20 | }, 21 | 22 | init() { 23 | const { models } = global.clipboard; 24 | 25 | const offsetX = 10; 26 | const offsetY = 10; 27 | 28 | this.params = { 29 | models: models.map(model => { 30 | const { x, y } = model; 31 | 32 | return { 33 | ...model, 34 | id: guid(), 35 | x: x + offsetX, 36 | y: y + offsetY, 37 | }; 38 | }), 39 | }; 40 | }, 41 | 42 | execute(graph) { 43 | const { models } = this.params; 44 | 45 | executeBatch(graph, () => { 46 | models.forEach(model => { 47 | graph.addItem(ItemType.Node, model); 48 | }); 49 | }); 50 | 51 | this.setSelectedItems( 52 | graph, 53 | models.map(model => model.id), 54 | ); 55 | }, 56 | 57 | undo(graph) { 58 | const { models } = this.params; 59 | 60 | executeBatch(graph, () => { 61 | models.forEach(model => { 62 | graph.removeItem(model.id); 63 | }); 64 | }); 65 | }, 66 | 67 | shortcuts: [ 68 | ['metaKey', 'v'], 69 | ['ctrlKey', 'v'], 70 | ], 71 | }; 72 | 73 | export default pasteCommand; 74 | -------------------------------------------------------------------------------- /src/components/Graph/command/pasteHere.ts: -------------------------------------------------------------------------------- 1 | import { guid } from '@/utils'; 2 | import global from '@/common/global'; 3 | import { NodeModel } from '@/common/interfaces'; 4 | import { BaseCommand } from '@/components/Graph/command/base'; 5 | import pasteCommand from './paste'; 6 | 7 | export interface PasteHereCommandParams { 8 | models: NodeModel[]; 9 | } 10 | 11 | const pasteHereCommand: BaseCommand = { 12 | ...pasteCommand, 13 | 14 | params: { 15 | models: [], 16 | }, 17 | 18 | init() { 19 | const { point, models } = global.clipboard; 20 | 21 | this.params = { 22 | models: models.map(model => { 23 | const { x, y } = model; 24 | 25 | const offsetX = point.x - x; 26 | const offsetY = point.y - y; 27 | 28 | return { 29 | ...model, 30 | id: guid(), 31 | x: x + offsetX, 32 | y: y + offsetY, 33 | }; 34 | }), 35 | }; 36 | }, 37 | 38 | shortcuts: [], 39 | }; 40 | 41 | export default pasteHereCommand; 42 | -------------------------------------------------------------------------------- /src/components/Graph/command/redo.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@/common/interfaces'; 2 | import CommandManager from '@/common/CommandManager'; 3 | 4 | const redoCommand: Command = { 5 | name: 'redo', 6 | 7 | params: {}, 8 | 9 | canExecute(graph) { 10 | const commandManager: CommandManager = graph.get('commandManager'); 11 | const { commandQueue, commandIndex } = commandManager; 12 | 13 | return commandIndex < commandQueue.length; 14 | }, 15 | 16 | shouldExecute() { 17 | return true; 18 | }, 19 | 20 | canUndo() { 21 | return false; 22 | }, 23 | 24 | init() {}, 25 | 26 | execute(graph) { 27 | const commandManager: CommandManager = graph.get('commandManager'); 28 | const { commandQueue, commandIndex } = commandManager; 29 | 30 | commandQueue[commandIndex].execute(graph); 31 | 32 | commandManager.commandIndex += 1; 33 | }, 34 | 35 | undo() {}, 36 | 37 | shortcuts: [ 38 | ['metaKey', 'shiftKey', 'z'], 39 | ['ctrlKey', 'shiftKey', 'z'], 40 | ], 41 | }; 42 | 43 | export default redoCommand; 44 | -------------------------------------------------------------------------------- /src/components/Graph/command/remove.ts: -------------------------------------------------------------------------------- 1 | import { isMind, executeBatch } from '@/utils'; 2 | import { ItemType } from '@/common/constants'; 3 | import { TreeGraph, MindData, NodeModel, EdgeModel } from '@/common/interfaces'; 4 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base'; 5 | 6 | export interface RemoveCommandParams { 7 | flow: { 8 | nodes: { 9 | [id: string]: NodeModel; 10 | }; 11 | edges: { 12 | [id: string]: EdgeModel; 13 | }; 14 | }; 15 | mind: { 16 | model: MindData | null; 17 | parent: string; 18 | }; 19 | } 20 | 21 | const removeCommand: BaseCommand = { 22 | ...baseCommand, 23 | 24 | params: { 25 | flow: { 26 | nodes: {}, 27 | edges: {}, 28 | }, 29 | mind: { 30 | model: null, 31 | parent: '', 32 | }, 33 | }, 34 | 35 | canExecute(graph) { 36 | const selectedNodes = this.getSelectedNodes(graph); 37 | const selectedEdges = this.getSelectedEdges(graph); 38 | 39 | return !!(selectedNodes.length || selectedEdges.length); 40 | }, 41 | 42 | init(graph) { 43 | const selectedNodes = this.getSelectedNodes(graph); 44 | const selectedEdges = this.getSelectedEdges(graph); 45 | 46 | if (isMind(graph)) { 47 | const selectedNode = selectedNodes[0]; 48 | const selectedNodeModel = selectedNode.getModel() as MindData; 49 | 50 | const selectedNodeParent = selectedNode.get('parent'); 51 | const selectedNodeParentModel = selectedNodeParent ? selectedNodeParent.getModel() : {}; 52 | 53 | this.params.mind = { 54 | model: selectedNodeModel, 55 | parent: selectedNodeParentModel.id, 56 | }; 57 | } else { 58 | const { nodes, edges } = this.params.flow; 59 | 60 | selectedNodes.forEach(node => { 61 | const nodeModel = node.getModel() as NodeModel; 62 | const nodeEdges = node.getEdges(); 63 | 64 | nodes[nodeModel.id] = nodeModel; 65 | 66 | nodeEdges.forEach(edge => { 67 | const edgeModel = edge.getModel(); 68 | 69 | edges[edgeModel.id] = edgeModel; 70 | }); 71 | }); 72 | 73 | selectedEdges.forEach(edge => { 74 | const edgeModel = edge.getModel(); 75 | 76 | edges[edgeModel.id] = edgeModel; 77 | }); 78 | } 79 | }, 80 | 81 | execute(graph) { 82 | if (isMind(graph)) { 83 | const { model } = this.params.mind; 84 | 85 | if (!model) { 86 | return; 87 | } 88 | 89 | (graph as TreeGraph).removeChild(model.id); 90 | } else { 91 | const { nodes, edges } = this.params.flow; 92 | 93 | executeBatch(graph, () => { 94 | [...Object.keys(nodes), ...Object.keys(edges)].forEach(id => { 95 | graph.removeItem(id); 96 | }); 97 | }); 98 | } 99 | }, 100 | 101 | undo(graph) { 102 | if (isMind(graph)) { 103 | const { model, parent } = this.params.mind; 104 | 105 | if (!model) { 106 | return; 107 | } 108 | 109 | (graph as TreeGraph).addChild(model, parent); 110 | } else { 111 | const { nodes, edges } = this.params.flow; 112 | 113 | executeBatch(graph, () => { 114 | Object.keys(nodes).forEach(id => { 115 | const model = nodes[id]; 116 | 117 | graph.addItem(ItemType.Node, model); 118 | }); 119 | 120 | Object.keys(edges).forEach(id => { 121 | const model = edges[id]; 122 | 123 | graph.addItem(ItemType.Edge, model); 124 | }); 125 | }); 126 | } 127 | }, 128 | 129 | shortcuts: ['Delete', 'Backspace'], 130 | }; 131 | 132 | export default removeCommand; 133 | -------------------------------------------------------------------------------- /src/components/Graph/command/undo.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@/common/interfaces'; 2 | import CommandManager from '@/common/CommandManager'; 3 | 4 | const undoCommand: Command = { 5 | name: 'undo', 6 | 7 | params: {}, 8 | 9 | canExecute(graph) { 10 | const commandManager: CommandManager = graph.get('commandManager'); 11 | const { commandIndex } = commandManager; 12 | 13 | return commandIndex > 0; 14 | }, 15 | 16 | shouldExecute() { 17 | return true; 18 | }, 19 | 20 | canUndo() { 21 | return false; 22 | }, 23 | 24 | init() {}, 25 | 26 | execute(graph) { 27 | const commandManager: CommandManager = graph.get('commandManager'); 28 | const { commandQueue, commandIndex } = commandManager; 29 | 30 | commandQueue[commandIndex - 1].undo(graph); 31 | 32 | commandManager.commandIndex -= 1; 33 | }, 34 | 35 | undo() {}, 36 | 37 | shortcuts: [ 38 | ['metaKey', 'z'], 39 | ['ctrlKey', 'z'], 40 | ], 41 | }; 42 | 43 | export default undoCommand; 44 | -------------------------------------------------------------------------------- /src/components/Graph/command/update.ts: -------------------------------------------------------------------------------- 1 | import pick from 'lodash/pick'; 2 | import { Graph, TreeGraph, NodeModel, EdgeModel } from '@/common/interfaces'; 3 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base'; 4 | 5 | export interface UpdateCommandParams { 6 | id: string; 7 | originModel: Partial | EdgeModel; 8 | updateModel: Partial | EdgeModel; 9 | forceRefreshLayout: boolean; 10 | } 11 | 12 | const updateCommand: BaseCommand = { 13 | ...baseCommand, 14 | 15 | params: { 16 | id: '', 17 | originModel: {}, 18 | updateModel: {}, 19 | forceRefreshLayout: false, 20 | }, 21 | 22 | canExecute(graph) { 23 | const selectedNodes = this.getSelectedNodes(graph); 24 | const selectedEdges = this.getSelectedEdges(graph); 25 | return (selectedNodes.length || selectedEdges.length) && (selectedNodes.length === 1 || selectedEdges.length === 1) 26 | ? true 27 | : false; 28 | }, 29 | 30 | init(graph) { 31 | const { id, updateModel } = this.params; 32 | 33 | const updatePaths = Object.keys(updateModel); 34 | const originModel = pick(graph.findById(id).getModel(), updatePaths); 35 | 36 | this.params.originModel = originModel; 37 | }, 38 | 39 | execute(graph) { 40 | const { id, updateModel, forceRefreshLayout } = this.params; 41 | 42 | graph.updateItem(id, updateModel); 43 | 44 | if (forceRefreshLayout) { 45 | graph.refreshLayout && graph.refreshLayout(false); 46 | } 47 | }, 48 | 49 | undo(graph) { 50 | const { id, originModel } = this.params; 51 | 52 | graph.updateItem(id, originModel); 53 | }, 54 | }; 55 | 56 | export default updateCommand; 57 | -------------------------------------------------------------------------------- /src/components/Graph/command/zoomIn.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base'; 2 | 3 | const DELTA = 0.05; 4 | 5 | const zoomInCommand: BaseCommand = { 6 | ...baseCommand, 7 | 8 | canUndo() { 9 | return false; 10 | }, 11 | 12 | execute(graph) { 13 | const ratio = 1 + DELTA; 14 | 15 | const zoom = graph.getZoom() * ratio; 16 | const maxZoom = graph.get('maxZoom'); 17 | 18 | if (zoom > maxZoom) { 19 | return; 20 | } 21 | 22 | graph.zoom(ratio); 23 | }, 24 | 25 | shortcuts: [ 26 | ['metaKey', '='], 27 | ['ctrlKey', '='], 28 | ], 29 | }; 30 | 31 | export default zoomInCommand; 32 | -------------------------------------------------------------------------------- /src/components/Graph/command/zoomOut.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base'; 2 | 3 | const DELTA = 0.05; 4 | 5 | const zoomOutCommand: BaseCommand = { 6 | ...baseCommand, 7 | 8 | canUndo() { 9 | return false; 10 | }, 11 | 12 | execute(graph) { 13 | const ratio = 1 - DELTA; 14 | 15 | const zoom = graph.getZoom() * ratio; 16 | const minZoom = graph.get('minZoom'); 17 | 18 | if (zoom < minZoom) { 19 | return; 20 | } 21 | 22 | graph.zoom(ratio); 23 | }, 24 | 25 | shortcuts: [ 26 | ['metaKey', '-'], 27 | ['ctrlKey', '-'], 28 | ], 29 | }; 30 | 31 | export default zoomOutCommand; 32 | -------------------------------------------------------------------------------- /src/components/Graph/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pick from 'lodash/pick'; 3 | import { isMind } from '@/utils'; 4 | import { track } from '@/helpers'; 5 | import global from '@/common/global'; 6 | import { 7 | GraphType, 8 | GraphCommonEvent, 9 | GraphNodeEvent, 10 | GraphEdgeEvent, 11 | GraphCanvasEvent, 12 | GraphCustomEvent, 13 | } from '@/common/constants'; 14 | import { 15 | Graph, 16 | FlowData, 17 | MindData, 18 | GraphNativeEvent, 19 | GraphReactEvent, 20 | GraphReactEventProps, 21 | } from '@/common/interfaces'; 22 | import { EditorPrivateContextProps, withEditorPrivateContext } from '@/components/EditorContext'; 23 | 24 | import baseCommands from './command'; 25 | import mindCommands from '@/components/Mind/command'; 26 | 27 | import './behavior'; 28 | 29 | interface GraphProps extends Partial, EditorPrivateContextProps { 30 | style?: React.CSSProperties; 31 | className?: string; 32 | containerId: string; 33 | data: FlowData | MindData; 34 | parseData(data: object): void; 35 | initGraph(width: number, height: number): Graph; 36 | } 37 | 38 | interface GraphState {} 39 | 40 | class GraphComponent extends React.Component { 41 | graph: Graph | null = null; 42 | 43 | componentDidMount() { 44 | this.initGraph(); 45 | this.bindEvent(); 46 | } 47 | 48 | componentDidUpdate(prevProps: GraphProps) { 49 | const { data } = this.props; 50 | 51 | if (data !== prevProps.data) { 52 | this.changeData(data); 53 | } 54 | } 55 | 56 | focusRootNode(graph: Graph, data: FlowData | MindData) { 57 | if (!isMind(graph)) { 58 | return; 59 | } 60 | 61 | const { id } = data as MindData; 62 | 63 | graph.focusItem(id); 64 | } 65 | 66 | initGraph() { 67 | const { containerId, parseData, initGraph, setGraph, commandManager } = this.props; 68 | const { clientWidth = 0, clientHeight = 0 } = document.getElementById(containerId) || {}; 69 | 70 | // 解析数据 71 | const data = { ...this.props.data }; 72 | 73 | parseData(data); 74 | 75 | // 初始画布 76 | this.graph = initGraph(clientWidth, clientHeight); 77 | 78 | this.graph.data(data); 79 | this.graph.render(); 80 | this.focusRootNode(this.graph, data); 81 | this.graph.setMode('default'); 82 | 83 | setGraph(this.graph); 84 | 85 | // 设置命令管理器 86 | this.graph.set('commandManager', commandManager); 87 | 88 | // 注册命令 89 | let commands = baseCommands; 90 | 91 | if (isMind(this.graph)) { 92 | commands = { 93 | ...commands, 94 | ...mindCommands, 95 | }; 96 | } 97 | 98 | Object.keys(commands).forEach(name => { 99 | commandManager.register(name, commands[name]); 100 | }); 101 | 102 | // 发送埋点 103 | if (global.trackable) { 104 | const graphType = isMind(this.graph) ? GraphType.Mind : GraphType.Flow; 105 | 106 | track(graphType); 107 | } 108 | } 109 | 110 | bindEvent() { 111 | const { graph, props } = this; 112 | 113 | if (!graph) { 114 | return; 115 | } 116 | 117 | const events: { 118 | [propName in GraphReactEvent]: GraphNativeEvent; 119 | } = { 120 | ...GraphCommonEvent, 121 | ...GraphNodeEvent, 122 | ...GraphEdgeEvent, 123 | ...GraphCanvasEvent, 124 | ...GraphCustomEvent, 125 | }; 126 | 127 | (Object.keys(events) as GraphReactEvent[]).forEach(event => { 128 | if (typeof props[event] === 'function') { 129 | graph.on(events[event], props[event]); 130 | } 131 | }); 132 | } 133 | 134 | changeData(data: any) { 135 | const { graph } = this; 136 | const { parseData } = this.props; 137 | 138 | if (!graph) { 139 | return; 140 | } 141 | 142 | parseData(data); 143 | 144 | graph.changeData(data); 145 | this.focusRootNode(graph, data); 146 | } 147 | 148 | render() { 149 | const { containerId, children } = this.props; 150 | 151 | return ( 152 |

153 | {children} 154 |
155 | ); 156 | } 157 | } 158 | 159 | export default withEditorPrivateContext(GraphComponent); 160 | -------------------------------------------------------------------------------- /src/components/ItemPanel/Item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pick from 'lodash/pick'; 3 | import global from '@/common/global'; 4 | import { ItemType, GraphMode } from '@/common/constants'; 5 | import { NodeModel } from '@/common/interfaces'; 6 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext'; 7 | 8 | export interface ItemProps extends EditorContextProps { 9 | style?: React.CSSProperties; 10 | className?: string; 11 | type?: ItemType; 12 | model: Partial; 13 | } 14 | 15 | export interface ItemState {} 16 | 17 | class Item extends React.Component { 18 | static defaultProps = { 19 | type: ItemType.Node, 20 | }; 21 | 22 | handleMouseDown = () => { 23 | const { graph, type, model } = this.props; 24 | 25 | if (type === ItemType.Node) { 26 | global.component.itemPanel.model = model; 27 | graph.setMode(GraphMode.AddNode); 28 | } 29 | }; 30 | 31 | render() { 32 | const { children } = this.props; 33 | 34 | return ( 35 |
36 | {children} 37 |
38 | ); 39 | } 40 | } 41 | 42 | export default withEditorContext(Item); 43 | -------------------------------------------------------------------------------- /src/components/ItemPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pick from 'lodash/pick'; 3 | import global from '@/common/global'; 4 | import { GraphMode } from '@/common/constants'; 5 | import { GShape, GGroup } from '@/common/interfaces'; 6 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext'; 7 | import Item from './Item'; 8 | 9 | interface ItemPanelProps extends EditorContextProps { 10 | style?: React.CSSProperties; 11 | className?: string; 12 | } 13 | 14 | interface ItemPanelState {} 15 | 16 | class ItemPanel extends React.Component { 17 | static Item = Item; 18 | 19 | componentDidMount() { 20 | document.addEventListener('mouseup', this.handleMouseUp, false); 21 | } 22 | 23 | componentWillUnmount() { 24 | document.removeEventListener('mouseup', this.handleMouseUp, false); 25 | } 26 | 27 | handleMouseUp = () => { 28 | const { graph } = this.props; 29 | 30 | if (graph.getCurrentMode() === GraphMode.Default) { 31 | return; 32 | } 33 | 34 | const group: GGroup = graph.get('group'); 35 | const shape: GShape = group.findByClassName(global.component.itemPanel.delegateShapeClassName) as GShape; 36 | 37 | if (shape) { 38 | shape.remove(true); 39 | graph.paint(); 40 | } 41 | 42 | global.component.itemPanel.model = null; 43 | graph.setMode(GraphMode.Default); 44 | }; 45 | 46 | render() { 47 | const { children } = this.props; 48 | 49 | return
{children}
; 50 | } 51 | } 52 | 53 | export { Item }; 54 | 55 | export default withEditorContext(ItemPanel); 56 | -------------------------------------------------------------------------------- /src/components/Mind/command/fold.ts: -------------------------------------------------------------------------------- 1 | import { TreeGraph, NodeModel } from '@/common/interfaces'; 2 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base'; 3 | 4 | export interface FoldCommandParams { 5 | id: string; 6 | } 7 | 8 | const foldCommand: BaseCommand = { 9 | ...baseCommand, 10 | 11 | params: { 12 | id: '', 13 | }, 14 | 15 | canExecute(graph: TreeGraph) { 16 | const selectedNodes = this.getSelectedNodes(graph); 17 | 18 | if (!selectedNodes.length) { 19 | return false; 20 | } 21 | 22 | const selectedNode = selectedNodes[0]; 23 | const selectedNodeModel = selectedNode.getModel() as NodeModel; 24 | 25 | if (!selectedNodeModel.children || !selectedNodeModel.children.length) { 26 | return false; 27 | } 28 | 29 | if (selectedNodeModel.collapsed) { 30 | return false; 31 | } 32 | 33 | return true; 34 | }, 35 | 36 | init(graph) { 37 | const selectedNode = this.getSelectedNodes(graph)[0]; 38 | const selectedNodeModel = selectedNode.getModel(); 39 | 40 | this.params = { 41 | id: selectedNodeModel.id, 42 | }; 43 | }, 44 | 45 | execute(graph: TreeGraph) { 46 | const { id } = this.params; 47 | 48 | const sourceData = graph.findDataById(id); 49 | 50 | sourceData.collapsed = !sourceData.collapsed; 51 | 52 | graph.refreshLayout(false); 53 | }, 54 | 55 | undo(graph) { 56 | this.execute(graph); 57 | }, 58 | 59 | shortcuts: [ 60 | ['metaKey', '/'], 61 | ['ctrlKey', '/'], 62 | ], 63 | }; 64 | 65 | export default foldCommand; 66 | -------------------------------------------------------------------------------- /src/components/Mind/command/index.ts: -------------------------------------------------------------------------------- 1 | import topic from './topic'; 2 | import subtopic from './subtopic'; 3 | import fold from './fold'; 4 | import unfold from './unfold'; 5 | 6 | export default { topic, subtopic, fold, unfold }; 7 | -------------------------------------------------------------------------------- /src/components/Mind/command/subtopic.ts: -------------------------------------------------------------------------------- 1 | import { TreeGraph, MindData } from '@/common/interfaces'; 2 | import { BaseCommand } from '@/components/Graph/command/base'; 3 | import topicCommand from './topic'; 4 | 5 | export interface SubtopicCommandParams { 6 | id: string; 7 | model: MindData; 8 | } 9 | 10 | const subtopicCommand: BaseCommand = { 11 | ...topicCommand, 12 | 13 | canExecute(graph) { 14 | return this.getSelectedNodes(graph)[0] ? true : false; 15 | }, 16 | 17 | execute(graph) { 18 | const { id, model } = this.params; 19 | 20 | // 添加节点 21 | graph.addChild(model, id); 22 | 23 | // 选中节点 24 | this.setSelectedItems(graph, [model.id]); 25 | 26 | // 编辑节点 27 | this.editSelectedNode(graph); 28 | }, 29 | 30 | shortcuts: ['Tab'], 31 | }; 32 | 33 | export default subtopicCommand; 34 | -------------------------------------------------------------------------------- /src/components/Mind/command/topic.ts: -------------------------------------------------------------------------------- 1 | import { guid } from '@/utils'; 2 | import { LABEL_DEFAULT_TEXT } from '@/common/constants'; 3 | import { TreeGraph, MindData } from '@/common/interfaces'; 4 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base'; 5 | 6 | export interface TopicCommandParams { 7 | id: string; 8 | model: MindData; 9 | } 10 | 11 | export const topicCommand: BaseCommand = { 12 | ...baseCommand, 13 | 14 | params: { 15 | id: '', 16 | model: { 17 | id: '', 18 | }, 19 | }, 20 | 21 | canExecute(graph) { 22 | const selectedNodes = this.getSelectedNodes(graph); 23 | 24 | return selectedNodes.length && selectedNodes.length === 1 && selectedNodes[0].get('parent'); 25 | }, 26 | 27 | init(graph) { 28 | if (this.params.id) { 29 | return; 30 | } 31 | 32 | const selectedNode = this.getSelectedNodes(graph)[0]; 33 | 34 | this.params = { 35 | id: selectedNode.get('id'), 36 | model: { 37 | id: guid(), 38 | label: LABEL_DEFAULT_TEXT, 39 | }, 40 | }; 41 | }, 42 | 43 | execute(graph) { 44 | const { id, model } = this.params; 45 | 46 | const parent = graph.findById(id).get('parent'); 47 | 48 | // 添加节点 49 | graph.addChild(model, parent); 50 | 51 | // 选中节点 52 | this.setSelectedItems(graph, [model.id]); 53 | 54 | // 编辑节点 55 | this.editSelectedNode(graph); 56 | }, 57 | 58 | undo(graph) { 59 | const { id, model } = this.params; 60 | 61 | this.setSelectedItems(graph, [id]); 62 | 63 | graph.removeChild(model.id); 64 | }, 65 | 66 | shortcuts: ['Enter'], 67 | }; 68 | 69 | export default topicCommand; 70 | -------------------------------------------------------------------------------- /src/components/Mind/command/unfold.ts: -------------------------------------------------------------------------------- 1 | import { TreeGraph, NodeModel } from '@/common/interfaces'; 2 | import { BaseCommand } from '@/components/Graph/command/base'; 3 | import foldCommand from './fold'; 4 | 5 | export interface UnfoldCommandParams { 6 | id: string; 7 | } 8 | 9 | const unfoldCommand: BaseCommand = { 10 | ...foldCommand, 11 | 12 | canExecute(graph: TreeGraph) { 13 | const selectedNodes = this.getSelectedNodes(graph); 14 | 15 | if (!selectedNodes.length) { 16 | return false; 17 | } 18 | 19 | const selectedNode = selectedNodes[0]; 20 | const selectedNodeModel = selectedNode.getModel() as NodeModel; 21 | 22 | if (!selectedNodeModel.children || !selectedNodeModel.children.length) { 23 | return false; 24 | } 25 | 26 | if (!selectedNodeModel.collapsed) { 27 | return false; 28 | } 29 | 30 | return true; 31 | }, 32 | 33 | shortcuts: [ 34 | ['metaKey', '/'], 35 | ['ctrlKey', '/'], 36 | ], 37 | }; 38 | 39 | export default unfoldCommand; 40 | -------------------------------------------------------------------------------- /src/components/Mind/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import omit from 'lodash/omit'; 3 | import merge from 'lodash/merge'; 4 | import G6 from '@antv/g6'; 5 | import { guid, recursiveTraversal } from '@/utils'; 6 | import global from '@/common/global'; 7 | import { MIND_CONTAINER_ID, GraphType } from '@/common/constants'; 8 | import { FOLD_BUTTON_CLASS_NAME, UNFOLD_BUTTON_CLASS_NAME } from '@/shape/nodes/bizMindNode'; 9 | import { Graph, GraphOptions, MindData, GraphReactEventProps } from '@/common/interfaces'; 10 | import behaviorManager from '@/common/behaviorManager'; 11 | import GraphComponent from '@/components/Graph'; 12 | 13 | import './command'; 14 | 15 | interface MindProps extends Partial { 16 | style?: React.CSSProperties; 17 | className?: string; 18 | data: MindData; 19 | graphConfig?: Partial; 20 | customModes?: (mode: string, behaviors: any) => object; 21 | } 22 | 23 | interface MindState {} 24 | 25 | class Mind extends React.Component { 26 | static defaultProps = { 27 | graphConfig: {}, 28 | }; 29 | 30 | graph: Graph | null = null; 31 | 32 | containerId = `${MIND_CONTAINER_ID}_${guid()}`; 33 | 34 | canDragOrZoomCanvas = () => { 35 | const { graph } = this; 36 | 37 | if (!graph) { 38 | return false; 39 | } 40 | 41 | return ( 42 | global.plugin.itemPopover.state === 'hide' && 43 | global.plugin.contextMenu.state === 'hide' && 44 | global.plugin.editableLabel.state === 'hide' 45 | ); 46 | }; 47 | 48 | canCollapseExpand = ({ target }) => { 49 | return target && [FOLD_BUTTON_CLASS_NAME, UNFOLD_BUTTON_CLASS_NAME].includes(target.get('className')); 50 | }; 51 | 52 | parseData = data => { 53 | recursiveTraversal(data, item => { 54 | const { id } = item; 55 | 56 | if (id) { 57 | return; 58 | } 59 | 60 | item.id = guid(); 61 | }); 62 | }; 63 | 64 | initGraph = (width: number, height: number) => { 65 | const { containerId } = this; 66 | const { graphConfig, customModes } = this.props; 67 | 68 | const modes: any = merge(behaviorManager.getRegisteredBehaviors(GraphType.Mind), { 69 | default: { 70 | 'click-item': { 71 | type: 'click-item', 72 | multiple: false, 73 | }, 74 | 'collapse-expand': { 75 | type: 'collapse-expand', 76 | shouldBegin: this.canCollapseExpand, 77 | }, 78 | 'drag-canvas': { 79 | type: 'drag-canvas', 80 | shouldBegin: this.canDragOrZoomCanvas, 81 | shouldUpdate: this.canDragOrZoomCanvas, 82 | }, 83 | 'zoom-canvas': { 84 | type: 'zoom-canvas', 85 | shouldUpdate: this.canDragOrZoomCanvas, 86 | }, 87 | }, 88 | }); 89 | 90 | Object.keys(modes).forEach(mode => { 91 | const behaviors = modes[mode]; 92 | 93 | modes[mode] = Object.values(customModes ? customModes(mode, behaviors) : behaviors); 94 | }); 95 | 96 | this.graph = new G6.TreeGraph({ 97 | container: containerId, 98 | width, 99 | height, 100 | modes, 101 | layout: { 102 | type: 'mindmap', 103 | direction: 'H', 104 | getWidth: () => 120, 105 | getHeight: () => 60, 106 | getHGap: () => 100, 107 | getVGap: () => 50, 108 | getSide: ({ data }) => { 109 | if (data.side) { 110 | return data.side; 111 | } 112 | 113 | return 'right'; 114 | }, 115 | }, 116 | animate: false, 117 | defaultNode: { 118 | type: 'bizMindNode', 119 | }, 120 | defaultEdge: { 121 | type: 'bizMindEdge', 122 | }, 123 | ...graphConfig, 124 | }); 125 | 126 | return this.graph; 127 | }; 128 | 129 | render() { 130 | const { containerId, parseData, initGraph } = this; 131 | const { data } = this.props; 132 | 133 | return ( 134 | 141 | ); 142 | } 143 | } 144 | 145 | export default Mind; 146 | -------------------------------------------------------------------------------- /src/components/Register/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import G6 from '@antv/g6'; 3 | import { Command, Behavior } from '@/common/interfaces'; 4 | import behaviorManager from '@/common/behaviorManager'; 5 | import { EditorPrivateContextProps, withEditorPrivateContext } from '@/components/EditorContext'; 6 | 7 | interface RegisterProps extends EditorPrivateContextProps { 8 | name: string; 9 | config: object; 10 | extend?: string; 11 | } 12 | interface RegisterState {} 13 | 14 | class Register extends React.Component { 15 | static create = function(type: string) { 16 | class TypedRegister extends Register { 17 | constructor(props: RegisterProps) { 18 | super(props, type); 19 | } 20 | } 21 | 22 | return withEditorPrivateContext(TypedRegister); 23 | }; 24 | 25 | constructor(props: RegisterProps, type: string) { 26 | super(props); 27 | 28 | const { name, config, extend, commandManager } = props; 29 | 30 | switch (type) { 31 | case 'node': 32 | G6.registerNode(name, config, extend); 33 | break; 34 | 35 | case 'edge': 36 | G6.registerEdge(name, config, extend); 37 | break; 38 | 39 | case 'command': 40 | commandManager.register(name, config as Command); 41 | break; 42 | 43 | case 'behavior': 44 | behaviorManager.register(name, config as Behavior); 45 | break; 46 | 47 | default: 48 | break; 49 | } 50 | } 51 | 52 | render() { 53 | return null; 54 | } 55 | } 56 | 57 | export const RegisterNode = Register.create('node'); 58 | export const RegisterEdge = Register.create('edge'); 59 | export const RegisterCommand = Register.create('command'); 60 | export const RegisterBehavior = Register.create('behavior'); 61 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import global from '@/common/global'; 2 | import { toQueryString } from '@/utils'; 3 | import { GraphType } from '@/common/constants'; 4 | 5 | const BASE_URL = 'http://gm.mmstat.com/fsp.1.1'; 6 | 7 | export function track(graphType: GraphType) { 8 | const version = global.version; 9 | const trackable = global.trackable; 10 | 11 | if (!trackable) { 12 | return; 13 | } 14 | 15 | const { location, navigator } = window; 16 | const image = new Image(); 17 | const params = toQueryString({ 18 | pid: 'ggeditor', 19 | code: '11', 20 | msg: 'syslog', 21 | page: `${location.protocol}//${location.host}${location.pathname}`, 22 | hash: location.hash, 23 | ua: navigator.userAgent, 24 | rel: version, 25 | c1: graphType, 26 | }); 27 | 28 | image.src = `${BASE_URL}?${params}`; 29 | } 30 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import G6 from '@antv/g6'; 2 | 3 | import '@/shape'; 4 | 5 | import * as Util from '@/utils'; 6 | 7 | import Editor from '@/components/Editor'; 8 | import Flow from '@/components/Flow'; 9 | import Mind from '@/components/Mind'; 10 | import Command from '@/components/Command'; 11 | import ItemPanel, { Item } from '@/components/ItemPanel'; 12 | import DetailPanel from '@/components/DetailPanel'; 13 | import { RegisterNode, RegisterEdge, RegisterCommand, RegisterBehavior } from '@/components/Register'; 14 | import { withEditorContext } from '@/components/EditorContext'; 15 | import { baseCommand } from '@/components/Graph/command/base'; 16 | 17 | import ItemPopover from '@/plugins/ItemPopover'; 18 | import ContextMenu from '@/plugins/ContextMenu'; 19 | import EditableLabel from '@/plugins/EditableLabel'; 20 | 21 | import global from '@/common/global'; 22 | import * as constants from '@/common/constants'; 23 | import CommandManager from '@/common/CommandManager'; 24 | import behaviorManager from '@/common/behaviorManager'; 25 | 26 | import { setAnchorPointsState } from '@/shape/common/anchor'; 27 | 28 | export { 29 | G6, 30 | Util, 31 | Flow, 32 | Mind, 33 | Command, 34 | Item, 35 | ItemPanel, 36 | DetailPanel, 37 | RegisterNode, 38 | RegisterEdge, 39 | RegisterCommand, 40 | RegisterBehavior, 41 | withEditorContext, 42 | baseCommand, 43 | ItemPopover, 44 | ContextMenu, 45 | EditableLabel, 46 | global, 47 | constants, 48 | CommandManager, 49 | behaviorManager, 50 | setAnchorPointsState, 51 | }; 52 | 53 | export default Editor; 54 | -------------------------------------------------------------------------------- /src/plugins/ContextMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { clearSelectedState } from '@/utils'; 4 | import global from '@/common/global'; 5 | import { ItemState, GraphCommonEvent, GraphNodeEvent, GraphEdgeEvent, GraphCanvasEvent } from '@/common/constants'; 6 | import { GraphEvent, Item, Node, Edge } from '@/common/interfaces'; 7 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext'; 8 | 9 | export enum ContextMenuType { 10 | Canvas = 'canvas', 11 | Node = 'node', 12 | Edge = 'edge', 13 | } 14 | 15 | interface ContextMenuProps extends EditorContextProps { 16 | /** 菜单类型 */ 17 | type?: ContextMenuType; 18 | /** 菜单内容 */ 19 | renderContent: (item: Item, position: { x: number; y: number }, hide: () => void) => React.ReactNode; 20 | } 21 | 22 | interface ContextMenuState { 23 | visible: boolean; 24 | content: React.ReactNode; 25 | } 26 | 27 | class ContextMenu extends React.Component { 28 | static defaultProps = { 29 | type: ContextMenuType.Canvas, 30 | }; 31 | 32 | state = { 33 | visible: false, 34 | content: null, 35 | }; 36 | 37 | componentDidMount() { 38 | const { graph, type } = this.props; 39 | 40 | switch (type) { 41 | case ContextMenuType.Canvas: 42 | graph.on(GraphCanvasEvent.onCanvasContextMenu, (e: GraphEvent) => { 43 | e.preventDefault(); 44 | 45 | const { x, y } = e; 46 | 47 | this.showContextMenu(x, y); 48 | }); 49 | break; 50 | 51 | case ContextMenuType.Node: 52 | graph.on(GraphNodeEvent.onNodeContextMenu, (e: GraphEvent) => { 53 | e.preventDefault(); 54 | 55 | const { x, y, item } = e; 56 | 57 | this.showContextMenu(x, y, item as Node); 58 | }); 59 | break; 60 | 61 | case ContextMenuType.Edge: 62 | graph.on(GraphEdgeEvent.onEdgeContextMenu, (e: GraphEvent) => { 63 | e.preventDefault(); 64 | 65 | const { x, y, item } = e; 66 | 67 | this.showContextMenu(x, y, item as Edge); 68 | }); 69 | break; 70 | 71 | default: 72 | break; 73 | } 74 | 75 | graph.on(GraphCommonEvent.onClick, () => { 76 | this.hideContextMenu(); 77 | }); 78 | } 79 | 80 | showContextMenu = (x: number, y: number, item?: Item) => { 81 | const { graph, renderContent } = this.props; 82 | 83 | clearSelectedState(graph); 84 | 85 | if (item) { 86 | graph.setItemState(item, ItemState.Selected, true); 87 | } 88 | 89 | global.plugin.contextMenu.state = 'show'; 90 | global.clipboard.point = { 91 | x, 92 | y, 93 | }; 94 | 95 | const position = graph.getCanvasByPoint(x, y); 96 | 97 | this.setState({ 98 | visible: true, 99 | content: renderContent(item, position, this.hideContextMenu), 100 | }); 101 | }; 102 | 103 | hideContextMenu = () => { 104 | global.plugin.contextMenu.state = 'hide'; 105 | 106 | this.setState({ 107 | visible: false, 108 | content: null, 109 | }); 110 | }; 111 | 112 | render() { 113 | const { graph } = this.props; 114 | const { visible, content } = this.state; 115 | 116 | if (!visible) { 117 | return null; 118 | } 119 | 120 | return ReactDOM.createPortal(content, graph.get('container')); 121 | } 122 | } 123 | 124 | export default withEditorContext(ContextMenu); 125 | -------------------------------------------------------------------------------- /src/plugins/EditableLabel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import G6 from '@antv/g6'; 4 | import { isMind, getSelectedNodes } from '@/utils'; 5 | import global from '@/common/global'; 6 | import { GraphMode, EditorEvent, GraphNodeEvent, LabelState } from '@/common/constants'; 7 | import { LabelStateEvent } from '@/common/interfaces'; 8 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext'; 9 | 10 | interface EditableLabelProps extends EditorContextProps { 11 | /** 标签图形类名 */ 12 | labelClassName?: string; 13 | /** 标签最大宽度 */ 14 | labelMaxWidth?: number; 15 | } 16 | 17 | interface EditableLabelState { 18 | visible: boolean; 19 | } 20 | 21 | class EditableLabel extends React.Component { 22 | el: HTMLDivElement = null; 23 | 24 | static defaultProps = { 25 | labelClassName: 'node-label', 26 | labelMaxWidth: 100, 27 | }; 28 | 29 | state = { 30 | visible: false, 31 | }; 32 | 33 | componentDidMount() { 34 | const { graph } = this.props; 35 | 36 | graph.on(EditorEvent.onLabelStateChange, ({ labelState }: LabelStateEvent) => { 37 | if (labelState === LabelState.Show) { 38 | this.showEditableLabel(); 39 | } else { 40 | this.hideEditableLabel(); 41 | } 42 | }); 43 | 44 | graph.on(GraphNodeEvent.onNodeDoubleClick, () => { 45 | this.showEditableLabel(); 46 | }); 47 | } 48 | 49 | update = () => { 50 | const { graph, executeCommand } = this.props; 51 | 52 | const node = getSelectedNodes(graph)[0]; 53 | const model = node.getModel(); 54 | 55 | const { textContent: label } = this.el; 56 | 57 | if (label === model.label) { 58 | return; 59 | } 60 | 61 | executeCommand('update', { 62 | id: model.id, 63 | updateModel: { 64 | label, 65 | }, 66 | forceRefreshLayout: isMind(graph), 67 | }); 68 | }; 69 | 70 | showEditableLabel = () => { 71 | global.plugin.editableLabel.state = 'show'; 72 | 73 | this.setState( 74 | { 75 | visible: true, 76 | }, 77 | () => { 78 | const { el } = this; 79 | 80 | if (el) { 81 | el.focus(); 82 | document.execCommand('selectAll', false, null); 83 | } 84 | }, 85 | ); 86 | }; 87 | 88 | hideEditableLabel = () => { 89 | global.plugin.editableLabel.state = 'hide'; 90 | 91 | this.setState({ 92 | visible: false, 93 | }); 94 | }; 95 | 96 | handleBlur = () => { 97 | this.update(); 98 | this.hideEditableLabel(); 99 | }; 100 | 101 | handleKeyDown = (e: React.KeyboardEvent) => { 102 | e.stopPropagation(); 103 | 104 | const { key } = e; 105 | 106 | if (['Tab'].includes(key)) { 107 | e.preventDefault(); 108 | } 109 | 110 | if (['Enter', 'Escape', 'Tab'].includes(key)) { 111 | this.update(); 112 | this.hideEditableLabel(); 113 | } 114 | }; 115 | 116 | render() { 117 | const { graph, labelClassName, labelMaxWidth } = this.props; 118 | 119 | const mode = graph.getCurrentMode(); 120 | const zoom = graph.getZoom(); 121 | 122 | if (mode === GraphMode.Readonly) { 123 | return null; 124 | } 125 | 126 | const node = getSelectedNodes(graph)[0]; 127 | 128 | if (!node) { 129 | return null; 130 | } 131 | 132 | const model = node.getModel(); 133 | const group = node.getContainer(); 134 | 135 | const label = model.label; 136 | const labelShape = group.findByClassName(labelClassName); 137 | 138 | if (!labelShape) { 139 | return null; 140 | } 141 | 142 | const { visible } = this.state; 143 | 144 | if (!visible) { 145 | return null; 146 | } 147 | 148 | // Get the label offset 149 | const { x: relativeX, y: relativeY } = labelShape.getBBox(); 150 | const { x: absoluteX, y: absoluteY } = G6.Util.applyMatrix( 151 | { 152 | x: relativeX, 153 | y: relativeY, 154 | }, 155 | node.getContainer().getMatrix(), 156 | ); 157 | 158 | const { x: left, y: top } = graph.getCanvasByPoint(absoluteX, absoluteY); 159 | 160 | // Get the label size 161 | const { width, height } = labelShape.getBBox(); 162 | 163 | // Get the label font 164 | const font = labelShape.attr('font'); 165 | 166 | const style: React.CSSProperties = { 167 | position: 'absolute', 168 | top, 169 | left, 170 | width: 'auto', 171 | height: 'auto', 172 | minWidth: width, 173 | minHeight: height, 174 | maxWidth: labelMaxWidth, 175 | font, 176 | background: 'white', 177 | border: '1px solid #1890ff', 178 | outline: 'none', 179 | transform: `scale(${zoom})`, 180 | transformOrigin: 'left top', 181 | }; 182 | 183 | return ReactDOM.createPortal( 184 |
{ 186 | this.el = el; 187 | }} 188 | style={style} 189 | contentEditable 190 | onBlur={this.handleBlur} 191 | onKeyDown={this.handleKeyDown} 192 | suppressContentEditableWarning 193 | > 194 | {label} 195 |
, 196 | graph.get('container'), 197 | ); 198 | } 199 | } 200 | 201 | export default withEditorContext(EditableLabel); 202 | -------------------------------------------------------------------------------- /src/plugins/ItemPopover/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import delay from 'lodash/delay'; 4 | import global from '@/common/global'; 5 | import { GraphNodeEvent } from '@/common/constants'; 6 | import { Item } from '@/common/interfaces'; 7 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext'; 8 | 9 | export enum ItemPopoverType { 10 | Node = 'node', 11 | Edge = 'edge', 12 | } 13 | 14 | interface ItemPopoverProps extends EditorContextProps { 15 | /** 浮层类型 */ 16 | type?: ItemPopoverType; 17 | /** 浮层内容 */ 18 | renderContent: ( 19 | item: Item, 20 | position: { minX: number; minY: number; maxX: number; maxY: number; centerX: number; centerY: number }, 21 | ) => React.ReactNode; 22 | } 23 | 24 | interface ItemPopoverState { 25 | visible: boolean; 26 | content: React.ReactNode; 27 | } 28 | 29 | class ItemPopover extends React.Component { 30 | static defaultProps = { 31 | type: ItemPopoverType.Node, 32 | }; 33 | 34 | state = { 35 | visible: false, 36 | content: null, 37 | }; 38 | 39 | mouseEnterTimeoutID = 0; 40 | mouseLeaveTimeoutID = 0; 41 | 42 | componentDidMount() { 43 | const { graph, type } = this.props; 44 | 45 | if (type === ItemPopoverType.Node) { 46 | graph.on(GraphNodeEvent.onNodeMouseEnter, ({ item }) => { 47 | clearTimeout(this.mouseLeaveTimeoutID); 48 | 49 | this.mouseEnterTimeoutID = delay(this.showItemPopover, 250, item); 50 | }); 51 | 52 | graph.on(GraphNodeEvent.onNodeMouseLeave, () => { 53 | clearTimeout(this.mouseEnterTimeoutID); 54 | 55 | this.mouseLeaveTimeoutID = delay(this.hideItemPopover, 250); 56 | }); 57 | } 58 | } 59 | 60 | showItemPopover = (item: Item) => { 61 | const { graph, renderContent } = this.props; 62 | 63 | global.plugin.itemPopover.state = 'show'; 64 | 65 | const { minX, minY, maxX, maxY, centerX, centerY } = item.getBBox(); 66 | 67 | const { x: itemMinX, y: itemMinY } = graph.getCanvasByPoint(minX, minY); 68 | const { x: itemMaxX, y: itemMaxY } = graph.getCanvasByPoint(maxX, maxY); 69 | const { x: itemCenterX, y: itemCenterY } = graph.getCanvasByPoint(centerX, centerY); 70 | 71 | const position = { 72 | minX: itemMinX, 73 | minY: itemMinY, 74 | maxX: itemMaxX, 75 | maxY: itemMaxY, 76 | centerX: itemCenterX, 77 | centerY: itemCenterY, 78 | }; 79 | 80 | this.setState({ 81 | visible: true, 82 | content: renderContent(item, position), 83 | }); 84 | }; 85 | 86 | hideItemPopover = () => { 87 | global.plugin.itemPopover.state = 'hide'; 88 | 89 | this.setState({ 90 | visible: false, 91 | content: null, 92 | }); 93 | }; 94 | 95 | render() { 96 | const { graph } = this.props; 97 | const { visible, content } = this.state; 98 | 99 | if (!visible) { 100 | return null; 101 | } 102 | 103 | return ReactDOM.createPortal(content, graph.get('container')); 104 | } 105 | } 106 | 107 | export default withEditorContext(ItemPopover); 108 | -------------------------------------------------------------------------------- /src/shape/common/anchor.ts: -------------------------------------------------------------------------------- 1 | import { ItemState, AnchorPointState } from '@/common/constants'; 2 | import { ShapeStyle, NodeModel, Item, Node } from '@/common/interfaces'; 3 | 4 | interface AnchorPointContextProps { 5 | getAnchorPoints?(model: NodeModel): number[][]; 6 | } 7 | 8 | type GetAnchorPointStyle = (item: Node, anchorPoint: number[]) => ShapeStyle; 9 | type GetAnchorPointDisabledStyle = (item: Node, anchorPoint: number[]) => ShapeStyle & { img?: string }; 10 | 11 | const ANCHOR_POINT_NAME = 'anchorPoint'; 12 | 13 | const getAnchorPointDefaultStyle: GetAnchorPointStyle = (item, anchorPoint) => { 14 | const { width, height } = item.getKeyShape().getBBox(); 15 | 16 | const [x, y] = anchorPoint; 17 | 18 | return { 19 | x: width * x, 20 | y: height * y - 3, 21 | r: 3, 22 | lineWidth: 2, 23 | fill: '#FFFFFF', 24 | stroke: '#5AAAFF', 25 | }; 26 | }; 27 | 28 | const getAnchorPointDefaultDisabledStyle: GetAnchorPointDisabledStyle = (item, anchorPoint) => { 29 | const { width, height } = item.getKeyShape().getBBox(); 30 | 31 | const [x, y] = anchorPoint; 32 | 33 | return { 34 | img: 35 | 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSI4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xLjUxNSAxLjE3Mmw1LjY1NyA1LjY1Nm0wLTUuNjU2TDEuNTE1IDYuODI4IiBzdHJva2U9IiNGRjYwNjAiIHN0cm9rZS13aWR0aD0iMS42IiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIi8+PC9zdmc+', 36 | x: width * x - 4, 37 | y: height * y - 8, 38 | width: 8, 39 | height: 8, 40 | }; 41 | }; 42 | 43 | function drawAnchorPoints( 44 | this: AnchorPointContextProps, 45 | item: Node, 46 | getAnchorPointStyle: GetAnchorPointStyle, 47 | getAnchorPointDisabledStyle: GetAnchorPointDisabledStyle, 48 | ) { 49 | const group = item.getContainer(); 50 | const model = item.getModel() as NodeModel; 51 | const anchorPoints = this.getAnchorPoints ? this.getAnchorPoints(model) : []; 52 | const anchorPointsState = item.get('anchorPointsState') || []; 53 | 54 | anchorPoints.forEach((anchorPoint, index) => { 55 | if (anchorPointsState[index] === AnchorPointState.Enabled) { 56 | group.addShape('circle', { 57 | name: ANCHOR_POINT_NAME, 58 | attrs: { 59 | ...getAnchorPointDefaultStyle(item, anchorPoint), 60 | ...getAnchorPointStyle(item, anchorPoint), 61 | }, 62 | isAnchorPoint: true, 63 | anchorPointIndex: index, 64 | anchorPointState: AnchorPointState.Enabled, 65 | }); 66 | } else { 67 | group.addShape('image', { 68 | name: ANCHOR_POINT_NAME, 69 | attrs: { 70 | ...getAnchorPointDefaultDisabledStyle(item, anchorPoint), 71 | ...getAnchorPointDisabledStyle(item, anchorPoint), 72 | }, 73 | isAnchorPoint: true, 74 | anchorPointIndex: index, 75 | anchorPointState: AnchorPointState.Disabled, 76 | }); 77 | } 78 | }); 79 | } 80 | 81 | function removeAnchorPoints(this: AnchorPointContextProps, item: Node) { 82 | const group = item.getContainer(); 83 | const anchorPoints = group.findAllByName(ANCHOR_POINT_NAME); 84 | 85 | anchorPoints.forEach(anchorPoint => { 86 | group.removeChild(anchorPoint); 87 | }); 88 | } 89 | 90 | function setAnchorPointsState( 91 | this: AnchorPointContextProps, 92 | name: string, 93 | value: string | boolean, 94 | item: Item, 95 | getAnchorPointStyle: GetAnchorPointStyle = () => ({}), 96 | getAnchorPointDisabledStyle: GetAnchorPointDisabledStyle = () => ({}), 97 | ) { 98 | if (name !== ItemState.ActiveAnchorPoints) { 99 | return; 100 | } 101 | 102 | if (value) { 103 | drawAnchorPoints.call(this, item as Node, getAnchorPointStyle, getAnchorPointDisabledStyle); 104 | } else { 105 | removeAnchorPoints.call(this, item as Node); 106 | } 107 | } 108 | 109 | export { setAnchorPointsState }; 110 | -------------------------------------------------------------------------------- /src/shape/edges/bizFlowEdge.ts: -------------------------------------------------------------------------------- 1 | import G6 from '@antv/g6'; 2 | import { ItemState } from '@/common/constants'; 3 | import { GShape, GGroup, CustomEdge } from '@/common/interfaces'; 4 | 5 | const EDGE_LABEL_CLASS_NAME = 'edge-label'; 6 | const EDGE_LABEL_WRAPPER_CLASS_NAME = 'edge-label-wrapper-label'; 7 | 8 | const bizFlowEdge: CustomEdge = { 9 | options: { 10 | style: { 11 | stroke: '#ccc1d8', 12 | lineWidth: 2, 13 | shadowColor: null, 14 | shadowBlur: 0, 15 | radius: 8, 16 | offset: 24, 17 | // startArrow: { 18 | // path: 'M 3,0 A 3,3,0,1,1,-3,0 A 3,3,0,1,1,3,0 Z', 19 | // d: 7, 20 | // }, 21 | // endArrow: { 22 | // path: 'M 3,0 L -3,-3 L -3,3 Z', 23 | // d: 5, 24 | // }, 25 | endArrow: { 26 | path: 'M 0,0 L 4,3 L 4,-3 Z', 27 | }, 28 | }, 29 | labelCfg: { 30 | style: { 31 | fill: '#000000', 32 | fontSize: 10, 33 | }, 34 | }, 35 | stateStyles: { 36 | [ItemState.Selected]: { 37 | stroke: '#5aaaff', 38 | shadowColor: '#5aaaff', 39 | shadowBlur: 24, 40 | }, 41 | [ItemState.HighLight]: { 42 | stroke: '#5aaaff', 43 | shadowColor: '#5aaaff', 44 | shadowBlur: 24, 45 | }, 46 | }, 47 | }, 48 | 49 | createLabelWrapper(group: GGroup) { 50 | const label = group.findByClassName(EDGE_LABEL_CLASS_NAME); 51 | const labelWrapper = group.findByClassName(EDGE_LABEL_WRAPPER_CLASS_NAME); 52 | 53 | if (!label) { 54 | return; 55 | } 56 | 57 | if (labelWrapper) { 58 | return; 59 | } 60 | 61 | group.addShape('rect', { 62 | className: EDGE_LABEL_WRAPPER_CLASS_NAME, 63 | attrs: { 64 | fill: '#e1e5e8', 65 | radius: 2, 66 | }, 67 | }); 68 | 69 | label.set('zIndex', 1); 70 | 71 | group.sort(); 72 | }, 73 | 74 | updateLabelWrapper(group: GGroup) { 75 | const label = group.findByClassName(EDGE_LABEL_CLASS_NAME); 76 | const labelWrapper = group.findByClassName(EDGE_LABEL_WRAPPER_CLASS_NAME); 77 | 78 | if (!label) { 79 | labelWrapper && labelWrapper.hide(); 80 | return; 81 | } else { 82 | labelWrapper && labelWrapper.show(); 83 | } 84 | 85 | if (!labelWrapper) { 86 | return; 87 | } 88 | 89 | const { minX, minY, width, height } = label.getBBox(); 90 | 91 | labelWrapper.attr({ 92 | x: minX - 5, 93 | y: minY - 3, 94 | width: width + 10, 95 | height: height + 6, 96 | }); 97 | }, 98 | 99 | afterDraw(model, group) { 100 | this.createLabelWrapper(group); 101 | this.updateLabelWrapper(group); 102 | }, 103 | 104 | afterUpdate(model, item) { 105 | const group = item.getContainer(); 106 | 107 | this.createLabelWrapper(group); 108 | this.updateLabelWrapper(group); 109 | }, 110 | 111 | setState(name, value, item) { 112 | const shape: GShape = item.get('keyShape'); 113 | 114 | if (!shape) { 115 | return; 116 | } 117 | 118 | const { style, stateStyles } = this.options; 119 | 120 | const stateStyle = stateStyles[name]; 121 | 122 | if (!stateStyle) { 123 | return; 124 | } 125 | 126 | if (value) { 127 | shape.attr({ 128 | ...style, 129 | ...stateStyle, 130 | }); 131 | } else { 132 | shape.attr(style); 133 | } 134 | }, 135 | }; 136 | 137 | G6.registerEdge('bizFlowEdge', bizFlowEdge, 'polyline'); 138 | -------------------------------------------------------------------------------- /src/shape/edges/bizMindEdge.ts: -------------------------------------------------------------------------------- 1 | import G6 from '@antv/g6'; 2 | import { ItemState } from '@/common/constants'; 3 | import { GShape, CustomEdge } from '@/common/interfaces'; 4 | 5 | const bizMindEdge: CustomEdge = { 6 | options: { 7 | style: { 8 | stroke: '#ccc1d8', 9 | lineWidth: 2, 10 | shadowColor: null, 11 | shadowBlur: 0, 12 | }, 13 | stateStyles: { 14 | [ItemState.Selected]: { 15 | stroke: '#5aaaff', 16 | shadowColor: '#5aaaff', 17 | shadowBlur: 24, 18 | }, 19 | [ItemState.HighLight]: { 20 | stroke: '#5aaaff', 21 | shadowColor: '#5aaaff', 22 | shadowBlur: 24, 23 | }, 24 | }, 25 | }, 26 | 27 | setState(name, value, item) { 28 | const shape: GShape = item.get('keyShape'); 29 | 30 | if (!shape) { 31 | return; 32 | } 33 | 34 | const { style, stateStyles } = this.options; 35 | 36 | const stateStyle = stateStyles[name]; 37 | 38 | if (!stateStyle) { 39 | return; 40 | } 41 | 42 | if (value) { 43 | shape.attr({ 44 | ...style, 45 | ...stateStyle, 46 | }); 47 | } else { 48 | shape.attr(style); 49 | } 50 | }, 51 | }; 52 | 53 | G6.registerEdge('bizMindEdge', bizMindEdge, 'cubic-horizontal'); 54 | -------------------------------------------------------------------------------- /src/shape/index.ts: -------------------------------------------------------------------------------- 1 | import './nodes/bizNode'; 2 | import './nodes/bizFlowNode'; 3 | import './nodes/bizMindNode'; 4 | import './edges/bizFlowEdge'; 5 | import './edges/bizMindEdge'; 6 | -------------------------------------------------------------------------------- /src/shape/nodes/bizFlowNode.ts: -------------------------------------------------------------------------------- 1 | import G6 from '@antv/g6'; 2 | import { CustomNode, Item } from '@/common/interfaces'; 3 | import { setAnchorPointsState } from '../common/anchor'; 4 | 5 | const bizFlowNode: CustomNode = { 6 | afterSetState(name: string, value: string | boolean, item: Item) { 7 | setAnchorPointsState.call(this, name, value, item); 8 | }, 9 | 10 | getAnchorPoints() { 11 | return [ 12 | [0.5, 0], 13 | [0.5, 1], 14 | [0, 0.5], 15 | [1, 0.5], 16 | ]; 17 | }, 18 | }; 19 | 20 | G6.registerNode('bizFlowNode', bizFlowNode, 'bizNode'); 21 | -------------------------------------------------------------------------------- /src/shape/nodes/bizMindNode.ts: -------------------------------------------------------------------------------- 1 | import G6 from '@antv/g6'; 2 | import { GGroup, Node, NodeModel, CustomNode } from '@/common/interfaces'; 3 | import { getNodeSide, getFoldButtonPath, getUnfoldButtonPath } from '../utils'; 4 | 5 | export const FOLD_BUTTON_CLASS_NAME = 'node-fold-button'; 6 | export const UNFOLD_BUTTON_CLASS_NAME = 'node-unfold-button'; 7 | 8 | const bizMindNode: CustomNode = { 9 | afterDraw(model, group) { 10 | this.drawButton(model, group); 11 | }, 12 | 13 | afterUpdate(model, item) { 14 | const group = item.getContainer(); 15 | 16 | this.drawButton(model, group); 17 | this.adjustButton(model, item); 18 | }, 19 | 20 | drawButton(model: NodeModel, group: GGroup) { 21 | const { children, collapsed } = model; 22 | 23 | [FOLD_BUTTON_CLASS_NAME, UNFOLD_BUTTON_CLASS_NAME].forEach(className => { 24 | const shape = group.findByClassName(className); 25 | 26 | if (shape) { 27 | shape.destroy(); 28 | } 29 | }); 30 | 31 | if (!children || !children.length) { 32 | return; 33 | } 34 | 35 | if (!collapsed) { 36 | group.addShape('path', { 37 | className: FOLD_BUTTON_CLASS_NAME, 38 | attrs: { 39 | path: getFoldButtonPath(), 40 | fill: '#ffffff', 41 | stroke: '#ccc1d8', 42 | }, 43 | }); 44 | } else { 45 | group.addShape('path', { 46 | className: UNFOLD_BUTTON_CLASS_NAME, 47 | attrs: { 48 | path: getUnfoldButtonPath(), 49 | fill: '#ffffff', 50 | stroke: '#ccc1d8', 51 | }, 52 | }); 53 | } 54 | }, 55 | 56 | adjustButton(model: NodeModel, item: Node) { 57 | const { children, collapsed } = model; 58 | 59 | if (!children || !children.length) { 60 | return; 61 | } 62 | 63 | const group = item.getContainer(); 64 | const shape = group.findByClassName(!collapsed ? FOLD_BUTTON_CLASS_NAME : UNFOLD_BUTTON_CLASS_NAME); 65 | 66 | const [width, height] = this.getSize(model); 67 | 68 | const x = getNodeSide(item) === 'left' ? -24 : width + 10; 69 | const y = height / 2 - 9; 70 | 71 | shape.translate(x, y); 72 | }, 73 | 74 | getAnchorPoints() { 75 | return [ 76 | [0, 0.5], 77 | [1, 0.5], 78 | ]; 79 | }, 80 | }; 81 | 82 | G6.registerNode('bizMindNode', bizMindNode, 'bizNode'); 83 | -------------------------------------------------------------------------------- /src/shape/nodes/bizNode.ts: -------------------------------------------------------------------------------- 1 | import G6 from '@antv/g6'; 2 | import merge from 'lodash/merge'; 3 | import isArray from 'lodash/isArray'; 4 | import { ItemState } from '@/common/constants'; 5 | import { GGroup, NodeModel, CustomNode } from '@/common/interfaces'; 6 | import { optimizeMultilineText } from '../utils'; 7 | 8 | const WRAPPER_BORDER_WIDTH = 2; 9 | const WRAPPER_HORIZONTAL_PADDING = 10; 10 | 11 | const WRAPPER_CLASS_NAME = 'node-wrapper'; 12 | const CONTENT_CLASS_NAME = 'node-content'; 13 | const LABEL_CLASS_NAME = 'node-label'; 14 | 15 | const bizNode: CustomNode = { 16 | options: { 17 | size: [120, 60], 18 | wrapperStyle: { 19 | fill: '#5487ea', 20 | radius: 8, 21 | }, 22 | contentStyle: { 23 | fill: '#ffffff', 24 | radius: 6, 25 | }, 26 | labelStyle: { 27 | fill: '#000000', 28 | textAlign: 'center', 29 | textBaseline: 'middle', 30 | }, 31 | stateStyles: { 32 | [ItemState.Active]: { 33 | wrapperStyle: {}, 34 | contentStyle: {}, 35 | labelStyle: {}, 36 | } as any, 37 | [ItemState.Selected]: { 38 | wrapperStyle: {}, 39 | contentStyle: {}, 40 | labelStyle: {}, 41 | } as any, 42 | }, 43 | }, 44 | 45 | getOptions(model: NodeModel) { 46 | return merge({}, this.options, this.getCustomConfig(model) || {}, model); 47 | }, 48 | 49 | draw(model, group) { 50 | const keyShape = this.drawWrapper(model, group); 51 | 52 | this.drawContent(model, group); 53 | this.drawLabel(model, group); 54 | 55 | return keyShape; 56 | }, 57 | 58 | drawWrapper(model: NodeModel, group: GGroup) { 59 | const [width, height] = this.getSize(model); 60 | const { wrapperStyle } = this.getOptions(model); 61 | 62 | const shape = group.addShape('rect', { 63 | className: WRAPPER_CLASS_NAME, 64 | draggable: true, 65 | attrs: { 66 | x: 0, 67 | y: -WRAPPER_BORDER_WIDTH * 2, 68 | width, 69 | height: height + WRAPPER_BORDER_WIDTH * 2, 70 | ...wrapperStyle, 71 | }, 72 | }); 73 | 74 | return shape; 75 | }, 76 | 77 | drawContent(model: NodeModel, group: GGroup) { 78 | const [width, height] = this.getSize(model); 79 | const { contentStyle } = this.getOptions(model); 80 | 81 | const shape = group.addShape('rect', { 82 | className: CONTENT_CLASS_NAME, 83 | draggable: true, 84 | attrs: { 85 | x: 0, 86 | y: 0, 87 | width, 88 | height, 89 | ...contentStyle, 90 | }, 91 | }); 92 | 93 | return shape; 94 | }, 95 | 96 | drawLabel(model: NodeModel, group: GGroup) { 97 | const [width, height] = this.getSize(model); 98 | const { labelStyle } = this.getOptions(model); 99 | 100 | const shape = group.addShape('text', { 101 | className: LABEL_CLASS_NAME, 102 | draggable: true, 103 | attrs: { 104 | x: width / 2, 105 | y: height / 2, 106 | text: model.label, 107 | ...labelStyle, 108 | }, 109 | }); 110 | 111 | return shape; 112 | }, 113 | 114 | setLabelText(model: NodeModel, group: GGroup) { 115 | const shape = group.findByClassName(LABEL_CLASS_NAME); 116 | 117 | if (!shape) { 118 | return; 119 | } 120 | 121 | const [width] = this.getSize(model); 122 | const { fontStyle, fontWeight, fontSize, fontFamily } = shape.attr(); 123 | 124 | const text = model.label as string; 125 | const font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`; 126 | 127 | shape.attr('text', optimizeMultilineText(text, font, 2, width - WRAPPER_HORIZONTAL_PADDING * 2)); 128 | }, 129 | 130 | update(model, item) { 131 | const group = item.getContainer(); 132 | 133 | this.setLabelText(model, group); 134 | }, 135 | 136 | setState(name, value, item) { 137 | const group = item.getContainer(); 138 | const model = item.getModel(); 139 | const states = item.getStates() as ItemState[]; 140 | 141 | [WRAPPER_CLASS_NAME, CONTENT_CLASS_NAME, LABEL_CLASS_NAME].forEach(className => { 142 | const shape = group.findByClassName(className); 143 | const options = this.getOptions(model); 144 | 145 | const shapeName = className.split('-')[1]; 146 | 147 | shape.attr({ 148 | ...options[`${shapeName}Style`], 149 | }); 150 | 151 | states.forEach(state => { 152 | if (options.stateStyles[state] && options.stateStyles[state][`${shapeName}Style`]) { 153 | shape.attr({ 154 | ...options.stateStyles[state][`${shapeName}Style`], 155 | }); 156 | } 157 | }); 158 | }); 159 | 160 | if (name === ItemState.Selected) { 161 | const wrapperShape = group.findByClassName(WRAPPER_CLASS_NAME); 162 | 163 | const [width, height] = this.getSize(model); 164 | 165 | if (value) { 166 | wrapperShape.attr({ 167 | x: -WRAPPER_BORDER_WIDTH, 168 | y: -WRAPPER_BORDER_WIDTH * 2, 169 | width: width + WRAPPER_BORDER_WIDTH * 2, 170 | height: height + WRAPPER_BORDER_WIDTH * 3, 171 | }); 172 | } else { 173 | wrapperShape.attr({ 174 | x: 0, 175 | y: -WRAPPER_BORDER_WIDTH * 2, 176 | width, 177 | height: height + WRAPPER_BORDER_WIDTH * 2, 178 | }); 179 | } 180 | } 181 | 182 | if (this.afterSetState) { 183 | this.afterSetState(name, value, item); 184 | } 185 | }, 186 | 187 | getSize(model: NodeModel) { 188 | const { size } = this.getOptions(model); 189 | 190 | if (!isArray(size)) { 191 | return [size, size]; 192 | } 193 | 194 | return size; 195 | }, 196 | 197 | getCustomConfig() { 198 | return {}; 199 | }, 200 | 201 | getAnchorPoints() { 202 | return []; 203 | }, 204 | }; 205 | 206 | G6.registerNode('bizNode', bizNode); 207 | -------------------------------------------------------------------------------- /src/shape/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '@/common/interfaces'; 2 | 3 | const canvas = document.createElement('canvas'); 4 | const canvasContext = canvas.getContext('2d'); 5 | 6 | export function getNodeSide(item: Node): 'left' | 'right' { 7 | const model = item.getModel(); 8 | 9 | if (model.side) { 10 | return model.side as 'left' | 'right'; 11 | } 12 | 13 | const parent = item.get('parent'); 14 | 15 | if (parent) { 16 | return getNodeSide(parent); 17 | } 18 | 19 | return 'right'; 20 | } 21 | 22 | export function getRectPath(x: number, y: number, w: number, h: number, r: number) { 23 | if (r) { 24 | return [ 25 | ['M', +x + +r, y], 26 | ['l', w - r * 2, 0], 27 | ['a', r, r, 0, 0, 1, r, r], 28 | ['l', 0, h - r * 2], 29 | ['a', r, r, 0, 0, 1, -r, r], 30 | ['l', r * 2 - w, 0], 31 | ['a', r, r, 0, 0, 1, -r, -r], 32 | ['l', 0, r * 2 - h], 33 | ['a', r, r, 0, 0, 1, r, -r], 34 | ['z'], 35 | ]; 36 | } 37 | 38 | const res = [['M', x, y], ['l', w, 0], ['l', 0, h], ['l', -w, 0], ['z']]; 39 | 40 | res.toString = toString; 41 | 42 | return res; 43 | } 44 | 45 | export function getFoldButtonPath() { 46 | const w = 14; 47 | const h = 14; 48 | const rect = getRectPath(0, 0, w, h, 2); 49 | const hp = `M${(w * 3) / 14},${h / 2}L${(w * 11) / 14},${h / 2}`; 50 | const vp = ''; 51 | 52 | return rect + hp + vp; 53 | } 54 | 55 | export function getUnfoldButtonPath() { 56 | const w = 14; 57 | const h = 14; 58 | const rect = getRectPath(0, 0, w, h, 2); 59 | const hp = `M${(w * 3) / 14},${h / 2}L${(w * 11) / 14},${h / 2}`; 60 | const vp = `M${w / 2},${(h * 3) / 14}L${w / 2},${(h * 11) / 14}`; 61 | 62 | return rect + hp + vp; 63 | } 64 | 65 | export function optimizeMultilineText(text: string, font: string, maxRows: number, maxWidth: number) { 66 | canvasContext.font = font; 67 | 68 | if (canvasContext.measureText(text).width <= maxWidth) { 69 | return text; 70 | } 71 | 72 | let multilineText = []; 73 | 74 | let tempText = ''; 75 | let tempTextWidth = 0; 76 | 77 | for (const char of text) { 78 | const { width } = canvasContext.measureText(char); 79 | 80 | if (tempTextWidth + width >= maxWidth) { 81 | multilineText.push(tempText); 82 | 83 | tempText = ''; 84 | tempTextWidth = 0; 85 | } 86 | 87 | tempText += char; 88 | tempTextWidth += width; 89 | } 90 | 91 | if (tempText) { 92 | multilineText.push(tempText); 93 | } 94 | 95 | if (multilineText.length > maxRows) { 96 | const ellipsis = '...'; 97 | const ellipsisWidth = canvasContext.measureText(ellipsis).width; 98 | 99 | let tempText = ''; 100 | let tempTextWidth = 0; 101 | 102 | for (const char of multilineText[maxRows - 1]) { 103 | const { width } = canvasContext.measureText(char); 104 | 105 | if (tempTextWidth + width > maxWidth - ellipsisWidth) { 106 | break; 107 | } 108 | 109 | tempText += char; 110 | tempTextWidth += width; 111 | } 112 | 113 | multilineText = multilineText.slice(0, maxRows - 1).concat(`${tempText}${ellipsis}`); 114 | } 115 | 116 | return multilineText.join('\n'); 117 | } 118 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import G6 from '@antv/g6'; 2 | import { ItemType, ItemState, GraphState, EditorEvent } from '@/common/constants'; 3 | import { Graph, TreeGraph, EdgeModel, Item, Node, Edge } from '@/common/interfaces'; 4 | 5 | /** 生成唯一标识 */ 6 | export function guid() { 7 | return 'xxxxxxxx'.replace(/[xy]/g, function(c) { 8 | const r = (Math.random() * 16) | 0; 9 | const v = c === 'x' ? r : (r & 0x3) | 0x8; 10 | return v.toString(16); 11 | }); 12 | } 13 | 14 | /** 拼接查询字符 */ 15 | export const toQueryString = (obj: object) => 16 | Object.keys(obj) 17 | .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`) 18 | .join('&'); 19 | 20 | /** 执行批量处理 */ 21 | export function executeBatch(graph: Graph, execute: Function) { 22 | const autoPaint = graph.get('autoPaint'); 23 | 24 | graph.setAutoPaint(false); 25 | 26 | execute(); 27 | 28 | graph.paint(); 29 | graph.setAutoPaint(autoPaint); 30 | } 31 | 32 | /** 执行递归遍历 */ 33 | export function recursiveTraversal(root, callback) { 34 | if (!root) { 35 | return; 36 | } 37 | 38 | callback(root); 39 | 40 | if (!root.children) { 41 | return; 42 | } 43 | 44 | root.children.forEach(item => recursiveTraversal(item, callback)); 45 | } 46 | 47 | /** 判断是否流程图 */ 48 | export function isFlow(graph: Graph) { 49 | return graph.constructor === G6.Graph; 50 | } 51 | 52 | /** 判断是否脑图 */ 53 | export function isMind(graph: Graph) { 54 | return graph.constructor === G6.TreeGraph; 55 | } 56 | 57 | /** 判断是否节点 */ 58 | export function isNode(item: Item) { 59 | return item.getType() === ItemType.Node; 60 | } 61 | 62 | /** 判断是否边线 */ 63 | export function isEdge(item: Item) { 64 | return item.getType() === ItemType.Edge; 65 | } 66 | 67 | /** 获取选中节点 */ 68 | export function getSelectedNodes(graph: Graph): Node[] { 69 | return graph.findAllByState(ItemType.Node, ItemState.Selected); 70 | } 71 | 72 | /** 获取选中边线 */ 73 | export function getSelectedEdges(graph: Graph): Edge[] { 74 | return graph.findAllByState(ItemType.Edge, ItemState.Selected); 75 | } 76 | 77 | /** 获取高亮边线 */ 78 | export function getHighlightEdges(graph: Graph): Edge[] { 79 | return graph.findAllByState(ItemType.Edge, ItemState.HighLight); 80 | } 81 | 82 | /** 获取图表状态 */ 83 | export function getGraphState(graph: Graph): GraphState { 84 | let graphState: GraphState = GraphState.MultiSelected; 85 | 86 | const selectedNodes = getSelectedNodes(graph); 87 | const selectedEdges = getSelectedEdges(graph); 88 | 89 | if (selectedNodes.length === 1 && !selectedEdges.length) { 90 | graphState = GraphState.NodeSelected; 91 | } 92 | 93 | if (selectedEdges.length === 1 && !selectedNodes.length) { 94 | graphState = GraphState.EdgeSelected; 95 | } 96 | 97 | if (!selectedNodes.length && !selectedEdges.length) { 98 | graphState = GraphState.CanvasSelected; 99 | } 100 | 101 | return graphState; 102 | } 103 | 104 | /** 设置选中元素 */ 105 | export function setSelectedItems(graph: Graph, items: Item[] | string[]) { 106 | executeBatch(graph, () => { 107 | const selectedNodes = getSelectedNodes(graph); 108 | const selectedEdges = getSelectedEdges(graph); 109 | 110 | [...selectedNodes, ...selectedEdges].forEach(node => { 111 | graph.setItemState(node, ItemState.Selected, false); 112 | }); 113 | 114 | items.forEach(item => { 115 | graph.setItemState(item, ItemState.Selected, true); 116 | }); 117 | }); 118 | 119 | graph.emit(EditorEvent.onGraphStateChange, { 120 | graphState: getGraphState(graph), 121 | }); 122 | } 123 | 124 | /** 清除选中状态 */ 125 | export function clearSelectedState(graph: Graph, shouldUpdate: (item: Item) => boolean = () => true) { 126 | const selectedNodes = getSelectedNodes(graph); 127 | const selectedEdges = getSelectedEdges(graph); 128 | 129 | executeBatch(graph, () => { 130 | [...selectedNodes, ...selectedEdges].forEach(item => { 131 | if (shouldUpdate(item)) { 132 | graph.setItemState(item, ItemState.Selected, false); 133 | } 134 | }); 135 | }); 136 | } 137 | 138 | /** 获取回溯路径 - Flow */ 139 | export function getFlowRecallEdges(graph: Graph, node: Node, targetIds: string[] = [], edges: Edge[] = []) { 140 | const inEdges: Edge[] = node.getInEdges(); 141 | 142 | if (!inEdges.length) { 143 | return []; 144 | } 145 | 146 | inEdges.map(edge => { 147 | const sourceId = (edge.getModel() as EdgeModel).source; 148 | const sourceNode = graph.findById(sourceId) as Node; 149 | 150 | edges.push(edge); 151 | 152 | const targetId = node.get('id'); 153 | 154 | targetIds.push(targetId); 155 | 156 | if (!targetIds.includes(sourceId)) { 157 | getFlowRecallEdges(graph, sourceNode, targetIds, edges); 158 | } 159 | }); 160 | 161 | return edges; 162 | } 163 | 164 | /** 获取回溯路径 - Mind */ 165 | export function getMindRecallEdges(graph: TreeGraph, node: Node, edges: Edge[] = []) { 166 | const parentNode = node.get('parent'); 167 | 168 | if (!parentNode) { 169 | return edges; 170 | } 171 | 172 | node.getEdges().forEach(edge => { 173 | const source = edge.getModel().source as Edge; 174 | 175 | if (source.get('id') === parentNode.get('id')) { 176 | edges.push(edge); 177 | } 178 | }); 179 | 180 | return getMindRecallEdges(graph, parentNode, edges); 181 | } 182 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "paths": { 6 | "@/*": ["lib/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "target": "esnext", 8 | "jsx": "react", 9 | "noImplicitThis": true, 10 | "strictBindCallApply": true, 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": ["src/*"] 14 | } 15 | }, 16 | "include": ["src"] 17 | } 18 | --------------------------------------------------------------------------------