├── .babelrc
├── .eslintrc.js
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── build
├── MyHtmlwebpackPlugin.js
├── webpack.base.config.js
├── webpack.config.dev.js
└── webpack.config.prod.js
├── dist
├── css
│ ├── cytoscape-context-menus.css
│ └── fce.1.0.0.min.css
├── demo.js
├── images
│ ├── choice.png
│ ├── create_file.png
│ ├── download.png
│ ├── eventbar.png
│ ├── import.png
│ ├── line-dotted.png
│ ├── rectangle.png
│ ├── redo.png
│ ├── remove.png
│ ├── round.png
│ ├── rounded_rectangle.png
│ ├── save.png
│ └── undo.png
├── index.html
├── js
│ ├── fce.1.0.0.min.js
│ └── lib
│ │ ├── cytoscape-context-menus.js
│ │ ├── cytoscape-edge-bend-editing.js
│ │ ├── cytoscape-edgehandles.js
│ │ ├── cytoscape-grid-guide.js
│ │ ├── cytoscape-node-resize.js
│ │ ├── cytoscape-undo-redo.js
│ │ ├── cytoscape-view-utilities.js
│ │ ├── cytoscape.cjs.js
│ │ ├── cytoscape.js
│ │ ├── cytoscape.min.js
│ │ ├── jquery.js
│ │ └── konva.min.js
└── plantuml
│ ├── example.html
│ ├── example.html.bak
│ ├── index.html
│ ├── jquery.js
│ ├── jquery_plantuml.js
│ ├── jquery_plantuml.zip
│ └── rawdeflate.js
├── example
└── img
│ ├── demo1.gif
│ ├── demo2.gif
│ ├── demo3.gif
│ └── demo4.gif
├── manifest.json
├── package.json
├── src
├── css
│ └── default.scss
├── images
│ ├── animation.png
│ └── icon
│ │ ├── auto_play.png
│ │ ├── fce-zoom-dom-plus.png
│ │ ├── fce-zoom-dom-reduce.png
│ │ ├── line-solid.png
│ │ ├── manual_play.png
│ │ └── pointer.png
├── index.html
└── js
│ ├── Listeners
│ ├── cytoscapeListener.js
│ ├── navbarsListener.js
│ └── zoomListener.js
│ ├── core
│ ├── Dom.js
│ ├── Listener.js
│ ├── Navbar.js
│ ├── Navbars.js
│ ├── Toolbar.js
│ ├── Toolbar
│ │ ├── Animation
│ │ │ ├── Auto.js
│ │ │ ├── Manual.js
│ │ │ └── index.js
│ │ └── index.js
│ ├── Toolbars.js
│ ├── Zoom.js
│ ├── basebar.js
│ └── basebars.js
│ ├── cytoscapeHelper.js
│ ├── cytoscapeHelper.js.bak
│ ├── defaultOptions.js
│ ├── index.js
│ ├── lib.js
│ └── utils
│ ├── cy.js
│ └── index.js
├── static
├── css
│ └── cytoscape-context-menus.css
├── demo.js
├── images
│ ├── choice.png
│ ├── create_file.png
│ ├── download.png
│ ├── eventbar.png
│ ├── import.png
│ ├── line-dotted.png
│ ├── rectangle.png
│ ├── redo.png
│ ├── remove.png
│ ├── round.png
│ ├── rounded_rectangle.png
│ ├── save.png
│ └── undo.png
└── js
│ └── lib
│ ├── cytoscape-context-menus.js
│ ├── cytoscape-edge-bend-editing.js
│ ├── cytoscape-edgehandles.js
│ ├── cytoscape-grid-guide.js
│ ├── cytoscape-node-resize.js
│ ├── cytoscape-undo-redo.js
│ ├── cytoscape-view-utilities.js
│ ├── cytoscape.cjs.js
│ ├── cytoscape.js
│ ├── cytoscape.min.js
│ ├── jquery.js
│ └── konva.min.js
└── test.html
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env",
5 | {
6 | "targets": {
7 | "browsers": ["IE >= 8"]
8 | },
9 | "useBuiltIns": true
10 | }
11 | ]
12 | ]
13 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "babel-eslint",
4 | env: {
5 | browser: true,
6 | node: true,
7 | commonjs: true,
8 | es6: true
9 | },
10 | extends: "eslint:recommended",
11 | parserOptions: {
12 | sourceType: "module"
13 | },
14 | plugins: ["html", "standard", "promise"],
15 | rules: {
16 | semi: ["error", "always"],
17 | "no-console": "off"
18 | },
19 | globals: {
20 | document: true,
21 | navigator: true,
22 | window: true,
23 | _: true,
24 | $: true
25 | }
26 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.ignoreLimitWarning": true
3 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 tongling
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # flow-chart-editor 流程设计器
2 |
3 | ## 背景
4 |
5 | 最近做的项目中有流程设计这个功能,且要求设计器具有可嵌套子流程功能,业务比较复杂,当时没有找到合适的设计器,最后选型 cytoscapejs,用 vue 架构了一个流程设计器,不过相对而言太复杂,业务特征太明显,故计划年后做出版较为通用的流程设计器,且增加演示动画功能(待完善)。本文是对目前所做设计器的一个展示。后续还会继续完善。
6 |
7 | [](https://www.npmjs.com/package/flow-chart-editor)
8 | [](https://npmjs.org/package/flow-chart-editor)
9 | 
10 | 
11 | [](https://gitter.im/tlzzu/flow-chart-editor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
12 |
13 | 基于[cytoscape.js](https://github.com/cytoscape/cytoscape.js)的流程设计器。[演示文档 Demo](https://tlzzu.github.io/flow-chart-editor/dist/index.html)。已纳入 SoDiao 豪华套餐。(_^▽^_)
14 |
15 | 优点如下:
16 |
17 | ```
18 | 1. 支持实/虚线、连线弯曲、撤销重做、放大缩小;
19 | 2. 可导出 json/png/jpg 文档;
20 | 3. toolbar自定义;
21 | 4. 允许在流程中嵌套**子流程**;
22 | 5. 支持只读、设计两种模式(敬请期待);
23 | 6. 支持设置**流程动画**(敬请期待);
24 | 7. ……后续再完善……
25 | ```
26 |
27 | > 在此,感谢 easyicon.net 提供的图标。
28 |
29 | [1. 预览-Preview](#1-预览-preview)
30 |
31 | [2. 安装使用-Install](#2-安装使用-install)
32 |
33 | [3. 二次开发-Build](#3-二次开发-build)
34 |
35 | [4. 文档-Document](#4-文档-document)
36 |
37 | [5. 依赖-Dependencies](#5-依赖-dependencies)
38 |
39 | [6. 错误提交-Bug](#6-错误提交-bug)
40 |
41 | [7. 捐赠-Donation](#6-捐赠-donation)
42 |
43 | [8. 许可证-LICENSE](#7-许可证-license)
44 |
45 | ## 1. 预览-Preview
46 |
47 | 预览效果如下:
48 | 
49 | 
50 | 
51 | 
52 |
53 | ## 2. 安装使用-Install
54 |
55 | ### npm 安装
56 |
57 | 推荐使用 npm 安装
58 |
59 | ```
60 | npm i flow-chart-editor -S
61 | ```
62 |
63 | 可在页面中引用
64 |
65 | ```
66 | import FCE from "flow-chart-editor";
67 |
68 | var fce=new FCE({
69 | el: document.getElementById("fce"),//初始化节点
70 | rightMenus:[{
71 | id: "id_alert",
72 | content: "弹出窗",
73 | tooltipText: "弹出窗",
74 | selector: "node,edge",//当在node,edge元素上右键时才显示
75 | onClickFunction: function(evt) {//点击后触发事件
76 | var target = evt.target || evt.cyTarget;
77 | alert('弹出信息!');
78 | },
79 | hasTrailingDivider: true
80 | }],
81 | toolbars: [{//自定义toolbar
82 | name: "rectangle",//节点名称
83 | icon: "images/rectangle.png",//toolbar的图片
84 | className: "",//自定义样式
85 | title: "矩形",//title值
86 | exec(evt, clickType, obj) {//选中该节点后,点击编辑区域后被触发事件
87 | const label = prompt("请输入节点名称:"),
88 | data = { id: new Date().getTime(), label: label };
89 | if (!label) return;
90 | if (clickType === "node") {
91 | data.parent = obj.id;
92 | }
93 | this.addNode(data, "rectangle");
94 | }
95 | },
96 | "animation"]//这里FCE内置的一种制作流程动画组件
97 | });
98 | ```
99 |
100 | ### 脚本引用
101 |
102 | ```
103 |
104 |
105 |
106 | flow-chart-editor流程设计器
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
155 |
156 |
157 | ```
158 |
159 | ## 3. 二次开发-Build
160 |
161 | 二次开发前请确保已经安装`node`及`webpack`。在控制台中执行 `npm run `,其中:
162 |
163 | * `dev`:开发模式,执行后可直接访问[http://localhost:9110/](http://localhost:9110/)直接调试。
164 | * `build`:执行打包,dist 中的文件会重新打包。
165 |
166 | ## 4. 文档-Document
167 |
168 | ```
169 | //todo 稍后完善。
170 | ```
171 |
172 | ## 5. 依赖-Dependencies
173 |
174 | [jquery ^3.2.1](https://github.com/jquery/jquery)
175 |
176 | [cytoscape ^3.2.0](https://github.com/cytoscape/cytoscape.js)
177 |
178 | ## 6. 错误提交-Bug
179 |
180 | 1. 可邮件至[dd@sodiao.org](mailto://dd@sodiao.org/);
181 | 2. 可以在 github 中的[ISS](https://github.com/tlzzu/flow-chart-editor/issues)中提交;
182 |
183 | ## 7. 捐赠-Donation
184 |
185 | 表示您对本项目的支持
186 | 
187 |
188 | ## 8. 许可证-LICENSE
189 |
190 | MIT.
191 |
192 | 欢迎下载适用!
193 |
--------------------------------------------------------------------------------
/build/MyHtmlwebpackPlugin.js:
--------------------------------------------------------------------------------
1 | function MyHtmlwebpackPlugin(options) {
2 | this.options = options;
3 | }
4 |
5 | MyHtmlwebpackPlugin.prototype.apply = function(compiler) {
6 | const js = this.options.paths.js,
7 | css = this.options.paths.css;
8 | compiler.plugin("compilation", function(compilation, options) {
9 | compilation.plugin("html-webpack-plugin-before-html-processing", function(
10 | htmlPluginData,
11 | callback
12 | ) {
13 | if (css && css.length) {
14 | for (let i = css.length - 1; i >= 0; i--) {
15 | htmlPluginData.assets.css.unshift(css[i]);
16 | }
17 | }
18 | if (js && js.length) {
19 | for (let i = js.length - 1; i >= 0; i--) {
20 | htmlPluginData.assets.js.unshift(js[i]);
21 | }
22 | }
23 | callback(null, htmlPluginData);
24 | });
25 | });
26 | };
27 |
28 | module.exports = MyHtmlwebpackPlugin;
--------------------------------------------------------------------------------
/build/webpack.base.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | var CopyWebpackPlugin = require('copy-webpack-plugin');
6 | const version = '1.0.0';
7 | module.exports = {
8 | entry: {
9 | index: './src/js/index.js',
10 | },
11 | devtool: 'eval-source-map', // 'source-map', // http://www.jianshu.com/p/42e11515c10f
12 | output: {
13 | filename: 'js/fce.' + version + '.min.js',
14 | path: path.join(__dirname, '../dist/'),
15 | },
16 | module: {
17 | rules: [{
18 | test: /\.js$/,
19 | exclude: path.join(__dirname, 'node_modules'),
20 | use: {
21 | loader: 'babel-loader',
22 | query: {
23 | presets: ['es2015'],
24 | },
25 | },
26 | include: path.join(__dirname, 'src'),
27 | },
28 | {
29 | // css / sass / scss loader for webpack
30 | test: /\.(css|sass|scss)$/,
31 | use: ExtractTextPlugin.extract({
32 | use: ['css-loader?minimize', 'sass-loader?minimize'],
33 | }),
34 | },
35 | {
36 | test: /\.less$/,
37 | use: ['css-loader', 'sass-loader'],
38 | },
39 | {
40 | test: /\.(png|svg|jpg|jpeg|gif)$/,
41 | loader: 'url-loader',
42 | query: { mimetype: 'image/png' },
43 | },
44 | {
45 | test: /\.html$/,
46 | use: [{ loader: 'html-loader' }],
47 | },
48 | {
49 | test: /\.(woff|woff2|eot|ttf|otf)$/,
50 | use: [{ loader: 'file-loader?limit=1024&name=fonts/[name].[ext]' }],
51 | },
52 | {
53 | test: /\.handlebars$/,
54 | use: [{ loader: 'handlebars-loader' }],
55 | },
56 | ],
57 | // postLoaders: [{
58 | // test: /\.js$/,
59 | // loaders: ['es3ify-loader'],
60 | // }, ],
61 | },
62 | node: {
63 | fs: 'empty',
64 | },
65 | plugins: [
66 | new ExtractTextPlugin({
67 | filename: 'css/fce.' + version + '.min.css',
68 | disable: false,
69 | allChunks: true,
70 | }),
71 | new HtmlWebpackPlugin({
72 | filename: path.join(__dirname, '..', '/dist/index.html'),
73 | template: './src/index.html',
74 | inject: 'head', // 在head中插入js
75 | chunks: ['index'],
76 | hash: true,
77 | }),
78 | new UglifyJSPlugin({
79 | ie8: true,
80 | compress: {
81 | warnings: false,
82 | drop_console: false,
83 | },
84 | }),
85 | new CopyWebpackPlugin([{
86 | from: path.resolve(__dirname, '../static'),
87 | to: '',
88 | ignore: ['.*'],
89 | }, ]),
90 | ],
91 | };
--------------------------------------------------------------------------------
/build/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const baseWebpackConfig = require("./webpack.base.config.js");
2 | const path = require("path");
3 | const webpack = require("webpack");
4 | const merge = require("webpack-merge");
5 |
6 | module.exports = merge(baseWebpackConfig, {
7 | devServer: {
8 | port: 9110,
9 | contentBase: path.join(__dirname, "dist"),
10 | publicPath: "/",
11 | compress: true,
12 | host: "localhost"
13 | },
14 | plugins: [
15 | new webpack.DefinePlugin({
16 | process: {
17 | env: {
18 | NODE_ENV: '"dev"'
19 | }
20 | }
21 | }),
22 | new webpack.ProvidePlugin({
23 | $: "jquery", // jquery
24 | jQuery: "jquery",
25 | "window.jQuery": "jquery",
26 | _: "lodash" // lodash
27 | })
28 | ]
29 | });
--------------------------------------------------------------------------------
/build/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | const baseWebpackConfig = require('./webpack.base.config.js');
2 |
3 | const webpack = require('webpack');
4 | const merge = require('webpack-merge');
5 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
6 |
7 | const MyHtmlwebpackPlugin = require('./MyHtmlwebpackPlugin.js');
8 |
9 | //设置排除项
10 | baseWebpackConfig.externals = {
11 | jquery: 'jQuery',
12 | konva: 'konva',
13 | cytoscape: 'cytoscape',
14 | 'cytoscape-node-resize': 'nodeResize',
15 | 'cytoscape-grid-guide': 'gridGuide',
16 | 'cytoscape-edgehandles': 'edgehandles',
17 | 'cytoscape-context-menus': 'contextMenus',
18 | 'cytoscape-edge-bend-editing': 'edgeBendEditing',
19 | 'cytoscape-undo-redo': 'undoRedo',
20 | 'cytoscape-view-utilities': 'viewUtilities',
21 | };
22 | module.exports = merge(baseWebpackConfig, {
23 | plugins: [
24 | new webpack.DefinePlugin({
25 | process: {
26 | env: {
27 | NODE_ENV: '"prod"',
28 | },
29 | },
30 | }),
31 | //扩展插入外部脚本
32 | new MyHtmlwebpackPlugin({
33 | paths: {
34 | css: ['css/cytoscape-context-menus.css'],
35 | js: [
36 | 'js/lib/cytoscape.js',
37 | 'js/lib/jquery.js',
38 | 'js/lib/konva.min.js',
39 | 'js/lib/cytoscape-node-resize.js',
40 | 'js/lib/cytoscape-grid-guide.js',
41 | 'js/lib/cytoscape-edgehandles.js',
42 | 'js/lib/cytoscape-context-menus.js',
43 | 'js/lib/cytoscape-edge-bend-editing.js',
44 | 'js/lib/cytoscape-undo-redo.js',
45 | 'js/lib/cytoscape-view-utilities.js',
46 | ],
47 | },
48 | }),
49 | new OptimizeCssAssetsPlugin({
50 | cssProcessorOptions: {
51 | safe: true,
52 | },
53 | }),
54 | // new UglifyJSPlugin({
55 | // ie8: true,
56 | // compress: {
57 | // warnings: false,
58 | // drop_console: false
59 | // }
60 | // }),
61 | // new HtmlWebpackPlugin({
62 | // filename: path.join(__dirname, "..", "/dist/index.html"),
63 | // template: "./src/index.html",
64 | // inject: "head", // 在head中插入js
65 | // chunks: ["index"]
66 | // }),
67 | new webpack.BannerPlugin(
68 | 'flow-chart-editor \nauthor: tlzzu@outlook.com \ncreatetime: ' + new Date().toUTCString(),
69 | ), // js中的备注
70 | ],
71 | });
--------------------------------------------------------------------------------
/dist/css/cytoscape-context-menus.css:
--------------------------------------------------------------------------------
1 | .cy-context-menus-cxt-menu{display:none;z-index:1000;position:absolute;border:1px solid #a0a0a0;padding:0;margin:0;width:auto}.cy-context-menus-cxt-menuitem{display:block;z-index:1000;width:100%;padding:3px 20px;position:relative;margin:0;background-color:#f8f8f8;font-weight:400;font-size:12px;white-space:nowrap;border:0;text-align:left}.cy-context-menus-cxt-menuitem:enabled{color:#000}.cy-context-menus-ctx-operation:focus{outline:none}.cy-context-menus-cxt-menuitem:hover{color:#fff;text-decoration:none;background-color:#0b9bcd;background-image:none;cursor:pointer}.cy-context-menus-cxt-menuitem[content]:before{content:attr(content)}.cy-context-menus-divider{border-bottom:1px solid #a0a0a0}
--------------------------------------------------------------------------------
/dist/css/fce.1.0.0.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * flow-chart-editor
3 | * author: tlzzu@outlook.com
4 | * createtime: Wed, 25 Apr 2018 07:52:17 GMT
5 | */.fce{position:relative;width:100%;height:100%;color:#000;border-top:1px solid #c8c8c8;border-left:1px solid #c8c8c8;border-right:1px solid #c8c8c8}.fce .canvas-pointer canvas{cursor:default}.fce .canvas-line canvas{cursor:crosshair}.fce *{margin:0;padding:0;border:0}.fce .fce-footer{z-index:1000;position:absolute;border-top:1px solid #c8c8c8;border-bottom:1px solid #c8c8c8;display:flex;flex:1;width:100%;bottom:0;background-color:#f4f4f4;line-height:20px;font-size:12px}.fce .fce-searcher{z-index:1000;position:absolute;border-top:1px solid #c8c8c8;border-bottom:1px solid #c8c8c8;z-index:1001;right:0;top:0;padding-right:5px;width:150px;font-size:13px;line-height:35px;border:none}.fce .fce-searcher input{-webkit-appearance:none;background-color:#fff;border-radius:4px;font-size:inherit;border:1px solid #d8dce5;box-sizing:border-box;color:#5a5e66;text-align:start;display:inline-block;font:400 13.3333px Arial;height:25px;margin-right:10px;line-height:1;outline:0;padding:0 10px;transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.fce .fce-searcher input:hover{border-color:#b4bccc}.fce .fce-toolbars{z-index:1000;position:absolute;border-top:1px solid #c8c8c8;border-bottom:1px solid #c8c8c8;display:flex;flex:1;width:100%;top:0;background-color:#dfdfdf;line-height:35px;border-top:0}.fce .fce-toolbars .fce-tool-bars .fce-base-bar{float:left;line-height:35px;padding:0 10px}.fce .fce-toolbars .fce-tool-bars .fce-base-bar img{width:20px;height:20px;vertical-align:middle}.fce .fce-toolbars .fce-tool-bars .fce-base-bar .fce-tool-bar-ext{display:none}.fce .fce-toolbars .fce-tool-bars .fce-base-bar .fce-tool-bar-temp{z-index:1000;position:absolute;border-top:1px solid #c8c8c8;border-bottom:1px solid #c8c8c8;display:block;min-width:40px;visibility:hidden}.fce .fce-toolbars .fce-tool-bars .fce-base-bar:hover{background-color:#add7f6}.fce .fce-toolbars .fce-tool-bars .fce-tool-bar-active{background-color:#5fb8fb;border-bottom:none}.fce .fce-toolbars .fce-tool-bars .fce-tool-bar-active .fce-tool-bar-ext{z-index:1000;position:absolute;border-top:1px solid #c8c8c8;border-bottom:1px solid #c8c8c8;background-color:#dfdfdf;border-bottom:none;border-left:1px solid #c8c8c8;border-right:1px solid #c8c8c8;display:block;line-height:35px;min-width:40px}.fce .fce-toolbars .fce-tool-bars .fce-tool-bar-active:hover{background-color:#5fb8fb}.fce .fce-base-bars .fce-base-bar{cursor:pointer}.fce .fce-tool-bar-ext .bar-auto_play{float:left;line-height:35px;padding:0 10px}.fce .fce-navbar{z-index:1000;position:absolute;bottom:30px;left:10px;height:200px;width:25px;border:1px solid #c8c8c8;border-radius:4px;text-align:center;background-color:#dfdfdf}.fce .fce-navbar .fce-zoom-dom{margin:0 auto}.fce .fce-navbar .fce-zoom-dom .fce-zoom-dom-background{background-color:#fff;margin:0 auto;width:2px;height:100%}.fce .fce-navbar .fce-zoom-dom .fce-zoom-dom-default{background-color:#fff;text-align:center;height:2px;position:absolute}.fce .fce-navbar .fce-zoom-dom .fce-zoom-dom-active{border-radius:10px;height:10px;background-color:#fff;position:absolute}.fce .fce-navbar .fce-zoom-dom .fce-zoom-dom-default{cursor:pointer}.fce .fce-navbar .fce-zoom-dom .fce-zoom-dom-reduce{width:12px;height:12px;cursor:pointer;margin-top:3px}.fce .fce-navbar .fce-zoom-dom .fce-zoom-dom-plus{width:12px;height:12px;cursor:pointer;margin-bottom:8px}.fce .fce-navbar .fce-zoom-dom .fce-zoom-dom-plus img,.fce .fce-navbar .fce-zoom-dom .fce-zoom-dom-reduce img{width:12px;height:12px;cursor:pointer;display:inline-block}.fce .fce-navbar .fce-nav-bars{position:absolute;bottom:0;margin:0 auto;width:25px}.fce .fce-navbar .fce-nav-bars .fce-nav-bar{width:100%;line-height:25px}.fce .fce-navbar .fce-nav-bars .fce-nav-bar img{width:15px;height:15px;vertical-align:middle}.fce .fce-navbar .fce-nav-bars .fce-nav-bar:hover{background-color:#add7f6}.fce .fce-navbar .fce-nav-bars .fce-nav-bar-active,.fce .fce-navbar .fce-nav-bars .fce-nav-bar-active:hover{background-color:#5fb8fb}.fce .fce-cy{width:100%;height:100%}
--------------------------------------------------------------------------------
/dist/demo.js:
--------------------------------------------------------------------------------
1 | var fce;
2 | window.onload = function() {
3 | fce = new FCE({
4 | el: document.getElementById('fce'),
5 | rightMenus: [{
6 | id: "id_alert",
7 | content: "弹出窗",
8 | tooltipText: "弹出窗",
9 | selector: "node,edge", //当在node,edge元素上右键时才显示
10 | onClickFunction: function(evt) { //点击后触发事件
11 | var target = evt.target || evt.cyTarget;
12 | alert('弹出信息!');
13 | },
14 | hasTrailingDivider: true
15 | }],
16 | toolbars: [{
17 | name: 'rectangle',
18 | icon: 'images/rectangle.png',
19 | className: '',
20 | title: '矩形',
21 | exec: function(evt, clickType, obj) {
22 | const label = prompt('请输入节点名称:'),
23 | data = { id: new Date().getTime(), label: label };
24 | if (!label) return;
25 | if (clickType === 'node') {
26 | data.parent = obj.id;
27 | }
28 | this.addNode(data, 'rectangle');
29 | },
30 | },
31 | {
32 | name: 'rounded_rectangle',
33 | icon: 'images/rounded_rectangle.png',
34 | className: '',
35 | title: '圆角矩形',
36 | exec: function(evt, clickType, obj) {
37 | const label = prompt('请输入节点名称:'),
38 | data = { id: new Date().getTime(), label: label };
39 | if (!label) return;
40 | if (clickType === 'node') {
41 | data.parent = obj.id;
42 | }
43 | this.addNode(data, 'roundrectangle');
44 | },
45 | },
46 | {
47 | name: 'choice',
48 | icon: 'images/choice.png',
49 | className: '',
50 | title: '菱形',
51 | exec: function(evt, clickType, obj) {
52 | const label = prompt('请输入节点名称:'),
53 | data = { id: new Date().getTime(), label: label };
54 | if (!label) return;
55 | if (clickType === 'node') {
56 | data.parent = obj.id;
57 | }
58 | this.addNode(data, 'diamond');
59 | },
60 | },
61 | {
62 | name: 'round',
63 | icon: 'images/round.png',
64 | className: '',
65 | title: '圆形',
66 | exec: function(evt, clickType, obj) {
67 | const label = prompt('请输入节点名称:'),
68 | data = { id: new Date().getTime(), label: label };
69 | if (!label) return;
70 | if (clickType === 'node') {
71 | data.parent = obj.id;
72 | }
73 | this.addNode(data, 'ellipse');
74 | },
75 | },
76 | {
77 | name: 'download-json',
78 | icon: 'images/download.png',
79 | className: '',
80 | title: '下载json文件',
81 | click: function(bar) {
82 | this.exportFile('json', '导出JSON文件');
83 | bar.cancelActive(); //取消自身选中
84 | },
85 | },
86 | {
87 | name: 'download-png',
88 | icon: 'images/download.png',
89 | className: '',
90 | title: '下载png文件',
91 | click: function(bar) {
92 | this.exportFile('png');
93 | bar.cancelActive(); //取消自身选中
94 | },
95 | },
96 | {
97 | name: 'download-jpg',
98 | icon: 'images/download.png',
99 | className: '',
100 | title: '下载jpg文件',
101 | click: function(bar) {
102 | this.exportFile('jpg');
103 | bar.cancelActive(); //取消自身选中
104 | },
105 | },
106 |
107 | {
108 | name: 'import',
109 | icon: 'images/import.png',
110 | className: '',
111 | title: '导入JSON文件',
112 | click: function(bar) {
113 | bar.cancelActive(); //取消自身选中
114 | var file = document.createElement('input'),
115 | self = this;
116 | file.setAttribute('type', 'file');
117 | file.onchange = function(evt) {
118 | var target = evt.target;
119 | if (target.files && target.files.length) {
120 | var fileInfo = target.files[0],
121 | name = fileInfo.name;
122 | if (!name.toLowerCase().endsWith('.json')) {
123 | alert('上传文件类型不符合要求!');
124 | } else {
125 | var reader = new FileReader();
126 | reader.onload = function(evt) {
127 | var json = JSON.parse(evt.target.result.toString());
128 | self.import(json);
129 | };
130 | reader.readAsText(fileInfo);
131 | }
132 | }
133 | };
134 | file.click();
135 | // this.import(json);
136 | // bar.cancelActive(); //取消自身选中
137 | },
138 | },
139 | 'animation',
140 | ],
141 | });
142 | fce.addListener('add_click', function() {
143 | debugger;
144 | console.log('编辑器被点击!');
145 | });
146 | fce.addListener('context_menus_rename', function(evt, clickType, data) {
147 | const label = prompt('请输入节点新名称:', data.label);
148 | if (label) {
149 | data.label = label;
150 | this.rename(data);
151 | }
152 | });
153 | fce.addListener('context_menus_remove', function(evt, clickType, data) {
154 | if (confirm('您确定要删除该节点吗?')) {
155 | debugger;
156 | this.remove(data.id);
157 | }
158 | });
159 | };
160 |
161 | // var fce
162 | // window.onload = function() {
163 | // fce = new FCE({
164 | // rightMenu: [{//右键菜单
165 |
166 | // }],
167 | // toolbars: [{
168 | // //不写默认使用fce自带的render方法
169 | // render: function() {
170 | // return document.createElement('div')
171 | // },
172 | // icon: {
173 | // src: "img/xxx.png",
174 | // width: 12,
175 | // height: 12,
176 | // },
177 | // class: '', //样式
178 |
179 | // fce: null, //这里是fce的指针
180 | // id: 'point',
181 | // title: "指针",
182 | // onclick: function() {
183 | // //这里的this是当前bar
184 | // }
185 | // }]
186 | // })
187 | // window.fce = fce
188 | // }
189 |
190 | // var bar = fce.getToolbarById('id') //根据id获取组件
191 | // bar.isShow() //true/false
192 | // bar.hide()
193 | // bar.show()
194 | // bar.addClass()
195 | // bar.removeClass() //空则为移除所有样式
196 | // //可以通过fire触发某事件,通过fce.on绑定某事件
197 | // fce.on('click', function() {
198 | // //绑定事件
199 | // })
--------------------------------------------------------------------------------
/dist/images/choice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/choice.png
--------------------------------------------------------------------------------
/dist/images/create_file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/create_file.png
--------------------------------------------------------------------------------
/dist/images/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/download.png
--------------------------------------------------------------------------------
/dist/images/eventbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/eventbar.png
--------------------------------------------------------------------------------
/dist/images/import.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/import.png
--------------------------------------------------------------------------------
/dist/images/line-dotted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/line-dotted.png
--------------------------------------------------------------------------------
/dist/images/rectangle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/rectangle.png
--------------------------------------------------------------------------------
/dist/images/redo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/redo.png
--------------------------------------------------------------------------------
/dist/images/remove.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/remove.png
--------------------------------------------------------------------------------
/dist/images/round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/round.png
--------------------------------------------------------------------------------
/dist/images/rounded_rectangle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/rounded_rectangle.png
--------------------------------------------------------------------------------
/dist/images/save.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/save.png
--------------------------------------------------------------------------------
/dist/images/undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/images/undo.png
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | flow-chart-editor 流程设计器
9 |
23 |
24 |
25 |
26 | flow-chart-editor(FCE) 流程设计器
27 |
28 |
注意:
29 |
30 | - 允许在流程中嵌套子流程;
31 | - 支持只读、设计两种模式(敬请期待);
32 | - 支持设置流程动画(敬请期待);
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/dist/js/lib/cytoscape-context-menus.js:
--------------------------------------------------------------------------------
1 | ;(function(){ 'use strict';
2 |
3 | var $ = typeof jQuery === typeof undefined ? null : jQuery;
4 |
5 | var register = function( cytoscape, $ ){
6 |
7 | if( !cytoscape ){ return; } // can't register if cytoscape unspecified
8 |
9 | var defaults = {
10 | // List of initial menu items
11 | menuItems: [
12 | /*
13 | {
14 | id: 'remove',
15 | content: 'remove',
16 | tooltipText: 'remove',
17 | selector: 'node, edge',
18 | onClickFunction: function () {
19 | console.log('remove element');
20 | },
21 | hasTrailingDivider: true
22 | },
23 | {
24 | id: 'hide',
25 | content: 'hide',
26 | tooltipText: 'remove',
27 | selector: 'node, edge',
28 | onClickFunction: function () {
29 | console.log('hide element');
30 | },
31 | disabled: true
32 | }*/
33 | ],
34 | // css classes that menu items will have
35 | menuItemClasses: [
36 | // add class names to this list
37 | ],
38 | // css classes that context menu will have
39 | contextMenuClasses: [
40 | // add class names to this list
41 | ]
42 | };
43 |
44 | var eventCyTapStart; // The event to be binded on tap start
45 |
46 | // To initialize with options.
47 | cytoscape('core', 'contextMenus', function (opts) {
48 | var cy = this;
49 |
50 | // Initilize scratch pad
51 | if (!cy.scratch('cycontextmenus')) {
52 | cy.scratch('cycontextmenus', {});
53 | }
54 |
55 | var options = getScratchProp('options');
56 | var $cxtMenu = getScratchProp('cxtMenu');
57 | var menuItemCSSClass = 'cy-context-menus-cxt-menuitem';
58 | var dividerCSSClass = 'cy-context-menus-divider';
59 |
60 | // Merge default options with the ones coming from parameter
61 | function extend(defaults, options) {
62 | var obj = {};
63 |
64 | for (var i in defaults) {
65 | obj[i] = defaults[i];
66 | }
67 |
68 | for (var i in options) {
69 | obj[i] = options[i];
70 | }
71 |
72 | return obj;
73 | };
74 |
75 | function getScratchProp(propname) {
76 | return cy.scratch('cycontextmenus')[propname];
77 | };
78 |
79 | function setScratchProp(propname, value) {
80 | cy.scratch('cycontextmenus')[propname] = value;
81 | };
82 |
83 | function preventDefaultContextTap() {
84 | $(".cy-context-menus-cxt-menu").contextmenu( function() {
85 | return false;
86 | });
87 | }
88 |
89 | // Get string representation of css classes
90 | function getMenuItemClassStr(classes, hasTrailingDivider) {
91 | var str = getClassStr(classes);
92 |
93 | str += ' ' + menuItemCSSClass;
94 |
95 | if(hasTrailingDivider) {
96 | str += ' ' + dividerCSSClass;
97 | }
98 |
99 | return str;
100 | }
101 |
102 | // Get string representation of css classes
103 | function getClassStr(classes) {
104 | var str = '';
105 |
106 | for( var i = 0; i < classes.length; i++ ) {
107 | var className = classes[i];
108 | str += className;
109 | if(i !== classes.length - 1) {
110 | str += ' ';
111 | }
112 | }
113 |
114 | return str;
115 | }
116 |
117 | function displayComponent($component) {
118 | $component.css('display', 'block');
119 | }
120 |
121 | function hideComponent($component) {
122 | $component.css('display', 'none');
123 | }
124 |
125 | function hideMenuItemComponents() {
126 | $cxtMenu.children().css('display', 'none');
127 | }
128 |
129 | function bindOnClickFunction($component, onClickFcn) {
130 | var callOnClickFcn;
131 |
132 | $component.on('click', callOnClickFcn = function() {
133 | onClickFcn(getScratchProp('currentCyEvent'));
134 | });
135 |
136 | $component.data('call-on-click-function', callOnClickFcn);
137 | }
138 |
139 | function bindCyCxttap($component, selector, coreAsWell) {
140 | function _cxtfcn(event) {
141 | setScratchProp('currentCyEvent', event);
142 | adjustCxtMenu(event); // adjust the position of context menu
143 | if ($component.data('show')) {
144 | // Now we have a visible element display context menu if it is not visible
145 | if (!$cxtMenu.is(':visible')) {
146 | displayComponent($cxtMenu);
147 | }
148 | // anyVisibleChild indicates if there is any visible child of context menu if not do not show the context menu
149 | setScratchProp('anyVisibleChild', true);// there is visible child
150 | displayComponent($component); // display the component
151 | }
152 |
153 | // If there is no visible element hide the context menu as well(If it is visible)
154 | if (!getScratchProp('anyVisibleChild') && $cxtMenu.is(':visible')) {
155 | hideComponent($cxtMenu);
156 | }
157 | }
158 |
159 | var cxtfcn;
160 | var cxtCoreFcn;
161 |
162 | if(coreAsWell) {
163 | cy.on('cxttap', cxtCoreFcn = function(event) {
164 | var target = event.target || event.cyTarget;
165 | if( target != cy ) {
166 | return;
167 | }
168 |
169 | _cxtfcn(event);
170 | });
171 | }
172 |
173 | if(selector) {
174 | cy.on('cxttap', selector, cxtfcn = function(event) {
175 | _cxtfcn(event);
176 | });
177 | }
178 |
179 | // Bind the event to menu item to be able to remove it back
180 | $component.data('cy-context-menus-cxtfcn', cxtfcn);
181 | $component.data('cy-context-menus-cxtcorefcn', cxtCoreFcn);
182 | }
183 |
184 | function bindCyEvents() {
185 | cy.on('tapstart', eventCyTapStart = function(){
186 | hideComponent($cxtMenu);
187 | setScratchProp('cxtMenuPosition', undefined);
188 | setScratchProp('currentCyEvent', undefined);
189 | });
190 | }
191 |
192 | function performBindings($component, onClickFcn, selector, coreAsWell) {
193 | bindOnClickFunction($component, onClickFcn);
194 | bindCyCxttap($component, selector, coreAsWell);
195 | }
196 |
197 | // Adjusts context menu if necessary
198 | function adjustCxtMenu(event) {
199 | var currentCxtMenuPosition = getScratchProp('cxtMenuPosition');
200 | var cyPos = event.position || event.cyPosition;
201 |
202 | if( currentCxtMenuPosition != cyPos ) {
203 | hideMenuItemComponents();
204 | setScratchProp('anyVisibleChild', false);// we hide all children there is no visible child remaining
205 | setScratchProp('cxtMenuPosition', cyPos);
206 |
207 | var containerPos = $(cy.container()).offset();
208 | var renderedPos = event.renderedPosition || event.cyRenderedPosition;
209 |
210 | var left = containerPos.left + renderedPos.x;
211 | var top = containerPos.top + renderedPos.y;
212 |
213 | $cxtMenu.css('left', left);
214 | $cxtMenu.css('top', top);
215 | }
216 | }
217 |
218 | function createAndAppendMenuItemComponents(menuItems) {
219 | for (var i = 0; i < menuItems.length; i++) {
220 | createAndAppendMenuItemComponent(menuItems[i]);
221 | }
222 | }
223 |
224 | function createAndAppendMenuItemComponent(menuItem) {
225 | // Create and append menu item
226 | var $menuItemComponent = createMenuItemComponent(menuItem);
227 | appendComponentToCxtMenu($menuItemComponent);
228 |
229 | performBindings($menuItemComponent, menuItem.onClickFunction, menuItem.selector, menuItem.coreAsWell);
230 | }//insertComponentBeforeExistingItem(component, existingItemID)
231 |
232 | function createAndInsertMenuItemComponentBeforeExistingComponent(menuItem, existingComponentID) {
233 | // Create and insert menu item
234 | var $menuItemComponent = createMenuItemComponent(menuItem);
235 | insertComponentBeforeExistingItem($menuItemComponent, existingComponentID);
236 |
237 | performBindings($menuItemComponent, menuItem.onClickFunction, menuItem.selector, menuItem.coreAsWell);
238 | }
239 |
240 | // create cxtMenu and append it to body
241 | function createAndAppendCxtMenuComponent() {
242 | var classes = getClassStr(options.contextMenuClasses);
243 | // classes += ' cy-context-menus-cxt-menu';
244 | $cxtMenu = $('');
245 | $cxtMenu.addClass('cy-context-menus-cxt-menu');
246 | setScratchProp('cxtMenu', $cxtMenu);
247 |
248 | $('body').append($cxtMenu);
249 | return $cxtMenu;
250 | }
251 |
252 | // Creates a menu item as an html component
253 | function createMenuItemComponent(item) {
254 | var classStr = getMenuItemClassStr(options.menuItemClasses, item.hasTrailingDivider);
255 | var itemStr = '';
266 | }
267 | else{
268 | itemStr += '>' + '
' + item.content + '';
271 | };
272 |
273 | var $menuItemComponent = $(itemStr);
274 |
275 | $menuItemComponent.data('selector', item.selector);
276 | $menuItemComponent.data('on-click-function', item.onClickFunction);
277 | $menuItemComponent.data('show', (typeof(item.show) === 'undefined' || item.show));
278 | return $menuItemComponent;
279 | }
280 |
281 | // Appends the given component to cxtMenu
282 | function appendComponentToCxtMenu(component) {
283 | $cxtMenu.append(component);
284 | bindMenuItemClickFunction(component);
285 | }
286 |
287 | // Insert the given component to cxtMenu just before the existing item with given ID
288 | function insertComponentBeforeExistingItem(component, existingItemID) {
289 | var $existingItem = $('#' + existingItemID);
290 | component.insertBefore($existingItem);
291 | }
292 |
293 | function destroyCxtMenu() {
294 | if(!getScratchProp('active')) {
295 | return;
296 | }
297 |
298 | removeAndUnbindMenuItems();
299 |
300 | cy.off('tapstart', eventCyTapStart);
301 |
302 | $cxtMenu.remove();
303 | $cxtMenu = undefined;
304 | setScratchProp($cxtMenu, undefined);
305 | setScratchProp('active', false);
306 | setScratchProp('anyVisibleChild', false);
307 | }
308 |
309 | function removeAndUnbindMenuItems() {
310 | var children = $cxtMenu.children();
311 |
312 | $(children).each(function() {
313 | removeAndUnbindMenuItem($(this));
314 | });
315 | }
316 |
317 | function removeAndUnbindMenuItem(itemID) {
318 | var $component = typeof itemID === 'string' ? $('#' + itemID) : itemID;
319 | var cxtfcn = $component.data('cy-context-menus-cxtfcn');
320 | var selector = $component.data('selector');
321 | var callOnClickFcn = $component.data('call-on-click-function');
322 | var cxtCoreFcn = $component.data('cy-context-menus-cxtcorefcn');
323 |
324 | if(cxtfcn) {
325 | cy.off('cxttap', selector, cxtfcn);
326 | }
327 |
328 | if(cxtCoreFcn) {
329 | cy.off('cxttap', cxtCoreFcn);
330 | }
331 |
332 | if(callOnClickFcn) {
333 | $component.off('click', callOnClickFcn);
334 | }
335 |
336 | $component.remove();
337 | }
338 |
339 | function moveBeforeOtherMenuItemComponent(componentID, existingComponentID) {
340 | if( componentID === existingComponentID ) {
341 | return;
342 | }
343 |
344 | var $component = $('#' + componentID).detach();
345 | var $existingComponent = $('#' + existingComponentID);
346 |
347 | $component.insertBefore($existingComponent);
348 | }
349 |
350 | function bindMenuItemClickFunction(component) {
351 | component.click( function() {
352 | hideComponent($cxtMenu);
353 | setScratchProp('cxtMenuPosition', undefined);
354 | });
355 | }
356 |
357 | function disableComponent(componentID) {
358 | $('#' + componentID).attr('disabled', true);
359 | }
360 |
361 | function enableComponent(componentID) {
362 | $('#' + componentID).attr('disabled', false);
363 | }
364 |
365 | function setTrailingDivider(componentID, status) {
366 | var $component = $('#' + componentID);
367 | if(status) {
368 | $component.addClass(dividerCSSClass);
369 | }
370 | else {
371 | $component.removeClass(dividerCSSClass);
372 | }
373 | }
374 |
375 | // Get an extension instance to enable users to access extension methods
376 | function getInstance(cy) {
377 | var instance = {
378 | // Returns whether the extension is active
379 | isActive: function() {
380 | return getScratchProp('active');
381 | },
382 | // Appends given menu item to the menu items list.
383 | appendMenuItem: function(item) {
384 | createAndAppendMenuItemComponent(item);
385 | return cy;
386 | },
387 | // Appends menu items in the given list to the menu items list.
388 | appendMenuItems: function(items) {
389 | createAndAppendMenuItemComponents(items);
390 | return cy;
391 | },
392 | // Removes the menu item with given ID.
393 | removeMenuItem: function(itemID) {
394 | removeAndUnbindMenuItem(itemID);
395 | return cy;
396 | },
397 | // Sets whether the menuItem with given ID will have a following divider.
398 | setTrailingDivider: function(itemID, status) {
399 | setTrailingDivider(itemID, status);
400 | return cy;
401 | },
402 | // Inserts given item before the existingitem.
403 | insertBeforeMenuItem: function(item, existingItemID) {
404 | createAndInsertMenuItemComponentBeforeExistingComponent(item, existingItemID);
405 | return cy;
406 | },
407 | // Moves the item with given ID before the existingitem.
408 | moveBeforeOtherMenuItem: function(itemID, existingItemID) {
409 | moveBeforeOtherMenuItemComponent(itemID, existingItemID);
410 | return cy;
411 | },
412 | // Disables the menu item with given ID.
413 | disableMenuItem: function(itemID) {
414 | disableComponent(itemID);
415 | return cy;
416 | },
417 | // Enables the menu item with given ID.
418 | enableMenuItem: function(itemID) {
419 | enableComponent(itemID);
420 | return cy;
421 | },
422 | // Disables the menu item with given ID.
423 | hideMenuItem: function(itemID) {
424 | $('#'+itemID).data('show', false);
425 | hideComponent($('#'+itemID));
426 | return cy;
427 | },
428 | // Enables the menu item with given ID.
429 | showMenuItem: function(itemID) {
430 | $('#'+itemID).data('show', true);
431 | displayComponent($('#'+itemID));
432 | return cy;
433 | },
434 | // Destroys the extension instance
435 | destroy: function() {
436 | destroyCxtMenu();
437 | return cy;
438 | }
439 | };
440 |
441 | return instance;
442 | }
443 |
444 | if ( opts !== 'get' ) {
445 | // merge the options with default ones
446 | options = extend(defaults, opts);
447 | setScratchProp('options', options);
448 |
449 | // Clear old context menu if needed
450 | if(getScratchProp('active')) {
451 | destroyCxtMenu();
452 | }
453 |
454 | setScratchProp('active', true);
455 |
456 | $cxtMenu = createAndAppendCxtMenuComponent();
457 |
458 | var menuItems = options.menuItems;
459 | createAndAppendMenuItemComponents(menuItems);
460 |
461 | bindCyEvents();
462 | preventDefaultContextTap();
463 | }
464 |
465 | return getInstance(this);
466 | });
467 | };
468 |
469 | if( typeof module !== 'undefined' && module.exports ){ // expose as a commonjs module
470 | module.exports = register;
471 | }
472 |
473 | if( typeof define !== 'undefined' && define.amd ){ // expose as an amd/requirejs module
474 | define('cytoscape-context-menus', function(){
475 | return register;
476 | });
477 | }
478 |
479 | if( typeof cytoscape !== 'undefined' && $ ){ // expose to global cytoscape (i.e. window.cytoscape)
480 | register( cytoscape, $ );
481 | }
482 |
483 | })();
484 |
--------------------------------------------------------------------------------
/dist/js/lib/cytoscape-undo-redo.js:
--------------------------------------------------------------------------------
1 | ;(function () {
2 | 'use strict';
3 |
4 | // registers the extension on a cytoscape lib ref
5 | var register = function (cytoscape) {
6 |
7 | if (!cytoscape) {
8 | return;
9 | } // can't register if cytoscape unspecified
10 |
11 | // Get scratch pad reserved for this extension on the given element or the core if 'name' parameter is not set,
12 | // if the 'name' parameter is set then return the related property in the scratch instead of the whole scratchpad
13 | function getScratch (eleOrCy, name) {
14 |
15 | if (eleOrCy.scratch("_undoRedo") === undefined) {
16 | eleOrCy.scratch("_undoRedo", {});
17 | }
18 |
19 | var scratchPad = eleOrCy.scratch("_undoRedo");
20 |
21 | return ( name === undefined ) ? scratchPad : scratchPad[name];
22 | }
23 |
24 | // Set the a field (described by 'name' parameter) of scratchPad (that is reserved for this extension
25 | // on an element or the core) to the given value (by 'val' parameter)
26 | function setScratch (eleOrCy, name, val) {
27 |
28 | var scratchPad = getScratch(eleOrCy);
29 | scratchPad[name] = val;
30 | eleOrCy.scratch("_undoRedo", scratchPad);
31 | }
32 |
33 | // Generate an instance of the extension for the given cy instance
34 | function generateInstance (cy) {
35 | var instance = {};
36 |
37 | instance.options = {
38 | isDebug: false, // Debug mode for console messages
39 | actions: {},// actions to be added
40 | undoableDrag: true, // Whether dragging nodes are undoable can be a function as well
41 | stackSizeLimit: undefined, // Size limit of undo stack, note that the size of redo stack cannot exceed size of undo stack
42 | beforeUndo: function () { // callback before undo is triggered.
43 |
44 | },
45 | afterUndo: function () { // callback after undo is triggered.
46 |
47 | },
48 | beforeRedo: function () { // callback before redo is triggered.
49 |
50 | },
51 | afterRedo: function () { // callback after redo is triggered.
52 |
53 | },
54 | ready: function () {
55 |
56 | }
57 | };
58 |
59 | instance.actions = {};
60 |
61 | instance.undoStack = [];
62 |
63 | instance.redoStack = [];
64 |
65 | //resets undo and redo stacks
66 | instance.reset = function(undos, redos)
67 | {
68 | this.undoStack = undos || [];
69 | this.redoStack = undos || [];
70 | }
71 |
72 | // Undo last action
73 | instance.undo = function () {
74 | if (!this.isUndoStackEmpty()) {
75 |
76 | var action = this.undoStack.pop();
77 | cy.trigger("beforeUndo", [action.name, action.args]);
78 |
79 | var res = this.actions[action.name]._undo(action.args);
80 |
81 | this.redoStack.push({
82 | name: action.name,
83 | args: res
84 | });
85 |
86 | cy.trigger("afterUndo", [action.name, action.args, res]);
87 | return res;
88 | } else if (this.options.isDebug) {
89 | console.log("Undoing cannot be done because undo stack is empty!");
90 | }
91 | };
92 |
93 | // Redo last action
94 | instance.redo = function () {
95 |
96 | if (!this.isRedoStackEmpty()) {
97 | var action = this.redoStack.pop();
98 |
99 | cy.trigger(action.firstTime ? "beforeDo" : "beforeRedo", [action.name, action.args]);
100 |
101 | if (!action.args)
102 | action.args = {};
103 | action.args.firstTime = action.firstTime ? true : false;
104 |
105 | var res = this.actions[action.name]._do(action.args);
106 |
107 | this.undoStack.push({
108 | name: action.name,
109 | args: res
110 | });
111 |
112 | if (this.options.stackSizeLimit != undefined && this.undoStack.length > this.options.stackSizeLimit ) {
113 | this.undoStack.shift();
114 | }
115 |
116 | cy.trigger(action.firstTime ? "afterDo" : "afterRedo", [action.name, action.args, res]);
117 | return res;
118 | } else if (this.options.isDebug) {
119 | console.log("Redoing cannot be done because redo stack is empty!");
120 | }
121 |
122 | };
123 |
124 | // Calls registered function with action name actionName via actionFunction(args)
125 | instance.do = function (actionName, args) {
126 |
127 | this.redoStack.length = 0;
128 | this.redoStack.push({
129 | name: actionName,
130 | args: args,
131 | firstTime: true
132 | });
133 |
134 | return this.redo();
135 | };
136 |
137 | // Undo all actions in undo stack
138 | instance.undoAll = function() {
139 |
140 | while( !this.isUndoStackEmpty() ) {
141 | this.undo();
142 | }
143 | };
144 |
145 | // Redo all actions in redo stack
146 | instance.redoAll = function() {
147 |
148 | while( !this.isRedoStackEmpty() ) {
149 | this.redo();
150 | }
151 | };
152 |
153 | // Register action with its undo function & action name.
154 | instance.action = function (actionName, _do, _undo) {
155 |
156 | this.actions[actionName] = {
157 | _do: _do,
158 | _undo: _undo
159 | };
160 |
161 |
162 | return this;
163 | };
164 |
165 | // Removes action stated with actionName param
166 | instance.removeAction = function (actionName) {
167 | delete this.actions[actionName];
168 | };
169 |
170 | // Gets whether undo stack is empty
171 | instance.isUndoStackEmpty = function () {
172 | return (this.undoStack.length === 0);
173 | };
174 |
175 | // Gets whether redo stack is empty
176 | instance.isRedoStackEmpty = function () {
177 | return (this.redoStack.length === 0);
178 | };
179 |
180 | // Gets actions (with their args) in undo stack
181 | instance.getUndoStack = function () {
182 | return this.undoStack;
183 | };
184 |
185 | // Gets actions (with their args) in redo stack
186 | instance.getRedoStack = function () {
187 | return this.redoStack;
188 | };
189 |
190 | return instance;
191 | }
192 |
193 | // design implementation
194 | cytoscape("core", "undoRedo", function (options, dontInit) {
195 | var cy = this;
196 | var instance = getScratch(cy, 'instance') || generateInstance(cy);
197 | setScratch(cy, 'instance', instance);
198 |
199 | if (options) {
200 | for (var key in options)
201 | if (instance.options.hasOwnProperty(key))
202 | instance.options[key] = options[key];
203 |
204 | if (options.actions)
205 | for (var key in options.actions)
206 | instance.actions[key] = options.actions[key];
207 |
208 | }
209 |
210 | if (!getScratch(cy, 'isInitialized') && !dontInit) {
211 |
212 | var defActions = defaultActions(cy);
213 | for (var key in defActions)
214 | instance.actions[key] = defActions[key];
215 |
216 |
217 | setDragUndo(cy, instance.options.undoableDrag);
218 | setScratch(cy, 'isInitialized', true);
219 | }
220 |
221 | instance.options.ready();
222 |
223 | return instance;
224 |
225 | });
226 |
227 | function setDragUndo(cy, undoable) {
228 | var lastMouseDownNodeInfo = null;
229 |
230 | cy.on("grab", "node", function () {
231 | if (typeof undoable === 'function' ? undoable.call(this) : undoable) {
232 | lastMouseDownNodeInfo = {};
233 | lastMouseDownNodeInfo.lastMouseDownPosition = {
234 | x: this.position("x"),
235 | y: this.position("y")
236 | };
237 | lastMouseDownNodeInfo.node = this;
238 | }
239 | });
240 | cy.on("free", "node", function () {
241 |
242 | var instance = getScratch(cy, 'instance');
243 |
244 | if (typeof undoable === 'function' ? undoable.call(this) : undoable) {
245 | if (lastMouseDownNodeInfo == null) {
246 | return;
247 | }
248 | var node = lastMouseDownNodeInfo.node;
249 | var lastMouseDownPosition = lastMouseDownNodeInfo.lastMouseDownPosition;
250 | var mouseUpPosition = {
251 | x: node.position("x"),
252 | y: node.position("y")
253 | };
254 | if (mouseUpPosition.x != lastMouseDownPosition.x ||
255 | mouseUpPosition.y != lastMouseDownPosition.y) {
256 | var positionDiff = {
257 | x: mouseUpPosition.x - lastMouseDownPosition.x,
258 | y: mouseUpPosition.y - lastMouseDownPosition.y
259 | };
260 |
261 | var nodes;
262 | if (node.selected()) {
263 | nodes = cy.nodes(":visible").filter(":selected");
264 | }
265 | else {
266 | nodes = cy.collection([node]);
267 | }
268 |
269 | var param = {
270 | positionDiff: positionDiff,
271 | nodes: nodes, move: false
272 | };
273 |
274 | instance.do("drag", param);
275 |
276 | lastMouseDownNodeInfo = null;
277 | }
278 | }
279 | });
280 | }
281 |
282 | // Default actions
283 | function defaultActions(cy) {
284 |
285 | function getTopMostNodes(nodes) {
286 | var nodesMap = {};
287 | for (var i = 0; i < nodes.length; i++) {
288 | nodesMap[nodes[i].id()] = true;
289 | }
290 | var roots = nodes.filter(function (ele, i) {
291 | if(typeof ele === "number") {
292 | ele = i;
293 | }
294 | var parent = ele.parent()[0];
295 | while(parent != null){
296 | if(nodesMap[parent.id()]){
297 | return false;
298 | }
299 | parent = parent.parent()[0];
300 | }
301 | return true;
302 | });
303 |
304 | return roots;
305 | }
306 |
307 | function moveNodes(positionDiff, nodes, notCalcTopMostNodes) {
308 | var topMostNodes = notCalcTopMostNodes?nodes:getTopMostNodes(nodes);
309 | for (var i = 0; i < topMostNodes.length; i++) {
310 | var node = topMostNodes[i];
311 | var oldX = node.position("x");
312 | var oldY = node.position("y");
313 | node.position({
314 | x: oldX + positionDiff.x,
315 | y: oldY + positionDiff.y
316 | });
317 | var children = node.children();
318 | moveNodes(positionDiff, children, true);
319 | }
320 | }
321 |
322 | function getEles(_eles) {
323 | return (typeof _eles === "string") ? cy.$(_eles) : _eles;
324 | }
325 |
326 | function restoreEles(_eles) {
327 | return getEles(_eles).restore();
328 | }
329 |
330 |
331 | function returnToPositions(positions) {
332 | var currentPositions = {};
333 | cy.nodes().positions(function (ele, i) {
334 | if(typeof ele === "number") {
335 | ele = i;
336 | }
337 |
338 | currentPositions[ele.id()] = {
339 | x: ele.position("x"),
340 | y: ele.position("y")
341 | };
342 | var pos = positions[ele.id()];
343 | return {
344 | x: pos.x,
345 | y: pos.y
346 | };
347 | });
348 |
349 | return currentPositions;
350 | }
351 |
352 | function getNodePositions() {
353 | var positions = {};
354 | var nodes = cy.nodes();
355 | for (var i = 0; i < nodes.length; i++) {
356 | var node = nodes[i];
357 | positions[node.id()] = {
358 | x: node.position("x"),
359 | y: node.position("y")
360 | };
361 | }
362 | return positions;
363 | }
364 |
365 | function changeParent(param) {
366 | var result = {
367 | };
368 | // If this is first time we should move the node to its new parent and relocate it by given posDiff params
369 | // else we should remove the moved eles and restore the eles to restore
370 | if (param.firstTime) {
371 | var newParentId = param.parentData == undefined ? null : param.parentData;
372 | // These eles includes the nodes and their connected edges and will be removed in nodes.move().
373 | // They should be restored in undo
374 | var withDescendant = param.nodes.union(param.nodes.descendants());
375 | result.elesToRestore = withDescendant.union(withDescendant.connectedEdges());
376 | // These are the eles created by nodes.move(), they should be removed in undo.
377 | result.movedEles = param.nodes.move({"parent": newParentId});
378 |
379 | var posDiff = {
380 | x: param.posDiffX,
381 | y: param.posDiffY
382 | };
383 |
384 | moveNodes(posDiff, result.movedEles);
385 | }
386 | else {
387 | result.elesToRestore = param.movedEles.remove();
388 | result.movedEles = param.elesToRestore.restore();
389 | }
390 |
391 | if (param.callback) {
392 | result.callback = param.callback; // keep the provided callback so it can be reused after undo/redo
393 | param.callback(result.movedEles); // apply the callback on newly created elements
394 | }
395 |
396 | return result;
397 | }
398 |
399 | // function registered in the defaultActions below
400 | // to be used like .do('batch', actionList)
401 | // allows to apply any quantity of registered action in one go
402 | // the whole batch can be undone/redone with one key press
403 | function batch (actionList, doOrUndo) {
404 | var tempStack = []; // corresponds to the results of every action queued in actionList
405 | var instance = getScratch(cy, 'instance'); // get extension instance through cy
406 | var actions = instance.actions;
407 |
408 | // here we need to check in advance if all the actions provided really correspond to available functions
409 | // if one of the action cannot be executed, the whole batch is corrupted because we can't go back after
410 | for (var i = 0; i < actionList.length; i++) {
411 | var action = actionList[i];
412 | if (!actions.hasOwnProperty(action.name)) {
413 | throw "Action " + action.name + " does not exist as an undoable function";
414 | }
415 | }
416 |
417 | for (var i = 0; i < actionList.length; i++) {
418 | var action = actionList[i];
419 | // firstTime property is automatically injected into actionList by the do() function
420 | // we use that to pass it down to the actions in the batch
421 | action.param.firstTime = actionList.firstTime;
422 | var actionResult;
423 | if (doOrUndo == "undo") {
424 | actionResult = actions[action.name]._undo(action.param);
425 | }
426 | else {
427 | actionResult = actions[action.name]._do(action.param);
428 | }
429 |
430 | tempStack.unshift({
431 | name: action.name,
432 | param: actionResult
433 | });
434 | }
435 |
436 | return tempStack;
437 | };
438 |
439 | return {
440 | "add": {
441 | _do: function (eles) {
442 | return eles.firstTime ? cy.add(eles) : restoreEles(eles);
443 | },
444 | _undo: cy.remove
445 | },
446 | "remove": {
447 | _do: cy.remove,
448 | _undo: restoreEles
449 | },
450 | "restore": {
451 | _do: restoreEles,
452 | _undo: cy.remove
453 | },
454 | "select": {
455 | _do: function (_eles) {
456 | return getEles(_eles).select();
457 | },
458 | _undo: function (_eles) {
459 | return getEles(_eles).unselect();
460 | }
461 | },
462 | "unselect": {
463 | _do: function (_eles) {
464 | return getEles(_eles).unselect();
465 | },
466 | _undo: function (_eles) {
467 | return getEles(_eles).select();
468 | }
469 | },
470 | "move": {
471 | _do: function (args) {
472 | var eles = getEles(args.eles);
473 | var nodes = eles.nodes();
474 | var edges = eles.edges();
475 |
476 | return {
477 | oldNodes: nodes,
478 | newNodes: nodes.move(args.location),
479 | oldEdges: edges,
480 | newEdges: edges.move(args.location)
481 | };
482 | },
483 | _undo: function (eles) {
484 | var newEles = cy.collection();
485 | var location = {};
486 | if (eles.newNodes.length > 0) {
487 | location.parent = eles.newNodes[0].parent();
488 |
489 | for (var i = 0; i < eles.newNodes.length; i++) {
490 | var newNode = eles.newNodes[i].move({
491 | parent: eles.oldNodes[i].parent()
492 | });
493 | newEles.union(newNode);
494 | }
495 | } else {
496 | location.source = location.newEdges[0].source();
497 | location.target = location.newEdges[0].target();
498 |
499 | for (var i = 0; i < eles.newEdges.length; i++) {
500 | var newEdge = eles.newEdges[i].move({
501 | source: eles.oldEdges[i].source(),
502 | target: eles.oldEdges[i].target()
503 | });
504 | newEles.union(newEdge);
505 | }
506 | }
507 | return {
508 | eles: newEles,
509 | location: location
510 | };
511 | }
512 | },
513 | "drag": {
514 | _do: function (args) {
515 | if (args.move)
516 | moveNodes(args.positionDiff, args.nodes);
517 | return args;
518 | },
519 | _undo: function (args) {
520 | var diff = {
521 | x: -1 * args.positionDiff.x,
522 | y: -1 * args.positionDiff.y
523 | };
524 | var result = {
525 | positionDiff: args.positionDiff,
526 | nodes: args.nodes,
527 | move: true
528 | };
529 | moveNodes(diff, args.nodes);
530 | return result;
531 | }
532 | },
533 | "layout": {
534 | _do: function (args) {
535 | if (args.firstTime){
536 | var positions = getNodePositions();
537 | var layout;
538 | if(args.eles) {
539 | layout = getEles(args.eles).layout(args.options);
540 | }
541 | else {
542 | layout = cy.layout(args.options);
543 | }
544 |
545 | // Do this check for cytoscape.js backward compatibility
546 | if (layout && layout.run) {
547 | layout.run();
548 | }
549 |
550 | return positions;
551 | } else
552 | return returnToPositions(args);
553 | },
554 | _undo: function (nodesData) {
555 | return returnToPositions(nodesData);
556 | }
557 | },
558 | "changeParent": {
559 | _do: function (args) {
560 | return changeParent(args);
561 | },
562 | _undo: function (args) {
563 | return changeParent(args);
564 | }
565 | },
566 | "batch": {
567 | _do: function (args) {
568 | return batch(args, "do");
569 | },
570 | _undo: function (args) {
571 | return batch(args, "undo");
572 | }
573 | }
574 | };
575 | }
576 |
577 | };
578 |
579 | if (typeof module !== 'undefined' && module.exports) { // expose as a commonjs module
580 | module.exports = register;
581 | }
582 |
583 | if (typeof define !== 'undefined' && define.amd) { // expose as an amd/requirejs module
584 | define('cytoscape.js-undo-redo', function () {
585 | return register;
586 | });
587 | }
588 |
589 | if (typeof cytoscape !== 'undefined') { // expose to global cytoscape (i.e. window.cytoscape)
590 | register(cytoscape);
591 | }
592 |
593 | })();
594 |
--------------------------------------------------------------------------------
/dist/plantuml/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Example
10 |
11 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/dist/plantuml/example.html.bak:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Example
10 |
11 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/dist/plantuml/index.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/dist/plantuml/index.html
--------------------------------------------------------------------------------
/dist/plantuml/jquery_plantuml.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function(){
2 | plantuml_runonce();
3 | });
4 |
5 |
6 |
7 | function encode64(data) {
8 | r = "";
9 | for (i=0; i> 2;
23 | c2 = ((b1 & 0x3) << 4) | (b2 >> 4);
24 | c3 = ((b2 & 0xF) << 2) | (b3 >> 6);
25 | c4 = b3 & 0x3F;
26 | r = "";
27 | r += encode6bit(c1 & 0x3F);
28 | r += encode6bit(c2 & 0x3F);
29 | r += encode6bit(c3 & 0x3F);
30 | r += encode6bit(c4 & 0x3F);
31 | return r;
32 | }
33 |
34 | function encode6bit(b) {
35 | if (b < 10) {
36 | return String.fromCharCode(48 + b);
37 | }
38 | b -= 10;
39 | if (b < 26) {
40 | return String.fromCharCode(65 + b);
41 | }
42 | b -= 26;
43 | if (b < 26) {
44 | return String.fromCharCode(97 + b);
45 | }
46 | b -= 26;
47 | if (b == 0) {
48 | return '-';
49 | }
50 | if (b == 1) {
51 | return '_';
52 | }
53 | return '?';
54 | }
55 |
56 | var deflater = window.SharedWorker && new SharedWorker('rawdeflate.js');
57 | if (deflater) {
58 | deflater.port.addEventListener('message', done_deflating, false);
59 | deflater.port.start();
60 | } else if (window.Worker) {
61 | deflater = new Worker('rawdeflate.js');
62 | deflater.onmessage = done_deflating;
63 | }
64 |
65 | function done_deflating(e) {
66 | var done = 0;
67 | $("img").each(function () {
68 | if (done==1) return;
69 | var u1 = $(this).attr("src");
70 | if (u1!=null) return;
71 | var u2 = $(this).attr("uml");
72 | if (u2=="") return;
73 | $(this).attr("src", "http://www.plantuml.com/plantuml/img/"+encode64(e.data));
74 | $(this).attr("uml", "");
75 | done = 1;
76 | });
77 | plantuml_runonce();
78 | }
79 |
80 | function plantuml_runonce() {
81 | var done = 0;
82 | $("img").each(function () {
83 | if (done==1) return;
84 | var u1 = $(this).attr("src");
85 | if (u1!=null) return;
86 | var u2 = $(this).attr("uml");
87 | if (u2=="") return;
88 | var s = unescape(encodeURIComponent(u2));
89 | if (deflater) {
90 | if (deflater.port && deflater.port.postMessage) {
91 | deflater.port.postMessage(s);
92 | } else {
93 | deflater.postMessage(s);
94 | }
95 | } else {
96 | setTimeout(function() {
97 | done_deflating({ data: deflate(s) });
98 | }, 100);
99 | }
100 | done = 1;
101 | });
102 | }
103 |
--------------------------------------------------------------------------------
/dist/plantuml/jquery_plantuml.zip:
--------------------------------------------------------------------------------
1 | PK
--------------------------------------------------------------------------------
/example/img/demo1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/example/img/demo1.gif
--------------------------------------------------------------------------------
/example/img/demo2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/example/img/demo2.gif
--------------------------------------------------------------------------------
/example/img/demo3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/example/img/demo3.gif
--------------------------------------------------------------------------------
/example/img/demo4.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/example/img/demo4.gif
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {"name":"vendor_577ab10f48be654a037a","content":{"../node_modules/.3.3.1@jquery/dist/jquery.js":{"id":2,"meta":{}},"../node_modules/.3.2.9@cytoscape/dist/cytoscape.cjs.js":{"id":3,"meta":{}},"../node_modules/.2.0.6@timers-browserify/main.js":{"id":4,"meta":{}},"../node_modules/.4.0.8@lodash.debounce/index.js":{"id":7,"meta":{}},"../node_modules/.0.2.6@heap/index.js":{"id":8,"meta":{}},"../node_modules/.0.2.6@heap/lib/heap.js":{"id":9,"meta":{}},"../node_modules/.3.11.0@webpack/buildin/global.js":{"id":0,"meta":{}},"../node_modules/.1.0.5@setimmediate/setImmediate.js":{"id":5,"meta":{}},"../node_modules/.0.11.10@process/browser.js":{"id":6,"meta":{}}}}
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flow-chart-editor",
3 | "version": "0.1.3",
4 | "license": "MIT",
5 | "author": "tlzzu",
6 | "description": "流程设计器",
7 | "homepage": "https://tlzzu.github.io/flow-chart-editor/dist/index.html",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/tlzzu/flow-chart-editor.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/tlzzu/flow-chart-editor/issues"
14 | },
15 | "contributors": [
16 | "tlzzu "
17 | ],
18 | "keywords": [
19 | "graph",
20 | "graph-theory",
21 | "network",
22 | "node",
23 | "edge",
24 | "vertex",
25 | "link",
26 | "analysis",
27 | "visualisation",
28 | "visualization",
29 | "draw",
30 | "render",
31 | "biojs",
32 | "cytoscape",
33 | "flow-chart-editor"
34 | ],
35 | "engines": {
36 | "node": ">=0.10"
37 | },
38 | "main": "src/js/index.js",
39 | "scripts": {
40 | "dev": "webpack-dev-server --config ./build/webpack.config.dev.js",
41 | "build": "webpack --config ./build/webpack.config.prod.js"
42 | },
43 | "dependencies": {
44 | "autoprefixer-loader": "^3.2.0",
45 | "babel-plugin-transform-runtime": "^6.23.0",
46 | "babel-runtime": "^6.26.0",
47 | "copy-webpack-plugin": "^4.2.3",
48 | "cytoscape": "*",
49 | "cytoscape-context-menus": "^3.0.5",
50 | "cytoscape-edge-bend-editing": "^1.5.4",
51 | "cytoscape-edgehandles": "^3.0.2",
52 | "cytoscape-grid-guide": "^2.0.5",
53 | "cytoscape-node-resize": "*",
54 | "cytoscape-undo-redo": "^1.3.0",
55 | "cytoscape-view-utilities": "^2.0.7",
56 | "es3ify-loader": "^0.2.0",
57 | "eslint-friendly-formatter": "^3.0.0",
58 | "extract-text-webpack-plugin": "^3.0.2",
59 | "jquery": "*",
60 | "konva": "^1.7.6",
61 | "url-loader": "^0.6.2"
62 | },
63 | "devDependencies": {
64 | "autoprefixer-loader": "^3.2.0",
65 | "babel": "^6.23.0",
66 | "babel-cli": "^6.26.0",
67 | "babel-core": "^6.25.0",
68 | "babel-eslint": "^7.2.3",
69 | "babel-loader": "^7.1.0",
70 | "babel-plugin-transform-runtime": "^6.23.0",
71 | "babel-polyfill": "*",
72 | "babel-preset-es2015": "^6.24.1",
73 | "babel-preset-es2015-ie": "^6.7.0",
74 | "babel-preset-es2016": "*",
75 | "babel-preset-es2017": "*",
76 | "babel-preset-stage-2": "^6.24.1",
77 | "babel-runtime": "^6.23.0",
78 | "compression-webpack-plugin": "^1.1.10",
79 | "css-loader": "^0.28.7",
80 | "eslint": "^4.2.0",
81 | "eslint-config-standard": "^10.2.1",
82 | "eslint-friendly-formatter": "^3.0.0",
83 | "eslint-plugin-html": "^3.1.1",
84 | "eslint-plugin-import": "^2.2.0",
85 | "eslint-plugin-node": "^4.2.2",
86 | "eslint-plugin-promise": "^3.5.0",
87 | "eslint-plugin-standard": "^3.0.1",
88 | "extract-text-webpack-plugin": "^3.0.2",
89 | "file-loader": "^0.11.2",
90 | "handlebars": "^4.0.10",
91 | "handlebars-loader": "^1.5.0",
92 | "html-loader": "^0.4.5",
93 | "html-webpack-plugin": "^2.28.0",
94 | "less": "^2.7.2",
95 | "less-loader": "^4.0.4",
96 | "lodash": "^4.17.4",
97 | "node-sass": "^4.7.2",
98 | "optimize-css-assets-webpack-plugin": "^2.0.0",
99 | "sass-loader": "^6.0.6",
100 | "style-loader": "^0.18.2",
101 | "uglifyjs-webpack-plugin": "^0.4.6",
102 | "webpack": "^3.3.0",
103 | "webpack-dev-server": "^2.5.0",
104 | "webpack-merge": "^4.1.0"
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/css/default.scss:
--------------------------------------------------------------------------------
1 | $default-color: black;
2 | $cy-bg-color: blue;
3 | $bar-bg-active: #5fb8fb;
4 | $border-color: #c8c8c8;
5 | $border-color-detail: 1px solid $border-color;
6 | $ft-bg-color: #f4f4f4;
7 | $zoombar-bg-color: white;
8 | $ft-line-height: 20px;
9 | $ft-font-size: 12px;
10 | $fce-zoom-default-height: 75px;
11 | $tbs-bg-color: #dfdfdf; //toolbar的背景色
12 | $tbs-line-height: 35px;
13 | $zoombar-height: 200px;
14 | $zoombar-width: 25px;
15 | $z-index: 1000;
16 | //代码块
17 | @mixin absolute-position {
18 | z-index: $z-index;
19 | position: absolute;
20 | border-top: $border-color-detail;
21 | border-bottom: $border-color-detail;
22 | }
23 |
24 | @mixin display-flex-1 {
25 | display: flex;
26 | flex: 1;
27 | width: 100%;
28 | }
29 |
30 | @mixin bar-cursor {
31 | cursor: pointer;
32 | }
33 |
34 | @mixin bar-hover {
35 | background-color: #add7f6;
36 | }
37 |
38 | .fce {
39 | .canvas-pointer canvas {
40 | cursor: default;
41 | }
42 | .canvas-line canvas {
43 | cursor: crosshair;
44 | }
45 | position: relative;
46 | width: 100%;
47 | height: 100%;
48 | color: $default-color;
49 | border-top: 1px solid $border-color;
50 | border-left: 1px solid $border-color;
51 | border-right: 1px solid $border-color;
52 | * {
53 | margin: 0;
54 | padding: 0;
55 | border: 0;
56 | }
57 | .fce-footer {
58 | @include absolute-position;
59 | @include display-flex-1;
60 | bottom: 0;
61 | background-color: $ft-bg-color;
62 | line-height: $ft-line-height;
63 | font-size: $ft-font-size;
64 | }
65 | .fce-searcher {
66 | @include absolute-position;
67 | z-index: $z-index + 1;
68 | right: 0;
69 | top: 0;
70 | padding-right: 5px;
71 | width: 150px;
72 | font-size: 13px;
73 | line-height: $tbs-line-height;
74 | border: none;
75 | input {
76 | -webkit-appearance: none;
77 | background-color: #fff;
78 | border-radius: 4px;
79 | font-size: inherit;
80 | border: 1px solid #d8dce5;
81 | box-sizing: border-box;
82 | color: #5a5e66;
83 | text-align: start;
84 | display: inline-block;
85 | font: 400 13.3333px Arial;
86 | height: 25px;
87 | margin-right: 10px;
88 | line-height: 1;
89 | outline: 0;
90 | padding: 0 10px;
91 | transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
92 | width: 100%;
93 | }
94 | input:hover {
95 | border-color: #b4bccc;
96 | }
97 | }
98 | @mixin btn-bar {
99 | float: left;
100 | line-height: 35px;
101 | padding: 0 10px;
102 | }
103 | .fce-toolbars {
104 | @include absolute-position;
105 | @include display-flex-1;
106 | top: 0;
107 | background-color: $tbs-bg-color;
108 | line-height: $tbs-line-height;
109 | border-top: 0;
110 | .fce-tool-bars {
111 | .fce-base-bar {
112 | @include btn-bar;
113 | img {
114 | width: 20px;
115 | height: 20px;
116 | vertical-align: middle;
117 | }
118 | .fce-tool-bar-ext {
119 | display: none;
120 | }
121 | .fce-tool-bar-temp {
122 | @include absolute-position;
123 | display: block;
124 | min-width: 40px;
125 | visibility: hidden;
126 | }
127 | }
128 | .fce-base-bar:hover {
129 | @include bar-hover;
130 | }
131 | .fce-tool-bar-active {
132 | background-color: $bar-bg-active;
133 | // border-left: $border-color-detail;
134 | // border-right: $border-color-detail;
135 | border-bottom: none;
136 | .fce-tool-bar-ext {
137 | @include absolute-position;
138 | background-color: $tbs-bg-color;
139 | border-bottom: none;
140 | border-left: $border-color-detail;
141 | border-right: $border-color-detail;
142 | display: block;
143 | line-height: 35px;
144 | min-width: 40px;
145 | }
146 | }
147 | .fce-tool-bar-active:hover {
148 | background-color: $bar-bg-active;
149 | }
150 | }
151 | }
152 | .fce-base-bars {
153 | .fce-base-bar {
154 | @include bar-cursor;
155 | }
156 | }
157 | .fce-tool-bar-ext {
158 | .bar-auto_play {
159 | @include btn-bar;
160 | }
161 | }
162 | .fce-navbar {
163 | @include absolute-position;
164 | bottom: 30px;
165 | left: 10px;
166 | height: $zoombar-height;
167 | width: $zoombar-width;
168 | border-left: 1px solid $border-color;
169 | border-right: 1px solid $border-color;
170 | border-radius: 4px;
171 | text-align: center;
172 | background-color: $tbs-bg-color;
173 | .fce-zoom-dom {
174 | margin: 0 auto;
175 | .fce-zoom-dom-background {
176 | background-color: $zoombar-bg-color;
177 | margin: 0 auto;
178 | width: 2px;
179 | height: 100%;
180 | }
181 | .fce-zoom-dom-default {
182 | background-color: $zoombar-bg-color;
183 | text-align: center;
184 | height: 2px;
185 | position: absolute;
186 | }
187 | .fce-zoom-dom-active {
188 | border-radius: 10px;
189 | height: 10px;
190 | background-color: $zoombar-bg-color;
191 | position: absolute;
192 | // top: $fce-zoom-default-height;
193 | }
194 | @mixin fce-zoom-dom-bar-default {
195 | width: 12px;
196 | height: 12px;
197 | @include bar-cursor;
198 | }
199 | .fce-zoom-dom-default {
200 | @include bar-cursor;
201 | }
202 | .fce-zoom-dom-reduce {
203 | @include fce-zoom-dom-bar-default;
204 | margin-top: 3px;
205 | }
206 | .fce-zoom-dom-plus {
207 | @include fce-zoom-dom-bar-default;
208 | margin-bottom: 8px;
209 | }
210 | .fce-zoom-dom-reduce img,
211 | .fce-zoom-dom-plus img {
212 | @include fce-zoom-dom-bar-default;
213 | display: inline-block;
214 | }
215 | }
216 | .fce-nav-bars {
217 | position: absolute;
218 | bottom: 0;
219 | margin: 0px auto;
220 | width: 25px;
221 | .fce-nav-bar {
222 | width: 100%;
223 | line-height: 25px;
224 | img {
225 | width: 15px;
226 | height: 15px;
227 | vertical-align: middle;
228 | }
229 | }
230 | .fce-nav-bar:hover {
231 | @include bar-hover;
232 | }
233 | .fce-nav-bar-active,
234 | .fce-nav-bar-active:hover {
235 | background-color: $bar-bg-active;
236 | }
237 | }
238 | }
239 | .fce-cy {
240 | // position: fixed;
241 | // background: $cy-bg-color;
242 | width: 100%;
243 | height: 100%;
244 | }
245 | }
--------------------------------------------------------------------------------
/src/images/animation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/src/images/animation.png
--------------------------------------------------------------------------------
/src/images/icon/auto_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/src/images/icon/auto_play.png
--------------------------------------------------------------------------------
/src/images/icon/fce-zoom-dom-plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/src/images/icon/fce-zoom-dom-plus.png
--------------------------------------------------------------------------------
/src/images/icon/fce-zoom-dom-reduce.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/src/images/icon/fce-zoom-dom-reduce.png
--------------------------------------------------------------------------------
/src/images/icon/line-solid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/src/images/icon/line-solid.png
--------------------------------------------------------------------------------
/src/images/icon/manual_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/src/images/icon/manual_play.png
--------------------------------------------------------------------------------
/src/images/icon/pointer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/src/images/icon/pointer.png
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | flow-chart-editor 流程设计器
9 |
23 |
24 |
25 |
26 | flow-chart-editor(FCE) 流程设计器
27 |
28 |
注意:
29 |
30 | - 允许在流程中嵌套子流程;
31 | - 支持只读、设计两种模式(敬请期待);
32 | - 支持设置流程动画(敬请期待);
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/js/Listeners/cytoscapeListener.js:
--------------------------------------------------------------------------------
1 | import { getClickType } from "../utils/cy";
2 | export default function() {
3 | const self = this;
4 | self.cy.on("tap", function(evt) {
5 | //只有nav没有控件选中时才可以添加,否则就是移动
6 |
7 | if (!self.navbars.activeBar) {
8 | const clickType = getClickType(evt),
9 | clickObject = clickType && evt.target ? evt.target.data() : null;
10 | self.mouseClickPosition = evt.position ?
11 | { x: evt.position.x, y: evt.position.y } :
12 | null;
13 | if (
14 | self.toolbars.activeBar &&
15 | self.toolbars.activeBar.options &&
16 | self.toolbars.activeBar.options.exec
17 | ) {
18 | self.toolbars.activeBar.options.exec.call(
19 | self,
20 | evt,
21 | clickType,
22 | clickObject
23 | );
24 | }
25 | self.fireEvent("add_click", evt, clickType, clickObject);
26 | }
27 | });
28 | self.cy.on("select", "node", function(evt) {
29 | if (!(self.navbars.activeBar && self.navbars.activeBar.name === "pointer")) {
30 | self.cyExtensions.nodeResize.removeGrapples();
31 | }
32 | //todo 这里需要出发选中节点事件,给予监听
33 | });
34 | }
--------------------------------------------------------------------------------
/src/js/Listeners/navbarsListener.js:
--------------------------------------------------------------------------------
1 | export default function(navbars) {
2 | const self = this;
3 | navbars.addListener('change', function(bar) {
4 | // 这里出发navbar变更事件
5 | if (!bar) return;
6 | self.navbars.setNavActiveBar(bar.name);
7 | });
8 | }
--------------------------------------------------------------------------------
/src/js/Listeners/zoomListener.js:
--------------------------------------------------------------------------------
1 | const zoomChange = function(value) {
2 | this.cy.zoom(value);
3 | const elements = this.cy.elements(),
4 | firstEle = elements && elements.length ? elements[0] : null;
5 | if (firstEle) {
6 | this.cy.center(firstEle);
7 | }
8 | };
9 | const initZoomListener = function(zoom) {
10 | const self = this;
11 | zoom.addChange(function(item) {
12 | zoomChange.call(self, this.getCyZoom(item));
13 | });
14 | };
15 | export { zoomChange, initZoomListener };
--------------------------------------------------------------------------------
/src/js/core/Dom.js:
--------------------------------------------------------------------------------
1 | //所有的dom操作都在这里
2 |
3 | /**
4 | * 创建element对象
5 | * @param {String} str
6 | * @returns {Element} el对象
7 | */
8 | const createElement = str => {
9 | return document.createElement(str);
10 | };
11 | /**
12 | * 初始化dom
13 | * @param {Element} el
14 | * @returns {Object} 返回一个object对象,{root:Element,toolbar:Element,cy:Element,zoom:Element,footer:Element,}
15 | */
16 |
17 | export default function(el) {
18 | const root = createElement("div");
19 | root.classList.add("fce");
20 |
21 | const toolbar = createElement("div");
22 | toolbar.classList.add("fce-toolbars");
23 |
24 | root.appendChild(toolbar);
25 |
26 | const searcher = createElement("div");
27 | searcher.classList.add("fce-searcher");
28 | const txtSearch = createElement("input");
29 | txtSearch.setAttribute("placeholder", "搜索当前流程图");
30 | txtSearch.setAttribute("type", "text");
31 | searcher.appendChild(txtSearch);
32 | root.appendChild(searcher);
33 |
34 | const cy = createElement("div");
35 | cy.classList.add("fce-cy");
36 | root.appendChild(cy);
37 |
38 | const zoom = createElement("div");
39 | zoom.classList.add("fce-navbar");
40 | root.appendChild(zoom);
41 |
42 | const footer = document.createElement("div");
43 | footer.classList.add("fce-footer");
44 | footer.innerHTML = "footer";
45 | root.appendChild(footer);
46 | el.appendChild(root);
47 | return { root, toolbar, searcher, cy, zoom, footer };
48 | }
--------------------------------------------------------------------------------
/src/js/core/Listener.js:
--------------------------------------------------------------------------------
1 | // /**
2 | // * 这里包含对Listener的管理具体有:
3 | // * mousedown:鼠标按下
4 | // * mouseup:鼠标松开
5 | // * click:点击
6 | // * beforeAddNode:新加node节点之前
7 | // * afterAddNode:加入node之后
8 | // */
9 | import utils from '../utils/index';
10 |
11 | /**
12 | * 添加事件监听
13 | * @param {String} types 方法类型
14 | * @param {Function} listener 具体监听方法
15 | */
16 | const addListener = function(types, listener) {
17 | if (!types) return;
18 | const typeArray = utils.classNamesToArray(types),
19 | self = this;
20 | utils.forEach(typeArray, function(type) {
21 | getListener.call(self, type).push(listener);
22 | });
23 | };
24 | /**
25 | * 移除监听的方法
26 | * @param {String} type 方法类型
27 | * @param {Function} listener 具体监听方法
28 | */
29 | const removeListener = function(type, listener) {
30 | if (!type) return;
31 | const listeners = getListener.call(this, type) || [];
32 | for (var i = 0, l = listeners.length; i < l; i++) {
33 | if (listeners[i] === listener) {
34 | listeners.splice(i, 1);
35 | i--;
36 | }
37 | }
38 | };
39 | /**
40 | * 查询监听方法
41 | * @param {String} type 查询类型
42 | * @returns {Array} 返回一个数组
43 | */
44 | const getListener = function(type) {
45 | const listeners = this.__private__.allListeners;
46 | type = type.toLowerCase();
47 | if (listeners[type]) {
48 | return listeners[type];
49 | } else {
50 | console.error('不存在该[' + type + ']类型事件!');
51 | console.trace();
52 | return [];
53 | }
54 | //return listeners[type] ? listeners[type] : [];
55 | };
56 | /**
57 | * 触发事件
58 | */
59 | const fireEvent = function(types, ...args) {
60 | const self = this,
61 | typeArray = utils.classNamesToArray(types);
62 | if (!typeArray && !typeArray.length) return;
63 | utils.forEach(typeArray, function(type) {
64 | const listeners = getListener.call(self, type);
65 | if (listeners) {
66 | let index = listeners.length;
67 | while (index--) {
68 | if (!listeners[index]) continue;
69 | listeners[index].apply(self, args);
70 | }
71 | }
72 | });
73 | };
74 |
75 | export default { addListener, removeListener, fireEvent };
--------------------------------------------------------------------------------
/src/js/core/Navbar.js:
--------------------------------------------------------------------------------
1 | import Basebar from "./basebar";
2 | /**
3 | * 事件都应该通过这里触发,base只负责渲染
4 | * 取消之前被触发事件
5 | * 选中后被触发事件
6 | * @param {Object} options
7 | */
8 | const Navbar = function(options) {
9 | this.options = options || {};
10 | Basebar.call(this);
11 | };
12 | Navbar.prototype = new Basebar();
13 | Navbar.prototype.constructor = Navbar;
14 |
15 | export default Navbar;
--------------------------------------------------------------------------------
/src/js/core/Navbars.js:
--------------------------------------------------------------------------------
1 | import Basebars from "./basebars";
2 | import Navbar from "./Navbar";
3 | import utils from "../utils/index";
4 |
5 | const defaultOptions = {
6 | activeClass: "fce-nav-bar-active",
7 | activeName: "pointer",
8 | className: "fce-nav-bars",
9 | change() {},
10 | bars: [{
11 | name: "pointer",
12 | icon: require("../../images/icon/pointer.png"),
13 | className: "fce-nav-bar",
14 | title: "指针",
15 | exec() {}
16 | },
17 | {
18 | name: "line",
19 | icon: require("../../images/icon/line-solid.png"),
20 | className: "fce-nav-bar",
21 | title: "连线",
22 | exec() {}
23 | }
24 | ]
25 | };
26 | const insideListener = function() {
27 | const self = this;
28 | utils.registerEvent(
29 | self.dom,
30 | "click",
31 | function(evt) {
32 | // 往上找,找到 fce-base-bar 的name,作为对比
33 | const current = utils.findParentElement(evt.target, "fce-base-bar");
34 | if (current) {
35 | const name = current.getAttribute ?
36 | current.getAttribute("name") :
37 | undefined;
38 | //取消toolbar的选中状态
39 | if (this.fce.toolbars.activeBar) {
40 | this.fce.toolbars.cancelActiveBar(this.fce.toolbars.activeBar.name);
41 | }
42 |
43 | if (name) {
44 | this.setActiveBar(name);
45 | this.fireEvent("change", this.bars[name]);
46 | }
47 | }
48 | }.bind(self)
49 | );
50 | };
51 | /**
52 | * navbars的对象
53 | * bar:fce-nav-bar
54 | * @param {Object} options 配置项
55 | */
56 | const Navbars = function(options) {
57 | this.options = options || defaultOptions;
58 | if (!this.__private__) this.__private__ = {};
59 | //bar的类型
60 | this.BarType = Navbar;
61 | this.__private__.allListeners = {
62 | change: [] //change事件
63 | };
64 | Basebars.call(this);
65 | const _render = this.render;
66 | this.render = function() {
67 | _render.call(this);
68 | insideListener.call(this);
69 | };
70 | };
71 | Navbars.prototype = new Basebars();
72 | Navbars.prototype.constructor = Navbars;
73 | /**
74 | * 设置nav的活跃bar
75 | * @param {String} name 如果那么、为空,则为初始化
76 | */
77 | Navbars.prototype.setNavActiveBar = function(name) {
78 | const self = this.fce;
79 | if (!name || name === "pointer") {
80 | self.__private__.allElements.cy.classList.remove("canvas-line");
81 | self.__private__.allElements.cy.classList.add("canvas-pointer");
82 | const handleNodes = self.cy.$(
83 | ".eh-handle,.eh-hover,.eh-source,.eh-target,.eh-preview,.eh-ghost-edge"
84 | );
85 | if (handleNodes && handleNodes.length > 0) {
86 | self.cy.remove(handleNodes);
87 | }
88 | self.cyExtensions.edgehandles.disable();
89 |
90 | if (name) {
91 | this.setActiveBar("pointer");
92 | } else if (!name) {
93 | self.cyExtensions.nodeResize.removeGrapples();
94 | if (this.activeBar) {
95 | this.cancelActiveBar(this.activeBar.name);
96 | }
97 | }
98 | } else if (name === "line") {
99 | self.__private__.allElements.cy.classList.remove("canvas-pointer");
100 | self.__private__.allElements.cy.classList.add("canvas-line");
101 | self.cyExtensions.edgehandles.enable();
102 | self.cyExtensions.nodeResize.removeGrapples();
103 | this.setActiveBar("line");
104 | } else {
105 | self.__private__.allElements.cy.classList.remove("canvas-line");
106 | self.__private__.allElements.cy.classList.add("canvas-pointer");
107 | console.error("未知nav-bar!");
108 | console.error(name);
109 | }
110 | };
111 | export default Navbars;
--------------------------------------------------------------------------------
/src/js/core/Toolbar.js:
--------------------------------------------------------------------------------
1 | import Basebar from "./basebar";
2 | /**
3 | * 事件都应该通过这里触发,base只负责渲染
4 | * 取消之前被触发事件
5 | * 选中后被触发事件
6 | * @param {Object} options
7 | */
8 | const Toolbar = function(options) {
9 | this.options = options || {};
10 | Basebar.call(this);
11 | };
12 | Toolbar.prototype = new Basebar();
13 | Toolbar.prototype.constructor = Toolbar;
14 | Toolbar.prototype.cancelActive = function() {
15 | //取消自身选中状态
16 | this.dom.classList.remove("fce-tool-bar-active");
17 | };
18 |
19 | export default Toolbar;
--------------------------------------------------------------------------------
/src/js/core/Toolbar/Animation/Auto.js:
--------------------------------------------------------------------------------
1 | import utils from "../../../utils/index";
2 | //自动模式
3 | const options = {
4 | root: {
5 | icon: require("../../../../images/icon/auto_play.png"),
6 | name: "auto_play",
7 | title: "自动播放"
8 | }
9 | };
10 | // const initRender = function() {};
11 |
12 | // const initChildrenRender = function() {};
13 |
14 | const Auto = function() {
15 | this.__private__ = this.__private__ || {};
16 | this.__private__.options = options;
17 | this.__private__.childrenDom = null; //详细按钮
18 | this.render = function(rootDom) {
19 | const dom = document.createElement("div"),
20 | img = document.createElement("img");
21 | dom.setAttribute("title", options.root.title);
22 | dom.setAttribute("name", options.root.name);
23 | dom.classList.add("bar-auto_play");
24 | utils.registerEvent(dom, "click", function(evt) {
25 | alert("敬请期待!");
26 | utils.preventDefault(evt);
27 | });
28 | img.src = options.root.icon;
29 | dom.appendChild(img);
30 |
31 | rootDom.__private__.dom.appendChild(dom);
32 | this.__private__.dom = dom;
33 | this.__private__.rootDom = rootDom; //根对象
34 | };
35 | };
36 |
37 | export default Auto;
--------------------------------------------------------------------------------
/src/js/core/Toolbar/Animation/Manual.js:
--------------------------------------------------------------------------------
1 | //手动模式
2 | const Manual = function () {
3 | this.__private__.options = {
4 | icoon: '',
5 | name:'',
6 | title:'手动播放'
7 | }
8 | }
9 |
10 | export default Manual;
--------------------------------------------------------------------------------
/src/js/core/Toolbar/Animation/index.js:
--------------------------------------------------------------------------------
1 | import utils from "../../../utils/index";
2 | import Listener from "../../../core/Listener";
3 | import Auto from "./Auto";
4 | import Manual from "./Manual";
5 | /**
6 | * 重置位置
7 | */
8 | const resetPosition = function() {
9 | const div = this.__private__.dom;
10 | div.style.left = ~~(
11 | this.__private__.dom.parentElement.offsetLeft -
12 | 1 -
13 | (div.offsetWidth - div.parentElement.offsetWidth) / 2
14 | ) + "px"; //-1是边框
15 | };
16 |
17 | const Animation = function() {
18 | this.__private__ = this.__private__ || {};
19 | // this.__private__.fce = {}; //fce对象实例
20 | // this.__private__.parentElement = null; //
21 | // this.__private__.dom = null;
22 | // this.__private__.bars = { auto: null, manual: null };
23 | this.init = function(parent, fce) {
24 | const dom = document.createElement("div");
25 | dom.className = "fce-tool-bar-temp";
26 | //todo 添加动画bar的样式效果
27 | parent.dom.appendChild(dom);
28 | this.__private__.dom = dom;
29 | const auto = new Auto();
30 | auto.render(this);
31 | //todo 手动动画
32 | // const manual = new Manual();
33 | // manual.render(this);
34 | resetPosition.call(this);
35 | dom.className = "fce-tool-bar-ext";
36 | };
37 | };
38 | //添加监听事件
39 | utils.forEachObject(Listener, function(item, key) {
40 | Animation.prototype[key] = item;
41 | });
42 |
43 | export default {
44 | name: "animation",
45 | icon: require("../../../../images/animation.png"),
46 | className: "fce-tool-bar-animation",
47 | title: "动画",
48 | render(fce, toolbars) {
49 | // const div = document.createElement("div");
50 | // div.className = "fce-tool-bar-temp";
51 | // //todo 添加动画bar的样式效果
52 | // this.dom.appendChild(div);
53 | // renderAnimationBar.call(this, div);
54 | // resetPosition.call(this);
55 | // div.className = "fce-tool-bar-ext";
56 | // utils.registerEvent(div, "click", function(evt) {
57 | // //todo 触发动画事件
58 | // utils.preventDefault(evt); //防止toolbar点击事件被触发
59 | // });
60 | const animation = new Animation();
61 | animation.init(this, fce, toolbars);
62 | },
63 | unselect() {},
64 |
65 | exec() {}
66 | };
--------------------------------------------------------------------------------
/src/js/core/Toolbar/index.js:
--------------------------------------------------------------------------------
1 | import animation from "./Animation/index";
2 |
3 | export default {
4 | animation: animation
5 | };
--------------------------------------------------------------------------------
/src/js/core/Toolbars.js:
--------------------------------------------------------------------------------
1 | import Basebars from "./basebars";
2 | import Toolbar from "./Toolbar";
3 | import utils from "../utils/index";
4 | import ToolbarItems from "./Toolbar/index";
5 | import { jquery } from "../lib";
6 |
7 | const defaultOptions = {
8 | activeClass: "fce-tool-bar-active",
9 | activeName: "",
10 | className: "fce-tool-bars",
11 | change() {},
12 | bars: null
13 | },
14 | barClassName = "fce-base-bar"; //"fce-tool-bar";
15 | const insideListener = function() {
16 | const self = this;
17 | utils.registerEvent(
18 | this.dom,
19 | "click",
20 | function(evt) {
21 | // 往上找,找到 fce-base-bar 的name,作为对比
22 | const current = utils.findParentElement(evt.target, "fce-base-bar");
23 | if (current) {
24 | const name = current.getAttribute ?
25 | current.getAttribute("name") :
26 | undefined;
27 | if (!name) return;
28 | if (this.activeBar && this.activeBar.name === name) {
29 | //再次点击 取消选中
30 | this.cancelActiveBar(name);
31 | //this.fce.navbars.setActiveBar("pointer");
32 | this.fce.navbars.setNavActiveBar("pointer");
33 | } else if (name) {
34 | this.setActiveBar(name);
35 | //this.fce.navbars.cancelActiveBar(this.fce.navbars.activeBar.name);
36 | this.fce.navbars.setNavActiveBar();
37 | this.fireEvent("change", this.bars[name]);
38 | }
39 | const bar = this.getBarByName(name);
40 | if (bar && bar.options && bar.options.click) {
41 | bar.options.click.call(this.fce, bar);
42 | }
43 | }
44 | }.bind(self)
45 | );
46 | };
47 | const Toolbars = function(options) {
48 | if (!options) return;
49 | if (!this.__private__) this.__private__ = {};
50 | const _options = jquery.extend(true, defaultOptions, { bars: options });
51 | if (!_options.bars) {
52 | _options.bars = [];
53 | }
54 | utils.forEach(_options.bars, function(item, index) {
55 | if (typeof item === "string") {
56 | const bar = ToolbarItems[item];
57 | if (bar) {
58 | _options.bars.splice(index, 1, bar);
59 | }
60 | } else {
61 | //对于自定义的bar,要给与其 className =barClassName
62 | const arr = utils.trim(item.className).split(/\s+/);
63 | if (!arr.includes(barClassName)) {
64 | arr.splice(0, 0, barClassName);
65 | item.className = arr.join(" ");
66 | }
67 | }
68 | });
69 | this.options = _options;
70 | //bar的类型
71 | this.BarType = Toolbar;
72 | this.__private__.allListeners = {
73 | change: [] //change事件
74 | };
75 | Basebars.call(this);
76 |
77 | if (this.options.activeName) {
78 | this.setActiveBar(this.options.activeName);
79 | }
80 |
81 | const _render = this.render;
82 | this.render = function() {
83 | _render.call(this);
84 | insideListener.call(this);
85 | };
86 | };
87 | Toolbars.prototype = new Basebars();
88 | Toolbars.prototype.constructor = Toolbars;
89 |
90 | export default Toolbars;
--------------------------------------------------------------------------------
/src/js/core/Zoom.js:
--------------------------------------------------------------------------------
1 | import utils from "../utils/index";
2 |
3 | const DEFAULT_WIDTH = 10,
4 | DEFAULT_HEIGHT = 110,
5 | DEFAULT_SURPLUS = 21,
6 | zoomOption = {
7 | defaultSize: 0,
8 | items: [
9 | { label: "缩小2倍", value: -2 },
10 | { label: "缩小1倍", value: -1 },
11 | { label: "正常", value: 0 },
12 | { label: "放大1倍", value: 1 },
13 | { label: "放大2倍", value: 2 }
14 | ]
15 | };
16 | const _resetActiveDom = function() {
17 | const items = this.__private__.options.items,
18 | item = this.__private__.selectItem;
19 | for (let i = 0, l = items.length; i < l; i++) {
20 | if (items[i].value === item.value) {
21 | this.activeDom.style.top = ~~(DEFAULT_HEIGHT * (1 - i / (l - 1)) - DEFAULT_WIDTH / 2) +
22 | DEFAULT_SURPLUS +
23 | "px";
24 | }
25 | }
26 | this.activeDom.setAttribute("title", this.__private__.selectItem.label);
27 | };
28 | /**
29 | * 重新设置当前位置
30 | * @param {Boolean} bo 是否第一次加载,默认为否
31 | */
32 | const _resetValue = function(bo = false) {
33 | const item = this.__private__.selectItem;
34 | _resetActiveDom.call(this);
35 | if (!bo) {
36 | for (let i = 0, l = this.__private__.changeListeners.length; i < l; i++) {
37 | this.__private__.changeListeners[i].call(this, item);
38 | }
39 | }
40 | };
41 | /**
42 | * 获取当前对象
43 | * @param {Array} arr
44 | * @param {Int} val
45 | * @param {Int} mult 缩放多少倍
46 | */
47 | const _getItem = function(arr, val, mult = 0) {
48 | if (!arr) return null;
49 | for (let i = 0, l = arr.length; i < l; i++) {
50 | const item = arr[i];
51 | if (item.value === val) {
52 | if (mult !== 0) {
53 | const newIndex = i + mult;
54 | if (newIndex < 0) {
55 | return arr[0];
56 | } else if (newIndex >= l) {
57 | return arr[l - 1];
58 | } else {
59 | return arr[newIndex];
60 | }
61 | }
62 | return item;
63 | }
64 | }
65 | return arr[arr.length - 1];
66 | };
67 | /**
68 | * 新建Zoom Element值
69 | */
70 | const _createZoomElement = function() {
71 | const self = this,
72 | root = document.createElement("div");
73 | root.classList.add("fce-zoom-dom");
74 | root.style.height = DEFAULT_HEIGHT + "px";
75 | root.style.width = DEFAULT_WIDTH + "px";
76 | //加
77 | const plus = document.createElement("div");
78 | plus.classList.add("fce-zoom-dom-plus");
79 | plus.setAttribute("title", "放大");
80 | const plusImg = document.createElement("img");
81 | plusImg.src = require("../../images/icon/fce-zoom-dom-plus.png");
82 | utils.registerEvent(plus, "click", function() {
83 | self.times(1);
84 | });
85 | plus.appendChild(plusImg);
86 | root.appendChild(plus);
87 |
88 | const bg = document.createElement("div");
89 | bg.classList.add("fce-zoom-dom-background");
90 | root.appendChild(bg);
91 | const _defalut = document.createElement("div");
92 | _defalut.classList.add("fce-zoom-dom-default");
93 | _defalut.style.top = ~~(DEFAULT_HEIGHT / 2) + DEFAULT_SURPLUS + "px";
94 | _defalut.style.width = DEFAULT_WIDTH + "px";
95 | _defalut.setAttribute("title", "正常");
96 | utils.registerEvent(_defalut, "click", function() {
97 | self.set(self.__private__.options.defaultSize);
98 | });
99 | root.appendChild(_defalut);
100 | const active = document.createElement("div");
101 | active.classList.add("fce-zoom-dom-active");
102 | active.style.width = DEFAULT_WIDTH + "px";
103 | self.activeDom = active;
104 | root.appendChild(active);
105 | //减
106 | const reduce = document.createElement("div");
107 | reduce.classList.add("fce-zoom-dom-reduce");
108 | reduce.setAttribute("title", "缩小");
109 | const reduceImg = document.createElement("img");
110 | reduceImg.src = require("../../images/icon/fce-zoom-dom-reduce.png");
111 | reduce.appendChild(reduceImg);
112 | utils.registerEvent(reduce, "click", function() {
113 | self.times(-1);
114 | });
115 | root.appendChild(reduce);
116 | return root;
117 | };
118 | /**
119 | * 初始化zoom对象
120 | * @param {Object} options {defaultSize:1,items:[{label:'正常',value:0}],change(){}}
121 | */
122 | const Zoom = function(options) {
123 | options = options || zoomOption;
124 | if (!this.__private__) this.__private__ = {};
125 | this.__private__.selectItem = _getItem(options.items, options.defaultSize); //{label:'正常',value:0}
126 | this.__private__.changeListeners = [];
127 | this.__private__.options = options;
128 | this.dom = _createZoomElement.call(this);
129 | _resetValue.call(this, true);
130 | };
131 | Zoom.prototype = {
132 | /**
133 | * 设置值
134 | * @param {Number} value 设置为多少倍
135 | */
136 | set: function(value) {
137 | const temp = _getItem(this.__private__.options.items, value);
138 | if (temp.value === this.__private__.selectItem.value) {
139 | return;
140 | }
141 | this.__private__.selectItem = temp;
142 | _resetValue.call(this);
143 | },
144 | /**
145 | * 获取当前值
146 | * @returns {Number} 返回当前值
147 | */
148 | get: function() {
149 | return this.__private__.selectItem.value;
150 | },
151 | /**
152 | * 设置倍数
153 | * @param {Number} mult 多少倍,正则为放大多少倍,负则为缩小多少倍
154 | */
155 | times: function(mult = 0) {
156 | const temp = _getItem(
157 | this.__private__.options.items,
158 | this.__private__.selectItem.value,
159 | mult
160 | );
161 | if (temp.value === this.__private__.selectItem.value) {
162 | return;
163 | }
164 | this.__private__.selectItem = temp;
165 | _resetValue.call(this);
166 | },
167 | getCyZoom: function(item) {
168 | const zoom = item || this.get();
169 | switch (zoom.value) {
170 | case -2:
171 | return 0.1;
172 | case -1:
173 | return 0.3;
174 | case 0:
175 | return 1;
176 | case 1:
177 | return 3;
178 | case 2:
179 | return 9;
180 | default:
181 | return 1;
182 | }
183 | },
184 | /**
185 | * 绑定变化时的监听函数
186 | */
187 | addChange: function(handler) {
188 | this.__private__.changeListeners.push(handler);
189 | },
190 | /**
191 | * 移除监听
192 | */
193 | removeChange: function(handler) {
194 | const listeners = this.__private__.changeListeners;
195 | for (let i = 0, l = listeners.length; i < l; i++) {
196 | const listener = listeners[i];
197 | if (listener === handler) {
198 | listeners.splice(i, 1);
199 | i--;
200 | }
201 | }
202 | }
203 | };
204 | export default Zoom;
--------------------------------------------------------------------------------
/src/js/core/basebar.js:
--------------------------------------------------------------------------------
1 | import utils from "../utils/index";
2 | const render = function() {
3 | const dom = document.createElement("div");
4 | dom.setAttribute("name", this.name);
5 | dom.className = this.options.className;
6 | dom.classList.add("fce-base-bar");
7 | const img = document.createElement("img");
8 | img.src = this.options.icon;
9 | img.setAttribute("title", this.options.title);
10 | dom.appendChild(img);
11 | this.dom = dom;
12 | };
13 | /**
14 | * 单个的bar
15 | * {name:'不能重复',icon:'',className:'',title:'',click(){}}
16 | */
17 | const Basebar = function() {
18 | if (!this.options) {
19 | return;
20 | }
21 | this.name = this.options.name;
22 | this.dom = null;
23 | // if (this.options.render) {
24 | // this.options.render.call(this);
25 | // } else if (this.render) {
26 | // this.render();
27 | // } else {
28 | // render.call(this);
29 | // }
30 | render.call(this);
31 | };
32 | Basebar.prototype = {
33 | // click(item) {
34 | // //这里要改变this指向
35 | // this.options.click.call(this, item);
36 | // },
37 | hasClass(className) {
38 | return this.dom.classList.contains(className);
39 | },
40 | addClass(classNames) {
41 | if (!classNames) return;
42 | const arr = utils.classNamesToArray(classNames),
43 | self = this;
44 | utils.forEach(arr, function(className) {
45 | if (!self.dom.classList.contains(className)) {
46 | self.dom.classList.add(className);
47 | }
48 | });
49 | },
50 | removeClass(classNames) {
51 | if (!classNames) return;
52 | const arr = utils.classNamesToArray(classNames),
53 | self = this;
54 | utils.forEach(arr, function(className) {
55 | if (self.dom.classList.contains(className)) {
56 | self.dom.classList.remove(className);
57 | }
58 | });
59 | }
60 | };
61 |
62 | export default Basebar;
--------------------------------------------------------------------------------
/src/js/core/basebars.js:
--------------------------------------------------------------------------------
1 | //保证同时只能有一个触发事件
2 | import Listener from "./Listener";
3 | import utils from "../utils/index";
4 | import BaseBar from "./basebar";
5 |
6 | /**
7 | * 初始化bars
8 | */
9 | const initBars = function() {
10 | const barOpts = this.options.bars || [],
11 | self = this;
12 | utils.forEach(barOpts, function(barOpt) {
13 | const bar = new self.BarType(barOpt);
14 | self.dom.appendChild(bar.dom);
15 | if (bar.options.render) {
16 | bar.options.render.call(bar, self.fce, self);
17 | }
18 | self.bars[bar.name] = bar;
19 | });
20 | };
21 | /**
22 | * bar的基类,不可直接被new
23 | */
24 | const Basebars = function() {
25 | //{bars:[{name:'不能重复',icon:'',className:'',title:'',isActive:true,change(){}}],activeClass:'',activeName:'',className:''}
26 | if (!this.options) {
27 | return;
28 | }
29 | //if (!this.__private__) this.__private__ = {};
30 | this.BarType = this.BarType ? this.BarType : BaseBar;
31 | this.bars = {}; //basebars类型 所有的初始化的bar
32 | this.activeBar = null; //basebars类型 当前激活的bar
33 | this.__private__.allListeners = this.__private__.allListeners || {}; //所有的change事件:change事件
34 | const dom = document.createElement("div");
35 | dom.className = this.options.className;
36 | dom.classList.add("fce-base-bars");
37 | this.dom = dom;
38 | };
39 | //将事件管理器赋予BaseBars
40 | utils.forEachObject(Listener, function(item, key) {
41 | Basebars.prototype[key] = item;
42 | });
43 | /**
44 | * 设置激活状态的bar
45 | * @param {String} name 当前bar的活动名
46 | */
47 | Basebars.prototype.setActiveBar = function(name) {
48 | if (!this.bars[name]) return;
49 | this.cancelActiveBar(name);
50 | this.bars[name].addClass(this.options.activeClass);
51 | this.activeBar = this.bars[name];
52 | };
53 | /**
54 | * 根据name获取bar
55 | * @param {*} name
56 | */
57 | Basebars.prototype.getBarByName = function(name) {
58 | if (!name) return;
59 | return this.bars[name];
60 | };
61 | Basebars.prototype.render = function() {
62 | initBars.call(this);
63 | };
64 | Basebars.prototype.cancelActiveBar = function(name) {
65 | if (!name && this.activeBar) {
66 | this.activeBar.removeClass(this.options.activeClass);
67 | } else {
68 | if (!this.bars[name]) return;
69 | for (let b in this.bars) {
70 | const bar = this.bars[b];
71 | if (bar.hasClass(this.options.activeClass)) {
72 | bar.removeClass(this.options.activeClass);
73 | this.activeBar = null;
74 | }
75 | }
76 | }
77 | this.activeBar = null;
78 | };
79 | //基础
80 | export default Basebars;
--------------------------------------------------------------------------------
/src/js/cytoscapeHelper.js:
--------------------------------------------------------------------------------
1 | import listener from "./Listeners/cytoscapeListener";
2 | import { cytoscape, jquery } from "./lib";
3 | import { getClickType } from './utils/cy';
4 |
5 | const cyOption = {
6 | //container: allElements["cy"],
7 | // boxSelectionEnabled: false,
8 | // autounselectify: true,
9 | userZoomingEnabled: false,
10 | maxZoom: 9,
11 | zoom: 1,
12 | minZoom: 0.1,
13 | zoomDelay: 45,
14 | layout: {
15 | name: "preset"
16 | },
17 | style: [{
18 | selector: "node",
19 | style: {
20 | // shape: 'data(faveShape)',
21 | content: "data(label)",
22 | // width: 'mapData(weight, 40, 80, 20, 60)',
23 | "text-valign": "center"
24 | }
25 | },
26 | {
27 | selector: "node.fce-shape-ellipse",
28 | style: {
29 | shape: 'ellipse',
30 | }
31 | },
32 | {
33 | selector: "node.fce-shape-triangle",
34 | style: {
35 | shape: 'triangle',
36 | }
37 | },
38 | {
39 | selector: "node.fce-shape-rectangle",
40 | style: {
41 | shape: 'rectangle',
42 | }
43 | },
44 | {
45 | selector: "node.fce-shape-roundrectangle",
46 | style: {
47 | shape: 'roundrectangle',
48 | }
49 | },
50 | {
51 | selector: "node.fce-shape-bottomroundrectangle",
52 | style: {
53 | shape: 'bottomroundrectangle',
54 | }
55 | },
56 | {
57 | selector: "node.fce-shape-cutrectangle",
58 | style: {
59 | shape: 'cutrectangle',
60 | }
61 | },
62 | {
63 | selector: "node.fce-shape-barrel",
64 | style: {
65 | shape: 'barrel',
66 | }
67 | },
68 | {
69 | selector: "node.fce-shape-rhomboid",
70 | style: {
71 | shape: 'rhomboid',
72 | }
73 | },
74 | {
75 | selector: "node.fce-shape-diamond",
76 | style: {
77 | shape: 'diamond',
78 | }
79 | },
80 | {
81 | selector: "node.fce-shape-pentagon",
82 | style: {
83 | shape: 'pentagon',
84 | }
85 | },
86 | {
87 | selector: "node.fce-shape-hexagon",
88 | style: {
89 | shape: 'hexagon',
90 | }
91 | },
92 | {
93 | selector: "node.fce-shape-concavehexagon",
94 | style: {
95 | shape: 'concavehexagon',
96 | }
97 | },
98 | {
99 | selector: "node.fce-shape-heptagon",
100 | style: {
101 | shape: 'heptagon',
102 | }
103 | },
104 | {
105 | selector: "node.fce-shape-octagon",
106 | style: {
107 | shape: 'octagon',
108 | }
109 | },
110 | {
111 | selector: "node.fce-shape-star",
112 | style: {
113 | shape: 'star',
114 | }
115 | },
116 | {
117 | selector: "node.fce-shape-tag",
118 | style: {
119 | shape: 'tag',
120 | }
121 | },
122 | {
123 | selector: "node.fce-shape-vee",
124 | style: {
125 | shape: 'vee',
126 | }
127 | },
128 | {
129 | selector: "node.fce-shape-polygon",
130 | style: {
131 | shape: 'polygon',
132 | }
133 | },
134 | {
135 | selector: "node:selected",
136 | style: {
137 | "border-width": "6px",
138 | "border-color": "#AAD8FF",
139 | "border-opacity": "0.5",
140 | "background-color": "#77828C",
141 | "text-outline-color": "#77828C"
142 | }
143 | },
144 | {
145 | selector: "edge",
146 | style: {
147 | label: "data(label)",
148 | "font-size": 10,
149 | "curve-style": "bezier",
150 | "line-style": "solid", // solid, dotted, or dashed.
151 | "target-arrow-shape": "triangle"
152 | }
153 | },
154 | {
155 | selector: "edge:selected",
156 | style: {
157 | "border-width": "6px",
158 | "border-color": "#AAD8FF",
159 | "border-opacity": "0.5",
160 | "background-color": "yellow", // "#77828C",
161 | "text-outline-color": "#77828C"
162 | }
163 | }, {
164 | selector: '.eh-handle',
165 | style: {
166 | 'background-color': 'red',
167 | width: 10,
168 | height: 10,
169 | shape: 'ellipse',
170 | 'overlay-opacity': 0,
171 | 'border-width': 12, // makes the handle easier to hit
172 | 'border-opacity': 0
173 | }
174 | }
175 | ]
176 | };
177 | /**
178 | * 初始化cy对象
179 | * @param {Object} options 配置项
180 | */
181 | const initCy = function(options) {
182 | options = jquery.extend(true, cyOption, options);
183 | //右键配置加载
184 |
185 | const self = this,
186 | cy = new cytoscape(options),
187 | //默认右键配置
188 | rightMenus = [{
189 | id: "fce_rename",
190 | content: "重命名",
191 | tooltipText: "重命名",
192 | selector: "node,edge",
193 | onClickFunction: function(evt) {
194 | var target = evt.target || evt.cyTarget,
195 | clickType = getClickType(evt);
196 | self.fireEvent('context_menus_rename', evt, clickType, target.data());
197 | },
198 | hasTrailingDivider: true
199 | }, {
200 | id: "fce_delete",
201 | content: "删除",
202 | tooltipText: "删除",
203 | selector: "node,edge",
204 | onClickFunction: function(evt) {
205 | var target = evt.target || evt.cyTarget,
206 | clickType = getClickType(evt);
207 | self.fireEvent('context_menus_remove', evt, clickType, target.data());
208 | },
209 | hasTrailingDivider: true
210 | }];
211 |
212 | if (options && options.rightMenus && options.rightMenus.length > 0) {
213 | for (let i = 0, l = options.rightMenus.length; i < l; i++) {
214 | const notexist = options.rightMenus[i];
215 | let have = false;
216 | for (let h = 0, count = rightMenus.length; h < count; h++) {
217 | const exist = rightMenus[h];
218 | if (notexist.id === exist.id) {
219 | have = true;
220 | break;
221 | }
222 | }
223 | if (have) {
224 | console.error('已存在id=' + notexist.id + '相同的右键按钮!');
225 | } else {
226 | rightMenus.push(notexist);
227 | }
228 | }
229 | }
230 |
231 |
232 | const gridGuide = cy.gridGuide({
233 | //网格功能
234 | //snapToGridDuringDrag: true, //todo 为了操作的灵活性,暂时去掉对齐功能
235 | snapToAlignmentLocationOnRelease: true,
236 | snapToAlignmentLocationDuringDrag: true,
237 | centerToEdgeAlignment: true,
238 | guidelinesTolerance: true,
239 | guidelinesStyle: {
240 | strokeStyle: "red",
241 | horizontalDistColor: "#ff0000",
242 | verticalDistColor: "green",
243 | initPosAlignmentColor: "#0000ff"
244 | }
245 | }),
246 | //右键 contextMenus
247 | contextMenus = cy.contextMenus({
248 | menuItems: rightMenus
249 | }),
250 | //连线
251 | edgehandles = cy.edgehandles({
252 | preview: true,
253 | hoverDelay: 150,
254 | handleNodes: "node", //连线节点必须满足样式
255 | handlePosition: "middle",
256 | handleInDrawMode: false,
257 | edgeType: function(sourceNode, targetNode) {
258 | return "flat";
259 | },
260 | loopAllowed: function(node) {
261 | return false;
262 | },
263 | nodeLoopOffset: -50,
264 | nodeParams: function(sourceNode, targetNode) {
265 | return {};
266 | },
267 | edgeParams: function(sourceNode, targetNode, i) {},
268 | disable: function() {},
269 | enable: function() {},
270 | show: function() {},
271 | hide: function() {},
272 | start: function() {},
273 | stop: function() {},
274 | cancel: function() {},
275 | hoverover: function() {},
276 | hoverout: function() {},
277 | previewon: function() {},
278 | previewoff: function() {},
279 | drawon: function() {},
280 | drawoff: function() {},
281 | complete: function(sourceNode, targetNode, addedEles) {}
282 | }),
283 | //连线折叠
284 | edgeBendEditing = cy.edgeBendEditing({
285 | bendPositionsFunction: function(ele) {
286 | return ele.data("bendPointPositions");
287 | },
288 | initBendPointsAutomatically: true,
289 | undoable: true,
290 | bendShapeSizeFactor: 6,
291 | enabled: true,
292 | addBendMenuItemTitle: "添加弯曲点",
293 | removeBendMenuItemTitle: "移除弯曲点"
294 | }),
295 | nodeResize = cy.nodeResize({
296 | undoable: true
297 | }),
298 | //初始化撤销、重做
299 | undoRedo = cy.undoRedo({
300 | isDebug: false,
301 | actions: {},
302 | undoableDrag: true,
303 | stackSizeLimit: undefined,
304 | ready: function() {}
305 | }),
306 | viewUtilities = cy.viewUtilities({
307 | neighbor: function(node) {
308 | return node.closedNeighborhood();
309 | },
310 | neighborSelectTime: 1000
311 | });
312 | //默认取消连线扩展
313 | edgehandles.disable();
314 | self.cy = cy;
315 |
316 | self.cyExtensions = {
317 | gridGuide,
318 | undoRedo,
319 | edgehandles,
320 | edgeBendEditing,
321 | viewUtilities,
322 | contextMenus,
323 | nodeResize
324 | };
325 | listener.call(self);
326 | };
327 |
328 | export { initCy };
--------------------------------------------------------------------------------
/src/js/cytoscapeHelper.js.bak:
--------------------------------------------------------------------------------
1 | import listener from "./Listeners/cytoscapeListener";
2 | import { cytoscape, jquery } from "./lib";
3 | import { getClickType } from './utils/cy';
4 |
5 | const cyOption = {
6 | //container: allElements["cy"],
7 | // boxSelectionEnabled: false,
8 | // autounselectify: true,
9 | userZoomingEnabled: false,
10 | maxZoom: 9,
11 | zoom: 1,
12 | minZoom: 0.1,
13 | zoomDelay: 45,
14 | layout: {
15 | name: "preset"
16 | },
17 | style: [{
18 | selector: "node",
19 | style: {
20 | // shape: 'data(faveShape)',
21 | content: "data(label)",
22 | // width: 'mapData(weight, 40, 80, 20, 60)',
23 | "text-valign": "center"
24 | }
25 | },
26 | {
27 | selector: "node.fce-shape-ellipse",
28 | style: {
29 | shape: 'ellipse',
30 | }
31 | },
32 | {
33 | selector: "node.fce-shape-triangle",
34 | style: {
35 | shape: 'triangle',
36 | }
37 | },
38 | {
39 | selector: "node.fce-shape-rectangle",
40 | style: {
41 | shape: 'rectangle',
42 | }
43 | },
44 | {
45 | selector: "node.fce-shape-roundrectangle",
46 | style: {
47 | shape: 'roundrectangle',
48 | }
49 | },
50 | {
51 | selector: "node.fce-shape-bottomroundrectangle",
52 | style: {
53 | shape: 'bottomroundrectangle',
54 | }
55 | },
56 | {
57 | selector: "node.fce-shape-cutrectangle",
58 | style: {
59 | shape: 'cutrectangle',
60 | }
61 | },
62 | {
63 | selector: "node.fce-shape-barrel",
64 | style: {
65 | shape: 'barrel',
66 | }
67 | },
68 | {
69 | selector: "node.fce-shape-rhomboid",
70 | style: {
71 | shape: 'rhomboid',
72 | }
73 | },
74 | {
75 | selector: "node.fce-shape-diamond",
76 | style: {
77 | shape: 'diamond',
78 | }
79 | },
80 | {
81 | selector: "node.fce-shape-pentagon",
82 | style: {
83 | shape: 'pentagon',
84 | }
85 | },
86 | {
87 | selector: "node.fce-shape-hexagon",
88 | style: {
89 | shape: 'hexagon',
90 | }
91 | },
92 | {
93 | selector: "node.fce-shape-concavehexagon",
94 | style: {
95 | shape: 'concavehexagon',
96 | }
97 | },
98 | {
99 | selector: "node.fce-shape-heptagon",
100 | style: {
101 | shape: 'heptagon',
102 | }
103 | },
104 | {
105 | selector: "node.fce-shape-octagon",
106 | style: {
107 | shape: 'octagon',
108 | }
109 | },
110 | {
111 | selector: "node.fce-shape-star",
112 | style: {
113 | shape: 'star',
114 | }
115 | },
116 | {
117 | selector: "node.fce-shape-tag",
118 | style: {
119 | shape: 'tag',
120 | }
121 | },
122 | {
123 | selector: "node.fce-shape-vee",
124 | style: {
125 | shape: 'vee',
126 | }
127 | },
128 | {
129 | selector: "node.fce-shape-polygon",
130 | style: {
131 | shape: 'polygon',
132 | }
133 | },
134 | {
135 | selector: "node:selected",
136 | style: {
137 | "border-width": "6px",
138 | "border-color": "#AAD8FF",
139 | "border-opacity": "0.5",
140 | "background-color": "#77828C",
141 | "text-outline-color": "#77828C"
142 | }
143 | },
144 | {
145 | selector: "edge",
146 | style: {
147 | label: "data(label)",
148 | "font-size": 10,
149 | "curve-style": "bezier",
150 | "line-style": "solid", // solid, dotted, or dashed.
151 | "target-arrow-shape": "triangle"
152 | }
153 | },
154 | {
155 | selector: "edge:selected",
156 | style: {
157 | "border-width": "6px",
158 | "border-color": "#AAD8FF",
159 | "border-opacity": "0.5",
160 | "background-color": "yellow", // "#77828C",
161 | "text-outline-color": "#77828C"
162 | }
163 | }, {
164 | selector: '.eh-handle',
165 | style: {
166 | 'background-color': 'red',
167 | width: 10,
168 | height: 10,
169 | shape: 'ellipse',
170 | 'overlay-opacity': 0,
171 | 'border-width': 12, // makes the handle easier to hit
172 | 'border-opacity': 0
173 | }
174 | }
175 | ]
176 | };
177 | /**
178 | * 初始化cy对象
179 | * @param {Object} options 配置项
180 | */
181 | const initCy = function(options) {
182 | options = jquery.extend(true, cyOption, options);
183 | //右键配置加载
184 |
185 | const self = this,
186 | cy = new cytoscape(options),
187 | //默认右键配置
188 | rightMenus = [{
189 | id: "fce_rename",
190 | content: "重命名",
191 | tooltipText: "重命名",
192 | selector: "node,edge",
193 | onClickFunction: function(evt) {
194 | var target = evt.target || evt.cyTarget,
195 | clickType = getClickType(evt);
196 | self.fireEvent('context_menus_rename', evt, clickType, target.data());
197 | },
198 | hasTrailingDivider: true
199 | }, {
200 | id: "fce_delete",
201 | content: "删除",
202 | tooltipText: "删除",
203 | selector: "node,edge",
204 | onClickFunction: function(evt) {
205 | var target = evt.target || evt.cyTarget,
206 | clickType = getClickType(evt);
207 | self.fireEvent('context_menus_remove', evt, clickType, target.data());
208 | },
209 | hasTrailingDivider: true
210 | }];
211 |
212 | if (options && options.rightMenus && options.rightMenus.length > 0) {
213 | for (let i = 0, l = options.rightMenus.length; i < l; i++) {
214 | const notexist = options.rightMenus[i];
215 | let have = false;
216 | for (let h = 0, count = rightMenus.length; h < count; h++) {
217 | const exist = rightMenus[h];
218 | if (notexist.id === exist.id) {
219 | have = true;
220 | break;
221 | }
222 | }
223 | if (have) {
224 | console.error('已存在id=' + notexist.id + '相同的右键按钮!');
225 | } else {
226 | rightMenus.push(notexist);
227 | }
228 | }
229 | }
230 |
231 |
232 | const gridGuide = cy.gridGuide({
233 | //网格功能
234 | //snapToGridDuringDrag: true, //todo 为了操作的灵活性,暂时去掉对齐功能
235 | snapToAlignmentLocationOnRelease: true,
236 | snapToAlignmentLocationDuringDrag: true,
237 | centerToEdgeAlignment: true,
238 | guidelinesTolerance: true,
239 | guidelinesStyle: {
240 | strokeStyle: "red",
241 | horizontalDistColor: "#ff0000",
242 | verticalDistColor: "green",
243 | initPosAlignmentColor: "#0000ff"
244 | }
245 | }),
246 | //右键 contextMenus
247 | contextMenus = cy.contextMenus({
248 | menuItems: rightMenus
249 | }),
250 | //连线
251 | edgehandles = cy.edgehandles({
252 | preview: true,
253 | hoverDelay: 150,
254 | handleNodes: "node", //连线节点必须满足样式
255 | handlePosition: "middle",
256 | handleInDrawMode: false,
257 | edgeType: function(sourceNode, targetNode) {
258 | return "flat";
259 | },
260 | loopAllowed: function(node) {
261 | return false;
262 | },
263 | nodeLoopOffset: -50,
264 | nodeParams: function(sourceNode, targetNode) {
265 | return {};
266 | },
267 | edgeParams: function(sourceNode, targetNode, i) {},
268 | disable: function() {},
269 | enable: function() {},
270 | show: function() {},
271 | hide: function() {},
272 | start: function() {},
273 | stop: function() {},
274 | cancel: function() {},
275 | hoverover: function() {},
276 | hoverout: function() {},
277 | previewon: function() {},
278 | previewoff: function() {},
279 | drawon: function() {},
280 | drawoff: function() {},
281 | complete: function(sourceNode, targetNode, addedEles) {}
282 | }),
283 | //连线折叠
284 | edgeBendEditing = cy.edgeBendEditing({
285 | bendPositionsFunction: function(ele) {
286 | return ele.data("bendPointPositions");
287 | },
288 | initBendPointsAutomatically: true,
289 | undoable: true,
290 | bendShapeSizeFactor: 6,
291 | enabled: true,
292 | addBendMenuItemTitle: "添加弯曲点",
293 | removeBendMenuItemTitle: "移除弯曲点"
294 | }),
295 | nodeResize = cy.nodeResize({
296 | undoable: true
297 | }),
298 | //初始化撤销、重做
299 | undoRedo = cy.undoRedo({
300 | isDebug: false,
301 | actions: {},
302 | undoableDrag: true,
303 | stackSizeLimit: undefined,
304 | ready: function() {}
305 | }),
306 | viewUtilities = cy.viewUtilities({
307 | neighbor: function(node) {
308 | return node.closedNeighborhood();
309 | },
310 | neighborSelectTime: 1000
311 | });
312 | //默认取消连线扩展
313 | edgehandles.disable();
314 | self.cy = cy;
315 |
316 | self.cyExtensions = {
317 | gridGuide,
318 | undoRedo,
319 | edgehandles,
320 | edgeBendEditing,
321 | viewUtilities,
322 | contextMenus,
323 | nodeResize
324 | };
325 | listener.call(self);
326 | };
327 |
328 | export { initCy };
--------------------------------------------------------------------------------
/src/js/defaultOptions.js:
--------------------------------------------------------------------------------
1 | // const toolbarOption = {
2 | // // 不写默认使用fce自带的render方法
3 | // render: function() {
4 | // return document.createElement("div");
5 | // },
6 | // icon: {
7 | // src: "img/xxx.png",
8 | // width: 12,
9 | // height: 12
10 | // },
11 | // classes: "", // 样式
12 | // isShow() {},
13 | // hide() {},
14 | // show() {},
15 | // addClass(_class) {},
16 | // hasClass(_class) {},
17 | // removeClass(_class) {},
18 | // fce: null, // 这里是fce的指针
19 | // id: "point",
20 | // title: "指针",
21 | // onclick: function() {
22 | // // 这里的this是当前bar
23 | // }
24 | // };
25 | /**
26 | * 默认配置信息
27 | */
28 | const defaultOptions = {
29 | el: null,
30 | mode: "DESIGN",
31 | ready() {
32 | console.log("fce加载完成!");
33 | },
34 | renderFooter: function() {
35 | // footer内容,可以自定义
36 | },
37 | rightMenus: [], // 右键配置 默认没有
38 | toolbars: [] // toolbar配置 默认没有
39 | };
40 | export { defaultOptions };
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | //"use strict";
2 | require("../css/default.scss");
3 | import { defaultOptions } from "./defaultOptions";
4 | import utils from "./utils/index";
5 | import { jquery } from "./lib";
6 | import fceDom from "./core/Dom";
7 | import Zoom from "./core/Zoom";
8 | import Navbars from "./core/Navbars";
9 | import Toolbars from "./core/Toolbars";
10 | import { initCy } from "./cytoscapeHelper";
11 | import Listener from "./core/Listener";
12 | import navbarsListener from "./Listeners/navbarsListener";
13 | import { zoomChange, initZoomListener } from "./Listeners/zoomListener";
14 |
15 | /**
16 | * 缩放组件
17 | * @param {*} options
18 | */
19 |
20 | const FCE = function(options) {
21 | const opt = jquery.extend(true, defaultOptions, options);
22 | if (!opt || !opt.el) {
23 | console.log("页面中不存在用于承载fce对象的dom元素");
24 | return;
25 | } else if (opt.el && typeof opt.el === "string") {
26 | opt.el = document.querySelector("#" + opt.el);
27 | }
28 | Object.assign(this, { __private__: { allListeners: {} } });
29 | const self = this,
30 | allElements = fceDom(opt.el), //所有的结构化的element元素
31 | zoom = new Zoom(),
32 | _navbars = new Navbars(),
33 | _toolbars = new Toolbars(opt.toolbars);
34 | allElements["toolbar"].appendChild(_toolbars.dom);
35 | _toolbars.render();
36 | allElements["zoom"].appendChild(zoom.dom);
37 | allElements["zoom"].appendChild(_navbars.dom);
38 | _navbars.render();
39 | if (_navbars.options.activeName) {
40 | _navbars.setActiveBar(_navbars.options.activeName);
41 | }
42 | //__private__ 表示不希望用户操作的对象
43 | self.__private__.allElements = allElements;
44 | self.__private__.options = opt;
45 | Object.assign(self.__private__.allListeners, {
46 | add_click: [],
47 | context_menus_rename: [], //右键重命名
48 | context_menus_remove: []
49 | });
50 | initCy.call(self, {
51 | container: allElements["cy"],
52 | rightMenus: options.rightMenus || []
53 | });
54 | navbarsListener.call(self, _navbars);
55 | initZoomListener.call(self, zoom);
56 | zoomChange.call(this, zoom.getCyZoom());
57 | self.zoom = zoom;
58 | _toolbars.fce = self;
59 | self.toolbars = _toolbars;
60 | _navbars.fce = self;
61 | self.navbars = _navbars;
62 | };
63 | // utils.forEachObject(Listener, function(item, key) {
64 | // FCE.prototype[key] = item;
65 | // });
66 | Object.assign(FCE.prototype, Listener, {
67 | /**
68 | * 根据id查找toolbar对象
69 | * @param {String} id id
70 | */
71 | getToolbarById(id = '测试') {
72 | if (!id) return;
73 | const current = this.__private__.allElements.toolbar.querySelector("#" + id);
74 | const name = current ? current.getAttribute("name") : null;
75 | if (!name) return;
76 | return this.getToolbarByName(name);
77 | },
78 | /**
79 | * 根据name获取到toolbar
80 | * @param {String} name
81 | */
82 | getToolbarByName(name) {
83 | return this.toolbars.getBarByName(name);
84 | },
85 | /**
86 | * 根据id获取编辑器中的元素
87 | * @param {String} id
88 | */
89 | getElementById(id) {
90 | if (!id) return;
91 | return this.cy.getElementById(id);
92 | },
93 | /**
94 | * 添加元素
95 | * @param {Object} opt {data:{}}
96 | */
97 | add(opt) {
98 | this.cy.add(opt);
99 | },
100 | /**
101 | * 设置模式
102 | * @param {String} mode 可选,如果为空则为获取当前模式,否则就是设置为指定模式。READONLY只读--查看、DESIGN设计--可编辑
103 | */
104 | mode(mode) {
105 | if (mode) {
106 | mode = utils.trim(mode).toUpperCase();
107 | //todo 修改模式
108 | this.__private__.options.mode = mode;
109 | } else {
110 | // 获取当前模式
111 | return this.__private__.options.mode;
112 | }
113 | },
114 | /**
115 | * 添加node
116 | * @param {Object} opt
117 | */
118 | addNode(data, type) {
119 | const def = {
120 | group: "nodes",
121 | position: this.mouseClickPosition,
122 | classes: "fce-shape-" + type
123 | };
124 | this.add(jquery.extend(true, def, { data: data }));
125 | },
126 | /**
127 | * 添加线条
128 | * @param {Object} opt
129 | */
130 | addEdge(data) {
131 | const def = { group: "edges" };
132 | this.add(jquery.extend(true, def, { data: data }));
133 | },
134 | /**
135 | * 重命名元素
136 | * @param {Object} data {id:'id',label:'label'}
137 | */
138 | rename(data) {
139 | this.cy.$("#" + data.id).data("label", data.label);
140 | },
141 | /**
142 | * 删除元素
143 | * @param {String} id
144 | */
145 | remove(id) {
146 | this.cy.$("#" + id).remove();
147 | },
148 | /**
149 | * 导入json
150 | * @param {String} json 导出json
151 | */
152 | import (json) {
153 | json = (typeof json === 'string') ? JSON.parse(json) : json;
154 | this.cy.json({
155 | elements: json.elements,
156 | //style: json.style,
157 | zoom: json.zoom,
158 | // pan: json.pan,
159 | // zoomingEnabled:json.zoomingEnabled,
160 | // userZoomingEnabled:json.userZoomingEnabled,
161 | // panningEnabled:json.panningEnabled,
162 | // userPanningEnabled:json.userPanningEnabled,
163 | // boxSelectionEnabled:json.boxSelectionEnabled,
164 | // autolock:json.autolock,
165 | // autoungrabify:json.autoungrabify,
166 | // autounselectify:json.autounselectify
167 | });
168 | },
169 | /**
170 | * 重做
171 | */
172 | redo() {
173 | this.cyExtensions.undoRedo.redo();
174 | },
175 | /**
176 | * 撤销
177 | */
178 | undo() {
179 | this.cyExtensions.undoRedo.undo();
180 | },
181 | /**
182 | * 导出文件
183 | * @param {String} type 文件类型 png、jpg、json默认为json
184 | * @param {String} fileName 文件名 默认以当前时间为文件名
185 | */
186 | exportFile(type, fileName = new Date().toJSON()) {
187 | type = utils.trim(type || "json").toLowerCase();
188 | let data;
189 | switch (type) {
190 | case "png":
191 | data = this.cy.png({ full: true, quality: 1 }); //完整内容
192 | break;
193 | case "jpg":
194 | data = this.cy.jpg({ full: true, quality: 1 });
195 | break;
196 | case "json":
197 | {
198 | const json = this.cy.json();
199 | this.navbars.setNavActiveBar('');
200 | //todo 添加动画内容
201 | data =
202 | "data:text/plain;charset=utf-8," +
203 | encodeURIComponent(JSON.stringify(json));
204 | }
205 | break;
206 | }
207 | const a = document.createElement("a");
208 | a.setAttribute("download", fileName + "." + type);
209 | a.setAttribute("href", data);
210 | a.click();
211 | },
212 | /**
213 | * 注销
214 | */
215 | destroy() {
216 | this.__private__.allElements.root.remove();
217 | this.cy.destroy();
218 | },
219 | /**
220 | * 隐藏
221 | */
222 | hide() {
223 | this.__private__.allElements.root.style.display = "none";
224 | },
225 | /**
226 | * 显示
227 | */
228 | show() {
229 | this.__private__.allElements.root.style.display = "block";
230 | }
231 | });
232 |
233 | const obj = { name: 'tongling' };
234 | const newObj = Object.assign({}, obj, { age: 22 });
235 | console.log(newObj);
236 |
237 | if (process.env.NODE_ENV === "dev" || process.env.NODE_ENV === "prod") {
238 | window.FCE = FCE;
239 | } else {
240 | module.exports = module.exports.default = FCE;
241 | }
--------------------------------------------------------------------------------
/src/js/lib.js:
--------------------------------------------------------------------------------
1 | let cytoscape, jquery;
2 | if (process.env.NODE_ENV === "prod") {
3 | cytoscape = window.cytoscape;
4 | jquery = window.jQuery;
5 | } else {
6 | require("../../static/css/cytoscape-context-menus.css");
7 | cytoscape = require("cytoscape");
8 | jquery = require("jquery");
9 | const cytoscape_grid = require("cytoscape-grid-guide");
10 | const edgehandles = require("cytoscape-edgehandles");
11 | const contextMenus = require("cytoscape-context-menus");
12 | const edgeBendEditing = require("cytoscape-edge-bend-editing");
13 | const undoRedo = require("cytoscape-undo-redo");
14 | const viewUtilities = require("cytoscape-view-utilities");
15 | const nodeResize = require("cytoscape-node-resize");
16 | const konva = require("konva");
17 | cytoscape_grid(cytoscape, jquery);
18 | contextMenus(cytoscape, jquery);
19 | edgeBendEditing(cytoscape, jquery);
20 | undoRedo(cytoscape);
21 | viewUtilities(cytoscape, jquery);
22 | nodeResize(cytoscape, jquery, konva);
23 | cytoscape.use(edgehandles, jquery);
24 | }
25 |
26 | export { jquery, cytoscape };
--------------------------------------------------------------------------------
/src/js/utils/cy.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 获取点击元素类型。node元素,line连线,空则为空白
3 | * @param {Event} evt
4 | */
5 | const getClickType = function(evt) {
6 | if (!evt || !evt.target) {
7 | return "";
8 | }
9 | const target = evt.target || cyTarget;
10 | if (!target) return "";
11 | else if (target.isNode && target.isNode()) return "node";
12 | else if (target.isEdge && target.isEdge()) return "line";
13 | else return "";
14 | };
15 |
16 | export { getClickType };
--------------------------------------------------------------------------------
/src/js/utils/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | /**
3 | * 去除空格
4 | * @param {String} str 需要去除空格的字符串
5 | */
6 | trim(str) {
7 | return str ? str.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g, "") : "";
8 | },
9 | /**
10 | * 将字符串按照规定切割符分割为数组
11 | * @param {String} str 数组
12 | * @param {String} splitStr 分隔符
13 | */
14 | classNamesToArray(str, splitStr = /\s+/) {
15 | return this.trim(str).split(splitStr);
16 | },
17 | /**
18 | * 阻止事件默认行为(包括事件冒泡)
19 | * @param {Event} evt 事件对象
20 | */
21 | preventDefault(evt) {
22 | evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false);
23 | evt.stopPropagation();
24 | },
25 | /**
26 | * 循环方法
27 | * @param {Array} arr 数组
28 | * @param {Function} handler 如果返回true,则中断不予返回
29 | */
30 | forEach(arr, handler) {
31 | if (!arr || !arr.length) return;
32 | for (let i = 0, l = arr.length; i < l; i++) {
33 | const item = arr[i];
34 | if (handler) {
35 | if (handler(item, i)) {
36 | return;
37 | }
38 | }
39 | }
40 | },
41 | /**
42 | * 循环对象
43 | * @param {Object} obj 被循环对象
44 | * @param {Function} handler 回调函数,如返回true,则停止循环
45 | */
46 | forEachObject(obj, handler) {
47 | if (!obj) return;
48 | for (let key in obj) {
49 | const item = obj[key];
50 | if (handler) {
51 | if (handler(item, key)) {
52 | return;
53 | }
54 | }
55 | }
56 | },
57 | /**
58 | * 想让找到符合要求的element元素
59 | * @param {Element} ele 元素
60 | * @param {String} classNames 样式
61 | */
62 | findParentElement(ele, classNames) {
63 | if (!ele) return null;
64 | classNames =
65 | typeof classNames === "string" ?
66 | this.trim(classNames).split(/\s+/) :
67 | classNames;
68 | let bo = false;
69 | this.forEach(classNames, item => {
70 | if (!bo && ele && ele.classList && ele.classList.contains(item)) {
71 | bo = true;
72 | }
73 | return bo;
74 | });
75 | if (bo) {
76 | return ele;
77 | }
78 | if (ele && ele.parentElement && ele.parentElement.nodeName !== "BODY") {
79 | return this.findParentElement(ele.parentElement, classNames);
80 | }
81 | },
82 | /**
83 | * 注册事件
84 | * @param {Element} ele
85 | * @param {String} type
86 | * @param {Function} handler
87 | * @param {Object} params
88 | */
89 | registerEvent(ele, type, handler, params) {
90 | if (!ele) {
91 | console.error("绑定事件时Element实例为空!");
92 | return;
93 | }
94 | if (ele.addEventListener) {
95 | ele.addEventListener(type, handler, params, false);
96 | } else if (ele.attachEvent) {
97 | // 如果支持attachEvent 就使用attachEvent去注册事件
98 | ele.attachEvent("on" + type, handler, params);
99 | } else {
100 | // 如果 attachEvent 和 addEventListner都不支持 就是用 onclick这种方式
101 | ele["on" + type] = handler;
102 | }
103 | },
104 | /**
105 | * 移除事件
106 | * @param {Element} ele
107 | * @param {String} type
108 | * @param {Function} handler
109 | */
110 | removeEvent(ele, type, handler) {
111 | if (ele.addEventListener) {
112 | ele.removeEventListener(type, handler, false);
113 | } else if (ele.attachEvent) {
114 | // 如果支持attachEvent 就使用attachEvent去注册事件
115 | ele.detachEvent("on" + type, handler);
116 | } else {
117 | // 如果 attachEvent 和 addEventListner都不支持 就是用 onclick这种方式
118 | ele["on" + type] = null;
119 | }
120 | }
121 | };
--------------------------------------------------------------------------------
/static/css/cytoscape-context-menus.css:
--------------------------------------------------------------------------------
1 | .cy-context-menus-cxt-menu {
2 | display:none;
3 | z-index:1000;
4 | position:absolute;
5 | border:1px solid #A0A0A0;
6 | padding: 0;
7 | margin: 0;
8 | width:auto;
9 | }
10 |
11 | .cy-context-menus-cxt-menuitem {
12 | display:block;
13 | z-index:1000;
14 | width: 100%;
15 | padding: 3px 20px;
16 | position:relative;
17 | margin:0;
18 | background-color:#f8f8f8;
19 | font-weight:normal;
20 | font-size: 12px;
21 | white-space:nowrap;
22 | border: 0;
23 | text-align: left;
24 | }
25 |
26 | .cy-context-menus-cxt-menuitem:enabled {
27 | color: #000000;
28 | }
29 |
30 | .cy-context-menus-ctx-operation:focus {
31 | outline: none;
32 | }
33 |
34 | .cy-context-menus-cxt-menuitem:hover {
35 | color: #ffffff;
36 | text-decoration: none;
37 | background-color: #0B9BCD;
38 | background-image: none;
39 | cursor: pointer;
40 | }
41 |
42 | .cy-context-menus-cxt-menuitem[content]:before {
43 | content:attr(content);
44 | }
45 |
46 | .cy-context-menus-divider {
47 | border-bottom:1px solid #A0A0A0;
48 | }
49 |
--------------------------------------------------------------------------------
/static/demo.js:
--------------------------------------------------------------------------------
1 | var fce;
2 | window.onload = function() {
3 | fce = new FCE({
4 | el: document.getElementById('fce'),
5 | rightMenus: [{
6 | id: "id_alert",
7 | content: "弹出窗",
8 | tooltipText: "弹出窗",
9 | selector: "node,edge", //当在node,edge元素上右键时才显示
10 | onClickFunction: function(evt) { //点击后触发事件
11 | var target = evt.target || evt.cyTarget;
12 | alert('弹出信息!');
13 | },
14 | hasTrailingDivider: true
15 | }],
16 | toolbars: [{
17 | name: 'rectangle',
18 | icon: 'images/rectangle.png',
19 | className: '',
20 | title: '矩形',
21 | exec: function(evt, clickType, obj) {
22 | const label = prompt('请输入节点名称:'),
23 | data = { id: new Date().getTime(), label: label };
24 | if (!label) return;
25 | if (clickType === 'node') {
26 | data.parent = obj.id;
27 | }
28 | this.addNode(data, 'rectangle');
29 | },
30 | },
31 | {
32 | name: 'rounded_rectangle',
33 | icon: 'images/rounded_rectangle.png',
34 | className: '',
35 | title: '圆角矩形',
36 | exec: function(evt, clickType, obj) {
37 | const label = prompt('请输入节点名称:'),
38 | data = { id: new Date().getTime(), label: label };
39 | if (!label) return;
40 | if (clickType === 'node') {
41 | data.parent = obj.id;
42 | }
43 | this.addNode(data, 'roundrectangle');
44 | },
45 | },
46 | {
47 | name: 'choice',
48 | icon: 'images/choice.png',
49 | className: '',
50 | title: '菱形',
51 | exec: function(evt, clickType, obj) {
52 | const label = prompt('请输入节点名称:'),
53 | data = { id: new Date().getTime(), label: label };
54 | if (!label) return;
55 | if (clickType === 'node') {
56 | data.parent = obj.id;
57 | }
58 | this.addNode(data, 'diamond');
59 | },
60 | },
61 | {
62 | name: 'round',
63 | icon: 'images/round.png',
64 | className: '',
65 | title: '圆形',
66 | exec: function(evt, clickType, obj) {
67 | const label = prompt('请输入节点名称:'),
68 | data = { id: new Date().getTime(), label: label };
69 | if (!label) return;
70 | if (clickType === 'node') {
71 | data.parent = obj.id;
72 | }
73 | this.addNode(data, 'ellipse');
74 | },
75 | },
76 | {
77 | name: 'download-json',
78 | icon: 'images/download.png',
79 | className: '',
80 | title: '下载json文件',
81 | click: function(bar) {
82 | this.exportFile('json', '导出JSON文件');
83 | bar.cancelActive(); //取消自身选中
84 | },
85 | },
86 | {
87 | name: 'download-png',
88 | icon: 'images/download.png',
89 | className: '',
90 | title: '下载png文件',
91 | click: function(bar) {
92 | this.exportFile('png');
93 | bar.cancelActive(); //取消自身选中
94 | },
95 | },
96 | {
97 | name: 'download-jpg',
98 | icon: 'images/download.png',
99 | className: '',
100 | title: '下载jpg文件',
101 | click: function(bar) {
102 | this.exportFile('jpg');
103 | bar.cancelActive(); //取消自身选中
104 | },
105 | },
106 |
107 | {
108 | name: 'import',
109 | icon: 'images/import.png',
110 | className: '',
111 | title: '导入JSON文件',
112 | click: function(bar) {
113 | bar.cancelActive(); //取消自身选中
114 | var file = document.createElement('input'),
115 | self = this;
116 | file.setAttribute('type', 'file');
117 | file.onchange = function(evt) {
118 | var target = evt.target;
119 | if (target.files && target.files.length) {
120 | var fileInfo = target.files[0],
121 | name = fileInfo.name;
122 | if (!name.toLowerCase().endsWith('.json')) {
123 | alert('上传文件类型不符合要求!');
124 | } else {
125 | var reader = new FileReader();
126 | reader.onload = function(evt) {
127 | var json = JSON.parse(evt.target.result.toString());
128 | self.import(json);
129 | };
130 | reader.readAsText(fileInfo);
131 | }
132 | }
133 | };
134 | file.click();
135 | // this.import(json);
136 | // bar.cancelActive(); //取消自身选中
137 | },
138 | },
139 | 'animation',
140 | ],
141 | });
142 | fce.addListener('add_click', function() {
143 | debugger;
144 | console.log('编辑器被点击!');
145 | });
146 | fce.addListener('context_menus_rename', function(evt, clickType, data) {
147 | const label = prompt('请输入节点新名称:', data.label);
148 | if (label) {
149 | data.label = label;
150 | this.rename(data);
151 | }
152 | });
153 | fce.addListener('context_menus_remove', function(evt, clickType, data) {
154 | if (confirm('您确定要删除该节点吗?')) {
155 | debugger;
156 | this.remove(data.id);
157 | }
158 | });
159 | };
160 |
161 | // var fce
162 | // window.onload = function() {
163 | // fce = new FCE({
164 | // rightMenu: [{//右键菜单
165 |
166 | // }],
167 | // toolbars: [{
168 | // //不写默认使用fce自带的render方法
169 | // render: function() {
170 | // return document.createElement('div')
171 | // },
172 | // icon: {
173 | // src: "img/xxx.png",
174 | // width: 12,
175 | // height: 12,
176 | // },
177 | // class: '', //样式
178 |
179 | // fce: null, //这里是fce的指针
180 | // id: 'point',
181 | // title: "指针",
182 | // onclick: function() {
183 | // //这里的this是当前bar
184 | // }
185 | // }]
186 | // })
187 | // window.fce = fce
188 | // }
189 |
190 | // var bar = fce.getToolbarById('id') //根据id获取组件
191 | // bar.isShow() //true/false
192 | // bar.hide()
193 | // bar.show()
194 | // bar.addClass()
195 | // bar.removeClass() //空则为移除所有样式
196 | // //可以通过fire触发某事件,通过fce.on绑定某事件
197 | // fce.on('click', function() {
198 | // //绑定事件
199 | // })
--------------------------------------------------------------------------------
/static/images/choice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/choice.png
--------------------------------------------------------------------------------
/static/images/create_file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/create_file.png
--------------------------------------------------------------------------------
/static/images/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/download.png
--------------------------------------------------------------------------------
/static/images/eventbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/eventbar.png
--------------------------------------------------------------------------------
/static/images/import.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/import.png
--------------------------------------------------------------------------------
/static/images/line-dotted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/line-dotted.png
--------------------------------------------------------------------------------
/static/images/rectangle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/rectangle.png
--------------------------------------------------------------------------------
/static/images/redo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/redo.png
--------------------------------------------------------------------------------
/static/images/remove.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/remove.png
--------------------------------------------------------------------------------
/static/images/round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/round.png
--------------------------------------------------------------------------------
/static/images/rounded_rectangle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/rounded_rectangle.png
--------------------------------------------------------------------------------
/static/images/save.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/save.png
--------------------------------------------------------------------------------
/static/images/undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tlzzu/flow-chart-editor/f70f53a9b354b41fb495f925c42564b3c1916993/static/images/undo.png
--------------------------------------------------------------------------------
/static/js/lib/cytoscape-context-menus.js:
--------------------------------------------------------------------------------
1 | ;(function(){ 'use strict';
2 |
3 | var $ = typeof jQuery === typeof undefined ? null : jQuery;
4 |
5 | var register = function( cytoscape, $ ){
6 |
7 | if( !cytoscape ){ return; } // can't register if cytoscape unspecified
8 |
9 | var defaults = {
10 | // List of initial menu items
11 | menuItems: [
12 | /*
13 | {
14 | id: 'remove',
15 | content: 'remove',
16 | tooltipText: 'remove',
17 | selector: 'node, edge',
18 | onClickFunction: function () {
19 | console.log('remove element');
20 | },
21 | hasTrailingDivider: true
22 | },
23 | {
24 | id: 'hide',
25 | content: 'hide',
26 | tooltipText: 'remove',
27 | selector: 'node, edge',
28 | onClickFunction: function () {
29 | console.log('hide element');
30 | },
31 | disabled: true
32 | }*/
33 | ],
34 | // css classes that menu items will have
35 | menuItemClasses: [
36 | // add class names to this list
37 | ],
38 | // css classes that context menu will have
39 | contextMenuClasses: [
40 | // add class names to this list
41 | ]
42 | };
43 |
44 | var eventCyTapStart; // The event to be binded on tap start
45 |
46 | // To initialize with options.
47 | cytoscape('core', 'contextMenus', function (opts) {
48 | var cy = this;
49 |
50 | // Initilize scratch pad
51 | if (!cy.scratch('cycontextmenus')) {
52 | cy.scratch('cycontextmenus', {});
53 | }
54 |
55 | var options = getScratchProp('options');
56 | var $cxtMenu = getScratchProp('cxtMenu');
57 | var menuItemCSSClass = 'cy-context-menus-cxt-menuitem';
58 | var dividerCSSClass = 'cy-context-menus-divider';
59 |
60 | // Merge default options with the ones coming from parameter
61 | function extend(defaults, options) {
62 | var obj = {};
63 |
64 | for (var i in defaults) {
65 | obj[i] = defaults[i];
66 | }
67 |
68 | for (var i in options) {
69 | obj[i] = options[i];
70 | }
71 |
72 | return obj;
73 | };
74 |
75 | function getScratchProp(propname) {
76 | return cy.scratch('cycontextmenus')[propname];
77 | };
78 |
79 | function setScratchProp(propname, value) {
80 | cy.scratch('cycontextmenus')[propname] = value;
81 | };
82 |
83 | function preventDefaultContextTap() {
84 | $(".cy-context-menus-cxt-menu").contextmenu( function() {
85 | return false;
86 | });
87 | }
88 |
89 | // Get string representation of css classes
90 | function getMenuItemClassStr(classes, hasTrailingDivider) {
91 | var str = getClassStr(classes);
92 |
93 | str += ' ' + menuItemCSSClass;
94 |
95 | if(hasTrailingDivider) {
96 | str += ' ' + dividerCSSClass;
97 | }
98 |
99 | return str;
100 | }
101 |
102 | // Get string representation of css classes
103 | function getClassStr(classes) {
104 | var str = '';
105 |
106 | for( var i = 0; i < classes.length; i++ ) {
107 | var className = classes[i];
108 | str += className;
109 | if(i !== classes.length - 1) {
110 | str += ' ';
111 | }
112 | }
113 |
114 | return str;
115 | }
116 |
117 | function displayComponent($component) {
118 | $component.css('display', 'block');
119 | }
120 |
121 | function hideComponent($component) {
122 | $component.css('display', 'none');
123 | }
124 |
125 | function hideMenuItemComponents() {
126 | $cxtMenu.children().css('display', 'none');
127 | }
128 |
129 | function bindOnClickFunction($component, onClickFcn) {
130 | var callOnClickFcn;
131 |
132 | $component.on('click', callOnClickFcn = function() {
133 | onClickFcn(getScratchProp('currentCyEvent'));
134 | });
135 |
136 | $component.data('call-on-click-function', callOnClickFcn);
137 | }
138 |
139 | function bindCyCxttap($component, selector, coreAsWell) {
140 | function _cxtfcn(event) {
141 | setScratchProp('currentCyEvent', event);
142 | adjustCxtMenu(event); // adjust the position of context menu
143 | if ($component.data('show')) {
144 | // Now we have a visible element display context menu if it is not visible
145 | if (!$cxtMenu.is(':visible')) {
146 | displayComponent($cxtMenu);
147 | }
148 | // anyVisibleChild indicates if there is any visible child of context menu if not do not show the context menu
149 | setScratchProp('anyVisibleChild', true);// there is visible child
150 | displayComponent($component); // display the component
151 | }
152 |
153 | // If there is no visible element hide the context menu as well(If it is visible)
154 | if (!getScratchProp('anyVisibleChild') && $cxtMenu.is(':visible')) {
155 | hideComponent($cxtMenu);
156 | }
157 | }
158 |
159 | var cxtfcn;
160 | var cxtCoreFcn;
161 |
162 | if(coreAsWell) {
163 | cy.on('cxttap', cxtCoreFcn = function(event) {
164 | var target = event.target || event.cyTarget;
165 | if( target != cy ) {
166 | return;
167 | }
168 |
169 | _cxtfcn(event);
170 | });
171 | }
172 |
173 | if(selector) {
174 | cy.on('cxttap', selector, cxtfcn = function(event) {
175 | _cxtfcn(event);
176 | });
177 | }
178 |
179 | // Bind the event to menu item to be able to remove it back
180 | $component.data('cy-context-menus-cxtfcn', cxtfcn);
181 | $component.data('cy-context-menus-cxtcorefcn', cxtCoreFcn);
182 | }
183 |
184 | function bindCyEvents() {
185 | cy.on('tapstart', eventCyTapStart = function(){
186 | hideComponent($cxtMenu);
187 | setScratchProp('cxtMenuPosition', undefined);
188 | setScratchProp('currentCyEvent', undefined);
189 | });
190 | }
191 |
192 | function performBindings($component, onClickFcn, selector, coreAsWell) {
193 | bindOnClickFunction($component, onClickFcn);
194 | bindCyCxttap($component, selector, coreAsWell);
195 | }
196 |
197 | // Adjusts context menu if necessary
198 | function adjustCxtMenu(event) {
199 | var currentCxtMenuPosition = getScratchProp('cxtMenuPosition');
200 | var cyPos = event.position || event.cyPosition;
201 |
202 | if( currentCxtMenuPosition != cyPos ) {
203 | hideMenuItemComponents();
204 | setScratchProp('anyVisibleChild', false);// we hide all children there is no visible child remaining
205 | setScratchProp('cxtMenuPosition', cyPos);
206 |
207 | var containerPos = $(cy.container()).offset();
208 | var renderedPos = event.renderedPosition || event.cyRenderedPosition;
209 |
210 | var left = containerPos.left + renderedPos.x;
211 | var top = containerPos.top + renderedPos.y;
212 |
213 | $cxtMenu.css('left', left);
214 | $cxtMenu.css('top', top);
215 | }
216 | }
217 |
218 | function createAndAppendMenuItemComponents(menuItems) {
219 | for (var i = 0; i < menuItems.length; i++) {
220 | createAndAppendMenuItemComponent(menuItems[i]);
221 | }
222 | }
223 |
224 | function createAndAppendMenuItemComponent(menuItem) {
225 | // Create and append menu item
226 | var $menuItemComponent = createMenuItemComponent(menuItem);
227 | appendComponentToCxtMenu($menuItemComponent);
228 |
229 | performBindings($menuItemComponent, menuItem.onClickFunction, menuItem.selector, menuItem.coreAsWell);
230 | }//insertComponentBeforeExistingItem(component, existingItemID)
231 |
232 | function createAndInsertMenuItemComponentBeforeExistingComponent(menuItem, existingComponentID) {
233 | // Create and insert menu item
234 | var $menuItemComponent = createMenuItemComponent(menuItem);
235 | insertComponentBeforeExistingItem($menuItemComponent, existingComponentID);
236 |
237 | performBindings($menuItemComponent, menuItem.onClickFunction, menuItem.selector, menuItem.coreAsWell);
238 | }
239 |
240 | // create cxtMenu and append it to body
241 | function createAndAppendCxtMenuComponent() {
242 | var classes = getClassStr(options.contextMenuClasses);
243 | // classes += ' cy-context-menus-cxt-menu';
244 | $cxtMenu = $('');
245 | $cxtMenu.addClass('cy-context-menus-cxt-menu');
246 | setScratchProp('cxtMenu', $cxtMenu);
247 |
248 | $('body').append($cxtMenu);
249 | return $cxtMenu;
250 | }
251 |
252 | // Creates a menu item as an html component
253 | function createMenuItemComponent(item) {
254 | var classStr = getMenuItemClassStr(options.menuItemClasses, item.hasTrailingDivider);
255 | var itemStr = '';
266 | }
267 | else{
268 | itemStr += '>' + '
' + item.content + '';
271 | };
272 |
273 | var $menuItemComponent = $(itemStr);
274 |
275 | $menuItemComponent.data('selector', item.selector);
276 | $menuItemComponent.data('on-click-function', item.onClickFunction);
277 | $menuItemComponent.data('show', (typeof(item.show) === 'undefined' || item.show));
278 | return $menuItemComponent;
279 | }
280 |
281 | // Appends the given component to cxtMenu
282 | function appendComponentToCxtMenu(component) {
283 | $cxtMenu.append(component);
284 | bindMenuItemClickFunction(component);
285 | }
286 |
287 | // Insert the given component to cxtMenu just before the existing item with given ID
288 | function insertComponentBeforeExistingItem(component, existingItemID) {
289 | var $existingItem = $('#' + existingItemID);
290 | component.insertBefore($existingItem);
291 | }
292 |
293 | function destroyCxtMenu() {
294 | if(!getScratchProp('active')) {
295 | return;
296 | }
297 |
298 | removeAndUnbindMenuItems();
299 |
300 | cy.off('tapstart', eventCyTapStart);
301 |
302 | $cxtMenu.remove();
303 | $cxtMenu = undefined;
304 | setScratchProp($cxtMenu, undefined);
305 | setScratchProp('active', false);
306 | setScratchProp('anyVisibleChild', false);
307 | }
308 |
309 | function removeAndUnbindMenuItems() {
310 | var children = $cxtMenu.children();
311 |
312 | $(children).each(function() {
313 | removeAndUnbindMenuItem($(this));
314 | });
315 | }
316 |
317 | function removeAndUnbindMenuItem(itemID) {
318 | var $component = typeof itemID === 'string' ? $('#' + itemID) : itemID;
319 | var cxtfcn = $component.data('cy-context-menus-cxtfcn');
320 | var selector = $component.data('selector');
321 | var callOnClickFcn = $component.data('call-on-click-function');
322 | var cxtCoreFcn = $component.data('cy-context-menus-cxtcorefcn');
323 |
324 | if(cxtfcn) {
325 | cy.off('cxttap', selector, cxtfcn);
326 | }
327 |
328 | if(cxtCoreFcn) {
329 | cy.off('cxttap', cxtCoreFcn);
330 | }
331 |
332 | if(callOnClickFcn) {
333 | $component.off('click', callOnClickFcn);
334 | }
335 |
336 | $component.remove();
337 | }
338 |
339 | function moveBeforeOtherMenuItemComponent(componentID, existingComponentID) {
340 | if( componentID === existingComponentID ) {
341 | return;
342 | }
343 |
344 | var $component = $('#' + componentID).detach();
345 | var $existingComponent = $('#' + existingComponentID);
346 |
347 | $component.insertBefore($existingComponent);
348 | }
349 |
350 | function bindMenuItemClickFunction(component) {
351 | component.click( function() {
352 | hideComponent($cxtMenu);
353 | setScratchProp('cxtMenuPosition', undefined);
354 | });
355 | }
356 |
357 | function disableComponent(componentID) {
358 | $('#' + componentID).attr('disabled', true);
359 | }
360 |
361 | function enableComponent(componentID) {
362 | $('#' + componentID).attr('disabled', false);
363 | }
364 |
365 | function setTrailingDivider(componentID, status) {
366 | var $component = $('#' + componentID);
367 | if(status) {
368 | $component.addClass(dividerCSSClass);
369 | }
370 | else {
371 | $component.removeClass(dividerCSSClass);
372 | }
373 | }
374 |
375 | // Get an extension instance to enable users to access extension methods
376 | function getInstance(cy) {
377 | var instance = {
378 | // Returns whether the extension is active
379 | isActive: function() {
380 | return getScratchProp('active');
381 | },
382 | // Appends given menu item to the menu items list.
383 | appendMenuItem: function(item) {
384 | createAndAppendMenuItemComponent(item);
385 | return cy;
386 | },
387 | // Appends menu items in the given list to the menu items list.
388 | appendMenuItems: function(items) {
389 | createAndAppendMenuItemComponents(items);
390 | return cy;
391 | },
392 | // Removes the menu item with given ID.
393 | removeMenuItem: function(itemID) {
394 | removeAndUnbindMenuItem(itemID);
395 | return cy;
396 | },
397 | // Sets whether the menuItem with given ID will have a following divider.
398 | setTrailingDivider: function(itemID, status) {
399 | setTrailingDivider(itemID, status);
400 | return cy;
401 | },
402 | // Inserts given item before the existingitem.
403 | insertBeforeMenuItem: function(item, existingItemID) {
404 | createAndInsertMenuItemComponentBeforeExistingComponent(item, existingItemID);
405 | return cy;
406 | },
407 | // Moves the item with given ID before the existingitem.
408 | moveBeforeOtherMenuItem: function(itemID, existingItemID) {
409 | moveBeforeOtherMenuItemComponent(itemID, existingItemID);
410 | return cy;
411 | },
412 | // Disables the menu item with given ID.
413 | disableMenuItem: function(itemID) {
414 | disableComponent(itemID);
415 | return cy;
416 | },
417 | // Enables the menu item with given ID.
418 | enableMenuItem: function(itemID) {
419 | enableComponent(itemID);
420 | return cy;
421 | },
422 | // Disables the menu item with given ID.
423 | hideMenuItem: function(itemID) {
424 | $('#'+itemID).data('show', false);
425 | hideComponent($('#'+itemID));
426 | return cy;
427 | },
428 | // Enables the menu item with given ID.
429 | showMenuItem: function(itemID) {
430 | $('#'+itemID).data('show', true);
431 | displayComponent($('#'+itemID));
432 | return cy;
433 | },
434 | // Destroys the extension instance
435 | destroy: function() {
436 | destroyCxtMenu();
437 | return cy;
438 | }
439 | };
440 |
441 | return instance;
442 | }
443 |
444 | if ( opts !== 'get' ) {
445 | // merge the options with default ones
446 | options = extend(defaults, opts);
447 | setScratchProp('options', options);
448 |
449 | // Clear old context menu if needed
450 | if(getScratchProp('active')) {
451 | destroyCxtMenu();
452 | }
453 |
454 | setScratchProp('active', true);
455 |
456 | $cxtMenu = createAndAppendCxtMenuComponent();
457 |
458 | var menuItems = options.menuItems;
459 | createAndAppendMenuItemComponents(menuItems);
460 |
461 | bindCyEvents();
462 | preventDefaultContextTap();
463 | }
464 |
465 | return getInstance(this);
466 | });
467 | };
468 |
469 | if( typeof module !== 'undefined' && module.exports ){ // expose as a commonjs module
470 | module.exports = register;
471 | }
472 |
473 | if( typeof define !== 'undefined' && define.amd ){ // expose as an amd/requirejs module
474 | define('cytoscape-context-menus', function(){
475 | return register;
476 | });
477 | }
478 |
479 | if( typeof cytoscape !== 'undefined' && $ ){ // expose to global cytoscape (i.e. window.cytoscape)
480 | register( cytoscape, $ );
481 | }
482 |
483 | })();
484 |
--------------------------------------------------------------------------------
/static/js/lib/cytoscape-undo-redo.js:
--------------------------------------------------------------------------------
1 | ;(function () {
2 | 'use strict';
3 |
4 | // registers the extension on a cytoscape lib ref
5 | var register = function (cytoscape) {
6 |
7 | if (!cytoscape) {
8 | return;
9 | } // can't register if cytoscape unspecified
10 |
11 | // Get scratch pad reserved for this extension on the given element or the core if 'name' parameter is not set,
12 | // if the 'name' parameter is set then return the related property in the scratch instead of the whole scratchpad
13 | function getScratch (eleOrCy, name) {
14 |
15 | if (eleOrCy.scratch("_undoRedo") === undefined) {
16 | eleOrCy.scratch("_undoRedo", {});
17 | }
18 |
19 | var scratchPad = eleOrCy.scratch("_undoRedo");
20 |
21 | return ( name === undefined ) ? scratchPad : scratchPad[name];
22 | }
23 |
24 | // Set the a field (described by 'name' parameter) of scratchPad (that is reserved for this extension
25 | // on an element or the core) to the given value (by 'val' parameter)
26 | function setScratch (eleOrCy, name, val) {
27 |
28 | var scratchPad = getScratch(eleOrCy);
29 | scratchPad[name] = val;
30 | eleOrCy.scratch("_undoRedo", scratchPad);
31 | }
32 |
33 | // Generate an instance of the extension for the given cy instance
34 | function generateInstance (cy) {
35 | var instance = {};
36 |
37 | instance.options = {
38 | isDebug: false, // Debug mode for console messages
39 | actions: {},// actions to be added
40 | undoableDrag: true, // Whether dragging nodes are undoable can be a function as well
41 | stackSizeLimit: undefined, // Size limit of undo stack, note that the size of redo stack cannot exceed size of undo stack
42 | beforeUndo: function () { // callback before undo is triggered.
43 |
44 | },
45 | afterUndo: function () { // callback after undo is triggered.
46 |
47 | },
48 | beforeRedo: function () { // callback before redo is triggered.
49 |
50 | },
51 | afterRedo: function () { // callback after redo is triggered.
52 |
53 | },
54 | ready: function () {
55 |
56 | }
57 | };
58 |
59 | instance.actions = {};
60 |
61 | instance.undoStack = [];
62 |
63 | instance.redoStack = [];
64 |
65 | //resets undo and redo stacks
66 | instance.reset = function(undos, redos)
67 | {
68 | this.undoStack = undos || [];
69 | this.redoStack = undos || [];
70 | }
71 |
72 | // Undo last action
73 | instance.undo = function () {
74 | if (!this.isUndoStackEmpty()) {
75 |
76 | var action = this.undoStack.pop();
77 | cy.trigger("beforeUndo", [action.name, action.args]);
78 |
79 | var res = this.actions[action.name]._undo(action.args);
80 |
81 | this.redoStack.push({
82 | name: action.name,
83 | args: res
84 | });
85 |
86 | cy.trigger("afterUndo", [action.name, action.args, res]);
87 | return res;
88 | } else if (this.options.isDebug) {
89 | console.log("Undoing cannot be done because undo stack is empty!");
90 | }
91 | };
92 |
93 | // Redo last action
94 | instance.redo = function () {
95 |
96 | if (!this.isRedoStackEmpty()) {
97 | var action = this.redoStack.pop();
98 |
99 | cy.trigger(action.firstTime ? "beforeDo" : "beforeRedo", [action.name, action.args]);
100 |
101 | if (!action.args)
102 | action.args = {};
103 | action.args.firstTime = action.firstTime ? true : false;
104 |
105 | var res = this.actions[action.name]._do(action.args);
106 |
107 | this.undoStack.push({
108 | name: action.name,
109 | args: res
110 | });
111 |
112 | if (this.options.stackSizeLimit != undefined && this.undoStack.length > this.options.stackSizeLimit ) {
113 | this.undoStack.shift();
114 | }
115 |
116 | cy.trigger(action.firstTime ? "afterDo" : "afterRedo", [action.name, action.args, res]);
117 | return res;
118 | } else if (this.options.isDebug) {
119 | console.log("Redoing cannot be done because redo stack is empty!");
120 | }
121 |
122 | };
123 |
124 | // Calls registered function with action name actionName via actionFunction(args)
125 | instance.do = function (actionName, args) {
126 |
127 | this.redoStack.length = 0;
128 | this.redoStack.push({
129 | name: actionName,
130 | args: args,
131 | firstTime: true
132 | });
133 |
134 | return this.redo();
135 | };
136 |
137 | // Undo all actions in undo stack
138 | instance.undoAll = function() {
139 |
140 | while( !this.isUndoStackEmpty() ) {
141 | this.undo();
142 | }
143 | };
144 |
145 | // Redo all actions in redo stack
146 | instance.redoAll = function() {
147 |
148 | while( !this.isRedoStackEmpty() ) {
149 | this.redo();
150 | }
151 | };
152 |
153 | // Register action with its undo function & action name.
154 | instance.action = function (actionName, _do, _undo) {
155 |
156 | this.actions[actionName] = {
157 | _do: _do,
158 | _undo: _undo
159 | };
160 |
161 |
162 | return this;
163 | };
164 |
165 | // Removes action stated with actionName param
166 | instance.removeAction = function (actionName) {
167 | delete this.actions[actionName];
168 | };
169 |
170 | // Gets whether undo stack is empty
171 | instance.isUndoStackEmpty = function () {
172 | return (this.undoStack.length === 0);
173 | };
174 |
175 | // Gets whether redo stack is empty
176 | instance.isRedoStackEmpty = function () {
177 | return (this.redoStack.length === 0);
178 | };
179 |
180 | // Gets actions (with their args) in undo stack
181 | instance.getUndoStack = function () {
182 | return this.undoStack;
183 | };
184 |
185 | // Gets actions (with their args) in redo stack
186 | instance.getRedoStack = function () {
187 | return this.redoStack;
188 | };
189 |
190 | return instance;
191 | }
192 |
193 | // design implementation
194 | cytoscape("core", "undoRedo", function (options, dontInit) {
195 | var cy = this;
196 | var instance = getScratch(cy, 'instance') || generateInstance(cy);
197 | setScratch(cy, 'instance', instance);
198 |
199 | if (options) {
200 | for (var key in options)
201 | if (instance.options.hasOwnProperty(key))
202 | instance.options[key] = options[key];
203 |
204 | if (options.actions)
205 | for (var key in options.actions)
206 | instance.actions[key] = options.actions[key];
207 |
208 | }
209 |
210 | if (!getScratch(cy, 'isInitialized') && !dontInit) {
211 |
212 | var defActions = defaultActions(cy);
213 | for (var key in defActions)
214 | instance.actions[key] = defActions[key];
215 |
216 |
217 | setDragUndo(cy, instance.options.undoableDrag);
218 | setScratch(cy, 'isInitialized', true);
219 | }
220 |
221 | instance.options.ready();
222 |
223 | return instance;
224 |
225 | });
226 |
227 | function setDragUndo(cy, undoable) {
228 | var lastMouseDownNodeInfo = null;
229 |
230 | cy.on("grab", "node", function () {
231 | if (typeof undoable === 'function' ? undoable.call(this) : undoable) {
232 | lastMouseDownNodeInfo = {};
233 | lastMouseDownNodeInfo.lastMouseDownPosition = {
234 | x: this.position("x"),
235 | y: this.position("y")
236 | };
237 | lastMouseDownNodeInfo.node = this;
238 | }
239 | });
240 | cy.on("free", "node", function () {
241 |
242 | var instance = getScratch(cy, 'instance');
243 |
244 | if (typeof undoable === 'function' ? undoable.call(this) : undoable) {
245 | if (lastMouseDownNodeInfo == null) {
246 | return;
247 | }
248 | var node = lastMouseDownNodeInfo.node;
249 | var lastMouseDownPosition = lastMouseDownNodeInfo.lastMouseDownPosition;
250 | var mouseUpPosition = {
251 | x: node.position("x"),
252 | y: node.position("y")
253 | };
254 | if (mouseUpPosition.x != lastMouseDownPosition.x ||
255 | mouseUpPosition.y != lastMouseDownPosition.y) {
256 | var positionDiff = {
257 | x: mouseUpPosition.x - lastMouseDownPosition.x,
258 | y: mouseUpPosition.y - lastMouseDownPosition.y
259 | };
260 |
261 | var nodes;
262 | if (node.selected()) {
263 | nodes = cy.nodes(":visible").filter(":selected");
264 | }
265 | else {
266 | nodes = cy.collection([node]);
267 | }
268 |
269 | var param = {
270 | positionDiff: positionDiff,
271 | nodes: nodes, move: false
272 | };
273 |
274 | instance.do("drag", param);
275 |
276 | lastMouseDownNodeInfo = null;
277 | }
278 | }
279 | });
280 | }
281 |
282 | // Default actions
283 | function defaultActions(cy) {
284 |
285 | function getTopMostNodes(nodes) {
286 | var nodesMap = {};
287 | for (var i = 0; i < nodes.length; i++) {
288 | nodesMap[nodes[i].id()] = true;
289 | }
290 | var roots = nodes.filter(function (ele, i) {
291 | if(typeof ele === "number") {
292 | ele = i;
293 | }
294 | var parent = ele.parent()[0];
295 | while(parent != null){
296 | if(nodesMap[parent.id()]){
297 | return false;
298 | }
299 | parent = parent.parent()[0];
300 | }
301 | return true;
302 | });
303 |
304 | return roots;
305 | }
306 |
307 | function moveNodes(positionDiff, nodes, notCalcTopMostNodes) {
308 | var topMostNodes = notCalcTopMostNodes?nodes:getTopMostNodes(nodes);
309 | for (var i = 0; i < topMostNodes.length; i++) {
310 | var node = topMostNodes[i];
311 | var oldX = node.position("x");
312 | var oldY = node.position("y");
313 | node.position({
314 | x: oldX + positionDiff.x,
315 | y: oldY + positionDiff.y
316 | });
317 | var children = node.children();
318 | moveNodes(positionDiff, children, true);
319 | }
320 | }
321 |
322 | function getEles(_eles) {
323 | return (typeof _eles === "string") ? cy.$(_eles) : _eles;
324 | }
325 |
326 | function restoreEles(_eles) {
327 | return getEles(_eles).restore();
328 | }
329 |
330 |
331 | function returnToPositions(positions) {
332 | var currentPositions = {};
333 | cy.nodes().positions(function (ele, i) {
334 | if(typeof ele === "number") {
335 | ele = i;
336 | }
337 |
338 | currentPositions[ele.id()] = {
339 | x: ele.position("x"),
340 | y: ele.position("y")
341 | };
342 | var pos = positions[ele.id()];
343 | return {
344 | x: pos.x,
345 | y: pos.y
346 | };
347 | });
348 |
349 | return currentPositions;
350 | }
351 |
352 | function getNodePositions() {
353 | var positions = {};
354 | var nodes = cy.nodes();
355 | for (var i = 0; i < nodes.length; i++) {
356 | var node = nodes[i];
357 | positions[node.id()] = {
358 | x: node.position("x"),
359 | y: node.position("y")
360 | };
361 | }
362 | return positions;
363 | }
364 |
365 | function changeParent(param) {
366 | var result = {
367 | };
368 | // If this is first time we should move the node to its new parent and relocate it by given posDiff params
369 | // else we should remove the moved eles and restore the eles to restore
370 | if (param.firstTime) {
371 | var newParentId = param.parentData == undefined ? null : param.parentData;
372 | // These eles includes the nodes and their connected edges and will be removed in nodes.move().
373 | // They should be restored in undo
374 | var withDescendant = param.nodes.union(param.nodes.descendants());
375 | result.elesToRestore = withDescendant.union(withDescendant.connectedEdges());
376 | // These are the eles created by nodes.move(), they should be removed in undo.
377 | result.movedEles = param.nodes.move({"parent": newParentId});
378 |
379 | var posDiff = {
380 | x: param.posDiffX,
381 | y: param.posDiffY
382 | };
383 |
384 | moveNodes(posDiff, result.movedEles);
385 | }
386 | else {
387 | result.elesToRestore = param.movedEles.remove();
388 | result.movedEles = param.elesToRestore.restore();
389 | }
390 |
391 | if (param.callback) {
392 | result.callback = param.callback; // keep the provided callback so it can be reused after undo/redo
393 | param.callback(result.movedEles); // apply the callback on newly created elements
394 | }
395 |
396 | return result;
397 | }
398 |
399 | // function registered in the defaultActions below
400 | // to be used like .do('batch', actionList)
401 | // allows to apply any quantity of registered action in one go
402 | // the whole batch can be undone/redone with one key press
403 | function batch (actionList, doOrUndo) {
404 | var tempStack = []; // corresponds to the results of every action queued in actionList
405 | var instance = getScratch(cy, 'instance'); // get extension instance through cy
406 | var actions = instance.actions;
407 |
408 | // here we need to check in advance if all the actions provided really correspond to available functions
409 | // if one of the action cannot be executed, the whole batch is corrupted because we can't go back after
410 | for (var i = 0; i < actionList.length; i++) {
411 | var action = actionList[i];
412 | if (!actions.hasOwnProperty(action.name)) {
413 | throw "Action " + action.name + " does not exist as an undoable function";
414 | }
415 | }
416 |
417 | for (var i = 0; i < actionList.length; i++) {
418 | var action = actionList[i];
419 | // firstTime property is automatically injected into actionList by the do() function
420 | // we use that to pass it down to the actions in the batch
421 | action.param.firstTime = actionList.firstTime;
422 | var actionResult;
423 | if (doOrUndo == "undo") {
424 | actionResult = actions[action.name]._undo(action.param);
425 | }
426 | else {
427 | actionResult = actions[action.name]._do(action.param);
428 | }
429 |
430 | tempStack.unshift({
431 | name: action.name,
432 | param: actionResult
433 | });
434 | }
435 |
436 | return tempStack;
437 | };
438 |
439 | return {
440 | "add": {
441 | _do: function (eles) {
442 | return eles.firstTime ? cy.add(eles) : restoreEles(eles);
443 | },
444 | _undo: cy.remove
445 | },
446 | "remove": {
447 | _do: cy.remove,
448 | _undo: restoreEles
449 | },
450 | "restore": {
451 | _do: restoreEles,
452 | _undo: cy.remove
453 | },
454 | "select": {
455 | _do: function (_eles) {
456 | return getEles(_eles).select();
457 | },
458 | _undo: function (_eles) {
459 | return getEles(_eles).unselect();
460 | }
461 | },
462 | "unselect": {
463 | _do: function (_eles) {
464 | return getEles(_eles).unselect();
465 | },
466 | _undo: function (_eles) {
467 | return getEles(_eles).select();
468 | }
469 | },
470 | "move": {
471 | _do: function (args) {
472 | var eles = getEles(args.eles);
473 | var nodes = eles.nodes();
474 | var edges = eles.edges();
475 |
476 | return {
477 | oldNodes: nodes,
478 | newNodes: nodes.move(args.location),
479 | oldEdges: edges,
480 | newEdges: edges.move(args.location)
481 | };
482 | },
483 | _undo: function (eles) {
484 | var newEles = cy.collection();
485 | var location = {};
486 | if (eles.newNodes.length > 0) {
487 | location.parent = eles.newNodes[0].parent();
488 |
489 | for (var i = 0; i < eles.newNodes.length; i++) {
490 | var newNode = eles.newNodes[i].move({
491 | parent: eles.oldNodes[i].parent()
492 | });
493 | newEles.union(newNode);
494 | }
495 | } else {
496 | location.source = location.newEdges[0].source();
497 | location.target = location.newEdges[0].target();
498 |
499 | for (var i = 0; i < eles.newEdges.length; i++) {
500 | var newEdge = eles.newEdges[i].move({
501 | source: eles.oldEdges[i].source(),
502 | target: eles.oldEdges[i].target()
503 | });
504 | newEles.union(newEdge);
505 | }
506 | }
507 | return {
508 | eles: newEles,
509 | location: location
510 | };
511 | }
512 | },
513 | "drag": {
514 | _do: function (args) {
515 | if (args.move)
516 | moveNodes(args.positionDiff, args.nodes);
517 | return args;
518 | },
519 | _undo: function (args) {
520 | var diff = {
521 | x: -1 * args.positionDiff.x,
522 | y: -1 * args.positionDiff.y
523 | };
524 | var result = {
525 | positionDiff: args.positionDiff,
526 | nodes: args.nodes,
527 | move: true
528 | };
529 | moveNodes(diff, args.nodes);
530 | return result;
531 | }
532 | },
533 | "layout": {
534 | _do: function (args) {
535 | if (args.firstTime){
536 | var positions = getNodePositions();
537 | var layout;
538 | if(args.eles) {
539 | layout = getEles(args.eles).layout(args.options);
540 | }
541 | else {
542 | layout = cy.layout(args.options);
543 | }
544 |
545 | // Do this check for cytoscape.js backward compatibility
546 | if (layout && layout.run) {
547 | layout.run();
548 | }
549 |
550 | return positions;
551 | } else
552 | return returnToPositions(args);
553 | },
554 | _undo: function (nodesData) {
555 | return returnToPositions(nodesData);
556 | }
557 | },
558 | "changeParent": {
559 | _do: function (args) {
560 | return changeParent(args);
561 | },
562 | _undo: function (args) {
563 | return changeParent(args);
564 | }
565 | },
566 | "batch": {
567 | _do: function (args) {
568 | return batch(args, "do");
569 | },
570 | _undo: function (args) {
571 | return batch(args, "undo");
572 | }
573 | }
574 | };
575 | }
576 |
577 | };
578 |
579 | if (typeof module !== 'undefined' && module.exports) { // expose as a commonjs module
580 | module.exports = register;
581 | }
582 |
583 | if (typeof define !== 'undefined' && define.amd) { // expose as an amd/requirejs module
584 | define('cytoscape.js-undo-redo', function () {
585 | return register;
586 | });
587 | }
588 |
589 | if (typeof cytoscape !== 'undefined') { // expose to global cytoscape (i.e. window.cytoscape)
590 | register(cytoscape);
591 | }
592 |
593 | })();
594 |
--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
41 |
42 |
43 |
44 |
49 |
50 |
51 |
--------------------------------------------------------------------------------