├── . npmignore ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .replit ├── LICENSE ├── README.md ├── README.zh-CN.md ├── deploy.sh ├── dist ├── cyeditor.common.js ├── cyeditor.common.js.map ├── cyeditor.css ├── cyeditor.umd.js ├── cyeditor.umd.js.map ├── cyeditor.umd.min.js ├── cyeditor.umd.min.js.map ├── demo.html ├── fonts │ ├── iconfont.a2f07e20.eot │ └── iconfont.bb604ce2.ttf └── img │ ├── diamond.a60ee67c.svg │ ├── ellipse.5d2931d6.svg │ ├── hexagon.824c4bcf.svg │ ├── iconfont.72e6c721.svg │ ├── pentagon.ab8426d3.svg │ ├── polygon.58cab152.svg │ ├── round-rectangle.6e6408f1.svg │ ├── star.a1743878.svg │ └── tag.247a67c3.svg ├── docs ├── .vuepress │ ├── components │ │ └── CyEditor.vue │ ├── config.js │ ├── enhanceAPP.js │ └── styles │ │ └── index.styl ├── README.md ├── config │ └── README.md └── guide │ └── README.md ├── examples ├── App.vue ├── cyeditor.js ├── example.png └── main.js ├── package.json ├── public ├── index.html └── main.css ├── src ├── assets │ ├── fonts │ │ ├── iconfont.css │ │ ├── iconfont.eot │ │ ├── iconfont.svg │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 │ ├── index.css │ └── node-svgs │ │ ├── diamond.svg │ │ ├── ellipse.svg │ │ ├── hexagon.svg │ │ ├── pentagon.svg │ │ ├── polygon.svg │ │ ├── round-rectangle.svg │ │ ├── star.svg │ │ └── tag.svg ├── defaults │ ├── edge-types.js │ ├── editor-config.js │ ├── index.js │ ├── node-types.js │ └── plugin-style.js ├── index.js ├── lib │ ├── cyeditor-clipboard │ │ └── index.js │ ├── cyeditor-context-menu │ │ └── index.js │ ├── cyeditor-drag-add-nodes │ │ └── index.js │ ├── cyeditor-edgehandles │ │ ├── edgehandles │ │ │ ├── cy-gestures-toggle.js │ │ │ ├── cy-listeners.js │ │ │ ├── defaults.js │ │ │ ├── drawing.js │ │ │ ├── gesture-lifecycle.js │ │ │ ├── index.js │ │ │ └── listeners.js │ │ └── index.js │ ├── cyeditor-edit-elements │ │ └── index.js │ ├── cyeditor-navigator │ │ └── index.js │ ├── cyeditor-node-resize │ │ └── index.js │ ├── cyeditor-snap-grid │ │ └── index.js │ ├── cyeditor-toolbar │ │ └── index.js │ ├── cyeditor-undo-redo │ │ └── index.js │ └── index.js └── utils │ ├── debounce.js │ ├── eventbus.js │ ├── index.js │ ├── localization.js │ ├── memorize.js │ └── throttle.js └── vue.config.js /. npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/ 4 | examples/ 5 | public/ 6 | 7 | # Log files 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | * text eol=lf 3 | *.png binary 4 | *.gif binary 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .vscode 4 | .git 5 | 6 | /node_modules 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | language = "nodejs" 2 | run = "npm run serve" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ryanlei 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 | English | [简体中文](README.zh-CN.md) 2 | 3 | # CyEditor 4 | 5 | A visual flow chart editor based on [cytoscape.js](https://github.com/cytoscape/cytoscape.js). 6 | 7 | ## Demo 8 | 9 | [Demo](https://demonray.github.io/cyeditor/) 10 | 11 | ![demo image](https://github.com/demonray/cyeditor/blob/master/examples/example.png) 12 | 13 | ## Installation 14 | 15 | ### npm 16 | 17 | ```sh 18 | npm install cyeditor 19 | ``` 20 | 21 | ### umd 22 | 23 | ```html 24 | 25 | ``` 26 | 27 | ### run demo 28 | 29 | ```sh 30 | $ git clone https://github.com/demonray/cyeditor.git 31 | $ cd cyeditor 32 | $ npm install 33 | $ npm run serve 34 | ``` 35 | 36 | 37 | ## Documentation 38 | 39 | * You can find the documentation on the [Github Pages](https://demonray.github.io/cyeditor/guide/) 40 | 41 | ### feature 42 | 43 | - [x] Navigator provides previews for easy viewing 44 | - [x] grid lines 45 | - [x] built-in shape and support for custom shapes 46 | - [x] configurable toolbar for common operations 47 | - [x] node size control and node information editing 48 | - [x] Support for custom right-click menu 49 | - [x] Support flow chart export to image, export json data 50 | - [ ] More edge type support, dotted line 51 | - [ ] Element information popper -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | 简体中文 | [English](README.md) 2 | 3 | # CyEditor 4 | 5 | 一个可视化等流程图编辑器基于 [cytoscape.js](https://github.com/cytoscape/cytoscape.js). 6 | 7 | ## Demo 8 | 9 | [示例](https://demonray.github.io/cyeditor/) 10 | 11 | ![demo image](https://github.com/demonray/cyeditor/blob/master/examples/example.png) 12 | 13 | ## 安装 14 | 15 | ### npm 16 | 17 | ```sh 18 | npm install cyeditor 19 | ``` 20 | 21 | ### umd 22 | 23 | ```html 24 | 25 | 26 | 27 | 28 | ``` 29 | 30 | ### 启动示例 31 | 32 | ```sh 33 | $ git clone https://github.com/demonray/cyeditor.git 34 | $ cd cyeditor 35 | $ npm install 36 | $ npm run serve 37 | ``` 38 | 39 | ## 文档 40 | 41 | * 详细文档见 [Github Pages](https://demonray.github.io/cyeditor/guide/) 42 | 43 | ### 特性 44 | 45 | - [x] 导航器提供预览图,方便查看 46 | - [x] 表格辅助 47 | - [x] 内置形状,并支持自定义形状 48 | - [x] 可配置的工具栏,提供常用操作 49 | - [x] 节点大小控制及节点信息编辑 50 | - [x] 支持自定义的右键菜单 51 | - [x] 支持流程图导出为图片,导出json数据 52 | - [ ] 更多边类型支持,虚线 53 | - [ ] 元素信息提示浮层 -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 确保脚本抛出遇到的错误 4 | set -e 5 | 6 | # 生成静态文件 7 | npm run docs:build 8 | 9 | # 进入生成的文件夹 10 | cd docs/.vuepress/dist 11 | 12 | # 如果是发布到自定义域名 13 | # echo 'www.example.com' > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # 如果发布到 https://.github.io 20 | # git push -f git@github.com:/.github.io.git master 21 | 22 | # 如果发布到 https://.github.io/ 23 | git push -f git@github.com:demonray/cyeditor.git master:gh-pages 24 | 25 | cd - 26 | -------------------------------------------------------------------------------- /dist/cyeditor.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:iconfont;src:url(fonts/iconfont.a2f07e20.eot);src:url(fonts/iconfont.a2f07e20.eot#iefix) format("embedded-opentype"),url("data:application/x-font-woff2;charset=utf-8;base64,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") format("woff2"),url(data:font/woff;base64,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) format("woff"),url(fonts/iconfont.bb604ce2.ttf) format("truetype"),url(img/iconfont.72e6c721.svg#iconfont) format("svg")}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-gongzuoliuchengtu:before{content:"\F0310"}.icon-grid:before{content:"\E8B4"}.icon-selection:before{content:"\EA77"}.icon-jsonfile:before{content:"\E659"}.icon-zoom:before{content:"\E662"}.icon-zoomin:before{content:"\E663"}.icon-copy:before{content:"\E6E5"}.icon-save:before{content:"\E618"}.icon-undo:before{content:"\E65A"}.icon-delete:before{content:"\E688"}.icon-save1:before{content:"\E6C0"}.icon-Line-Tool:before{content:"\EA98"}.icon-Bezier-:before{content:"\EAA6"}.icon-Redo:before{content:"\ED8A"}.icon-fullscreen:before{content:"\E731"}.icon-fullscreen-exit:before{content:"\E732"}.icon-paste:before{content:"\E621"}.icon-arrow-to-bottom:before{content:"\E7E0"}.icon-top-arrow-from-top:before{content:"\E83D"}.cy-editor-container{position:relative;width:100%;height:100%}#editor{width:100%;display:flex;position:absolute;top:44px;bottom:0;border:1px solid #e9e9e9;box-sizing:border-box}#editor .left{width:200px;left:0;z-index:2;background:#f7f9fb;overflow:auto}#editor .left .shapes{padding:16px;text-align:left;box-sizing:border-box}#editor .left .title{padding:8px 20px;background:#ebeef2;box-sizing:border-box}#editor .shapes img.shape-item{width:80px;height:80px;padding:4px;border-radius:2px;border:1px solid transparent;box-sizing:border-box}#editor .shapes img.shape-item:hover{cursor:move;border:1px solid #ccc}#editor .right{width:220px;right:0;z-index:2;background:#f7f9fb}#editor .right .panel-title{height:32px;border-top:1px solid #dce3e8;border-bottom:1px solid #dce3e8;background:#ebeef2;color:#000;line-height:28px;padding-left:12px;box-sizing:border-box}#editor .right .panel-body{padding:16px 8px;box-sizing:border-box}#editor .right .checkbox{width:16px;height:16px;box-sizing:border-box;padding:0;margin:0;vertical-align:text-bottom}#editor .right .input{margin:0;list-style:none;position:relative;display:inline-block;padding:2px 6px;width:100%;height:28px;line-height:1.5;color:rgba(0,0,0,.65);background-color:#fff;background-image:none;border:1px solid #d9d9d9;border-radius:4px;transition:all .3s;-webkit-appearance:none;outline:none;box-sizing:border-box}#editor .right .input:hover{border-color:#40a9ff;-webkit-appearance:none}#editor .right .info-item{width:150px;float:right}#editor .right .input.width{width:65px;margin-right:6px}#editor .right .input.height{width:65px}#editor .right .color-input{width:26px;padding:0;vertical-align:middle}#info .info-item-wrap{margin-bottom:12px;overflow:hidden;height:32px;line-height:32px}#cy{flex:1;z-index:999;overflow:hidden}#thumb{position:relative;width:200px;margin:10px auto;height:160px;border:none}#toolbar{padding:8px 10px;width:100%;border:1px solid #e9e9e9;z-index:3;box-shadow:0 8px 12px 0 rgba(0,52,107,.04);box-sizing:border-box}#toolbar .command{width:28px;height:26px;line-height:26px;margin:0 6px;border-radius:2px;display:inline-block;border:1px solid rgba(2,2,2,0);text-align:center}#toolbar .disable{color:rgba(0,0,0,.25)}#toolbar .selected{color:#40a9ff}#toolbar .separator{margin:4px;border-left:1px solid #e9e9e9}#toolbar .command:hover{cursor:pointer;border:1px solid #e9e9e9}.cytoscape-navigator{position:fixed;border:1px solid #e4e4e4;background:#fff;z-index:99999;width:100%;height:100%;min-width:100px;min-height:100px;bottom:0;right:0;overflow:hidden}.cytoscape-navigator>img{max-width:100%;max-height:100%}.cytoscape-navigator>canvas{position:absolute;top:0;left:0;z-index:101}.cytoscape-navigatorView{position:absolute;top:0;left:0;cursor:move;background:#b7e1ed;opacity:.5;z-index:102}.cytoscape-navigatorOverlay{position:absolute;top:0;right:0;bottom:0;left:0;z-index:103}.cy-editor-ctx-menu{position:absolute;width:150px;z-index:1000;border:1px solid #ddd;background:#ebeef2;border-radius:4px;box-shadow:0 4px 8px 0 rgba(2,16,31,.1);display:none;padding:6px 0}.cy-editor-ctx-menu .ctx-menu-item{height:30px;line-height:30px;padding:0 15px;border:1px solid #ebeef2;box-sizing:border-box}.cy-editor-ctx-menu .ctx-menu-divider{height:1px;background:#ddd}.cy-editor-ctx-menu .ctx-menu-item:hover{background:#40a9ff;color:#fff;border:1px solid #40a9ff}.cy-editor-ctx-menu .ctx-menu-item-disabled{color:grey}.cy-editor-ctx-menu .ctx-menu-item-disabled:hover{background:transparent;color:grey;border:1px solid #fff} -------------------------------------------------------------------------------- /dist/demo.html: -------------------------------------------------------------------------------- 1 | 2 | cyeditor demo 3 | 4 | 5 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /dist/fonts/iconfont.a2f07e20.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demonray/cyeditor/30edc1218d1e46120fa710b239b3a4023a6ed96f/dist/fonts/iconfont.a2f07e20.eot -------------------------------------------------------------------------------- /dist/fonts/iconfont.bb604ce2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demonray/cyeditor/30edc1218d1e46120fa710b239b3a4023a6ed96f/dist/fonts/iconfont.bb604ce2.ttf -------------------------------------------------------------------------------- /dist/img/diamond.a60ee67c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 3 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /dist/img/ellipse.5d2931d6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /dist/img/hexagon.824c4bcf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /dist/img/pentagon.ab8426d3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layer 1 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /dist/img/polygon.58cab152.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Layer 1 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dist/img/round-rectangle.6e6408f1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /dist/img/star.a1743878.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /dist/img/tag.247a67c3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/.vuepress/components/CyEditor.vue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'CyEditor', 3 | description: 'A visual flow chart editor based on cytoscape.js.', 4 | head: [ 5 | ['link', { rel: 'icon', href: '/logo.jpg' }], 6 | ], 7 | base: '/cyeditor/', 8 | markdown: { 9 | lineNumbers: false 10 | }, 11 | themeConfig: { 12 | nav:[ 13 | {text: '示例', link: '/'}, 14 | {text: '指南', link: '/guide/'}, 15 | {text: '配置', link: '/config/'}, 16 | {text: 'Github', link: 'https://github.com/demonray/cyeditor'} 17 | ], 18 | sidebar: ['/guide/', '/config/'], 19 | sidebarDepth: 2, // 侧边栏显示2级 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/.vuepress/enhanceAPP.js: -------------------------------------------------------------------------------- 1 | export default ({ Vue, isServer }) => { 2 | if (!isServer) { 3 | // 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | main.home 2 | max-width 85% 3 | margin 0 auto 4 | padding-left 0 5 | padding-right 0 6 | 7 | .footer 8 | margin-top 20px 9 | border-top none 10 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | footer: MIT Licensed | Copyright © 2019-present Ryan 4 | --- 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/config/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # 配置 6 | 7 | ```javascript 8 | let config = { 9 | cy: {}, // options for cytoscape 10 | editor: {} 11 | } 12 | let cyEditor = new CyEditor(config) 13 | ``` 14 | 15 | ## editor基本配置 16 | 17 | ### container 18 | 19 | - 类型: `string` 20 | - 默认值: `''` 21 | 22 | Dom 元素或者选择器,编辑器渲染容器 23 | 24 | ### zoomRate 25 | 26 | - 类型: `number` 27 | - 默认值: `0.2` 28 | 29 | 缩放速率,每次放大或者缩小比例 30 | 31 | ### lineType 32 | 33 | - 类型: `string` 34 | - 默认值: `'bezier'` 35 | 36 | 边类型,'bezier','straight','taxi' 37 | 38 | ### noderesize 39 | 40 | - 类型: `Boolean` 41 | - 默认值: `true` 42 | 43 | 通过拖拽改变大小 44 | 45 | ### dragAddNodes 46 | 47 | - Type: `Boolean` 48 | - Default: `true` 49 | 50 | 51 | 52 | ### elementsInfo 53 | 54 | - 类型: `Boolean` 55 | - 默认值: `true` 56 | 57 | 节点信息面板 58 | 59 | ### toolbar 60 | 61 | - 类型: `toolbar` 62 | - 默认值: `true` 63 | 64 | 工具栏配置 65 | 66 | ### snapGrid 67 | 68 | - 类型: `Boolean` 69 | - 默认值: `true` 70 | 71 | 表格辅助 72 | 73 | 74 | ### contextMenu 75 | 76 | - 类型: `boolean` 77 | - 默认值: `true` 78 | 79 | 右键菜单配置 80 | 81 | 82 | ### navigator 83 | 84 | - 类型: `Boolean` 85 | - 默认值: `true` 86 | 87 | 导航器,提供缩略图预览 88 | 89 | ### useDefaultNodeTypes 90 | 91 | - 类型: `Boolean` 92 | - 默认值: `true` 93 | 94 | 是否使用内置的节点类型 95 | 96 | ### nodeTypes 97 | 98 | - 类型: `Array` 99 | - 默认值: `[]` 100 | 101 | 节点类型定义 102 | 103 | 104 | ## cy基本配置 105 | 106 | 详见 cytoscape.js [basic options](http://js.cytoscape.org/#getting-started/specifying-basic-options) 107 | 108 | ### 注意 109 | ::: tip 提示 110 | container 配置项会被忽略 111 | ::: 112 | 113 | ::: danger 提示 114 | 配置 style 会覆盖编辑器定义的内置样式,可能造成节点形状丢失,颜色控制失效等问题 115 | ::: -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # 指南 6 | 7 | ## 介绍 8 | 9 | ### 简介 10 | 11 | 可视化的流程图编辑器基于[cytoscape.js](https://github.com/cytoscape/cytoscape.js). 12 | 13 | ### 特性 14 | 15 | - [x] 导航器提供预览图,方便查看 16 | - [x] 表格辅助 17 | - [x] 内置形状,并支持自定义形状 18 | - [x] 可配置的工具栏,提供常用操作 19 | - [x] 节点大小控制及节点信息编辑 20 | - [x] 支持自定义的右键菜单 21 | - [x] 支持流程图导出为图片,导出json数据 22 | - [ ] 更多边类型支持,虚线 23 | - [ ] 元素信息提示浮层 24 | 25 | ## 开始使用 26 | 27 | ### 安装 28 | 29 | #### npm 30 | 31 | 使用 npm 的方式安装,与 webpack 打包工具配合使用。 32 | 33 | ```sh 34 | npm install cyeditor 35 | ``` 36 | 37 | #### CDN 38 | 39 | 目前可以通过 [jsdelivr](https://cdn.jsdelivr.net/npm/cyeditor/) 获取到最新版本的资源,在页面上引入 js 和 css 文件即可开始使用。 40 | 41 | ```html 42 | 43 | 44 | 45 | 46 | ``` 47 | 48 | ### Vue 组件 49 | 50 | 创建你的vue组件, cyeditor.js: 51 | 52 | ```javascript 53 | import CyEditor from 'cyeditor' 54 | export default { 55 | name: 'CyEditor', 56 | props: { 57 | value: { 58 | type: Object, 59 | default: () => ({}) 60 | }, 61 | cyConfig: { 62 | type: Object, 63 | default: () => ({}) 64 | }, 65 | editorConfig: { 66 | type: Object, 67 | default: () => ({}) 68 | } 69 | }, 70 | mounted () { 71 | const container = this.$el 72 | let config = { 73 | cy: { 74 | ...this.cyConfig 75 | }, 76 | editor: { 77 | container, 78 | ...this.editorConfig 79 | } 80 | } 81 | this.cyEditor = new CyEditor(config) 82 | this.cyEditor.json(this.value) 83 | this.cyEditor.on('change', (scope, editor) => { 84 | let json = this.cyEditor.json() 85 | console.log(json) 86 | }) 87 | }, 88 | render (h) { 89 | return h('div') 90 | } 91 | } 92 | ``` 93 | 94 | 然后像使用普通vue组件一样: 95 | 96 | ```html 97 | 106 | 107 | 232 | 239 | ``` -------------------------------------------------------------------------------- /examples/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 152 | 159 | -------------------------------------------------------------------------------- /examples/cyeditor.js: -------------------------------------------------------------------------------- 1 | import CyEditor from '../src' 2 | export default { 3 | name: 'CyEditor', 4 | props: { 5 | value: { 6 | type: Object, 7 | default: () => ({ 8 | boxSelectionEnabled: true, 9 | elements: null, 10 | pan: { x: 0, y: 0 }, 11 | panningEnabled: true, 12 | userPanningEnabled: true, 13 | userZoomingEnabled: true, 14 | zoom: 1, 15 | zoomingEnabled: true 16 | }) 17 | }, 18 | cyConfig: { 19 | type: Object, 20 | default: () => ({}) 21 | }, 22 | editorConfig: { 23 | type: Object, 24 | default: () => ({}) 25 | } 26 | }, 27 | mounted () { 28 | const container = this.$el 29 | let config = { 30 | cy: { 31 | ...this.cyConfig 32 | }, 33 | editor: { 34 | container, 35 | ...this.editorConfig 36 | } 37 | } 38 | this.cyEditor = new CyEditor(config) 39 | this.cyEditor.json(this.value) 40 | this.cyEditor.on('change', (scope, editor) => { 41 | // let json = this.cyEditor.json() 42 | }) 43 | }, 44 | render (h) { 45 | return h('div') 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demonray/cyeditor/30edc1218d1e46120fa710b239b3a4023a6ed96f/examples/example.png -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/21. 3 | */ 4 | import Vue from 'vue' 5 | import App from './App.vue' 6 | import '../public/main.css' 7 | 8 | new Vue({ 9 | render: h => h(App) 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cyeditor", 3 | "version": "1.0.3", 4 | "private": false, 5 | "description": "CyEditor, A visual flow chart editor based on cytoscape.js", 6 | "main": "dist/cyeditor.umd.js", 7 | "jsdelivr": "dist/cytoscape.umd.min.js", 8 | "keywords": [ 9 | "cyeditor", 10 | "flowchart", 11 | "flow chart", 12 | "flow editor", 13 | "process editor", 14 | "flow chart editor", 15 | "vue flow chart", 16 | "cytoscape editor", 17 | "javascript flow", 18 | "graph editor", 19 | "diagram" 20 | ], 21 | "scripts": { 22 | "serve": "vue-cli-service serve examples/main.js", 23 | "build": "vue-cli-service build --target lib --name cyeditor src/index.js", 24 | "lint": "vue-cli-service lint --fix", 25 | "docs:dev": "vuepress dev docs", 26 | "docs:build": "vuepress build docs", 27 | "test": "echo \"Error: no test specified\" && exit 1", 28 | "precommit": "lint-staged", 29 | "deploy": "./deploy.sh" 30 | }, 31 | "husky": { 32 | "hooks": { 33 | "postcommit": "git update-index --again", 34 | "pre-commit": "lint-staged" 35 | } 36 | }, 37 | "lint-staged": { 38 | "src/**/*": [ 39 | "eslint --fix", 40 | "git add" 41 | ] 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/demonray/cyeditor.git" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/demonray/cyeditor/issues" 49 | }, 50 | "author": "ryanlei ", 51 | "license": "ISC", 52 | "dependencies": { 53 | "cytoscape": "^3.7.0" 54 | }, 55 | "devDependencies": { 56 | "@vue/cli-plugin-babel": "^3.5.1", 57 | "@vue/cli-plugin-eslint": "^3.5.1", 58 | "@vue/cli-service": "^3.5.1", 59 | "@vue/eslint-config-standard": "^4.0.0", 60 | "babel-core": "^6.26.3", 61 | "babel-eslint": "^10.0.1", 62 | "eslint": "^5.15.3", 63 | "eslint-plugin-vue": "^5.2.2", 64 | "stylus": "^0.54.5", 65 | "stylus-loader": "^3.0.2", 66 | "vue": "^2.6.10", 67 | "vue-template-compiler": "^2.6.10", 68 | "vuepress": "^1.0.2", 69 | "webpack": ">=4 < 4.29" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Demo 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/main.css: -------------------------------------------------------------------------------- 1 | *, *::before, *::after { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | width: 100%; 7 | height: 100%; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | body { 13 | font-size: 12px; 14 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 15 | color: #333; 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "iconfont"; 2 | src: url('iconfont.eot?t=1560583966869'); /* IE9 */ 3 | src: url('iconfont.eot?t=1560583966869#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), 5 | url('iconfont.woff?t=1560583966869') format('woff'), 6 | url('iconfont.ttf?t=1560583966869') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 7 | url('iconfont.svg?t=1560583966869#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family: "iconfont" !important; 12 | font-size: 16px; 13 | font-style: normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-gongzuoliuchengtu:before { 19 | content: "\f0310"; 20 | } 21 | 22 | .icon-grid:before { 23 | content: "\e8b4"; 24 | } 25 | 26 | .icon-selection:before { 27 | content: "\ea77"; 28 | } 29 | 30 | .icon-jsonfile:before { 31 | content: "\e659"; 32 | } 33 | 34 | .icon-zoom:before { 35 | content: "\e662"; 36 | } 37 | 38 | .icon-zoomin:before { 39 | content: "\e663"; 40 | } 41 | 42 | .icon-copy:before { 43 | content: "\e6e5"; 44 | } 45 | 46 | .icon-save:before { 47 | content: "\e618"; 48 | } 49 | 50 | .icon-undo:before { 51 | content: "\e65a"; 52 | } 53 | 54 | .icon-delete:before { 55 | content: "\e688"; 56 | } 57 | 58 | .icon-save1:before { 59 | content: "\e6c0"; 60 | } 61 | 62 | .icon-Line-Tool:before { 63 | content: "\ea98"; 64 | } 65 | 66 | .icon-Bezier-:before { 67 | content: "\eaa6"; 68 | } 69 | 70 | .icon-Redo:before { 71 | content: "\ed8a"; 72 | } 73 | 74 | .icon-fullscreen:before { 75 | content: "\e731"; 76 | } 77 | 78 | .icon-fullscreen-exit:before { 79 | content: "\e732"; 80 | } 81 | 82 | .icon-paste:before { 83 | content: "\e621"; 84 | } 85 | 86 | .icon-arrow-to-bottom:before { 87 | content: "\e7e0"; 88 | } 89 | 90 | .icon-top-arrow-from-top:before { 91 | content: "\e83d"; 92 | } 93 | 94 | -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demonray/cyeditor/30edc1218d1e46120fa710b239b3a4023a6ed96f/src/assets/fonts/iconfont.eot -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demonray/cyeditor/30edc1218d1e46120fa710b239b3a4023a6ed96f/src/assets/fonts/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demonray/cyeditor/30edc1218d1e46120fa710b239b3a4023a6ed96f/src/assets/fonts/iconfont.woff -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demonray/cyeditor/30edc1218d1e46120fa710b239b3a4023a6ed96f/src/assets/fonts/iconfont.woff2 -------------------------------------------------------------------------------- /src/assets/index.css: -------------------------------------------------------------------------------- 1 | @import './fonts/iconfont.css'; 2 | 3 | .cy-editor-container { 4 | position: relative; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | #editor { 10 | width: 100%; 11 | display: flex; 12 | position: absolute; 13 | top: 44px; 14 | bottom: 0; 15 | border: 1px solid #e9e9e9; 16 | box-sizing: border-box; 17 | } 18 | 19 | #editor .left { 20 | width: 200px; 21 | left: 0px; 22 | z-index: 2; 23 | background: #F7F9FB; 24 | overflow: auto; 25 | } 26 | 27 | #editor .left .shapes { 28 | padding: 16px; 29 | text-align: left; 30 | box-sizing: border-box; 31 | } 32 | 33 | #editor .left .title { 34 | padding: 8px 20px; 35 | background: #EBEEF2; 36 | box-sizing: border-box; 37 | } 38 | 39 | #editor .shapes img.shape-item { 40 | width: 80px; 41 | height: 80px; 42 | padding: 4px; 43 | border-radius: 2px; 44 | border: 1px solid rgba(0,0,0,0); 45 | box-sizing: border-box; 46 | } 47 | #editor .shapes img.shape-item:hover { 48 | cursor: move; 49 | border: 1px solid #ccc; 50 | } 51 | 52 | #editor .right { 53 | width: 220px; 54 | right: 0px; 55 | z-index: 2; 56 | background: #F7F9FB; 57 | } 58 | 59 | #editor .right .panel-title { 60 | height: 32px; 61 | border-top: 1px solid #DCE3E8; 62 | border-bottom: 1px solid #DCE3E8; 63 | background: #EBEEF2; 64 | color: #000; 65 | line-height: 28px; 66 | padding-left: 12px; 67 | box-sizing: border-box; 68 | 69 | } 70 | 71 | #editor .right .panel-body { 72 | padding: 16px 8px; 73 | box-sizing: border-box; 74 | } 75 | 76 | #editor .right .checkbox { 77 | width: 16px; 78 | height: 16px; 79 | box-sizing: border-box; 80 | padding: 0; 81 | margin: 0; 82 | vertical-align: text-bottom; 83 | } 84 | 85 | #editor .right .input { 86 | box-sizing: border-box; 87 | margin: 0; 88 | list-style: none; 89 | position: relative; 90 | display: inline-block; 91 | padding: 2px 6px; 92 | width: 100%; 93 | height: 28px; 94 | line-height: 1.5; 95 | color: rgba(0, 0, 0, 0.65); 96 | background-color: #fff; 97 | background-image: none; 98 | border: 1px solid #d9d9d9; 99 | border-radius: 4px; 100 | transition: all .3s; 101 | -webkit-appearance: none; 102 | outline: none; 103 | box-sizing: border-box; 104 | } 105 | 106 | #editor .right .input:hover { 107 | border-color: #40a9ff; 108 | -webkit-appearance: none 109 | } 110 | 111 | #editor .right .info-item { 112 | float: right; 113 | } 114 | 115 | 116 | #editor .right .input.width { 117 | width: 65px; 118 | margin-right: 6px 119 | } 120 | 121 | #editor .right .input.height { 122 | width: 65px 123 | } 124 | 125 | #editor .right .color-input { 126 | width: 26px; 127 | padding: 0; 128 | vertical-align: middle 129 | } 130 | 131 | #info .info-item-wrap { 132 | display: inline-grid; 133 | margin-bottom: 12px; 134 | grid-column-gap: 10px; 135 | grid-template-columns: auto auto; 136 | overflow: hidden; 137 | height: 32px; 138 | line-height: 32px; 139 | } 140 | 141 | #cy { 142 | flex: 1; 143 | z-index: 999; 144 | overflow: hidden 145 | } 146 | 147 | #thumb { 148 | position: relative; 149 | width: 200px; 150 | margin: 10px auto; 151 | height: 160px; 152 | border: none; 153 | } 154 | 155 | #toolbar { 156 | padding: 8px 10px; 157 | width: 100%; 158 | border: 1px solid #E9E9E9; 159 | z-index: 3; 160 | box-shadow: 0px 8px 12px 0px rgba(0, 52, 107, 0.04); 161 | box-sizing: border-box; 162 | } 163 | 164 | #toolbar .command { 165 | width: 28px; 166 | height: 26px; 167 | line-height:26px; 168 | margin: 0px 6px; 169 | border-radius: 2px; 170 | display: inline-block; 171 | border: 1px solid rgba(2,2,2,0); 172 | text-align: center; 173 | } 174 | 175 | #toolbar .disable { 176 | color: rgba(0,0,0,0.25); 177 | } 178 | #toolbar .selected { 179 | color: #40a9ff; 180 | } 181 | 182 | #toolbar .separator { 183 | margin: 4px; 184 | border-left: 1px solid #E9E9E9; 185 | } 186 | 187 | #toolbar .command:hover { 188 | cursor: pointer; 189 | border: 1px solid #E9E9E9; 190 | } 191 | 192 | /* 193 | * navigator 194 | */ 195 | 196 | .cytoscape-navigator { 197 | position: fixed; 198 | border: 1px solid #e4e4e4; 199 | background: #fff; 200 | z-index: 99999; 201 | width: 100%; 202 | height: 100%; 203 | min-width: 100px; 204 | min-height: 100px; 205 | bottom: 0; 206 | right: 0; 207 | overflow: hidden; 208 | } 209 | 210 | .cytoscape-navigator > img{ 211 | max-width: 100%; 212 | max-height: 100%; 213 | } 214 | 215 | .cytoscape-navigator > canvas{ 216 | position: absolute; 217 | top: 0; 218 | left: 0; 219 | z-index: 101; 220 | } 221 | 222 | .cytoscape-navigatorView{ 223 | position: absolute; 224 | top: 0; 225 | left: 0; 226 | cursor: move; 227 | background: #B7E1ED; 228 | opacity: 0.50; 229 | z-index: 102; 230 | } 231 | 232 | .cytoscape-navigatorOverlay{ 233 | position: absolute; 234 | top: 0; 235 | right: 0; 236 | bottom: 0; 237 | left: 0; 238 | z-index: 103; 239 | } 240 | 241 | /* 242 | * context menu 243 | */ 244 | .cy-editor-ctx-menu { 245 | position: absolute; 246 | width: 150px; 247 | z-index: 1000; 248 | border: 1px solid #ddd; 249 | background: #EBEEF2; 250 | border-radius: 4px; 251 | box-shadow: 0px 4px 8px 0px rgba(2, 16, 31, 0.1); 252 | display: none; 253 | padding: 6px 0; 254 | } 255 | .cy-editor-ctx-menu .ctx-menu-item { 256 | height: 30px; 257 | line-height: 30px; 258 | padding: 0 15px; 259 | border: 1px solid #EBEEF2; 260 | box-sizing: border-box; 261 | } 262 | .cy-editor-ctx-menu .ctx-menu-divider { 263 | height: 1px; 264 | background: #ddd 265 | } 266 | .cy-editor-ctx-menu .ctx-menu-item:hover { 267 | background: #40a9ff; 268 | color: #fff; 269 | border: 1px solid #40a9ff; 270 | } 271 | .cy-editor-ctx-menu .ctx-menu-item-disabled { 272 | color: gray; 273 | } 274 | .cy-editor-ctx-menu .ctx-menu-item-disabled:hover { 275 | background: transparent; 276 | color: gray; 277 | border: 1px solid #fff; 278 | } 279 | -------------------------------------------------------------------------------- /src/assets/node-svgs/diamond.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 3 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/assets/node-svgs/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/assets/node-svgs/hexagon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/node-svgs/pentagon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layer 1 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/node-svgs/polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Layer 1 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/node-svgs/round-rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/assets/node-svgs/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/node-svgs/tag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/defaults/edge-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/27. 3 | */ 4 | 5 | const defaultEdgeTypes = {} 6 | 7 | const defaultEdgeStyles = [ 8 | { 9 | selector: 'edge', 10 | style: { 11 | 'curve-style': 'bezier', 12 | 'target-arrow-shape': 'triangle', 13 | 'width': 2 14 | } 15 | }, 16 | { 17 | 'selector': 'edge[lineType]', 18 | 'style': { 19 | 'curve-style': 'data(lineType)' 20 | } 21 | }, 22 | { 23 | selector: 'edge[lineColor]', 24 | style: { 25 | 'target-arrow-shape': 'triangle', 26 | 'width': 2, 27 | 'line-color': 'data(lineColor)', 28 | 'source-arrow-color': 'data(lineColor)', 29 | 'target-arrow-color': 'data(lineColor)', 30 | 'mid-source-arrow-color': 'data(lineColor)', 31 | 'mid-target-arrow-color': 'data(lineColor)' 32 | } 33 | }, 34 | { 35 | selector: 'edge[lineColor]:active', 36 | style: { 37 | 'overlay-color': '#0169D9', 38 | 'overlay-padding': 3, 39 | 'overlay-opacity': 0.25, 40 | 'line-color': 'data(lineColor)', 41 | 'source-arrow-color': 'data(lineColor)', 42 | 'target-arrow-color': 'data(lineColor)', 43 | 'mid-source-arrow-color': 'data(lineColor)', 44 | 'mid-target-arrow-color': 'data(lineColor)' 45 | } 46 | }, 47 | { 48 | selector: 'edge[lineColor]:selected', 49 | style: { 50 | 'overlay-color': '#0169D9', 51 | 'overlay-padding': 3, 52 | 'overlay-opacity': 0.25, 53 | 'line-color': 'data(lineColor)', 54 | 'source-arrow-color': 'data(lineColor)', 55 | 'target-arrow-color': 'data(lineColor)', 56 | 'mid-source-arrow-color': 'data(lineColor)', 57 | 'mid-target-arrow-color': 'data(lineColor)' 58 | } 59 | } 60 | ] 61 | 62 | function getEdgeConf (type) { 63 | return defaultEdgeTypes.find(item => item.type === type) 64 | } 65 | 66 | export { defaultEdgeTypes, defaultEdgeStyles, getEdgeConf } 67 | -------------------------------------------------------------------------------- /src/defaults/editor-config.js: -------------------------------------------------------------------------------- 1 | import { defaultNodeTypes, defaultNodeStyles } from './node-types' 2 | import { defaultEdgeStyles } from './edge-types' 3 | import pluginStyles from './plugin-style' 4 | 5 | export default { 6 | cy: { 7 | layout: { 8 | name: 'concentric', 9 | fit: false, 10 | concentric: function (n) { return 0 }, 11 | minNodeSpacing: 100 12 | }, 13 | styleEnabled: true, 14 | style: [ 15 | ...defaultEdgeStyles, 16 | ...defaultNodeStyles, 17 | ...pluginStyles 18 | ], 19 | minZoom: 0.1, 20 | maxZoom: 10 21 | }, 22 | editor: { 23 | container: '', 24 | zoomRate: 0.2, 25 | lineType: 'bezier', 26 | noderesize: true, 27 | dragAddNodes: true, 28 | elementsInfo: true, 29 | toolbar: true, // boolean or array: ['undo', 'redo', 'copy', 'paste', 'delete', 'zoomin', 'zoomout', 'fit', 'leveldown', 'levelup', 'gridon', 'boxselect', 'line-straight', 'line-taxi', 'line-bezier', 'save'] 30 | snapGrid: true, 31 | contextMenu: true, 32 | navigator: true, 33 | useDefaultNodeTypes: true, // whether nodeTypes should concat with defaultNodeTypes 34 | nodeTypes: defaultNodeTypes, 35 | autoSave: true, // Todo 36 | beforeAdd (el) { 37 | return true 38 | }, 39 | afterAdd (el) { 40 | 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/defaults/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/24. 3 | */ 4 | import pluginStyles from './plugin-style' 5 | import defaultEditorConfig from './editor-config' 6 | import { defaultNodeTypes, defaultNodeStyles } from './node-types' 7 | import { defaultEdgeTypes, defaultEdgeStyles, getEdgeConf } from './edge-types' 8 | 9 | const defaultConfData = { 10 | node: { 11 | type: 'rectangle', 12 | bg: '#999', 13 | resize: true, 14 | name: '', 15 | width: 80, 16 | height: 80, 17 | image: '' 18 | }, 19 | edge: { 20 | lineColor: '#999' 21 | } 22 | } 23 | 24 | export { 25 | defaultConfData, 26 | defaultNodeTypes, 27 | defaultNodeStyles, 28 | defaultEdgeTypes, 29 | defaultEdgeStyles, 30 | getEdgeConf, 31 | pluginStyles, 32 | defaultEditorConfig 33 | } 34 | -------------------------------------------------------------------------------- /src/defaults/node-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/24. 3 | */ 4 | 5 | import roundRectangle from '../assets/node-svgs/round-rectangle.svg' 6 | import ellipse from '../assets/node-svgs/ellipse.svg' 7 | import hexagon from '../assets/node-svgs/hexagon.svg' 8 | import star from '../assets/node-svgs/star.svg' 9 | import pentagon from '../assets/node-svgs/pentagon.svg' 10 | import diamond from '../assets/node-svgs/diamond.svg' 11 | import tag from '../assets/node-svgs/tag.svg' 12 | import polygon from '../assets/node-svgs/polygon.svg' 13 | import utils from '../utils' 14 | 15 | const defaultNodeTypes = [ 16 | { 17 | type: 'ellipse', 18 | src: ellipse, 19 | bg: '#FFC069', 20 | resize: true, 21 | width: 76, 22 | height: 76, 23 | category: utils.localize('node-types-base-shape'), 24 | buildIn: true 25 | }, 26 | { 27 | type: 'round-rectangle', 28 | src: roundRectangle, 29 | bg: '#1890FF', 30 | resize: true, 31 | width: 76, 32 | height: 56, 33 | category: utils.localize('node-types-base-shape'), 34 | buildIn: true 35 | }, 36 | { 37 | type: 'diamond', 38 | src: diamond, 39 | bg: '#5CDBD3', 40 | resize: true, 41 | width: 76, 42 | height: 76, 43 | category: utils.localize('node-types-base-shape'), 44 | buildIn: true 45 | }, 46 | { 47 | type: 'pentagon', 48 | src: pentagon, 49 | bg: '#722ed1', 50 | resize: true, 51 | width: 76, 52 | height: 76, 53 | category: utils.localize('node-types-base-shape'), 54 | buildIn: true 55 | }, 56 | { 57 | type: 'tag', 58 | src: tag, 59 | bg: '#efbae4', 60 | resize: true, 61 | width: 70, 62 | height: 76, 63 | category: utils.localize('node-types-base-shape'), 64 | buildIn: true 65 | }, 66 | { 67 | type: 'star', 68 | src: star, 69 | bg: '#00e217', 70 | resize: true, 71 | width: 76, 72 | height: 76, 73 | category: utils.localize('node-types-base-shape'), 74 | buildIn: true 75 | }, 76 | { 77 | type: 'hexagon', 78 | src: hexagon, 79 | bg: '#ea9f00', 80 | resize: true, 81 | width: 76, 82 | height: 70, 83 | category: utils.localize('node-types-base-shape'), 84 | buildIn: true 85 | }, 86 | { 87 | 'type': 'polygon', 88 | 'src': polygon, 89 | bg: '#f7130e', 90 | resize: true, 91 | width: 76, 92 | height: 76, 93 | 'points': [ 94 | -0.33, -1, 95 | 0.33, -1, 96 | 0.33, -0.33, 97 | 1, -0.33, 98 | 1, 0.33, 99 | 0.33, 0.33, 100 | 0.33, 1, 101 | -0.33, 1, 102 | -0.33, 0.33, 103 | -1, 0.33, 104 | -1, -0.33, 105 | -0.33, -0.33 106 | ], 107 | category: utils.localize('node-types-base-shape'), 108 | buildIn: true 109 | } 110 | ] 111 | const defaultNodeStyles = [{ 112 | 'selector': 'node[type]', 113 | 'style': { 114 | 'shape': 'data(type)', 115 | 'label': 'data(type)', 116 | 'height': 'data(height)', 117 | 'width': 'data(width)', 118 | 'text-valign': 'center', 119 | 'text-halign': 'center' 120 | } 121 | }, { 122 | 'selector': 'node[points]', 123 | 'style': { 124 | 'shape-polygon-points': 'data(points)', 125 | 'label': 'polygon\n(custom points)', 126 | 'text-wrap': 'wrap' 127 | } 128 | }, { 129 | 'selector': 'node[name]', 130 | 'style': { 131 | 'content': 'data(name)' 132 | } 133 | }, { 134 | 'selector': 'node[image]', 135 | 'style': { 136 | 'background-opacity': 0, 137 | 'background-fit': 'cover', 138 | 'background-image': (e) => { return e.data('image') || { value: '' } } 139 | } 140 | }, { 141 | 'selector': 'node[bg]', 142 | 'style': { 143 | 'background-opacity': 0.45, 144 | 'background-color': 'data(bg)', 145 | 'border-width': 1, 146 | 'border-opacity': 0.8, 147 | 'border-color': 'data(bg)' 148 | } 149 | }, { 150 | selector: 'node:active', 151 | style: { 152 | 'overlay-color': '#0169D9', 153 | 'overlay-padding': 12, 154 | 'overlay-opacity': 0.25 155 | } 156 | }, { 157 | selector: 'node:selected', 158 | style: { 159 | 'overlay-color': '#0169D9', 160 | 'overlay-padding': 12, 161 | 'overlay-opacity': 0.25 162 | } 163 | }] 164 | 165 | export { 166 | defaultNodeTypes, 167 | defaultNodeStyles 168 | } 169 | -------------------------------------------------------------------------------- /src/defaults/plugin-style.js: -------------------------------------------------------------------------------- 1 | const pluginStyles = [ 2 | { 3 | selector: '.eh-handle', 4 | style: { 5 | 'background-color': 'red', 6 | 'width': 12, 7 | 'height': 12, 8 | 'shape': 'ellipse', 9 | 'overlay-opacity': 0, 10 | 'border-width': 12, // makes the handle easier to hit 11 | 'border-opacity': 0, 12 | 'background-opacity': 0.5 13 | } 14 | }, 15 | { 16 | selector: '.eh-hover', 17 | style: { 18 | 'background-color': 'red', 19 | 'background-opacity': 0.5 20 | } 21 | }, 22 | { 23 | selector: '.eh-source', 24 | style: { 25 | 'border-width': 2, 26 | 'border-color': 'red' 27 | } 28 | }, 29 | { 30 | selector: '.eh-target', 31 | style: { 32 | 'border-width': 2, 33 | 'border-color': 'red' 34 | } 35 | }, 36 | { 37 | selector: '.eh-preview, .eh-ghost-edge', 38 | style: { 39 | 'background-color': 'red', 40 | 'line-color': 'red', 41 | 'target-arrow-color': 'red', 42 | 'source-arrow-color': 'red' 43 | } 44 | }, 45 | { 46 | selector: '.eh-ghost-edge.eh-preview-active', 47 | style: { 48 | 'opacity': 0 49 | } 50 | } 51 | ] 52 | 53 | export default pluginStyles 54 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './assets/index.css' 2 | import CyEditor from './lib' 3 | export default CyEditor 4 | -------------------------------------------------------------------------------- /src/lib/cyeditor-clipboard/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/4/1. 3 | */ 4 | import utils from '../../utils' 5 | 6 | let defaults = { 7 | clipboardSize: 0, 8 | beforeCopy: null, 9 | afterCopy: null, 10 | beforePaste: null, 11 | afterPaste: null 12 | } 13 | 14 | class Clipboard { 15 | constructor (cy, params) { 16 | this.cy = cy 17 | this._options = Object.assign({}, defaults, params) 18 | this._counter = 0 19 | this._listeners = {} 20 | return this._init() 21 | } 22 | _init () { 23 | // get the scratchpad reserved for this extension on cy 24 | let scratchPad = this._getScratch() 25 | 26 | this._oldIdToNewId = {} 27 | 28 | if (!scratchPad.isInitialized) { 29 | scratchPad.isInitialized = true 30 | let clipboard = {} 31 | 32 | scratchPad.instance = { 33 | copy: (eles, _id) => { 34 | let id = _id || this._getItemId() 35 | eles.unselect() 36 | let descs = eles.nodes().descendants() 37 | let nodes = eles.nodes().union(descs).filter(':visible') 38 | let edges = nodes.edgesWith(nodes).filter(':visible') 39 | 40 | if (this._options.beforeCopy) { 41 | this._options.beforeCopy(nodes.union(edges)) 42 | } 43 | clipboard[id] = { nodes: nodes.jsons(), edges: edges.jsons() } 44 | if (this._options.afterCopy) { 45 | this._options.afterCopy(clipboard[id]) 46 | } 47 | this._targetPos = nodes[0] ? nodes[0].position() : { x: 0, y: 0 } 48 | return id 49 | }, 50 | paste: (_id) => { 51 | let id = _id || this._getItemId(true) 52 | let res = this.cy.collection() 53 | if (this._options.beforePaste) { 54 | this._options.beforePaste(clipboard[id]) 55 | } 56 | if (clipboard[id]) { 57 | let nodes = this.changeIds(clipboard[id].nodes) 58 | let edges = this.changeIds(clipboard[id].edges) 59 | this._oldIdToNewId = {} 60 | this.cy.batch(() => { 61 | res = this.cy.add(nodes).union(this.cy.add(edges)) 62 | res.select() 63 | }) 64 | } 65 | if (this._options.afterPaste) { 66 | this._options.afterPaste(res) 67 | } 68 | this.cy.trigger('cyeditor.paste') 69 | return res 70 | } 71 | } 72 | } 73 | 74 | return scratchPad.instance // return the extension instance 75 | } 76 | _getItemId (last) { 77 | return last ? 'item_' + this._counter : 'item_' + (++this._counter) 78 | } 79 | changeIds (jsons) { 80 | jsons = utils.extend(true, [], jsons) 81 | for (let i = 0; i < jsons.length; i++) { 82 | let jsonFirst = jsons[i] 83 | let id = utils.guid() 84 | this._oldIdToNewId[jsonFirst.data.id] = id 85 | jsonFirst.data.id = id 86 | } 87 | 88 | for (let j = 0; j < jsons.length; j++) { 89 | let json = jsons[j] 90 | let fields = ['source', 'target', 'parent'] 91 | for (let k = 0; k < fields.length; k++) { 92 | let field = fields[k] 93 | if (json.data[field] && this._oldIdToNewId[json.data[field]]) { json.data[field] = this._oldIdToNewId[json.data[field]] } 94 | } 95 | if (json.position.x) { 96 | json.position.x += 50 97 | json.position.y += 50 98 | } 99 | } 100 | 101 | return jsons 102 | } 103 | _getScratch () { 104 | if (!this.cy.scratch('_clipboard')) { 105 | this.cy.scratch('_clipboard', { }) 106 | } 107 | return this.cy.scratch('_clipboard') 108 | } 109 | 110 | destroy () { 111 | this.cy.off('mousemove', this._listeners.onmousemove) 112 | } 113 | } 114 | 115 | export default (cytoscape) => { 116 | if (!cytoscape) { return } 117 | 118 | cytoscape('core', 'clipboard', function (options) { 119 | return new Clipboard(this, options) 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /src/lib/cyeditor-context-menu/index.js: -------------------------------------------------------------------------------- 1 | import utils from '../../utils' 2 | 3 | const defaults = { 4 | menus: [ 5 | { 6 | id: 'remove', 7 | content: 'remove', 8 | disabled: false, 9 | divider: true 10 | }, 11 | { 12 | id: 'hide', 13 | content: 'hide', 14 | disabled: false 15 | } 16 | ], 17 | beforeShow: () => { return true }, 18 | beforeClose: () => { return true } 19 | } 20 | 21 | class ContextMenu { 22 | constructor (cy, params) { 23 | this.cy = cy 24 | this._options = Object.assign({}, defaults, params) 25 | this._listeners = {} 26 | this._init() 27 | } 28 | _init () { 29 | this._initContainer() 30 | this._initDom() 31 | this._initEvents() 32 | } 33 | 34 | _initContainer () { 35 | this._container = this.cy.container() 36 | this.ctxmenu = document.createElement('div') 37 | this.ctxmenu.className = 'cy-editor-ctx-menu' 38 | this._container.append(this.ctxmenu) 39 | } 40 | 41 | _initDom () { 42 | let domItem = '' 43 | this._options.menus.forEach(item => { 44 | domItem += `
${item.content}
` 45 | if (item.divider) { 46 | domItem += '
' 47 | } 48 | }) 49 | this.ctxmenu.innerHTML = domItem 50 | } 51 | 52 | _initEvents () { 53 | this._listeners.eventCyTap = (event) => { 54 | let renderedPos = event.renderedPosition || event.cyRenderedPosition 55 | let left = renderedPos.x 56 | let top = renderedPos.y 57 | utils.css(this.ctxmenu, { 58 | top: top + 'px', 59 | left: left + 'px' 60 | }) 61 | this.show(event) 62 | } 63 | this._listeners.eventTapstart = (e) => { 64 | this.close(e) 65 | } 66 | this._listeners.click = (e) => { 67 | const id = e.target.getAttribute('data-menu-item') 68 | const menuItem = this._options.menus.find(item => item.id === id) 69 | this.cy.trigger('cyeditor.ctxmenu', menuItem) 70 | } 71 | 72 | this.ctxmenu.addEventListener('mousedown', this._listeners.click) 73 | this.cy.on('cxttap', this._listeners.eventCyTap) 74 | this.cy.on('tapstart', this._listeners.eventTapstart) 75 | } 76 | 77 | disable (id, disabled = true) { 78 | const items = utils.query(`.cy-editor-ctx-menu [data-menu-item=${id}]`) 79 | items.forEach(menuItem => { 80 | if (disabled) { 81 | utils.addClass(menuItem, 'ctx-menu-item-disabled') 82 | } else { 83 | utils.removeClass(menuItem, 'ctx-menu-item-disabled') 84 | } 85 | }) 86 | } 87 | 88 | changeMenus (menus) { 89 | this._options.menus = menus 90 | this._initDom() 91 | } 92 | 93 | show (e) { 94 | if (typeof this._options.beforeShow === 'function' && !this.isShow) { 95 | const show = this._options.beforeShow(e, this._options.menus.slice(0)) 96 | if (!show) return 97 | if (show && show.then) { 98 | show.then((res) => { 99 | if (res) { 100 | utils.css(this.ctxmenu, { 101 | display: 'block' 102 | }) 103 | this.isShow = true 104 | } 105 | }) 106 | return 107 | } 108 | if (Array.isArray(show)) { 109 | this.changeMenus(show) 110 | } 111 | utils.css(this.ctxmenu, { 112 | display: 'block' 113 | }) 114 | this.isShow = true 115 | } 116 | } 117 | 118 | close (e) { 119 | if (typeof this._options.beforeShow === 'function' && this.isShow) { 120 | const close = this._options.beforeClose(e) 121 | if (close === true) { 122 | utils.css(this.ctxmenu, { 123 | display: 'none' 124 | }) 125 | this.isShow = false 126 | } else if (close.then) { 127 | close.then(() => { 128 | utils.css(this.ctxmenu, { 129 | display: 'none' 130 | }) 131 | this.isShow = false 132 | }) 133 | } 134 | } 135 | } 136 | 137 | destroyCxtMenu () { 138 | this.ctxmenu.removeEventListener('mousedown', this._listeners.click) 139 | this.cy.off('tapstart', this._listeners.eventTapstart) 140 | this.cy.off('cxttap', this._listeners.eventCyTap) 141 | } 142 | } 143 | 144 | export default (cytoscape) => { 145 | if (!cytoscape) { 146 | return 147 | } 148 | 149 | cytoscape('core', 'contextMenu', function (options) { 150 | return new ContextMenu(this, options) 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /src/lib/cyeditor-drag-add-nodes/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/22. 3 | */ 4 | import utils from '../../utils' 5 | 6 | const defaults = { 7 | container: false, 8 | addWhenDrop: true, 9 | nodeTypes: [] 10 | } 11 | 12 | class DragAddNodes { 13 | constructor (cy, params) { 14 | this.cy = cy 15 | this._options = Object.assign({}, defaults, params) 16 | this._options.nodeTypes.forEach(item => { 17 | item.width = item.width || 76 18 | item.height = item.height || 76 19 | item.category = item.category || 'other' 20 | }) 21 | 22 | if (this._options.nodeTypes.length < 1) return 23 | this._initShapePanel() 24 | this._initShapeItems() 25 | this._initEvents() 26 | } 27 | 28 | _initShapeItems () { 29 | let shapes = this._options.nodeTypes.filter(item => item.type && item.src) 30 | shapes.forEach(item => { 31 | item._id = utils.guid() 32 | }) 33 | let categorys = {} 34 | let other = [] 35 | shapes.forEach(item => { 36 | if (item.category) { 37 | if (categorys[item.category]) { 38 | categorys[item.category].push(item) 39 | } else { 40 | categorys[item.category] = [item] 41 | } 42 | } else { 43 | other.push(item) 44 | } 45 | }) 46 | if (other.length) { 47 | categorys.other = other 48 | } 49 | let categoryDom = Object.keys(categorys).map(item => { 50 | let shapeItems = categorys[item].map(data => { 51 | return `` 52 | }).join('') 53 | return `
54 |
${item}
55 |
${shapeItems}
56 |
` 57 | }).join('') 58 | this._shapePanel.innerHTML = categoryDom 59 | } 60 | 61 | _initEvents () { 62 | let rightContainers = this.cy.container() 63 | let handler = (e) => { 64 | e.preventDefault() 65 | } 66 | 67 | utils.query('.shape-item').forEach(item => { 68 | item.addEventListener('dragstart', (e) => { 69 | e.dataTransfer.setData('id', e.target.getAttribute('data-id')) 70 | }) 71 | }) 72 | 73 | rightContainers.addEventListener('drop', (e) => { 74 | let shapeId = e.dataTransfer.getData('id') 75 | let pos = e.target.compareDocumentPosition(rightContainers) 76 | if (pos === 10) { 77 | let rect = { x: e.offsetX, y: e.offsetY } 78 | if (shapeId) { 79 | const shape = this._options.nodeTypes.find(item => item._id === shapeId) 80 | this._addNodeToCy(shape, rect) 81 | } 82 | } 83 | }) 84 | 85 | rightContainers.addEventListener('dragenter', handler) 86 | rightContainers.addEventListener('dragover', handler) 87 | } 88 | 89 | _addNodeToCy ({ type, width, height, bg, resize, name = '', points, buildIn = false, src }, rect) { 90 | let data = { type, name, resize, bg, width, height } 91 | if (!buildIn) { 92 | data.image = src 93 | } 94 | let node = { 95 | group: 'nodes', 96 | data, 97 | position: rect 98 | } 99 | if (points) { 100 | node.data.points = points 101 | } 102 | if (this._options.addWhenDrop) { 103 | this.cy.trigger('cyeditor.addnode', node) 104 | } 105 | } 106 | 107 | _initShapePanel () { 108 | let { _options } = this 109 | if (_options.container) { 110 | if (typeof _options.container === 'string') { 111 | this._shapePanel = utils.query(_options.container)[ 0 ] 112 | } else if (utils.isNode(_options.container)) { 113 | this._shapePanel = _options.container 114 | } 115 | if (!this._shapePanel) { 116 | console.error('There is no any element matching your container') 117 | } 118 | } else { 119 | this._shapePanel = document.createElement('div') 120 | document.body.appendChild(this._shapePanel) 121 | } 122 | } 123 | } 124 | 125 | export default (cytoscape) => { 126 | if (!cytoscape) { return } 127 | 128 | cytoscape('core', 'dragAddNodes', function (params) { 129 | return new DragAddNodes(this, params) 130 | }) 131 | } 132 | -------------------------------------------------------------------------------- /src/lib/cyeditor-edgehandles/edgehandles/cy-gestures-toggle.js: -------------------------------------------------------------------------------- 1 | function disableGestures () { 2 | this.saveGestureState() 3 | this.cy 4 | .zoomingEnabled(false) 5 | .panningEnabled(false) 6 | .boxSelectionEnabled(false) 7 | 8 | if (this.options.disableBrowserGestures) { 9 | let wlOpts = this.windowListenerOptions 10 | 11 | window.addEventListener('touchstart', this.preventDefault, wlOpts) 12 | window.addEventListener('touchmove', this.preventDefault, wlOpts) 13 | window.addEventListener('wheel', this.preventDefault, wlOpts) 14 | } 15 | 16 | return this 17 | } 18 | 19 | function resetGestures () { 20 | (this.cy 21 | .zoomingEnabled(this.lastZoomingEnabled) 22 | .panningEnabled(this.lastPanningEnabled) 23 | .boxSelectionEnabled(this.lastBoxSelectionEnabled) 24 | ) 25 | 26 | if (this.options.disableBrowserGestures) { 27 | let wlOpts = this.windowListenerOptions 28 | 29 | window.removeEventListener('touchstart', this.preventDefault, wlOpts) 30 | window.removeEventListener('touchmove', this.preventDefault, wlOpts) 31 | window.removeEventListener('wheel', this.preventDefault, wlOpts) 32 | } 33 | 34 | return this 35 | } 36 | 37 | function saveGestureState () { 38 | let { 39 | cy 40 | } = this 41 | 42 | this.lastPanningEnabled = cy.panningEnabled() 43 | this.lastZoomingEnabled = cy.zoomingEnabled() 44 | this.lastBoxSelectionEnabled = cy.boxSelectionEnabled() 45 | 46 | return this 47 | } 48 | 49 | export default { 50 | disableGestures, 51 | resetGestures, 52 | saveGestureState 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/cyeditor-edgehandles/edgehandles/cy-listeners.js: -------------------------------------------------------------------------------- 1 | function addCytoscapeListeners () { 2 | let { 3 | cy, 4 | options 5 | } = this 6 | 7 | // grabbing nodes 8 | this.addListener(cy, 'drag', () => { 9 | this.grabbingNode = true 10 | }) 11 | this.addListener(cy, 'free', () => { 12 | this.grabbingNode = false 13 | }) 14 | 15 | // show handle on hover 16 | this.addListener(cy, 'mouseover', 'node', e => { 17 | this.show(e.target) 18 | }) 19 | 20 | // hide handle on tap handle 21 | this.addListener(cy, 'tap', 'node', e => { 22 | let node = e.target 23 | 24 | if (!node.same(this.handleNode)) { 25 | this.show(node) 26 | } 27 | }) 28 | 29 | // hide handle when source node moved 30 | this.addListener(cy, 'position', 'node', e => { 31 | if (e.target.same(this.sourceNode)) { 32 | this.hide() 33 | } 34 | }) 35 | 36 | // start on tapstart handle 37 | // start on tapstart node (draw mode) 38 | // toggle on source node 39 | this.addListener(cy, 'tapstart', 'node', e => { 40 | let node = e.target 41 | 42 | if (node.same(this.handleNode)) { 43 | this.start(this.sourceNode) 44 | } else if (this.drawMode) { 45 | this.start(node) 46 | } else if (node.same(this.sourceNode)) { 47 | this.hide() 48 | } 49 | }) 50 | 51 | // update line on drag 52 | this.addListener(cy, 'tapdrag', e => { 53 | this.update(e.position) 54 | }) 55 | 56 | // hover over preview 57 | this.addListener(cy, 'tapdragover', 'node', e => { 58 | this.preview(e.target) 59 | }) 60 | 61 | // hover out unpreview 62 | this.addListener(cy, 'tapdragout', 'node', e => { 63 | if (options.snap && e.target.same(this.targetNode)) { 64 | // then keep the preview 65 | } else { 66 | this.unpreview(e.target) 67 | } 68 | }) 69 | 70 | // stop gesture on tapend 71 | this.addListener(cy, 'tapend', () => { 72 | this.stop() 73 | }) 74 | 75 | // hide handle if source node is removed 76 | this.addListener(cy, 'remove', e => { 77 | if (e.target.same(this.sourceNode)) { 78 | this.hide() 79 | } 80 | }) 81 | 82 | return this 83 | } 84 | 85 | export default { 86 | addCytoscapeListeners 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/cyeditor-edgehandles/edgehandles/defaults.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preview: true, // whether to show added edges preview before releasing selection 3 | hoverDelay: 150, // time spent hovering over a target node before it is considered selected 4 | handleNodes: 'node', // selector/filter function for whether edges can be made from a given node 5 | snap: false, // when enabled, the edge can be drawn by just moving close to a target node (can be confusing on compound graphs) 6 | snapThreshold: 50, // the target node must be less than or equal to this many pixels away from the cursor/finger 7 | snapFrequency: 15, // the number of times per second (Hz) that snap checks done (lower is less expensive) 8 | noEdgeEventsInDraw: false, // set events:no to edges during draws, prevents mouseouts on compounds 9 | disableBrowserGestures: true, // during an edge drawing gesture, disable browser gestures such as two-finger trackpad swipe and pinch-to-zoom 10 | handlePosition: function (node) { 11 | return 'middle top' // sets the position of the handle in the format of "X-AXIS Y-AXIS" such as "left top", "middle top" 12 | }, 13 | handleInDrawMode: false, // whether to show the handle in draw mode 14 | edgeType: function (sourceNode, targetNode) { 15 | // can return 'flat' for flat edges between nodes or 'node' for intermediate node between them 16 | // returning null/undefined means an edge can't be added between the two nodes 17 | return 'flat' 18 | }, 19 | loopAllowed: function (node) { 20 | // for the specified node, return whether edges from itself to itself are allowed 21 | return false 22 | }, 23 | nodeLoopOffset: -50, // offset for edgeType: 'node' loops 24 | nodeParams: function (sourceNode, targetNode) { 25 | // for edges between the specified source and target 26 | // return element object to be passed to cy.add() for intermediary node 27 | return {} 28 | }, 29 | edgeParams: function (sourceNode, targetNode, i) { 30 | // for edges between the specified source and target 31 | // return element object to be passed to cy.add() for edge 32 | // NB: i indicates edge index in case of edgeType: 'node' 33 | return {} 34 | }, 35 | ghostEdgeParams: function () { 36 | // return element object to be passed to cy.add() for the ghost edge 37 | // (default classes are always added for you) 38 | return {} 39 | }, 40 | show: function (sourceNode) { 41 | // fired when handle is shown 42 | }, 43 | hide: function (sourceNode) { 44 | // fired when the handle is hidden 45 | }, 46 | start: function (sourceNode) { 47 | // fired when edgehandles interaction starts (drag on handle) 48 | }, 49 | complete: function (sourceNode, targetNode, addedEles) { 50 | // fired when edgehandles is done and elements are added 51 | }, 52 | stop: function (sourceNode) { 53 | // fired when edgehandles interaction is stopped (either complete with added edges or incomplete) 54 | }, 55 | cancel: function (sourceNode, cancelledTargets) { 56 | // fired when edgehandles are cancelled (incomplete gesture) 57 | }, 58 | hoverover: function (sourceNode, targetNode) { 59 | // fired when a target is hovered 60 | }, 61 | hoverout: function (sourceNode, targetNode) { 62 | // fired when a target isn't hovered anymore 63 | }, 64 | previewon: function (sourceNode, targetNode, previewEles) { 65 | // fired when preview is shown 66 | }, 67 | previewoff: function (sourceNode, targetNode, previewEles) { 68 | // fired when preview is hidden 69 | }, 70 | drawon: function () { 71 | // fired when draw mode enabled 72 | }, 73 | drawoff: function () { 74 | // fired when draw mode disabled 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/lib/cyeditor-edgehandles/edgehandles/drawing.js: -------------------------------------------------------------------------------- 1 | const isString = x => typeof x === typeof '' 2 | 3 | function getEleJson (overrides, params, addedClasses) { 4 | let json = {} 5 | 6 | // basic values 7 | Object.assign(json, params, overrides) 8 | 9 | // make sure params can specify data but that overrides take precedence 10 | Object.assign(json.data, params.data, overrides.data) 11 | 12 | if (isString(params.classes)) { 13 | json.classes = params.classes + ' ' + addedClasses 14 | } else if (Array.isArray(params.classes)) { 15 | json.classes = params.classes.join(' ') + ' ' + addedClasses 16 | } else { 17 | json.classes = addedClasses 18 | } 19 | 20 | return json 21 | } 22 | 23 | function makeEdges (preview = false) { 24 | let { 25 | cy, 26 | options, 27 | presumptiveTargets, 28 | previewEles, 29 | active 30 | } = this 31 | 32 | let source = this.sourceNode 33 | let target = this.targetNode 34 | let classes = preview ? 'eh-preview' : '' 35 | let added = cy.collection() 36 | let edgeType = options.edgeType(source, target) 37 | 38 | // can't make edges outside of regular gesture lifecycle 39 | if (!active) { 40 | return 41 | } 42 | 43 | // must have a non-empty edge type 44 | if (!edgeType) { 45 | return 46 | } 47 | 48 | // can't make preview if disabled 49 | if (preview && !options.preview) { 50 | return 51 | } 52 | 53 | // detect cancel 54 | if (!target || target.size() === 0) { 55 | previewEles.remove() 56 | 57 | this.emit('cancel', this.mp(), source, presumptiveTargets) 58 | 59 | return 60 | } 61 | 62 | // just remove preview class if we already have the edges 63 | if (!preview && options.preview) { 64 | previewEles.removeClass('eh-preview').style('events', '') 65 | 66 | this.emit('complete', this.mp(), source, target, previewEles) 67 | 68 | return 69 | } 70 | 71 | let p1 = source.position() 72 | let p2 = target.position() 73 | 74 | let p 75 | if (source.same(target)) { 76 | p = { 77 | x: p1.x + options.nodeLoopOffset, 78 | y: p1.y + options.nodeLoopOffset 79 | } 80 | } else { 81 | p = { 82 | x: (p1.x + p2.x) / 2, 83 | y: (p1.y + p2.y) / 2 84 | } 85 | } 86 | 87 | if (edgeType === 'node') { 88 | let interNode = cy.add( 89 | getEleJson({ 90 | group: 'nodes', 91 | position: p 92 | }, 93 | options.nodeParams(source, target), 94 | classes 95 | )) 96 | 97 | let source2inter = cy.add( 98 | getEleJson({ 99 | group: 'edges', 100 | data: { 101 | source: source.id(), 102 | target: interNode.id() 103 | } 104 | }, 105 | options.edgeParams(source, target, 0), 106 | classes 107 | ) 108 | ) 109 | 110 | let inter2target = cy.add( 111 | getEleJson({ 112 | group: 'edges', 113 | data: { 114 | source: interNode.id(), 115 | target: target.id() 116 | } 117 | }, 118 | options.edgeParams(source, target, 1), 119 | classes 120 | ) 121 | ) 122 | 123 | added = added.merge(interNode).merge(source2inter).merge(inter2target) 124 | } else { // flat 125 | let source2target = cy.add( 126 | getEleJson({ 127 | group: 'edges', 128 | data: { 129 | source: source.id(), 130 | target: target.id() 131 | } 132 | }, 133 | options.edgeParams(source, target, 0), 134 | classes 135 | ) 136 | ) 137 | 138 | added = added.merge(source2target) 139 | } 140 | 141 | if (preview) { 142 | this.previewEles = added 143 | 144 | added.style('events', 'no') 145 | } else { 146 | added.style('events', '') 147 | 148 | this.emit('complete', this.mp(), source, target, added) 149 | } 150 | 151 | return this 152 | } 153 | 154 | function makePreview () { 155 | this.makeEdges(true) 156 | 157 | return this 158 | } 159 | 160 | function previewShown () { 161 | return this.previewEles.nonempty() && this.previewEles.inside() 162 | } 163 | 164 | function removePreview () { 165 | if (this.previewShown()) { 166 | this.previewEles.remove() 167 | } 168 | 169 | return this 170 | } 171 | 172 | function handleShown () { 173 | return this.handleNode.nonempty() && this.handleNode.inside() 174 | } 175 | 176 | function removeHandle () { 177 | if (this.handleShown()) { 178 | this.handleNode.remove() 179 | } 180 | 181 | return this 182 | } 183 | 184 | function setHandleFor (node) { 185 | let { 186 | options, 187 | cy 188 | } = this 189 | 190 | let handlePosition = typeof options.handlePosition === typeof '' ? () => options.handlePosition : options.handlePosition 191 | 192 | let p = node.position() 193 | let h = node.outerHeight() 194 | let w = node.outerWidth() 195 | 196 | // store how much we should move the handle from origin(p.x, p.y) 197 | let moveX = 0 198 | let moveY = 0 199 | 200 | // grab axes 201 | let axes = handlePosition(node).toLowerCase().split(/\s+/) 202 | let axisX = axes[0] 203 | let axisY = axes[1] 204 | 205 | // based on handlePosition move left/right/top/bottom. Middle/middle will just be normal 206 | if (axisX === 'left') { 207 | moveX = -(w / 2) 208 | } else if (axisX === 'right') { 209 | moveX = w / 2 210 | } 211 | if (axisY === 'top') { 212 | moveY = -(h / 2) 213 | } else if (axisY === 'bottom') { 214 | moveY = h / 2 215 | } 216 | 217 | // set handle x and y based on adjusted positions 218 | let hx = this.hx = p.x + moveX 219 | let hy = this.hy = p.y + moveY 220 | let pos = { 221 | x: hx, 222 | y: hy 223 | } 224 | 225 | if (this.handleShown()) { 226 | this.handleNode.position(pos) 227 | } else { 228 | cy.batch(() => { 229 | this.handleNode = cy.add({ 230 | classes: 'eh-handle', 231 | position: pos, 232 | grabbable: false, 233 | selectable: false 234 | }) 235 | 236 | this.handleNode.style('z-index', 9007199254740991) 237 | }) 238 | } 239 | 240 | return this 241 | } 242 | 243 | function updateEdge () { 244 | let { 245 | sourceNode, 246 | ghostNode, 247 | cy, 248 | mx, 249 | my, 250 | options 251 | } = this 252 | let x = mx 253 | let y = my 254 | let ghostEdge, ghostEles 255 | 256 | // can't draw a line without having the starting node 257 | if (!sourceNode) { 258 | return 259 | } 260 | 261 | if (!ghostNode || ghostNode.length === 0 || ghostNode.removed()) { 262 | ghostEles = this.ghostEles = cy.collection() 263 | 264 | cy.batch(() => { 265 | ghostNode = this.ghostNode = cy.add({ 266 | group: 'nodes', 267 | classes: 'eh-ghost eh-ghost-node', 268 | position: { 269 | x: 0, 270 | y: 0 271 | } 272 | }) 273 | 274 | ghostNode.style({ 275 | 'background-color': 'blue', 276 | 'width': 0.0001, 277 | 'height': 0.0001, 278 | 'opacity': 0, 279 | 'events': 'no' 280 | }) 281 | 282 | let ghostEdgeParams = options.ghostEdgeParams() 283 | 284 | ghostEdge = cy.add(Object.assign({}, ghostEdgeParams, { 285 | group: 'edges', 286 | data: Object.assign({}, ghostEdgeParams.data, { 287 | source: sourceNode.id(), 288 | target: ghostNode.id() 289 | }) 290 | })) 291 | 292 | ghostEdge.addClass('eh-ghost eh-ghost-edge') 293 | 294 | ghostEdge.style({ 295 | 'events': 'no' 296 | }) 297 | }) 298 | 299 | ghostEles.merge(ghostNode).merge(ghostEdge) 300 | } 301 | 302 | ghostNode.position({ 303 | x, 304 | y 305 | }) 306 | 307 | return this 308 | } 309 | 310 | export default { 311 | makeEdges, 312 | makePreview, 313 | removePreview, 314 | previewShown, 315 | updateEdge, 316 | handleShown, 317 | setHandleFor, 318 | removeHandle 319 | } 320 | -------------------------------------------------------------------------------- /src/lib/cyeditor-edgehandles/edgehandles/gesture-lifecycle.js: -------------------------------------------------------------------------------- 1 | 2 | import utils from '../../../utils' 3 | 4 | function canStartOn (node) { 5 | const { options, previewEles, ghostEles, handleNode } = this 6 | const isPreview = el => previewEles.anySame(el) 7 | const isGhost = el => ghostEles.anySame(el) 8 | const userFilter = el => el.filter(options.handleNodes).length > 0 9 | const isHandle = el => handleNode.same(el) 10 | const isTemp = el => isPreview(el) || isHandle(el) || isGhost(el) 11 | 12 | const { enabled, active, grabbingNode } = this 13 | 14 | return ( 15 | enabled && !active && !grabbingNode && 16 | (node == null || (!isTemp(node) && userFilter(node))) 17 | ) 18 | } 19 | 20 | function canStartDrawModeOn (node) { 21 | return this.canStartOn(node) && this.drawMode 22 | } 23 | 24 | function canStartNonDrawModeOn (node) { 25 | return this.canStartOn(node) && !this.drawMode 26 | } 27 | 28 | function show (node) { 29 | let { options, drawMode } = this 30 | 31 | if (!this.canStartOn(node) || (drawMode && !options.handleInDrawMode)) { return } 32 | 33 | this.sourceNode = node 34 | 35 | this.setHandleFor(node) 36 | 37 | this.emit('show', this.hp(), this.sourceNode) 38 | 39 | return this 40 | } 41 | 42 | function hide () { 43 | this.removeHandle() 44 | 45 | this.emit('hide', this.hp(), this.sourceNode) 46 | 47 | return this 48 | } 49 | 50 | function start (node) { 51 | if (!this.canStartOn(node)) { return } 52 | 53 | this.active = true 54 | 55 | this.sourceNode = node 56 | this.sourceNode.addClass('eh-source') 57 | 58 | this.disableGestures() 59 | this.disableEdgeEvents() 60 | 61 | this.emit('start', this.hp(), node) 62 | } 63 | 64 | function update (pos) { 65 | if (!this.active) { return } 66 | 67 | let p = pos 68 | 69 | this.mx = p.x 70 | this.my = p.y 71 | 72 | this.updateEdge() 73 | this.throttledSnap() 74 | 75 | return this 76 | } 77 | 78 | function snap () { 79 | if (!this.active || !this.options.snap) { return false } 80 | 81 | let cy = this.cy 82 | let tgt = this.targetNode 83 | let threshold = this.options.snapThreshold 84 | let sqThreshold = n => { let r = getRadius(n); let t = r + threshold; return t * t } 85 | let mousePos = this.mp() 86 | let sqDist = (p1, p2) => (p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y) 87 | let getRadius = n => (n.outerWidth() + n.outerHeight()) / 4 88 | let nodeSqDist = utils.memoize(n => sqDist(n.position(), mousePos), n => n.id()) 89 | let isWithinTheshold = n => nodeSqDist(n) <= sqThreshold(n) 90 | let cmpSqDist = (n1, n2) => nodeSqDist(n1) - nodeSqDist(n2) 91 | let allowHoverDelay = false 92 | 93 | let nodesByDist = cy.nodes(isWithinTheshold).sort(cmpSqDist) 94 | let snapped = false 95 | 96 | if (tgt.nonempty() && !isWithinTheshold(tgt)) { 97 | this.unpreview(tgt) 98 | } 99 | 100 | for (let i = 0; i < nodesByDist.length; i++) { 101 | let n = nodesByDist[i] 102 | 103 | if (n.same(tgt) || this.preview(n, allowHoverDelay)) { 104 | snapped = true 105 | break 106 | } 107 | } 108 | 109 | return snapped 110 | } 111 | 112 | function preview (target, allowHoverDelay = true) { 113 | let { options, sourceNode, ghostNode, ghostEles, presumptiveTargets, previewEles, active } = this 114 | let source = sourceNode 115 | let isLoop = target.same(source) 116 | let loopAllowed = options.loopAllowed(target) 117 | let isGhost = target.same(ghostNode) 118 | let noEdge = !options.edgeType(source, target) 119 | let isHandle = target.same(this.handleNode) 120 | let isExistingTgt = target.same(this.targetNode) 121 | 122 | if (!active || isHandle || isGhost || noEdge || isExistingTgt || (isLoop && !loopAllowed)) { return false } 123 | 124 | if (this.targetNode.nonempty()) { 125 | this.unpreview(this.targetNode) 126 | } 127 | 128 | clearTimeout(this.previewTimeout) 129 | 130 | let applyPreview = () => { 131 | this.targetNode = target 132 | 133 | presumptiveTargets.merge(target) 134 | 135 | target.addClass('eh-presumptive-target') 136 | target.addClass('eh-target') 137 | 138 | this.emit('hoverover', this.mp(), source, target) 139 | 140 | if (options.preview) { 141 | target.addClass('eh-preview') 142 | 143 | ghostEles.addClass('eh-preview-active') 144 | sourceNode.addClass('eh-preview-active') 145 | target.addClass('eh-preview-active') 146 | 147 | this.makePreview() 148 | 149 | this.emit('previewon', this.mp(), source, target, previewEles) 150 | } 151 | } 152 | 153 | if (allowHoverDelay && options.hoverDelay > 0) { 154 | this.previewTimeout = setTimeout(applyPreview, options.hoverDelay) 155 | } else { 156 | applyPreview() 157 | } 158 | 159 | return true 160 | } 161 | 162 | function unpreview (target) { 163 | if (!this.active || target.same(this.handleNode)) { return } 164 | 165 | let { previewTimeout, sourceNode, previewEles, ghostEles, cy } = this 166 | clearTimeout(previewTimeout) 167 | this.previewTimeout = null 168 | 169 | let source = sourceNode 170 | 171 | target.removeClass('eh-preview eh-target eh-presumptive-target eh-preview-active') 172 | ghostEles.removeClass('eh-preview-active') 173 | sourceNode.removeClass('eh-preview-active') 174 | 175 | this.targetNode = cy.collection() 176 | 177 | this.removePreview(source, target) 178 | 179 | this.emit('hoverout', this.mp(), source, target) 180 | this.emit('previewoff', this.mp(), source, target, previewEles) 181 | 182 | return this 183 | } 184 | 185 | function stop () { 186 | if (!this.active) { return } 187 | 188 | let { sourceNode, targetNode, ghostEles, presumptiveTargets } = this 189 | 190 | clearTimeout(this.previewTimeout) 191 | 192 | sourceNode.removeClass('eh-source') 193 | targetNode.removeClass('eh-target eh-preview eh-hover') 194 | presumptiveTargets.removeClass('eh-presumptive-target') 195 | 196 | this.makeEdges() 197 | 198 | this.removeHandle() 199 | 200 | ghostEles.remove() 201 | 202 | this.clearCollections() 203 | 204 | this.resetGestures() 205 | this.enableEdgeEvents() 206 | 207 | this.active = false 208 | 209 | this.emit('stop', this.mp(), sourceNode) 210 | 211 | return this 212 | } 213 | 214 | export default { 215 | show, 216 | hide, 217 | start, 218 | update, 219 | preview, 220 | unpreview, 221 | stop, 222 | snap, 223 | canStartOn, 224 | canStartDrawModeOn, 225 | canStartNonDrawModeOn 226 | } 227 | -------------------------------------------------------------------------------- /src/lib/cyeditor-edgehandles/edgehandles/index.js: -------------------------------------------------------------------------------- 1 | import utils from '../../../utils' 2 | import defaults from './defaults' 3 | import cyGesturesToggle from './cy-gestures-toggle' 4 | import cyListeners from './cy-listeners' 5 | import drawing from './drawing' 6 | import gestureLifecycle from './gesture-lifecycle' 7 | import listeners from './listeners' 8 | 9 | class Edgehandles { 10 | constructor (cy, options) { 11 | this.cy = cy 12 | this.listeners = [] 13 | // edgehandles gesture state 14 | this.enabled = true 15 | this.drawMode = false 16 | this.active = false 17 | this.grabbingNode = false 18 | // edgehandles elements 19 | this.handleNode = cy.collection() 20 | this.clearCollections() 21 | // handle 22 | this.hx = 0 23 | this.hy = 0 24 | this.hr = 0 25 | // mouse position 26 | this.mx = 0 27 | this.my = 0 28 | this.options = Object.assign({}, defaults, options) 29 | this.saveGestureState() 30 | this.addListeners() 31 | this.throttledSnap = utils.throttle(this.snap.bind(this), 1000 / options.snapFrequency) 32 | this.preventDefault = e => e.preventDefault() 33 | let supportsPassive = false 34 | try { 35 | let opts = Object.defineProperty({}, 'passive', { 36 | get: function () { 37 | supportsPassive = true 38 | } 39 | }) 40 | window.addEventListener('test', null, opts) 41 | } catch (err) {} 42 | if (supportsPassive) { 43 | this.windowListenerOptions = { 44 | capture: true, 45 | passive: false 46 | } 47 | } else { 48 | this.windowListenerOptions = true 49 | } 50 | } 51 | destroy () { 52 | this.removeListeners() 53 | } 54 | setOptions (options) { 55 | Object.assign(this.options, options) 56 | } 57 | mp () { 58 | return { 59 | x: this.mx, 60 | y: this.my 61 | } 62 | } 63 | hp () { 64 | return { 65 | x: this.hx, 66 | y: this.hy 67 | } 68 | } 69 | clearCollections () { 70 | this.previewEles = this.cy.collection() 71 | this.ghostEles = this.cy.collection() 72 | this.ghostNode = this.cy.collection() 73 | this.sourceNode = this.cy.collection() 74 | this.targetNode = this.cy.collection() 75 | this.presumptiveTargets = this.cy.collection() 76 | } 77 | enable () { 78 | this.enabled = true 79 | this.emit('enable') 80 | return this 81 | } 82 | 83 | disable () { 84 | this.enabled = false 85 | this.emit('disable') 86 | return this 87 | } 88 | toggleDrawMode (bool) { 89 | let { 90 | cy, 91 | options 92 | } = this 93 | 94 | this.drawMode = bool != null ? bool : !this.drawMode 95 | 96 | if (this.drawMode) { 97 | this.prevUngrabifyState = cy.autoungrabify() 98 | cy.autoungrabify(true) 99 | if (!options.handleInDrawMode && this.handleShown()) { 100 | this.hide() 101 | } 102 | this.emit('drawon') 103 | } else { 104 | cy.autoungrabify(this.prevUngrabifyState) 105 | this.emit('drawoff') 106 | } 107 | return this 108 | } 109 | 110 | enableDrawMode () { 111 | return this.toggleDrawMode(true) 112 | } 113 | 114 | disableDrawMode () { 115 | return this.toggleDrawMode(false) 116 | } 117 | 118 | disableEdgeEvents () { 119 | if (this.options.noEdgeEventsInDraw) { 120 | this.cy.edges().style('events', 'no') 121 | } 122 | 123 | return this 124 | } 125 | 126 | enableEdgeEvents () { 127 | if (this.options.noEdgeEventsInDraw) { 128 | this.cy.edges().style('events', '') 129 | } 130 | 131 | return this 132 | } 133 | } 134 | 135 | let proto = Edgehandles.prototype 136 | let extend = obj => Object.assign(proto, obj) 137 | const fns = [cyGesturesToggle, cyListeners, drawing, gestureLifecycle, listeners] 138 | fns.forEach(extend) 139 | 140 | export default Edgehandles 141 | -------------------------------------------------------------------------------- /src/lib/cyeditor-edgehandles/edgehandles/listeners.js: -------------------------------------------------------------------------------- 1 | function addListeners () { 2 | this.addCytoscapeListeners() 3 | this.addListener(this.cy, 'destroy', () => this.destroy()) 4 | return this 5 | } 6 | 7 | function removeListeners () { 8 | for (let i = this.listeners.length - 1; i >= 0; i--) { 9 | let l = this.listeners[i] 10 | this.removeListener(l.target, l.event, l.selector, l.callback, l.options) 11 | } 12 | return this 13 | } 14 | 15 | function getListener (target, event, selector, callback, options) { 16 | if (typeof selector !== typeof '') { 17 | callback = selector 18 | options = callback 19 | selector = null 20 | } 21 | 22 | if (options == null) { 23 | options = false 24 | } 25 | 26 | return { target, event, selector, callback, options } 27 | } 28 | 29 | function isDom (target) { 30 | return target instanceof Element 31 | } 32 | 33 | function addListener (target, event, selector, callback, options) { 34 | let l = getListener(target, event, selector, callback, options) 35 | 36 | this.listeners.push(l) 37 | 38 | if (isDom(l.target)) { 39 | l.target.addEventListener(l.event, l.callback, l.options) 40 | } else { 41 | if (l.selector) { 42 | l.target.addListener(l.event, l.selector, l.callback, l.options) 43 | } else { 44 | l.target.addListener(l.event, l.callback, l.options) 45 | } 46 | } 47 | 48 | return this 49 | } 50 | 51 | function removeListener (target, event, selector, callback, options) { 52 | let l = getListener(target, event, selector, callback, options) 53 | 54 | for (let i = this.listeners.length - 1; i >= 0; i--) { 55 | let l2 = this.listeners[i] 56 | 57 | if ( 58 | l.target === l2.target && 59 | l.event === l2.event && 60 | (l.selector == null || l.selector === l2.selector) && 61 | (l.callback == null || l.callback === l2.callback) 62 | ) { 63 | this.listeners.splice(i, 1) 64 | 65 | if (isDom(l.target)) { 66 | l.target.removeEventListener(l.event, l.callback, l.options) 67 | } else { 68 | if (l.selector) { 69 | l.target.removeListener(l.event, l.selector, l.callback, l.options) 70 | } else { 71 | l.target.removeListener(l.event, l.callback, l.options) 72 | } 73 | } 74 | 75 | break 76 | } 77 | } 78 | 79 | return this 80 | } 81 | 82 | function emit (type, position, ...args) { 83 | let { options, cy } = this 84 | 85 | cy.emit({ type: `eh${type}`, position }, args) 86 | 87 | let handler = options[ type ] 88 | 89 | if (handler != null) { 90 | handler(...args) 91 | } 92 | 93 | return this 94 | } 95 | 96 | export default { addListener, addListeners, removeListener, removeListeners, emit } 97 | -------------------------------------------------------------------------------- /src/lib/cyeditor-edgehandles/index.js: -------------------------------------------------------------------------------- 1 | import Edgehandles from './edgehandles' 2 | 3 | export default (cytoscape) => { 4 | if (!cytoscape) { return } 5 | 6 | cytoscape('core', 'edgehandles', function (options) { 7 | return new Edgehandles(this, options) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/cyeditor-edit-elements/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/25. 3 | */ 4 | import utils from '../../utils' 5 | 6 | const defaults = { 7 | container: false, 8 | attrs: [ 'name', 'height', 'width', 'color', 'background-color' ] 9 | } 10 | 11 | class EditElements { 12 | constructor (cy, params) { 13 | this.cy = cy 14 | this._options = Object.assign({}, defaults, params) 15 | this._infos = {} 16 | this.selected = [] 17 | this._initPanel() 18 | this._initEvents() 19 | } 20 | 21 | _initEvents () { 22 | this._panel.addEventListener('input', (e) => { // debounce todo 23 | if (this._options.attrs.indexOf(e.target.name) > -1) { 24 | this._infos[e.target.name] = e.target.value 25 | this._changeElementInfo(e.target.name, e.target.value) 26 | } 27 | }) 28 | this.cy.on('select unselect', () => { 29 | this.showElementsInfo() 30 | }) 31 | } 32 | 33 | _initPanel () { 34 | let { _options } = this 35 | if (_options.container) { 36 | if (typeof _options.container === 'string') { 37 | this._panel = utils.query(_options.container)[ 0 ] 38 | } else if (utils.isNode(_options.container)) { 39 | this._panel = _options.container 40 | } 41 | if (!this._panel) { 42 | console.error('There is no any element matching your container') 43 | return 44 | } 45 | } else { 46 | this._panel = document.createElement('div') 47 | document.body.appendChild(this._panel) 48 | } 49 | this._panelHtml() 50 | this._panel.style.display = 'none' 51 | } 52 | 53 | _panelHtml (params = { showName: true, showBgColor: true, showColor: true, showRect: true, colorTitle: utils.localize('elements-text') }) { 54 | this._panel.innerHTML = `
${utils.localize('elements-title')}${params.title || ''}
55 |
56 |
${utils.localize('elements-label')}: 57 | 58 |
59 |
${utils.localize('elements-wrap')}: 60 |
61 | 62 | 63 |
64 |
65 |
${params.colorTitle}: 66 |
67 | 68 |
69 |
70 |
${utils.localize('elements-background-color')}: 71 |
72 | 73 |
74 |
75 |
` 76 | } 77 | 78 | _changeElementInfo (name, value) { 79 | if (name === 'name') { 80 | this.selected[0].data({ name: value }) 81 | return 82 | } 83 | this.selected.forEach(item => { 84 | if (item.isEdge() && name === 'color') { 85 | name = 'lineColor' 86 | } 87 | if (name === 'background-color') { 88 | name = 'bg' 89 | } 90 | item.data({ 91 | [name]: value 92 | }) 93 | }) 94 | } 95 | 96 | showElementsInfo () { 97 | let selected = this.cy.$(':selected') 98 | this.selected = selected 99 | let allNode = selected.every(it => it.isNode()) 100 | let opt = { showName: allNode, showBgColor: allNode, showColor: true, showRect: allNode, colorTitle: allNode ? utils.localize('elements-text-color') : utils.localize('elements-color') } 101 | if (selected.length > 1) { 102 | this._infos.name = '' 103 | this._panelHtml(opt) 104 | } else if (selected.length === 1) { 105 | this._panelHtml(opt) 106 | this._panel.style.display = 'block' 107 | let el = selected[0] 108 | this._options.attrs.forEach(item => { 109 | if (item === 'name') { // from data 110 | this._infos[item] = el.data('name') 111 | } else if (item === 'color' || item === 'background-color') { 112 | let color = el.numericStyle(item) 113 | this._infos[item] = '#' + utils.RGBToHex(...color) 114 | } else { 115 | this._infos[item] = el.numericStyle(item) 116 | } 117 | }) 118 | } else { 119 | this._panel.style.display = 'none' 120 | } 121 | this._options.attrs.filter(item => this._infos[item]).forEach(name => { 122 | let item = utils.query(`#info-items input[name=${name}`) 123 | if (item.length) { 124 | item[0].value = this._infos[name] 125 | } 126 | }) 127 | } 128 | } 129 | 130 | export default (cytoscape) => { 131 | if (!cytoscape) { return } 132 | 133 | cytoscape('core', 'editElements', function (params) { 134 | return new EditElements(this, params) 135 | }) 136 | } 137 | -------------------------------------------------------------------------------- /src/lib/cyeditor-navigator/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/20. 3 | */ 4 | import utils from '../../utils' 5 | 6 | let defaults = { 7 | container: false, // can be a selector 8 | viewLiveFramerate: 0, // set false to update graph pan only on drag end; set 0 to do it instantly; set a number (frames per second) to update not more than N times per second 9 | dblClickDelay: 200, // milliseconds 10 | removeCustomContainer: true, // destroy the container specified by user on plugin destroy 11 | rerenderDelay: 500 // ms to throttle rerender updates to the panzoom for performance 12 | } 13 | 14 | class Navigator { 15 | constructor (cy, options) { 16 | this.cy = cy 17 | this._options = utils.extend({}, defaults, options) 18 | this._init(cy, options) 19 | } 20 | 21 | _init () { 22 | this._cyListeners = [] 23 | this._contianer = this.cy.container() 24 | 25 | // Cache bounding box 26 | this.boundingBox = this.bb() 27 | 28 | let eleRect = this._contianer.getBoundingClientRect() 29 | // Cache sizes 30 | this.width = eleRect.width 31 | this.height = eleRect.height 32 | 33 | // Init components 34 | this._initPanel() 35 | this._initThumbnail() 36 | this._initView() 37 | this._initOverlay() 38 | } 39 | 40 | _addCyListener (events, handler) { 41 | this._cyListeners.push({ 42 | events: events, 43 | handler: handler 44 | }) 45 | 46 | this.cy.on(events, handler) 47 | } 48 | 49 | _removeCyListeners () { 50 | let cy = this.cy 51 | 52 | this._cyListeners.forEach(function (l) { 53 | cy.off(l.events, l.handler) 54 | }) 55 | 56 | cy.offRender(this._onRenderHandler) 57 | } 58 | 59 | _initPanel () { 60 | let options = this._options 61 | 62 | if (options.container) { 63 | if (typeof options.container === 'string') { 64 | this.$panel = utils.query(options.container)[ 0 ] 65 | } else if (utils.isNode(options.container)) { 66 | this.$panel = options.container 67 | } 68 | if (!this.$panel) { 69 | console.error('There is no any element matching your container') 70 | return 71 | } 72 | } else { 73 | this.$panel = document.createElement('div') 74 | document.body.appendChild(this.$panel) 75 | } 76 | 77 | this.$panel.classList.add('cytoscape-navigator') 78 | 79 | this._setupPanel() 80 | this._addCyListener('resize', this.resize.bind(this)) 81 | } 82 | 83 | _setupPanel () { 84 | let panelRect = this.$panel.getBoundingClientRect() 85 | // Cache sizes 86 | this.panelWidth = panelRect.width 87 | this.panelHeight = panelRect.height 88 | } 89 | 90 | _initThumbnail () { 91 | // Create thumbnail 92 | this.$thumbnail = document.createElement('img') 93 | 94 | // Add thumbnail canvas to the DOM 95 | this.$panel.appendChild(this.$thumbnail) 96 | 97 | // Setup thumbnail 98 | this._setupThumbnailSizes() 99 | this._updateThumbnailImage() 100 | } 101 | 102 | _setupThumbnailSizes () { 103 | // Update bounding box cache 104 | this.boundingBox = this.bb() 105 | 106 | this.thumbnailZoom = Math.min(this.panelHeight / this.boundingBox.h, this.panelWidth / this.boundingBox.w) 107 | 108 | // Used on thumbnail generation 109 | this.thumbnailPan = { 110 | x: (this.panelWidth - this.thumbnailZoom * (this.boundingBox.x1 + this.boundingBox.x2)) / 2, 111 | y: (this.panelHeight - this.thumbnailZoom * (this.boundingBox.y1 + this.boundingBox.y2)) / 2 112 | } 113 | } 114 | 115 | // If bounding box has changed then update sizes 116 | // Otherwise just update the thumbnail 117 | _checkThumbnailSizesAndUpdate () { 118 | // Cache previous values 119 | let _zoom = this.thumbnailZoom 120 | let _panX = this.thumbnailPan.x 121 | let _panY = this.thumbnailPan.y 122 | 123 | this._setupThumbnailSizes() 124 | 125 | this._updateThumbnailImage() 126 | if (_zoom !== this.thumbnailZoom || _panX !== this.thumbnailPan.x || _panY !== this.thumbnailPan.y) { 127 | this._setupView() 128 | } 129 | } 130 | 131 | _initView () { 132 | this.$view = document.createElement('div') 133 | this.$view.className = 'cytoscape-navigatorView' 134 | this.$panel.appendChild(this.$view) 135 | 136 | let viewStyle = window.getComputedStyle(this.$view) 137 | 138 | // Compute borders 139 | this.viewBorderTop = parseInt(viewStyle[ 'border-top-width' ], 10) 140 | this.viewBorderRight = parseInt(viewStyle[ 'border-right-width' ], 10) 141 | this.viewBorderBottom = parseInt(viewStyle[ 'border-bottom-width' ], 10) 142 | this.viewBorderLeft = parseInt(viewStyle[ 'border-left-width' ], 10) 143 | 144 | // Abstract borders 145 | this.viewBorderHorizontal = this.viewBorderLeft + this.viewBorderRight 146 | this.viewBorderVertical = this.viewBorderTop + this.viewBorderBottom 147 | 148 | this._setupView() 149 | 150 | // Hook graph zoom and pan 151 | this._addCyListener('zoom pan', this._setupView.bind(this)) 152 | } 153 | 154 | _setupView () { 155 | if (this.viewLocked) { return } 156 | 157 | let cyZoom = this.cy.zoom() 158 | let cyPan = this.cy.pan() 159 | 160 | // Horizontal computation 161 | this.viewW = this.width / cyZoom * this.thumbnailZoom 162 | this.viewX = -cyPan.x * this.viewW / this.width + this.thumbnailPan.x - this.viewBorderLeft 163 | 164 | // Vertical computation 165 | this.viewH = this.height / cyZoom * this.thumbnailZoom 166 | this.viewY = -cyPan.y * this.viewH / this.height + this.thumbnailPan.y - this.viewBorderTop 167 | 168 | // CSS view 169 | this.$view.style.width = this.viewW + 'px' 170 | this.$view.style.height = this.viewH + 'px' 171 | this.$view.style.position = 'absolute' 172 | this.$view.style.left = this.viewX + 'px' 173 | this.$view.style.top = this.viewY + 'px' 174 | } 175 | 176 | /* 177 | * Used inner attributes 178 | * 179 | * timeout {number} used to keep stable frame rate 180 | * lastMoveStartTime {number} 181 | * inMovement {boolean} 182 | * hookPoint {object} {x: 0, y: 0} 183 | */ 184 | _initOverlay () { 185 | // Used to capture mouse events 186 | this.$overlay = document.createElement('div') 187 | this.$overlay.className = 'cytoscape-navigatorOverlay' 188 | 189 | // Add overlay to the DOM 190 | this.$panel.appendChild(this.$overlay) 191 | 192 | // Init some attributes 193 | this.overlayHookPointX = 0 194 | this.overlayHookPointY = 0 195 | 196 | // Listen for events 197 | this._initEventsHandling() 198 | } 199 | 200 | _initEventsHandling () { 201 | let eventsLocal = [ 202 | // Mouse events 203 | 'mousedown', 204 | 'mousewheel', 205 | 'DOMMouseScroll', // Mozilla specific event 206 | // Touch events 207 | 'touchstart' 208 | ] 209 | let eventsGlobal = [ 210 | 'mouseup', 211 | 'mouseout', 212 | 'mousemove', 213 | // Touch events 214 | 'touchmove', 215 | 'touchend' 216 | ] 217 | 218 | // handle events and stop their propagation 219 | let overlayListener = (env) => { 220 | // Touch events 221 | let ev = utils.extend({}, env) 222 | if (ev.type === 'touchstart') { 223 | // Will count as middle of View 224 | ev.offsetX = this.viewX + this.viewW / 2 225 | ev.offsetY = this.viewY + this.viewH / 2 226 | } 227 | 228 | // Normalize offset for browsers which do not provide that value 229 | if (ev.offsetX === undefined || ev.offsetY === undefined) { 230 | let targetOffset = utils.offset(ev.target) 231 | ev.offsetX = ev.pageX - targetOffset.left 232 | ev.offsetY = ev.pageY - targetOffset.top 233 | } 234 | 235 | if (ev.type === 'mousedown' || ev.type === 'touchstart') { 236 | this._eventMoveStart(ev) 237 | } else if (ev.type === 'mousewheel' || ev.type === 'DOMMouseScroll') { 238 | this._eventZoom(ev) 239 | } 240 | 241 | env.preventDefault() 242 | // Prevent default and propagation 243 | // Don't use peventPropagation as it breaks mouse events 244 | return false 245 | } 246 | 247 | // Hook global events 248 | let globalListener = (env) => { 249 | let ev = utils.extend({}, env) 250 | // Do not make any computations if it is has no effect on Navigator 251 | if (!this.overlayInMovement) return 252 | // Touch events 253 | if (ev.type === 'touchend') { 254 | // Will count as middle of View 255 | ev.offsetX = this.viewX + this.viewW / 2 256 | ev.offsetY = this.viewY + this.viewH / 2 257 | } else if (ev.type === 'touchmove') { 258 | // Hack - we take in account only first touch 259 | ev.pageX = ev.touches[ 0 ].pageX 260 | ev.pageY = ev.touches[ 0 ].pageY 261 | } 262 | 263 | // Normalize offset for browsers which do not provide that value 264 | if (ev.target && (ev.offsetX === undefined || ev.offsetY === undefined)) { 265 | let targetOffset = utils.offset(ev.target) 266 | ev.offsetX = ev.pageX - targetOffset.left 267 | ev.offsetY = ev.pageY - targetOffset.top 268 | } 269 | 270 | // Translate global events into local coordinates 271 | if (ev.target && ev.target !== this.$overlay) { 272 | let targetOffset = utils.offset(ev.target) 273 | let overlayOffset = utils.offset(this.$overlay) 274 | 275 | if (targetOffset && overlayOffset) { 276 | ev.offsetX = ev.offsetX - overlayOffset.left + targetOffset.left 277 | ev.offsetY = ev.offsetY - overlayOffset.top + targetOffset.top 278 | } else { 279 | return false 280 | } 281 | } 282 | 283 | if (ev.type === 'mousemove' || ev.type === 'touchmove') { 284 | this._eventMove(ev) 285 | } else if (ev.type === 'mouseup' || ev.type === 'touchend') { 286 | this._eventMoveEnd(ev) 287 | } 288 | 289 | env.preventDefault() 290 | // Prevent default and propagation 291 | // Don't use peventPropagation as it breaks mouse events 292 | return false 293 | } 294 | 295 | eventsLocal.forEach((item) => { 296 | this.$overlay.addEventListener(item, overlayListener) 297 | }) 298 | 299 | eventsGlobal.forEach((item) => { 300 | window.addEventListener(item, globalListener) 301 | }) 302 | 303 | this._removeEventsHandling = () => { 304 | eventsGlobal.forEach(item => { 305 | window.removeEventListener(item, globalListener) 306 | }) 307 | eventsLocal.forEach(item => { 308 | this.$overlay.addEventListener(item, overlayListener) 309 | }) 310 | } 311 | } 312 | 313 | _eventMoveStart (ev) { 314 | let now = new Date().getTime() 315 | 316 | // Check if it was double click 317 | if (this.overlayLastMoveStartTime && 318 | this.overlayLastMoveStartTime + this._options.dblClickDelay > now) { 319 | // Reset lastMoveStartTime 320 | this.overlayLastMoveStartTime = 0 321 | // Enable View in order to move it to the center 322 | this.overlayInMovement = true 323 | 324 | // Set hook point as View center 325 | this.overlayHookPointX = this.viewW / 2 326 | this.overlayHookPointY = this.viewH / 2 327 | 328 | // Move View to start point 329 | if (this._options.viewLiveFramerate !== false) { 330 | this._eventMove({ 331 | offsetX: this.panelWidth / 2, 332 | offsetY: this.panelHeight / 2 333 | }) 334 | } else { 335 | this._eventMoveEnd({ 336 | offsetX: this.panelWidth / 2, 337 | offsetY: this.panelHeight / 2 338 | }) 339 | } 340 | 341 | this.cy.reset() 342 | 343 | // View should be inactive as we don't want to move it right after double click 344 | this.overlayInMovement = false 345 | } else { 346 | // This is a single click 347 | // Take care as single click happens before double click 2 times 348 | this.overlayLastMoveStartTime = now 349 | this.overlayInMovement = true 350 | // Lock view moving caused by cy events 351 | this.viewLocked = true 352 | 353 | // if event started in View 354 | if (ev.offsetX >= this.viewX && ev.offsetX <= this.viewX + this.viewW && 355 | ev.offsetY >= this.viewY && ev.offsetY <= this.viewY + this.viewH 356 | ) { 357 | this.overlayHookPointX = ev.offsetX - this.viewX 358 | this.overlayHookPointY = ev.offsetY - this.viewY 359 | } else { 360 | // Set hook point as View center 361 | this.overlayHookPointX = this.viewW / 2 362 | this.overlayHookPointY = this.viewH / 2 363 | 364 | // Move View to start point 365 | this._eventMove(ev) 366 | } 367 | } 368 | } 369 | 370 | _eventMove (ev) { 371 | this._checkMousePosition(ev) 372 | 373 | // break if it is useless event 374 | if (!this.overlayInMovement) { 375 | return 376 | } 377 | 378 | // Update cache 379 | this.viewX = ev.offsetX - this.overlayHookPointX 380 | this.viewY = ev.offsetY - this.overlayHookPointY 381 | 382 | // Update view position 383 | this.$view.style.left = this.viewX + 'px' 384 | this.$view.style.top = this.viewY + 'px' 385 | 386 | // Move Cy 387 | if (this._options.viewLiveFramerate !== false) { 388 | // trigger instantly 389 | if (this._options.viewLiveFramerate === 0) { 390 | this._moveCy() 391 | } else if (!this.overlayTimeout) { 392 | // Set a timeout for graph movement 393 | this.overlayTimeout = setTimeout(() => { 394 | this._moveCy() 395 | this.overlayTimeout = false 396 | }, 1000 / this._options.viewLiveFramerate) 397 | } 398 | } 399 | } 400 | 401 | _checkMousePosition (ev) { 402 | // If mouse in over View 403 | if (ev.offsetX > this.viewX && ev.offsetX < this.viewX + this.viewBorderHorizontal + this.viewW && 404 | ev.offsetY > this.viewY && ev.offsetY < this.viewY + this.viewBorderVertical + this.viewH) { 405 | this.$panel.classList.add('mouseover-view') 406 | } else { 407 | this.$panel.classList.remove('mouseover-view') 408 | } 409 | } 410 | 411 | _eventMoveEnd (ev) { 412 | // Unlock view changing caused by graph events 413 | this.viewLocked = false 414 | 415 | // Remove class when mouse is not over Navigator 416 | this.$panel.classList.remove('mouseover-view') 417 | 418 | if (!this.overlayInMovement) { 419 | return 420 | } 421 | 422 | // Trigger one last move 423 | this._eventMove(ev) 424 | 425 | // If mode is not live then move graph on drag end 426 | if (this._options.viewLiveFramerate === false) { 427 | this._moveCy() 428 | } 429 | 430 | // Stop movement permission 431 | this.overlayInMovement = false 432 | } 433 | 434 | _eventZoom (ev) { 435 | let zoomRate = Math.pow(10, ev.wheelDeltaY / 1000 || ev.wheelDelta / 1000 || ev.detail / -32) 436 | let mousePosition = { 437 | left: ev.offsetX, 438 | top: ev.offsetY 439 | } 440 | if (this.cy.zoomingEnabled()) { 441 | this._zoomCy(zoomRate, mousePosition) 442 | } 443 | } 444 | 445 | _updateThumbnailImage () { 446 | if (this._thumbnailUpdating) { 447 | return 448 | } 449 | 450 | this._thumbnailUpdating = true 451 | 452 | let render = () => { 453 | this._checkThumbnailSizesAndUpdate() 454 | this._setupView() 455 | 456 | let img = this.$thumbnail 457 | if (!img) return 458 | 459 | let w = this.panelWidth 460 | let h = this.panelHeight 461 | let bb = this.boundingBox 462 | let zoom = Math.min(w / bb.w, h / bb.h) 463 | 464 | let translate = { 465 | x: (w - zoom * (bb.w)) / 2, 466 | y: (h - zoom * (bb.h)) / 2 467 | } 468 | 469 | let png = this.cy.png({ 470 | full: true, 471 | scale: zoom 472 | }) 473 | 474 | if (png.indexOf('image/png') < 0) { 475 | img.removeAttribute('src') 476 | } else { 477 | img.setAttribute('src', png) 478 | } 479 | 480 | img.style.position = 'absolute' 481 | img.style.left = translate.x + 'px' 482 | img.style.top = translate.y + 'px' 483 | } 484 | 485 | this._onRenderHandler = utils.throttle(render, this._options.rerenderDelay) 486 | 487 | this.cy.onRender(this._onRenderHandler) 488 | } 489 | 490 | _moveCy () { 491 | this.cy.pan({ 492 | x: -(this.viewX + this.viewBorderLeft - this.thumbnailPan.x) * this.width / this.viewW, 493 | y: -(this.viewY + this.viewBorderLeft - this.thumbnailPan.y) * this.height / this.viewH 494 | }) 495 | } 496 | 497 | _zoomCy (zoomRate) { 498 | let zoomCenter = { 499 | x: this.width / 2, 500 | y: this.height / 2 501 | } 502 | 503 | this.cy.zoom({ 504 | level: this.cy.zoom() * zoomRate, 505 | renderedPosition: zoomCenter 506 | }) 507 | } 508 | 509 | destroy () { 510 | this._removeCyListeners() 511 | this._removeEventsHandling() 512 | 513 | // If container is not created by navigator and its removal is prohibited 514 | if (this._options.container && !this._options.removeCustomContainer) { 515 | let childs = this.$panel.childNodes 516 | for (let i = childs.length - 1; i >= 0; i--) { 517 | this.$panel.removeChild(childs[ i ]) 518 | } 519 | } else { 520 | this.$panel.parentNode.removeChild(this.$panel) 521 | } 522 | } 523 | 524 | resize () { 525 | // Cache sizes 526 | let panelRect = this._contianer.getBoundingClientRect() 527 | this.width = panelRect.width 528 | this.height = panelRect.height 529 | this._setupPanel() 530 | this._checkThumbnailSizesAndUpdate() 531 | this._setupView() 532 | } 533 | 534 | bb () { 535 | let bb = this.cy.elements().boundingBox() 536 | 537 | if (bb.w === 0 || bb.h === 0) { 538 | return { 539 | x1: 0, 540 | x2: Infinity, 541 | y1: 0, 542 | y2: Infinity, 543 | w: Infinity, 544 | h: Infinity 545 | } 546 | } 547 | 548 | return bb 549 | } 550 | } 551 | 552 | export default (cytoscape) => { 553 | if (!cytoscape) { return } 554 | 555 | cytoscape('core', 'navigator', function (options) { 556 | return new Navigator(this, options) 557 | }) 558 | } 559 | -------------------------------------------------------------------------------- /src/lib/cyeditor-node-resize/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/22. 3 | */ 4 | 5 | import utils from '../../utils' 6 | 7 | let defaults = { 8 | handleColor: '#000000', // the colour of the handle and the line drawn from it 9 | enabled: true, // whether to start the plugin in the enabled state 10 | minNodeWidth: 30, 11 | minNodeHeight: 30, 12 | triangleSize: 10, 13 | selector: 'node', 14 | lines: 3, 15 | padding: 5, 16 | 17 | start: function (sourceNode) { 18 | // fired when noderesize interaction starts (drag on handle) 19 | }, 20 | complete: function (sourceNode, targetNodes, addedEntities) { 21 | // fired when noderesize is done and entities are added 22 | }, 23 | stop: function (sourceNode) { 24 | // fired when noderesize interaction is stopped (either complete with added edges or incomplete) 25 | } 26 | } 27 | 28 | /** 29 | * Checks if the point p is inside the triangle p0,p1,p2 30 | * using barycentric coordinates 31 | */ 32 | function ptInTriangle (p, p0, p1, p2) { 33 | let A = 1 / 2 * (-p1.y * p2.x + p0.y * (-p1.x + p2.x) + p0.x * (p1.y - p2.y) + p1.x * p2.y) 34 | let sign = A < 0 ? -1 : 1 35 | let s = (p0.y * p2.x - p0.x * p2.y + (p2.y - p0.y) * p.x + (p0.x - p2.x) * p.y) * sign 36 | let t = (p0.x * p1.y - p0.y * p1.x + (p0.y - p1.y) * p.x + (p1.x - p0.x) * p.y) * sign 37 | 38 | return s > 0 && t > 0 && (s + t) < 2 * A * sign 39 | } 40 | 41 | class NodeResize { 42 | constructor (cy, params) { 43 | this.cy = cy 44 | this._container = this.cy.container() 45 | this._listeners = {} 46 | this._drawMode = false 47 | this._drawsClear = true 48 | this._options = {} 49 | this._init(params) 50 | } 51 | 52 | destroy () { 53 | let data = this._options 54 | 55 | if (!data) { 56 | return 57 | } 58 | data.unbind() 59 | this._options = {} 60 | } 61 | 62 | disable () { 63 | this._options.enabled = false 64 | this._options.disabled = true 65 | } 66 | 67 | enable () { 68 | this._options.enabled = true 69 | this._options.disabled = false 70 | } 71 | 72 | resize () { 73 | this.cy.trigger('cyeditor.noderesize-resize') 74 | } 75 | 76 | drawon () { 77 | this._drawMode = true 78 | this._prevUngrabifyState = this.cy.autoungrabify() 79 | this.cy.autoungrabify(true) 80 | this.cy.trigger('cyeditor.noderesize-drawon') 81 | } 82 | 83 | drawoff () { 84 | this._drawMode = false 85 | this.cy.autoungrabify(this._prevUngrabifyState) 86 | this.cy.trigger('cyeditor.noderesize-drawoff') 87 | } 88 | 89 | _init (params) { 90 | this._options = utils.extend(true, {}, defaults, params) 91 | this.canvas = document.createElement('canvas') 92 | this.ctx = this.canvas.getContext('2d') 93 | this._container.append(this.canvas) 94 | this._listeners._sizeCanvas = utils.debounce(this._sizeCanvas, 250).bind(this) 95 | this._listeners._sizeCanvas() 96 | 97 | this._initEvents() 98 | } 99 | 100 | _sizeCanvas () { 101 | let rect = this._container.getBoundingClientRect() 102 | this.canvas.width = rect.width 103 | this.canvas.height = rect.height 104 | utils.css(this.canvas, { 105 | 'position': 'absolute', 106 | 'top': 0, 107 | 'left': 0, 108 | 'zIndex': '999' 109 | }) 110 | 111 | setTimeout(() => { 112 | let canvasBb = utils.offset(this.canvas) 113 | let containerBb = utils.offset(this._container) 114 | utils.css(this.canvas, { 115 | 'top': -(canvasBb.top - containerBb.top) + 'px', 116 | 'left': -(canvasBb.left - containerBb.left) + 'px' 117 | }) 118 | }, 0) 119 | } 120 | 121 | clearDraws () { 122 | if (this._drawsClear) { 123 | return 124 | } 125 | 126 | let containerRect = this._container.getBoundingClientRect() 127 | 128 | let w = containerRect.width 129 | let h = containerRect.height 130 | 131 | this.canvas.getContext('2d').clearRect(0, 0, w, h) 132 | this._drawsClear = true 133 | } 134 | 135 | _disableGestures () { 136 | this._lastPanningEnabled = this.cy.panningEnabled() 137 | this._lastZoomingEnabled = this.cy.zoomingEnabled() 138 | this._lastBoxSelectionEnabled = this.cy.boxSelectionEnabled() 139 | 140 | this.cy 141 | .zoomingEnabled(false) 142 | .panningEnabled(false) 143 | .boxSelectionEnabled(false) 144 | } 145 | 146 | _resetGestures () { 147 | this.cy 148 | .zoomingEnabled(this._lastZoomingEnabled) 149 | .panningEnabled(this._lastPanningEnabled) 150 | .boxSelectionEnabled(this._lastBoxSelectionEnabled) 151 | } 152 | 153 | _resetToDefaultState () { 154 | this.clearDraws() 155 | this.sourceNode = null 156 | this._resetGestures() 157 | } 158 | 159 | _drawHandle (node) { 160 | this.ctx.fillStyle = this._options.handleColor 161 | this.ctx.strokeStyle = this._options.handleColor 162 | let padding = this._options.padding * this.cy.zoom() 163 | let p = node.renderedPosition() 164 | let w = node.renderedOuterWidth() + padding * 2 165 | let h = node.renderedOuterHeight() + padding * 2 166 | let ts = this._options.triangleSize * this.cy.zoom() 167 | 168 | let x1 = p.x + w / 2 - ts 169 | let y1 = p.y + h / 2 170 | let x2 = p.x + w / 2 171 | let y2 = p.y + h / 2 - ts 172 | 173 | let lines = this._options.lines 174 | let wStep = ts / lines 175 | let hStep = ts / lines 176 | let lw = 1.5 * this.cy.zoom() 177 | for (let i = 0; i < lines - 1; i++) { 178 | this.ctx.beginPath() 179 | this.ctx.moveTo(x1, y1) 180 | this.ctx.lineTo(x2, y2) 181 | this.ctx.lineTo(x2, y2 + lw) 182 | this.ctx.lineTo(x1 + lw, y1) 183 | this.ctx.lineTo(x1, y1) 184 | this.ctx.closePath() 185 | this.ctx.fill() 186 | x1 += wStep 187 | y2 += hStep 188 | } 189 | this.ctx.beginPath() 190 | this.ctx.moveTo(x2, y1) 191 | this.ctx.lineTo(x2, y2) 192 | this.ctx.lineTo(x1, y1) 193 | this.ctx.closePath() 194 | this.ctx.fill() 195 | 196 | this._drawsClear = false 197 | } 198 | 199 | _initEvents () { 200 | window.addEventListener('resize', this._listeners._sizeCanvas) 201 | this.cy.on('cyeditor.noderesize-resize', this._listeners._sizeCanvas) 202 | this._grabbingNode = false 203 | 204 | let hoverTimeout 205 | this._lastPanningEnabled = this.cy.panningEnabled() 206 | this._lastZoomingEnabled = this.cy.zoomingEnabled() 207 | this._lastBoxSelectionEnabled = this.cy.boxSelectionEnabled() 208 | 209 | this.cy.style().selector('.noderesize-resized').css({ 210 | 'width': 'data(width)', 211 | 'height': 'data(height)' 212 | }) 213 | 214 | this._listeners.transformHandler = () => { 215 | this.clearDraws() 216 | } 217 | this._listeners._startHandler = this._startHandler.bind(this) 218 | 219 | this.cy.bind('zoom pan', this._listeners.transformHandler) 220 | 221 | let hoverHandler = () => { 222 | if (this._options.disabledd || this._drawMode) { 223 | return // ignore preview nodes 224 | } 225 | 226 | if (this._mdownOnHandle) { // only handle mdown case 227 | return false 228 | } 229 | } 230 | let leaveHandler = () => { 231 | if (this._drawMode) { 232 | return 233 | } 234 | 235 | if (this._mdownOnHandle) { 236 | clearTimeout(hoverTimeout) 237 | } 238 | } 239 | let freeNodeHandler = () => { 240 | this._grabbingNode = false 241 | } 242 | let dragNodeHandler = () => { 243 | if (this._drawMode) { 244 | return 245 | } 246 | setTimeout(() => this.clearDraws(), 50) 247 | } 248 | let removeHandler = (e) => { 249 | let id = e.target.id() 250 | 251 | if (id === this._lastActiveId) { 252 | setTimeout(() => { 253 | this._resetToDefaultState() 254 | }, 16) 255 | } 256 | } 257 | let tapToStartHandler = (e) => { 258 | let node = e.target 259 | 260 | if (!this.sourceNode) { // must not be active 261 | setTimeout(() => { 262 | this.clearDraws() // clear just in case 263 | 264 | this._drawHandle(node) 265 | 266 | node.trigger('cyeditor.noderesize-showhandle') 267 | }, 16) 268 | } 269 | } 270 | let dragHandler = () => { 271 | this._grabbingNode = true 272 | } 273 | let grabHandler = () => { 274 | this.clearDraws() 275 | } 276 | let selector = this._options.selector 277 | this.cy.on('mouseover tap', selector, this._listeners._startHandler) 278 | .on('mouseover tapdragover', selector, hoverHandler) 279 | .on('mouseout tapdragout', selector, leaveHandler) 280 | .on('drag position', selector, dragNodeHandler) 281 | .on('grab', selector, grabHandler) 282 | .on('drag', selector, dragHandler) 283 | .on('free', selector, freeNodeHandler) 284 | .on('remove', selector, removeHandler) 285 | .on('tap', selector, tapToStartHandler) 286 | 287 | this._options.unbind = () => { 288 | window.removeEventListener('resize', this._listeners._sizeCanvas) 289 | this.cy.off('mouseover', selector, this._listeners._startHandler) 290 | .off('mouseover', selector, hoverHandler) 291 | .off('mouseout', selector, leaveHandler) 292 | .off('drag position', selector, dragNodeHandler) 293 | .off('grab', selector, grabHandler) 294 | .off('free', selector, freeNodeHandler) 295 | .off('remove', selector, removeHandler) 296 | .off('tap', selector, tapToStartHandler) 297 | 298 | this.cy.unbind('zoom pan', this._listeners.transformHandler) 299 | } 300 | } 301 | 302 | _startHandler (e) { 303 | let node = e.target 304 | 305 | if (this._options.disabledd || this._drawMode || this._mdownOnHandle || this._grabbingNode || node.isParent()) { 306 | return // don't override existing handle that's being dragged also don't trigger when grabbing a node etc 307 | } 308 | 309 | if (this._listeners.lastMdownHandler) { 310 | this._container.removeEventListener('mousedown', this._listeners.lastMdownHandler, true) 311 | this._container.removeEventListener('touchstart', this._listeners.lastMdownHandler, true) 312 | } 313 | 314 | this._lastActiveId = node.id() 315 | 316 | // remove old handle 317 | this.clearDraws() 318 | 319 | // add new handle 320 | this._drawHandle(node) 321 | 322 | node.trigger('cyeditor.noderesize-showhandle') 323 | let lastPosition = {} 324 | 325 | let mdownHandler = (e) => { 326 | this._container.removeEventListener('mousedown', mdownHandler, true) 327 | this._container.removeEventListener('touchstart', mdownHandler, true) 328 | 329 | let pageX = !e.touches ? e.pageX : e.touches[ 0 ].pageX 330 | let pageY = !e.touches ? e.pageY : e.touches[ 0 ].pageY 331 | let x = pageX - utils.offset(this._container).left 332 | let y = pageY - utils.offset(this._container).top 333 | lastPosition.x = x 334 | lastPosition.y = y 335 | 336 | if (e.button !== 0 && !e.touches) { 337 | return // sorry, no right clicks allowed 338 | } 339 | 340 | let padding = this._options.padding 341 | let rp = node.renderedPosition() 342 | let w = node.renderedOuterWidth() + padding * 2 343 | let h = node.renderedOuterHeight() + padding * 2 344 | let ts = this._options.triangleSize * this.cy.zoom() 345 | 346 | let x1 = rp.x + w / 2 - ts 347 | let y1 = rp.y + h / 2 348 | let x2 = rp.x + w / 2 349 | let y2 = rp.y + h / 2 - ts 350 | 351 | let p = { x: x, y: y } 352 | let p0 = { x: x1, y: y1 } 353 | let p1 = { x: x2, y: y2 } 354 | let p2 = { x: rp.x + w / 2, y: rp.y + h / 2 } 355 | 356 | if (!ptInTriangle(p, p0, p1, p2)) { 357 | return // only consider this a proper mousedown if on the handle 358 | } 359 | 360 | node.addClass('noderesize-resized') 361 | 362 | this._mdownOnHandle = true 363 | 364 | e.preventDefault() 365 | e.stopPropagation() 366 | 367 | this.sourceNode = node 368 | 369 | node.trigger('cyeditor.noderesize-start') 370 | let originalSize = { 371 | width: node.width(), 372 | height: node.height() 373 | } 374 | 375 | let doneMoving = (dmEvent) => { 376 | if (!this._mdownOnHandle) { 377 | return 378 | } 379 | 380 | this._mdownOnHandle = false 381 | window.removeEventListener('mousemove', moveHandler) 382 | window.removeEventListener('touchmove', moveHandler) 383 | this._resetToDefaultState() 384 | 385 | this._options.stop(node) 386 | node.trigger('cyeditor.noderesize-stop') 387 | this.cy.trigger('cyeditor.noderesize-resized', 388 | [ 389 | node, 390 | originalSize, 391 | { 392 | width: node.width(), 393 | height: node.height() 394 | } 395 | ] 396 | ) 397 | } 398 | 399 | [ 'mouseup', 'touchend', 'touchcancel', 'blur' ].forEach(function (e) { 400 | utils.once(window, e, doneMoving) 401 | }) 402 | window.addEventListener('mousemove', moveHandler) 403 | window.addEventListener('touchmove', moveHandler) 404 | this._disableGestures() 405 | this._options.start(node) 406 | 407 | return false 408 | } 409 | 410 | let moveHandler = (e) => { 411 | let pageX = !e.touches ? e.pageX : e.touches[ 0 ].pageX 412 | let pageY = !e.touches ? e.pageY : e.touches[ 0 ].pageY 413 | let x = pageX - utils.offset(this._container).left 414 | let y = pageY - utils.offset(this._container).top 415 | 416 | let dx = x - lastPosition.x 417 | let dy = y - lastPosition.y 418 | 419 | lastPosition.x = x 420 | lastPosition.y = y 421 | let keepAspectRatio = e.ctrlKey 422 | let w = node.data('width') || node.width() 423 | let h = node.data('height') || node.height() 424 | 425 | if (keepAspectRatio) { 426 | let aspectRatio = w / h 427 | if (dy === 0) { 428 | dy = dx = dx * aspectRatio 429 | } else { 430 | dx = dy = (dy < 0 ? Math.min(dx, dy) : Math.max(dx, dy)) * aspectRatio 431 | } 432 | } 433 | dx /= this.cy.zoom() 434 | dy /= this.cy.zoom() 435 | 436 | node.data('width', Math.max(w + dx * 2, this._options.minNodeWidth)) 437 | node.data('height', Math.max(h + dy * 2, this._options.minNodeHeight)) 438 | 439 | this.cy.trigger('cyeditor.noderesize-resizing', [ node, { 440 | width: node.width(), 441 | height: node.height() 442 | } ]) 443 | 444 | this.clearDraws() 445 | this._drawHandle(node) 446 | 447 | return false 448 | } 449 | 450 | this._container.addEventListener('mousedown', mdownHandler, true) 451 | this._container.addEventListener('touchstart', mdownHandler, true) 452 | this._listeners.lastMdownHandler = mdownHandler 453 | } 454 | } 455 | 456 | export default (cytoscape) => { 457 | if (!cytoscape) { return } 458 | 459 | cytoscape('core', 'noderesize', function (options) { 460 | return new NodeResize(this, options) 461 | }) 462 | } 463 | -------------------------------------------------------------------------------- /src/lib/cyeditor-snap-grid/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/21. 3 | */ 4 | import utils from '../../utils' 5 | 6 | class SnapToGrid { 7 | constructor (cy, params) { 8 | this.cy = cy 9 | let defaults = { 10 | stackOrder: -1, 11 | gridSpacing: 35, 12 | strokeStyle: '#CCCCCC', 13 | lineWidth: 1.0, 14 | lineDash: [ 5, 8 ], 15 | zoomDash: true, 16 | panGrid: true, 17 | snapToGrid: true, 18 | drawGrid: true 19 | } 20 | this._options = utils.extend(true, {}, defaults, params) 21 | this._init() 22 | this._initEvents() 23 | } 24 | 25 | _init () { 26 | this._container = this.cy.container() 27 | this.canvas = document.createElement('canvas') 28 | this.ctx = this.canvas.getContext('2d') 29 | this._container.append(this.canvas) 30 | } 31 | 32 | _initEvents () { 33 | window.addEventListener('resize', () => this._resizeCanvas()) 34 | 35 | this.cy.ready(() => { 36 | this._resizeCanvas() 37 | if (this._options.snapToGrid) { 38 | this.snapAll() 39 | } 40 | this.cy.on('zoom', () => this._drawGrid()) 41 | this.cy.on('pan', () => this._drawGrid()) 42 | this.cy.on('free', (e) => this._nodeFreed(e)) 43 | this.cy.on('add', (e) => this._nodeAdded(e)) 44 | }) 45 | } 46 | 47 | _resizeCanvas () { 48 | let rect = this._container.getBoundingClientRect() 49 | this.canvas.height = rect.height 50 | this.canvas.width = rect.width 51 | this.canvas.style.position = 'absolute' 52 | this.canvas.style.top = 0 53 | this.canvas.style.left = 0 54 | this.canvas.style.zIndex = this._options.stackOrder 55 | 56 | setTimeout(() => { 57 | let canvasBb = utils.offset(this.canvas) 58 | let containerBb = utils.offset(this._container) 59 | this.canvas.style.top = -(canvasBb.top - containerBb.top) 60 | this.canvas.style.left = -(canvasBb.left - containerBb.left) 61 | this._drawGrid() 62 | }, 0) 63 | } 64 | 65 | _drawGrid () { 66 | this.clear() 67 | 68 | if (!this._options.drawGrid) { 69 | return 70 | } 71 | 72 | let zoom = this.cy.zoom() 73 | let rect = this._container.getBoundingClientRect() 74 | let canvasWidth = rect.width 75 | let canvasHeight = rect.height 76 | let increment = this._options.gridSpacing * zoom 77 | let pan = this.cy.pan() 78 | let initialValueX = pan.x % increment 79 | let initialValueY = pan.y % increment 80 | 81 | this.ctx.strokeStyle = this._options.strokeStyle 82 | this.ctx.lineWidth = this._options.lineWidth 83 | 84 | if (this._options.zoomDash) { 85 | let zoomedDash = this._options.lineDash.slice() 86 | 87 | for (let i = 0; i < zoomedDash.length; i++) { 88 | zoomedDash[ i ] = this._options.lineDash[ i ] * zoom 89 | } 90 | this.ctx.setLineDash(zoomedDash) 91 | } else { 92 | this.ctx.setLineDash(this._options.lineDash) 93 | } 94 | 95 | if (this._options.panGrid) { 96 | this.ctx.lineDashOffset = -pan.y 97 | } else { 98 | this.ctx.lineDashOffset = 0 99 | } 100 | 101 | for (let i = initialValueX; i < canvasWidth; i += increment) { 102 | this.ctx.beginPath() 103 | this.ctx.moveTo(i, 0) 104 | this.ctx.lineTo(i, canvasHeight) 105 | this.ctx.stroke() 106 | } 107 | 108 | if (this._options.panGrid) { 109 | this.ctx.lineDashOffset = -pan.x 110 | } else { 111 | this.ctx.lineDashOffset = 0 112 | } 113 | 114 | for (let i = initialValueY; i < canvasHeight; i += increment) { 115 | this.ctx.beginPath() 116 | this.ctx.moveTo(0, i) 117 | this.ctx.lineTo(canvasWidth, i) 118 | this.ctx.stroke() 119 | } 120 | } 121 | 122 | _nodeFreed (ev) { 123 | if (this._options.snapToGrid) { 124 | this.snapNode(ev.target) 125 | } 126 | } 127 | 128 | _nodeAdded (ev) { 129 | if (this._options.snapToGrid) { 130 | this.snapNode(ev.target) 131 | } 132 | } 133 | snapNode (node) { 134 | let pos = node.position() 135 | 136 | let cellX = Math.floor(pos.x / this._options.gridSpacing) 137 | let cellY = Math.floor(pos.y / this._options.gridSpacing) 138 | 139 | node.position({ 140 | x: (cellX + 0.5) * this._options.gridSpacing, 141 | y: (cellY + 0.5) * this._options.gridSpacing 142 | }) 143 | } 144 | 145 | snapAll () { 146 | this.cy.nodes().each((node) => { 147 | this.snapNode(node) 148 | }) 149 | } 150 | 151 | refresh () { 152 | this._resizeCanvas() 153 | } 154 | 155 | snapOn () { 156 | this._options.snapToGrid = true 157 | this.snapAll() 158 | this.cy.trigger('cyeditor.snapgridon') 159 | } 160 | 161 | snapOff () { 162 | this._options.snapToGrid = false 163 | this.cy.trigger('cyeditor.snapgridoff') 164 | } 165 | 166 | gridOn () { 167 | this._options.drawGrid = true 168 | this._drawGrid() 169 | this.cy.trigger('cyeditor.gridon') 170 | } 171 | 172 | gridOff () { 173 | this._options.drawGrid = false 174 | this._drawGrid() 175 | this.cy.trigger('cyeditor.gridoff') 176 | } 177 | 178 | clear () { 179 | let rect = this._container.getBoundingClientRect() 180 | let width = rect.width 181 | let height = rect.height 182 | 183 | this.ctx.clearRect(0, 0, width, height) 184 | } 185 | } 186 | 187 | export default (cytoscape) => { 188 | if (!cytoscape) { return } 189 | 190 | cytoscape('core', 'snapToGrid', function (options) { 191 | return new SnapToGrid(this, options) 192 | }) 193 | } 194 | -------------------------------------------------------------------------------- /src/lib/cyeditor-toolbar/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/28. 3 | */ 4 | import utils from '../../utils' 5 | 6 | let defaults = { 7 | container: false, 8 | commands: [ 9 | { command: 'undo', icon: 'icon-undo', disabled: true, title: utils.localize('toolbar-undo') }, 10 | { command: 'redo', icon: 'icon-Redo', disabled: true, title: utils.localize('toolbar-redo') }, 11 | { command: 'zoomin', icon: 'icon-zoomin', disabled: false, title: utils.localize('toolbar-zoomin'), separator: true }, 12 | { command: 'zoomout', icon: 'icon-zoom', disabled: false, title: utils.localize('toolbar-zoomout') }, 13 | { command: 'boxselect', icon: 'icon-selection', disabled: false, title: utils.localize('toolbar-boxselect'), selected: false }, 14 | { command: 'copy', icon: 'icon-copy', disabled: true, title: utils.localize('toolbar-copy'), separator: true }, 15 | { command: 'paste', icon: 'icon-paste', disabled: true, title: utils.localize('toolbar-paste') }, 16 | { command: 'delete', icon: 'icon-delete', disabled: true, title: utils.localize('toolbar-delete') }, 17 | { command: 'leveldown', icon: 'icon-arrow-to-bottom', disabled: true, title: utils.localize('toolbar-leveldown') }, 18 | { command: 'levelup', icon: 'icon-top-arrow-from-top', disabled: true, title: utils.localize('toolbar-levelup') }, 19 | { command: 'line-straight', icon: 'icon-Line-Tool', disabled: false, title: utils.localize('toolbar-line-straight'), selected: false, separator: true }, 20 | { command: 'line-taxi', icon: 'icon-gongzuoliuchengtu', disabled: false, title: utils.localize('toolbar-line-taxi'), selected: false }, 21 | { command: 'line-bezier', icon: 'icon-Bezier-', disabled: false, title: utils.localize('toolbar-line-bezier'), selected: false }, 22 | { command: 'gridon', icon: 'icon-grid', disabled: false, title: utils.localize('toolbar-gridon'), selected: false, separator: true }, 23 | { command: 'fit', icon: 'icon-fullscreen', disabled: false, title: utils.localize('toolbar-fit') }, 24 | { command: 'save', icon: 'icon-save', disabled: false, title: utils.localize('toolbar-save'), separator: true } 25 | ] 26 | } 27 | class Toolbar { 28 | constructor (cy, params) { 29 | this.cy = cy 30 | this._init(params) 31 | this._listeners = {} 32 | this._initEvents() 33 | } 34 | 35 | _init (params) { 36 | this._options = Object.assign({}, defaults, params) 37 | if (Array.isArray(this._options.toolbar)) { 38 | this._options.commands = this._options.commands.filter(item => this._options.toolbar.indexOf(item.command) > -1) 39 | } 40 | 41 | this._initShapePanel() 42 | } 43 | 44 | _initEvents () { 45 | this._listeners.command = (e) => { 46 | let command = e.target.getAttribute('data-command') 47 | if (!command) { return } 48 | let commandOpt = this._options.commands.find(it => it.command === command) 49 | if (['boxselect', 'gridon'].indexOf(command) > -1) { 50 | this.rerender(command, { selected: !commandOpt.selected }) 51 | } else if (['line-straight', 'line-bezier', 'line-taxi'].indexOf(command) > -1) { 52 | this.rerender('line-straight', { selected: command === 'line-straight' }) 53 | this.rerender('line-bezier', { selected: command === 'line-bezier' }) 54 | this.rerender('line-taxi', { selected: command === 'line-taxi' }) 55 | } else if (command === 'fit') { 56 | this.rerender('fit', { icon: commandOpt.icon === 'icon-fullscreen' ? 'icon-fullscreen-exit' : 'icon-fullscreen' }) 57 | } 58 | if (commandOpt) { 59 | this.cy.trigger('cyeditor.toolbar-command', commandOpt) 60 | } 61 | } 62 | this._panel.addEventListener('click', this._listeners.command) 63 | this._listeners.select = this._selectChange.bind(this) 64 | this.cy.on('select unselect', this._listeners.select) 65 | } 66 | 67 | _selectChange () { 68 | let selected = this.cy.$(':selected') 69 | if (selected && selected.length !== this._last_selected_length) { 70 | let hasSelected = selected.length > 0 71 | this._options.commands.forEach(item => { 72 | if ([ 'delete', 'copy', 'leveldown', 'levelup' ].indexOf(item.command) > -1) { 73 | item.disabled = !hasSelected 74 | } 75 | }) 76 | this._panelHtml() 77 | } 78 | this._last_selected_length = selected 79 | } 80 | 81 | _initShapePanel () { 82 | let { _options } = this 83 | if (_options.container) { 84 | if (typeof _options.container === 'string') { 85 | this._panel = utils.query(_options.container)[ 0 ] 86 | } else if (utils.isNode(_options.container)) { 87 | this._panel = _options.container 88 | } 89 | if (!this._panel) { 90 | console.error('There is no any element matching your container') 91 | return 92 | } 93 | } else { 94 | this._panel = document.createElement('div') 95 | document.body.appendChild(this._panel) 96 | } 97 | this._panelHtml() 98 | } 99 | 100 | _panelHtml () { 101 | let icons = '' 102 | this._options.commands.forEach(({ command, title, icon, disabled, selected, separator }) => { 103 | let cls = `${icon} ${disabled ? 'disable' : ''} ${selected === true ? 'selected' : ''}` 104 | if (separator) icons += '' 105 | icons += `` 106 | }) 107 | this._panel.innerHTML = icons 108 | } 109 | 110 | rerender (cmd, options = {}) { 111 | let cmdItem = this._options.commands.find(it => it.command === cmd) 112 | let opt = Object.assign(cmdItem, options) 113 | if (opt) { 114 | let iconEls = utils.query(`i[data-command=${cmd}]`) 115 | iconEls.forEach(item => { 116 | if (item.parentNode === this._panel) { 117 | if (opt.icon) { 118 | item.className = `iconfont command ${opt.icon}` 119 | } 120 | if (opt.disabled) { 121 | utils.addClass(item, 'disable') 122 | } else { 123 | utils.removeClass(item, 'disable') 124 | } 125 | if (opt.selected) { 126 | utils.addClass(item, 'selected') 127 | } else { 128 | utils.removeClass(item, 'selected') 129 | } 130 | } 131 | }) 132 | } 133 | } 134 | } 135 | 136 | export default (cytoscape) => { 137 | if (!cytoscape) { return } 138 | 139 | cytoscape('core', 'toolbar', function (options) { 140 | return new Toolbar(this, options) 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /src/lib/cyeditor-undo-redo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/4/1. 3 | */ 4 | // Get scratch pad reserved for this extension on the given element or the core if 'name' parameter is not set, 5 | // if the 'name' parameter is set then return the related property in the scratch instead of the whole scratchpad 6 | function getScratch (eleOrCy, name) { 7 | if (eleOrCy.scratch('_undoRedo') === undefined) { 8 | eleOrCy.scratch('_undoRedo', {}) 9 | } 10 | 11 | let scratchPad = eleOrCy.scratch('_undoRedo') 12 | 13 | return (name === undefined) ? scratchPad : scratchPad[name] 14 | } 15 | 16 | // Set the a field (described by 'name' parameter) of scratchPad (that is reserved for this extension 17 | // on an element or the core) to the given value (by 'val' parameter) 18 | function setScratch (eleOrCy, name, val) { 19 | let scratchPad = getScratch(eleOrCy) 20 | scratchPad[name] = val 21 | eleOrCy.scratch('_undoRedo', scratchPad) 22 | } 23 | 24 | // Generate an instance of the extension for the given cy instance 25 | function generateInstance (cy) { 26 | let instance = {} 27 | 28 | instance.options = { 29 | isDebug: false, // Debug mode for console messages 30 | actions: {}, // actions to be added 31 | undoableDrag: true, // Whether dragging nodes are undoable can be a function as well 32 | stackSizeLimit: undefined // Size limit of undo stack, note that the size of redo stack cannot exceed size of undo stack 33 | } 34 | 35 | instance.actions = {} 36 | 37 | instance.undoStack = [] 38 | 39 | instance.redoStack = [] 40 | 41 | // resets undo and redo stacks 42 | instance.reset = function (undos, redos) { 43 | this.undoStack = undos || [] 44 | this.redoStack = undos || [] 45 | } 46 | 47 | // Undo last action 48 | instance.undo = function () { 49 | if (!this.isUndoStackEmpty()) { 50 | let action = this.undoStack.pop() 51 | cy.trigger('cyeditor.beforeUndo', [action.name, action.args]) 52 | 53 | let res = this.actions[action.name]._undo(action.args) 54 | 55 | this.redoStack.push({ 56 | name: action.name, 57 | args: res 58 | }) 59 | 60 | cy.trigger('cyeditor.afterUndo', [action.name, action.args, res]) 61 | if (this.options.isDebug) console.log(this.redoStack, this.undoStack) 62 | return res 63 | } else if (this.options.isDebug) { 64 | console.log('Undoing cannot be done because undo stack is empty!') 65 | } 66 | } 67 | 68 | // Redo last action 69 | instance.redo = function () { 70 | if (!this.isRedoStackEmpty()) { 71 | let action = this.redoStack.pop() 72 | 73 | cy.trigger(action.firstTime ? 'cyeditor.beforeDo' : 'cyeditor.beforeRedo', [action.name, action.args]) 74 | 75 | if (!action.args) { action.args = {} } 76 | action.args.firstTime = !!action.firstTime 77 | 78 | let res = this.actions[action.name]._do(action.args) 79 | 80 | this.undoStack.push({ 81 | name: action.name, 82 | args: res 83 | }) 84 | 85 | if (this.options.stackSizeLimit !== undefined && this.undoStack.length > this.options.stackSizeLimit) { 86 | this.undoStack.shift() 87 | } 88 | 89 | cy.trigger(action.firstTime ? 'cyeditor.afterDo' : 'cyeditor.afterRedo', [action.name, action.args, res]) 90 | if (this.options.isDebug) console.log(this.redoStack, this.undoStack) 91 | return res 92 | } else if (this.options.isDebug) { 93 | console.log('Redoing cannot be done because redo stack is empty!') 94 | } 95 | } 96 | 97 | // Calls registered function with action name actionName via actionFunction(args) 98 | instance.do = function (actionName, args) { 99 | this.redoStack.length = 0 100 | this.redoStack.push({ 101 | name: actionName, 102 | args: args, 103 | firstTime: true 104 | }) 105 | 106 | return this.redo() 107 | } 108 | 109 | // Undo all actions in undo stack 110 | instance.undoAll = function () { 111 | while (!this.isUndoStackEmpty()) { 112 | this.undo() 113 | } 114 | } 115 | 116 | // Redo all actions in redo stack 117 | instance.redoAll = function () { 118 | while (!this.isRedoStackEmpty()) { 119 | this.redo() 120 | } 121 | } 122 | 123 | // Register action with its undo function & action name. 124 | instance.action = function (actionName, _do, _undo) { 125 | this.actions[actionName] = { 126 | _do: _do, 127 | _undo: _undo 128 | } 129 | 130 | return this 131 | } 132 | 133 | // Removes action stated with actionName param 134 | instance.removeAction = function (actionName) { 135 | delete this.actions[actionName] 136 | } 137 | 138 | // Gets whether undo stack is empty 139 | instance.isUndoStackEmpty = function () { 140 | return (this.undoStack.length === 0) 141 | } 142 | 143 | // Gets whether redo stack is empty 144 | instance.isRedoStackEmpty = function () { 145 | return (this.redoStack.length === 0) 146 | } 147 | 148 | // Gets actions (with their args) in undo stack 149 | instance.getUndoStack = function () { 150 | return this.undoStack 151 | } 152 | 153 | // Gets actions (with their args) in redo stack 154 | instance.getRedoStack = function () { 155 | return this.redoStack 156 | } 157 | 158 | return instance 159 | } 160 | 161 | function setDragUndo (cy, undoable) { 162 | let lastMouseDownNodeInfo = null 163 | 164 | cy.on('grab', 'node', function () { 165 | if (typeof undoable === 'function' ? undoable.call(this) : undoable) { 166 | lastMouseDownNodeInfo = {} 167 | lastMouseDownNodeInfo.lastMouseDownPosition = { 168 | x: this.position('x'), 169 | y: this.position('y') 170 | } 171 | lastMouseDownNodeInfo.node = this 172 | } 173 | }) 174 | cy.on('free', 'node', function () { 175 | let instance = getScratch(cy, 'instance') 176 | 177 | if (typeof undoable === 'function' ? undoable.call(this) : undoable) { 178 | if (lastMouseDownNodeInfo === null) { 179 | return 180 | } 181 | let node = lastMouseDownNodeInfo.node 182 | let lastMouseDownPosition = lastMouseDownNodeInfo.lastMouseDownPosition 183 | let mouseUpPosition = { 184 | x: node.position('x'), 185 | y: node.position('y') 186 | } 187 | if (mouseUpPosition.x !== lastMouseDownPosition.x || 188 | mouseUpPosition.y !== lastMouseDownPosition.y) { 189 | let positionDiff = { 190 | x: mouseUpPosition.x - lastMouseDownPosition.x, 191 | y: mouseUpPosition.y - lastMouseDownPosition.y 192 | } 193 | 194 | let nodes 195 | if (node.selected()) { 196 | nodes = cy.nodes(':visible').filter(':selected') 197 | } else { 198 | nodes = cy.collection([node]) 199 | } 200 | 201 | let param = { 202 | positionDiff: positionDiff, 203 | nodes: nodes, 204 | move: false 205 | } 206 | 207 | instance.do('drag', param) 208 | 209 | lastMouseDownNodeInfo = null 210 | } 211 | } 212 | }) 213 | } 214 | 215 | // Default actions 216 | function defaultActions (cy) { 217 | function getTopMostNodes (nodes) { 218 | let nodesMap = {} 219 | for (let i = 0; i < nodes.length; i++) { 220 | nodesMap[nodes[i].id()] = true 221 | } 222 | let roots = nodes.filter(function (ele, i) { 223 | if (typeof ele === 'number') { 224 | ele = i 225 | } 226 | let parent = ele.parent()[0] 227 | while (parent !== null && parent) { 228 | if (parent && nodesMap[parent.id()]) { 229 | return false 230 | } 231 | parent = parent.parent()[0] 232 | } 233 | return true 234 | }) 235 | 236 | return roots 237 | } 238 | 239 | function moveNodes (positionDiff, nodes, notCalcTopMostNodes) { 240 | let topMostNodes = notCalcTopMostNodes ? nodes : getTopMostNodes(nodes) 241 | for (let i = 0; i < topMostNodes.length; i++) { 242 | let node = topMostNodes[i] 243 | let oldX = node.position('x') 244 | let oldY = node.position('y') 245 | // Only simple nodes are moved since the movement of compounds caused the position to be moved twice 246 | if (!node.isParent()) { 247 | node.position({ 248 | x: oldX + positionDiff.x, 249 | y: oldY + positionDiff.y 250 | }) 251 | } 252 | let children = node.children() 253 | moveNodes(positionDiff, children, true) 254 | } 255 | } 256 | 257 | function getEles (_eles) { 258 | return (typeof _eles === 'string') ? cy.$(_eles) : _eles 259 | } 260 | 261 | function restoreEles (_eles) { 262 | return getEles(_eles).restore() 263 | } 264 | 265 | function returnToPositions (positions) { 266 | let currentPositions = {} 267 | cy.nodes().positions(function (ele, i) { 268 | if (typeof ele === 'number') { 269 | ele = i 270 | } 271 | 272 | currentPositions[ele.id()] = { 273 | x: ele.position('x'), 274 | y: ele.position('y') 275 | } 276 | let pos = positions[ele.id()] 277 | return { 278 | x: pos.x, 279 | y: pos.y 280 | } 281 | }) 282 | 283 | return currentPositions 284 | } 285 | 286 | function getNodePositions () { 287 | let positions = {} 288 | let nodes = cy.nodes() 289 | for (let i = 0; i < nodes.length; i++) { 290 | let node = nodes[i] 291 | positions[node.id()] = { 292 | x: node.position('x'), 293 | y: node.position('y') 294 | } 295 | } 296 | return positions 297 | } 298 | 299 | function changeParent (param) { 300 | let result = { 301 | } 302 | // If this is first time we should move the node to its new parent and relocate it by given posDiff params 303 | // else we should remove the moved eles and restore the eles to restore 304 | if (param.firstTime) { 305 | let newParentId = param.parentData === undefined ? null : param.parentData 306 | // These eles includes the nodes and their connected edges and will be removed in nodes.move(). 307 | // They should be restored in undo 308 | let withDescendant = param.nodes.union(param.nodes.descendants()) 309 | result.elesToRestore = withDescendant.union(withDescendant.connectedEdges()) 310 | // These are the eles created by nodes.move(), they should be removed in undo. 311 | result.movedEles = param.nodes.move({ 'parent': newParentId }) 312 | 313 | let posDiff = { 314 | x: param.posDiffX, 315 | y: param.posDiffY 316 | } 317 | 318 | moveNodes(posDiff, result.movedEles) 319 | } else { 320 | result.elesToRestore = param.movedEles.remove() 321 | result.movedEles = param.elesToRestore.restore() 322 | } 323 | 324 | if (param.callback) { 325 | result.callback = param.callback // keep the provided callback so it can be reused after undo/redo 326 | param.callback(result.movedEles) // apply the callback on newly created elements 327 | } 328 | 329 | return result 330 | } 331 | 332 | // function registered in the defaultActions below 333 | // to be used like .do('batch', actionList) 334 | // allows to apply any quantity of registered action in one go 335 | // the whole batch can be undone/redone with one key press 336 | function batch (actionList, doOrUndo) { 337 | let tempStack = [] // corresponds to the results of every action queued in actionList 338 | let instance = getScratch(cy, 'instance') // get extension instance through cy 339 | let actions = instance.actions 340 | 341 | // here we need to check in advance if all the actions provided really correspond to available functions 342 | // if one of the action cannot be executed, the whole batch is corrupted because we can't go back after 343 | for (let i = 0; i < actionList.length; i++) { 344 | let action = actionList[i] 345 | if (!actions.hasOwnProperty(action.name)) { 346 | console.error('Action ' + action.name + ' does not exist as an undoable function') 347 | } 348 | } 349 | 350 | for (let i = 0; i < actionList.length; i++) { 351 | let action = actionList[i] 352 | // firstTime property is automatically injected into actionList by the do() function 353 | // we use that to pass it down to the actions in the batch 354 | action.param.firstTime = actionList.firstTime 355 | let actionResult 356 | if (doOrUndo === 'undo') { 357 | actionResult = actions[action.name]._undo(action.param) 358 | } else { 359 | actionResult = actions[action.name]._do(action.param) 360 | } 361 | 362 | tempStack.unshift({ 363 | name: action.name, 364 | param: actionResult 365 | }) 366 | } 367 | 368 | return tempStack 369 | }; 370 | 371 | return { 372 | 'add': { 373 | _do: function (eles) { 374 | return eles.firstTime ? cy.add(eles) : restoreEles(eles) 375 | }, 376 | _undo: cy.remove 377 | }, 378 | 'remove': { 379 | _do: cy.remove, 380 | _undo: restoreEles 381 | }, 382 | 'restore': { 383 | _do: restoreEles, 384 | _undo: cy.remove 385 | }, 386 | 'select': { 387 | _do: function (_eles) { 388 | return getEles(_eles).select() 389 | }, 390 | _undo: function (_eles) { 391 | return getEles(_eles).unselect() 392 | } 393 | }, 394 | 'unselect': { 395 | _do: function (_eles) { 396 | return getEles(_eles).unselect() 397 | }, 398 | _undo: function (_eles) { 399 | return getEles(_eles).select() 400 | } 401 | }, 402 | 'move': { 403 | _do: function (args) { 404 | let eles = getEles(args.eles) 405 | let nodes = eles.nodes() 406 | let edges = eles.edges() 407 | 408 | return { 409 | oldNodes: nodes, 410 | newNodes: nodes.move(args.location), 411 | oldEdges: edges, 412 | newEdges: edges.move(args.location) 413 | } 414 | }, 415 | _undo: function (eles) { 416 | let newEles = cy.collection() 417 | let location = {} 418 | if (eles.newNodes.length > 0) { 419 | location.parent = eles.newNodes[0].parent() 420 | 421 | for (let i = 0; i < eles.newNodes.length; i++) { 422 | let newNode = eles.newNodes[i].move({ 423 | parent: eles.oldNodes[i].parent() 424 | }) 425 | newEles.union(newNode) 426 | } 427 | } else { 428 | location.source = location.newEdges[0].source() 429 | location.target = location.newEdges[0].target() 430 | 431 | for (let i = 0; i < eles.newEdges.length; i++) { 432 | let newEdge = eles.newEdges[i].move({ 433 | source: eles.oldEdges[i].source(), 434 | target: eles.oldEdges[i].target() 435 | }) 436 | newEles.union(newEdge) 437 | } 438 | } 439 | return { 440 | eles: newEles, 441 | location: location 442 | } 443 | } 444 | }, 445 | 'drag': { 446 | _do: function (args) { 447 | if (args.move) { moveNodes(args.positionDiff, args.nodes) } 448 | return args 449 | }, 450 | _undo: function (args) { 451 | let diff = { 452 | x: -1 * args.positionDiff.x, 453 | y: -1 * args.positionDiff.y 454 | } 455 | let result = { 456 | positionDiff: args.positionDiff, 457 | nodes: args.nodes, 458 | move: true 459 | } 460 | moveNodes(diff, args.nodes) 461 | return result 462 | } 463 | }, 464 | 'layout': { 465 | _do: function (args) { 466 | if (args.firstTime) { 467 | let positions = getNodePositions() 468 | let layout 469 | if (args.eles) { 470 | layout = getEles(args.eles).layout(args.options) 471 | } else { 472 | layout = cy.layout(args.options) 473 | } 474 | 475 | // Do this check for cytoscape.js backward compatibility 476 | if (layout && layout.run) { 477 | layout.run() 478 | } 479 | 480 | return positions 481 | } else { return returnToPositions(args) } 482 | }, 483 | _undo: function (nodesData) { 484 | return returnToPositions(nodesData) 485 | } 486 | }, 487 | 'changeParent': { 488 | _do: function (args) { 489 | return changeParent(args) 490 | }, 491 | _undo: function (args) { 492 | return changeParent(args) 493 | } 494 | }, 495 | 'batch': { 496 | _do: function (args) { 497 | return batch(args, 'do') 498 | }, 499 | _undo: function (args) { 500 | return batch(args, 'undo') 501 | } 502 | } 503 | } 504 | } 505 | 506 | export default (cytoscape) => { 507 | if (!cytoscape) { return } 508 | 509 | cytoscape('core', 'undoRedo', function (options, dontInit) { 510 | let cy = this 511 | let instance = getScratch(cy, 'instance') || generateInstance(cy) 512 | setScratch(cy, 'instance', instance) 513 | 514 | if (options) { 515 | for (let key in options) { 516 | if (instance.options.hasOwnProperty(key)) { instance.options[key] = options[key] } 517 | } 518 | 519 | if (options.actions) { 520 | for (let key in options.actions) { instance.actions[key] = options.actions[key] } 521 | } 522 | } 523 | 524 | if (!getScratch(cy, 'isInitialized') && !dontInit) { 525 | let defActions = defaultActions(cy) 526 | for (let key in defActions) { instance.actions[key] = defActions[key] } 527 | 528 | setDragUndo(cy, instance.options.undoableDrag) 529 | setScratch(cy, 'isInitialized', true) 530 | } 531 | 532 | return instance 533 | }) 534 | } 535 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/25. 3 | */ 4 | 5 | import cytoscape from 'cytoscape' 6 | import utils from '../utils' 7 | import EventBus from '../utils/eventbus' 8 | import toolbar from './cyeditor-toolbar' 9 | import snapGrid from './cyeditor-snap-grid' 10 | import undoRedo from './cyeditor-undo-redo' 11 | import clipboard from './cyeditor-clipboard' 12 | import cynavigator from './cyeditor-navigator' 13 | import edgehandles from './cyeditor-edgehandles' 14 | import noderesize from './cyeditor-node-resize' 15 | import editElements from './cyeditor-edit-elements' 16 | import dragAddNodes from './cyeditor-drag-add-nodes' 17 | import contextMenu from './cyeditor-context-menu' 18 | import { defaultConfData, defaultEditorConfig, defaultNodeTypes } from '../defaults' 19 | 20 | cytoscape.use(edgehandles) 21 | cytoscape.use(cynavigator) 22 | cytoscape.use(snapGrid) 23 | cytoscape.use(noderesize) 24 | cytoscape.use(dragAddNodes) 25 | cytoscape.use(editElements) 26 | cytoscape.use(toolbar) 27 | cytoscape.use(clipboard) 28 | cytoscape.use(undoRedo) 29 | cytoscape.use(contextMenu) 30 | 31 | class CyEditor extends EventBus { 32 | constructor (params = defaultEditorConfig) { 33 | super() 34 | this._plugins = {} 35 | this._listeners = {} 36 | this._init(params) 37 | } 38 | 39 | _init (params) { 40 | this._initOptions(params) 41 | this._initDom() 42 | this._initCy() 43 | this._initPlugin() 44 | this._initEvents() 45 | } 46 | 47 | _verifyParams (params) { 48 | const mustBe = (arr, type) => { 49 | let valid = true 50 | arr.forEach(item => { 51 | const typeItem = typeof params[item] 52 | if (typeItem !== type) { 53 | console.warn(`'editor.${item}' must be ${type}`) 54 | valid = false 55 | } 56 | }) 57 | return valid 58 | } 59 | const { 60 | zoomRate, 61 | toolbar, 62 | nodeTypes 63 | } = params 64 | mustBe(['noderesize', 'dragAddNodes', 'elementsInfo', 'snapGrid', 'navigator', 'useDefaultNodeTypes', 'autoSave'], 'boolean') 65 | mustBe(['beforeAdd', 'afterAdd'], 'function') 66 | 67 | if (zoomRate <= 0 || zoomRate >= 1 || typeof zoomRate !== 'number') { 68 | console.warn(`'editor.zoomRate' must be < 1 and > 0`) 69 | } 70 | 71 | if (typeof toolbar !== 'boolean' && !Array.isArray(toolbar)) { 72 | console.warn(`'editor.nodeTypes' must be boolean or array`) 73 | } 74 | 75 | if (!Array.isArray(nodeTypes)) { 76 | console.warn(`'editor.nodeTypes' must be array`) 77 | } 78 | } 79 | 80 | _initOptions (params = {}) { 81 | this.editorOptions = Object.assign({}, defaultEditorConfig.editor, params.editor) 82 | this._verifyParams(this.editorOptions) 83 | const { useDefaultNodeTypes, zoomRate } = this.editorOptions 84 | this._handleOptonsChange = { 85 | snapGrid: this._snapGridChange, 86 | lineType: this._lineTypeChange 87 | } 88 | if (params.editor && params.editor.nodeTypes && useDefaultNodeTypes) { 89 | this.setOption('nodeTypes', defaultNodeTypes.concat(params.editor.nodeTypes)) 90 | } 91 | if (zoomRate <= 0 || zoomRate >= 1) { 92 | console.error('zoomRate must be float number, greater than 0 and less than 1') 93 | } 94 | this.cyOptions = Object.assign({}, defaultEditorConfig.cy, params.cy) 95 | const { elements } = this.cyOptions 96 | if (elements) { 97 | if (Array.isArray(elements.nodes)) { 98 | elements.nodes.forEach(node => { 99 | node.data = Object.assign({}, defaultConfData.node, node.data) 100 | }) 101 | } 102 | if (Array.isArray(elements.edges)) { 103 | elements.edges.forEach(edge => { 104 | edge.data = Object.assign({}, defaultConfData.edge, edge.data) 105 | }) 106 | } 107 | } 108 | } 109 | 110 | _initCy () { 111 | this.cyOptions.container = '#cy' 112 | if (typeof this.cyOptions.container === 'string') { 113 | this.cyOptions.container = utils.query(this.cyOptions.container)[ 0 ] 114 | } 115 | if (!this.cyOptions.container) { 116 | console.error('There is no any element matching your container') 117 | return 118 | } 119 | this.cy = cytoscape(this.cyOptions) 120 | } 121 | 122 | _initDom () { 123 | let { dragAddNodes, navigator, elementsInfo, toolbar, container } = this.editorOptions 124 | let left = dragAddNodes ? `
` : '' 125 | let navigatorDom = navigator ? `
${utils.localize('window-navigator')}
` : '' 126 | let infoDom = elementsInfo ? `
` : '' 127 | let domHtml = toolbar ? '
' : '' 128 | let right = '' 129 | if (navigator || elementsInfo) { 130 | right = `
131 | ${navigatorDom} 132 | ${infoDom} 133 |
` 134 | } 135 | domHtml += `
136 | ${left} 137 |
138 | ${right} 139 |
` 140 | let editorContianer 141 | if (container) { 142 | if (typeof container === 'string') { 143 | editorContianer = utils.query(container)[ 0 ] 144 | } else if (utils.isNode(container)) { 145 | editorContianer = container 146 | } 147 | if (!editorContianer) { 148 | console.error('There is no any element matching your container') 149 | return 150 | } 151 | } else { 152 | editorContianer = document.createElement('div') 153 | editorContianer.className = 'cy-editor-container' 154 | document.body.appendChild(editorContianer) 155 | } 156 | editorContianer.innerHTML = domHtml 157 | } 158 | 159 | _initEvents () { 160 | const { editElements, edgehandles, noderesize, undoRedo } = this._plugins 161 | 162 | this._listeners.showElementInfo = () => { 163 | if (editElements) { 164 | editElements.showElementsInfo() 165 | } 166 | } 167 | 168 | this._listeners.handleCommand = this._handleCommand.bind(this) 169 | 170 | this._listeners.hoverout = (e) => { 171 | if (edgehandles) { 172 | edgehandles.active = true 173 | edgehandles.stop(e) 174 | } 175 | if (noderesize) { 176 | noderesize.clearDraws() 177 | } 178 | } 179 | 180 | this._listeners.select = (e) => { 181 | if (this._doAction === 'select') return 182 | if (undoRedo) { 183 | this._undoRedoAction('select', e.target) 184 | } 185 | } 186 | 187 | this._listeners.addEles = (evt, el) => { 188 | if (el.position) { 189 | let panZoom = { pan: this.cy.pan(), zoom: this.cy.zoom() } 190 | let x = (el.position.x - panZoom.pan.x) / panZoom.zoom 191 | let y = (el.position.y - panZoom.pan.y) / panZoom.zoom 192 | el.position = { x, y } 193 | } 194 | el.firstTime = true 195 | if (!this._hook('beforeAdd', el, true)) return 196 | if (undoRedo) { 197 | this._undoRedoAction('add', el) 198 | } else { 199 | this.cy.add(el) 200 | } 201 | this._hook('afterAdd', el) 202 | this.emit('change', el, this) 203 | } 204 | 205 | this._listeners._changeUndoRedo = this._changeUndoRedo.bind(this) 206 | 207 | this.cy.on('cyeditor.noderesize-resized cyeditor.noderesize-resizing', this._listeners.showElementInfo) 208 | .on('cyeditor.toolbar-command', this._listeners.handleCommand) 209 | .on('click', this._listeners.hoverout) 210 | .on('select', this._listeners.select) 211 | .on('cyeditor.addnode', this._listeners.addEles) 212 | .on('cyeditor.afterDo cyeditor.afterRedo cyeditor.afterUndo', this._listeners._changeUndoRedo) 213 | this.emit('ready') 214 | } 215 | 216 | _initPlugin () { 217 | const { dragAddNodes, elementsInfo, toolbar, 218 | contextMenu, snapGrid, navigator, noderesize } = this.editorOptions 219 | // edge 220 | this._plugins.edgehandles = this.cy.edgehandles({ 221 | snap: false, 222 | handlePosition () { 223 | return 'middle middle' 224 | }, 225 | edgeParams: this._edgeParams.bind(this) 226 | }) 227 | 228 | // drag node add to cy 229 | if (dragAddNodes) { 230 | this._plugins.dragAddNodes = this.cy.dragAddNodes({ 231 | container: '.left', 232 | nodeTypes: this.editorOptions.nodeTypes 233 | }) 234 | } 235 | 236 | // edit panel 237 | if (elementsInfo) { 238 | this._plugins.editElements = this.cy.editElements({ 239 | container: '#info' 240 | }) 241 | } 242 | 243 | // toolbar 244 | if (Array.isArray(toolbar) || toolbar === true) { 245 | this._plugins.toolbar = this.cy.toolbar({ 246 | container: '#toolbar', 247 | toolbar: toolbar 248 | }) 249 | if (toolbar === true || toolbar.indexOf('gridon') > -1) { 250 | this.setOption('snapGrid', true) 251 | } 252 | } 253 | 254 | let needUndoRedo = toolbar === true 255 | let needClipboard = toolbar === true 256 | if (Array.isArray(toolbar)) { 257 | needUndoRedo = toolbar.indexOf('undo') > -1 || 258 | toolbar.indexOf('redo') > -1 259 | needClipboard = toolbar.indexOf('copy') > -1 || 260 | toolbar.indexOf('paset') > -1 261 | } 262 | 263 | // clipboard 264 | if (needClipboard) { 265 | this._plugins.clipboard = this.cy.clipboard() 266 | } 267 | // undo-redo 268 | if (needUndoRedo) { 269 | this._plugins.undoRedo = this.cy.undoRedo() 270 | } 271 | 272 | // snap-grid 273 | if (snapGrid) { 274 | this._plugins.cySnapToGrid = this.cy.snapToGrid() 275 | } 276 | 277 | // navigator 278 | if (navigator) { 279 | this.cy.navigator({ 280 | container: '#thumb' 281 | }) 282 | } 283 | 284 | // noderesize 285 | if (noderesize) { 286 | this._plugins.noderesize = this.cy.noderesize({ 287 | selector: 'node[resize]' 288 | }) 289 | } 290 | 291 | // context-menu 292 | if (contextMenu) { 293 | this._plugins.contextMenu = this.cy.contextMenu() 294 | } 295 | } 296 | 297 | _snapGridChange () { 298 | if (!this._plugins.cySnapToGrid) return 299 | if (this.editorOptions.snapGrid) { 300 | this._plugins.cySnapToGrid.gridOn() 301 | this._plugins.cySnapToGrid.snapOn() 302 | } else { 303 | this._plugins.cySnapToGrid.gridOff() 304 | this._plugins.cySnapToGrid.snapOff() 305 | } 306 | } 307 | 308 | _edgeParams () { 309 | return { 310 | data: { lineType: this.editorOptions.lineType } 311 | } 312 | } 313 | 314 | _lineTypeChange (value) { 315 | let selected = this.cy.$('edge:selected') 316 | if (selected.length < 1) { 317 | selected = this.cy.$('edge') 318 | } 319 | selected.forEach(item => { 320 | item.data({ 321 | lineType: value 322 | }) 323 | }) 324 | } 325 | 326 | _handleCommand (evt, item) { 327 | switch (item.command) { 328 | case 'undo' : 329 | this.undo() 330 | break 331 | case 'redo' : 332 | this.redo() 333 | break 334 | case 'gridon' : 335 | this.toggleGrid() 336 | break 337 | case 'zoomin' : 338 | this.zoom(1) 339 | break 340 | case 'zoomout' : 341 | this.zoom(-1) 342 | break 343 | case 'levelup' : 344 | this.changeLevel(1) 345 | break 346 | case 'leveldown' : 347 | this.changeLevel(-1) 348 | break 349 | case 'copy' : 350 | this.copy() 351 | break 352 | case 'paste' : 353 | this.paste() 354 | break 355 | case 'fit' : 356 | this.fit() 357 | break 358 | case 'save' : 359 | this.save() 360 | break 361 | case 'delete' : 362 | this.deleteSelected() 363 | break 364 | case 'line-bezier' : 365 | this.setOption('lineType', 'bezier') 366 | break 367 | case 'line-taxi' : 368 | this.setOption('lineType', 'taxi') 369 | break 370 | case 'line-straight' : 371 | this.setOption('lineType', 'straight') 372 | break 373 | case 'boxselect': 374 | this.cy.userPanningEnabled(!item.selected) 375 | this.cy.boxSelectionEnabled(item.selected) 376 | break 377 | default: 378 | break 379 | } 380 | } 381 | 382 | _changeUndoRedo () { 383 | if (!this._plugins.undoRedo || !this._plugins.toolbar) return 384 | let canRedo = this._plugins.undoRedo.isRedoStackEmpty() 385 | let canUndo = this._plugins.undoRedo.isUndoStackEmpty() 386 | if (canRedo !== this.lastCanRedo || canUndo !== this.lastCanUndo) { 387 | this._plugins.toolbar.rerender('undo', { disabled: canUndo }) 388 | this._plugins.toolbar.rerender('redo', { disabled: canRedo }) 389 | } 390 | this.lastCanRedo = canRedo 391 | this.lastCanUndo = canUndo 392 | } 393 | 394 | _undoRedoAction (cmd, options) { 395 | this._doAction = cmd 396 | this._plugins.undoRedo.do(cmd, options) 397 | } 398 | 399 | _hook (hook, params, result = false) { 400 | if (typeof this.editorOptions[hook] === 'function') { 401 | const res = this.editorOptions[hook](params) 402 | return result ? res : true 403 | } 404 | } 405 | 406 | /** 407 | * change editor option, support snapGrid, lineType 408 | * @param {string|object} key 409 | * @param {*} value 410 | */ 411 | setOption (key, value) { 412 | if (typeof key === 'string') { 413 | this.editorOptions[key] = value 414 | if (typeof this._handleOptonsChange[key] === 'function') { 415 | this._handleOptonsChange[key].call(this, value) 416 | } 417 | } else if (typeof key === 'object') { 418 | Object.assign(this.editorOptions, key) 419 | } 420 | } 421 | 422 | undo () { 423 | if (this._plugins.undoRedo) { 424 | let stack = this._plugins.undoRedo.getRedoStack() 425 | if (stack.length) { 426 | this._doAction = stack[stack.length - 1].action 427 | } 428 | this._plugins.undoRedo.undo() 429 | } else { 430 | console.warn('Can not `undo`, please check the initialize option `editor.toolbar`') 431 | } 432 | } 433 | 434 | redo () { 435 | if (this._plugins.undoRedo) { 436 | let stack = this._plugins.undoRedo.getUndoStack() 437 | if (stack.length) { 438 | this._doAction = stack[stack.length - 1].action 439 | } 440 | this._plugins.undoRedo.redo() 441 | } else { 442 | console.warn('Can not `redo`, please check the initialize option `editor.toolbar`') 443 | } 444 | } 445 | 446 | copy () { 447 | if (this._plugins.clipboard) { 448 | let selected = this.cy.$(':selected') 449 | if (selected.length) { 450 | this._cpids = this._plugins.clipboard.copy(selected) 451 | if (this._cpids && this._plugins.toolbar) { 452 | this._plugins.toolbar.rerender('paste', { disabled: false }) 453 | } 454 | } 455 | } else { 456 | console.warn('Can not `copy`, please check the initialize option `editor.toolbar`') 457 | } 458 | } 459 | 460 | paste () { 461 | if (this._plugins.clipboard) { 462 | if (this._cpids) { 463 | this._plugins.clipboard.paste(this._cpids) 464 | } 465 | } else { 466 | console.warn('Can not `paste`, please check the initialize option `editor.toolbar`') 467 | } 468 | } 469 | 470 | changeLevel (type = 0) { 471 | let selected = this.cy.$(':selected') 472 | if (selected.length) { 473 | selected.forEach(el => { 474 | let pre = el.style() 475 | el.style('z-index', pre.zIndex - 0 + type > -1 ? pre.zIndex - 0 + type : 0) 476 | }) 477 | } 478 | } 479 | 480 | deleteSelected () { 481 | let selected = this.cy.$(':selected') 482 | if (selected.length) { 483 | if (this._plugins.undoRedo) { 484 | this._undoRedoAction('remove', selected) 485 | } 486 | this.cy.remove(selected) 487 | } 488 | } 489 | 490 | async save () { 491 | try { 492 | let blob = await this.cy.png({ output: 'blob-promise' }) 493 | if (window.navigator.msSaveBlob) { 494 | window.navigator.msSaveBlob(blob, `chart-${Date.now()}.png`) 495 | } else { 496 | let a = document.createElement('a') 497 | a.download = `chart-${Date.now()}.png` 498 | a.href = window.URL.createObjectURL(blob) 499 | a.click() 500 | } 501 | } catch (e) { 502 | console.log(e) 503 | } 504 | } 505 | 506 | fit () { 507 | if (!this._fit_status) { 508 | this._fit_status = { pan: this.cy.pan(), zoom: this.cy.zoom() } 509 | this.cy.fit() 510 | } else { 511 | this.cy.viewport({ 512 | zoom: this._fit_status.zoom, 513 | pan: this._fit_status.pan 514 | }) 515 | this._fit_status = null 516 | } 517 | } 518 | 519 | zoom (type = 1, level) { 520 | level = level || this.editorOptions.zoomRate 521 | let w = this.cy.width() 522 | let h = this.cy.height() 523 | let zoom = this.cy.zoom() + level * type 524 | let pan = this.cy.pan() 525 | pan.x = pan.x + -1 * w * level * type / 2 526 | pan.y = pan.y + -1 * h * level * type / 2 527 | this.cy.viewport({ 528 | zoom, 529 | pan 530 | }) 531 | } 532 | 533 | toggleGrid () { 534 | if (this._plugins.cySnapToGrid) { 535 | this.setOption('snapGrid', !this.editorOptions.snapGrid) 536 | } else { 537 | console.warn('Can not `toggleGrid`, please check the initialize option') 538 | } 539 | } 540 | 541 | jpg (opt = {}) { 542 | return this.cy.png(opt) 543 | } 544 | 545 | png (opt) { 546 | return this.cy.png(opt) 547 | } 548 | /** 549 | * Export the graph as JSON or Import the graph as JSON 550 | * @param {*} opt params for cy.json(opt) 551 | * @param {*} keys JSON Object keys 552 | */ 553 | json (opt = false, keys) { 554 | keys = keys || ['boxSelectionEnabled', 'elements', 'pan', 'panningEnabled', 'userPanningEnabled', 'userZoomingEnabled', 'zoom', 'zoomingEnabled'] 555 | // export 556 | let json = {} 557 | if (typeof opt === 'boolean') { 558 | let cyjson = this.cy.json(opt) 559 | keys.forEach(key => { json[key] = cyjson[key] }) 560 | return json 561 | } 562 | // import 563 | if (typeof opt === 'object') { 564 | json = {} 565 | keys.forEach(key => { json[key] = opt[key] }) 566 | } 567 | return this.cy.json(json) 568 | } 569 | 570 | /** 571 | * get or set data 572 | * @param {string|object} name 573 | * @param {*} value 574 | */ 575 | data (name, value) { 576 | return this.cy.data(name, value) 577 | } 578 | 579 | /** 580 | * remove data 581 | * @param {string} names split by space 582 | */ 583 | removeData (names) { 584 | this.cy.removeData(names) 585 | } 586 | 587 | destroy () { 588 | this.cy.removeAllListeners() 589 | this.cy.destroy() 590 | } 591 | } 592 | 593 | export default CyEditor 594 | -------------------------------------------------------------------------------- /src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | const debounce = (function () { 2 | var FUNC_ERROR_TEXT = 'Expected a function' 3 | 4 | var nativeMax = Math.max 5 | var nativeNow = Date.now 6 | 7 | var now = nativeNow || function () { 8 | return new Date().getTime() 9 | } 10 | 11 | function debounce (func, wait, options) { 12 | var args 13 | var maxTimeoutId 14 | var result 15 | var stamp 16 | var thisArg 17 | var timeoutId 18 | var trailingCall 19 | var lastCalled = 0 20 | var maxWait = false 21 | var trailing = true 22 | 23 | if (typeof func !== 'function') { 24 | throw new TypeError(FUNC_ERROR_TEXT) 25 | } 26 | wait = wait < 0 ? 0 : (+wait || 0) 27 | if (options === true) { 28 | var leading = true 29 | trailing = false 30 | } else if (isObject(options)) { 31 | leading = !!options.leading 32 | maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait) 33 | trailing = 'trailing' in options ? !!options.trailing : trailing 34 | } 35 | 36 | function cancel () { 37 | if (timeoutId) { 38 | clearTimeout(timeoutId) 39 | } 40 | if (maxTimeoutId) { 41 | clearTimeout(maxTimeoutId) 42 | } 43 | lastCalled = 0 44 | maxTimeoutId = timeoutId = trailingCall = undefined 45 | } 46 | 47 | function complete (isCalled, id) { 48 | if (id) { 49 | clearTimeout(id) 50 | } 51 | maxTimeoutId = timeoutId = trailingCall = undefined 52 | if (isCalled) { 53 | lastCalled = now() 54 | result = func.apply(thisArg, args) 55 | if (!timeoutId && !maxTimeoutId) { 56 | args = thisArg = undefined 57 | } 58 | } 59 | } 60 | 61 | function delayed () { 62 | var remaining = wait - (now() - stamp) 63 | if (remaining <= 0 || remaining > wait) { 64 | complete(trailingCall, maxTimeoutId) 65 | } else { 66 | timeoutId = setTimeout(delayed, remaining) 67 | } 68 | } 69 | 70 | function maxDelayed () { 71 | complete(trailing, timeoutId) 72 | } 73 | 74 | function debounced () { 75 | args = arguments 76 | stamp = now() 77 | thisArg = this 78 | trailingCall = trailing && (timeoutId || !leading) 79 | 80 | if (maxWait === false) { 81 | var leadingCall = leading && !timeoutId 82 | } else { 83 | if (!maxTimeoutId && !leading) { 84 | lastCalled = stamp 85 | } 86 | var remaining = maxWait - (stamp - lastCalled) 87 | var isCalled = remaining <= 0 || remaining > maxWait 88 | 89 | if (isCalled) { 90 | if (maxTimeoutId) { 91 | maxTimeoutId = clearTimeout(maxTimeoutId) 92 | } 93 | lastCalled = stamp 94 | result = func.apply(thisArg, args) 95 | } else if (!maxTimeoutId) { 96 | maxTimeoutId = setTimeout(maxDelayed, remaining) 97 | } 98 | } 99 | if (isCalled && timeoutId) { 100 | timeoutId = clearTimeout(timeoutId) 101 | } else if (!timeoutId && wait !== maxWait) { 102 | timeoutId = setTimeout(delayed, wait) 103 | } 104 | if (leadingCall) { 105 | isCalled = true 106 | result = func.apply(thisArg, args) 107 | } 108 | if (isCalled && !timeoutId && !maxTimeoutId) { 109 | args = thisArg = undefined 110 | } 111 | return result 112 | } 113 | 114 | debounced.cancel = cancel 115 | return debounced 116 | } 117 | 118 | function isObject (value) { 119 | // Avoid a V8 JIT bug in Chrome 19-20. 120 | // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. 121 | var type = typeof value 122 | return !!value && (type === 'object' || type === 'function') 123 | } 124 | 125 | return debounce 126 | })() 127 | 128 | export default debounce 129 | -------------------------------------------------------------------------------- /src/utils/eventbus.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-mixed-operators */ 2 | export class EventBus { 3 | constructor () { 4 | this.events = {} 5 | } 6 | 7 | /** 8 | * Adds listener to EventBus 9 | * @param {string} type The name of the event to listen for 10 | * @param {function} callback Callback to call when event was triggered 11 | * @param {object} scope The scope in which the callback shall be executed 12 | * @param {...any} args Any number of args to be passed to the callback 13 | */ 14 | on (type, callback, scope, ...args) { 15 | if (typeof this.events[type] === 'undefined') { // Check if there is already event of this type registered 16 | this.events[type] = [] // If not, create array for it 17 | } 18 | this.events[type].push({ 19 | scope, 20 | callback, 21 | args 22 | }) // Finally push new event to events array 23 | } 24 | 25 | /** 26 | * Removes listener from EventBus 27 | * @param {string} type The name of the event to remove 28 | * @param {function} callback Callback of the event to remove 29 | * @param {object} scope The scope of the to be removed event 30 | */ 31 | off (type, callback, scope) { 32 | if (typeof this.events[type] === 'undefined') { // Check if event of this type exists 33 | return // If not just return 34 | } 35 | 36 | // keep all elements that aren't equal to the passed event 37 | const filterFn = event => event.scope !== scope || event.callback !== callback 38 | this.events[type] = this.events[type].filter(filterFn) 39 | } 40 | 41 | /** 42 | * Checks if the passed event is registered in the EventBus 43 | * @param {string} type Type of the to be checked event 44 | * @param {callback} callback Callback of the to be checked event 45 | * @param {object} scope Scope of the to be checked event 46 | */ 47 | has (type, callback, scope) { 48 | if (typeof this.events[type] === 'undefined') { // Check if the passed type even exists 49 | return false // If not, quit method 50 | } 51 | 52 | // If callback and scope are undefined then every registered event is match, thus any event of the type matches 53 | let numOfCallbacks = this.events[type].length 54 | if (callback === undefined && scope === undefined) { // If scope and callback are not defined 55 | return numOfCallbacks > 0 // If there are any callbacks we can be sure it matches the passed one 56 | } 57 | 58 | const conditionFn = event => { 59 | const scopeIsSame = scope ? event.scope === scope : true // Check if scope is equal to the one passed 60 | const callbackIsSame = event.callback === callback // Check if callback is equal to the one passed 61 | if (scopeIsSame && callbackIsSame) { // Check if current event and passed event are equal 62 | return true // If so, break loop and return true 63 | } 64 | } 65 | return this.events[type].some(conditionFn) 66 | } 67 | 68 | /** 69 | * Emits an event on the EventBus 70 | * @param {string} type Type of event to emit 71 | * @param {object} target The caller 72 | * @param {...any} args Any number of args to be passed to the callback 73 | */ 74 | emit (type, target, ...args) { 75 | if (typeof this.events[type] === 'undefined') { // Check if any event of the passed type exists 76 | return // If not, quit method 77 | } 78 | 79 | let bag = { 80 | type, 81 | target 82 | } 83 | 84 | const events = this.events[type].slice() // Little hack to clone array 85 | 86 | for (const event of events) { // Iterate all events 87 | if (event && event.callback) { // Check if callback of event is set 88 | event.callback.apply(event.scope, [bag, ...args, ...event.args]) // Call callback 89 | } 90 | } 91 | } 92 | 93 | debug () { 94 | let str = '' 95 | for (const [type, events] of Object.entries(this.events)) { 96 | for (const event of events) { 97 | let className = event.scope && event.scope.constructor.name || 'Anonymous' 98 | str += `${className} listening for "${type}"\n` 99 | } 100 | } 101 | return str 102 | } 103 | } 104 | 105 | export default EventBus 106 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DemonRay on 2019/3/24. 3 | */ 4 | 5 | import memoize from './memorize' 6 | import debounce from './debounce' 7 | import throttle from './throttle' 8 | import localize from './localization' 9 | 10 | const class2type = {} 11 | 12 | const getProto = Object.getPrototypeOf 13 | 14 | const toString = class2type.toString 15 | 16 | const hasOwn = class2type.hasOwnProperty 17 | 18 | const fnToString = hasOwn.toString 19 | 20 | const ObjectFunctionString = fnToString.call(Object) 21 | 22 | const isFunction = function isFunction (obj) { 23 | return typeof obj === 'function' && typeof obj.nodeType !== 'number' 24 | } 25 | 26 | const RGBToHex = (r, g, b) => ((r << 16) + (g << 8) + b).toString(16).padStart(6, '0') 27 | 28 | const query = document.querySelectorAll.bind(document) 29 | 30 | function isPlainObject (obj) { 31 | let proto, Ctor 32 | 33 | // Detect obvious negatives 34 | // Use toString instead of jQuery.type to catch host objects 35 | if (!obj || toString.call(obj) !== '[object Object]') { 36 | return false 37 | } 38 | 39 | proto = getProto(obj) 40 | 41 | // Objects with no prototype (e.g., `Object.create( null )`) are plain 42 | if (!proto) { 43 | return true 44 | } 45 | 46 | // Objects with prototype are plain iff they were constructed by a global Object function 47 | Ctor = hasOwn.call(proto, 'constructor') && proto.constructor 48 | return typeof Ctor === 'function' && fnToString.call(Ctor) === ObjectFunctionString 49 | } 50 | 51 | function extend () { 52 | let options; let name; let src; let copy; let copyIsArray; let clone 53 | let target = arguments[ 0 ] || {} 54 | let i = 1 55 | let length = arguments.length 56 | let deep = false 57 | 58 | // Handle a deep copy situation 59 | if (typeof target === 'boolean') { 60 | deep = target 61 | 62 | // Skip the boolean and the target 63 | target = arguments[ i ] || {} 64 | i++ 65 | } 66 | 67 | // Handle case when target is a string or something (possible in deep copy) 68 | if (typeof target !== 'object' && !isFunction(target)) { 69 | target = {} 70 | } 71 | 72 | // Extend jQuery itself if only one argument is passed 73 | if (i === length) { 74 | target = this 75 | i-- 76 | } 77 | 78 | for (; i < length; i++) { 79 | // Only deal with non-null/undefined values 80 | if ((options = arguments[ i ]) != null) { 81 | // Extend the base object 82 | for (name in options) { 83 | src = target[ name ] 84 | copy = options[ name ] 85 | 86 | // Prevent never-ending loop 87 | if (target === copy) { 88 | continue 89 | } 90 | 91 | // Recurse if we're merging plain objects or arrays 92 | if (deep && copy && (isPlainObject(copy) || 93 | (copyIsArray = Array.isArray(copy)))) { 94 | if (copyIsArray) { 95 | copyIsArray = false 96 | clone = src && Array.isArray(src) ? src : [] 97 | } else { 98 | clone = src && isPlainObject(src) ? src : {} 99 | } 100 | 101 | // Never move original objects, clone them 102 | target[ name ] = extend(deep, clone, copy) 103 | 104 | // Don't bring in undefined values 105 | } else if (copy !== undefined) { 106 | target[ name ] = copy 107 | } 108 | } 109 | } 110 | } 111 | 112 | // Return the modified object 113 | return target 114 | } 115 | 116 | function $ (id) { return document.getElementById(id) } 117 | 118 | function once (dom, type, callback) { 119 | const handle = function () { 120 | callback() 121 | dom.removeEventListener(type, handle) 122 | } 123 | dom.addEventListener(type, handle) 124 | } 125 | 126 | function isNode (obj) { 127 | if (obj && obj.nodeType === 1) { 128 | if (window.Node && (obj instanceof Node)) { 129 | return true 130 | } 131 | } 132 | } 133 | 134 | function css (el, attr) { 135 | if (typeof attr === 'string') { // get 136 | var win = el.ownerDocument.defaultView 137 | return win.getComputedStyle(el, null)[ attr ] 138 | } else if (typeof attr === 'object') { // set 139 | for (var k in attr) { 140 | el.style[ k ] = attr[ k ] 141 | } 142 | } 143 | } 144 | 145 | function addClass (el, className) { 146 | el.classList.add(className) 147 | } 148 | 149 | function removeClass (el, className) { 150 | el.classList.remove(className) 151 | } 152 | 153 | function hasClass (el, className) { 154 | return el.classList.contains(className) 155 | } 156 | 157 | function offset (el) { 158 | const box = el.getBoundingClientRect() 159 | 160 | return { 161 | top: box.top + window.pageYOffset - document.documentElement.clientTop, 162 | left: box.left + window.pageXOffset - document.documentElement.clientLeft 163 | } 164 | } 165 | 166 | function guid () { 167 | function s4 () { 168 | return Math.floor((1 + Math.random()) * 0x10000) 169 | .toString(16) 170 | .substring(1) 171 | } 172 | 173 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 174 | s4() + '-' + s4() + s4() + s4() 175 | } 176 | 177 | export default { 178 | hasOwn, 179 | isFunction, 180 | query, 181 | isPlainObject, 182 | extend, 183 | css, 184 | addClass, 185 | removeClass, 186 | hasClass, 187 | offset, 188 | once, 189 | $, 190 | isNode, 191 | debounce, 192 | throttle, 193 | RGBToHex, 194 | memoize, 195 | guid, 196 | localize 197 | } 198 | -------------------------------------------------------------------------------- /src/utils/localization.js: -------------------------------------------------------------------------------- 1 | const locale = 'en' 2 | 3 | const data = { 4 | en: { 5 | 'toolbar-undo': 'Undo', 6 | 'toolbar-redo': 'Redo', 7 | 'toolbar-zoomin': 'Zoom in', 8 | 'toolbar-zoomout': 'Zoom out', 9 | 'toolbar-boxselect': 'Select', 10 | 'toolbar-copy': 'Copy', 11 | 'toolbar-paste': 'Paste', 12 | 'toolbar-delete': 'Delete', 13 | 'toolbar-leveldown': 'Move Down', 14 | 'toolbar-levelup': 'Move Up', 15 | 'toolbar-line-straight': 'Line', 16 | 'toolbar-line-taxi': 'Elbow connector', 17 | 'toolbar-line-bezier': 'Curved connector', 18 | 'toolbar-gridon': 'Show grid', 19 | 'toolbar-fit': 'Snap to grid', 20 | 'toolbar-save': 'Save', 21 | 'element-text': 'Text', 22 | 'elements-title': 'Element', 23 | 'elements-label': 'Label', 24 | 'elements-wrap': 'Size', 25 | 'elements-background-color': 'Background', 26 | 'elements-text-color': 'Text color', 27 | 'elements-color': 'Color', 28 | 'window-navigator': 'Navigator', 29 | 'node-types-base-shape': 'Base Shape' 30 | }, 31 | cn: { 32 | 'toolbar-undo': '撤销', 33 | 'toolbar-redo': '重做', 34 | 'toolbar-zoomin': '放大', 35 | 'toolbar-zoomout': '缩小', 36 | 'toolbar-boxselect': '框选', 37 | 'toolbar-copy': '复制', 38 | 'toolbar-paste': '粘贴', 39 | 'toolbar-delete': '删除', 40 | 'toolbar-leveldown': '层级后置', 41 | 'toolbar-levelup': '层级前置', 42 | 'toolbar-line-straight': '直线', 43 | 'toolbar-line-taxi': '折线', 44 | 'toolbar-line-bezier': '曲线', 45 | 'toolbar-gridon': '表格辅助', 46 | 'toolbar-fit': '适应画布', 47 | 'toolbar-save': '保存', 48 | 'element-text': '文字', 49 | 'elements-title': '元素', 50 | 'elements-label': '名称', 51 | 'elements-wrap': '尺寸', 52 | 'elements-background-color': '背景', 53 | 'elements-text-color': '文字', 54 | 'elements-color': '颜色', 55 | 'window-navigator': '导航器', 56 | 'node-types-base-shape': '基础形状' 57 | } 58 | } 59 | 60 | function localize (key) { 61 | if (key in data[locale]) { 62 | return data[locale][key] 63 | } else { 64 | return ('Unknown: ' + key) 65 | } 66 | } 67 | 68 | export default localize 69 | -------------------------------------------------------------------------------- /src/utils/memorize.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Creates a map cache object to store key-value pairs. 4 | * 5 | * @private 6 | * @constructor 7 | * @param {Array} [entries] The key-value pairs to cache. 8 | */ 9 | function MapCache(entries) { 10 | var index = -1, 11 | length = entries == null ? 0 : entries.length; 12 | 13 | this.clear(); 14 | while (++index < length) { 15 | var entry = entries[index]; 16 | this.set(entry[0], entry[1]); 17 | } 18 | } 19 | 20 | /** 21 | * Removes all key-value entries from the map. 22 | * 23 | * @private 24 | * @name clear 25 | * @memberOf MapCache 26 | */ 27 | function mapCacheClear() { 28 | this.size = 0; 29 | this.__data__ = { 30 | 'hash': new Hash, 31 | 'map': new (Map || ListCache), 32 | 'string': new Hash 33 | }; 34 | } 35 | 36 | /** 37 | * Removes `key` and its value from the map. 38 | * 39 | * @private 40 | * @name delete 41 | * @memberOf MapCache 42 | * @param {string} key The key of the value to remove. 43 | * @returns {boolean} Returns `true` if the entry was removed, else `false`. 44 | */ 45 | function mapCacheDelete(key) { 46 | var result = getMapData(this, key)['delete'](key); 47 | this.size -= result ? 1 : 0; 48 | return result; 49 | } 50 | 51 | /** 52 | * Gets the map value for `key`. 53 | * 54 | * @private 55 | * @name get 56 | * @memberOf MapCache 57 | * @param {string} key The key of the value to get. 58 | * @returns {*} Returns the entry value. 59 | */ 60 | function mapCacheGet(key) { 61 | return getMapData(this, key).get(key); 62 | } 63 | 64 | /** 65 | * Checks if a map value for `key` exists. 66 | * 67 | * @private 68 | * @name has 69 | * @memberOf MapCache 70 | * @param {string} key The key of the entry to check. 71 | * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. 72 | */ 73 | function mapCacheHas(key) { 74 | return getMapData(this, key).has(key); 75 | } 76 | 77 | /** 78 | * Sets the map `key` to `value`. 79 | * 80 | * @private 81 | * @name set 82 | * @memberOf MapCache 83 | * @param {string} key The key of the value to set. 84 | * @param {*} value The value to set. 85 | * @returns {Object} Returns the map cache instance. 86 | */ 87 | function mapCacheSet(key, value) { 88 | var data = getMapData(this, key), 89 | size = data.size; 90 | 91 | data.set(key, value); 92 | this.size += data.size == size ? 0 : 1; 93 | return this; 94 | } 95 | 96 | // Add methods to `MapCache`. 97 | MapCache.prototype.clear = mapCacheClear; 98 | MapCache.prototype['delete'] = mapCacheDelete; 99 | MapCache.prototype.get = mapCacheGet; 100 | MapCache.prototype.has = mapCacheHas; 101 | MapCache.prototype.set = mapCacheSet; 102 | 103 | const FUNC_ERROR_TEXT = 'Expected a function'; 104 | 105 | function memoize(func, resolver) { 106 | if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { 107 | throw new TypeError(FUNC_ERROR_TEXT); 108 | } 109 | var memoized = function() { 110 | var args = arguments, 111 | key = resolver ? resolver.apply(this, args) : args[0], 112 | cache = memoized.cache; 113 | 114 | if (cache.has(key)) { 115 | return cache.get(key); 116 | } 117 | var result = func.apply(this, args); 118 | memoized.cache = cache.set(key, result) || cache; 119 | return result; 120 | }; 121 | memoized.cache = new (memoize.Cache || MapCache); 122 | return memoized; 123 | } 124 | 125 | // Expose `MapCache`. 126 | memoize.Cache = MapCache; 127 | 128 | export default memoize 129 | -------------------------------------------------------------------------------- /src/utils/throttle.js: -------------------------------------------------------------------------------- 1 | import debounce from './debounce' 2 | const throttle = function (func, wait, options) { 3 | var leading = true 4 | var trailing = true 5 | 6 | if (options === false) { 7 | leading = false 8 | } else if (typeof options === typeof {}) { 9 | leading = 'leading' in options ? options.leading : leading 10 | trailing = 'trailing' in options ? options.trailing : trailing 11 | } 12 | options = options || {} 13 | options.leading = leading 14 | options.maxWait = wait 15 | options.trailing = trailing 16 | 17 | return debounce(func, wait, options) 18 | } 19 | export default throttle 20 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | // vue.config.js 2 | 3 | /** 4 | * @type {import('@vue/cli-service').ProjectOptions} 5 | */ 6 | module.exports = { 7 | // options... 8 | devServer: { 9 | disableHostCheck: true 10 | } 11 | } --------------------------------------------------------------------------------