├── public ├── favicon.ico └── index.html ├── babel.config.js ├── screenshot └── screenshot.png ├── src ├── assets │ └── logo-retina.png ├── common │ ├── toolbar │ │ ├── icons-32.png │ │ ├── Toolbar.vue │ │ ├── toolbar.css │ │ ├── Toolbar.js │ │ └── Exportpane.js │ ├── property │ │ ├── property.css │ │ ├── form.css │ │ ├── PropertyPane.vue │ │ ├── input.js │ │ └── PropertyPane.js │ ├── vue │ │ ├── Graph.vue │ │ ├── GraphWithToolbar.vue │ │ ├── graph.editor.css │ │ └── GraphEditor.vue │ ├── toolbox │ │ ├── ToolBox.vue │ │ ├── toolbox.css │ │ ├── DragSupport.js │ │ └── ToolBox.js │ ├── css │ │ └── global.css │ ├── io │ │ ├── JSONPane.js │ │ └── JSONSerializer.js │ ├── utils.js │ ├── popup │ │ ├── popup.css │ │ ├── Popup.js │ │ └── PopupMenu.js │ ├── modal │ │ ├── Dialog.js │ │ └── dialog.css │ ├── FileSupport.js │ ├── color-picker │ │ ├── color-picker.css │ │ └── ColorPicker.js │ ├── i18n.js │ ├── interaction │ │ └── EdgeInteraction.js │ └── overview │ │ └── Overview.js ├── main.js ├── es6 │ ├── template标签.html │ ├── qunee.css │ └── 自定义标签.html ├── App.vue ├── app.css ├── index_es6.html ├── js │ ├── FlowSupport.js │ ├── GridBackground.js │ └── main.js └── graph.editor.js ├── .gitignore ├── vue.config.js ├── README.md ├── package.json └── vue2js.js /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunee/graph.editor_vue/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /screenshot/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunee/graph.editor_vue/HEAD/screenshot/screenshot.png -------------------------------------------------------------------------------- /src/assets/logo-retina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunee/graph.editor_vue/HEAD/src/assets/logo-retina.png -------------------------------------------------------------------------------- /src/common/toolbar/icons-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunee/graph.editor_vue/HEAD/src/common/toolbar/icons-32.png -------------------------------------------------------------------------------- /src/common/property/property.css: -------------------------------------------------------------------------------- 1 | .q-property-pane { 2 | display: none; 3 | padding: 10px; 4 | background-color: #FFF; 5 | min-width: 220px; 6 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App) 8 | }).$mount('#app') -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | /.idea/ 4 | /other/ 5 | /tsdoc/typings/ 6 | /public/data/cisco/ 7 | /public/data/images/ 8 | /public/data/cisco.json 9 | /public/data/topo.json 10 | graph.editor.vue.zip 11 | graph.editor.vue_es6.zip 12 | -------------------------------------------------------------------------------- /src/common/vue/Graph.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/common/toolbox/ToolBox.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/common/css/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif; 3 | color: #666666; 4 | font-size: 14px; 5 | margin: 0px; 6 | } 7 | a { 8 | color: #555; 9 | } 10 | a:hover { 11 | color: #40B0FF !important; 12 | text-decoration: none !important; 13 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // publicPath: process.env.NODE_ENV === 'production' 3 | // ? '/qunee_by_vue/dist/' // 打包后发布路径 4 | // : '/' // 开发环境相对路径 5 | // , 6 | publicPath: '', 7 | productionSourceMap:false, 8 | devServer: { 9 | overlay: { 10 | warnings: true, 11 | errors: true 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/common/io/JSONPane.js: -------------------------------------------------------------------------------- 1 | let template = ` 2 | let
3 | let 4 | let
5 | let
更新
6 | let
提交
7 | let
8 | let
` 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graph.editor 2 | 3 | ~~老版本地址:https://github.com/samsha/graph.editor~~ 4 | 5 | 本项目则基于Qunee for HTML5图形组件,是Qunee的扩展项目,新版本使用es6语法开发,删除了第三方组件的依赖 6 | 7 | 本项目本身是一个vue项目,但没有vue也可以正常使用 8 | 9 | 在线示例:[http://demo.qunee.com/editor/](http://demo.qunee.com/editor/) 10 | 11 | 开发方式说明 12 | 13 | 1,es6开发,无需安装vue环境,支持chrome,safari,firefox,edge等支持es6的浏览器 14 | 15 | 直接部署访问 src/index_es6.html 16 | 17 | 2,vue开发,需要安装vue环境,参照vue项目的部署方式进行 18 | 19 | ![拓扑图编辑器截图1](screenshot/screenshot.png) -------------------------------------------------------------------------------- /src/common/vue/GraphWithToolbar.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/common/property/form.css: -------------------------------------------------------------------------------- 1 | .q-btn { 2 | white-space: nowrap; 3 | } 4 | 5 | .q-from-item input { 6 | min-width: 20px; 7 | /*padding: 5px 10px;*/ 8 | line-height: 1.5; 9 | flex: 1 1 auto; 10 | } 11 | 12 | .q-input-group, .q-from-item { 13 | display: flex; 14 | align-items: center; 15 | } 16 | 17 | .q-from-item { 18 | margin-bottom: 5px; 19 | font-size: 0.9em; 20 | } 21 | 22 | .q-from-item > * { 23 | width: 50%; 24 | box-sizing: border-box; 25 | } 26 | 27 | .q-from-item > label { 28 | text-align: right; 29 | padding-right: 15px; 30 | } -------------------------------------------------------------------------------- /src/common/toolbar/Toolbar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graph.editor", 3 | "version": "0.1.1", 4 | "author": "Sam Sha", 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.6.4", 11 | "vue": "^2.6.12", 12 | "vue-router": "^3.1.5", 13 | "vuex": "^3.1.2" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "^4.2.0", 17 | "@vue/cli-plugin-router": "^4.2.0", 18 | "@vue/cli-plugin-vuex": "^4.2.0", 19 | "@vue/cli-service": "^4.2.0", 20 | "less": "^3.0.4", 21 | "less-loader": "^5.0.0", 22 | "vue": "^2.6.12", 23 | "vue-template-compiler": "^2.6.12" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Qunee Graph Editor V2.7.8.5 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/common/property/PropertyPane.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/common/utils.js: -------------------------------------------------------------------------------- 1 | export function createElement({tagName = 'div', className, parent, html}) { 2 | let element = document.createElement(tagName); 3 | className && (element.className = className); 4 | parent && parent.appendChild(element); 5 | html && (element.innerHTML = html); 6 | return element; 7 | } 8 | 9 | export function getQStyleSheet() { 10 | const q_style_id = 'qunee-styles'; 11 | let q_style = document.getElementById(q_style_id); 12 | if(q_style && q_style.sheet){ 13 | return q_style.sheet; 14 | } 15 | let head = document.head; 16 | let style = document.createElement('style'); 17 | style.id = q_style_id; 18 | head.insertBefore(style, head.childNodes[0]); 19 | return style.sheet; 20 | } 21 | -------------------------------------------------------------------------------- /vue2js.js: -------------------------------------------------------------------------------- 1 | import * as compiler from 'vue-template-compiler'; 2 | import fs from 'fs'; 3 | 4 | let file = './src/common/vue/GraphEditor.vue'; 5 | const content = fs.readFileSync(file, 'utf-8'); 6 | 7 | let parsed = compiler.parseComponent(content);//'./src/common/vue/GraphEditor.vue'); 8 | const template = parsed.template ? parsed.template.content : ''; 9 | const script = parsed.script ? parsed.script.content : ''; 10 | 11 | const templateEscaped = template.trim().replace(/`/g, '\\`'); 12 | const scriptWithTemplate = script.match(/export default ?\{/) 13 | ? script.replace(/export default ?\{/, `$&\n\ttemplate: \`\n${templateEscaped}\`,`) 14 | : `${script}\n export default {\n\ttemplate: \`\n${templateEscaped}\`};`; 15 | 16 | fs.writeFileSync(file + '.js', scriptWithTemplate) -------------------------------------------------------------------------------- /src/es6/template标签.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 16 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/common/popup/popup.css: -------------------------------------------------------------------------------- 1 | 2 | .dropdown-menu { 3 | position: absolute; 4 | z-index: 1000; 5 | float: left; 6 | min-width: 160px; 7 | padding: 5px 0; 8 | margin: 2px 0 0; 9 | font-size: 14px; 10 | list-style: none; 11 | background-color: #fff; 12 | border: 1px solid #ccc; 13 | border-radius: 4px; 14 | box-shadow: 0 6px 12px rgba(0,0,0,0.175); 15 | background-clip: padding-box; 16 | } 17 | .dropdown-menu>li>a { 18 | display: block; 19 | padding: 3px 20px; 20 | clear: both; 21 | font-weight: normal; 22 | line-height: 1.428571429; 23 | color: #333; 24 | white-space: nowrap; 25 | text-decoration: none; 26 | } 27 | .dropdown-menu>li>a:hover, .dropdown-menu>li>a:focus { 28 | background-color: #f5f5f5; 29 | color: #40B0FF !important; 30 | text-decoration: none !important; 31 | } 32 | .dropdown-menu .divider { 33 | height: 1px; 34 | margin: 9px 0; 35 | overflow: hidden; 36 | background-color: #e5e5e5; 37 | } -------------------------------------------------------------------------------- /src/common/vue/graph.editor.css: -------------------------------------------------------------------------------- 1 | 2 | .Q-Tooltip { 3 | z-index: 1000; 4 | } 5 | 6 | .q-max .toolbox { 7 | display: none; 8 | } 9 | 10 | .toolbox, .property { 11 | box-shadow: 0px 5px 5px #888; 12 | top: 0px; 13 | bottom: 0px; 14 | border-top: solid 1px #CCC; 15 | overflow-y: auto; 16 | } 17 | 18 | .property { 19 | position: absolute; 20 | right: 0px; 21 | width: 25%; 22 | max-width: 300px; 23 | background-color: rgba(255, 255, 255, 0.9); 24 | } 25 | 26 | .toolbar { 27 | padding: 5px; 28 | text-align: center; 29 | box-shadow: 0px 0px 5px #888; 30 | } 31 | 32 | .json-pane { 33 | z-index: 10; 34 | position: absolute; 35 | right: 0; 36 | top: 0px; 37 | bottom: 0px; 38 | min-width: 360px; 39 | width: 35%; 40 | background: rgba(250, 254, 156, 0.8); 41 | box-shadow: 0px 1px 5px #888; 42 | border-top: solid 5px #2898E0; 43 | } 44 | 45 | .json-pane textarea { 46 | height: 100%; 47 | width: 100%; 48 | background: transparent; 49 | border: none; 50 | outline: none; 51 | padding: 10px; 52 | font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; 53 | font-size: 0.9em; 54 | color: #000; 55 | } -------------------------------------------------------------------------------- /src/common/modal/Dialog.js: -------------------------------------------------------------------------------- 1 | import {createElement} from "../utils.js"; 2 | 3 | let currentDialog; 4 | 5 | export function hide() { 6 | if (!currentDialog) { 7 | return 8 | } 9 | if (!currentDialog.parentNode) { 10 | currentDialog = null; 11 | return; 12 | } 13 | currentDialog.parentNode.classList.remove('modal-open'); 14 | currentDialog.parentNode.removeChild(currentDialog); 15 | currentDialog = null; 16 | } 17 | 18 | const ModalTemplate = ` 19 | ` 23 | 24 | 25 | export function showDialog({relatedTarget = document.body, content}) { 26 | currentDialog && this.hide(); 27 | relatedTarget.classList.add('modal-open'); 28 | 29 | currentDialog = createElement({ 30 | html: ModalTemplate, 31 | className: 'modal in' 32 | }); 33 | let body = currentDialog.querySelector('.modal-content'); 34 | if(content){ 35 | if(content instanceof HTMLElement){ 36 | body.appendChild(content); 37 | }else{ 38 | body.innerHTML = content; 39 | } 40 | } 41 | 42 | currentDialog.onclick = function (evt) { 43 | if (evt.target !== evt.currentTarget) { 44 | return; 45 | } 46 | hide(); 47 | } 48 | relatedTarget.appendChild(currentDialog); 49 | return body; 50 | } -------------------------------------------------------------------------------- /src/common/FileSupport.js: -------------------------------------------------------------------------------- 1 | export function readTextFile(file, ext, cb) { 2 | let name = file.name; 3 | if (typeof ext === 'string') { 4 | let reg = new RegExp('.' + ext + '$', 'gi'); 5 | if (!reg.test(name)) { 6 | alert('Please selects .' + ext + ' file'); 7 | return; 8 | } 9 | } else if (ext instanceof Function) { 10 | cb = ext; 11 | } 12 | let fileReader = new FileReader(); 13 | fileReader.onload = function (evt) { 14 | cb(fileReader.result); 15 | } 16 | fileReader.readAsText(file, 'utf-8'); 17 | } 18 | 19 | let _isFileSaverSupported; 20 | 21 | export function isBlobSupported(){ 22 | if(_isFileSaverSupported === undefined){ 23 | try { 24 | _isFileSaverSupported = !!new Blob; 25 | } catch (e) {} 26 | } 27 | return _isFileSaverSupported; 28 | } 29 | 30 | //https://stackoverflow.com/questions/24007073/open-links-made-by-createobjecturl-in-ie11 31 | export function saveAs(blob, filename){ 32 | if(!isBlobSupported()){ 33 | throw 'file saver not be supported' 34 | } 35 | if(navigator.msSaveOrOpenBlob){ 36 | return navigator.msSaveOrOpenBlob(blob, filename); 37 | } 38 | let a = document.createElement("a"); 39 | a.style.display = 'none'; 40 | document.body.appendChild(a); 41 | let csvUrl = URL.createObjectURL(blob); 42 | a.href = csvUrl; 43 | a.download = filename; 44 | a.click(); 45 | URL.revokeObjectURL(a.href) 46 | a.remove(); 47 | 48 | } -------------------------------------------------------------------------------- /src/es6/qunee.css: -------------------------------------------------------------------------------- 1 | /*仅用于 shadow dom*/ 2 | .Q-Graph { 3 | overflow: hidden; 4 | text-align: left; 5 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 6 | outline: none; 7 | } 8 | .Q-CanvasPanel { 9 | width: 100%; 10 | height: 100%; 11 | position: relative; 12 | overflow: hidden; 13 | text-align: left; 14 | outline: none; 15 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 16 | user-select: none; 17 | } 18 | .Q-Canvas { 19 | position: absolute; 20 | user-select: none; 21 | outline: none; 22 | transform-origin: 0px 0px; 23 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 24 | } 25 | .Q-Graph-ScrollPane { 26 | opacity: 0; 27 | transition: opacity 3s cubic-bezier(0.8, 0, 0.8, 1) 0s; 28 | } 29 | .Q-Graph-ScrollBar { 30 | position: absolute; 31 | box-sizing: border-box; 32 | box-shadow: rgb(255 255 255) 0px 0px 1px; 33 | background-color: rgba(120, 120, 120, 0.3); 34 | border-radius: 4px; 35 | margin: 1px; 36 | } 37 | .Q-Graph-ScrollBar--V.Both { 38 | margin-bottom: 8px; 39 | } 40 | .Q-Graph-ScrollBar--V { 41 | width: 8px; 42 | right: 0px; 43 | } 44 | .Q-Graph-ScrollBar--H.Both { 45 | margin-right: 8px; 46 | } 47 | .Q-Graph-ScrollBar--H { 48 | height: 8px; 49 | bottom: 0px; 50 | } 51 | .Q-Graph-ScrollBar.hover, .Q-Graph-ScrollBar:hover { 52 | background-color: rgb(126, 126, 126); 53 | transition: background-color 0.2s linear 0s; 54 | } 55 | .Q-CanvasPanel:hover .Q-Graph-ScrollPane{ 56 | opacity:1; 57 | transition:opacity 0.3s linear; 58 | } -------------------------------------------------------------------------------- /src/common/color-picker/color-picker.css: -------------------------------------------------------------------------------- 1 | .q-flex { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .q-flex__fill { 7 | flex-grow: 1; 8 | } 9 | 10 | .q-color__block { 11 | width: 100%; 12 | height: 100%; 13 | } 14 | 15 | .q-color-input { 16 | display: flex; 17 | align-items: center; 18 | } 19 | 20 | .q-color-input > input { 21 | flex-grow: 1; 22 | } 23 | 24 | .q-color-input > span { 25 | min-width: 1.5em; 26 | min-height: 1.5em; 27 | height: 100%; 28 | margin-left: 3px; 29 | background-color: #F00; 30 | } 31 | 32 | .q-color-picker { 33 | position: absolute; 34 | z-index: 1000; 35 | background-color: #FFF; 36 | width: 260px; 37 | /*border: solid 1px #888;*/ 38 | padding: 5px; 39 | font-size: 0.9em; 40 | /*transform: translateX(-50%);*/ 41 | box-shadow: 0 5px 10px #888; 42 | } 43 | 44 | .q-color-picker label { 45 | white-space: nowrap; 46 | user-select: none; 47 | margin-right: 3px; 48 | } 49 | 50 | .q-color { 51 | width: 25px; 52 | height: 25px; 53 | margin-left: 3px; 54 | border: 1px solid rgb(204, 204, 204); 55 | } 56 | 57 | .q-color-picker .q-color { 58 | width: 45px; 59 | height: 1.5em; 60 | } 61 | 62 | .q-color-input > span, .q-color { 63 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); 64 | } 65 | 66 | .q-color__text{ 67 | letter-spacing: -0.1em; 68 | } 69 | 70 | .q-color-spectrum { 71 | height: 200px; 72 | width: 100%; 73 | margin: 5px 0px; 74 | cursor: crosshair; 75 | } 76 | 77 | .q-color-spectrum > canvas { 78 | width: 100%; 79 | height: 100%; 80 | } -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: #555; 3 | text-decoration: none; 4 | } 5 | 6 | input:focus { 7 | border-color: #66afe9; 8 | outline: 0; 9 | box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 8px rgba(102,175,233,0.6); 10 | } 11 | 12 | #app { 13 | display: flex; 14 | flex-direction: column; 15 | position: absolute; 16 | top: 0px; 17 | bottom: 40px; 18 | width: 100%; 19 | } 20 | 21 | #editor { 22 | flex-grow: 1; 23 | } 24 | 25 | header { 26 | text-align: left; 27 | display: flex; 28 | align-items: center; 29 | margin: 0px 15px; 30 | margin-right: 5px; 31 | } 32 | 33 | header ul { 34 | text-align: right; 35 | flex-grow: 1; 36 | margin: 0px; 37 | } 38 | 39 | header li { 40 | display: inline-block; 41 | padding: 12px 10px; 42 | } 43 | 44 | header li.active { 45 | background-color: #EEE; 46 | } 47 | 48 | header li > a { 49 | color: #333; 50 | font-weight: 100; 51 | font-size: 1.2em; 52 | } 53 | 54 | .q-logo { 55 | background-image: url(./assets/logo-retina.png); 56 | background-size: 200px 32px; 57 | width: 200px; 58 | height: 32px; 59 | margin-right: 15px; 60 | } 61 | 62 | footer { 63 | position: fixed; 64 | bottom: 0px; 65 | width: 100%; 66 | text-align: center; 67 | border-top: solid 1px #DDD; 68 | padding: 10px; 69 | background-color: #FFF; 70 | height: 40px; 71 | font-size: 0.9em; 72 | } 73 | 74 | * { 75 | box-sizing: border-box; 76 | } 77 | 78 | .q-graph-editor { 79 | overflow: hidden; 80 | display: flex; 81 | flex-direction: column; 82 | border-top: solid 1px #CCC; 83 | background-color: #FFF; 84 | } 85 | 86 | .q-max { 87 | position: fixed !important; 88 | z-index: 1000 !important; 89 | left: 0px !important; 90 | right: 0px !important; 91 | top: 0px !important; 92 | bottom: 0px !important; 93 | height: auto !important; 94 | } -------------------------------------------------------------------------------- /src/es6/自定义标签.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 11 | 51 | 52 | 53 |
q-label
54 | 55 |

56 | 57 | 58 | -------------------------------------------------------------------------------- /src/common/modal/dialog.css: -------------------------------------------------------------------------------- 1 | 2 | /*modal fade in*/ 3 | /*.fade.in {*/ 4 | /* opacity: 1;*/ 5 | /*}*/ 6 | 7 | /*.fade {*/ 8 | /* opacity: 0;*/ 9 | /* -webkit-transition: opacity .15s linear;*/ 10 | /* transition: opacity .15s linear;*/ 11 | /*}*/ 12 | 13 | .modal { 14 | position: fixed; 15 | top: 0px; 16 | right: 0px; 17 | bottom: 0px; 18 | left: 0px; 19 | z-index: 1040; 20 | overflow: auto; 21 | overflow-y: scroll; 22 | } 23 | 24 | /*modal-backdrop*/ 25 | .modal-backdrop { 26 | position: fixed; 27 | top: 0px; 28 | right: 0px; 29 | bottom: 0px; 30 | left: 0px; 31 | background-color: #000; 32 | pointer-events: none; 33 | } 34 | 35 | .modal-backdrop.in { 36 | opacity: .5; 37 | filter: alpha(opacity=50); 38 | } 39 | 40 | /*modal-dialog*/ 41 | .modal.in .modal-dialog { 42 | transition: transform .3s ease-out; 43 | /*height: 500px;*/ 44 | position: relative; 45 | } 46 | 47 | .modal-dialog { 48 | width: auto; 49 | margin: 20px auto; 50 | } 51 | 52 | /*.modal.fade .modal-dialog {*/ 53 | /* -webkit-transform: translate(0, -25%);*/ 54 | /* -ms-transform: translate(0, -25%);*/ 55 | /* transform: translate(0, -25%);*/ 56 | /* -webkit-transition: -webkit-transform .3s ease-out;*/ 57 | /* -moz-transition: -moz-transform .3s ease-out;*/ 58 | /* -o-transition: -o-transform .3s ease-out;*/ 59 | /* transition: transform .3s ease-out;*/ 60 | /*}*/ 61 | 62 | /*.modal-dialog {*/ 63 | /* position: relative;*/ 64 | /* z-index: 1050;*/ 65 | /* width: auto;*/ 66 | /* margin: 10px;*/ 67 | /*}*/ 68 | 69 | @media screen and (min-width: 768px) { 70 | .modal-dialog { 71 | width: 600px; 72 | margin: 30px auto; 73 | } 74 | } 75 | 76 | /*modal-content*/ 77 | .modal-content { 78 | position: relative; 79 | background-color: #fff; 80 | border: 1px solid rgba(0, 0, 0, 0.2); 81 | border-radius: 6px; 82 | outline: 0; 83 | box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); 84 | background-clip: padding-box; 85 | padding: 20px; 86 | } 87 | 88 | /*modal-body*/ 89 | .modal-body { 90 | position: relative; 91 | padding: 20px; 92 | } 93 | -------------------------------------------------------------------------------- /src/common/toolbox/toolbox.css: -------------------------------------------------------------------------------- 1 | .q-toolbox { 2 | overflow-y: auto; 3 | background-color: #FFF; 4 | box-shadow: 0px 2px 5px #888; 5 | } 6 | 7 | .q-toolbox__button-bar { 8 | text-align: right; 9 | padding: 5px; 10 | /*border-top: solid 1px #ccc;*/ 11 | } 12 | 13 | /*group*/ 14 | .q-group { 15 | /*border-bottom: solid 1px #CECECE;*/ 16 | } 17 | 18 | .q-group__title{ 19 | padding: 4px 0px; 20 | background-color: #FFF; 21 | text-align: center; 22 | /*border-bottom: solid 1px #BEC1C7;*/ 23 | cursor: pointer; 24 | font-weight: 800; 25 | font-size: 1.1em; 26 | user-select: none; 27 | -webkit-user-select: none; 28 | color: #555; 29 | position: relative; 30 | } 31 | .q-group{ 32 | border-top: solid 1px #ccc; 33 | 34 | } 35 | .q-group:first-child{ 36 | border-top: none; 37 | 38 | } 39 | 40 | .q-group__title .q-icon { 41 | position: absolute; 42 | right: 5px; 43 | top: 5px; 44 | } 45 | 46 | .q-group__title:hover, .q-group--closed > .q-group__title:hover { 47 | /*background-color: #F0F0F0;*/ 48 | /*border-top: solid 1px #FFF;*/ 49 | /*background-color: #EEE;*/ 50 | background-color: #FFF; 51 | color: #000; 52 | box-shadow: none; 53 | } 54 | 55 | .q-group--closed:hover { 56 | border-top: none; 57 | } 58 | 59 | .q-group__items { 60 | padding-bottom: 10px; 61 | min-height: 70px; 62 | /*max-height: 300px;*/ 63 | background-color: #FFF; 64 | overflow-y: auto; 65 | text-align: center; 66 | line-height: 0px; 67 | } 68 | 69 | .q-group--closed { 70 | /*border-top: solid 1px #BBB;*/ 71 | } 72 | 73 | .q-group--closed > .q-group__title { 74 | background-color: #F5F5F5; 75 | font-weight: 200; 76 | /*border-top: none;*/ 77 | box-shadow: inset 0 1px 5px 0px rgba(0, 0, 0, 0.1); 78 | 79 | } 80 | 81 | .q-group--closed > .q-group__items { 82 | display: none; 83 | } 84 | 85 | .group-expand { 86 | width: 16px; 87 | height: 16px; 88 | -moz-transition: transform 0.3s ease-in-out; 89 | -ms-transition: transform 0.3s ease-in-out; 90 | -o-transition: transform 0.3s ease-in-out; 91 | -webkit-transition: transform 0.3s linear; 92 | transition: transform 0.3s linear; 93 | } 94 | 95 | .q-group--closed > .q-group__title > .group-expand { 96 | -moz-transform: rotate(-90deg); 97 | -ms-transform: rotate(-90deg); 98 | -o-transform: rotate(-90deg); 99 | -webkit-transform: rotate(-90deg); 100 | transform: rotate(-90deg); 101 | } 102 | 103 | .q-group__item { 104 | display: inline-block; 105 | vertical-align: middle; 106 | } 107 | 108 | .q-group__item > img, .q-group__item > canvas, .q-group__item > div { 109 | margin: 5px; 110 | line-height: 0px; 111 | display: block; 112 | } 113 | 114 | .q-group__item:hover { 115 | background: #DDD; 116 | } 117 | -------------------------------------------------------------------------------- /src/common/popup/Popup.js: -------------------------------------------------------------------------------- 1 | function getPageXY(evt) { 2 | if (evt.touches && evt.touches.length) { 3 | evt = evt.touches[0]; 4 | } 5 | return {x: evt.pageX, y: evt.pageY}; 6 | } 7 | 8 | function showDivAt(div, x, y) { 9 | if (!div.parentElement) { 10 | document.body.appendChild(div); 11 | } 12 | if (x instanceof MouseEvent) { 13 | var xy = getPageXY(x); 14 | x = xy.x; 15 | y = xy.y; 16 | } 17 | 18 | var body = document.documentElement; 19 | var bounds = {x: window.pageXOffset, y: window.pageYOffset, width: body.clientWidth - 2, height: body.clientHeight - 2} 20 | var width = div.offsetWidth; 21 | var height = div.offsetHeight; 22 | 23 | if (x + width > bounds.x + bounds.width) { 24 | x = bounds.x + bounds.width - width; 25 | } 26 | if (x < bounds.x) { 27 | x = bounds.x; 28 | } 29 | if (y + height > bounds.y + bounds.height) { 30 | y = bounds.y + bounds.height - height; 31 | } 32 | if (y < bounds.y) { 33 | y = bounds.y; 34 | } 35 | div.style.left = x + 'px'; 36 | div.style.top = y + 'px'; 37 | } 38 | 39 | function isDescendant(parent, child) { 40 | while (child) { 41 | if (child === parent) { 42 | return true; 43 | } 44 | child = child.parentNode; 45 | } 46 | return false; 47 | } 48 | 49 | export function showPopup(div, x, y, host) { 50 | if (typeof div == 'string') { 51 | div = document.getElementById(div); 52 | if (!div) { 53 | throw new Error('div cannot be found'); 54 | } 55 | } 56 | if (div.style.display === 'none') { 57 | div.style.display = ''; 58 | } 59 | showDivAt(div, x, y); 60 | autoHide(div, host); 61 | } 62 | 63 | export function hidePopup(div) { 64 | div.parentNode && div.parentNode.removeChild(div); 65 | if (div._onWindowMousedown) { 66 | window.removeEventListener("mousedown", div._onWindowMousedown); 67 | div._onWindowMousedown = null; 68 | } 69 | } 70 | 71 | function isInDom(e) { 72 | if (document.contains) { 73 | return document.contains(e); 74 | } 75 | while (e.parentNode) e = e.parentNode; 76 | return e === document; 77 | } 78 | 79 | /** 80 | * 自动隐藏,点击组件外面,或者esc时,自动隐藏组件 81 | */ 82 | function autoHide(div, host) { 83 | div.__host = host; 84 | if (!div._onWindowMousedown) { 85 | div._onWindowMousedown = function (evt) { 86 | if (!isInDom(this)) { 87 | hidePopup(this); 88 | } 89 | if (isDescendant(this, evt.target) || (this.__host && isDescendant(this.__host, evt.target))) { 90 | return; 91 | } 92 | // var xy = getPageXY(evt); 93 | // if (inBounds(this, xy.x, xy.y)) { 94 | // return; 95 | // } 96 | hidePopup(this); 97 | }.bind(div) 98 | } 99 | window.addEventListener("mousedown", div._onWindowMousedown, true); 100 | } 101 | -------------------------------------------------------------------------------- /src/index_es6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Qunee Graph Editor V2.7.8.5 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | 29 |
30 | 59 |
60 | 61 | 68 | 72 |
73 | 74 | -------------------------------------------------------------------------------- /src/common/i18n.js: -------------------------------------------------------------------------------- 1 | var i18n = { 2 | 'zh-cn': { 3 | 'Name': '名称', 4 | 'Render Color': '渲染色', 5 | 'Border': '边框', 6 | 'Border Color': '边框颜色', 7 | 'Location': '坐标', 8 | 'Size': '尺寸', 9 | 'Rotate': '旋转', 10 | 'Label Color': '文本颜色', 11 | 'Background Color': '背景色', 12 | 'Font Size': '字体大小', 13 | 'json file is empty': 'JSON文件为空', 14 | 'Save Error': '保存错误', 15 | 'Save Success': '保存成功', 16 | 'Update': '更新', 17 | 'Export JSON': '导出JSON', 18 | 'Load File ...': '加载文件 ...', 19 | 'Download File': '下载文件', 20 | 'Save': '保存', 21 | 'Rename': '重命名', 22 | 'Input Element Name': '输入图元名称', 23 | 'Solid Line': '实线样式', 24 | 'Dashed Line': '虚线样式', 25 | 'Line Width': '连线宽度', 26 | 'Input Line Width': '输入连线宽度', 27 | 'Line Color': '连线颜色', 28 | 'Input Line Color': '输入连线颜色', 29 | 'Out of Group': '脱离分组', 30 | 'Send to Top': '置顶显示', 31 | 'Send to Bottom': '置底显示', 32 | 'Reset Layer': '恢复默认层', 33 | 'Clear Graph': '清空画布', 34 | 'Zoom In': '放大', 35 | 'Zoom Out': '缩小', 36 | '1:1': '1:1', 37 | 'Pan Mode': '平移模式', 38 | 'Rectangle Select': '框选模式', 39 | 'Text': '文字', 40 | 'Basic Nodes': '基本节点', 41 | 'Register Images': '注册图片', 42 | 'Default Shapes': '默认形状', 43 | 'Element counts and loading time - Commercial license support more elements and more than 50% performance increase': '图元加载数量与时间 - 商业授权支持更高图元数量, 以及50%以上的性能提升', 44 | 'Submit': '提交', 45 | 'Element Counts': '图元数量', 46 | 'with Edge': '包含连线', 47 | 'with Group': '包含分组', 48 | 'Show Label': '显示标签', 49 | 'Zoom to Overview': '缩放到窗口', 50 | 'Delayed Rendering': '延迟绘制', 51 | 'Angle:': '角度:', 52 | 'Regular': '均匀分配', 53 | 'Proportional': '按需分配', 54 | 'Radius:': '半径:', 55 | 'Uniform': '统一半径', 56 | 'Variable': '可变半径', 57 | 'Min Radius': '最小半径', 58 | 'Default Gap': '默认间距', 59 | 'Start Angle': '起始角度', 60 | 'Beijing': '北京', 61 | 'Shanghai': '上海', 62 | 'Changsha': '长沙', 63 | 'Import Error': '导入异常', 64 | 'Message': '消息', 65 | 'Image export preview': '图片导出预览', 66 | 'Canvas Size': '画布大小', 67 | 'Double click to select the whole canvas range': '双击选择全画布范围', 68 | 'Export Range': '导出范围', 69 | 'Scale': '缩放比例', 70 | 'Output Size': '输出大小', 71 | 'Export': '导出', 72 | 'Print': '打印', 73 | 'Image size is too large, the export may appear memory error': '图幅太大,导出时可能出现内存不足', 74 | 'New Project': '新项目', 75 | 'Application Host': '应用主机', 76 | 'CPU Usage': 'CPU占用', 77 | 'Memory Usage': '内存占用', 78 | 'Hydrological stations': '水文监测站', 79 | 'Show Double Lane': '显示双车道', 80 | 'Show Traffic': '显示路况信息', 81 | //toolbar 82 | 'Default Mode': '默认模式', 83 | 'Rectangle Selection': '框选模式', 84 | 'View Mode': '浏览模式', 85 | 'Create Edge': '创建连线', 86 | 'Create L Edge': '创建L型连线', 87 | 'Create Shape': '创建多边形', 88 | 'Create Line': '创建线条', 89 | 'Export Image': '导出图片', 90 | 'Double click merge': '双击合并', 91 | 'Double click Edit': '双击编辑', 92 | 'Public service \n center network topology': '公共事业服务\n中心网络拓扑图', 93 | 'Storage': '存储', 94 | 'Fiber Switch': '光纤交换机', 95 | 'Switch': '交换机', 96 | 'Core Switch': '核心交换机', 97 | 'Firewall': '防火墙', 98 | 'Router': '路由器', 99 | 'Intranet PC': '内网PC', 100 | 'Public Sector Service Center \n Center LAN': '公共事业服务中心\n中心局域网', 101 | 'Branches': '分支机构', 102 | 'Sinopec internal network area': '中国石化内部网络区', 103 | 'Remote access area': '远程接入区', 104 | 'Security management area': '安全管理区', 105 | 'Sinopec \nInternet node \n network': '中国石化\nInternet节点\n网络', 106 | 'Headquarters core \n switch': '总部核心\n交换机', 107 | 'Found the password guessing attack event \n ...': '发现口令猜测攻击事件\n ...', 108 | 'Server\nSwitch': '服务器\n交换机', 109 | 'Sinopec headquarters network area': '中国石化总部网络区域', 110 | 'Management system network area': '管理系统网络区域', 111 | '': 'TMS,TMS\n数据库,财务公司\n新SAP,浪潮\n报表,共享平台\n数据库,固定报表\n数据库', 112 | } 113 | } 114 | 115 | var lang = navigator.language || navigator.browserLanguage; 116 | lang = lang.toLowerCase(); 117 | 118 | export function getI18NString(key) { 119 | if(!i18n[lang]){ 120 | return key; 121 | } 122 | var result = i18n[lang][key]; 123 | if (result === undefined) { 124 | return key; 125 | } 126 | return result; 127 | } 128 | -------------------------------------------------------------------------------- /src/common/interaction/EdgeInteraction.js: -------------------------------------------------------------------------------- 1 | export class EdgeInteraction extends Q.DrawableInteraction{ 2 | doDraw (g, scale) { 3 | 4 | } 5 | onmousemove(evt, graph){ 6 | if (!this.element) { 7 | return; 8 | } 9 | // var data = this.element; 10 | // var edgeType = data.edgeType; 11 | // var edgeUI = graph.getUI(data); 12 | // var fromUI = graph.getUI(data.fromAgent); 13 | // var toUI = graph.getUI(data.toAgent); 14 | // var sourceBounds = edgeUI.getEndPointBounds(fromUI); 15 | // var targetBounds = edgeUI.getEndPointBounds(toUI); 16 | // 17 | // var xGap = calculateXGap(sourceBounds, targetBounds); 18 | // var yGap = calculateYGap(sourceBounds, targetBounds); 19 | // Q.log('xGap', xGap, 'yGap', yGap); 20 | } 21 | onstart(evt, graph) { 22 | if (this.element) { 23 | return; 24 | } 25 | var data = graph.getElement(evt); 26 | if (!(data instanceof Q.Edge) || this.element == data || !isOrthogonalEdge(data)) { 27 | return; 28 | } 29 | this.element = data; 30 | 31 | var edgeType = data.edgeType; 32 | var edgeUI = graph.getUI(data); 33 | var fromUI = graph.getUI(data.fromAgent); 34 | var toUI = graph.getUI(data.toAgent); 35 | var sourceBounds = edgeUI.getEndPointBounds(fromUI); 36 | var targetBounds = edgeUI.getEndPointBounds(toUI); 37 | 38 | var isH = isHorizontal(edgeType, sourceBounds, targetBounds); 39 | 40 | var split_value; 41 | if(graph.getStyle(data, Q.Styles.EDGE_SPLIT_BY_PERCENT)){ 42 | var split_percent = graph.getStyle(data, Q.Styles.EDGE_SPLIT_PERCENT); 43 | Q.log('split_percent', split_percent) 44 | split_value = split_percent * (isH ? calculateXGap(sourceBounds, targetBounds) : calculateYGap(sourceBounds, targetBounds)); 45 | }else{ 46 | split_value = graph.getStyle(data, Q.Styles.EDGE_SPLIT_VALUE); 47 | } 48 | } 49 | startdrag(evt) { 50 | if (!this.element) { 51 | return; 52 | } 53 | 54 | } 55 | ondrag(evt) { 56 | if (!this.element) { 57 | return; 58 | } 59 | 60 | } 61 | enddrag(evt) { 62 | if (!this.element) { 63 | return; 64 | } 65 | 66 | } 67 | } 68 | 69 | function OrthogonalEdgeInteraction(graph) { 70 | Q.doSuperConstructor(this, OrthogonalEdgeInteraction, arguments); 71 | } 72 | 73 | function isOrthogonalEdge(edge) { 74 | if (edge.hasPathSegments()) { 75 | return false; 76 | } 77 | var type = edge.edgeType; 78 | return type == Q.Consts.EDGE_TYPE_ORTHOGONAL 79 | || type == Q.Consts.EDGE_TYPE_ORTHOGONAL_HORIZONTAL 80 | || type == Q.Consts.EDGE_TYPE_HORIZONTAL_VERTICAL 81 | || type == Q.Consts.EDGE_TYPE_ORTHOGONAL_VERTICAL 82 | || type == Q.Consts.EDGE_TYPE_VERTICAL_HORIZONTAL 83 | || type == Q.Consts.EDGE_TYPE_EXTEND_TOP 84 | || type == Q.Consts.EDGE_TYPE_EXTEND_LEFT 85 | || type == Q.Consts.EDGE_TYPE_EXTEND_BOTTOM 86 | || type == Q.Consts.EDGE_TYPE_EXTEND_RIGHT; 87 | } 88 | 89 | function calculateXGap(sourceBounds, targetBounds) { 90 | var sumWidth = Math.max(sourceBounds.x + sourceBounds.width, targetBounds.x 91 | + targetBounds.width) 92 | - Math.min(sourceBounds.x, targetBounds.x); 93 | return sumWidth - sourceBounds.width - targetBounds.width; 94 | } 95 | 96 | function calculateYGap(sourceBounds, targetBounds) { 97 | var sumHeight = Math.max(sourceBounds.y + sourceBounds.height, 98 | targetBounds.y + targetBounds.height) 99 | - Math.min(sourceBounds.y, targetBounds.y); 100 | return sumHeight - sourceBounds.height - targetBounds.height; 101 | } 102 | 103 | function isHorizontal(edgeType, sourceBounds, targetBounds) { 104 | if (edgeType != null) { 105 | if (edgeType == Consts.EDGE_TYPE_ELBOW_HORIZONTAL 106 | || edgeType == Consts.EDGE_TYPE_ORTHOGONAL_HORIZONTAL 107 | || edgeType == Consts.EDGE_TYPE_HORIZONTAL_VERTICAL 108 | || edgeType == Consts.EDGE_TYPE_EXTEND_LEFT 109 | || edgeType == Consts.EDGE_TYPE_EXTEND_RIGHT) { 110 | return true; 111 | } else if (edgeType == Consts.EDGE_TYPE_ELBOW_VERTICAL 112 | || edgeType == Consts.EDGE_TYPE_ORTHOGONAL_VERTICAL 113 | || edgeType == Consts.EDGE_TYPE_VERTICAL_HORIZONTAL 114 | || edgeType == Consts.EDGE_TYPE_EXTEND_TOP 115 | || edgeType == Consts.EDGE_TYPE_EXTEND_BOTTOM) { 116 | return false; 117 | } 118 | } 119 | var xGap = calculateXGap(sourceBounds, targetBounds); 120 | var yGap = calculateYGap(sourceBounds, targetBounds); 121 | return xGap >= yGap; 122 | } -------------------------------------------------------------------------------- /src/js/FlowSupport.js: -------------------------------------------------------------------------------- 1 | let FLOWING_FORWARD = 'forward'; 2 | let FLOWING_BACKWARD = 'backward'; 3 | import Q from '../lib/qunee-es.js'; 4 | 5 | 6 | export function addFlowSupport(graph){ 7 | new FlowingSupport(graph); 8 | } 9 | class FlowingSupport { 10 | constructor(graph) { 11 | this.length = 0; 12 | this._interval = 300; 13 | this.perStep = 10; 14 | this.map = {}; 15 | this.graph = graph; 16 | this._onStep = function(){ 17 | if (!this.length) { 18 | this._timer = null; 19 | return; 20 | } 21 | let perStep = this.perStep;//Math.max(this.perStep / this.graph.scale, 1); 22 | for (let id in this.map) { 23 | let element = this.map[id]; 24 | let ui = this.graph.getUI(id); 25 | if (!ui) { 26 | // this._doRemove(id); 27 | continue; 28 | } 29 | let lineLength = ui.length; 30 | if (!lineLength) { 31 | continue; 32 | } 33 | let x = element._flowingIcon._offset || 0; 34 | x += element.flow == FLOWING_BACKWARD ? -perStep : perStep; 35 | x %= lineLength; 36 | element._flowingIcon._offset = x; 37 | element._flowingIcon.position = {x: x, y: 0}; 38 | this.graph.invalidateUI(ui); 39 | } 40 | this._timer = setTimeout(this._onStep, this._interval); 41 | }.bind(this) 42 | 43 | graph.dataPropertyChangeDispatcher.addListener(function (evt) { 44 | if (evt.propertyName !== 'flow') { 45 | return; 46 | } 47 | this.onChanged(evt.source); 48 | }.bind(this)); 49 | graph.listChangeDispatcher.addListener(function (evt) { 50 | if (evt.kind == Q.ListEvent.KIND_CLEAR) { 51 | this.clear(); 52 | } else if (evt.kind == Q.ListEvent.KIND_REMOVE) { 53 | this.onRemove(evt.data); 54 | } else if (evt.kind == Q.ListEvent.KIND_ADD) { 55 | this.onAdd(evt.data); 56 | } 57 | }.bind(this)); 58 | } 59 | 60 | clear() { 61 | this.map = {}; 62 | this.length = 0; 63 | } 64 | 65 | _add(item) { 66 | if (this.map[item.id]) { 67 | return; 68 | } 69 | this.length++; 70 | this.map[item.id] = item; 71 | 72 | if (!item._flowingIcon) { 73 | let ui = new Q.ImageUI(Q.Shapes.getShape(Q.Consts.SHAPE_CIRCLE)); 74 | ui.fillColor = '#Fdd'; 75 | ui.layoutByPath = true; 76 | ui.position = {x: 0, y: 0}; 77 | ui.size = {width: 16}; 78 | ui.renderColor = "#F00"; 79 | item.addUI(ui); 80 | item._flowingIcon = ui; 81 | } 82 | 83 | this.start(); 84 | } 85 | 86 | _remove(item) { 87 | if (!this.map[item.id]) { 88 | return; 89 | } 90 | if (item._flowingIcon) { 91 | item.removeUI(item._flowingIcon); 92 | item._flowingIcon = null; 93 | } 94 | delete this.map[item.id]; 95 | this.length--; 96 | } 97 | 98 | start() { 99 | if (this._timer) { 100 | return; 101 | } 102 | this._timer = setTimeout(this._onStep, this._interval); 103 | } 104 | 105 | onChanged(element) { 106 | element.flow ? this._add(element) : this._remove(element); 107 | } 108 | 109 | _dataToArray(data) { 110 | if (Q.isArray(data)) { 111 | return data; 112 | } 113 | return [data]; 114 | } 115 | 116 | onRemove(data) { 117 | data = this._dataToArray(data); 118 | data.forEach(function (item) { 119 | if (this.map[item.id]) { 120 | this._remove(item); 121 | } 122 | }, this); 123 | } 124 | 125 | onAdd(data) { 126 | data = this._dataToArray(data); 127 | data.forEach(function (item) { 128 | if (item instanceof Q.Edge && item.flow && !this.map[item.id]) { 129 | this._add(item); 130 | } 131 | }, this); 132 | } 133 | } 134 | 135 | Object.defineProperties(Q.Edge.prototype, { 136 | flow: { 137 | get() { 138 | return this._flow; 139 | }, 140 | set(v) { 141 | if (this._flow == v) { 142 | return; 143 | } 144 | let old = this._flow; 145 | this._flow = v; 146 | this.firePropertyChangeEvent('flow', v, old); 147 | } 148 | } 149 | }); 150 | Object.defineProperties(Q.ShapeNode.prototype, { 151 | flow: { 152 | get() { 153 | return this._flow; 154 | }, 155 | set(v) { 156 | if (this._flow == v) { 157 | return; 158 | } 159 | let old = this._flow; 160 | this._flow = v; 161 | this.firePropertyChangeEvent('flow', v, old); 162 | } 163 | } 164 | }); 165 | -------------------------------------------------------------------------------- /src/js/GridBackground.js: -------------------------------------------------------------------------------- 1 | import Q from '../lib/qunee-es.js'; 2 | 3 | export class GridBackground { 4 | constructor(graph) { 5 | this.graph = graph; 6 | graph.onPropertyChange('viewport', this.update.bind(this)); 7 | graph.onPropertyChange('transform', this.update.bind(this)); 8 | 9 | this.canvas = Q.createCanvas(graph.width, graph.height, true); 10 | //this.canvas.style.backgroundColor = '#FFD'; 11 | this.canvas.style.position = 'absolute'; 12 | this.canvas.style.top = '0px'; 13 | this.canvas.style['-webkit-user-select'] = 'none'; 14 | this.canvas.style['-webkit-tap-highlight-color'] = 'rgba(0, 0, 0, 0)'; 15 | 16 | this.scaleCanvas = Q.createCanvas(graph.width, graph.height, true); 17 | this.scaleCanvas.style.position = 'absolute'; 18 | this.scaleCanvas.style.top = '0px'; 19 | this.scaleCanvas.style['-webkit-user-select'] = 'none'; 20 | this.scaleCanvas.style['-webkit-tap-highlight-color'] = 'rgba(0, 0, 0, 0)'; 21 | 22 | graph.canvasPanel.insertBefore(this.canvas, graph.canvasPanel.firstChild); 23 | graph.canvasPanel.appendChild(this.scaleCanvas); 24 | 25 | this.update(); 26 | } 27 | 28 | update() { 29 | let graph = this.graph; 30 | let canvas = this.canvas; 31 | let scaleCanvas = this.scaleCanvas; 32 | graph.callLater(function () { 33 | canvas.setSize(graph.width, graph.height); 34 | canvas.width = canvas.width;//clear canvas 35 | scaleCanvas.setSize(graph.width, graph.height); 36 | scaleCanvas.width = canvas.width;//clear canvas 37 | 38 | let scale = graph.scale; 39 | let gap = 50 / scale; 40 | let currentCell = this.currentCell = 10 * (Math.round(gap / 10) || 1); 41 | 42 | scale = graph.scale * canvas.ratio; 43 | let bounds = graph.viewportBounds; 44 | let g = canvas.g; 45 | 46 | g.save(); 47 | this._doTransform(g, scale, bounds); 48 | 49 | g.beginPath(); 50 | let x = bounds.x, y = bounds.y, right = bounds.right, bottom = bounds.bottom; 51 | if (x % currentCell !== 0) { 52 | x -= (x % currentCell); 53 | } 54 | if (y % currentCell !== 0) { 55 | y -= (y % currentCell); 56 | } 57 | while (x < right) { 58 | g.moveTo(x, bounds.y); 59 | g.lineTo(x, bottom); 60 | x += currentCell; 61 | } 62 | while (y < bottom) { 63 | g.moveTo(bounds.x, y); 64 | g.lineTo(right, y); 65 | y += currentCell; 66 | } 67 | 68 | g.lineWidth = 1 / scale; 69 | g.strokeStyle = '#CCC'; 70 | g.stroke(); 71 | 72 | scaleCanvas.g.save(); 73 | this._doTransform(scaleCanvas.g, scale, bounds); 74 | this.drawScales(scaleCanvas.g, bounds, scale, scaleCanvas.ratio); 75 | scaleCanvas.g.restore(); 76 | 77 | g.restore(); 78 | }, this); 79 | } 80 | 81 | _doTransform(g, scale, bounds) { 82 | g.translate(-scale * bounds.x, -scale * bounds.y); 83 | g.scale(scale, scale); 84 | } 85 | 86 | drawText(g, text, x, y, fontSize, textAlign, textBaseline, rotate) { 87 | fontSize = fontSize || 7; 88 | g.save(); 89 | let fontScale = 3; 90 | fontSize *= fontScale; 91 | g.font = 'normal ' + fontSize + 'px helvetica arial'; 92 | g.fillStyle = '#555'; 93 | g.textAlign = textAlign || 'center'; 94 | g.textBaseline = textBaseline || 'top'; 95 | g.translate(x, y); 96 | if (rotate) { 97 | g.rotate(rotate); 98 | } 99 | g.scale(1 / fontScale, 1 / fontScale); 100 | g.fillText(text, 0, 0); 101 | g.restore(); 102 | } 103 | 104 | drawScales(g, bounds, scale, ratio) { 105 | g.beginPath(); 106 | 107 | let scaleLength = 5 * ratio / scale; 108 | 109 | //g.moveTo(bounds.x, bounds.y); 110 | //g.lineTo(bounds.right, bounds.y); 111 | //g.moveTo(bounds.x, bounds.y); 112 | //g.lineTo(bounds.x, bounds.bottom); 113 | // 114 | //g.lineWidth = 5 / scale; 115 | //g.strokeStyle = '#2898E0'; 116 | //g.stroke(); 117 | 118 | let fontSize = 12 * ratio / scale; 119 | 120 | g.beginPath(); 121 | let x = bounds.x; 122 | x = this.currentCell * Math.ceil(x / this.currentCell); 123 | while (x < bounds.right) { 124 | g.moveTo(x, bounds.y); 125 | g.lineTo(x, bounds.y + scaleLength + scaleLength); 126 | this.drawText(g, '' + x | 0, x, bounds.y + scaleLength + scaleLength, fontSize); 127 | x += this.currentCell; 128 | } 129 | let y = bounds.y; 130 | y = this.currentCell * Math.ceil(y / this.currentCell); 131 | while (y < bounds.bottom) { 132 | g.moveTo(bounds.x, y); 133 | g.lineTo(bounds.x + scaleLength + scaleLength, y); 134 | this.drawText(g, '' + y | 0, bounds.x + scaleLength + scaleLength, y, fontSize, 'center', 'top', -Math.PI / 6); 135 | y += this.currentCell; 136 | } 137 | g.lineWidth = 1 / scale; 138 | g.strokeStyle = '#000'; 139 | g.stroke(); 140 | } 141 | } -------------------------------------------------------------------------------- /src/graph.editor.js: -------------------------------------------------------------------------------- 1 | import Q from "./lib/qunee-es.js"; 2 | import {getI18NString} from "./common/i18n.js"; 3 | import {Overview} from "./common/overview/Overview.js"; 4 | import {addFlowSupport} from "./js/FlowSupport.js"; 5 | import {exportJSON, parseJSON} from "./common/io/JSONSerializer.js"; 6 | import {createButtonGroup, Toolbar} from "./common/toolbar/Toolbar.js"; 7 | import {ToolBox} from "./common/toolbox/ToolBox.js"; 8 | import {PopupMenu} from "./common/popup/PopupMenu.js"; 9 | import {PropertyPane} from "./common/property/PropertyPane.js"; 10 | 11 | function forEachItem(dom, call) { 12 | let children = dom.children; 13 | let i = 0, l = children.length; 14 | while (i < l) { 15 | let item = children[i++]; 16 | call(item); 17 | forEachItem(item, call); 18 | } 19 | } 20 | 21 | export class Editor { 22 | _checkVue(dom) { 23 | let refs = this.$refs = {}; 24 | forEachItem(dom, function (item) { 25 | let ref = item.getAttribute('ref'); 26 | if (ref) { 27 | refs[ref] = item; 28 | } 29 | let click = item.getAttribute('@click'); 30 | if (click) { 31 | item.onclick = function (evt) { 32 | this[click](evt); 33 | }.bind(this) 34 | } 35 | }.bind(this)) 36 | } 37 | 38 | constructor(container, options) { 39 | let template = document.getElementById(options.template); 40 | let dom = template.content.cloneNode(true); 41 | this._checkVue(dom); 42 | if(typeof container === 'string'){ 43 | container = document.getElementById(container); 44 | } 45 | this.container = container; 46 | container.appendChild(dom); 47 | let graph = this.graph = new Q.Graph(this.$refs.canvas); 48 | graph.editable = true; 49 | graph.originAtCenter = false; 50 | 51 | let overview = new Overview(this.$refs.overview, graph); 52 | 53 | this.toolbar = new Toolbar(this.$refs.toolbar); 54 | this.toolbar.setGraph(graph); 55 | this.toolbox = new ToolBox(this.$refs.toolbox); 56 | this.propertyPane = new PropertyPane(graph, this.$refs.property); 57 | this.initPopupMenu(graph); 58 | 59 | let demoBtnGroup = createButtonGroup([{ 60 | name: 'edit mode', type: 'checkbox', checked: graph.editable, action(evt) { 61 | this.setEditable(evt.target.checked) 62 | } 63 | }, { 64 | name: getI18NString('show json panel'), iconClass: 'q-icon toolbar-json', 65 | action(evt) { 66 | let jsonPane = this.$refs.json_pane; 67 | let visible = jsonPane.style.display === 'none'; 68 | visible ? evt.currentTarget.classList.add('active') : evt.currentTarget.classList.remove('active'); 69 | jsonPane.style.display = visible ? '' : 'none'; 70 | if (visible) { 71 | this.exportJSON(); 72 | } 73 | } 74 | }, { 75 | name: getI18NString('show overview'), iconClass: 'q-icon toolbar-overview', 76 | selected: true, 77 | action(evt) { 78 | let visible = !overview.visible; 79 | visible ? evt.currentTarget.classList.add('active') : evt.currentTarget.classList.remove('active'); 80 | overview.setVisible(visible); 81 | overview.setGraph(visible ? graph : null); 82 | } 83 | }, { 84 | name: getI18NString('max'), iconClass: 'q-icon toolbar-max', 85 | action(evt) { 86 | let el = this.container; 87 | let isMax = !el.classList.contains('q-max'); 88 | isMax ? evt.currentTarget.classList.add('active') : evt.currentTarget.classList.remove('active'); 89 | isMax ? el.classList.add('q-max') : el.classList.remove('q-max'); 90 | graph.updateViewport(); 91 | } 92 | }], this); 93 | demoBtnGroup.style.float = 'right'; 94 | this.$refs.toolbar_pane.appendChild(demoBtnGroup); 95 | } 96 | 97 | setEditable(editable) { 98 | this.graph.editable = editable; 99 | this.graph.interactionMode = editable ? Q.Consts.INTERACTION_MODE_DEFAULT : Q.Consts.INTERACTION_MODE_VIEW; 100 | this.graph.updateViewport(); 101 | 102 | this.toolbar.setMode(editable ? 'default' : 'view'); 103 | 104 | function setVisible(div, visible) { 105 | div.style.display = visible ? '' : 'none'; 106 | } 107 | 108 | setVisible(this.$refs.toolbox, editable); 109 | setVisible(this.$refs.property, editable); 110 | setVisible(this.$refs.json_btn_group, editable); 111 | } 112 | 113 | exportJSON() { 114 | this.$refs.json_textarea.value = exportJSON(this.graph, true, {space: ' '}); 115 | } 116 | 117 | submitJSON() { 118 | let json = this.$refs.json_textarea.value; 119 | this.graph.clear(); 120 | parseJSON(json, this.graph); 121 | } 122 | 123 | initPopupMenu(graph) { 124 | addFlowSupport(graph); 125 | let popupmenu = graph.popupmenu = new PopupMenu(graph); 126 | let superGetMenuItems = popupmenu.getMenuItems; 127 | popupmenu.getMenuItems = function (graph, element, evt) { 128 | let items = superGetMenuItems.apply(this, arguments); 129 | if (!element) { 130 | return items; 131 | } 132 | if (element instanceof Q.Group) { 133 | items.unshift({ 134 | text: 'Ellipse Group', 135 | action() { 136 | element.groupType = Q.Consts.GROUP_TYPE_ELLIPSE; 137 | } 138 | }) 139 | items.unshift({ 140 | text: 'Rect Group', 141 | action() { 142 | element.groupType = Q.Consts.GROUP_TYPE_RECT; 143 | } 144 | }) 145 | } 146 | if (element instanceof Q.Edge || element instanceof Q.ShapeNode) { 147 | let flow = element.flow !== true; 148 | items.unshift({ 149 | text: flow ? 'Flow' : 'Stop Flow', 150 | action() { 151 | element.flow = flow; 152 | } 153 | }) 154 | } 155 | return items; 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /src/common/property/input.js: -------------------------------------------------------------------------------- 1 | import {createElement} from "../utils.js"; 2 | import {createColorInput} from "../color-picker/ColorPicker.js"; 3 | import Q from "../../lib/qunee-es.js"; 4 | 5 | class StringEditor { 6 | constructor(property, parent, getter, setter, scope) { 7 | this.getter = getter; 8 | this.setter = setter; 9 | this.scope = scope; 10 | this.property = property; 11 | 12 | this.createHtml(parent); 13 | } 14 | 15 | _getValue() { 16 | return this.getter.call(this.scope); 17 | } 18 | 19 | update() { 20 | this.value = this._getValue(); 21 | } 22 | 23 | setValue(v) { 24 | this.input.value = valueToString(v, this.property.type); 25 | } 26 | 27 | getValue() { 28 | return stringToValue(this.input.value, this.property.type); 29 | } 30 | 31 | createHtml(parent) { 32 | let input; 33 | let property = this.property; 34 | if (Array.isArray(property.options)) { 35 | input = createElement({ 36 | tagName: 'select', 37 | className: "form-control", 38 | parent: parent 39 | }); 40 | 41 | property.options.forEach(function (item, i) { 42 | let option = document.createElement('option'); 43 | option.value = item; 44 | option.innerText = item; 45 | input.appendChild(option); 46 | }) 47 | } else { 48 | input = createElement({ 49 | tagName: 'input', 50 | className: "form-control", 51 | parent: parent 52 | }); 53 | } 54 | 55 | this.input = input; 56 | 57 | if (property.readonly) { 58 | input.setAttribute('readonly', 'readonly'); 59 | } 60 | this.update(); 61 | 62 | input.oninput = function (evt) { 63 | if (this.ajdusting) { 64 | return; 65 | } 66 | this.setter.call(this.scope, this); 67 | }.bind(this); 68 | } 69 | } 70 | 71 | Object.defineProperties(StringEditor.prototype, { 72 | value: { 73 | get() { 74 | return this.getValue(); 75 | }, 76 | set(v) { 77 | this.ajdusting = true; 78 | this.setValue(v); 79 | this.ajdusting = false; 80 | } 81 | } 82 | }) 83 | 84 | class BooleanEditor extends StringEditor { 85 | setValue(v) { 86 | if (v) { 87 | this.input.setAttribute('checked', 'checked') 88 | } else { 89 | this.input.removeAttribute('checked') 90 | } 91 | // this.input.setAttribute('checked', v ? 'checked' : false); 92 | } 93 | 94 | getValue() { 95 | return stringToValue(this.input.checked, this.property.type); 96 | } 97 | 98 | createHtml(parent) { 99 | let property = this.property; 100 | let input = createElement({ 101 | tagName: 'input', 102 | parent: parent 103 | }); 104 | input.setAttribute('type', 'checkbox'); 105 | this.input = input; 106 | 107 | if (property.readonly) { 108 | input.setAttribute('readonly', 'readonly'); 109 | } 110 | this.update(); 111 | input.onclick = function (evt) { 112 | if (this.ajdusting) { 113 | return; 114 | } 115 | this.setter.call(this.scope, this); 116 | }.bind(this) 117 | } 118 | } 119 | 120 | class ColorEditor extends StringEditor { 121 | createHtml (parent) { 122 | let colorInput = this.colorInput = createColorInput({ 123 | onchange: function(color) { 124 | this.value = color; 125 | }.bind(this) 126 | }) 127 | parent.appendChild(colorInput.dom); 128 | this.input = colorInput.input; 129 | 130 | this.update(); 131 | } 132 | 133 | setValue(v) { 134 | this.input.value = valueToString(v, this.property.type); 135 | this.setter.call(this.scope, this); 136 | } 137 | 138 | getValue() { 139 | return stringToValue(this.input.value, this.property.type); 140 | } 141 | } 142 | 143 | 144 | function numberToString(number) { 145 | return number | 0; 146 | //return number.toFixed(2); 147 | } 148 | 149 | function valueToString(value, type) { 150 | if (!value) { 151 | return value; 152 | } 153 | if (type == 'point') { 154 | return numberToString(value.x) + ',' + numberToString(value.y); 155 | } 156 | if (type == 'size') { 157 | return numberToString(value.width) + ',' + numberToString(value.height); 158 | } 159 | if (type == 'degree') { 160 | return '' + (value * 180 / Math.PI) | 0; 161 | } 162 | return value.toString(); 163 | } 164 | 165 | let positions = {}; 166 | for (let name in Q.Position) { 167 | let p = Q.Position[name]; 168 | if (name == "random" || !(p instanceof Q.Position)) { 169 | continue; 170 | } 171 | positions[p.toString()] = p; 172 | } 173 | 174 | export function stringToValue(string, type) { 175 | // if (type == 'color' && $.colorpicker && $.colorpicker.Color) { 176 | // return new $.colorpicker.Color(string, null, null, "hex", true).toString() 177 | // } 178 | if (type == 'position') { 179 | return positions[string]; 180 | } 181 | if (type == 'number') { 182 | return parseFloat(string) || 0; 183 | } 184 | if (type == 'boolean') { 185 | return string ? true : false; 186 | } 187 | if (type == 'point') { 188 | let xy = string.split(','); 189 | if (xy.length == 2) { 190 | return {x: parseFloat(xy[0] || 0), y: parseFloat(xy[1]) || 0}; 191 | } 192 | return; 193 | } 194 | if (type == 'size') { 195 | let xy = string.split(','); 196 | if (xy.length == 2) { 197 | let w = parseFloat(xy[0]) || 0; 198 | let h = parseFloat(xy[1]) || 0; 199 | if (w && h) { 200 | return {width: w, height: h}; 201 | } 202 | } 203 | return; 204 | } 205 | if (type == 'degree') { 206 | return parseInt(string) * Math.PI / 180 207 | } 208 | return string; 209 | } 210 | 211 | export function createCellEditor(item, parent, getter, setter, scope) { 212 | let type = item.type; 213 | if (type == 'color') { 214 | return new ColorEditor(item, parent, getter, setter, scope); 215 | } 216 | if (type == 'boolean') { 217 | return new BooleanEditor(item, parent, getter, setter, scope); 218 | } 219 | return new StringEditor(item, parent, getter, setter, scope); 220 | } -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | import Q from "../lib/qunee-es.js"; 2 | import {GridBackground} from "./GridBackground.js"; 3 | import {parseJSON} from "../common/io/JSONSerializer.js"; 4 | 5 | Q.registerImage('lamp', Q.Shapes.getShape(Q.Consts.SHAPE_CIRCLE, -8, -8, 16, 16)); 6 | 7 | let lampGradient = new Q.Gradient(Q.Consts.GRADIENT_TYPE_RADIAL, [Q.toColor(0xAAFFFFFF), Q.toColor(0x33EEEEEE), Q.toColor(0x44888888), Q.toColor(0x33666666)], 8 | [0.1, 0.3, 0.7, 0.9], 0, -0.2, -0.2); 9 | 10 | Q.Group.prototype.linkable = false; 11 | 12 | function createLampStyles(color) { 13 | let styles = {}; 14 | styles[Q.Styles.SHAPE_FILL_COLOR] = color; 15 | styles[Q.Styles.SHAPE_STROKE] = 0.5; 16 | styles[Q.Styles.SHAPE_STROKE_STYLE] = '#CCC'; 17 | styles[Q.Styles.LABEL_BACKGROUND_COLOR] = '#FF0'; 18 | styles[Q.Styles.SHAPE_FILL_COLOR] = color; 19 | styles[Q.Styles.LABEL_SIZE] = {width: 100, height: 20}; 20 | styles[Q.Styles.LABEL_PADDING] = 5; 21 | styles[Q.Styles.LABEL_OFFSET_Y] = -10; 22 | styles[Q.Styles.SHAPE_FILL_GRADIENT] = lampGradient; 23 | styles[Q.Styles.LABEL_POSITION] = 'ct'; 24 | styles[Q.Styles.LABEL_ANCHOR_POSITION] = 'lb'; 25 | return styles; 26 | } 27 | 28 | let customShape = new Q.Path(); 29 | customShape.moveTo(-200, -50); 30 | customShape.lineTo(200, -50); 31 | customShape.lineTo(200, 50); 32 | customShape.lineTo(-100, 50); 33 | 34 | let editor_config = { 35 | images: [ 36 | { 37 | name: '自定义Shape', 38 | images: [{ 39 | image: 'lamp', customDrop: true 40 | } 41 | ] 42 | },{ 43 | // name: 'Cisco图标', 44 | // root: 'data/cisco/', 45 | // images: ['ATMSwitch.png', 'multilayerSwitch.png', 'workgroupSwitch.png', 'workgroupSwitchSubdued.png', '100BaseT_hub.png', 'cisco_hub.png', 'switch1100.png'] 46 | // }, { 47 | name: '自定义图标', 48 | imageWidth: 30, 49 | imageHeight: 30, 50 | images: [{ 51 | image: 'lamp', 52 | properties: { 53 | name: 'Message' 54 | }, 55 | styles: createLampStyles('#F00') 56 | }, { 57 | image: 'lamp', 58 | properties: { 59 | name: 'Message' 60 | }, 61 | br: true, 62 | styles: createLampStyles('#FF0') 63 | }, { 64 | image: 'lamp', 65 | properties: { 66 | name: 'Message' 67 | }, 68 | styles: createLampStyles('#0F0') 69 | }, { 70 | image: 'lamp', 71 | properties: { 72 | name: 'Message' 73 | }, 74 | styles: createLampStyles('#0FF') 75 | }, { 76 | image: 'lamp', 77 | properties: { 78 | name: 'Message' 79 | }, 80 | styles: createLampStyles('#00F') 81 | }, { 82 | image: 'lamp', 83 | properties: { 84 | name: 'Message' 85 | }, 86 | styles: createLampStyles('#F0F') 87 | }] 88 | }], 89 | callback: function (editor) { 90 | let graph = editor.graph; 91 | let defaultStyles = graph.styles = {}; 92 | defaultStyles[Q.Styles.ARROW_TO] = false; 93 | 94 | graph.dropAction = function (evt, xy, info) { 95 | if (!info.customDrop) { 96 | return 97 | } 98 | //自己处理拖拽事件,返回false时,qunee就不再处理 99 | let shape = this.createShapeNode('自定义shape', customShape, xy.x, xy.y); 100 | return false; 101 | } 102 | 103 | graph.moveToCenter(); 104 | // graph.createNode('10000', 10000, 10000); 105 | 106 | new GridBackground(graph); 107 | 108 | let currentCell = 10; 109 | 110 | function snapToGrid(x, y) { 111 | let gap = currentCell; 112 | x = Math.round(x / gap) * gap; 113 | y = Math.round(y / gap) * gap; 114 | return [x, y]; 115 | } 116 | 117 | /** 118 | * 根据拖拽信息,找到拖拽源头对象 119 | */ 120 | function findDragTargetByDropEvent(dropEvent){ 121 | let dragInfo = dropEvent.dataTransfer.getData('text'); 122 | let parent = editor.toolbox.dom; 123 | let list = parent.querySelectorAll('[draggable=true]'); 124 | let i = 0, l = list.length; 125 | while(i < l){ 126 | let item = list[i++]; 127 | if(item.getAttribute('draginfo') === dragInfo){ 128 | return item.parentElement; 129 | } 130 | } 131 | } 132 | 133 | graph.interactionDispatcher.addListener(function (evt) { 134 | if (evt.kind == Q.InteractionEvent.ELEMENT_CREATED) { 135 | // let target = findDragTargetByDropEvent(evt.event); 136 | // if(target){ 137 | // target.parentElement.removeChild(target); 138 | // } 139 | 140 | let node = evt.data; 141 | if (!(node instanceof Q.Node)) { 142 | return 143 | } 144 | let ps = snapToGrid(node.x, node.y); 145 | node.setLocation(ps[0], ps[1]); 146 | if (node instanceof Q.ShapeNode) { 147 | node.zIndex = 10; 148 | } 149 | return; 150 | } 151 | if (evt.kind == Q.InteractionEvent.ELEMENT_MOVE_END) { 152 | let datas = evt.datas; 153 | datas.forEach(function (node) { 154 | if (!(node instanceof Q.Node) || node instanceof Q.Group) { 155 | return 156 | } 157 | let ps = snapToGrid(node.x, node.y); 158 | node.setLocation(ps[0], ps[1]); 159 | }); 160 | return; 161 | } 162 | // if (evt.kind == Q.InteractionEvent.POINT_MOVE_END) { 163 | // let line = evt.data; 164 | // Q.log(evt.point); 165 | // let segment = evt.point.segment; 166 | // segment.points = snapToGrid(segment.points[0], segment.points[1]); 167 | // line.invalidate(); 168 | // return; 169 | // } 170 | }); 171 | } 172 | } 173 | 174 | export function initDemo(editor, dataUrl){ 175 | let graph = editor.graph; 176 | editor.toolbox.loadImageBox(editor_config.images); 177 | dataUrl && Q.loadJSON(dataUrl, function (json) { 178 | parseJSON(json, graph); 179 | graph.moveToCenter(); 180 | }) 181 | editor_config.callback(editor); 182 | } 183 | -------------------------------------------------------------------------------- /src/common/toolbox/DragSupport.js: -------------------------------------------------------------------------------- 1 | 2 | import Q from '../../lib/qunee-es.js'; 3 | import {exportJSON} from "../io/JSONSerializer.js"; 4 | 5 | ///drag and drop 6 | var DRAGINFO_PREFIX = "draginfo"; 7 | 8 | function ondrag(evt) { 9 | evt = evt || window.event; 10 | var dataTransfer = evt.dataTransfer; 11 | var img = evt.target; 12 | dataTransfer.setData("text", img.getAttribute(DRAGINFO_PREFIX)); 13 | } 14 | 15 | export function createDNDImage(parent, src, title, info) { 16 | var img = document.createElement("img"); 17 | img.src = src; 18 | img.setAttribute("title", title); 19 | if(!info){ 20 | info = { 21 | label: title 22 | }; 23 | } 24 | if (!info.image && (!info.type || info.type == "Node")) { 25 | info.image = src; 26 | } 27 | appendDNDInfo(img, info); 28 | parent.appendChild(img); 29 | return img; 30 | } 31 | 32 | exportJSON 33 | export function appendDNDInfo(img, info) { 34 | img.setAttribute("draggable", "true"); 35 | img.setAttribute(DRAGINFO_PREFIX, exportJSON(info, true)); 36 | img.ondragstart = ondrag; 37 | return img; 38 | } 39 | 40 | var isIE9_10 = /MSIE 9/i.test(navigator.userAgent) || /MSIE 10/i.test(navigator.userAgent); 41 | var dragSupport = !isIE9_10; 42 | if (!dragSupport) { 43 | var DRAG_INFO = {}; 44 | var getMousePageLocation = function (evt) { 45 | return { 46 | x: evt.pageX, 47 | y: evt.pageY 48 | } 49 | } 50 | var body = document.documentElement; 51 | var enableDrag = function () { 52 | 53 | body.addEventListener('mousemove', function (evt) { 54 | if (!DRAG_INFO.target) { 55 | return; 56 | } 57 | Q.stopEvent(evt); 58 | var point = getMousePageLocation(evt); 59 | if (!DRAG_INFO.dragElement) { 60 | var target = DRAG_INFO.target; 61 | if (Math.abs(point.x - DRAG_INFO.dragPoint.x) <= 5 || Math.abs(point.y - DRAG_INFO.dragPoint.y) <= 5) { 62 | return 63 | } 64 | var div = document.createElement('div'); 65 | div.style.position = 'absolute'; 66 | div.style.zIndex = 10000; 67 | 68 | var dragButton = target.cloneNode(true); 69 | if (/canvas/i.test(dragButton.tagName)) { 70 | dragButton.getContext('2d').drawImage(target, 0, 0); 71 | } else { 72 | div.style.maxWidth = '30px'; 73 | div.style.maxWidth = '30px'; 74 | div.style.cursor = 'move' 75 | } 76 | //dragButton.style.pointerEvents = 'none'; 77 | //div.style.pointerEvents = 'none'; 78 | dragButton.id = null; 79 | //div.setAttribute('class', 'drag-element'); 80 | div.appendChild(dragButton); 81 | body.appendChild(div); 82 | DRAG_INFO.dragElement = div; 83 | 84 | var event = {target: target} 85 | //start drag 86 | if (target.ondragstart instanceof Function) { 87 | DRAG_INFO.dataTransfer = event.dataTransfer = { 88 | datas: {}, 89 | setData: function (name, value) { 90 | this.datas[name] = value; 91 | }, 92 | getData: function (name) { 93 | return this.datas[name]; 94 | } 95 | } 96 | target.ondragstart(event); 97 | } 98 | } 99 | DRAG_INFO.dragElement.style.left = (point.x - DRAG_INFO.dragElement.clientWidth / 2) + 'px'; 100 | DRAG_INFO.dragElement.style.top = (point.y - DRAG_INFO.dragElement.clientHeight / 2) + 'px'; 101 | }, false); 102 | body.addEventListener('mouseup', function (evt) { 103 | if (!DRAG_INFO.target) { 104 | return; 105 | } 106 | delete DRAG_INFO.dragPoint; 107 | delete DRAG_INFO.target; 108 | 109 | if (DRAG_INFO.dragElement) { 110 | body.removeChild(DRAG_INFO.dragElement); 111 | delete DRAG_INFO.dragElement; 112 | } 113 | 114 | var point = getMousePageLocation(evt); 115 | 116 | var graphs = document.getElementsByClassName('Q-CanvasPanel'); 117 | var i = 0; 118 | while (i < graphs.length) { 119 | var graph = graphs[i]; 120 | ++i; 121 | var viewport = getClientRect(graph); 122 | if (!containPoint(viewport, point)) { 123 | continue; 124 | } 125 | if (graph.ondrop instanceof Function) { 126 | evt.dataTransfer = DRAG_INFO.dataTransfer; 127 | graph.ondrop(evt); 128 | } 129 | break; 130 | } 131 | delete DRAG_INFO.dataTransfer; 132 | }, false); 133 | } 134 | var containPoint = function (rect, point) { 135 | return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; 136 | } 137 | var getOffset = function (element) { 138 | var left = 0; 139 | var top = 0; 140 | while (element.offsetParent) { 141 | left += element.clientLeft + element.offsetLeft - element.scrollLeft; 142 | top += element.clientTop + element.offsetTop - element.scrollTop; 143 | element = element.offsetParent; 144 | } 145 | return {x: left, y: top}; 146 | } 147 | var getClientRect = function (root) { 148 | var offset = getOffset(root); 149 | var x = offset.x + root.scrollLeft; 150 | var y = offset.y + root.scrollTop; 151 | var width = root.clientWidth; 152 | var height = root.clientHeight; 153 | return { 154 | x: x, 155 | y: y, 156 | left: x, 157 | top: y, 158 | right: x + width, 159 | bottom: y + height, 160 | width: width, 161 | height: height 162 | } 163 | } 164 | var appendDragInfo2 = function (button) { 165 | button.onmousedown = function (evt) { 166 | DRAG_INFO.dragPoint = getMousePageLocation(evt); 167 | DRAG_INFO.target = button; 168 | Q.stopEvent(evt); 169 | } 170 | return button; 171 | } 172 | 173 | appendDNDInfo = function (img, info) { 174 | img.setAttribute("draggable", "true"); 175 | img.setAttribute(DRAGINFO_PREFIX, JSON.stringify(info)); 176 | img.ondragstart = ondrag; 177 | 178 | appendDragInfo2(img) 179 | return img; 180 | } 181 | enableDrag(); 182 | } 183 | 184 | -------------------------------------------------------------------------------- /src/common/toolbar/toolbar.css: -------------------------------------------------------------------------------- 1 | .Q-Toolbar{ 2 | box-sizing: border-box; 3 | white-space: nowrap; 4 | } 5 | .Q-Toolbar>div:not(:last-child){ 6 | margin-right: 5px; 7 | } 8 | 9 | .btn-group { 10 | position: relative; 11 | display: inline-block; 12 | vertical-align: middle; 13 | } 14 | 15 | .btn-group > .btn { 16 | margin: 0px -1px; 17 | position: relative; 18 | float: left; 19 | color: #333; 20 | background-color: #fff; 21 | border: 1px solid #ccc; 22 | text-align: center; 23 | cursor: pointer; 24 | /*background-image: none;*/ 25 | user-select: none; 26 | padding: 5px 10px; 27 | border-radius: 3px; 28 | font-size: 0.8em; 29 | /*display: inline-block;*/ 30 | display: flex; 31 | align-items: center !important; 32 | min-height: 20px; 33 | } 34 | 35 | .btn-group > .btn > .label-with-icon { 36 | margin-left: .4rem; 37 | } 38 | 39 | .btn-group > .btn:first-child { 40 | margin-left: 0; 41 | } 42 | 43 | .btn-group > .btn:first-child:not(:last-child) { 44 | border-top-right-radius: 0; 45 | border-bottom-right-radius: 0; 46 | } 47 | 48 | .btn-group > .btn:last-child { 49 | margin-left: 0; 50 | } 51 | 52 | .btn-group > .btn:last-child:not(:first-child) { 53 | border-top-left-radius: 0; 54 | border-bottom-left-radius: 0; 55 | } 56 | 57 | .btn-group > .btn:not(:first-child):not(:last-child) { 58 | border-radius: 0; 59 | } 60 | .btn { 61 | padding: 5px 10px; 62 | font-size: 12px; 63 | line-height: 1.5; 64 | border-radius: 3px; 65 | display: inline-block; 66 | vertical-align: middle; 67 | background-color: #fff; 68 | border: 1px solid #ccc; 69 | text-align: center; 70 | cursor: pointer; 71 | } 72 | .btn:active, .btn.active { 73 | background-color: #ebebeb; 74 | /*-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);*/ 75 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 76 | /*z-index: 2;*/ 77 | } 78 | 79 | .btn:hover, .btn:focus { 80 | color: #333; 81 | background-color: #ebebeb; 82 | border-color: #adadad; 83 | /*z-index: 2;*/ 84 | } 85 | 86 | .input-group .form-control { 87 | position: relative; 88 | /*z-index: 2;*/ 89 | float: left; 90 | width: 100%; 91 | margin-bottom: 0; 92 | display: table-cell; 93 | border-top-right-radius: 0; 94 | border-bottom-right-radius: 0; 95 | } 96 | .input-group{ 97 | position: relative; 98 | display: table; 99 | border-collapse: separate; 100 | box-sizing: border-box; 101 | } 102 | .input-group-btn{ 103 | position: relative; 104 | font-size: 0; 105 | white-space: nowrap; 106 | } 107 | .toolbar>.btn:not(:last-child), .toolbar>.btn-group:not(:last-child) { 108 | margin-right: 5px; 109 | } 110 | 111 | /*重新设置*/ 112 | .Q-Toolbar>div{ 113 | display: inline-block; 114 | vertical-align: middle; 115 | } 116 | .Q-Toolbar>.Q-Search>div{ 117 | display: table; 118 | } 119 | .Q-Toolbar>.Q-Search>div>*{ 120 | display: table-cell; 121 | vertical-align: middle; 122 | } 123 | .Q-Toolbar>.Q-Search input{ 124 | border-top-right-radius: 0; 125 | border-bottom-right-radius: 0; 126 | } 127 | .Q-Toolbar>.Q-Search .btn{ 128 | border-bottom-left-radius: 0; 129 | border-top-left-radius: 0; 130 | margin-left: -1px; 131 | } 132 | .Q-Toolbar input:focus { 133 | border-color: #66afe9; 134 | outline: 0; 135 | box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 8px rgba(102,175,233,0.6); 136 | } 137 | 138 | #toolbar { 139 | position: relative; 140 | } 141 | #toolbar .btn, #toolbar .btn-group { 142 | margin-right: 5px; 143 | } 144 | 145 | #toolbar .btn-group .btn { 146 | margin: 0px -1px; 147 | } 148 | 149 | /*icons*/ 150 | .q-icon { 151 | background-image: url(./icons-32.png); 152 | background-size: 479px 16px; 153 | } 154 | .toolbar-add{ background-position: 0px 0px; width: 16px; height: 16px; } 155 | .toolbar-default{ background-position: -18px 0px; width: 16px; height: 16px; } 156 | .toolbar-download{ background-position: -37px 0px; width: 16px; height: 16px; } 157 | .toolbar-edge_VH{ background-position: -55px 0px; width: 16px; height: 16px; } 158 | .toolbar-edge{ background-position: -74px 0px; width: 16px; height: 16px; } 159 | .toolbar-expand{ background-position: -92px 0px; width: 16px; height: 16px; } 160 | .toolbar-json{ background-position: -111px 0px; width: 16px; height: 16px; } 161 | .toolbar-line{ background-position: -129px 0px; width: 16px; height: 16px; } 162 | .toolbar-magnifying_glass{ background-position: -148px 0px; width: 16px; height: 16px; } 163 | .toolbar-max{ background-position: -166px 0px; width: 16px; height: 16px; } 164 | .toolbar-new_window{ background-position: -185px 0px; width: 16px; height: 16px; } 165 | .toolbar-new{ background-position: -203px 0px; width: 16px; height: 16px; } 166 | .toolbar-overview{ background-position: -222px 0px; width: 16px; height: 16px; } 167 | .toolbar-pan{ background-position: -240px 0px; width: 16px; height: 16px; } 168 | .toolbar-polygon{ background-position: -259px 0px; width: 16px; height: 16px; } 169 | .toolbar-print{ background-position: -277px 0px; width: 16px; height: 16px; } 170 | .toolbar-rectangle_selection{ background-position: -296px 0px; width: 16px; height: 16px; } 171 | .toolbar-remove{ background-position: -314px 0px; width: 16px; height: 16px; } 172 | .toolbar-save{ background-position: -333px 0px; width: 16px; height: 16px; } 173 | .toolbar-search{ background-position: -351px 0px; width: 16px; height: 16px; } 174 | .toolbar-update{ background-position: -370px 0px; width: 16px; height: 16px; } 175 | .toolbar-upload{ background-position: -388px 0px; width: 16px; height: 16px; } 176 | .toolbar-zoom_overview{ background-position: -407px 0px; width: 16px; height: 16px; } 177 | .toolbar-zoomin{ background-position: -425px 0px; width: 16px; height: 16px; } 178 | .toolbar-zoomout{ background-position: -444px 0px; width: 16px; height: 16px; } 179 | .toolbar-zoomreset{ background-position: -462px 0px; width: 16px; height: 16px; } 180 | 181 | 182 | 183 | /*input file*/ 184 | .btn-file { 185 | position: relative; 186 | overflow: hidden; 187 | } 188 | 189 | .btn-file input[type=file] { 190 | position: absolute; 191 | top: 0; 192 | right: 0; 193 | min-width: 100%; 194 | min-height: 100%; 195 | font-size: 100px; 196 | text-align: right; 197 | filter: alpha(opacity=0); 198 | opacity: 0; 199 | background-color: red; 200 | cursor: inherit; 201 | display: block; 202 | } 203 | 204 | input[readonly] { 205 | background-color: white !important; 206 | cursor: text !important; 207 | } 208 | 209 | .form-control{ 210 | display: table-cell; 211 | height: 30px; 212 | padding: 5px 10px; 213 | font-size: 12px; 214 | line-height: 1.5; 215 | border-radius: 3px; 216 | color: #555; 217 | background-color: #fff; 218 | background-image: none; 219 | border: 1px solid #ccc; 220 | } 221 | 222 | * { 223 | box-sizing: border-box; 224 | } 225 | 226 | 227 | .btn{ 228 | height: 30px; 229 | } 230 | -------------------------------------------------------------------------------- /src/common/vue/GraphEditor.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /src/common/overview/Overview.js: -------------------------------------------------------------------------------- 1 | import Q from '../../lib/qunee-es.js'; 2 | 3 | function globalToLocal(evt, div) { 4 | if (evt.touches) { 5 | if (evt.changedTouches && evt.changedTouches.length) { 6 | evt = evt.changedTouches[0]; 7 | } else { 8 | evt = evt.touches[0]; 9 | } 10 | } 11 | let clientRect = div.getBoundingClientRect(); 12 | let x = evt.clientX || 0; 13 | let y = evt.clientY || 0; 14 | if (Q.isTouchSupport && Q.isSafari) { 15 | if (window.pageXOffset && x == evt.pageX) { 16 | x -= window.pageXOffset; 17 | } 18 | if (window.pageYOffset && y == evt.pageY) { 19 | y -= window.pageYOffset; 20 | } 21 | } 22 | return [x - clientRect.left, y - clientRect.top]; 23 | } 24 | 25 | export class Overview { 26 | constructor(html, graph) { 27 | this._invalidateGraphFlag = false; 28 | this.visible = true; 29 | this.html = html; 30 | this.canvas = Q.createCanvas(true); 31 | this.html.appendChild(this.canvas); 32 | new Q.DragSupport(this.canvas, this); 33 | this.setGraph(graph); 34 | } 35 | 36 | setGraph(graph) { 37 | if (this.graph == graph) { 38 | return; 39 | } 40 | this._uninstall(); 41 | this.graph = graph; 42 | this._install(); 43 | } 44 | 45 | _install() { 46 | if (!this.graph) { 47 | return; 48 | } 49 | if (!this._onPropertyChanged) { 50 | this._onPropertyChanged = function (evt) { 51 | let kind = evt.kind; 52 | if (kind == 'element.bounds') { 53 | this._invalidateGraph(); 54 | return; 55 | } 56 | if (kind == 'transform' || kind == 'viewport') { 57 | this.invalidate(); 58 | } 59 | }.bind(this) 60 | this._onDataChanged = function (evt) { 61 | this._invalidateGraph(); 62 | }.bind(this) 63 | } 64 | this.graph.propertyChangeDispatcher.addListener(this._onPropertyChanged); 65 | this.graph.dataPropertyChangeDispatcher.addListener(this._onDataChanged); 66 | this.graph.listChangeDispatcher.addListener(this._onDataChanged); 67 | this._invalidateGraph(true); 68 | } 69 | 70 | _uninstall() { 71 | if (!this.graph || !this._onPropertyChanged) { 72 | return; 73 | } 74 | this.graph.propertyChangeDispatcher.removeListener(this._onPropertyChanged); 75 | this.graph.dataPropertyChangeDispatcher.removeListener(this._onDataChanged); 76 | this.graph.listChangeDispatcher.removeListener(this._onDataChanged); 77 | this.imageInfo = null; 78 | this.bounds = null; 79 | this.scale = null; 80 | } 81 | 82 | _toCanvas(x, y) { 83 | x = this.scale * (x - this.bounds.x); 84 | y = this.scale * (y - this.bounds.y); 85 | return [x, y] 86 | } 87 | 88 | _toGraph(evt) { 89 | let xy = globalToLocal(evt, this.html); 90 | let x = xy[0] / this.scale + this.bounds.x; 91 | let y = xy[1] / this.scale + this.bounds.y; 92 | return [x, y] 93 | } 94 | 95 | _validateGraph() { 96 | this._invalidateGraphFlag = false; 97 | if (!this.visible) { 98 | return; 99 | } 100 | let width = this.html.clientWidth, height = this.html.clientHeight; 101 | if (!width || !height) { 102 | return; 103 | } 104 | let bounds = new Q.Rect(); 105 | bounds.add(this.graph.bounds); 106 | let imageScale = Math.min(width / bounds.width, height / bounds.height) * this.canvas.ratio; 107 | 108 | this.imageInfo = this.exportGraphImage(imageScale, bounds); 109 | 110 | this.imageInfo.scale = imageScale; 111 | this.imageInfo.bounds = bounds; 112 | 113 | this.invalidate(); 114 | } 115 | 116 | exportGraphImage(scale, bounds) { 117 | return this.graph.exportImage(scale, bounds); 118 | } 119 | 120 | _invalidateGraph(force) { 121 | if (!this.graph || (!force && this._invalidateGraphFlag)) { 122 | return; 123 | } 124 | this._invalidateGraphFlag = true; 125 | this.graph.callLater(this._validateGraph, this, force ? 100 : 1000); 126 | } 127 | 128 | invalidate(force) { 129 | if (!force && this._invalidateFlag) { 130 | return; 131 | } 132 | this._invalidateFlag = true; 133 | setTimeout(this.validate.bind(this)); 134 | } 135 | 136 | validate() { 137 | this._invalidateFlag = false; 138 | let imageInfo = this.imageInfo; 139 | if (!imageInfo) { 140 | return; 141 | } 142 | let viewportBounds = this.graph.viewportBounds; 143 | if (!viewportBounds.height || !viewportBounds.width) { 144 | return; 145 | } 146 | 147 | let canvas = this.canvas; 148 | let ratio = canvas.ratio; 149 | let g = canvas.getContext('2d'); 150 | let width = this.html.clientWidth, height = this.html.clientHeight; 151 | canvas.style.width = width + 'px'; 152 | canvas.style.height = height + 'px'; 153 | canvas.width = width * ratio; 154 | canvas.height = height * ratio; 155 | g.scale(ratio, ratio); 156 | 157 | let bounds = new Q.Rect(imageInfo.bounds); 158 | bounds.add(viewportBounds); 159 | let scale = Math.min(width / bounds.width, height / bounds.height); 160 | this.scale = scale; 161 | 162 | let offsetX = (width / scale - bounds.width) / 2; 163 | let offsetY = (height / scale - bounds.height) / 2; 164 | bounds.x -= offsetX; 165 | bounds.y -= offsetY; 166 | bounds.width = width / scale; 167 | bounds.height = height / scale; 168 | this.bounds = bounds; 169 | 170 | g.save(); 171 | let xy = this._toCanvas(imageInfo.bounds.x, imageInfo.bounds.y); 172 | g.translate(xy[0], xy[1]); 173 | g.scale(scale / imageInfo.scale, scale / imageInfo.scale); 174 | g.drawImage(this.imageInfo.canvas, 0, 0); 175 | g.restore(); 176 | 177 | g.beginPath(); 178 | g.moveTo(0, 0); 179 | g.lineTo(canvas.width, 0); 180 | g.lineTo(canvas.width, canvas.height); 181 | g.lineTo(0, canvas.height); 182 | g.lineTo(0, 0); 183 | 184 | xy = this._toCanvas(viewportBounds.x, viewportBounds.y); 185 | let x = xy[0]; 186 | let y = xy[1]; 187 | width = viewportBounds.width * scale; 188 | height = viewportBounds.height * scale; 189 | 190 | 191 | g.moveTo(x, y); 192 | g.lineTo(x, y + height); 193 | g.lineTo(x + width, y + height); 194 | g.lineTo(x + width, y); 195 | g.closePath(); 196 | g.fillStyle = "rgba(30, 30, 30, 0.3)"; 197 | g.fill(); 198 | g.lineWidth = 0.5; 199 | g.strokeStyle = '#333'; 200 | g.strokeRect(x, y, width, height) 201 | } 202 | 203 | accept() { 204 | return this.graph != null; 205 | } 206 | 207 | startdrag(evt) { 208 | this.enddrag(); 209 | if (!this.scale) { 210 | return; 211 | } 212 | let xy = this._toGraph(evt); 213 | let viewport = this.graph.viewportBounds; 214 | if (viewport.contains(xy[0], xy[1])) { 215 | this._dragInfo = { 216 | scale: this.scale / this.graph.scale, 217 | point: xy 218 | }; 219 | this.graph.stopAnimation() 220 | } 221 | } 222 | 223 | ondrag(evt) { 224 | if (!this._dragInfo) { 225 | return; 226 | } 227 | let scale = this._dragInfo.scale; 228 | let dx = evt.dx; 229 | let dy = evt.dy; 230 | dx /= scale; 231 | dy /= scale; 232 | this.graph.translate(-dx, -dy, false) 233 | } 234 | 235 | enddrag() { 236 | this._dragInfo = null; 237 | } 238 | 239 | onstart(evt) { 240 | Q.stopEvent(evt); 241 | let xy = this._toGraph(evt); 242 | this.graph.centerTo(xy[0], xy[1]) 243 | } 244 | 245 | onmousewheel(evt) { 246 | Q.stopEvent(evt); 247 | let xy = this._toGraph(evt); 248 | xy = this.graph.toCanvas(xy[0], xy[1]); 249 | this.graph.zoomAt(Math.pow(this.graph.scaleStep, evt.delta), xy.x, xy.y); 250 | } 251 | 252 | ondblclick(evt) { 253 | let graph = this.graph; 254 | if (graph.enableDoubleClickToOverview) { 255 | let resetScale = graph.resetScale || 1; 256 | if (Math.abs(graph.scale - resetScale) < 0.001) { 257 | graph.zoomToOverview(); 258 | } else { 259 | graph.moveToCenter(resetScale) 260 | } 261 | } 262 | } 263 | 264 | setVisible(visible) { 265 | this.visible = visible; 266 | if (visible) { 267 | this.html.style.display = 'block'; 268 | } else { 269 | this.html.style.display = 'none'; 270 | } 271 | this._invalidateGraph(); 272 | } 273 | } -------------------------------------------------------------------------------- /src/common/color-picker/ColorPicker.js: -------------------------------------------------------------------------------- 1 | import {hidePopup, showPopup} from "../popup/Popup.js"; 2 | 3 | let color_input_template = ` 4 | 5 |
6 |
7 |
8 | ` 9 | 10 | let color_picker_template = ` 11 |
12 |
13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 | 21 |
22 | 23 | 100% 24 |
25 |
` 26 | 27 | 28 | let is = { 29 | hex: a => /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(a), 30 | rgb: a => /^rgb/.test(a), 31 | } 32 | 33 | 34 | function hexToRgb(hexValue) { 35 | let rgx = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 36 | let hex = hexValue.replace(rgx, (m, r, g, b) => r + r + g + g + b + b); 37 | let rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 38 | let r = parseInt(rgb[1], 16) || 0; 39 | let g = parseInt(rgb[2], 16) || 0; 40 | let b = parseInt(rgb[3], 16) || 0; 41 | return [r, g, b, 1]; 42 | } 43 | 44 | function colorToRGBA(color) { 45 | if (is.hex(color)) { 46 | return hexToRgb(color); 47 | } 48 | if (is.rgb(color)) { 49 | let start = color.indexOf("("); 50 | let end = color.indexOf(")"); 51 | if (start < 0 || end < start) { 52 | return; 53 | } 54 | color = color.substring(start + 1, end); 55 | color = color.split(","); 56 | if (color.length < 3) { 57 | return; 58 | } 59 | let r = parseInt(color[0]) || 0; 60 | let g = parseInt(color[1]) || 0; 61 | let b = parseInt(color[2]) || 0; 62 | if (color.length == 3) { 63 | return [r, g, b, 1] 64 | } 65 | return [r, g, b, parseFloat(color[3])]; 66 | } 67 | } 68 | 69 | function createElementByTemplate(html) { 70 | let template = document.createElement('template'); 71 | if (!template || !template.content) { 72 | template = document.createElement('div'); 73 | template.innerHTML = html.trim(); 74 | let node = template.childNodes[0]; 75 | template.removeChild(node); 76 | return node; 77 | } 78 | template.innerHTML = html.trim(); 79 | return template.content.firstChild; 80 | } 81 | 82 | 83 | function isDescendant(parent, child) { 84 | while (child) { 85 | if (child === parent) { 86 | return true; 87 | } 88 | child = child.parentNode; 89 | } 90 | return false; 91 | } 92 | 93 | export function createColorInput({value = '#FFF', onchange, dom}) { 94 | dom = dom || document.createElement('div'); 95 | dom.classList.add('q-color-input'); 96 | dom.innerHTML = color_input_template.trim(); 97 | let input = dom.getElementsByTagName('input')[0]; 98 | let icon = dom.getElementsByClassName('q-color__block')[0]; 99 | input.value = icon.style.backgroundColor = value; 100 | input.oninput = function () { 101 | colorPicker.setValue(input.value); 102 | icon.style.backgroundColor = colorPicker.value; 103 | onchange && onchange(input.value); 104 | } 105 | let colorPicker = new ColorPicker(); 106 | colorPicker.input = input; 107 | colorPicker.setValue(value); 108 | colorPicker.onchange = function (color) { 109 | icon.style.backgroundColor = input.value = color; 110 | // input.focus(); 111 | onchange && onchange(color); 112 | } 113 | 114 | input.onfocus = function () { 115 | let rect = dom.getBoundingClientRect(); 116 | colorPicker.show(window.pageXOffset + rect.left, window.pageYOffset + rect.top + rect.height + 5); 117 | } 118 | input.onblur = function (){ 119 | if(!colorPicker.dom){ 120 | return 121 | } 122 | setTimeout(function(){ 123 | if(document.activeElement !== document.body && !isDescendant(colorPicker.dom, document.activeElement)){ 124 | colorPicker.hide(); 125 | } 126 | }) 127 | } 128 | return { 129 | dom, 130 | input 131 | }; 132 | } 133 | 134 | function rgbToColor(rgb, alpha) { 135 | return alpha === 1 ? 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')' 136 | : 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + alpha.toFixed(2) + ')'; 137 | } 138 | 139 | class ColorPicker { 140 | 141 | constructor() { 142 | this.value = '#FFFFFF'; 143 | } 144 | _init() { 145 | let dom = this.dom = createElementByTemplate(color_picker_template); 146 | 147 | let color_spectrum = dom.getElementsByClassName('q-color-spectrum')[0]; 148 | let color_text = dom.getElementsByClassName('q-color__text')[0]; 149 | let color_icon = dom.getElementsByClassName('q-color__block')[0]; 150 | let alpha_slider = dom.getElementsByClassName('q-alpha__slider')[0]; 151 | let alpha_text = dom.getElementsByClassName('q-alpha__text')[0]; 152 | 153 | let updateValue = function (silent) { 154 | let value = _rgb ? rgbToColor(_rgb, _alpha) : null; 155 | color_icon.style.backgroundColor = color_text.innerHTML = value; 156 | if(value != this.value){ 157 | this.value = value; 158 | !silent && this.onchange && this.onchange(value); 159 | } 160 | }.bind(this) 161 | 162 | let _rgb, _alpha = 1; 163 | 164 | setValue(this.value); 165 | 166 | function setValue(value) { 167 | _rgb = colorToRGBA(value); 168 | if(_rgb){ 169 | _alpha = _rgb[3]; 170 | alpha_slider.value = _alpha * 100; 171 | alpha_text.innerHTML = alpha_slider.value + '%'; 172 | } 173 | updateValue(true); 174 | } 175 | 176 | this._setValue = setValue; 177 | 178 | //slider在ie下不支持oninput 179 | alpha_slider.oninput = alpha_slider.onchange = function () { 180 | alpha_text.innerHTML = alpha_slider.value + '%'; 181 | _alpha = parseFloat(alpha_slider.value) / 100; 182 | updateValue(); 183 | }; 184 | 185 | appendColorSpectrum(color_spectrum, function (rgb) { 186 | _rgb = rgb; 187 | updateValue(); 188 | this.input && this.input.focus(); 189 | }.bind(this)) 190 | } 191 | 192 | show(x, y) { 193 | if (!this.dom) { 194 | this._init(); 195 | } 196 | showPopup(this.dom, x, y, this.input); 197 | // document.body.appendChild(this.dom); 198 | // this.dom.style.left = x + 'px'; 199 | // this.dom.style.top = y + 'px'; 200 | } 201 | 202 | hide(){ 203 | this.dom && hidePopup(this.dom); 204 | // this.dom && this.dom.parentNode && this.dom.parentNode.removeChild(this.dom); 205 | } 206 | 207 | setValue(value) { 208 | this.value = value; 209 | this._setValue && this._setValue(value); 210 | } 211 | } 212 | 213 | function fillLinearGradient(g, width, height, colors, isH, positions) { 214 | let gradient = isH ? g.createLinearGradient(0, 0, width, 0) : g.createLinearGradient(0, 0, 0, height); 215 | let step = 1 / (colors.length - 1); 216 | colors.forEach(function (color, index) { 217 | if (positions) { 218 | gradient.addColorStop(positions[index], color); 219 | } else { 220 | gradient.addColorStop(index * step, color); 221 | } 222 | }) 223 | g.fillStyle = gradient; 224 | g.fillRect(0, 0, width, height); 225 | } 226 | 227 | function appendColorSpectrum(container, onColorChoose) { 228 | let canvas = document.createElement('canvas'); 229 | container.appendChild(canvas); 230 | 231 | let imageData; 232 | 233 | function toLocal(evt, div) { 234 | let clientRect = div.getBoundingClientRect(); 235 | return {x: evt.clientX - Math.round(clientRect.left), y: evt.clientY - Math.round(clientRect.top)} 236 | } 237 | 238 | function init() { 239 | if(!container.offsetWidth){ 240 | return; 241 | } 242 | let ratio = devicePixelRatio || 1; 243 | let canvasWidth = container.offsetWidth, canvasHeight = container.offsetHeight; 244 | canvas.width = canvasWidth * ratio; 245 | canvas.height = canvasHeight * ratio; 246 | let g = canvas.getContext('2d'); 247 | let hPadding = 5, vPadding = 2;//给两侧留出一定宽度,方便选取黑白颜色 248 | g.save(); 249 | g.scale(ratio, ratio); 250 | g.fillStyle = '#F00'; 251 | g.fillRect(0, 0, canvas.width, canvas.height); 252 | let gradientWidth = canvasWidth - hPadding * 2; 253 | g.translate(hPadding, vPadding); 254 | fillLinearGradient(g, gradientWidth, canvasHeight - vPadding * 2, ['#F00', '#FF0', '#0F0', '#0FF', '#00F', '#F0F', '#F00'], false); 255 | g.translate(0, -vPadding); 256 | fillLinearGradient(g, gradientWidth, canvasHeight, ['#FFF', 'rgba(255,255,255, 0)', 'rgba(0,0,0,0)', '#000'], true, [0, 0.5, 0.5, 1]); 257 | g.translate(-hPadding, 0); 258 | g.fillStyle = '#FFF'; 259 | g.fillRect(0, 0, hPadding, canvasHeight); 260 | g.fillStyle = '#000'; 261 | g.fillRect(canvasWidth - hPadding, 0, hPadding, canvasHeight); 262 | 263 | g.restore(); 264 | 265 | imageData = g.getImageData(0, 0, canvas.width, canvas.height).data; 266 | 267 | canvas.onmousedown = canvas.onmousemove = function (evt) { 268 | if (!evt.buttons) { 269 | return 270 | } 271 | evt.preventDefault(); 272 | evt.stopPropagation(); 273 | let xy = toLocal(evt, canvas); 274 | let color = getRGB(xy.x * ratio, xy.y * ratio); 275 | onColorChoose && onColorChoose(color); 276 | }.bind(this) 277 | } 278 | 279 | container.offsetWidth ? init() : setTimeout(init.bind(this)); 280 | 281 | function getRGB(x, y) { 282 | if (x < 0) { 283 | x = 0; 284 | } else if (x > canvas.width) { 285 | x = canvas.width; 286 | } 287 | if (y < 0) { 288 | y = 0; 289 | } else if (y > canvas.height - 1) { 290 | y = canvas.height - 1; 291 | } 292 | let index = (y * canvas.width + x) * 4; 293 | 294 | return [imageData[index++], imageData[index++], imageData[index++]]; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/common/popup/PopupMenu.js: -------------------------------------------------------------------------------- 1 | import Q from '../../lib/qunee-es.js'; 2 | import {getI18NString} from "../i18n.js"; 3 | import {hidePopup, showPopup} from "./Popup.js"; 4 | 5 | function setZIndexWithChildren(data, zIndex) { 6 | data.zIndex = zIndex; 7 | data.forEachChild(function (c) { 8 | setZIndexWithChildren(c, zIndex) 9 | }) 10 | } 11 | 12 | let menuClassName = 'dropdown-menu'; 13 | 14 | const Separator = 'divider'; 15 | 16 | export class PopupMenu { 17 | constructor(items) { 18 | this._invalidateFlag = true; 19 | 20 | 21 | this.items = items || []; 22 | } 23 | 24 | add(item) { 25 | this.items.push(item); 26 | this._invalidateFlag = true; 27 | } 28 | 29 | addSeparator() { 30 | this.add(Separator); 31 | } 32 | 33 | showAt(x, y) { 34 | if (!this.items || !this.items.length) { 35 | return false; 36 | } 37 | if (this._invalidateFlag) { 38 | this.render(); 39 | } 40 | showPopup(this.dom, x, y); 41 | // this.dom.style.display = "block"; 42 | // document.body.appendChild(this.dom); 43 | // showDivAt(this.dom, x, y); 44 | } 45 | 46 | hide() { 47 | this.dom && hidePopup(this.dom); 48 | // if (this.dom && this.dom.parentNode) { 49 | // this.dom.parentNode.removeChild(this.dom); 50 | // } 51 | } 52 | 53 | isShowing() { 54 | return this.dom.parentNode !== null; 55 | } 56 | 57 | render() { 58 | this._invalidateFlag = false; 59 | if (!this.dom) { 60 | this.dom = document.createElement('ul'); 61 | this.dom.setAttribute("role", "menu"); 62 | this.dom.className = menuClassName; 63 | this.dom.addEventListener(Q.isTouchSupport ? "touchstart" : "mousedown", function (evt) { 64 | Q.stopEvent(evt); 65 | }, false); 66 | } else { 67 | this.dom.innerHTML = ""; 68 | } 69 | for (let i = 0, l = this.items.length; i < l; i++) { 70 | let item = this.renderItem(this.items[i]); 71 | this.dom.appendChild(item); 72 | } 73 | } 74 | 75 | html2Escape(sHtml) { 76 | return sHtml.replace(/[<>&"]/g, function (c) { 77 | return {'<': '<', '>': '>', '&': '&', '"': '"'}[c]; 78 | }); 79 | } 80 | 81 | renderItem(menuItem, zIndex) { 82 | let dom = document.createElement('li'); 83 | dom.setAttribute("role", "presentation"); 84 | if (menuItem == Separator) { 85 | dom.className = Separator; 86 | dom.innerHTML = " "; 87 | return dom; 88 | } 89 | if (Q.isString(menuItem)) { 90 | dom.innerHTML = '' + this.html2Escape(menuItem) + ''; 91 | return dom; 92 | } 93 | if (menuItem.selected) { 94 | dom.style.backgroundPosition = '3px 5px'; 95 | dom.style.backgroundRepeat = 'no-repeat'; 96 | dom.style.backgroundImage = "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAPklEQVQ4y2P4//8/AyWYYdQA7AYAAZuamlo7ED+H4naQGNEGQDX/R8PtpBjwHIsBz+lqAGVeoDgQR1MiaRgAnxW7Q0QEK0cAAAAASUVORK5CYII=')"; 97 | } 98 | let a = document.createElement("a"); 99 | a.setAttribute("role", "menuitem"); 100 | a.setAttribute("tabindex", "-1"); 101 | a.setAttribute("href", "javascript:void(0)"); 102 | dom.appendChild(a); 103 | 104 | if (menuItem.html) { 105 | a.innerHTML = menuItem.html; 106 | } else { 107 | let text = menuItem.text || menuItem.name; 108 | if (text) { 109 | a.innerHTML = this.html2Escape(text); 110 | } 111 | } 112 | let className = menuItem.className; 113 | if (className) { 114 | dom.className = className; 115 | } 116 | let call = menuItem.action; 117 | let self = this; 118 | 119 | let onclick = function (evt) { 120 | if (call) { 121 | call.call(menuItem.scope, evt, menuItem); 122 | } 123 | if (!Q.isIOS) { 124 | evt.target.focus(); 125 | } 126 | setTimeout(function () { 127 | self.hide(); 128 | }, 100); 129 | } 130 | if (Q.isTouchSupport) { 131 | // dom.ontouchstart = onclick; 132 | a.ontouchstart = onclick; 133 | } else { 134 | dom.onclick = onclick; 135 | } 136 | return dom; 137 | } 138 | 139 | getMenuItems(graph, data, evt) { 140 | let items = []; 141 | //items.push({ 142 | // text: '添加主机', 143 | // action(evt, item){ 144 | // alert('添加主机');//这里实现弹出页面 145 | // } 146 | //}) 147 | function getSelectedNodes(graph) { 148 | let nodes = []; 149 | graph.selectionModel.forEach(function (e) { 150 | e instanceof Q.Node && nodes.push(e); 151 | }) 152 | return nodes; 153 | } 154 | 155 | 156 | /** 157 | * 左对齐 158 | */ 159 | function alignLeft(graph, nodeUIs) { 160 | let left; 161 | nodeUIs.forEach(function (e) { 162 | if (left === undefined) { 163 | left = e.x; 164 | } else { 165 | left = Math.min(left, e.x); 166 | } 167 | }); 168 | nodeUIs.forEach(function (node) { 169 | node.x = left; 170 | }); 171 | } 172 | 173 | if (data) { 174 | let nodes = getSelectedNodes(graph); 175 | if (nodes.length >= 2) { 176 | items.push({ 177 | text: 'Align Left', action() { 178 | alignLeft(graph, nodes) 179 | } 180 | }) 181 | } 182 | let isShapeNode = data instanceof Q.ShapeNode; 183 | let isGroup = data instanceof Q.Group; 184 | let isNode = !isShapeNode && data instanceof Q.Node; 185 | let isEdge = data instanceof Q.Edge; 186 | 187 | items.push({ 188 | text: getI18NString('Rename'), action(evt, item) { 189 | Q.prompt(getI18NString('Input Element Name'), data.name || '', function (name) { 190 | if (name === null) { 191 | return; 192 | } 193 | data.name = name; 194 | }) 195 | } 196 | }); 197 | if (isEdge) { 198 | let isDashLine = data.getStyle(Q.Styles.EDGE_LINE_DASH) || Q.DefaultStyles[Q.Styles.EDGE_LINE_DASH]; 199 | items.push({ 200 | text: isDashLine ? getI18NString('Solid Line') : getI18NString('Dashed Line'), 201 | action(evt, item) { 202 | data.setStyle(Q.Styles.EDGE_LINE_DASH, isDashLine ? null : [5, 3]); 203 | } 204 | }); 205 | items.push({ 206 | text: getI18NString('Line Width'), action(evt, item) { 207 | Q.prompt(getI18NString('Input Line Width'), data.getStyle(Q.Styles.EDGE_WIDTH) || Q.DefaultStyles[Q.Styles.EDGE_WIDTH], function (lineWidth) { 208 | if (lineWidth === null) { 209 | return; 210 | } 211 | lineWidth = parseFloat(lineWidth); 212 | data.setStyle(Q.Styles.EDGE_WIDTH, lineWidth); 213 | }) 214 | } 215 | }); 216 | items.push({ 217 | text: getI18NString('Line Color'), action(evt, item) { 218 | Q.prompt(getI18NString('Input Line Color'), data.getStyle(Q.Styles.EDGE_COLOR) || Q.DefaultStyles[Q.Styles.EDGE_COLOR], function (color) { 219 | if (color === null) { 220 | return; 221 | } 222 | data.setStyle(Q.Styles.EDGE_COLOR, color); 223 | }) 224 | } 225 | }); 226 | } else if (data.parent instanceof Q.Group) { 227 | items.push({ 228 | text: getI18NString('Out of Group'), action() { 229 | data.parent = null; 230 | } 231 | }) 232 | } 233 | items.push(Separator); 234 | 235 | items.push({ 236 | text: getI18NString('Send to Top'), action(evt, item) { 237 | // data.zIndex = 1; 238 | setZIndexWithChildren(data, 1); 239 | graph.sendToTop(data); 240 | graph.invalidate(); 241 | } 242 | }); 243 | items.push({ 244 | text: getI18NString('Send to Bottom'), action(evt, item) { 245 | // data.zIndex = -1; 246 | setZIndexWithChildren(data, -1); 247 | graph.sendToBottom(data); 248 | graph.invalidate(); 249 | } 250 | }); 251 | items.push({ 252 | text: getI18NString('Reset Layer'), action(evt, item) { 253 | // data.zIndex = 0; 254 | setZIndexWithChildren(data, 0); 255 | graph.invalidate(); 256 | } 257 | }); 258 | items.push(Separator); 259 | } 260 | items.push({ 261 | text: getI18NString('Clear Graph'), action() { 262 | graph.clear(); 263 | } 264 | }) 265 | items.push(Separator); 266 | 267 | items.push({ 268 | text: getI18NString('Zoom In'), action(evt, item) { 269 | let localXY = graph.globalToLocal(evt); 270 | graph.zoomIn(localXY.x, localXY.y, true); 271 | } 272 | }); 273 | items.push({ 274 | text: getI18NString('Zoom Out'), action(evt, item) { 275 | let localXY = graph.globalToLocal(evt); 276 | graph.zoomOut(localXY.x, localXY.y, true); 277 | } 278 | }); 279 | items.push({ 280 | text: getI18NString('1:1'), action(evt, item) { 281 | let localXY = graph.globalToLocal(evt); 282 | graph.scale = 1; 283 | } 284 | }); 285 | items.push(Separator); 286 | let currentMode = graph.interactionMode; 287 | let interactons = [ 288 | {text: getI18NString('Pan Mode'), value: Q.Consts.INTERACTION_MODE_DEFAULT}, 289 | {text: getI18NString('Rectangle Select'), value: Q.Consts.INTERACTION_MODE_SELECTION} 290 | ]; 291 | for (let i = 0, l = interactons.length; i < l; i++) { 292 | let mode = interactons[i]; 293 | if (mode.value == currentMode) { 294 | mode.selected = true; 295 | } 296 | mode.action = function (evt, item) { 297 | graph.interactionMode = item.value; 298 | }; 299 | items.push(mode) 300 | } 301 | items.push(Separator); 302 | items.push({html: 'Qunee' + ' - ' + Q.version + ''}); 303 | return items; 304 | } 305 | 306 | get items() { 307 | return this._items; 308 | } 309 | 310 | set items(v) { 311 | this._items = v; 312 | this._invalidateFlag = true; 313 | } 314 | } 315 | 316 | let _contextmenuListener = { 317 | onstart(evt, graph) { 318 | graph._popupmenu.hide(); 319 | } 320 | } 321 | 322 | function getPageXY(evt) { 323 | if (evt.touches && evt.touches.length) { 324 | evt = evt.touches[0]; 325 | } 326 | return {x: evt.pageX, y: evt.pageY}; 327 | } 328 | 329 | function showMenu(evt, graph) { 330 | let menu = graph.popupmenu; 331 | let xy = getPageXY(evt); 332 | let x = xy.x, y = xy.y; 333 | 334 | let items = menu.getMenuItems(graph, graph.getElement(evt), evt); 335 | 336 | if (!items) { 337 | return; 338 | } 339 | menu.items = items; 340 | menu.showAt(x, y); 341 | 342 | Q.stopEvent(evt); 343 | } 344 | 345 | if (Q.isTouchSupport) { 346 | _contextmenuListener.onlongpress = function (evt, graph) { 347 | showMenu(evt, graph); 348 | } 349 | } 350 | 351 | Object.defineProperties(Q.Graph.prototype, { 352 | popupmenu: { 353 | get() { 354 | return this._popupmenu; 355 | }, 356 | set(v) { 357 | if (this._popupmenu == v) { 358 | return; 359 | } 360 | this._popupmenu = v; 361 | 362 | if (!this._contextmenuListener) { 363 | this._contextmenuListener = _contextmenuListener; 364 | this.addCustomInteraction(this._contextmenuListener); 365 | this.html.oncontextmenu = function (evt) { 366 | if (!this.popupmenu) { 367 | return; 368 | } 369 | showMenu(evt, this); 370 | }.bind(this); 371 | } 372 | } 373 | } 374 | }); -------------------------------------------------------------------------------- /src/common/toolbox/ToolBox.js: -------------------------------------------------------------------------------- 1 | import Q from '../../lib/qunee-es.js'; 2 | import {appendDNDInfo} from "../toolbox/DragSupport.js"; 3 | import {getI18NString} from "../i18n.js"; 4 | 5 | let createElement = function (className, parent, tag, html) { 6 | let element = document.createElement(tag || 'div'); 7 | element.className = className; 8 | html && (element.innerHTML = html); 9 | if (parent) { 10 | parent.appendChild(element); 11 | } 12 | return element; 13 | } 14 | 15 | let forEach = function (object, call, scope) { 16 | if (Array.isArray(object)) { 17 | return object.forEach(function (v) { 18 | call.call(this, v); 19 | }, scope); 20 | } 21 | for (let name in object) { 22 | call.call(scope, object[name], name); 23 | } 24 | } 25 | let defaultImageStyles = { 26 | fillColor: '#EEE', 27 | lineWidth: 1, 28 | strokeStyle: '#2898E0', 29 | padding: {left: 1, top: 1, right: 5, bottom: 5}, 30 | shadowColor: '#888', 31 | shadowOffsetX: 2, 32 | shadowOffsetY: 2, 33 | shadowBlur: 3 34 | } 35 | let nodeImageStyles = {}; 36 | nodeImageStyles[Q.Styles.RENDER_COLOR] = 'renderColor'; 37 | nodeImageStyles[Q.Styles.RENDER_COLOR_BLEND_MODE] = 'renderColorBlendMode'; 38 | nodeImageStyles[Q.Styles.SHAPE_FILL_COLOR] = 'fillColor'; 39 | nodeImageStyles[Q.Styles.SHAPE_STROKE_STYLE] = 'strokeStyle'; 40 | nodeImageStyles[Q.Styles.SHAPE_LINE_DASH] = 'borderLineDash'; 41 | nodeImageStyles[Q.Styles.SHAPE_LINE_DASH_OFFSET] = 'borderLineDashOffset'; 42 | //nodeImageStyles[Q.Styles.SHAPE_FILL_GRADIENT] = 'fillGradient'; 43 | nodeImageStyles[Q.Styles.SHAPE_OUTLINE] = 'outline'; 44 | nodeImageStyles[Q.Styles.SHAPE_OUTLINE_STYLE] = 'outlineStyle'; 45 | nodeImageStyles[Q.Styles.LINE_CAP] = 'lineGap'; 46 | nodeImageStyles[Q.Styles.LINE_JOIN] = 'lineJoin'; 47 | nodeImageStyles[Q.Styles.BACKGROUND_COLOR] = 'backgroundColor'; 48 | nodeImageStyles[Q.Styles.BACKGROUND_GRADIENT] = 'backgroundGradient'; 49 | nodeImageStyles[Q.Styles.BORDER] = 'border'; 50 | nodeImageStyles[Q.Styles.BORDER_COLOR] = 'borderColor'; 51 | nodeImageStyles[Q.Styles.BORDER_LINE_DASH] = 'borderLineDash'; 52 | nodeImageStyles[Q.Styles.BORDER_LINE_DASH_OFFSET] = 'borderLineDashOffset'; 53 | //Styles.IMAGE_BACKGROUND_COLOR = "image.background.color"; 54 | //Styles.IMAGE_BACKGROUND_GRADIENT = "image.background.gradient"; 55 | //Styles.IMAGE_BORDER = "image.border.width"; 56 | //Styles.IMAGE_BORDER_STYLE = Styles.IMAGE_BORDER_COLOR = "image.border.style"; 57 | //Styles.IMAGE_BORDER_LINE_DASH = "image.border.line.dash"; 58 | //Styles.IMAGE_BORDER_LINE_DASH_OFFSET = "image.border.line.dash.offset"; 59 | //Styles.IMAGE_RADIUS = Styles.IMAGE_BORDER_RADIUS = "image.radius"; 60 | //Styles.IMAGE_PADDING = "image.padding"; 61 | 62 | 63 | function mixStyles(styles) { 64 | if (!styles) { 65 | return defaultImageStyles; 66 | } 67 | let result = {}; 68 | for (let name in defaultImageStyles) { 69 | result[name] = defaultImageStyles[name]; 70 | } 71 | for (let name in styles) { 72 | let propertyName = nodeImageStyles[name]; 73 | if (propertyName) { 74 | result[propertyName] = styles[name]; 75 | } 76 | } 77 | return result; 78 | } 79 | 80 | let onGroupTitleClick = function (evt) { 81 | let parent = evt.target.parentNode; 82 | while (parent && !parent.classList.contains('q-group')) { 83 | parent = parent.parentNode; 84 | } 85 | closeGroup(parent); 86 | } 87 | 88 | function closeGroup(parent, open) { 89 | if (!parent) { 90 | return; 91 | } 92 | if (open === undefined) { 93 | open = parent.classList.contains('q-group--closed'); 94 | } 95 | if (open) { 96 | parent.classList.remove('q-group--closed'); 97 | } else { 98 | parent.classList.add('q-group--closed'); 99 | } 100 | } 101 | 102 | function isImage(image) { 103 | return typeof image === 'string' || image.draw instanceof Function; 104 | } 105 | 106 | export class ToolBox { 107 | constructor(html, groups) { 108 | this.imageWidth = 40; 109 | this.imageHeight = 40; 110 | this._groups = {}; 111 | this._index = 0; 112 | 113 | this.dom = html; 114 | this.init(groups); 115 | } 116 | 117 | 118 | loadImageBox(json, insert) { 119 | if (typeof json === 'string') { 120 | json = JSON.parse(json); 121 | } 122 | if (Array.isArray(json)) { 123 | return json.forEach(function(item){ 124 | this.loadImageBox(item, insert); 125 | }.bind(this)); 126 | } 127 | if (insert) { 128 | let firstGroup = this.dom.getElementsByClassName('q-group').item(0); 129 | if (firstGroup) { 130 | this.dom.insertBefore(this._createGroup(json, json.prefix), firstGroup); 131 | return; 132 | } 133 | } 134 | return this.dom.appendChild(this._createGroup(json, json.prefix)); 135 | } 136 | 137 | //初始化拖拽节点列表 138 | init(groups) { 139 | let toolbox = this.dom; 140 | toolbox.classList.add('q-toolbox'); 141 | // if (isRequestFileSupported()) { 142 | // let buttonBar = createElement('q-toolbox__button-bar', toolbox); 143 | // buttonBar.appendChild(createButton({ 144 | // type: 'file', 145 | // name: getI18NString('Load Images...'), 146 | // iconClass: 'q-icon toolbar-add', 147 | // action(files) { 148 | // //加载图元库文件 149 | // if (!files[0]) { 150 | // return; 151 | // } 152 | // readTextFile(files[0], 'json', function (json) { 153 | // if (json) { 154 | // this.loadImageBox(json, true); 155 | // } 156 | // }.bind(this)); 157 | // } 158 | // }, this)); 159 | // } 160 | let basicNodes = [{ 161 | label: 'Node', 162 | image: 'Q-node' 163 | }, { 164 | type: 'Text', 165 | label: 'Text', 166 | html: '' + getI18NString('Text') + '', 167 | styles: { 168 | 'label.background.color': '#2898E0', 169 | 'label.color': '#FFF', 170 | 'label.padding': new Q.Insets(3, 5) 171 | } 172 | }, { 173 | type: 'Group', 174 | label: 'Group', 175 | image: 'Q-group', 176 | properties: { 177 | // groupType: Q.Consts.GROUP_TYPE_ELLIPSE, 178 | minSize: {width: 100, height: 100, x: 0, y: 0} 179 | } 180 | }, { 181 | label: 'SubNetwork', 182 | image: 'Q-subnetwork', 183 | properties: {enableSubNetwork: true} 184 | }]; 185 | 186 | let innerGroups = [{prefix: 'Q-', name: 'basic.nodes', displayName: getI18NString('Basic Nodes'), images: basicNodes}, { 187 | prefix: 'Q-', 188 | name: 'register.images', 189 | displayName: getI18NString('Register Images'), 190 | images: Q.getAllImages(), 191 | close: true 192 | }, { 193 | name: 'default.shapes', 194 | displayName: getI18NString('Default Shapes'), 195 | prefix: 'Q-', 196 | images: Q.Shapes.getAllShapes(this.imageWidth, this.imageHeight), 197 | close: true 198 | }]; 199 | this.loadImageBox(innerGroups); 200 | if (groups) { 201 | this.loadImageBox(groups); 202 | } 203 | Q.Shapes.getShape(Q.Consts.SHAPE_CIRCLE, 100, 100) 204 | } 205 | 206 | _getGroup(name) { 207 | return this._groups[name]; 208 | } 209 | 210 | hideDefaultGroups() { 211 | this.hideGroup('basic.nodes'); 212 | this.hideGroup('register.images'); 213 | this.hideGroup('default.shapes'); 214 | } 215 | 216 | hideGroup(name) { 217 | let group = this._getGroup(name); 218 | if (group) { 219 | group.style.display = 'none'; 220 | } 221 | } 222 | 223 | showGroup(name) { 224 | let group = this._getGroup(name); 225 | if (group) { 226 | group.style.display = ''; 227 | } 228 | } 229 | 230 | _createGroup(groupInfo) { 231 | let name = groupInfo.name; 232 | let root = groupInfo.root || ''; 233 | let images = groupInfo.images; 234 | let close = groupInfo.close; 235 | let displayName = groupInfo.displayName || name; 236 | 237 | let group = createElement('q-group'); 238 | group.id = name; 239 | this._groups[name] = group; 240 | 241 | let title = createElement('q-group__title', group); 242 | title.onclick = onGroupTitleClick; 243 | createElement(null, title, 'span', displayName); 244 | createElement('q-icon group-expand toolbar-expand', title, 'span'); 245 | let items = createElement('q-group__items', group); 246 | let clearDiv = document.createElement('div'); 247 | clearDiv.style.clear = 'both'; 248 | group.appendChild(clearDiv); 249 | if (close) { 250 | closeGroup(group); 251 | } 252 | 253 | if (!images) { 254 | return group; 255 | } 256 | 257 | //let images = [{ 258 | // type: '图元类型', 259 | // label: '图元文本', 260 | // image: '图元图片', 261 | // imageName: '图片名称', 262 | // styles: '图元样式', 263 | // properties: '图元属性', 264 | // clientProperties: '图元client属性', 265 | // html: '拖拽html内容' 266 | //}, 'a.png', {draw(g){}}]; 267 | //let group = { 268 | // name: '分组名称', 269 | // root: '根目录', 270 | // imageWidth: '', 271 | // imageHeight: '', 272 | // size: 'q-icon size', 273 | // images: images//'拖拽图片信息' 274 | //} 275 | 276 | let imageWidth = groupInfo.imageWidth || this.imageWidth; 277 | let imageHeight = groupInfo.imageHeight || this.imageHeight; 278 | let showLabel = groupInfo.showLabel; 279 | 280 | function fixImagePath(image, name, isIcon) { 281 | if (!image) { 282 | return image; 283 | } 284 | if (typeof image === 'string') { 285 | return root + image; 286 | } 287 | if (image.draw instanceof Function) { 288 | if (isIcon) { 289 | return image; 290 | } 291 | let imageName = image.imageName || image.name || name || 'drawable-' + this._index++; 292 | if (!Q.hasImage(imageName)) { 293 | Q.registerImage(imageName, image); 294 | } 295 | return imageName; 296 | } 297 | throw new Error('image format error'); 298 | } 299 | 300 | forEach(images, function (imageInfo, name) { 301 | if (name == '_classPath' || name == '_className') { 302 | return; 303 | } 304 | 305 | let image, icon; 306 | if (isImage(imageInfo)) { 307 | icon = image = fixImagePath(imageInfo, name); 308 | imageInfo = { 309 | image: image 310 | } 311 | } else { 312 | image = imageInfo.image = fixImagePath(imageInfo.image, name); 313 | icon = imageInfo.icon ? fixImagePath(imageInfo.icon, name, true) : image; 314 | } 315 | 316 | let imageDiv, tooltip; 317 | if (imageInfo.html) { 318 | imageDiv = document.createElement('div'); 319 | imageDiv.style.width = imageWidth + 'px'; 320 | imageDiv.style.height = imageHeight + 'px'; 321 | imageDiv.style.lineHeight = imageHeight + 'px'; 322 | imageDiv.style.overflow = 'hidden'; 323 | imageDiv.innerHTML = imageInfo.html; 324 | } else if (icon) { 325 | imageDiv = Q.createCanvas(imageWidth, imageHeight, true); 326 | Q.drawImage(icon, imageDiv, mixStyles(imageInfo.styles)); 327 | if (groupInfo.size) { 328 | if (!imageInfo.properties) { 329 | imageInfo.properties = {} 330 | } 331 | if (!imageInfo.properties.size) { 332 | imageInfo.properties.size = groupInfo.size; 333 | } 334 | } 335 | tooltip = image; 336 | } else { 337 | return; 338 | } 339 | tooltip = imageInfo.tooltip || imageInfo.label || tooltip || name; 340 | imageDiv.setAttribute('title', tooltip); 341 | let item = createElement('q-group__item', items); 342 | appendDNDInfo(imageDiv, imageInfo); 343 | item.appendChild(imageDiv); 344 | if (tooltip && (showLabel || imageInfo.showLabel)) { 345 | let sortName = tooltip; 346 | let sortLength = 10; 347 | if (sortName.length > sortLength) { 348 | sortName = '...' + sortName.substring(sortName.length - sortLength + 2) 349 | } 350 | let label = document.createElement('div'); 351 | label.style.lineHeight = '1em'; 352 | label.style.overFlow = 'hide' 353 | label.style.marginTop = '0px' 354 | label.textContent = sortName; 355 | item.appendChild(label); 356 | } 357 | if (imageInfo.br) { 358 | items.appendChild(document.createElement('br')); 359 | } 360 | 361 | }, this) 362 | return group; 363 | } 364 | } -------------------------------------------------------------------------------- /src/common/property/PropertyPane.js: -------------------------------------------------------------------------------- 1 | import {getI18NString} from "../i18n.js"; 2 | import Q from "../../lib/qunee-es.js"; 3 | import {createElement} from "../utils.js"; 4 | import {createCellEditor, stringToValue} from "./input.js"; 5 | 6 | let elementProperties = [{name: 'name', displayName: 'Name'}, { 7 | style: Q.Styles.LABEL_FONT_SIZE, 8 | type: 'number', 9 | displayName: 'Font Size' 10 | }, {style: Q.Styles.LABEL_COLOR, type: 'color', displayName: 'Label Color'}, { 11 | style: Q.Styles.RENDER_COLOR, 12 | type: 'color', 13 | displayName: 'Render Color' 14 | }, {style: Q.Styles.LABEL_POSITION, displayName: 'Label Position'}, { 15 | style: Q.Styles.LABEL_ANCHOR_POSITION, 16 | displayName: 'Label Anchor Position' 17 | }]; 18 | let nodeProperties = [{name: 'size', type: 'size', displayName: 'Size'}, { 19 | name: 'location', 20 | type: 'point', 21 | displayName: 'Location' 22 | }, {name: 'rotate', type: 'number', displayName: 'Rotate'}, { 23 | style: Q.Styles.BORDER, 24 | type: 'number', 25 | displayName: 'Border' 26 | }, { 27 | style: Q.Styles.BORDER_COLOR, 28 | type: 'color', 29 | displayName: 'Border Color' 30 | }]; 31 | let shapeProperties = [{ 32 | style: Q.Styles.SHAPE_FILL_COLOR, 33 | type: 'color', 34 | displayName: 'Fill Color' 35 | }, { 36 | style: Q.Styles.SHAPE_STROKE_STYLE, 37 | type: 'color', 38 | displayName: 'Stroke Color' 39 | }, { 40 | style: Q.Styles.SHAPE_STROKE, 41 | type: 'number', 42 | displayName: 'Stroke' 43 | }]; 44 | let edgeProperties = [{name: 'angle', type: 'degree', displayName: 'angle 0-360°'}, { 45 | style: Q.Styles.BORDER, 46 | display: 'none' 47 | }, { 48 | style: Q.Styles.EDGE_WIDTH, 49 | type: 'number', 50 | displayName: 'Edge Width' 51 | }, {style: Q.Styles.EDGE_COLOR, type: 'color', displayName: 'Edge Color'}, { 52 | style: Q.Styles.ARROW_TO, 53 | type: 'boolean', 54 | displayName: 'Arrow To' 55 | }]; 56 | let textProperties = [{name: 'size', display: 'none'}, { 57 | style: Q.Styles.LABEL_SIZE, 58 | type: 'size', 59 | displayName: 'Size' 60 | }, { 61 | style: Q.Styles.RENDER_COLOR, 62 | display: 'none' 63 | }, {style: Q.Styles.LABEL_BACKGROUND_COLOR, type: 'color', displayName: 'Background Color'}]; 64 | 65 | //let propertiesMap = { 66 | // 'Q.Element': { 67 | // class: Q.Element, 68 | // properties: { 69 | // name: {name: 'name'}, 70 | // 'S:edge.width': {name: 'edge.width', type: 'number', propertyType: 'style'}, 71 | // 'S:edge.color': {name: 'edge.color', type: 'color', propertyType: 'style'} 72 | // } 73 | // } 74 | //}; 75 | let DEFAULT_PROPERTY_MAP = {}; 76 | 77 | let classIndex = 0; 78 | 79 | function getClassName(clazz) { 80 | let name = clazz._classPath || clazz._tempName; 81 | if (!name) { 82 | name = clazz._tempName = 'class-' + classIndex++; 83 | } 84 | return name; 85 | } 86 | 87 | function getPropertiesByTypeFrom(clazz, create, propertyMap) { 88 | let name = getClassName(clazz); 89 | if (!create) { 90 | return propertyMap[name]; 91 | } 92 | return propertyMap[name] = {class: clazz, properties: {}}; 93 | } 94 | 95 | function getPropertyKey(name, propertyType) { 96 | if (propertyType == Q.Consts.PROPERTY_TYPE_STYLE) { 97 | return 'S:' + name; 98 | } 99 | if (propertyType == Q.Consts.PROPERTY_TYPE_CLIENT) { 100 | return 'C:' + name; 101 | } 102 | return name; 103 | } 104 | 105 | 106 | export function registerDefaultProperties(options) { 107 | registerProperties(DEFAULT_PROPERTY_MAP, options) 108 | } 109 | 110 | function registerProperties(propertyMap, options) { 111 | let clazz = options.class; 112 | if (!clazz) { 113 | throw new Error('class property can not be null'); 114 | } 115 | let properties = options.properties; 116 | 117 | let name = getClassName(clazz); 118 | if (!properties) { 119 | delete propertyMap[name]; 120 | return; 121 | } 122 | let property = getPropertiesByTypeFrom(clazz, true, propertyMap); 123 | 124 | if (name in propertyMap) { 125 | property = propertyMap[name]; 126 | } else { 127 | property = propertyMap[name] = {className: clazz, properties: {}}; 128 | } 129 | 130 | formatProperties(options, property.properties); 131 | } 132 | 133 | function formatProperties(options, result) { 134 | result = result || {}; 135 | let properties = options.properties; 136 | let groupName = options.group || 'Element' 137 | properties.forEach(function (item) { 138 | let key; 139 | if (item.style) { 140 | item.propertyType = Q.Consts.PROPERTY_TYPE_STYLE; 141 | item.name = item.style; 142 | } else if (item.client) { 143 | item.propertyType = Q.Consts.PROPERTY_TYPE_CLIENT; 144 | item.name = item.client; 145 | } else if (item.name) { 146 | item.propertyType = Q.Consts.PROPERTY_TYPE_ACCESSOR; 147 | } else { 148 | return; 149 | } 150 | key = item.key = getPropertyKey(item.name, item.propertyType); 151 | if (!item.groupName) { 152 | item.groupName = groupName; 153 | } 154 | result[key] = item; 155 | }) 156 | return result; 157 | } 158 | 159 | registerDefaultProperties({ 160 | class: Q.Element, 161 | properties: elementProperties, 162 | group: 'Element' 163 | }) 164 | 165 | registerDefaultProperties({ 166 | class: Q.Node, 167 | properties: nodeProperties, 168 | group: 'Node' 169 | }) 170 | 171 | registerDefaultProperties({ 172 | class: Q.Edge, 173 | properties: edgeProperties, 174 | group: 'Edge' 175 | }) 176 | 177 | registerDefaultProperties({ 178 | class: Q.Text, 179 | properties: textProperties, 180 | group: 'Text' 181 | }) 182 | 183 | registerDefaultProperties({ 184 | class: Q.ShapeNode, 185 | properties: shapeProperties, 186 | group: 'Shape' 187 | }) 188 | 189 | let groupProperties = [{name: 'minSize', type: 'size'}]; 190 | registerDefaultProperties({ 191 | class: Q.Group, 192 | properties: groupProperties, 193 | group: 'Group' 194 | }) 195 | 196 | function getProperties(data, properties, propertyMap) { 197 | if (!propertyMap) { 198 | propertyMap = DEFAULT_PROPERTY_MAP; 199 | } 200 | properties = properties || {}; 201 | for (let name in propertyMap) { 202 | if (!(data instanceof propertyMap[name].class)) { 203 | continue; 204 | } 205 | let map = propertyMap[name].properties; 206 | for (let key in map) { 207 | let p = map[key]; 208 | if (p.display == 'none') { 209 | delete properties[key]; 210 | } else { 211 | properties[key] = p; 212 | } 213 | } 214 | } 215 | return properties; 216 | } 217 | 218 | class PropertyGroup { 219 | constructor(properties) { 220 | this.properties = properties; 221 | let groups = {}; 222 | let length = 0; 223 | for (let key in properties) { 224 | length++; 225 | let groupName = properties[key].groupName; 226 | let group = groups[groupName]; 227 | if (!group) { 228 | group = groups[groupName] = {}; 229 | } 230 | group[key] = properties[key]; 231 | } 232 | this.group = groups; 233 | this.length = length; 234 | } 235 | 236 | contains(name, propertyType) { 237 | let key = getPropertyKey(name, propertyType); 238 | return this.properties[key]; 239 | } 240 | 241 | isEmpty() { 242 | return this.length == 0; 243 | } 244 | 245 | } 246 | 247 | function getElementProperty(graph, element, name, type) { 248 | if (!type || type == Q.Consts.PROPERTY_TYPE_ACCESSOR) { 249 | return element[name]; 250 | } 251 | if (type == Q.Consts.PROPERTY_TYPE_STYLE) { 252 | return graph.getStyle(element, name); 253 | } 254 | if (type == Q.Consts.PROPERTY_TYPE_CLIENT) { 255 | return element.get(name); 256 | } 257 | } 258 | 259 | function setElementProperty(value, element, name, type) { 260 | if (!type || type == Q.Consts.PROPERTY_TYPE_ACCESSOR) { 261 | return element[name] = value; 262 | } 263 | if (type == Q.Consts.PROPERTY_TYPE_STYLE) { 264 | return element.setStyle(name, value); 265 | } 266 | if (type == Q.Consts.PROPERTY_TYPE_CLIENT) { 267 | return element.set(name, value); 268 | } 269 | } 270 | 271 | export class PropertyPane { 272 | constructor(graph, html) { 273 | this._formItems = null; 274 | this.adjusting = false; 275 | this._cellEditors = null; 276 | this.showDefaultProperties = true; 277 | 278 | this._propertyMap = {}; 279 | this._formItems = []; 280 | this.html = this.container = html; 281 | this.dom = createElement({className: 'q-from', parent: html, tagName: 'form'}); 282 | this.graph = graph; 283 | 284 | graph.dataPropertyChangeDispatcher.addListener(function (evt) { 285 | this.onDataPropertyChange(evt); 286 | }.bind(this)); 287 | 288 | graph.selectionChangeDispatcher.addListener(function (evt) { 289 | if(!this.graph.editable){ 290 | return; 291 | } 292 | this.datas = this.graph.selectionModel.toDatas(); 293 | }.bind(this)); 294 | } 295 | 296 | onValueChange(value, item) { 297 | this.setValue(value, item); 298 | } 299 | 300 | _containsElement(data) { 301 | for (let d in this.datas) { 302 | if (d == data) { 303 | return true; 304 | } 305 | } 306 | } 307 | 308 | _containsProperty(name, type) { 309 | return this.propertyGroup && this.propertyGroup.contains(name, type); 310 | } 311 | 312 | _getCellEditors(name, propertyType) { 313 | if (!this._cellEditors) { 314 | return; 315 | } 316 | let key = getPropertyKey(name, propertyType); 317 | return this._cellEditors[key]; 318 | } 319 | 320 | onDataPropertyChange(evt) { 321 | if (this.adjusting) { 322 | return; 323 | } 324 | if (!this.datas || !this.datas.length) { 325 | return null; 326 | } 327 | let data = evt.source; 328 | if (!this._containsElement(data)) { 329 | let editors = this._getCellEditors(evt.kind, evt.propertyType); 330 | if (!editors) { 331 | return; 332 | } 333 | if (!Q.isArray(editors)) { 334 | editors = [editors]; 335 | } 336 | editors.forEach(function (editor) { 337 | editor.update(); 338 | }) 339 | } 340 | } 341 | 342 | clear() { 343 | // $('.colorpicker-element').colorpicker('hide'); 344 | this.dom.innerHTML = ''; 345 | this._formItems = []; 346 | this._cellEditors = null; 347 | this.setVisible(false); 348 | } 349 | 350 | setVisible(visible) { 351 | let display = visible ? 'block' : 'none'; 352 | if (this.container) { 353 | this.container.style.display = display; 354 | } else { 355 | this.dom.style.display = display; 356 | } 357 | } 358 | 359 | createItem(parent, property) { 360 | let formItem = createElement({className: 'q-from-item', parent: parent}); 361 | createElement({ 362 | parent: formItem, 363 | tagName: 'label', 364 | html: getI18NString(property.displayName || property.name) 365 | }); 366 | // let inputDIV = createElement({parent: formItem, tagName: 'input'}); 367 | 368 | let cellEditor = createCellEditor(property, formItem, function () { 369 | return this.getValue(property); 370 | }.bind(this), function (editor) { 371 | this.onValueChange(editor.value, property); 372 | }.bind(this)); 373 | 374 | let key = getPropertyKey(property.name, property.propertyType); 375 | if (!this._cellEditors) { 376 | this._cellEditors = {}; 377 | } 378 | let editors = this._cellEditors[key]; 379 | if (!editors) { 380 | this._cellEditors[key] = [cellEditor]; 381 | } else { 382 | editors.push(cellEditor); 383 | } 384 | return formItem; 385 | } 386 | 387 | setValue(value, property) { 388 | if (!this.datas || !this.datas.length) { 389 | return null; 390 | } 391 | this.adjusting = true; 392 | 393 | if (property.type && property.type != 'string' && Q.isString(value)) { 394 | value = stringToValue(value, property.type); 395 | } 396 | 397 | this.datas.forEach(function (data) { 398 | let old = getElementProperty(this.graph, data, property.name, property.propertyType); 399 | if (old === value) { 400 | return; 401 | } 402 | setElementProperty(value, data, property.name, property.propertyType); 403 | }, this) 404 | 405 | this.adjusting = false; 406 | } 407 | 408 | getValue(property) { 409 | if (!this.datas || !this.datas.length) { 410 | return null; 411 | } 412 | if (this.datas.length == 1) { 413 | return getElementProperty(this.graph, this.datas[0], property.name, property.propertyType) || ''; 414 | } 415 | } 416 | 417 | createItemGroup(name, properties) { 418 | let group = createElement({className: 'class-group', parent: this.dom}); 419 | createElement({tagName: 'h4', parent: group, html: name}); 420 | for (let name in properties) { 421 | this.createItem(group, properties[name]); 422 | } 423 | } 424 | 425 | register(options) { 426 | registerProperties(this._propertyMap, options); 427 | } 428 | 429 | getCustomPropertyDefinitions(data) { 430 | return data.propertyDefinitions; 431 | } 432 | 433 | getProperties(data) { 434 | let properties = {}; 435 | if (this.showDefaultProperties) { 436 | getProperties(data, properties); 437 | } 438 | if (this._propertyMap) { 439 | getProperties(data, properties, this._propertyMap); 440 | } 441 | let propertyDefinitions = this.getCustomPropertyDefinitions(data); 442 | if (propertyDefinitions) { 443 | let map = formatProperties(propertyDefinitions); 444 | for (let name in map) { 445 | properties[name] = map[name]; 446 | } 447 | } 448 | return properties; 449 | } 450 | 451 | _getProperties(data) { 452 | let properties = this.getProperties(data); 453 | return new PropertyGroup(properties); 454 | } 455 | 456 | _show() { 457 | let datas = this.datas; 458 | if (!datas || !datas.length) { 459 | return; 460 | } 461 | this.setVisible(true); 462 | this.propertyGroup = this._getProperties(datas[0]); 463 | 464 | let group = this.propertyGroup.group; 465 | for (let groupName in group) { 466 | this.createItemGroup(groupName, group[groupName]); 467 | } 468 | } 469 | 470 | get datas() { 471 | return this._datas; 472 | } 473 | 474 | set datas(datas) { 475 | if (this._datas == datas) { 476 | return; 477 | } 478 | if (datas && !Q.isArray(datas)) { 479 | datas = [datas]; 480 | } 481 | this.clear(); 482 | if (!datas.length) { 483 | this._datas = null; 484 | return; 485 | } 486 | this._datas = datas; 487 | if (datas.length == 1) { 488 | // getPropertyInfoFromServer(datas[0], this._show.bind(this)) 489 | this._show(); 490 | } 491 | } 492 | } -------------------------------------------------------------------------------- /src/common/toolbar/Toolbar.js: -------------------------------------------------------------------------------- 1 | import {getI18NString} from "../i18n.js"; 2 | import {showExportPanel} from "./Exportpane.js"; 3 | import {isBlobSupported, readTextFile, saveAs} from "../FileSupport.js"; 4 | import {exportJSON, parseJSON} from "../io/JSONSerializer.js"; 5 | 6 | function createDivByClassName(className) { 7 | let div = document.createElement('div'); 8 | div.className = className; 9 | return div; 10 | } 11 | 12 | export function createButton(info, toolbar) { 13 | if (info.type == "search") { 14 | let div = createDivByClassName("Q-Search"); 15 | div.innerHTML = '
\ 16 | \ 17 | \ 18 |
\ 19 |
\ 20 |
'; 21 | let input = div.getElementsByTagName("input")[0]; 22 | if (info.id) { 23 | input.id = info.id; 24 | } 25 | let button = div.querySelectorAll('.btn')[0]; 26 | 27 | if (info.iconClass) { 28 | button.appendChild(createDivByClassName(info.iconClass)); 29 | } else if (info.name) { 30 | button.appendChild(document.createTextNode(" " + info.name)); 31 | } 32 | info.input = input; 33 | if (info.search) { 34 | let clear = function () { 35 | info.searchInfo = null; 36 | } 37 | let doSearch = function (prov) { 38 | let value = input.value; 39 | if (!value) { 40 | clear(); 41 | return; 42 | } 43 | if (!info.searchInfo || info.searchInfo.value != value) { 44 | let result = info.search.call(this, value, info); 45 | if (!result || !result.length) { 46 | clear(); 47 | return; 48 | } 49 | info.searchInfo = {value: value, result: result}; 50 | } 51 | doNext(prov); 52 | }.bind(toolbar) 53 | let doNext = function (prov) { 54 | if (!(info.select instanceof Function) || !info.searchInfo || !info.searchInfo.result || !info.searchInfo.result.length) { 55 | return; 56 | } 57 | let searchInfo = info.searchInfo; 58 | let result = info.searchInfo.result; 59 | if (result.length == 1) { 60 | info.select(result[0], 0); 61 | return; 62 | } 63 | if (searchInfo.index === undefined) { 64 | searchInfo.index = 0; 65 | } else { 66 | searchInfo.index += prov ? -1 : 1; 67 | if (searchInfo.index < 0) { 68 | searchInfo.index += result.length; 69 | } 70 | searchInfo.index %= result.length; 71 | } 72 | if (info.select.call(this, result[searchInfo.index], searchInfo.index) === false) { 73 | info.searchInfo = null; 74 | doSearch(); 75 | } 76 | }.bind(toolbar) 77 | 78 | if (info.oninput) { 79 | input.oninput = function (evt) { 80 | info.oninput.call(toolbar, evt, info); 81 | } 82 | } 83 | input.onkeydown = function (evt) { 84 | if (evt.key === 'Escape' && input.value) { 85 | clear(); 86 | input.value = ""; 87 | evt.preventDefault(); 88 | return; 89 | } 90 | if (evt.key === 'Enter') { 91 | doSearch(evt.shiftKey); 92 | } 93 | }.bind(toolbar) 94 | button.onclick = doSearch; 95 | } 96 | return div; 97 | } 98 | if (info.type == 'file') { 99 | let label = document.createElement('span'); 100 | let input = document.createElement('input'); 101 | label.className = 'file-input btn btn-file'; 102 | input.setAttribute('type', 'file'); 103 | input.className = 'btn-file'; 104 | if (info.action) { 105 | input.onchange = function (evt) { 106 | let input = evt.currentTarget; 107 | let files = input.files; 108 | label = input.value.replace(/\\/g, '/').replace(/.*\//, ''); 109 | if (files.length) { 110 | info.action.call(toolbar, files, label, evt); 111 | } 112 | input.value = null; 113 | }; 114 | } 115 | label.appendChild(input); 116 | 117 | if (info.icon) { 118 | let icon = document.createElement('img'); 119 | icon.src = info.icon; 120 | label.appendChild(icon); 121 | } else if (info.iconClass) { 122 | label.appendChild(createDivByClassName(info.iconClass)); 123 | } else if (info.name) { 124 | label.appendChild(document.createTextNode(" " + info.name)); 125 | } 126 | if (info.name) { 127 | label.setAttribute("title", info.name); 128 | } 129 | return label; 130 | } 131 | if (info.type == "input") { 132 | let div = document.createElement("div"); 133 | div.style.display = "inline-block"; 134 | div.style.verticalAlign = "middle"; 135 | div.innerHTML = '
\ 136 | \ 137 | \ 138 | \ 139 | \ 140 |
'; 141 | let input = div.getElementsByTagName("input")[0]; 142 | let button = div.getElementsByTagName("button")[0]; 143 | button.innerHTML = info.name; 144 | info.input = input; 145 | if (info.action) { 146 | button.onclick = function (evt) { 147 | info.action.call(toolbar, evt, info); 148 | } 149 | } 150 | return div; 151 | } else if (info.type == "select") { 152 | let div = document.createElement("select"); 153 | div.className = "form-control"; 154 | let options = info.options; 155 | options.forEach(function (v) { 156 | let option = document.createElement("option"); 157 | option.innerHTML = v; 158 | option.value = v; 159 | div.appendChild(option); 160 | }); 161 | div.value = info.value; 162 | if (info.action) { 163 | div.onValueChange = function (evt) { 164 | info.action.call(toolbar, evt, info); 165 | } 166 | } 167 | return div; 168 | } 169 | let label, button; 170 | if (!info.type) { 171 | label = document.createElement("div"); 172 | } else { 173 | label = document.createElement("label"); 174 | button = document.createElement("input"); 175 | info.input = button; 176 | info.checked && (button.checked = info.checked); 177 | button.setAttribute('type', info.type); 178 | label.appendChild(button); 179 | if (info.selected) { 180 | button.setAttribute('checked', 'checked'); 181 | if (info.type == 'radio') { 182 | label.className += "active"; 183 | } 184 | } 185 | } 186 | label.className += "btn"; 187 | if (info.icon) { 188 | let icon = document.createElement('img'); 189 | icon.src = info.icon; 190 | label.appendChild(icon); 191 | } else if (info.iconClass) { 192 | label.appendChild(createDivByClassName(info.iconClass)); 193 | } else if (info.name) { 194 | label.appendChild(document.createTextNode(" " + info.name)); 195 | } 196 | if (info.name) { 197 | label.setAttribute("title", info.name); 198 | } 199 | (button || label).onclick = info.action ? function (evt) { 200 | info.action.call(toolbar, evt, info); 201 | } : function (evt) { 202 | toolbar && toolbar.onclick && toolbar.onclick(evt, info); 203 | }; 204 | if (info.selected) { 205 | label.classList.add('active'); 206 | } 207 | return label; 208 | } 209 | 210 | function createBtnGroup(dom) { 211 | if (!dom) { 212 | dom = document.createElement("div"); 213 | } 214 | dom.classList.add('btn-group'); 215 | return dom; 216 | } 217 | 218 | export function createButtonGroup(buttons, scope, dom) { 219 | dom = createBtnGroup(dom); 220 | buttons.forEach(function (info) { 221 | let button = createButton(info, scope); 222 | if (button) { 223 | button.info = info; 224 | dom.appendChild(button); 225 | } 226 | }) 227 | return dom; 228 | } 229 | 230 | function createButtons(buttonGroup, parent, scope, map = {}) { 231 | function getGroup(name) { 232 | let buttonGroup = map[name]; 233 | if (!buttonGroup) { 234 | buttonGroup = createBtnGroup(); 235 | buttonGroup.name = name; 236 | parent.appendChild(buttonGroup); 237 | map[name] = buttonGroup; 238 | } 239 | return buttonGroup; 240 | } 241 | 242 | forEachButton(buttonGroup, function (item, groupName) { 243 | let dom = createButton(item, scope); 244 | dom.info = item; 245 | if (!groupName) { 246 | parent.appendChild(dom); 247 | } else { 248 | let group = getGroup(groupName); 249 | group.appendChild(dom); 250 | } 251 | }) 252 | return map; 253 | } 254 | 255 | function forEachButton(buttonGroup, call) { 256 | for (let name in buttonGroup) { 257 | let info = buttonGroup[name]; 258 | if (!Array.isArray(info)) { 259 | call(info); 260 | continue; 261 | } 262 | info.forEach(function (item) { 263 | call(item, name); 264 | }) 265 | } 266 | } 267 | 268 | const default_buttons = { 269 | interactionModes: [ 270 | { 271 | name: getI18NString('Default Mode'), 272 | interactionMode: 'default', 273 | iconClass: 'q-icon toolbar-default' 274 | }, 275 | { 276 | name: getI18NString('Rectangle Selection'), 277 | interactionMode: 'selection', 278 | iconClass: 'q-icon toolbar-rectangle_selection' 279 | }, 280 | { 281 | name: getI18NString('View Mode'), 282 | interactionMode: 'view', 283 | iconClass: 'q-icon toolbar-pan' 284 | } 285 | ], 286 | zoom: [ 287 | { 288 | name: getI18NString('Zoom In'), iconClass: 'q-icon toolbar-zoomin', action: function () { 289 | this.graph.zoomIn() 290 | } 291 | }, 292 | { 293 | name: getI18NString('Zoom Out'), iconClass: 'q-icon toolbar-zoomout', action: function () { 294 | this.graph.zoomOut() 295 | } 296 | }, 297 | { 298 | name: '1:1', iconClass: 'q-icon toolbar-zoomreset', action: function () { 299 | this.graph.moveToCenter(1);// = 1; 300 | } 301 | }, 302 | { 303 | name: getI18NString('Zoom to Overview'), 304 | iconClass: 'q-icon toolbar-zoom_overview', 305 | action: function () { 306 | this.graph.zoomToOverview() 307 | } 308 | } 309 | ], 310 | editor: [ 311 | { 312 | name: getI18NString('Create Edge'), 313 | interactionMode: 'create.edge', 314 | iconClass: 'q-icon toolbar-edge' 315 | }, 316 | { 317 | name: getI18NString('Create L Edge'), 318 | interactionMode: 'create.simple.edge', 319 | iconClass: 'q-icon toolbar-edge_VH', 320 | edgeType: 'orthogonal.V.H' 321 | }, 322 | { 323 | name: getI18NString('Create Shape'), 324 | interactionMode: 'create.shape', 325 | iconClass: 'q-icon toolbar-polygon' 326 | }, 327 | { 328 | name: getI18NString('Create Line'), 329 | interactionMode: 'create.line', 330 | iconClass: 'q-icon toolbar-line' 331 | } 332 | ], 333 | search: { 334 | name: 'Find', 335 | placeholder: 'Name', 336 | iconClass: 'q-icon toolbar-search', 337 | type: 'search', 338 | id: 'search_input', 339 | search: function (name, info) { 340 | let result = []; 341 | let reg = new RegExp(name, 'i'); 342 | let graph = this.graph; 343 | graph.forEach(function (e) { 344 | if (graph.isVisible(e) && e.name && reg.test(e.name)) { 345 | result.push(e.id); 346 | } 347 | }); 348 | return result; 349 | }, 350 | select: function (item) { 351 | let graph = this.graph || this.view; 352 | item = graph.graphModel.getById(item); 353 | if (!item) { 354 | return false; 355 | } 356 | graph.setSelection(item); 357 | graph.sendToTop(item); 358 | let bounds = graph.getUIBounds(item); 359 | if (bounds) { 360 | graph.centerTo(bounds.cx, bounds.cy, Math.max(2, graph.scale), true); 361 | } 362 | } 363 | }, 364 | export_image: { 365 | name: getI18NString('Export Image'), iconClass: 'q-icon toolbar-print', action: function () { 366 | showExportPanel(this.graph); 367 | } 368 | }, 369 | json: [{ 370 | iconClass: 'q-icon toolbar-upload', 371 | name: getI18NString('Load File ...'), 372 | type: 'file', 373 | action(files) { 374 | console.log('upload files', files) 375 | if (!files[0]) { 376 | return; 377 | } 378 | let graph = this.graph || this.view; 379 | readTextFile(files[0], 'json', function (json) { 380 | if (!json) { 381 | alert(getI18NString('json file is empty')); 382 | return; 383 | } 384 | parseJSON(json, graph); 385 | }); 386 | } 387 | }] 388 | } 389 | 390 | 391 | if (isBlobSupported()) { 392 | default_buttons.json.push({ 393 | iconClass: 'q-icon toolbar-download', 394 | name: getI18NString('Download File'), 395 | action() { 396 | let graph = this.graph || this.view; 397 | let name = graph.name || 'graph'; 398 | let json = exportJSON(graph, true); 399 | saveAs(new Blob([json], {type: "text/plain;charset=utf-8"}), name + ".json"); 400 | } 401 | }) 402 | } 403 | 404 | export class Toolbar { 405 | constructor(dom) { 406 | if (!dom) { 407 | dom = document.createElement('div'); 408 | } 409 | dom.classList.add('Q-Toolbar'); 410 | this.dom = dom; 411 | 412 | setTimeout(function () { 413 | this._init(); 414 | }.bind(this)) 415 | } 416 | 417 | setGraph(graph) { 418 | let old = this.graph; 419 | if (old) { 420 | old.propertyChangeDispatcher.removeListener(this._onInteractionModeChange, this); 421 | } 422 | this.graph = graph; 423 | this._updateButtonStatus(); 424 | if (graph) { 425 | graph.propertyChangeDispatcher.addListener(this._onInteractionModeChange, this); 426 | } 427 | } 428 | 429 | _onInteractionModeChange(evt) { 430 | if (evt.kind == 'interactionMode') { 431 | this._updateButtonStatus(); 432 | } 433 | } 434 | 435 | _updateButtonStatus() { 436 | let g = this.graph; 437 | let mode = g ? g.interactionMode : null; 438 | let interactionProperties = g ? g.interactionProperties : null; 439 | let buttons = this.dom.querySelectorAll('.btn'); 440 | let i = 0, l = buttons.length; 441 | while (i < l) { 442 | let btn = buttons[i++]; 443 | if (!btn.info || !btn.info.interactionMode) { 444 | continue 445 | } 446 | if (mode && btn.info.interactionMode === mode) { 447 | if (!interactionProperties || interactionProperties === btn.info) { 448 | btn.classList.add('active'); 449 | continue; 450 | } 451 | } 452 | btn.classList.remove('active'); 453 | } 454 | } 455 | 456 | _init() { 457 | let buttons = get(this.mode); 458 | buttons && createButtons(buttons, this.dom, this); 459 | } 460 | 461 | setMode(mode){ 462 | this.mode = mode; 463 | this.dom.innerHTML = ''; 464 | this._init(); 465 | } 466 | 467 | onclick(evt, info) { 468 | if (!this.graph) { 469 | return 470 | } 471 | if (info && info.interactionMode) { 472 | this.graph.interactionProperties = info; 473 | this.graph.interactionMode = info.interactionMode; 474 | } 475 | } 476 | } 477 | 478 | let BUTTON_MAP = {}; 479 | 480 | function register(type, buttons) { 481 | BUTTON_MAP[type] = buttons; 482 | } 483 | 484 | function get(type) { 485 | let buttons = BUTTON_MAP[type || 'default']; 486 | if (!buttons) { 487 | throw 'No toolbar buttons are defined' 488 | } 489 | return Object.assign({}, buttons); 490 | } 491 | 492 | register('default', default_buttons); 493 | register('view', { 494 | zoom: default_buttons.zoom, 495 | search: default_buttons.search, 496 | export_image: default_buttons.export_image 497 | }); 498 | 499 | -------------------------------------------------------------------------------- /src/common/io/JSONSerializer.js: -------------------------------------------------------------------------------- 1 | //json export and parse support 2 | import Q from '../../lib/qunee-es.js'; 3 | 4 | //v1.8 5 | function isEmptyObject(obj) { 6 | if (!(obj instanceof Object)) { 7 | return !obj; 8 | } 9 | if (Array.isArray(obj)) { 10 | return obj.length == 0; 11 | } 12 | for (let key in obj) { 13 | return false; 14 | } 15 | return true; 16 | } 17 | 18 | function getByPath(pathName, scope) { 19 | let paths = pathName.split('.'); 20 | if(!scope){ 21 | if(paths[0] === 'Q'){ 22 | scope = Q; 23 | paths.shift(); 24 | }else{ 25 | scope = window; 26 | } 27 | } 28 | let i = -1; 29 | while (scope && ++i < paths.length) { 30 | let path = paths[i]; 31 | scope = scope[path]; 32 | } 33 | return scope; 34 | } 35 | 36 | function loadClassPath(object, namespace, loadChild) { 37 | object._classPath = namespace; 38 | if (object instanceof Function) { 39 | object.prototype._className = object._classPath; 40 | object.prototype._class = object; 41 | // Q.log(v._className); 42 | // continue; 43 | } 44 | if (loadChild === false) { 45 | return; 46 | } 47 | for (let name in object) { 48 | if (name[0] == '_' || name[0] == '$' || name == 'superclass' || name == 'constructor' || name == 'prototype' || name.indexOf('.') >= 0) { 49 | continue; 50 | } 51 | let v = object[name]; 52 | if (!v || !(v instanceof Object) || v._classPath) { 53 | continue; 54 | } 55 | loadClassPath(v, namespace + '.' + name); 56 | } 57 | } 58 | 59 | let prototypes = {}; 60 | 61 | function getPrototype(data) { 62 | let className = data._className; 63 | if (!className) { 64 | return null; 65 | } 66 | let prototype = prototypes[className]; 67 | if (!prototype) { 68 | let clazz = data._class; 69 | prototype = prototypes[className] = new clazz(); 70 | } 71 | return prototype; 72 | } 73 | 74 | function equals(a, b) { 75 | return a == b || (a && b && a.equals && a.equals(b)); 76 | } 77 | 78 | Q.HashList.prototype.toJSON = function (serializer) { 79 | let datas = []; 80 | this.forEach(function (data) { 81 | datas.push(serializer.toJSON(data)); 82 | }) 83 | return datas; 84 | } 85 | 86 | Q.HashList.prototype.parseJSON = function (json, serializer) { 87 | let result = []; 88 | json.forEach(function (item) { 89 | let data = serializer.parseJSON(item); 90 | this.add(data); 91 | result.push(data); 92 | }, this) 93 | return result; 94 | } 95 | 96 | function exportElementProperties(serializer, properties, info, element) { 97 | let prototype = getPrototype(element); 98 | properties.forEach(function (name) { 99 | let value = element[name]; 100 | if (!equals(value, prototype[name])) { 101 | let json = serializer.toJSON(value); 102 | if (json || !value) { 103 | info[name] = json; 104 | } 105 | } 106 | }, element); 107 | } 108 | 109 | function exportProperties(serializer, properties) { 110 | let info; 111 | for (let s in properties) { 112 | if (!info) { 113 | info = {}; 114 | } 115 | info[s] = serializer.toJSON(properties[s]); 116 | } 117 | return info; 118 | } 119 | 120 | let wirtableUIProperties = { 121 | class: false, 122 | id: false, 123 | "fillGradient": false, 124 | "syncSelectionStyles": false, 125 | "originalBounds": false, 126 | "parent": false, 127 | "font": false, 128 | "$data": false, 129 | "$x": false, 130 | "$y": false 131 | }; 132 | 133 | Q.BaseUI.prototype.toJSON = function (serializer) { 134 | let json = {}; 135 | for (let name in this) { 136 | if (name[0] == '_' || (name[0] == '$' && name[1] == '_') || (name.indexOf('$invalidate') == 0) || wirtableUIProperties[name] === false) { 137 | continue; 138 | } 139 | let value = this[name]; 140 | if (value instanceof Function || value == this.class.prototype[name]) { 141 | continue; 142 | } 143 | //wirtableUIProperties[name] = true; 144 | 145 | try { 146 | json[name] = serializer.toJSON(value); 147 | } catch (error) { 148 | 149 | } 150 | } 151 | return json; 152 | } 153 | //new Q.ImageUI().toJSON(); 154 | //new Q.LabelUI().toJSON(); 155 | //Q.log(JSON.stringify(wirtableUIProperties)) 156 | 157 | Q.BaseUI.prototype.parseJSON = function (info, serializer) { 158 | for (let name in info) { 159 | let v = serializer.parseJSON(info[name]); 160 | this[name] = v; 161 | } 162 | } 163 | 164 | let OUTPUT_PROPERTIES = ['userId', 'rotatable', 'editable', 'layoutable', 'visible', 'busLayout', 'enableSubNetwork', 'zIndex', 'tooltipType', 'tooltip', 'movable', 'selectable', 'resizable', 'uiClass', 'name', 'parent', 'host']; 165 | 166 | Q.Element.prototype.addOutProperty = function (name) { 167 | if (!this.outputProperties) { 168 | this.outputProperties = []; 169 | } 170 | this.outputProperties.push(name); 171 | } 172 | Q.Element.prototype.removeOutProperty = function (name) { 173 | if (this.outputProperties) { 174 | let index = this.outputProperties.indexOf(name); 175 | if (index >= 0) { 176 | this.outputProperties.splice(index, 1); 177 | } 178 | } 179 | } 180 | Q.Element.prototype.toJSON = function (serializer) { 181 | let info = {}; 182 | let outputProperties = OUTPUT_PROPERTIES; 183 | if (this.outputProperties) { 184 | outputProperties = outputProperties.concat(this.outputProperties); 185 | } 186 | exportElementProperties(serializer, outputProperties, info, this); 187 | if (this.styles) { 188 | let styles = exportProperties(serializer, this.styles); 189 | if (styles) { 190 | info.styles = styles; 191 | } 192 | } 193 | if (this.properties) { 194 | let properties = exportProperties(serializer, this.properties); 195 | if (properties) { 196 | info.properties = properties; 197 | } 198 | } 199 | let bindingUIs = this.bindingUIs; 200 | if (bindingUIs) { 201 | let bindingJSONs = []; 202 | //let binding = {id: ui.id, ui: ui, bindingProperties: bindingProperties}; 203 | bindingUIs.forEach(function (binding) { 204 | if (binding.ui.serializable === false) { 205 | return; 206 | } 207 | let uiJSON = serializer.toJSON(binding.ui); 208 | bindingJSONs.push({ 209 | ui: uiJSON, 210 | bindingProperties: binding.bindingProperties 211 | }) 212 | }) 213 | info.bindingUIs = bindingJSONs; 214 | } 215 | return info; 216 | } 217 | Q.Element.prototype.parseJSON = function (info, serializer) { 218 | if (info.styles) { 219 | let styles = {}; 220 | for (let n in info.styles) { 221 | styles[n] = serializer.parseJSON(info.styles[n]); 222 | } 223 | this.putStyles(styles, true); 224 | //delete info.styles; 225 | } 226 | if (info.properties) { 227 | let properties = {}; 228 | for (let n in info.properties) { 229 | properties[n] = serializer.parseJSON(info.properties[n]); 230 | } 231 | this.properties = properties; 232 | } 233 | if (info.bindingUIs) { 234 | info.bindingUIs.forEach(function (binding) { 235 | let ui = serializer.parseJSON(binding.ui); 236 | if (!ui) { 237 | return; 238 | } 239 | this.addUI(ui, binding.bindingProperties); 240 | 241 | //let circle = new Q.ImageUI(ui.data); 242 | //circle.lineWidth = 2; 243 | //circle.strokeStyle = '#ff9f00'; 244 | //this.addUI(circle); 245 | }, this) 246 | } 247 | for (let n in info) { 248 | if (n == 'id' || n == 'styles' || n == 'properties' || n == 'bindingUIs') { 249 | continue; 250 | } 251 | let v = serializer.parseJSON(info[n]); 252 | this[n] = v; 253 | } 254 | } 255 | Q.Node.prototype.toJSON = function (serializer) { 256 | let info = Q.doSuper(this, Q.Node, 'toJSON', arguments); 257 | exportElementProperties(serializer, ['location', 'size', 'image', 'rotate', 'anchorPosition', 'parentChildrenDirection', 'layoutType', 'hGap', 'vGap'], info, this); 258 | return info; 259 | } 260 | Q.Group.prototype.toJSON = function (serializer) { 261 | let info = Q.doSuper(this, Q.Group, 'toJSON', arguments); 262 | exportElementProperties(serializer, ['minSize', 'groupType', 'padding', 'groupImage', 'expanded'], info, this); 263 | return info; 264 | } 265 | Q.ShapeNode.prototype.toJSON = function (serializer) { 266 | let info = Q.doSuper(this, Q.Node, 'toJSON', arguments); 267 | exportElementProperties(serializer, ['location', 'rotate', 'anchorPosition', 'path'], info, this); 268 | return info; 269 | } 270 | Q.Edge.prototype.toJSON = function (serializer) { 271 | let info = Q.doSuper(this, Q.Edge, 'toJSON', arguments); 272 | exportElementProperties(serializer, ['angle', 'from', 'to', 'edgeType', 'angle', 'bundleEnabled', 'pathSegments'], info, this); 273 | return info; 274 | } 275 | 276 | function JSONSerializer(options) { 277 | if (options) { 278 | this.withGlobalRefs = options.withGlobalRefs !== false; 279 | } 280 | this.reset(); 281 | } 282 | 283 | JSONSerializer.prototype = { 284 | _refs: null, 285 | _refValues: null, 286 | _index: 1, 287 | root: null, 288 | reset: function () { 289 | this._globalRefs = {}; 290 | this._elementRefs = {}; 291 | this._refs = {}; 292 | this._refValues = {}; 293 | this._index = 1; 294 | }, 295 | getREF: function (id) { 296 | return this._refs[id]; 297 | }, 298 | clearRef: function () { 299 | for (let id in this._globalRefs) { 300 | delete this._globalRefs[id]._value; 301 | } 302 | for (let id in this._refValues) { 303 | delete this._refValues[id]._refId; 304 | } 305 | this.reset(); 306 | }, 307 | elementToJSON: function (element) { 308 | return this._toJSON(element); 309 | }, 310 | _elementRefs: null, 311 | _globalRefs: null, 312 | withGlobalRefs: true, 313 | toJSON: function (value) { 314 | if (!(value instanceof Object)) { 315 | return value; 316 | } 317 | if (value instanceof Function && !value._classPath) { 318 | return null; 319 | } 320 | if (!this.withGlobalRefs) { 321 | return this._toJSON(value); 322 | } 323 | if (value instanceof Q.Element) { 324 | let id = getElementId(value); 325 | this._elementRefs[id] = true; 326 | return {_ref: id}; 327 | } 328 | if (value._refId === undefined) { 329 | let json = this._toJSON(value); 330 | if (!json) { 331 | return json; 332 | } 333 | //添加引用标记,下次遇到这个对象时,不需要再toJSON,而是直接输出引用,比如{"$ref": 1} 334 | let id = value._refId = this._index++; 335 | //将对象暂时存放在_refValues中,以便在导出完成后,删除掉上一步对value增加的_refId属性 336 | this._refValues[id] = value; 337 | this._refs[id] = json; 338 | return json; 339 | } 340 | //遇到相同的对象,将对象信息存放到全局map,网元属性只需要存放引用id,比如{"$ref": 1} 341 | //全局map中存放在g属性中,以id为key,json为value,如下: 342 | //"refs": { 343 | // "1": { 344 | // "_classPath": "Q.Position.LEFT_BOTTOM" 345 | // } 346 | //}, 347 | //"datas": [ 348 | // { 349 | // "_className": "Q.Node", 350 | // "json": { 351 | // "name": "A", 352 | // "styles": { 353 | // "property": { 354 | // "$ref": 1 355 | // } 356 | // }, 357 | let id = value._refId; 358 | if (!this._globalRefs[id]) { 359 | //如果还没有加入到全局引用区,则将json放入到_globalRefs,同时将原来的json变成引用方式 360 | let json = this._refs[id]; 361 | if (!json) { 362 | return json; 363 | } 364 | let clone = {}; 365 | for (let name in json) { 366 | clone[name] = json[name]; 367 | delete json[name]; 368 | } 369 | json.$ref = id; 370 | this._globalRefs[id] = clone; 371 | } 372 | return {$ref: id}; 373 | }, 374 | _toJSON: function (value) { 375 | if (value._classPath) { 376 | return {_classPath: value._classPath}; 377 | } 378 | let json 379 | if (!value._className) { 380 | if (Q.isArray(value)) { 381 | json = []; 382 | value.forEach(function (v) { 383 | json.push(this.toJSON(v)); 384 | }, this) 385 | return json; 386 | } else { 387 | json = {}; 388 | let prototype; 389 | if (value.class) { 390 | prototype = value.class.prototype; 391 | } 392 | for (let name in value) { 393 | let v = value[name]; 394 | if (v instanceof Function || (prototype && v == prototype[name])) { 395 | continue; 396 | } 397 | json[name] = this.toJSON(value[name]); 398 | } 399 | return json; 400 | } 401 | 402 | return value; 403 | } 404 | let result = {_className: value._className}; 405 | if (value.toJSON) { 406 | result.json = value.toJSON(this); 407 | } else { 408 | result.json = value; 409 | } 410 | return result; 411 | }, 412 | jsonToElement: function (json) { 413 | //如果之前解析的数据中引用到了此节点,此节点其实已经被解析了,这里只需要返回引用就可以了 414 | if (json._refId !== undefined && json._refId in this._refs) { 415 | return this._refs[json._refId]; 416 | } 417 | return this._parseJSON(json); 418 | }, 419 | parseJSON: function (json) { 420 | if (!(json instanceof Object)) { 421 | return json; 422 | } 423 | if (!this.withGlobalRefs) { 424 | return this._parseJSON(json); 425 | } 426 | //全局引用 427 | if (json.$ref !== undefined) { 428 | //从全局引用中获取json信息 429 | let gJson = this._globalRefs[json.$ref]; 430 | if (!gJson) { 431 | return; 432 | } 433 | //将json信息解析成对象,并缓存在json的_value属性中 434 | if (gJson._value === undefined) { 435 | gJson._value = this.parseJSON(gJson); 436 | } 437 | return gJson._value; 438 | } 439 | //如果属性为element引用,先从_elementRefs中找到对应element的json信息,然后将此json信息解析成element 440 | if (json._ref !== undefined) { 441 | let elementJson = this._elementRefs[json._ref]; 442 | if (!elementJson) { 443 | return; 444 | } 445 | return this.jsonToElement(elementJson); 446 | } 447 | ////如果json包含_refId属性,说明这是一个element类型,直接调用jsonToElement,不过应该不会出现 448 | //if (json._refId !== undefined) { 449 | // return this.jsonToElement(json); 450 | //} 451 | return this._parseJSON(json); 452 | }, 453 | _parseJSON: function (json) { 454 | if (!(json instanceof Object)) { 455 | return json; 456 | } 457 | if (json._classPath) { 458 | return getByPath(json._classPath); 459 | } 460 | if (json._className) { 461 | let F = getByPath(json._className); 462 | let v = new F(); 463 | ///防止相互引用导致的问题 464 | if (this.withGlobalRefs && json._refId !== undefined) { 465 | this._refs[json._refId] = v; 466 | } 467 | if (v && json.json) { 468 | json = json.json; 469 | if (v.parseJSON) { 470 | v.parseJSON(json, this); 471 | } else { 472 | for (let n in json) { 473 | v[n] = json[n]; 474 | } 475 | } 476 | } 477 | return v; 478 | } 479 | if (Q.isArray(json)) { 480 | let result = []; 481 | json.forEach(function (j) { 482 | result.push(this.parseJSON(j)); 483 | }, this) 484 | return result; 485 | } 486 | let result = {}; 487 | for (let name in json) { 488 | result[name] = this.parseJSON(json[name]) 489 | } 490 | return result; 491 | } 492 | } 493 | 494 | function getElementId(element) { 495 | return element.userId || element.id; 496 | } 497 | 498 | function graphModelToJSON(model, filter) { 499 | let serializer = new JSONSerializer(); 500 | let json = { 501 | version: '2.0', 502 | refs: {} 503 | }; 504 | let datas = []; 505 | let map = {}; 506 | if (model.currentSubNetwork) { 507 | let elementJson = serializer.elementToJSON(model.currentSubNetwork); 508 | if (elementJson) { 509 | json.currentSubNetwork = {_ref: elementJson._refId = model.currentSubNetwork.id}; 510 | } 511 | } 512 | model.forEach(function (d) { 513 | if (filter && filter(d) === false) { 514 | return; 515 | } 516 | let elementJson = serializer.elementToJSON(d); 517 | if (elementJson) { 518 | datas.push(elementJson); 519 | map[getElementId(d)] = elementJson; 520 | } 521 | }); 522 | if (serializer._elementRefs) { 523 | for (let id in serializer._elementRefs) { 524 | map[id]._refId = id; 525 | } 526 | } 527 | if (serializer._globalRefs) { 528 | json.refs = serializer._globalRefs; 529 | } 530 | serializer.clearRef(); 531 | 532 | json.datas = datas; 533 | for (let name in json) { 534 | if (isEmptyObject(json[name])) { 535 | delete json[name]; 536 | } 537 | } 538 | return json; 539 | } 540 | 541 | Q.GraphModel.prototype.toJSON = function (filter) { 542 | return graphModelToJSON(this, filter); 543 | } 544 | 545 | function versionToNumber(version) { 546 | let index = version.indexOf('.'); 547 | if (index < 0) { 548 | return parseFloat(version); 549 | } 550 | version = version.substring(0, index) + '.' + version.substring(index).replace(/\./g, ''); 551 | return parseFloat(version); 552 | } 553 | 554 | Q.GraphModel.prototype.parseJSON = function (json, options) { 555 | options = options || {}; 556 | let datas = json.datas; 557 | if (!datas || !(datas.length > 0)) { 558 | return; 559 | } 560 | let result = []; 561 | let serializer = new JSONSerializer(options, json.g); 562 | let elementRefs = {}; 563 | datas.forEach(function (info) { 564 | if (info._refId) { 565 | elementRefs[info._refId] = info; 566 | } 567 | }) 568 | serializer._globalRefs = json.refs || {}; 569 | serializer._elementRefs = elementRefs; 570 | 571 | datas.forEach(function (json) { 572 | let element = serializer.jsonToElement(json); 573 | if (element instanceof Q.Element) { 574 | result.push(element); 575 | this.add(element); 576 | } 577 | }, this); 578 | 579 | if (this.currentSubNetwork) { 580 | let currentSubNetwork = this.currentSubNetwork; 581 | result.forEach(function (e) { 582 | if (!e.parent) { 583 | e.parent = currentSubNetwork; 584 | } 585 | }) 586 | } 587 | 588 | if (json.currentSubNetwork) { 589 | let currentSubNetwork = serializer.getREF(json.currentSubNetwork._ref); 590 | if (currentSubNetwork) { 591 | this.currentSubNetwork = currentSubNetwork; 592 | } 593 | } 594 | serializer.clearRef(); 595 | return result; 596 | } 597 | 598 | Q.Graph.prototype.toJSON = Q.Graph.prototype.exportJSON = function (toString, options) { 599 | options = options || {}; 600 | let json = this.graphModel.toJSON(options.filter); 601 | json.scale = this.scale; 602 | json.tx = this.tx; 603 | json.ty = this.ty; 604 | if (toString) { 605 | json = JSON.stringify(json, options.replacer, options.space || '\t') 606 | } 607 | return json; 608 | } 609 | Q.Graph.prototype.parseJSON = function (json, options) { 610 | if (Q.isString(json)) { 611 | json = JSON.parse(json); 612 | } 613 | options = options || {} 614 | let result = this.graphModel.parseJSON(json, options); 615 | let scale = json.scale; 616 | if (scale && options.transform !== false) { 617 | this.originAtCenter = false; 618 | this.translateTo(json.tx || 0, json.ty || 0, scale); 619 | } 620 | return result; 621 | } 622 | 623 | loadClassPath(Q, 'Q'); 624 | Q.loadClassPath = loadClassPath; 625 | 626 | export function exportJSON(object, toString, options = {}) { 627 | if(object.exportJSON){ 628 | return object.exportJSON(toString, options); 629 | } 630 | let json = new JSONSerializer({withGlobalRefs: false}).toJSON(object); 631 | if(toString){ 632 | return JSON.stringify(json, options.replacer, options.space || '\t') 633 | } 634 | return json; 635 | } 636 | export function parseJSON(json, graph) { 637 | if (Q.isString(json)) { 638 | json = JSON.parse(json); 639 | } 640 | if(graph && graph.parseJSON){ 641 | return graph.parseJSON(json); 642 | } 643 | try { 644 | return new JSONSerializer({withGlobalRefs: false}).parseJSON(json); 645 | } catch (error) { 646 | } 647 | } 648 | -------------------------------------------------------------------------------- /src/common/toolbar/Exportpane.js: -------------------------------------------------------------------------------- 1 | import Q from '../../lib/qunee-es.js'; 2 | import {getI18NString} from "../i18n.js"; 3 | import {showDialog} from "../modal/Dialog.js"; 4 | import {createElement} from "../utils.js"; 5 | import {isBlobSupported, saveAs} from "../FileSupport.js"; 6 | 7 | let template = '\ 8 |

' + getI18NString('Image export preview') + '

\ 9 |
\ 10 | \ 11 | \ 12 |
\ 13 |
\ 14 |
\ 15 |
\ 16 |
\ 17 |
\ 18 | \ 19 | \ 20 |
\ 21 |
\ 22 | \ 23 |
\ 24 |
\ 25 | \ 26 |
\ 27 |
\ 28 | \ 29 | \ 30 |
'; 31 | 32 | class ResizeBox { 33 | constructor(parent, onBoundsChange) { 34 | this.onBoundsChange = onBoundsChange; 35 | this.parent = parent; 36 | this.handleSize = Q.isTouchSupport ? 20 : 8; 37 | 38 | this.boundsDiv = this._createDiv(this.parent); 39 | this.boundsDiv.type = "border"; 40 | this.boundsDiv.style.position = "absolute"; 41 | this.boundsDiv.style.border = "dashed 1px #888"; 42 | let handles = "lt,t,rt,l,r,lb,b,rb"; 43 | handles = handles.split(","); 44 | for (let i = 0, l = handles.length; i < l; i++) { 45 | let name = handles[i]; 46 | let handle = this._createDiv(this.parent); 47 | handle.type = "handle"; 48 | handle.name = name; 49 | handle.style.position = "absolute"; 50 | handle.style.backgroundColor = "#FFF"; 51 | handle.style.border = "solid 1px #555"; 52 | handle.style.width = handle.style.height = this.handleSize + "px"; 53 | let cursor; 54 | if (name == 'lt' || name == 'rb') { 55 | cursor = "nwse-resize"; 56 | } else if (name == 'rt' || name == 'lb') { 57 | cursor = "nesw-resize"; 58 | } else if (name == 't' || name == 'b') { 59 | cursor = "ns-resize"; 60 | } else { 61 | cursor = "ew-resize"; 62 | } 63 | handle.style.cursor = cursor; 64 | this[handles[i]] = handle; 65 | } 66 | this.interaction = new Q.DragSupport(this.parent, this); 67 | } 68 | 69 | destroy() { 70 | this.interaction.destroy(); 71 | } 72 | 73 | update(width, height) { 74 | this.wholeBounds = new Q.Rect(0, 0, width, height); 75 | this._setBounds(this.wholeBounds.clone()); 76 | } 77 | 78 | ondblclick(evt) { 79 | if (this._bounds.equals(this.wholeBounds)) { 80 | if (!this.oldBounds) { 81 | this.oldBounds = this.wholeBounds.clone().grow(-this.wholeBounds.height / 5, -this.wholeBounds.width / 5); 82 | } 83 | this._setBounds(this.oldBounds, true); 84 | return; 85 | } 86 | this._setBounds(this.wholeBounds.clone(), true); 87 | } 88 | 89 | startdrag(evt) { 90 | if (evt.target.type) { 91 | this.dragItem = evt.target; 92 | } 93 | } 94 | 95 | ondrag(evt) { 96 | if (!this.dragItem) { 97 | return; 98 | } 99 | Q.stopEvent(evt); 100 | let dx = evt.dx; 101 | let dy = evt.dy; 102 | if (this.dragItem.type == "border") { 103 | this._bounds.offset(dx, dy); 104 | this._setBounds(this._bounds, true); 105 | } else if (this.dragItem.type == "handle") { 106 | let name = this.dragItem.name; 107 | if (name[0] == 'l') { 108 | this._bounds.x += dx; 109 | this._bounds.width -= dx; 110 | } else if (name[0] == 'r') { 111 | this._bounds.width += dx; 112 | } 113 | if (name[name.length - 1] == 't') { 114 | this._bounds.y += dy; 115 | this._bounds.height -= dy; 116 | } else if (name[name.length - 1] == 'b') { 117 | this._bounds.height += dy; 118 | } 119 | this._setBounds(this._bounds, true); 120 | } 121 | 122 | } 123 | 124 | enddrag(evt) { 125 | if (!this.dragItem) { 126 | return; 127 | } 128 | this.dragItem = false; 129 | if (this._bounds.width < 0) { 130 | this._bounds.x += this._bounds.width; 131 | this._bounds.width = -this._bounds.width; 132 | } else if (this._bounds.width == 0) { 133 | this._bounds.width = 1; 134 | } 135 | if (this._bounds.height < 0) { 136 | this._bounds.y += this._bounds.height; 137 | this._bounds.height = -this._bounds.height; 138 | } else if (this._bounds.height == 0) { 139 | this._bounds.height = 1; 140 | } 141 | if (this._bounds.width > this.wholeBounds.width) { 142 | this._bounds.width = this.wholeBounds.width; 143 | } 144 | if (this._bounds.height > this.wholeBounds.height) { 145 | this._bounds.height = this.wholeBounds.height; 146 | } 147 | if (this._bounds.x < 0) { 148 | this._bounds.x = 0; 149 | } 150 | if (this._bounds.y < 0) { 151 | this._bounds.y = 0; 152 | } 153 | if (this._bounds.right > this.wholeBounds.width) { 154 | this._bounds.x -= this._bounds.right - this.wholeBounds.width; 155 | } 156 | if (this._bounds.bottom > this.wholeBounds.height) { 157 | this._bounds.y -= this._bounds.bottom - this.wholeBounds.height; 158 | } 159 | 160 | this._setBounds(this._bounds, true); 161 | } 162 | 163 | _createDiv(parent) { 164 | let div = document.createElement("div"); 165 | parent.appendChild(div); 166 | return div; 167 | } 168 | 169 | _setHandleLocation(handle, x, y) { 170 | handle.style.left = (x - this.handleSize / 2) + "px"; 171 | handle.style.top = (y - this.handleSize / 2) + "px"; 172 | } 173 | 174 | _setBounds(bounds) { 175 | if (!bounds.equals(this.wholeBounds)) { 176 | this.oldBounds = bounds; 177 | } 178 | this._bounds = bounds; 179 | bounds = bounds.clone(); 180 | bounds.width += 1; 181 | bounds.height += 1; 182 | this.boundsDiv.style.left = bounds.x + "px"; 183 | this.boundsDiv.style.top = bounds.y + "px"; 184 | this.boundsDiv.style.width = bounds.width + "px"; 185 | this.boundsDiv.style.height = bounds.height + "px"; 186 | 187 | this._setHandleLocation(this.lt, bounds.x, bounds.y); 188 | this._setHandleLocation(this.t, bounds.cx, bounds.y); 189 | this._setHandleLocation(this.rt, bounds.right, bounds.y); 190 | this._setHandleLocation(this.l, bounds.x, bounds.cy); 191 | this._setHandleLocation(this.r, bounds.right, bounds.cy); 192 | this._setHandleLocation(this.lb, bounds.x, bounds.bottom); 193 | this._setHandleLocation(this.b, bounds.cx, bounds.bottom); 194 | this._setHandleLocation(this.rb, bounds.right, bounds.bottom); 195 | if (this.onBoundsChange) { 196 | this.onBoundsChange(this._bounds); 197 | } 198 | } 199 | 200 | get bounds() { 201 | return this._bounds; 202 | } 203 | 204 | set bounds(v) { 205 | this._setBounds(v); 206 | } 207 | } 208 | 209 | class ExportPanel { 210 | _getChild(selector) { 211 | return this.html.querySelector(selector); 212 | } 213 | 214 | initCanvas() { 215 | let export_canvas = this._getChild('.graph-export-panel__export_canvas'); 216 | export_canvas.innerHTML = ""; 217 | 218 | let canvas = Q.createCanvas(true); 219 | export_canvas.appendChild(canvas); 220 | this.canvas = canvas; 221 | 222 | let export_bounds = this._getChild(".graph-export-panel__export_bounds"); 223 | let export_size = this._getChild(".graph-export-panel__export_size"); 224 | let clipBounds; 225 | let drawPreview = function () { 226 | let canvas = this.canvas; 227 | let g = canvas.g; 228 | let ratio = canvas.ratio || 1; 229 | g.save(); 230 | //g.scale(1/g.ratio, 1/g.ratio); 231 | g.clearRect(0, 0, canvas.width, canvas.height); 232 | g.drawImage(this.imageInfo.canvas, 0, 0); 233 | g.beginPath(); 234 | g.moveTo(0, 0); 235 | g.lineTo(canvas.width, 0); 236 | g.lineTo(canvas.width, canvas.height); 237 | g.lineTo(0, canvas.height); 238 | g.lineTo(0, 0); 239 | 240 | let x = clipBounds.x * ratio, y = clipBounds.y * ratio, width = clipBounds.width * ratio, 241 | height = clipBounds.height * ratio; 242 | g.moveTo(x, y); 243 | g.lineTo(x, y + height); 244 | g.lineTo(x + width, y + height); 245 | g.lineTo(x + width, y); 246 | g.closePath(); 247 | g.fillStyle = "rgba(0, 0, 0, 0.3)"; 248 | g.fill(); 249 | g.restore(); 250 | } 251 | let onBoundsChange = function (bounds) { 252 | clipBounds = bounds; 253 | this.clipBounds = clipBounds; 254 | drawPreview.call(this); 255 | let w = clipBounds.width / this.imageInfo.scale | 0; 256 | let h = clipBounds.height / this.imageInfo.scale | 0; 257 | export_bounds.textContent = (clipBounds.x / this.imageInfo.scale | 0) + ", " 258 | + (clipBounds.y / this.imageInfo.scale | 0) + ", " + w + ", " + h; 259 | this.updateOutputSize(); 260 | } 261 | this.updateOutputSize = function () { 262 | let export_scale = this._getChild(".graph-export-panel__export_scale"); 263 | let scale = export_scale.value; 264 | let w = clipBounds.width / this.imageInfo.scale * scale | 0; 265 | let h = clipBounds.height / this.imageInfo.scale * scale | 0; 266 | let info = w + " X " + h; 267 | if (w * h > 3000 * 4000) { 268 | info += "" + getI18NString('Image size is too large, the export may appear memory error') + ""; 269 | } 270 | export_size.innerHTML = info; 271 | } 272 | let resizeHandler = new ResizeBox(canvas.parentNode, onBoundsChange.bind(this)); 273 | this.update = function () { 274 | let ratio = this.canvas.ratio || 1; 275 | let width = this.imageInfo.width / ratio; 276 | let height = this.imageInfo.height / ratio; 277 | this.canvas.setSize(width, height); 278 | resizeHandler.update(width, height); 279 | } 280 | } 281 | 282 | destroy() { 283 | this.graph = null; 284 | this.imageInfo = null 285 | this.clipBounds = null; 286 | this.bounds = null; 287 | } 288 | 289 | _initDom() { 290 | let export_panel = this.html = createElement({ 291 | html: template 292 | }) 293 | export_panel.addEventListener("mousedown", function (evt) { 294 | if (evt.target == export_panel) { 295 | this.destroy(); 296 | } 297 | }.bind(this), false); 298 | let export_scale = this._getChild(".graph-export-panel__export_scale"); 299 | let export_scale_label = this._getChild(".graph-export-panel__export_scale_label"); 300 | export_scale.onchange = function (evt) { 301 | export_scale_label.textContent = this.scale = export_scale.value; 302 | this.updateOutputSize(); 303 | }.bind(this); 304 | this.export_scale = export_scale; 305 | 306 | let export_submit = this._getChild(".graph-export-panel__export_submit"); 307 | export_submit.onclick = this._doExport.bind(this, 'file'); 308 | let print_submit = this._getChild(".graph-export-panel__print_submit"); 309 | print_submit.onclick = this._doExport.bind(this, 'print'); 310 | } 311 | 312 | _doExport(type){ 313 | let scale = this.export_scale.value; 314 | let s = this.imageInfo.scale; 315 | let clipBounds = new Q.Rect(this.clipBounds.x / s, this.clipBounds.y / s, this.clipBounds.width / s, this.clipBounds.height / s); 316 | clipBounds.offset(this.bounds.x, this.bounds.y); 317 | if(type === 'print'){ 318 | return printGraph(this.graph, clipBounds, scale); 319 | } 320 | exportImageFile(this.graph, clipBounds, scale); 321 | } 322 | 323 | show(graph) { 324 | if (!this.html) { 325 | this._initDom(); 326 | } 327 | 328 | showDialog({ 329 | content: this.html 330 | }) 331 | 332 | this.graph = graph; 333 | let bounds = graph.bounds; 334 | this.bounds = bounds; 335 | 336 | let canvas_size = this._getChild(".graph-export-panel__canvas_size"); 337 | canvas_size.textContent = (bounds.width | 0) + " X " + (bounds.height | 0); 338 | 339 | let size = Math.min(500, screen.width / 1.3); 340 | let imageScale; 341 | if (bounds.width > bounds.height) { 342 | imageScale = Math.min(1, size / bounds.width); 343 | } else { 344 | imageScale = Math.min(1, size / bounds.height); 345 | } 346 | if (!this.canvas) { 347 | this.initCanvas(); 348 | } 349 | this.imageInfo = graph.exportImage(imageScale * this.canvas.ratio); 350 | this.imageInfo.scale = imageScale; 351 | 352 | this.update(); 353 | } 354 | } 355 | 356 | let exportPanel; 357 | 358 | export function showExportPanel(graph) { 359 | if (!exportPanel) { 360 | exportPanel = new ExportPanel(); 361 | } 362 | exportPanel.show(graph); 363 | } 364 | 365 | function doPrint(image, doc = document, win = window) { 366 | function __print() { 367 | function closePrint() { 368 | win.removeEventListener('afterprint', closePrint); 369 | doc.body.removeChild(image); 370 | image.classList.remove('q-print__body'); 371 | doc.documentElement.classList.remove('q-print'); 372 | } 373 | 374 | function showPrint() { 375 | doc.documentElement.classList.add('q-print'); 376 | image.classList.add('q-print__body'); 377 | doc.body.appendChild(image); 378 | win.removeEventListener('beforeprint', showPrint); 379 | win.addEventListener('afterprint', closePrint); 380 | } 381 | 382 | win.addEventListener('beforeprint', showPrint); 383 | win.print(); 384 | ///ie, edge浏览器不会阻塞,异步调用print 385 | ///safari第一次会阻塞进程,但第二次则不会阻塞,会显示是否打印的确认框,如果点击取消,不会进入打印状态 386 | } 387 | 388 | if (!image.width) { 389 | image.onload = __print 390 | } else { 391 | __print(); 392 | } 393 | } 394 | 395 | function getStyleByID(id, doc = document) { 396 | let style = doc.getElementById(id); 397 | if (style && style.sheet) { 398 | return style; 399 | } 400 | style = doc.createElement('style'); 401 | style.id = id; 402 | doc.head.insertBefore(style, doc.head.childNodes[0]); 403 | return style; 404 | } 405 | 406 | function initPrintStyle(doc = document) { 407 | const print_css = ` 408 | @media print { 409 | html.q-print, html.q-print > body { 410 | margin: 0 !important; 411 | box-sizing: border-box !important; 412 | height: 100% !important; 413 | width: 100% !important; 414 | display: flex !important; 415 | align-items: center !important; 416 | } 417 | html.q-print > body > *:not(.q-print__body){ 418 | display: none !important; 419 | } 420 | } 421 | .q-print__body { 422 | position: absolute; 423 | margin: auto; 424 | top: 0px; 425 | left: 0px; 426 | right: 0px; 427 | bottom: 0px; 428 | max-width: 100%; 429 | max-height: 100%; 430 | flex: 0 0 auto; 431 | }` 432 | getStyleByID('qunee-styles__print', doc).appendChild(doc.createTextNode(print_css)); 433 | } 434 | 435 | initPrintStyle(); 436 | 437 | function showImageInNewWindow(imageInfo, name, print) { 438 | let win = window.open(); 439 | let doc = win.document; 440 | doc.title = name || "export image"; 441 | doc.body.style.textAlign = "center"; 442 | doc.body.style.margin = "0px"; 443 | 444 | if (print === true) { 445 | let style = doc.createElement("style"); 446 | style.setAttribute("type", "text/css"); 447 | style.setAttribute("media", "print"); 448 | let printCSS = "img {max-width: 100%; max-height: 100%;}"; 449 | if (imageInfo.width / imageInfo.height > 1.2) { 450 | printCSS += "\n @page { size: landscape; }"; 451 | } else { 452 | printCSS += "\n @page { size: portrait; }"; 453 | } 454 | style.appendChild(doc.createTextNode(printCSS)); 455 | doc.head.appendChild(style); 456 | } 457 | 458 | let img = doc.createElement("img"); 459 | let imageStyles = { 460 | 'max-width': '100%', 461 | 'max-height': '100%', 462 | 'position': 'absolute', 463 | 'margin': 'auto', 464 | 'top': 0, 465 | 'left': 0, 466 | 'right': 0, 467 | 'bottom': 0 468 | } 469 | for (let name in imageStyles) { 470 | img.style[name] = imageStyles[name]; 471 | } 472 | 473 | if (print === true) { 474 | img.onload = function () { 475 | win.print(); 476 | win.close(); 477 | } 478 | } 479 | img.src = imageInfo.data; 480 | doc.body.appendChild(img); 481 | } 482 | 483 | function printGraph(graph, bounds, scale) { 484 | let imageInfo = exportImageInfo(graph, bounds, scale); 485 | let img = document.createElement("img"); 486 | img.src = imageInfo.data; 487 | doPrint(img); 488 | } 489 | 490 | function exportImageFile(graph, bounds, scale){ 491 | let name = graph.name || 'graph'; 492 | let imageInfo = exportImageInfo(graph, bounds, scale); 493 | if (!isBlobSupported()) { 494 | return showImageInNewWindow(imageInfo, name); 495 | } 496 | saveImage(imageInfo, name); 497 | } 498 | 499 | const maxLength = 32767, maxSize = 16384 * 16384; 500 | 501 | function isImageTooBig(width, height) { 502 | return (Q.isFirefox || Q.isChrome) && width >= maxLength || height >= maxLength || width * height >= maxSize; 503 | } 504 | 505 | function exportImageInfo(graph, bounds, scale) { 506 | scale = parseFloat(scale) || 1; 507 | let width = Math.ceil(bounds.width * scale); 508 | let height = Math.ceil(bounds.height * scale); 509 | 510 | if (!isImageTooBig(width, height)) { 511 | return graph.exportImage(scale, bounds); 512 | } 513 | 514 | //图片太大,超过支持尺寸 515 | let hCount = Math.ceil((width + 1) / maxLength); 516 | let vCount = Math.ceil((height + 1) / maxLength); 517 | let minCells = Math.ceil((width * height + 1) / maxSize); 518 | if (minCells > hCount * vCount) { 519 | if (width > height) { 520 | hCount = Math.ceil(minCells / vCount); 521 | } else { 522 | vCount = Math.ceil(minCells / hCount); 523 | } 524 | } 525 | let cellWidth = Math.ceil(width / hCount), cellHeight = Math.ceil(height / vCount); 526 | 527 | function toImage(x, y) { 528 | let perWidth = cellWidth / scale, perHeight = cellHeight / scale; 529 | return graph.exportImage(scale, new Q.Rect(bounds.x + x * perWidth, bounds.y + y * perHeight, perWidth, perHeight)) 530 | } 531 | 532 | let svg = ''; 534 | let x = 0; 535 | while (x < hCount) { 536 | let y = 0; 537 | while (y < vCount) { 538 | let imageInfo = toImage(x, y); 539 | svg += ''; 541 | y++; 542 | } 543 | x++; 544 | } 545 | svg += ''; 546 | return { 547 | width: width, 548 | height: height, 549 | data: 'data:image/svg+xml, ' + svg, 550 | svg 551 | } 552 | } 553 | 554 | function saveImage(imageInfo, name) { 555 | if (imageInfo.svg) { 556 | return saveSVG(imageInfo.svg, name); 557 | } 558 | saveCanvas(imageInfo.canvas, name); 559 | } 560 | 561 | function saveSVG(svg, name) { 562 | svg = '\n' + svg; 563 | let blob = new Blob([svg], {type: "image/svg+xml"}); 564 | saveAs(blob, name + ".svg"); 565 | } 566 | 567 | function saveCanvas(canvas, name) { 568 | let type = "image/png"; 569 | name += '.png'; 570 | if (canvas.toBlob) { 571 | return canvas.toBlob(function (blob) { 572 | saveAs(blob, name); 573 | }, type); 574 | } 575 | 576 | let binStr = atob(canvas.toDataURL(type).split(',')[1]); 577 | let len = binStr.length, arr = new Uint8Array(len); 578 | 579 | for (let i = 0; i < len; i++) { 580 | arr[i] = binStr.charCodeAt(i); 581 | } 582 | saveAs(new Blob([arr], {type}), name); 583 | } --------------------------------------------------------------------------------