diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..7e3649a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.env b/.env new file mode 100644 index 0000000..847f6f8 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +BROWSER=none +ESLINT=1 diff --git a/.eslintrc b/.eslintrc new file mode 100755 index 0000000..f626954 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "eslint-config-umi" +} diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 6704566..7d50fdf --- a/.gitignore +++ b/.gitignore @@ -1,104 +1,18 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +# dependencies +/node_modules +/npm-debug.log* +/yarn-error.log +/yarn.lock +/package-lock.json -# Runtime data -pids -*.pid -*.seed -*.pid.lock +# production +/dist -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov +# misc +.DS_Store -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port +# umi +.umi +.umi-production diff --git a/.umirc.ts b/.umirc.ts new file mode 100644 index 0000000..62afa6d --- /dev/null +++ b/.umirc.ts @@ -0,0 +1,37 @@ +import { IConfig } from 'umi-types' + +// ref: https://umijs.org/config/ +const config: IConfig = { + treeShaking: true, + disableCSSModules: true, + plugins: [ + // ref: https://umijs.org/plugin/umi-plugin-react.html + [ + 'umi-plugin-react', + { + antd: true, + dva: false, + dynamicImport: false, + title: 'iMove', + dll: false, + + routes: { + exclude: [/components\//], + }, + }, + ], + ], + extraBabelPlugins: [ + [ + 'import', + { + libraryName: '@antv/x6-components', + libraryDirectory: 'es', + transformToDefaultImport: false, + style: true, + }, + ], + ], +} + +export default config diff --git a/LICENSE b/LICENSE deleted file mode 100644 index a20fb03..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 拾邑 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 15829a4..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# iMove -move your mouse, generate flow chart diff --git a/package.json b/package.json new file mode 100644 index 0000000..bdb3f66 --- /dev/null +++ b/package.json @@ -0,0 +1,57 @@ +{ + "private": true, + "name": "@antv/x6-example-features", + "version": "0.2.3", + "scripts": { + "start": "umi dev", + "build": "umi build", + "lint": "yarn lint:ts", + "lint:ts": "tslint -c tslint.json -p tsconfig.json --fix", + "precommit": "lint-staged" + }, + "dependencies": { + "@antv/x6": "^0.3.0", + "@antv/x6-components": "^0.2.0", + "@antv/x6-react-shape": "^0.2.0", + "antd": "^3.19.5", + "classnames": "^2.2.6", + "react": "^16.12.0", + "react-dom": "^16.8.6" + }, + "devDependencies": { + "@types/classnames": "^2.2.10", + "@types/jest": "^23.3.12", + "@types/react": "^16.7.18", + "@types/react-dom": "^16.0.11", + "@types/react-test-renderer": "^16.0.3", + "babel-eslint": "^9.0.0", + "babel-plugin-import": "^1.13.0", + "eslint": "^5.4.0", + "eslint-config-umi": "^1.4.0", + "eslint-plugin-flowtype": "^2.50.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-jsx-a11y": "^5.1.1", + "eslint-plugin-react": "^7.11.1", + "lint-staged": "^7.2.2", + "react-test-renderer": "^16.7.0", + "tslint": "^5.12.0", + "tslint-eslint-rules": "^5.4.0", + "tslint-react": "^3.6.0", + "umi": "^2.9.0", + "umi-plugin-react": "^1.8.0", + "umi-types": "^0.3.0" + }, + "lint-staged": { + "./src/**/*.{ts,tsx}": [ + "tslint -c tslint.json -p ./tsconfig.json --fix", + "git add" + ], + "*.{js,jsx}": [ + "eslint --fix", + "git add" + ] + }, + "engines": { + "node": ">=8.0.0" + } +} diff --git a/src/global.css b/src/global.css new file mode 100644 index 0000000..e4a5833 --- /dev/null +++ b/src/global.css @@ -0,0 +1,7 @@ +html, body, #root { + height: 100%; +} + +body { + margin: 0; +} diff --git a/src/pages/flowchart/data.ts b/src/pages/flowchart/data.ts new file mode 100644 index 0000000..70cf6dd --- /dev/null +++ b/src/pages/flowchart/data.ts @@ -0,0 +1,398 @@ +export interface DataItem { + width: number; + height: number; + data: { + type: string; + title: string; + icon?: string; + }; +} + +const items: DataItem[] = [ + { + width: 56, + height: 56, + data: { + type: "start", + title: "开始" + } + }, + { + width: 56, + height: 56, + data: { + type: "end", + title: "结束" + } + }, + { + width: 72, + height: 72, + data: { + type: "status", + title: "状态节点" + } + }, + { + width: 128, + height: 48, + data: { + type: "execute", + title: "执行节点", + icon: + '' + } + }, + { + width: 148, + height: 48, + data: { + type: "cond", + title: "条件节点", + icon: + '' + } + } +]; + +const map: { [key: string]: DataItem } = {}; + +items.forEach(item => { + map[item.data.type] = item; +}); + +export const data = { items, map }; + +export function isCircle(item: DataItem) { + return item.data.type === "start" || item.data.type === "end"; +} + +export function isGroup(type: string) { + return type === "combine" || type === "group"; +} + +export function isFunc(type: string) { + return type === "approvement" || type === "execute"; +} + +export const mockGraphData = { + nodes: [ + { + id: "cell-2", + data: { + type: "start", + title: "开始" + }, + shape: "html", + resizable: false, + perimeter: "ellipse", + visible: true, + x: -200, + y: -292, + width: 56, + height: 56, + relative: false, + translateControlPoints: true + }, + { + id: "cell-3", + data: { + type: "status", + title: "idle", + serviceType: "none" + }, + shape: "html", + resizable: true, + perimeter: "rectangle", + visible: true, + x: -208, + y: -200, + width: 72, + height: 72, + relative: false, + translateControlPoints: true + }, + { + id: "cell-4", + data: { + type: "status", + title: "loading", + serviceType: "invoke", + api: "douban.api" + }, + shape: "html", + resizable: true, + perimeter: "rectangle", + visible: true, + x: -208, + y: -70, + width: 72, + height: 72, + relative: false, + translateControlPoints: true + }, + { + id: "cell-5", + data: { + type: "execute", + title: "reject", + icon: + '' + }, + shape: "html", + resizable: true, + perimeter: "rectangle", + visible: true, + x: -370, + y: 60, + width: 128, + height: 48, + relative: false, + translateControlPoints: true + }, + { + id: "cell-6", + data: { + type: "execute", + title: "resolve", + icon: + '', + action: "empty", + contextKey: "file", + contextValue: "result.a" + }, + shape: "html", + resizable: true, + perimeter: "rectangle", + visible: true, + x: -100, + y: 60, + width: 128, + height: 48, + relative: false, + translateControlPoints: true + }, + { + id: "cell-7", + data: { + type: "status", + title: "fail" + }, + shape: "html", + resizable: true, + perimeter: "rectangle", + visible: true, + x: -342, + y: 190, + width: 72, + height: 72, + relative: false, + translateControlPoints: true + }, + { + id: "cell-9", + data: { + type: "status", + title: "success" + }, + shape: "html", + resizable: true, + perimeter: "rectangle", + visible: true, + x: -72, + y: 190, + width: 72, + height: 72, + relative: false, + translateControlPoints: true + } + ], + edges: [ + { + id: "cell-10", + data: null, + align: "center", + verticalAlign: "middle", + fontColor: "rgba(0, 0, 0, 1)", + shape: "connector", + endArrow: "classic", + stroke: "#888", + edge: "elbow", + elbow: "vertical", + labelBackgroundColor: "#f8f9fa", + rounded: true, + movable: false, + sourceAnchorX: "0.5", + sourceAnchorY: "1", + targetAnchorX: "0.5", + targetAnchorY: "0", + sourceAnchorDx: "0", + targetAnchorDx: "0", + targetAnchorDy: "0", + visible: true, + x: 0, + y: 0, + width: 0, + height: 0, + relative: true, + translateControlPoints: true, + source: "cell-2", + target: "cell-3" + }, + { + id: "cell-11", + data: null, + align: "center", + verticalAlign: "middle", + fontColor: "rgba(0, 0, 0, 1)", + shape: "connector", + endArrow: "classic", + stroke: "#888", + edge: "elbow", + elbow: "vertical", + labelBackgroundColor: "#f8f9fa", + rounded: true, + movable: false, + sourceAnchorX: "0.5", + sourceAnchorY: "1", + targetAnchorX: "0.5", + targetAnchorY: "0", + sourceAnchorDx: "0", + targetAnchorDx: "0", + targetAnchorDy: "0", + visible: true, + x: 0, + y: 0, + width: 0, + height: 0, + relative: true, + translateControlPoints: true, + source: "cell-3", + target: "cell-4" + }, + { + id: "cell-12", + data: null, + align: "center", + verticalAlign: "middle", + fontColor: "rgba(0, 0, 0, 1)", + shape: "connector", + endArrow: "classic", + stroke: "#888", + edge: "elbow", + elbow: "vertical", + labelBackgroundColor: "#f8f9fa", + rounded: true, + movable: false, + sourceAnchorX: "0.5", + sourceAnchorY: "1", + targetAnchorX: "0.5", + targetAnchorY: "0", + sourceAnchorDx: "0", + targetAnchorDx: "0", + targetAnchorDy: "0", + visible: true, + x: 0, + y: 0, + width: 0, + height: 0, + relative: true, + translateControlPoints: true, + source: "cell-4", + target: "cell-5" + }, + { + id: "cell-13", + data: null, + align: "center", + verticalAlign: "middle", + fontColor: "rgba(0, 0, 0, 1)", + shape: "connector", + endArrow: "classic", + stroke: "#888", + edge: "elbow", + elbow: "vertical", + labelBackgroundColor: "#f8f9fa", + rounded: true, + movable: false, + sourceAnchorX: "0.5", + sourceAnchorY: "1", + targetAnchorX: "0.5", + targetAnchorY: "0", + sourceAnchorDx: "0", + targetAnchorDx: "0", + targetAnchorDy: "0", + visible: true, + x: 0, + y: 0, + width: 0, + height: 0, + relative: true, + translateControlPoints: true, + source: "cell-4", + target: "cell-6" + }, + { + id: "cell-14", + data: null, + align: "center", + verticalAlign: "middle", + fontColor: "rgba(0, 0, 0, 1)", + shape: "connector", + endArrow: "classic", + stroke: "#888", + edge: "elbow", + elbow: "vertical", + labelBackgroundColor: "#f8f9fa", + rounded: true, + movable: false, + sourceAnchorX: "0.5", + sourceAnchorY: "1", + targetAnchorX: "0.5", + targetAnchorY: "0", + sourceAnchorDx: "0", + targetAnchorDx: "0", + targetAnchorDy: "0", + visible: true, + x: 0, + y: 0, + width: 0, + height: 0, + relative: true, + translateControlPoints: true, + source: "cell-5", + target: "cell-7" + }, + { + id: "cell-15", + data: null, + align: "center", + verticalAlign: "middle", + fontColor: "rgba(0, 0, 0, 1)", + shape: "connector", + endArrow: "classic", + stroke: "#888", + edge: "elbow", + elbow: "vertical", + labelBackgroundColor: "#f8f9fa", + rounded: true, + movable: false, + sourceAnchorX: "0.5", + sourceAnchorY: "1", + targetAnchorX: "0.5", + targetAnchorY: "0", + sourceAnchorDx: "0", + targetAnchorDx: "0", + targetAnchorDy: "0", + visible: true, + x: 0, + y: 0, + width: 0, + height: 0, + relative: true, + translateControlPoints: true, + source: "cell-6", + target: "cell-9" + } + ] +}; diff --git a/src/pages/flowchart/index.less b/src/pages/flowchart/index.less new file mode 100644 index 0000000..9754d17 --- /dev/null +++ b/src/pages/flowchart/index.less @@ -0,0 +1,171 @@ +body { + overflow: hidden; +} + +.flowchart-wrap { + width: 100%; + height: 100%; + overflow: hidden; +} + +.flowchart-main { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; +} + +.flowchart-toolbar { + height: 41; + border-bottom: 1px solid #e9e9e9; + background-color: #fbfbfb; + padding: 4px 8px; +} + +.flowchart-graph { + flex: 1; + overflow: hidden; + + .graph { + box-shadow: none; + } +} + +.flowchart-sidebar, +.flowchart-format { + background-color: #fbfbfb; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.flowchart-sidebar { + border-right: 1px solid #e9e9e9; +} + +.flowchart-format { + border-left: 1px solid #e9e9e9; +} + +.flowchart-sidebar-title { + height: 41px; + text-align: center; + border-bottom: 1px solid #e9e9e9; + font-weight: bold; + line-height: 40px; + font-size: 16px; +} + +.flowchart-sidebar-content { + flex: 1; + padding: 8px 16px; +} + +.x6-node { + foreignobject { + filter: drop-shadow(2px 4px 4px rgba(80, 80, 80, 0.15)); + overflow: visible; + + > div { + overflow: visible !important; + } + } +} + +.flowchart-node-wrap { + overflow: hidden; + margin: 16px auto; + cursor: move; + + &.start, + &.end { + border-radius: 100%; + } +} + +.flowchart-node { + font-family: 'PingFangSC-Regular', 'PingFang SC'; + font-size: 14px; + font-weight: 400; + display: flex; + align-items: center; + width: 100%; + height: 100%; + color: rgba(0, 0, 0, 0.85); + border: 1px solid #e9e9e9; + text-align: center; + // box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15); + background: #ffffff; + user-select: none; + border-radius: 4px; + + &.is-gruop { + align-items: flex-start; + + .icon { + margin-left: 8px; + margin-top: 8px; + } + } + + .anticon { + font-size: 16px; + margin-left: 16px; + } + + .icon { + font-size: 16px; + margin-left: 16px; + font-style: normal; + line-height: 0; + display: inline-block; + text-align: center; + vertical-align: -0.125em; + text-transform: none; + color: #666666; + + text-rendering: optimizelegibility; + -webkit-font-smoothing: antialiased; + } + + span { + width: 100%; + } + + &.start, + &.end { + border-radius: 100%; + justify-content: center; + + .text { + margin-left: 0; + } + } + + &.end { + border-color: #b0b0b0; + background: #cccccc; + } +} + +.ping { + animation: ping 0.8s ease-in-out infinite both; + transform-origin: 50% 50%; +} + +@keyframes ping { + 0% { + transform: scale(0.2); + opacity: 0.8; + } + 80% { + transform: scale(1.2); + opacity: 0; + } + 100% { + transform: scale(2.2); + opacity: 0; + } +} diff --git a/src/pages/flowchart/index.tsx b/src/pages/flowchart/index.tsx new file mode 100644 index 0000000..d20ace9 --- /dev/null +++ b/src/pages/flowchart/index.tsx @@ -0,0 +1,71 @@ +import React from 'react' +import { SplitBox } from '@antv/x6-components' +import { Graph, DomEvent } from '@antv/x6' +import { Sidebar } from './sidebar' +import { GraphToolbar } from './toolbar' +import { createGraph } from './util' +import { Setting } from './settings' +import './index.less' + +export default class FlowChart extends React.Component<{}, FlowChart.State> { + private graph: Graph + private container: HTMLDivElement + + componentDidMount() { + DomEvent.disableContextMenu(this.container) + this.graph = createGraph(this.container) + this.graph.center() + this.setState({ inited: true }) + } + + refContainer = (container: HTMLDivElement) => { + this.container = container + } + + render() { + return ( +
+ +
+
iMove
+
基础组件
+
+ {this.graph && this.state.inited && ( + + )} +
+
+ +
+
+ {this.graph && } +
+
+
+
+
+ {this.graph && this.state.inited && } + + +
+ ) + } +} + +export namespace FlowChart { + export interface State { + inited: boolean + } +} diff --git a/src/pages/flowchart/settings/execute.tsx b/src/pages/flowchart/settings/execute.tsx new file mode 100644 index 0000000..395f195 --- /dev/null +++ b/src/pages/flowchart/settings/execute.tsx @@ -0,0 +1,70 @@ +import * as React from "react"; +import { Select, Input } from "antd"; + +const { Option } = Select; + +class ExecuteSetting extends React.Component< + ExecuteSetting.Props, + ExecuteSetting.State +> { + render() { + return ( +
+ + + + +
+ ); + } +} + +export default ExecuteSetting; + +export namespace ExecuteSetting { + export interface Props { + data: any; + onTitleChange: (e: any) => void; + onActionChange: (action: string) => void; + onKeyChange: (e: any) => void; + onValueChange: (e: any) => void; + } + + export interface State {} +} diff --git a/src/pages/flowchart/settings/index.tsx b/src/pages/flowchart/settings/index.tsx new file mode 100644 index 0000000..f8f5ff9 --- /dev/null +++ b/src/pages/flowchart/settings/index.tsx @@ -0,0 +1,138 @@ +import React from "react"; +import { Graph, Cell } from "@antv/x6"; +import StateSetting from "./state"; +import ExecuteSetting from "./execute"; + +export class Setting extends React.Component { + constructor(props: Setting.Props) { + super(props); + this.state = this.getNextState(props); + + props.graph.on("selection:changed", () => { + this.setState(this.getNextState()); + }); + } + + getNextState(props: Setting.Props = this.props) { + return { + selectedCells: props.graph.getSelectedCells() + }; + } + + onStatusTitleChange = (e: any) => { + const value = e.target.value; + if (value && value.trim()) { + const cell = this.state.selectedCells[0]; + const data = cell.data; + this.props.graph.setCellData(cell, { + ...data, + title: value.trim() + }); + } + }; + + onExcuteTitleChange = (title: string) => { + const cell = this.state.selectedCells[0]; + const data = cell.data; + this.props.graph.setCellData(cell, { + ...data, + title + }); + }; + + onSerTypeChange = (serviceType: string) => { + const cell = this.state.selectedCells[0]; + const data = cell.data; + this.props.graph.setCellData(cell, { + ...data, + serviceType + }); + }; + + onActionChange = (action: string) => { + const cell = this.state.selectedCells[0]; + const data = cell.data; + this.props.graph.setCellData(cell, { + ...data, + action + }); + }; + + onApiChange = (e: any) => { + const value = e.target.value; + if (value && value.trim()) { + const cell = this.state.selectedCells[0]; + const data = cell.data; + this.props.graph.setCellData(cell, { + ...data, + api: value.trim() + }); + } + }; + + onKeyChange = (e: any) => { + const value = e.target.value; + if (value && value.trim()) { + const cell = this.state.selectedCells[0]; + const data = cell.data; + this.props.graph.setCellData(cell, { + ...data, + contextKey: value.trim() + }); + } + }; + + onValueChange = (e: any) => { + const value = e.target.value; + if (value && value.trim()) { + const cell = this.state.selectedCells[0]; + const data = cell.data; + this.props.graph.setCellData(cell, { + ...data, + contextValue: value.trim() + }); + } + }; + + render() { + if (this.state.selectedCells.length === 1) { + const cell = this.state.selectedCells[0]; + const data = cell.data; + return ( +
+
流程设置
+
+ {data && data.type === "status" && ( + + )} + {data && data.type === "execute" && ( + + )} +
+
+ ); + } + return null; + } +} + +export namespace Setting { + export interface Props { + graph: Graph; + } + + export interface State { + selectedCells: Cell[]; + } +} diff --git a/src/pages/flowchart/settings/state.tsx b/src/pages/flowchart/settings/state.tsx new file mode 100644 index 0000000..fb30853 --- /dev/null +++ b/src/pages/flowchart/settings/state.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { Input, Select } from "antd"; + +const { Option } = Select; + +class StateSetting extends React.Component< + StateSetting.Props, + StateSetting.State +> { + render() { + return ( +
+ + + +
+ ); + } +} + +export default StateSetting; + +export namespace StateSetting { + export interface Props { + data: any; + onTitleChange: (e: any) => void; + onApiChange: (e: any) => void; + onSerTypeChange: (serviceType: string) => void; + } + + export interface State {} +} diff --git a/src/pages/flowchart/sidebar.tsx b/src/pages/flowchart/sidebar.tsx new file mode 100644 index 0000000..4179b8b --- /dev/null +++ b/src/pages/flowchart/sidebar.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import classNames from "classnames"; +import { Icon } from "@antv/x6-components"; +import { Graph, Dnd } from "@antv/x6"; +import { data, DataItem, isGroup } from "./data"; +import { addNode } from "./util"; + +export class Sidebar extends React.Component { + private container: HTMLDivElement; + + refContainer = (container: HTMLDivElement) => { + this.container = container; + }; + + componentDidMount() { + this.container.childNodes.forEach((elem: HTMLElement) => { + const type = elem.getAttribute("data-type") || ""; + const itemData = data.map[type]; + + const dnd = new Dnd(elem, { + data: itemData, + getGraph: () => this.props.graph, + getTargetCell: ({ graph, graphX, graphY }) => { + const cell = graph.getCellAt(graphX, graphY); + if (cell != null && cell.data != null && isGroup(cell.data.type)) { + return cell; + } + return null; + }, + createDragElement: ({ element }) => { + const elem = document.createElement("div") as HTMLDivElement; + const w = element.offsetWidth; + const h = element.offsetHeight; + elem.style.width = `${w}px`; + elem.style.height = `${h}px`; + elem.style.border = "1px dashed #000"; + elem.style.cursor = "move"; + return elem; + }, + createPreviewElement: ({ graph, element }) => { + const elem = document.createElement("div") as HTMLDivElement; + const w = element.offsetWidth; + const h = element.offsetHeight; + const s = graph.view.scale; + elem.style.width = `${w * s}px`; + elem.style.height = `${h * s}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, graph, targetCell, targetPosition }) => { + if (data != null) { + const group = isGroup(data.data.type); + const parent = targetCell || undefined; + let x = targetPosition.x; + let y = targetPosition.y; + // relative parent position + if (parent != null) { + const geom = parent.getGeometry()!; + x -= geom.bounds.x; + y -= geom.bounds.y; + } + + const cell = addNode( + graph, + data, + x, + y, + group ? 200 : null, + group ? 160 : null, + null, + parent + ); + + graph.unSelectCells(graph.getSelectedCells()).selectCell(cell); + } + }); + }); + } + + render() { + return ( +
+ {data.items.map(({ data, width, height }) => ( +
+
+ {data.icon && } + {data.title} +
+
+ ))} +
+ ); + } +} + +export namespace Sidebar { + export interface Props { + graph: Graph; + } +} diff --git a/src/pages/flowchart/toolbar.tsx b/src/pages/flowchart/toolbar.tsx new file mode 100644 index 0000000..fb53d9e --- /dev/null +++ b/src/pages/flowchart/toolbar.tsx @@ -0,0 +1,250 @@ +import React from "react"; +import { Toolbar, Menu } from "@antv/x6-components"; +import { Graph, UndoManager } from "@antv/x6"; + +export class GraphToolbar extends React.Component< + GraphToolbar.Props, + GraphToolbar.State +> { + private graph: Graph; + private undoManager: UndoManager; + private commands: GraphToolbar.Command[][]; + + constructor(props: GraphToolbar.Props) { + super(props); + + this.graph = props.graph; + this.undoManager = UndoManager.create(props.graph); + this.commands = [ + [ + { + name: "zoomIn", + icon: "zoom-in", + tooltip: "放大", + handler: () => { + let scale = this.graph.view.scale; + 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; + } + this.graph.zoomTo(scale); + } + }, + { + name: "zoomOut", + icon: "zoom-out", + tooltip: "缩小", + handler: () => { + let scale = this.graph.view.scale; + 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; + } + this.graph.zoomTo(scale); + } + } + ], + [ + { + name: "undo", + icon: "undo", + tooltip: "撤销", + shortcut: "Cmd + Z", + handler: () => this.undoManager.undo() + }, + { + name: "redo", + icon: "redo", + tooltip: "重做", + shortcut: "Cmd + Shift + Z", + handler: () => this.undoManager.redo() + } + ], + [ + { + name: "delete", + icon: "delete", + tooltip: "删除", + shortcut: "Delete", + handler: () => this.graph.deleteCells() + }, + { + name: "save", + icon: "save", + tooltip: "保存", + shortcut: "Cmd + S", + handler: () => { + const graphData = this.graph.toJSON(); + console.log(JSON.stringify(graphData)); + } + } + ] + ]; + + this.commands.forEach(items => + items.forEach(item => { + if (item.shortcut) { + if (item.shortcut === "Delete") { + this.graph.bindKey(["del", "backspace"], item.handler); + } else { + this.graph.bindKey(item.shortcut, item.handler); + } + } + }) + ); + + this.graph.on("selection:changed", this.updateState); + this.graph.view.on("scale", this.updateState); + this.graph.view.on("scaleAndTranslate", this.updateState); + this.graph.model.on("change", this.updateState); + this.undoManager.on("undo", this.updateState); + this.undoManager.on("redo", this.updateState); + + this.state = this.getNextState(); + } + + updateState = () => { + this.setState(this.getNextState()); + }; + + getNextState() { + return { + zoomIn: this.graph.view.scale === this.graph.maxScale, + zoomOut: this.graph.view.scale === this.graph.minScale, + undo: !this.undoManager.canUndo(), + redo: !this.undoManager.canRedo(), + delete: this.graph.getSelectedCells().length <= 0 + }; + } + + resetZoom = () => { + if (this.graph.view.scale !== 1) { + this.graph.zoomTo(1); + } + }; + + onClick = (name: string) => { + if (name === "resetView") { + this.graph.zoomTo(1); + this.graph.center(); + } else if (name === "fitWindow") { + this.graph.fit(8); + // this.graph.center() + } else if ( + name === "25" || + name === "50" || + name === "75" || + name === "100" || + name === "125" || + name === "150" || + name === "200" || + name === "400" + ) { + const scale = parseInt(name, 10) / 100; + this.graph.zoomTo(scale); + } + }; + + renderZoomDropdown() { + const MenuItem = Menu.Item; + return ( + + 25% + 50% + 75% + 100% + 125% + 150% + 200% + 400% + + ); + } + + render() { + const Item = Toolbar.Item; + const Group = Toolbar.Group; + + return ( + + + {(this.graph.view.scale * 100).toFixed(0)}% + + + + + + {this.commands.map(items => ( + i.name).join("-")}> + {items.map(item => ( + + ))} + + ))} + + ); + } +} + +export namespace GraphToolbar { + export interface Props { + graph: Graph; + } + + export interface State { + zoomIn: boolean; + zoomOut: boolean; + redo: boolean; + undo: boolean; + delete: boolean; + } + + export interface Command { + name: string; + icon: string; + tooltip: string; + shortcut?: string; + handler: () => void; + } +} diff --git a/src/pages/flowchart/util.ts b/src/pages/flowchart/util.ts new file mode 100644 index 0000000..c95506d --- /dev/null +++ b/src/pages/flowchart/util.ts @@ -0,0 +1,185 @@ +import { Cell, Graph, Style } from "@antv/x6"; +import { DataItem, isCircle, isGroup, mockGraphData } from "./data"; + +export function createGraph(container: HTMLDivElement) { + return new Graph(container, { + rotate: false, + resize: true, + folding: false, + infinite: true, // 无限大画布 + // pageVisible: true, + // pageBreak: { + // enabled: true, + // dsahed: true, + // stroke: '#c0c0c0', + // }, + // pageFormat: { + // width: 800, + // height: 960, + // }, + // mouseWheel: true, + rubberband: true, + allowLoops: true, + backgroundColor: "#f8f9fa", + grid: { + type: "dot", + color: "#bcbcbc" + }, + guide: { + enabled: true, + dashed: true, + stroke: "#ff5500" + }, + connection: { + enabled: true, + hotspotable: false, + livePreview: true, + createEdge(options) { + const style = options.style; + fixEdgeStyle(style, style); + return this.createEdge(options); + } + }, + connectionPreview: { + stroke: "#1890ff" + }, + connectionHighlight: {}, + allowDanglingEdges: false, + 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 + }, + dropEnabled: true, + dropTargetHighlight: { + stroke: "#87d068", + opacity: 1 + }, + anchor: { + inductiveSize: 24 + }, + // anchorTip: { + // enabled: true, + // opacity: 1, + // className: 'ping', + // }, + anchorHighlight: { + strokeWidth: 16 + }, + isValidDropTarget(target) { + if (target && target.data) { + return isGroup(target.data.type); + } + }, + isLabelMovable() { + return false; + }, + shouldRedrawOnDataChange() { + return true; + }, + getAnchors(cell) { + if (cell != null && this.model.isNode(cell)) { + const type = cell.data.type; + if (type === "start") { + return [[0.5, 1]]; + } + + if (isGroup(type)) { + return [ + [0.25, 0], + [0.5, 0], + [0.75, 0], + + [0, 0.25], + [0, 0.5], + [0, 0.75], + + [1, 0.25], + [1, 0.5], + [1, 0.75], + + [0.25, 1], + [0.5, 1], + [0.75, 1] + ]; + } + + return [[0.5, 0], [0, 0.5], [1, 0.5], [0.5, 1]]; + } + return null; + }, + getHtml(cell) { + const data = cell.data; + const group = isGroup(data.type); + return ` +
+ ${data.icon ? `${data.icon}` : ""} + ${group ? "" : `${data.title}`} +
+ `; + } + }); +} + +export function addNode( + graph: Graph, + item: DataItem, + x: number, + y: number, + width?: number | null, + height?: number | null, + title?: string | null, + parent?: Cell +) { + const data = { ...item.data }; + if (title) { + data.title = title; + } + + return graph.addNode({ + x, + y, + data, + parent, + width: width || item.width, + height: height || item.height, + shape: "html", + resizable: !isCircle(item), + perimeter: isCircle(item) ? "ellipse" : "rectangle" + }); +} + +function fixEdgeStyle(raw: Style, result: Style) { + if (raw.targetAnchorX === 1 || raw.targetAnchorX === 0) { + result.elbow = "horizontal"; + } +} + +export function addEdge(graph: Graph, options: any) { + const style: Style = {}; + fixEdgeStyle(options, style); + graph.addEdge({ ...options, style }); +} + +// export function graph2flow(graphJson: any) { +// const { nodes, edges } = mockGraphData; + +// nodes.forEach() +// } diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100755 index 0000000..3d49937 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import FlowChart from "./flowchart"; + +export default function() { + return ; +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..2ebd9db --- /dev/null +++ b/src/util.ts @@ -0,0 +1,44 @@ +export function randomColor() { + var letters = "0123456789ABCDEF"; + var color = "#"; + for (var i = 0; i < 6; i += 1) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; +} + +export function invertColor(hex: string, bw: boolean) { + if (hex.indexOf("#") === 0) { + hex = hex.slice(1); + } + // convert 3-digit hex to 6-digits. + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + + if (hex.length !== 6) { + throw new Error("Invalid HEX color."); + } + + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + + if (bw) { + // http://stackoverflow.com/a/3943023/112731 + return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? "#000000" : "#FFFFFF"; + } + + // invert color components + const rr = (255 - r).toString(16); + const gg = (255 - g).toString(16); + const bb = (255 - b).toString(16); + + // pad each with zeros and return + return "#" + padZero(rr) + padZero(gg) + padZero(bb); +} + +function padZero(str: string, len: number = 2) { + var zeros = new Array(len).join("0"); + return (zeros + str).slice(-len); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..71d78b9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "baseUrl": ".", + "importHelpers": true, + "sourceMap": true, + "esModuleInterop": true, + "noImplicitAny": true, + "noEmitOnError": true, + "noUnusedLocals": true, + "strictNullChecks": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "allowSyntheticDefaultImports": true, + "paths": { + "@/*": [ + "src/*" + ] + } + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..07ed8c2 --- /dev/null +++ b/tslint.json @@ -0,0 +1,25 @@ +{ + "extends": ["tslint-react", "tslint-eslint-rules"], + "rules": { + "semicolon": [true, "never"], + "quotemark": [true, "single", "jsx-double"], + "trailing-comma": [ + true, + { + "singleline": "never", + "multiline": { + "objects": "always", + "arrays": "always", + "functions": "always", + "typeLiterals": "ignore" + }, + "esSpecCompliant": true + } + ], + "no-construct": true, + "no-debugger": true, + "no-reference": true, + "jsx-no-multiline-js": false, + "jsx-no-lambda": false + } +} diff --git a/typings.d.ts b/typings.d.ts new file mode 100644 index 0000000..6fe5a28 --- /dev/null +++ b/typings.d.ts @@ -0,0 +1 @@ +declare module '*.less'