# react-x6-editor
**Repository Path**: osswork/react-x6-editor
## Basic Information
- **Project Name**: react-x6-editor
- **Description**: 是一个基于@antv/x6以及plain-design-composition封装的React可视化流程编辑组件;旨在于封装开箱即用的常用功能,包括快速定义画布组件、快速定义画布React组件、撤销重做、放大缩小、数据导入导出、冻结画布、拦截新增(删除)节点(边)等功能;
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 5
- **Created**: 2022-04-09
- **Last Updated**: 2022-06-21
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
[toc]
- 在线示例:[http://martsforever-pot.gitee.io/react-x6-editor/](http://martsforever-pot.gitee.io/react-x6-editor/)
- Vue3.0版本:[https://gitee.com/martsforever-pot/vue-x6-editor](https://gitee.com/martsforever-pot/vue-x6-editor)
# 简介
- 是一个基于[@antv/x6](https://antv-x6.gitee.io/zh/docs/tutorial/about/)以及[plain-design-composition](http://plain-pot.gitee.io/plain-design-doc/#composition%2Fcomposition-introduce.entire)封装的React可视化流程编辑组件;
- 旨在于封装开箱即用的常用功能,包括快速定义画布组件、快速定义画布React组件、撤销重做、放大缩小、数据导入导出、冻结画布、拦截新增(删除)节点(边)等功能;
- 注意的是,目前这个版本的`plain-design-composition`依赖于`Vue3.0`响应式API,所以目前无法兼容IE浏览器。需要将`plain-design-composition`替换为基于`Vue2.0`响应式API的`plain-design-composition-v2`;
# 安装
## 安装依赖
```bash
npm i react-x6-editor plain-design-compositoin @antv/x6 @antv/x6-react-shape -S
```
## 示例代码
- `plain-design-composition`是一个基于Vue响应式API以及ReactHook封装的,在React中使用组合式API开发的库,所以示例中`designPage`返回的是一个ReactHook函数组件;
- 目前仅支持通过组合式API的方式使用,如果希望定义的组合式组件有更多的功能(属性props,派发事件event,插槽slots),请见`plain-design-composition`的文档说明,使用`designComponent`开发;
```jsx
import React from "react";
import ReactDOM from "react-dom";
import {designPage} from 'plain-design-composition';
import {createBranchNode, createEndNode, createFlowEdge, createFlowNode, createRenderNode, createStartNode, ReactX6Editor, useFlowEditor} from "react-x6-editor";
import 'react-x6-editor/dist/react-x6-editor.css'
import './main.scss';
const App = designPage(() => {
  const editor = useFlowEditor();
  /*注册预定义的组件*/
  editor.components.registry(createStartNode());
  editor.components.registry(createFlowNode());
  editor.components.registry(createBranchNode());
  editor.components.registry(createEndNode());
  editor.components.registry(createFlowEdge());
  editor.components.registry(createRenderNode());
  /*异步加载数据*/
  import('./flow.data.json').then(data => {editor.methods.update(data.default);});
  return () => (
    
      
    
  );
});
ReactDOM.render(<>
  
>, document.body.querySelector('#app'));
```
# API说明
## useFlowEditor对象参数类型
- useFlowEditor(options)
|属性名称|类型|说明|
|---|---|---|
|height|string,number|画布高度|
|width|string,number|画布宽度|
|processGraphConfig|(data: { graphConfig: Partial, module: iGraphModule, config: iFlowEditorConfig }) => Partial|在创建graph对象之前,处理grapgConfig对象;
|nodeConfig|Object|节点默认配置,请见下文nodeConfig说明|
|onlyOneStartNode|boolean|只允许有一个开始节点|
|cannotDeleteStartNode|boolean|不可以删除所有开始节点|
|onlyOneEndNode|boolean|只允许有一个结束节点|
|cannotDeleteEndNode|boolean|不可以删除所有结束节点|
- nodeConfig
|属性名称|类型|说明|
|---|---|---|
|deepColor|string|节点深色(主色调)|
|lightColor|string|节点浅色|
|edgeLightColor|string|连接线浅色|
|edgeDeepColor|string|连接线深色|
|fontSize|string|节点文字大小|
|height|nuimber|节点默认高度|
|width|number|节点默认宽度|
## x6画布参数设置
- 通过属性graphConfig设置一些功能的启用状态,比如默认关闭网格
- 关于画布的属性设置请见文档:[ API:Graph](https://antv-x6.gitee.io/zh/docs/api/graph/graph)
```jsx
const editor = useFlowEditor({
    graphConfig: {
      grid: false,
    }
});
```
## 自定义(继承)组件
- `editor.components.registry(createFlowNode())`;
- createFlowNode如下示例所示:
```jsx
export function createFlowNode(custom: { name?: string, icon?: string, defaultLabel?: string } = {}): iFlowEditorComponent {
  const code = 'flow-node';
  return {
    name: custom.name || '流程节点',
    icon: custom.icon || 'node',
    code,
    defaultNodeData: ({ config }) => ({
      width: config.nodeConfig.width,
      height: config.nodeConfig.height,
    }),
    process: ({ Graph, config }) => {
      Graph.registerNode(code, {
        inherit: 'rect',
        attrs: {
          body: {
            stroke: config.nodeConfig.lightColor,
            fill: config.nodeConfig.lightColor,
          },
          label: {
            text: custom.defaultLabel || '流程节点',
            fill: config.nodeConfig.deepColor,
            fontsize: config.nodeConfig.fontSize,
          }
        },
        ports: {
          ...createFlowConnectPorts(config),
        }
      }, true);
    }
  };
}
```
## 自定义(继承)连接线
- `editor.components.registry(createFlowEdge())`;
- createFlowEdge如下示例所示,具体定义连接线请参考x6文档;
```jsx
export function createFlowEdge(): iFlowEditorRegister {
  const code = 'flow-edge';
  return {
    code,
    process: ({ Graph, config }) => {
      Graph.registerEdge(code, {
        ...createDefaultEdge(config),
      }, true);
    },
  };
}
```
## 定义React渲染的节点
- `editor.components.registry(createRenderNode());`
- createRenderNode如下示例所示,示例中的`DemoRenderNodeContent`就是要实现的React组件;
```jsx
import {iFlowEditorComponent} from "../utils/flow.type.base";
import {BaseReactComponent} from "../utils/BaseReactComponent";
import {DemoRenderNodeContent} from "./DemoRenderNodeContent";
import {createFlowConnectPorts} from "../utils/FlowConnectPorts";
export function createRenderNode(custom: { name?: string, icon?: string, defaultLabel?: string } = {}): iFlowEditorComponent {
  const code = 'render-node';
  return {
    name: custom.name || 'React节点',
    icon: custom.icon || 'data',
    code,
    defaultNodeData: ({ config }) => ({
      width: 300,
      height: 100,
    }),
    process: async ({ Graph, config }) => {
      Graph.registerNode(code, {
        inherit: 'react-shape',
        component: (
          
            
          
        ),
        ports: {
          ...createFlowConnectPorts(config),
        }
      }, true);
    }
  };
}
```
## Hooks钩子(拦截器)
- editor有很多内置的钩子函数,用于监听(拦截)各种行为,比如监听节点的点击事件:
```jsx
editor.hooks.onClickNode.use(({ node }) => {
    console.log('click node', node);
});
```
- 钩子函数可以阻止,在添加的拦截函数中抛出异常或者返回`Promise.reject()`就可以阻止行为。比如添加节点的时候限制只能有一个开始(结束)节点的控制行为:
```jsx
hooks.onAddNode.use(async ({ node }) => {
if (node.shape == START_NODE) {
  if (!config.onlyOneStartNode) {return; }
  const { cells } = await methods.getGraphData();
  if (!!cells.find(i => i.shape === START_NODE && i.id !== node.id)) {
    alert('只能存在一个开始节点!');
    return Promise.reject('only one start node!');
  }
} else if (node.shape == END_NODE) {
  if (!config.onlyOneEndNode) {return; }
  const { cells } = await methods.getGraphData();
  if (!!cells.find(i => i.shape === END_NODE && i.id !== node.id)) {
    alert('只能存在一个结束节点!');
    return Promise.reject('only one end node!');
  }
}
});
```
## 所有可用的Hook钩子
```jsx
export function useFlowEditorHooks() {
  const hooks = {
    onMouseupCanvas: createHooks<(e: MouseEvent) => void>(),                                // 点击画布动作
    onGraphLoaded: createHooks<(module: iGraphModule) => void>(),                           // Graph模块加载完毕
    onGraphReady: createHooks<(module: iGraphModule) => void>(),                            // graph初始化准备完毕触发动作,onGraphLoaded执行完毕之后就会执行这个动作
    onFlowMounted: createHooks<() => void>(),                                               // ReactX6Editor组件的mounted动作
    onProcessOperators: createSyncHooks<(operators: iFlowEditorOperatorMeta[]) => void>(),  // 处理操作栏按钮
    /*节点钩子*/
    onClickNode: createHooks<(e: NodeView.PositionEventArgs) => void>(),               // 单击节点
    onDblclickNode: createHooks<(e: NodeView.PositionEventArgs) => void>(),            // 双击节点
    onNodeContextmenu: createHooks<(e: NodeView.PositionEventArgs) => void>(),         // 右击节点触发动作
    onAddNode: createHooks<(e: { node: Node, cell: Cell, index: number }) => void>(),       // 添加节点钩子
    onDeleteNode: createHooks<(e: { node: Node, cell: Cell, index: number }) => void>(),    // 删除节点钩子
    /*连接线钩子*/
    onClickEdge: createHooks<(e: EdgeView.PositionEventArgs) => void>(),               // 单击连接线
    onDblclickEdge: createHooks<(e: EdgeView.PositionEventArgs) => void>(),            // 双击连接线
    onEdgeContextmenu: createHooks<(e: EdgeView.PositionEventArgs) => void>(),         // 右击边触发动作
    onVertexContextmenu: createHooks<(e: EdgeView.PositionEventArgs) => void>(),       // 右击拐点触发动作
    onAddEdge: createHooks<(e: { edge: Edge, cell: Cell, index: number }) => void>(),       // 添加连接线钩子
    onDeleteEdge: createHooks<(e: { edge: Edge, cell: Cell, index: number }) => void>(),    // 删除连接线钩子
    onConnectedEdge: createHooks<(e: EdgeView.EventArgs["edge:connected"]) => void>(),      // 连接线连接节点钩子
    /*画布钩子*/
    onBlankContextmenu: createHooks<(e: CellView.PositionEventArgs) => void>(),             // 右击拐点触发动作
    /*其他*/
    onCreateContextmenu: createHooks<(e: { edge?: Edge, node?: Node, view?: View, type: eCreateContextmenuType | keyof typeof eCreateContextmenuType, options: iFlowContextmenuOption[], pos: { x: number, y: number } }) => void>(),
  };
  return hooks;
}
```
## 自定义右击菜单
```jsx
  editor.hooks.onCreateContextmenu.use(({ options, edge, node, type }) => {
    switch (type) {
      case 'node':
        options.push({
          label: '自定义node选项',
          icon: 'data',
          handler: () => alert('自定义node选项')
        });
        options.push({
          label: '自定义node选项2',
          icon: () => ,
          handler: () => alert('自定义node选项2')
        });
        return;
    }
  });
```
## 自定义操作栏按钮
```jsx
  const editor = useFlowEditor({
    onlyOneStartNode: true,
    operators: [
      {
        label: '选项一',
        icon: () => (),
        handler: () => console.log('option 1...')
      }
    ]
  });
```
## 可用函数methods
- `useFlowEditor`创建的对象有许多可用函数,使用示例如下所示:
```jsx
// 获取x6创建的graph对象
const { graph } = await editor.getGraph();
// 获取所有的cell
const cells = graph.getSelectedCells();
```
- 所有可用函数如下所示
|函数名称|类型|说明|
|---|---|---|
| getGraph | ()=>Promise | 异步获取graph实例对象,因为是按需加载antv/x6以及需要等待Editor挂载,所以graph初始化是一个异步的过程 |
| update | (data:iFlowData)=>Promise | 更新数据 |
| getGraphData | ()=><{ cells: iFlowMetaData[] }> | 从graph中解析json数据 |
| deleteSelection | ()=>Promise) | 删除选中节点 |
| undo | ()=>:Promise | 撤销 |
| redo | ()=>:Promise | 重做 |
| undoAndClearRedo | ()=>Promise | ,撤回,并且清空重做列表 |
| setFrozen | (frozen?:boolean)=>Promise | 冻结 |
## createFlowEditorUser
- 可以通过函数`createFlowEditorUser`自定义默认配置的组合函数,比如`useFlowEditor`的源码如下所示
```jsx
export const useFlowEditor = createFlowEditorUser({
  height: '100%',
  width: '100%',
  processGraphConfig: ({ graphConfig, module, config }) => {
    return Object.assign({}, {
      background: { color: '#fff' },        // 默认背景色为白色
      grid: {                               // 默认开启网格
        size: 10,
        visible: true,
      },
      connecting: {                         // 默认连接线为平滑连接线
        connector: 'smooth',
        allowBlank: false,
        allowLoop: false,
        allowEdge: false,
        highlight: true,
        createEdge() {
          return new module.Shape.Edge({
            shape: 'flow-edge',
            ...createDefaultEdge(config)
          });
        },
      },
      panning: true,                        // 拖拽画布移动所有节点
      resizing: true,                       // 节点选中之后可以调整大小
      rotating: true,                       // 节点选中之后可以旋转
      selecting: {                          // 默认开启框选功能
        enabled: true,
        rubberband: true,
        showNodeSelectionBox: true,
        modifiers: ['shift', 'ctrl', 'meta'],
      },
      snapline: {                           // 辅助对齐线
        enabled: true,
        clean: false,
      },
      keyboard: true,                       // 开启键盘快捷键功能
      clipboard: true,                      // 开启剪切板功能
      history: true,                        // 开启操作历史功能
    } as X6GraphConfig, graphConfig);
  },
  nodeConfig: {
    deepColor: '#1f74ff',                   // 深色(主色调)
    lightColor: '#deeaff',                  // 浅色(主色调背景色)
    edgeLightColor: '#b1cdfa',              // 连接线浅色
    edgeDeepColor: '#1f74ff',               // 连接线深色
    fontSize: '14',                         // 节点文字大小
    width: 80,                              // 默认宽度
    height: 40,                             // 默认高度
  },
  onlyOneStartNode: false,
  onlyOneEndNode: false,
  cannotDeleteStartNode: false,
  cannotDeleteEndNode: false,
});
```
# 操作说明
## 节点连接桩
- 鼠标进入节点会显示节点的连接桩,拖拽连接桩到另一个节点或者另一个节点的连接桩,会创建一条连接线;
- 点击节选会使得节点处于选中状态,处于选中状态的节点不会显示连接桩;但是此时可以拖拽调整节点大小。
- 默认情况下,一个节点连接到另一个节点,不会保留连接线的开始节点连接桩以及目标节点的连接桩。右击节点,启用【保留连接桩】,此时从该节点连接的连线在创建的时候就不会自动去掉连接桩,同理如果这个节点为连接线的目标节点也是一样。如果需要保留连接线的连接桩,需要右击节点勾选保留连接桩;
## 连接线拐点
- 默认情况下拐点的功能,是单击(双击)拖拽连接线添加拐点,或者移动拐点,双击存在的拐点删除拐点。
- 为了支持业务需求,目前默认禁用了这些行为。单击拖拽连接线空白处不会添加拐点,双击拐点不会删除拐点。而是右击连接线,在右击的位置添加拐点,右击拐点选择删除拐点。
- 单击连接线的时候会选中连接线,之后可以删除。同时预留双击连接线的功能用来处理更多业务需求;
## 可交互的React节点
- 默认情况下,选中节点之后,选中框会挡住节点。此时如果节点是自定义React渲染的节点,那么选中框会挡住React节点的内容(按钮、输入框等等)的点击动作。
- 目前为了优化这个逻辑,监听了选中的动作。当单选的时候,通过css选择器的方式,设置选中框的div的css属性`pointer-events`为`none`以避免挡住节点内容的操作;多选的时候则不做处理,允许选择框挡住节点(方便拖拽整体移动节点);
# 快捷键
## 多选
在画布上摁住shift可以框选多个节点,框选完毕之后,可以摁住ctrl或者meta键选中某个节点,以实现额外添加或者取消选中节点的功能;
## 复制
选中节点之后`meta/ctrl + c`可以复制节点;`meta/ctrl + v`可以粘贴节点;
## 剪切
选中节点之后`meta/ctrl + x`可以复制节点;`meta/ctrl + v`可以粘贴节点;
## 撤销
`meta/ctrl + z` 撤销刚刚的动作;
## 重做
`meta/ctrl + shift + z` 重做刚刚的动作;
## 全选
`meta/ctrl + shift + a` 全选画布中的节点;
## 删除
`backspace/delete` 删除选中节点;
## 放大
`meta/ctrl + 1` 放大画布
## 缩小
`meta/ctrl + 2` 缩小画布