background

Due to recent projects to support, graphics editing such a demand, the hope can custom ICONS, cables and so on function, with little time, so I checked the online, found x6 can achieve our demand, x6 is a graph editor AntV engine, can also, but also has a lot of bugs, after all has not been a year, also need to grinding, But it’s enough to support our project.

Preview the address.

Technology stack

  • react
  • typescript
  • react-dnd
  • x6
  • antd
├ ─ ─ the configConfig file for project scaffolding├ ─ ─ the script# Project profile├ ─ ─ the SRC# source code│ ├ ─ ─ assets# Global CSS, JS, IAMGE and other static resources│ ├ ─ ─ the components# Global common component│ ├ ─ ─ the config# Global configuration of graphics│ ├ ─ ─ graph# graphic example│ ├ ─ ─ graphTemplateType# Template for graphics│ ├ ─ ─ hooksGlobal hooks #│ ├ ─ ─ the ICONS# Project all SVG ICONS│ ├ ─ ─ interfacesThe global TS file type│ ├ ─ ─ the coreThe code to perform the operation│ ├ ─ ─ layout# layout│ ├ ─ ─ utils# public public method│ ├ ─ ─ index. The CSS# global style│ ├ ─ ─ index. The TSX# entry file load component initialization etc│ └ ─ ─ the react - app - env. Which sReact global module declaration file├ ─ ─ editorconfig# Code style uniform profile├ ─ ─. Eslintrc. Js# esLint configuration item├ ─ ─ eslintignore# eslint ignores files├ ─ ─ prettierrc# prettierrc configuration item├ ─ ─ prettierignore# prettierIgnore Ignores the file├ ─ ─ tsconfig. JsonProject global TS configuration file└ ─ ─ package. Json# package.json
Copy the code

Start by creating the Graph object

Create index.ts in the graph file to instantiate the graph object

import { Graph, FunctionExt, Shape } from '@antv/x6';
export default class FlowGraph {
  public static graph: Graph;
  public static init() {
    this.graph = new Graph({
      container: document.getElementById('container')! , width:1000.height: 800.resizing: {
        enabled: true,},grid: {
        size: 10.visible: true.type: 'doubleMesh'.args: [{color: '#cccccc'.thickness: 1}, {color: '#5F95FF'.thickness: 1.factor: 4,}]},selecting: {
        enabled: true.multiple: true.rubberband: true.movable: true.showNodeSelectionBox: true.filter: ['groupNode'],},connecting: {
        anchor: 'center'.connectionPoint: 'anchor'.allowBlank: false.highlight: true.snap: true.createEdge() {
          return new Shape.Edge({
            attrs: {
              line: {
                stroke: '#5F95FF'.strokeWidth: 1.targetMarker: {
                  name: 'classic'.size: 8,}}},router: {
              name: 'manhattan',},zIndex: 0}); },validateConnection({ sourceView, targetView, sourceMagnet, targetMagnet, }) {
          if (sourceView === targetView) {
            return false;
          }
          if(! sourceMagnet) {return false;
          }
          if(! targetMagnet) {return false;
          }
          return true; }},highlighting: {
        magnetAvailable: {
          name: 'stroke'.args: {
            padding: 4.attrs: {
              strokeWidth: 4.stroke: 'rgba (223234255).,},},},},snapline: true.history: true.clipboard: {
        enabled: true,},keyboard: {
        enabled: true,},embedding: {
        enabled: true.findParent({ node }) {
          const bbox = node.getBBox();
          return this.getNodes().filter((node) = > {
            const data = node.getData<any> ();if (data && data.parent) {
              const targetBBox = node.getBBox();
              return bbox.isIntersectWithRect(targetBBox);
            }
            return false; }); ,}}});return this.graph; }}Copy the code

Create a custom node name

Registerednode. ts is created under graph to create custom node names

import { Graph, Dom } from '@antv/x6';
import { shapeName } from '@/config';
import { portsConfig } from '@/config/portsConfig';
export const FlowChartRect = Graph.registerNode(shapeName.flowChartRect, {
  inherit: 'rect'.width: 80.height: 42.attrs: {
    body: {
      stroke: '#5F95FF'.strokeWidth: 1.fill: 'rgba (95149255,0.05)',},fo: {
      refWidth: '100%'.refHeight: '100%',},foBody: {
      xmlns: Dom.ns.xhtml,
      style: {
        width: '100%'.height: '100%'.display: 'flex'.justifyContent: 'center'.alignItems: 'center',}},'edit-text': {
      contenteditable: 'false'.class: 'x6-edit-text'.style: {
        width: '100%'.textAlign: 'center'.fontSize: 12.color: 'rgba (0,0,0,0.85)',}},text: {
      fontSize: 12.fill: 'rgba (0,0,0,0.85)'.textWrap: {
        text: ' '.width: -10,}}},markup: [{tagName: 'rect'.selector: 'body'}, {tagName: 'text'.selector: 'text'}, {tagName: 'foreignObject'.selector: 'fo'.children: [{ns: Dom.ns.xhtml,
          tagName: 'body'.selector: 'foBody'.children: [{tagName: 'div'.selector: 'edit-text',},],},],},ports: portsConfig,
});
Copy the code

Create a base folder under graph, change the folder to hold the base custom nodes, and create a new index.ts folder under /graph/base

import { shapeName } from '@/config';
import { Dom } from '@antv/x6';
import { portsConfig } from '@/config/portsConfig';
export const roundedRectangle = {
  shape: shapeName.flowChartRect,
  attrs: {
    body: {
      rx: 24.ry: 24,},text: {
      textWrap: {
        text: ' ',},},},};export const rectangle = {
  shape: shapeName.flowChartRect,
  attrs: {
    text: {
      textWrap: {
        text: ' ',},},},};Copy the code

Operation node

Under the core file created dragTarget. Ts, dropTarget. Ts this two files, a drag and drop goal, a drag and drop the destination

Dragtarget. ts, used to render a graph with a list of target elements to drag

import React, { memo, FC } from 'react';
import { useDrag } from 'react-dnd';
import { tempalteType } from '@/graphTemplateType';
import { overHiddleText } from '@/utils';
import style from './index.module.scss';
import SvgCompent from '@/components/svgIcon';
const DragTarget: FC<{
  itemValue: tempalteType;
}> = memo(function DragTarget({ itemValue }) {
  const [, drager] = useDrag({
    type: 'Box'.item: itemValue,
  });

  return (
    <a ref={drager} className={style.templateRender} title={itemValue.title}>
      <SvgCompent iconClass={itemValue.type} fontSize="60px" />
      <p>{overHiddleText(itemValue.title, 7)}</p>
    </a>
  );
});

export default DragTarget;

Copy the code

Droptarget. ts, used to render and instantiate graphic elements

import React, { memo, useState, useRef, useEffect } from 'react';
import { useDrop } from 'react-dnd';
import style from './index.module.scss';
import { Drawer } from 'antd';
import { useOnResize, useKeydown } from '@/hooks';
import FlowGraph from '@/graph';
import { formatGroupInfoToNodeMeta } from '@/utils/formatGroupInfoToNodeMeta';
import { tempalteType } from '@/graphTemplateType';
import ConfigPanel from '@/core/ConfigPanel';
import { UnorderedListOutlined } from '@ant-design/icons';
import '@/graph/registeredNode';
import '@/graph/reactRegisteredNode';

const closeStyle: React.CSSProperties = {
  right: '0px'};const DropTarget = memo(function DropTarget(props) {
  const [visible, setVisible] = useState<boolean> (true);
  const [isRender, setIsRender] = useState<boolean> (false);
  const { width, height } = useOnResize();
  const onClose = () = > setVisible(false);
  const containerRef = useRef<HTMLDivElement | null> (null);
  const keyDown = useKeydown([isRender]);
  const [collectProps, droper] = useDrop({
    accept: 'Box'.collect: (minoter) = > ({
      isOver: minoter.isOver(),
      canDrop: minoter.canDrop(),
      item: minoter.getItem(),
    }),
    drop: (item: tempalteType, monitor) = > {
      // Drag the current offset of the component
      const currentMouseOffset = monitor.getClientOffset();
      // The component is initially dragged at offset
      const sourceMouseOffset = monitor.getInitialClientOffset();
      const sourceElementOffset = monitor.getInitialSourceClientOffset();
      constdiffX = sourceMouseOffset! .x - sourceElementOffset! .x;constdiffY = sourceMouseOffset! .y - sourceElementOffset! .y;constx = currentMouseOffset! .x - diffX;consty = currentMouseOffset! .y - diffY;// Convert actual coordinates like x and y to canvas local coordinates
      const point = FlowGraph.graph.clientToLocal(x, y);
      constcreateNodeData = formatGroupInfoToNodeMeta(item, point); FlowGraph.graph.addNode(createNodeData); }}); useEffect(() = > {
    const graph = FlowGraph.init();
    if (graph) {
      setIsRender(true); }} []); useEffect(() = > {
    if (FlowGraph.isGraphReady()) {
      FlowGraph.graph.resize(width - 300, height);
    }
  }, [width, height]);
  return (
    <div className={style.warp}>
      <div
        ref={(ele)= > {
          containerRef.current = ele;
          droper(ele);
        }}
        className={style.dropTarget}
        id="container"
      ></div>
      <Drawer
        placement="right"
        mask={false}
        onClose={onClose}
        visible={visible}
        width={300}
      >
        <div className={style.config}>{isRender && <ConfigPanel />}</div>
      </Drawer>
      <div
        className={style.close}
        style={! visible ? closeStyle : undefined}
        onClick={()= > setVisible(true)}
      >
        <UnorderedListOutlined />
      </div>
    </div>
  );
});

export default DropTarget;

Copy the code

FormatGroupInfoToNodeMeta function

export const formatGroupInfoToNodeMeta = <T = tempalteType>(
  dropItem: T,
  point: { x: number; y: number },
) => {
  const { category, type } = dropItem as unknown as tempalteType;
  const { x, y } = point;
  let createNode = { x, y, data: dropItem };
  switch (category) {
    case 'base':
      createNode = Object.assign(
        {},
        createNode,
        filterNode(baseGraphNodeList, type),
      );
      break;
    default:
      break;
  }
  return createNode;
};

Copy the code

Tips: the above code is not complete, just a thought, please refer to the project’s source code, address

conclusion

Preview the address

Project code address

Ant-simple-pro is simple, beautiful and easy to use. It supports vue3, React and Angular.