├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── README.md
├── build
├── build.js
├── check-versions.js
├── logo.png
├── utils.js
├── vue-loader.conf.js
├── webpack.base.conf.js
├── webpack.dev.conf.js
├── webpack.prod.conf.js
└── webpack.test.conf.js
├── config
├── dev.env.js
├── index.js
├── prod.env.js
└── test.env.js
├── index.html
├── package.json
├── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ ├── FcCanvas.vue
│ ├── FcConnector.vue
│ ├── FcEdge.vue
│ ├── FcEdgeLabel.vue
│ ├── FcMagnet.vue
│ └── FcNode.vue
├── config
│ └── flowchart.js
├── main.js
├── pages
│ └── Index.vue
├── router
│ └── index.js
├── service
│ ├── edgedragging.js
│ ├── edgedrawing.js
│ ├── modelvalidation.js
│ ├── nodedragging.js
│ ├── rectangleselect.js
│ ├── store.js
│ └── topsort.js
└── utils
│ ├── setDragImage.polyfill.js
│ └── uuid.js
├── static
└── .gitkeep
└── test
├── e2e
├── custom-assertions
│ └── elementCount.js
├── nightwatch.conf.js
├── runner.js
└── specs
│ └── test.js
└── unit
├── .eslintrc
├── index.js
├── karma.conf.js
└── specs
└── HelloWorld.spec.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": ["transform-vue-jsx", "transform-runtime"],
12 | "env": {
13 | "test": {
14 | "presets": ["env", "stage-2"],
15 | "plugins": ["transform-vue-jsx", "istanbul"]
16 | }
17 | },
18 | "ignore":["/src/utils"]
19 | }
20 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /build/
2 | /config/
3 | /dist/
4 | /*.js
5 | /test/unit/coverage/
6 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 |
6 | // parser: 'babel-eslint',
7 | parserOptions: {
8 | sourceType: 'module',
9 | parser: 'babel-eslint',
10 | },
11 | env: {
12 | browser: true,
13 | },
14 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
15 | extends: [
16 | "plugin:vue/recommended",
17 | "standard"
18 | ],
19 | // required to lint *.vue files
20 | plugins: [
21 | 'vue'
22 | ],
23 | // add your custom rules here
24 | 'rules': {
25 | // allow paren-less arrow functions
26 | 'arrow-parens': 0,
27 | // allow async-await
28 | 'generator-star-spacing': 0,
29 | // allow debugger during development
30 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | /dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | /test/unit/coverage/
8 | /test/e2e/reports/
9 | selenium-debug.log
10 |
11 | # Editor directories and files
12 | .idea
13 | .vscode
14 | *.suo
15 | *.ntvs*
16 | *.njsproj
17 | *.sln
18 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | "postcss-import": {},
6 | "postcss-url": {},
7 | // to edit target browsers: use "browserslist" field in package.json
8 | "autoprefixer": {}
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## vue-flowchart
2 |
3 | > 基于Vue.js的流程图框架
4 |
5 | 目前进度:
6 |
7 | - [x] 数据显示
8 | - [x] 节点拖拽
9 | - [x] 连线拖拽
10 | - [x] 多选,划框选择
11 | - [x] 多元素拖拽
12 | - [x] 节点创建:点击新增&预置节点拖拽新增
13 | - [x] 节点、连线编辑和删除
14 | - [x] 画板自适应
15 | - [x] 复制粘贴节点
16 | - [ ] 快捷键处理
17 | - [ ] 画板缩放
18 | - [x] 撤销重做
19 | - [ ] 节点svg化
20 | - [x] 悬浮节点提示
21 |
22 |
23 | ## 构建
24 |
25 | ``` bash
26 | # install dependencies
27 | npm install
28 |
29 | # serve with hot reload at localhost:8080
30 | npm run dev
31 |
32 | # build for production with minification
33 | npm run build
34 |
35 | # build for production and view the bundle analyzer report
36 | npm run build --report
37 |
38 | # run unit tests
39 | npm run unit
40 |
41 | # run e2e tests
42 | npm run e2e
43 |
44 | # run all tests
45 | npm test
46 | ```
47 | ## 使用文档
48 |
49 | ...
50 |
51 | ## 定义
52 |
53 | ### 画板 Canvas
54 |
55 | 分为两种组件,一种是主界面,一种是放置模板节点的这里叫模板界面,可以有多个模板界面。
56 |
57 | 对于模板界面,没有连线,且仅支持节点拖拽和鼠标悬浮事件
58 |
59 | canvas画板由一个 model 对象控制数据,其中包含两种元素,即节点和连线
60 |
61 | ```javascript
62 | {
63 | nodes: [Node],
64 | edges: [Edge]
65 | }
66 | ```
67 |
68 | 元素的操作,除了在各种组件上的处理外,事件还会派发到canvas上,由canvas决定是否推入历史栈
69 |
70 | 一些功能按钮,在外部通过slot插槽传入canvas组件,样式让用户自定义
71 |
72 | ### 节点 Node
73 |
74 | 主要元素之一
75 |
76 | ```js
77 | {
78 | id: Number | String, //唯一标识符
79 | name: String,//节点名称
80 | x: Number, // 节点相对canvas的x坐标
81 | y: Number, // 节点相对canvas的y坐标
82 | connectors: [Connector],// 连接点
83 | readonly: Boolean=false,// 是否只读,只读模式下仅支持鼠标悬浮事件
84 | addition:Object// 支持拓展
85 | }
86 | ```
87 |
88 | ### 连接点 Connector
89 |
90 | 一个node最多仅有两个connector(左右两侧两种类型)
91 |
92 | 用于连线时使用
93 |
94 | ```javascript
95 | {
96 | // 连接点类型,取值范围:leftConnector/rightConnector
97 | [type: String]: {
98 | id: Number | String, //唯一标识符
99 | }
100 | }
101 | ```
102 |
103 | 组件配置:
104 |
105 |
106 | ### 连线 Edge
107 |
108 | 主要元素之一
109 |
110 | 由某节点的rightConnector连接点和另一节点的leftConnector连接点相连,带箭头
111 |
112 | ```javascript
113 | {
114 | source: Connector.id,
115 | destination: Connector.id,
116 | active: Boolean=false,// 应用连线流动动画
117 | label:String//连线的标签
118 | }
119 | ```
120 |
121 | ### 标签
122 |
123 | 处于连线中心
124 |
125 | ### 其他坐标
126 |
127 | event.clientX: 基于网页左上角的x坐标
128 |
129 | getBoundingClientRect: 相对视口的坐标(相对),而不是网页左上角(绝对),比如有滚动条且向下滚动,获得的top值偏小
130 |
131 | ## 拖拽
132 |
133 | 连线随节点变动而变动
134 |
135 | 如何做到高效实时拖拽?
136 |
137 | ### 1. 监听待拖放节点的drag事件,跟随clientX/Y移动
138 |
139 | ```js
140 | handleDragstart () {
141 | console.log('node Dragstart:', event)
142 | let elementBox = this.$el.getBoundingClientRect()
143 | this.eventPointOffset.x = event.clientX - elementBox.left
144 | this.eventPointOffset.y = event.clientY - elementBox.top
145 | // this.nodedraggingservice.dragstart(event)
146 | let dataTransfer = event.dataTransfer
147 | dataTransfer.dropEffect = 'move'
148 | dataTransfer.setData('Text', event.target.id)
149 | dataTransfer.setDragImage(this.$el, this.eventPointOffset.x, this.eventPointOffset.y)
150 | this.$emit('node-dragstart', this.node)
151 | this.updateConnectorPosition()
152 | },
153 | handleDragging (event) {
154 | console.log('handleDragging')
155 | if (!(event.clientX && event.clientY)) {
156 | return
157 | }
158 | let newNode = Object.assign(this.node, {
159 | x: event.clientX - this.canvas.left - this.eventPointOffset.x,
160 | y: event.clientY - this.canvas.top - this.eventPointOffset.y
161 | })
162 | this.updateNode({
163 | node: this.node,
164 | newNode
165 | })
166 | this.updateConnectorPosition()
167 | },
168 | handleDragend () {
169 | console.log('node Dragend:', event)
170 | let newNode = Object.assign(this.node, {
171 | x: event.clientX - this.canvas.left - this.eventPointOffset.x,
172 | y: event.clientY - this.canvas.top - this.eventPointOffset.y
173 | })
174 | this.updateNode({
175 | node: this.node,
176 | newNode,
177 | isPushState: true
178 | })
179 | this.$emit('node-dragend', event)
180 | // this.updateConnectorPosition()
181 | }
182 | ```
183 | firefox 下 drag,dragend拿到的event.clientX/Y 为0,只能从容器的 dragover 和drop事件的event中获取
184 |
185 | firefox 必须使用`dataTransfer.setData('text', xxx)` ie11上会报错且getData拿不到数据?
186 |
187 | dragover时拿不到`dataTransfer.getData`
188 |
189 | ### 2. 触发容器的dragover事件
190 |
191 | 这样做的好处在于,被拖动元素将不可拖动至任意地方,仅在容器中重绘
192 |
193 | chrome 中 dragover 不能拿到`event.dataTransfer.getData('Text')`
194 |
195 | ### 采用mouse代替drag
196 |
197 | 从测试上来看,mouse兼容性好,且性能更好一点
198 | ### 批量拖拽
199 |
200 | ## 鼠标悬浮mouseOver
201 |
202 | 类似 mouseover,它们两者之间的差别是 mouseenter 不会冒泡(bubble)
--------------------------------------------------------------------------------
/build/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | process.env.NODE_ENV = 'production'
5 |
6 | const ora = require('ora')
7 | const rm = require('rimraf')
8 | const path = require('path')
9 | const chalk = require('chalk')
10 | const webpack = require('webpack')
11 | const config = require('../config')
12 | const webpackConfig = require('./webpack.prod.conf')
13 |
14 | const spinner = ora('building for production...')
15 | spinner.start()
16 |
17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
18 | if (err) throw err
19 | webpack(webpackConfig, (err, stats) => {
20 | spinner.stop()
21 | if (err) throw err
22 | process.stdout.write(stats.toString({
23 | colors: true,
24 | modules: false,
25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
26 | chunks: false,
27 | chunkModules: false
28 | }) + '\n\n')
29 |
30 | if (stats.hasErrors()) {
31 | console.log(chalk.red(' Build failed with errors.\n'))
32 | process.exit(1)
33 | }
34 |
35 | console.log(chalk.cyan(' Build complete.\n'))
36 | console.log(chalk.yellow(
37 | ' Tip: built files are meant to be served over an HTTP server.\n' +
38 | ' Opening index.html over file:// won\'t work.\n'
39 | ))
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/build/check-versions.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const chalk = require('chalk')
3 | const semver = require('semver')
4 | const packageConfig = require('../package.json')
5 | const shell = require('shelljs')
6 |
7 | function exec (cmd) {
8 | return require('child_process').execSync(cmd).toString().trim()
9 | }
10 |
11 | const versionRequirements = [
12 | {
13 | name: 'node',
14 | currentVersion: semver.clean(process.version),
15 | versionRequirement: packageConfig.engines.node
16 | }
17 | ]
18 |
19 | if (shell.which('npm')) {
20 | versionRequirements.push({
21 | name: 'npm',
22 | currentVersion: exec('npm --version'),
23 | versionRequirement: packageConfig.engines.npm
24 | })
25 | }
26 |
27 | module.exports = function () {
28 | const warnings = []
29 |
30 | for (let i = 0; i < versionRequirements.length; i++) {
31 | const mod = versionRequirements[i]
32 |
33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
34 | warnings.push(mod.name + ': ' +
35 | chalk.red(mod.currentVersion) + ' should be ' +
36 | chalk.green(mod.versionRequirement)
37 | )
38 | }
39 | }
40 |
41 | if (warnings.length) {
42 | console.log('')
43 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
44 | console.log()
45 |
46 | for (let i = 0; i < warnings.length; i++) {
47 | const warning = warnings[i]
48 | console.log(' ' + warning)
49 | }
50 |
51 | console.log()
52 | process.exit(1)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/build/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/francecil/vue-flowchart/89c614b682934d600125850d9095ee9da18eb0b2/build/logo.png
--------------------------------------------------------------------------------
/build/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const config = require('../config')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 | const packageConfig = require('../package.json')
6 |
7 | exports.assetsPath = function (_path) {
8 | const assetsSubDirectory = process.env.NODE_ENV === 'production'
9 | ? config.build.assetsSubDirectory
10 | : config.dev.assetsSubDirectory
11 |
12 | return path.posix.join(assetsSubDirectory, _path)
13 | }
14 |
15 | exports.cssLoaders = function (options) {
16 | options = options || {}
17 |
18 | const cssLoader = {
19 | loader: 'css-loader',
20 | options: {
21 | sourceMap: options.sourceMap
22 | }
23 | }
24 |
25 | const postcssLoader = {
26 | loader: 'postcss-loader',
27 | options: {
28 | sourceMap: options.sourceMap
29 | }
30 | }
31 |
32 | // generate loader string to be used with extract text plugin
33 | function generateLoaders (loader, loaderOptions) {
34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
35 |
36 | if (loader) {
37 | loaders.push({
38 | loader: loader + '-loader',
39 | options: Object.assign({}, loaderOptions, {
40 | sourceMap: options.sourceMap
41 | })
42 | })
43 | }
44 |
45 | // Extract CSS when that option is specified
46 | // (which is the case during production build)
47 | if (options.extract) {
48 | return ExtractTextPlugin.extract({
49 | use: loaders,
50 | fallback: 'vue-style-loader'
51 | })
52 | } else {
53 | return ['vue-style-loader'].concat(loaders)
54 | }
55 | }
56 |
57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
58 | return {
59 | css: generateLoaders(),
60 | postcss: generateLoaders(),
61 | less: generateLoaders('less'),
62 | sass: generateLoaders('sass', { indentedSyntax: true }),
63 | scss: generateLoaders('sass'),
64 | stylus: generateLoaders('stylus'),
65 | styl: generateLoaders('stylus')
66 | }
67 | }
68 |
69 | // Generate loaders for standalone style files (outside of .vue)
70 | exports.styleLoaders = function (options) {
71 | const output = []
72 | const loaders = exports.cssLoaders(options)
73 |
74 | for (const extension in loaders) {
75 | const loader = loaders[extension]
76 | output.push({
77 | test: new RegExp('\\.' + extension + '$'),
78 | use: loader
79 | })
80 | }
81 |
82 | return output
83 | }
84 |
85 | exports.createNotifierCallback = () => {
86 | const notifier = require('node-notifier')
87 |
88 | return (severity, errors) => {
89 | if (severity !== 'error') return
90 |
91 | const error = errors[0]
92 | const filename = error.file && error.file.split('!').pop()
93 |
94 | notifier.notify({
95 | title: packageConfig.name,
96 | message: severity + ': ' + error.name,
97 | subtitle: filename || '',
98 | icon: path.join(__dirname, 'logo.png')
99 | })
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const config = require('../config')
4 | const isProduction = process.env.NODE_ENV === 'production'
5 | const sourceMapEnabled = isProduction
6 | ? config.build.productionSourceMap
7 | : config.dev.cssSourceMap
8 |
9 | module.exports = {
10 | loaders: utils.cssLoaders({
11 | sourceMap: sourceMapEnabled,
12 | extract: isProduction
13 | }),
14 | cssSourceMap: sourceMapEnabled,
15 | cacheBusting: config.dev.cacheBusting,
16 | transformToRequire: {
17 | video: ['src', 'poster'],
18 | source: 'src',
19 | img: 'src',
20 | image: 'xlink:href'
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const config = require('../config')
5 | const vueLoaderConfig = require('./vue-loader.conf')
6 | require('babel-polyfill')
7 |
8 | function resolve (dir) {
9 | return path.join(__dirname, '..', dir)
10 | }
11 |
12 | const createLintingRule = () => ({
13 | test: /\.(js|vue)$/,
14 | loader: 'eslint-loader',
15 | enforce: 'pre',
16 | include: [resolve('src'), resolve('test')],
17 | options: {
18 | formatter: require('eslint-friendly-formatter'),
19 | emitWarning: !config.dev.showEslintErrorsInOverlay
20 | }
21 | })
22 |
23 | module.exports = {
24 | context: path.resolve(__dirname, '../'),
25 | entry: {
26 | app: ['babel-polyfill', './src/main.js']
27 | },
28 | output: {
29 | path: config.build.assetsRoot,
30 | filename: '[name].js',
31 | publicPath: process.env.NODE_ENV === 'production'
32 | ? config.build.assetsPublicPath
33 | : config.dev.assetsPublicPath
34 | },
35 | resolve: {
36 | extensions: ['.js', '.vue', '.json'],
37 | alias: {
38 | 'vue$': 'vue/dist/vue.esm.js',
39 | '@': resolve('src'),
40 | }
41 | },
42 | module: {
43 | rules: [
44 | ...(config.dev.useEslint ? [createLintingRule()] : []),
45 | {
46 | test: /\.vue$/,
47 | loader: 'vue-loader',
48 | options: vueLoaderConfig
49 | },
50 | {
51 | test: /\.js$/,
52 | loader: 'babel-loader',
53 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
54 | },
55 | {
56 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
57 | loader: 'url-loader',
58 | options: {
59 | limit: 10000,
60 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
61 | }
62 | },
63 | {
64 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
65 | loader: 'url-loader',
66 | options: {
67 | limit: 10000,
68 | name: utils.assetsPath('media/[name].[hash:7].[ext]')
69 | }
70 | },
71 | {
72 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
73 | loader: 'url-loader',
74 | options: {
75 | limit: 10000,
76 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
77 | }
78 | }
79 | ]
80 | },
81 | node: {
82 | // prevent webpack from injecting useless setImmediate polyfill because Vue
83 | // source contains it (although only uses it if it's native).
84 | setImmediate: false,
85 | // prevent webpack from injecting mocks to Node native modules
86 | // that does not make sense for the client
87 | dgram: 'empty',
88 | fs: 'empty',
89 | net: 'empty',
90 | tls: 'empty',
91 | child_process: 'empty'
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const webpack = require('webpack')
4 | const config = require('../config')
5 | const merge = require('webpack-merge')
6 | const path = require('path')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
11 | const portfinder = require('portfinder')
12 |
13 | const HOST = process.env.HOST
14 | const PORT = process.env.PORT && Number(process.env.PORT)
15 |
16 | const devWebpackConfig = merge(baseWebpackConfig, {
17 | module: {
18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
19 | },
20 | // cheap-module-eval-source-map is faster for development
21 | devtool: config.dev.devtool,
22 |
23 | // these devServer options should be customized in /config/index.js
24 | devServer: {
25 | clientLogLevel: 'warning',
26 | historyApiFallback: {
27 | rewrites: [
28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
29 | ],
30 | },
31 | hot: true,
32 | contentBase: false, // since we use CopyWebpackPlugin.
33 | compress: true,
34 | host: HOST || config.dev.host,
35 | port: PORT || config.dev.port,
36 | open: config.dev.autoOpenBrowser,
37 | overlay: config.dev.errorOverlay
38 | ? { warnings: false, errors: true }
39 | : false,
40 | publicPath: config.dev.assetsPublicPath,
41 | proxy: config.dev.proxyTable,
42 | quiet: true, // necessary for FriendlyErrorsPlugin
43 | watchOptions: {
44 | poll: config.dev.poll,
45 | }
46 | },
47 | plugins: [
48 | new webpack.DefinePlugin({
49 | 'process.env': require('../config/dev.env')
50 | }),
51 | new webpack.HotModuleReplacementPlugin(),
52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
53 | new webpack.NoEmitOnErrorsPlugin(),
54 | // https://github.com/ampedandwired/html-webpack-plugin
55 | new HtmlWebpackPlugin({
56 | filename: 'index.html',
57 | template: 'index.html',
58 | inject: true
59 | }),
60 | // copy custom static assets
61 | new CopyWebpackPlugin([
62 | {
63 | from: path.resolve(__dirname, '../static'),
64 | to: config.dev.assetsSubDirectory,
65 | ignore: ['.*']
66 | }
67 | ])
68 | ]
69 | })
70 |
71 | module.exports = new Promise((resolve, reject) => {
72 | portfinder.basePort = process.env.PORT || config.dev.port
73 | portfinder.getPort((err, port) => {
74 | if (err) {
75 | reject(err)
76 | } else {
77 | // publish the new Port, necessary for e2e tests
78 | process.env.PORT = port
79 | // add port to devServer config
80 | devWebpackConfig.devServer.port = port
81 |
82 | // Add FriendlyErrorsPlugin
83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
84 | compilationSuccessInfo: {
85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
86 | },
87 | onErrors: config.dev.notifyOnErrors
88 | ? utils.createNotifierCallback()
89 | : undefined
90 | }))
91 |
92 | resolve(devWebpackConfig)
93 | }
94 | })
95 | })
96 |
--------------------------------------------------------------------------------
/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const webpack = require('webpack')
5 | const config = require('../config')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
13 |
14 | const env = process.env.NODE_ENV === 'testing'
15 | ? require('../config/test.env')
16 | : require('../config/prod.env')
17 |
18 | const webpackConfig = merge(baseWebpackConfig, {
19 | module: {
20 | rules: utils.styleLoaders({
21 | sourceMap: config.build.productionSourceMap,
22 | extract: true,
23 | usePostCSS: true
24 | })
25 | },
26 | devtool: config.build.productionSourceMap ? config.build.devtool : false,
27 | output: {
28 | path: config.build.assetsRoot,
29 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
30 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
31 | },
32 | plugins: [
33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
34 | new webpack.DefinePlugin({
35 | 'process.env': env
36 | }),
37 | new UglifyJsPlugin({
38 | uglifyOptions: {
39 | compress: {
40 | warnings: false
41 | }
42 | },
43 | sourceMap: config.build.productionSourceMap,
44 | parallel: true
45 | }),
46 | // extract css into its own file
47 | new ExtractTextPlugin({
48 | filename: utils.assetsPath('css/[name].[contenthash].css'),
49 | // Setting the following option to `false` will not extract CSS from codesplit chunks.
50 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
51 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
52 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
53 | allChunks: true,
54 | }),
55 | // Compress extracted CSS. We are using this plugin so that possible
56 | // duplicated CSS from different components can be deduped.
57 | new OptimizeCSSPlugin({
58 | cssProcessorOptions: config.build.productionSourceMap
59 | ? { safe: true, map: { inline: false } }
60 | : { safe: true }
61 | }),
62 | // generate dist index.html with correct asset hash for caching.
63 | // you can customize output by editing /index.html
64 | // see https://github.com/ampedandwired/html-webpack-plugin
65 | new HtmlWebpackPlugin({
66 | filename: process.env.NODE_ENV === 'testing'
67 | ? 'index.html'
68 | : config.build.index,
69 | template: 'index.html',
70 | inject: true,
71 | minify: {
72 | removeComments: true,
73 | collapseWhitespace: true,
74 | removeAttributeQuotes: true
75 | // more options:
76 | // https://github.com/kangax/html-minifier#options-quick-reference
77 | },
78 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
79 | chunksSortMode: 'dependency'
80 | }),
81 | // keep module.id stable when vendor modules does not change
82 | new webpack.HashedModuleIdsPlugin(),
83 | // enable scope hoisting
84 | new webpack.optimize.ModuleConcatenationPlugin(),
85 | // split vendor js into its own file
86 | new webpack.optimize.CommonsChunkPlugin({
87 | name: 'vendor',
88 | minChunks (module) {
89 | // any required modules inside node_modules are extracted to vendor
90 | return (
91 | module.resource &&
92 | /\.js$/.test(module.resource) &&
93 | module.resource.indexOf(
94 | path.join(__dirname, '../node_modules')
95 | ) === 0
96 | )
97 | }
98 | }),
99 | // extract webpack runtime and module manifest to its own file in order to
100 | // prevent vendor hash from being updated whenever app bundle is updated
101 | new webpack.optimize.CommonsChunkPlugin({
102 | name: 'manifest',
103 | minChunks: Infinity
104 | }),
105 | // This instance extracts shared chunks from code splitted chunks and bundles them
106 | // in a separate chunk, similar to the vendor chunk
107 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
108 | new webpack.optimize.CommonsChunkPlugin({
109 | name: 'app',
110 | async: 'vendor-async',
111 | children: true,
112 | minChunks: 3
113 | }),
114 |
115 | // copy custom static assets
116 | new CopyWebpackPlugin([
117 | {
118 | from: path.resolve(__dirname, '../static'),
119 | to: config.build.assetsSubDirectory,
120 | ignore: ['.*']
121 | }
122 | ])
123 | ]
124 | })
125 |
126 | if (config.build.productionGzip) {
127 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
128 |
129 | webpackConfig.plugins.push(
130 | new CompressionWebpackPlugin({
131 | asset: '[path].gz[query]',
132 | algorithm: 'gzip',
133 | test: new RegExp(
134 | '\\.(' +
135 | config.build.productionGzipExtensions.join('|') +
136 | ')$'
137 | ),
138 | threshold: 10240,
139 | minRatio: 0.8
140 | })
141 | )
142 | }
143 |
144 | if (config.build.bundleAnalyzerReport) {
145 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
146 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
147 | }
148 |
149 | module.exports = webpackConfig
150 |
--------------------------------------------------------------------------------
/build/webpack.test.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // This is the webpack config used for unit tests.
3 |
4 | const utils = require('./utils')
5 | const webpack = require('webpack')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 |
9 | const webpackConfig = merge(baseWebpackConfig, {
10 | // use inline sourcemap for karma-sourcemap-loader
11 | module: {
12 | rules: utils.styleLoaders()
13 | },
14 | devtool: '#inline-source-map',
15 | resolveLoader: {
16 | alias: {
17 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option
18 | // see discussion at https://github.com/vuejs/vue-loader/issues/724
19 | 'scss-loader': 'sass-loader'
20 | }
21 | },
22 | plugins: [
23 | new webpack.DefinePlugin({
24 | 'process.env': require('../config/test.env')
25 | })
26 | ]
27 | })
28 |
29 | // no need for app entry during tests
30 | delete webpackConfig.entry
31 |
32 | module.exports = webpackConfig
33 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 |
5 | module.exports = merge(prodEnv, {
6 | NODE_ENV: '"development"'
7 | })
8 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // Template version: 1.3.1
3 | // see http://vuejs-templates.github.io/webpack for documentation.
4 |
5 | const path = require('path')
6 |
7 | module.exports = {
8 | dev: {
9 |
10 | // Paths
11 | assetsSubDirectory: 'static',
12 | assetsPublicPath: '/',
13 | proxyTable: {},
14 |
15 | // Various Dev Server settings
16 | host: '10.8.149.94', // can be overwritten by process.env.HOST
17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
18 | autoOpenBrowser: false,
19 | errorOverlay: true,
20 | notifyOnErrors: true,
21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
22 |
23 | // Use Eslint Loader?
24 | // If true, your code will be linted during bundling and
25 | // linting errors and warnings will be shown in the console.
26 | useEslint: true,
27 | // If true, eslint errors and warnings will also be shown in the error overlay
28 | // in the browser.
29 | showEslintErrorsInOverlay: false,
30 |
31 | /**
32 | * Source Maps
33 | */
34 |
35 | // https://webpack.js.org/configuration/devtool/#development
36 | devtool: 'cheap-module-eval-source-map',
37 |
38 | // If you have problems debugging vue-files in devtools,
39 | // set this to false - it *may* help
40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting
41 | cacheBusting: true,
42 |
43 | cssSourceMap: true
44 | },
45 |
46 | build: {
47 | // Template for index.html
48 | index: path.resolve(__dirname, '../dist/index.html'),
49 |
50 | // Paths
51 | assetsRoot: path.resolve(__dirname, '../dist'),
52 | assetsSubDirectory: 'static',
53 | assetsPublicPath: '/',
54 |
55 | /**
56 | * Source Maps
57 | */
58 |
59 | productionSourceMap: true,
60 | // https://webpack.js.org/configuration/devtool/#production
61 | devtool: '#source-map',
62 |
63 | // Gzip off by default as many popular static hosts such as
64 | // Surge or Netlify already gzip all static assets for you.
65 | // Before setting to `true`, make sure to:
66 | // npm install --save-dev compression-webpack-plugin
67 | productionGzip: false,
68 | productionGzipExtensions: ['js', 'css'],
69 |
70 | // Run the build command with an extra argument to
71 | // View the bundle analyzer report after build finishes:
72 | // `npm run build --report`
73 | // Set to `true` or `false` to always turn it on or off
74 | bundleAnalyzerReport: process.env.npm_config_report
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | NODE_ENV: '"production"'
4 | }
5 |
--------------------------------------------------------------------------------
/config/test.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const devEnv = require('./dev.env')
4 |
5 | module.exports = merge(devEnv, {
6 | NODE_ENV: '"testing"'
7 | })
8 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vue-flowchart
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-flowchart",
3 | "version": "1.0.0",
4 | "description": "A Vue.js project",
5 | "author": "gahing",
6 | "private": true,
7 | "scripts": {
8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
9 | "start": "npm run dev",
10 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
11 | "e2e": "node test/e2e/runner.js",
12 | "test": "npm run unit && npm run e2e",
13 | "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
14 | "build": "node build/build.js"
15 | },
16 | "dependencies": {
17 | "element-ui": "^2.5.4",
18 | "vue": "^2.5.2",
19 | "vue-router": "^3.0.1"
20 | },
21 | "devDependencies": {
22 | "autoprefixer": "^7.1.2",
23 | "babel-core": "^6.22.1",
24 | "babel-eslint": "^8.2.1",
25 | "babel-helper-vue-jsx-merge-props": "^2.0.3",
26 | "babel-loader": "^7.1.1",
27 | "babel-plugin-istanbul": "^4.1.1",
28 | "babel-plugin-syntax-jsx": "^6.18.0",
29 | "babel-plugin-transform-runtime": "^6.22.0",
30 | "babel-plugin-transform-vue-jsx": "^3.5.0",
31 | "babel-polyfill": "^6.26.0",
32 | "babel-preset-env": "^1.3.2",
33 | "babel-preset-stage-2": "^6.22.0",
34 | "babel-register": "^6.22.0",
35 | "chai": "^4.1.2",
36 | "chalk": "^2.0.1",
37 | "chromedriver": "^2.27.2",
38 | "copy-webpack-plugin": "^4.0.1",
39 | "cross-env": "^5.0.1",
40 | "cross-spawn": "^5.0.1",
41 | "css-loader": "^0.28.0",
42 | "eslint": "^4.15.0",
43 | "eslint-config-standard": "^10.2.1",
44 | "eslint-friendly-formatter": "^3.0.0",
45 | "eslint-loader": "^1.7.1",
46 | "eslint-plugin-import": "^2.7.0",
47 | "eslint-plugin-node": "^5.2.0",
48 | "eslint-plugin-promise": "^3.4.0",
49 | "eslint-plugin-standard": "^3.0.1",
50 | "eslint-plugin-vue": "^4.0.0",
51 | "extract-text-webpack-plugin": "^3.0.0",
52 | "file-loader": "^1.1.4",
53 | "friendly-errors-webpack-plugin": "^1.6.1",
54 | "html-webpack-plugin": "^2.30.1",
55 | "inject-loader": "^3.0.0",
56 | "karma": "^1.4.1",
57 | "karma-coverage": "^1.1.1",
58 | "karma-mocha": "^1.3.0",
59 | "karma-phantomjs-launcher": "^1.0.2",
60 | "karma-phantomjs-shim": "^1.4.0",
61 | "karma-sinon-chai": "^1.3.1",
62 | "karma-sourcemap-loader": "^0.3.7",
63 | "karma-spec-reporter": "0.0.31",
64 | "karma-webpack": "^2.0.2",
65 | "mocha": "^3.2.0",
66 | "nightwatch": "^0.9.12",
67 | "node-notifier": "^5.1.2",
68 | "optimize-css-assets-webpack-plugin": "^3.2.0",
69 | "ora": "^1.2.0",
70 | "phantomjs-prebuilt": "^2.1.14",
71 | "portfinder": "^1.0.13",
72 | "postcss-import": "^11.0.0",
73 | "postcss-loader": "^2.0.8",
74 | "postcss-url": "^7.2.1",
75 | "rimraf": "^2.6.0",
76 | "selenium-server": "^3.0.1",
77 | "semver": "^5.3.0",
78 | "shelljs": "^0.7.6",
79 | "sinon": "^4.0.0",
80 | "sinon-chai": "^2.8.0",
81 | "uglifyjs-webpack-plugin": "^1.1.1",
82 | "url-loader": "^0.5.8",
83 | "vue-loader": "^13.3.0",
84 | "vue-style-loader": "^3.0.1",
85 | "vue-template-compiler": "^2.5.2",
86 | "webpack": "^3.6.0",
87 | "webpack-bundle-analyzer": "^3.6.0",
88 | "webpack-dev-server": "^2.11.3",
89 | "webpack-merge": "^4.1.0"
90 | },
91 | "engines": {
92 | "node": ">= 6.0.0",
93 | "npm": ">= 3.0.0"
94 | },
95 | "browserslist": [
96 | "> 1%",
97 | "last 2 versions",
98 | "not ie <= 8"
99 | ]
100 | }
101 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
15 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/francecil/vue-flowchart/89c614b682934d600125850d9095ee9da18eb0b2/src/assets/logo.png
--------------------------------------------------------------------------------
/src/components/FcCanvas.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
80 |
81 |
97 |
98 |
104 |
105 | {{ edgeDragging.prevEdge.label }}
106 |
107 |
108 |
109 |
122 |
123 |
129 |
130 |
131 |
132 |
457 |
576 |
--------------------------------------------------------------------------------
/src/components/FcConnector.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
135 |
--------------------------------------------------------------------------------
/src/components/FcEdge.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
100 |
128 |
--------------------------------------------------------------------------------
/src/components/FcEdgeLabel.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 | #
16 |
17 |
21 | ×
22 |
23 |
{{ edge.label }}
26 |
27 |
28 |
29 |
126 |
176 |
--------------------------------------------------------------------------------
/src/components/FcMagnet.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
59 |
--------------------------------------------------------------------------------
/src/components/FcNode.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
{{ node.name }}
17 |
18 |
19 |
25 |
32 |
33 |
34 |
35 |
41 |
48 |
49 |
50 |
51 |
57 | #
58 |
59 |
65 | ×
66 |
67 |
76 |
77 |
78 |
79 |
242 |
344 |
--------------------------------------------------------------------------------
/src/config/flowchart.js:
--------------------------------------------------------------------------------
1 |
2 | const constants = {
3 | htmlPrefix: 'fc',
4 | leftConnectorType: 'leftConnector',
5 | rightConnectorType: 'rightConnector',
6 | curvedStyle: 'curved',
7 | lineStyle: 'line',
8 | dragAnimationRepaint: 'repaint',
9 | dragAnimationShadow: 'shadow'
10 | }
11 | constants.canvasClass = constants.htmlPrefix + '-canvas'
12 | constants.selectedClass = constants.htmlPrefix + '-selected'
13 | constants.editClass = constants.htmlPrefix + '-edit'
14 | constants.activeClass = constants.htmlPrefix + '-active'
15 | constants.hoverClass = constants.htmlPrefix + '-hover'
16 | constants.draggingClass = constants.htmlPrefix + '-dragging'
17 | constants.edgeClass = constants.htmlPrefix + '-edge'
18 | constants.edgeLabelClass = constants.htmlPrefix + '-edge-label'
19 | constants.connectorClass = constants.htmlPrefix + '-connector'
20 | constants.magnetClass = constants.htmlPrefix + '-magnet'
21 | constants.nodeClass = constants.htmlPrefix + '-node'
22 | constants.nodeOverlayClass = constants.htmlPrefix + '-node-overlay'
23 | constants.leftConnectorClass = constants.htmlPrefix + '-' + constants.leftConnectorType + 's'
24 | constants.rightConnectorClass = constants.htmlPrefix + '-' + constants.rightConnectorType + 's'
25 | constants.canvasResizeThreshold = 300
26 | constants.canvasResizeStep = 200
27 | export default constants
28 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | // The Vue build version to load with the `import` command
2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
3 | import Vue from 'vue'
4 | import App from './App'
5 | import router from './router'
6 | import ElementUI from 'element-ui'
7 | import 'element-ui/lib/theme-chalk/index.css'
8 | Vue.config.productionTip = false
9 | Vue.use(ElementUI)
10 | /* eslint-disable no-new */
11 | new Vue({
12 | el: '#app',
13 | router,
14 | components: { App },
15 | template: ''
16 | })
17 |
--------------------------------------------------------------------------------
/src/pages/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
13 |
14 |
15 | Add Node
19 |
20 |
24 | Delete Selected
25 |
26 |
29 | Select All
30 |
31 |
34 | Copy
35 |
36 |
37 |
40 | Paste
41 |
42 |
46 | Undo
47 |
48 |
52 | Redo
53 |
54 |
55 |
66 |
67 |
68 |
69 |
70 |
71 |
250 |
251 |
252 |
278 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | Vue.use(Router)
5 |
6 | export default new Router({
7 | routes: [
8 | {
9 | path: '/',
10 | name: 'Index',
11 | component: () => import('@/pages/Index.vue')
12 | }
13 | ]
14 | })
15 |
--------------------------------------------------------------------------------
/src/service/edgedragging.js:
--------------------------------------------------------------------------------
1 | import flowchartConstants from '@/config/flowchart'
2 | import Modelvalidation from '@/service/modelvalidation'
3 | function EdgeDraggingFactory (store, initialState = {}) {
4 | this.isValidEdgeCallback = () => {
5 | return true
6 | }
7 | this.edgeAddCallback = () => {}
8 | this.store = store
9 | this.draggedEdgeSource = null
10 | for (let prop in initialState) {
11 | if (initialState.hasOwnProperty(prop) && this.hasOwnProperty(prop)) {
12 | this[prop] = initialState[prop]
13 | }
14 | }
15 | }
16 | // let dragImage = null
17 | // // 一个透明图像
18 | // const getDragImage = function () {
19 | // if (!dragImage) {
20 | // dragImage = new Image()
21 | // dragImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
22 | // dragImage.style.visibility = 'hidden'
23 | // }
24 | // return dragImage
25 | // }
26 | EdgeDraggingFactory.prototype.init = function () {
27 | let edgeDragging = {
28 | dragging: false,
29 | dragPoint1: null,
30 | dragPoint2: null,
31 | prevEdge: null
32 | }
33 | this.store.commit('UPDATE_EDGE_DRAGGING', edgeDragging)
34 | }
35 | EdgeDraggingFactory.prototype.dragstart = function (event, connector, type) {
36 | let swapConnector = null
37 | let prevEdge = null
38 | let edgeDragging = {}
39 | edgeDragging.dragging = true
40 | // 拖拽点为左类型,则删去原来连线并记录
41 | if (type === flowchartConstants.leftConnectorType) {
42 | for (let edge of this.store.state.model.edges) {
43 | // 选择第一个,当多个的话 拖拽结束会把前面的edge放到列表后面
44 | if (edge.destination === connector.id) {
45 | swapConnector = {
46 | id: edge.source
47 | }
48 | prevEdge = edge
49 | this.store.deleteEdge({
50 | edge
51 | })
52 | break
53 | }
54 | }
55 | }
56 |
57 | if (swapConnector) {
58 | this.draggedEdgeSource = swapConnector
59 | edgeDragging.dragPoint1 = this.store.getConnector(swapConnector.id)
60 | edgeDragging.prevEdge = prevEdge
61 | } else {
62 | this.draggedEdgeSource = connector
63 | edgeDragging.dragPoint1 = this.store.getConnector(connector.id)
64 | }
65 |
66 | edgeDragging.dragPoint2 = {
67 | x: event.clientX - this.store.getCanvasOffsetRelativeLeft(),
68 | y: event.clientY - this.store.getCanvasOffsetRelativeTop()
69 | }
70 | // try {
71 | // event.dataTransfer.setData('text', 'Just to support firefox')
72 | // } catch (error) {
73 | // console.warn('ie will report error:', error)
74 | // }
75 | // if (event.dataTransfer.setDragImage) {
76 | // event.dataTransfer.setDragImage(getDragImage(), 0, 0)
77 | // }
78 | this.store.commit('UPDATE_EDGE_DRAGGING', edgeDragging)
79 | }
80 |
81 | EdgeDraggingFactory.prototype.drop = async function (connector) {
82 | let edgeDragging = this.store.state.edgeDragging
83 | try {
84 | if (edgeDragging.dragging) {
85 | let invalid = false
86 | // 验证起始连线是否合法
87 | try {
88 | Modelvalidation.validateEdges(this.store.state.model.edges.concat([{
89 | source: this.draggedEdgeSource.id,
90 | destination: connector.id
91 | }]), this.store.state.model.nodes)
92 | } catch (error) {
93 | console.warn(error)
94 | if (error instanceof Modelvalidation.ModelvalidationError && edgeDragging.prevEdge) {
95 | invalid = true
96 | } else {
97 | throw error
98 | }
99 | }
100 | let isPushState = true
101 | let edge = {
102 | source: this.draggedEdgeSource.id,
103 | destination: connector.id
104 | }
105 | if (edgeDragging.prevEdge) {
106 | // 连线失败时,恢复原来连线
107 | if (invalid) {
108 | edge = edgeDragging.prevEdge
109 | } else {
110 | edge = Object.assign(edgeDragging.prevEdge, edge)
111 | }
112 | isPushState = false
113 | } else {
114 | try {
115 | edge.label = await this.edgeAddCallback()
116 | } catch (error) {
117 | throw error
118 | }
119 | }
120 |
121 | this.store.addEdge({edge, isPushState: isPushState})
122 | }
123 | } catch (error) {
124 |
125 | }
126 |
127 | this.init()
128 | }
129 | EdgeDraggingFactory.prototype.dragend = function (event) {
130 | let edgeDragging = this.store.state.edgeDragging
131 | if (edgeDragging.dragging) {
132 | if (edgeDragging.prevEdge) {
133 | this.store.addEdge({edge: edgeDragging.prevEdge})
134 | }
135 | this.init()
136 | }
137 | }
138 | EdgeDraggingFactory.prototype.dragover = function (event) {
139 | let edgeDragging = this.store.state.edgeDragging
140 | if (edgeDragging.dragging) {
141 | this.store.commit('UPDATE_EDGE_DRAGGING', {
142 | dragPoint2: {
143 | x: event.clientX - this.store.getCanvasOffsetRelativeLeft(),
144 | y: event.clientY - this.store.getCanvasOffsetRelativeTop()
145 | }
146 | })
147 | }
148 | }
149 | export default EdgeDraggingFactory
150 |
--------------------------------------------------------------------------------
/src/service/edgedrawing.js:
--------------------------------------------------------------------------------
1 | import flowchartConstants from '@/config/flowchart'
2 | function EdgedrawingService (flowchartConstants) {
3 | function computeEdgeTangentOffset (pt1, pt2) {
4 | return (pt2.y - pt1.y) / 2
5 | }
6 |
7 | function computeEdgeSourceTangent (pt1, pt2) {
8 | return {
9 | x: pt1.x,
10 | y: pt1.y + computeEdgeTangentOffset(pt1, pt2)
11 | }
12 | }
13 |
14 | function computeEdgeDestinationTangent (pt1, pt2) {
15 | return {
16 | x: pt2.x,
17 | y: pt2.y - computeEdgeTangentOffset(pt1, pt2)
18 | }
19 | }
20 | // 获取的D属性
21 | this.getEdgeDAttribute = function (pt1, pt2, style) {
22 | var dAddribute = 'M ' + pt1.x + ', ' + pt1.y + ' '
23 | if (style === flowchartConstants.curvedStyle) {
24 | var sourceTangent = computeEdgeSourceTangent(pt1, pt2)
25 | var destinationTangent = computeEdgeDestinationTangent(pt1, pt2)
26 | dAddribute += 'C ' + sourceTangent.x + ', ' + sourceTangent.y + ' ' + (destinationTangent.x - 50) + ', ' + destinationTangent.y + ' ' + pt2.x + ', ' + pt2.y
27 | } else {
28 | dAddribute += 'L ' + pt2.x + ', ' + pt2.y
29 | }
30 | return dAddribute
31 | }
32 | this.getEdgeCenter = function (pt1, pt2) {
33 | return {
34 | x: (pt1.x + pt2.x) / 2,
35 | y: (pt1.y + pt2.y) / 2
36 | }
37 | }
38 | }
39 | export default new EdgedrawingService(flowchartConstants)
40 |
--------------------------------------------------------------------------------
/src/service/modelvalidation.js:
--------------------------------------------------------------------------------
1 | import flowchartConstants from '@/config/flowchart'
2 | import { checkGraph } from '@/service/topsort'
3 | function Modelvalidation () {
4 | function ModelvalidationError (message) {
5 | this.message = message
6 | }
7 | ModelvalidationError.prototype = new Error()
8 | ModelvalidationError.prototype.name = 'ModelvalidationError'
9 | ModelvalidationError.prototype.constructor = ModelvalidationError
10 | this.ModelvalidationError = ModelvalidationError
11 |
12 | this.validateModel = function (model) {
13 | console.log('validateModel')
14 | if (model === null) return
15 | this.validateNodes(model.nodes)
16 | this._validateEdges(model.edges, model.nodes)
17 | return model
18 | }
19 |
20 | this.validateNodes = function (nodes) {
21 | let ids = []
22 | for (let node of nodes) {
23 | this.validateNode(node)
24 | if (ids.indexOf(node.id) !== -1) {
25 | throw new ModelvalidationError('Id not unique.')
26 | }
27 | ids.push(node.id)
28 | }
29 |
30 | let connectorIds = []
31 | for (let node of nodes) {
32 | for (let type in node.connectors) {
33 | this.validateConnector(node.connectors[type])
34 | if (connectorIds.indexOf(node.connectors[type].id) !== -1) {
35 | throw new ModelvalidationError('Id not unique.')
36 | }
37 | connectorIds.push(node.connectors[type].id)
38 | }
39 | }
40 | return nodes
41 | }
42 |
43 | this.validateNode = function (node) {
44 | if (node.id === undefined) {
45 | throw new ModelvalidationError('Id not valid.')
46 | }
47 | if (typeof node.name !== 'string') {
48 | throw new ModelvalidationError('Name not valid.')
49 | }
50 | if (typeof node.x !== 'number' || node.x < 0) {
51 | throw new ModelvalidationError('Coordinates not valid.')
52 | }
53 | if (typeof node.y !== 'number' || node.y < 0) {
54 | throw new ModelvalidationError('Coordinates not valid.')
55 | }
56 | if (typeof node.connectors !== 'object' && node.connectors !== undefined) {
57 | throw new ModelvalidationError('Connectors not valid.')
58 | }
59 | for (let type in node.connectors) {
60 | if (type !== flowchartConstants.leftConnectorType && type !== flowchartConstants.rightConnectorType) {
61 | throw new ModelvalidationError('Connectors not valid.')
62 | }
63 | this.validateConnector(node.connectors[type])
64 | }
65 | return node
66 | }
67 |
68 | this._validateEdges = function (edges, nodes) {
69 | edges.forEach((edge) => {
70 | this._validateEdge(edge, nodes)
71 | })
72 | // 验证重复边
73 | for (let i = 0; i < edges.length; i++) {
74 | for (let j = i + 1; j < edges.length; j++) {
75 | if ((edges[i].source === edges[j].source && edges[i].destination === edges[j].destination) || (edges[i].source === edges[j].destination && edges[i].destination === edges[j].source)) {
76 | throw new ModelvalidationError('Duplicated edge.')
77 | }
78 | }
79 | }
80 |
81 | if (!checkGraph({nodes: nodes, edges: edges})) {
82 | throw new ModelvalidationError('Graph has a circle.')
83 | }
84 |
85 | return edges
86 | }
87 |
88 | this.validateEdges = function (edges, nodes) {
89 | this.validateNodes(nodes)
90 | return this._validateEdges(edges, nodes)
91 | }
92 |
93 | this._validateEdge = function (edge, nodes) {
94 | if (edge.source === undefined) {
95 | throw new ModelvalidationError('Source not valid.')
96 | }
97 | if (edge.destination === undefined) {
98 | throw new ModelvalidationError('Destination not valid.')
99 | }
100 |
101 | if (edge.source === edge.destination) {
102 | throw new ModelvalidationError('Edge with same source and destination connectors.')
103 | }
104 | let sourceNode = nodes.filter(function (node) {
105 | return node.connectors && node.connectors[flowchartConstants.rightConnectorType] && node.connectors[flowchartConstants.rightConnectorType].id === edge.source
106 | })[0]
107 | if (!sourceNode) {
108 | throw new ModelvalidationError('Source not valid.')
109 | }
110 | let destinationNode = nodes.filter(function (node) {
111 | return node.connectors && node.connectors[flowchartConstants.leftConnectorType] && node.connectors[flowchartConstants.leftConnectorType].id === edge.destination
112 | })[0]
113 | if (!destinationNode) {
114 | throw new ModelvalidationError('Destination not valid.')
115 | }
116 | if (sourceNode === destinationNode) {
117 | throw new ModelvalidationError('Edge with same source and destination nodes.')
118 | }
119 | return edge
120 | }
121 |
122 | this.validateEdge = function (edge, nodes) {
123 | this.validateNodes(nodes)
124 | return this._validateEdge(edge, nodes)
125 | }
126 |
127 | this.validateConnector = function (connector) {
128 | if (connector.id === undefined) {
129 | throw new ModelvalidationError('Id not valid.')
130 | }
131 | return connector
132 | }
133 | }
134 | export default new Modelvalidation()
135 |
--------------------------------------------------------------------------------
/src/service/nodedragging.js:
--------------------------------------------------------------------------------
1 | import flowchartConstants from '@/config/flowchart'
2 | import UUIDjs from '@/utils/uuid'
3 | // require('../utils/setDragImage.polyfill.js')
4 |
5 | function NodeDraggingFactory (store, initialState = {}) {
6 | // 待拖拽点相关信息,dragover时使用
7 | this.dropNodeInfo = null
8 | this.nodeAddCallback = () => { }
9 | // 所有待拖拽点
10 | this.draggedNodes = []
11 | this.store = store
12 | this.automaticResize = true
13 | this.dragAnimation = flowchartConstants.dragAnimationRepaint
14 | for (let prop in initialState) {
15 | if (initialState.hasOwnProperty(prop) && this.hasOwnProperty(prop)) {
16 | this[prop] = initialState[prop]
17 | }
18 | }
19 | }
20 | const resizeCanvas = function (node, automaticResize, store) {
21 | // 调整canvas画板,仅会不断变大
22 | if (automaticResize && !store.isDropSource()) {
23 | let canvasOffset = store.state.canvasOffset
24 | let newOffset = {
25 | width: canvasOffset.width,
26 | height: canvasOffset.height
27 | }
28 | let hasChange = false
29 | if (canvasOffset.width < node.x + flowchartConstants.canvasResizeThreshold) {
30 | hasChange = true
31 | newOffset.width = canvasOffset.width + flowchartConstants.canvasResizeStep
32 | }
33 | if (canvasOffset.height < node.y + flowchartConstants.canvasResizeThreshold) {
34 | hasChange = true
35 | newOffset.height = canvasOffset.height + flowchartConstants.canvasResizeStep
36 | }
37 | if (hasChange) {
38 | store.commit('UPDATE_CANVAS_OFFSET', newOffset)
39 | }
40 | }
41 | }
42 | // let dragImage = null
43 | // // 一个透明图像
44 | // const getDragImage = function () {
45 | // if (!dragImage) {
46 | // dragImage = new Image()
47 | // dragImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
48 | // dragImage.style.visibility = 'hidden'
49 | // }
50 | // return dragImage
51 | // }
52 | const getDragOffset = function (event, dropNodeInfo, canvasOffset) {
53 | return {
54 | x: event.clientX - dropNodeInfo.eventPointOffset.x - canvasOffset.left - dropNodeInfo.node.x,
55 | y: event.clientY - dropNodeInfo.eventPointOffset.y - canvasOffset.top - dropNodeInfo.node.y
56 | }
57 | }
58 | NodeDraggingFactory.prototype.init = function () {
59 | this.dropNodeInfo = null
60 | this.draggedNodes.length = 0
61 | let nodeDragging = {
62 | dragging: false,
63 | downOffset: null
64 | }
65 | console.log('nodedragging init')
66 | this.store.commit('UPDATE_NODE_DRAGGING', nodeDragging)
67 | }
68 | NodeDraggingFactory.prototype.dragstart = function (event, node, eventPointOffset) {
69 | if (node.readonly) {
70 | return
71 | }
72 | let nodeDragging = {}
73 | nodeDragging.dragging = true
74 | nodeDragging.downOffset = {
75 | x: event.clientX,
76 | y: event.clientY
77 | }
78 | this.dropNodeInfo = {
79 | node,
80 | eventPointOffset
81 | }
82 | this.draggedNodes.length = 0
83 | // 被拖拽节点处于选择状态,故可能还有其他被选择节点,将会一起拖动
84 | if (this.store.isSelectedObject(node)) {
85 | this.draggedNodes = this.store.getSelectedNodes()
86 | } else {
87 | this.draggedNodes.push(node)
88 | }
89 | if (this.store.isDropSource()) {
90 | this.dropNodeInfo.isDropSource = true
91 | event.dataTransfer.setData('text', JSON.stringify(this.dropNodeInfo))
92 | // if (event.dataTransfer.setDragImage) {
93 | // event.dataTransfer.setDragImage(getDragImage(), 0, 0)
94 | // }
95 | }
96 | this.store.commit('UPDATE_NODE_DRAGGING', nodeDragging)
97 | }
98 |
99 | NodeDraggingFactory.prototype.drop = async function (event) {
100 | if (this.store.isDropSource()) {
101 | return
102 | }
103 | try {
104 | let dropNodeInfo = this.dropNodeInfo
105 | if (event.dataTransfer) {
106 | let dropNodeInfoStr = event.dataTransfer.getData('text')
107 | // 画板属于dropsource 或 dropNodeInfo信息不存在
108 | if (!dropNodeInfoStr) {
109 | return
110 | }
111 | dropNodeInfo = JSON.parse(dropNodeInfoStr)
112 | }
113 | // 原节点属于类型节点
114 | if (dropNodeInfo.isDropSource) {
115 | let name = await this.nodeAddCallback(dropNodeInfo.node.name)
116 | let newNode = Object.assign(dropNodeInfo.node, {
117 | id: UUIDjs.create('node'),
118 | name: name,
119 | x: event.clientX - dropNodeInfo.eventPointOffset.x - this.store.getCanvasOffsetRelativeLeft(),
120 | y: event.clientY - dropNodeInfo.eventPointOffset.y - this.store.getCanvasOffsetRelativeTop()
121 | })
122 | for (let type in newNode.connectors) {
123 | newNode.connectors[type].id = UUIDjs.create('connector')
124 | }
125 | this.store.addNode({ node: newNode, isPushState: true })
126 | } else {
127 | let downOffset = this.store.state.nodeDragging.downOffset
128 | // console.log(downOffset.x, event.clientX, downOffset.y, event.clientY)
129 | if (downOffset.x === event.clientX && downOffset.y === event.clientY) {
130 | // handle click
131 | setTimeout(() => {
132 | this.init()
133 | }, 0)
134 | return
135 | } else {
136 | // 节点属于目标画板节点,直接应用
137 | let offset = getDragOffset(event, this.dropNodeInfo, {
138 | left: this.store.getCanvasOffsetRelativeLeft(),
139 | top: this.store.getCanvasOffsetRelativeTop()
140 | })
141 | for (let node of this.draggedNodes) {
142 | let newNode = Object.assign(node, {
143 | x: node.x + offset.x,
144 | y: node.y + offset.y
145 | })
146 | this.store.updateNode({
147 | node: node,
148 | newNode,
149 | isPushState: true
150 | })
151 | }
152 | }
153 | }
154 | } catch (error) {
155 |
156 | }
157 |
158 | this.init()
159 | }
160 | NodeDraggingFactory.prototype.dragover = function (event) {
161 | // 画板属于dropsource 或 拖拽节点不在目标画板
162 | if (this.store.isDropSource() || !this.dropNodeInfo) {
163 | return
164 | }
165 | if (!this.store.state.nodeDragging.dragging) {
166 | return
167 | }
168 | let offset = getDragOffset(event, this.dropNodeInfo, {
169 | left: this.store.getCanvasOffsetRelativeLeft(),
170 | top: this.store.getCanvasOffsetRelativeTop()
171 | })
172 | for (let node of this.draggedNodes) {
173 | let newNode = Object.assign(node, {
174 | x: node.x + offset.x,
175 | y: node.y + offset.y
176 | })
177 | this.store.updateNode({
178 | node: node,
179 | newNode
180 | })
181 | resizeCanvas(newNode, this.automaticResize, this.store)
182 | }
183 | }
184 | export default NodeDraggingFactory
185 |
--------------------------------------------------------------------------------
/src/service/rectangleselect.js:
--------------------------------------------------------------------------------
1 | function RectangleSelectFactory (store, initialState = {}) {
2 | this.store = store
3 | this.selectRect = {
4 | x1: 0,
5 | x2: 0,
6 | y1: 0,
7 | y2: 0
8 | }
9 | this.startSelect = false
10 | for (let prop in initialState) {
11 | if (initialState.hasOwnProperty(prop) && this.hasOwnProperty(prop)) {
12 | this[prop] = initialState[prop]
13 | }
14 | }
15 | }
16 |
17 | function updateSelectRect (selectRect, store) {
18 | var x3 = Math.min(selectRect.x1, selectRect.x2)
19 | var x4 = Math.max(selectRect.x1, selectRect.x2)
20 | var y3 = Math.min(selectRect.y1, selectRect.y2)
21 | var y4 = Math.max(selectRect.y1, selectRect.y2)
22 | store.commit('UPDATE_RECTANGLE_SELECT', {
23 | left: x3,
24 | top: y3,
25 | width: x4 - x3,
26 | height: y4 - y3,
27 | visibility: 'visible'
28 | })
29 | }
30 | RectangleSelectFactory.prototype.init = function () {
31 | this.store.commit('UPDATE_RECTANGLE_SELECT', {
32 | left: 0,
33 | top: 0,
34 | width: 0,
35 | height: 0,
36 | visibility: 'hidden'
37 | })
38 | this.selectRect = {
39 | x1: 0,
40 | x2: 0,
41 | y1: 0,
42 | y2: 0
43 | }
44 | this.startSelect = false
45 | }
46 | RectangleSelectFactory.prototype.mousedown = function (e) {
47 | if (this.store.isEditable() && !e.ctrlKey && !e.metaKey && e.button === 0) {
48 | this.startSelect = true
49 | this.selectRect.x1 = Math.round(e.clientX - this.store.getCanvasOffsetRelativeLeft())
50 | this.selectRect.y1 = Math.round(e.clientY - this.store.getCanvasOffsetRelativeTop())
51 | }
52 | }
53 | RectangleSelectFactory.prototype.mousemove = function (e) {
54 | if (this.store.isEditable() && !e.ctrlKey && !e.metaKey && e.button === 0 && this.startSelect) {
55 | this.selectRect.x2 = Math.round(e.clientX - this.store.getCanvasOffsetRelativeLeft())
56 | this.selectRect.y2 = Math.round(e.clientY - this.store.getCanvasOffsetRelativeTop())
57 | updateSelectRect(this.selectRect, this.store)
58 | }
59 | }
60 | RectangleSelectFactory.prototype.mouseup = function (e) {
61 | if (this.store.isEditable() && !e.ctrlKey && !e.metaKey && e.button === 0 && this.startSelect) {
62 | // var rectBox = rectangleSelectService.selectElement.getBoundingClientRect()
63 | // rectBox.parentOffset = jquery(modelservice.getCanvasHtmlElement()).offset()
64 | this.store.selectAllInRect()
65 | this.init()
66 | }
67 | }
68 |
69 | export default RectangleSelectFactory
70 |
--------------------------------------------------------------------------------
/src/service/store.js:
--------------------------------------------------------------------------------
1 | import UUIDjs from '@/utils/uuid'
2 | const SET_MODEL = 'SET_MODEL'
3 | const UPDATE_NODE = 'UPDATE_NODE'
4 | const ADD_NODE = 'ADD_NODE'
5 | const DELETE_NODE = 'DELETE_NODE'
6 | const UPDATE_EDGE = 'UPDATE_EDGE'
7 | const ADD_EDGE = 'ADD_EDGE'
8 | const DELETE_EDGE = 'DELETE_EDGE'
9 | const UPDATE_CONNECTOR = 'UPDATE_CONNECTOR'
10 | const DELETE_CONNECTOR = 'DELETE_CONNECTOR'
11 | const UPDATE_CANVAS_OFFSET = 'UPDATE_CANVAS_OFFSET'
12 | const DESELECT_ALL = 'DESELECT_ALL'
13 | const DESELECT_OBJECT = 'DESELECT_OBJECT'
14 | const SELECT_OBJECT = 'SELECT_OBJECT'
15 | const SET_NODE_ELEMENT = 'SET_NODE_ELEMENT'
16 | const SET_CANVAS_CONTAINER = 'SET_CANVAS_CONTAINER'
17 | const UPDATE_NODE_DRAGGING = 'UPDATE_NODE_DRAGGING'
18 | const UPDATE_EDGE_DRAGGING = 'UPDATE_EDGE_DRAGGING'
19 | const UPDATE_RECTANGLE_SELECT = 'UPDATE_RECTANGLE_SELECT'
20 | const UPDATE_CLIPBOARD = 'UPDATE_CLIPBOARD'
21 | const INIT_SNAPSHOTS = 'INIT_SNAPSHOTS'
22 | const EXECUTE_SNAPSHOT = 'EXECUTE_SNAPSHOT'
23 | const UPDATE_SNAPSHOT_MODEL = 'UPDATE_SNAPSHOT_MODEL'
24 | const INIT_STORE = 'INIT_STORE'
25 | const CanvasStore = function (canvas, initialState = {}) {
26 | if (!canvas) {
27 | throw new Error('Canvas is required.')
28 | }
29 | this.canvas = canvas
30 |
31 | this.state = {
32 | model: null,
33 | dropTargetId: null,
34 | canvasOffset: {
35 | width: 0,
36 | height: 0
37 | },
38 | connectors: {},
39 | // 当前选中的元素,包括节点和连线
40 | selectedObjects: [],
41 | // node对应的dom节点
42 | nodeElements: {},
43 | canvasContainer: null,
44 | // 连线相关
45 | edgeDragging: {
46 | dragging: false,
47 | dragPoint1: null,
48 | dragPoint2: null,
49 | prevEdge: null
50 | },
51 | nodeDragging: {
52 | dragging: false,
53 | downOffset: null
54 | },
55 | // 选择区域
56 | rectangleSelect: {
57 | left: 0,
58 | top: 0,
59 | width: 0,
60 | height: 0,
61 | visibility: 'hidden'
62 | },
63 | // 剪切板
64 | clipboard: 'null',
65 | // 撤回重做历史栈
66 | snapshots: [],
67 | // 所处历史栈下标
68 | cursor: -1,
69 | MAX_HISTORY: 100
70 | }
71 |
72 | for (let prop in initialState) {
73 | if (initialState.hasOwnProperty(prop) && this.state.hasOwnProperty(prop)) {
74 | this.state[prop] = initialState[prop]
75 | }
76 | }
77 | }
78 | /** *************** mutations *****************/
79 | CanvasStore.prototype.mutations = {
80 | [SET_MODEL] (state, model) {
81 | state.model = model || {}
82 | },
83 | [UPDATE_CANVAS_OFFSET] (state, offset) {
84 | Object.assign(state.canvasOffset, offset)
85 | },
86 | [ADD_NODE] (state, node) {
87 | state.model.nodes.push(node)
88 | },
89 | [UPDATE_NODE] (state, {node, newNode}) {
90 | let index = state.model.nodes.indexOf(node)
91 | if (index !== -1) {
92 | Object.assign(state.model.nodes[index], newNode)
93 | }
94 | },
95 | [DELETE_NODE] (state, node) {
96 | let index = state.model.nodes.indexOf(node)
97 | if (index !== -1) {
98 | state.model.nodes.splice(index, 1)
99 | delete state.nodeElements[node.id]
100 | }
101 | },
102 | [UPDATE_EDGE] (state, {edge, newEdge}) {
103 | let index = state.model.edges.indexOf(edge)
104 | if (index !== -1) {
105 | Object.assign(state.model.edges[index], newEdge)
106 | }
107 | },
108 | [DELETE_EDGE] (state, edge) {
109 | let index = state.model.edges.indexOf(edge)
110 | if (index !== -1) {
111 | state.model.edges.splice(index, 1)
112 | }
113 | },
114 | [ADD_EDGE] (state, edge) {
115 | state.model.edges.push(edge)
116 | },
117 | [UPDATE_CONNECTOR] (state, {connectorId, x, y}) {
118 | if (state.connectors[connectorId]) {
119 | state.connectors[connectorId].x = x
120 | state.connectors[connectorId].y = y
121 | } else {
122 | this.canvas.$set(state.connectors, connectorId, {x, y})
123 | }
124 | },
125 | [DELETE_CONNECTOR] (state, connectorId) {
126 | delete state.connectors[connectorId]
127 | },
128 | [DESELECT_ALL] (state) {
129 | state.selectedObjects.splice(0, state.selectedObjects.length)
130 | },
131 | [SELECT_OBJECT] (state, object) {
132 | if (object && state.selectedObjects.indexOf(object) === -1) {
133 | state.selectedObjects.push(object)
134 | }
135 | },
136 | [DESELECT_OBJECT] (state, object) {
137 | let index = state.selectedObjects.indexOf(object)
138 | if (index !== -1) {
139 | state.selectedObjects.splice(index, 1)
140 | }
141 | },
142 | [SET_NODE_ELEMENT] (state, {nodeId, element}) {
143 | // console.log('SET_NODE_ELEMENT', nodeId)
144 | state.nodeElements[nodeId] = element
145 | },
146 | [SET_CANVAS_CONTAINER] (state, element) {
147 | state.canvasContainer = element
148 | },
149 | [UPDATE_NODE_DRAGGING] (state, nodeDragging) {
150 | Object.assign(state.nodeDragging, nodeDragging)
151 | },
152 | [UPDATE_EDGE_DRAGGING] (state, edgeDragging) {
153 | Object.assign(state.edgeDragging, edgeDragging)
154 | },
155 | [UPDATE_RECTANGLE_SELECT] (state, rectangleSelect) {
156 | Object.assign(state.rectangleSelect, rectangleSelect)
157 | },
158 | [UPDATE_CLIPBOARD] (state, str) {
159 | state.clipboard = str
160 | },
161 | [EXECUTE_SNAPSHOT] (state) {
162 | let snapshot = JSON.stringify(state.model)
163 | // 比如当前索引为3 进行新操作后 就需要把 snapshots 数组中索引>3的数据删掉
164 | state.snapshots.splice(state.cursor + 1, state.snapshots.length)
165 | state.snapshots.push(snapshot)
166 | state.cursor++
167 | while (state.snapshots.length > state.MAX_HISTORY) {
168 | state.snapshots.shift()
169 | state.cursor--
170 | }
171 | },
172 | [INIT_SNAPSHOTS] (state) {
173 | state.snapshots = []
174 | state.cursor = -1
175 | },
176 | [UPDATE_SNAPSHOT_MODEL] (state, newCursor) {
177 | state.cursor = newCursor
178 | this.state.model = JSON.parse(state.snapshots[newCursor])
179 | },
180 | [INIT_STORE] (state) {
181 | state.connectors = {}
182 | // 当前选中的元素,包括节点和连线
183 | state.selectedObjects = []
184 | // node对应的dom节点
185 | state.nodeElements = {}
186 | state.nodeDragging = {
187 | dragging: false,
188 | downOffset: null
189 | }
190 | // 连线相关
191 | state.edgeDragging = {
192 | dragging: false,
193 | dragPoint1: null,
194 | dragPoint2: null,
195 | prevEdge: null
196 | }
197 | // 选择区域
198 | state.rectangleSelect = {
199 | left: 0,
200 | top: 0,
201 | width: 0,
202 | height: 0,
203 | visibility: 'hidden'
204 | }
205 | // 剪切板
206 | state.clipboard = 'null'
207 | }
208 | }
209 | CanvasStore.prototype.commit = function (name, ...args) {
210 | // console.log(name, args)
211 | const mutations = this.mutations
212 | if (mutations[name]) {
213 | mutations[name].apply(this, [this.state].concat(args))
214 | } else {
215 | throw new Error(`Action not found: ${name}`)
216 | }
217 | }
218 | /** *************** getters *****************/
219 | // 预置节点画板
220 | CanvasStore.prototype.isDropSource = function () {
221 | return !!this.state.dropTargetId
222 | }
223 | // 当前画板为主画板
224 | CanvasStore.prototype.isEditable = function () {
225 | return !this.state.dropTargetId
226 | }
227 | // 当前元素是否被选中
228 | CanvasStore.prototype.isSelectedObject = function (object) {
229 | return this.state.selectedObjects.indexOf(object) !== -1
230 | }
231 | // 当前元素是否为可编辑状态
232 | CanvasStore.prototype.isEditObject = function (object) {
233 | return this.state.selectedObjects.length === 1 &&
234 | this.isSelectedObject(object)
235 | }
236 | CanvasStore.prototype.getConnector = function (id) {
237 | return this.state.connectors[id]
238 | }
239 | // 所有被选中的节点
240 | CanvasStore.prototype.getSelectedNodes = function () {
241 | return this.state.model.nodes ? this.state.model.nodes.filter((node) => this.isSelectedObject(node)) : []
242 | }
243 | // canvas的绝对位置,left,top值不受滚动条影响
244 | CanvasStore.prototype.getCanvasOffsetLeft = function () {
245 | return this.state.canvasContainer ? this.state.canvasContainer.getBoundingClientRect().left + this.state.canvasContainer.parentElement.scrollLeft : 0
246 | }
247 | CanvasStore.prototype.getCanvasOffsetTop = function () {
248 | return this.state.canvasContainer ? this.state.canvasContainer.getBoundingClientRect().top + this.state.canvasContainer.parentElement.scrollTop : 0
249 | }
250 | // canvas的相对位置,会偏小
251 | CanvasStore.prototype.getCanvasOffsetRelativeLeft = function () {
252 | return this.state.canvasContainer ? this.state.canvasContainer.getBoundingClientRect().left : 0
253 | }
254 | CanvasStore.prototype.getCanvasOffsetRelativeTop = function () {
255 | return this.state.canvasContainer ? this.state.canvasContainer.getBoundingClientRect().top : 0
256 | }
257 | /** *************** actions *****************/
258 | CanvasStore.prototype.initModel = function (model) {
259 | this.commit(INIT_STORE)
260 | this.commit(INIT_SNAPSHOTS)
261 | this.commit(SET_MODEL, model)
262 | this.commit(EXECUTE_SNAPSHOT)
263 | }
264 | CanvasStore.prototype.addNode = function ({node, isPushState}) {
265 | if (node.id === undefined) {
266 | node.id = UUIDjs.create('node')
267 | }
268 | if (node.x === undefined) {
269 | node.x = Math.round(Math.random() * 200 + 300)
270 | }
271 | if (node.y === undefined) {
272 | node.y = Math.round(Math.random() * 200 + 300)
273 | }
274 | this.commit(ADD_NODE, node)
275 | if (isPushState) {
276 | this.commit(EXECUTE_SNAPSHOT)
277 | }
278 | }
279 | CanvasStore.prototype.updateNode = function ({node, newNode, isPushState}) {
280 | this.commit(UPDATE_NODE, {node, newNode})
281 | if (isPushState) {
282 | this.commit(EXECUTE_SNAPSHOT)
283 | }
284 | }
285 | CanvasStore.prototype.deleteNode = function ({node, isPushState}) {
286 | this.commit(DELETE_NODE, node)
287 | this.commit(DESELECT_OBJECT, node)
288 | // 删除节点 同时删除连接点和相关连线
289 | if (node.connectors) {
290 | let connectorIds = []
291 | for (let type in node.connectors) {
292 | let connector = node.connectors[type]
293 | this.commit(DELETE_CONNECTOR, connector.id)
294 | connectorIds.push(connector.id)
295 | }
296 | for (let i = 0; i < this.state.model.edges.length;) {
297 | let edge = this.state.model.edges[i]
298 | if (connectorIds.indexOf(edge.source) !== -1 || connectorIds.indexOf(edge.destination) !== -1) {
299 | this.deleteEdge({
300 | edge,
301 | isPushState: false
302 | })
303 | } else {
304 | i++
305 | }
306 | }
307 | }
308 | if (isPushState) {
309 | this.commit(EXECUTE_SNAPSHOT)
310 | }
311 | }
312 | CanvasStore.prototype.addEdge = function ({edge, isPushState}) {
313 | this.commit(ADD_EDGE, edge)
314 | if (isPushState) {
315 | this.commit(EXECUTE_SNAPSHOT)
316 | }
317 | }
318 | CanvasStore.prototype.updateEdge = function ({edge, newEdge, isPushState}) {
319 | this.commit(UPDATE_EDGE, {edge, newEdge})
320 | if (isPushState) {
321 | this.commit(EXECUTE_SNAPSHOT)
322 | }
323 | }
324 | CanvasStore.prototype.deleteEdge = function ({edge, isPushState}) {
325 | this.commit(DELETE_EDGE, edge)
326 | this.commit(DESELECT_OBJECT, edge)
327 | if (isPushState) {
328 | this.commit(EXECUTE_SNAPSHOT)
329 | }
330 | }
331 | // 删除选中元素
332 | CanvasStore.prototype.deleteSelected = function (isPushState) {
333 | while (this.state.selectedObjects.length > 0) {
334 | let item = this.state.selectedObjects[0]
335 | if (item.id !== undefined) {
336 | this.deleteNode({
337 | node: item
338 | })
339 | } else {
340 | this.deleteEdge({
341 | edge: item
342 | })
343 | }
344 | }
345 | if (isPushState) {
346 | this.commit(EXECUTE_SNAPSHOT)
347 | }
348 | }
349 | // 更新选择元素列表
350 | CanvasStore.prototype.updateSelecctedObjects = function ({object, ctrlKey}) {
351 | if (ctrlKey) {
352 | this.toggleSelectedObject(object)
353 | } else {
354 | this.deselectAll()
355 | this.commit(SELECT_OBJECT, object)
356 | }
357 | }
358 | // 全选
359 | CanvasStore.prototype.selectAll = function () {
360 | this.deselectAll()
361 | for (let node of this.state.model.nodes) {
362 | if (!node.readonly) {
363 | this.commit(SELECT_OBJECT, node)
364 | }
365 | }
366 | for (let edge of this.state.model.edges) {
367 | this.commit(SELECT_OBJECT, edge)
368 | }
369 | }
370 | // 反选全部数据
371 | CanvasStore.prototype.deselectAll = function () {
372 | this.commit(DESELECT_ALL)
373 | }
374 | // 更新当前所选元素状态
375 | CanvasStore.prototype.toggleSelectedObject = function (object) {
376 | if (this.isSelectedObject(object)) {
377 | this.commit(DESELECT_OBJECT, object)
378 | } else {
379 | this.commit(SELECT_OBJECT, object)
380 | }
381 | }
382 | // 选中选择框中元素
383 | CanvasStore.prototype.selectAllInRect = function () {
384 | this.commit(DESELECT_ALL)
385 | let rectBox = {
386 | left: this.state.rectangleSelect.left,
387 | right: this.state.rectangleSelect.left + this.state.rectangleSelect.width,
388 | top: this.state.rectangleSelect.top,
389 | bottom: this.state.rectangleSelect.top + this.state.rectangleSelect.height
390 | }
391 | if (rectBox.left === rectBox.right && rectBox.top === rectBox.bottom) {
392 | // click handle
393 | return
394 | }
395 | let canvasLeft = this.getCanvasOffsetRelativeLeft()
396 | let canvasTop = this.getCanvasOffsetRelativeTop()
397 | for (let node of this.state.model.nodes) {
398 | let nodeElement = this.state.nodeElements[node.id]
399 | let nodeElementBox = nodeElement.getBoundingClientRect()
400 | if (!node.readonly) {
401 | let x = nodeElementBox.left + nodeElementBox.width / 2 - canvasLeft
402 | let y = nodeElementBox.top + nodeElementBox.height / 2 - canvasTop
403 | if (inRectBox(x, y, rectBox)) {
404 | this.commit(SELECT_OBJECT, node)
405 | }
406 | }
407 | }
408 | for (let edge of this.state.model.edges) {
409 | let start = this.getConnector(edge.source)
410 | let end = this.getConnector(edge.destination)
411 | let x = (start.x + end.x) / 2
412 | let y = (start.y + end.y) / 2
413 | if (inRectBox(x, y, rectBox)) {
414 | this.commit(SELECT_OBJECT, edge)
415 | }
416 | }
417 | }
418 | // 复制选中元素
419 | CanvasStore.prototype.copyData = function () {
420 | this.commit(UPDATE_CLIPBOARD, 'null')
421 | // 无选择节点 直接返回
422 | if (this.getSelectedNodes().length === 0) {
423 | return
424 | }
425 | let model = {
426 | nodes: [],
427 | edges: []
428 | }
429 | for (let item of this.state.selectedObjects) {
430 | if (item.id !== undefined) {
431 | model.nodes.push(item)
432 | } else {
433 | model.edges.push(item)
434 | }
435 | }
436 | this.commit(UPDATE_CLIPBOARD, JSON.stringify(model))
437 | }
438 | // 粘贴选中元素
439 | CanvasStore.prototype.pasteData = function () {
440 | let model = JSON.parse(this.state.clipboard)
441 | if (model) {
442 | // 修改元素
443 | for (let node of model.nodes) {
444 | node.x = node.x + 50
445 | node.y = node.y + 50
446 | node.id = UUIDjs.create('node')
447 | for (let type in node.connectors) {
448 | let tempId = node.connectors[type].id
449 | node.connectors[type].id = UUIDjs.create('connector')
450 | for (let edge of model.edges) {
451 | if (edge.source === tempId) {
452 | edge.source = node.connectors[type].id
453 | }
454 | if (edge.destination === tempId) {
455 | edge.destination = node.connectors[type].id
456 | }
457 | }
458 | }
459 | this.addNode({node})
460 | }
461 | for (let edge of model.edges) {
462 | this.addEdge({edge})
463 | }
464 | }
465 | }
466 | // 撤回
467 | CanvasStore.prototype.undo = function () {
468 | if (this.state.cursor === 0) {
469 | return false
470 | }
471 | this.commit(INIT_STORE)
472 | this.commit(UPDATE_SNAPSHOT_MODEL, this.state.cursor - 1)
473 | }
474 | // 重做
475 | CanvasStore.prototype.redo = function () {
476 | if (this.state.cursor === this.state.snapshots.length - 1) {
477 | return false
478 | }
479 | this.commit(INIT_STORE)
480 | this.commit(UPDATE_SNAPSHOT_MODEL, this.state.cursor + 1)
481 | }
482 | CanvasStore.prototype.hasUndo = function () {
483 | return this.state.cursor > 0
484 | }
485 | CanvasStore.prototype.hasRedo = function () {
486 | return this.state.cursor < this.state.snapshots.length - 1
487 | }
488 | /** ******** utils *********/
489 | function inRectBox (x, y, rectBox) {
490 | // console.log(x, y, rectBox)
491 | return x >= rectBox.left && x <= rectBox.right &&
492 | y >= rectBox.top && y <= rectBox.bottom
493 | }
494 | export default CanvasStore
495 |
--------------------------------------------------------------------------------
/src/service/topsort.js:
--------------------------------------------------------------------------------
1 | import flowchartConstants from '@/config/flowchart'
2 | /**
3 | * Kahn算法 检测有向图是否存在环
4 | * @param {*} graph
5 | */
6 | export function checkGraph (graph) {
7 | // 计算出所有顶点的入度
8 | let adjacentList = {}
9 | graph.nodes.forEach(function (node) {
10 | adjacentList[node.id] = {incoming: 0, outgoing: []}
11 | })
12 | graph.edges.forEach(function (edge) {
13 | let sourceNode = graph.nodes.filter(function (node) {
14 | return node.connectors && node.connectors[flowchartConstants.rightConnectorType] && node.connectors[flowchartConstants.rightConnectorType].id === edge.source
15 | })[0]
16 | let destinationNode = graph.nodes.filter(function (node) {
17 | return node.connectors && node.connectors[flowchartConstants.leftConnectorType] && node.connectors[flowchartConstants.leftConnectorType].id === edge.destination
18 | })[0]
19 |
20 | adjacentList[sourceNode.id].outgoing.push(destinationNode.id)
21 | adjacentList[destinationNode.id].incoming++
22 | })
23 | // 保存出队元素
24 | let orderedNodes = []
25 | // 将所有入度为0的点加入到一个队列
26 | let sourceNodes = []
27 | for (let id in adjacentList) {
28 | if (adjacentList[id].incoming === 0) {
29 | sourceNodes.push(id)
30 | }
31 | }
32 | // 记录访问过的顶点个数
33 | let count = 0
34 | while (sourceNodes.length !== 0) {
35 | // 将队头元素pop
36 | let sourceNode = sourceNodes.pop()
37 | count++
38 | // 将这个点的所有相邻点的入度减一,如果某个相邻点的入度减小为0,则将这个相邻点加入到队列中
39 | for (let destinationNode of adjacentList[sourceNode].outgoing) {
40 | if (--adjacentList[destinationNode].incoming === 0) {
41 | sourceNodes.push(destinationNode)
42 | }
43 | }
44 | orderedNodes.push(sourceNode)
45 | }
46 | return count === graph.nodes.length
47 | }
48 |
--------------------------------------------------------------------------------
/src/utils/setDragImage.polyfill.js:
--------------------------------------------------------------------------------
1 | if (typeof DataTransfer.prototype.setDragImage !== 'function') {
2 | DataTransfer.prototype.setDragImage = function (image, offsetX, offsetY) {
3 | var randomDraggingClassName,
4 | dragStylesCSS,
5 | dragStylesEl,
6 | headEl,
7 | parentFn,
8 | eventTarget
9 |
10 | // generate a random class name that will be added to the element
11 | randomDraggingClassName = 'setdragimage-ie-dragging-' + Math.round(Math.random() * Math.pow(10, 5)) + '-' + Date.now()
12 |
13 | // prepare the rules for the random class
14 | dragStylesCSS = [
15 | '.' + randomDraggingClassName,
16 | '{',
17 | 'background: url("' + image.src + '") no-repeat #fff 0 0 !important;',
18 | 'width: ' + image.width + 'px !important;',
19 | 'height: ' + image.height + 'px !important;',
20 | 'text-indent: -9999px !important;',
21 | 'border: 0 !important;',
22 | 'outline: 0 !important;',
23 | '}',
24 | '.' + randomDraggingClassName + ' * {',
25 | 'display: none !important;',
26 | '}'
27 | ]
28 | // create the element and add it to the head of the page
29 | dragStylesEl = document.createElement('style')
30 | dragStylesEl.innerText = dragStylesCSS.join('')
31 | headEl = document.getElementsByTagName('head')[0]
32 | headEl.appendChild(dragStylesEl)
33 |
34 | /*
35 | since we can't get the target element over which the drag start event occurred
36 | (because the `this` represents the DataTransfer object and not the element),
37 | we will walk through the parents of the current functions until we find one
38 | whose first argument is a drag event
39 | */
40 | parentFn = DataTransfer.prototype.setDragImage.caller
41 | while (!(parentFn.arguments[0] instanceof DragEvent)) {
42 | parentFn = parentFn.caller
43 | }
44 |
45 | // then, we get the target element from the event (event.target)
46 | eventTarget = parentFn.arguments[0].target
47 | // and add the class we prepared to it
48 | eventTarget.classList.add(randomDraggingClassName)
49 |
50 | /* immediately after adding the class, we remove it. in this way the browser will
51 | have time to make a snapshot and use it just so it looks like the drag element */
52 | setTimeout(function () {
53 | // remove the styles
54 | headEl.removeChild(dragStylesEl)
55 | // remove the class
56 | eventTarget.classList.remove(randomDraggingClassName)
57 | }, 0)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/utils/uuid.js:
--------------------------------------------------------------------------------
1 | function UUIDjs () {
2 |
3 | }
4 | UUIDjs.create = function (pre) {
5 | return (pre ? pre + '-' : '') + (Math.random() + '').substr(2, 8) + '-' + ((new Date()).getTime() + '').substr(-4, 4)
6 | }
7 | export default UUIDjs
8 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/francecil/vue-flowchart/89c614b682934d600125850d9095ee9da18eb0b2/static/.gitkeep
--------------------------------------------------------------------------------
/test/e2e/custom-assertions/elementCount.js:
--------------------------------------------------------------------------------
1 | // A custom Nightwatch assertion.
2 | // The assertion name is the filename.
3 | // Example usage:
4 | //
5 | // browser.assert.elementCount(selector, count)
6 | //
7 | // For more information on custom assertions see:
8 | // http://nightwatchjs.org/guide#writing-custom-assertions
9 |
10 | exports.assertion = function (selector, count) {
11 | this.message = 'Testing if element <' + selector + '> has count: ' + count
12 | this.expected = count
13 | this.pass = function (val) {
14 | return val === this.expected
15 | }
16 | this.value = function (res) {
17 | return res.value
18 | }
19 | this.command = function (cb) {
20 | var self = this
21 | return this.api.execute(function (selector) {
22 | return document.querySelectorAll(selector).length
23 | }, [selector], function (res) {
24 | cb.call(self, res)
25 | })
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/test/e2e/nightwatch.conf.js:
--------------------------------------------------------------------------------
1 | require('babel-register')
2 | var config = require('../../config')
3 |
4 | // http://nightwatchjs.org/gettingstarted#settings-file
5 | module.exports = {
6 | src_folders: ['test/e2e/specs'],
7 | output_folder: 'test/e2e/reports',
8 | custom_assertions_path: ['test/e2e/custom-assertions'],
9 |
10 | selenium: {
11 | start_process: true,
12 | server_path: require('selenium-server').path,
13 | host: '127.0.0.1',
14 | port: 4444,
15 | cli_args: {
16 | 'webdriver.chrome.driver': require('chromedriver').path
17 | }
18 | },
19 |
20 | test_settings: {
21 | default: {
22 | selenium_port: 4444,
23 | selenium_host: 'localhost',
24 | silent: true,
25 | globals: {
26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
27 | }
28 | },
29 |
30 | chrome: {
31 | desiredCapabilities: {
32 | browserName: 'chrome',
33 | javascriptEnabled: true,
34 | acceptSslCerts: true
35 | }
36 | },
37 |
38 | firefox: {
39 | desiredCapabilities: {
40 | browserName: 'firefox',
41 | javascriptEnabled: true,
42 | acceptSslCerts: true
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/test/e2e/runner.js:
--------------------------------------------------------------------------------
1 | // 1. start the dev server using production config
2 | process.env.NODE_ENV = 'testing'
3 |
4 | const webpack = require('webpack')
5 | const DevServer = require('webpack-dev-server')
6 |
7 | const webpackConfig = require('../../build/webpack.prod.conf')
8 | const devConfigPromise = require('../../build/webpack.dev.conf')
9 |
10 | let server
11 |
12 | devConfigPromise.then(devConfig => {
13 | const devServerOptions = devConfig.devServer
14 | const compiler = webpack(webpackConfig)
15 | server = new DevServer(compiler, devServerOptions)
16 | const port = devServerOptions.port
17 | const host = devServerOptions.host
18 | return server.listen(port, host)
19 | })
20 | .then(() => {
21 | // 2. run the nightwatch test suite against it
22 | // to run in additional browsers:
23 | // 1. add an entry in test/e2e/nightwatch.conf.js under "test_settings"
24 | // 2. add it to the --env flag below
25 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
26 | // For more information on Nightwatch's config file, see
27 | // http://nightwatchjs.org/guide#settings-file
28 | let opts = process.argv.slice(2)
29 | if (opts.indexOf('--config') === -1) {
30 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
31 | }
32 | if (opts.indexOf('--env') === -1) {
33 | opts = opts.concat(['--env', 'chrome'])
34 | }
35 |
36 | const spawn = require('cross-spawn')
37 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })
38 |
39 | runner.on('exit', function (code) {
40 | server.close()
41 | process.exit(code)
42 | })
43 |
44 | runner.on('error', function (err) {
45 | server.close()
46 | throw err
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/test/e2e/specs/test.js:
--------------------------------------------------------------------------------
1 | // For authoring Nightwatch tests, see
2 | // http://nightwatchjs.org/guide#usage
3 |
4 | module.exports = {
5 | 'default e2e tests': function (browser) {
6 | // automatically uses dev Server port from /config.index.js
7 | // default: http://localhost:8080
8 | // see nightwatch.conf.js
9 | const devServer = browser.globals.devServerURL
10 |
11 | browser
12 | .url(devServer)
13 | .waitForElementVisible('#app', 5000)
14 | .assert.elementPresent('.hello')
15 | .assert.containsText('h1', 'Welcome to Your Vue.js App')
16 | .assert.elementCount('img', 1)
17 | .end()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "expect": true,
7 | "sinon": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/test/unit/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | Vue.config.productionTip = false
4 |
5 | // require all test files (files that ends with .spec.js)
6 | const testsContext = require.context('./specs', true, /\.spec$/)
7 | testsContext.keys().forEach(testsContext)
8 |
9 | // require all src files except main.js for coverage.
10 | // you can also change this to match only the subset of files that
11 | // you want coverage for.
12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
13 | srcContext.keys().forEach(srcContext)
14 |
--------------------------------------------------------------------------------
/test/unit/karma.conf.js:
--------------------------------------------------------------------------------
1 | // This is a karma config file. For more details see
2 | // http://karma-runner.github.io/0.13/config/configuration-file.html
3 | // we are also using it with karma-webpack
4 | // https://github.com/webpack/karma-webpack
5 |
6 | var webpackConfig = require('../../build/webpack.test.conf')
7 |
8 | module.exports = function karmaConfig (config) {
9 | config.set({
10 | // to run in additional browsers:
11 | // 1. install corresponding karma launcher
12 | // http://karma-runner.github.io/0.13/config/browsers.html
13 | // 2. add it to the `browsers` array below.
14 | browsers: ['PhantomJS'],
15 | frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
16 | reporters: ['spec', 'coverage'],
17 | files: ['./index.js'],
18 | preprocessors: {
19 | './index.js': ['webpack', 'sourcemap']
20 | },
21 | webpack: webpackConfig,
22 | webpackMiddleware: {
23 | noInfo: true
24 | },
25 | coverageReporter: {
26 | dir: './coverage',
27 | reporters: [
28 | { type: 'lcov', subdir: '.' },
29 | { type: 'text-summary' }
30 | ]
31 | }
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/test/unit/specs/HelloWorld.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import HelloWorld from '@/components/HelloWorld'
3 |
4 | describe('HelloWorld.vue', () => {
5 | it('should render correct contents', () => {
6 | const Constructor = Vue.extend(HelloWorld)
7 | const vm = new Constructor().$mount()
8 | expect(vm.$el.querySelector('.hello h1').textContent)
9 | .to.equal('Welcome to Your Vue.js App')
10 | })
11 | })
12 |
--------------------------------------------------------------------------------