├── .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 | [![npm](https://img.shields.io/npm/v/flow-chart-editor.svg?maxAge=3600)](https://www.npmjs.com/package/flow-chart-editor) 8 | [![NPM downloads](http://img.shields.io/npm/dm/flow-chart-editor.svg)](https://npmjs.org/package/flow-chart-editor) 9 | ![JS gzip size](http://img.badgesize.io/tlzzu/flow-chart-editor/master/lib/index.js.svg?compression=gzip&label=gzip%20size:%20JS) 10 | ![CSS gzip size](http://img.badgesize.io/tlzzu/flow-chart-editor/master/lib/style.css.svg?compression=gzip&label=gzip%20size:%20CSS) 11 | [![Join the chat at https://gitter.im/tlzzu/flow-chart-editor](https://badges.gitter.im/tlzzu/flow-chart-editor.svg)](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 | ![](https://images2018.cnblogs.com/blog/544734/201803/544734-20180309005503770-1121231687.gif) 49 | ![](https://images2018.cnblogs.com/blog/544734/201803/544734-20180309005628409-455120421.gif) 50 | ![](https://images2018.cnblogs.com/blog/544734/201803/544734-20180309005635324-1573303451.gif) 51 | ![](https://images2018.cnblogs.com/blog/544734/201803/544734-20180309005652863-1604639382.gif) 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 | ![image](https://github.com/tlzzu/SoDiaoEditor.v2/raw/master/data/img/ds.png) 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 | 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 = ''; 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 = ''; 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 |
45 |
46 |
47 |
48 |
49 | 50 | 51 | --------------------------------------------------------------------------------