├── .gitignore
├── LICENSE
├── README.en-US.md
├── README.md
├── example
├── index.html
├── index.jsx
├── index.less
├── menu.jsx
├── mock_data
│ └── data.jsx
├── package.json
├── tsconfig.json
└── webpack.config.js
├── package.json
├── rollup.config.js
├── src
├── adaptor.js
├── canvas
│ ├── canvas.js
│ ├── collapse-menu.jsx
│ ├── edge.js
│ ├── empty.js
│ ├── endpoint.js
│ ├── node.js
│ └── right-menu.js
├── component
│ ├── action-menu.tsx
│ ├── edge-render.tsx
│ └── tooltip
│ │ ├── index.less
│ │ └── index.tsx
├── config.ts
├── index.less
├── index.tsx
└── static
│ └── iconfont.css
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 | package-lock.json
5 | es
6 | pack
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Alibaba Cloud
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.en-US.md:
--------------------------------------------------------------------------------
1 |
2 | A React-based DAG diagram for data visualization modeling, suitable for UML, database modeling, data warehouse construction and other businesses.
3 |
4 |
5 | English | [简体中文](./README.md)
6 |
7 |
8 |
9 |
10 |
11 | ## ✨ Feature
12 |
13 | * support custom field properties
14 | * support custom title, title Icon
15 | * support the shrinking / expanding state of nodes, and show the mapping relationship after shrinking
16 | * support custom edge attributes and custom edge label
17 | * support the node, field's status of hover, focus, linked and full chain highlight
18 | * support the right-click menu of node and edge
19 | * support minimap and highlight state of minimap
20 | * support custom empty field content
21 |
22 | ## 📦 Install
23 |
24 | ```
25 | npm install react-visual-modeling
26 | ```
27 |
28 | ## API:
29 |
30 | ### VisualModeling properties
31 |
32 | | Property | Description | Type | Default |
33 | |:-------------:|:-------------------------------------------------------------------:|:----------------------------------------------------------------:|:-------:|
34 | | data | data | any | - |
35 | | width | component width | number | string | - |
36 | | height | component height | number | string | - |
37 | | className | component className | string | - |
38 | | columns | property settings for each column of fields[columns Prop](#columns) | Array< [columns](#columns)> | - |
39 | | nodeMenu | Node Right-click Menu Configuration | Array< [menu](#menu-type)> | [ ] |
40 | | edgeMenu | Edge Right-click Menu Configuration | Array< [menu](#menu-type)> | [ ] |
41 | | config | As configured above[config Prop](#config) | any | - |
42 | | emptyContent | show content when table field is empty | string | JSX. Element | - |
43 | | emptyWidth | table container width when table field is empty | number | string | - |
44 | | onLoaded | canvas loaded event | (canvas) => void | - |
45 | | onChange | canvas data change event | (data) => void | - |
46 | | onFocusNode | focus node events | (node) => void | - |
47 | | onFocusEdge | focus edge events | (edge) => void | - |
48 | | onFocusCanvas | focus canvas blank events | () => void | - |
49 | | onDblClickNode| double click node events | () => void | - |
50 |
51 |
52 |
53 | ### columns
54 |
55 | property settings for each column of fields
56 |
57 | | Property | Description | Type | Default |
58 | |:----------:|:----------------------------------------------------------------------------------:|:-----------------------------------------:|:-------:|
59 | | title | name of each column | string | - |
60 | | key | the unique mark of each column, corresponding to the key value on the data | string | - |
61 | | width | width of each column | number | - |
62 | | primaryKey | whether the value corresponding to the key in this column is used as a unique sign | boolean | - |
63 | | render | Customize the style of each column | (key) => void | - |
64 |
65 |
66 |
67 | ### menu
68 |
69 | right-click menu configuration for'Node/Edge'
70 |
71 | | Property | Description | Type | Default |
72 | |:--------:|:---------------------------------------:|:-----------------------------------------------:|:-------:|
73 | | title | name of each column | string | - |
74 | | key | unique flag for each column menu | string | - |
75 | | render | Customize the style of each column menu | (key) => void | - |
76 | | onClick | Click Callback for Each Column | (key, data) => void | - |
77 |
78 |
79 |
80 | ### config
81 |
82 | the configuration of canvas
83 |
84 | | Property | Description | Type | Default |
85 | |:------------------:|:------------------------------------------------------------------------------:|:---------------------------------------------------------------:|:-------:|
86 | | showActionIcon | whether show operation icon: zoom in, zoom out, focus | boolean | - |
87 | | allowKeyboard | allow keyboard to delete events. Todo: supports shift multiple selection later | boolean | - |
88 | | collapse | whether to allow node shrinkage | [collapse Prop](#collapse-prop) { } | - |
89 | | titleRender | rendering methods for node's title | (title) => void | - |
90 | | titleExtIconRender | rendering method of buttons on right side of node | (node) => void | - |
91 | | labelRender | rendering method of edge's label | (label) => void | - |
92 | | minimap | whether to show minimap | [minimap Prop](#minimap-prop) { } | - |
93 |
94 |
95 |
96 | ### collapse
97 |
98 | the configuration of node contraction
99 |
100 | | Property | Description | Type | Default |
101 | |:-----------:|:-------------------------------:|:-----------------------------------:|:------------------------------------:|
102 | | enable | whether to allow node shrinkage | boolean | - |
103 | | defaultMode | default presentation form | string | show as 'expand/collapse' by default |
104 |
105 |
106 |
107 | ### minimap
108 |
109 | the configuration of minimap
110 |
111 | | Property | Description | Type | Default |
112 | |:--------:|:-----------------------:|:---------------------------------------------------------------------------:|:-------:|
113 | | enable | whether to show minimap | boolean | - |
114 | | config | the config of minimap | [minimap Config Prop](#minimap-config-prop) { } | - |
115 |
116 |
117 |
118 | ### minimap Config
119 |
120 | the config of minimap
121 |
122 | | Property | Description | Type | Default |
123 | |:---------------:|:-----------------:|:-------------------------------:|:-------:|
124 | | nodeColor | node color | any | - |
125 | | activeNodeColor | node active color | any | - |
126 |
127 |
128 |
129 | ## 🔗API
130 |
131 | ``` JSX
132 | import VisualModeling from 'react-visual-modeling';
133 | import 'react-visual-modeling/dist/index.css';
134 | {}}
141 | onChange={() => {}}
142 | onFocusNode={() => {}}
143 | onFocusEdge={() => {}}
144 | onFocusCanvas={() => {}}
145 | onDblClickNode={() => {}}
146 | >
147 |
148 | ```
149 |
150 | ## 🔗API
151 |
152 | ``` javascript
153 | interface columns { // property settings for each column of fields
154 | title ? : string, // name of each column
155 | key: string, // the unique mark of each column, corresponding to the key value on the data
156 | width ? : number, // width of each column
157 | primaryKey: boolean, // whether the value corresponding to the key in this column is used as a unique sign
158 | render ? (value: any, rowData: any) : void // Customize the style of each column
159 | }
160 |
161 | interface config { //
162 | showActionIcon ? : boolean, // whether show operation icon: zoom in, zoom out, focus
163 | allowKeyboard ? : boolean, // allow keyboard to delete events. Todo: supports shift multiple selection later
164 | collapse: {
165 | enable: boolean, // allow node shrinkage
166 | defaultMode: string // show as 'expand/collapse' by default
167 | },
168 | titleRender ? (title: JSX.Element) : void, // rendering methods for node's title
169 | titleExtIconRender ? (node: JSX.Element) : void, // rendering method of buttons on right side of node
170 | labelRender ? (label: JSX.Element) : void, // rendering method of edge's label
171 | minimap: { // whether to show minimap
172 | enable: boolean,
173 | config: {
174 | nodeColor: any, // node color
175 | activeNodeColor: any // active node color
176 | }
177 | }
178 | }
179 |
180 | interface menu { // right-click menu configuration for'Node/Edge'
181 | title ? : string, // name of each column
182 | key: string, // unique flag for each column menu
183 | render ? (key: string) : void, // Customize the style of each column menu
184 | onClick ? (key: string, data: any) : void, // Click Callback for Each Column
185 | }
186 |
187 | interface props {
188 | width ? : number | string, // component width
189 | height ? : number | string, // component height
190 | className ? : string, // component className
191 | columns: Array < columns > , // similar to antd's table column concept
192 | nodeMenu: Array < menu > , // Node Right-click Menu Configuration
193 | edgeMenu: Array < menu > , // Edge Right-click Menu Configuration
194 | config: config, // As configured above
195 | data: any, // data
196 | emptyContent ? : string | JSX.Element; // show content when table field is empty
197 | emptyWidth ? : number | string; // table container width when table field is empty
198 | onLoaded(canvas: any): void, // canvas loaded event
199 | onChange(data: any): void, // canvas data change event
200 | onFocusNode(node: any): void, // focus node events
201 | onFocusEdge(edge: any): void, // focus edge events
202 | onFocusCanvas(): void, // focus canvas blank events
203 | };
204 | ```
205 |
206 | If you need more customized requirements, you can refer to issue or [butterfly](https://github.com/alibaba/butterfly/blob/master/README.en-US.md) to customize your needs
207 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 🎨可视化模型设计器
2 |
3 | 
4 | 
5 |
6 | [English](./README.en-US.md) | 简体中文
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## ✨ 特性
14 |
15 | * 支持定制字段属性
16 | * 支持定制title,title的icon
17 | * 支持节点的收缩/展开状态,以及收缩后的映射关系
18 | * 支持定制线段的属性以及自定义label
19 | * 支持节点,字段的hover,focus,linked以及全链路高亮状态
20 | * 支持节点,线段的右键菜单
21 | * 支持minimap,以及minimap的联动移动和高亮状态
22 | * 支持空字段内容定制
23 |
24 | ## 📦 安装
25 |
26 | ``` shell
27 | $ npm install react-visual-modeling butterfly-dag -S
28 | ```
29 |
30 | ## 🧤`Props`
31 |
32 | |参数|说明|类型|默认值|
33 | |----|----|----|----|
34 | |data|画布数据|any|-|
35 | |width|组件宽度| `number` \| `string` |-|
36 | |height|组件高度| `number` \| `string` |-|
37 | |className|组件类名 | `string` |-|
38 | |columns| 列的配置描述, 见[columns props](#columns) | Array<[columns](#columns)> | - |
39 | |nodeMenu| 节点右键菜单配置| Array<[menu](#menu-type)> | [ ] |
40 | |edgeMenu| 线段右键菜单配置| Array<[menu](#menu-type)> | [ ] |
41 | |actionMenu | 右上角菜单配置 | `action[]` | [] |
42 | |config| 组件的画布配置,见[config props](#config) | any | |-|
43 | |emptyContent| 当表字段为空时显示内容 | `string` \| `JSX. Element`| - |
44 | |emptyWidth| 当表字段为空时表容器宽度 | `number` \| `string`| - |
45 | |onLoaded| 渲染完毕事件 |`(canvas) => void` | - |
46 | |onChange| 图内数据变化事件|`(data) => void`| - |
47 | |onFocusNode |聚焦节点事件 |`(node) => void`| - |
48 | |onFocusEdge |聚焦线段事件 |`(edge) => void`| - |
49 | |onFocusCanvas | 聚焦空白处事件 | `() => void` | |-|
50 | |onDblClickNode| 双击节点事件 |`(node) => void`| - |
51 | | selectable | 是否开启框选 | `boolean` | false |
52 | |onSelect | 框选事件 | `(nodes, edges) => void`| - |
53 |
54 |
55 |
56 | ### columns
57 |
58 | > 节点字段每列的属性设置
59 |
60 | |参数|说明|类型|默认值|
61 | |---|---|---|---|
62 | |title|每列的名字| `string` |-|
63 | | key| 每列的唯一标志,对应数据上的key值 | `string` |-|
64 | |width|每列宽度| `number` ||-|
65 | | primaryKey | 这列的key对应的value是否作为键值对 | `boolean` |-|
66 | |render|支持每列的自定义样式|`(key) => void`|-|
67 |
68 |
69 |
70 | ### menu
71 |
72 | > '节点/线段'的右键菜单配置
73 |
74 | |参数| 说明|类型| 默认值 |
75 | |---|---|---|---|
76 | | title |每列的展示的名字| `string` ||-|
77 | |key| 每列的唯一标志,对应数据上的key值 | `string` ||-|
78 | | render | 支持每列的自定义样式 | `(key) => void` ||-|
79 | | onClick | 每列的点击回调| `(key, data) => void` | |-|
80 |
81 |
82 |
83 | ### config
84 |
85 | > 画布配置
86 |
87 | |参数|说明|类型|默认值|
88 | |---|---|---|---|
89 | |showActionIcon| 是否展示操作icon:放大,缩小,聚焦 | `boolean` |-|
90 | |allowKeyboard|允许键盘删除事件| `boolean` |-|
91 | | collapse |是否允许节点收缩| [collapse prop](#collapse-prop) { }|-|
92 | | titleRender| 节点title的渲染方法 | `(title) => void` |-|
93 | | titleExtIconRender | 节点右侧按钮的渲染方法 | `(node) => void` |-|
94 | | labelRender| 线段label的渲染方法 | `(label) => void` |-|
95 | |minimap | 是否开启缩略图| [minimap prop](#minimap-prop) { }|-|
96 |
97 |
98 |
99 | ### collapse
100 |
101 | > 节点收缩属性
102 |
103 | | 参数| 说明 | 类型| 默认值 |
104 | |---|---|---|---|
105 | |enable| 是否允许节点收缩 | `boolean` | - |
106 | | defaultMode |默认展示形式 | `string` | 默认以"展开/收缩"形式展示 |
107 |
108 |
109 |
110 | ### minimap
111 |
112 | > 缩略图属性
113 |
114 | |参数|说明|类型|默认值|
115 | |---|---|---|---|
116 | | enable | 是否开启缩略图 | `boolean` | false |
117 | | config | 缩略图的配置 | [minimap props](#minimap-config-prop) | {} |
118 |
119 |
120 |
121 | ### minimap config
122 |
123 | > 缩略图的配置
124 |
125 | |参数|说明|类型|默认值|
126 | |---|---|---|---|
127 | |nodeColor|节点颜色|`string`|-|
128 | |activeNodeColor|节点激活颜色|`string`| -|
129 |
130 |
131 |
132 | ## Usage
133 |
134 | ``` JSX
135 | import VisualModeling from 'react-visual-modeling';
136 | import 'react-visual-modeling/dist/index.css';
137 |
138 | // data 参考 example/mock_data/data.jsx
139 | {}}
146 | onChange={() => {}}
147 | onFocusNode={() => {}}
148 | onFocusEdge={() => {}}
149 | onFocusCanvas={() => {}}
150 | onDblClickNode={(node) => {}} // Double Click Node Event
151 | />
152 | ```
153 |
154 | ## Interface
155 |
156 | ```ts
157 | // 组件 Props 定义
158 | interface IProps {
159 | width?: number | string, // 组件宽
160 | height?: number | string, // 组件高
161 | className?: string, // 组件classname
162 | columns: Array< columns > , // 跟antd的table的column的概念类似
163 | nodeMenu?: Array< menu > , // 节点右键菜单配置
164 | edgeMenu?: Array< menu > , // 线段右键菜单配置
165 | actionMenu?: action[], // 右上角菜单配置,默认配置的key为 zoom-in(缩小), zoom-out(放大), fit(适配画布)
166 | config?: config, // 往下看
167 | data: IData, // 数据入参,往下看
168 | emptyContent?: JSX.Element; // 当表字段为空时显示内容
169 | emptyWidth?: number | string; // 当表字段为空时表容器宽度
170 | onLoaded(canvas: any): void, // 渲染完毕事件
171 | onChange(data: any): void, // 图内数据变化事件
172 | onFocusNode(node: any): void, // 聚焦节点事件
173 | onFocusEdge(edge: any): void, // 聚焦线段事件
174 | onFocusCanvas(): void, // 聚焦空白处事件
175 | onDblClickNode ? (node: any) : void, // 双击节点事件
176 | onSelect(nodes: any, edges: any): void, // 画布框选事件
177 | selectable: boolean, // 是否可框选
178 | };
179 |
180 | // 节点字段每列的属性设置
181 | interface columns {
182 | title?: string, // 每列的名字
183 | key: string, // 每列的唯一标志,对应数据上的key值
184 | width?: number, // 每列宽度(px)
185 | primaryKey: boolean, // 这列的key对应的value是否作为键值对
186 | render?: (value: any, rowData: any) => void // 可自定义每列的样式
187 | }
188 |
189 | // 画布显示配置
190 | interface config {
191 | butterfly: any; // butterfly-dag的配置,参考:https://github.com/alibaba/butterfly/blob/dev/v4/docs/zh-CN/canvas.md
192 | showActionIcon?: boolean, // 是否展示操作icon:放大,缩小,聚焦
193 | allowKeyboard?: boolean, // 允许键盘删除事件,TODO: 以后支持shift多选
194 | collapse:{
195 | enable: boolean, // 允许节点收缩
196 | defaultMode: string // 默认以"展开/收缩"形式展示
197 | },
198 | titleRender?: (title: JSX.Element) => void, // 节点title的渲染方法
199 | titleExtIconRender?: (node: JSX.Element) => void, // 节点右侧按钮的渲染方法
200 | labelRender?: (label: JSX.Element) => void, // 线段label的渲染方法
201 | minimap: { // 是否开启缩略图
202 | enable: boolean,
203 | config: {
204 | nodeColor: any, // 节点颜色
205 | activeNodeColor: any // 节点激活颜色
206 | }
207 | }
208 | }
209 |
210 | // 输入数据定义
211 | interface IData {
212 | nodes: { // 节点
213 | id: string | number;
214 | title: string;
215 | fields: {id: string, [key: string]: any}[]; // 当前节点字段列表
216 | [key: string]: any;
217 | }[],
218 | edges: {
219 | id: string | number,
220 | sourceNode: string, // 源节点ID
221 | targetNode: string, // 目标节点ID
222 | source: string, // 源节点列ID
223 | target: string, // 目标节点列ID
224 | }[]
225 | }
226 |
227 | // '节点/线段'的右键菜单配置
228 | interface menu {
229 | title?: string, // 每列的展示的名字
230 | key: string, // 每列的唯一标志,对应数据上的key值
231 | render?: (key: string) => JSX.Element, // 支持每列的自定义样式
232 | onClick?: (key: string, data: any) => void, // 每列的点击回调
233 | }
234 |
235 | // action菜单(右上角)
236 | interface action {
237 | key: string; // 唯一标识
238 | title: string; // 名字
239 | icon: string | JSX.Element; // 图标
240 | onClick: (canvas: any) => void; // 响应函数
241 | disable: boolean; // false 则不显示
242 | }
243 |
244 | ```
245 |
246 | ## 常用功能
247 |
248 | ### 1. 隐藏默认 `actionMenu` 和添加自定义 `actionMenu`
249 |
250 | ```jsx
251 | import {StarOutlined} from '@ant-design/icons';
252 |
253 | // 默认的三个 actionMenu 为 zoom-in, zoom-out, fit
254 | const actionMenu = [
255 | {
256 | key: 'zoom-in',
257 | disable: true
258 | },
259 | {
260 | icon: ,
261 | key: 'star',
262 | onClick: () => {
263 | alert('点击收藏!')
264 | }
265 | }
266 | ]
267 |
268 |
269 | ```
270 |
271 | ### 2. 设置连线配置
272 |
273 | ```jsx
274 | const config = {
275 | butterfly: {
276 | theme: {
277 | edge: {
278 | shapeType: 'Manhattan',
279 | }
280 | }
281 | }
282 | }
283 |
284 |
285 | ```
286 |
287 | ### 3. 实现框选功能
288 | ```jsx
289 | // 框选结果
290 | const onSelect = (nodes, edges) => {
291 | console.log(nodes, edges);
292 | }
293 |
294 |
298 | ```
299 |
300 |
301 | 如需要更多定制的需求,您可以提issue或者参考[Butterfly](https://github.com/alibaba/butterfly)来定制您需要的需求
302 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | DTDesign-React数据建模组件
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import ReactDOM from 'react-dom';
4 | import {Layout, Tooltip} from 'antd';
5 | import {BrowserRouter as Router} from 'react-router-dom';
6 |
7 | import TableBuilding from '../src/index.tsx';
8 | import {nodeMenu, edgeMenu, actionMenu} from './menu';
9 | import * as MockData from './mock_data/data.jsx';
10 |
11 | import 'antd/dist/antd.css';
12 | import './index.less';
13 |
14 | const {Header} = Layout;
15 | const {columns, data} = MockData;
16 |
17 | const config = {
18 | // butterfly-dag 属性
19 | butterfly:{
20 | theme:{
21 | edge: {
22 | // shapeType: 'Manhattan',
23 | }
24 | },
25 | },
26 |
27 | // 网格布局
28 | gridMode: {
29 | theme: {
30 | shapeType: 'circle', // 展示的类型,支持line & circle
31 | gap: 20, // 网格间隙
32 | circleRadiu: 1.5, // 圆点半径
33 | circleColor: 'rgba(255, 255, 255, 0.08)', // 圆点颜色
34 | }
35 | },
36 |
37 | // 键盘事件
38 | allowKeyboard: true,
39 |
40 | // 小地图相关
41 | minimap: {
42 | enable: true,
43 | config: {
44 | nodeColor: 'rgba(216, 216, 216, 0.13)',
45 | activeNodeColor: '#F66902',
46 | viewportStyle: {
47 | 'background-color': 'rgba(216, 216, 216, 0.07)'
48 | }
49 | }
50 | },
51 |
52 | // 是否表格可折叠
53 | collapse: {
54 | enable: true,
55 | showCollapseDetail: true
56 | },
57 | titleRender: (title) => {
58 | return title;
59 | },
60 | titleExtIconRender: () => {
61 | return (
62 |
63 |
66 |
67 | );
68 | },
69 | labelRender: (label) => {
70 | if(!label) {
71 | return 'connection';
72 | }
73 |
74 | return (
75 |
76 | {label}
77 |
78 | )
79 | }
80 | };
81 |
82 | class Component extends React.Component {
83 | constructor(props) {
84 | super(props);
85 | this.canvas = null;
86 | this.state = {
87 | columns: _.cloneDeep(columns),
88 | data: {},
89 | selectable: false,
90 | collapse: false
91 | };
92 | }
93 |
94 | componentWillMount() {
95 | this._data = _.cloneDeep(data);
96 | this.setState({
97 | data: this._data
98 | });
99 | }
100 |
101 | onAddEdge = () => {
102 | const data = this.state.data;
103 |
104 | data.edges.push({
105 | "id": 1,
106 | "sourceNode": "aaa",
107 | "targetNode": "bbb",
108 | "source": "field_1",
109 | "target": "field_2"
110 | });
111 |
112 | this.setState({
113 | data: {...data}
114 | });
115 | }
116 |
117 | onDelEdge = () => {
118 | const data = this.state.data;
119 | data.edges.pop();
120 |
121 | this.setState({
122 | data: {...data}
123 | });
124 | }
125 |
126 | onSetGridMode = () => {
127 | this.setState({
128 | selectable: true
129 | });
130 | }
131 |
132 | render() {
133 | const {selectable} = this.state;
134 |
135 | return (
136 | {
139 | // 自定义注册箭头
140 | const {Arrow} = utils;
141 | Arrow.registerArrow([{
142 | key: 'arrow1',
143 | type: 'svg',
144 | width: 14,
145 | height: 14,
146 | content: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyBjbGFzcz0iaWNvbiIgd2lkdGg9IjIwMHB4IiBoZWlnaHQ9IjIwMC4wMHB4IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTg0NS4zNTQ2NjcgMjYuNDk2bDQ1LjQ4MjY2NiA3Mi4xOTJMMzAyLjEyMjY2NyA0NjkuMzMzMzMzSDkxNy4zMzMzMzN2ODUuMzMzMzM0SDM2MC40OTA2NjdsNTMwLjk0NCAzNzMuNjc0NjY2LTQ5LjA2NjY2NyA2OS43Ni02NDUuODAyNjY3LTQ1NC40IDM2LjUyMjY2Ny01Mi4wMTA2NjYtMzUuOTI1MzMzLTU3LjA0NTMzNEw4NDUuMzU0NjY3IDI2LjQ1MzMzM3oiIGZpbGw9IiNGNjY5MDIiIC8+PHBhdGggZD0iTTI3Ny4zMzMzMzMgNTEybS0xMjggMGExMjggMTI4IDAgMSAwIDI1NiAwIDEyOCAxMjggMCAxIDAtMjU2IDBaIiBmaWxsPSIjRjY2OTAyIiAvPjxwYXRoIGQ9Ik0yNzcuMzMzMzMzIDM0MS4zMzMzMzNhMTcwLjY2NjY2NyAxNzAuNjY2NjY3IDAgMSAxIDAgMzQxLjMzMzMzNCAxNzAuNjY2NjY3IDE3MC42NjY2NjcgMCAwIDEgMC0zNDEuMzMzMzM0eiBtMCA4NS4zMzMzMzRhODUuMzMzMzMzIDg1LjMzMzMzMyAwIDEgMCAwIDE3MC42NjY2NjYgODUuMzMzMzMzIDg1LjMzMzMzMyAwIDAgMCAwLTE3MC42NjY2NjZ6IiBmaWxsPSIjRkZCMjdCIiAvPjwvc3ZnPg=='
147 | }]);
148 | }}
149 |
150 | onLoaded={(canvas) => {
151 | this.canvas = canvas;
152 | canvas.on('events', (data) => {
153 | // console.log(data);
154 | });
155 | }}
156 |
157 | // =========== 节点Table相关属性 ===========
158 | columns={this.state.columns}
159 | data={this.state.data}
160 | onDblClickNode={(node) => {}}
161 | emptyContent={
162 |
163 |
暂无数据
164 |
{
167 | e.stopPropagation();
168 | console.log('自定义空状态');
169 | }}
170 | >
171 | + 添加字段
172 |
173 |
174 | }
175 |
176 | // =========== 菜单相关属性 ===========
177 | nodeMenu={nodeMenu}
178 | edgeMenu={edgeMenu}
179 | actionMenu={actionMenu({
180 | onAddEdge: this.onAddEdge,
181 | onDelEdge: this.onDelEdge,
182 | onSetGridMode: this.onSetGridMode
183 | })}
184 |
185 | // =========== 画布配置 ===========
186 | config={{
187 | ...config,
188 | collapse: {
189 | status: this.state.collapse,
190 | showCollapseDetail: true
191 | }
192 | }}
193 |
194 | // =========== 框选配置 ===========
195 | selectable={selectable}
196 | onSelect={() => {
197 | this.setState({
198 | selectable: false
199 | })
200 | }}
201 |
202 | beforeDeleteNode={(nodes) => {
203 | // 返回false或者Promise.reject则不会删除
204 | }}
205 | beforeDeleteEdge={(edges) => {
206 | console.log(edges);
207 | // 返回false或者Promise.reject则不会删除
208 | }}
209 | />
210 | )
211 | }
212 | }
213 |
214 | ReactDOM.render((
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | ), document.getElementById('main'));
224 |
--------------------------------------------------------------------------------
/example/index.less:
--------------------------------------------------------------------------------
1 | html, body {
2 | padding: 0;
3 | margin: 0;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
8 | #main {
9 | width: 100%;
10 | height: 100%;
11 | .ant-layout, .ant-layout-content {
12 | height: 100%;
13 | }
14 |
15 | .menu {
16 | height: 100%;
17 | width: 200px;
18 | overflow-y: auto;
19 | }
20 |
21 | .header.ant-layout-header {
22 | background-color: #212528;
23 | color: #fff;
24 | height: 50px;
25 | line-height: 50px;
26 | }
27 |
28 | section.ant-layout {
29 | background: #2E2E2E;
30 | }
31 |
32 | .butterfly-table-building {
33 | width: 100%;
34 | height: 100%;
35 | }
36 | }
37 | .view-port-background {
38 | background-color: rgba(216, 216, 216, 0.05);
39 | }
40 |
41 | .empty-content {
42 | height: 100px;
43 | padding-top: 20px;
44 | text-align: center;
45 | .desc {
46 | color: #474747;
47 | }
48 | .add-field {
49 | color: #0070cc;
50 | cursor: pointer;
51 | }
52 | }
--------------------------------------------------------------------------------
/example/menu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | StarOutlined, PlusOutlined,
4 | MinusOutlined, GatewayOutlined
5 | } from '@ant-design/icons';
6 |
7 | // 节点菜单
8 | export const nodeMenu = [
9 | {
10 | key: 'setting',
11 | title: '节点设置',
12 | onClick: (key, data) => {
13 | console.log('click setting')
14 | }
15 | },
16 | {
17 | key: 'delete',
18 | render: (key, data) => {
19 | return 节点删除
20 | },
21 | onClick: (key, data) => {
22 | console.log('delete node');
23 | }
24 | }
25 | ];
26 |
27 | // 边菜单
28 | export const edgeMenu= [
29 | {
30 | key: 'setting',
31 | title: '线段设置',
32 | onClick: (key, data) => {
33 | console.log('click setting')
34 | }
35 | },
36 | {
37 | key: 'delete',
38 | render: (key, data) => {
39 | return 线段删除
40 | },
41 | onClick: (key, data) => {
42 | console.log('delete node');
43 | }
44 | }
45 | ];
46 |
47 | export const actionMenu = ({
48 | onAddEdge,
49 | onDelEdge,
50 | onSetGridMode
51 | }) => [
52 | {
53 | key: 'zoom-in',
54 | disable: true
55 | },
56 | {
57 | icon: ,
58 | key: 'star',
59 | onClick: () => {
60 | alert('点击收藏!')
61 | }
62 | },
63 | {
64 | icon: ,
65 | key: 'plus',
66 | title: '添加一条连线',
67 | onClick: () => {
68 | onAddEdge();
69 | }
70 | },
71 | {
72 | icon: ,
73 | key: 'minus',
74 | title: '删除一条连线',
75 | onClick: () => {
76 | onDelEdge();
77 | }
78 | },
79 | {
80 | icon: ,
81 | title: '框选',
82 | key: 'select',
83 | onClick: () => {
84 | onSetGridMode();
85 | }
86 | }
87 | ]
--------------------------------------------------------------------------------
/example/mock_data/data.jsx:
--------------------------------------------------------------------------------
1 | export const columns = [
2 | {
3 | key: 'id',
4 | primaryKey: true,
5 | width: 76
6 | },
7 | {
8 | key: 'type',
9 | width: 60,
10 | render: (val, row) => {
11 | return val.toString().toUpperCase();
12 | }
13 | },
14 | {
15 | key: 'desc',
16 | width: 90
17 | }
18 | ];
19 |
20 | export const data = {
21 | "nodes": [
22 | {
23 | "top": 300,
24 | "left": 200,
25 | "id": "aaa",
26 | "title": "aaa",
27 | "fields": [
28 | {
29 | "id": "field_1",
30 | "type": "string",
31 | "desc": "字段1"
32 | },
33 | {
34 | "id": "field_2",
35 | "type": "string",
36 | "desc": "字段2"
37 | },
38 | {
39 | "id": "field_3",
40 | "type": "string",
41 | "desc": "字段3"
42 | }
43 | ]
44 | },
45 | {
46 | "top": 500,
47 | "left": 600,
48 | "id": "bbb",
49 | "title": "bbb",
50 | "fields": [
51 | {
52 | "id": "field_1",
53 | "type": "string",
54 | "desc": "字段1"
55 | },
56 | {
57 | "id": "field_2",
58 | "type": "string",
59 | "desc": "字段2"
60 | },
61 | {
62 | "id": "field_3",
63 | "type": "string",
64 | "desc": "字段3"
65 | }
66 | ]
67 | },
68 | {
69 | "top": 300,
70 | "left": 1000,
71 | "id": "ccc",
72 | "title": "ccc",
73 | "fields": [
74 | {
75 | "id": "field_1",
76 | "type": "string",
77 | "desc": "字段1"
78 | },
79 | {
80 | "id": "field_2",
81 | "type": "string",
82 | "desc": "字段2"
83 | },
84 | {
85 | "id": "field_3",
86 | "type": "string",
87 | "desc": "字段3"
88 | }
89 | ]
90 | },
91 | {
92 | "top": 100,
93 | "left": 600,
94 | "id": "ddd",
95 | "title": "ddd",
96 | "fields": [
97 | {
98 | "id": "field_1",
99 | "type": "string",
100 | "desc": "字段1"
101 | },
102 | {
103 | "id": "field_2",
104 | "type": "string",
105 | "desc": "字段2"
106 | },
107 | {
108 | "id": "field_3",
109 | "type": "string",
110 | "desc": "字段3"
111 | }
112 | ]
113 | },
114 | {
115 | "top": 50,
116 | "left": 1000,
117 | "id": "eee",
118 | "title": "eee",
119 | "fields": [
120 | {
121 | "id": "field_1",
122 | "type": "string",
123 | "desc": "字段1"
124 | },
125 | {
126 | "id": "field_2",
127 | "type": "string",
128 | "desc": "字段2"
129 | },
130 | {
131 | "id": "field_3",
132 | "type": "string",
133 | "desc": "字段3"
134 | }
135 | ]
136 | },
137 | {
138 | "top": 540,
139 | "left": 1000,
140 | "id": "fff",
141 | "title": "自定义空内容",
142 | "fields": []
143 | }
144 | ],
145 | "edges": [
146 | {
147 | "id": 1,
148 | "sourceNode": "aaa",
149 | "targetNode": "bbb",
150 | "source": "field_1",
151 | "target": "field_1",
152 | "label": "label"
153 | },
154 | {
155 | "id": 2,
156 | "sourceNode": "aaa",
157 | "targetNode": "bbb",
158 | "source": "field_3",
159 | "target": "field_3"
160 | },
161 | {
162 | "id": 3,
163 | "sourceNode": "aaa",
164 | "targetNode": "ddd",
165 | "source": "field_2",
166 | "target": "field_2"
167 | },
168 | {
169 | "id": 4,
170 | "sourceNode": "ddd",
171 | "targetNode": "eee",
172 | "source": "field_2",
173 | "target": "field_2"
174 | },
175 | {
176 | "id": 5,
177 | "sourceNode": "ddd",
178 | "targetNode": "ccc",
179 | "source": "field_2",
180 | "target": "field_2"
181 | },
182 | {
183 | "id": 6,
184 | "sourceNode": "bbb",
185 | "targetNode": "eee",
186 | "source": "field_1",
187 | "target": "field_3"
188 | },
189 | {
190 | "id": 7,
191 | "sourceNode": "bbb",
192 | "targetNode": "ccc",
193 | "source": "field_3",
194 | "target": "field_1"
195 | },
196 | {
197 | "id": 8,
198 | "sourceNode": "bbb",
199 | "targetNode": "ccc",
200 | "source": "field_3",
201 | "target": "field_3",
202 | "arrowShapeType": "arrow1"
203 | }
204 | ]
205 | };
206 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "webpack-dev-server"
9 | },
10 | "author": "jambin",
11 | "license": "MIT",
12 | "dependencies": {
13 | "antd": "~4.10.3",
14 | "jquery": "^3.4.1",
15 | "react": "~16.12.0",
16 | "react-dom": "^16.13.1",
17 | "react-router": "~5.1.2",
18 | "react-router-dom": "~5.1.2",
19 | "webpack": "^4.41.6",
20 | "webpack-dev-server": "^3.10.3"
21 | },
22 | "devDependencies": {
23 | "@babel/core": "~7.8.3",
24 | "@babel/plugin-proposal-class-properties": "~7.8.3",
25 | "@babel/plugin-proposal-object-rest-spread": "~7.8.3",
26 | "@babel/plugin-transform-runtime": "^7.12.1",
27 | "@babel/preset-env": "~7.8.3",
28 | "@babel/preset-react": "~7.8.3",
29 | "babel-loader": "8.0.6",
30 | "babel-plugin-transform-es2015-modules-commonjs": "~6.26.2",
31 | "css-loader": "~1.0.0",
32 | "eslint": "~5.16.0",
33 | "eslint-config-aliyun": "~2.0.3",
34 | "eslint-plugin-react": "~7.13.0",
35 | "file-loader": "~2.0.0",
36 | "html-webpack-plugin": "^3.2.0",
37 | "less": "~3.7.0",
38 | "less-loader": "~4.1.0",
39 | "mini-css-extract-plugin": "~0.9.0",
40 | "style-loader": "~0.21.0",
41 | "ts-loader": "~8.0.14",
42 | "url-loader": "~1.0.1",
43 | "webpack": "~4.41.5",
44 | "webpack-cli": "^3.0.8",
45 | "webpack-dev-server": "~3.10.1"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "noImplicitAny": false,
5 | "module": "commonjs",
6 | "target": "es5",
7 | "jsx": "react",
8 | "allowJs": true
9 | }
10 | }
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | const path = require('path');
6 |
7 | module.exports = {
8 | entry: {
9 | app: './index.jsx'
10 | },
11 | output: {
12 | filename: '[name].js',
13 | chunkFilename: '[name].js'
14 | },
15 | resolve: {
16 | modules: [
17 | path.resolve(process.cwd(), 'node_modules'),
18 | path.resolve(process.cwd(), '../node_modules'),
19 | 'node_modules'
20 | ],
21 | extensions: ['.js', '.jsx', '.ts', '.tsx']
22 | },
23 | module: {
24 | rules: [
25 | {
26 | test: /\.tsx?$/,
27 | use: 'ts-loader',
28 | exclude: /node_modules/,
29 | },
30 | {
31 | test: /\.(js|jsx)$/,
32 | exclude: /(node_modules|bower_components)/,
33 | use: {
34 | loader: 'babel-loader',
35 | options: {
36 | presets: [
37 | '@babel/preset-env',
38 | '@babel/preset-react'
39 | ],
40 | plugins: [
41 | '@babel/plugin-transform-runtime',
42 | '@babel/plugin-transform-modules-commonjs',
43 | '@babel/plugin-proposal-object-rest-spread',
44 | '@babel/plugin-proposal-class-properties',
45 | ]
46 | }
47 | }
48 | },
49 | {
50 | test: /\.(woff|woff2|eot|ttf|otf)$/,
51 | use: {
52 | loader: 'file-loader',
53 |
54 | options: {
55 | name: '[name][hash].[ext]',
56 | outputPath: 'fonts/'
57 | }
58 | }
59 | },
60 | {
61 | test: /\.(less|css)$/,
62 | use: [
63 | {
64 | loader: 'style-loader'
65 | },
66 | {
67 | loader: 'css-loader'
68 | },
69 | {
70 | loader: 'less-loader',
71 | options: {
72 | javascriptEnabled: true
73 | }
74 | }
75 | ]
76 | },
77 | {
78 | test: /\.(png|jpg|gif|svg)$/,
79 | use: [
80 | {
81 | loader: 'url-loader'
82 | }
83 | ]
84 | }
85 | ]
86 | },
87 | plugins: [
88 | new MiniCssExtractPlugin({
89 | filename: '[name].css'
90 | }),
91 | new HtmlWebpackPlugin({
92 | template: './index.html'
93 | })
94 | ],
95 | devServer: {
96 | contentBase: './dist', // 本地服务器所加载的页面所在的目录
97 | historyApiFallback: true, // 不跳转
98 | inline: true, // 实时刷新
99 | index: 'index.html',
100 | port: 8080,
101 | open: true
102 | }
103 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-visual-modeling",
3 | "version": "1.1.5",
4 | "description": "一个基于React的数据可视化建模的DAG图,适用于UML,数据库建模,数据仓库建设等业务",
5 | "main": "dist/index.js",
6 | "pack": "pack/index.js",
7 | "directories": {
8 | "example": "example"
9 | },
10 | "scripts": {
11 | "test": "echo \"Error: no test specified\" && exit 1",
12 | "build": "rollup -c && cp -r ./src/static ./dist && cp -r ./src/static ./pack",
13 | "dev": "rollup -w -c",
14 | "prepublishOnly": "npm run build",
15 | "start": "cd example && npm start"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git@gitlab.alibaba-inc.com:DataQ-FE-Components/butterfly-table-building.git"
20 | },
21 | "author": "无惟",
22 | "license": "MIT",
23 | "dependencies": {
24 | "butterfly-dag": "^4.0.2",
25 | "jquery": "^3.5.1",
26 | "react-data-mapping": "^1.1.2"
27 | },
28 | "peerDependencies": {
29 | "react": ">15.6.1, <17.0.0",
30 | "react-dom": ">15.6.1, <17.0.0",
31 | "lodash": "^4.17.20"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "~7.12.0",
35 | "@babel/plugin-proposal-class-properties": "~7.12.1",
36 | "@babel/plugin-proposal-object-rest-spread": "~7.12.0",
37 | "@babel/plugin-transform-modules-commonjs": "^7.12.1",
38 | "@babel/plugin-transform-runtime": "^7.12.1",
39 | "@babel/preset-env": "~7.12.0",
40 | "@babel/preset-react": "~7.12.1",
41 | "@babel/preset-typescript": "~7.12.7",
42 | "@types/jquery": "^3.5.5",
43 | "@types/lodash": "~4.14.167",
44 | "@types/react": "~17.0.0",
45 | "@types/react-dom": "^17.0.0",
46 | "babel-loader": "~8.2.0",
47 | "babel-plugin-transform-es2015-modules-commonjs": "~6.26.2",
48 | "less": "~3.12.2",
49 | "postcss": "~8.1.7",
50 | "rollup": "~2.38.0",
51 | "rollup-plugin-babel": "~4.4.0",
52 | "rollup-plugin-commonjs": "~10.1.0",
53 | "rollup-plugin-extensions": "~0.1.0",
54 | "rollup-plugin-json": "~4.0.0",
55 | "rollup-plugin-peer-deps-external": "~2.2.4",
56 | "rollup-plugin-postcss": "~4.0.0",
57 | "rollup-plugin-typescript2": "~0.29.0",
58 | "rollup-plugin-url": "~3.0.1",
59 | "typescript": "~4.1.3"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import json from 'rollup-plugin-json';
3 | import babel from 'rollup-plugin-babel';
4 | import postcss from 'rollup-plugin-postcss';
5 | import commonjs from 'rollup-plugin-commonjs';
6 | import extensions from 'rollup-plugin-extensions';
7 | import external from 'rollup-plugin-peer-deps-external';
8 | import url from "rollup-plugin-url";
9 | import typescript from 'rollup-plugin-typescript2';
10 |
11 | import pkg from './package.json';
12 |
13 | const config = {
14 | presets: [
15 | '@babel/preset-typescript',
16 | '@babel/preset-react',
17 | '@babel/preset-env'
18 | ],
19 | plugins: [
20 | '@babel/plugin-proposal-class-properties'
21 | ]
22 | };
23 |
24 |
25 | const plugins = [
26 | extensions({
27 | extensions: ['.js'],
28 | resolveIndex: true,
29 | }),
30 | external(),
31 | babel(Object.assign({
32 | exclude: [
33 | 'node_modules/**',
34 | ]
35 | }, config)),
36 | postcss({
37 | extract: true,
38 | modules: false,
39 | use: [
40 | [
41 | 'less',
42 | {
43 | javascriptEnabled: true,
44 | }
45 | ]
46 | ]
47 | }),
48 | commonjs(),
49 | json(),
50 | // url({
51 | // limit: 100 * 1024, // inline files < 100k, copy files > 100k
52 | // include: ["**/*.svg", "**/*.eot", "**/*.tff", "**/*.woff", "**/*.woff2"], // defaults to .svg, .png, .jpg and .gif files
53 | // emitFiles: true // defaults to true
54 | // }),
55 | typescript({
56 | tsconfigOverride: {
57 | compilerOptions: {
58 | module: "es2015"
59 | }
60 | }
61 | })
62 | ];
63 |
64 | const rollupCfg = [];
65 |
66 | // all in one 构建
67 | rollupCfg.push({
68 | input: path.join(__dirname, 'src/index.tsx'),
69 | output: [
70 | {
71 | file: pkg.pack,
72 | format: 'cjs',
73 | exports: 'named',
74 | sourcemap: 'inline'
75 | },
76 | {
77 | file: pkg.main,
78 | format: 'es',
79 | sourcemap: 'inline'
80 | }
81 | ],
82 | plugins
83 | });
84 |
85 | export default rollupCfg;
86 |
--------------------------------------------------------------------------------
/src/adaptor.js:
--------------------------------------------------------------------------------
1 | import * as _ from 'lodash';
2 |
3 | import Edge from './canvas/edge';
4 | import TableNode from './canvas/node';
5 |
6 | export const transformInitData = (info) => {
7 | let {
8 | columns, data, config,
9 | nodeMenu, edgeMenu, emptyContent,
10 | emptyWidth
11 | } = info;
12 |
13 | let nodes = (data.nodes || []).map((item) => {
14 | return _.assign(item, {
15 | _columns: columns,
16 | _config: config,
17 | _menu: nodeMenu,
18 | Class: TableNode,
19 | _emptyWidth: emptyWidth,
20 | _emptyContent: emptyContent
21 | });
22 | });
23 |
24 | let edges = (data.edges || []).map((item) => {
25 | return _.assign(item, {
26 | id: `${item.sourceNode}-${item.source}-${item.targetNode}-${item.target}`,
27 | type: 'endpoint',
28 | _config: config,
29 | _menu: edgeMenu,
30 | Class: Edge,
31 | label: item.label,
32 | });
33 | });
34 |
35 | return {
36 | nodes,
37 | edges
38 | }
39 | }
40 |
41 | export const diffPropsData = (newData, oldData, options) => {
42 | const isSameNode = (a, b) => a.id === b.id;
43 | let addNodes = _.differenceWith(newData.nodes, oldData.nodes, isSameNode);
44 | let rmNodes = _.differenceWith(oldData.nodes, newData.nodes, isSameNode);
45 |
46 | const isSameEdge = (a, b) => {
47 | return (
48 | a.sourceNode === b.sourceNode &&
49 | a.targetNode === b.targetNode &&
50 | a.source === b.source &&
51 | a.target === b.target
52 | );
53 | }
54 |
55 | const isSameLabel = (a, b) => {
56 | if(typeof a === 'string') {
57 | return a === b;
58 | }
59 |
60 | try {
61 | return JSON.stringify(a) === JSON.stringify(b);
62 | } catch (e) {
63 | return a === b;
64 | }
65 | }
66 |
67 | let addEdges = _.differenceWith(newData.edges, oldData.edges, isSameEdge);
68 | let rmEdges = _.differenceWith(oldData.edges, newData.edges, isSameEdge);
69 |
70 | // 线段的label有变化
71 | let updateLabel = [];
72 | newData.edges.forEach((a) => {
73 | let edge = _.find(oldData.edges, (b) => {
74 | return isSameEdge(a, b);
75 | });
76 |
77 | if(!edge) {
78 | return;
79 | }
80 |
81 | if(isSameLabel(a.label, edge.label)) {
82 | return;
83 | }
84 |
85 | updateLabel.push({
86 | edge,
87 | label: a.label
88 | });
89 | });
90 |
91 | let updateTitle = [];
92 | let addFields = [];
93 | let rmFields = [];
94 | let updateFields = [];
95 | let newCol = [];
96 | let columns = options.newCol;
97 |
98 | // columns有变化
99 | let addCol = _.differenceWith(options.newCol, options.oldCol, (a, b) => a.key === b.key);
100 | let oldCol = _.differenceWith(options.oldCol, options.newCol, (a, b) => a.key === b.key);
101 |
102 | if (addCol.length !== 0 || oldCol.length !== 0) {
103 | newCol = options.newCol;
104 | }
105 |
106 | (newData.nodes || []).forEach((newNode) => {
107 | let _addFields = [];
108 | let _rmFields = [];
109 | let _updateFields = [];
110 | let oldNode = _.find((oldData.nodes || []), (item) => {
111 | return newNode.id === item.id;
112 | });
113 | if (!oldNode) {
114 | return false;
115 | }
116 | // 表名有变化
117 | if (newNode.title !== oldNode.title) {
118 | updateTitle.push({
119 | nodeId: newNode.id,
120 | title: newNode.title
121 | });
122 | }
123 |
124 | // 列属性有变化
125 | let _primaryKey = _.find(columns, (item) => {
126 | return item.primaryKey;
127 | }).key;
128 |
129 | _addFields = _.differenceWith(newNode.fields, oldNode.fields, (a, b) => {
130 | return a[_primaryKey] === b[_primaryKey];
131 | });
132 |
133 | _rmFields = _.differenceWith(oldNode.fields, newNode.fields, (a, b) => {
134 | return a[_primaryKey] === b[_primaryKey];
135 | });
136 |
137 | newNode.fields.forEach((newField) => {
138 | let oldField = _.find(oldNode.fields, (item) => {
139 | return newField[_primaryKey] === item[_primaryKey];
140 | });
141 | if (!oldField) {
142 | return;
143 | }
144 | for(let i = 0; i < columns.length; i++) {
145 | let col = columns[i];
146 | if (!col.primaryKey && newField[col.key] !== oldField[col.key]) {
147 | _updateFields.push(newField);
148 | return;
149 | }
150 | }
151 | })
152 |
153 | _addFields.length > 0 && addFields.push({
154 | nodeId: newNode.id,
155 | fields: _addFields
156 | });
157 |
158 | _rmFields.length > 0 && rmFields.push({
159 | nodeId: newNode.id,
160 | fields: _rmFields
161 | });
162 |
163 | _updateFields.length > 0 && updateFields.push({
164 | nodeId: newNode.id,
165 | fields: _updateFields
166 | });
167 | });
168 |
169 | return {
170 | addNodes,
171 | rmNodes,
172 | addEdges,
173 | rmEdges,
174 | updateLabel,
175 | updateTitle,
176 | addFields,
177 | rmFields,
178 | updateFields,
179 | newCol
180 | };
181 | }
--------------------------------------------------------------------------------
/src/canvas/canvas.js:
--------------------------------------------------------------------------------
1 | import {Canvas, Tips} from 'butterfly-dag';
2 | import $ from 'jquery';
3 |
4 | import CollapseMenuGen from './collapse-menu.jsx';
5 |
6 | export default class TableCanvas extends Canvas {
7 | constructor(opts) {
8 | super(opts);
9 | this.originEdges = [];
10 | this._focusItem = null;
11 | this._enableHoverChain = opts.data.enableHoverChain;
12 | this._enableFocusChain = opts.data.enableFocusChain;
13 | this._showCollapseDetail = opts.data.showCollapseDetail;
14 | this.attachEvent();
15 | }
16 |
17 | attachEvent() {
18 | // 线段删除特殊处理
19 | this.on('custom.endpoint.dragNode', (data) => {
20 | let point = data.data;
21 | let node = this.getNode(point.nodeId);
22 | let linkedPoint = node.getEndpoint(point.id + '-right', 'source');
23 | this.emit('InnerEvents', {
24 | type: 'endpoint:drag',
25 | data: linkedPoint
26 | });
27 | });
28 |
29 | // 移动节点时,隐藏菜单
30 | this.on('system.drag.start', (data) => {
31 | Tips.closeMenu();
32 | });
33 |
34 | if (this._enableHoverChain) {
35 | this.on('custom.endpoint.hover', (data) => {
36 | let point = data.point;
37 | this.focusChain(point.nodeId, point.id, 'hover-chain');
38 | });
39 | this.on('custom.endpoint.unHover', (data) => {
40 | let point = data.point;
41 | this.unfocusChain(point.nodeId, point.id, 'hover-chain');
42 | });
43 | }
44 | if (this._enableFocusChain) {
45 | this.on('custom.endpoint.focus', (data) => {
46 | let point = data.point;
47 | this.focusChain(point.nodeId, point.id, 'focus-chain');
48 | });
49 | }
50 |
51 | this.on('custom.node.expand', (data) => {
52 | this.expand(data.nodeId);
53 | });
54 |
55 | this.on('custom.node.collapse', (data) => {
56 | this.collapse(data.nodeId);
57 | });
58 |
59 | this.on('custom.edge.showCollapseInfo', (data) => {
60 | const {container, pos, edge} = data;
61 | if (this._showCollapseDetail) {
62 | CollapseMenuGen({
63 | container,
64 | pos,
65 | edge,
66 | originEdgesInfo: this.originEdges
67 | });
68 | }
69 | });
70 |
71 | this.on('system.links.delete', (data) => {
72 | data.links.forEach((delLink) => {
73 | this.originEdges = this.originEdges.filter((originLink) => {
74 | return !(
75 | originLink.sourceNode === delLink.sourceNode.id,
76 | originLink.source === _.get(delLink, 'sourceEndpoint.options.originId'),
77 | originLink.targetNode === delLink.targetNode.id,
78 | originLink.target === _.get(delLink, 'targetEndpoint.options.originId')
79 | );
80 | });
81 | });
82 | });
83 |
84 | this.on('system.nodes.delete', (data) => {
85 | data.nodes.forEach((item) => {
86 | this.originEdges = this.originEdges.filter((originLink) => {
87 | return originLink.sourceNode !== item.id && originLink.targetNode !== item.id;
88 | });
89 | })
90 | });
91 | }
92 |
93 | updateNodes(nodeInfos) {
94 | (nodeInfos.updateTitle || []).forEach((info) => {
95 | let node = this.getNode(info.nodeId);
96 | node._updateTitle(info.title);
97 | });
98 |
99 | if (nodeInfos.newCol && nodeInfos.newCol.length > 0) {
100 | let edges = this.edges.map(item => item);
101 | let _originEdges = this.originEdges;
102 | this.nodes.forEach((item) => {
103 | item._updateCol(nodeInfos.newCol);
104 | });
105 | this.originEdges = _originEdges;
106 |
107 | /**
108 | * 更新col时会将之前的 endpoint 删掉,
109 | * 此时需要重新更新
110 | */
111 | edges.forEach(edge => {
112 | const newSrcEndpoint = edge.sourceNode.getEndpoint(edge.sourceEndpoint.id);
113 | edge.sourceEndpoint = newSrcEndpoint;
114 |
115 | const newTgtEndpoint = edge.targetNode.getEndpoint(edge.targetEndpoint.id);
116 | edge.targetEndpoint = newTgtEndpoint;
117 | });
118 |
119 | this.addEdges(edges, true);
120 | }
121 |
122 | (nodeInfos.addFields || []).forEach((info) => {
123 | let node = this.getNode(info.nodeId);
124 | node._addFields(info.fields);
125 | });
126 | (nodeInfos.rmFields || []).forEach((info) => {
127 | let node = this.getNode(info.nodeId);
128 | node._rmFields(info.fields);
129 | });
130 | (nodeInfos.updateFields || []).forEach((info) => {
131 | let node = this.getNode(info.nodeId);
132 | node._updateFields(info.fields);
133 | });
134 | }
135 |
136 | updateLabel(infos) {
137 | infos.forEach((info) => {
138 | let _targetEdge = info.edge;
139 | let edge = _.find(this.edges, (item) => {
140 | return (
141 | _targetEdge.sourceNode === item.sourceNode.id &&
142 | _targetEdge.targetNode === item.targetNode.id &&
143 | _targetEdge.source === item.sourceEndpoint.options.originId &&
144 | _targetEdge.target === item.targetEndpoint.options.originId
145 | );
146 | });
147 |
148 | this.getEdge(info.edge.id);
149 | if (edge) {
150 | edge.updateLabel(info.label);
151 | $(edge.labelDom).on('dblclick',(e) => {
152 | e.preventDefault();
153 | e.stopPropagation();
154 | edge.emit('custom.edge.dblClick',{
155 | edge
156 | });
157 | });
158 | }
159 | })
160 | }
161 |
162 | expand(nodeId) {
163 | let node = this.getNode(nodeId);
164 |
165 | if(!node) {
166 | return;
167 | }
168 |
169 | let oldEdges = this.getNeighborEdges(nodeId);
170 | this.removeEdges(oldEdges, true);
171 | node._expand();
172 | let newEdges = [];
173 | // 从全局展开图里面纠正线段
174 | this.originEdges.forEach((item) => {
175 | if (item.sourceNode === nodeId) {
176 | let targetNode = this.getNode(item.targetNode);
177 | if (targetNode.status === 'collapse') {
178 | newEdges.push(_.assign({}, item, {
179 | sourceNode: item.sourceNode,
180 | source: `${item.source}-right`,
181 | targetNode: item.targetNode,
182 | target: `${targetNode.id}-left`
183 | }));
184 | } else {
185 | newEdges.push(_.assign({}, item, {
186 | sourceNode: item.sourceNode,
187 | source: `${item.source}-right`,
188 | targetNode: item.targetNode,
189 | target: `${item.target}-left`
190 | }));
191 | }
192 | } else if (item.targetNode == nodeId) {
193 | let sourceNode = this.getNode(item.sourceNode);
194 | if (sourceNode.status === 'collapse') {
195 | newEdges.push(_.assign({}, item, {
196 | sourceNode: item.sourceNode,
197 | source: `${sourceNode.id}-right`,
198 | targetNode: item.targetNode,
199 | target: `${item.target}-left`
200 | }));
201 | } else {
202 | newEdges.push(_.assign({}, item, {
203 | sourceNode: item.sourceNode,
204 | source: `${item.source}-right`,
205 | targetNode: item.targetNode,
206 | target: `${item.target}-left`
207 | }));
208 | }
209 | }
210 | });
211 | // 去重
212 | newEdges = _.uniqWith(newEdges, (a, b) => {
213 | return (
214 | a.sourceNode === b.sourceNode &&
215 | a.targetNode === b.targetNode &&
216 | a.souce === b.souce &&
217 | a.target === b.target
218 | );
219 | });
220 |
221 | this.addEdges(newEdges, true);
222 |
223 | this.emit('table.canvas.expand');
224 | }
225 |
226 | collapse(nodeId) {
227 | let node = this.getNode(nodeId);
228 | if(!node) {
229 | return;
230 | }
231 |
232 | let oldEdges = this.getNeighborEdges(nodeId);
233 | let oldEdgesInfo = oldEdges.map((item) => {
234 | return item.options;
235 | });
236 |
237 | this.removeEdges(oldEdges, true);
238 | let newEdgesInfos = node._collapse(oldEdgesInfo);
239 | let newEdges = this.addEdges(newEdgesInfos, true);
240 |
241 | newEdges.forEach((item) => {
242 | item.collapse = true;
243 | item.sourceEndpoint.updatePos();
244 | item.targetEndpoint.updatePos();
245 | item.redraw();
246 | });
247 | this.emit('table.canvas.collapse');
248 | }
249 |
250 | focusChain(nodeId, pointId, addClass) {
251 | let chain = this._findChain(nodeId, pointId);
252 | chain.edges.forEach((item) => {
253 | item.focusChain(addClass);
254 | });
255 | chain.point.forEach((item) => {
256 | item.point.focusChain(addClass);
257 | });
258 | if (this._focusItem && addClass === 'focus-chain') {
259 | this.unfocusChain(this._focusItem.nodeId, this._focusItem.pointId, addClass);
260 | }
261 | if (addClass === 'focus-chain') {
262 | this._focusItem = {
263 | nodeId: nodeId,
264 | pointId: pointId
265 | }
266 | }
267 | }
268 |
269 | unfocusChain(nodeId, pointId, rmClass) {
270 | let chain = this._findChain(nodeId, pointId);
271 | chain.edges.forEach((item) => {
272 | item.unfocusChain(rmClass);
273 | });
274 | chain.point.forEach((item) => {
275 | item.point.unfocusChain(rmClass);
276 | });
277 | }
278 |
279 | focusItems(nodes = [], edges = []) {
280 | this.emit('custom.item.focus', {
281 | nodes,
282 | edges
283 | });
284 | nodes.forEach((item) => {
285 | item.focus();
286 | });
287 | edges.forEach((item) => {
288 | item.focus();
289 | });
290 | }
291 |
292 | _findChain(nodeId, pointId) {
293 | let resultPoints = [];
294 | let resultEdges = [];
295 | let queue = [{nodeId, pointId}];
296 | while(queue.length > 0) {
297 | let item = queue.pop();
298 | let hasExist = _.some(resultPoints, (point) => {
299 | return item.nodeId === point.id && item.pointId === point.point.id;
300 | });
301 | if (hasExist) {
302 | return;
303 | } else {
304 | let node = this.getNode(item.nodeId);
305 | let point = node.getEndpoint(item.pointId);
306 | resultPoints.push({
307 | node,
308 | point
309 | });
310 | }
311 | let targetEdge = this.getNeighborEdgesByEndpoint(item.nodeId, `${item.pointId}-left`);
312 | let sourceEdge = this.getNeighborEdgesByEndpoint(item.nodeId, `${item.pointId}-right`);
313 | targetEdge.forEach((edge) => {
314 | let node = edge.sourceNode;
315 | let point = node.getEndpoint(_.get(edge, 'sourceEndpoint.options.originId'));
316 | if (!point) {
317 | return;
318 | }
319 | let hasExist = _.some(resultPoints, (item) => {
320 | return item.node.id === edge.sourceNode.id && item.point.id === point.id;
321 | });
322 | if (!hasExist) {
323 | resultEdges.push(edge);
324 | queue.push({
325 | nodeId: edge.sourceNode.id,
326 | pointId: point.id
327 | });
328 | }
329 | });
330 | sourceEdge.forEach((edge) => {
331 | let node = edge.targetNode;
332 | let point = node.getEndpoint(_.get(edge, 'targetEndpoint.options.originId'));
333 | if (!point) {
334 | return;
335 | }
336 | let hasExist = _.some(resultPoints, (item) => {
337 | return item.node.id === edge.targetNode.id && item.point.id === point.id;
338 | });
339 | if (!hasExist) {
340 | resultEdges.push(edge);
341 | queue.push({
342 | nodeId: edge.targetNode.id,
343 | pointId: point.id
344 | });
345 | }
346 | });
347 | }
348 | return {
349 | edges: resultEdges,
350 | point: resultPoints
351 | }
352 | }
353 |
354 | unfocus() {
355 | if (this._focusItem && this._enableFocusChain) {
356 | this.unfocusChain(this._focusItem.nodeId, this._focusItem.pointId, 'focus-chain');
357 | this._focusItem = null;
358 | }
359 | }
360 | };
--------------------------------------------------------------------------------
/src/canvas/collapse-menu.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import $ from 'jquery';
3 | import * as React from 'react';
4 | import * as ReactDOM from 'react-dom';
5 | import ButterflyDataMapping from 'react-data-mapping';
6 | import 'react-data-mapping/dist/index.css';
7 |
8 | const Tips = require('butterfly-dag').Tips;
9 |
10 | let _genTipDom = (result) => {
11 | let dom = $('');
12 | let title = $('');
13 | let content = $('')
14 | let canvas = (
15 |
29 | );
30 | ReactDOM.render(
31 | canvas,
32 | content[0]
33 | );
34 | dom.append(title).append(content);
35 | return dom[0]
36 | }
37 |
38 | let transformData = (currentEdge, originEdgesInfo) => {
39 | let sourceNode = currentEdge.sourceNode;
40 | let targetNode = currentEdge.targetNode;
41 | let columns = (sourceNode.options._columns || []).filter((item) => {
42 | return item.primaryKey;
43 | });
44 | let links = originEdgesInfo.filter((item) => {
45 | return item.sourceNode === sourceNode.id && item.targetNode === targetNode.id;
46 | });
47 | let sourceData = {
48 | title: sourceNode.id,
49 | fields: sourceNode.options.fields
50 | };
51 | let targetData = {
52 | title: targetNode.id,
53 | fields: targetNode.options.fields
54 | };
55 | let mappingData = links.map((item) => {
56 | return {
57 | source: item.source,
58 | target: item.target
59 | }
60 | });
61 | return {
62 | columns,
63 | sourceData,
64 | targetData,
65 | mappingData
66 | }
67 | }
68 |
69 | export default (opts) => {
70 | const {container, pos, edge, originEdgesInfo} = opts;
71 | let result = transformData(edge, originEdgesInfo);
72 | Tips.createMenu({
73 | className: 'visual-modeling-collapse-menu',
74 | targetDom: container,
75 | genTipDom: () => { return _genTipDom(result) },
76 | placement: 'right',
77 | action: null,
78 | x: pos[0],
79 | y: pos[1],
80 | closable: true
81 | }, (dom) => {
82 | setTimeout(() => {
83 | const info = {
84 | top: $(container).offset().top,
85 | left: $(container).offset().left,
86 | width: $(container).outerWidth(),
87 | height: $(container).outerHeight(),
88 | actualWidth: $(dom).outerWidth(),
89 | actualHeight: $(dom).outerHeight()
90 | };
91 | let top = pos[1] - info.actualHeight / 2 - info.height / 2 + 9;
92 | let left = pos[0] + info.width - 12;
93 | $(dom).css({
94 | top: top,
95 | left: left
96 | });
97 | }, 100)
98 | });
99 | }
100 |
--------------------------------------------------------------------------------
/src/canvas/edge.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import {Edge} from 'butterfly-dag';
3 |
4 | import RightMenuGen from './right-menu';
5 |
6 | export default class BaseEdge extends Edge {
7 | mounted() {
8 | // todo 这块可以优化
9 | this._createRightMenu();
10 |
11 | $(this.eventHandlerDom).on('dblclick',(e) => {
12 | e.preventDefault();
13 | e.stopPropagation();
14 | this.emit('custom.edge.dblClick',{
15 | edge: this
16 | });
17 | });
18 | $(this.labelDom).on('dblclick',(e) => {
19 | e.preventDefault();
20 | e.stopPropagation();
21 | this.emit('custom.edge.dblClick',{
22 | edge: this
23 | });
24 | });
25 | }
26 |
27 | draw(obj) {
28 | let path = super.draw(obj);
29 | path.setAttribute('class', 'butterflies-link visual-modeling-link');
30 | return path;
31 | }
32 |
33 | drawArrow(arrow) {
34 | let path = super.drawArrow(arrow);
35 | if (path) {
36 | path.setAttribute('class', 'butterflies-arrow visual-modeling-arrow');
37 | }
38 | return path;
39 | }
40 |
41 | focus() {
42 | $(this.dom).addClass('focus');
43 | $(this.arrowDom).addClass('focus');
44 | $(this.labelDom).addClass('focus');
45 | }
46 |
47 | unfocus() {
48 | $(this.dom).removeClass('focus');
49 | $(this.arrowDom).removeClass('focus');
50 | $(this.labelDom).removeClass('focus');
51 | }
52 |
53 | focusChain(addClass = 'hover-chain') {
54 | $(this.dom).addClass(addClass);
55 | $(this.arrowDom).addClass(addClass);
56 | $(this.labelDom).addClass(addClass);
57 | }
58 |
59 | unfocusChain(rmClass = 'hover-chain') {
60 | $(this.dom).removeClass(rmClass);
61 | $(this.arrowDom).removeClass(rmClass);
62 | $(this.labelDom).removeClass(rmClass);
63 | }
64 |
65 | drawLabel(label) {
66 |
67 | let sourceNode = this.sourceNode;
68 | let targetNode = this.targetNode;
69 | let labelRender = _.get(this, 'options._config.labelRender');
70 |
71 | if (sourceNode.status === 'collapse' && targetNode.status === 'collapse') {
72 | let container = $('...');
73 | container.on('click', this._showCollapseMoldal.bind(this));
74 | return container[0];
75 | }
76 |
77 | let dom = null;
78 | // 存在 labelRender 但是没有 label 的时候,需要 labelRender 拿到这个 dom 去渲染
79 | if(labelRender) {
80 | const span = document.createElement('span');
81 | span.className = 'visual-modeling-label';
82 | span.style.position = 'absolute';
83 | span.style.zIndex = 500;
84 | dom = span;
85 | } else if (label && typeof label === 'string') {
86 | let container = $('');
87 | container.text(label);
88 | dom = container[0];
89 | }
90 |
91 | $(dom).on('click', () => {
92 | this.emit('system.link.click', {
93 | edge: this
94 | });
95 | });
96 |
97 | return dom;
98 | }
99 |
100 | isConnect() {
101 | if (this.sourceNode.id === this.targetNode.id) {
102 | return false;
103 | }
104 | return true;
105 | }
106 |
107 | // 右键菜单
108 | _createRightMenu() {
109 | let menus = _.get(this, 'options._menu', []);
110 | if (menus.length > 0) {
111 | $(this.eventHandlerDom).contextmenu((e) => {
112 | e.preventDefault();
113 | e.stopPropagation();
114 | RightMenuGen(this.dom, 'edge', [e.clientX, e.clientY], menus, this.options);
115 | })
116 | }
117 | }
118 |
119 | _showCollapseMoldal(e) {
120 | e.preventDefault();
121 | e.stopPropagation();
122 | this.emit('custom.edge.showCollapseInfo', {
123 | container: e.target,
124 | pos: [e.clientX, e.clientY],
125 | edge: this
126 | });
127 | }
128 | }
129 |
130 |
--------------------------------------------------------------------------------
/src/canvas/empty.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 |
5 | const isReactEle = (HTMLElement) => {
6 | return React.isValidElement(HTMLElement);
7 | };
8 |
9 | /**
10 | * params {Object} config
11 | * params {JSX.Element | String} config.content
12 | * params {Number | String} config.width
13 | */
14 | export default (config) => {
15 | const content = config.content;
16 | const container = config.container[0];
17 | let width = config.width;
18 |
19 | if (!width) {
20 | width = '150px';
21 | }
22 |
23 | if (typeof config.width === 'number') {
24 | width = config.width + 'px';
25 | }
26 |
27 | let emptyDom = '';
28 |
29 | if (content) {
30 | if (isReactEle(content)) {
31 | emptyDom = ReactDOM.render(content, container);
32 | } else {
33 | emptyDom = $(content);
34 | }
35 | } else {
36 | emptyDom = $('');
37 | const iconDom = $('');
38 |
39 | emptyDom.append(iconDom);
40 | }
41 |
42 | return emptyDom;
43 | };
44 |
--------------------------------------------------------------------------------
/src/canvas/endpoint.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import {Endpoint} from 'butterfly-dag';
3 |
4 | class NewEndPoint extends Endpoint {
5 | constructor(opts) {
6 | super(opts);
7 | }
8 | attachEvent() {
9 | $(this.dom).on('mousedown', (e) => {
10 | const LEFT_KEY = 0;
11 | if (e.button !== LEFT_KEY) {
12 | return;
13 | }
14 | e.preventDefault();
15 | e.stopPropagation();
16 | if (this.options._isNodeSelf) {
17 | this.emit('custom.endpoint.dragNode', {
18 | data: this
19 | });
20 | }
21 | this.emit('custom.endpoint.focus', {
22 | point: this
23 | });
24 | });
25 |
26 | // todo: 高亮整条链路
27 | if (this.options._isNodeSelf) {
28 | $(this.dom).on('mouseover', (e) => {
29 | this.emit('custom.endpoint.hover', {
30 | point: this
31 | });
32 | });
33 |
34 | $(this.dom).on('mouseout', (e) => {
35 | this.emit('custom.endpoint.unHover', {
36 | point: this
37 | });
38 | });
39 | }
40 | }
41 | focusChain(addClass) {
42 | $(this.dom).addClass(addClass);
43 | $(this.arrowDom).addClass(addClass);
44 | }
45 | unfocusChain(rmClass) {
46 | $(this.dom).removeClass(rmClass);
47 | $(this.arrowDom).removeClass(rmClass);
48 | }
49 | }
50 |
51 | export default NewEndPoint;
52 |
--------------------------------------------------------------------------------
/src/canvas/node.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import * as _ from 'lodash';
3 | import { Node } from 'butterfly-dag';
4 | import * as ReactDOM from 'react-dom';
5 |
6 | import emptyDom from './empty';
7 | import Endpoint from './endpoint';
8 | import RightMenuGen from './right-menu';
9 |
10 | export default class TableNode extends Node {
11 | constructor(opts) {
12 | super(opts);
13 |
14 | this.fieldsList = [];
15 |
16 | // 每列宽度
17 | this.COLUMN_WIDTH = 60;
18 | this.status = 'expand';
19 | this.emptyDataTree = undefined;
20 | }
21 |
22 | draw(obj) {
23 | let _dom = obj.dom;
24 | if (!_dom) {
25 | _dom = $('')
26 | .attr('class', 'node table-node')
27 | .attr('id', obj.name);
28 | }
29 | const node = $(_dom);
30 | // 计算节点坐标
31 | if (obj.top !== undefined) {
32 | node.css('top', `${obj.top}px`);
33 | }
34 | if (obj.left !== undefined) {
35 | node.css('left', `${obj.left}px`);
36 | }
37 |
38 | this._createTableTitle(node);
39 | this._createExtIcon(node);
40 | this._createFields(node);
41 | return node[0];
42 | }
43 |
44 | mounted() {
45 | // 生成field的endpoint
46 | this._createNodeEndpoint();
47 |
48 | if (this.fieldsList.length === 0) {
49 | $(this.dom).find('.title').css('width', this.options._emptyWidth || 150);
50 | }
51 |
52 | $(this.dom).on('dblclick', (e) => {
53 | this.emit('system.node.dblClick', {
54 | node: this
55 | });
56 | });
57 | // 生成右键菜单
58 | this._createRightMenu();
59 | }
60 |
61 | focus() {
62 | $(this.dom).addClass('focus');
63 | this.options.minimapActive = true;
64 | }
65 |
66 | unfocus() {
67 | $(this.dom).removeClass('focus');
68 | this.options.minimapActive = false;
69 | }
70 |
71 | _expand() {
72 | if (this.status === 'expand') {
73 | console.warn(`节点${this.id}已经是展开状态`)
74 | return;
75 | }
76 |
77 | // 清除新锚点
78 | this._rmTitleEndpoint();
79 |
80 | // 隐藏字段
81 | this.fieldsList.forEach((item) => {
82 | $(item.dom).css('display', 'flex');
83 | let points = [
84 | this.getEndpoint(item.id),
85 | this.getEndpoint(`${item.id}-left`),
86 | this.getEndpoint(`${item.id}-right`),
87 | ]
88 |
89 | points.forEach((item) => {
90 | item.updatePos();
91 | });
92 | });
93 |
94 | // 记录状态
95 | this.status = 'expand';
96 | // 改变伸缩状态
97 | $(this.dom).removeClass('collapse');
98 | }
99 |
100 | _collapse(oldEdges) {
101 | if (this.status === 'collapse') {
102 | console.warn(`节点${this.id}已经是收缩状态`)
103 | return;
104 | }
105 | // 生成新锚点
106 | this._createTitleEndpoint();
107 | // 隐藏字段
108 | this.fieldsList.forEach((item) => {
109 | $(item.dom).css('display', 'none');
110 | });
111 | // 记录状态
112 | this.status = 'collapse';
113 | // 改变伸缩状态
114 | $(this.dom).addClass('collapse');
115 |
116 | // 生成新线段,并去重
117 | let newEdges = [];
118 | oldEdges.forEach((item) => {
119 | let updateObj = {};
120 | if (item.sourceNode === this.id) {
121 | updateObj['source'] = `${this.id}-right`;
122 | } else if (item.targeNode = this.id) {
123 | updateObj['target'] = `${this.id}-left`;
124 | }
125 |
126 | let newEdgeObj = _.assign({}, item, updateObj);
127 |
128 | let hasExist = _.some(newEdges, (item) => {
129 | return (
130 | newEdgeObj.sourceNode === item.sourceNode &&
131 | newEdgeObj.source === item.source &&
132 | newEdgeObj.targetNode === item.targetNode &&
133 | newEdgeObj.target === item.target
134 | )
135 | });
136 | if (!hasExist) {
137 | newEdges.push(newEdgeObj);
138 | }
139 | });
140 |
141 | return newEdges;
142 | }
143 | _createTableTitle(container = this.dom) {
144 | let title = _.get(this, 'options.title');
145 | let titleRender = _.get(this, 'options._config.titleRender');
146 | let titleDom = $(``);
147 | $(container).append(titleDom);
148 | if (title) {
149 | if (titleRender) {
150 | let titleTextDom = $(``);
151 | $(titleDom).append(titleTextDom);
152 | ReactDOM.render(
153 | titleRender(title, this.options),
154 | titleTextDom[0],
155 | () => {
156 | this._updateEndpointPos();
157 | }
158 | );
159 | } else {
160 | let titleTextDom = $(`${title}
`);
161 | $(titleDom).append(titleTextDom);
162 | }
163 | }
164 | }
165 | _createExtIcon(container = this.dom) {
166 | let titleDom = $(container).find('.title');
167 | let titleIcon = $('');
168 | // 展开收缩icon
169 | let collapseIcon = $('');
170 | collapseIcon.on('click', (e) => {
171 | if (this.status === 'collapse') {
172 | this.emit('custom.node.expand', {
173 | nodeId: this.id
174 | });
175 | } else {
176 | this.emit('custom.node.collapse', {
177 | nodeId: this.id
178 | });
179 | }
180 | });
181 | titleIcon.append(collapseIcon);
182 | // 删除icon
183 | let deleteIcon = $('');
184 | deleteIcon.on('click', (e) => {
185 | this.emit('custom.node.delete', {
186 | node: this
187 | });
188 | });
189 | titleIcon.append(deleteIcon);
190 | let extIcon = $('');
191 | let titleIconRender = _.get(this, 'options._config.titleExtIconRender');
192 | if (titleIconRender) {
193 | titleIcon.prepend(extIcon);
194 | ReactDOM.render(
195 | titleIconRender(this.options),
196 | extIcon[0]
197 | );
198 | }
199 | titleDom.append(titleIcon);
200 | }
201 | _createFields(container = this.dom, fieldList) {
202 | let fields = fieldList || _.get(this, 'options.fields');
203 | let coloums = _.get(this, 'options._columns', []);
204 | let _primaryKey = _.get(coloums, '[0].key');
205 |
206 | if (fields && fields.length) {
207 | return fields.map((_field) => {
208 | let fieldDom = $('');
209 | coloums.forEach((_col) => {
210 | if (_col.render) {
211 | let fieldItemDom = $(``);
212 | fieldItemDom.css('width', (_col.width || this.COLUMN_WIDTH) + 'px').attr('dataType', _col.key);
213 | fieldDom.append(fieldItemDom);
214 | ReactDOM.render(
215 | _col.render(_field[_col.key], _field),
216 | fieldItemDom[0]
217 | );
218 | } else {
219 | let fieldItemDom = $(`${_field[_col.key]}`);
220 | fieldItemDom.css('width', (_col.width || this.COLUMN_WIDTH) + 'px').attr('dataType', _col.key);
221 | fieldDom.append(fieldItemDom);
222 | }
223 | if (_col.primaryKey) {
224 | _primaryKey = _col.key;
225 | }
226 | });
227 | let leftPoint = $('');
228 | let rightPoint = $('');
229 | fieldDom.append(leftPoint).append(rightPoint);
230 | container.append(fieldDom);
231 | let _newFieldItem = {
232 | id: _field[_primaryKey],
233 | dom: fieldDom
234 | };
235 | this.fieldsList.push(_newFieldItem);
236 | return _newFieldItem;
237 | });
238 | } else {
239 | if(this.emptyDataTree){
240 | return this.emptyDataTree;
241 | }
242 | const _emptyContent = _.get(this.options, '_emptyContent');
243 | const noDataCon = $('');
244 | container.append(noDataCon);
245 | const noDataTree = emptyDom({
246 | content: _emptyContent,
247 | container: noDataCon,
248 | width: this.options._emptyWidth
249 | });
250 | container.append(noDataTree);
251 | const _newFieldItem = {
252 | id: 0,
253 | __type: 'no-data',
254 | dom: noDataTree
255 | };
256 | this.emptyDataTree = [_newFieldItem];
257 | return [_newFieldItem];
258 | }
259 | }
260 | _createNodeEndpoint(fieldList) {
261 | let _fieldList = (fieldList || this.fieldsList);
262 | _fieldList.forEach((item) => {
263 | this.addEndpoint({
264 | id: item.id,
265 | type: 'onlyConnect',
266 | _isNodeSelf: true,
267 | dom: item.dom[0],
268 | Class: Endpoint
269 | });
270 | this.addEndpoint({
271 | id: item.id + '-right',
272 | originId: item.id,
273 | orientation: [1,0],
274 | type: 'source',
275 | _isNodeSelf: false,
276 | dom: $(item.dom).find('.right-point')[0],
277 | Class: Endpoint,
278 | linkable: false,
279 | disLinkable: false
280 | });
281 | this.addEndpoint({
282 | id: item.id + '-left',
283 | originId: item.id,
284 | orientation: [-1,0],
285 | type: 'target',
286 | _isNodeSelf: false,
287 | dom: $(item.dom).find('.left-point')[0],
288 | Class: Endpoint,
289 | linkable: false,
290 | disLinkable: false
291 | });
292 | });
293 | }
294 | _rmNodeEndpoint(ids) {
295 | ids.forEach((id) => {
296 | this.removeEndpoint(id);
297 | })
298 | }
299 | _updateTitle(newTitle) {
300 | if (newTitle !== this.options.title) {
301 | this.options.title = newTitle;
302 | let titleTextDom = $(this.dom).find('.title-text');
303 | let titleRender = _.get(this, 'options._config.titleRender');
304 | if (titleRender) {
305 | ReactDOM.unmountComponentAtNode(titleTextDom[0]);
306 | ReactDOM.render(
307 | titleRender(newTitle),
308 | titleTextDom[0],
309 | () => {
310 | this._updateEndpointPos();
311 | }
312 | );
313 | } else {
314 | titleTextDom.text(newTitle);
315 | }
316 | }
317 | }
318 |
319 | _addFields(fields) {
320 | $(this.dom).find('.no-data').remove();
321 | this.emptyDataTree = undefined;
322 | let _newFieldsList = this._createFields($(this.dom), fields);
323 | if (_newFieldsList.length >= 1 && _.get(_newFieldsList, ['0', '__type']) !== 'no-data') {
324 | this._createNodeEndpoint(_newFieldsList);
325 | }
326 | }
327 |
328 | _rmFields(fields = this.fieldsList) {
329 | // 寻找primaryKey
330 | let columns = _.get(this, 'options._columns', []);
331 | let _primaryKey = columns[0].key;
332 | columns.forEach((col) => {
333 | if (col.primaryKey) {
334 | _primaryKey = col.key;
335 | }
336 | });
337 | // 删除field
338 | let ids = [];
339 | fields.forEach((field) => {
340 | ids = ids.concat([field.id, `${field.id}-left`, `${field.id}-right`]);
341 | });
342 | ids.forEach((id) => {
343 | this.removeEndpoint(id);
344 | this.fieldsList = this.fieldsList.filter((item) => {
345 | return id !== item.id;
346 | });
347 | this.options.fields = (this.options.fields || []).filter((item) => {
348 | return id !== item[_primaryKey];
349 | });
350 | });
351 | }
352 |
353 | _updateFields(fields) {
354 | let columns = _.get(this, 'options._columns', []);
355 | let _primaryKey = _.find(columns, (item) => {
356 | return item.primaryKey;
357 | }).key;
358 | fields.forEach((newField) => {
359 | let oldFieldInfo = _.find(this.options.fields || [], (item) => {
360 | return newField[_primaryKey] === item[_primaryKey];
361 | });
362 | let oldFieldItem = _.find(this.fieldsList || [], (item) => {
363 | return newField[_primaryKey] === item.id;
364 | });
365 | if (!oldFieldInfo || !oldFieldItem) {
366 | return;
367 | }
368 | columns.forEach((col) => {
369 | let targetDom = $(oldFieldItem.dom).find(`[dataType="${col.key}"]`);
370 | if (col.render) {
371 | ReactDOM.unmountComponentAtNode(targetDom[0]);
372 | ReactDOM.render(
373 | col.render(newField[col.key], newField),
374 | targetDom[0]
375 | );
376 | } else {
377 | $(targetDom).text(newField[col.key]);
378 | }
379 | })
380 | });
381 | }
382 |
383 | // 更新col
384 | _updateCol(newCol) {
385 | let fields = this.fieldsList;
386 | let ids = [];
387 | fields.forEach((field) => {
388 | ids = ids.concat([field.id, `${field.id}-left`, `${field.id}-right`]);
389 | });
390 | ids.forEach((id) => {
391 | this.removeEndpoint(id);
392 | });
393 | this.fieldsList = [];
394 | _.set(this, 'options._columns', newCol);
395 | this._addFields();
396 |
397 | }
398 |
399 | _createTitleEndpoint() {
400 | let titleDom = $(this.dom).find('.title');
401 | let leftPoint = $('');
402 | let rightPoint = $('');
403 | titleDom.append(leftPoint).append(rightPoint);
404 |
405 | this.addEndpoint({
406 | id: this.id + '-right',
407 | orientation: [1,0],
408 | type: 'source',
409 | _isNodeSelf: false,
410 | dom: rightPoint[0],
411 | Class: Endpoint,
412 | linkable: false,
413 | disLinkable: false
414 | });
415 | this.addEndpoint({
416 | id: this.id + '-left',
417 | orientation: [-1,0],
418 | type: 'target',
419 | _isNodeSelf: false,
420 | dom: leftPoint[0],
421 | Class: Endpoint,
422 | linkable: false,
423 | disLinkable: false
424 | });
425 | }
426 |
427 | _rmTitleEndpoint() {
428 | this.removeEndpoint(`${this.id}-left`);
429 | this.removeEndpoint(`${this.id}-right`);
430 | }
431 |
432 | // 右键菜单
433 | _createRightMenu() {
434 | let menus = _.get(this, 'options._menu', []);
435 | if (menus.length > 0) {
436 | $(this.dom).contextmenu((e) => {
437 | e.preventDefault();
438 | e.stopPropagation();
439 | RightMenuGen(this.dom, 'node', [e.clientX, e.clientY], menus, this.options);
440 | })
441 | }
442 | }
443 |
444 | _updateEndpointPos() {
445 | (this.endpoints || []).forEach((item) => {
446 | item.updatePos();
447 | });
448 | }
449 | };
--------------------------------------------------------------------------------
/src/canvas/right-menu.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import * as ReactDOM from 'react-dom';
3 |
4 | const Tips = require('butterfly-dag').Tips;
5 |
6 | let _genTipDom = (menuData, data) => {
7 | let dom = $('');
8 | menuData.forEach((item) => {
9 | let menuItem = $('');
10 | if (item.onClick) {
11 | menuItem.on('click', (e) => {
12 | item.onClick(item.key, data);
13 | if (item.closable) {
14 | Tips.closeMenu();
15 | }
16 | });
17 | }
18 | dom.append(menuItem);
19 | if (item.render) {
20 | ReactDOM.render(
21 | item.render(item.key, data),
22 | menuItem[0]
23 | );
24 | } else {
25 | menuItem.text(item.title || item.key);
26 | }
27 | });
28 | return dom[0]
29 | }
30 |
31 | export default (container, type, pos, menuData, data) => {
32 | Tips.createMenu({
33 | className: `butterfly-${type}-menu`,
34 | targetDom: container,
35 | genTipDom: () => { return _genTipDom(menuData, data) },
36 | placement: 'right',
37 | action: null,
38 | x: pos[0],
39 | y: pos[1],
40 | closable: true
41 | });
42 | }
--------------------------------------------------------------------------------
/src/component/action-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as _ from 'lodash';
2 | import * as React from 'react';
3 |
4 | import {actions} from '../config';
5 | import Tooltip from './tooltip/index';
6 |
7 | export interface action {
8 | key: string, // 唯一表示
9 | icon: string | JSX.Element, // 图标
10 | title?: string; // 提示
11 | onClick: (canvas: any) => void; // 点击响应函数
12 | disable: boolean; // 是否禁用
13 | }
14 |
15 | interface IProps {
16 | canvas: any; // 画布实例
17 | actionMenu: action[]; // action菜单
18 | visible: boolean; // 是否可见
19 | }
20 |
21 | const ActionMenu = (props: IProps) => {
22 | let {canvas, actionMenu = [], visible} = props;
23 |
24 | if(!visible) {
25 | return null;
26 | }
27 |
28 | if(!Array.isArray(actionMenu)) {
29 | actionMenu = [];
30 | }
31 |
32 | // 合并action菜单
33 | let sysActions = _.cloneDeep(actions);
34 | const allActions = [];
35 |
36 | for(let action of actionMenu) {
37 | const sysAction = _.find(sysActions, (a) => {
38 | return a.key === action.key
39 | });
40 |
41 | if(!sysAction) {
42 | allActions.push(action);
43 |
44 | continue;
45 | }
46 |
47 | // 合并用户同名 key
48 | _.merge(sysAction, action);
49 | allActions.push(sysAction);
50 |
51 | // 移除用户覆盖的 action
52 | sysActions = sysActions.filter(action => action.key !== sysAction.key);
53 | }
54 |
55 | sysActions.forEach(sysAction => {
56 | allActions.unshift(sysAction);
57 | });
58 |
59 | // 兼容多类型图标渲染
60 | const renderIcon = (icon) => {
61 | if(typeof icon === 'string') {
62 | return
63 | }
64 |
65 | if(React.isValidElement(icon)) {
66 | return icon;
67 | }
68 |
69 | return null;
70 | }
71 |
72 | return (
73 |
74 | {
75 | allActions.map(action => {
76 | if(action.disable) {
77 | return null;
78 | }
79 |
80 | return (
81 |
action.onClick(canvas)}
85 | >
86 |
87 | {renderIcon(action.icon)}
88 |
89 |
90 | )
91 | })
92 | }
93 |
94 | );
95 | }
96 |
97 | export default ActionMenu;
98 |
--------------------------------------------------------------------------------
/src/component/edge-render.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 |
4 | interface IProps {
5 | canvas: any;
6 | labelRender: (label: any, edge: any) => JSX.Element;
7 | }
8 |
9 | const EdgeRender = (props: IProps) => {
10 | const {canvas, labelRender} = props;
11 |
12 | if (!canvas) {
13 | return null;
14 | }
15 |
16 | if (!canvas.edges) {
17 | return null;
18 | }
19 |
20 | return canvas.edges.map(edge => {
21 | if(!edge || !edge.labelDom) {
22 | return null;
23 | }
24 |
25 | const {sourceNode, targetNode} = edge;
26 | const isCollapse = sourceNode.status === 'collapse' && targetNode.status === 'collapse'
27 |
28 | if(isCollapse) {
29 | return null;
30 | }
31 |
32 | return (
33 | ReactDOM.createPortal(
34 | labelRender(edge.label, edge),
35 | edge.labelDom
36 | )
37 | )
38 | })
39 | }
40 |
41 | export default EdgeRender;
42 |
--------------------------------------------------------------------------------
/src/component/tooltip/index.less:
--------------------------------------------------------------------------------
1 | .react-visual-modeling-tooltip {
2 | position: relative;
3 | display: block;
4 | height: 100%;
5 | width: 100%;
6 |
7 | &:hover {
8 | .react-visual-modeling-title {
9 | display: block;
10 | }
11 | }
12 |
13 | .react-visual-modeling-title {
14 | display: none;
15 |
16 | word-wrap: break-word;
17 | background-color: rgba(0,0,0,.75);
18 | border-radius: 2px;
19 | box-shadow: 0 3px 6px -4px rgba(0,0,0,12%), 0 6px 16px 0 rgba(0,0,0,8%), 0 9px 28px 8px rgba(0,0,0,5%);
20 | color: #fff;
21 | min-height: 32px;
22 | min-width: 30px;
23 | padding: 0 8px;
24 | line-height: 32px;
25 | text-align: left;
26 | text-decoration: none;
27 | position: absolute;
28 | right: calc(100% + 5px);
29 | white-space: nowrap;
30 | top: 50%;
31 | height: 32px;
32 | transform: translateY(-50%);
33 |
34 | &::before {
35 | background-color: rgba(0, 0, 0, 0.75);
36 | content: " ";
37 | display: block;
38 | height: 5px;
39 | margin: auto;
40 | pointer-events: auto;
41 | position: absolute;
42 | width: 5px;
43 | display: block;
44 | position: absolute;
45 | right: -2px;
46 | top: 50%;
47 | box-shadow: 3px 3px 7px rgba(0,0,0,7%);
48 | transform: translateY(-50%) rotate(45deg);
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/component/tooltip/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import './index.less';
4 |
5 | interface IProps {
6 | children: JSX.Element,
7 | title: JSX.Element
8 | };
9 |
10 | const Tooltip = (props: IProps) => {
11 | const {title, children} = props;
12 |
13 | if(!title) {
14 | return children;
15 | }
16 |
17 | return (
18 |
19 | {title}
20 | {children}
21 |
22 | )
23 | }
24 |
25 | export default Tooltip;
26 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 组件各项配置
3 | * @author gorgear
4 | */
5 | import Edge from './canvas/edge';
6 |
7 | // butterfly 默认配置
8 | export const bfCfg = {
9 | disLinkable: false,
10 | linkable: true,
11 | draggable: true,
12 | zoomable: true,
13 | moveable: true,
14 | theme: {
15 | edge: {
16 | shapeType: 'AdvancedBezier',
17 | arrow: true,
18 | isExpandWidth: true,
19 | arrowPosition: 1,
20 | arrowOffset: 5,
21 | Class: Edge
22 | },
23 | endpoint: {
24 | expandArea: {
25 | left: 0,
26 | right: 0,
27 | top: 0,
28 | botton: 0
29 | }
30 | },
31 | autoFixCanvas: {
32 | enable: true,
33 | autoMovePadding: [20, 20, 20, 20]
34 | },
35 | }
36 | };
37 |
38 | export const actions = [
39 | {
40 | key: 'zoom-in',
41 | icon: 'table-build-icon table-build-icon-zoom-in',
42 | title: '放大',
43 | onClick: (canvas) => {
44 | canvas.zoom(canvas._zoomData + 0.1);
45 | }
46 | },
47 | {
48 | key: 'zoom-out',
49 | icon: 'table-build-icon table-build-icon-zoom-out',
50 | title: '缩小',
51 | onClick: (canvas) => {
52 | canvas.zoom(canvas._zoomData - 0.1);
53 | }
54 | },
55 | {
56 | key: 'fit',
57 | icon: 'table-build-icon table-build-icon-quanping2',
58 | title: '居中',
59 | onClick: (canvas) => {
60 | canvas.focusCenterWithAnimate(undefined, () => {
61 | console.log('complete!!!')
62 | });
63 | }
64 | }
65 | ];
66 |
--------------------------------------------------------------------------------
/src/index.less:
--------------------------------------------------------------------------------
1 | @import './static/iconfont.css';
2 |
3 | .butterfly-table-building {
4 | background: #2E2E2E;
5 | outline: none;
6 | box-shadow: none;
7 | user-select: none;
8 | .table-node {
9 | position: absolute;
10 | border: 1px solid #5A5A5A;
11 | border-radius: 4px;
12 | color: #fff;
13 | background: #252525;
14 | &.collapse {
15 | .title {
16 | border-bottom: none;
17 | }
18 | }
19 | .title {
20 | position: relative;
21 | border-bottom: 1px solid #5A5A5A;
22 | padding: 0 50px 0 15px;
23 | height: 30px;
24 | line-height: 30px;
25 | min-width: 150px;
26 | cursor: pointer;
27 | .title-text {
28 | overflow: hidden;
29 | text-overflow: ellipsis;
30 | white-space: nowrap;
31 | }
32 | .point {
33 | position: absolute;
34 | border-radius: 50%;
35 | visibility: hidden;
36 | &.show {
37 | visibility: visible;
38 | }
39 | &.left-point {
40 | top: 50%;
41 | left: 0px;
42 | }
43 | &.right-point{
44 | top: 50%;
45 | right: 0px;
46 | }
47 | }
48 | .title-icon-con {
49 | position: absolute;
50 | display: none;
51 | top: 0;
52 | right: 15px;
53 | color: #484848;
54 | i {
55 | padding-left: 3px;
56 | &:hover {
57 | color: rgba(255, 255, 255, 0.5);
58 | }
59 | }
60 | .table-build-icon-xiala{
61 | display: inline-block;
62 | }
63 | }
64 | }
65 |
66 | .field {
67 | position: relative;
68 | cursor: move;
69 | height: 24px;
70 | line-height: 24px;
71 | align-items: center;
72 | display: flex;
73 | &:hover {
74 | background: rgba(216, 216, 216, 0.06);
75 | .point {
76 | visibility: visible;
77 | }
78 | }
79 | &.hover-chain {
80 | background: rgba(246, 106, 2, 0.3);
81 | }
82 | &.focus-chain {
83 | background: #F66902;
84 | }
85 | .field-item {
86 | overflow: hidden;
87 | white-space: nowrap;
88 | display: inline-block;
89 | text-align: center;
90 | }
91 | .point {
92 | position: absolute;
93 | border-radius: 50%;
94 | visibility: hidden;
95 | &.show {
96 | visibility: visible;
97 | }
98 | &.left-point {
99 | top: 50%;
100 | left: 0px;
101 | }
102 | &.right-point{
103 | top: 50%;
104 | right: 0px;
105 | }
106 | }
107 | }
108 | &.focus {
109 | border: 1px solid #F66902;
110 | box-shadow: 0px 0px 5px #f66902;
111 | }
112 | &:hover {
113 | .title-icon-con {
114 | display: inline-block;
115 | }
116 | }
117 | .no-data {
118 | margin: 10px 0;
119 | color: #474747;
120 | text-align: center;
121 | .no-data-icon {
122 | font-size: 36px;
123 | }
124 | }
125 | }
126 | .visual-modeling-link{
127 | &.focus {
128 | stroke: #F66902;
129 | }
130 | &.hover-chain {
131 | stroke: rgba(246, 106, 2, 0.3);
132 | }
133 | &.focus-chain {
134 | stroke: #F66902;
135 | }
136 | }
137 | .visual-modeling-label {
138 | transform: scale(0.8);
139 | background: #313131;
140 | border: 1px solid #828282;
141 | padding: 1px 6px;
142 | border-radius: 13px;
143 | color: #fff;
144 | &.focus {
145 | border: 1px solid #F66902;
146 | }
147 | &.hover-chain {
148 | background: rgba(246, 106, 2, 0.3);
149 | }
150 | &.focus-chain {
151 | background: #F66902;
152 | }
153 | &.butterflies-collapse-label{
154 | padding: 0px 10px;
155 | height: 16px;
156 | line-height: 8px;
157 | font-weight: 900;
158 | cursor: pointer;
159 | background: #252525;
160 | &:hover {
161 | background: #383838;
162 | }
163 | }
164 | }
165 | .visual-modeling-arrow {
166 | &.focus {
167 | stroke: #F66902;
168 | }
169 | &.hover-chain {
170 | stroke: rgba(246, 106, 2, 0.3);
171 | }
172 | &.focus-chain {
173 | stroke: #F66902;
174 | }
175 | }
176 | .butterflies-link-event-handler {
177 | &:hover {
178 | cursor: pointer;
179 | }
180 | }
181 | .table-build-canvas-action {
182 | background: #333;
183 | box-shadow: 0 0 9px 0 rgba(0, 0, 0, .5);
184 | position: absolute;
185 | right: 10px;
186 | top: 10px;
187 | z-index: 999;
188 | border: 1px solid #676565;
189 | div {
190 | height: 24px;
191 | width: 24px;
192 | text-align: center;
193 | line-height: 24px;
194 | cursor: pointer;
195 | color: #fff;
196 | opacity: .7;
197 | border-bottom: 1px solid #676565;
198 | i {
199 | -webkit-text-stroke-width: 0;
200 | font-size: 14px;
201 | }
202 | }
203 | div:hover{
204 | background: #0f0f0f;
205 | }
206 | div:last-child {
207 | border-bottom: none;
208 | }
209 | }
210 | }
211 | .visual-modeling-collapse-menu{
212 | width: 275px;
213 | .butterfly-tooltip-inner {
214 | max-width: 500px!important;
215 | text-align: left!important;
216 | .menu-title {
217 | margin-bottom: 0;
218 | padding: 3px 0;
219 | border-bottom: 1px solid #5A5A5A;
220 | }
221 | .menu-container {
222 | width: 250px;
223 | .butterfly-data-mapping {
224 | margin-top: 10px;
225 | .butterfly-svg {
226 | position: absolute;
227 | }
228 | }
229 | }
230 | }
231 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 |
4 | import $ from 'jquery';
5 | import * as _ from 'lodash';
6 |
7 | import {bfCfg} from './config';
8 | import {Arrow} from 'butterfly-dag';
9 | import Canvas from './canvas/canvas';
10 | import EdgeRender from './component/edge-render'
11 | import ActionMenu, {action} from './component/action-menu';
12 | import {transformInitData, diffPropsData} from './adaptor';
13 |
14 | import 'butterfly-dag/dist/index.css';
15 |
16 | import './index.less';
17 |
18 | // 跟antd的table的column的概念类似
19 | interface columns {
20 | title?: string,
21 | key: string,
22 | width?: number,
23 | primaryKey: boolean,
24 | render?(value: any, rowData: any): void
25 | }
26 |
27 | interface config {
28 | showActionIcon?: boolean, // 是否展示操作icon:放大,缩小,聚焦
29 | allowKeyboard?: boolean, // 允许键盘删除事件,todo以后支持shift多选
30 | collapse: {
31 | enable: boolean, // todo: 允许节点收缩
32 | defaultMode: string, // todo: 默认以哪种形式展示
33 | status: boolean, // 是否节点收缩
34 | showCollapseDetail: boolean // 展示收缩edge的详情
35 | },
36 | enableHoverChain: boolean,
37 | enableFoucsChain: boolean,
38 | titleRender?(title: JSX.Element): void, // 节点title的渲染方法
39 | titleExtIconRender?(node: JSX.Element): void, // 节点右侧按钮的渲染方法
40 | labelRender?(label: JSX.Element): void, // 线段label的渲染方法
41 | autoLayout: {
42 | enable: boolean, // 是否开启自动布局
43 | isAlways: boolean, // 是否添加节点后就重新布局
44 | type: string, // 算法类型
45 | config: any // 算法配置
46 | },
47 | minimap: { // 是否开启缩略图
48 | enable: boolean,
49 | config: {
50 | nodeColor: any
51 | }
52 | },
53 | gridMode: {
54 | isAdsorb: boolean,
55 | theme: {
56 | shapeType: string, // 展示的类型,支持line & circle
57 | gap: number, // 网格间隙
58 | lineWidth: 1, // 线段粗细
59 | lineColor: string, // 线段颜色
60 | circleRadiu: number, // 圆点半径
61 | circleColor: string // 圆点颜色
62 | }
63 | },
64 | butterfly: any; // 小蝴蝶的画布配置,参考:https://github.com/alibaba/butterfly/blob/dev/v4/docs/zh-CN/canvas.md
65 | }
66 |
67 | // 右键菜单配置
68 | interface menu {
69 | title?: string,
70 | key: string,
71 | render?(key: string): void,
72 | onClick?(node: any): void,
73 | }
74 |
75 | interface ComProps {
76 | width?: number | string, // 组件宽
77 | height?: number | string, // 组件高
78 | className?: string, // 组件classname
79 | columns: columns[], // 跟antd的table的column的概念类似
80 | nodeMenu: menu[], // 节点右键菜单配置
81 | edgeMenu: menu[], // 线段右键菜单配置
82 | actionMenu: action[], // action菜单
83 | config: config, // 如上述配置
84 | data: any, // 数据
85 | emptyWidth?: number | string, // 空数据时默认标题宽度
86 | emptyContent?: string | JSX.Element, // 空数据显示内容
87 | selectable: boolean; // 开启框选模式
88 | beforeDeleteNode: Promise | boolean, // 删除节点前方法,可做二次删除确认
89 | beforeDeleteEdge: Promise | boolean, // 删除线段前方法,可做二次删除确认
90 | onLoaded(canvas: any, utils: any): void, // 渲染完毕事件
91 | onChange(data: any): void, // 图内数据变化事件
92 | onFocusNode(node: any): void, // 聚焦节点事件
93 | onFocusEdge(edge: any): void, // 聚焦线段事件
94 | onFocusCanvas(): void, // 聚焦空白处事件
95 | onDblClickNode?(node: any): void, // 双击节点事件
96 | onDblClickEdge?(edge: any): void, // 双击线段事件
97 | onSelect(nodes: any, edges: any): void, // 选中事件
98 |
99 | // TODO: 展开/收缩节点
100 | // onDeleteNodes(nodeInfo: any): void,
101 | // onDeleteEdges(edgeInfo: any): void,
102 | // onConnectEdges(edgeInfo: any): void,
103 | // onReConnectEdges(addEdgeInfo: any, rmEdgeInfo: any): void,
104 | };
105 |
106 | const noop = () => null;
107 |
108 | export default class TableBuilding extends React.Component {
109 | protected canvas: any;
110 | protected canvasData: any;
111 | private _focusNodes: any;
112 | private _columns: any;
113 | private _focusLinks: any;
114 | private _enableHoverChain: any;
115 | private _enableFocusChain: any;
116 | private root: any;
117 | props: any;
118 |
119 | constructor(props: ComProps) {
120 | super(props);
121 | this.canvas = null;
122 | this.canvasData = null;
123 | this.root = null;
124 |
125 | this._focusNodes = [];
126 | this._focusLinks = [];
127 |
128 | this._enableHoverChain = _.get(props, 'config.enableHoverChain', true);
129 | this._enableFocusChain = _.get(props, 'config.enableFocusChain', false);
130 | }
131 |
132 | componentDidMount() {
133 | const {beforeLoad = noop, config = {}} = this.props;
134 |
135 | let root = ReactDOM.findDOMNode(this) as HTMLElement;
136 | this.root = root;
137 |
138 | if (this.props.width !== undefined) {
139 | root.style.width = (this.props.width || 500) + 'px';
140 | }
141 |
142 | if (this.props.height !== undefined) {
143 | root.style.height = (this.props.height || 500) + 'px';
144 | }
145 |
146 | let result = transformInitData({
147 | columns: this.props.columns,
148 | config: this.props.config,
149 | nodeMenu: this.props.nodeMenu,
150 | edgeMenu: this.props.edgeMenu,
151 | data: _.cloneDeep(this.props.data),
152 | emptyContent: this.props.emptyContent,
153 | emptyWidth: this.props.emptyWidth
154 | });
155 |
156 | this.canvasData = result;
157 | this.canvas = new Canvas(
158 | _.merge(
159 | {},
160 | // 默认配置
161 | bfCfg,
162 | // 用户配置
163 | (config.butterfly || {}),
164 | // 固定配置
165 | {
166 | root,
167 | data: {
168 | enableHoverChain: this._enableHoverChain,
169 | enableFocusChain: this._enableFocusChain,
170 | showCollapseDetail: _.get(this.props, 'config.collapse.showCollapseDetail', false)
171 | }
172 | }
173 | )
174 | );
175 |
176 | beforeLoad({
177 | canvas: this.canvas,
178 | Arrow
179 | });
180 |
181 | this.canvas.draw(result, () => {
182 | this.props.onLoaded && this.props.onLoaded(this.canvas);
183 | let minimap = _.get(this, 'props.config.minimap', {});
184 |
185 | if (_.get(this, 'props.config.allowKeyboard')) {
186 | $(root).attr('tabindex', 0).focus();
187 | root.addEventListener('keydown', this._deleteFocusItem.bind(this));
188 | }
189 |
190 | const minimapCfg = _.assign({}, minimap.config, {
191 | events: [
192 | 'system.node.click',
193 | 'system.canvas.click'
194 | ]
195 | });
196 |
197 | if (minimap && minimap.enable) {
198 | this.canvas.setMinimap(true, minimapCfg);
199 | }
200 |
201 | if (_.get(this, 'props.config.collapse.defaultMode') === 'collapse') {
202 | this.canvas.nodes.forEach((item) => {
203 | this.canvas.collapse(item.id);
204 | })
205 | }
206 |
207 | if (_.get(this, 'props.config.gridMode')) {
208 | this.canvas.setGridMode(true, _.assign({}, _.get(this, 'props.config.gridMode', {})))
209 | }
210 |
211 | this.forceUpdate();
212 | });
213 |
214 | this.initEvents();
215 | }
216 |
217 | /**
218 | * 初始化butterfly事件
219 | */
220 | initEvents() {
221 | const {config, edgeMenu} = this.props;
222 | let isAfterSelect = false;
223 |
224 | let _addLinks = (links: any) => {
225 | let newLinkOpts = links.map((item: any) => {
226 | let _oldSource = _.get(item, 'sourceEndpoint.id', '').replace('-right', '');
227 | let _oldTarget = _.get(item, 'targetEndpoint.id', '').replace('-left', '');
228 | let _newSource = _oldSource + '-right';
229 | let _newTarget = _oldTarget + '-left';
230 | return {
231 | id: item.options.id || `${item.options.sourceNode}-${_oldSource}-${item.options.targetNode}-${_oldTarget}`,
232 | sourceNode: item.options.sourceNode,
233 | targetNode: item.options.targetNode,
234 | arrowShapeType: item.arrowShapeType,
235 | source: _newSource,
236 | target: _newTarget,
237 | _menu: item.options._menu || edgeMenu,
238 | _config: item.options._config || config,
239 | type: 'endpoint',
240 | label: item.label
241 | };
242 | });
243 |
244 | this.canvas.removeEdges(links, true);
245 | let newEdge = this.canvas.addEdges(newLinkOpts, true);
246 | newEdge.forEach((item) => {
247 | this.canvas.originEdges.push(_.assign({}, item.options, {
248 | source: _.get(item, 'sourceEndpoint.options.originId'),
249 | target: _.get(item, 'targetEndpoint.options.originId'),
250 | }));
251 | });
252 |
253 | return newEdge;
254 | }
255 |
256 | this.canvas.on('system.link.connect', (data: any) => {
257 | let newEdges = _addLinks(data.links || []);
258 | this.onConnectEdges(newEdges);
259 | this.forceUpdate();
260 | });
261 |
262 | this.canvas.on('system.link.reconnect', (data: any) => {
263 | let _addEdges = _addLinks(data.addLinks || []);
264 | this.onReConnectEdges(_addEdges, data.delLinks);
265 |
266 | this.forceUpdate();
267 | });
268 |
269 | this.canvas.on('custom.edge.dblClick', (data: any) => {
270 | this.props.onDblClickEdge && this.props.onDblClickEdge(data.edge);
271 | });
272 |
273 | this.canvas.on('system.node.click', (data: any) => {
274 | $(this.root).attr('tabindex', 0).focus();
275 | this._focusNode(data.node);
276 | });
277 |
278 | this.canvas.on('system.node.dblClick', (data: any) => {
279 | this.props.onDblClickNode && this.props.onDblClickNode(data.node);
280 | });
281 |
282 | this.canvas.on('system.link.click', (data: any) => {
283 | $(this.root).attr('tabindex', 0).focus();
284 | this._focusLink(data.edge);
285 | });
286 |
287 | this.canvas.on('system.canvas.click', (data: any) => {
288 | $(this.root).attr('tabindex', 0).focus();
289 | if(isAfterSelect) {
290 | return;
291 | }
292 | this._unfocus();
293 | this.props.onFocusCanvas && this.props.onFocusCanvas();
294 | this.canvas.unfocus();
295 | });
296 |
297 | this.canvas.on('system.multiple.select', ({data}) => {
298 |
299 | $(this.root).attr('tabindex', 0).focus();
300 |
301 | // 加这个判断是为了防止[system.canvas.click]事件和当前事件冲突
302 | isAfterSelect = true;
303 |
304 | const {nodes, edges} = data;
305 | this._unfocus();
306 |
307 | nodes.forEach(node => {
308 | node.focus();
309 | this._focusNodes.push(node);
310 | });
311 |
312 | edges.forEach(edge => {
313 | edge.focus();
314 | this._focusLinks.push(edge);
315 | })
316 |
317 | _.isFunction(this.props.onSelect) && this.props.onSelect(nodes, edges);
318 |
319 | // 防止误触
320 | setTimeout(() => {
321 | isAfterSelect = false;
322 | }, 100);
323 | });
324 |
325 | this.canvas.on('custom.node.delete', (data: any) => {
326 | this.onDeleteNodes([data.node]);
327 | });
328 |
329 | this.canvas.on('custom.item.focus', (data: any) => {
330 | this._unfocus();
331 | this._focusNodes = this._focusNodes.concat(data.nodes || []);
332 | this._focusLinks = this._focusLinks.concat(data.edges || []);
333 |
334 | });
335 |
336 | this.canvas.on('table.canvas.expand', () => {
337 | this.forceUpdate();
338 | });
339 |
340 | this.canvas.on('table.canvas.collapse', () => {
341 | this.forceUpdate();
342 | });
343 | }
344 |
345 |
346 | shouldComponentUpdate(newProps: ComProps, newState: any) {
347 |
348 | if (this.canvas.isSelectMode !== !!newProps.selectable) {
349 | this.canvas.setSelectMode(!!newProps.selectable);
350 | }
351 |
352 | // 更新节点
353 | let result = transformInitData({
354 | columns: this.props.columns,
355 | config: this.props.config,
356 | nodeMenu: this.props.nodeMenu,
357 | edgeMenu: this.props.edgeMenu,
358 | data: _.cloneDeep(newProps.data),
359 | emptyContent: this.props.emptyContent,
360 | emptyWidth: this.props.emptyWidth
361 | });
362 |
363 | let diffInfo = diffPropsData(result, this.canvasData, {
364 | oldCol: this.props.columns,
365 | newCol: newProps.columns
366 | });
367 | if (diffInfo.addNodes.length > 0) {
368 | this.canvas.addNodes(diffInfo.addNodes);
369 | }
370 | if (diffInfo.rmNodes.length > 0) {
371 | this.canvas.removeNodes(diffInfo.rmNodes.map((item) => item.id));
372 | }
373 |
374 | // 更新节点中的字段
375 | let _isDiffNode = (
376 | diffInfo.updateTitle.length > 0 ||
377 | diffInfo.updateFields.length > 0 ||
378 | diffInfo.addFields.length > 0 ||
379 | diffInfo.rmFields.length > 0 ||
380 | diffInfo.newCol.length > 0
381 | );
382 |
383 | if (_isDiffNode) {
384 | this.canvas.updateNodes(diffInfo);
385 | }
386 |
387 | if (diffInfo.addEdges.length > 0) {
388 | this.canvas.addEdges(diffInfo.addEdges);
389 | }
390 |
391 | if (diffInfo.rmEdges.length > 0) {
392 | this.canvas.removeEdges(diffInfo.rmEdges.map(edge => edge.id));
393 | }
394 |
395 | if (diffInfo.updateLabel.length > 0) {
396 | this.canvas.updateLabel(diffInfo.updateLabel);
397 | }
398 |
399 | let newCollapse = _.get(newProps, 'config.collapse.status', false);
400 | let oldCollapse = _.get(this.props, 'config.collapse.status', false);
401 |
402 | if (newCollapse !== oldCollapse) {
403 | this.canvas.nodes.forEach((node) => {
404 | newCollapse && this.canvas.collapse(node.id);
405 | !newCollapse && this.canvas.expand(node.id);
406 | });
407 | }
408 |
409 | this.canvasData = result;
410 | return true;
411 | }
412 |
413 | componentWillUnmount() {
414 | if (_.get(this, 'props.config.allowKeyboard')) {
415 | this.root.removeEventListener('keydown', this._deleteFocusItem);
416 | }
417 | }
418 |
419 | onConnectEdges(links) {
420 | let linksInfo = links.map((item) => {
421 | return _.assign(item.options, {
422 | source: _.get(item, 'options.source', '').replace('-right', ''),
423 | target: _.get(item, 'options.target', '').replace('-left', ''),
424 | _sourceNode: item.sourceNode,
425 | _targetNode: item.targetNode,
426 | _sourceEndpoint: item.sourceEndpoint,
427 | _targetEndpoint: item.targetEndpoint
428 | });
429 | });
430 |
431 | let newEdges = _.differenceWith(linksInfo, this.canvasData.edges, (a: any, b: any) => {
432 | return (
433 | a.sourceNode === b.sourceNode &&
434 | a.targetNode === b.targetNode &&
435 | a.source === b.source &&
436 | a.target === b.target
437 | );
438 | });
439 |
440 | this.canvasData.edges = this.canvasData.edges.concat(newEdges);
441 |
442 | this.props.onChange && this.props.onChange({
443 | type: 'system.link.connect',
444 | links: linksInfo
445 | });
446 | }
447 |
448 | onReConnectEdges(addLinks, rmLinks) {
449 | let addLinksInfo = addLinks.map((item) => {
450 | return _.assign(item.options, {
451 | source: _.get(item, 'options.source', '').replace('-right', ''),
452 | target: _.get(item, 'options.target', '').replace('-left', '')
453 | });
454 | });
455 | let rmLinksInfo = rmLinks.map((item) => {
456 | return _.assign(item.options, {
457 | source: _.get(item, 'options.source', '').replace('-right', ''),
458 | target: _.get(item, 'options.target', '').replace('-left', '')
459 | });
460 | });
461 | this.props.onChange && this.props.onChange({
462 | type: 'system.link.reconnect',
463 | addLinks: addLinksInfo,
464 | rmLinks: rmLinksInfo
465 | });
466 | }
467 |
468 | onDeleteNodes(nodes) {
469 |
470 | let beforeDeleteNode = this.props.beforeDeleteNode || function() {return true};
471 |
472 | Promise.resolve(beforeDeleteNode(nodes))
473 | .then((result) => {
474 | if (result === false) {
475 | return false;
476 | } else {
477 | let neighborLinksInfo = [];
478 | nodes.forEach((node) => {
479 | let links = this.canvas.getNeighborEdges(node.id);
480 | let linksInfo = links.map((link) => {
481 | return link.options;
482 | });
483 | neighborLinksInfo = neighborLinksInfo.concat(linksInfo);
484 |
485 | node.remove();
486 | });
487 |
488 | let nodesInfo = nodes.map((item) => {
489 | return item.options;
490 | });
491 |
492 | this.props.onChange && this.props.onChange({
493 | type: 'system.node.delete',
494 | nodes: nodesInfo,
495 | neighborLinks: neighborLinksInfo
496 | });
497 | }
498 | }).catch(() => {});
499 | }
500 |
501 | onDeleteEdges(links) {
502 |
503 | let beforeDeleteEdge = this.props.beforeDeleteEdge || function() {return true};
504 |
505 | Promise.resolve(beforeDeleteEdge(links))
506 | .then((result) => {
507 | if (result === false) {
508 | return;
509 | } else {
510 | let linksInfo = links.map((item) => {
511 | return item.options;
512 | });
513 |
514 | this.props.onChange && this.props.onChange({
515 | type: 'system.link.delete',
516 | links: linksInfo
517 | });
518 | }
519 | }).catch(() => {});
520 | }
521 |
522 | _genClassName() {
523 | return (this.props.className || '') + ' butterfly-table-building'
524 | }
525 |
526 | // 聚焦节点
527 | _focusNode(node) {
528 | this._unfocus();
529 | node.focus();
530 | this._focusNodes.push(node);
531 | this.props.onFocusNode && this.props.onFocusNode(node);
532 | }
533 |
534 | // 聚焦线段
535 | _focusLink(edge) {
536 | this._unfocus();
537 | edge.focus();
538 | this._focusLinks.push(edge);
539 | this.props.onFocusEdge && this.props.onFocusEdge(edge);
540 | }
541 |
542 | // 失焦
543 | _unfocus() {
544 | this._focusNodes.forEach((item) => {
545 | item.unfocus();
546 | });
547 |
548 | this._focusLinks.forEach((item) => {
549 | item.unfocus();
550 | });
551 |
552 | this._focusNodes = [];
553 | this._focusLinks = [];
554 | }
555 |
556 | _deleteFocusItem(e) {
557 | // todo: 这块需要好好思考下
558 | if (e.key === 'Delete' || e.key === 'Backspace') {
559 | if (this._focusNodes && this._focusNodes.length > 0) {
560 | this.onDeleteNodes(this._focusNodes);
561 | }
562 | if (this._focusLinks && this._focusLinks.length > 0) {
563 | this.onDeleteEdges(this._focusLinks);
564 | }
565 | }
566 | }
567 |
568 | _delNodes(nodes) {
569 | return nodes.map((item) => {
570 | return item.options;
571 | });
572 | }
573 |
574 | _delEdges(edges) {
575 | return edges.map((item) => {
576 | return item.options;
577 | });
578 | }
579 |
580 | render() {
581 | const {canvas} = this;
582 | const {actionMenu = []} = this.props;
583 | const actionMenuVisible = _.get(this, 'props.config.showActionIcon', true);
584 | const labelRender = _.get(this, 'props.config.labelRender', noop);
585 | const selectable = !!this.props.selectable;
586 |
587 | return (
588 |
604 | )
605 | }
606 | }
--------------------------------------------------------------------------------
/src/static/iconfont.css:
--------------------------------------------------------------------------------
1 | @font-face {font-family: "table-build-icon";
2 | src: url('//at.alicdn.com/t/font_2369312_0qxuga95yni.eot?t=1613541691358'); /* IE9 */
3 | src: url('//at.alicdn.com/t/font_2369312_0qxuga95yni.eot?t=1613541691358#iefix') format('embedded-opentype'), /* IE6-IE8 */
4 | url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAWYAAsAAAAACwgAAAVJAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDXgqHYIZwATYCJAMcCxAABCAFhU0HdRu9CcgekqRfBAooB8AVIgAgHv6b+7wvM0k+0O4WFKJDxxJRoaoDZCNqRDULu7bEx/9BNjU0TOnmTEyU/l5+nyu9iVB6E1lcy0dQ7yjCtAP5asw+f81h2h6gEhNyRu9qTW0QXSSueyHEbu4ng1QEktWdCkX+6oACqFPlKkx1jbM1Ea9eJrYj80ZpAi1TmUHWBzx9tUayOBBYcKzYw9aok5QXrKERqSbezeIJKDWKu6wLgMfFz8d3NiFKoUzisrYcvVats0/oy4+Q+E84kAhwZX/NMD8iYTEgE7cmW6+AVI0uBrX8v5nt0NLTUKg+5WeNZ91n056TL5/+/w90t8bXQ0+K1/Yvr1IqJKJCwQ6Ue8YqPqGJQPBJNGAYkgBbDmlCoOQzKVDw9WnIuyCv3+lhMhBLAIjXYsNSLyeVJMnydHk1FK3ElFEpRTZ7omrKeZByBq0J/7YCTgLDwMaNo2kbEc/nP08mnveM7yZ5nBwumPCK5jKahiPQkHwuQSeII1cp7hzQIjsxj2GGSR6CtNfmTWVKsXrHJVn9o2CiFTna2rrRLvxlF2U+pzni7Bfy4l1d/d04LtE7OEAlHbdZIEg84lME7XaELkcKjguQIZ0IYS0piMz/neUNpM6Tl2CYktps+Rviezi8WTy+WzGxl8TxbkI+rourUM48SxA8T+Q/jaeftOyDcXPOiqW3bnTeRzXhdoHiSjeG3e9F4NUexJ/EhbS+k3nWdCp++iL7uroATu6KSE+PK0DfvQ2J2wQMqQeq+RaHV+sA9SGFOdbYXq8ROKQRoOZXHEzkbD959gyl5q+m+hpBfKgyb6+WSUmyukNhjvWUjiqZnCScokQVeS4/ZymnLieVU7i0VreWKiwWuuL8ggCVS52hqlY5JTj9/+9MUc6Pqcd09IxfE3CdApCVVlbIwsPC/7cMXI98EpOebSFZe/dCUmJGaQBquXma1x562GonIqC2cMqs3bc7fffuNKbCO+PoDDqt5TQNgxfbViobLt+CiFp1GT34589B+FBcQQ82U10OwokbNybIB+kKnCD+eEe/sg7RNzcdG/c3qefSB+6oLNu9863BW4Acp4VUVFBaokapL6NZiC6bJRKxs3vR799R8u2jKXvhPvjtexVD96Loqd+2o2M6tO7oqA0QP6ggfrctg91tKWAFd+LPcyW7nd/nsx02keXrgJvPdbDO/6Yt8OVXmPo2GwRPg/BtwfTxZ/iau+IqW8dx8aJesbs6tDA8nfZDX9AivS0YYNiTvsN6ETuSpN8LjhXpB2wan0x3U/qf+3dceBn9OVPK7asolsF4zDVX9AxXZMBn/9AQX/wri8wjOfDGqTp7uyeM6aptMRgu0JJX+AN3jrq3v/BE9nzCAN5AoTYHkgEzaVa6QaltA1QGEqFlkcKP28asA0nkJhaaFhCmuwyFnt+QTPdXko2hDqVJ4e6vppSx0LIranNsmxtnW741hDkpyGmG4lxrXPVKPqnLPjCJnPqAlQjv+kFewZhaO3aHfijNAtwI1hZW4xO3ZCg9xgu1Dd5pXZ0LBTVWQ8A+yiNmC1U1NiQnl0V1eby3tCyYapDMrdStiXVSBOWIAnI0QDGHZbhZc26SLEcHjIhicyHAFMFa/oF4CmyeL2Xua2n6D5LKAg89HrBmwWoL6W3QbLXjtSyvVfKcqpPNLkjWpywNBJyLn7TZzAJV0/4Qsx05qWiL4diepTypgBuUxw3rz+UWtMSNqKOIFDnKqKKOhvpcpdWiuGJcXY8mDox3KjqorLI+7zcVI+MkN9uUmIncert6eWi02Xr4bswwPtpWCwAAAA==') format('woff2'),
5 | url('//at.alicdn.com/t/font_2369312_0qxuga95yni.woff?t=1613541691358') format('woff'),
6 | url('//at.alicdn.com/t/font_2369312_0qxuga95yni.ttf?t=1613541691358') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
7 | url('//at.alicdn.com/t/font_2369312_0qxuga95yni.svg?t=1613541691358#table-build-icon') format('svg'); /* iOS 4.1- */
8 | }
9 | @font-face {
10 | font-family: 'table-build-icon'; /* project id 2369312 */
11 | src: url('//at.alicdn.com/t/font_2369312_wsg9aeubvfs.eot');
12 | src: url('//at.alicdn.com/t/font_2369312_wsg9aeubvfs.eot?#iefix') format('embedded-opentype'),
13 | url('//at.alicdn.com/t/font_2369312_wsg9aeubvfs.woff2') format('woff2'),
14 | url('//at.alicdn.com/t/font_2369312_wsg9aeubvfs.woff') format('woff'),
15 | url('//at.alicdn.com/t/font_2369312_wsg9aeubvfs.ttf') format('truetype'),
16 | url('//at.alicdn.com/t/font_2369312_wsg9aeubvfs.svg#table-build-icon') format('svg');
17 | }
18 |
19 | .table-build-icon {
20 | font-family: "table-build-icon" !important;
21 | font-size: 16px;
22 | font-style: normal;
23 | -webkit-font-smoothing: antialiased;
24 | -moz-osx-font-smoothing: grayscale;
25 | }
26 |
27 | .table-build-icon-zoom-in:before {
28 | content: "\e604";
29 | }
30 |
31 | .table-build-icon-quanping2:before {
32 | content: "\e78b";
33 | }
34 |
35 | .table-build-icon-zoom-out:before {
36 | content: "\e9e5";
37 | }
38 |
39 | .table-build-icon-xiala:before {
40 | content: "\e608";
41 | }
42 |
43 | .table-build-icon-canvas-cuo:before {
44 | content: "\e61f";
45 | }
46 |
47 | .table-build-icon-iconfontxiaogantanhao:before {
48 | content: "\e60d";
49 | }
50 |
51 | .table-build-icon-kongshuju:before {
52 | content: "\e600";
53 | }
54 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "noImplicitAny": false,
5 | "module": "commonjs",
6 | "target": "es2015",
7 | "jsx": "react",
8 | "allowJs": true,
9 | "esModuleInterop": true
10 | }
11 | }
--------------------------------------------------------------------------------