, shouldRender: (context: CP) => boolean = () => true) {
4 | return function(WrappedComponent: React.ComponentType
) {
5 | type WrappedComponentProps = Omit, keyof CP>;
6 | type WrappedComponentPropsWithForwardRef = WrappedComponentProps & {
7 | forwardRef: React.Ref;
8 | };
9 |
10 | const InjectContext: React.FC = props => {
11 | const { forwardRef, ...rest } = props;
12 |
13 | let refProp = {};
14 |
15 | if (WrappedComponent.prototype.isReactComponent) {
16 | refProp = {
17 | ref: forwardRef,
18 | };
19 | } else {
20 | refProp = {
21 | forwardRef,
22 | };
23 | }
24 |
25 | return (
26 |
27 | {context =>
28 | shouldRender(context) ? : null
29 | }
30 |
31 | );
32 | };
33 |
34 | return React.forwardRef((props, ref) => );
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Command/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { EditorEvent } from '@/common/constants';
3 | import CommandManager from '@/common/CommandManager';
4 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext';
5 |
6 | interface CommandProps extends EditorContextProps {
7 | name: string;
8 | className?: string;
9 | disabledClassName?: string;
10 | }
11 |
12 | interface CommandState {}
13 |
14 | class Command extends React.Component {
15 | static defaultProps = {
16 | className: 'command',
17 | disabledClassName: 'command-disabled',
18 | };
19 |
20 | state = {
21 | disabled: false,
22 | };
23 |
24 | componentDidMount() {
25 | const { graph, name } = this.props;
26 |
27 | const commandManager: CommandManager = graph.get('commandManager');
28 |
29 | this.setState({
30 | disabled: !commandManager.canExecute(graph, name),
31 | });
32 |
33 | graph.on(EditorEvent.onGraphStateChange, () => {
34 | this.setState({
35 | disabled: !commandManager.canExecute(graph, name),
36 | });
37 | });
38 | }
39 |
40 | handleClick = () => {
41 | const { name, executeCommand } = this.props;
42 |
43 | executeCommand(name);
44 | };
45 |
46 | render() {
47 | const { graph } = this.props;
48 |
49 | if (!graph) {
50 | return null;
51 | }
52 |
53 | const { className, disabledClassName, children } = this.props;
54 | const { disabled } = this.state;
55 |
56 | return (
57 |
58 | {children}
59 |
60 | );
61 | }
62 | }
63 |
64 | export default withEditorContext(Command);
65 |
--------------------------------------------------------------------------------
/src/components/DetailPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getSelectedNodes, getSelectedEdges } from '@/utils';
3 | import { GraphState, EditorEvent } from '@/common/constants';
4 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext';
5 | import { Node, Edge, GraphStateEvent } from '@/common/interfaces';
6 |
7 | type DetailPanelType = 'node' | 'edge' | 'multi' | 'canvas';
8 |
9 | export interface DetailPanelComponentProps {
10 | type: DetailPanelType;
11 | nodes: Node[];
12 | edges: Edge[];
13 | }
14 |
15 | class DetailPanel {
16 | static create = function(type: DetailPanelType) {
17 | return function(WrappedComponent: React.ComponentType
) {
18 | type TypedPanelProps = EditorContextProps & Omit
;
19 | type TypedPanelState = { graphState: GraphState };
20 |
21 | class TypedPanel extends React.Component {
22 | state = {
23 | graphState: GraphState.CanvasSelected,
24 | };
25 |
26 | componentDidMount() {
27 | const { graph } = this.props;
28 |
29 | graph.on(EditorEvent.onGraphStateChange, ({ graphState }: GraphStateEvent) => {
30 | this.setState({
31 | graphState,
32 | });
33 | });
34 | }
35 |
36 | render() {
37 | const { graph } = this.props;
38 | const { graphState } = this.state;
39 |
40 | if (graphState !== `${type}Selected`) {
41 | return null;
42 | }
43 |
44 | const nodes = getSelectedNodes(graph);
45 | const edges = getSelectedEdges(graph);
46 |
47 | return ;
48 | }
49 | }
50 |
51 | return withEditorContext(TypedPanel);
52 | };
53 | };
54 | }
55 |
56 | export default DetailPanel;
57 |
--------------------------------------------------------------------------------
/src/components/Editor/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import isArray from 'lodash/isArray';
3 | import pick from 'lodash/pick';
4 | import global from '@/common/global';
5 | import { RendererType, EditorEvent, GraphCommonEvent } from '@/common/constants';
6 | import { Graph, CommandEvent } from '@/common/interfaces';
7 | import CommandManager from '@/common/CommandManager';
8 | import {
9 | EditorContext,
10 | EditorPrivateContext,
11 | EditorContextProps,
12 | EditorPrivateContextProps,
13 | } from '@/components/EditorContext';
14 |
15 | interface EditorProps {
16 | style?: React.CSSProperties;
17 | className?: string;
18 | [EditorEvent.onBeforeExecuteCommand]?: (e: CommandEvent) => void;
19 | [EditorEvent.onAfterExecuteCommand]?: (e: CommandEvent) => void;
20 | }
21 |
22 | interface EditorState extends EditorContextProps, EditorPrivateContextProps {}
23 |
24 | class Editor extends React.Component {
25 | static setTrackable(trackable: boolean) {
26 | global.trackable = trackable;
27 | }
28 |
29 | static defaultProps = {
30 | [EditorEvent.onBeforeExecuteCommand]: () => {},
31 | [EditorEvent.onAfterExecuteCommand]: () => {},
32 | };
33 |
34 | lastMousedownTarget: HTMLElement | null = null;
35 |
36 | constructor(props: EditorProps) {
37 | super(props);
38 |
39 | this.state = {
40 | graph: null,
41 | setGraph: this.setGraph,
42 | executeCommand: this.executeCommand,
43 | commandManager: new CommandManager(),
44 | };
45 |
46 | this.lastMousedownTarget = null;
47 | }
48 |
49 | shouldTriggerShortcut(graph: Graph, target: HTMLElement | null) {
50 | const renderer: RendererType = graph.get('renderer');
51 | const canvasElement = graph.get('canvas').get('el');
52 |
53 | if (!target) {
54 | return false;
55 | }
56 |
57 | if (target === canvasElement) {
58 | return true;
59 | }
60 |
61 | if (renderer === RendererType.Svg) {
62 | if (target.nodeName === 'svg') {
63 | return true;
64 | }
65 |
66 | let parentNode = target.parentNode;
67 |
68 | while (parentNode && parentNode.nodeName !== 'BODY') {
69 | if (parentNode.nodeName === 'svg') {
70 | return true;
71 | } else {
72 | parentNode = parentNode.parentNode;
73 | }
74 | }
75 |
76 | return false;
77 | }
78 | }
79 |
80 | bindEvent(graph: Graph) {
81 | const { props } = this;
82 |
83 | graph.on(EditorEvent.onBeforeExecuteCommand, props[EditorEvent.onBeforeExecuteCommand]);
84 | graph.on(EditorEvent.onAfterExecuteCommand, props[EditorEvent.onAfterExecuteCommand]);
85 | }
86 |
87 | bindShortcut(graph: Graph) {
88 | const { commandManager } = this.state;
89 |
90 | window.addEventListener(GraphCommonEvent.onMouseDown, e => {
91 | this.lastMousedownTarget = e.target as HTMLElement;
92 | });
93 |
94 | graph.on(GraphCommonEvent.onKeyDown, (e: any) => {
95 | if (!this.shouldTriggerShortcut(graph, this.lastMousedownTarget)) {
96 | return;
97 | }
98 |
99 | Object.values(commandManager.command).some(command => {
100 | const { name, shortcuts } = command;
101 |
102 | const flag = shortcuts.some((shortcut: string | string[]) => {
103 | const { key } = e;
104 |
105 | if (!isArray(shortcut)) {
106 | return shortcut === key;
107 | }
108 |
109 | return shortcut.every((item, index) => {
110 | if (index === shortcut.length - 1) {
111 | return item === key;
112 | }
113 |
114 | return e[item];
115 | });
116 | });
117 |
118 | if (flag) {
119 | if (commandManager.canExecute(graph, name)) {
120 | // Prevent default
121 | e.preventDefault();
122 |
123 | // Execute command
124 | this.executeCommand(name);
125 |
126 | return true;
127 | }
128 | }
129 |
130 | return false;
131 | });
132 | });
133 | }
134 |
135 | setGraph = (graph: Graph) => {
136 | this.setState({
137 | graph,
138 | });
139 |
140 | this.bindEvent(graph);
141 | this.bindShortcut(graph);
142 | };
143 |
144 | executeCommand = (name: string, params?: object) => {
145 | const { graph, commandManager } = this.state;
146 |
147 | if (graph) {
148 | commandManager.execute(graph, name, params);
149 | }
150 | };
151 |
152 | render() {
153 | const { children } = this.props;
154 | const { graph, setGraph, executeCommand, commandManager } = this.state;
155 |
156 | return (
157 |
164 |
170 | {children}
171 |
172 |
173 | );
174 | }
175 | }
176 |
177 | export default Editor;
178 |
--------------------------------------------------------------------------------
/src/components/EditorContext/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Graph } from '@/common/interfaces';
3 | import withContext from '@/common/withContext';
4 | import CommandManager from '@/common/CommandManager';
5 |
6 | export interface EditorContextProps {
7 | graph: Graph | null;
8 | executeCommand: (name: string, params?: object) => void;
9 | commandManager: CommandManager;
10 | }
11 |
12 | export interface EditorPrivateContextProps {
13 | setGraph: (graph: Graph) => void;
14 | commandManager: CommandManager;
15 | }
16 |
17 | export const EditorContext = React.createContext({} as EditorContextProps);
18 | export const EditorPrivateContext = React.createContext({} as EditorPrivateContextProps);
19 |
20 | export const withEditorContext = withContext(EditorContext, context => !!context.graph);
21 | export const withEditorPrivateContext = withContext(EditorPrivateContext);
22 |
--------------------------------------------------------------------------------
/src/components/Flow/behavior/dragAddEdge.ts:
--------------------------------------------------------------------------------
1 | import isPlainObject from 'lodash/isPlainObject';
2 | import { guid } from '@/utils';
3 | import { ItemType, ItemState, GraphType, AnchorPointState, GraphCustomEvent } from '@/common/constants';
4 | import { Node, Edge, Behavior, GraphEvent, EdgeModel, AnchorPoint } from '@/common/interfaces';
5 | import behaviorManager from '@/common/behaviorManager';
6 |
7 | interface DragAddEdgeBehavior extends Behavior {
8 | edge: Edge | null;
9 | isEnabledAnchorPoint(e: GraphEvent): boolean;
10 | isNotSelf(e: GraphEvent): boolean;
11 | canFindTargetAnchorPoint(e: GraphEvent): boolean;
12 | shouldAddDelegateEdge(e: GraphEvent): boolean;
13 | shouldAddRealEdge(): boolean;
14 | handleNodeMouseEnter(e: GraphEvent): void;
15 | handleNodeMouseLeave(e: GraphEvent): void;
16 | handleNodeMouseDown(e: GraphEvent): void;
17 | handleMouseMove(e: GraphEvent): void;
18 | handleMouseUp(e: GraphEvent): void;
19 | }
20 |
21 | interface DefaultConfig {
22 | /** 边线类型 */
23 | edgeType: string;
24 | /** 获取来源节点锚点状态 */
25 | getAnchorPointStateOfSourceNode(sourceNode: Node, sourceAnchorPoint: AnchorPoint): AnchorPointState;
26 | /** 获取目标节点锚点状态 */
27 | getAnchorPointStateOfTargetNode(
28 | sourceNode: Node,
29 | sourceAnchorPoint: AnchorPoint,
30 | targetNode: Node,
31 | targetAnchorPoint: AnchorPoint,
32 | ): AnchorPointState;
33 | }
34 |
35 | const dragAddEdgeBehavior: DragAddEdgeBehavior & ThisType = {
36 | edge: null,
37 |
38 | graphType: GraphType.Flow,
39 |
40 | getDefaultCfg(): DefaultConfig {
41 | return {
42 | edgeType: 'bizFlowEdge',
43 | getAnchorPointStateOfSourceNode: () => AnchorPointState.Enabled,
44 | getAnchorPointStateOfTargetNode: () => AnchorPointState.Enabled,
45 | };
46 | },
47 |
48 | getEvents() {
49 | return {
50 | 'node:mouseenter': 'handleNodeMouseEnter',
51 | 'node:mouseleave': 'handleNodeMouseLeave',
52 | 'node:mousedown': 'handleNodeMouseDown',
53 | mousemove: 'handleMouseMove',
54 | mouseup: 'handleMouseUp',
55 | };
56 | },
57 |
58 | isEnabledAnchorPoint(e) {
59 | const { target } = e;
60 |
61 | return !!target.get('isAnchorPoint') && target.get('anchorPointState') === AnchorPointState.Enabled;
62 | },
63 |
64 | isNotSelf(e) {
65 | const { edge } = this;
66 | const { item } = e;
67 |
68 | return item.getModel().id !== edge.getSource().getModel().id;
69 | },
70 |
71 | getTargetNodes(sourceId: string) {
72 | const { graph } = this;
73 |
74 | const nodes = graph.getNodes();
75 |
76 | return nodes.filter(node => node.getModel().id !== sourceId);
77 | },
78 |
79 | canFindTargetAnchorPoint(e) {
80 | return this.isEnabledAnchorPoint(e) && this.isNotSelf(e);
81 | },
82 |
83 | shouldAddDelegateEdge(e) {
84 | return this.isEnabledAnchorPoint(e);
85 | },
86 |
87 | shouldAddRealEdge() {
88 | const { edge } = this;
89 |
90 | const target = edge.getTarget();
91 |
92 | return !isPlainObject(target);
93 | },
94 |
95 | handleNodeMouseEnter(e) {
96 | const { graph, getAnchorPointStateOfSourceNode } = this;
97 |
98 | const sourceNode = e.item as Node;
99 | const sourceAnchorPoints = sourceNode.getAnchorPoints() as AnchorPoint[];
100 | const sourceAnchorPointsState = [];
101 |
102 | sourceAnchorPoints.forEach(sourceAnchorPoint => {
103 | sourceAnchorPointsState.push(getAnchorPointStateOfSourceNode(sourceNode, sourceAnchorPoint));
104 | });
105 |
106 | sourceNode.set('anchorPointsState', sourceAnchorPointsState);
107 |
108 | graph.setItemState(sourceNode, ItemState.ActiveAnchorPoints, true);
109 | },
110 |
111 | handleNodeMouseLeave(e) {
112 | const { graph, edge } = this;
113 | const { item } = e;
114 |
115 | if (!edge) {
116 | item.set('anchorPointsState', []);
117 | graph.setItemState(item, ItemState.ActiveAnchorPoints, false);
118 | }
119 | },
120 |
121 | handleNodeMouseDown(e) {
122 | if (!this.shouldBegin(e) || !this.shouldAddDelegateEdge(e)) {
123 | return;
124 | }
125 |
126 | const { graph, edgeType, getAnchorPointStateOfTargetNode } = this;
127 | const { target } = e;
128 |
129 | const sourceNode = e.item as Node;
130 | const sourceNodeId = sourceNode.getModel().id;
131 | const sourceAnchorPointIndex = target.get('anchorPointIndex');
132 | const sourceAnchorPoint = sourceNode.getAnchorPoints()[sourceAnchorPointIndex] as AnchorPoint;
133 |
134 | const model: EdgeModel = {
135 | id: guid(),
136 | type: edgeType,
137 | source: sourceNodeId,
138 | sourceAnchor: sourceAnchorPointIndex,
139 | target: {
140 | x: e.x,
141 | y: e.y,
142 | } as any,
143 | };
144 |
145 | this.edge = graph.addItem(ItemType.Edge, model);
146 |
147 | graph.getNodes().forEach(targetNode => {
148 | if (targetNode.getModel().id === sourceNodeId) {
149 | return;
150 | }
151 |
152 | const targetAnchorPoints = targetNode.getAnchorPoints() as AnchorPoint[];
153 | const targetAnchorPointsState = [];
154 |
155 | targetAnchorPoints.forEach(targetAnchorPoint => {
156 | targetAnchorPointsState.push(
157 | getAnchorPointStateOfTargetNode(sourceNode, sourceAnchorPoint, targetNode, targetAnchorPoint),
158 | );
159 | });
160 |
161 | targetNode.set('anchorPointsState', targetAnchorPointsState);
162 |
163 | graph.setItemState(targetNode, ItemState.ActiveAnchorPoints, true);
164 | });
165 | },
166 |
167 | handleMouseMove(e) {
168 | const { graph, edge } = this;
169 |
170 | if (!edge) {
171 | return;
172 | }
173 |
174 | if (this.canFindTargetAnchorPoint(e)) {
175 | const { item, target } = e;
176 |
177 | const targetId = item.getModel().id;
178 | const targetAnchor = target.get('anchorPointIndex');
179 |
180 | graph.updateItem(edge, {
181 | target: targetId,
182 | targetAnchor,
183 | });
184 | } else {
185 | graph.updateItem(edge, {
186 | target: {
187 | x: e.x,
188 | y: e.y,
189 | } as any,
190 | targetAnchor: undefined,
191 | });
192 | }
193 | },
194 |
195 | handleMouseUp() {
196 | const { graph, edge } = this;
197 |
198 | if (!edge) {
199 | return;
200 | }
201 |
202 | if (!this.shouldAddRealEdge()) {
203 | graph.removeItem(this.edge);
204 | }
205 |
206 | graph.emit(GraphCustomEvent.onAfterConnect, {
207 | edge: this.edge,
208 | });
209 |
210 | this.edge = null;
211 |
212 | graph.getNodes().forEach(node => {
213 | node.set('anchorPointsState', []);
214 | graph.setItemState(node, ItemState.ActiveAnchorPoints, false);
215 | });
216 | },
217 | };
218 |
219 | behaviorManager.register('drag-add-edge', dragAddEdgeBehavior);
220 |
--------------------------------------------------------------------------------
/src/components/Flow/behavior/dragAddNode.ts:
--------------------------------------------------------------------------------
1 | import isArray from 'lodash/isArray';
2 | import { guid } from '@/utils';
3 | import global from '@/common/global';
4 | import { ItemType, GraphType, GraphMode, EditorCommand } from '@/common/constants';
5 | import { GShape, GGroup, NodeModel, Behavior, GraphEvent } from '@/common/interfaces';
6 | import CommandManager from '@/common/CommandManager';
7 | import behaviorManager from '@/common/behaviorManager';
8 |
9 | interface DragAddNodeBehavior extends Behavior {
10 | shape: GShape | null;
11 | handleCanvasMouseEnter(e: GraphEvent): void;
12 | handleMouseMove(e: GraphEvent): void;
13 | handleMouseUp(e: GraphEvent): void;
14 | }
15 |
16 | const dragAddNodeBehavior: DragAddNodeBehavior = {
17 | shape: null,
18 |
19 | graphType: GraphType.Flow,
20 |
21 | graphMode: GraphMode.AddNode,
22 |
23 | getEvents() {
24 | return {
25 | 'canvas:mouseenter': 'handleCanvasMouseEnter',
26 | mousemove: 'handleMouseMove',
27 | mouseup: 'handleMouseUp',
28 | };
29 | },
30 |
31 | handleCanvasMouseEnter(e) {
32 | const { graph, shape } = this;
33 |
34 | if (shape) {
35 | return;
36 | }
37 |
38 | const group: GGroup = graph.get('group');
39 | const model: Partial = global.component.itemPanel.model;
40 |
41 | const { size = 100 } = model;
42 |
43 | let width = 0;
44 | let height = 0;
45 |
46 | if (isArray(size)) {
47 | width = size[0];
48 | height = size[1];
49 | } else {
50 | width = size;
51 | height = size;
52 | }
53 |
54 | const x = e.x - width / 2;
55 | const y = e.y - height / 2;
56 |
57 | this.shape = group.addShape('rect', {
58 | className: global.component.itemPanel.delegateShapeClassName,
59 | attrs: {
60 | x,
61 | y,
62 | width,
63 | height,
64 | fill: '#f3f9ff',
65 | fillOpacity: 0.5,
66 | stroke: '#1890ff',
67 | strokeOpacity: 0.9,
68 | lineDash: [5, 5],
69 | },
70 | });
71 |
72 | graph.paint();
73 | },
74 |
75 | handleMouseMove(e) {
76 | const { graph } = this;
77 | const { width, height } = this.shape.getBBox();
78 |
79 | const x = e.x - width / 2;
80 | const y = e.y - height / 2;
81 |
82 | this.shape.attr({
83 | x,
84 | y,
85 | });
86 |
87 | graph.paint();
88 | },
89 |
90 | handleMouseUp(e) {
91 | const { graph } = this;
92 | const { width, height } = this.shape.getBBox();
93 |
94 | let x = e.x;
95 | let y = e.y;
96 |
97 | const model: Partial = global.component.itemPanel.model;
98 |
99 | if (model.center === 'topLeft') {
100 | x -= width / 2;
101 | y -= height / 2;
102 | }
103 |
104 | this.shape.remove(true);
105 |
106 | const commandManager: CommandManager = graph.get('commandManager');
107 |
108 | commandManager.execute(graph, EditorCommand.Add, {
109 | type: ItemType.Node,
110 | model: {
111 | id: guid(),
112 | x,
113 | y,
114 | ...model,
115 | },
116 | });
117 | },
118 | };
119 |
120 | behaviorManager.register('drag-add-node', dragAddNodeBehavior);
121 |
--------------------------------------------------------------------------------
/src/components/Flow/behavior/index.ts:
--------------------------------------------------------------------------------
1 | import './dragAddNode';
2 | import './dragAddEdge';
3 |
--------------------------------------------------------------------------------
/src/components/Flow/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import omit from 'lodash/omit';
3 | import merge from 'lodash/merge';
4 | import G6 from '@antv/g6';
5 | import { guid } from '@/utils';
6 | import global from '@/common/global';
7 | import { FLOW_CONTAINER_ID, GraphType } from '@/common/constants';
8 | import { Graph, GraphOptions, FlowData, GraphEvent, GraphReactEventProps } from '@/common/interfaces';
9 | import behaviorManager from '@/common/behaviorManager';
10 | import GraphComponent from '@/components/Graph';
11 |
12 | import './behavior';
13 |
14 | interface FlowProps extends Partial {
15 | style?: React.CSSProperties;
16 | className?: string;
17 | data: FlowData;
18 | graphConfig?: Partial;
19 | customModes?: (mode: string, behaviors: any) => object;
20 | }
21 |
22 | interface FlowState {}
23 |
24 | class Flow extends React.Component {
25 | static defaultProps = {
26 | graphConfig: {},
27 | };
28 |
29 | graph: Graph | null = null;
30 |
31 | containerId = `${FLOW_CONTAINER_ID}_${guid()}`;
32 |
33 | canDragNode = (e: GraphEvent) => {
34 | return !['anchor', 'banAnchor'].some(item => item === e.target.get('className'));
35 | };
36 |
37 | canDragOrZoomCanvas = () => {
38 | const { graph } = this;
39 |
40 | if (!graph) {
41 | return false;
42 | }
43 |
44 | return (
45 | global.plugin.itemPopover.state === 'hide' &&
46 | global.plugin.contextMenu.state === 'hide' &&
47 | global.plugin.editableLabel.state === 'hide'
48 | );
49 | };
50 |
51 | parseData = data => {
52 | const { nodes, edges } = data;
53 |
54 | [...nodes, ...edges].forEach(item => {
55 | const { id } = item;
56 |
57 | if (id) {
58 | return;
59 | }
60 |
61 | item.id = guid();
62 | });
63 | };
64 |
65 | initGraph = (width: number, height: number) => {
66 | const { containerId } = this;
67 | const { graphConfig, customModes } = this.props;
68 |
69 | const modes: any = merge(behaviorManager.getRegisteredBehaviors(GraphType.Flow), {
70 | default: {
71 | 'drag-node': {
72 | type: 'drag-node',
73 | enableDelegate: true,
74 | shouldBegin: this.canDragNode,
75 | },
76 | 'drag-canvas': {
77 | type: 'drag-canvas',
78 | shouldBegin: this.canDragOrZoomCanvas,
79 | shouldUpdate: this.canDragOrZoomCanvas,
80 | },
81 | 'zoom-canvas': {
82 | type: 'zoom-canvas',
83 | shouldUpdate: this.canDragOrZoomCanvas,
84 | },
85 | 'recall-edge': 'recall-edge',
86 | 'brush-select': 'brush-select',
87 | },
88 | });
89 |
90 | Object.keys(modes).forEach(mode => {
91 | const behaviors = modes[mode];
92 |
93 | modes[mode] = Object.values(customModes ? customModes(mode, behaviors) : behaviors);
94 | });
95 |
96 | this.graph = new G6.Graph({
97 | container: containerId,
98 | width,
99 | height,
100 | modes,
101 | defaultNode: {
102 | type: 'bizFlowNode',
103 | },
104 | defaultEdge: {
105 | type: 'bizFlowEdge',
106 | },
107 | ...graphConfig,
108 | });
109 |
110 | return this.graph;
111 | };
112 |
113 | render() {
114 | const { containerId, parseData, initGraph } = this;
115 |
116 | return (
117 |
123 | );
124 | }
125 | }
126 |
127 | export default Flow;
128 |
--------------------------------------------------------------------------------
/src/components/Graph/behavior/clickItem.ts:
--------------------------------------------------------------------------------
1 | import { isMind, isEdge, getGraphState, clearSelectedState } from '@/utils';
2 | import { ItemState, GraphState, EditorEvent } from '@/common/constants';
3 | import { Item, Behavior } from '@/common/interfaces';
4 | import behaviorManager from '@/common/behaviorManager';
5 |
6 | interface ClickItemBehavior extends Behavior {
7 | /** 处理点击事件 */
8 | handleItemClick({ item }: { item: Item }): void;
9 | /** 处理画布点击 */
10 | handleCanvasClick(): void;
11 | /** 处理按键按下 */
12 | handleKeyDown(e: KeyboardEvent): void;
13 | /** 处理按键抬起 */
14 | handleKeyUp(e: KeyboardEvent): void;
15 | }
16 |
17 | interface DefaultConfig {
18 | /** 是否支持多选 */
19 | multiple: boolean;
20 | /** 是否按下多选 */
21 | keydown: boolean;
22 | /** 多选按键码值 */
23 | keyCode: number;
24 | }
25 |
26 | const clickItemBehavior: ClickItemBehavior & ThisType = {
27 | getDefaultCfg(): DefaultConfig {
28 | return {
29 | multiple: true,
30 | keydown: false,
31 | keyCode: 17,
32 | };
33 | },
34 |
35 | getEvents() {
36 | return {
37 | 'node:click': 'handleItemClick',
38 | 'edge:click': 'handleItemClick',
39 | 'canvas:click': 'handleCanvasClick',
40 | keydown: 'handleKeyDown',
41 | keyup: 'handleKeyUp',
42 | };
43 | },
44 |
45 | handleItemClick({ item }) {
46 | const { graph } = this;
47 |
48 | if (isMind(graph) && isEdge(item)) {
49 | return;
50 | }
51 |
52 | const isSelected = item.hasState(ItemState.Selected);
53 |
54 | if (this.multiple && this.keydown) {
55 | graph.setItemState(item, ItemState.Selected, !isSelected);
56 | } else {
57 | clearSelectedState(graph, selectedItem => {
58 | return selectedItem !== item;
59 | });
60 |
61 | if (!isSelected) {
62 | graph.setItemState(item, ItemState.Selected, true);
63 | }
64 | }
65 |
66 | graph.emit(EditorEvent.onGraphStateChange, {
67 | graphState: getGraphState(graph),
68 | });
69 | },
70 |
71 | handleCanvasClick() {
72 | const { graph } = this;
73 |
74 | clearSelectedState(graph);
75 |
76 | graph.emit(EditorEvent.onGraphStateChange, {
77 | graphState: GraphState.CanvasSelected,
78 | });
79 | },
80 |
81 | handleKeyDown(e) {
82 | this.keydown = (e.keyCode || e.which) === this.keyCode;
83 | },
84 |
85 | handleKeyUp() {
86 | this.keydown = false;
87 | },
88 | };
89 |
90 | behaviorManager.register('click-item', clickItemBehavior);
91 |
--------------------------------------------------------------------------------
/src/components/Graph/behavior/dragCanvas.ts:
--------------------------------------------------------------------------------
1 | import { Behavior, GraphEvent } from '@/common/interfaces';
2 | import behaviorManager from '@/common/behaviorManager';
3 |
4 | interface DragCanvasBehavior extends Behavior {
5 | /** 开始拖拽坐标 */
6 | origin: {
7 | x: number;
8 | y: number;
9 | } | null;
10 | /** 当前按键码值 */
11 | keyCode: number | null;
12 | /** 正在拖拽标识 */
13 | dragging: boolean;
14 | /** 是否能够拖拽 */
15 | canDrag(): boolean;
16 | /** 更新当前窗口 */
17 | updateViewport(e: GraphEvent): void;
18 | /** 处理画布拖拽开始 */
19 | handleCanvasDragStart(e: GraphEvent): void;
20 | /** 处理画布拖拽 */
21 | handleCanvasDrag(e: GraphEvent): void;
22 | /** 处理画布拖拽结束 */
23 | handleCanvasDragEnd(e?: GraphEvent): void;
24 | /** 处理窗口鼠标弹起 */
25 | handleWindowMouseUp: (e: MouseEvent) => void | null;
26 | /** 处理鼠标移出画布 */
27 | handleCanvasMouseLeave(e: GraphEvent): void;
28 | /** 处理画布鼠标右键 */
29 | handleCanvasContextMenu(e: GraphEvent): void;
30 | /** 处理按键按下 */
31 | handleKeyDown(e: KeyboardEvent): void;
32 | /** 处理按键抬起 */
33 | handleKeyUp(e: KeyboardEvent): void;
34 | }
35 |
36 | interface DefaultConfig {
37 | /** 允许拖拽 KeyCode */
38 | allowKeyCode: number[];
39 | /** 禁止拖拽 KeyCode */
40 | notAllowKeyCode: number[];
41 | }
42 |
43 | const dragCanvasBehavior: DragCanvasBehavior & ThisType = {
44 | origin: null,
45 |
46 | keyCode: null,
47 |
48 | dragging: false,
49 |
50 | handleWindowMouseUp: null,
51 |
52 | getDefaultCfg(): DefaultConfig {
53 | return {
54 | allowKeyCode: [],
55 | notAllowKeyCode: [16],
56 | };
57 | },
58 |
59 | getEvents() {
60 | return {
61 | 'canvas:dragstart': 'handleCanvasDragStart',
62 | 'canvas:drag': 'handleCanvasDrag',
63 | 'canvas:dragend': 'handleCanvasDragEnd',
64 | 'canvas:mouseleave': 'handleCanvasMouseLeave',
65 | 'canvas:contextmenu': 'handleCanvasContextMenu',
66 | keydown: 'handleKeyDown',
67 | keyup: 'handleKeyUp',
68 | };
69 | },
70 |
71 | canDrag() {
72 | const { keyCode, allowKeyCode, notAllowKeyCode } = this;
73 |
74 | let isAllow = !!!allowKeyCode.length;
75 |
76 | if (!keyCode) {
77 | return isAllow;
78 | }
79 |
80 | if (allowKeyCode.length && allowKeyCode.includes(keyCode)) {
81 | isAllow = true;
82 | }
83 |
84 | if (notAllowKeyCode.includes(keyCode)) {
85 | isAllow = false;
86 | }
87 |
88 | return isAllow;
89 | },
90 |
91 | updateViewport(e) {
92 | const { clientX, clientY } = e;
93 |
94 | const dx = clientX - this.origin.x;
95 | const dy = clientY - this.origin.y;
96 |
97 | this.origin = {
98 | x: clientX,
99 | y: clientY,
100 | };
101 |
102 | this.graph.translate(dx, dy);
103 | this.graph.paint();
104 | },
105 |
106 | handleCanvasDragStart(e) {
107 | if (!this.shouldBegin.call(this, e)) {
108 | return;
109 | }
110 |
111 | if (!this.canDrag()) {
112 | return;
113 | }
114 |
115 | this.origin = {
116 | x: e.clientX,
117 | y: e.clientY,
118 | };
119 |
120 | this.dragging = false;
121 | },
122 |
123 | handleCanvasDrag(e) {
124 | if (!this.shouldUpdate.call(this, e)) {
125 | return;
126 | }
127 |
128 | if (!this.canDrag()) {
129 | return;
130 | }
131 |
132 | if (!this.origin) {
133 | return;
134 | }
135 |
136 | if (!this.dragging) {
137 | this.dragging = true;
138 | } else {
139 | this.updateViewport(e);
140 | }
141 | },
142 |
143 | handleCanvasDragEnd(e) {
144 | if (!this.shouldEnd.call(this, e)) {
145 | return;
146 | }
147 |
148 | if (!this.canDrag()) {
149 | return;
150 | }
151 |
152 | this.origin = null;
153 | this.dragging = false;
154 |
155 | if (this.handleWindowMouseUp) {
156 | document.body.removeEventListener('mouseup', this.handleWindowMouseUp, false);
157 | this.handleWindowMouseUp = null;
158 | }
159 | },
160 |
161 | handleCanvasMouseLeave() {
162 | const canvasElement = this.graph.get('canvas').get('el');
163 |
164 | if (this.handleWindowMouseUp) {
165 | return;
166 | }
167 |
168 | this.handleWindowMouseUp = e => {
169 | if (e.target !== canvasElement) {
170 | this.handleCanvasDragEnd();
171 | }
172 | };
173 |
174 | document.body.addEventListener('mouseup', this.handleWindowMouseUp, false);
175 | },
176 |
177 | handleCanvasContextMenu() {
178 | this.origin = null;
179 | this.dragging = false;
180 | },
181 |
182 | handleKeyDown(e) {
183 | this.keyCode = e.keyCode || e.which;
184 | },
185 |
186 | handleKeyUp() {
187 | this.keyCode = null;
188 | },
189 | };
190 |
191 | behaviorManager.register('drag-canvas', dragCanvasBehavior);
192 |
--------------------------------------------------------------------------------
/src/components/Graph/behavior/hoverItem.ts:
--------------------------------------------------------------------------------
1 | import { ItemState } from '@/common/constants';
2 | import { Item, Behavior } from '@/common/interfaces';
3 | import behaviorManager from '@/common/behaviorManager';
4 |
5 | interface HoverItemBehavior extends Behavior {
6 | /** 处理鼠标进入 */
7 | handleItemMouseenter({ item }: { item: Item }): void;
8 | /** 处理鼠标移出 */
9 | handleItemMouseleave({ item }: { item: Item }): void;
10 | }
11 |
12 | const hoverItemBehavior: HoverItemBehavior = {
13 | getEvents() {
14 | return {
15 | 'node:mouseenter': 'handleItemMouseenter',
16 | 'edge:mouseenter': 'handleItemMouseenter',
17 | 'node:mouseleave': 'handleItemMouseleave',
18 | 'edge:mouseleave': 'handleItemMouseleave',
19 | };
20 | },
21 |
22 | handleItemMouseenter({ item }) {
23 | const { graph } = this;
24 |
25 | graph.setItemState(item, ItemState.Active, true);
26 | },
27 |
28 | handleItemMouseleave({ item }) {
29 | const { graph } = this;
30 |
31 | graph.setItemState(item, ItemState.Active, false);
32 | },
33 | };
34 |
35 | behaviorManager.register('hover-item', hoverItemBehavior);
36 |
--------------------------------------------------------------------------------
/src/components/Graph/behavior/index.ts:
--------------------------------------------------------------------------------
1 | import './clickItem';
2 | import './hoverItem';
3 | import './dragCanvas';
4 | import './recallEdge';
5 |
--------------------------------------------------------------------------------
/src/components/Graph/behavior/recallEdge.ts:
--------------------------------------------------------------------------------
1 | import { isFlow, isMind, getFlowRecallEdges, getMindRecallEdges, executeBatch } from '@/utils';
2 | import { ItemState } from '@/common/constants';
3 | import { TreeGraph, Node, Edge, Behavior, GraphEvent } from '@/common/interfaces';
4 | import behaviorManager from '@/common/behaviorManager';
5 |
6 | interface RecallEdgeBehavior extends Behavior {
7 | /** 当前高亮边线 Id */
8 | edgeIds: string[];
9 | /** 设置高亮状态 */
10 | setHighLightState(edges: Edge[]): void;
11 | /** 清除高亮状态 */
12 | clearHighLightState(): void;
13 | /** 处理节点点击 */
14 | handleNodeClick(e: GraphEvent): void;
15 | /** 处理边线点击 */
16 | handleEdgeClick(e: GraphEvent): void;
17 | /** 处理画布点击 */
18 | handleCanvasClick(e: GraphEvent): void;
19 | }
20 |
21 | const recallEdgeBehavior: RecallEdgeBehavior = {
22 | edgeIds: [],
23 |
24 | getEvents() {
25 | return {
26 | 'node:click': 'handleNodeClick',
27 | 'edge:click': 'handleEdgeClick',
28 | 'canvas:click': 'handleCanvasClick',
29 | };
30 | },
31 |
32 | setHighLightState(edges: Edge[]) {
33 | const { graph } = this;
34 |
35 | this.clearHighLightState();
36 |
37 | executeBatch(graph, () => {
38 | edges.forEach(item => {
39 | graph.setItemState(item, ItemState.HighLight, true);
40 | });
41 | });
42 |
43 | this.edgeIds = edges.map(edge => edge.get('id'));
44 | },
45 |
46 | clearHighLightState() {
47 | const { graph } = this;
48 |
49 | executeBatch(graph, () => {
50 | this.edgeIds.forEach(id => {
51 | const item = graph.findById(id);
52 |
53 | if (item && !item.destroyed) {
54 | graph.setItemState(item, ItemState.HighLight, false);
55 | }
56 | });
57 | });
58 |
59 | this.edgeIds = [];
60 | },
61 |
62 | handleNodeClick({ item }) {
63 | const { graph } = this;
64 |
65 | let edges: Edge[] = [];
66 |
67 | if (isFlow(graph)) {
68 | edges = getFlowRecallEdges(graph, item as Node);
69 | }
70 |
71 | if (isMind(graph)) {
72 | edges = getMindRecallEdges(graph as TreeGraph, item as Node);
73 | }
74 |
75 | this.setHighLightState(edges);
76 | },
77 |
78 | handleEdgeClick() {
79 | this.clearHighLightState();
80 | },
81 |
82 | handleCanvasClick() {
83 | this.clearHighLightState();
84 | },
85 | };
86 |
87 | behaviorManager.register('recall-edge', recallEdgeBehavior);
88 |
--------------------------------------------------------------------------------
/src/components/Graph/command/add.ts:
--------------------------------------------------------------------------------
1 | import { guid } from '@/utils';
2 | import { ItemType } from '@/common/constants';
3 | import { NodeModel, EdgeModel } from '@/common/interfaces';
4 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base';
5 |
6 | export interface AddCommandParams {
7 | type: ItemType;
8 | model: NodeModel | EdgeModel;
9 | }
10 |
11 | const addCommand: BaseCommand = {
12 | ...baseCommand,
13 |
14 | params: {
15 | type: ItemType.Node,
16 | model: {
17 | id: '',
18 | },
19 | },
20 |
21 | init() {
22 | const { model } = this.params;
23 |
24 | if (model.id) {
25 | return;
26 | }
27 |
28 | model.id = guid();
29 | },
30 |
31 | execute(graph) {
32 | const { type, model } = this.params;
33 |
34 | graph.add(type, model);
35 |
36 | this.setSelectedItems(graph, [model.id]);
37 | },
38 |
39 | undo(graph) {
40 | const { model } = this.params;
41 |
42 | graph.remove(model.id);
43 | },
44 | };
45 |
46 | export default addCommand;
47 |
--------------------------------------------------------------------------------
/src/components/Graph/command/base.ts:
--------------------------------------------------------------------------------
1 | import { isMind, getSelectedNodes, getSelectedEdges, setSelectedItems } from '@/utils';
2 | import { LabelState, EditorEvent } from '@/common/constants';
3 | import { Graph, Item, Node, Edge, Command } from '@/common/interfaces';
4 |
5 | export interface BaseCommand extends Command
{
6 | /** 判断是否脑图 */
7 | isMind(graph: G): boolean;
8 | /** 获取选中节点 */
9 | getSelectedNodes(graph: G): Node[];
10 | /** 获取选中连线 */
11 | getSelectedEdges(graph: G): Edge[];
12 | /** 设置选中元素 */
13 | setSelectedItems(graph: G, items: Item[] | string[]): void;
14 | /** 编辑选中节点 */
15 | editSelectedNode(graph: G): void;
16 | }
17 |
18 | export const baseCommand: BaseCommand = {
19 | name: '',
20 |
21 | params: {},
22 |
23 | canExecute() {
24 | return true;
25 | },
26 |
27 | shouldExecute() {
28 | return true;
29 | },
30 |
31 | canUndo() {
32 | return true;
33 | },
34 |
35 | init() {},
36 |
37 | execute() {},
38 |
39 | undo() {},
40 |
41 | shortcuts: [],
42 |
43 | isMind,
44 |
45 | getSelectedNodes,
46 |
47 | getSelectedEdges,
48 |
49 | setSelectedItems,
50 |
51 | editSelectedNode(graph) {
52 | graph.emit(EditorEvent.onLabelStateChange, {
53 | labelState: LabelState.Show,
54 | });
55 | },
56 | };
57 |
--------------------------------------------------------------------------------
/src/components/Graph/command/copy.ts:
--------------------------------------------------------------------------------
1 | import cloneDeep from 'lodash/cloneDeep';
2 | import global from '@/common/global';
3 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base';
4 | import { NodeModel } from '@/common/interfaces';
5 |
6 | const copyCommand: BaseCommand = {
7 | ...baseCommand,
8 |
9 | canExecute(graph) {
10 | return !!this.getSelectedNodes(graph).length;
11 | },
12 |
13 | canUndo() {
14 | return false;
15 | },
16 |
17 | execute(graph) {
18 | const selectedNodes = this.getSelectedNodes(graph);
19 |
20 | global.clipboard.models = cloneDeep(selectedNodes.map(node => node.getModel() as NodeModel));
21 | },
22 |
23 | shortcuts: [
24 | ['metaKey', 'c'],
25 | ['ctrlKey', 'c'],
26 | ],
27 | };
28 |
29 | export default copyCommand;
30 |
--------------------------------------------------------------------------------
/src/components/Graph/command/index.ts:
--------------------------------------------------------------------------------
1 | import redo from './redo';
2 | import undo from './undo';
3 | import add from './add';
4 | import remove from './remove';
5 | import update from './update';
6 | import copy from './copy';
7 | import paste from './paste';
8 | import pasteHere from './pasteHere';
9 | import zoomIn from './zoomIn';
10 | import zoomOut from './zoomOut';
11 |
12 | export default { redo, undo, add, remove, update, copy, paste, pasteHere, zoomIn, zoomOut };
13 |
--------------------------------------------------------------------------------
/src/components/Graph/command/paste.ts:
--------------------------------------------------------------------------------
1 | import { guid, executeBatch } from '@/utils';
2 | import global from '@/common/global';
3 | import { ItemType } from '@/common/constants';
4 | import { NodeModel } from '@/common/interfaces';
5 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base';
6 |
7 | export interface PasteCommandParams {
8 | models: NodeModel[];
9 | }
10 |
11 | const pasteCommand: BaseCommand = {
12 | ...baseCommand,
13 |
14 | params: {
15 | models: [],
16 | },
17 |
18 | canExecute() {
19 | return !!global.clipboard.models.length;
20 | },
21 |
22 | init() {
23 | const { models } = global.clipboard;
24 |
25 | const offsetX = 10;
26 | const offsetY = 10;
27 |
28 | this.params = {
29 | models: models.map(model => {
30 | const { x, y } = model;
31 |
32 | return {
33 | ...model,
34 | id: guid(),
35 | x: x + offsetX,
36 | y: y + offsetY,
37 | };
38 | }),
39 | };
40 | },
41 |
42 | execute(graph) {
43 | const { models } = this.params;
44 |
45 | executeBatch(graph, () => {
46 | models.forEach(model => {
47 | graph.addItem(ItemType.Node, model);
48 | });
49 | });
50 |
51 | this.setSelectedItems(
52 | graph,
53 | models.map(model => model.id),
54 | );
55 | },
56 |
57 | undo(graph) {
58 | const { models } = this.params;
59 |
60 | executeBatch(graph, () => {
61 | models.forEach(model => {
62 | graph.removeItem(model.id);
63 | });
64 | });
65 | },
66 |
67 | shortcuts: [
68 | ['metaKey', 'v'],
69 | ['ctrlKey', 'v'],
70 | ],
71 | };
72 |
73 | export default pasteCommand;
74 |
--------------------------------------------------------------------------------
/src/components/Graph/command/pasteHere.ts:
--------------------------------------------------------------------------------
1 | import { guid } from '@/utils';
2 | import global from '@/common/global';
3 | import { NodeModel } from '@/common/interfaces';
4 | import { BaseCommand } from '@/components/Graph/command/base';
5 | import pasteCommand from './paste';
6 |
7 | export interface PasteHereCommandParams {
8 | models: NodeModel[];
9 | }
10 |
11 | const pasteHereCommand: BaseCommand = {
12 | ...pasteCommand,
13 |
14 | params: {
15 | models: [],
16 | },
17 |
18 | init() {
19 | const { point, models } = global.clipboard;
20 |
21 | this.params = {
22 | models: models.map(model => {
23 | const { x, y } = model;
24 |
25 | const offsetX = point.x - x;
26 | const offsetY = point.y - y;
27 |
28 | return {
29 | ...model,
30 | id: guid(),
31 | x: x + offsetX,
32 | y: y + offsetY,
33 | };
34 | }),
35 | };
36 | },
37 |
38 | shortcuts: [],
39 | };
40 |
41 | export default pasteHereCommand;
42 |
--------------------------------------------------------------------------------
/src/components/Graph/command/redo.ts:
--------------------------------------------------------------------------------
1 | import { Command } from '@/common/interfaces';
2 | import CommandManager from '@/common/CommandManager';
3 |
4 | const redoCommand: Command = {
5 | name: 'redo',
6 |
7 | params: {},
8 |
9 | canExecute(graph) {
10 | const commandManager: CommandManager = graph.get('commandManager');
11 | const { commandQueue, commandIndex } = commandManager;
12 |
13 | return commandIndex < commandQueue.length;
14 | },
15 |
16 | shouldExecute() {
17 | return true;
18 | },
19 |
20 | canUndo() {
21 | return false;
22 | },
23 |
24 | init() {},
25 |
26 | execute(graph) {
27 | const commandManager: CommandManager = graph.get('commandManager');
28 | const { commandQueue, commandIndex } = commandManager;
29 |
30 | commandQueue[commandIndex].execute(graph);
31 |
32 | commandManager.commandIndex += 1;
33 | },
34 |
35 | undo() {},
36 |
37 | shortcuts: [
38 | ['metaKey', 'shiftKey', 'z'],
39 | ['ctrlKey', 'shiftKey', 'z'],
40 | ],
41 | };
42 |
43 | export default redoCommand;
44 |
--------------------------------------------------------------------------------
/src/components/Graph/command/remove.ts:
--------------------------------------------------------------------------------
1 | import { isMind, executeBatch } from '@/utils';
2 | import { ItemType } from '@/common/constants';
3 | import { TreeGraph, MindData, NodeModel, EdgeModel } from '@/common/interfaces';
4 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base';
5 |
6 | export interface RemoveCommandParams {
7 | flow: {
8 | nodes: {
9 | [id: string]: NodeModel;
10 | };
11 | edges: {
12 | [id: string]: EdgeModel;
13 | };
14 | };
15 | mind: {
16 | model: MindData | null;
17 | parent: string;
18 | };
19 | }
20 |
21 | const removeCommand: BaseCommand = {
22 | ...baseCommand,
23 |
24 | params: {
25 | flow: {
26 | nodes: {},
27 | edges: {},
28 | },
29 | mind: {
30 | model: null,
31 | parent: '',
32 | },
33 | },
34 |
35 | canExecute(graph) {
36 | const selectedNodes = this.getSelectedNodes(graph);
37 | const selectedEdges = this.getSelectedEdges(graph);
38 |
39 | return !!(selectedNodes.length || selectedEdges.length);
40 | },
41 |
42 | init(graph) {
43 | const selectedNodes = this.getSelectedNodes(graph);
44 | const selectedEdges = this.getSelectedEdges(graph);
45 |
46 | if (isMind(graph)) {
47 | const selectedNode = selectedNodes[0];
48 | const selectedNodeModel = selectedNode.getModel() as MindData;
49 |
50 | const selectedNodeParent = selectedNode.get('parent');
51 | const selectedNodeParentModel = selectedNodeParent ? selectedNodeParent.getModel() : {};
52 |
53 | this.params.mind = {
54 | model: selectedNodeModel,
55 | parent: selectedNodeParentModel.id,
56 | };
57 | } else {
58 | const { nodes, edges } = this.params.flow;
59 |
60 | selectedNodes.forEach(node => {
61 | const nodeModel = node.getModel() as NodeModel;
62 | const nodeEdges = node.getEdges();
63 |
64 | nodes[nodeModel.id] = nodeModel;
65 |
66 | nodeEdges.forEach(edge => {
67 | const edgeModel = edge.getModel();
68 |
69 | edges[edgeModel.id] = edgeModel;
70 | });
71 | });
72 |
73 | selectedEdges.forEach(edge => {
74 | const edgeModel = edge.getModel();
75 |
76 | edges[edgeModel.id] = edgeModel;
77 | });
78 | }
79 | },
80 |
81 | execute(graph) {
82 | if (isMind(graph)) {
83 | const { model } = this.params.mind;
84 |
85 | if (!model) {
86 | return;
87 | }
88 |
89 | (graph as TreeGraph).removeChild(model.id);
90 | } else {
91 | const { nodes, edges } = this.params.flow;
92 |
93 | executeBatch(graph, () => {
94 | [...Object.keys(nodes), ...Object.keys(edges)].forEach(id => {
95 | graph.removeItem(id);
96 | });
97 | });
98 | }
99 | },
100 |
101 | undo(graph) {
102 | if (isMind(graph)) {
103 | const { model, parent } = this.params.mind;
104 |
105 | if (!model) {
106 | return;
107 | }
108 |
109 | (graph as TreeGraph).addChild(model, parent);
110 | } else {
111 | const { nodes, edges } = this.params.flow;
112 |
113 | executeBatch(graph, () => {
114 | Object.keys(nodes).forEach(id => {
115 | const model = nodes[id];
116 |
117 | graph.addItem(ItemType.Node, model);
118 | });
119 |
120 | Object.keys(edges).forEach(id => {
121 | const model = edges[id];
122 |
123 | graph.addItem(ItemType.Edge, model);
124 | });
125 | });
126 | }
127 | },
128 |
129 | shortcuts: ['Delete', 'Backspace'],
130 | };
131 |
132 | export default removeCommand;
133 |
--------------------------------------------------------------------------------
/src/components/Graph/command/undo.ts:
--------------------------------------------------------------------------------
1 | import { Command } from '@/common/interfaces';
2 | import CommandManager from '@/common/CommandManager';
3 |
4 | const undoCommand: Command = {
5 | name: 'undo',
6 |
7 | params: {},
8 |
9 | canExecute(graph) {
10 | const commandManager: CommandManager = graph.get('commandManager');
11 | const { commandIndex } = commandManager;
12 |
13 | return commandIndex > 0;
14 | },
15 |
16 | shouldExecute() {
17 | return true;
18 | },
19 |
20 | canUndo() {
21 | return false;
22 | },
23 |
24 | init() {},
25 |
26 | execute(graph) {
27 | const commandManager: CommandManager = graph.get('commandManager');
28 | const { commandQueue, commandIndex } = commandManager;
29 |
30 | commandQueue[commandIndex - 1].undo(graph);
31 |
32 | commandManager.commandIndex -= 1;
33 | },
34 |
35 | undo() {},
36 |
37 | shortcuts: [
38 | ['metaKey', 'z'],
39 | ['ctrlKey', 'z'],
40 | ],
41 | };
42 |
43 | export default undoCommand;
44 |
--------------------------------------------------------------------------------
/src/components/Graph/command/update.ts:
--------------------------------------------------------------------------------
1 | import pick from 'lodash/pick';
2 | import { Graph, TreeGraph, NodeModel, EdgeModel } from '@/common/interfaces';
3 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base';
4 |
5 | export interface UpdateCommandParams {
6 | id: string;
7 | originModel: Partial | EdgeModel;
8 | updateModel: Partial | EdgeModel;
9 | forceRefreshLayout: boolean;
10 | }
11 |
12 | const updateCommand: BaseCommand = {
13 | ...baseCommand,
14 |
15 | params: {
16 | id: '',
17 | originModel: {},
18 | updateModel: {},
19 | forceRefreshLayout: false,
20 | },
21 |
22 | canExecute(graph) {
23 | const selectedNodes = this.getSelectedNodes(graph);
24 | const selectedEdges = this.getSelectedEdges(graph);
25 | return (selectedNodes.length || selectedEdges.length) && (selectedNodes.length === 1 || selectedEdges.length === 1)
26 | ? true
27 | : false;
28 | },
29 |
30 | init(graph) {
31 | const { id, updateModel } = this.params;
32 |
33 | const updatePaths = Object.keys(updateModel);
34 | const originModel = pick(graph.findById(id).getModel(), updatePaths);
35 |
36 | this.params.originModel = originModel;
37 | },
38 |
39 | execute(graph) {
40 | const { id, updateModel, forceRefreshLayout } = this.params;
41 |
42 | graph.updateItem(id, updateModel);
43 |
44 | if (forceRefreshLayout) {
45 | graph.refreshLayout && graph.refreshLayout(false);
46 | }
47 | },
48 |
49 | undo(graph) {
50 | const { id, originModel } = this.params;
51 |
52 | graph.updateItem(id, originModel);
53 | },
54 | };
55 |
56 | export default updateCommand;
57 |
--------------------------------------------------------------------------------
/src/components/Graph/command/zoomIn.ts:
--------------------------------------------------------------------------------
1 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base';
2 |
3 | const DELTA = 0.05;
4 |
5 | const zoomInCommand: BaseCommand = {
6 | ...baseCommand,
7 |
8 | canUndo() {
9 | return false;
10 | },
11 |
12 | execute(graph) {
13 | const ratio = 1 + DELTA;
14 |
15 | const zoom = graph.getZoom() * ratio;
16 | const maxZoom = graph.get('maxZoom');
17 |
18 | if (zoom > maxZoom) {
19 | return;
20 | }
21 |
22 | graph.zoom(ratio);
23 | },
24 |
25 | shortcuts: [
26 | ['metaKey', '='],
27 | ['ctrlKey', '='],
28 | ],
29 | };
30 |
31 | export default zoomInCommand;
32 |
--------------------------------------------------------------------------------
/src/components/Graph/command/zoomOut.ts:
--------------------------------------------------------------------------------
1 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base';
2 |
3 | const DELTA = 0.05;
4 |
5 | const zoomOutCommand: BaseCommand = {
6 | ...baseCommand,
7 |
8 | canUndo() {
9 | return false;
10 | },
11 |
12 | execute(graph) {
13 | const ratio = 1 - DELTA;
14 |
15 | const zoom = graph.getZoom() * ratio;
16 | const minZoom = graph.get('minZoom');
17 |
18 | if (zoom < minZoom) {
19 | return;
20 | }
21 |
22 | graph.zoom(ratio);
23 | },
24 |
25 | shortcuts: [
26 | ['metaKey', '-'],
27 | ['ctrlKey', '-'],
28 | ],
29 | };
30 |
31 | export default zoomOutCommand;
32 |
--------------------------------------------------------------------------------
/src/components/Graph/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import pick from 'lodash/pick';
3 | import { isMind } from '@/utils';
4 | import { track } from '@/helpers';
5 | import global from '@/common/global';
6 | import {
7 | GraphType,
8 | GraphCommonEvent,
9 | GraphNodeEvent,
10 | GraphEdgeEvent,
11 | GraphCanvasEvent,
12 | GraphCustomEvent,
13 | } from '@/common/constants';
14 | import {
15 | Graph,
16 | FlowData,
17 | MindData,
18 | GraphNativeEvent,
19 | GraphReactEvent,
20 | GraphReactEventProps,
21 | } from '@/common/interfaces';
22 | import { EditorPrivateContextProps, withEditorPrivateContext } from '@/components/EditorContext';
23 |
24 | import baseCommands from './command';
25 | import mindCommands from '@/components/Mind/command';
26 |
27 | import './behavior';
28 |
29 | interface GraphProps extends Partial, EditorPrivateContextProps {
30 | style?: React.CSSProperties;
31 | className?: string;
32 | containerId: string;
33 | data: FlowData | MindData;
34 | parseData(data: object): void;
35 | initGraph(width: number, height: number): Graph;
36 | }
37 |
38 | interface GraphState {}
39 |
40 | class GraphComponent extends React.Component {
41 | graph: Graph | null = null;
42 |
43 | componentDidMount() {
44 | this.initGraph();
45 | this.bindEvent();
46 | }
47 |
48 | componentDidUpdate(prevProps: GraphProps) {
49 | const { data } = this.props;
50 |
51 | if (data !== prevProps.data) {
52 | this.changeData(data);
53 | }
54 | }
55 |
56 | focusRootNode(graph: Graph, data: FlowData | MindData) {
57 | if (!isMind(graph)) {
58 | return;
59 | }
60 |
61 | const { id } = data as MindData;
62 |
63 | graph.focusItem(id);
64 | }
65 |
66 | initGraph() {
67 | const { containerId, parseData, initGraph, setGraph, commandManager } = this.props;
68 | const { clientWidth = 0, clientHeight = 0 } = document.getElementById(containerId) || {};
69 |
70 | // 解析数据
71 | const data = { ...this.props.data };
72 |
73 | parseData(data);
74 |
75 | // 初始画布
76 | this.graph = initGraph(clientWidth, clientHeight);
77 |
78 | this.graph.data(data);
79 | this.graph.render();
80 | this.focusRootNode(this.graph, data);
81 | this.graph.setMode('default');
82 |
83 | setGraph(this.graph);
84 |
85 | // 设置命令管理器
86 | this.graph.set('commandManager', commandManager);
87 |
88 | // 注册命令
89 | let commands = baseCommands;
90 |
91 | if (isMind(this.graph)) {
92 | commands = {
93 | ...commands,
94 | ...mindCommands,
95 | };
96 | }
97 |
98 | Object.keys(commands).forEach(name => {
99 | commandManager.register(name, commands[name]);
100 | });
101 |
102 | // 发送埋点
103 | if (global.trackable) {
104 | const graphType = isMind(this.graph) ? GraphType.Mind : GraphType.Flow;
105 |
106 | track(graphType);
107 | }
108 | }
109 |
110 | bindEvent() {
111 | const { graph, props } = this;
112 |
113 | if (!graph) {
114 | return;
115 | }
116 |
117 | const events: {
118 | [propName in GraphReactEvent]: GraphNativeEvent;
119 | } = {
120 | ...GraphCommonEvent,
121 | ...GraphNodeEvent,
122 | ...GraphEdgeEvent,
123 | ...GraphCanvasEvent,
124 | ...GraphCustomEvent,
125 | };
126 |
127 | (Object.keys(events) as GraphReactEvent[]).forEach(event => {
128 | if (typeof props[event] === 'function') {
129 | graph.on(events[event], props[event]);
130 | }
131 | });
132 | }
133 |
134 | changeData(data: any) {
135 | const { graph } = this;
136 | const { parseData } = this.props;
137 |
138 | if (!graph) {
139 | return;
140 | }
141 |
142 | parseData(data);
143 |
144 | graph.changeData(data);
145 | this.focusRootNode(graph, data);
146 | }
147 |
148 | render() {
149 | const { containerId, children } = this.props;
150 |
151 | return (
152 |
153 | {children}
154 |
155 | );
156 | }
157 | }
158 |
159 | export default withEditorPrivateContext(GraphComponent);
160 |
--------------------------------------------------------------------------------
/src/components/ItemPanel/Item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import pick from 'lodash/pick';
3 | import global from '@/common/global';
4 | import { ItemType, GraphMode } from '@/common/constants';
5 | import { NodeModel } from '@/common/interfaces';
6 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext';
7 |
8 | export interface ItemProps extends EditorContextProps {
9 | style?: React.CSSProperties;
10 | className?: string;
11 | type?: ItemType;
12 | model: Partial;
13 | }
14 |
15 | export interface ItemState {}
16 |
17 | class Item extends React.Component {
18 | static defaultProps = {
19 | type: ItemType.Node,
20 | };
21 |
22 | handleMouseDown = () => {
23 | const { graph, type, model } = this.props;
24 |
25 | if (type === ItemType.Node) {
26 | global.component.itemPanel.model = model;
27 | graph.setMode(GraphMode.AddNode);
28 | }
29 | };
30 |
31 | render() {
32 | const { children } = this.props;
33 |
34 | return (
35 |
36 | {children}
37 |
38 | );
39 | }
40 | }
41 |
42 | export default withEditorContext(Item);
43 |
--------------------------------------------------------------------------------
/src/components/ItemPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import pick from 'lodash/pick';
3 | import global from '@/common/global';
4 | import { GraphMode } from '@/common/constants';
5 | import { GShape, GGroup } from '@/common/interfaces';
6 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext';
7 | import Item from './Item';
8 |
9 | interface ItemPanelProps extends EditorContextProps {
10 | style?: React.CSSProperties;
11 | className?: string;
12 | }
13 |
14 | interface ItemPanelState {}
15 |
16 | class ItemPanel extends React.Component {
17 | static Item = Item;
18 |
19 | componentDidMount() {
20 | document.addEventListener('mouseup', this.handleMouseUp, false);
21 | }
22 |
23 | componentWillUnmount() {
24 | document.removeEventListener('mouseup', this.handleMouseUp, false);
25 | }
26 |
27 | handleMouseUp = () => {
28 | const { graph } = this.props;
29 |
30 | if (graph.getCurrentMode() === GraphMode.Default) {
31 | return;
32 | }
33 |
34 | const group: GGroup = graph.get('group');
35 | const shape: GShape = group.findByClassName(global.component.itemPanel.delegateShapeClassName) as GShape;
36 |
37 | if (shape) {
38 | shape.remove(true);
39 | graph.paint();
40 | }
41 |
42 | global.component.itemPanel.model = null;
43 | graph.setMode(GraphMode.Default);
44 | };
45 |
46 | render() {
47 | const { children } = this.props;
48 |
49 | return {children}
;
50 | }
51 | }
52 |
53 | export { Item };
54 |
55 | export default withEditorContext(ItemPanel);
56 |
--------------------------------------------------------------------------------
/src/components/Mind/command/fold.ts:
--------------------------------------------------------------------------------
1 | import { TreeGraph, NodeModel } from '@/common/interfaces';
2 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base';
3 |
4 | export interface FoldCommandParams {
5 | id: string;
6 | }
7 |
8 | const foldCommand: BaseCommand = {
9 | ...baseCommand,
10 |
11 | params: {
12 | id: '',
13 | },
14 |
15 | canExecute(graph: TreeGraph) {
16 | const selectedNodes = this.getSelectedNodes(graph);
17 |
18 | if (!selectedNodes.length) {
19 | return false;
20 | }
21 |
22 | const selectedNode = selectedNodes[0];
23 | const selectedNodeModel = selectedNode.getModel() as NodeModel;
24 |
25 | if (!selectedNodeModel.children || !selectedNodeModel.children.length) {
26 | return false;
27 | }
28 |
29 | if (selectedNodeModel.collapsed) {
30 | return false;
31 | }
32 |
33 | return true;
34 | },
35 |
36 | init(graph) {
37 | const selectedNode = this.getSelectedNodes(graph)[0];
38 | const selectedNodeModel = selectedNode.getModel();
39 |
40 | this.params = {
41 | id: selectedNodeModel.id,
42 | };
43 | },
44 |
45 | execute(graph: TreeGraph) {
46 | const { id } = this.params;
47 |
48 | const sourceData = graph.findDataById(id);
49 |
50 | sourceData.collapsed = !sourceData.collapsed;
51 |
52 | graph.refreshLayout(false);
53 | },
54 |
55 | undo(graph) {
56 | this.execute(graph);
57 | },
58 |
59 | shortcuts: [
60 | ['metaKey', '/'],
61 | ['ctrlKey', '/'],
62 | ],
63 | };
64 |
65 | export default foldCommand;
66 |
--------------------------------------------------------------------------------
/src/components/Mind/command/index.ts:
--------------------------------------------------------------------------------
1 | import topic from './topic';
2 | import subtopic from './subtopic';
3 | import fold from './fold';
4 | import unfold from './unfold';
5 |
6 | export default { topic, subtopic, fold, unfold };
7 |
--------------------------------------------------------------------------------
/src/components/Mind/command/subtopic.ts:
--------------------------------------------------------------------------------
1 | import { TreeGraph, MindData } from '@/common/interfaces';
2 | import { BaseCommand } from '@/components/Graph/command/base';
3 | import topicCommand from './topic';
4 |
5 | export interface SubtopicCommandParams {
6 | id: string;
7 | model: MindData;
8 | }
9 |
10 | const subtopicCommand: BaseCommand = {
11 | ...topicCommand,
12 |
13 | canExecute(graph) {
14 | return this.getSelectedNodes(graph)[0] ? true : false;
15 | },
16 |
17 | execute(graph) {
18 | const { id, model } = this.params;
19 |
20 | // 添加节点
21 | graph.addChild(model, id);
22 |
23 | // 选中节点
24 | this.setSelectedItems(graph, [model.id]);
25 |
26 | // 编辑节点
27 | this.editSelectedNode(graph);
28 | },
29 |
30 | shortcuts: ['Tab'],
31 | };
32 |
33 | export default subtopicCommand;
34 |
--------------------------------------------------------------------------------
/src/components/Mind/command/topic.ts:
--------------------------------------------------------------------------------
1 | import { guid } from '@/utils';
2 | import { LABEL_DEFAULT_TEXT } from '@/common/constants';
3 | import { TreeGraph, MindData } from '@/common/interfaces';
4 | import { BaseCommand, baseCommand } from '@/components/Graph/command/base';
5 |
6 | export interface TopicCommandParams {
7 | id: string;
8 | model: MindData;
9 | }
10 |
11 | export const topicCommand: BaseCommand = {
12 | ...baseCommand,
13 |
14 | params: {
15 | id: '',
16 | model: {
17 | id: '',
18 | },
19 | },
20 |
21 | canExecute(graph) {
22 | const selectedNodes = this.getSelectedNodes(graph);
23 |
24 | return selectedNodes.length && selectedNodes.length === 1 && selectedNodes[0].get('parent');
25 | },
26 |
27 | init(graph) {
28 | if (this.params.id) {
29 | return;
30 | }
31 |
32 | const selectedNode = this.getSelectedNodes(graph)[0];
33 |
34 | this.params = {
35 | id: selectedNode.get('id'),
36 | model: {
37 | id: guid(),
38 | label: LABEL_DEFAULT_TEXT,
39 | },
40 | };
41 | },
42 |
43 | execute(graph) {
44 | const { id, model } = this.params;
45 |
46 | const parent = graph.findById(id).get('parent');
47 |
48 | // 添加节点
49 | graph.addChild(model, parent);
50 |
51 | // 选中节点
52 | this.setSelectedItems(graph, [model.id]);
53 |
54 | // 编辑节点
55 | this.editSelectedNode(graph);
56 | },
57 |
58 | undo(graph) {
59 | const { id, model } = this.params;
60 |
61 | this.setSelectedItems(graph, [id]);
62 |
63 | graph.removeChild(model.id);
64 | },
65 |
66 | shortcuts: ['Enter'],
67 | };
68 |
69 | export default topicCommand;
70 |
--------------------------------------------------------------------------------
/src/components/Mind/command/unfold.ts:
--------------------------------------------------------------------------------
1 | import { TreeGraph, NodeModel } from '@/common/interfaces';
2 | import { BaseCommand } from '@/components/Graph/command/base';
3 | import foldCommand from './fold';
4 |
5 | export interface UnfoldCommandParams {
6 | id: string;
7 | }
8 |
9 | const unfoldCommand: BaseCommand = {
10 | ...foldCommand,
11 |
12 | canExecute(graph: TreeGraph) {
13 | const selectedNodes = this.getSelectedNodes(graph);
14 |
15 | if (!selectedNodes.length) {
16 | return false;
17 | }
18 |
19 | const selectedNode = selectedNodes[0];
20 | const selectedNodeModel = selectedNode.getModel() as NodeModel;
21 |
22 | if (!selectedNodeModel.children || !selectedNodeModel.children.length) {
23 | return false;
24 | }
25 |
26 | if (!selectedNodeModel.collapsed) {
27 | return false;
28 | }
29 |
30 | return true;
31 | },
32 |
33 | shortcuts: [
34 | ['metaKey', '/'],
35 | ['ctrlKey', '/'],
36 | ],
37 | };
38 |
39 | export default unfoldCommand;
40 |
--------------------------------------------------------------------------------
/src/components/Mind/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import omit from 'lodash/omit';
3 | import merge from 'lodash/merge';
4 | import G6 from '@antv/g6';
5 | import { guid, recursiveTraversal } from '@/utils';
6 | import global from '@/common/global';
7 | import { MIND_CONTAINER_ID, GraphType } from '@/common/constants';
8 | import { FOLD_BUTTON_CLASS_NAME, UNFOLD_BUTTON_CLASS_NAME } from '@/shape/nodes/bizMindNode';
9 | import { Graph, GraphOptions, MindData, GraphReactEventProps } from '@/common/interfaces';
10 | import behaviorManager from '@/common/behaviorManager';
11 | import GraphComponent from '@/components/Graph';
12 |
13 | import './command';
14 |
15 | interface MindProps extends Partial {
16 | style?: React.CSSProperties;
17 | className?: string;
18 | data: MindData;
19 | graphConfig?: Partial;
20 | customModes?: (mode: string, behaviors: any) => object;
21 | }
22 |
23 | interface MindState {}
24 |
25 | class Mind extends React.Component {
26 | static defaultProps = {
27 | graphConfig: {},
28 | };
29 |
30 | graph: Graph | null = null;
31 |
32 | containerId = `${MIND_CONTAINER_ID}_${guid()}`;
33 |
34 | canDragOrZoomCanvas = () => {
35 | const { graph } = this;
36 |
37 | if (!graph) {
38 | return false;
39 | }
40 |
41 | return (
42 | global.plugin.itemPopover.state === 'hide' &&
43 | global.plugin.contextMenu.state === 'hide' &&
44 | global.plugin.editableLabel.state === 'hide'
45 | );
46 | };
47 |
48 | canCollapseExpand = ({ target }) => {
49 | return target && [FOLD_BUTTON_CLASS_NAME, UNFOLD_BUTTON_CLASS_NAME].includes(target.get('className'));
50 | };
51 |
52 | parseData = data => {
53 | recursiveTraversal(data, item => {
54 | const { id } = item;
55 |
56 | if (id) {
57 | return;
58 | }
59 |
60 | item.id = guid();
61 | });
62 | };
63 |
64 | initGraph = (width: number, height: number) => {
65 | const { containerId } = this;
66 | const { graphConfig, customModes } = this.props;
67 |
68 | const modes: any = merge(behaviorManager.getRegisteredBehaviors(GraphType.Mind), {
69 | default: {
70 | 'click-item': {
71 | type: 'click-item',
72 | multiple: false,
73 | },
74 | 'collapse-expand': {
75 | type: 'collapse-expand',
76 | shouldBegin: this.canCollapseExpand,
77 | },
78 | 'drag-canvas': {
79 | type: 'drag-canvas',
80 | shouldBegin: this.canDragOrZoomCanvas,
81 | shouldUpdate: this.canDragOrZoomCanvas,
82 | },
83 | 'zoom-canvas': {
84 | type: 'zoom-canvas',
85 | shouldUpdate: this.canDragOrZoomCanvas,
86 | },
87 | },
88 | });
89 |
90 | Object.keys(modes).forEach(mode => {
91 | const behaviors = modes[mode];
92 |
93 | modes[mode] = Object.values(customModes ? customModes(mode, behaviors) : behaviors);
94 | });
95 |
96 | this.graph = new G6.TreeGraph({
97 | container: containerId,
98 | width,
99 | height,
100 | modes,
101 | layout: {
102 | type: 'mindmap',
103 | direction: 'H',
104 | getWidth: () => 120,
105 | getHeight: () => 60,
106 | getHGap: () => 100,
107 | getVGap: () => 50,
108 | getSide: ({ data }) => {
109 | if (data.side) {
110 | return data.side;
111 | }
112 |
113 | return 'right';
114 | },
115 | },
116 | animate: false,
117 | defaultNode: {
118 | type: 'bizMindNode',
119 | },
120 | defaultEdge: {
121 | type: 'bizMindEdge',
122 | },
123 | ...graphConfig,
124 | });
125 |
126 | return this.graph;
127 | };
128 |
129 | render() {
130 | const { containerId, parseData, initGraph } = this;
131 | const { data } = this.props;
132 |
133 | return (
134 |
141 | );
142 | }
143 | }
144 |
145 | export default Mind;
146 |
--------------------------------------------------------------------------------
/src/components/Register/index.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import G6 from '@antv/g6';
3 | import { Command, Behavior } from '@/common/interfaces';
4 | import behaviorManager from '@/common/behaviorManager';
5 | import { EditorPrivateContextProps, withEditorPrivateContext } from '@/components/EditorContext';
6 |
7 | interface RegisterProps extends EditorPrivateContextProps {
8 | name: string;
9 | config: object;
10 | extend?: string;
11 | }
12 | interface RegisterState {}
13 |
14 | class Register extends React.Component {
15 | static create = function(type: string) {
16 | class TypedRegister extends Register {
17 | constructor(props: RegisterProps) {
18 | super(props, type);
19 | }
20 | }
21 |
22 | return withEditorPrivateContext(TypedRegister);
23 | };
24 |
25 | constructor(props: RegisterProps, type: string) {
26 | super(props);
27 |
28 | const { name, config, extend, commandManager } = props;
29 |
30 | switch (type) {
31 | case 'node':
32 | G6.registerNode(name, config, extend);
33 | break;
34 |
35 | case 'edge':
36 | G6.registerEdge(name, config, extend);
37 | break;
38 |
39 | case 'command':
40 | commandManager.register(name, config as Command);
41 | break;
42 |
43 | case 'behavior':
44 | behaviorManager.register(name, config as Behavior);
45 | break;
46 |
47 | default:
48 | break;
49 | }
50 | }
51 |
52 | render() {
53 | return null;
54 | }
55 | }
56 |
57 | export const RegisterNode = Register.create('node');
58 | export const RegisterEdge = Register.create('edge');
59 | export const RegisterCommand = Register.create('command');
60 | export const RegisterBehavior = Register.create('behavior');
61 |
--------------------------------------------------------------------------------
/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | import global from '@/common/global';
2 | import { toQueryString } from '@/utils';
3 | import { GraphType } from '@/common/constants';
4 |
5 | const BASE_URL = 'http://gm.mmstat.com/fsp.1.1';
6 |
7 | export function track(graphType: GraphType) {
8 | const version = global.version;
9 | const trackable = global.trackable;
10 |
11 | if (!trackable) {
12 | return;
13 | }
14 |
15 | const { location, navigator } = window;
16 | const image = new Image();
17 | const params = toQueryString({
18 | pid: 'ggeditor',
19 | code: '11',
20 | msg: 'syslog',
21 | page: `${location.protocol}//${location.host}${location.pathname}`,
22 | hash: location.hash,
23 | ua: navigator.userAgent,
24 | rel: version,
25 | c1: graphType,
26 | });
27 |
28 | image.src = `${BASE_URL}?${params}`;
29 | }
30 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import G6 from '@antv/g6';
2 |
3 | import '@/shape';
4 |
5 | import * as Util from '@/utils';
6 |
7 | import Editor from '@/components/Editor';
8 | import Flow from '@/components/Flow';
9 | import Mind from '@/components/Mind';
10 | import Command from '@/components/Command';
11 | import ItemPanel, { Item } from '@/components/ItemPanel';
12 | import DetailPanel from '@/components/DetailPanel';
13 | import { RegisterNode, RegisterEdge, RegisterCommand, RegisterBehavior } from '@/components/Register';
14 | import { withEditorContext } from '@/components/EditorContext';
15 | import { baseCommand } from '@/components/Graph/command/base';
16 |
17 | import ItemPopover from '@/plugins/ItemPopover';
18 | import ContextMenu from '@/plugins/ContextMenu';
19 | import EditableLabel from '@/plugins/EditableLabel';
20 |
21 | import global from '@/common/global';
22 | import * as constants from '@/common/constants';
23 | import CommandManager from '@/common/CommandManager';
24 | import behaviorManager from '@/common/behaviorManager';
25 |
26 | import { setAnchorPointsState } from '@/shape/common/anchor';
27 |
28 | export {
29 | G6,
30 | Util,
31 | Flow,
32 | Mind,
33 | Command,
34 | Item,
35 | ItemPanel,
36 | DetailPanel,
37 | RegisterNode,
38 | RegisterEdge,
39 | RegisterCommand,
40 | RegisterBehavior,
41 | withEditorContext,
42 | baseCommand,
43 | ItemPopover,
44 | ContextMenu,
45 | EditableLabel,
46 | global,
47 | constants,
48 | CommandManager,
49 | behaviorManager,
50 | setAnchorPointsState,
51 | };
52 |
53 | export default Editor;
54 |
--------------------------------------------------------------------------------
/src/plugins/ContextMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { clearSelectedState } from '@/utils';
4 | import global from '@/common/global';
5 | import { ItemState, GraphCommonEvent, GraphNodeEvent, GraphEdgeEvent, GraphCanvasEvent } from '@/common/constants';
6 | import { GraphEvent, Item, Node, Edge } from '@/common/interfaces';
7 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext';
8 |
9 | export enum ContextMenuType {
10 | Canvas = 'canvas',
11 | Node = 'node',
12 | Edge = 'edge',
13 | }
14 |
15 | interface ContextMenuProps extends EditorContextProps {
16 | /** 菜单类型 */
17 | type?: ContextMenuType;
18 | /** 菜单内容 */
19 | renderContent: (item: Item, position: { x: number; y: number }, hide: () => void) => React.ReactNode;
20 | }
21 |
22 | interface ContextMenuState {
23 | visible: boolean;
24 | content: React.ReactNode;
25 | }
26 |
27 | class ContextMenu extends React.Component {
28 | static defaultProps = {
29 | type: ContextMenuType.Canvas,
30 | };
31 |
32 | state = {
33 | visible: false,
34 | content: null,
35 | };
36 |
37 | componentDidMount() {
38 | const { graph, type } = this.props;
39 |
40 | switch (type) {
41 | case ContextMenuType.Canvas:
42 | graph.on(GraphCanvasEvent.onCanvasContextMenu, (e: GraphEvent) => {
43 | e.preventDefault();
44 |
45 | const { x, y } = e;
46 |
47 | this.showContextMenu(x, y);
48 | });
49 | break;
50 |
51 | case ContextMenuType.Node:
52 | graph.on(GraphNodeEvent.onNodeContextMenu, (e: GraphEvent) => {
53 | e.preventDefault();
54 |
55 | const { x, y, item } = e;
56 |
57 | this.showContextMenu(x, y, item as Node);
58 | });
59 | break;
60 |
61 | case ContextMenuType.Edge:
62 | graph.on(GraphEdgeEvent.onEdgeContextMenu, (e: GraphEvent) => {
63 | e.preventDefault();
64 |
65 | const { x, y, item } = e;
66 |
67 | this.showContextMenu(x, y, item as Edge);
68 | });
69 | break;
70 |
71 | default:
72 | break;
73 | }
74 |
75 | graph.on(GraphCommonEvent.onClick, () => {
76 | this.hideContextMenu();
77 | });
78 | }
79 |
80 | showContextMenu = (x: number, y: number, item?: Item) => {
81 | const { graph, renderContent } = this.props;
82 |
83 | clearSelectedState(graph);
84 |
85 | if (item) {
86 | graph.setItemState(item, ItemState.Selected, true);
87 | }
88 |
89 | global.plugin.contextMenu.state = 'show';
90 | global.clipboard.point = {
91 | x,
92 | y,
93 | };
94 |
95 | const position = graph.getCanvasByPoint(x, y);
96 |
97 | this.setState({
98 | visible: true,
99 | content: renderContent(item, position, this.hideContextMenu),
100 | });
101 | };
102 |
103 | hideContextMenu = () => {
104 | global.plugin.contextMenu.state = 'hide';
105 |
106 | this.setState({
107 | visible: false,
108 | content: null,
109 | });
110 | };
111 |
112 | render() {
113 | const { graph } = this.props;
114 | const { visible, content } = this.state;
115 |
116 | if (!visible) {
117 | return null;
118 | }
119 |
120 | return ReactDOM.createPortal(content, graph.get('container'));
121 | }
122 | }
123 |
124 | export default withEditorContext(ContextMenu);
125 |
--------------------------------------------------------------------------------
/src/plugins/EditableLabel/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import G6 from '@antv/g6';
4 | import { isMind, getSelectedNodes } from '@/utils';
5 | import global from '@/common/global';
6 | import { GraphMode, EditorEvent, GraphNodeEvent, LabelState } from '@/common/constants';
7 | import { LabelStateEvent } from '@/common/interfaces';
8 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext';
9 |
10 | interface EditableLabelProps extends EditorContextProps {
11 | /** 标签图形类名 */
12 | labelClassName?: string;
13 | /** 标签最大宽度 */
14 | labelMaxWidth?: number;
15 | }
16 |
17 | interface EditableLabelState {
18 | visible: boolean;
19 | }
20 |
21 | class EditableLabel extends React.Component {
22 | el: HTMLDivElement = null;
23 |
24 | static defaultProps = {
25 | labelClassName: 'node-label',
26 | labelMaxWidth: 100,
27 | };
28 |
29 | state = {
30 | visible: false,
31 | };
32 |
33 | componentDidMount() {
34 | const { graph } = this.props;
35 |
36 | graph.on(EditorEvent.onLabelStateChange, ({ labelState }: LabelStateEvent) => {
37 | if (labelState === LabelState.Show) {
38 | this.showEditableLabel();
39 | } else {
40 | this.hideEditableLabel();
41 | }
42 | });
43 |
44 | graph.on(GraphNodeEvent.onNodeDoubleClick, () => {
45 | this.showEditableLabel();
46 | });
47 | }
48 |
49 | update = () => {
50 | const { graph, executeCommand } = this.props;
51 |
52 | const node = getSelectedNodes(graph)[0];
53 | const model = node.getModel();
54 |
55 | const { textContent: label } = this.el;
56 |
57 | if (label === model.label) {
58 | return;
59 | }
60 |
61 | executeCommand('update', {
62 | id: model.id,
63 | updateModel: {
64 | label,
65 | },
66 | forceRefreshLayout: isMind(graph),
67 | });
68 | };
69 |
70 | showEditableLabel = () => {
71 | global.plugin.editableLabel.state = 'show';
72 |
73 | this.setState(
74 | {
75 | visible: true,
76 | },
77 | () => {
78 | const { el } = this;
79 |
80 | if (el) {
81 | el.focus();
82 | document.execCommand('selectAll', false, null);
83 | }
84 | },
85 | );
86 | };
87 |
88 | hideEditableLabel = () => {
89 | global.plugin.editableLabel.state = 'hide';
90 |
91 | this.setState({
92 | visible: false,
93 | });
94 | };
95 |
96 | handleBlur = () => {
97 | this.update();
98 | this.hideEditableLabel();
99 | };
100 |
101 | handleKeyDown = (e: React.KeyboardEvent) => {
102 | e.stopPropagation();
103 |
104 | const { key } = e;
105 |
106 | if (['Tab'].includes(key)) {
107 | e.preventDefault();
108 | }
109 |
110 | if (['Enter', 'Escape', 'Tab'].includes(key)) {
111 | this.update();
112 | this.hideEditableLabel();
113 | }
114 | };
115 |
116 | render() {
117 | const { graph, labelClassName, labelMaxWidth } = this.props;
118 |
119 | const mode = graph.getCurrentMode();
120 | const zoom = graph.getZoom();
121 |
122 | if (mode === GraphMode.Readonly) {
123 | return null;
124 | }
125 |
126 | const node = getSelectedNodes(graph)[0];
127 |
128 | if (!node) {
129 | return null;
130 | }
131 |
132 | const model = node.getModel();
133 | const group = node.getContainer();
134 |
135 | const label = model.label;
136 | const labelShape = group.findByClassName(labelClassName);
137 |
138 | if (!labelShape) {
139 | return null;
140 | }
141 |
142 | const { visible } = this.state;
143 |
144 | if (!visible) {
145 | return null;
146 | }
147 |
148 | // Get the label offset
149 | const { x: relativeX, y: relativeY } = labelShape.getBBox();
150 | const { x: absoluteX, y: absoluteY } = G6.Util.applyMatrix(
151 | {
152 | x: relativeX,
153 | y: relativeY,
154 | },
155 | node.getContainer().getMatrix(),
156 | );
157 |
158 | const { x: left, y: top } = graph.getCanvasByPoint(absoluteX, absoluteY);
159 |
160 | // Get the label size
161 | const { width, height } = labelShape.getBBox();
162 |
163 | // Get the label font
164 | const font = labelShape.attr('font');
165 |
166 | const style: React.CSSProperties = {
167 | position: 'absolute',
168 | top,
169 | left,
170 | width: 'auto',
171 | height: 'auto',
172 | minWidth: width,
173 | minHeight: height,
174 | maxWidth: labelMaxWidth,
175 | font,
176 | background: 'white',
177 | border: '1px solid #1890ff',
178 | outline: 'none',
179 | transform: `scale(${zoom})`,
180 | transformOrigin: 'left top',
181 | };
182 |
183 | return ReactDOM.createPortal(
184 | {
186 | this.el = el;
187 | }}
188 | style={style}
189 | contentEditable
190 | onBlur={this.handleBlur}
191 | onKeyDown={this.handleKeyDown}
192 | suppressContentEditableWarning
193 | >
194 | {label}
195 |
,
196 | graph.get('container'),
197 | );
198 | }
199 | }
200 |
201 | export default withEditorContext(EditableLabel);
202 |
--------------------------------------------------------------------------------
/src/plugins/ItemPopover/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import delay from 'lodash/delay';
4 | import global from '@/common/global';
5 | import { GraphNodeEvent } from '@/common/constants';
6 | import { Item } from '@/common/interfaces';
7 | import { EditorContextProps, withEditorContext } from '@/components/EditorContext';
8 |
9 | export enum ItemPopoverType {
10 | Node = 'node',
11 | Edge = 'edge',
12 | }
13 |
14 | interface ItemPopoverProps extends EditorContextProps {
15 | /** 浮层类型 */
16 | type?: ItemPopoverType;
17 | /** 浮层内容 */
18 | renderContent: (
19 | item: Item,
20 | position: { minX: number; minY: number; maxX: number; maxY: number; centerX: number; centerY: number },
21 | ) => React.ReactNode;
22 | }
23 |
24 | interface ItemPopoverState {
25 | visible: boolean;
26 | content: React.ReactNode;
27 | }
28 |
29 | class ItemPopover extends React.Component {
30 | static defaultProps = {
31 | type: ItemPopoverType.Node,
32 | };
33 |
34 | state = {
35 | visible: false,
36 | content: null,
37 | };
38 |
39 | mouseEnterTimeoutID = 0;
40 | mouseLeaveTimeoutID = 0;
41 |
42 | componentDidMount() {
43 | const { graph, type } = this.props;
44 |
45 | if (type === ItemPopoverType.Node) {
46 | graph.on(GraphNodeEvent.onNodeMouseEnter, ({ item }) => {
47 | clearTimeout(this.mouseLeaveTimeoutID);
48 |
49 | this.mouseEnterTimeoutID = delay(this.showItemPopover, 250, item);
50 | });
51 |
52 | graph.on(GraphNodeEvent.onNodeMouseLeave, () => {
53 | clearTimeout(this.mouseEnterTimeoutID);
54 |
55 | this.mouseLeaveTimeoutID = delay(this.hideItemPopover, 250);
56 | });
57 | }
58 | }
59 |
60 | showItemPopover = (item: Item) => {
61 | const { graph, renderContent } = this.props;
62 |
63 | global.plugin.itemPopover.state = 'show';
64 |
65 | const { minX, minY, maxX, maxY, centerX, centerY } = item.getBBox();
66 |
67 | const { x: itemMinX, y: itemMinY } = graph.getCanvasByPoint(minX, minY);
68 | const { x: itemMaxX, y: itemMaxY } = graph.getCanvasByPoint(maxX, maxY);
69 | const { x: itemCenterX, y: itemCenterY } = graph.getCanvasByPoint(centerX, centerY);
70 |
71 | const position = {
72 | minX: itemMinX,
73 | minY: itemMinY,
74 | maxX: itemMaxX,
75 | maxY: itemMaxY,
76 | centerX: itemCenterX,
77 | centerY: itemCenterY,
78 | };
79 |
80 | this.setState({
81 | visible: true,
82 | content: renderContent(item, position),
83 | });
84 | };
85 |
86 | hideItemPopover = () => {
87 | global.plugin.itemPopover.state = 'hide';
88 |
89 | this.setState({
90 | visible: false,
91 | content: null,
92 | });
93 | };
94 |
95 | render() {
96 | const { graph } = this.props;
97 | const { visible, content } = this.state;
98 |
99 | if (!visible) {
100 | return null;
101 | }
102 |
103 | return ReactDOM.createPortal(content, graph.get('container'));
104 | }
105 | }
106 |
107 | export default withEditorContext(ItemPopover);
108 |
--------------------------------------------------------------------------------
/src/shape/common/anchor.ts:
--------------------------------------------------------------------------------
1 | import { ItemState, AnchorPointState } from '@/common/constants';
2 | import { ShapeStyle, NodeModel, Item, Node } from '@/common/interfaces';
3 |
4 | interface AnchorPointContextProps {
5 | getAnchorPoints?(model: NodeModel): number[][];
6 | }
7 |
8 | type GetAnchorPointStyle = (item: Node, anchorPoint: number[]) => ShapeStyle;
9 | type GetAnchorPointDisabledStyle = (item: Node, anchorPoint: number[]) => ShapeStyle & { img?: string };
10 |
11 | const ANCHOR_POINT_NAME = 'anchorPoint';
12 |
13 | const getAnchorPointDefaultStyle: GetAnchorPointStyle = (item, anchorPoint) => {
14 | const { width, height } = item.getKeyShape().getBBox();
15 |
16 | const [x, y] = anchorPoint;
17 |
18 | return {
19 | x: width * x,
20 | y: height * y - 3,
21 | r: 3,
22 | lineWidth: 2,
23 | fill: '#FFFFFF',
24 | stroke: '#5AAAFF',
25 | };
26 | };
27 |
28 | const getAnchorPointDefaultDisabledStyle: GetAnchorPointDisabledStyle = (item, anchorPoint) => {
29 | const { width, height } = item.getKeyShape().getBBox();
30 |
31 | const [x, y] = anchorPoint;
32 |
33 | return {
34 | img:
35 | 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSI4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xLjUxNSAxLjE3Mmw1LjY1NyA1LjY1Nm0wLTUuNjU2TDEuNTE1IDYuODI4IiBzdHJva2U9IiNGRjYwNjAiIHN0cm9rZS13aWR0aD0iMS42IiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIi8+PC9zdmc+',
36 | x: width * x - 4,
37 | y: height * y - 8,
38 | width: 8,
39 | height: 8,
40 | };
41 | };
42 |
43 | function drawAnchorPoints(
44 | this: AnchorPointContextProps,
45 | item: Node,
46 | getAnchorPointStyle: GetAnchorPointStyle,
47 | getAnchorPointDisabledStyle: GetAnchorPointDisabledStyle,
48 | ) {
49 | const group = item.getContainer();
50 | const model = item.getModel() as NodeModel;
51 | const anchorPoints = this.getAnchorPoints ? this.getAnchorPoints(model) : [];
52 | const anchorPointsState = item.get('anchorPointsState') || [];
53 |
54 | anchorPoints.forEach((anchorPoint, index) => {
55 | if (anchorPointsState[index] === AnchorPointState.Enabled) {
56 | group.addShape('circle', {
57 | name: ANCHOR_POINT_NAME,
58 | attrs: {
59 | ...getAnchorPointDefaultStyle(item, anchorPoint),
60 | ...getAnchorPointStyle(item, anchorPoint),
61 | },
62 | isAnchorPoint: true,
63 | anchorPointIndex: index,
64 | anchorPointState: AnchorPointState.Enabled,
65 | });
66 | } else {
67 | group.addShape('image', {
68 | name: ANCHOR_POINT_NAME,
69 | attrs: {
70 | ...getAnchorPointDefaultDisabledStyle(item, anchorPoint),
71 | ...getAnchorPointDisabledStyle(item, anchorPoint),
72 | },
73 | isAnchorPoint: true,
74 | anchorPointIndex: index,
75 | anchorPointState: AnchorPointState.Disabled,
76 | });
77 | }
78 | });
79 | }
80 |
81 | function removeAnchorPoints(this: AnchorPointContextProps, item: Node) {
82 | const group = item.getContainer();
83 | const anchorPoints = group.findAllByName(ANCHOR_POINT_NAME);
84 |
85 | anchorPoints.forEach(anchorPoint => {
86 | group.removeChild(anchorPoint);
87 | });
88 | }
89 |
90 | function setAnchorPointsState(
91 | this: AnchorPointContextProps,
92 | name: string,
93 | value: string | boolean,
94 | item: Item,
95 | getAnchorPointStyle: GetAnchorPointStyle = () => ({}),
96 | getAnchorPointDisabledStyle: GetAnchorPointDisabledStyle = () => ({}),
97 | ) {
98 | if (name !== ItemState.ActiveAnchorPoints) {
99 | return;
100 | }
101 |
102 | if (value) {
103 | drawAnchorPoints.call(this, item as Node, getAnchorPointStyle, getAnchorPointDisabledStyle);
104 | } else {
105 | removeAnchorPoints.call(this, item as Node);
106 | }
107 | }
108 |
109 | export { setAnchorPointsState };
110 |
--------------------------------------------------------------------------------
/src/shape/edges/bizFlowEdge.ts:
--------------------------------------------------------------------------------
1 | import G6 from '@antv/g6';
2 | import { ItemState } from '@/common/constants';
3 | import { GShape, GGroup, CustomEdge } from '@/common/interfaces';
4 |
5 | const EDGE_LABEL_CLASS_NAME = 'edge-label';
6 | const EDGE_LABEL_WRAPPER_CLASS_NAME = 'edge-label-wrapper-label';
7 |
8 | const bizFlowEdge: CustomEdge = {
9 | options: {
10 | style: {
11 | stroke: '#ccc1d8',
12 | lineWidth: 2,
13 | shadowColor: null,
14 | shadowBlur: 0,
15 | radius: 8,
16 | offset: 24,
17 | // startArrow: {
18 | // path: 'M 3,0 A 3,3,0,1,1,-3,0 A 3,3,0,1,1,3,0 Z',
19 | // d: 7,
20 | // },
21 | // endArrow: {
22 | // path: 'M 3,0 L -3,-3 L -3,3 Z',
23 | // d: 5,
24 | // },
25 | endArrow: {
26 | path: 'M 0,0 L 4,3 L 4,-3 Z',
27 | },
28 | },
29 | labelCfg: {
30 | style: {
31 | fill: '#000000',
32 | fontSize: 10,
33 | },
34 | },
35 | stateStyles: {
36 | [ItemState.Selected]: {
37 | stroke: '#5aaaff',
38 | shadowColor: '#5aaaff',
39 | shadowBlur: 24,
40 | },
41 | [ItemState.HighLight]: {
42 | stroke: '#5aaaff',
43 | shadowColor: '#5aaaff',
44 | shadowBlur: 24,
45 | },
46 | },
47 | },
48 |
49 | createLabelWrapper(group: GGroup) {
50 | const label = group.findByClassName(EDGE_LABEL_CLASS_NAME);
51 | const labelWrapper = group.findByClassName(EDGE_LABEL_WRAPPER_CLASS_NAME);
52 |
53 | if (!label) {
54 | return;
55 | }
56 |
57 | if (labelWrapper) {
58 | return;
59 | }
60 |
61 | group.addShape('rect', {
62 | className: EDGE_LABEL_WRAPPER_CLASS_NAME,
63 | attrs: {
64 | fill: '#e1e5e8',
65 | radius: 2,
66 | },
67 | });
68 |
69 | label.set('zIndex', 1);
70 |
71 | group.sort();
72 | },
73 |
74 | updateLabelWrapper(group: GGroup) {
75 | const label = group.findByClassName(EDGE_LABEL_CLASS_NAME);
76 | const labelWrapper = group.findByClassName(EDGE_LABEL_WRAPPER_CLASS_NAME);
77 |
78 | if (!label) {
79 | labelWrapper && labelWrapper.hide();
80 | return;
81 | } else {
82 | labelWrapper && labelWrapper.show();
83 | }
84 |
85 | if (!labelWrapper) {
86 | return;
87 | }
88 |
89 | const { minX, minY, width, height } = label.getBBox();
90 |
91 | labelWrapper.attr({
92 | x: minX - 5,
93 | y: minY - 3,
94 | width: width + 10,
95 | height: height + 6,
96 | });
97 | },
98 |
99 | afterDraw(model, group) {
100 | this.createLabelWrapper(group);
101 | this.updateLabelWrapper(group);
102 | },
103 |
104 | afterUpdate(model, item) {
105 | const group = item.getContainer();
106 |
107 | this.createLabelWrapper(group);
108 | this.updateLabelWrapper(group);
109 | },
110 |
111 | setState(name, value, item) {
112 | const shape: GShape = item.get('keyShape');
113 |
114 | if (!shape) {
115 | return;
116 | }
117 |
118 | const { style, stateStyles } = this.options;
119 |
120 | const stateStyle = stateStyles[name];
121 |
122 | if (!stateStyle) {
123 | return;
124 | }
125 |
126 | if (value) {
127 | shape.attr({
128 | ...style,
129 | ...stateStyle,
130 | });
131 | } else {
132 | shape.attr(style);
133 | }
134 | },
135 | };
136 |
137 | G6.registerEdge('bizFlowEdge', bizFlowEdge, 'polyline');
138 |
--------------------------------------------------------------------------------
/src/shape/edges/bizMindEdge.ts:
--------------------------------------------------------------------------------
1 | import G6 from '@antv/g6';
2 | import { ItemState } from '@/common/constants';
3 | import { GShape, CustomEdge } from '@/common/interfaces';
4 |
5 | const bizMindEdge: CustomEdge = {
6 | options: {
7 | style: {
8 | stroke: '#ccc1d8',
9 | lineWidth: 2,
10 | shadowColor: null,
11 | shadowBlur: 0,
12 | },
13 | stateStyles: {
14 | [ItemState.Selected]: {
15 | stroke: '#5aaaff',
16 | shadowColor: '#5aaaff',
17 | shadowBlur: 24,
18 | },
19 | [ItemState.HighLight]: {
20 | stroke: '#5aaaff',
21 | shadowColor: '#5aaaff',
22 | shadowBlur: 24,
23 | },
24 | },
25 | },
26 |
27 | setState(name, value, item) {
28 | const shape: GShape = item.get('keyShape');
29 |
30 | if (!shape) {
31 | return;
32 | }
33 |
34 | const { style, stateStyles } = this.options;
35 |
36 | const stateStyle = stateStyles[name];
37 |
38 | if (!stateStyle) {
39 | return;
40 | }
41 |
42 | if (value) {
43 | shape.attr({
44 | ...style,
45 | ...stateStyle,
46 | });
47 | } else {
48 | shape.attr(style);
49 | }
50 | },
51 | };
52 |
53 | G6.registerEdge('bizMindEdge', bizMindEdge, 'cubic-horizontal');
54 |
--------------------------------------------------------------------------------
/src/shape/index.ts:
--------------------------------------------------------------------------------
1 | import './nodes/bizNode';
2 | import './nodes/bizFlowNode';
3 | import './nodes/bizMindNode';
4 | import './edges/bizFlowEdge';
5 | import './edges/bizMindEdge';
6 |
--------------------------------------------------------------------------------
/src/shape/nodes/bizFlowNode.ts:
--------------------------------------------------------------------------------
1 | import G6 from '@antv/g6';
2 | import { CustomNode, Item } from '@/common/interfaces';
3 | import { setAnchorPointsState } from '../common/anchor';
4 |
5 | const bizFlowNode: CustomNode = {
6 | afterSetState(name: string, value: string | boolean, item: Item) {
7 | setAnchorPointsState.call(this, name, value, item);
8 | },
9 |
10 | getAnchorPoints() {
11 | return [
12 | [0.5, 0],
13 | [0.5, 1],
14 | [0, 0.5],
15 | [1, 0.5],
16 | ];
17 | },
18 | };
19 |
20 | G6.registerNode('bizFlowNode', bizFlowNode, 'bizNode');
21 |
--------------------------------------------------------------------------------
/src/shape/nodes/bizMindNode.ts:
--------------------------------------------------------------------------------
1 | import G6 from '@antv/g6';
2 | import { GGroup, Node, NodeModel, CustomNode } from '@/common/interfaces';
3 | import { getNodeSide, getFoldButtonPath, getUnfoldButtonPath } from '../utils';
4 |
5 | export const FOLD_BUTTON_CLASS_NAME = 'node-fold-button';
6 | export const UNFOLD_BUTTON_CLASS_NAME = 'node-unfold-button';
7 |
8 | const bizMindNode: CustomNode = {
9 | afterDraw(model, group) {
10 | this.drawButton(model, group);
11 | },
12 |
13 | afterUpdate(model, item) {
14 | const group = item.getContainer();
15 |
16 | this.drawButton(model, group);
17 | this.adjustButton(model, item);
18 | },
19 |
20 | drawButton(model: NodeModel, group: GGroup) {
21 | const { children, collapsed } = model;
22 |
23 | [FOLD_BUTTON_CLASS_NAME, UNFOLD_BUTTON_CLASS_NAME].forEach(className => {
24 | const shape = group.findByClassName(className);
25 |
26 | if (shape) {
27 | shape.destroy();
28 | }
29 | });
30 |
31 | if (!children || !children.length) {
32 | return;
33 | }
34 |
35 | if (!collapsed) {
36 | group.addShape('path', {
37 | className: FOLD_BUTTON_CLASS_NAME,
38 | attrs: {
39 | path: getFoldButtonPath(),
40 | fill: '#ffffff',
41 | stroke: '#ccc1d8',
42 | },
43 | });
44 | } else {
45 | group.addShape('path', {
46 | className: UNFOLD_BUTTON_CLASS_NAME,
47 | attrs: {
48 | path: getUnfoldButtonPath(),
49 | fill: '#ffffff',
50 | stroke: '#ccc1d8',
51 | },
52 | });
53 | }
54 | },
55 |
56 | adjustButton(model: NodeModel, item: Node) {
57 | const { children, collapsed } = model;
58 |
59 | if (!children || !children.length) {
60 | return;
61 | }
62 |
63 | const group = item.getContainer();
64 | const shape = group.findByClassName(!collapsed ? FOLD_BUTTON_CLASS_NAME : UNFOLD_BUTTON_CLASS_NAME);
65 |
66 | const [width, height] = this.getSize(model);
67 |
68 | const x = getNodeSide(item) === 'left' ? -24 : width + 10;
69 | const y = height / 2 - 9;
70 |
71 | shape.translate(x, y);
72 | },
73 |
74 | getAnchorPoints() {
75 | return [
76 | [0, 0.5],
77 | [1, 0.5],
78 | ];
79 | },
80 | };
81 |
82 | G6.registerNode('bizMindNode', bizMindNode, 'bizNode');
83 |
--------------------------------------------------------------------------------
/src/shape/nodes/bizNode.ts:
--------------------------------------------------------------------------------
1 | import G6 from '@antv/g6';
2 | import merge from 'lodash/merge';
3 | import isArray from 'lodash/isArray';
4 | import { ItemState } from '@/common/constants';
5 | import { GGroup, NodeModel, CustomNode } from '@/common/interfaces';
6 | import { optimizeMultilineText } from '../utils';
7 |
8 | const WRAPPER_BORDER_WIDTH = 2;
9 | const WRAPPER_HORIZONTAL_PADDING = 10;
10 |
11 | const WRAPPER_CLASS_NAME = 'node-wrapper';
12 | const CONTENT_CLASS_NAME = 'node-content';
13 | const LABEL_CLASS_NAME = 'node-label';
14 |
15 | const bizNode: CustomNode = {
16 | options: {
17 | size: [120, 60],
18 | wrapperStyle: {
19 | fill: '#5487ea',
20 | radius: 8,
21 | },
22 | contentStyle: {
23 | fill: '#ffffff',
24 | radius: 6,
25 | },
26 | labelStyle: {
27 | fill: '#000000',
28 | textAlign: 'center',
29 | textBaseline: 'middle',
30 | },
31 | stateStyles: {
32 | [ItemState.Active]: {
33 | wrapperStyle: {},
34 | contentStyle: {},
35 | labelStyle: {},
36 | } as any,
37 | [ItemState.Selected]: {
38 | wrapperStyle: {},
39 | contentStyle: {},
40 | labelStyle: {},
41 | } as any,
42 | },
43 | },
44 |
45 | getOptions(model: NodeModel) {
46 | return merge({}, this.options, this.getCustomConfig(model) || {}, model);
47 | },
48 |
49 | draw(model, group) {
50 | const keyShape = this.drawWrapper(model, group);
51 |
52 | this.drawContent(model, group);
53 | this.drawLabel(model, group);
54 |
55 | return keyShape;
56 | },
57 |
58 | drawWrapper(model: NodeModel, group: GGroup) {
59 | const [width, height] = this.getSize(model);
60 | const { wrapperStyle } = this.getOptions(model);
61 |
62 | const shape = group.addShape('rect', {
63 | className: WRAPPER_CLASS_NAME,
64 | draggable: true,
65 | attrs: {
66 | x: 0,
67 | y: -WRAPPER_BORDER_WIDTH * 2,
68 | width,
69 | height: height + WRAPPER_BORDER_WIDTH * 2,
70 | ...wrapperStyle,
71 | },
72 | });
73 |
74 | return shape;
75 | },
76 |
77 | drawContent(model: NodeModel, group: GGroup) {
78 | const [width, height] = this.getSize(model);
79 | const { contentStyle } = this.getOptions(model);
80 |
81 | const shape = group.addShape('rect', {
82 | className: CONTENT_CLASS_NAME,
83 | draggable: true,
84 | attrs: {
85 | x: 0,
86 | y: 0,
87 | width,
88 | height,
89 | ...contentStyle,
90 | },
91 | });
92 |
93 | return shape;
94 | },
95 |
96 | drawLabel(model: NodeModel, group: GGroup) {
97 | const [width, height] = this.getSize(model);
98 | const { labelStyle } = this.getOptions(model);
99 |
100 | const shape = group.addShape('text', {
101 | className: LABEL_CLASS_NAME,
102 | draggable: true,
103 | attrs: {
104 | x: width / 2,
105 | y: height / 2,
106 | text: model.label,
107 | ...labelStyle,
108 | },
109 | });
110 |
111 | return shape;
112 | },
113 |
114 | setLabelText(model: NodeModel, group: GGroup) {
115 | const shape = group.findByClassName(LABEL_CLASS_NAME);
116 |
117 | if (!shape) {
118 | return;
119 | }
120 |
121 | const [width] = this.getSize(model);
122 | const { fontStyle, fontWeight, fontSize, fontFamily } = shape.attr();
123 |
124 | const text = model.label as string;
125 | const font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
126 |
127 | shape.attr('text', optimizeMultilineText(text, font, 2, width - WRAPPER_HORIZONTAL_PADDING * 2));
128 | },
129 |
130 | update(model, item) {
131 | const group = item.getContainer();
132 |
133 | this.setLabelText(model, group);
134 | },
135 |
136 | setState(name, value, item) {
137 | const group = item.getContainer();
138 | const model = item.getModel();
139 | const states = item.getStates() as ItemState[];
140 |
141 | [WRAPPER_CLASS_NAME, CONTENT_CLASS_NAME, LABEL_CLASS_NAME].forEach(className => {
142 | const shape = group.findByClassName(className);
143 | const options = this.getOptions(model);
144 |
145 | const shapeName = className.split('-')[1];
146 |
147 | shape.attr({
148 | ...options[`${shapeName}Style`],
149 | });
150 |
151 | states.forEach(state => {
152 | if (options.stateStyles[state] && options.stateStyles[state][`${shapeName}Style`]) {
153 | shape.attr({
154 | ...options.stateStyles[state][`${shapeName}Style`],
155 | });
156 | }
157 | });
158 | });
159 |
160 | if (name === ItemState.Selected) {
161 | const wrapperShape = group.findByClassName(WRAPPER_CLASS_NAME);
162 |
163 | const [width, height] = this.getSize(model);
164 |
165 | if (value) {
166 | wrapperShape.attr({
167 | x: -WRAPPER_BORDER_WIDTH,
168 | y: -WRAPPER_BORDER_WIDTH * 2,
169 | width: width + WRAPPER_BORDER_WIDTH * 2,
170 | height: height + WRAPPER_BORDER_WIDTH * 3,
171 | });
172 | } else {
173 | wrapperShape.attr({
174 | x: 0,
175 | y: -WRAPPER_BORDER_WIDTH * 2,
176 | width,
177 | height: height + WRAPPER_BORDER_WIDTH * 2,
178 | });
179 | }
180 | }
181 |
182 | if (this.afterSetState) {
183 | this.afterSetState(name, value, item);
184 | }
185 | },
186 |
187 | getSize(model: NodeModel) {
188 | const { size } = this.getOptions(model);
189 |
190 | if (!isArray(size)) {
191 | return [size, size];
192 | }
193 |
194 | return size;
195 | },
196 |
197 | getCustomConfig() {
198 | return {};
199 | },
200 |
201 | getAnchorPoints() {
202 | return [];
203 | },
204 | };
205 |
206 | G6.registerNode('bizNode', bizNode);
207 |
--------------------------------------------------------------------------------
/src/shape/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { Node } from '@/common/interfaces';
2 |
3 | const canvas = document.createElement('canvas');
4 | const canvasContext = canvas.getContext('2d');
5 |
6 | export function getNodeSide(item: Node): 'left' | 'right' {
7 | const model = item.getModel();
8 |
9 | if (model.side) {
10 | return model.side as 'left' | 'right';
11 | }
12 |
13 | const parent = item.get('parent');
14 |
15 | if (parent) {
16 | return getNodeSide(parent);
17 | }
18 |
19 | return 'right';
20 | }
21 |
22 | export function getRectPath(x: number, y: number, w: number, h: number, r: number) {
23 | if (r) {
24 | return [
25 | ['M', +x + +r, y],
26 | ['l', w - r * 2, 0],
27 | ['a', r, r, 0, 0, 1, r, r],
28 | ['l', 0, h - r * 2],
29 | ['a', r, r, 0, 0, 1, -r, r],
30 | ['l', r * 2 - w, 0],
31 | ['a', r, r, 0, 0, 1, -r, -r],
32 | ['l', 0, r * 2 - h],
33 | ['a', r, r, 0, 0, 1, r, -r],
34 | ['z'],
35 | ];
36 | }
37 |
38 | const res = [['M', x, y], ['l', w, 0], ['l', 0, h], ['l', -w, 0], ['z']];
39 |
40 | res.toString = toString;
41 |
42 | return res;
43 | }
44 |
45 | export function getFoldButtonPath() {
46 | const w = 14;
47 | const h = 14;
48 | const rect = getRectPath(0, 0, w, h, 2);
49 | const hp = `M${(w * 3) / 14},${h / 2}L${(w * 11) / 14},${h / 2}`;
50 | const vp = '';
51 |
52 | return rect + hp + vp;
53 | }
54 |
55 | export function getUnfoldButtonPath() {
56 | const w = 14;
57 | const h = 14;
58 | const rect = getRectPath(0, 0, w, h, 2);
59 | const hp = `M${(w * 3) / 14},${h / 2}L${(w * 11) / 14},${h / 2}`;
60 | const vp = `M${w / 2},${(h * 3) / 14}L${w / 2},${(h * 11) / 14}`;
61 |
62 | return rect + hp + vp;
63 | }
64 |
65 | export function optimizeMultilineText(text: string, font: string, maxRows: number, maxWidth: number) {
66 | canvasContext.font = font;
67 |
68 | if (canvasContext.measureText(text).width <= maxWidth) {
69 | return text;
70 | }
71 |
72 | let multilineText = [];
73 |
74 | let tempText = '';
75 | let tempTextWidth = 0;
76 |
77 | for (const char of text) {
78 | const { width } = canvasContext.measureText(char);
79 |
80 | if (tempTextWidth + width >= maxWidth) {
81 | multilineText.push(tempText);
82 |
83 | tempText = '';
84 | tempTextWidth = 0;
85 | }
86 |
87 | tempText += char;
88 | tempTextWidth += width;
89 | }
90 |
91 | if (tempText) {
92 | multilineText.push(tempText);
93 | }
94 |
95 | if (multilineText.length > maxRows) {
96 | const ellipsis = '...';
97 | const ellipsisWidth = canvasContext.measureText(ellipsis).width;
98 |
99 | let tempText = '';
100 | let tempTextWidth = 0;
101 |
102 | for (const char of multilineText[maxRows - 1]) {
103 | const { width } = canvasContext.measureText(char);
104 |
105 | if (tempTextWidth + width > maxWidth - ellipsisWidth) {
106 | break;
107 | }
108 |
109 | tempText += char;
110 | tempTextWidth += width;
111 | }
112 |
113 | multilineText = multilineText.slice(0, maxRows - 1).concat(`${tempText}${ellipsis}`);
114 | }
115 |
116 | return multilineText.join('\n');
117 | }
118 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import G6 from '@antv/g6';
2 | import { ItemType, ItemState, GraphState, EditorEvent } from '@/common/constants';
3 | import { Graph, TreeGraph, EdgeModel, Item, Node, Edge } from '@/common/interfaces';
4 |
5 | /** 生成唯一标识 */
6 | export function guid() {
7 | return 'xxxxxxxx'.replace(/[xy]/g, function(c) {
8 | const r = (Math.random() * 16) | 0;
9 | const v = c === 'x' ? r : (r & 0x3) | 0x8;
10 | return v.toString(16);
11 | });
12 | }
13 |
14 | /** 拼接查询字符 */
15 | export const toQueryString = (obj: object) =>
16 | Object.keys(obj)
17 | .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
18 | .join('&');
19 |
20 | /** 执行批量处理 */
21 | export function executeBatch(graph: Graph, execute: Function) {
22 | const autoPaint = graph.get('autoPaint');
23 |
24 | graph.setAutoPaint(false);
25 |
26 | execute();
27 |
28 | graph.paint();
29 | graph.setAutoPaint(autoPaint);
30 | }
31 |
32 | /** 执行递归遍历 */
33 | export function recursiveTraversal(root, callback) {
34 | if (!root) {
35 | return;
36 | }
37 |
38 | callback(root);
39 |
40 | if (!root.children) {
41 | return;
42 | }
43 |
44 | root.children.forEach(item => recursiveTraversal(item, callback));
45 | }
46 |
47 | /** 判断是否流程图 */
48 | export function isFlow(graph: Graph) {
49 | return graph.constructor === G6.Graph;
50 | }
51 |
52 | /** 判断是否脑图 */
53 | export function isMind(graph: Graph) {
54 | return graph.constructor === G6.TreeGraph;
55 | }
56 |
57 | /** 判断是否节点 */
58 | export function isNode(item: Item) {
59 | return item.getType() === ItemType.Node;
60 | }
61 |
62 | /** 判断是否边线 */
63 | export function isEdge(item: Item) {
64 | return item.getType() === ItemType.Edge;
65 | }
66 |
67 | /** 获取选中节点 */
68 | export function getSelectedNodes(graph: Graph): Node[] {
69 | return graph.findAllByState(ItemType.Node, ItemState.Selected);
70 | }
71 |
72 | /** 获取选中边线 */
73 | export function getSelectedEdges(graph: Graph): Edge[] {
74 | return graph.findAllByState(ItemType.Edge, ItemState.Selected);
75 | }
76 |
77 | /** 获取高亮边线 */
78 | export function getHighlightEdges(graph: Graph): Edge[] {
79 | return graph.findAllByState(ItemType.Edge, ItemState.HighLight);
80 | }
81 |
82 | /** 获取图表状态 */
83 | export function getGraphState(graph: Graph): GraphState {
84 | let graphState: GraphState = GraphState.MultiSelected;
85 |
86 | const selectedNodes = getSelectedNodes(graph);
87 | const selectedEdges = getSelectedEdges(graph);
88 |
89 | if (selectedNodes.length === 1 && !selectedEdges.length) {
90 | graphState = GraphState.NodeSelected;
91 | }
92 |
93 | if (selectedEdges.length === 1 && !selectedNodes.length) {
94 | graphState = GraphState.EdgeSelected;
95 | }
96 |
97 | if (!selectedNodes.length && !selectedEdges.length) {
98 | graphState = GraphState.CanvasSelected;
99 | }
100 |
101 | return graphState;
102 | }
103 |
104 | /** 设置选中元素 */
105 | export function setSelectedItems(graph: Graph, items: Item[] | string[]) {
106 | executeBatch(graph, () => {
107 | const selectedNodes = getSelectedNodes(graph);
108 | const selectedEdges = getSelectedEdges(graph);
109 |
110 | [...selectedNodes, ...selectedEdges].forEach(node => {
111 | graph.setItemState(node, ItemState.Selected, false);
112 | });
113 |
114 | items.forEach(item => {
115 | graph.setItemState(item, ItemState.Selected, true);
116 | });
117 | });
118 |
119 | graph.emit(EditorEvent.onGraphStateChange, {
120 | graphState: getGraphState(graph),
121 | });
122 | }
123 |
124 | /** 清除选中状态 */
125 | export function clearSelectedState(graph: Graph, shouldUpdate: (item: Item) => boolean = () => true) {
126 | const selectedNodes = getSelectedNodes(graph);
127 | const selectedEdges = getSelectedEdges(graph);
128 |
129 | executeBatch(graph, () => {
130 | [...selectedNodes, ...selectedEdges].forEach(item => {
131 | if (shouldUpdate(item)) {
132 | graph.setItemState(item, ItemState.Selected, false);
133 | }
134 | });
135 | });
136 | }
137 |
138 | /** 获取回溯路径 - Flow */
139 | export function getFlowRecallEdges(graph: Graph, node: Node, targetIds: string[] = [], edges: Edge[] = []) {
140 | const inEdges: Edge[] = node.getInEdges();
141 |
142 | if (!inEdges.length) {
143 | return [];
144 | }
145 |
146 | inEdges.map(edge => {
147 | const sourceId = (edge.getModel() as EdgeModel).source;
148 | const sourceNode = graph.findById(sourceId) as Node;
149 |
150 | edges.push(edge);
151 |
152 | const targetId = node.get('id');
153 |
154 | targetIds.push(targetId);
155 |
156 | if (!targetIds.includes(sourceId)) {
157 | getFlowRecallEdges(graph, sourceNode, targetIds, edges);
158 | }
159 | });
160 |
161 | return edges;
162 | }
163 |
164 | /** 获取回溯路径 - Mind */
165 | export function getMindRecallEdges(graph: TreeGraph, node: Node, edges: Edge[] = []) {
166 | const parentNode = node.get('parent');
167 |
168 | if (!parentNode) {
169 | return edges;
170 | }
171 |
172 | node.getEdges().forEach(edge => {
173 | const source = edge.getModel().source as Edge;
174 |
175 | if (source.get('id') === parentNode.get('id')) {
176 | edges.push(edge);
177 | }
178 | });
179 |
180 | return getMindRecallEdges(graph, parentNode, edges);
181 | }
182 |
--------------------------------------------------------------------------------
/tsconfig.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "paths": {
6 | "@/*": ["lib/*"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "declaration": true,
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "target": "esnext",
8 | "jsx": "react",
9 | "noImplicitThis": true,
10 | "strictBindCallApply": true,
11 | "baseUrl": ".",
12 | "paths": {
13 | "@/*": ["src/*"]
14 | }
15 | },
16 | "include": ["src"]
17 | }
18 |
--------------------------------------------------------------------------------