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.