Browse Source

feat: x6 upgrade and refactor code

master
smallstonesk 5 years ago
parent
commit
797674f7fa
  1. 4401
      packages/core/package-lock.json
  2. 42
      packages/core/package.json
  3. 10
      packages/core/rollup.config.js
  4. 97
      packages/core/src/common/baseCell/behavior.ts
  5. 130
      packages/core/src/common/baseCell/branch.ts
  6. 11
      packages/core/src/common/baseCell/index.ts
  7. 99
      packages/core/src/common/baseCell/start.ts
  8. 21
      packages/core/src/common/previewCell/behavior.ts
  9. 29
      packages/core/src/common/previewCell/branch.ts
  10. 11
      packages/core/src/common/previewCell/index.ts
  11. 22
      packages/core/src/common/previewCell/start.ts
  12. 139
      packages/core/src/common/shortcuts.ts
  13. 34
      packages/core/src/components/cells/action.tsx
  14. 33
      packages/core/src/components/cells/decision.tsx
  15. 27
      packages/core/src/components/cells/end.tsx
  16. 15
      packages/core/src/components/cells/index.tsx
  17. 27
      packages/core/src/components/cells/start.tsx
  18. 31
      packages/core/src/components/colorPicker/index.tsx
  19. 76
      packages/core/src/components/schema-form.tsx
  20. 164
      packages/core/src/components/setting.tsx
  21. 131
      packages/core/src/components/sidebar.tsx
  22. 84
      packages/core/src/components/toolbar.tsx
  23. 13
      packages/core/src/components/xIcon/index.tsx
  24. 68
      packages/core/src/data/cells.ts
  25. 89
      packages/core/src/data/commands.tsx
  26. 22
      packages/core/src/index.module.less
  27. 130
      packages/core/src/index.tsx
  28. 4
      packages/core/src/model/cells.ts
  29. 9
      packages/core/src/model/commands.ts
  30. 2
      packages/core/src/model/index.ts
  31. 19
      packages/core/src/mods/flowChart/index.module.less
  32. 30
      packages/core/src/mods/flowChart/index.tsx
  33. 12
      packages/core/src/mods/header/index.module.less
  34. 18
      packages/core/src/mods/header/index.tsx
  35. 9
      packages/core/src/mods/settingBar/components/checkbox/index.module.less
  36. 34
      packages/core/src/mods/settingBar/components/checkbox/index.tsx
  37. 27
      packages/core/src/mods/settingBar/components/code/index.module.less
  38. 130
      packages/core/src/mods/settingBar/components/code/index.tsx
  39. 9
      packages/core/src/mods/settingBar/components/input/index.module.less
  40. 32
      packages/core/src/mods/settingBar/components/input/index.tsx
  41. 27
      packages/core/src/mods/settingBar/components/json/index.module.less
  42. 145
      packages/core/src/mods/settingBar/components/json/index.tsx
  43. 35
      packages/core/src/mods/settingBar/index.module.less
  44. 58
      packages/core/src/mods/settingBar/index.tsx
  45. 5
      packages/core/src/mods/settingBar/mods/basic/index.module.less
  46. 88
      packages/core/src/mods/settingBar/mods/basic/index.tsx
  47. 23
      packages/core/src/mods/settingBar/mods/config/index.module.less
  48. 120
      packages/core/src/mods/settingBar/mods/config/index.tsx
  49. 20
      packages/core/src/mods/sideBar/index.module.less
  50. 109
      packages/core/src/mods/sideBar/index.tsx
  51. 18
      packages/core/src/mods/toolBar/index.module.less
  52. 35
      packages/core/src/mods/toolBar/index.tsx
  53. 54
      packages/core/src/mods/toolBar/widgets/bgColor.tsx
  54. 32
      packages/core/src/mods/toolBar/widgets/bold.tsx
  55. 27
      packages/core/src/mods/toolBar/widgets/bringToBack.tsx
  56. 29
      packages/core/src/mods/toolBar/widgets/bringToTop.tsx
  57. 21
      packages/core/src/mods/toolBar/widgets/fitWindow.tsx
  58. 52
      packages/core/src/mods/toolBar/widgets/fontSize.tsx
  59. 102
      packages/core/src/mods/toolBar/widgets/horizontalAlign.tsx
  60. 91
      packages/core/src/mods/toolBar/widgets/index.module.less
  61. 35
      packages/core/src/mods/toolBar/widgets/index.tsx
  62. 32
      packages/core/src/mods/toolBar/widgets/italic.tsx
  63. 54
      packages/core/src/mods/toolBar/widgets/lineColor.tsx
  64. 51
      packages/core/src/mods/toolBar/widgets/makeBtnWidget.tsx
  65. 51
      packages/core/src/mods/toolBar/widgets/makeDropdownWidget.tsx
  66. 23
      packages/core/src/mods/toolBar/widgets/redo.tsx
  67. 20
      packages/core/src/mods/toolBar/widgets/save.tsx
  68. 54
      packages/core/src/mods/toolBar/widgets/textColor.tsx
  69. 32
      packages/core/src/mods/toolBar/widgets/underline.tsx
  70. 23
      packages/core/src/mods/toolBar/widgets/undo.tsx
  71. 102
      packages/core/src/mods/toolBar/widgets/verticalAlign.tsx
  72. 64
      packages/core/src/mods/toolBar/widgets/zoom.tsx
  73. 9
      packages/core/src/typings.d.ts
  74. 88
      packages/core/src/utils/create-graph.tsx
  75. 88
      packages/core/src/utils/createGraph.ts
  76. 26
      packages/core/src/utils/index.ts
  77. 69
      packages/core/src/utils/patch-dnd.tsx
  78. 16
      packages/core/src/utils/styles/cells.ts
  79. 68
      packages/core/src/utils/tool.ts
  80. 7
      packages/core/tsconfig.json

4401
packages/core/package-lock.json

File diff suppressed because it is too large

42
packages/core/package.json

@ -1,6 +1,6 @@
{
"name": "@imove/core",
"version": "0.3.3",
"version": "0.3.4",
"description": "流程编排",
"keywords": [
"imove",
@ -35,27 +35,41 @@
"bugs": {
"url": "https://github.com/suanmei/iMove/issues"
},
"devDependencies": {
"dependencies": {
"@ant-design/icons": "^4.0.6",
"@antv/x6": "^0.3.4",
"@antv/x6-react-shape": "^0.2.2",
"@emotion/core": "^10.0.28",
"@emotion/styled": "^10.0.27",
"@antv/x6": "^0.10.59",
"@antv/x6-components": "^0.10.1",
"ace-builds": "^1.4.12",
"antd": "^4.5.3",
"babel-runtime": "^6.26.0",
"react-ace": "^9.1.3",
"react-color": "^2.18.1",
"react-json-view": "^1.19.1"
},
"devDependencies": {
"@types/react": "^16.9.34",
"@types/react-color": "^3.0.4",
"@types/react-dom": "^16.9.7",
"antd": "^4.1.4",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"autoprefixer": "^9.8.6",
"awesome-typescript-loader": "^5.2.1",
"babel-loader": "^8.1.0",
"css-loader": "^4.2.1",
"file-loader": "^6.0.0",
"less": "^3.12.2",
"less-loader": "^6.2.0",
"postcss-loader": "^3.0.0",
"rollup": "^2.6.1",
"rollup-plugin-jsx": "^1.0.3",
"rollup-plugin-postcss": "^3.1.3",
"rollup-plugin-postcss-modules": "^2.0.1",
"rollup-plugin-typescript": "^1.0.1",
"watch": "^1.0.2"
},
"peerDependencies": {
"@ant-design/icons": "^4.0.6",
"@antv/x6": "^0.3.4",
"@antv/x6-react-shape": "^0.2.2",
"@emotion/core": "^10.0.28",
"@emotion/styled": "^10.0.27",
"antd": "^4.1.4",
"@antv/x6": "0.10.59",
"@antv/x6-components": "^0.10.1",
"antd": "^4.5.3",
"react": "^16.13.1",
"react-dom": "^16.13.1"
},

10
packages/core/rollup.config.js

@ -1,7 +1,13 @@
import rollupBaseConfig from '../../rollup.config';
import pkg from './package.json';
import postcss from 'rollup-plugin-postcss';
import typescript from 'rollup-plugin-typescript';
import rollupBaseConfig from '../../rollup.config';
export default Object.assign(rollupBaseConfig, {
plugins: [
postcss(),
typescript(),
],
output: [
{
file: pkg.main,
@ -12,5 +18,5 @@ export default Object.assign(rollupBaseConfig, {
format: 'es',
},
],
external: Object.keys(pkg.peerDependencies).concat(/^antd\//),
external: Object.keys(pkg.peerDependencies),
});

97
packages/core/src/common/baseCell/behavior.ts

@ -0,0 +1,97 @@
import {Shape} from '@antv/x6';
const schema = {
base: Shape.Rect,
shape: 'imove-behavior',
width: 80,
height: 40,
label: '处理',
attrs: {
body: {
fill: '#AACCF7',
stroke: '#5E9CEE'
},
label: {
fill: '#333'
}
},
ports: {
groups: {
top: {
position: 'top',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
},
right: {
position: 'right',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
},
bottom: {
position: 'bottom',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
},
left: {
position: 'left',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
},
},
items: [
{
id: 'top',
group: 'top'
},
{
id: 'right',
group: 'right'
},
{
id: 'bottom',
group: 'bottom'
},
{
id: 'left',
group: 'left'
}
]
},
data: {
label: '处理',
configSchema: '{\n \n}',
configData: {},
dependencies: '{\n \n}',
code: 'export default async function(ctx) {\n \n}'
}
};
export default schema;

130
packages/core/src/common/baseCell/branch.ts

@ -0,0 +1,130 @@
import {Shape} from '@antv/x6';
const schema = {
base: Shape.Polygon,
shape: 'imove-branch',
width: 80,
height: 40,
attrs: {
body: {
strokeWidth: 2,
stroke: '#333',
fill: '#FFFC7C',
refPoints: '0,10 10,0 20,10 10,20'
},
label: {
refX: 0.5,
refY: 0.5,
text: '判断',
fill: '#333',
fontSize: 14,
textAnchor: 'middle',
textVerticalAnchor: 'middle'
}
},
ports: {
groups: {
top: {
position: 'top',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
},
right: {
zIndex: 999999999,
position: 'right',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
label: {
position: 'right'
}
},
bottom: {
zIndex: 999999999,
position: 'bottom',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
label: {
position: 'bottom'
}
},
left: {
position: 'left',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
},
},
items: [
{
id: 'top',
group: 'top'
},
{
id: 'right',
group: 'right',
attrs: {
text: {
text: '是'
}
}
},
{
id: 'bottom',
group: 'bottom',
attrs: {
text: {
text: '否'
}
}
},
{
id: 'left',
group: 'left'
}
]
},
data: {
label: '判断',
configSchema: '{\n \n}',
configData: {},
dependencies: '{\n \n}',
ports: {
right: {
condition: 'true'
},
bottom: {
condition: 'false'
}
},
code: 'export default async function(ctx) {\n return true;\n}'
}
};
export default schema;

11
packages/core/src/common/baseCell/index.ts

@ -0,0 +1,11 @@
import start from './start';
import branch from './branch';
import behavior from './behavior';
const cellSchemaMap: {[key: string]: any} = {
'imove-start': start,
'imove-branch': branch,
'imove-behavior': behavior
};
export default cellSchemaMap;

99
packages/core/src/common/baseCell/start.ts

@ -0,0 +1,99 @@
import {Shape} from '@antv/x6';
const schema = {
base: Shape.Circle,
shape: 'imove-start',
width: 80,
height: 80,
label: '开始',
attrs: {
body: {
fill: '#333',
strokeWidth: 1,
stroke: '#333'
},
label: {
fill: '#FFF'
}
},
ports: {
groups: {
top: {
position: 'top',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
},
right: {
position: 'right',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
},
bottom: {
position: 'bottom',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
},
left: {
position: 'left',
attrs: {
circle: {
r: 3,
magnet: true,
stroke: '#666',
strokeWidth: 1,
fill: '#fff'
},
},
},
},
items: [
{
id: 'top',
group: 'top'
},
{
id: 'right',
group: 'right'
},
{
id: 'bottom',
group: 'bottom'
},
{
id: 'left',
group: 'left'
}
]
},
data: {
label: '开始',
configSchema: '{\n \n}',
configData: {},
trigger: 'start',
dependencies: '{\n \n}',
code: 'export default async function(ctx) {\n \n}'
}
};
export default schema;

21
packages/core/src/common/previewCell/behavior.ts

@ -0,0 +1,21 @@
import {Shape} from '@antv/x6';
const schema = {
base: Shape.Rect,
shape: 'imove-behavior-preview',
top: 20,
width: 80,
height: 40,
label: '处理',
attrs: {
body: {
fill: '#AACCF7',
stroke: '#5E9CEE'
},
label: {
fill: '#333'
}
}
};
export default schema;

29
packages/core/src/common/previewCell/branch.ts

@ -0,0 +1,29 @@
import {Shape} from '@antv/x6';
const schema = {
base: Shape.Polygon,
shape: 'imove-branch-preview',
top: 20,
width: 80,
height: 40,
label: '分支',
attrs: {
body: {
strokeWidth: 2,
stroke: '#333',
fill: '#FFFC7C',
refPoints: '0,10 10,0 20,10 10,20'
},
label: {
refX: 0.5,
refY: 0.5,
text: '判断',
fill: '#333',
fontSize: 14,
textAnchor: 'middle',
textVerticalAnchor: 'middle'
}
}
};
export default schema;

11
packages/core/src/common/previewCell/index.ts

@ -0,0 +1,11 @@
import start from './start';
import branch from './branch';
import behavior from './behavior';
const cellSchemaMap: {[key: string]: any} = {
'imove-start-preview': start,
'imove-branch-preview': branch,
'imove-behavior-preview': behavior
};
export default cellSchemaMap;

22
packages/core/src/common/previewCell/start.ts

@ -0,0 +1,22 @@
import {Shape} from '@antv/x6';
const schema = {
base: Shape.Circle,
shape: 'imove-start-preview',
top: 0,
width: 80,
height: 80,
label: '开始',
attrs: {
body: {
strokeWidth: 0,
stroke: '#333',
fill: '#333'
},
label: {
fill: '#FFF'
}
},
};
export default schema;

139
packages/core/src/common/shortcuts.ts

@ -0,0 +1,139 @@
import {Graph} from '@antv/x6';
import {safeGet} from '../utils';
interface Shortcut {
keys: string | string[];
handler: (flowChart: Graph) => void;
}
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 1.5;
const ZOOM_STEP = 0.1;
const shortcuts: {[key: string]: Shortcut} = {
save: {
keys: `meta + s`,
handler(flowChart: Graph) {
// TODO: save data
console.log(JSON.stringify(flowChart.toJSON(), null, 2));
fetch('http://localhost:3456/api/save', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({dsl: flowChart.toJSON()})
});
return false;
}
},
undo: {
keys: `meta + z`,
handler(flowChart: Graph) {
flowChart.undo();
return false;
}
},
redo: {
keys: `meta + shift + z`,
handler(flowChart: Graph) {
flowChart.redo();
return false;
}
},
zoomIn: {
keys: `meta + shift + \+`,
handler(flowChart: Graph) {
const nextZoom = (flowChart.zoom() + ZOOM_STEP).toPrecision(2);
flowChart.zoomTo(Number(nextZoom), {maxScale: MAX_ZOOM});
return false;
}
},
zoomOut: {
keys: `meta + shift + \-`,
handler(flowChart: Graph) {
const nextZoom = (flowChart.zoom() - ZOOM_STEP).toPrecision(2);
flowChart.zoomTo(Number(nextZoom), {minScale: MIN_ZOOM});
return false;
}
},
copy: {
keys: 'meta + c',
handler(flowChart: Graph) {
const cells = flowChart.getSelectedCells();
if (cells.length > 0) {
flowChart.copy(cells);
}
return false;
}
},
paste: {
keys: 'meta + v',
handler(flowChart: Graph) {
if (!flowChart.isClipboardEmpty()) {
const cells = flowChart.paste({offset: 32});
flowChart.cleanSelection();
flowChart.select(cells);
}
return false;
}
},
delete: {
keys: ['backspace', 'del'],
handler(flowChart: Graph) {
flowChart.removeCells(flowChart.getSelectedCells());
flowChart.trigger('toolBar:forceUpdate');
return false;
}
},
selectAll: {
keys: 'meta + a',
handler(flowChart: Graph) {
flowChart.select(flowChart.getCells());
return false;
}
},
bold: {
keys: 'meta + b',
handler(flowChart: Graph) {
const cells = flowChart.getSelectedCells();
if (cells.length > 0) {
const isAlreadyBold = safeGet(cells, '0.attrs.label.fontWeight', 'normal') === 'bold';
cells.forEach((cell) => {
cell.setAttrs({label: {fontWeight: isAlreadyBold ? 'normal' : 'bold'}});
});
flowChart.trigger('toolBar:forceUpdate');
}
return false;
}
},
italic: {
keys: 'meta + i',
handler(flowChart: Graph) {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
const isAlreadyItalic = safeGet(cells, '0.attrs.label.fontStyle', 'normal') === 'italic';
cells.forEach((cell) => {
cell.setAttrs({label: {fontStyle: isAlreadyItalic ? 'normal' : 'italic'}});
});
flowChart.trigger('toolBar:forceUpdate');
}
return false;
}
},
underline: {
keys: 'meta + u',
handler(flowChart: Graph) {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
const isAlreadyUnderline = safeGet(cells, '0.attrs.label.textDecoration', 'none') === 'underline';
cells.forEach((cell) => {
cell.setAttrs({label: {textDecoration: isAlreadyUnderline ? 'none' : 'underline'}});
});
flowChart.trigger('toolBar:forceUpdate');
}
return false;
}
}
};
export default shortcuts;

34
packages/core/src/components/cells/action.tsx

@ -1,34 +0,0 @@
/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import { CellProps } from '../../model';
import Label from '../../utils/styles/cells';
const StateCell = ({ label, scale = 1 }: CellProps): JSX.Element => {
const width = 48 * scale;
const height = 30 * scale;
return (
<div>
<svg
css={css`
width: ${width}px;
height: ${height}px;
`}
>
<rect
css={css`
fill: #daeffa;
stroke: #82caed;
`}
x="1"
y="1"
width={width - 2}
height={height - 2}
strokeWidth="2"
/>
</svg>
{label && <Label>{label}</Label>}
</div>
);
};
export default StateCell;

33
packages/core/src/components/cells/decision.tsx

@ -1,33 +0,0 @@
/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import { CellProps } from '../../model';
import Label from '../../utils/styles/cells';
const DecisionCell = ({ label, scale = 1 }: CellProps): JSX.Element => {
const width = 48 * scale;
const height = 30 * scale;
return (
<div>
<svg
css={css`
width: ${width}px;
height: ${height}px;
`}
>
<polygon
css={css`
fill: #fef0dd;
stroke: #fccd8e;
`}
points={`${1 * scale},${15 * scale} ${24 * scale},${29 * scale} ${47 * scale},${
15 * scale
} ${24 * scale},${1 * scale}`}
strokeWidth="2"
/>
</svg>
{label && <Label>{label}</Label>}
</div>
);
};
export default DecisionCell;

27
packages/core/src/components/cells/end.tsx

@ -1,27 +0,0 @@
/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import { CellProps } from '../../model';
const EndCell = ({ scale = 1 }: CellProps): JSX.Element => {
const width = 20 * scale;
const radius = 10 * scale;
return (
<svg
css={css`
width: ${width}px;
height: ${width}px;
`}
>
<circle
css={{ fill: '#f55753' }}
cx={radius}
cy={radius}
r={radius}
strokeWidth={2 * scale}
/>
<circle css={{ fill: '#faaba9' }} cx={radius} cy={radius} r={8 * scale} />
</svg>
);
};
export default EndCell;

15
packages/core/src/components/cells/index.tsx

@ -1,15 +0,0 @@
import Start from './start';
import Action from './action';
import Decision from './decision';
import End from './end';
const CellMap = {
start: Start,
action: Action,
decision: Decision,
end: End,
};
export default CellMap;
export type CellTypes = keyof typeof CellMap;

27
packages/core/src/components/cells/start.tsx

@ -1,27 +0,0 @@
/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import { CellProps } from '../../model';
const StartCell = ({ scale = 1 }: CellProps): JSX.Element => {
const width = 20 * scale;
const radius = 10 * scale;
return (
<svg
css={css`
width: ${width}px;
height: ${width}px;
`}
>
<circle
css={{ fill: '#76ca02' }}
cx={radius}
cy={radius}
r={radius}
strokeWidth={2 * scale}
/>
<circle css={{ fill: '#bbe581' }} cx={radius} cy={radius} r={8 * scale} />
</svg>
);
};
export default StartCell;

31
packages/core/src/components/colorPicker/index.tsx

@ -0,0 +1,31 @@
import React, {useState, useEffect} from 'react';
import {SketchPicker, ColorResult} from 'react-color';
interface IProps {
color: string;
onChangeComplete: (color: string) => void;
}
const ColorPicker: React.FC<IProps> = (props) => {
const {color, onChangeComplete: changeCb} = props;
const [curColor, setCurColor] = useState(color);
// life
useEffect(() => setCurColor(color), [color]);
// events
const onChange = (color: ColorResult): void => setCurColor(color.hex);
const onChangeComplete = (color: ColorResult): void => changeCb(color.hex);
return (
<SketchPicker
color={curColor}
onChange={onChange}
onChangeComplete={onChangeComplete}
/>
);
};
export default ColorPicker;

76
packages/core/src/components/schema-form.tsx

@ -1,76 +0,0 @@
import * as React from 'react';
import { Form, Input, InputNumber } from 'antd';
import 'antd/lib/input-number/style';
const { Item } = Form;
const labelCol = { span: 7 };
export interface SchemaFormProps {
schema: any;
data: any;
}
export interface SchemaFormState {
data: any;
}
class SchemaForm extends React.Component<SchemaFormProps, SchemaFormState> {
constructor(props: SchemaFormProps) {
super(props);
this.state = {
data: props.data,
};
}
changeValue = (e: React.ChangeEvent<HTMLInputElement>, key: string): void => {
const { data } = this.state;
const value = e.target.value.trim();
if (!data.input) {
data.input = {};
}
data.input[key] = value;
this.setState({ data });
};
changeNumValue = (value: number | undefined, key: string): void => {
const { data } = this.state;
if (!data.input) {
data.input = {};
}
data.input[key] = value;
this.setState({ data });
};
render(): JSX.Element {
const { data } = this.state;
const {
schema: { properties },
} = this.props;
return (
<Form labelCol={labelCol} initialValues={data}>
{Object.keys(properties).map((key) => (
<Item key={key} label={properties[key].title} name={`input.${properties[key].title}`}>
{properties[key].type === 'string' ? (
<Input
onChange={(e): void => {
this.changeValue(e, key);
}}
/>
) : (
<InputNumber
onChange={(value): void => {
this.changeNumValue(value, key);
}}
/>
)}
</Item>
))}
</Form>
);
}
}
export default SchemaForm;

164
packages/core/src/components/setting.tsx

@ -1,164 +0,0 @@
/** @jsx jsx */
import { Component } from 'react';
import { jsx, css } from '@emotion/core';
import styled from '@emotion/styled';
import { Graph, Cell } from '@antv/x6';
import { Collapse, Form, Input } from 'antd';
import SchemaForm from './schema-form';
import 'antd/lib/form/style';
import 'antd/lib/input/style';
const { Panel } = Collapse;
const { Item } = Form;
const labelCol = { span: 7 };
const Header = styled.h3`
margin-bottom: 0;
padding-left: 20px;
height: 36px;
line-height: 36px;
font-size: 12px;
color: #555;
border-bottom: 1px solid #ddd;
`;
const SettingCollapse = styled(Collapse)`
font-size: 14px;
border: none;
> .ant-collapse-item > .ant-collapse-header {
height: 36px;
line-height: 12px;
font-size: 12px;
color: #666;
}
`;
interface SettingProps {
graph: Graph;
}
interface SettingState {
cell: Cell<any> | null;
}
class Setting extends Component<SettingProps, SettingState> {
constructor(props: SettingProps) {
super(props);
this.state = {
cell: null,
};
const { graph } = this.props;
graph.on('selection:changed', () => {
const cells = graph.getSelectedCells();
if (cells.length === 0) {
this.setState({ cell: null });
return;
}
const cell = cells[0];
if (cell.isEdge()) {
cell.data = {};
}
this.setState({ cell });
});
}
render(): JSX.Element {
const { graph } = this.props;
const { cell } = this.state;
let cellType = '';
if (cell) {
if (cell.isNode()) {
cellType = cell.data.type.toUpperCase();
} else {
cellType = 'EDGE';
}
} else {
cellType = 'FLOW';
}
return (
<div
css={css`
height: 100%;
overflow: auto;
border-left: 1px solid #ddd;
`}
>
<Header>{cellType}</Header>
{cell && !['start', 'end'].includes(cell.data.type) && (
<SettingCollapse
defaultActiveKey={['general', 'event', 'input', 'output']}
key={cell && cell.id ? cell.id : ''}
>
<Panel header="General" key="general">
<Form labelCol={labelCol} initialValues={{ label: cell.data.label }}>
<Item label="名称" name="label">
<Input
onChange={(e): void => {
cell.data.label = e.target.value.trim();
graph.setCellData(cell, cell.data);
}}
/>
</Item>
</Form>
</Panel>
{/* <Panel header="Event" key="event">
<Form
labelCol={labelCol}
initialValues={{ event: cell.data && cell.data.data && cell.data.data.event }}
>
<Item label="触发事件" name="event">
<Input
onChange={(e): void => {
if (!cell.data.data) {
cell.data.data = {};
}
cell.data.data.event = e.target.value.trim();
graph.setCellData(cell, cell.data);
}}
/>
</Item>
</Form>
</Panel> */}
{cell.data && cell.data.schema && Object.keys(cell.data.schema.properties).length && (
<Panel header="Input" key="input">
<SchemaForm schema={cell.data.schema} data={cell.data.data} />
</Panel>
)}
<Panel header="Output" key="output">
<Form
labelCol={labelCol}
initialValues={{ event: cell.data && cell.data.data && cell.data.data.contextKey }}
>
<Item label="存储字段" name="output.contextKey">
<Input
onChange={(e): void => {
if (!cell.data.data) {
cell.data.data = {};
}
if (!cell.data.data.output) {
cell.data.data.output = {};
}
cell.data.data.output.contextKey = e.target.value.trim();
graph.setCellData(cell, cell.data);
}}
/>
</Item>
</Form>
</Panel>
</SettingCollapse>
)}
</div>
);
}
}
export default Setting;

131
packages/core/src/components/sidebar.tsx

@ -1,131 +0,0 @@
/** @jsx jsx */
import { useRef, useEffect, ReactNode } from 'react';
import { jsx, css } from '@emotion/core';
import styled from '@emotion/styled';
import { Graph } from '@antv/x6';
import { Collapse } from 'antd';
import { generals, DataItem } from '../data/cells';
import patchDnd from '../utils/patch-dnd';
import Cells from './cells';
import 'antd/lib/collapse/style';
const { Panel } = Collapse;
const ToolCollapse = styled(Collapse)`
border: none;
> .ant-collapse-item > .ant-collapse-header {
height: 36px;
line-height: 12px;
font-size: 12px;
color: #666;
}
`;
const ToolPanel = styled(Panel)`
margin-right: -1px;
.ant-collapse-content-box {
margin-bottom: -1px;
padding: 0;
overflow: hidden;
}
`;
interface SidebarProps {
graph: Graph;
cells: DataItem[];
}
const CellWrapper = ({ data, children }: { data: DataItem; children: ReactNode }): JSX.Element => {
return (
<div
css={css`
box-sizing: border-box;
display: inline-block;
padding: 10px 5px;
width: 50%;
height: 80px;
text-align: center;
vertical-align: top;
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
:hover {
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
}
`}
>
<div
css={css`
display: flex;
align-items: center;
justify-content: center;
height: 40px;
`}
>
{children}
</div>
<span
css={css`
font-size: 11px;
line-height: 19px;
`}
>
{data.label}
</span>
</div>
);
};
const Sidebar = ({ graph, cells }: SidebarProps): JSX.Element => {
const generalRef = useRef<HTMLDivElement>(null);
const gistRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const generalContainer = generalRef.current;
patchDnd(generalContainer, graph, generals);
const gistContainer = gistRef.current;
patchDnd(gistContainer, graph, cells);
});
return (
<div
css={css`
height: 100%;
overflow: auto;
border-right: 1px solid #ddd;
`}
>
<ToolCollapse defaultActiveKey={['general', 'gist']}>
<ToolPanel header="General" key="general">
<div ref={generalRef}>
{generals.map((cell) => {
const Cell = Cells[cell.type];
return (
<CellWrapper key={cell.type} data={cell}>
<Cell />
</CellWrapper>
);
})}
</div>
</ToolPanel>
<ToolPanel header="业务元件" key="gist">
<div ref={gistRef}>
{cells.map((cell) => {
const Cell = Cells[cell.type];
return (
<CellWrapper key={`${cell.type}${cell.label}`} data={cell}>
<Cell />
</CellWrapper>
);
})}
</div>
</ToolPanel>
</ToolCollapse>
</div>
);
};
export default Sidebar;

84
packages/core/src/components/toolbar.tsx

@ -1,84 +0,0 @@
import * as React from 'react';
import styled from '@emotion/styled';
import { Graph, UndoManager } from '@antv/x6';
import { Tooltip } from 'antd';
import { bindKey } from '../utils/tool';
import getCommands from '../data/commands';
import 'antd/lib/tooltip/style';
export interface ToolbarProps {
graph: Graph;
onSave: (data: { nodes: any; edges: any }) => void;
}
const Group = styled.div`
position: relative;
display: inline-block;
box-sizing: border-box;
padding: 5px;
line-height: 25px;
:first-of-type {
:before {
display: none;
}
}
:before {
content: '';
display: block;
position: absolute;
width: 1px;
height: 12px;
background: #d2d2d2;
top: 50%;
left: 0;
transform: translateY(-50%);
}
`;
const Item = styled.span`
display: inline-block;
margin: 0 2px;
padding: 0 5px;
border-radius: 3px;
cursor: pointer;
:hover {
background: #ddd;
}
`;
const Toolbar = ({ graph, onSave }: ToolbarProps): JSX.Element => {
const undoManager = UndoManager.create(graph);
const commands = getCommands(graph, undoManager);
bindKey(commands, graph);
return (
<div>
{commands.map((items) => (
<Group key={items.map((i) => i.name).join('-')}>
{items.map((item) => (
<Tooltip title={item.tooltip} key={item.name}>
<Item
onClick={(): void => {
if (item.name === 'save') {
const graphData = graph.toJSON();
onSave(graphData);
return;
}
item.handler();
}}
>
{item.icon}
</Item>
</Tooltip>
))}
</Group>
))}
</div>
);
};
export default Toolbar;

13
packages/core/src/components/xIcon/index.tsx

@ -0,0 +1,13 @@
import React from 'react';
import {createFromIconfontCN} from '@ant-design/icons';
interface IconFontProps {
type: string;
className?: string;
}
const XIcon: React.SFC<IconFontProps> = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_2024452_lw8k0l261fe.js'
});
export default XIcon;

68
packages/core/src/data/cells.ts

@ -1,68 +0,0 @@
import { CellTypes } from '../components/cells';
interface Style {
width: number;
height: number;
scale: number;
}
export interface DataItem {
label: string;
type: CellTypes;
style: Style;
data?: any;
isEdge?: boolean;
schema?: any;
}
export const generals: DataItem[] = [
{
label: 'Start Event',
type: 'start',
style: {
width: 20,
height: 20,
scale: 2,
},
},
{
label: 'End Event',
type: 'end',
style: {
width: 20,
height: 20,
scale: 2,
},
},
];
const items: any = [];
new Array(localStorage.length).fill(1).forEach((val, index) => {
const item = localStorage.key(index);
if (item && item?.startsWith('cell')) {
let data: any = localStorage.getItem(item);
if (data) {
data = JSON.parse(data);
(data as any).type = data.type.toLowerCase();
items.push(data);
}
}
});
export const gists: DataItem[] = items.map((item: any) => {
return {
label: item.label,
type: item.type,
style: {
width: 48,
height: 30,
scale: 2,
},
data: {
code: item.code,
dependencies: [{ '@ali/rax-base': '^2.1.9' }],
},
schema: item.schema,
};
});

89
packages/core/src/data/commands.tsx

@ -1,89 +0,0 @@
import * as React from 'react';
import {
UndoOutlined,
RedoOutlined,
ZoomInOutlined,
ZoomOutOutlined,
RetweetOutlined,
FullscreenOutlined,
DeleteOutlined,
SaveOutlined,
} from '@ant-design/icons';
import { Graph, UndoManager, Platform } from '@antv/x6';
import * as Tool from '../utils/tool';
import { Commands } from '../model';
export default function getCommands(graph: Graph, undoManager: UndoManager): Commands {
const ctrlKey = Platform.IS_MAC ? 'Cmd' : 'Ctrl';
return [
[
{
name: 'undo',
icon: <UndoOutlined />,
tooltip: '撤销',
shortcut: `${ctrlKey} + Z`,
handler: (): void => undoManager.undo(),
},
{
name: 'redo',
icon: <RedoOutlined />,
tooltip: '重做',
shortcut: `${ctrlKey} + Shift + Z`,
handler: (): void => undoManager.redo(),
},
],
[
{
name: 'zoomIn',
icon: <ZoomInOutlined />,
tooltip: '放大',
handler: Tool.zoomIn(graph),
},
{
name: 'zoomOut',
icon: <ZoomOutOutlined />,
tooltip: '缩小',
handler: Tool.zoomOut(graph),
},
],
[
{
name: 'resetView',
icon: <RetweetOutlined />,
tooltip: '重置视口',
handler: (): void => {
graph.zoomTo(1);
graph.center();
},
},
{
name: 'fitWindow',
icon: <FullscreenOutlined />,
tooltip: '适应窗口',
handler: (): void => {
graph.fit(8);
},
},
],
[
{
name: 'delete',
icon: <DeleteOutlined />,
tooltip: '删除',
shortcut: 'Delete',
handler: (): void => graph.deleteCells(),
},
],
[
{
name: 'save',
icon: <SaveOutlined />,
tooltip: '保存',
shortcut: `${ctrlKey} + S`,
handler: (): void => {
// code
},
},
],
];
}

22
packages/core/src/index.module.less

@ -0,0 +1,22 @@
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
.body {
flex: 1;
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
.full {
flex: 1;
display: flex;
flex-direction: column;
}
}
}

130
packages/core/src/index.tsx

@ -1,109 +1,49 @@
import * as React from 'react';
import { Layout } from 'antd';
import { Graph, Shape, DomEvent } from '@antv/x6';
import { ReactShape } from '@antv/x6-react-shape';
import styled from '@emotion/styled';
import Sidebar from './components/sidebar';
import Toolbar from './components/toolbar';
import Setting from './components/setting';
import createGraph from './utils/create-graph';
import { DataItem } from './data/cells';
import 'antd/lib/layout/style';
import React, {useState} from 'react';
Shape.register('react', ReactShape, true);
import '@antv/x6/dist/x6.css';
import styles from './index.module.less';
const { Header, Sider, Content } = Layout;
import {Graph} from '@antv/x6';
import Header from './mods/header';
import SideBar from './mods/sideBar';
import ToolBar from './mods/toolBar';
import FlowChart from './mods/flowChart';
import SettingBar from './mods/settingBar';
const IMoveLayout = styled(Layout)`
height: 100%;
`;
const IMoveHeader = styled(Header)`
height: 50px;
background: #fff;
border-bottom: 1px solid #ddd;
`;
const IMoveTitle = styled.h2`
line-height: 50px;
`;
const IMoveSider = styled(Sider)`
background: #fff;
`;
const IMoveContent = styled(Content)`
position: relative;
padding-top: 35px;
`;
const ToolWrapper = styled.div`
position: absolute;
top: 0;
width: 100%;
height: 35px;
line-height: 35px;
background: #fff;
`;
interface CoreProps {
cells: DataItem[];
interface IProps {
onSave: (data: {nodes: any; edges: any}) => void;
}
interface CoreState {
inited: boolean;
}
class Core extends React.Component<CoreProps, CoreState> {
private graph: Graph | null = null;
private container = React.createRef<HTMLDivElement>();
const Core: React.FC<IProps> = props => {
constructor(props: CoreProps) {
super(props);
this.state = {
inited: false,
};
}
const {onSave} = props;
const [flowChart, setGraph] = useState<Graph>();
componentDidMount(): void {
const container = this.container.current;
if (container) {
DomEvent.disableContextMenu(container);
this.graph = createGraph(container);
this.graph.center();
this.setState({
inited: true,
const onFlowChartReady = (flowChart: Graph): void => {
setGraph(flowChart);
flowChart.on('node:added', (args) => {
flowChart.cleanSelection();
flowChart.select(args.cell);
});
}
}
flowChart.on('selection:changed', () => {
flowChart.trigger('toolBar:forceUpdate');
flowChart.trigger('settingBar:forceUpdate');
});
};
render(): JSX.Element {
const { inited } = this.state;
const { cells, onSave } = this.props;
return (
<IMoveLayout>
<IMoveHeader>
<IMoveTitle>iMove</IMoveTitle>
</IMoveHeader>
<IMoveLayout>
<IMoveSider>
{this.graph && inited && <Sidebar graph={this.graph} cells={cells} />}2
</IMoveSider>
<IMoveContent>
<ToolWrapper>
{this.graph && inited && <Toolbar graph={this.graph} onSave={onSave} />}
</ToolWrapper>
<div ref={this.container} tabIndex={-1} />
</IMoveContent>
<IMoveSider width="300">
{this.graph && inited && <Setting graph={this.graph} />}
</IMoveSider>
</IMoveLayout>
</IMoveLayout>
<div className={styles.container}>
<Header title={'iMove'}/>
<div className={styles.body}>
<SideBar flowChart={flowChart}/>
<div className={styles.full}>
<ToolBar flowChart={flowChart}/>
<FlowChart onReady={onFlowChartReady}/>
</div>
<SettingBar flowChart={flowChart}/>
</div>
</div>
);
}
}
};
export default Core;

4
packages/core/src/model/cells.ts

@ -1,4 +0,0 @@
export interface CellProps {
label?: string;
scale?: number;
}

9
packages/core/src/model/commands.ts

@ -1,9 +0,0 @@
interface Command {
name: string;
icon: JSX.Element;
tooltip: string;
shortcut?: string;
handler: () => void;
}
export type Commands = Command[][];

2
packages/core/src/model/index.ts

@ -1,2 +0,0 @@
export * from './cells';
export * from './commands';

19
packages/core/src/mods/flowChart/index.module.less

@ -0,0 +1,19 @@
.container {
position: relative;
display: flex;
flex: 1;
width: 100%;
height: 100%;
.flowChart {
width: 100%;
height: 100%;
}
.miniMap {
position: absolute;
right: 40px;
bottom: 40px;
}
}

30
packages/core/src/mods/flowChart/index.tsx

@ -0,0 +1,30 @@
import React, {useRef, useEffect} from 'react';
import styles from './index.module.less';
import {Graph} from '@antv/x6';
import createGraph from '../../utils/createGraph';
interface IProps {
onReady: (graph: Graph) => void;
}
const FlowChart: React.FC<IProps> = props => {
const {onReady} = props;
const graphRef = useRef<HTMLDivElement>(null);
const miniMapRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if(graphRef.current && miniMapRef.current) {
const graph = createGraph(graphRef.current, miniMapRef.current);
onReady(graph);
}
}, []);
return (
<div className={styles.container}>
<div className={styles.flowChart} ref={graphRef}/>
<div className={styles.miniMap} ref={miniMapRef}/>
</div>
);
};
export default FlowChart;

12
packages/core/src/mods/header/index.module.less

@ -0,0 +1,12 @@
.container {
height: 48px;
line-height: 48px;
background-color: #FEFEFE;
border-bottom: 1px solid #D9D9D9;
}
.titleText {
margin-left: 20px;
font-size: 24px;
font-weight: 500;
}

18
packages/core/src/mods/header/index.tsx

@ -0,0 +1,18 @@
import React from 'react';
import styles from './index.module.less';
interface IProps {
title?: string;
}
const Header: React.FC<IProps> = props => {
const {title = 'iMove'} = props;
return (
<div className={styles.container}>
<span className={styles.titleText}>{title}</span>
</div>
);
}
export default Header;

9
packages/core/src/mods/settingBar/components/checkbox/index.module.less

@ -0,0 +1,9 @@
.container {
margin-top: 20px;
.titleText {
font-size: 14px;
margin-bottom: 10px;
}
}

34
packages/core/src/mods/settingBar/components/checkbox/index.tsx

@ -0,0 +1,34 @@
import React from 'react';
import 'antd/es/checkbox/style';
import styles from './index.module.less';
import {Checkbox as AntdCheckbox} from 'antd';
import {CheckboxChangeEvent} from 'antd/lib/checkbox';
interface IProps {
value: any;
name: string;
title: string;
disabled?: boolean;
description?: string;
onValueChange: (val: boolean) => void;
}
const Checkbox: React.FC<IProps> = props => {
const {name, title, value, disabled, onValueChange} = props;
const onChange = (evt: CheckboxChangeEvent): void => {
console.log(evt.target.checked)
if(evt.target.checked !== value) {
onValueChange(evt.target.checked);
}
};
return (
<div className={styles.container}>
<p className={styles.titleText}>{title}</p>
<AntdCheckbox name={name} checked={value} disabled={disabled} onChange={onChange}/>
</div>
);
};
export default Checkbox;

27
packages/core/src/mods/settingBar/components/code/index.module.less

@ -0,0 +1,27 @@
.container {
margin-top: 20px;
.titleText {
font-size: 14px;
margin-bottom: 10px;
}
.btn {
margin-top: 5px;
}
}
.editModal {
:global {
.ant-modal-body {
padding: 0;
}
}
.editor {
width: 100%;
height: 400;
}
}

130
packages/core/src/mods/settingBar/components/code/index.tsx

@ -0,0 +1,130 @@
import React, {useState, useEffect} from 'react';
import 'antd/es/modal/style';
import 'antd/es/button/style';
import styles from './index.module.less';
import AceEditor from 'react-ace';
import {Button, Modal} from 'antd';
import 'ace-builds/src-noconflict/theme-dracula';
import 'ace-builds/src-noconflict/mode-javascript';
import 'ace-builds/src-noconflict/snippets/javascript';
import 'ace-builds/src-noconflict/ext-language_tools';
interface IProps {
value: any;
name: string;
title: string;
onValueChange: (value: string) => void;
}
const Code: React.FC<IProps> = props => {
const {title, value, onValueChange} = props;
const [visible, setVisible] = useState<boolean>(false);
// events
const onClickEdit = (): void => {
setVisible(true);
};
const onClickCancel = (): void => {
setVisible(false);
};
const onClickOk = (newJson: string): void => {
setVisible(false);
onValueChange(newJson);
};
return (
<div className={styles.container}>
<p className={styles.titleText}>{title}</p>
<Button
block={true}
className={styles.btn}
onClick={onClickEdit}
>
</Button>
<EditModal
title={title}
value={value}
visible={visible}
onOk={onClickOk}
onCancel={onClickCancel}
/>
</div>
);
};
interface IEditorModalProps {
visible: boolean;
title: string;
value: string;
onOk: (val: string) => void;
onCancel: () => void;
}
const CODE_EDITOR_STYLE = {
width: '100%',
height: 400
};
const CODE_EDITOR_OPTIONS = {
useWorker: false
};
const EditModal: React.FC<IEditorModalProps> = props => {
const {visible, title, value, onOk, onCancel} = props;
const [code, setCode] = useState<string>(value);
// life
useEffect(() => {
// set value when opening modal
// clear content when closing modal
if(visible) {
setCode(value);
} else {
setCode('');
}
}, [visible]);
// events
const onClickOk = (): void => {
onOk(code);
};
const onChangeCode = (newCode: string): void => {
if(newCode !== code) {
setCode(newCode);
}
};
return (
<Modal
className={styles.editModal}
width={800}
title={title}
okText={'保存'}
visible={visible}
cancelText={'取消'}
onOk={onClickOk}
onCancel={onCancel}
>
<AceEditor
style={CODE_EDITOR_STYLE}
tabSize={2}
value={code}
focus={true}
theme={'dracula'}
mode={'javascript'}
name={'code-editor'}
enableSnippets={true}
enableLiveAutocompletion={true}
enableBasicAutocompletion={true}
setOptions={CODE_EDITOR_OPTIONS}
onChange={onChangeCode}
/>
</Modal>
);
};
export default Code;

9
packages/core/src/mods/settingBar/components/input/index.module.less

@ -0,0 +1,9 @@
.container {
margin-top: 20px;
.titleText {
font-size: 14px;
margin-bottom: 10px;
}
}

32
packages/core/src/mods/settingBar/components/input/index.tsx

@ -0,0 +1,32 @@
import React from 'react';
import 'antd/es/input/style';
import styles from './index.module.less';
import {Input as AntdInput} from 'antd';
interface IProps {
value: any;
name: string;
title: string;
disabled?: boolean;
description?: string;
onValueChange: (val: string) => void;
}
const Input: React.FC<IProps> = props => {
const {name, title, value, disabled, onValueChange} = props;
const onChange = (evt: React.ChangeEvent<HTMLInputElement>): void => {
if(evt.target.value !== value) {
onValueChange(evt.target.value);
}
};
return (
<div className={styles.container}>
<p className={styles.titleText}>{title}</p>
<AntdInput name={name} value={value} disabled={disabled} onChange={onChange}/>
</div>
);
};
export default Input;

27
packages/core/src/mods/settingBar/components/json/index.module.less

@ -0,0 +1,27 @@
.container {
margin-top: 20px;
.titleText {
font-size: 14px;
margin-bottom: 10px;
}
.btn {
margin-top: 5px;
}
}
.editModal {
:global {
.ant-modal-body {
padding: 0;
}
}
.editor {
width: 100%;
height: 400;
}
}

145
packages/core/src/mods/settingBar/components/json/index.tsx

@ -0,0 +1,145 @@
import React, {useState, useEffect} from 'react';
import 'antd/es/modal/style';
import 'antd/es/button/style';
import 'antd/es/message/style';
import styles from './index.module.less';
import AceEditor from 'react-ace';
import JsonView from 'react-json-view';
import {Button, Modal, message} from 'antd';
import {safeParse} from '../../../../utils';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-dracula';
import 'ace-builds/src-noconflict/snippets/json';
import 'ace-builds/src-noconflict/ext-language_tools';
interface IJsonProps {
value: any;
name: string;
title: string;
onValueChange: (value: string) => void;
}
const Json: React.FC<IJsonProps> = props => {
const {title, value, onValueChange} = props;
const [visible, setVisible] = useState<boolean>(false);
// events
const onClickEdit = (): void => {
setVisible(true);
};
const onClickCancel = (): void => {
setVisible(false);
};
const onClickOk = (newJson: string): void => {
setVisible(false);
onValueChange(newJson);
};
return (
<div className={styles.container}>
<p className={styles.titleText}>{title}</p>
<JsonView
name={null}
collapsed={true}
enableClipboard={false}
displayDataTypes={false}
displayObjectSize={false}
src={safeParse(value)}
/>
<Button
block={true}
className={styles.btn}
onClick={onClickEdit}
>
</Button>
<EditModal
title={title}
value={value}
visible={visible}
onOk={onClickOk}
onCancel={onClickCancel}
/>
</div>
);
};
interface IEditorModalProps {
visible: boolean;
title: string;
value: string;
onOk: (val: string) => void;
onCancel: () => void;
}
const CODE_EDITOR_STYLE = {
width: '100%',
height: 400
};
const CODE_EDITOR_OPTIONS = {
useWorker: false
};
const EditModal: React.FC<IEditorModalProps> = props => {
const {visible, title, value, onOk, onCancel} = props;
const [json, setJson] = useState<string>('');
// life
useEffect(() => {
// set value when opening modal
// clear content when closing modal
if(visible) {
setJson(value);
} else {
setJson('');
}
}, [visible]);
// events
const onClickOk = (): void => {
try {
JSON.parse(json);
onOk(json);
} catch(err) {
message.error('内容必须是合法json');
console.log('save failed, the error is:', err.message);
}
};
const onChangeJson = (newJson: string): void => {
if(newJson !== json) {
setJson(newJson);
}
};
return (
<Modal
className={styles.editModal}
title={title}
okText={'保存'}
visible={visible}
cancelText={'取消'}
onOk={onClickOk}
onCancel={onCancel}
>
<AceEditor
style={CODE_EDITOR_STYLE}
tabSize={2}
value={json}
focus={true}
mode={'json'}
theme={'dracula'}
name={'json-editor'}
enableSnippets={true}
enableBasicAutocompletion={true}
setOptions={CODE_EDITOR_OPTIONS}
onChange={onChangeJson}
/>
</Modal>
);
};
export default Json;

35
packages/core/src/mods/settingBar/index.module.less

@ -0,0 +1,35 @@
.container {
display: flex;
flex-direction: column;
width: 350px;
height: 100%;
border-left: 1px solid #D9D9D9;
&.center {
justify-content: center;
align-items: center;
}
:global {
.ant-tabs-nav {
margin-bottom: 0;
}
.ant-tabs-nav-list {
display: flex;
flex: 1;
}
.ant-tabs-tab {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
padding: 10px 0;
background-color: #FAFAFA;
border-bottom: 1px solid #D9D9D9;
}
}
}

58
packages/core/src/mods/settingBar/index.tsx

@ -0,0 +1,58 @@
import React, {useEffect, useReducer} from 'react';
import 'antd/es/tabs/style';
import 'antd/es/empty/style';
import styles from './index.module.less';
import {Empty, Tabs} from 'antd';
import Basic from './mods/basic';
import Config from './mods/config';
import {Graph} from '@antv/x6';
const {TabPane} = Tabs;
interface IProps {
flowChart: Graph | undefined;
}
const SettingBar: React.FC<IProps> = props => {
const {flowChart} = props;
const [ignored, forceUpdate] = useReducer(n => n + 1, 0);
useEffect(() => {
flowChart && flowChart.on('settingBar:forceUpdate', forceUpdate);
return () => {
flowChart && flowChart.off('settingBar:forceUpdate');
};
}, [flowChart]);
if(!flowChart) {
return (
<div className={`${styles.container} ${styles.center}`}>
<Empty description={'请选择一个节点'} image={Empty.PRESENTED_IMAGE_SIMPLE}/>
</div>
);
} else {
const nodes = flowChart.getSelectedCells().filter(v => v.shape !== 'edge');
if(nodes.length === 1) {
return (
<div className={styles.container}>
<Tabs tabBarGutter={0} defaultActiveKey={'basic'} tabBarStyle={{display: 'flex', flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<TabPane tab={'基础信息'} key={'basic'}>
<Basic selectedCell={nodes[0]}/>
</TabPane>
<TabPane tab={'投放配置'} key={'config'}>
<Config selectedCell={nodes[0]}/>
</TabPane>
</Tabs>
</div>
);
} else {
return (
<div className={`${styles.container} ${styles.center}`}>
<Empty description={'请选择一个节点'} image={Empty.PRESENTED_IMAGE_SIMPLE}/>
</div>
);
}
}
};
export default SettingBar;

5
packages/core/src/mods/settingBar/mods/basic/index.module.less

@ -0,0 +1,5 @@
.container {
padding: 0 20px;
height: 100%;
overflow: scroll;
}

88
packages/core/src/mods/settingBar/mods/basic/index.tsx

@ -0,0 +1,88 @@
import React, {useState, useEffect} from 'react';
import styles from './index.module.less';
import {Cell} from '@antv/x6';
import Json from '../../components/json';
import Code from '../../components/code';
import Input from '../../components/input';
interface IProps {
selectedCell: Cell;
}
interface IBasicData {
label: string;
code: string;
trigger?: string;
dependencies: string;
configSchema: string;
}
const Basic: React.FC<IProps> = props => {
const {selectedCell} = props;
const [data, setData] = useState<IBasicData>();
const {label, code, trigger, dependencies, configSchema} = data || {};
// life
useEffect(() => {
setData(selectedCell.getData());
}, [selectedCell]);
// events
const commonChange = (key: string, val: string): void => {
selectedCell.setData({[key]: val});
setData(Object.assign({}, data, {[key]: val}));
};
const onChangeLabel = (val: string): void => {
commonChange('label', val);
selectedCell.setAttrs({label: {text: val}});
};
const onChangeConfigSchema = (val: string): void => {
commonChange('configSchema', val);
selectedCell.trigger('change:configSchema', {configSchema: val});
};
const onChangeCode = (val: string): void => commonChange('code', val);
const onChangeTrigger = (val: string): void => commonChange('trigger', val);
const onChangeDependencies = (val: string): void => commonChange('dependencies', val);
return (
<div className={styles.container}>
<Input
name={'label'}
title={'显示名称'}
value={label}
onValueChange={onChangeLabel}
/>
{selectedCell.shape === 'imove-start' && (
<Input
name={'trigger'}
title={'逻辑触发名称'}
value={trigger}
onValueChange={onChangeTrigger}
/>
)}
<Code
name={'code'}
title={'代码'}
value={code}
onValueChange={onChangeCode}
/>
<Json
name={'configSchema'}
title={'投放配置schema'}
value={configSchema}
onValueChange={onChangeConfigSchema}
/>
<Json
name={'dependencies'}
title={'依赖'}
value={dependencies}
onValueChange={onChangeDependencies}
/>
</div>
);
};
export default Basic;

23
packages/core/src/mods/settingBar/mods/config/index.module.less

@ -0,0 +1,23 @@
.container {
display: flex;
flex-direction: column;
padding: 0 20px;
height: 100%;
.empty {
margin-top: 100px;
}
.footContainer {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 20px;
.saveBtn {
margin-left: 20px;
}
}
}

120
packages/core/src/mods/settingBar/mods/config/index.tsx

@ -0,0 +1,120 @@
import React, {useState, useEffect} from 'react';
import styles from './index.module.less';
import {Cell} from '@antv/x6';
import {Button ,Empty} from 'antd';
import Input from '../../components/input';
import Checkbox from '../../components/checkbox';
import {safeParse} from '../../../../utils';
// FIXME
// save original config in tempConfig when change to edit mode
// and recover original config when click cancel button
let tempConfig: any = null;
enum Mode {
edit = 'edit',
readOnly = 'readOnly'
}
interface IConfig {
[key: string]: any;
}
interface IProps {
selectedCell: Cell;
}
const Config: React.FC<IProps> = props => {
const {selectedCell} = props;
const [mode, setMode] = useState<Mode>(Mode.readOnly);
const [configData, setConfigData] = useState<IConfig>({});
const [configSchema, setConfigSchema] = useState<IConfig>({});
// life
useEffect(() => {
const {configSchema, configData = {}} = selectedCell.getData() || {};
setConfigData(configData);
setConfigSchema(safeParse(configSchema));
selectedCell.on('change:configSchema', (data: {configSchema: string}) => {
setConfigSchema(safeParse(data.configSchema));
});
return () => {
selectedCell.off('change:configSchema');
};
}, [selectedCell]);
// events
const onFieldValueChange = (key: string, value: any) => {
setConfigData(Object.assign({}, configData, {[key]: value}));
};
const onClickEdit = (): void => {
setMode(Mode.edit);
tempConfig = configData;
};
const onClickCancel = (): void => {
setMode(Mode.readOnly);
setConfigData(tempConfig);
};
const onClickSave = (): void => {
setMode(Mode.readOnly);
selectedCell.setData({configData});
};
// no config schema
if(!configSchema || Object.keys(configSchema).length === 0) {
return (
<div className={styles.container}>
<Empty
className={styles.empty}
description={'请编辑投放配置schema'}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</div>
);
} else {
return (
<div className={styles.container}>
{Object.keys(configSchema).map((key, idx) => {
const {title, description, type} = configSchema[key];
const FieldComponent = Helper.getFieldComponent(type);
return (
<FieldComponent
key={idx}
name={key}
title={title}
value={configData[key]}
description={description}
disabled={mode === Mode.readOnly}
onValueChange={(value: any) => onFieldValueChange(key, value)}
/>
);
})}
{mode === Mode.readOnly ? (
<div className={styles.footContainer}>
<Button block onClick={onClickEdit}></Button>
</div>
) : (
<div className={styles.footContainer}>
<Button onClick={onClickCancel}></Button>
<Button className={styles.saveBtn} type={'primary'} onClick={onClickSave}></Button>
</div>
)}
</div>
);
}
};
const Helper = {
getFieldComponent(type: string) {
if(type === 'boolean') {
return Checkbox;
} else {
return Input;
}
}
}
export default Config;

20
packages/core/src/mods/sideBar/index.module.less

@ -0,0 +1,20 @@
.container {
width: 235px;
border-right: 1px solid #D9D9D9;
:global {
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
padding: 10px 16px 10px 40px;
}
}
.collapse {
border: 0;
border-radius: 0;
}
}
.chart {
width: 100%;
}

109
packages/core/src/mods/sideBar/index.tsx

@ -0,0 +1,109 @@
import React, {useState, useEffect, useRef} from 'react';
import 'antd/es/collapse/style';
import styles from './index.module.less';
import {Collapse} from 'antd';
import {Addon, Events, Graph, Node} from '@antv/x6';
import previewCellSchemaMap from '../../common/previewCell';
const {Dnd} = Addon;
const {Panel} = Collapse;
const CELL_SIZE = 80;
const CELL_SCALE = 0.7;
const GENERAL_GROUP = {
key: 'general',
name: '通用元件',
cells: ['imove-start-preview', 'imove-branch-preview', 'imove-behavior-preview']
};
const SHAPE_REFLECT_MAP: {[key: string]: string} = {
'imove-start-preview': 'imove-start',
'imove-branch-preview': 'imove-branch',
'imove-behavior-preview': 'imove-behavior',
};
interface IGroupItem {
key: string;
name: string;
cells: string[];
}
interface ISideBarPorps {
flowChart: Graph | undefined;
}
const SideBar: React.FC<ISideBarPorps> = props => {
const {flowChart} = props;
const [dnd, setDnd] = useState<Addon.Dnd>();
const [groups, setGroups] = useState<IGroupItem[]>([]);
// life
useEffect(() => {
// TODO: fetch to get custom group data
setGroups([GENERAL_GROUP]);
}, []);
useEffect(() => {
if(flowChart) {
setDnd(new Dnd({target: flowChart, scaled: true}));
}
}, [flowChart]);
return (
<div className={styles.container}>
{flowChart && dnd && (
<Collapse className={styles.collapse} defaultActiveKey={['general', 'custom']}>
{groups.map(group => (
<Panel key={group.key} header={group.name}>
<PanelContent dnd={dnd} cells={group.cells} flowChart={flowChart}/>
</Panel>
))}
</Collapse>
)}
</div>
);
};
interface IPanelContentProps {
dnd: Addon.Dnd;
cells: string[];
flowChart: Graph;
}
const PanelContent: React.FC<IPanelContentProps> = props => {
const {dnd, cells, flowChart} = props;
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if(ref.current && flowChart) {
const graph = new Graph({
container: ref.current,
width: ref.current.offsetWidth,
height: Math.ceil(cells.length / 3) * CELL_SIZE * CELL_SCALE,
interacting: false,
preventDefaultBlankAction: false,
});
cells.forEach((cell: any, index: number) => {
const rowIdx = Math.floor(index / 3);
const colIdx = index % 3;
const {top} = previewCellSchemaMap[cell] || {};
graph.addNode({
shape: cell,
x: colIdx * (CELL_SIZE + 16 / CELL_SCALE),
y: rowIdx * (CELL_SIZE + 16 / CELL_SCALE) + top
});
});
graph.scale(CELL_SCALE, CELL_SCALE);
graph.on('cell:mousedown', (args: Events.EventArgs['cell:mousedown']) => {
const {node, e} = args;
let newNode = node;
if(SHAPE_REFLECT_MAP[node.shape]) {
newNode = Node.create({shape: SHAPE_REFLECT_MAP[node.shape]});
}
dnd.start(newNode, e);
});
}
}, [flowChart]);
return <div className={styles.chart} ref={ref}/>;
};
export default SideBar;

18
packages/core/src/mods/toolBar/index.module.less

@ -0,0 +1,18 @@
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 43px;
background-color: #FAFAFA;
border-bottom: 1px solid #D9D9D9;
.group {
display: flex;
flex-direction: row;
align-items: center;
height: 43px;
border-right: 1px solid #D9D9D9;
}
}

35
packages/core/src/mods/toolBar/index.tsx

@ -0,0 +1,35 @@
import React, {useEffect, useReducer} from 'react';
import 'antd/es/tooltip/style';
import styles from './index.module.less';
import {Graph} from '@antv/x6';
import widgets from './widgets';
interface IProps {
flowChart: Graph | undefined;
}
const ToolBar: React.FC<IProps> = props => {
const {flowChart} = props;
const [ignored, forceUpdate] = useReducer(n => n + 1, 0);
useEffect(() => {
flowChart && flowChart.on('toolBar:forceUpdate', forceUpdate);
return () => {
flowChart && flowChart.off('toolBar:forceUpdate');
};
}, [flowChart]);
return (
<div className={styles.container}>
{flowChart && widgets.map((group, index) => (
<div key={index} className={styles.group}>
{group.map((ToolItem, index) => {
return <ToolItem key={index} flowChart={flowChart}/>;
})}
</div>
))}
</div>
);
};
export default ToolBar;

54
packages/core/src/mods/toolBar/widgets/bgColor.tsx

@ -0,0 +1,54 @@
import React from 'react';
import 'antd/es/menu/style';
import styles from './index.module.less';
import {Graph} from '@antv/x6';
import {safeGet} from '../../../utils';
import XIcon from '../../../components/xIcon';
import makeDropdownWidget from './makeDropdownWidget';
import ColorPicker from '../../../components/colorPicker';
interface IProps {
flowChart: Graph;
}
const BgColor: React.FC<IProps> = makeDropdownWidget({
tooltip: '填充颜色',
getIcon(flowChart: Graph) {
let bgColor = '#DDD';
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
bgColor = safeGet(cells, '0.attrs.body.fill', '#575757');
}
return (
<div className={styles.bgColorContainer}>
<XIcon className={styles.fillIcon} type={'icon-tianchong'}/>
<div className={styles.colorPreview} style={{backgroundColor: bgColor}}/>
</div>
);
},
getOverlay(flowChart: Graph, onChange: (data: any) => void) {
let bgColor = '#DDD';
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
bgColor = safeGet(cells, '0.attrs.body.fill', '#575757');
}
const onChangeComplete = (color: string) => onChange(color);
return (
<ColorPicker color={bgColor} onChangeComplete={onChangeComplete}/>
);
},
handler: (flowChart: Graph, value: any) => {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
cells.forEach(cell => cell.setAttrs({body: {fill: value}}));
flowChart.trigger('toolBar:forceUpdate');
}
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
},
});
export default BgColor;

32
packages/core/src/mods/toolBar/widgets/bold.tsx

@ -0,0 +1,32 @@
import React from 'react';
import {Graph} from '@antv/x6';
import {safeGet} from '../../../utils';
import makeBtnWidget from './makeBtnWidget';
import {BoldOutlined} from '@ant-design/icons';
import shortcuts from '../../../common/shortcuts';
interface IProps {
flowChart: Graph
}
const Bold: React.FC<IProps> = makeBtnWidget({
tooltip: '加粗',
handler: shortcuts['bold'].handler,
getIcon() {
return <BoldOutlined/>;
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
},
selected(flowChart: Graph) {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
return safeGet(cells, '0.attrs.label.fontWeight', 'normal') === 'bold';
} else {
return false;
}
}
});
export default Bold;

27
packages/core/src/mods/toolBar/widgets/bringToBack.tsx

@ -0,0 +1,27 @@
import React from 'react';
import {Graph} from '@antv/x6';
import makeBtnWidget from './makeBtnWidget';
import XIcon from '../../../components/xIcon';
interface IProps {
flowChart: Graph
}
const BringToBack: React.FC<IProps> = makeBtnWidget({
tooltip: '置于底层',
getIcon() {
return <XIcon type={'icon-bringtobottom'}/>;
},
handler(flowChart: Graph) {
const cells = flowChart.getSelectedCells();
if(cells.length === 1) {
cells[0].toBack();
}
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
}
});
export default BringToBack;

29
packages/core/src/mods/toolBar/widgets/bringToTop.tsx

@ -0,0 +1,29 @@
//at.alicdn.com/t/font_2024452_s39le1337k9.js
import React from 'react';
import {Graph} from '@antv/x6';
import makeBtnWidget from './makeBtnWidget';
import XIcon from '../../../components/xIcon';
import shortcuts from '../../../common/shortcuts';
interface IProps {
flowChart: Graph
}
const BringToTop: React.FC<IProps> = makeBtnWidget({
tooltip: '置于顶层',
getIcon() {
return <XIcon type={'icon-bringtotop'}/>;
},
handler(flowChart: Graph) {
const cells = flowChart.getSelectedCells();
if(cells.length === 1) {
cells[0].toFront();
}
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
}
});
export default BringToTop;

21
packages/core/src/mods/toolBar/widgets/fitWindow.tsx

@ -0,0 +1,21 @@
import React from 'react';
import {Graph} from '@antv/x6';
import makeBtnWidget from './makeBtnWidget';
import {FullscreenExitOutlined} from '@ant-design/icons';
interface IProps {
flowChart: Graph
}
const FitWindow: React.FC<IProps> = makeBtnWidget({
tooltip: '适配窗口',
getIcon() {
return <FullscreenExitOutlined/>;
},
handler(flowChart: Graph) {
flowChart.zoomToFit({minScale: 0.5, maxScale: 1});
}
});
export default FitWindow;

52
packages/core/src/mods/toolBar/widgets/fontSize.tsx

@ -0,0 +1,52 @@
import React from 'react';
import 'antd/es/menu/style';
import {Menu} from 'antd';
import {Graph} from '@antv/x6';
import {safeGet} from '../../../utils';
import makeDropdownWidget from './makeDropdownWidget';
interface IProps {
flowChart: Graph
}
const MenuItem = Menu.Item;
const FONT_SIZE_SET = [
9, 10, 11, 12, 13, 14, 15, 16, 19, 22, 24, 29, 32
];
const FontSize: React.FC<IProps> = makeDropdownWidget({
tooltip: '字号',
getIcon(flowChart: Graph) {
let fontSize = 14;
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
fontSize = safeGet(cells, '0.attrs.label.fontSize', 14);
}
return <span style={{fontSize: 14}}>{fontSize}px</span>;
},
getOverlay(flowChart: Graph, onChange: (data: any) => void) {
return (
<Menu onClick={(args) => onChange(args.key)}>
{FONT_SIZE_SET.map(fontSize => (
<MenuItem key={fontSize}>
<span>{fontSize} px</span>
</MenuItem>
))}
</Menu>
);
},
handler: (flowChart: Graph, value: any) => {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
cells.forEach(cell => cell.setAttrs({label: {fontSize: value}}));
flowChart.trigger('toolBar:forceUpdate');
}
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
},
});
export default FontSize;

102
packages/core/src/mods/toolBar/widgets/horizontalAlign.tsx

@ -0,0 +1,102 @@
import React, { ReactElement } from 'react';
import 'antd/es/menu/style';
import {Menu} from 'antd';
import {Graph} from '@antv/x6';
import {safeGet} from '../../../utils';
import makeDropdownWidget from './makeDropdownWidget';
import {AlignLeftOutlined, AlignCenterOutlined, AlignRightOutlined} from '@ant-design/icons';
interface IProps {
flowChart: Graph;
}
interface AlignItem {
text: string;
icon: ReactElement;
attrs: {
refX: number;
refX2?: number;
textAnchor: string;
align: {
horizontal: string;
}
}
}
const MenuItem = Menu.Item;
const ALIGN_MAP: {[key: string]: AlignItem} = {
left: {
text: '左对齐',
icon: <AlignLeftOutlined/>,
attrs: {
refX: 0,
refX2: 5,
textAnchor: 'start',
align: {
horizontal: 'left'
}
}
},
center: {
text: '居中对齐',
icon: <AlignCenterOutlined/>,
attrs: {
refX: 0.5,
refX2: 0,
textAnchor: 'middle',
align: {
horizontal: 'center'
}
}
},
right: {
text: '右对齐',
icon: <AlignRightOutlined/>,
attrs: {
refX: 0.99,
refX2: -5,
textAnchor: 'end',
align: {
horizontal: 'right'
}
}
}
};
const HorizontalAlign: React.FC<IProps> = makeDropdownWidget({
tooltip: '水平对齐',
getIcon(flowChart: Graph) {
let alignType = 'center';
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
alignType = safeGet(cells, '0.attrs.label.align.horizontal', 'center');
}
return ALIGN_MAP[alignType].icon;
},
getOverlay(flowChart: Graph, onChange: (data: any) => void) {
return (
<Menu onClick={({key}) => onChange(ALIGN_MAP[key].attrs)}>
{Object.keys(ALIGN_MAP).map((alignType) => (
<MenuItem key={alignType}>
{ALIGN_MAP[alignType].icon}
<span>{ALIGN_MAP[alignType].text}</span>
</MenuItem>
))}
</Menu>
);
},
handler: (flowChart: Graph, value: any) => {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
cells.forEach(cell => cell.setAttrs({label: value}));
flowChart.trigger('toolBar:forceUpdate');
}
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
},
});
export default HorizontalAlign;

91
packages/core/src/mods/toolBar/widgets/index.module.less

@ -0,0 +1,91 @@
.btnWidget {
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
margin-left: 4px;
padding: 0 8px;
min-width: 34px;
height: 34px;
font-size: 16px;
border-radius: 2px;
border: 0.5px solid transparent;
.caret {
margin-left: 4px;
font-size: 10px;
}
&:last-child {
margin-right: 4px;
}
&:hover {
cursor: pointer;
border: 0.5px solid #CCC;
}
&.selected {
border: 0.5px solid #CCC;
box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
background-color: #EEE;
background-image: linear-gradient(top,#eee,#e0e0e0);
}
&.disabled {
color: #CCC;
}
&.disabled:hover {
cursor: default;
border: 0.5px solid transparent;
}
}
.zoomContainer {
display: flex;
flex-direction: row;
align-items: center;
.zoomText {
margin-left: 4px;
width: 45px;
text-align: center;
}
}
.bgColorContainer, .textColorContainer, .lineColorContainer {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
.fillIcon {
position: relative;
top: -2px;
font-size: 15px;
}
.textIcon {
position: relative;
top: -2px;
font-size: 15px;
}
.lineIcon {
position: relative;
top: -2px;
font-size: 15px;
}
.colorPreview {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1.6px;
}
}

35
packages/core/src/mods/toolBar/widgets/index.tsx

@ -0,0 +1,35 @@
import React from 'react';
import {Graph} from '@antv/x6';
import Save from './save';
import Undo from './undo';
import Redo from './redo';
import Zoom from './zoom';
import Bold from './bold';
import Italic from './italic';
import BgColor from './bgColor';
import FontSize from './fontSize';
import TextColor from './textColor';
import LineColor from './lineColor';
import Underline from './underline';
import FitWindow from './fitWindow';
import BringToTop from './bringToTop';
import BringToBack from './bringToBack';
import VerticalAlign from './verticalAlign';
import HorizontalAlign from './horizontalAlign';
interface IProps {
flowChart: Graph;
}
const tools: React.FC<IProps>[][] = [
[Save, FitWindow, Undo, Redo],
[Zoom],
[FontSize, Bold, Italic, Underline, TextColor],
[HorizontalAlign, VerticalAlign],
[BgColor, LineColor],
[BringToTop, BringToBack]
];
export default tools;

32
packages/core/src/mods/toolBar/widgets/italic.tsx

@ -0,0 +1,32 @@
import React from 'react';
import {Graph} from '@antv/x6';
import {safeGet} from '../../../utils';
import makeBtnWidget from './makeBtnWidget';
import {ItalicOutlined} from '@ant-design/icons';
import shortcuts from '../../../common/shortcuts';
interface IProps {
flowChart: Graph
}
const Italic: React.FC<IProps> = makeBtnWidget({
tooltip: '斜体',
handler: shortcuts['italic'].handler,
getIcon() {
return <ItalicOutlined/>;
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
},
selected(flowChart: Graph) {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
return safeGet(cells, '0.attrs.label.fontStyle', 'normal') === 'italic';
} else {
return false;
}
}
});
export default Italic;

54
packages/core/src/mods/toolBar/widgets/lineColor.tsx

@ -0,0 +1,54 @@
import React from 'react';
import 'antd/es/menu/style';
import styles from './index.module.less';
import {Graph} from '@antv/x6';
import {safeGet} from '../../../utils';
import {HighlightOutlined} from '@ant-design/icons';
import makeDropdownWidget from './makeDropdownWidget';
import ColorPicker from '../../../components/colorPicker';
interface IProps {
flowChart: Graph;
}
const LineColor: React.FC<IProps> = makeDropdownWidget({
tooltip: '线条颜色',
getIcon(flowChart: Graph) {
let lineColor = '#DDD';
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
lineColor = safeGet(cells, '0.attrs.body.stroke', '#333');
}
return (
<div className={styles.lineColorContainer}>
<HighlightOutlined className={styles.lineIcon}/>
<div className={styles.colorPreview} style={{backgroundColor: lineColor}}/>
</div>
);
},
getOverlay(flowChart: Graph, onChange: (data: any) => void) {
let lineColor = '#DDD';
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
lineColor = safeGet(cells, '0.attrs.body.stroke', '#333');
}
const onChangeComplete = (color: string) => onChange(color);
return (
<ColorPicker color={lineColor} onChangeComplete={onChangeComplete}/>
);
},
handler: (flowChart: Graph, value: any) => {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
cells.forEach(cell => cell.setAttrs({body: {stroke: value}}));
flowChart.trigger('toolBar:forceUpdate');
}
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
},
});
export default LineColor;

51
packages/core/src/mods/toolBar/widgets/makeBtnWidget.tsx

@ -0,0 +1,51 @@
import React, {ReactElement} from 'react';
import 'antd/es/tooltip/style';
import styles from './index.module.less';
import {Tooltip} from 'antd';
import {Graph} from '@antv/x6';
interface IOptions {
tooltip: string;
getIcon: (flowChart: Graph) => ReactElement;
handler: (flowChart: Graph) => void;
disabled?: (flowChart: Graph) => boolean;
selected?: (flowChart: Graph) => boolean;
}
interface IBtnWidgetProps {
flowChart: Graph;
}
const makeBtnWidget = (options: IOptions) => {
const Widget: React.FC<IBtnWidgetProps> = (props) => {
const {flowChart} = props;
const {tooltip, getIcon, handler} = options;
const iconWrapperCls = [styles.btnWidget];
let {disabled = false, selected = false} = options;
if(typeof disabled === 'function') {
disabled = disabled(flowChart);
disabled && iconWrapperCls.push(styles.disabled);
}
if(typeof selected === 'function') {
selected = selected(flowChart);
selected && iconWrapperCls.push(styles.selected);
}
const onClick = (): void => {
if(disabled) return;
handler(flowChart);
flowChart.trigger('toolBar:forceUpdate');
}
return (
<Tooltip title={tooltip}>
<div className={iconWrapperCls.join(' ')} onClick={onClick}>
{getIcon(flowChart)}
</div>
</Tooltip>
);
};
return Widget;
};
export default makeBtnWidget;

51
packages/core/src/mods/toolBar/widgets/makeDropdownWidget.tsx

@ -0,0 +1,51 @@
import React, {ReactElement} from 'react';
import 'antd/es/tooltip/style';
import 'antd/es/dropdown/style';
import styles from './index.module.less';
import {Graph} from '@antv/x6';
import {Tooltip, Dropdown} from 'antd';
import {CaretDownOutlined} from '@ant-design/icons';
interface IOptions {
tooltip: string;
getIcon: (flowChart: Graph) => ReactElement;
getOverlay: (flowChart: Graph, onChange: (data: any) => void) => ReactElement;
handler: (flowChart: Graph, data: any) => void;
disabled?: (flowChart: Graph) => boolean;
}
interface IDropdownWidgetProps {
flowChart: Graph;
}
const makeDropdownWidget = (options: IOptions) => {
const Widget: React.FC<IDropdownWidgetProps> = (props) => {
const {flowChart} = props;
const {tooltip, getIcon, getOverlay, handler} = options;
const iconWrapperCls = [styles.btnWidget];
let {disabled = false} = options;
if(typeof disabled === 'function') {
disabled = disabled(flowChart);
disabled && iconWrapperCls.push(styles.disabled);
}
const onChange = (data: any): void => {
if(disabled) return;
handler(flowChart, data);
flowChart.trigger('toolBar:forceUpdate');
};
return (
<Tooltip title={tooltip}>
<Dropdown disabled={disabled} overlay={getOverlay(flowChart, onChange)} trigger={['click']}>
<div className={iconWrapperCls.join(' ')}>
{getIcon(flowChart)} <CaretDownOutlined className={styles.caret}/>
</div>
</Dropdown>
</Tooltip>
);
};
return Widget;
};
export default makeDropdownWidget;

23
packages/core/src/mods/toolBar/widgets/redo.tsx

@ -0,0 +1,23 @@
import React from 'react';
import {Graph} from '@antv/x6';
import makeBtnWidget from './makeBtnWidget';
import {RedoOutlined} from '@ant-design/icons';
import shortcuts from '../../../common/shortcuts';
interface IProps {
flowChart: Graph
}
const Save: React.FC<IProps> = makeBtnWidget({
tooltip: '重做',
handler: shortcuts['redo'].handler,
getIcon() {
return <RedoOutlined/>;
},
disabled(flowChart: Graph) {
return !flowChart.canRedo();
}
});
export default Save;

20
packages/core/src/mods/toolBar/widgets/save.tsx

@ -0,0 +1,20 @@
import React from 'react';
import {Graph} from '@antv/x6';
import makeBtnWidget from './makeBtnWidget';
import {SaveOutlined} from '@ant-design/icons';
import shortcuts from '../../../common/shortcuts';
interface IProps {
flowChart: Graph
}
const Save: React.FC<IProps> = makeBtnWidget({
tooltip: '保存',
handler: shortcuts['save'].handler,
getIcon() {
return <SaveOutlined/>;
}
});
export default Save;

54
packages/core/src/mods/toolBar/widgets/textColor.tsx

@ -0,0 +1,54 @@
import React from 'react';
import 'antd/es/menu/style';
import styles from './index.module.less';
import {Graph} from '@antv/x6';
import {safeGet} from '../../../utils';
import XIcon from '../../../components/xIcon';
import makeDropdownWidget from './makeDropdownWidget';
import ColorPicker from '../../../components/colorPicker';
interface IProps {
flowChart: Graph;
}
const TextColor: React.FC<IProps> = makeDropdownWidget({
tooltip: '文本颜色',
getIcon(flowChart: Graph) {
let textColor = '#DDD';
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
textColor = safeGet(cells, '0.attrs.label.fill', '#575757');
}
return (
<div className={styles.textColorContainer}>
<XIcon className={styles.textIcon} type={'icon-A'}/>
<div className={styles.colorPreview} style={{backgroundColor: textColor}}/>
</div>
);
},
getOverlay(flowChart: Graph, onChange: (data: any) => void) {
let textColor = '#DDD';
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
textColor = safeGet(cells, '0.attrs.label.fill', '#575757');
}
const onChangeComplete = (color: string) => onChange(color);
return (
<ColorPicker color={textColor} onChangeComplete={onChangeComplete}/>
);
},
handler: (flowChart: Graph, value: any) => {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
cells.forEach(cell => cell.setAttrs({label: {fill: value}}));
flowChart.trigger('toolBar:forceUpdate');
}
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
},
});
export default TextColor;

32
packages/core/src/mods/toolBar/widgets/underline.tsx

@ -0,0 +1,32 @@
import React from 'react';
import {Graph} from '@antv/x6';
import {safeGet} from '../../../utils';
import makeBtnWidget from './makeBtnWidget';
import {UnderlineOutlined} from '@ant-design/icons';
import shortcuts from '../../../common/shortcuts';
interface IProps {
flowChart: Graph
}
const Underline: React.FC<IProps> = makeBtnWidget({
tooltip: '下划线',
handler: shortcuts['underline'].handler,
getIcon() {
return <UnderlineOutlined/>;
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
},
selected(flowChart: Graph) {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
return safeGet(cells, '0.attrs.label.textDecoration', 'none') === 'underline';
} else {
return false;
}
}
});
export default Underline;

23
packages/core/src/mods/toolBar/widgets/undo.tsx

@ -0,0 +1,23 @@
import React from 'react';
import {Graph} from '@antv/x6';
import makeBtnWidget from './makeBtnWidget';
import {UndoOutlined} from '@ant-design/icons';
import shortcuts from '../../../common/shortcuts';
interface IProps {
flowChart: Graph
}
const Save: React.FC<IProps> = makeBtnWidget({
tooltip: '撤销',
handler: shortcuts['undo'].handler,
getIcon() {
return <UndoOutlined/>;
},
disabled(flowChart: Graph) {
return !flowChart.canUndo();
}
});
export default Save;

102
packages/core/src/mods/toolBar/widgets/verticalAlign.tsx

@ -0,0 +1,102 @@
import React, {ReactElement} from 'react';
import 'antd/es/menu/style';
import {Menu} from 'antd';
import {Graph} from '@antv/x6';
import {safeGet} from '../../../utils';
import makeDropdownWidget from './makeDropdownWidget';
import {VerticalAlignTopOutlined, VerticalAlignMiddleOutlined, VerticalAlignBottomOutlined} from '@ant-design/icons';
interface IProps {
flowChart: Graph;
}
interface AlignItem {
text: string;
icon: ReactElement;
attrs: {
refY: number;
refY2?: number;
textVerticalAlign: string;
align: {
vertical: string;
}
}
}
const MenuItem = Menu.Item;
const ALIGN_MAP: {[key: string]: AlignItem} = {
top: {
text: '上对齐',
icon: <VerticalAlignTopOutlined/>,
attrs: {
refY: 0,
refY2: 10,
textVerticalAlign: 'start',
align: {
vertical: 'top'
}
}
},
center: {
text: '居中对齐',
icon: <VerticalAlignMiddleOutlined/>,
attrs: {
refY: 0.5,
refY2: 0,
textVerticalAlign: 'middle',
align: {
vertical: 'center'
}
}
},
bottom: {
text: '下对齐',
icon: <VerticalAlignBottomOutlined/>,
attrs: {
refY: 0.99,
refY2: -10,
textVerticalAlign: 'end',
align: {
vertical: 'bottom'
}
}
}
};
const VerticalAlign: React.FC<IProps> = makeDropdownWidget({
tooltip: '垂直对齐',
getIcon(flowChart: Graph) {
let alignType = 'center';
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
alignType = safeGet(cells, '0.attrs.label.align.vertical', 'center');
}
return ALIGN_MAP[alignType].icon;
},
getOverlay(flowChart: Graph, onChange: (data: any) => void) {
return (
<Menu onClick={({key}) => onChange(ALIGN_MAP[key].attrs)}>
{Object.keys(ALIGN_MAP).map((alignType: string) => (
<MenuItem key={alignType}>
{ALIGN_MAP[alignType].icon}
<span>{ALIGN_MAP[alignType].text}</span>
</MenuItem>
))}
</Menu>
);
},
handler: (flowChart: Graph, value: any) => {
const cells = flowChart.getSelectedCells();
if(cells.length > 0) {
cells.forEach(cell => cell.setAttrs({label: value}));
flowChart.trigger('toolBar:forceUpdate');
}
},
disabled(flowChart: Graph) {
return flowChart.getSelectedCellCount() === 0;
},
});
export default VerticalAlign;

64
packages/core/src/mods/toolBar/widgets/zoom.tsx

@ -0,0 +1,64 @@
import React, {useState, useEffect} from 'react';
import styles from './index.module.less';
import {Graph} from '@antv/x6';
import makeBtnWidget from './makeBtnWidget';
import shortcuts from '../../../common/shortcuts';
import {ZoomOutOutlined, ZoomInOutlined} from '@ant-design/icons';
interface IProps {
flowChart: Graph
}
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 1.5;
const ZoomOut: React.FC<IProps> = makeBtnWidget({
tooltip: '缩小',
handler: shortcuts['zoomOut'].handler,
getIcon() {
return <ZoomOutOutlined/>;
},
disabled(flowChart: Graph) {
return flowChart.zoom() <= MIN_ZOOM;
}
});
const ZoomIn: React.FC<IProps> = makeBtnWidget({
tooltip: '放大',
handler: shortcuts['zoomIn'].handler,
getIcon() {
return <ZoomInOutlined/>;
},
disabled(flowChart: Graph) {
return flowChart.zoom() >= MAX_ZOOM;
}
});
const Zoom: React.FC<IProps> = props => {
const {flowChart} = props;
const [scale, setScale] = useState<number>(flowChart.zoom());
useEffect(() => {
flowChart.on('scale', () => {
setScale(flowChart.zoom());
});
}, [flowChart]);
return (
<div className={styles.zoomContainer}>
<ZoomOut {...props}/>
<span className={styles.zoomText}>
{Helper.scaleFormatter(scale)}
</span>
<ZoomIn {...props}/>
</div>
);
};
const Helper = {
scaleFormatter(scale: number): string {
return (scale * 100).toFixed(0) + '%';
}
};
export default Zoom;

9
packages/core/src/typings.d.ts

@ -0,0 +1,9 @@
declare module '*.module.css' {
const classes: {[key: string]: string};
export default classes;
}
declare module '*.less' {
const classes: {[key: string]: string};
export default classes;
}

88
packages/core/src/utils/create-graph.tsx

@ -1,88 +0,0 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Graph } from '@antv/x6';
export default function createGraph(container: HTMLDivElement): Graph {
return new Graph(container, {
rotate: false,
resize: false,
infinite: true,
rubberband: true,
backgroundColor: '#f8f9fa',
grid: {
type: 'dot',
color: '#bcbcbc',
},
guide: {
enabled: true,
dashed: true,
stroke: '#ff5500',
},
keyboard: {
enabled: true,
global: false,
escape: true,
},
selectionPreview: {
dashed: false,
strokeWidth: 2,
},
nodeStyle: {
fill: 'rgba(0, 0, 0, 0)',
stroke: 'none',
label: false,
editable: false,
},
edgeStyle: {
edge: 'elbow',
elbow: 'vertical',
labelBackgroundColor: '#f8f9fa',
rounded: true,
movable: false,
},
connection: {
enabled: true,
hotspotable: false,
livePreview: true,
},
connectionPreview: {
stroke: '#1890ff',
},
anchor: {
inductiveSize: 10,
},
anchorHighlight: {
strokeWidth: 16,
},
shouldRedrawOnDataChange(): boolean {
return true;
},
getAnchors(cell): [number, number][] | null {
if (cell === null || cell.data === null) return null;
if (cell.data.type === 'start') {
return [[0.5, 1]];
}
if (cell.data.type === 'end') {
return [[0.5, 0]];
}
return [
[0.5, 0],
[0, 0.5],
[1, 0.5],
[0.5, 1],
];
},
getLabel: (cell): HTMLElement | string => {
if (cell.isEdge()) {
const div = document.createElement('div');
ReactDom.render(<div>{cell.data.label}</div>, div);
return div;
}
return '';
},
});
}

88
packages/core/src/utils/createGraph.ts

@ -0,0 +1,88 @@
import {Graph} from '@antv/x6';
import shortcuts from '../common/shortcuts';
import baseCellSchemaMap from '../common/baseCell';
import previewCellSchemaMap from '../common/previewCell';
// X6 register base/preview cell shape
[baseCellSchemaMap, previewCellSchemaMap]
.forEach(schemas => Object.values(schemas).forEach(schema => {
const {base, ...rest} = schema;
base.define(rest);
}));
const createGraph = (container: HTMLDivElement, miniMapContainer: HTMLDivElement): Graph => {
// create graph instance
const graph = new Graph({
container,
rotating: false,
resizing: true,
clipboard: true,
// https://x6.antv.vision/zh/docs/tutorial/intermediate/connector
connecting: {
snap: true,
dangling: true,
highlight: true,
router: {
name: 'manhattan'
},
validateConnection({sourceView, targetView}) {
return sourceView !== targetView;
}
},
// https://x6.antv.vision/zh/docs/tutorial/basic/background
background: {
color: '#f8f9fa'
},
// https://x6.antv.vision/zh/docs/tutorial/basic/grid
grid: {
visible: true
},
// https://x6.antv.vision/zh/docs/tutorial/basic/selection
selecting: {
enabled: true,
multiple: true,
rubberband: true,
movable: true,
showNodeSelectionBox: true
},
// https://x6.antv.vision/zh/docs/tutorial/basic/snapline
snapline: {
enabled: true,
clean: 100
},
// https://x6.antv.vision/zh/docs/tutorial/basic/keyboard
keyboard: {
enabled: true,
global: false
},
// https://x6.antv.vision/zh/docs/tutorial/basic/history
history: {
enabled: true
},
// https://x6.antv.vision/zh/docs/tutorial/basic/minimap
minimap: {
enabled: true,
padding: 0,
width: 200,
height: 160,
minScale: 0.5,
maxScale: 1.5,
container: miniMapContainer,
},
// https://x6.antv.vision/zh/docs/tutorial/basic/scroller
scroller: {
enabled: true
}
});
// register shortcuts
Object.values(shortcuts).forEach(shortcut => {
const {keys, handler} = shortcut;
graph.bindKey(keys, () => handler(graph));
});
return graph;
}
export default createGraph;

26
packages/core/src/utils/index.ts

@ -0,0 +1,26 @@
export const safeParse = (json: string): Object => {
try {
return JSON.parse(json);
} catch(err) {
return {};
}
};
export const safeGet = (obj: any, keyChain: string, defaultVal: any): any => {
if(typeof obj !== 'object' || obj === null) {
return defaultVal;
}
let retVal = obj;
const keys = keyChain.split('.');
for(const key of keys) {
if(retVal[key] === undefined) {
return defaultVal;
} else {
retVal = retVal[key];
}
}
return retVal;
};

69
packages/core/src/utils/patch-dnd.tsx

@ -1,69 +0,0 @@
import * as React from 'react';
import { Graph, Dnd } from '@antv/x6';
import CellMap from '../components/cells';
import { DataItem } from '../data/cells';
export default function patchDnd(
container: HTMLDivElement | null,
graph: Graph,
cellData: DataItem[]
): void {
if (!container) return;
(container.childNodes as NodeListOf<HTMLElement>).forEach((node, index) => {
const itemData = cellData[index];
const dnd = new Dnd<DataItem>(node, {
data: itemData,
getGraph: (): Graph => graph,
createDragElement: ({ data }): HTMLDivElement => {
const elem = document.createElement('div');
if (typeof data === 'undefined') return elem;
const { width, height, scale } = data.style;
elem.style.width = `${width * scale}px`;
elem.style.height = `${height * scale}px`;
elem.style.border = '1px dashed #000';
elem.style.cursor = 'move';
return elem;
},
createPreviewElement: ({ data }): HTMLDivElement => {
const elem = document.createElement('div');
if (typeof data === 'undefined') return elem;
const { width, height, scale } = data.style;
elem.style.width = `${width * scale}px`;
elem.style.height = `${height * scale}px`;
elem.style.border = '1px dashed #000';
elem.style.cursor = 'move';
return elem;
},
});
dnd.on('dragPrepare', ({ dragElement }) => {
dragElement.style.margin = '0';
});
dnd.on('drop', ({ data, targetPosition }) => {
if (typeof data === 'undefined') return;
const { width, height, scale } = data.style;
const { x, y } = targetPosition;
const Cell = CellMap[data.type];
const cell = graph.addNode({
x,
y,
data,
width: width * scale,
height: height * scale,
shape: 'react',
component: (newData) => {
if (!cell) return null;
const { label } = newData.data;
return <Cell label={label} scale={scale} />;
},
});
graph.unSelectCells(graph.getSelectedCells()).selectCell(cell);
});
});
}

16
packages/core/src/utils/styles/cells.ts

@ -1,16 +0,0 @@
import styled from '@emotion/styled';
const Label = styled.div`
position: absolute;
width: 100%;
top: 50%;
padding: 0 5px;
transform: translateY(-50%);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
text-align: center;
`;
export default Label;

68
packages/core/src/utils/tool.ts

@ -1,68 +0,0 @@
import { Graph } from '@antv/x6';
import { Commands } from '../model';
export function zoomIn(graph: Graph) {
return (): void => {
let { scale } = graph.view;
if (scale >= 8) {
scale += 8;
} else if (scale >= 4) {
scale += 4;
} else if (scale >= 2) {
scale += 1;
} else if (scale >= 1.5) {
scale += 0.5;
} else if (scale >= 1) {
scale += 0.25;
} else if (scale >= 0.7) {
scale += 0.15;
} else if (scale >= 0.4) {
scale += 0.1;
} else if (scale >= 0.15) {
scale += 0.05;
} else if (scale >= 0.01) {
scale += 0.01;
}
graph.zoomTo(scale);
};
}
export function zoomOut(graph: Graph) {
return (): void => {
let { scale } = graph.view;
if (scale <= 0.15) {
scale -= 0.01;
} else if (scale <= 0.4) {
scale -= 0.05;
} else if (scale <= 0.7) {
scale -= 0.1;
} else if (scale <= 1) {
scale -= 0.15;
} else if (scale <= 1.5) {
scale -= 0.25;
} else if (scale <= 2) {
scale -= 0.5;
} else if (scale <= 4) {
scale -= 1;
} else if (scale <= 8) {
scale -= 4;
} else if (scale <= 16) {
scale -= 8;
}
graph.zoomTo(scale);
};
}
export function bindKey(commands: Commands, graph: Graph): void {
commands.forEach((items) =>
items.forEach((item) => {
if (item.shortcut) {
if (item.shortcut === 'Delete') {
graph.bindKey(['del', 'backspace'], item.handler);
} else {
graph.bindKey(item.shortcut, item.handler);
}
}
})
);
}

7
packages/core/tsconfig.json

@ -1,7 +1,10 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "dist/types"
"outDir": "dist/types",
"jsx": "react",
"esModuleInterop": true
},
"include": ["./src"]
"include": ["./src"],
"exclude": ["node_modules"]
}

Loading…
Cancel
Save