├── 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 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/common/toolbox/ToolBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | 
--------------------------------------------------------------------------------
/src/common/vue/GraphWithToolbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
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 |
2 |
3 |
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 |
2 |
3 |
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 |
9 |
14 | 模板
15 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
18 |
23 |
24 |
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 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
43 |
44 |
45 |
46 |
48 |
49 |
50 |
51 |
52 |
Update
53 |
Submit
54 |
55 |
56 |
57 |
58 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
Update
17 |
Submit
18 |
19 |
20 |
21 |
22 |
23 |
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 |
8 | `
9 |
10 | let color_picker_template = `
11 |
12 |
13 |
14 |
15 |
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 = '';
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 | \
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 | //图片太大,超过