diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js index 6edf02c10b..38eceb6980 100644 --- a/frontend/config/webpack.config.dev.js +++ b/frontend/config/webpack.config.dev.js @@ -53,32 +53,37 @@ module.exports = { // We include the app code last so that if there is a runtime error during // initialization, it doesn't blow up the WebpackDevServer client, and // changing JS code would still trigger a refresh. - ], - wiki: [ - require.resolve('./polyfills'), - require.resolve('react-dev-utils/webpackHotDevClient'), - paths.appSrc + "/wiki.js", - ], - repoview: [ - require.resolve('./polyfills'), - require.resolve('react-dev-utils/webpackHotDevClient'), - paths.appSrc + "/repo-wiki-mode.js", - ], - fileHistory: [ - require.resolve('./polyfills'), - require.resolve('react-dev-utils/webpackHotDevClient'), - paths.appSrc + "/file-history.js", - ], - app: [ - require.resolve('./polyfills'), - require.resolve('react-dev-utils/webpackHotDevClient'), - paths.appSrc + "/app.js", - ], - draftReview: [ - require.resolve('./polyfills'), - require.resolve('react-dev-utils/webpackHotDevClient'), - paths.appSrc + "/draft-review.js", - ] + ], + wiki: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/wiki.js", + ], + repoview: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/repo-wiki-mode.js", + ], + fileHistory: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/file-history.js", + ], + app: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/app.js", + ], + draftReview: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/draft-review.js", + ], + draw: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/draw/draw.js", + ] }, output: { diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js index 9cd16b9fe2..192f7e3aa5 100644 --- a/frontend/config/webpack.config.prod.js +++ b/frontend/config/webpack.config.prod.js @@ -64,6 +64,7 @@ module.exports = { fileHistory: [require.resolve('./polyfills'), paths.appSrc + "/file-history.js"], app: [require.resolve('./polyfills'), paths.appSrc + "/app.js"], draftReview: [require.resolve('./polyfills'), paths.appSrc + "/draft-review.js"], + draw: [require.resolve('./polyfills'), paths.appSrc + "/draw/draw.js"], }, output: { @@ -99,7 +100,7 @@ module.exports = { // for React Native Web. extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'], alias: { - + // Support React Native Web // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ 'react-native': 'react-native-web', @@ -130,7 +131,7 @@ module.exports = { options: { formatter: eslintFormatter, eslintPath: require.resolve('eslint'), - + }, loader: require.resolve('eslint-loader'), }, @@ -158,7 +159,7 @@ module.exports = { include: paths.appSrc, loader: require.resolve('babel-loader'), options: { - + compact: true, }, }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 12c43ddc81..6cec4783c2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -501,7 +501,7 @@ }, "axios": { "version": "0.18.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", "requires": { "follow-redirects": "^1.3.0", @@ -1896,7 +1896,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -1947,7 +1947,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -3838,7 +3838,7 @@ }, "events": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", "dev": true }, @@ -11183,7 +11183,7 @@ }, "yargs": { "version": "3.10.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", "dev": true, "requires": { @@ -11671,7 +11671,7 @@ }, "load-json-file": { "version": "2.0.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { @@ -11961,7 +11961,7 @@ }, "yargs": { "version": "6.6.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", "dev": true, "requires": { diff --git a/frontend/src/draw/draw-viewer.js b/frontend/src/draw/draw-viewer.js new file mode 100644 index 0000000000..9952cd6c94 --- /dev/null +++ b/frontend/src/draw/draw-viewer.js @@ -0,0 +1,145 @@ +import { seafileAPI } from '../utils/seafile-api'; + +var mxRectangle = window.mxRectangle; +var mxGraph = window.mxGraph; +var mxCodec = window.mxCodec; +var mxUtils = window.mxUtils; + +class DrawViewer { + + constructor(graph) { + this.graph = graph; + graph.setEnabled(false); + } + + loadFile() { + seafileAPI.getFileContent(window.app.config.rawPath).then((res) => { + var doc = mxUtils.parseXml(res.data); + console.log(doc.documentElement); + this.setGraphXml(doc.documentElement); + }); + } + + readGraphState(node) { + this.graph.gridEnabled = false; + this.graph.gridSize = parseFloat(node.getAttribute('gridSize')) || window.mxGraph.prototype.gridSize; + this.graph.graphHandler.guidesEnabled = node.getAttribute('guides') != '0'; + this.graph.setTooltips(node.getAttribute('tooltips') != '0'); + this.graph.setConnectable(node.getAttribute('connect') != '0'); + this.graph.connectionArrowsEnabled = node.getAttribute('arrows') != '0'; + this.graph.foldingEnabled = node.getAttribute('fold') != '0'; + + if (this.graph.foldingEnabled) + { + this.graph.foldingEnabled = false; + this.graph.cellRenderer.forceControlClickHandler = this.graph.foldingEnabled; + } + + var ps = node.getAttribute('pageScale'); + + if (ps != null) + { + this.graph.pageScale = ps; + } + else + { + this.graph.pageScale = window.mxGraph.prototype.pageScale; + } + + + this.graph.pageVisible = false; + this.graph.pageBreaksVisible = this.graph.pageVisible; + this.graph.preferPageSize = this.graph.pageBreaksVisible; + + var pw = node.getAttribute('pageWidth'); + var ph = node.getAttribute('pageHeight'); + + if (pw != null && ph != null) + { + this.graph.pageFormat = new mxRectangle(0, 0, parseFloat(pw), parseFloat(ph)); + } + + // Loads the persistent state settings + var bg = node.getAttribute('background'); + + if (bg != null && bg.length > 0) + { + this.graph.background = bg; + } + else + { + this.graph.background = this.graph.defaultGraphBackground; + } + } + + /** + * Sets the XML node for the current diagram. + */ + setGraphXml(node) { + if (node != null) + { + var dec = new mxCodec(node.ownerDocument); + + if (node.nodeName == 'mxGraphModel') + { + this.graph.model.beginUpdate(); + + try + { + this.graph.model.clear(); + this.graph.view.scale = 1; + this.readGraphState(node); + this.updateGraphComponents(); + dec.decode(node, this.graph.getModel()); + } + finally + { + this.graph.model.endUpdate(); + } + } + } + else + { + this.resetGraph(); + this.graph.model.clear(); + } + } + + /** + * Keeps the graph container in sync with the persistent graph state + */ + updateGraphComponents() { + var graph = this.graph; + + if (graph.container != null) + { + graph.view.validateBackground(); + graph.container.style.overflow = (graph.scrollbars) ? 'auto' : 'hidden'; + } + } + + /** + * Sets the XML node for the current diagram. + */ + resetGraph() { + this.graph.gridEnabled = false; + this.graph.graphHandler.guidesEnabled = true; + this.graph.setTooltips(true); + this.graph.setConnectable(true); + this.graph.foldingEnabled = true; + this.graph.scrollbars = this.graph.defaultScrollbars; + this.graph.pageVisible = this.graph.defaultPageVisible; + this.graph.pageBreaksVisible = this.graph.pageVisible; + this.graph.preferPageSize = this.graph.pageBreaksVisible; + this.graph.background = this.graph.defaultGraphBackground; + this.graph.pageScale = mxGraph.prototype.pageScale; + this.graph.pageFormat = mxGraph.prototype.pageFormat; + this.graph.currentScale = 1; + this.graph.currentTranslate.x = 0; + this.graph.currentTranslate.y = 0; + this.updateGraphComponents(); + this.graph.view.setScale(1); + } +} + +export default DrawViewer; diff --git a/frontend/src/draw/draw.js b/frontend/src/draw/draw.js new file mode 100644 index 0000000000..a5cc7e9b93 --- /dev/null +++ b/frontend/src/draw/draw.js @@ -0,0 +1,26 @@ +import { seafileAPI } from '../utils/seafile-api'; +import DrawViewer from './draw-viewer'; + + +function loadFile(editorUi) { + return seafileAPI.getFileContent(window.app.config.rawPath).then((res) => { + var doc = window.mxUtils.parseXml(res.data); + //console.log(doc.documentElement); + editorUi.editor.setGraphXml(doc.documentElement); + editorUi.editor.setModified(false); + editorUi.editor.undoManager.clear(); + }); +} + +function saveFile(content) { + return seafileAPI.getUpdateLink(window.app.config.repoID, window.app.config.parentDir) + .then((res) => { + return seafileAPI.updateFile(res.data, window.app.config.path, + window.app.config.filename, content); + }); +} + + +window.loadFile = loadFile; +window.saveFile = saveFile; +window.DrawViewer = DrawViewer; diff --git a/media/grapheditor/common.css b/media/grapheditor/common.css new file mode 100644 index 0000000000..37bfb0eac0 --- /dev/null +++ b/media/grapheditor/common.css @@ -0,0 +1,162 @@ +div.mxRubberband { + position: absolute; + overflow: hidden; + border-style: solid; + border-width: 1px; + border-color: #0000FF; + background: #0077FF; +} +.mxCellEditor { + background: url(data:image/gif;base64,R0lGODlhMAAwAIAAAP///wAAACH5BAEAAAAALAAAAAAwADAAAAIxhI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8egpAAA7); + _background: url('../images/transparent.gif'); + border-color: transparent; + border-style: solid; + display: inline-block; + position: absolute; + overflow: visible; + word-wrap: normal; + border-width: 0; + min-width: 1px; + resize: none; + padding: 0px; + margin: 0px; +} +.mxPlainTextEditor * { + padding: 0px; + margin: 0px; +} +div.mxWindow { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: url('../images/window.gif'); + border:1px solid #c3c3c3; + position: absolute; + overflow: hidden; + z-index: 1; +} +table.mxWindow { + border-collapse: collapse; + table-layout: fixed; + font-family: Arial; + font-size: 8pt; +} +td.mxWindowTitle { + background: url('../images/window-title.gif') repeat-x; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; + font-weight: bold; + overflow: hidden; + height: 13px; + padding: 2px; + padding-top: 4px; + padding-bottom: 6px; + color: black; +} +td.mxWindowPane { + vertical-align: top; + padding: 0px; +} +div.mxWindowPane { + overflow: hidden; + position: relative; +} +td.mxWindowPane td { + font-family: Arial; + font-size: 8pt; +} +td.mxWindowPane input, td.mxWindowPane select, td.mxWindowPane textarea, td.mxWindowPane radio { + border-color: #8C8C8C; + border-style: solid; + border-width: 1px; + font-family: Arial; + font-size: 8pt; + padding: 1px; +} +td.mxWindowPane button { + background: url('../images/button.gif') repeat-x; + font-family: Arial; + font-size: 8pt; + padding: 2px; + float: left; +} +img.mxToolbarItem { + margin-right: 6px; + margin-bottom: 6px; + border-width: 1px; +} +select.mxToolbarCombo { + vertical-align: top; + border-style: inset; + border-width: 2px; +} +div.mxToolbarComboContainer { + padding: 2px; +} +img.mxToolbarMode { + margin: 2px; + margin-right: 4px; + margin-bottom: 4px; + border-width: 0px; +} +img.mxToolbarModeSelected { + margin: 0px; + margin-right: 2px; + margin-bottom: 2px; + border-width: 2px; + border-style: inset; +} +div.mxTooltip { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: #FFFFCC; + border-style: solid; + border-width: 1px; + border-color: black; + font-family: Arial; + font-size: 8pt; + position: absolute; + cursor: default; + padding: 4px; + color: black; +} +div.mxPopupMenu { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: url('../images/window.gif'); + position: absolute; + border-style: solid; + border-width: 1px; + border-color: black; +} +table.mxPopupMenu { + border-collapse: collapse; + margin-top: 1px; + margin-bottom: 1px; +} +tr.mxPopupMenuItem { + color: black; + cursor: pointer; +} +tr.mxPopupMenuItemHover { + background-color: #000066; + color: #FFFFFF; + cursor: pointer; +} +td.mxPopupMenuItem { + padding: 2px 30px 2px 10px; + white-space: nowrap; + font-family: Arial; + font-size: 8pt; +} +td.mxPopupMenuIcon { + background-color: #D0D0D0; + padding: 2px 4px 2px 4px; +} +.mxDisabled { + opacity: 0.2 !important; + cursor:default !important; +} diff --git a/media/grapheditor/deflate/base64.js b/media/grapheditor/deflate/base64.js new file mode 100644 index 0000000000..1e870ac40d --- /dev/null +++ b/media/grapheditor/deflate/base64.js @@ -0,0 +1,151 @@ + +/** +* +* Base64 encode / decode +* http://www.webtoolkit.info/ +* +**/ + +var Base64 = { + + // private property + _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", + + // public method for encoding + encode : function (input, binary) { + binary = (binary != null) ? binary : false; + var output = ""; + var chr1, chr2, chr3, enc1, enc2, enc3, enc4; + var i = 0; + + if (!binary) + { + input = Base64._utf8_encode(input); + } + + while (i < input.length) { + + chr1 = input.charCodeAt(i++); + chr2 = input.charCodeAt(i++); + chr3 = input.charCodeAt(i++); + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } + + output = output + + this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); + + } + + return output; + }, + + // public method for decoding + decode : function (input, binary) { + binary = (binary != null) ? binary : false; + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + + while (i < input.length) { + + enc1 = this._keyStr.indexOf(input.charAt(i++)); + enc2 = this._keyStr.indexOf(input.charAt(i++)); + enc3 = this._keyStr.indexOf(input.charAt(i++)); + enc4 = this._keyStr.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output = output + String.fromCharCode(chr1); + + if (enc3 != 64) { + output = output + String.fromCharCode(chr2); + } + if (enc4 != 64) { + output = output + String.fromCharCode(chr3); + } + + } + + if (!binary) + { + output = Base64._utf8_decode(output); + } + + return output; + + }, + + // private method for UTF-8 encoding + _utf8_encode : function (string) { + string = string.replace(/\r\n/g,"\n"); + var utftext = ""; + + for (var n = 0; n < string.length; n++) { + + var c = string.charCodeAt(n); + + if (c < 128) { + utftext += String.fromCharCode(c); + } + else if((c > 127) && (c < 2048)) { + utftext += String.fromCharCode((c >> 6) | 192); + utftext += String.fromCharCode((c & 63) | 128); + } + else { + utftext += String.fromCharCode((c >> 12) | 224); + utftext += String.fromCharCode(((c >> 6) & 63) | 128); + utftext += String.fromCharCode((c & 63) | 128); + } + + } + + return utftext; + }, + + // private method for UTF-8 decoding + _utf8_decode : function (utftext) { + var string = ""; + var i = 0; + var c = c1 = c2 = 0; + + while ( i < utftext.length ) { + + c = utftext.charCodeAt(i); + + if (c < 128) { + string += String.fromCharCode(c); + i++; + } + else if((c > 191) && (c < 224)) { + c2 = utftext.charCodeAt(i+1); + string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); + i += 2; + } + else { + c2 = utftext.charCodeAt(i+1); + c3 = utftext.charCodeAt(i+2); + string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); + i += 3; + } + + } + + return string; + } + +} diff --git a/media/grapheditor/deflate/pako.min.js b/media/grapheditor/deflate/pako.min.js new file mode 100644 index 0000000000..92405bd2e7 --- /dev/null +++ b/media/grapheditor/deflate/pako.min.js @@ -0,0 +1,3 @@ +/* pako 1.0.3 nodeca/pako */ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.pako=t()}}(function(){return function t(e,a,i){function n(s,o){if(!a[s]){if(!e[s]){var l="function"==typeof require&&require;if(!o&&l)return l(s,!0);if(r)return r(s,!0);var h=new Error("Cannot find module '"+s+"'");throw h.code="MODULE_NOT_FOUND",h}var d=a[s]={exports:{}};e[s][0].call(d.exports,function(t){var a=e[s][1][t];return n(a?a:t)},d,d.exports,t,e,a,i)}return a[s].exports}for(var r="function"==typeof require&&require,s=0;s0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new f,this.strm.avail_out=0;var a=o.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==b)throw new Error(d[a]);if(e.header&&o.deflateSetHeader(this.strm,e.header),e.dictionary){var n;if(n="string"==typeof e.dictionary?h.string2buf(e.dictionary):"[object ArrayBuffer]"===_.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,a=o.deflateSetDictionary(this.strm,n),a!==b)throw new Error(d[a]);this._dict_set=!0}}function n(t,e){var a=new i(e);if(a.push(t,!0),a.err)throw a.msg;return a.result}function r(t,e){return e=e||{},e.raw=!0,n(t,e)}function s(t,e){return e=e||{},e.gzip=!0,n(t,e)}var o=t("./zlib/deflate"),l=t("./utils/common"),h=t("./utils/strings"),d=t("./zlib/messages"),f=t("./zlib/zstream"),_=Object.prototype.toString,u=0,c=4,b=0,g=1,m=2,w=-1,p=0,v=8;i.prototype.push=function(t,e){var a,i,n=this.strm,r=this.options.chunkSize;if(this.ended)return!1;i=e===~~e?e:e===!0?c:u,"string"==typeof t?n.input=h.string2buf(t):"[object ArrayBuffer]"===_.call(t)?n.input=new Uint8Array(t):n.input=t,n.next_in=0,n.avail_in=n.input.length;do{if(0===n.avail_out&&(n.output=new l.Buf8(r),n.next_out=0,n.avail_out=r),a=o.deflate(n,i),a!==g&&a!==b)return this.onEnd(a),this.ended=!0,!1;0!==n.avail_out&&(0!==n.avail_in||i!==c&&i!==m)||("string"===this.options.to?this.onData(h.buf2binstring(l.shrinkBuf(n.output,n.next_out))):this.onData(l.shrinkBuf(n.output,n.next_out)))}while((n.avail_in>0||0===n.avail_out)&&a!==g);return i===c?(a=o.deflateEnd(this.strm),this.onEnd(a),this.ended=!0,a===b):i!==m||(this.onEnd(b),n.avail_out=0,!0)},i.prototype.onData=function(t){this.chunks.push(t)},i.prototype.onEnd=function(t){t===b&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=l.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},a.Deflate=i,a.deflate=n,a.deflateRaw=r,a.gzip=s},{"./utils/common":3,"./utils/strings":4,"./zlib/deflate":8,"./zlib/messages":13,"./zlib/zstream":15}],2:[function(t,e,a){"use strict";function i(t){if(!(this instanceof i))return new i(t);this.options=o.assign({chunkSize:16384,windowBits:0,to:""},t||{});var e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0===(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new f,this.strm.avail_out=0;var a=s.inflateInit2(this.strm,e.windowBits);if(a!==h.Z_OK)throw new Error(d[a]);this.header=new _,s.inflateGetHeader(this.strm,this.header)}function n(t,e){var a=new i(e);if(a.push(t,!0),a.err)throw a.msg;return a.result}function r(t,e){return e=e||{},e.raw=!0,n(t,e)}var s=t("./zlib/inflate"),o=t("./utils/common"),l=t("./utils/strings"),h=t("./zlib/constants"),d=t("./zlib/messages"),f=t("./zlib/zstream"),_=t("./zlib/gzheader"),u=Object.prototype.toString;i.prototype.push=function(t,e){var a,i,n,r,d,f,_=this.strm,c=this.options.chunkSize,b=this.options.dictionary,g=!1;if(this.ended)return!1;i=e===~~e?e:e===!0?h.Z_FINISH:h.Z_NO_FLUSH,"string"==typeof t?_.input=l.binstring2buf(t):"[object ArrayBuffer]"===u.call(t)?_.input=new Uint8Array(t):_.input=t,_.next_in=0,_.avail_in=_.input.length;do{if(0===_.avail_out&&(_.output=new o.Buf8(c),_.next_out=0,_.avail_out=c),a=s.inflate(_,h.Z_NO_FLUSH),a===h.Z_NEED_DICT&&b&&(f="string"==typeof b?l.string2buf(b):"[object ArrayBuffer]"===u.call(b)?new Uint8Array(b):b,a=s.inflateSetDictionary(this.strm,f)),a===h.Z_BUF_ERROR&&g===!0&&(a=h.Z_OK,g=!1),a!==h.Z_STREAM_END&&a!==h.Z_OK)return this.onEnd(a),this.ended=!0,!1;_.next_out&&(0!==_.avail_out&&a!==h.Z_STREAM_END&&(0!==_.avail_in||i!==h.Z_FINISH&&i!==h.Z_SYNC_FLUSH)||("string"===this.options.to?(n=l.utf8border(_.output,_.next_out),r=_.next_out-n,d=l.buf2string(_.output,n),_.next_out=r,_.avail_out=c-r,r&&o.arraySet(_.output,_.output,n,r,0),this.onData(d)):this.onData(o.shrinkBuf(_.output,_.next_out)))),0===_.avail_in&&0===_.avail_out&&(g=!0)}while((_.avail_in>0||0===_.avail_out)&&a!==h.Z_STREAM_END);return a===h.Z_STREAM_END&&(i=h.Z_FINISH),i===h.Z_FINISH?(a=s.inflateEnd(this.strm),this.onEnd(a),this.ended=!0,a===h.Z_OK):i!==h.Z_SYNC_FLUSH||(this.onEnd(h.Z_OK),_.avail_out=0,!0)},i.prototype.onData=function(t){this.chunks.push(t)},i.prototype.onEnd=function(t){t===h.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=o.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},a.Inflate=i,a.inflate=n,a.inflateRaw=r,a.ungzip=n},{"./utils/common":3,"./utils/strings":4,"./zlib/constants":6,"./zlib/gzheader":9,"./zlib/inflate":11,"./zlib/messages":13,"./zlib/zstream":15}],3:[function(t,e,a){"use strict";var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;a.assign=function(t){for(var e=Array.prototype.slice.call(arguments,1);e.length;){var a=e.shift();if(a){if("object"!=typeof a)throw new TypeError(a+"must be non-object");for(var i in a)a.hasOwnProperty(i)&&(t[i]=a[i])}}return t},a.shrinkBuf=function(t,e){return t.length===e?t:t.subarray?t.subarray(0,e):(t.length=e,t)};var n={arraySet:function(t,e,a,i,n){if(e.subarray&&t.subarray)return void t.set(e.subarray(a,a+i),n);for(var r=0;r=252?6:l>=248?5:l>=240?4:l>=224?3:l>=192?2:1;o[254]=o[254]=1,a.string2buf=function(t){var e,a,i,r,s,o=t.length,l=0;for(r=0;r>>6,e[s++]=128|63&a):a<65536?(e[s++]=224|a>>>12,e[s++]=128|a>>>6&63,e[s++]=128|63&a):(e[s++]=240|a>>>18,e[s++]=128|a>>>12&63,e[s++]=128|a>>>6&63,e[s++]=128|63&a);return e},a.buf2binstring=function(t){return i(t,t.length)},a.binstring2buf=function(t){for(var e=new n.Buf8(t.length),a=0,i=e.length;a4)h[n++]=65533,a+=s-1;else{for(r&=2===s?31:3===s?15:7;s>1&&a1?h[n++]=65533:r<65536?h[n++]=r:(r-=65536,h[n++]=55296|r>>10&1023,h[n++]=56320|1023&r)}return i(h,n)},a.utf8border=function(t,e){var a;for(e=e||t.length,e>t.length&&(e=t.length),a=e-1;a>=0&&128===(192&t[a]);)a--;return a<0?e:0===a?e:a+o[t[a]]>e?a:e}},{"./common":3}],5:[function(t,e,a){"use strict";function i(t,e,a,i){for(var n=65535&t|0,r=t>>>16&65535|0,s=0;0!==a;){s=a>2e3?2e3:a,a-=s;do n=n+e[i++]|0,r=r+n|0;while(--s);n%=65521,r%=65521}return n|r<<16|0}e.exports=i},{}],6:[function(t,e,a){"use strict";e.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},{}],7:[function(t,e,a){"use strict";function i(){for(var t,e=[],a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t}return e}function n(t,e,a,i){var n=r,s=i+a;t^=-1;for(var o=i;o>>8^n[255&(t^e[o])];return t^-1}var r=i();e.exports=n},{}],8:[function(t,e,a){"use strict";function i(t,e){return t.msg=D[e],e}function n(t){return(t<<1)-(t>4?9:0)}function r(t){for(var e=t.length;--e>=0;)t[e]=0}function s(t){var e=t.state,a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(R.arraySet(t.output,e.pending_buf,e.pending_out,a,t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))}function o(t,e){C._tr_flush_block(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,s(t.strm)}function l(t,e){t.pending_buf[t.pending++]=e}function h(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function d(t,e,a,i){var n=t.avail_in;return n>i&&(n=i),0===n?0:(t.avail_in-=n,R.arraySet(e,t.input,t.next_in,n,a),1===t.state.wrap?t.adler=N(t.adler,e,n,a):2===t.state.wrap&&(t.adler=O(t.adler,e,n,a)),t.next_in+=n,t.total_in+=n,n)}function f(t,e){var a,i,n=t.max_chain_length,r=t.strstart,s=t.prev_length,o=t.nice_match,l=t.strstart>t.w_size-ft?t.strstart-(t.w_size-ft):0,h=t.window,d=t.w_mask,f=t.prev,_=t.strstart+dt,u=h[r+s-1],c=h[r+s];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do if(a=e,h[a+s]===c&&h[a+s-1]===u&&h[a]===h[r]&&h[++a]===h[r+1]){r+=2,a++;do;while(h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&r<_);if(i=dt-(_-r),r=_-dt,i>s){if(t.match_start=e,s=i,i>=o)break;u=h[r+s-1],c=h[r+s]}}while((e=f[e&d])>l&&0!==--n);return s<=t.lookahead?s:t.lookahead}function _(t){var e,a,i,n,r,s=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=s+(s-ft)){R.arraySet(t.window,t.window,s,s,0),t.match_start-=s,t.strstart-=s,t.block_start-=s,a=t.hash_size,e=a;do i=t.head[--e],t.head[e]=i>=s?i-s:0;while(--a);a=s,e=a;do i=t.prev[--e],t.prev[e]=i>=s?i-s:0;while(--a);n+=s}if(0===t.strm.avail_in)break;if(a=d(t.strm,t.window,t.strstart+t.lookahead,n),t.lookahead+=a,t.lookahead+t.insert>=ht)for(r=t.strstart-t.insert,t.ins_h=t.window[r],t.ins_h=(t.ins_h<t.pending_buf_size-5&&(a=t.pending_buf_size-5);;){if(t.lookahead<=1){if(_(t),0===t.lookahead&&e===I)return vt;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var i=t.block_start+a;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,o(t,!1),0===t.strm.avail_out))return vt;if(t.strstart-t.block_start>=t.w_size-ft&&(o(t,!1),0===t.strm.avail_out))return vt}return t.insert=0,e===F?(o(t,!0),0===t.strm.avail_out?yt:xt):t.strstart>t.block_start&&(o(t,!1),0===t.strm.avail_out)?vt:vt}function c(t,e){for(var a,i;;){if(t.lookahead=ht&&(t.ins_h=(t.ins_h<=ht)if(i=C._tr_tally(t,t.strstart-t.match_start,t.match_length-ht),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=ht){t.match_length--;do t.strstart++,t.ins_h=(t.ins_h<=ht&&(t.ins_h=(t.ins_h<4096)&&(t.match_length=ht-1)),t.prev_length>=ht&&t.match_length<=t.prev_length){n=t.strstart+t.lookahead-ht,i=C._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-ht),t.lookahead-=t.prev_length-1,t.prev_length-=2;do++t.strstart<=n&&(t.ins_h=(t.ins_h<=ht&&t.strstart>0&&(n=t.strstart-1,i=s[n],i===s[++n]&&i===s[++n]&&i===s[++n])){r=t.strstart+dt;do;while(i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&nt.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=ht?(a=C._tr_tally(t,1,t.match_length-ht),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=C._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(o(t,!1),0===t.strm.avail_out))return vt}return t.insert=0,e===F?(o(t,!0),0===t.strm.avail_out?yt:xt):t.last_lit&&(o(t,!1),0===t.strm.avail_out)?vt:kt}function m(t,e){for(var a;;){if(0===t.lookahead&&(_(t),0===t.lookahead)){if(e===I)return vt;break}if(t.match_length=0,a=C._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(o(t,!1),0===t.strm.avail_out))return vt}return t.insert=0,e===F?(o(t,!0),0===t.strm.avail_out?yt:xt):t.last_lit&&(o(t,!1),0===t.strm.avail_out)?vt:kt}function w(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n}function p(t){t.window_size=2*t.w_size,r(t.head),t.max_lazy_match=Z[t.level].max_lazy,t.good_match=Z[t.level].good_length,t.nice_match=Z[t.level].nice_length,t.max_chain_length=Z[t.level].max_chain,t.strstart=0,t.block_start=0,t.lookahead=0,t.insert=0,t.match_length=t.prev_length=ht-1,t.match_available=0,t.ins_h=0}function v(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=V,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new R.Buf16(2*ot),this.dyn_dtree=new R.Buf16(2*(2*rt+1)),this.bl_tree=new R.Buf16(2*(2*st+1)),r(this.dyn_ltree),r(this.dyn_dtree),r(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new R.Buf16(lt+1),this.heap=new R.Buf16(2*nt+1),r(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new R.Buf16(2*nt+1),r(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function k(t){var e;return t&&t.state?(t.total_in=t.total_out=0,t.data_type=Q,e=t.state,e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=e.wrap?ut:wt,t.adler=2===e.wrap?0:1,e.last_flush=I,C._tr_init(e),H):i(t,K)}function y(t){var e=k(t);return e===H&&p(t.state),e}function x(t,e){return t&&t.state?2!==t.state.wrap?K:(t.state.gzhead=e,H):K}function z(t,e,a,n,r,s){if(!t)return K;var o=1;if(e===Y&&(e=6),n<0?(o=0,n=-n):n>15&&(o=2,n-=16),r<1||r>$||a!==V||n<8||n>15||e<0||e>9||s<0||s>W)return i(t,K);8===n&&(n=9);var l=new v;return t.state=l,l.strm=t,l.wrap=o,l.gzhead=null,l.w_bits=n,l.w_size=1<L||e<0)return t?i(t,K):K;if(o=t.state,!t.output||!t.input&&0!==t.avail_in||o.status===pt&&e!==F)return i(t,0===t.avail_out?P:K);if(o.strm=t,a=o.last_flush,o.last_flush=e,o.status===ut)if(2===o.wrap)t.adler=0,l(o,31),l(o,139),l(o,8),o.gzhead?(l(o,(o.gzhead.text?1:0)+(o.gzhead.hcrc?2:0)+(o.gzhead.extra?4:0)+(o.gzhead.name?8:0)+(o.gzhead.comment?16:0)),l(o,255&o.gzhead.time),l(o,o.gzhead.time>>8&255),l(o,o.gzhead.time>>16&255),l(o,o.gzhead.time>>24&255),l(o,9===o.level?2:o.strategy>=G||o.level<2?4:0),l(o,255&o.gzhead.os),o.gzhead.extra&&o.gzhead.extra.length&&(l(o,255&o.gzhead.extra.length),l(o,o.gzhead.extra.length>>8&255)),o.gzhead.hcrc&&(t.adler=O(t.adler,o.pending_buf,o.pending,0)),o.gzindex=0,o.status=ct):(l(o,0),l(o,0),l(o,0),l(o,0),l(o,0),l(o,9===o.level?2:o.strategy>=G||o.level<2?4:0),l(o,zt),o.status=wt);else{var _=V+(o.w_bits-8<<4)<<8,u=-1;u=o.strategy>=G||o.level<2?0:o.level<6?1:6===o.level?2:3,_|=u<<6,0!==o.strstart&&(_|=_t),_+=31-_%31,o.status=wt,h(o,_),0!==o.strstart&&(h(o,t.adler>>>16),h(o,65535&t.adler)),t.adler=1}if(o.status===ct)if(o.gzhead.extra){for(d=o.pending;o.gzindex<(65535&o.gzhead.extra.length)&&(o.pending!==o.pending_buf_size||(o.gzhead.hcrc&&o.pending>d&&(t.adler=O(t.adler,o.pending_buf,o.pending-d,d)),s(t),d=o.pending,o.pending!==o.pending_buf_size));)l(o,255&o.gzhead.extra[o.gzindex]),o.gzindex++;o.gzhead.hcrc&&o.pending>d&&(t.adler=O(t.adler,o.pending_buf,o.pending-d,d)),o.gzindex===o.gzhead.extra.length&&(o.gzindex=0,o.status=bt)}else o.status=bt;if(o.status===bt)if(o.gzhead.name){d=o.pending;do{if(o.pending===o.pending_buf_size&&(o.gzhead.hcrc&&o.pending>d&&(t.adler=O(t.adler,o.pending_buf,o.pending-d,d)),s(t),d=o.pending,o.pending===o.pending_buf_size)){f=1;break}f=o.gzindexd&&(t.adler=O(t.adler,o.pending_buf,o.pending-d,d)),0===f&&(o.gzindex=0,o.status=gt)}else o.status=gt;if(o.status===gt)if(o.gzhead.comment){d=o.pending;do{if(o.pending===o.pending_buf_size&&(o.gzhead.hcrc&&o.pending>d&&(t.adler=O(t.adler,o.pending_buf,o.pending-d,d)),s(t),d=o.pending,o.pending===o.pending_buf_size)){f=1;break}f=o.gzindexd&&(t.adler=O(t.adler,o.pending_buf,o.pending-d,d)),0===f&&(o.status=mt)}else o.status=mt;if(o.status===mt&&(o.gzhead.hcrc?(o.pending+2>o.pending_buf_size&&s(t),o.pending+2<=o.pending_buf_size&&(l(o,255&t.adler),l(o,t.adler>>8&255),t.adler=0,o.status=wt)):o.status=wt),0!==o.pending){if(s(t),0===t.avail_out)return o.last_flush=-1,H}else if(0===t.avail_in&&n(e)<=n(a)&&e!==F)return i(t,P);if(o.status===pt&&0!==t.avail_in)return i(t,P);if(0!==t.avail_in||0!==o.lookahead||e!==I&&o.status!==pt){var c=o.strategy===G?m(o,e):o.strategy===X?g(o,e):Z[o.level].func(o,e);if(c!==yt&&c!==xt||(o.status=pt),c===vt||c===yt)return 0===t.avail_out&&(o.last_flush=-1),H;if(c===kt&&(e===U?C._tr_align(o):e!==L&&(C._tr_stored_block(o,0,0,!1),e===T&&(r(o.head),0===o.lookahead&&(o.strstart=0,o.block_start=0,o.insert=0))),s(t),0===t.avail_out))return o.last_flush=-1,H}return e!==F?H:o.wrap<=0?j:(2===o.wrap?(l(o,255&t.adler),l(o,t.adler>>8&255),l(o,t.adler>>16&255),l(o,t.adler>>24&255),l(o,255&t.total_in),l(o,t.total_in>>8&255),l(o,t.total_in>>16&255),l(o,t.total_in>>24&255)):(h(o,t.adler>>>16),h(o,65535&t.adler)),s(t),o.wrap>0&&(o.wrap=-o.wrap),0!==o.pending?H:j)}function E(t){var e;return t&&t.state?(e=t.state.status,e!==ut&&e!==ct&&e!==bt&&e!==gt&&e!==mt&&e!==wt&&e!==pt?i(t,K):(t.state=null,e===wt?i(t,M):H)):K}function A(t,e){var a,i,n,s,o,l,h,d,f=e.length;if(!t||!t.state)return K;if(a=t.state,s=a.wrap,2===s||1===s&&a.status!==ut||a.lookahead)return K;for(1===s&&(t.adler=N(t.adler,e,f,0)),a.wrap=0,f>=a.w_size&&(0===s&&(r(a.head),a.strstart=0,a.block_start=0,a.insert=0),d=new R.Buf8(a.w_size),R.arraySet(d,e,f-a.w_size,a.w_size,0),e=d,f=a.w_size),o=t.avail_in,l=t.next_in,h=t.input,t.avail_in=f,t.next_in=0,t.input=e,_(a);a.lookahead>=ht;){i=a.strstart,n=a.lookahead-(ht-1);do a.ins_h=(a.ins_h<>>24,b>>>=y,g-=y,y=k>>>16&255,0===y)A[o++]=65535&k;else{if(!(16&y)){if(0===(64&y)){k=m[(65535&k)+(b&(1<>>=y,g-=y),g<15&&(b+=E[r++]<>>24,b>>>=y,g-=y,y=k>>>16&255,!(16&y)){if(0===(64&y)){k=w[(65535&k)+(b&(1<d){t.msg="invalid distance too far back",a.mode=i;break t}if(b>>>=y,g-=y,y=o-l,z>y){if(y=z-y,y>_&&a.sane){t.msg="invalid distance too far back",a.mode=i;break t}if(B=0,S=c,0===u){if(B+=f-y,y2;)A[o++]=S[B++],A[o++]=S[B++],A[o++]=S[B++],x-=3;x&&(A[o++]=S[B++],x>1&&(A[o++]=S[B++]))}else{B=o-z;do A[o++]=A[B++],A[o++]=A[B++],A[o++]=A[B++],x-=3;while(x>2);x&&(A[o++]=A[B++],x>1&&(A[o++]=A[B++]))}break}}break}}while(r>3,r-=x,g-=x<<3,b&=(1<>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function n(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new w.Buf16(320),this.work=new w.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function r(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=T,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new w.Buf32(bt),e.distcode=e.distdyn=new w.Buf32(gt),e.sane=1,e.back=-1,Z):N}function s(t){var e;return t&&t.state?(e=t.state,e.wsize=0,e.whave=0,e.wnext=0,r(t)):N}function o(t,e){var a,i;return t&&t.state?(i=t.state,e<0?(a=0,e=-e):(a=(e>>4)+1,e<48&&(e&=15)),e&&(e<8||e>15)?N:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,s(t))):N}function l(t,e){var a,i;return t?(i=new n,t.state=i,i.window=null,a=o(t,e),a!==Z&&(t.state=null),a):N}function h(t){return l(t,wt)}function d(t){if(pt){var e;for(g=new w.Buf32(512),m=new w.Buf32(32),e=0;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(y(z,t.lens,0,288,g,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;y(B,t.lens,0,32,m,0,t.work,{bits:5}),pt=!1}t.lencode=g,t.lenbits=9,t.distcode=m,t.distbits=5}function f(t,e,a,i){var n,r=t.state;return null===r.window&&(r.wsize=1<=r.wsize?(w.arraySet(r.window,e,a-r.wsize,r.wsize,0),r.wnext=0,r.whave=r.wsize):(n=r.wsize-r.wnext,n>i&&(n=i),w.arraySet(r.window,e,a-i,n,r.wnext),i-=n,i?(w.arraySet(r.window,e,a-i,i,0),r.wnext=i,r.whave=r.wsize):(r.wnext+=n,r.wnext===r.wsize&&(r.wnext=0),r.whave>>8&255,a.check=v(a.check,Et,2,0),_=0,u=0,a.mode=F;break}if(a.flags=0,a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&_)<<8)+(_>>8))%31){t.msg="incorrect header check",a.mode=_t;break}if((15&_)!==U){t.msg="unknown compression method",a.mode=_t;break}if(_>>>=4,u-=4,yt=(15&_)+8,0===a.wbits)a.wbits=yt;else if(yt>a.wbits){t.msg="invalid window size",a.mode=_t;break}a.dmax=1<>8&1),512&a.flags&&(Et[0]=255&_,Et[1]=_>>>8&255,a.check=v(a.check,Et,2,0)),_=0,u=0,a.mode=L;case L:for(;u<32;){if(0===l)break t;l--,_+=n[s++]<>>8&255,Et[2]=_>>>16&255,Et[3]=_>>>24&255,a.check=v(a.check,Et,4,0)),_=0,u=0,a.mode=H;case H:for(;u<16;){if(0===l)break t;l--,_+=n[s++]<>8),512&a.flags&&(Et[0]=255&_,Et[1]=_>>>8&255,a.check=v(a.check,Et,2,0)),_=0,u=0,a.mode=j;case j:if(1024&a.flags){for(;u<16;){if(0===l)break t;l--,_+=n[s++]<>>8&255,a.check=v(a.check,Et,2,0)),_=0,u=0}else a.head&&(a.head.extra=null);a.mode=K;case K:if(1024&a.flags&&(g=a.length,g>l&&(g=l),g&&(a.head&&(yt=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Array(a.head.extra_len)),w.arraySet(a.head.extra,n,s,g,yt)),512&a.flags&&(a.check=v(a.check,n,g,s)),l-=g,s+=g,a.length-=g),a.length))break t;a.length=0,a.mode=M;case M:if(2048&a.flags){if(0===l)break t;g=0;do yt=n[s+g++],a.head&&yt&&a.length<65536&&(a.head.name+=String.fromCharCode(yt));while(yt&&g>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=X;break;case q:for(;u<32;){if(0===l)break t;l--,_+=n[s++]<>>=7&u,u-=7&u,a.mode=ht;break}for(;u<3;){if(0===l)break t;l--,_+=n[s++]<>>=1,u-=1,3&_){case 0:a.mode=J;break;case 1:if(d(a),a.mode=at,e===A){_>>>=2,u-=2;break t}break;case 2:a.mode=$;break;case 3:t.msg="invalid block type",a.mode=_t}_>>>=2,u-=2;break;case J:for(_>>>=7&u,u-=7&u;u<32;){if(0===l)break t;l--,_+=n[s++]<>>16^65535)){t.msg="invalid stored block lengths",a.mode=_t;break}if(a.length=65535&_,_=0,u=0,a.mode=Q,e===A)break t;case Q:a.mode=V;case V:if(g=a.length){if(g>l&&(g=l),g>h&&(g=h),0===g)break t;w.arraySet(r,n,s,g,o),l-=g,s+=g,h-=g,o+=g,a.length-=g;break}a.mode=X;break;case $:for(;u<14;){if(0===l)break t; +l--,_+=n[s++]<>>=5,u-=5,a.ndist=(31&_)+1,_>>>=5,u-=5,a.ncode=(15&_)+4,_>>>=4,u-=4,a.nlen>286||a.ndist>30){t.msg="too many length or distance symbols",a.mode=_t;break}a.have=0,a.mode=tt;case tt:for(;a.have>>=3,u-=3}for(;a.have<19;)a.lens[At[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,zt={bits:a.lenbits},xt=y(x,a.lens,0,19,a.lencode,0,a.work,zt),a.lenbits=zt.bits,xt){t.msg="invalid code lengths set",a.mode=_t;break}a.have=0,a.mode=et;case et:for(;a.have>>24,mt=St>>>16&255,wt=65535&St,!(gt<=u);){if(0===l)break t;l--,_+=n[s++]<>>=gt,u-=gt,a.lens[a.have++]=wt;else{if(16===wt){for(Bt=gt+2;u>>=gt,u-=gt,0===a.have){t.msg="invalid bit length repeat",a.mode=_t;break}yt=a.lens[a.have-1],g=3+(3&_),_>>>=2,u-=2}else if(17===wt){for(Bt=gt+3;u>>=gt,u-=gt,yt=0,g=3+(7&_),_>>>=3,u-=3}else{for(Bt=gt+7;u>>=gt,u-=gt,yt=0,g=11+(127&_),_>>>=7,u-=7}if(a.have+g>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=_t;break}for(;g--;)a.lens[a.have++]=yt}}if(a.mode===_t)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=_t;break}if(a.lenbits=9,zt={bits:a.lenbits},xt=y(z,a.lens,0,a.nlen,a.lencode,0,a.work,zt),a.lenbits=zt.bits,xt){t.msg="invalid literal/lengths set",a.mode=_t;break}if(a.distbits=6,a.distcode=a.distdyn,zt={bits:a.distbits},xt=y(B,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,zt),a.distbits=zt.bits,xt){t.msg="invalid distances set",a.mode=_t;break}if(a.mode=at,e===A)break t;case at:a.mode=it;case it:if(l>=6&&h>=258){t.next_out=o,t.avail_out=h,t.next_in=s,t.avail_in=l,a.hold=_,a.bits=u,k(t,b),o=t.next_out,r=t.output,h=t.avail_out,s=t.next_in,n=t.input,l=t.avail_in,_=a.hold,u=a.bits,a.mode===X&&(a.back=-1);break}for(a.back=0;St=a.lencode[_&(1<>>24,mt=St>>>16&255,wt=65535&St,!(gt<=u);){if(0===l)break t;l--,_+=n[s++]<>pt)],gt=St>>>24,mt=St>>>16&255,wt=65535&St,!(pt+gt<=u);){if(0===l)break t;l--,_+=n[s++]<>>=pt,u-=pt,a.back+=pt}if(_>>>=gt,u-=gt,a.back+=gt,a.length=wt,0===mt){a.mode=lt;break}if(32&mt){a.back=-1,a.mode=X;break}if(64&mt){t.msg="invalid literal/length code",a.mode=_t;break}a.extra=15&mt,a.mode=nt;case nt:if(a.extra){for(Bt=a.extra;u>>=a.extra,u-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=rt;case rt:for(;St=a.distcode[_&(1<>>24,mt=St>>>16&255,wt=65535&St,!(gt<=u);){if(0===l)break t;l--,_+=n[s++]<>pt)],gt=St>>>24,mt=St>>>16&255,wt=65535&St,!(pt+gt<=u);){if(0===l)break t;l--,_+=n[s++]<>>=pt,u-=pt,a.back+=pt}if(_>>>=gt,u-=gt,a.back+=gt,64&mt){t.msg="invalid distance code",a.mode=_t;break}a.offset=wt,a.extra=15&mt,a.mode=st;case st:if(a.extra){for(Bt=a.extra;u>>=a.extra,u-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=_t;break}a.mode=ot;case ot:if(0===h)break t;if(g=b-h,a.offset>g){if(g=a.offset-g,g>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=_t;break}g>a.wnext?(g-=a.wnext,m=a.wsize-g):m=a.wnext-g,g>a.length&&(g=a.length),bt=a.window}else bt=r,m=o-a.offset,g=a.length;g>h&&(g=h),h-=g,a.length-=g;do r[o++]=bt[m++];while(--g);0===a.length&&(a.mode=it);break;case lt:if(0===h)break t;r[o++]=a.length,h--,a.mode=it;break;case ht:if(a.wrap){for(;u<32;){if(0===l)break t;l--,_|=n[s++]<=1&&0===j[N];N--);if(O>N&&(O=N),0===N)return b[g++]=20971520,b[g++]=20971520,w.bits=1,0;for(C=1;C0&&(t===o||1!==N))return-1;for(K[1]=0,Z=1;Zr||t===h&&T>s)return 1;for(var Y=0;;){Y++,B=Z-I,m[R]z?(S=M[P+m[R]],E=L[H+m[R]]):(S=96,E=0),p=1<>I)+v]=B<<24|S<<16|E|0;while(0!==v);for(p=1<>=1;if(0!==p?(F&=p-1,F+=p):F=0,R++,0===--j[Z]){if(Z===N)break;Z=e[a+m[R]]}if(Z>O&&(F&y)!==k){for(0===I&&(I=O),x+=C,D=Z-I,U=1<r||t===h&&T>s)return 1;k=F&y,b[k]=O<<24|D<<16|x-g|0}}return 0!==F&&(b[x+F]=Z-I<<24|64<<16|0),w.bits=O,0}},{"../utils/common":3}],13:[function(t,e,a){"use strict";e.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},{}],14:[function(t,e,a){"use strict";function i(t){for(var e=t.length;--e>=0;)t[e]=0}function n(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}function r(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}function s(t){return t<256?lt[t]:lt[256+(t>>>7)]}function o(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function l(t,e,a){t.bi_valid>W-a?(t.bi_buf|=e<>W-t.bi_valid,t.bi_valid+=a-W):(t.bi_buf|=e<>>=1,a<<=1;while(--e>0);return a>>>1}function f(t){16===t.bi_valid?(o(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}function _(t,e){var a,i,n,r,s,o,l=e.dyn_tree,h=e.max_code,d=e.stat_desc.static_tree,f=e.stat_desc.has_stree,_=e.stat_desc.extra_bits,u=e.stat_desc.extra_base,c=e.stat_desc.max_length,b=0;for(r=0;r<=X;r++)t.bl_count[r]=0;for(l[2*t.heap[t.heap_max]+1]=0,a=t.heap_max+1;ac&&(r=c,b++),l[2*i+1]=r,i>h||(t.bl_count[r]++,s=0,i>=u&&(s=_[i-u]),o=l[2*i],t.opt_len+=o*(r+s),f&&(t.static_len+=o*(d[2*i+1]+s)));if(0!==b){do{for(r=c-1;0===t.bl_count[r];)r--;t.bl_count[r]--,t.bl_count[r+1]+=2,t.bl_count[c]--,b-=2}while(b>0);for(r=c;0!==r;r--)for(i=t.bl_count[r];0!==i;)n=t.heap[--a],n>h||(l[2*n+1]!==r&&(t.opt_len+=(r-l[2*n+1])*l[2*n],l[2*n+1]=r),i--)}}function u(t,e,a){var i,n,r=new Array(X+1),s=0;for(i=1;i<=X;i++)r[i]=s=s+a[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=d(r[o]++,o))}}function c(){var t,e,a,i,r,s=new Array(X+1);for(a=0,i=0;i>=7;i8?o(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0}function m(t,e,a,i){g(t),i&&(o(t,a),o(t,~a)),N.arraySet(t.pending_buf,t.window,e,a,t.pending),t.pending+=a}function w(t,e,a,i){var n=2*e,r=2*a;return t[n]>1;a>=1;a--)p(t,r,a);n=l;do a=t.heap[1],t.heap[1]=t.heap[t.heap_len--],p(t,r,1),i=t.heap[1],t.heap[--t.heap_max]=a,t.heap[--t.heap_max]=i,r[2*n]=r[2*a]+r[2*i],t.depth[n]=(t.depth[a]>=t.depth[i]?t.depth[a]:t.depth[i])+1,r[2*a+1]=r[2*i+1]=n,t.heap[1]=n++,p(t,r,1);while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],_(t,e),u(r,h,t.bl_count)}function y(t,e,a){var i,n,r=-1,s=e[1],o=0,l=7,h=4;for(0===s&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=s,s=e[2*(i+1)+1],++o=3&&0===t.bl_tree[2*nt[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e}function B(t,e,a,i){var n;for(l(t,e-257,5),l(t,a-1,5),l(t,i-4,4),n=0;n>>=1)if(1&a&&0!==t.dyn_ltree[2*e])return D;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return I;for(e=32;e0?(t.strm.data_type===U&&(t.strm.data_type=S(t)),k(t,t.l_desc),k(t,t.d_desc),s=z(t),n=t.opt_len+3+7>>>3,r=t.static_len+3+7>>>3,r<=n&&(n=r)):n=r=a+5,a+4<=n&&e!==-1?A(t,e,a,i):t.strategy===O||r===n?(l(t,(F<<1)+(i?1:0),3),v(t,st,ot)):(l(t,(L<<1)+(i?1:0),3),B(t,t.l_desc.max_code+1,t.d_desc.max_code+1,s+1),v(t,t.dyn_ltree,t.dyn_dtree)),b(t),i&&g(t)}function C(t,e,a){return t.pending_buf[t.d_buf+2*t.last_lit]=e>>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&a,t.last_lit++,0===e?t.dyn_ltree[2*a]++:(t.matches++,e--,t.dyn_ltree[2*(ht[a]+M+1)]++,t.dyn_dtree[2*s(e)]++),t.last_lit===t.lit_bufsize-1}var N=t("../utils/common"),O=4,D=0,I=1,U=2,T=0,F=1,L=2,H=3,j=258,K=29,M=256,P=M+1+K,Y=30,q=19,G=2*P+1,X=15,W=16,J=7,Q=256,V=16,$=17,tt=18,et=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],at=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],it=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],nt=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],rt=512,st=new Array(2*(P+2));i(st);var ot=new Array(2*Y);i(ot);var lt=new Array(rt);i(lt);var ht=new Array(j-H+1);i(ht);var dt=new Array(K);i(dt);var ft=new Array(Y);i(ft);var _t,ut,ct,bt=!1;a._tr_init=E,a._tr_stored_block=A,a._tr_flush_block=R,a._tr_tally=C,a._tr_align=Z},{"../utils/common":3}],15:[function(t,e,a){"use strict";function i(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}e.exports=i},{}],"/":[function(t,e,a){"use strict";var i=t("./lib/utils/common").assign,n=t("./lib/deflate"),r=t("./lib/inflate"),s=t("./lib/zlib/constants"),o={};i(o,n,r,s),e.exports=o},{"./lib/deflate":1,"./lib/inflate":2,"./lib/utils/common":3,"./lib/zlib/constants":6}]},{},[])("/")}); diff --git a/media/grapheditor/images/checkmark.gif b/media/grapheditor/images/checkmark.gif new file mode 100644 index 0000000000..d79444daa9 Binary files /dev/null and b/media/grapheditor/images/checkmark.gif differ diff --git a/media/grapheditor/images/clear.gif b/media/grapheditor/images/clear.gif new file mode 100644 index 0000000000..c6acf0a434 Binary files /dev/null and b/media/grapheditor/images/clear.gif differ diff --git a/media/grapheditor/images/close.png b/media/grapheditor/images/close.png new file mode 100644 index 0000000000..d319efb6e1 Binary files /dev/null and b/media/grapheditor/images/close.png differ diff --git a/media/grapheditor/images/collapsed.gif b/media/grapheditor/images/collapsed.gif new file mode 100644 index 0000000000..ce977743d5 Binary files /dev/null and b/media/grapheditor/images/collapsed.gif differ diff --git a/media/grapheditor/images/dropdown.gif b/media/grapheditor/images/dropdown.gif new file mode 100644 index 0000000000..9c94ea5af2 Binary files /dev/null and b/media/grapheditor/images/dropdown.gif differ diff --git a/media/grapheditor/images/dropdown.png b/media/grapheditor/images/dropdown.png new file mode 100644 index 0000000000..0aeac2db68 Binary files /dev/null and b/media/grapheditor/images/dropdown.png differ diff --git a/media/grapheditor/images/edit.gif b/media/grapheditor/images/edit.gif new file mode 100644 index 0000000000..3c07607352 Binary files /dev/null and b/media/grapheditor/images/edit.gif differ diff --git a/media/grapheditor/images/expanded.gif b/media/grapheditor/images/expanded.gif new file mode 100644 index 0000000000..461297fc80 Binary files /dev/null and b/media/grapheditor/images/expanded.gif differ diff --git a/media/grapheditor/images/grid.gif b/media/grapheditor/images/grid.gif new file mode 100644 index 0000000000..f4e7063e87 Binary files /dev/null and b/media/grapheditor/images/grid.gif differ diff --git a/media/grapheditor/images/handle-fixed.png b/media/grapheditor/images/handle-fixed.png new file mode 100644 index 0000000000..b4b600b1a5 Binary files /dev/null and b/media/grapheditor/images/handle-fixed.png differ diff --git a/media/grapheditor/images/handle-main.png b/media/grapheditor/images/handle-main.png new file mode 100644 index 0000000000..ee067ff811 Binary files /dev/null and b/media/grapheditor/images/handle-main.png differ diff --git a/media/grapheditor/images/handle-rotate.png b/media/grapheditor/images/handle-rotate.png new file mode 100644 index 0000000000..f3dd46e2d1 Binary files /dev/null and b/media/grapheditor/images/handle-rotate.png differ diff --git a/media/grapheditor/images/handle-secondary.png b/media/grapheditor/images/handle-secondary.png new file mode 100644 index 0000000000..b4a30900a9 Binary files /dev/null and b/media/grapheditor/images/handle-secondary.png differ diff --git a/media/grapheditor/images/handle-terminal.png b/media/grapheditor/images/handle-terminal.png new file mode 100644 index 0000000000..ec03b316c1 Binary files /dev/null and b/media/grapheditor/images/handle-terminal.png differ diff --git a/media/grapheditor/images/help.png b/media/grapheditor/images/help.png new file mode 100644 index 0000000000..17ee2eb85e Binary files /dev/null and b/media/grapheditor/images/help.png differ diff --git a/media/grapheditor/images/locked.png b/media/grapheditor/images/locked.png new file mode 100644 index 0000000000..8dbac82415 Binary files /dev/null and b/media/grapheditor/images/locked.png differ diff --git a/media/grapheditor/images/logo.png b/media/grapheditor/images/logo.png new file mode 100644 index 0000000000..053a1eb2b1 Binary files /dev/null and b/media/grapheditor/images/logo.png differ diff --git a/media/grapheditor/images/nocolor.png b/media/grapheditor/images/nocolor.png new file mode 100644 index 0000000000..aec4534d6a Binary files /dev/null and b/media/grapheditor/images/nocolor.png differ diff --git a/media/grapheditor/images/refresh.png b/media/grapheditor/images/refresh.png new file mode 100644 index 0000000000..b131204409 Binary files /dev/null and b/media/grapheditor/images/refresh.png differ diff --git a/media/grapheditor/images/round-drop.png b/media/grapheditor/images/round-drop.png new file mode 100644 index 0000000000..b5e2fb603c Binary files /dev/null and b/media/grapheditor/images/round-drop.png differ diff --git a/media/grapheditor/images/search.png b/media/grapheditor/images/search.png new file mode 100644 index 0000000000..d763a72d3b Binary files /dev/null and b/media/grapheditor/images/search.png differ diff --git a/media/grapheditor/images/tooltip.png b/media/grapheditor/images/tooltip.png new file mode 100644 index 0000000000..ad20c04e87 Binary files /dev/null and b/media/grapheditor/images/tooltip.png differ diff --git a/media/grapheditor/images/transparent.gif b/media/grapheditor/images/transparent.gif new file mode 100644 index 0000000000..76040f2b09 Binary files /dev/null and b/media/grapheditor/images/transparent.gif differ diff --git a/media/grapheditor/images/triangle-down.png b/media/grapheditor/images/triangle-down.png new file mode 100644 index 0000000000..d03b98fc5f Binary files /dev/null and b/media/grapheditor/images/triangle-down.png differ diff --git a/media/grapheditor/images/triangle-left.png b/media/grapheditor/images/triangle-left.png new file mode 100644 index 0000000000..44b8425511 Binary files /dev/null and b/media/grapheditor/images/triangle-left.png differ diff --git a/media/grapheditor/images/triangle-right.png b/media/grapheditor/images/triangle-right.png new file mode 100644 index 0000000000..98656281fc Binary files /dev/null and b/media/grapheditor/images/triangle-right.png differ diff --git a/media/grapheditor/images/triangle-up.png b/media/grapheditor/images/triangle-up.png new file mode 100644 index 0000000000..6d93676735 Binary files /dev/null and b/media/grapheditor/images/triangle-up.png differ diff --git a/media/grapheditor/images/unlocked.png b/media/grapheditor/images/unlocked.png new file mode 100644 index 0000000000..5ea44c1d35 Binary files /dev/null and b/media/grapheditor/images/unlocked.png differ diff --git a/media/grapheditor/index.html b/media/grapheditor/index.html new file mode 100644 index 0000000000..1b270ca296 --- /dev/null +++ b/media/grapheditor/index.html @@ -0,0 +1,110 @@ + + + + + Grapheditor + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/media/grapheditor/js/Actions.js b/media/grapheditor/js/Actions.js new file mode 100644 index 0000000000..0389311cd7 --- /dev/null +++ b/media/grapheditor/js/Actions.js @@ -0,0 +1,1412 @@ +/** + * Copyright (c) 2006-2012, JGraph Ltd + */ +/** + * Constructs the actions object for the given UI. + */ +function Actions(editorUi) +{ + this.editorUi = editorUi; + this.actions = new Object(); + this.init(); +}; + +/** + * Adds the default actions. + */ +Actions.prototype.init = function() +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + var isGraphEnabled = function() + { + return Action.prototype.isEnabled.apply(this, arguments) && graph.isEnabled(); + }; + + // File actions + this.addAction('new...', function() { graph.openLink(ui.getUrl()); }); + this.addAction('open...', function() + { + window.openNew = true; + window.openKey = 'open'; + + ui.openFile(); + }); + this.addAction('import...', function() + { + window.openNew = false; + window.openKey = 'import'; + + // Closes dialog after open + window.openFile = new OpenFile(mxUtils.bind(this, function() + { + ui.hideDialog(); + })); + + window.openFile.setConsumer(mxUtils.bind(this, function(xml, filename) + { + try + { + var doc = mxUtils.parseXml(xml); + editor.graph.setSelectionCells(editor.graph.importGraphModel(doc.documentElement)); + } + catch (e) + { + mxUtils.alert(mxResources.get('invalidOrMissingFile') + ': ' + e.message); + } + })); + + // Removes openFile if dialog is closed + ui.showDialog(new OpenDialog(this).container, 320, 220, true, true, function() + { + window.openFile = null; + }); + }).isEnabled = isGraphEnabled; + this.addAction('save', function() { ui.saveFile(false); }, null, null, Editor.ctrlKey + '+S').isEnabled = isGraphEnabled; + this.addAction('saveAs...', function() { ui.saveFile(true); }, null, null, Editor.ctrlKey + '+Shift+S').isEnabled = isGraphEnabled; + this.addAction('export...', function() { ui.showDialog(new ExportDialog(ui).container, 300, 230, true, true); }); + this.addAction('editDiagram...', function() + { + var dlg = new EditDiagramDialog(ui); + ui.showDialog(dlg.container, 620, 420, true, false); + dlg.init(); + }); + this.addAction('pageSetup...', function() { ui.showDialog(new PageSetupDialog(ui).container, 320, 220, true, true); }).isEnabled = isGraphEnabled; + this.addAction('print...', function() { ui.showDialog(new PrintDialog(ui).container, 300, 180, true, true); }, null, 'sprite-print', Editor.ctrlKey + '+P'); + this.addAction('preview', function() { mxUtils.show(graph, null, 10, 10); }); + + // Edit actions + this.addAction('undo', function() { ui.undo(); }, null, 'sprite-undo', Editor.ctrlKey + '+Z'); + this.addAction('redo', function() { ui.redo(); }, null, 'sprite-redo', (!mxClient.IS_WIN) ? Editor.ctrlKey + '+Shift+Z' : Editor.ctrlKey + '+Y'); + this.addAction('cut', function() { mxClipboard.cut(graph); }, null, 'sprite-cut', Editor.ctrlKey + '+X'); + this.addAction('copy', function() { mxClipboard.copy(graph); }, null, 'sprite-copy', Editor.ctrlKey + '+C'); + this.addAction('paste', function() + { + if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent())) + { + mxClipboard.paste(graph); + } + }, false, 'sprite-paste', Editor.ctrlKey + '+V'); + this.addAction('pasteHere', function(evt) + { + if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent())) + { + graph.getModel().beginUpdate(); + try + { + var cells = mxClipboard.paste(graph); + + if (cells != null) + { + var includeEdges = true; + + for (var i = 0; i < cells.length && includeEdges; i++) + { + includeEdges = includeEdges && graph.model.isEdge(cells[i]); + } + + var t = graph.view.translate; + var s = graph.view.scale; + var dx = t.x; + var dy = t.y; + var bb = null; + + if (cells.length == 1 && includeEdges) + { + var geo = graph.getCellGeometry(cells[0]); + + if (geo != null) + { + bb = geo.getTerminalPoint(true); + } + } + + bb = (bb != null) ? bb : graph.getBoundingBoxFromGeometry(cells, includeEdges); + + if (bb != null) + { + var x = Math.round(graph.snap(graph.popupMenuHandler.triggerX / s - dx)); + var y = Math.round(graph.snap(graph.popupMenuHandler.triggerY / s - dy)); + + graph.cellsMoved(cells, x - bb.x, y - bb.y); + } + } + } + finally + { + graph.getModel().endUpdate(); + } + } + }); + + this.addAction('copySize', function(evt) + { + var cell = graph.getSelectionCell(); + + if (graph.isEnabled() && cell != null && graph.getModel().isVertex(cell)) + { + var geo = graph.getCellGeometry(cell); + + if (geo != null) + { + ui.copiedSize = new mxRectangle(geo.x, geo.y, geo.width, geo.height); + } + } + }, null, null, 'Alt+Shit+X'); + + this.addAction('pasteSize', function(evt) + { + if (graph.isEnabled() && !graph.isSelectionEmpty() && ui.copiedSize != null) + { + graph.getModel().beginUpdate(); + + try + { + var cells = graph.getSelectionCells(); + + for (var i = 0; i < cells.length; i++) + { + if (graph.getModel().isVertex(cells[i])) + { + var geo = graph.getCellGeometry(cells[i]); + + if (geo != null) + { + geo = geo.clone(); + geo.width = ui.copiedSize.width; + geo.height = ui.copiedSize.height; + + graph.getModel().setGeometry(cells[i], geo); + } + } + } + } + finally + { + graph.getModel().endUpdate(); + } + } + }, null, null, 'Alt+Shit+V'); + + function deleteCells(includeEdges) + { + // Cancels interactive operations + graph.escape(); + var cells = graph.getDeletableCells(graph.getSelectionCells()); + + if (cells != null && cells.length > 0) + { + var parents = graph.model.getParents(cells); + graph.removeCells(cells, includeEdges); + + // Selects parents for easier editing of groups + if (parents != null) + { + var select = []; + + for (var i = 0; i < parents.length; i++) + { + if (graph.model.contains(parents[i]) && + (graph.model.isVertex(parents[i]) || + graph.model.isEdge(parents[i]))) + { + select.push(parents[i]); + } + } + + graph.setSelectionCells(select); + } + } + }; + + this.addAction('delete', function(evt) + { + deleteCells(evt != null && mxEvent.isShiftDown(evt)); + }, null, null, 'Delete'); + this.addAction('deleteAll', function() + { + deleteCells(true); + }, null, null, Editor.ctrlKey + '+Delete'); + this.addAction('duplicate', function() + { + graph.setSelectionCells(graph.duplicateCells()); + }, null, null, Editor.ctrlKey + '+D'); + this.put('turn', new Action(mxResources.get('turn') + ' / ' + mxResources.get('reverse'), function() + { + graph.turnShapes(graph.getSelectionCells()); + }, null, null, Editor.ctrlKey + '+R')); + this.addAction('selectVertices', function() { graph.selectVertices(); }, null, null, Editor.ctrlKey + '+Shift+I'); + this.addAction('selectEdges', function() { graph.selectEdges(); }, null, null, Editor.ctrlKey + '+Shift+E'); + this.addAction('selectAll', function() { graph.selectAll(null, true); }, null, null, Editor.ctrlKey + '+A'); + this.addAction('selectNone', function() { graph.clearSelection(); }, null, null, Editor.ctrlKey + '+Shift+A'); + this.addAction('lockUnlock', function() + { + if (!graph.isSelectionEmpty()) + { + graph.getModel().beginUpdate(); + try + { + var defaultValue = graph.isCellMovable(graph.getSelectionCell()) ? 1 : 0; + graph.toggleCellStyles(mxConstants.STYLE_MOVABLE, defaultValue); + graph.toggleCellStyles(mxConstants.STYLE_RESIZABLE, defaultValue); + graph.toggleCellStyles(mxConstants.STYLE_ROTATABLE, defaultValue); + graph.toggleCellStyles(mxConstants.STYLE_DELETABLE, defaultValue); + graph.toggleCellStyles(mxConstants.STYLE_EDITABLE, defaultValue); + graph.toggleCellStyles('connectable', defaultValue); + } + finally + { + graph.getModel().endUpdate(); + } + } + }, null, null, Editor.ctrlKey + '+L'); + + // Navigation actions + this.addAction('home', function() { graph.home(); }, null, null, 'Home'); + this.addAction('exitGroup', function() { graph.exitGroup(); }, null, null, Editor.ctrlKey + '+Shift+Home'); + this.addAction('enterGroup', function() { graph.enterGroup(); }, null, null, Editor.ctrlKey + '+Shift+End'); + this.addAction('collapse', function() { graph.foldCells(true); }, null, null, Editor.ctrlKey + '+Home'); + this.addAction('expand', function() { graph.foldCells(false); }, null, null, Editor.ctrlKey + '+End'); + + // Arrange actions + this.addAction('toFront', function() { graph.orderCells(false); }, null, null, Editor.ctrlKey + '+Shift+F'); + this.addAction('toBack', function() { graph.orderCells(true); }, null, null, Editor.ctrlKey + '+Shift+B'); + this.addAction('group', function() + { + if (graph.getSelectionCount() == 1) + { + graph.setCellStyles('container', '1'); + } + else + { + graph.setSelectionCell(graph.groupCells(null, 0)); + } + }, null, null, Editor.ctrlKey + '+G'); + this.addAction('ungroup', function() + { + if (graph.getSelectionCount() == 1 && graph.getModel().getChildCount(graph.getSelectionCell()) == 0) + { + graph.setCellStyles('container', '0'); + } + else + { + graph.setSelectionCells(graph.ungroupCells()); + } + }, null, null, Editor.ctrlKey + '+Shift+U'); + this.addAction('removeFromGroup', function() { graph.removeCellsFromParent(); }); + // Adds action + this.addAction('edit', function() + { + if (graph.isEnabled()) + { + graph.startEditingAtCell(); + } + }, null, null, 'F2/Enter'); + this.addAction('editData...', function() + { + var cell = graph.getSelectionCell() || graph.getModel().getRoot(); + ui.showDataDialog(cell); + }, null, null, Editor.ctrlKey + '+M'); + this.addAction('editTooltip...', function() + { + var graph = ui.editor.graph; + + if (graph.isEnabled() && !graph.isSelectionEmpty()) + { + var cell = graph.getSelectionCell(); + var tooltip = ''; + + if (mxUtils.isNode(cell.value)) + { + var tmp = cell.value.getAttribute('tooltip'); + + if (tmp != null) + { + tooltip = tmp; + } + } + + var dlg = new TextareaDialog(ui, mxResources.get('editTooltip') + ':', tooltip, function(newValue) + { + graph.setTooltipForCell(cell, newValue); + }); + ui.showDialog(dlg.container, 320, 200, true, true); + dlg.init(); + } + }, null, null, 'Alt+Shift+T'); + this.addAction('openLink', function() + { + var link = graph.getLinkForCell(graph.getSelectionCell()); + + if (link != null) + { + graph.openLink(link); + } + }); + this.addAction('editLink...', function() + { + var graph = ui.editor.graph; + + if (graph.isEnabled() && !graph.isSelectionEmpty()) + { + var cell = graph.getSelectionCell(); + var value = graph.getLinkForCell(cell) || ''; + + ui.showLinkDialog(value, mxResources.get('apply'), function(link) + { + link = mxUtils.trim(link); + graph.setLinkForCell(cell, (link.length > 0) ? link : null); + }); + } + }, null, null, 'Alt+Shift+L'); + this.addAction('insertLink...', function() + { + if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent())) + { + ui.showLinkDialog('', mxResources.get('insert'), function(link, docs) + { + link = mxUtils.trim(link); + + if (link.length > 0) + { + var icon = null; + var title = graph.getLinkTitle(link); + + if (docs != null && docs.length > 0) + { + icon = docs[0].iconUrl; + title = docs[0].name || docs[0].type; + title = title.charAt(0).toUpperCase() + title.substring(1); + + if (title.length > 30) + { + title = title.substring(0, 30) + '...'; + } + } + + var pt = graph.getFreeInsertPoint(); + var linkCell = new mxCell(title, new mxGeometry(pt.x, pt.y, 100, 40), + 'fontColor=#0000EE;fontStyle=4;rounded=1;overflow=hidden;' + ((icon != null) ? + 'shape=label;imageWidth=16;imageHeight=16;spacingLeft=26;align=left;image=' + icon : + 'spacing=10;')); + linkCell.vertex = true; + + graph.setLinkForCell(linkCell, link); + graph.cellSizeUpdated(linkCell, true); + + graph.getModel().beginUpdate(); + try + { + linkCell = graph.addCell(linkCell); + graph.fireEvent(new mxEventObject('cellsInserted', 'cells', [linkCell])); + } + finally + { + graph.getModel().endUpdate(); + } + + graph.setSelectionCell(linkCell); + graph.scrollCellToVisible(graph.getSelectionCell()); + } + }); + } + }).isEnabled = isGraphEnabled; + this.addAction('link...', mxUtils.bind(this, function() + { + var graph = ui.editor.graph; + + if (graph.isEnabled()) + { + if (graph.cellEditor.isContentEditing()) + { + var elt = graph.getSelectedElement(); + var link = graph.getParentByName(elt, 'A', graph.cellEditor.textarea); + var oldValue = ''; + + // Workaround for FF returning the outermost selected element after double + // click on a DOM hierarchy with a link inside (but not as topmost element) + if (link == null && elt != null && elt.getElementsByTagName != null) + { + // Finds all links in the selected DOM and uses the link + // where the selection text matches its text content + var links = elt.getElementsByTagName('a'); + + for (var i = 0; i < links.length && link == null; i++) + { + if (links[i].textContent == elt.textContent) + { + graph.selectNode(links[i]); + link = links[i]; + } + } + } + + if (link != null && link.nodeName == 'A') + { + oldValue = link.getAttribute('href') || ''; + } + + var selState = graph.cellEditor.saveSelection(); + + ui.showLinkDialog(oldValue, mxResources.get('apply'), mxUtils.bind(this, function(value) + { + graph.cellEditor.restoreSelection(selState); + + if (value != null) + { + graph.insertLink(value); + } + })); + } + else if (graph.isSelectionEmpty()) + { + this.get('insertLink').funct(); + } + else + { + this.get('editLink').funct(); + } + } + })).isEnabled = isGraphEnabled; + this.addAction('autosize', function() + { + var cells = graph.getSelectionCells(); + + if (cells != null) + { + graph.getModel().beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var cell = cells[i]; + + if (graph.getModel().getChildCount(cell)) + { + graph.updateGroupBounds([cell], 20); + } + else + { + var state = graph.view.getState(cell); + var geo = graph.getCellGeometry(cell); + + if (graph.getModel().isVertex(cell) && state != null && state.text != null && + geo != null && graph.isWrapping(cell)) + { + geo = geo.clone(); + geo.height = state.text.boundingBox.height / graph.view.scale; + graph.getModel().setGeometry(cell, geo); + } + else + { + graph.updateCellSize(cell); + } + } + } + } + finally + { + graph.getModel().endUpdate(); + } + } + }, null, null, Editor.ctrlKey + '+Shift+Y'); + this.addAction('formattedText', function() + { + var state = graph.getView().getState(graph.getSelectionCell()); + + if (state != null) + { + var value = '1'; + graph.stopEditing(); + + graph.getModel().beginUpdate(); + try + { + if (state.style['html'] == '1') + { + value = null; + var label = graph.convertValueToString(state.cell); + + if (mxUtils.getValue(state.style, 'nl2Br', '1') != '0') + { + // Removes newlines from HTML and converts breaks to newlines + // to match the HTML output in plain text + label = label.replace(/\n/g, '').replace(//g, '\n'); + } + + // Removes HTML tags + var temp = document.createElement('div'); + temp.innerHTML = label; + label = mxUtils.extractTextWithWhitespace(temp.childNodes); + + graph.cellLabelChanged(state.cell, label); + } + else + { + // Converts HTML tags to text + var label = mxUtils.htmlEntities(graph.convertValueToString(state.cell), false); + + if (mxUtils.getValue(state.style, 'nl2Br', '1') != '0') + { + // Converts newlines in plain text to breaks in HTML + // to match the plain text output + label = label.replace(/\n/g, '
'); + } + + graph.cellLabelChanged(state.cell, graph.sanitizeHtml(label)); + } + + graph.setCellStyles('html', value); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', ['html'], + 'values', [(value != null) ? value : '0'], 'cells', + graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + } + }); + this.addAction('wordWrap', function() + { + var state = graph.getView().getState(graph.getSelectionCell()); + var value = 'wrap'; + + graph.stopEditing(); + + if (state != null && state.style[mxConstants.STYLE_WHITE_SPACE] == 'wrap') + { + value = null; + } + + graph.setCellStyles(mxConstants.STYLE_WHITE_SPACE, value); + }); + this.addAction('rotation', function() + { + var value = '0'; + var state = graph.getView().getState(graph.getSelectionCell()); + + if (state != null) + { + value = state.style[mxConstants.STYLE_ROTATION] || value; + } + + var dlg = new FilenameDialog(ui, value, mxResources.get('apply'), function(newValue) + { + if (newValue != null && newValue.length > 0) + { + graph.setCellStyles(mxConstants.STYLE_ROTATION, newValue); + } + }, mxResources.get('enterValue') + ' (' + mxResources.get('rotation') + ' 0-360)'); + + ui.showDialog(dlg.container, 375, 80, true, true); + dlg.init(); + }); + // View actions + this.addAction('resetView', function() + { + graph.zoomTo(1); + ui.resetScrollbars(); + }, null, null, Editor.ctrlKey + '+H'); + this.addAction('zoomIn', function(evt) { graph.zoomIn(); }, null, null, Editor.ctrlKey + ' + (Numpad) / Alt+Mousewheel'); + this.addAction('zoomOut', function(evt) { graph.zoomOut(); }, null, null, Editor.ctrlKey + ' - (Numpad) / Alt+Mousewheel'); + this.addAction('fitWindow', function() { graph.fit(); }, null, null, Editor.ctrlKey + '+Shift+H'); + this.addAction('fitPage', mxUtils.bind(this, function() + { + if (!graph.pageVisible) + { + this.get('pageView').funct(); + } + + var fmt = graph.pageFormat; + var ps = graph.pageScale; + var cw = graph.container.clientWidth - 10; + var ch = graph.container.clientHeight - 10; + var scale = Math.floor(20 * Math.min(cw / fmt.width / ps, ch / fmt.height / ps)) / 20; + graph.zoomTo(scale); + + if (mxUtils.hasScrollbars(graph.container)) + { + var pad = graph.getPagePadding(); + graph.container.scrollTop = pad.y * graph.view.scale - 1; + graph.container.scrollLeft = Math.min(pad.x * graph.view.scale, (graph.container.scrollWidth - graph.container.clientWidth) / 2) - 1; + } + }), null, null, Editor.ctrlKey + '+J'); + this.addAction('fitTwoPages', mxUtils.bind(this, function() + { + if (!graph.pageVisible) + { + this.get('pageView').funct(); + } + + var fmt = graph.pageFormat; + var ps = graph.pageScale; + var cw = graph.container.clientWidth - 10; + var ch = graph.container.clientHeight - 10; + + var scale = Math.floor(20 * Math.min(cw / (2 * fmt.width) / ps, ch / fmt.height / ps)) / 20; + graph.zoomTo(scale); + + if (mxUtils.hasScrollbars(graph.container)) + { + var pad = graph.getPagePadding(); + graph.container.scrollTop = Math.min(pad.y, (graph.container.scrollHeight - graph.container.clientHeight) / 2); + graph.container.scrollLeft = Math.min(pad.x, (graph.container.scrollWidth - graph.container.clientWidth) / 2); + } + }), null, null, Editor.ctrlKey + '+Shift+J'); + this.addAction('fitPageWidth', mxUtils.bind(this, function() + { + if (!graph.pageVisible) + { + this.get('pageView').funct(); + } + + var fmt = graph.pageFormat; + var ps = graph.pageScale; + var cw = graph.container.clientWidth - 10; + + var scale = Math.floor(20 * cw / fmt.width / ps) / 20; + graph.zoomTo(scale); + + if (mxUtils.hasScrollbars(graph.container)) + { + var pad = graph.getPagePadding(); + graph.container.scrollLeft = Math.min(pad.x * graph.view.scale, + (graph.container.scrollWidth - graph.container.clientWidth) / 2); + } + })); + this.put('customZoom', new Action(mxResources.get('custom') + '...', mxUtils.bind(this, function() + { + var dlg = new FilenameDialog(this.editorUi, parseInt(graph.getView().getScale() * 100), mxResources.get('apply'), mxUtils.bind(this, function(newValue) + { + var val = parseInt(newValue); + + if (!isNaN(val) && val > 0) + { + graph.zoomTo(val / 100); + } + }), mxResources.get('zoom') + ' (%)'); + this.editorUi.showDialog(dlg.container, 300, 80, true, true); + dlg.init(); + }), null, null, Editor.ctrlKey + '+0')); + this.addAction('pageScale...', mxUtils.bind(this, function() + { + var dlg = new FilenameDialog(this.editorUi, parseInt(graph.pageScale * 100), mxResources.get('apply'), mxUtils.bind(this, function(newValue) + { + var val = parseInt(newValue); + + if (!isNaN(val) && val > 0) + { + ui.setPageScale(val / 100); + } + }), mxResources.get('pageScale') + ' (%)'); + this.editorUi.showDialog(dlg.container, 300, 80, true, true); + dlg.init(); + })); + + // Option actions + var action = null; + action = this.addAction('grid', function() + { + graph.setGridEnabled(!graph.isGridEnabled()); + ui.fireEvent(new mxEventObject('gridEnabledChanged')); + }, null, null, Editor.ctrlKey + '+Shift+G'); + action.setToggleAction(true); + action.setSelectedCallback(function() { return graph.isGridEnabled(); }); + action.setEnabled(false); + + action = this.addAction('guides', function() + { + graph.graphHandler.guidesEnabled = !graph.graphHandler.guidesEnabled; + ui.fireEvent(new mxEventObject('guidesEnabledChanged')); + }); + action.setToggleAction(true); + action.setSelectedCallback(function() { return graph.graphHandler.guidesEnabled; }); + action.setEnabled(false); + + action = this.addAction('tooltips', function() + { + graph.tooltipHandler.setEnabled(!graph.tooltipHandler.isEnabled()); + }); + action.setToggleAction(true); + action.setSelectedCallback(function() { return graph.tooltipHandler.isEnabled(); }); + + action = this.addAction('collapseExpand', function() + { + var change = new ChangePageSetup(ui); + change.ignoreColor = true; + change.ignoreImage = true; + change.foldingEnabled = !graph.foldingEnabled; + + graph.model.execute(change); + }); + action.setToggleAction(true); + action.setSelectedCallback(function() { return graph.foldingEnabled; }); + action.isEnabled = isGraphEnabled; + action = this.addAction('scrollbars', function() + { + ui.setScrollbars(!ui.hasScrollbars()); + }); + action.setToggleAction(true); + action.setSelectedCallback(function() { return graph.scrollbars; }); + action = this.addAction('pageView', mxUtils.bind(this, function() + { + ui.setPageVisible(!graph.pageVisible); + })); + action.setToggleAction(true); + action.setSelectedCallback(function() { return graph.pageVisible; }); + action = this.addAction('connectionArrows', function() + { + graph.connectionArrowsEnabled = !graph.connectionArrowsEnabled; + ui.fireEvent(new mxEventObject('connectionArrowsChanged')); + }, null, null, 'Alt+Shift+A'); + action.setToggleAction(true); + action.setSelectedCallback(function() { return graph.connectionArrowsEnabled; }); + action = this.addAction('connectionPoints', function() + { + graph.setConnectable(!graph.connectionHandler.isEnabled()); + ui.fireEvent(new mxEventObject('connectionPointsChanged')); + }, null, null, 'Alt+Shift+P'); + action.setToggleAction(true); + action.setSelectedCallback(function() { return graph.connectionHandler.isEnabled(); }); + action = this.addAction('copyConnect', function() + { + graph.connectionHandler.setCreateTarget(!graph.connectionHandler.isCreateTarget()); + ui.fireEvent(new mxEventObject('copyConnectChanged')); + }); + action.setToggleAction(true); + action.setSelectedCallback(function() { return graph.connectionHandler.isCreateTarget(); }); + action.isEnabled = isGraphEnabled; + action = this.addAction('autosave', function() + { + ui.editor.setAutosave(!ui.editor.autosave); + }); + action.setToggleAction(true); + action.setSelectedCallback(function() { return ui.editor.autosave; }); + action.isEnabled = isGraphEnabled; + action.visible = false; + + // Help actions + this.addAction('help', function() + { + var ext = ''; + + if (mxResources.isLanguageSupported(mxClient.language)) + { + ext = '_' + mxClient.language; + } + + graph.openLink(RESOURCES_PATH + '/help' + ext + '.html'); + }); + + var showingAbout = false; + + this.put('about', new Action(mxResources.get('about') + ' Graph Editor...', function() + { + if (!showingAbout) + { + ui.showDialog(new AboutDialog(ui).container, 320, 280, true, true, function() + { + showingAbout = false; + }); + + showingAbout = true; + } + }, null, null, 'F1')); + + // Font style actions + var toggleFontStyle = mxUtils.bind(this, function(key, style, fn, shortcut) + { + return this.addAction(key, function() + { + if (fn != null && graph.cellEditor.isContentEditing()) + { + fn(); + } + else + { + graph.stopEditing(false); + + graph.getModel().beginUpdate(); + try + { + graph.toggleCellStyleFlags(mxConstants.STYLE_FONTSTYLE, style); + + // Removes bold and italic tags and CSS styles inside labels + if ((style & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + graph.updateLabelElements(graph.getSelectionCells(), function(elt) + { + elt.style.fontWeight = null; + + if (elt.nodeName == 'B') + { + graph.replaceElement(elt); + } + }); + } + else if ((style & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + graph.updateLabelElements(graph.getSelectionCells(), function(elt) + { + elt.style.fontStyle = null; + + if (elt.nodeName == 'I') + { + graph.replaceElement(elt); + } + }); + } + else if ((style & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) + { + graph.updateLabelElements(graph.getSelectionCells(), function(elt) + { + elt.style.textDecoration = null; + + if (elt.nodeName == 'U') + { + graph.replaceElement(elt); + } + }); + } + } + finally + { + graph.getModel().endUpdate(); + } + } + }, null, null, shortcut); + }); + + toggleFontStyle('bold', mxConstants.FONT_BOLD, function() { document.execCommand('bold', false, null); }, Editor.ctrlKey + '+B'); + toggleFontStyle('italic', mxConstants.FONT_ITALIC, function() { document.execCommand('italic', false, null); }, Editor.ctrlKey + '+I'); + toggleFontStyle('underline', mxConstants.FONT_UNDERLINE, function() { document.execCommand('underline', false, null); }, Editor.ctrlKey + '+U'); + + // Color actions + this.addAction('fontColor...', function() { ui.menus.pickColor(mxConstants.STYLE_FONTCOLOR, 'forecolor', '000000'); }); + this.addAction('strokeColor...', function() { ui.menus.pickColor(mxConstants.STYLE_STROKECOLOR); }); + this.addAction('fillColor...', function() { ui.menus.pickColor(mxConstants.STYLE_FILLCOLOR); }); + this.addAction('gradientColor...', function() { ui.menus.pickColor(mxConstants.STYLE_GRADIENTCOLOR); }); + this.addAction('backgroundColor...', function() { ui.menus.pickColor(mxConstants.STYLE_LABEL_BACKGROUNDCOLOR, 'backcolor'); }); + this.addAction('borderColor...', function() { ui.menus.pickColor(mxConstants.STYLE_LABEL_BORDERCOLOR); }); + + // Format actions + this.addAction('vertical', function() { ui.menus.toggleStyle(mxConstants.STYLE_HORIZONTAL, true); }); + this.addAction('shadow', function() { ui.menus.toggleStyle(mxConstants.STYLE_SHADOW); }); + this.addAction('solid', function() + { + graph.getModel().beginUpdate(); + try + { + graph.setCellStyles(mxConstants.STYLE_DASHED, null); + graph.setCellStyles(mxConstants.STYLE_DASH_PATTERN, null); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], + 'values', [null, null], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + }); + this.addAction('dashed', function() + { + graph.getModel().beginUpdate(); + try + { + graph.setCellStyles(mxConstants.STYLE_DASHED, '1'); + graph.setCellStyles(mxConstants.STYLE_DASH_PATTERN, null); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], + 'values', ['1', null], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + }); + this.addAction('dotted', function() + { + graph.getModel().beginUpdate(); + try + { + graph.setCellStyles(mxConstants.STYLE_DASHED, '1'); + graph.setCellStyles(mxConstants.STYLE_DASH_PATTERN, '1 4'); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], + 'values', ['1', '1 4'], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + }); + this.addAction('sharp', function() + { + graph.getModel().beginUpdate(); + try + { + graph.setCellStyles(mxConstants.STYLE_ROUNDED, '0'); + graph.setCellStyles(mxConstants.STYLE_CURVED, '0'); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_ROUNDED, mxConstants.STYLE_CURVED], + 'values', ['0', '0'], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + }); + this.addAction('rounded', function() + { + graph.getModel().beginUpdate(); + try + { + graph.setCellStyles(mxConstants.STYLE_ROUNDED, '1'); + graph.setCellStyles(mxConstants.STYLE_CURVED, '0'); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_ROUNDED, mxConstants.STYLE_CURVED], + 'values', ['1', '0'], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + }); + this.addAction('toggleRounded', function() + { + if (!graph.isSelectionEmpty() && graph.isEnabled()) + { + graph.getModel().beginUpdate(); + try + { + var cells = graph.getSelectionCells(); + var state = graph.view.getState(cells[0]); + var style = (state != null) ? state.style : graph.getCellStyle(cells[0]); + var value = (mxUtils.getValue(style, mxConstants.STYLE_ROUNDED, '0') == '1') ? '0' : '1'; + + graph.setCellStyles(mxConstants.STYLE_ROUNDED, value); + graph.setCellStyles(mxConstants.STYLE_CURVED, null); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_ROUNDED, mxConstants.STYLE_CURVED], + 'values', [value, '0'], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + } + }); + this.addAction('curved', function() + { + graph.getModel().beginUpdate(); + try + { + graph.setCellStyles(mxConstants.STYLE_ROUNDED, '0'); + graph.setCellStyles(mxConstants.STYLE_CURVED, '1'); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_ROUNDED, mxConstants.STYLE_CURVED], + 'values', ['0', '1'], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + }); + this.addAction('collapsible', function() + { + var state = graph.view.getState(graph.getSelectionCell()); + var value = '1'; + + if (state != null && graph.getFoldingImage(state) != null) + { + value = '0'; + } + + graph.setCellStyles('collapsible', value); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', ['collapsible'], + 'values', [value], 'cells', graph.getSelectionCells())); + }); + this.addAction('editStyle...', mxUtils.bind(this, function() + { + var cells = graph.getSelectionCells(); + + if (cells != null && cells.length > 0) + { + var model = graph.getModel(); + + var dlg = new TextareaDialog(this.editorUi, mxResources.get('editStyle') + ':', + model.getStyle(cells[0]) || '', function(newValue) + { + if (newValue != null) + { + graph.setCellStyle(mxUtils.trim(newValue), cells); + } + }, null, null, 400, 220); + this.editorUi.showDialog(dlg.container, 420, 300, true, true); + dlg.init(); + } + }), null, null, Editor.ctrlKey + '+E'); + this.addAction('setAsDefaultStyle', function() + { + if (graph.isEnabled() && !graph.isSelectionEmpty()) + { + ui.setDefaultStyle(graph.getSelectionCell()); + } + }, null, null, Editor.ctrlKey + '+Shift+D'); + this.addAction('clearDefaultStyle', function() + { + if (graph.isEnabled()) + { + ui.clearDefaultStyle(); + } + }, null, null, Editor.ctrlKey + '+Shift+R'); + this.addAction('addWaypoint', function() + { + var cell = graph.getSelectionCell(); + + if (cell != null && graph.getModel().isEdge(cell)) + { + var handler = editor.graph.selectionCellsHandler.getHandler(cell); + + if (handler instanceof mxEdgeHandler) + { + var t = graph.view.translate; + var s = graph.view.scale; + var dx = t.x; + var dy = t.y; + + var parent = graph.getModel().getParent(cell); + var pgeo = graph.getCellGeometry(parent); + + while (graph.getModel().isVertex(parent) && pgeo != null) + { + dx += pgeo.x; + dy += pgeo.y; + + parent = graph.getModel().getParent(parent); + pgeo = graph.getCellGeometry(parent); + } + + var x = Math.round(graph.snap(graph.popupMenuHandler.triggerX / s - dx)); + var y = Math.round(graph.snap(graph.popupMenuHandler.triggerY / s - dy)); + + handler.addPointAt(handler.state, x, y); + } + } + }); + this.addAction('removeWaypoint', function() + { + // TODO: Action should run with "this" set to action + var rmWaypointAction = ui.actions.get('removeWaypoint'); + + if (rmWaypointAction.handler != null) + { + // NOTE: Popupevent handled and action updated in Menus.createPopupMenu + rmWaypointAction.handler.removePoint(rmWaypointAction.handler.state, rmWaypointAction.index); + } + }); + this.addAction('clearWaypoints', function() + { + var cells = graph.getSelectionCells(); + + if (cells != null) + { + cells = graph.addAllEdges(cells); + + graph.getModel().beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var cell = cells[i]; + + if (graph.getModel().isEdge(cell)) + { + var geo = graph.getCellGeometry(cell); + + if (geo != null) + { + geo = geo.clone(); + geo.points = null; + graph.getModel().setGeometry(cell, geo); + } + } + } + } + finally + { + graph.getModel().endUpdate(); + } + } + }, null, null, 'Alt+Shift+C'); + action = this.addAction('subscript', mxUtils.bind(this, function() + { + if (graph.cellEditor.isContentEditing()) + { + document.execCommand('subscript', false, null); + } + }), null, null, Editor.ctrlKey + '+,'); + action = this.addAction('superscript', mxUtils.bind(this, function() + { + if (graph.cellEditor.isContentEditing()) + { + document.execCommand('superscript', false, null); + } + }), null, null, Editor.ctrlKey + '+.'); + this.addAction('image...', function() + { + if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent())) + { + var title = mxResources.get('image') + ' (' + mxResources.get('url') + '):'; + var state = graph.getView().getState(graph.getSelectionCell()); + var value = ''; + + if (state != null) + { + value = state.style[mxConstants.STYLE_IMAGE] || value; + } + + var selectionState = graph.cellEditor.saveSelection(); + + ui.showImageDialog(title, value, function(newValue, w, h) + { + // Inserts image into HTML text + if (graph.cellEditor.isContentEditing()) + { + graph.cellEditor.restoreSelection(selectionState); + graph.insertImage(newValue, w, h); + } + else + { + var cells = graph.getSelectionCells(); + + if (newValue != null && (newValue.length > 0 || cells.length > 0)) + { + var select = null; + + graph.getModel().beginUpdate(); + try + { + // Inserts new cell if no cell is selected + if (cells.length == 0) + { + var pt = graph.getFreeInsertPoint(); + cells = [graph.insertVertex(graph.getDefaultParent(), null, '', pt.x, pt.y, w, h, + 'shape=image;imageAspect=0;aspect=fixed;verticalLabelPosition=bottom;verticalAlign=top;')]; + select = cells; + graph.fireEvent(new mxEventObject('cellsInserted', 'cells', select)); + } + + graph.setCellStyles(mxConstants.STYLE_IMAGE, (newValue.length > 0) ? newValue : null, cells); + + // Sets shape only if not already shape with image (label or image) + var state = graph.view.getState(cells[0]); + var style = (state != null) ? state.style : graph.getCellStyle(cells[0]); + + if (style[mxConstants.STYLE_SHAPE] != 'image' && style[mxConstants.STYLE_SHAPE] != 'label') + { + graph.setCellStyles(mxConstants.STYLE_SHAPE, 'image', cells); + } + else if (newValue.length == 0) + { + graph.setCellStyles(mxConstants.STYLE_SHAPE, null, cells); + } + + if (graph.getSelectionCount() == 1) + { + if (w != null && h != null) + { + var cell = cells[0]; + var geo = graph.getModel().getGeometry(cell); + + if (geo != null) + { + geo = geo.clone(); + geo.width = w; + geo.height = h; + graph.getModel().setGeometry(cell, geo); + } + } + } + } + finally + { + graph.getModel().endUpdate(); + } + + if (select != null) + { + graph.setSelectionCells(select); + graph.scrollCellToVisible(select[0]); + } + } + } + }, graph.cellEditor.isContentEditing(), !graph.cellEditor.isContentEditing()); + } + }).isEnabled = isGraphEnabled; + this.addAction('insertImage...', function() + { + if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent())) + { + graph.clearSelection(); + ui.actions.get('image').funct(); + } + }).isEnabled = isGraphEnabled; + action = this.addAction('layers', mxUtils.bind(this, function() + { + if (this.layersWindow == null) + { + // LATER: Check outline window for initial placement + this.layersWindow = new LayersWindow(ui, document.body.offsetWidth - 280, 120, 220, 180); + this.layersWindow.window.addListener('show', function() + { + ui.fireEvent(new mxEventObject('layers')); + }); + this.layersWindow.window.addListener('hide', function() + { + ui.fireEvent(new mxEventObject('layers')); + }); + this.layersWindow.window.setVisible(true); + ui.fireEvent(new mxEventObject('layers')); + } + else + { + this.layersWindow.window.setVisible(!this.layersWindow.window.isVisible()); + } + }), null, null, Editor.ctrlKey + '+Shift+L'); + action.setToggleAction(true); + action.setSelectedCallback(mxUtils.bind(this, function() { return this.layersWindow != null && this.layersWindow.window.isVisible(); })); + action = this.addAction('formatPanel', mxUtils.bind(this, function() + { + ui.toggleFormatPanel(); + }), null, null, Editor.ctrlKey + '+Shift+P'); + action.setToggleAction(true); + action.setSelectedCallback(mxUtils.bind(this, function() { return ui.formatWidth > 0; })); + action = this.addAction('outline', mxUtils.bind(this, function() + { + if (this.outlineWindow == null) + { + // LATER: Check layers window for initial placement + this.outlineWindow = new OutlineWindow(ui, document.body.offsetWidth - 260, 100, 180, 180); + this.outlineWindow.window.addListener('show', function() + { + ui.fireEvent(new mxEventObject('outline')); + }); + this.outlineWindow.window.addListener('hide', function() + { + ui.fireEvent(new mxEventObject('outline')); + }); + this.outlineWindow.window.setVisible(true); + ui.fireEvent(new mxEventObject('outline')); + } + else + { + this.outlineWindow.window.setVisible(!this.outlineWindow.window.isVisible()); + } + }), null, null, Editor.ctrlKey + '+Shift+O'); + + action.setToggleAction(true); + action.setSelectedCallback(mxUtils.bind(this, function() { return this.outlineWindow != null && this.outlineWindow.window.isVisible(); })); +}; + +/** + * Registers the given action under the given name. + */ +Actions.prototype.addAction = function(key, funct, enabled, iconCls, shortcut) +{ + var title; + + if (key.substring(key.length - 3) == '...') + { + key = key.substring(0, key.length - 3); + title = mxResources.get(key) + '...'; + } + else + { + title = mxResources.get(key); + } + + return this.put(key, new Action(title, funct, enabled, iconCls, shortcut)); +}; + +/** + * Registers the given action under the given name. + */ +Actions.prototype.put = function(name, action) +{ + this.actions[name] = action; + + return action; +}; + +/** + * Returns the action for the given name or null if no such action exists. + */ +Actions.prototype.get = function(name) +{ + return this.actions[name]; +}; + +/** + * Constructs a new action for the given parameters. + */ +function Action(label, funct, enabled, iconCls, shortcut) +{ + mxEventSource.call(this); + this.label = label; + this.funct = this.createFunction(funct); + this.enabled = (enabled != null) ? enabled : true; + this.iconCls = iconCls; + this.shortcut = shortcut; + this.visible = true; +}; + +// Action inherits from mxEventSource +mxUtils.extend(Action, mxEventSource); + +/** + * Sets the enabled state of the action and fires a stateChanged event. + */ +Action.prototype.createFunction = function(funct) +{ + return funct; +}; + +/** + * Sets the enabled state of the action and fires a stateChanged event. + */ +Action.prototype.setEnabled = function(value) +{ + if (this.enabled != value) + { + this.enabled = value; + this.fireEvent(new mxEventObject('stateChanged')); + } +}; + +/** + * Sets the enabled state of the action and fires a stateChanged event. + */ +Action.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Sets the enabled state of the action and fires a stateChanged event. + */ +Action.prototype.setToggleAction = function(value) +{ + this.toggleAction = value; +}; + +/** + * Sets the enabled state of the action and fires a stateChanged event. + */ +Action.prototype.setSelectedCallback = function(funct) +{ + this.selectedCallback = funct; +}; + +/** + * Sets the enabled state of the action and fires a stateChanged event. + */ +Action.prototype.isSelected = function() +{ + return this.selectedCallback(); +}; diff --git a/media/grapheditor/js/Dialogs.js b/media/grapheditor/js/Dialogs.js new file mode 100644 index 0000000000..c7dbf6c089 --- /dev/null +++ b/media/grapheditor/js/Dialogs.js @@ -0,0 +1,2542 @@ +/** + * Copyright (c) 2006-2012, JGraph Ltd + */ +/** + * Constructs a new open dialog. + */ +var OpenDialog = function() +{ + var iframe = document.createElement('iframe'); + iframe.style.backgroundColor = 'transparent'; + iframe.allowTransparency = 'true'; + iframe.style.borderStyle = 'none'; + iframe.style.borderWidth = '0px'; + iframe.style.overflow = 'hidden'; + iframe.frameBorder = '0'; + + // Adds padding as a workaround for box model in older IE versions + var dx = (mxClient.IS_VML && (document.documentMode == null || document.documentMode < 8)) ? 20 : 0; + + iframe.setAttribute('width', (((Editor.useLocalStorage) ? 640 : 320) + dx) + 'px'); + iframe.setAttribute('height', (((Editor.useLocalStorage) ? 480 : 220) + dx) + 'px'); + iframe.setAttribute('src', OPEN_FORM); + + this.container = iframe; +}; + +/** + * Constructs a new color dialog. + */ +var ColorDialog = function(editorUi, color, apply, cancelFn) +{ + this.editorUi = editorUi; + + var input = document.createElement('input'); + input.style.marginBottom = '10px'; + input.style.width = '216px'; + + // Required for picker to render in IE + if (mxClient.IS_IE) + { + input.style.marginTop = '10px'; + document.body.appendChild(input); + } + + this.init = function() + { + if (!mxClient.IS_TOUCH) + { + input.focus(); + } + }; + + var picker = new jscolor.color(input); + picker.pickerOnfocus = false; + picker.showPicker(); + + var div = document.createElement('div'); + jscolor.picker.box.style.position = 'relative'; + jscolor.picker.box.style.width = '230px'; + jscolor.picker.box.style.height = '100px'; + jscolor.picker.box.style.paddingBottom = '10px'; + div.appendChild(jscolor.picker.box); + + var center = document.createElement('center'); + + function createRecentColorTable() + { + var table = addPresets((ColorDialog.recentColors.length == 0) ? ['FFFFFF'] : + ColorDialog.recentColors, 11, 'FFFFFF', true); + table.style.marginBottom = '8px'; + + return table; + }; + + function addPresets(presets, rowLength, defaultColor, addResetOption) + { + rowLength = (rowLength != null) ? rowLength : 12; + var table = document.createElement('table'); + table.style.borderCollapse = 'collapse'; + table.setAttribute('cellspacing', '0'); + table.style.marginBottom = '20px'; + table.style.cellSpacing = '0px'; + var tbody = document.createElement('tbody'); + table.appendChild(tbody); + + var rows = presets.length / rowLength; + + for (var row = 0; row < rows; row++) + { + var tr = document.createElement('tr'); + + for (var i = 0; i < rowLength; i++) + { + (function(clr) + { + var td = document.createElement('td'); + td.style.border = '1px solid black'; + td.style.padding = '0px'; + td.style.width = '16px'; + td.style.height = '16px'; + + if (clr == null) + { + clr = defaultColor; + } + + if (clr == 'none') + { + td.style.background = 'url(\'' + Dialog.prototype.noColorImage + '\')'; + } + else + { + td.style.backgroundColor = '#' + clr; + } + + tr.appendChild(td); + + if (clr != null) + { + td.style.cursor = 'pointer'; + + mxEvent.addListener(td, 'click', function() + { + if (clr == 'none') + { + picker.fromString('ffffff'); + input.value = 'none'; + } + else + { + picker.fromString(clr); + } + }); + } + })(presets[row * rowLength + i]); + } + + tbody.appendChild(tr); + } + + if (addResetOption) + { + var td = document.createElement('td'); + td.setAttribute('title', mxResources.get('reset')); + td.style.border = '1px solid black'; + td.style.padding = '0px'; + td.style.width = '16px'; + td.style.height = '16px'; + td.style.backgroundImage = 'url(\'' + Dialog.prototype.closeImage + '\')'; + td.style.backgroundPosition = 'center center'; + td.style.backgroundRepeat = 'no-repeat'; + td.style.cursor = 'pointer'; + + tr.appendChild(td); + + mxEvent.addListener(td, 'click', function() + { + ColorDialog.resetRecentColors(); + table.parentNode.replaceChild(createRecentColorTable(), table); + }); + } + + center.appendChild(table); + + return table; + }; + + div.appendChild(input); + mxUtils.br(div); + + // Adds recent colors + createRecentColorTable(); + + // Adds presets + var table = addPresets(this.presetColors); + table.style.marginBottom = '8px'; + table = addPresets(this.defaultColors); + table.style.marginBottom = '16px'; + + div.appendChild(center); + + var buttons = document.createElement('div'); + buttons.style.textAlign = 'right'; + buttons.style.whiteSpace = 'nowrap'; + + var cancelBtn = mxUtils.button(mxResources.get('cancel'), function() + { + editorUi.hideDialog(); + + if (cancelFn != null) + { + cancelFn(); + } + }); + cancelBtn.className = 'geBtn'; + + if (editorUi.editor.cancelFirst) + { + buttons.appendChild(cancelBtn); + } + + var applyFunction = (apply != null) ? apply : this.createApplyFunction(); + + var applyBtn = mxUtils.button(mxResources.get('apply'), function() + { + var color = input.value; + ColorDialog.addRecentColor(color, 12); + + if (color != 'none' && color.charAt(0) != '#') + { + color = '#' + color; + } + + applyFunction(color); + editorUi.hideDialog(); + }); + applyBtn.className = 'geBtn gePrimaryBtn'; + buttons.appendChild(applyBtn); + + if (!editorUi.editor.cancelFirst) + { + buttons.appendChild(cancelBtn); + } + + if (color != null) + { + if (color == 'none') + { + picker.fromString('ffffff'); + input.value = 'none'; + } + else + { + picker.fromString(color); + } + } + + div.appendChild(buttons); + this.picker = picker; + this.colorInput = input; + + // LATER: Only fires if input if focused, should always + // fire if this dialog is showing. + mxEvent.addListener(div, 'keydown', function(e) + { + if (e.keyCode == 27) + { + editorUi.hideDialog(); + + if (cancelFn != null) + { + cancelFn(); + } + + mxEvent.consume(e); + } + }); + + this.container = div; +}; + +/** + * Creates function to apply value + */ +ColorDialog.prototype.presetColors = ['E6D0DE', 'CDA2BE', 'B5739D', 'E1D5E7', 'C3ABD0', 'A680B8', 'D4E1F5', 'A9C4EB', '7EA6E0', 'D5E8D4', '9AC7BF', '67AB9F', 'D5E8D4', 'B9E0A5', '97D077', 'FFF2CC', 'FFE599', 'FFD966', 'FFF4C3', 'FFCE9F', 'FFB570', 'F8CECC', 'F19C99', 'EA6B66']; + +/** + * Creates function to apply value + */ +ColorDialog.prototype.defaultColors = ['none', 'FFFFFF', 'E6E6E6', 'CCCCCC', 'B3B3B3', '999999', '808080', '666666', '4D4D4D', '333333', '1A1A1A', '000000', 'FFCCCC', 'FFE6CC', 'FFFFCC', 'E6FFCC', 'CCFFCC', 'CCFFE6', 'CCFFFF', 'CCE5FF', 'CCCCFF', 'E5CCFF', 'FFCCFF', 'FFCCE6', + 'FF9999', 'FFCC99', 'FFFF99', 'CCFF99', '99FF99', '99FFCC', '99FFFF', '99CCFF', '9999FF', 'CC99FF', 'FF99FF', 'FF99CC', 'FF6666', 'FFB366', 'FFFF66', 'B3FF66', '66FF66', '66FFB3', '66FFFF', '66B2FF', '6666FF', 'B266FF', 'FF66FF', 'FF66B3', 'FF3333', 'FF9933', 'FFFF33', + '99FF33', '33FF33', '33FF99', '33FFFF', '3399FF', '3333FF', '9933FF', 'FF33FF', 'FF3399', 'FF0000', 'FF8000', 'FFFF00', '80FF00', '00FF00', '00FF80', '00FFFF', '007FFF', '0000FF', '7F00FF', 'FF00FF', 'FF0080', 'CC0000', 'CC6600', 'CCCC00', '66CC00', '00CC00', '00CC66', + '00CCCC', '0066CC', '0000CC', '6600CC', 'CC00CC', 'CC0066', '990000', '994C00', '999900', '4D9900', '009900', '00994D', '009999', '004C99', '000099', '4C0099', '990099', '99004D', '660000', '663300', '666600', '336600', '006600', '006633', '006666', '003366', '000066', + '330066', '660066', '660033', '330000', '331A00', '333300', '1A3300', '003300', '00331A', '003333', '001933', '000033', '190033', '330033', '33001A']; + +/** + * Creates function to apply value + */ +ColorDialog.prototype.createApplyFunction = function() +{ + return mxUtils.bind(this, function(color) + { + var graph = this.editorUi.editor.graph; + + graph.getModel().beginUpdate(); + try + { + graph.setCellStyles(this.currentColorKey, color); + this.editorUi.fireEvent(new mxEventObject('styleChanged', 'keys', [this.currentColorKey], + 'values', [color], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + }); +}; + +/** + * + */ +ColorDialog.recentColors = []; + +/** + * Adds recent color for later use. + */ +ColorDialog.addRecentColor = function(color, max) +{ + if (color != null) + { + mxUtils.remove(color, ColorDialog.recentColors); + ColorDialog.recentColors.splice(0, 0, color); + + if (ColorDialog.recentColors.length >= max) + { + ColorDialog.recentColors.pop(); + } + } +}; + +/** + * Adds recent color for later use. + */ +ColorDialog.resetRecentColors = function() +{ + ColorDialog.recentColors = []; +}; + +/** + * Constructs a new about dialog. + */ +var AboutDialog = function(editorUi) +{ + var div = document.createElement('div'); + div.setAttribute('align', 'center'); + var h3 = document.createElement('h3'); + mxUtils.write(h3, mxResources.get('about') + ' GraphEditor'); + div.appendChild(h3); + var img = document.createElement('img'); + img.style.border = '0px'; + img.setAttribute('width', '176'); + img.setAttribute('width', '151'); + img.setAttribute('src', IMAGE_PATH + '/logo.png'); + div.appendChild(img); + mxUtils.br(div); + mxUtils.write(div, 'Powered by mxGraph ' + mxClient.VERSION); + mxUtils.br(div); + var link = document.createElement('a'); + link.setAttribute('href', 'http://www.jgraph.com/'); + link.setAttribute('target', '_blank'); + mxUtils.write(link, 'www.jgraph.com'); + div.appendChild(link); + mxUtils.br(div); + mxUtils.br(div); + var closeBtn = mxUtils.button(mxResources.get('close'), function() + { + editorUi.hideDialog(); + }); + closeBtn.className = 'geBtn gePrimaryBtn'; + div.appendChild(closeBtn); + + this.container = div; +}; + +/** + * Constructs a new filename dialog. + */ +var FilenameDialog = function(editorUi, filename, buttonText, fn, label, validateFn, content, helpLink, closeOnBtn, cancelFn) +{ + closeOnBtn = (closeOnBtn != null) ? closeOnBtn : true; + var row, td; + + var table = document.createElement('table'); + var tbody = document.createElement('tbody'); + table.style.marginTop = '8px'; + + row = document.createElement('tr'); + + td = document.createElement('td'); + td.style.whiteSpace = 'nowrap'; + td.style.fontSize = '10pt'; + td.style.width = '120px'; + mxUtils.write(td, (label || mxResources.get('filename')) + ':'); + + row.appendChild(td); + + var nameInput = document.createElement('input'); + nameInput.setAttribute('value', filename || ''); + nameInput.style.marginLeft = '4px'; + nameInput.style.width = '180px'; + + var genericBtn = mxUtils.button(buttonText, function() + { + if (validateFn == null || validateFn(nameInput.value)) + { + if (closeOnBtn) + { + editorUi.hideDialog(); + } + + fn(nameInput.value); + } + }); + genericBtn.className = 'geBtn gePrimaryBtn'; + + this.init = function() + { + if (label == null && content != null) + { + return; + } + + nameInput.focus(); + + if (mxClient.IS_GC || mxClient.IS_FF || document.documentMode >= 5 || mxClient.IS_QUIRKS) + { + nameInput.select(); + } + else + { + document.execCommand('selectAll', false, null); + } + + // Installs drag and drop handler for links + if (Graph.fileSupport) + { + // Setup the dnd listeners + var dlg = table.parentNode; + var graph = editorUi.editor.graph; + var dropElt = null; + + mxEvent.addListener(dlg, 'dragleave', function(evt) + { + if (dropElt != null) + { + dropElt.style.backgroundColor = ''; + dropElt = null; + } + + evt.stopPropagation(); + evt.preventDefault(); + }); + + mxEvent.addListener(dlg, 'dragover', mxUtils.bind(this, function(evt) + { + // IE 10 does not implement pointer-events so it can't have a drop highlight + if (dropElt == null && (!mxClient.IS_IE || document.documentMode > 10)) + { + dropElt = nameInput; + dropElt.style.backgroundColor = '#ebf2f9'; + } + + evt.stopPropagation(); + evt.preventDefault(); + })); + + mxEvent.addListener(dlg, 'drop', mxUtils.bind(this, function(evt) + { + if (dropElt != null) + { + dropElt.style.backgroundColor = ''; + dropElt = null; + } + + if (mxUtils.indexOf(evt.dataTransfer.types, 'text/uri-list') >= 0) + { + nameInput.value = decodeURIComponent(evt.dataTransfer.getData('text/uri-list')); + genericBtn.click(); + } + + evt.stopPropagation(); + evt.preventDefault(); + })); + } + }; + + td = document.createElement('td'); + td.appendChild(nameInput); + row.appendChild(td); + + if (label != null || content == null) + { + tbody.appendChild(row); + } + + if (content != null) + { + row = document.createElement('tr'); + td = document.createElement('td'); + td.colSpan = 2; + td.appendChild(content); + row.appendChild(td); + tbody.appendChild(row); + } + + row = document.createElement('tr'); + td = document.createElement('td'); + td.colSpan = 2; + td.style.paddingTop = '20px'; + td.style.whiteSpace = 'nowrap'; + td.setAttribute('align', 'right'); + + var cancelBtn = mxUtils.button(mxResources.get('cancel'), function() + { + editorUi.hideDialog(); + + if (cancelFn != null) + { + cancelFn(); + } + }); + cancelBtn.className = 'geBtn'; + + if (editorUi.editor.cancelFirst) + { + td.appendChild(cancelBtn); + } + + if (helpLink != null) + { + var helpBtn = mxUtils.button(mxResources.get('help'), function() + { + editorUi.editor.graph.openLink(helpLink); + }); + + helpBtn.className = 'geBtn'; + td.appendChild(helpBtn); + } + + mxEvent.addListener(nameInput, 'keypress', function(e) + { + if (e.keyCode == 13) + { + genericBtn.click(); + } + }); + + td.appendChild(genericBtn); + + if (!editorUi.editor.cancelFirst) + { + td.appendChild(cancelBtn); + } + + row.appendChild(td); + tbody.appendChild(row); + table.appendChild(tbody); + + this.container = table; +}; + +/** + * Constructs a new textarea dialog. + */ +var TextareaDialog = function(editorUi, title, url, fn, cancelFn, cancelTitle, w, h, addButtons, noHide, noWrap, applyTitle) +{ + w = (w != null) ? w : 300; + h = (h != null) ? h : 120; + noHide = (noHide != null) ? noHide : false; + var row, td; + + var table = document.createElement('table'); + var tbody = document.createElement('tbody'); + + row = document.createElement('tr'); + + td = document.createElement('td'); + td.style.fontSize = '10pt'; + td.style.width = '100px'; + mxUtils.write(td, title); + + row.appendChild(td); + tbody.appendChild(row); + + row = document.createElement('tr'); + td = document.createElement('td'); + + var nameInput = document.createElement('textarea'); + + if (noWrap) + { + nameInput.setAttribute('wrap', 'off'); + } + + nameInput.setAttribute('spellcheck', 'false'); + nameInput.setAttribute('autocorrect', 'off'); + nameInput.setAttribute('autocomplete', 'off'); + nameInput.setAttribute('autocapitalize', 'off'); + + mxUtils.write(nameInput, url || ''); + nameInput.style.resize = 'none'; + nameInput.style.width = w + 'px'; + nameInput.style.height = h + 'px'; + + this.textarea = nameInput; + + this.init = function() + { + nameInput.focus(); + nameInput.scrollTop = 0; + }; + + td.appendChild(nameInput); + row.appendChild(td); + + tbody.appendChild(row); + + row = document.createElement('tr'); + td = document.createElement('td'); + td.style.paddingTop = '14px'; + td.style.whiteSpace = 'nowrap'; + td.setAttribute('align', 'right'); + + var cancelBtn = mxUtils.button(cancelTitle || mxResources.get('cancel'), function() + { + editorUi.hideDialog(); + + if (cancelFn != null) + { + cancelFn(); + } + }); + cancelBtn.className = 'geBtn'; + + if (editorUi.editor.cancelFirst) + { + td.appendChild(cancelBtn); + } + + if (addButtons != null) + { + addButtons(td, nameInput); + } + + if (fn != null) + { + var genericBtn = mxUtils.button(applyTitle || mxResources.get('apply'), function() + { + if (!noHide) + { + editorUi.hideDialog(); + } + + fn(nameInput.value); + }); + + genericBtn.className = 'geBtn gePrimaryBtn'; + td.appendChild(genericBtn); + } + + if (!editorUi.editor.cancelFirst) + { + td.appendChild(cancelBtn); + } + + row.appendChild(td); + tbody.appendChild(row); + table.appendChild(tbody); + this.container = table; +}; + +/** + * Constructs a new edit file dialog. + */ +var EditDiagramDialog = function(editorUi) +{ + var div = document.createElement('div'); + div.style.textAlign = 'right'; + var textarea = document.createElement('textarea'); + textarea.setAttribute('wrap', 'off'); + textarea.setAttribute('spellcheck', 'false'); + textarea.setAttribute('autocorrect', 'off'); + textarea.setAttribute('autocomplete', 'off'); + textarea.setAttribute('autocapitalize', 'off'); + textarea.style.overflow = 'auto'; + textarea.style.resize = 'none'; + textarea.style.width = '600px'; + textarea.style.height = '360px'; + textarea.style.marginBottom = '16px'; + + textarea.value = mxUtils.getPrettyXml(editorUi.editor.getGraphXml()); + div.appendChild(textarea); + + this.init = function() + { + textarea.focus(); + }; + + // Enables dropping files + if (Graph.fileSupport) + { + function handleDrop(evt) + { + evt.stopPropagation(); + evt.preventDefault(); + + if (evt.dataTransfer.files.length > 0) + { + var file = evt.dataTransfer.files[0]; + var reader = new FileReader(); + + reader.onload = function(e) + { + textarea.value = e.target.result; + }; + + reader.readAsText(file); + } + else + { + textarea.value = editorUi.extractGraphModelFromEvent(evt); + } + }; + + function handleDragOver(evt) + { + evt.stopPropagation(); + evt.preventDefault(); + }; + + // Setup the dnd listeners. + textarea.addEventListener('dragover', handleDragOver, false); + textarea.addEventListener('drop', handleDrop, false); + } + + var cancelBtn = mxUtils.button(mxResources.get('cancel'), function() + { + editorUi.hideDialog(); + }); + cancelBtn.className = 'geBtn'; + + if (editorUi.editor.cancelFirst) + { + div.appendChild(cancelBtn); + } + + var select = document.createElement('select'); + select.style.width = '180px'; + select.className = 'geBtn'; + + if (editorUi.editor.graph.isEnabled()) + { + var replaceOption = document.createElement('option'); + replaceOption.setAttribute('value', 'replace'); + mxUtils.write(replaceOption, mxResources.get('replaceExistingDrawing')); + select.appendChild(replaceOption); + } + + var newOption = document.createElement('option'); + newOption.setAttribute('value', 'new'); + mxUtils.write(newOption, mxResources.get('openInNewWindow')); + + if (EditDiagramDialog.showNewWindowOption) + { + select.appendChild(newOption); + } + + if (editorUi.editor.graph.isEnabled()) + { + var importOption = document.createElement('option'); + importOption.setAttribute('value', 'import'); + mxUtils.write(importOption, mxResources.get('addToExistingDrawing')); + select.appendChild(importOption); + } + + div.appendChild(select); + + var okBtn = mxUtils.button(mxResources.get('ok'), function() + { + // Removes all illegal control characters before parsing + var data = editorUi.editor.graph.zapGremlins(mxUtils.trim(textarea.value)); + var error = null; + + if (select.value == 'new') + { + window.openFile = new OpenFile(function() + { + editorUi.hideDialog(); + window.openFile = null; + }); + + window.openFile.setData(data, null); + editorUi.editor.graph.openLink(editorUi.getUrl()); + } + else if (select.value == 'replace') + { + editorUi.editor.graph.model.beginUpdate(); + try + { + editorUi.editor.setGraphXml(mxUtils.parseXml(data).documentElement); + // LATER: Why is hideDialog between begin-/endUpdate faster? + editorUi.hideDialog(); + } + catch (e) + { + error = e; + } + finally + { + editorUi.editor.graph.model.endUpdate(); + } + } + else if (select.value == 'import') + { + editorUi.editor.graph.model.beginUpdate(); + try + { + var doc = mxUtils.parseXml(data); + var model = new mxGraphModel(); + var codec = new mxCodec(doc); + codec.decode(doc.documentElement, model); + + var children = model.getChildren(model.getChildAt(model.getRoot(), 0)); + editorUi.editor.graph.setSelectionCells(editorUi.editor.graph.importCells(children)); + + // LATER: Why is hideDialog between begin-/endUpdate faster? + editorUi.hideDialog(); + } + catch (e) + { + error = e; + } + finally + { + editorUi.editor.graph.model.endUpdate(); + } + } + + if (error != null) + { + mxUtils.alert(error.message); + } + }); + okBtn.className = 'geBtn gePrimaryBtn'; + div.appendChild(okBtn); + + if (!editorUi.editor.cancelFirst) + { + div.appendChild(cancelBtn); + } + + this.container = div; +}; + +/** + * + */ +EditDiagramDialog.showNewWindowOption = true; + +/** + * Constructs a new export dialog. + */ +var ExportDialog = function(editorUi) +{ + var graph = editorUi.editor.graph; + var bounds = graph.getGraphBounds(); + var scale = graph.view.scale; + + var width = Math.ceil(bounds.width / scale); + var height = Math.ceil(bounds.height / scale); + + var row, td; + + var table = document.createElement('table'); + var tbody = document.createElement('tbody'); + table.setAttribute('cellpadding', (mxClient.IS_SF) ? '0' : '2'); + + row = document.createElement('tr'); + + td = document.createElement('td'); + td.style.fontSize = '10pt'; + td.style.width = '100px'; + mxUtils.write(td, mxResources.get('filename') + ':'); + + row.appendChild(td); + + var nameInput = document.createElement('input'); + nameInput.setAttribute('value', editorUi.editor.getOrCreateFilename()); + nameInput.style.width = '180px'; + + td = document.createElement('td'); + td.appendChild(nameInput); + row.appendChild(td); + + tbody.appendChild(row); + + row = document.createElement('tr'); + + td = document.createElement('td'); + td.style.fontSize = '10pt'; + mxUtils.write(td, mxResources.get('format') + ':'); + + row.appendChild(td); + + var imageFormatSelect = document.createElement('select'); + imageFormatSelect.style.width = '180px'; + + var pngOption = document.createElement('option'); + pngOption.setAttribute('value', 'png'); + mxUtils.write(pngOption, mxResources.get('formatPng')); + imageFormatSelect.appendChild(pngOption); + + var gifOption = document.createElement('option'); + + if (ExportDialog.showGifOption) + { + gifOption.setAttribute('value', 'gif'); + mxUtils.write(gifOption, mxResources.get('formatGif')); + imageFormatSelect.appendChild(gifOption); + } + + var jpgOption = document.createElement('option'); + jpgOption.setAttribute('value', 'jpg'); + mxUtils.write(jpgOption, mxResources.get('formatJpg')); + imageFormatSelect.appendChild(jpgOption); + + var pdfOption = document.createElement('option'); + pdfOption.setAttribute('value', 'pdf'); + mxUtils.write(pdfOption, mxResources.get('formatPdf')); + imageFormatSelect.appendChild(pdfOption); + + var svgOption = document.createElement('option'); + svgOption.setAttribute('value', 'svg'); + mxUtils.write(svgOption, mxResources.get('formatSvg')); + imageFormatSelect.appendChild(svgOption); + + if (ExportDialog.showXmlOption) + { + var xmlOption = document.createElement('option'); + xmlOption.setAttribute('value', 'xml'); + mxUtils.write(xmlOption, mxResources.get('formatXml')); + imageFormatSelect.appendChild(xmlOption); + } + + td = document.createElement('td'); + td.appendChild(imageFormatSelect); + row.appendChild(td); + + tbody.appendChild(row); + + row = document.createElement('tr'); + + td = document.createElement('td'); + td.style.fontSize = '10pt'; + mxUtils.write(td, mxResources.get('zoom') + ' (%):'); + + row.appendChild(td); + + var zoomInput = document.createElement('input'); + zoomInput.setAttribute('type', 'number'); + zoomInput.setAttribute('value', '100'); + zoomInput.style.width = '180px'; + + td = document.createElement('td'); + td.appendChild(zoomInput); + row.appendChild(td); + + tbody.appendChild(row); + + row = document.createElement('tr'); + + td = document.createElement('td'); + td.style.fontSize = '10pt'; + mxUtils.write(td, mxResources.get('width') + ':'); + + row.appendChild(td); + + var widthInput = document.createElement('input'); + widthInput.setAttribute('value', width); + widthInput.style.width = '180px'; + + td = document.createElement('td'); + td.appendChild(widthInput); + row.appendChild(td); + + tbody.appendChild(row); + + row = document.createElement('tr'); + + td = document.createElement('td'); + td.style.fontSize = '10pt'; + mxUtils.write(td, mxResources.get('height') + ':'); + + row.appendChild(td); + + var heightInput = document.createElement('input'); + heightInput.setAttribute('value', height); + heightInput.style.width = '180px'; + + td = document.createElement('td'); + td.appendChild(heightInput); + row.appendChild(td); + + tbody.appendChild(row); + + row = document.createElement('tr'); + + td = document.createElement('td'); + td.style.fontSize = '10pt'; + mxUtils.write(td, mxResources.get('background') + ':'); + + row.appendChild(td); + + var transparentCheckbox = document.createElement('input'); + transparentCheckbox.setAttribute('type', 'checkbox'); + transparentCheckbox.checked = graph.background == null || graph.background == mxConstants.NONE; + + td = document.createElement('td'); + td.appendChild(transparentCheckbox); + mxUtils.write(td, mxResources.get('transparent')); + + row.appendChild(td); + + tbody.appendChild(row); + + row = document.createElement('tr'); + + td = document.createElement('td'); + td.style.fontSize = '10pt'; + mxUtils.write(td, mxResources.get('borderWidth') + ':'); + + row.appendChild(td); + + var borderInput = document.createElement('input'); + borderInput.setAttribute('type', 'number'); + borderInput.setAttribute('value', ExportDialog.lastBorderValue); + borderInput.style.width = '180px'; + + td = document.createElement('td'); + td.appendChild(borderInput); + row.appendChild(td); + + tbody.appendChild(row); + table.appendChild(tbody); + + // Handles changes in the export format + function formatChanged() + { + var name = nameInput.value; + var dot = name.lastIndexOf('.'); + + if (dot > 0) + { + nameInput.value = name.substring(0, dot + 1) + imageFormatSelect.value; + } + else + { + nameInput.value = name + '.' + imageFormatSelect.value; + } + + if (imageFormatSelect.value === 'xml') + { + zoomInput.setAttribute('disabled', 'true'); + widthInput.setAttribute('disabled', 'true'); + heightInput.setAttribute('disabled', 'true'); + borderInput.setAttribute('disabled', 'true'); + } + else + { + zoomInput.removeAttribute('disabled'); + widthInput.removeAttribute('disabled'); + heightInput.removeAttribute('disabled'); + borderInput.removeAttribute('disabled'); + } + + if (imageFormatSelect.value === 'png' || imageFormatSelect.value === 'svg') + { + transparentCheckbox.removeAttribute('disabled'); + } + else + { + transparentCheckbox.setAttribute('disabled', 'disabled'); + } + }; + + mxEvent.addListener(imageFormatSelect, 'change', formatChanged); + formatChanged(); + + function checkValues() + { + if (widthInput.value * heightInput.value > MAX_AREA || widthInput.value <= 0) + { + widthInput.style.backgroundColor = 'red'; + } + else + { + widthInput.style.backgroundColor = ''; + } + + if (widthInput.value * heightInput.value > MAX_AREA || heightInput.value <= 0) + { + heightInput.style.backgroundColor = 'red'; + } + else + { + heightInput.style.backgroundColor = ''; + } + }; + + mxEvent.addListener(zoomInput, 'change', function() + { + var s = Math.max(0, parseFloat(zoomInput.value) || 100) / 100; + zoomInput.value = parseFloat((s * 100).toFixed(2)); + + if (width > 0) + { + widthInput.value = Math.floor(width * s); + heightInput.value = Math.floor(height * s); + } + else + { + zoomInput.value = '100'; + widthInput.value = width; + heightInput.value = height; + } + + checkValues(); + }); + + mxEvent.addListener(widthInput, 'change', function() + { + var s = parseInt(widthInput.value) / width; + + if (s > 0) + { + zoomInput.value = parseFloat((s * 100).toFixed(2)); + heightInput.value = Math.floor(height * s); + } + else + { + zoomInput.value = '100'; + widthInput.value = width; + heightInput.value = height; + } + + checkValues(); + }); + + mxEvent.addListener(heightInput, 'change', function() + { + var s = parseInt(heightInput.value) / height; + + if (s > 0) + { + zoomInput.value = parseFloat((s * 100).toFixed(2)); + widthInput.value = Math.floor(width * s); + } + else + { + zoomInput.value = '100'; + widthInput.value = width; + heightInput.value = height; + } + + checkValues(); + }); + + row = document.createElement('tr'); + td = document.createElement('td'); + td.setAttribute('align', 'right'); + td.style.paddingTop = '22px'; + td.colSpan = 2; + + var saveBtn = mxUtils.button(mxResources.get('export'), mxUtils.bind(this, function() + { + if (parseInt(zoomInput.value) <= 0) + { + mxUtils.alert(mxResources.get('drawingEmpty')); + } + else + { + var name = nameInput.value; + var format = imageFormatSelect.value; + var s = Math.max(0, parseFloat(zoomInput.value) || 100) / 100; + var b = Math.max(0, parseInt(borderInput.value)); + var bg = graph.background; + + if ((format == 'svg' || format == 'png') && transparentCheckbox.checked) + { + bg = null; + } + else if (bg == null || bg == mxConstants.NONE) + { + bg = '#ffffff'; + } + + ExportDialog.lastBorderValue = b; + ExportDialog.exportFile(editorUi, name, format, bg, s, b); + } + })); + saveBtn.className = 'geBtn gePrimaryBtn'; + + var cancelBtn = mxUtils.button(mxResources.get('cancel'), function() + { + editorUi.hideDialog(); + }); + cancelBtn.className = 'geBtn'; + + if (editorUi.editor.cancelFirst) + { + td.appendChild(cancelBtn); + td.appendChild(saveBtn); + } + else + { + td.appendChild(saveBtn); + td.appendChild(cancelBtn); + } + + row.appendChild(td); + tbody.appendChild(row); + table.appendChild(tbody); + this.container = table; +}; + +/** + * Remembers last value for border. + */ +ExportDialog.lastBorderValue = 0; + +/** + * Global switches for the export dialog. + */ +ExportDialog.showGifOption = true; + +/** + * Global switches for the export dialog. + */ +ExportDialog.showXmlOption = true; + +/** + * Hook for getting the export format. Returns null for the default + * intermediate XML export format or a function that returns the + * parameter and value to be used in the request in the form + * key=value, where value should be URL encoded. + */ +ExportDialog.exportFile = function(editorUi, name, format, bg, s, b) +{ + var graph = editorUi.editor.graph; + + if (format == 'xml') + { + ExportDialog.saveLocalFile(editorUi, mxUtils.getXml(editorUi.editor.getGraphXml()), name, format); + } + else if (format == 'svg') + { + ExportDialog.saveLocalFile(editorUi, mxUtils.getXml(graph.getSvg(bg, s, b)), name, format); + } + else + { + var bounds = graph.getGraphBounds(); + + // New image export + var xmlDoc = mxUtils.createXmlDocument(); + var root = xmlDoc.createElement('output'); + xmlDoc.appendChild(root); + + // Renders graph. Offset will be multiplied with state's scale when painting state. + var xmlCanvas = new mxXmlCanvas2D(root); + xmlCanvas.translate(Math.floor((b / s - bounds.x) / graph.view.scale), + Math.floor((b / s - bounds.y) / graph.view.scale)); + xmlCanvas.scale(s / graph.view.scale); + + var imgExport = new mxImageExport() + imgExport.drawState(graph.getView().getState(graph.model.root), xmlCanvas); + + // Puts request data together + var param = 'xml=' + encodeURIComponent(mxUtils.getXml(root)); + var w = Math.ceil(bounds.width * s / graph.view.scale + 2 * b); + var h = Math.ceil(bounds.height * s / graph.view.scale + 2 * b); + + // Requests image if request is valid + if (param.length <= MAX_REQUEST_SIZE && w * h < MAX_AREA) + { + editorUi.hideDialog(); + var req = new mxXmlRequest(EXPORT_URL, 'format=' + format + + '&filename=' + encodeURIComponent(name) + + '&bg=' + ((bg != null) ? bg : 'none') + + '&w=' + w + '&h=' + h + '&' + param); + req.simulate(document, '_blank'); + } + else + { + mxUtils.alert(mxResources.get('drawingTooLarge')); + } + } +}; + +/** + * Hook for getting the export format. Returns null for the default + * intermediate XML export format or a function that returns the + * parameter and value to be used in the request in the form + * key=value, where value should be URL encoded. + */ +ExportDialog.saveLocalFile = function(editorUi, data, filename, format) +{ + if (data.length < MAX_REQUEST_SIZE) + { + editorUi.hideDialog(); + var req = new mxXmlRequest(SAVE_URL, 'xml=' + encodeURIComponent(data) + '&filename=' + + encodeURIComponent(filename) + '&format=' + format); + req.simulate(document, '_blank'); + } + else + { + mxUtils.alert(mxResources.get('drawingTooLarge')); + mxUtils.popup(xml); + } +}; + +/** + * Constructs a new metadata dialog. + */ +var EditDataDialog = function(ui, cell) +{ + var div = document.createElement('div'); + var graph = ui.editor.graph; + + var value = graph.getModel().getValue(cell); + + // Converts the value to an XML node + if (!mxUtils.isNode(value)) + { + var doc = mxUtils.createXmlDocument(); + var obj = doc.createElement('object'); + obj.setAttribute('label', value || ''); + value = obj; + } + + // Creates the dialog contents + var form = new mxForm('properties'); + form.table.style.width = '100%'; + + var attrs = value.attributes; + var names = []; + var texts = []; + var count = 0; + + var id = EditDataDialog.getDisplayIdForCell(ui, cell); + + // FIXME: Fix remove button for quirks mode + var addRemoveButton = function(text, name) + { + var wrapper = document.createElement('div'); + wrapper.style.position = 'relative'; + wrapper.style.paddingRight = '20px'; + wrapper.style.boxSizing = 'border-box'; + wrapper.style.width = '100%'; + + var removeAttr = document.createElement('a'); + var img = mxUtils.createImage(Dialog.prototype.closeImage); + img.style.height = '9px'; + img.style.fontSize = '9px'; + img.style.marginBottom = (mxClient.IS_IE11) ? '-1px' : '5px'; + + removeAttr.className = 'geButton'; + removeAttr.setAttribute('title', mxResources.get('delete')); + removeAttr.style.position = 'absolute'; + removeAttr.style.top = '4px'; + removeAttr.style.right = '0px'; + removeAttr.style.margin = '0px'; + removeAttr.style.width = '9px'; + removeAttr.style.height = '9px'; + removeAttr.style.cursor = 'pointer'; + removeAttr.appendChild(img); + + var removeAttrFn = (function(name) + { + return function() + { + var count = 0; + + for (var j = 0; j < names.length; j++) + { + if (names[j] == name) + { + texts[j] = null; + form.table.deleteRow(count + ((id != null) ? 1 : 0)); + + break; + } + + if (texts[j] != null) + { + count++; + } + } + }; + })(name); + + mxEvent.addListener(removeAttr, 'click', removeAttrFn); + + var parent = text.parentNode; + wrapper.appendChild(text); + wrapper.appendChild(removeAttr); + parent.appendChild(wrapper); + }; + + var addTextArea = function(index, name, value) + { + names[index] = name; + texts[index] = form.addTextarea(names[count] + ':', value, 2); + texts[index].style.width = '100%'; + + addRemoveButton(texts[index], name); + }; + + var temp = []; + var isLayer = graph.getModel().getParent(cell) == graph.getModel().getRoot(); + + for (var i = 0; i < attrs.length; i++) + { + if ((isLayer || attrs[i].nodeName != 'label') && attrs[i].nodeName != 'placeholders') + { + temp.push({name: attrs[i].nodeName, value: attrs[i].nodeValue}); + } + } + + // Sorts by name + temp.sort(function(a, b) + { + if (a.name < b.name) + { + return -1; + } + else if (a.name > b.name) + { + return 1; + } + else + { + return 0; + } + }); + + if (id != null) + { + var text = document.createElement('input'); + text.style.width = '280px'; + text.style.textAlign = 'center'; + text.setAttribute('type', 'text'); + text.setAttribute('readOnly', 'true'); + text.setAttribute('value', id); + + form.addField(mxResources.get('id') + ':', text); + } + + for (var i = 0; i < temp.length; i++) + { + addTextArea(count, temp[i].name, temp[i].value); + count++; + } + + var top = document.createElement('div'); + top.style.cssText = 'position:absolute;left:30px;right:30px;overflow-y:auto;top:30px;bottom:80px;'; + top.appendChild(form.table); + + var newProp = document.createElement('div'); + newProp.style.whiteSpace = 'nowrap'; + newProp.style.marginTop = '6px'; + + var nameInput = document.createElement('input'); + nameInput.setAttribute('placeholder', mxResources.get('enterPropertyName')); + nameInput.setAttribute('type', 'text'); + nameInput.setAttribute('size', (mxClient.IS_IE || mxClient.IS_IE11) ? '18' : '22'); + nameInput.style.marginLeft = '2px'; + + newProp.appendChild(nameInput); + top.appendChild(newProp); + div.appendChild(top); + + var addBtn = mxUtils.button(mxResources.get('addProperty'), function() + { + var name = nameInput.value; + + // Avoid ':' in attribute names which seems to be valid in Chrome + if (name.length > 0 && name != 'label' && name != 'placeholders' && name.indexOf(':') < 0) + { + try + { + var idx = mxUtils.indexOf(names, name); + + if (idx >= 0 && texts[idx] != null) + { + texts[idx].focus(); + } + else + { + // Checks if the name is valid + var clone = value.cloneNode(false); + clone.setAttribute(name, ''); + + if (idx >= 0) + { + names.splice(idx, 1); + texts.splice(idx, 1); + } + + names.push(name); + var text = form.addTextarea(name + ':', '', 2); + text.style.width = '100%'; + texts.push(text); + addRemoveButton(text, name); + + text.focus(); + } + + nameInput.value = ''; + } + catch (e) + { + mxUtils.alert(e); + } + } + else + { + mxUtils.alert(mxResources.get('invalidName')); + } + }); + + this.init = function() + { + if (texts.length > 0) + { + texts[0].focus(); + } + else + { + nameInput.focus(); + } + }; + + addBtn.setAttribute('disabled', 'disabled'); + addBtn.style.marginLeft = '10px'; + addBtn.style.width = '144px'; + newProp.appendChild(addBtn); + + var cancelBtn = mxUtils.button(mxResources.get('cancel'), function() + { + ui.hideDialog.apply(ui, arguments); + }); + + cancelBtn.className = 'geBtn'; + + var applyBtn = mxUtils.button(mxResources.get('apply'), function() + { + try + { + ui.hideDialog.apply(ui, arguments); + + // Clones and updates the value + value = value.cloneNode(true); + var removeLabel = false; + + for (var i = 0; i < names.length; i++) + { + if (texts[i] == null) + { + value.removeAttribute(names[i]); + } + else + { + value.setAttribute(names[i], texts[i].value); + removeLabel = removeLabel || (names[i] == 'placeholder' && + value.getAttribute('placeholders') == '1'); + } + } + + // Removes label if placeholder is assigned + if (removeLabel) + { + value.removeAttribute('label'); + } + + // Updates the value of the cell (undoable) + graph.getModel().setValue(cell, value); + } + catch (e) + { + mxUtils.alert(e); + } + }); + applyBtn.className = 'geBtn gePrimaryBtn'; + + function updateAddBtn() + { + if (nameInput.value.length > 0) + { + addBtn.removeAttribute('disabled'); + } + else + { + addBtn.setAttribute('disabled', 'disabled'); + } + }; + + mxEvent.addListener(nameInput, 'keyup', updateAddBtn); + + // Catches all changes that don't fire a keyup (such as paste via mouse) + mxEvent.addListener(nameInput, 'change', updateAddBtn); + + var buttons = document.createElement('div'); + buttons.style.cssText = 'position:absolute;left:30px;right:30px;text-align:right;bottom:30px;height:40px;' + + if (ui.editor.graph.getModel().isVertex(cell) || ui.editor.graph.getModel().isEdge(cell)) + { + var replace = document.createElement('span'); + replace.style.marginRight = '10px'; + var input = document.createElement('input'); + input.setAttribute('type', 'checkbox'); + input.style.marginRight = '6px'; + + if (value.getAttribute('placeholders') == '1') + { + input.setAttribute('checked', 'checked'); + input.defaultChecked = true; + } + + mxEvent.addListener(input, 'click', function() + { + if (value.getAttribute('placeholders') == '1') + { + value.removeAttribute('placeholders'); + } + else + { + value.setAttribute('placeholders', '1'); + } + }); + + replace.appendChild(input); + mxUtils.write(replace, mxResources.get('placeholders')); + + if (EditDataDialog.placeholderHelpLink != null) + { + var link = document.createElement('a'); + link.setAttribute('href', EditDataDialog.placeholderHelpLink); + link.setAttribute('title', mxResources.get('help')); + link.setAttribute('target', '_blank'); + link.style.marginLeft = '10px'; + link.style.cursor = 'help'; + + var icon = document.createElement('img'); + icon.setAttribute('border', '0'); + icon.setAttribute('valign', 'middle'); + icon.style.marginTop = (mxClient.IS_IE11) ? '0px' : '-4px'; + icon.setAttribute('src', Editor.helpImage); + link.appendChild(icon); + + replace.appendChild(link); + } + + buttons.appendChild(replace); + } + + if (ui.editor.cancelFirst) + { + buttons.appendChild(cancelBtn); + buttons.appendChild(applyBtn); + } + else + { + buttons.appendChild(applyBtn); + buttons.appendChild(cancelBtn); + } + + div.appendChild(buttons); + this.container = div; +}; + +/** + * Optional help link. + */ +EditDataDialog.getDisplayIdForCell = function(ui, cell) +{ + var id = null; + + if (ui.editor.graph.getModel().getParent(cell) != null) + { + id = cell.getId(); + } + + return id; +}; + +/** + * Optional help link. + */ +EditDataDialog.placeholderHelpLink = null; + +/** + * Constructs a new link dialog. + */ +var LinkDialog = function(editorUi, initialValue, btnLabel, fn) +{ + var div = document.createElement('div'); + mxUtils.write(div, mxResources.get('editLink') + ':'); + + var inner = document.createElement('div'); + inner.className = 'geTitle'; + inner.style.backgroundColor = 'transparent'; + inner.style.borderColor = 'transparent'; + inner.style.whiteSpace = 'nowrap'; + inner.style.textOverflow = 'clip'; + inner.style.cursor = 'default'; + + if (!mxClient.IS_VML) + { + inner.style.paddingRight = '20px'; + } + + var linkInput = document.createElement('input'); + linkInput.setAttribute('value', initialValue); + linkInput.setAttribute('placeholder', 'http://www.example.com/'); + linkInput.setAttribute('type', 'text'); + linkInput.style.marginTop = '6px'; + linkInput.style.width = '400px'; + linkInput.style.backgroundImage = 'url(\'' + Dialog.prototype.clearImage + '\')'; + linkInput.style.backgroundRepeat = 'no-repeat'; + linkInput.style.backgroundPosition = '100% 50%'; + linkInput.style.paddingRight = '14px'; + + var cross = document.createElement('div'); + cross.setAttribute('title', mxResources.get('reset')); + cross.style.position = 'relative'; + cross.style.left = '-16px'; + cross.style.width = '12px'; + cross.style.height = '14px'; + cross.style.cursor = 'pointer'; + + // Workaround for inline-block not supported in IE + cross.style.display = (mxClient.IS_VML) ? 'inline' : 'inline-block'; + cross.style.top = ((mxClient.IS_VML) ? 0 : 3) + 'px'; + + // Needed to block event transparency in IE + cross.style.background = 'url(' + IMAGE_PATH + '/transparent.gif)'; + + mxEvent.addListener(cross, 'click', function() + { + linkInput.value = ''; + linkInput.focus(); + }); + + inner.appendChild(linkInput); + inner.appendChild(cross); + div.appendChild(inner); + + this.init = function() + { + linkInput.focus(); + + if (mxClient.IS_GC || mxClient.IS_FF || document.documentMode >= 5 || mxClient.IS_QUIRKS) + { + linkInput.select(); + } + else + { + document.execCommand('selectAll', false, null); + } + }; + + var btns = document.createElement('div'); + btns.style.marginTop = '18px'; + btns.style.textAlign = 'right'; + + mxEvent.addListener(linkInput, 'keypress', function(e) + { + if (e.keyCode == 13) + { + editorUi.hideDialog(); + fn(linkInput.value); + } + }); + + var cancelBtn = mxUtils.button(mxResources.get('cancel'), function() + { + editorUi.hideDialog(); + }); + cancelBtn.className = 'geBtn'; + + if (editorUi.editor.cancelFirst) + { + btns.appendChild(cancelBtn); + } + + var mainBtn = mxUtils.button(btnLabel, function() + { + editorUi.hideDialog(); + fn(linkInput.value); + }); + mainBtn.className = 'geBtn gePrimaryBtn'; + btns.appendChild(mainBtn); + + if (!editorUi.editor.cancelFirst) + { + btns.appendChild(cancelBtn); + } + + div.appendChild(btns); + + this.container = div; +}; + +/** + * + */ +var OutlineWindow = function(editorUi, x, y, w, h) +{ + var graph = editorUi.editor.graph; + + var div = document.createElement('div'); + div.style.position = 'absolute'; + div.style.width = '100%'; + div.style.height = '100%'; + div.style.border = '1px solid whiteSmoke'; + div.style.overflow = 'hidden'; + + this.window = new mxWindow(mxResources.get('outline'), div, x, y, w, h, true, true); + this.window.minimumSize = new mxRectangle(0, 0, 80, 80); + this.window.destroyOnClose = false; + this.window.setMaximizable(false); + this.window.setResizable(true); + this.window.setClosable(true); + this.window.setVisible(true); + + this.window.setLocation = function(x, y) + { + var iw = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + var ih = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; + + x = Math.max(0, Math.min(x, iw - this.table.clientWidth)); + y = Math.max(0, Math.min(y, ih - this.table.clientHeight - 48)); + + if (this.getX() != x || this.getY() != y) + { + mxWindow.prototype.setLocation.apply(this, arguments); + } + }; + + var resizeListener = mxUtils.bind(this, function() + { + var x = this.window.getX(); + var y = this.window.getY(); + + this.window.setLocation(x, y); + }); + + mxEvent.addListener(window, 'resize', resizeListener); + + var outline = editorUi.createOutline(this.window); + + this.destroy = function() + { + mxEvent.removeListener(window, 'resize', resizeListener); + this.window.destroy(); + outline.destroy(); + } + + this.window.addListener(mxEvent.RESIZE, mxUtils.bind(this, function() + { + outline.update(false); + outline.outline.sizeDidChange(); + })); + + this.window.addListener(mxEvent.SHOW, mxUtils.bind(this, function() + { + outline.suspended = false; + outline.outline.refresh(); + outline.update(); + })); + + this.window.addListener(mxEvent.HIDE, mxUtils.bind(this, function() + { + outline.suspended = true; + })); + + this.window.addListener(mxEvent.NORMALIZE, mxUtils.bind(this, function() + { + outline.suspended = false; + outline.update(); + })); + + this.window.addListener(mxEvent.MINIMIZE, mxUtils.bind(this, function() + { + outline.suspended = true; + })); + + var outlineCreateGraph = outline.createGraph; + outline.createGraph = function(container) + { + var g = outlineCreateGraph.apply(this, arguments); + g.gridEnabled = false; + g.pageScale = graph.pageScale; + g.pageFormat = graph.pageFormat; + g.background = (graph.background == null || graph.background == mxConstants.NONE) ? graph.defaultPageBackgroundColor : graph.background; + g.pageVisible = graph.pageVisible; + + var current = mxUtils.getCurrentStyle(graph.container); + div.style.backgroundColor = current.backgroundColor; + + return g; + }; + + function update() + { + outline.outline.pageScale = graph.pageScale; + outline.outline.pageFormat = graph.pageFormat; + outline.outline.pageVisible = graph.pageVisible; + outline.outline.background = (graph.background == null || graph.background == mxConstants.NONE) ? graph.defaultPageBackgroundColor : graph.background;; + + var current = mxUtils.getCurrentStyle(graph.container); + div.style.backgroundColor = current.backgroundColor; + + if (graph.view.backgroundPageShape != null && outline.outline.view.backgroundPageShape != null) + { + outline.outline.view.backgroundPageShape.fill = graph.view.backgroundPageShape.fill; + } + + outline.outline.refresh(); + }; + + outline.init(div); + + editorUi.editor.addListener('resetGraphView', update); + editorUi.addListener('pageFormatChanged', update); + editorUi.addListener('backgroundColorChanged', update); + editorUi.addListener('backgroundImageChanged', update); + editorUi.addListener('pageViewChanged', function() + { + update(); + outline.update(true); + }); + + if (outline.outline.dialect == mxConstants.DIALECT_SVG) + { + var zoomInAction = editorUi.actions.get('zoomIn'); + var zoomOutAction = editorUi.actions.get('zoomOut'); + + mxEvent.addMouseWheelListener(function(evt, up) + { + var outlineWheel = false; + var source = mxEvent.getSource(evt); + + while (source != null) + { + if (source == outline.outline.view.canvas.ownerSVGElement) + { + outlineWheel = true; + break; + } + + source = source.parentNode; + } + + if (outlineWheel) + { + if (up) + { + zoomInAction.funct(); + } + else + { + zoomOutAction.funct(); + } + + mxEvent.consume(evt); + } + }); + } +}; + +/** + * + */ +var LayersWindow = function(editorUi, x, y, w, h) +{ + var graph = editorUi.editor.graph; + + var div = document.createElement('div'); + div.style.userSelect = 'none'; + div.style.background = (Dialog.backdropColor == 'white') ? 'whiteSmoke' : Dialog.backdropColor; + div.style.border = '1px solid whiteSmoke'; + div.style.height = '100%'; + div.style.marginBottom = '10px'; + div.style.overflow = 'auto'; + + var tbarHeight = (!EditorUi.compactUi) ? '30px' : '26px'; + + var listDiv = document.createElement('div') + listDiv.style.backgroundColor = (Dialog.backdropColor == 'white') ? '#dcdcdc' : '#e5e5e5'; + listDiv.style.position = 'absolute'; + listDiv.style.overflow = 'auto'; + listDiv.style.left = '0px'; + listDiv.style.right = '0px'; + listDiv.style.top = '0px'; + listDiv.style.bottom = (parseInt(tbarHeight) + 7) + 'px'; + div.appendChild(listDiv); + + var dragSource = null; + var dropIndex = null; + + mxEvent.addListener(div, 'dragover', function(evt) + { + evt.dataTransfer.dropEffect = 'move'; + dropIndex = 0; + evt.stopPropagation(); + evt.preventDefault(); + }); + + // Workaround for "no element found" error in FF + mxEvent.addListener(div, 'drop', function(evt) + { + evt.stopPropagation(); + evt.preventDefault(); + }); + + var layerCount = null; + var selectionLayer = null; + + var ldiv = document.createElement('div'); + + ldiv.className = 'geToolbarContainer'; + ldiv.style.position = 'absolute'; + ldiv.style.bottom = '0px'; + ldiv.style.left = '0px'; + ldiv.style.right = '0px'; + ldiv.style.height = tbarHeight; + ldiv.style.overflow = 'hidden'; + ldiv.style.padding = (!EditorUi.compactUi) ? '1px' : '4px 0px 3px 0px'; + ldiv.style.backgroundColor = (Dialog.backdropColor == 'white') ? 'whiteSmoke' : Dialog.backdropColor; + ldiv.style.borderWidth = '1px 0px 0px 0px'; + ldiv.style.borderColor = '#c3c3c3'; + ldiv.style.borderStyle = 'solid'; + ldiv.style.display = 'block'; + ldiv.style.whiteSpace = 'nowrap'; + + if (mxClient.IS_QUIRKS) + { + ldiv.style.filter = 'none'; + } + + var link = document.createElement('a'); + link.className = 'geButton'; + + if (mxClient.IS_QUIRKS) + { + link.style.filter = 'none'; + } + + var removeLink = link.cloneNode(); + removeLink.innerHTML = '
'; + + mxEvent.addListener(removeLink, 'click', function(evt) + { + if (graph.isEnabled()) + { + graph.model.beginUpdate(); + try + { + var index = graph.model.root.getIndex(selectionLayer); + graph.removeCells([selectionLayer], false); + + // Creates default layer if no layer exists + if (graph.model.getChildCount(graph.model.root) == 0) + { + graph.model.add(graph.model.root, new mxCell()); + graph.setDefaultParent(null); + } + else if (index > 0 && index <= graph.model.getChildCount(graph.model.root)) + { + graph.setDefaultParent(graph.model.getChildAt(graph.model.root, index - 1)); + } + else + { + graph.setDefaultParent(null); + } + } + finally + { + graph.model.endUpdate(); + } + } + + mxEvent.consume(evt); + }); + + if (!graph.isEnabled()) + { + removeLink.className = 'geButton mxDisabled'; + } + + ldiv.appendChild(removeLink); + + var insertLink = link.cloneNode(); + insertLink.innerHTML = '
'; + + mxEvent.addListener(insertLink, 'click', function(evt) + { + if (graph.isEnabled() && !graph.isSelectionEmpty()) + { + graph.moveCells(graph.getSelectionCells(), 0, 0, false, selectionLayer); + } + }); + + ldiv.appendChild(insertLink); + + var dataLink = link.cloneNode(); + dataLink.innerHTML = '
'; + dataLink.setAttribute('title', mxResources.get('rename')); + + mxEvent.addListener(dataLink, 'click', function(evt) + { + if (graph.isEnabled()) + { + editorUi.showDataDialog(selectionLayer); + } + + mxEvent.consume(evt); + }); + + if (!graph.isEnabled()) + { + dataLink.className = 'geButton mxDisabled'; + } + + ldiv.appendChild(dataLink); + + function renameLayer(layer) + { + if (graph.isEnabled() && layer != null) + { + var label = graph.convertValueToString(layer); + var dlg = new FilenameDialog(editorUi, label || mxResources.get('background'), mxResources.get('rename'), mxUtils.bind(this, function(newValue) + { + if (newValue != null) + { + graph.cellLabelChanged(layer, newValue); + } + }), mxResources.get('enterName')); + editorUi.showDialog(dlg.container, 300, 100, true, true); + dlg.init(); + } + }; + + var duplicateLink = link.cloneNode(); + duplicateLink.innerHTML = '
'; + + mxEvent.addListener(duplicateLink, 'click', function(evt) + { + if (graph.isEnabled()) + { + var newCell = null; + graph.model.beginUpdate(); + try + { + newCell = graph.cloneCells([selectionLayer])[0]; + graph.cellLabelChanged(newCell, mxResources.get('untitledLayer')); + newCell.setVisible(true); + newCell = graph.addCell(newCell, graph.model.root); + graph.setDefaultParent(newCell); + } + finally + { + graph.model.endUpdate(); + } + + if (newCell != null && !graph.isCellLocked(newCell)) + { + graph.selectAll(newCell); + } + } + }); + + if (!graph.isEnabled()) + { + duplicateLink.className = 'geButton mxDisabled'; + } + + ldiv.appendChild(duplicateLink); + + var addLink = link.cloneNode(); + addLink.innerHTML = '
'; + addLink.setAttribute('title', mxResources.get('addLayer')); + + mxEvent.addListener(addLink, 'click', function(evt) + { + if (graph.isEnabled()) + { + graph.model.beginUpdate(); + + try + { + var cell = graph.addCell(new mxCell(mxResources.get('untitledLayer')), graph.model.root); + graph.setDefaultParent(cell); + } + finally + { + graph.model.endUpdate(); + } + } + + mxEvent.consume(evt); + }); + + if (!graph.isEnabled()) + { + addLink.className = 'geButton mxDisabled'; + } + + ldiv.appendChild(addLink); + + div.appendChild(ldiv); + + function refresh() + { + layerCount = graph.model.getChildCount(graph.model.root) + listDiv.innerHTML = ''; + + function addLayer(index, label, child, defaultParent) + { + var ldiv = document.createElement('div'); + ldiv.className = 'geToolbarContainer'; + + ldiv.style.overflow = 'hidden'; + ldiv.style.position = 'relative'; + ldiv.style.padding = '4px'; + ldiv.style.height = '22px'; + ldiv.style.display = 'block'; + ldiv.style.backgroundColor = 'whiteSmoke'; + ldiv.style.borderWidth = '0px 0px 1px 0px'; + ldiv.style.borderColor = '#c3c3c3'; + ldiv.style.borderStyle = 'solid'; + ldiv.style.whiteSpace = 'nowrap'; + ldiv.setAttribute('title', label); + + var left = document.createElement('div'); + left.style.display = 'inline-block'; + left.style.width = '100%'; + left.style.textOverflow = 'ellipsis'; + left.style.overflow = 'hidden'; + + mxEvent.addListener(ldiv, 'dragover', function(evt) + { + evt.dataTransfer.dropEffect = 'move'; + dropIndex = index; + evt.stopPropagation(); + evt.preventDefault(); + }); + + mxEvent.addListener(ldiv, 'dragstart', function(evt) + { + dragSource = ldiv; + + // Workaround for no DnD on DIV in FF + if (mxClient.IS_FF) + { + // LATER: Check what triggers a parse as XML on this in FF after drop + evt.dataTransfer.setData('Text', ''); + } + }); + + mxEvent.addListener(ldiv, 'dragend', function(evt) + { + if (dragSource != null && dropIndex != null) + { + graph.addCell(child, graph.model.root, dropIndex); + } + + dragSource = null; + dropIndex = null; + evt.stopPropagation(); + evt.preventDefault(); + }); + + var btn = document.createElement('img'); + btn.setAttribute('draggable', 'false'); + btn.setAttribute('align', 'top'); + btn.setAttribute('border', '0'); + btn.style.padding = '4px'; + btn.setAttribute('title', mxResources.get('lockUnlock')); + + var state = graph.view.getState(child); + var style = (state != null) ? state.style : graph.getCellStyle(child); + + if (mxUtils.getValue(style, 'locked', '0') == '1') + { + btn.setAttribute('src', Dialog.prototype.lockedImage); + } + else + { + btn.setAttribute('src', Dialog.prototype.unlockedImage); + } + + if (graph.isEnabled()) + { + btn.style.cursor = 'pointer'; + } + + mxEvent.addListener(btn, 'click', function(evt) + { + if (graph.isEnabled()) + { + var value = null; + + graph.getModel().beginUpdate(); + try + { + value = (mxUtils.getValue(style, 'locked', '0') == '1') ? null : '1'; + graph.setCellStyles('locked', value, [child]); + } + finally + { + graph.getModel().endUpdate(); + } + + if (value == '1') + { + graph.removeSelectionCells(graph.getModel().getDescendants(child)); + } + + mxEvent.consume(evt); + } + }); + + left.appendChild(btn); + + var inp = document.createElement('input'); + inp.setAttribute('type', 'checkbox'); + inp.setAttribute('title', mxResources.get('hideIt', [child.value || mxResources.get('background')])); + inp.style.marginLeft = '4px'; + inp.style.marginRight = '6px'; + inp.style.marginTop = '4px'; + left.appendChild(inp); + + if (graph.model.isVisible(child)) + { + inp.setAttribute('checked', 'checked'); + inp.defaultChecked = true; + } + + mxEvent.addListener(inp, 'click', function(evt) + { + graph.model.setVisible(child, !graph.model.isVisible(child)); + mxEvent.consume(evt); + }); + + mxUtils.write(left, label); + ldiv.appendChild(left); + + if (graph.isEnabled()) + { + // Fallback if no drag and drop is available + if (mxClient.IS_TOUCH || mxClient.IS_POINTER || mxClient.IS_VML || + (mxClient.IS_IE && document.documentMode < 10)) + { + var right = document.createElement('div'); + right.style.display = 'block'; + right.style.textAlign = 'right'; + right.style.whiteSpace = 'nowrap'; + right.style.position = 'absolute'; + right.style.right = '6px'; + right.style.top = '6px'; + + // Poor man's change layer order + if (index > 0) + { + var img2 = document.createElement('a'); + + img2.setAttribute('title', mxResources.get('toBack')); + + img2.className = 'geButton'; + img2.style.cssFloat = 'none'; + img2.innerHTML = '▼'; + img2.style.width = '14px'; + img2.style.height = '14px'; + img2.style.fontSize = '14px'; + img2.style.margin = '0px'; + img2.style.marginTop = '-1px'; + right.appendChild(img2); + + mxEvent.addListener(img2, 'click', function(evt) + { + if (graph.isEnabled()) + { + graph.addCell(child, graph.model.root, index - 1); + } + + mxEvent.consume(evt); + }); + } + + if (index >= 0 && index < layerCount - 1) + { + var img1 = document.createElement('a'); + + img1.setAttribute('title', mxResources.get('toFront')); + + img1.className = 'geButton'; + img1.style.cssFloat = 'none'; + img1.innerHTML = '▲'; + img1.style.width = '14px'; + img1.style.height = '14px'; + img1.style.fontSize = '14px'; + img1.style.margin = '0px'; + img1.style.marginTop = '-1px'; + right.appendChild(img1); + + mxEvent.addListener(img1, 'click', function(evt) + { + if (graph.isEnabled()) + { + graph.addCell(child, graph.model.root, index + 1); + } + + mxEvent.consume(evt); + }); + } + + ldiv.appendChild(right); + } + + if (mxClient.IS_SVG && (!mxClient.IS_IE || document.documentMode >= 10)) + { + ldiv.setAttribute('draggable', 'true'); + ldiv.style.cursor = 'move'; + } + } + + mxEvent.addListener(ldiv, 'dblclick', function(evt) + { + var nodeName = mxEvent.getSource(evt).nodeName; + + if (nodeName != 'INPUT' && nodeName != 'IMG') + { + renameLayer(child); + mxEvent.consume(evt); + } + }); + + if (graph.getDefaultParent() == child) + { + ldiv.style.background = '#e6eff8'; + ldiv.style.fontWeight = (graph.isEnabled()) ? 'bold' : ''; + selectionLayer = child; + } + else + { + mxEvent.addListener(ldiv, 'click', function(evt) + { + if (graph.isEnabled()) + { + graph.setDefaultParent(defaultParent); + graph.view.setCurrentRoot(null); + refresh(); + } + }); + } + + listDiv.appendChild(ldiv); + }; + + // Cannot be moved or deleted + for (var i = layerCount - 1; i >= 0; i--) + { + (mxUtils.bind(this, function(child) + { + addLayer(i, graph.convertValueToString(child) || + mxResources.get('background'), child, child); + }))(graph.model.getChildAt(graph.model.root, i)); + } + + var label = graph.convertValueToString(selectionLayer) || mxResources.get('background'); + removeLink.setAttribute('title', mxResources.get('removeIt', [label])); + insertLink.setAttribute('title', mxResources.get('moveSelectionTo', [label])); + duplicateLink.setAttribute('title', mxResources.get('duplicateIt', [label])); + dataLink.setAttribute('title', mxResources.get('editData')); + + if (graph.isSelectionEmpty()) + { + insertLink.className = 'geButton mxDisabled'; + } + }; + + refresh(); + graph.model.addListener(mxEvent.CHANGE, function() + { + refresh(); + }); + + graph.selectionModel.addListener(mxEvent.CHANGE, function() + { + if (graph.isSelectionEmpty()) + { + insertLink.className = 'geButton mxDisabled'; + } + else + { + insertLink.className = 'geButton'; + } + }); + + this.window = new mxWindow(mxResources.get('layers'), div, x, y, w, h, true, true); + this.window.minimumSize = new mxRectangle(0, 0, 120, 120); + this.window.destroyOnClose = false; + this.window.setMaximizable(false); + this.window.setResizable(true); + this.window.setClosable(true); + this.window.setVisible(true); + + // Make refresh available via instance + this.refreshLayers = refresh; + + this.window.setLocation = function(x, y) + { + var iw = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + var ih = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; + + x = Math.max(0, Math.min(x, iw - this.table.clientWidth)); + y = Math.max(0, Math.min(y, ih - this.table.clientHeight - 48)); + + if (this.getX() != x || this.getY() != y) + { + mxWindow.prototype.setLocation.apply(this, arguments); + } + }; + + var resizeListener = mxUtils.bind(this, function() + { + var x = this.window.getX(); + var y = this.window.getY(); + + this.window.setLocation(x, y); + }); + + mxEvent.addListener(window, 'resize', resizeListener); + + this.destroy = function() + { + mxEvent.removeListener(window, 'resize', resizeListener); + this.window.destroy(); + } +}; diff --git a/media/grapheditor/js/Editor.js b/media/grapheditor/js/Editor.js new file mode 100644 index 0000000000..3205f94346 --- /dev/null +++ b/media/grapheditor/js/Editor.js @@ -0,0 +1,2252 @@ +/** + * Copyright (c) 2006-2012, JGraph Ltd + */ +/** + * Editor constructor executed on page load. + */ +Editor = function(chromeless, themes, model, graph, editable) +{ + mxEventSource.call(this); + this.chromeless = (chromeless != null) ? chromeless : this.chromeless; + this.initStencilRegistry(); + this.graph = graph || this.createGraph(themes, model); + this.editable = (editable != null) ? editable : !chromeless; + this.undoManager = this.createUndoManager(); + this.status = ''; + + this.getOrCreateFilename = function() + { + return this.filename || mxResources.get('drawing', [Editor.pageCounter]) + '.xml'; + }; + + this.getFilename = function() + { + return this.filename; + }; + + // Sets the status and fires a statusChanged event + this.setStatus = function(value) + { + this.status = value; + this.fireEvent(new mxEventObject('statusChanged')); + }; + + // Returns the current status + this.getStatus = function() + { + return this.status; + }; + + // Updates modified state if graph changes + this.graphChangeListener = function(sender, eventObject) + { + var edit = (eventObject != null) ? eventObject.getProperty('edit') : null; + + if (edit == null || !edit.ignoreEdit) + { + this.setModified(true); + } + }; + + this.graph.getModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function() + { + this.graphChangeListener.apply(this, arguments); + })); + + // Sets persistent graph state defaults + this.graph.resetViewOnRootChange = false; + this.init(); +}; + +/** + * Counts open editor tabs (must be global for cross-window access) + */ +Editor.pageCounter = 0; + +// Cross-domain window access is not allowed in FF, so if we +// were opened from another domain then this will fail. +(function() +{ + try + { + var op = window; + + while (op.opener != null && typeof op.opener.Editor !== 'undefined' && + !isNaN(op.opener.Editor.pageCounter) && + // Workaround for possible infinite loop in FF https://drawio.atlassian.net/browse/DS-795 + op.opener != op) + { + op = op.opener; + } + + // Increments the counter in the first opener in the chain + if (op != null) + { + op.Editor.pageCounter++; + Editor.pageCounter = op.Editor.pageCounter; + } + } + catch (e) + { + // ignore + } +})(); + +/** + * Specifies if local storage should be used (eg. on the iPad which has no filesystem) + */ +Editor.useLocalStorage = typeof(Storage) != 'undefined' && mxClient.IS_IOS; + +/** + * Images below are for lightbox and embedding toolbars. + */ +Editor.helpImage = (mxClient.IS_SVG) ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAXVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC5BxTwAAAAH3RSTlMAlUF8boNQIE0LBgOgkGlHNSwqFIx/dGVUOjApmV9ezNACSAAAAIVJREFUGNNtjNsOgzAMQ5NeoVcKDAZs+//PXLKI8YKlWvaRU7jXuFpb9qsbdK05XILUiE8JHQox1Pv3OgFUzf1AGqWqUg+QBwLF0YAeegBlCNgRWOpB5vUfTCmeoHQ/wNdy0jLH/cM+b+wLTw4n/7ACEmHVVy8h6qy8V7MNcGowWpsNbvUFcGUEdSi1s/oAAAAASUVORK5CYII=' : + IMAGE_PATH + '/help.png'; + +/** + * Sets the default font size. + */ +Editor.checkmarkImage = (mxClient.IS_SVG) ? 'data:image/gif;base64,R0lGODlhFQAVAMQfAGxsbHx8fIqKioaGhvb29nJycvr6+sDAwJqamltbW5OTk+np6YGBgeTk5Ly8vJiYmP39/fLy8qWlpa6ursjIyOLi4vj4+N/f3+3t7fT09LCwsHZ2dubm5r6+vmZmZv///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjAgNjEuMTM0Nzc3LCAyMDEwLzAyLzEyLTE3OjMyOjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OEY4NTZERTQ5QUFBMTFFMUE5MTVDOTM5MUZGMTE3M0QiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OEY4NTZERTU5QUFBMTFFMUE5MTVDOTM5MUZGMTE3M0QiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4Rjg1NkRFMjlBQUExMUUxQTkxNUM5MzkxRkYxMTczRCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4Rjg1NkRFMzlBQUExMUUxQTkxNUM5MzkxRkYxMTczRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6alpKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmFgX15dXFtaWVhXVlVUU1JRUE9OTUxLSklIR0ZFRENCQUA/Pj08Ozo5ODc2NTQzMjEwLy4tLCsqKSgnJiUkIyIhIB8eHRwbGhkYFxYVFBMSERAPDg0MCwoJCAcGBQQDAgEAACH5BAEAAB8ALAAAAAAVABUAAAVI4CeOZGmeaKqubKtylktSgCOLRyLd3+QJEJnh4VHcMoOfYQXQLBcBD4PA6ngGlIInEHEhPOANRkaIFhq8SuHCE1Hb8Lh8LgsBADs=' : + IMAGE_PATH + '/checkmark.gif'; + +/** + * Images below are for lightbox and embedding toolbars. + */ +Editor.maximizeImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVBAMAAABbObilAAAAElBMVEUAAAAAAAAAAAAAAAAAAAAAAADgKxmiAAAABXRSTlMA758vX1Pw3BoAAABJSURBVAjXY8AJQkODGBhUQ0MhbAUGBiYY24CBgRnGFmZgMISwgwwDGRhEhVVBbAVmEQYGRwMmBjIAQi/CTIRd6G5AuA3dzYQBAHj0EFdHkvV4AAAAAElFTkSuQmCC'; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.zoomOutImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVBAMAAABbObilAAAAElBMVEUAAAAAAAAsLCxxcXEhISFgYGChjTUxAAAAAXRSTlMAQObYZgAAAEdJREFUCNdjIAMwCQrB2YKCggJQJqMwA7MglK1owMBgqABVApITgLJZXFxgbIQ4Qj3CHIT5ggoIe5kgNkM1KSDYKBKqxPkDAPo5BAZBE54hAAAAAElFTkSuQmCC'; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.zoomInImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVBAMAAABbObilAAAAElBMVEUAAAAAAAAsLCwhISFxcXFgYGBavKaoAAAAAXRSTlMAQObYZgAAAElJREFUCNdjIAMwCQrB2YKCggJQJqMIA4sglK3owMzgqABVwsDMwCgAZTMbG8PYCHGEeoQ5CPMFFRD2MkFshmpSQLBRJFSJ8wcAEqcEM2uhl2MAAAAASUVORK5CYII='; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.zoomFitImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVBAMAAABbObilAAAAD1BMVEUAAAAAAAAwMDBwcHBgYGC1xl09AAAAAXRSTlMAQObYZgAAAEFJREFUCNdjIAMwCQrB2YKCggJQJqMwA7MglK1owMBgqABVApITwMdGqEeYgzBfUAFhLxPEZqgmBQQbRUKFOH8AAK5OA3lA+FFOAAAAAElFTkSuQmCC'; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.layersImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAMAAACeyVWkAAAAaVBMVEUAAAAgICAICAgdHR0PDw8WFhYICAgLCwsXFxcvLy8ODg4uLi4iIiIqKiokJCQYGBgKCgonJycFBQUCAgIqKiocHBwcHBwODg4eHh4cHBwnJycJCQkUFBQqKiojIyMuLi4ZGRkgICAEBATOWYXAAAAAGnRSTlMAD7+fnz8/H7/ff18/77+vr5+fn39/b28fH2xSoKsAAACQSURBVBjTrYxJEsMgDARZZMAY73sgCcn/HxnhKtnk7j6oRq0psfuoyndZ/SuODkHPLzfVT6KeyPePnJ7KrnkRjWMXTn4SMnN8mXe2SSM3ts8L/ZUxxrbAULSYJJULE0Iw9pjpenoICcgcX61mGgTgtCv9Be99pzCoDhNQWQnchD1mup5++CYGcoQexajZbfwAj/0MD8ZOaUgAAAAASUVORK5CYII='; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.previousImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABmJLR0QA/wD/AP+gvaeTAAAAh0lEQVQ4je3UsQnCUBCA4U8hpa1NsoEjpHQJS0dxADdwEMuMIJkgA1hYChbGQgMi+JC8q4L/AB/vDu7x74cWWEZhJU44RmA1zujR5GIbXF9YNrjD/Q0bDRY4fEBZ4P4LlgTnCbAf84pUM8/9hY08tMUtEoQ1LpEgrNBFglChFXR6Q6GfwwR6AGKJMF74Vtt3AAAAAElFTkSuQmCC'; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.nextImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABmJLR0QA/wD/AP+gvaeTAAAAi0lEQVQ4jeXUIQ7CUAwA0MeGxWI2yylwnALJUdBcgYvM7QYLmjOQIAkIPmJZghiIvypoUtX0tfnJL38X5ZfaEgUeUcManFBHgS0SLlhHggk3bCPBhCf2keCQR8wjwYTDp6YiZxJmOU1jGw7vGALescuBxsArNlOwd/CM1VSM/ut1qCIw+uOwiMJ+OF4CQzBCXm3hyAAAAABJRU5ErkJggg=='; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.editImage = (mxClient.IS_SVG) ? 'data:image/gif;base64,R0lGODlhCwALAIABAFdXV////yH5BAEAAAEALAAAAAALAAsAAAIZjB8AiKuc4jvLOGqzrjX6zmkWyChXaUJBAQA7' : IMAGE_PATH + '/edit.gif'; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.zoomOutLargeImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAilBMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////2N2iNAAAALXRSTlMA+vTcKMM96GRBHwXxi0YaX1HLrKWhiHpWEOnOr52Vb2xKSDcT19PKv5l/Ngdk8+viAAABJklEQVQ4y4WT2XaDMAxEvWD2nSSUNEnTJN3r//+9Sj7ILAY6L0ijC4ONYVZRpo6cByrz2YKSUGorGTpz71lPVHvT+avoB5wIkU/mxk8veceSuNoLg44IzziXjvpih72wKQnm8yc2UoiP/LAd8jQfe2Xf4Pq+2EyYIvv9wbzHHCgwxDdlBtWZOdqDfTCVgqpygQpsZaojVAVc9UjQxnAJDIBhiQv84tq3gMQCAVTxVoSibXJf8tMuc7e1TB/DCmejBNg/w1Y3c+AM5vv4w7xM59/oXamrHaLVqPQ+OTCnmMZxgz0SdL5zji0/ld6j88qGa5KIiBB6WeJGKfUKwSMKLuXgvl1TW0tm5R9UQL/efSDYsnzxD8CinhBsTTdugJatKpJwf8v+ADb8QmvW7AeAAAAAAElFTkSuQmCC'; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.zoomInLargeImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAilBMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////2N2iNAAAALXRSTlMA+vTcKMM96GRBHwXxi0YaX1HLrKWhiHpWEOnOr52Vb2xKSDcT19PKv5l/Ngdk8+viAAABKElEQVQ4y4WT6WKCMBCENwkBwn2oFKvWqr3L+79es4EkQIDOH2d3Pxk2ABiJlB8JCXjqw4LikHVGLHTm3nM3UeVN5690GBBN0GwyV/3kkrUQR+WeKnREeKpzaXWd77CmJiXGfPIEI4V4yQ9TIW/ntlcMBe731Vts9w5TWG8F5j3mQI4hvrKpdGeYA7CX9qAcl650gVJartxRuhyHVghF8idQAIbFLvCLu28BsQEC6aKtCK6Pyb3JT7PmbmtNH8Ny56CotD/2qOs5cJbuffxgXmCib+xddVU5RNOhkvvkhTlFehzVWCOh3++MYElOhfdovaImnRYVmqDdsuhNp1QrBBE6uGC2+3ZNjGdg5B94oD+9uyVgWT79BwAxEBTWdOu3bWBVgsn/N/AHUD9IC01Oe40AAAAASUVORK5CYII='; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.actualSizeLargeImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAilBMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////2N2iNAAAALXRSTlMA+vTcKMM96GRBHwXxi0YaX1HLrKWhiHpWEOnOr52Vb2xKSDcT19PKv5l/Ngdk8+viAAABIUlEQVQ4y4WT2XqDIBCFBxDc9yTWNEnTJN3r+79eGT4BEbXnaubMr8dBBaM450dCQp4LWFAascGIRd48eB4cNYE7f6XjgGiCFs5c+dml6CFN6j1V6IQIlHPpdV/usKcmJcV88gQTRXjLD9Mhb+fWq8YG9/uCmTCFjeeDeY85UGKIUGUuqzN42kv7oCouq9oHamlzVR1lVfpAIu1QVRiW+sAv7r4FpAYIZZVsRXB9TP5Dfpo1d1trCgzz1iiptH/sUbdz4CzN9+mLeXHn3+hdddd4RDegsrvzwZwSs2GLPRJidAqCLTlVwaMPqpYMWjTWBB2WRW86pVkhSKyDK2bdt2tmagZG4sBD/evdLQHLEvQfAOKRoLCmG1FAB6uKmby+gz+REDn7O5+EwQAAAABJRU5ErkJggg=='; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.printLargeImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAXVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9RKvvlAAAAHnRSTlMAydnl77qbMLT093H7K4Nd4Ktn082+lYt5bkklEgP44nQSAAAApUlEQVQ4y73P2Q6DIBRF0cOgbRHHzhP//5m9mBAQKjG1cT0Yc7ITAMu1LNQgUZiQ2DYoNQ0sCQb6qgHAfRx48opq3J9AZ6xuF7uOew8Ik1OsCZRS2UAC9V+D9a+QZYxNA45YFQftPtSkATOhw7dAc0vPBwKWiIOjP0JZ0yMuQJ27g36DipOUsqRAM0dR8KD1/ILHaHSE/w8DIx09E3g/BTce6rHUB5sAPKvfF+JdAAAAAElFTkSuQmCC'; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.layersLargeImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAmVBMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+/v7///+bnZkkAAAAMnRSTlMABPr8ByiD88KsTi/rvJb272mjeUA1CuPe1M/KjVxYHxMP6KZ0S9nYzGRGGRaznpGIbzaGUf0AAAHESURBVDjLbZLZYoIwEEVDgLCjbKIgAlqXqt3m/z+uNwu1rcyDhjl3ktnYL7OY254C0VX3yWFZfzDrOClbbgKxi0YDHjwl4jbnRkXxJS/C1YP3DbBhD1n7Ex4uaAqdVDb3yJ/4J/3nJD2to/ngQz/DfUvzMp4JJ5sSCaF5oXmemgQDfDxzbi+Kq4sU+vNcuAmx94JtyOP2DD4Epz2asWSCz4Z/4fECxyNj9zC9xNLHcdPEO+awDKeSaUu0W4twZQiO2hYVisTR3RCtK/c1X6t4xMEpiGqXqVntEBLolkZZsKY4QtwH6jzq67dEHlJysB1aNOD3XT7n1UkasQN59L4yC2RELMDSeCRtz3yV22Ub3ozIUTknYx8JWqDdQxbUes98cR2kZtUSveF/bAhcedwEWmlxIkpZUy4XOCb6VBjjxHvbwo/1lBAHHi2JCr0NI570QhyHq/DhJoE2lLgyA4RVe6KmZ47O/3b86MCP0HWa73A8/C3SUc5Qc1ajt6fgpXJ+RGpMvDSchepZDOOQRcZVIKcK90x2D7etqtI+56+u6n3sPriO6nfphitR4+O2m3EbM7lh3me1FM1o+LMI887rN+s3/wZdTFlpNVJiOAAAAABJRU5ErkJggg=='; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.closeLargeImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAuvAIg/dDM/QlOeuFhj0S5s4vKgzjxJRQNiLSey0AAADNSURBVDjLfZLbEoMgDEQjRRRs1XqX///QNmOHJSnjPkHOGR7IEmeoGtJZstnwjqbRfIsmgEdtPCqe9Ynz7ZSc07rE2QiSc+qv8TvjRXA2PDUm3dpe82iJhOEUfxJJo3aCv+jKmRmH4lcCjCjeh9GWOdL/GZZkXH3PYYDrHBnfc4D/RVZf5sjoC1was+Y6HQxwaUxFvq/a0Pv343VCTxfBSRiB+ab3M3eiQZXmMNBJ3Y8pGRZtYQ7DgHMXJEdPLTaN/qBjzJOBc3nmNcbsA16bMR0oLqf+AAAAAElFTkSuQmCC'; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.editLargeImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA+hzi3nRQWyXzkm0h2j3u54gzEgSXjlYoTBgJxL2loGpAOS3Jt7Wxm35Ga7gRAAAA6UlEQVQ4y63Q2XaCMBSF4Q0JBasoQ5DJqbXjfv8HbCK2BZNwo/8FXHx7rcMC7lQu0iX8qU/qtvAWCpoqH8dYzS0SwaV5eK/UAf8X9pd2CWKzuF5Jrftp1owXwnIGLUaL3PYndOHf4kNNXWrXK/m7CHunk7K8LE6YtBpcknwG9GKxnroY+ylBXcx4xKyx/u/EuXi509cP9V7OO1oyHnzrdFTcqLG/4ibBA5pIMr/4xvKzuQDkVy9wW8SgBFD6HDvuzMvrZcC9QlkfMzI7w64m+b4PqBMNHB05lH21PVxJo2/fBXxV4hB38PcD+5AkI4FuETsAAAAASUVORK5CYII='; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.previousLargeImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAPFBMVEUAAAD////////////////////////////////////////////////////////////////////////////YSWgTAAAAE3RSTlMA7fci493c0MW8uJ6CZks4MxQHEZL6ewAAAFZJREFUOMvdkskRgDAMA4lDwg2B7b9XOlge/KKvdsa25KFb5XlRvxXC/DNBEv8IFNjBgGdDgXtFgTyhwDXiQAUHCvwa4Uv6mR6UR+1led2mVonvl+tML45qCQNQLIx7AAAAAElFTkSuQmCC'; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.nextLargeImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAPFBMVEUAAAD////////////////////////////////////////////////////////////////////////////YSWgTAAAAE3RSTlMA7fci493c0MW8uJ6CZks4MxQHEZL6ewAAAFRJREFUOMvd0skRgCAQBVEFwQ0V7fxzNQP6wI05v6pZ/kyj1b7FNgik2gQzzLcAwiUAigHOTwDHK4A1CmB5BJANJG1hQ9qafYcqFlZP3IFc9eVGrR+iIgkDQRUXIAAAAABJRU5ErkJggg=='; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.ctrlKey = (mxClient.IS_MAC) ? 'Cmd' : 'Ctrl'; + +/** + * Specifies if the diagram should be saved automatically if possible. Default + * is true. + */ +Editor.popupsAllowed = true; + +/** + * Editor inherits from mxEventSource + */ +mxUtils.extend(Editor, mxEventSource); + +/** + * Stores initial state of mxClient.NO_FO. + */ +Editor.prototype.originalNoForeignObject = mxClient.NO_FO; + +/** + * Specifies the image URL to be used for the transparent background. + */ +Editor.prototype.transparentImage = (mxClient.IS_SVG) ? 'data:image/gif;base64,R0lGODlhMAAwAIAAAP///wAAACH5BAEAAAAALAAAAAAwADAAAAIxhI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8egpAAA7' : + IMAGE_PATH + '/transparent.gif'; + +/** + * Specifies if the canvas should be extended in all directions. Default is true. + */ +Editor.prototype.extendCanvas = true; + +/** + * Specifies if the app should run in chromeless mode. Default is false. + * This default is only used if the contructor argument is null. + */ +Editor.prototype.chromeless = false; + +/** + * Specifies the order of OK/Cancel buttons in dialogs. Default is true. + * Cancel first is used on Macs, Windows/Confluence uses cancel last. + */ +Editor.prototype.cancelFirst = true; + +/** + * Specifies if the editor is enabled. Default is true. + */ +Editor.prototype.enabled = true; + +/** + * Contains the name which was used for the last save. Default value is null. + */ +Editor.prototype.filename = null; + +/** + * Contains the current modified state of the diagram. This is false for + * new diagrams and after the diagram was saved. + */ +Editor.prototype.modified = false; + +/** + * Specifies if the diagram should be saved automatically if possible. Default + * is true. + */ +Editor.prototype.autosave = true; + +/** + * Specifies the top spacing for the initial page view. Default is 0. + */ +Editor.prototype.initialTopSpacing = 0; + +/** + * Specifies the app name. Default is document.title. + */ +Editor.prototype.appName = document.title; + +/** + * + */ +Editor.prototype.editBlankUrl = window.location.protocol + '//' + window.location.host + '/'; + +/** + * Initializes the environment. + */ +Editor.prototype.init = function() { }; + +/** + * Sets the XML node for the current diagram. + */ +Editor.prototype.isChromelessView = function() +{ + return this.chromeless; +}; + +/** + * Sets the XML node for the current diagram. + */ +Editor.prototype.setAutosave = function(value) +{ + this.autosave = value; + this.fireEvent(new mxEventObject('autosaveChanged')); +}; + +/** + * + */ +Editor.prototype.getEditBlankUrl = function(params) +{ + return this.editBlankUrl + params; +} + +/** + * + */ +Editor.prototype.editAsNew = function(xml, title) +{ + var p = (title != null) ? '?title=' + encodeURIComponent(title) : ''; + + if (urlParams['ui'] != null) + { + p += ((p.length > 0) ? '&' : '?') + 'ui=' + urlParams['ui']; + } + + if (this.editorWindow != null && !this.editorWindow.closed) + { + this.editorWindow.focus(); + } + else + { + if (typeof window.postMessage !== 'undefined' && (document.documentMode == null || document.documentMode >= 10)) + { + if (this.editorWindow == null) + { + mxEvent.addListener(window, 'message', mxUtils.bind(this, function(evt) + { + if (evt.data == 'ready' && evt.source == this.editorWindow) + { + this.editorWindow.postMessage(xml, '*'); + } + })); + } + + this.editorWindow = this.graph.openLink(this.getEditBlankUrl(p + + ((p.length > 0) ? '&' : '?') + 'client=1'), null, true); + } + else + { + this.editorWindow = this.graph.openLink(this.getEditBlankUrl(p) + + '#R' + encodeURIComponent(xml)); + } + } +}; + +/** + * Sets the XML node for the current diagram. + */ +Editor.prototype.createGraph = function(themes, model) +{ + var graph = new Graph(null, model, null, null, themes); + graph.transparentBackground = false; + + // Opens all links in a new window while editing + if (!this.chromeless) + { + graph.isBlankLink = function(href) + { + return !this.isExternalProtocol(href); + }; + } + + return graph; +}; + +/** + * Sets the XML node for the current diagram. + */ +Editor.prototype.resetGraph = function() +{ + this.graph.gridEnabled = !this.isChromelessView() || urlParams['grid'] == '1'; + this.graph.graphHandler.guidesEnabled = true; + this.graph.setTooltips(true); + this.graph.setConnectable(true); + this.graph.foldingEnabled = true; + this.graph.scrollbars = this.graph.defaultScrollbars; + this.graph.pageVisible = this.graph.defaultPageVisible; + this.graph.pageBreaksVisible = this.graph.pageVisible; + this.graph.preferPageSize = this.graph.pageBreaksVisible; + this.graph.background = this.graph.defaultGraphBackground; + this.graph.pageScale = mxGraph.prototype.pageScale; + this.graph.pageFormat = mxGraph.prototype.pageFormat; + this.graph.currentScale = 1; + this.graph.currentTranslate.x = 0; + this.graph.currentTranslate.y = 0; + this.updateGraphComponents(); + this.graph.view.setScale(1); +}; + +/** + * Sets the XML node for the current diagram. + */ +Editor.prototype.readGraphState = function(node) +{ + this.graph.gridEnabled = node.getAttribute('grid') != '0' && (!this.isChromelessView() || urlParams['grid'] == '1'); + this.graph.gridSize = parseFloat(node.getAttribute('gridSize')) || mxGraph.prototype.gridSize; + this.graph.graphHandler.guidesEnabled = node.getAttribute('guides') != '0'; + this.graph.setTooltips(node.getAttribute('tooltips') != '0'); + this.graph.setConnectable(node.getAttribute('connect') != '0'); + this.graph.connectionArrowsEnabled = node.getAttribute('arrows') != '0'; + this.graph.foldingEnabled = node.getAttribute('fold') != '0'; + + if (this.isChromelessView() && this.graph.foldingEnabled) + { + this.graph.foldingEnabled = urlParams['nav'] == '1'; + this.graph.cellRenderer.forceControlClickHandler = this.graph.foldingEnabled; + } + + var ps = node.getAttribute('pageScale'); + + if (ps != null) + { + this.graph.pageScale = ps; + } + else + { + this.graph.pageScale = mxGraph.prototype.pageScale; + } + + if (!this.graph.isLightboxView()) + { + var pv = node.getAttribute('page'); + + if (pv != null) + { + this.graph.pageVisible = (pv != '0'); + } + else + { + this.graph.pageVisible = this.graph.defaultPageVisible; + } + } + else + { + this.graph.pageVisible = false; + } + + this.graph.pageBreaksVisible = this.graph.pageVisible; + this.graph.preferPageSize = this.graph.pageBreaksVisible; + + var pw = node.getAttribute('pageWidth'); + var ph = node.getAttribute('pageHeight'); + + if (pw != null && ph != null) + { + this.graph.pageFormat = new mxRectangle(0, 0, parseFloat(pw), parseFloat(ph)); + } + + // Loads the persistent state settings + var bg = node.getAttribute('background'); + + if (bg != null && bg.length > 0) + { + this.graph.background = bg; + } + else + { + this.graph.background = this.graph.defaultGraphBackground; + } +}; + +/** + * Sets the XML node for the current diagram. + */ +Editor.prototype.setGraphXml = function(node) +{ + if (node != null) + { + var dec = new mxCodec(node.ownerDocument); + + if (node.nodeName == 'mxGraphModel') + { + this.graph.model.beginUpdate(); + + try + { + this.graph.model.clear(); + this.graph.view.scale = 1; + this.readGraphState(node); + this.updateGraphComponents(); + dec.decode(node, this.graph.getModel()); + } + finally + { + this.graph.model.endUpdate(); + } + + this.fireEvent(new mxEventObject('resetGraphView')); + } + else if (node.nodeName == 'root') + { + this.resetGraph(); + + // Workaround for invalid XML output in Firefox 20 due to bug in mxUtils.getXml + var wrapper = dec.document.createElement('mxGraphModel'); + wrapper.appendChild(node); + + dec.decode(wrapper, this.graph.getModel()); + this.updateGraphComponents(); + this.fireEvent(new mxEventObject('resetGraphView')); + } + else + { + throw { + message: mxResources.get('cannotOpenFile'), + node: node, + toString: function() { return this.message; } + }; + } + } + else + { + this.resetGraph(); + this.graph.model.clear(); + this.fireEvent(new mxEventObject('resetGraphView')); + } +}; + +/** + * Returns the XML node that represents the current diagram. + */ +Editor.prototype.getGraphXml = function(ignoreSelection) +{ + ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; + var node = null; + + if (ignoreSelection) + { + var enc = new mxCodec(mxUtils.createXmlDocument()); + node = enc.encode(this.graph.getModel()); + } + else + { + node = this.graph.encodeCells(mxUtils.sortCells(this.graph.model.getTopmostCells( + this.graph.getSelectionCells()))); + } + + if (this.graph.view.translate.x != 0 || this.graph.view.translate.y != 0) + { + node.setAttribute('dx', Math.round(this.graph.view.translate.x * 100) / 100); + node.setAttribute('dy', Math.round(this.graph.view.translate.y * 100) / 100); + } + + node.setAttribute('grid', (this.graph.isGridEnabled()) ? '1' : '0'); + node.setAttribute('gridSize', this.graph.gridSize); + node.setAttribute('guides', (this.graph.graphHandler.guidesEnabled) ? '1' : '0'); + node.setAttribute('tooltips', (this.graph.tooltipHandler.isEnabled()) ? '1' : '0'); + node.setAttribute('connect', (this.graph.connectionHandler.isEnabled()) ? '1' : '0'); + node.setAttribute('arrows', (this.graph.connectionArrowsEnabled) ? '1' : '0'); + node.setAttribute('fold', (this.graph.foldingEnabled) ? '1' : '0'); + node.setAttribute('page', (this.graph.pageVisible) ? '1' : '0'); + node.setAttribute('pageScale', this.graph.pageScale); + node.setAttribute('pageWidth', this.graph.pageFormat.width); + node.setAttribute('pageHeight', this.graph.pageFormat.height); + + if (this.graph.background != null) + { + node.setAttribute('background', this.graph.background); + } + + return node; +}; + +/** + * Keeps the graph container in sync with the persistent graph state + */ +Editor.prototype.updateGraphComponents = function() +{ + var graph = this.graph; + + if (graph.container != null) + { + graph.view.validateBackground(); + graph.container.style.overflow = (graph.scrollbars) ? 'auto' : 'hidden'; + + this.fireEvent(new mxEventObject('updateGraphComponents')); + } +}; + +/** + * Sets the modified flag. + */ +Editor.prototype.setModified = function(value) +{ + this.modified = value; +}; + +/** + * Sets the filename. + */ +Editor.prototype.setFilename = function(value) +{ + this.filename = value; +}; + +/** + * Creates and returns a new undo manager. + */ +Editor.prototype.createUndoManager = function() +{ + var graph = this.graph; + var undoMgr = new mxUndoManager(); + + this.undoListener = function(sender, evt) + { + undoMgr.undoableEditHappened(evt.getProperty('edit')); + }; + + // Installs the command history + var listener = mxUtils.bind(this, function(sender, evt) + { + this.undoListener.apply(this, arguments); + }); + + graph.getModel().addListener(mxEvent.UNDO, listener); + graph.getView().addListener(mxEvent.UNDO, listener); + + // Keeps the selection in sync with the history + var undoHandler = function(sender, evt) + { + var cand = graph.getSelectionCellsForChanges(evt.getProperty('edit').changes); + var model = graph.getModel(); + var cells = []; + + for (var i = 0; i < cand.length; i++) + { + if ((model.isVertex(cand[i]) || model.isEdge(cand[i])) && graph.view.getState(cand[i]) != null) + { + cells.push(cand[i]); + } + } + + graph.setSelectionCells(cells); + }; + + undoMgr.addListener(mxEvent.UNDO, undoHandler); + undoMgr.addListener(mxEvent.REDO, undoHandler); + + return undoMgr; +}; + +/** + * Adds basic stencil set (no namespace). + */ +Editor.prototype.initStencilRegistry = function() { }; + +/** + * Creates and returns a new undo manager. + */ +Editor.prototype.destroy = function() +{ + if (this.graph != null) + { + this.graph.destroy(); + this.graph = null; + } +}; + +/** + * Class for asynchronously opening a new window and loading a file at the same + * time. This acts as a bridge between the open dialog and the new editor. + */ +OpenFile = function(done) +{ + this.producer = null; + this.consumer = null; + this.done = done; + this.args = null; +}; + +/** + * Registers the editor from the new window. + */ +OpenFile.prototype.setConsumer = function(value) +{ + this.consumer = value; + this.execute(); +}; + +/** + * Sets the data from the loaded file. + */ +OpenFile.prototype.setData = function() +{ + this.args = arguments; + this.execute(); +}; + +/** + * Displays an error message. + */ +OpenFile.prototype.error = function(msg) +{ + this.cancel(true); + mxUtils.alert(msg); +}; + +/** + * Consumes the data. + */ +OpenFile.prototype.execute = function() +{ + if (this.consumer != null && this.args != null) + { + this.cancel(false); + this.consumer.apply(this, this.args); + } +}; + +/** + * Cancels the operation. + */ +OpenFile.prototype.cancel = function(cancel) +{ + if (this.done != null) + { + this.done((cancel != null) ? cancel : true); + } +}; + +/** + * Basic dialogs that are available in the viewer (print dialog). + */ +function Dialog(editorUi, elt, w, h, modal, closable, onClose, noScroll, transparent) +{ + var dx = 0; + + if (mxClient.IS_VML && (document.documentMode == null || document.documentMode < 8)) + { + // Adds padding as a workaround for box model in older IE versions + // This needs to match the total padding of geDialog in CSS + dx = 80; + } + + w += dx; + h += dx; + + var w0 = w; + var h0 = h; + + // clientHeight check is attempted fix for print dialog offset in viewer lightbox + var dh = (document.documentElement.clientHeight > 0) ? document.documentElement.clientHeight : + Math.max(document.body.clientHeight || 0, document.documentElement.clientHeight); + var left = Math.max(1, Math.round((document.body.clientWidth - w - 64) / 2)); + var top = Math.max(1, Math.round((dh - h - editorUi.footerHeight) / 3)); + + // Keeps window size inside available space + if (!mxClient.IS_QUIRKS) + { + elt.style.maxHeight = '100%'; + } + + w = Math.min(w, document.body.scrollWidth - 64); + h = Math.min(h, dh - 64); + + // Increments zIndex to put subdialogs and background over existing dialogs and background + if (editorUi.dialogs.length > 0) + { + this.zIndex += editorUi.dialogs.length * 2; + } + + if (this.bg == null) + { + this.bg = editorUi.createDiv('background'); + this.bg.style.position = 'absolute'; + this.bg.style.background = Dialog.backdropColor; + this.bg.style.height = dh + 'px'; + this.bg.style.right = '0px'; + this.bg.style.zIndex = this.zIndex - 2; + + mxUtils.setOpacity(this.bg, this.bgOpacity); + + if (mxClient.IS_QUIRKS) + { + new mxDivResizer(this.bg); + } + } + + var origin = mxUtils.getDocumentScrollOrigin(document); + this.bg.style.left = origin.x + 'px'; + this.bg.style.top = origin.y + 'px'; + left += origin.x; + top += origin.y; + + if (modal) + { + document.body.appendChild(this.bg); + } + + var div = editorUi.createDiv(transparent? 'geTransDialog' : 'geDialog'); + var pos = this.getPosition(left, top, w, h); + left = pos.x; + top = pos.y; + + div.style.width = w + 'px'; + div.style.height = h + 'px'; + div.style.left = left + 'px'; + div.style.top = top + 'px'; + div.style.zIndex = this.zIndex; + + div.appendChild(elt); + document.body.appendChild(div); + + // Adds vertical scrollbars if needed + if (!noScroll && elt.clientHeight > div.clientHeight - 64) + { + elt.style.overflowY = 'auto'; + } + + if (closable) + { + var img = document.createElement('img'); + + img.setAttribute('src', Dialog.prototype.closeImage); + img.setAttribute('title', mxResources.get('close')); + img.className = 'geDialogClose'; + img.style.top = (top + 14) + 'px'; + img.style.left = (left + w + 38 - dx) + 'px'; + img.style.zIndex = this.zIndex; + + mxEvent.addListener(img, 'click', mxUtils.bind(this, function() + { + editorUi.hideDialog(true); + })); + + document.body.appendChild(img); + this.dialogImg = img; + + mxEvent.addGestureListeners(this.bg, null, null, mxUtils.bind(this, function(evt) + { + editorUi.hideDialog(true); + })); + } + + this.resizeListener = mxUtils.bind(this, function() + { + dh = Math.max(document.body.clientHeight, document.documentElement.clientHeight); + this.bg.style.height = dh + 'px'; + + left = Math.max(1, Math.round((document.body.clientWidth - w - 64) / 2)); + top = Math.max(1, Math.round((dh - h - editorUi.footerHeight) / 3)); + w = Math.min(w0, document.body.scrollWidth - 64); + h = Math.min(h0, dh - 64); + + var pos = this.getPosition(left, top, w, h); + left = pos.x; + top = pos.y; + + div.style.left = left + 'px'; + div.style.top = top + 'px'; + div.style.width = w + 'px'; + div.style.height = h + 'px'; + + // Adds vertical scrollbars if needed + if (!noScroll && elt.clientHeight > div.clientHeight - 64) + { + elt.style.overflowY = 'auto'; + } + + if (this.dialogImg != null) + { + this.dialogImg.style.top = (top + 14) + 'px'; + this.dialogImg.style.left = (left + w + 38 - dx) + 'px'; + } + }); + + mxEvent.addListener(window, 'resize', this.resizeListener); + + this.onDialogClose = onClose; + this.container = div; + + editorUi.editor.fireEvent(new mxEventObject('showDialog')); +}; + +/** + * + */ +Dialog.backdropColor = 'white'; + +/** + * + */ +Dialog.prototype.zIndex = mxPopupMenu.prototype.zIndex - 1; + +/** + * + */ +Dialog.prototype.noColorImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/nocolor.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkEzRDlBMUUwODYxMTExRTFCMzA4RDdDMjJBMEMxRDM3IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkEzRDlBMUUxODYxMTExRTFCMzA4RDdDMjJBMEMxRDM3Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QTNEOUExREU4NjExMTFFMUIzMDhEN0MyMkEwQzFEMzciIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QTNEOUExREY4NjExMTFFMUIzMDhEN0MyMkEwQzFEMzciLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5xh3fmAAAABlBMVEX////MzMw46qqDAAAAGElEQVR42mJggAJGKGAYIIGBth8KAAIMAEUQAIElnLuQAAAAAElFTkSuQmCC'; + +/** + * + */ +Dialog.prototype.closeImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/close.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJAQMAAADaX5RTAAAABlBMVEV7mr3///+wksspAAAAAnRSTlP/AOW3MEoAAAAdSURBVAgdY9jXwCDDwNDRwHCwgeExmASygSL7GgB12QiqNHZZIwAAAABJRU5ErkJggg=='; + +/** + * + */ +Dialog.prototype.clearImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/clear.gif' : 'data:image/gif;base64,R0lGODlhDQAKAIABAMDAwP///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjAgNjEuMTM0Nzc3LCAyMDEwLzAyLzEyLTE3OjMyOjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUIzOEM1NzI4NjEyMTFFMUEzMkNDMUE3NjZERDE2QjIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUIzOEM1NzM4NjEyMTFFMUEzMkNDMUE3NjZERDE2QjIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QjM4QzU3MDg2MTIxMUUxQTMyQ0MxQTc2NkREMTZCMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QjM4QzU3MTg2MTIxMUUxQTMyQ0MxQTc2NkREMTZCMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6alpKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmFgX15dXFtaWVhXVlVUU1JRUE9OTUxLSklIR0ZFRENCQUA/Pj08Ozo5ODc2NTQzMjEwLy4tLCsqKSgnJiUkIyIhIB8eHRwbGhkYFxYVFBMSERAPDg0MCwoJCAcGBQQDAgEAACH5BAEAAAEALAAAAAANAAoAAAIXTGCJebD9jEOTqRlttXdrB32PJ2ncyRQAOw=='; + +/** + * + */ +Dialog.prototype.lockedImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/locked.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAMAAABhq6zVAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MzdDMDZCODExNzIxMTFFNUI0RTk5NTg4OTcyMUUyODEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MzdDMDZCODIxNzIxMTFFNUI0RTk5NTg4OTcyMUUyODEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozN0MwNkI3RjE3MjExMUU1QjRFOTk1ODg5NzIxRTI4MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDozN0MwNkI4MDE3MjExMUU1QjRFOTk1ODg5NzIxRTI4MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvqMCFYAAAAVUExURZmZmb+/v7KysqysrMzMzLGxsf///4g8N1cAAAAHdFJOU////////wAaSwNGAAAAPElEQVR42lTMQQ4AIQgEwUa0//9kTQirOweYOgDqAMbZUr10AGlAwx4/BJ2QJ4U0L5brYjovvpv32xZgAHZaATFtMbu4AAAAAElFTkSuQmCC'; + +/** + * + */ +Dialog.prototype.unlockedImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/unlocked.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAMAAABhq6zVAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MzdDMDZCN0QxNzIxMTFFNUI0RTk5NTg4OTcyMUUyODEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MzdDMDZCN0UxNzIxMTFFNUI0RTk5NTg4OTcyMUUyODEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozN0MwNkI3QjE3MjExMUU1QjRFOTk1ODg5NzIxRTI4MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDozN0MwNkI3QzE3MjExMUU1QjRFOTk1ODg5NzIxRTI4MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkKMpVwAAAAYUExURZmZmbKysr+/v6ysrOXl5czMzLGxsf///zHN5lwAAAAIdFJOU/////////8A3oO9WQAAADxJREFUeNpUzFESACAEBNBVsfe/cZJU+8Mzs8CIABCidtfGOndnYsT40HDSiCcbPdoJo10o9aI677cpwACRoAF3dFNlswAAAABJRU5ErkJggg=='; + +/** + * Removes the dialog from the DOM. + */ +Dialog.prototype.bgOpacity = 80; + +/** + * Removes the dialog from the DOM. + */ +Dialog.prototype.getPosition = function(left, top) +{ + return new mxPoint(left, top); +}; + +/** + * Removes the dialog from the DOM. + */ +Dialog.prototype.close = function(cancel) +{ + if (this.onDialogClose != null) + { + this.onDialogClose(cancel); + this.onDialogClose = null; + } + + if (this.dialogImg != null) + { + this.dialogImg.parentNode.removeChild(this.dialogImg); + this.dialogImg = null; + } + + if (this.bg != null && this.bg.parentNode != null) + { + this.bg.parentNode.removeChild(this.bg); + } + + mxEvent.removeListener(window, 'resize', this.resizeListener); + this.container.parentNode.removeChild(this.container); +}; + +/** + * Constructs a new print dialog. + */ +var PrintDialog = function(editorUi, title) +{ + this.create(editorUi, title); +}; + +/** + * Constructs a new print dialog. + */ +PrintDialog.prototype.create = function(editorUi) +{ + var graph = editorUi.editor.graph; + var row, td; + + var table = document.createElement('table'); + table.style.width = '100%'; + table.style.height = '100%'; + var tbody = document.createElement('tbody'); + + row = document.createElement('tr'); + + var onePageCheckBox = document.createElement('input'); + onePageCheckBox.setAttribute('type', 'checkbox'); + td = document.createElement('td'); + td.setAttribute('colspan', '2'); + td.style.fontSize = '10pt'; + td.appendChild(onePageCheckBox); + + var span = document.createElement('span'); + mxUtils.write(span, ' ' + mxResources.get('fitPage')); + td.appendChild(span); + + mxEvent.addListener(span, 'click', function(evt) + { + onePageCheckBox.checked = !onePageCheckBox.checked; + pageCountCheckBox.checked = !onePageCheckBox.checked; + mxEvent.consume(evt); + }); + + mxEvent.addListener(onePageCheckBox, 'change', function() + { + pageCountCheckBox.checked = !onePageCheckBox.checked; + }); + + row.appendChild(td); + tbody.appendChild(row); + + row = row.cloneNode(false); + + var pageCountCheckBox = document.createElement('input'); + pageCountCheckBox.setAttribute('type', 'checkbox'); + td = document.createElement('td'); + td.style.fontSize = '10pt'; + td.appendChild(pageCountCheckBox); + + var span = document.createElement('span'); + mxUtils.write(span, ' ' + mxResources.get('posterPrint') + ':'); + td.appendChild(span); + + mxEvent.addListener(span, 'click', function(evt) + { + pageCountCheckBox.checked = !pageCountCheckBox.checked; + onePageCheckBox.checked = !pageCountCheckBox.checked; + mxEvent.consume(evt); + }); + + row.appendChild(td); + + var pageCountInput = document.createElement('input'); + pageCountInput.setAttribute('value', '1'); + pageCountInput.setAttribute('type', 'number'); + pageCountInput.setAttribute('min', '1'); + pageCountInput.setAttribute('size', '4'); + pageCountInput.setAttribute('disabled', 'disabled'); + pageCountInput.style.width = '50px'; + + td = document.createElement('td'); + td.style.fontSize = '10pt'; + td.appendChild(pageCountInput); + mxUtils.write(td, ' ' + mxResources.get('pages') + ' (max)'); + row.appendChild(td); + tbody.appendChild(row); + + mxEvent.addListener(pageCountCheckBox, 'change', function() + { + if (pageCountCheckBox.checked) + { + pageCountInput.removeAttribute('disabled'); + } + else + { + pageCountInput.setAttribute('disabled', 'disabled'); + } + + onePageCheckBox.checked = !pageCountCheckBox.checked; + }); + + row = row.cloneNode(false); + + td = document.createElement('td'); + mxUtils.write(td, mxResources.get('pageScale') + ':'); + row.appendChild(td); + + td = document.createElement('td'); + var pageScaleInput = document.createElement('input'); + pageScaleInput.setAttribute('value', '100 %'); + pageScaleInput.setAttribute('size', '5'); + pageScaleInput.style.width = '50px'; + + td.appendChild(pageScaleInput); + row.appendChild(td); + tbody.appendChild(row); + + row = document.createElement('tr'); + td = document.createElement('td'); + td.colSpan = 2; + td.style.paddingTop = '20px'; + td.setAttribute('align', 'right'); + + // Overall scale for print-out to account for print borders in dialogs etc + function preview(print) + { + var autoOrigin = onePageCheckBox.checked || pageCountCheckBox.checked; + var printScale = parseInt(pageScaleInput.value) / 100; + + if (isNaN(printScale)) + { + printScale = 1; + pageScaleInput.value = '100%'; + } + + // Workaround to match available paper size in actual print output + printScale *= 0.75; + + var pf = graph.pageFormat || mxConstants.PAGE_FORMAT_A4_PORTRAIT; + var scale = 1 / graph.pageScale; + + if (autoOrigin) + { + var pageCount = (onePageCheckBox.checked) ? 1 : parseInt(pageCountInput.value); + + if (!isNaN(pageCount)) + { + scale = mxUtils.getScaleForPageCount(pageCount, graph, pf); + } + } + + // Negative coordinates are cropped or shifted if page visible + var gb = graph.getGraphBounds(); + var border = 0; + var x0 = 0; + var y0 = 0; + + // Applies print scale + pf = mxRectangle.fromRectangle(pf); + pf.width = Math.ceil(pf.width * printScale); + pf.height = Math.ceil(pf.height * printScale); + scale *= printScale; + + // Starts at first visible page + if (!autoOrigin && graph.pageVisible) + { + var layout = graph.getPageLayout(); + x0 -= layout.x * pf.width; + y0 -= layout.y * pf.height; + } + else + { + autoOrigin = true; + } + + var preview = PrintDialog.createPrintPreview(graph, scale, pf, border, x0, y0, autoOrigin); + preview.open(); + + if (print) + { + PrintDialog.printPreview(preview); + } + }; + + var cancelBtn = mxUtils.button(mxResources.get('cancel'), function() + { + editorUi.hideDialog(); + }); + cancelBtn.className = 'geBtn'; + + if (editorUi.editor.cancelFirst) + { + td.appendChild(cancelBtn); + } + + if (PrintDialog.previewEnabled) + { + var previewBtn = mxUtils.button(mxResources.get('preview'), function() + { + editorUi.hideDialog(); + preview(false); + }); + previewBtn.className = 'geBtn'; + td.appendChild(previewBtn); + } + + var printBtn = mxUtils.button(mxResources.get((!PrintDialog.previewEnabled) ? 'ok' : 'print'), function() + { + editorUi.hideDialog(); + preview(true); + }); + printBtn.className = 'geBtn gePrimaryBtn'; + td.appendChild(printBtn); + + if (!editorUi.editor.cancelFirst) + { + td.appendChild(cancelBtn); + } + + row.appendChild(td); + tbody.appendChild(row); + + table.appendChild(tbody); + this.container = table; +}; + +/** + * Constructs a new print dialog. + */ +PrintDialog.printPreview = function(preview) +{ + if (preview.wnd != null) + { + var printFn = function() + { + preview.wnd.focus(); + preview.wnd.print(); + preview.wnd.close(); + }; + + // Workaround for Google Chrome which needs a bit of a + // delay in order to render the SVG contents + // Needs testing in production + if (mxClient.IS_GC) + { + window.setTimeout(printFn, 500); + } + else + { + printFn(); + } + } +}; + +/** + * Constructs a new print dialog. + */ +PrintDialog.createPrintPreview = function(graph, scale, pf, border, x0, y0, autoOrigin) +{ + var preview = new mxPrintPreview(graph, scale, pf, border, x0, y0); + preview.title = mxResources.get('preview'); + preview.printBackgroundImage = true; + preview.autoOrigin = autoOrigin; + var bg = graph.background; + + if (bg == null || bg == '' || bg == mxConstants.NONE) + { + bg = '#ffffff'; + } + + preview.backgroundColor = bg; + + var writeHead = preview.writeHead; + + // Adds a border in the preview + preview.writeHead = function(doc) + { + writeHead.apply(this, arguments); + + doc.writeln(''); + }; + + return preview; +}; + +/** + * Specifies if the preview button should be enabled. Default is true. + */ +PrintDialog.previewEnabled = true; + +/** + * Constructs a new page setup dialog. + */ +var PageSetupDialog = function(editorUi) +{ + var graph = editorUi.editor.graph; + var row, td; + + var table = document.createElement('table'); + table.style.width = '100%'; + table.style.height = '100%'; + var tbody = document.createElement('tbody'); + + row = document.createElement('tr'); + + td = document.createElement('td'); + td.style.verticalAlign = 'top'; + td.style.fontSize = '10pt'; + mxUtils.write(td, mxResources.get('paperSize') + ':'); + + row.appendChild(td); + + td = document.createElement('td'); + td.style.verticalAlign = 'top'; + td.style.fontSize = '10pt'; + + var accessor = PageSetupDialog.addPageFormatPanel(td, 'pagesetupdialog', graph.pageFormat); + + row.appendChild(td); + tbody.appendChild(row); + + row = document.createElement('tr'); + + td = document.createElement('td'); + mxUtils.write(td, mxResources.get('background') + ':'); + + row.appendChild(td); + + td = document.createElement('td'); + td.style.whiteSpace = 'nowrap'; + + var backgroundInput = document.createElement('input'); + backgroundInput.setAttribute('type', 'text'); + var backgroundButton = document.createElement('button'); + + backgroundButton.style.width = '18px'; + backgroundButton.style.height = '18px'; + backgroundButton.style.marginRight = '20px'; + backgroundButton.style.backgroundPosition = 'center center'; + backgroundButton.style.backgroundRepeat = 'no-repeat'; + + var newBackgroundColor = graph.background; + + function updateBackgroundColor() + { + if (newBackgroundColor == null || newBackgroundColor == mxConstants.NONE) + { + backgroundButton.style.backgroundColor = ''; + backgroundButton.style.backgroundImage = 'url(\'' + Dialog.prototype.noColorImage + '\')'; + } + else + { + backgroundButton.style.backgroundColor = newBackgroundColor; + backgroundButton.style.backgroundImage = ''; + } + }; + + updateBackgroundColor(); + + mxEvent.addListener(backgroundButton, 'click', function(evt) + { + editorUi.pickColor(newBackgroundColor || 'none', function(color) + { + newBackgroundColor = color; + updateBackgroundColor(); + }); + mxEvent.consume(evt); + }); + + td.appendChild(backgroundButton); + + mxUtils.write(td, mxResources.get('gridSize') + ':'); + + var gridSizeInput = document.createElement('input'); + gridSizeInput.setAttribute('type', 'number'); + gridSizeInput.setAttribute('min', '0'); + gridSizeInput.style.width = '40px'; + gridSizeInput.style.marginLeft = '6px'; + + gridSizeInput.value = graph.getGridSize(); + td.appendChild(gridSizeInput); + + mxEvent.addListener(gridSizeInput, 'change', function() + { + var value = parseInt(gridSizeInput.value); + gridSizeInput.value = Math.max(1, (isNaN(value)) ? graph.getGridSize() : value); + }); + + row.appendChild(td); + tbody.appendChild(row); + + row = document.createElement('tr'); + td = document.createElement('td'); + + mxUtils.write(td, mxResources.get('image') + ':'); + + row.appendChild(td); + td = document.createElement('td'); + + var changeImageLink = document.createElement('a'); + changeImageLink.style.textDecoration = 'underline'; + changeImageLink.style.cursor = 'pointer'; + changeImageLink.style.color = '#a0a0a0'; + + var newBackgroundImage = graph.backgroundImage; + + function updateBackgroundImage() + { + if (newBackgroundImage == null) + { + changeImageLink.removeAttribute('title'); + changeImageLink.style.fontSize = ''; + changeImageLink.innerHTML = mxResources.get('change') + '...'; + } + else + { + changeImageLink.setAttribute('title', newBackgroundImage.src); + changeImageLink.style.fontSize = '11px'; + changeImageLink.innerHTML = newBackgroundImage.src.substring(0, 42) + '...'; + } + }; + + mxEvent.addListener(changeImageLink, 'click', function(evt) + { + editorUi.showBackgroundImageDialog(function(image) + { + newBackgroundImage = image; + updateBackgroundImage(); + }); + + mxEvent.consume(evt); + }); + + updateBackgroundImage(); + + td.appendChild(changeImageLink); + + row.appendChild(td); + tbody.appendChild(row); + + row = document.createElement('tr'); + td = document.createElement('td'); + td.colSpan = 2; + td.style.paddingTop = '16px'; + td.setAttribute('align', 'right'); + + var cancelBtn = mxUtils.button(mxResources.get('cancel'), function() + { + editorUi.hideDialog(); + }); + cancelBtn.className = 'geBtn'; + + if (editorUi.editor.cancelFirst) + { + td.appendChild(cancelBtn); + } + + var applyBtn = mxUtils.button(mxResources.get('apply'), function() + { + editorUi.hideDialog(); + + if (graph.gridSize !== gridSizeInput.value) + { + graph.setGridSize(parseInt(gridSizeInput.value)); + } + + var change = new ChangePageSetup(editorUi, newBackgroundColor, + newBackgroundImage, accessor.get()); + change.ignoreColor = graph.background == newBackgroundColor; + + var oldSrc = (graph.backgroundImage != null) ? graph.backgroundImage.src : null; + var newSrc = (newBackgroundImage != null) ? newBackgroundImage.src : null; + + change.ignoreImage = oldSrc === newSrc; + + if (graph.pageFormat.width != change.previousFormat.width || + graph.pageFormat.height != change.previousFormat.height || + !change.ignoreColor || !change.ignoreImage) + { + graph.model.execute(change); + } + }); + applyBtn.className = 'geBtn gePrimaryBtn'; + td.appendChild(applyBtn); + + if (!editorUi.editor.cancelFirst) + { + td.appendChild(cancelBtn); + } + + row.appendChild(td); + tbody.appendChild(row); + + table.appendChild(tbody); + this.container = table; +}; + +/** + * + */ +PageSetupDialog.addPageFormatPanel = function(div, namePostfix, pageFormat, pageFormatListener) +{ + var formatName = 'format-' + namePostfix; + + var portraitCheckBox = document.createElement('input'); + portraitCheckBox.setAttribute('name', formatName); + portraitCheckBox.setAttribute('type', 'radio'); + portraitCheckBox.setAttribute('value', 'portrait'); + + var landscapeCheckBox = document.createElement('input'); + landscapeCheckBox.setAttribute('name', formatName); + landscapeCheckBox.setAttribute('type', 'radio'); + landscapeCheckBox.setAttribute('value', 'landscape'); + + var paperSizeSelect = document.createElement('select'); + paperSizeSelect.style.marginBottom = '8px'; + paperSizeSelect.style.width = '202px'; + + var formatDiv = document.createElement('div'); + formatDiv.style.marginLeft = '4px'; + formatDiv.style.width = '210px'; + formatDiv.style.height = '24px'; + + portraitCheckBox.style.marginRight = '6px'; + formatDiv.appendChild(portraitCheckBox); + + var portraitSpan = document.createElement('span'); + portraitSpan.style.maxWidth = '100px'; + mxUtils.write(portraitSpan, mxResources.get('portrait')); + formatDiv.appendChild(portraitSpan); + + landscapeCheckBox.style.marginLeft = '10px'; + landscapeCheckBox.style.marginRight = '6px'; + formatDiv.appendChild(landscapeCheckBox); + + var landscapeSpan = document.createElement('span'); + landscapeSpan.style.width = '100px'; + mxUtils.write(landscapeSpan, mxResources.get('landscape')); + formatDiv.appendChild(landscapeSpan) + + var customDiv = document.createElement('div'); + customDiv.style.marginLeft = '4px'; + customDiv.style.width = '210px'; + customDiv.style.height = '24px'; + + var widthInput = document.createElement('input'); + widthInput.setAttribute('size', '7'); + widthInput.style.textAlign = 'right'; + customDiv.appendChild(widthInput); + mxUtils.write(customDiv, ' in x '); + + var heightInput = document.createElement('input'); + heightInput.setAttribute('size', '7'); + heightInput.style.textAlign = 'right'; + customDiv.appendChild(heightInput); + mxUtils.write(customDiv, ' in'); + + formatDiv.style.display = 'none'; + customDiv.style.display = 'none'; + + var pf = new Object(); + var formats = PageSetupDialog.getFormats(); + + for (var i = 0; i < formats.length; i++) + { + var f = formats[i]; + pf[f.key] = f; + + var paperSizeOption = document.createElement('option'); + paperSizeOption.setAttribute('value', f.key); + mxUtils.write(paperSizeOption, f.title); + paperSizeSelect.appendChild(paperSizeOption); + } + + var customSize = false; + + function listener(sender, evt, force) + { + if (force || (widthInput != document.activeElement && heightInput != document.activeElement)) + { + var detected = false; + + for (var i = 0; i < formats.length; i++) + { + var f = formats[i]; + + // Special case where custom was chosen + if (customSize) + { + if (f.key == 'custom') + { + paperSizeSelect.value = f.key; + customSize = false; + } + } + else if (f.format != null) + { + // Fixes wrong values for previous A4 and A5 page sizes + if (f.key == 'a4') + { + if (pageFormat.width == 826) + { + pageFormat = mxRectangle.fromRectangle(pageFormat); + pageFormat.width = 827; + } + else if (pageFormat.height == 826) + { + pageFormat = mxRectangle.fromRectangle(pageFormat); + pageFormat.height = 827; + } + } + else if (f.key == 'a5') + { + if (pageFormat.width == 584) + { + pageFormat = mxRectangle.fromRectangle(pageFormat); + pageFormat.width = 583; + } + else if (pageFormat.height == 584) + { + pageFormat = mxRectangle.fromRectangle(pageFormat); + pageFormat.height = 583; + } + } + + if (pageFormat.width == f.format.width && pageFormat.height == f.format.height) + { + paperSizeSelect.value = f.key; + portraitCheckBox.setAttribute('checked', 'checked'); + portraitCheckBox.defaultChecked = true; + portraitCheckBox.checked = true; + landscapeCheckBox.removeAttribute('checked'); + landscapeCheckBox.defaultChecked = false; + landscapeCheckBox.checked = false; + detected = true; + } + else if (pageFormat.width == f.format.height && pageFormat.height == f.format.width) + { + paperSizeSelect.value = f.key; + portraitCheckBox.removeAttribute('checked'); + portraitCheckBox.defaultChecked = false; + portraitCheckBox.checked = false; + landscapeCheckBox.setAttribute('checked', 'checked'); + landscapeCheckBox.defaultChecked = true; + landscapeCheckBox.checked = true; + detected = true; + } + } + } + + // Selects custom format which is last in list + if (!detected) + { + widthInput.value = pageFormat.width / 100; + heightInput.value = pageFormat.height / 100; + portraitCheckBox.setAttribute('checked', 'checked'); + paperSizeSelect.value = 'custom'; + formatDiv.style.display = 'none'; + customDiv.style.display = ''; + } + else + { + formatDiv.style.display = ''; + customDiv.style.display = 'none'; + } + } + }; + + listener(); + + div.appendChild(paperSizeSelect); + mxUtils.br(div); + + div.appendChild(formatDiv); + div.appendChild(customDiv); + + var currentPageFormat = pageFormat; + + var update = function(evt, selectChanged) + { + var f = pf[paperSizeSelect.value]; + + if (f.format != null) + { + widthInput.value = f.format.width / 100; + heightInput.value = f.format.height / 100; + customDiv.style.display = 'none'; + formatDiv.style.display = ''; + } + else + { + formatDiv.style.display = 'none'; + customDiv.style.display = ''; + } + + if (isNaN(parseFloat(widthInput.value))) + { + widthInput.value = pageFormat.width / 100; + } + + if (isNaN(parseFloat(heightInput.value))) + { + heightInput.value = pageFormat.height / 100; + } + + var newPageFormat = new mxRectangle(0, 0, + Math.floor(parseFloat(widthInput.value) * 100), + Math.floor(parseFloat(heightInput.value) * 100)); + + if (paperSizeSelect.value != 'custom' && landscapeCheckBox.checked) + { + newPageFormat = new mxRectangle(0, 0, newPageFormat.height, newPageFormat.width); + } + + // Initial select of custom should not update page format to avoid update of combo + if ((!selectChanged || !customSize) && (newPageFormat.width != currentPageFormat.width || + newPageFormat.height != currentPageFormat.height)) + { + currentPageFormat = newPageFormat; + + // Updates page format and reloads format panel + if (pageFormatListener != null) + { + pageFormatListener(currentPageFormat); + } + } + }; + + mxEvent.addListener(portraitSpan, 'click', function(evt) + { + portraitCheckBox.checked = true; + update(evt); + mxEvent.consume(evt); + }); + + mxEvent.addListener(landscapeSpan, 'click', function(evt) + { + landscapeCheckBox.checked = true; + update(evt); + mxEvent.consume(evt); + }); + + mxEvent.addListener(widthInput, 'blur', update); + mxEvent.addListener(widthInput, 'click', update); + mxEvent.addListener(heightInput, 'blur', update); + mxEvent.addListener(heightInput, 'click', update); + mxEvent.addListener(landscapeCheckBox, 'change', update); + mxEvent.addListener(portraitCheckBox, 'change', update); + mxEvent.addListener(paperSizeSelect, 'change', function(evt) + { + // Handles special case where custom was chosen + customSize = paperSizeSelect.value == 'custom'; + update(evt, true); + }); + + update(); + + return {set: function(value) + { + pageFormat = value; + listener(null, null, true); + },get: function() + { + return currentPageFormat; + }, widthInput: widthInput, + heightInput: heightInput}; +}; + +/** + * + */ +PageSetupDialog.getFormats = function() +{ + return [{key: 'letter', title: 'US-Letter (8,5" x 11")', format: mxConstants.PAGE_FORMAT_LETTER_PORTRAIT}, + {key: 'legal', title: 'US-Legal (8,5" x 14")', format: new mxRectangle(0, 0, 850, 1400)}, + {key: 'tabloid', title: 'US-Tabloid (279 mm x 432 mm)', format: new mxRectangle(0, 0, 1100, 1700)}, + {key: 'a0', title: 'A0 (841 mm x 1189 mm)', format: new mxRectangle(0, 0, 3300, 4681)}, + {key: 'a1', title: 'A1 (594 mm x 841 mm)', format: new mxRectangle(0, 0, 2339, 3300)}, + {key: 'a2', title: 'A2 (420 mm x 594 mm)', format: new mxRectangle(0, 0, 1654, 2336)}, + {key: 'a3', title: 'A3 (297 mm x 420 mm)', format: new mxRectangle(0, 0, 1169, 1654)}, + {key: 'a4', title: 'A4 (210 mm x 297 mm)', format: mxConstants.PAGE_FORMAT_A4_PORTRAIT}, + {key: 'a5', title: 'A5 (148 mm x 210 mm)', format: new mxRectangle(0, 0, 583, 827)}, + {key: 'a6', title: 'A6 (105 mm x 148 mm)', format: new mxRectangle(0, 0, 413, 583)}, + {key: 'a7', title: 'A7 (74 mm x 105 mm)', format: new mxRectangle(0, 0, 291, 413)}, + {key: 'custom', title: mxResources.get('custom'), format: null}]; +}; + +/** + * Static overrides + */ +(function() +{ + // Uses HTML for background pages (to support grid background image) + mxGraphView.prototype.validateBackgroundPage = function() + { + var graph = this.graph; + + if (graph.container != null && !graph.transparentBackground) + { + if (graph.pageVisible) + { + var bounds = this.getBackgroundPageBounds(); + + if (this.backgroundPageShape == null) + { + // Finds first element in graph container + var firstChild = graph.container.firstChild; + + while (firstChild != null && firstChild.nodeType != mxConstants.NODETYPE_ELEMENT) + { + firstChild = firstChild.nextSibling; + } + + if (firstChild != null) + { + this.backgroundPageShape = this.createBackgroundPageShape(bounds); + this.backgroundPageShape.scale = 1; + + // Shadow filter causes problems in outline window in quirks mode. IE8 standards + // also has known rendering issues inside mxWindow but not using shadow is worse. + this.backgroundPageShape.isShadow = !mxClient.IS_QUIRKS; + this.backgroundPageShape.dialect = mxConstants.DIALECT_STRICTHTML; + this.backgroundPageShape.init(graph.container); + + // Required for the browser to render the background page in correct order + firstChild.style.position = 'absolute'; + graph.container.insertBefore(this.backgroundPageShape.node, firstChild); + this.backgroundPageShape.redraw(); + + this.backgroundPageShape.node.className = 'geBackgroundPage'; + + // Adds listener for double click handling on background + mxEvent.addListener(this.backgroundPageShape.node, 'dblclick', + mxUtils.bind(this, function(evt) + { + graph.dblClick(evt); + }) + ); + + // Adds basic listeners for graph event dispatching outside of the + // container and finishing the handling of a single gesture + mxEvent.addGestureListeners(this.backgroundPageShape.node, + mxUtils.bind(this, function(evt) + { + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt)); + }), + mxUtils.bind(this, function(evt) + { + // Hides the tooltip if mouse is outside container + if (graph.tooltipHandler != null && graph.tooltipHandler.isHideOnHover()) + { + graph.tooltipHandler.hide(); + } + + if (graph.isMouseDown && !mxEvent.isConsumed(evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt)); + } + }), + mxUtils.bind(this, function(evt) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt)); + }) + ); + } + } + else + { + this.backgroundPageShape.scale = 1; + this.backgroundPageShape.bounds = bounds; + this.backgroundPageShape.redraw(); + } + } + else if (this.backgroundPageShape != null) + { + this.backgroundPageShape.destroy(); + this.backgroundPageShape = null; + } + + this.validateBackgroundStyles(); + } + }; + + // Updates the CSS of the background to draw the grid + mxGraphView.prototype.validateBackgroundStyles = function() + { + var graph = this.graph; + var color = (graph.background == null || graph.background == mxConstants.NONE) ? graph.defaultPageBackgroundColor : graph.background; + var gridColor = (color != null && this.gridColor != color.toLowerCase()) ? this.gridColor : '#ffffff'; + var image = 'none'; + var position = ''; + + if (graph.isGridEnabled()) + { + var phase = 10; + + if (mxClient.IS_SVG) + { + // Generates the SVG required for drawing the dynamic grid + image = unescape(encodeURIComponent(this.createSvgGrid(gridColor))); + image = (window.btoa) ? btoa(image) : Base64.encode(image, true); + image = 'url(' + 'data:image/svg+xml;base64,' + image + ')' + phase = graph.gridSize * this.scale * this.gridSteps; + } + else + { + // Fallback to grid wallpaper with fixed size + image = 'url(' + this.gridImage + ')'; + } + + var x0 = 0; + var y0 = 0; + + if (graph.view.backgroundPageShape != null) + { + var bds = this.getBackgroundPageBounds(); + + x0 = 1 + bds.x; + y0 = 1 + bds.y; + } + + // Computes the offset to maintain origin for grid + position = -Math.round(phase - mxUtils.mod(this.translate.x * this.scale - x0, phase)) + 'px ' + + -Math.round(phase - mxUtils.mod(this.translate.y * this.scale - y0, phase)) + 'px'; + } + + var canvas = graph.view.canvas; + + if (canvas.ownerSVGElement != null) + { + canvas = canvas.ownerSVGElement; + } + + if (graph.view.backgroundPageShape != null) + { + graph.view.backgroundPageShape.node.style.backgroundPosition = position; + graph.view.backgroundPageShape.node.style.backgroundImage = image; + graph.view.backgroundPageShape.node.style.backgroundColor = color; + graph.container.className = 'geDiagramContainer geDiagramBackdrop'; + canvas.style.backgroundImage = 'none'; + canvas.style.backgroundColor = ''; + } + else + { + graph.container.className = 'geDiagramContainer'; + canvas.style.backgroundPosition = position; + canvas.style.backgroundColor = color; + canvas.style.backgroundImage = image; + } + }; + + // Returns the SVG required for painting the background grid. + mxGraphView.prototype.createSvgGrid = function(color) + { + var tmp = this.graph.gridSize * this.scale; + + while (tmp < this.minGridSize) + { + tmp *= 2; + } + + var tmp2 = this.gridSteps * tmp; + + // Small grid lines + var d = []; + + for (var i = 1; i < this.gridSteps; i++) + { + var tmp3 = i * tmp; + d.push('M 0 ' + tmp3 + ' L ' + tmp2 + ' ' + tmp3 + ' M ' + tmp3 + ' 0 L ' + tmp3 + ' ' + tmp2); + } + + // KNOWN: Rounding errors for certain scales (eg. 144%, 121% in Chrome, FF and Safari). Workaround + // in Chrome is to use 100% for the svg size, but this results in blurred grid for large diagrams. + var size = tmp2; + var svg = '' + + '' + + '' + + '' + + ''; + + return svg; + }; + + // Adds panning for the grid with no page view and disabled scrollbars + var mxGraphPanGraph = mxGraph.prototype.panGraph; + mxGraph.prototype.panGraph = function(dx, dy) + { + mxGraphPanGraph.apply(this, arguments); + + if (this.shiftPreview1 != null) + { + var canvas = this.view.canvas; + + if (canvas.ownerSVGElement != null) + { + canvas = canvas.ownerSVGElement; + } + + var phase = this.gridSize * this.view.scale * this.view.gridSteps; + var position = -Math.round(phase - mxUtils.mod(this.view.translate.x * this.view.scale + dx, phase)) + 'px ' + + -Math.round(phase - mxUtils.mod(this.view.translate.y * this.view.scale + dy, phase)) + 'px'; + canvas.style.backgroundPosition = position; + } + }; + + // Draws page breaks only within the page + mxGraph.prototype.updatePageBreaks = function(visible, width, height) + { + var scale = this.view.scale; + var tr = this.view.translate; + var fmt = this.pageFormat; + var ps = scale * this.pageScale; + + var bounds2 = this.view.getBackgroundPageBounds(); + + width = bounds2.width; + height = bounds2.height; + var bounds = new mxRectangle(scale * tr.x, scale * tr.y, fmt.width * ps, fmt.height * ps); + + // Does not show page breaks if the scale is too small + visible = visible && Math.min(bounds.width, bounds.height) > this.minPageBreakDist; + + var horizontalCount = (visible) ? Math.ceil(height / bounds.height) - 1 : 0; + var verticalCount = (visible) ? Math.ceil(width / bounds.width) - 1 : 0; + var right = bounds2.x + width; + var bottom = bounds2.y + height; + + if (this.horizontalPageBreaks == null && horizontalCount > 0) + { + this.horizontalPageBreaks = []; + } + + if (this.verticalPageBreaks == null && verticalCount > 0) + { + this.verticalPageBreaks = []; + } + + var drawPageBreaks = mxUtils.bind(this, function(breaks) + { + if (breaks != null) + { + var count = (breaks == this.horizontalPageBreaks) ? horizontalCount : verticalCount; + + for (var i = 0; i <= count; i++) + { + var pts = (breaks == this.horizontalPageBreaks) ? + [new mxPoint(Math.round(bounds2.x), Math.round(bounds2.y + (i + 1) * bounds.height)), + new mxPoint(Math.round(right), Math.round(bounds2.y + (i + 1) * bounds.height))] : + [new mxPoint(Math.round(bounds2.x + (i + 1) * bounds.width), Math.round(bounds2.y)), + new mxPoint(Math.round(bounds2.x + (i + 1) * bounds.width), Math.round(bottom))]; + + if (breaks[i] != null) + { + breaks[i].points = pts; + breaks[i].redraw(); + } + else + { + var pageBreak = new mxPolyline(pts, this.pageBreakColor); + pageBreak.dialect = this.dialect; + pageBreak.isDashed = this.pageBreakDashed; + pageBreak.pointerEvents = false; + pageBreak.init(this.view.backgroundPane); + pageBreak.redraw(); + + breaks[i] = pageBreak; + } + } + + for (var i = count; i < breaks.length; i++) + { + breaks[i].destroy(); + } + + breaks.splice(count, breaks.length - count); + } + }); + + drawPageBreaks(this.horizontalPageBreaks); + drawPageBreaks(this.verticalPageBreaks); + }; + + // Disables removing relative children from parents + var mxGraphHandlerShouldRemoveCellsFromParent = mxGraphHandler.prototype.shouldRemoveCellsFromParent; + mxGraphHandler.prototype.shouldRemoveCellsFromParent = function(parent, cells, evt) + { + for (var i = 0; i < cells.length; i++) + { + if (this.graph.getModel().isVertex(cells[i])) + { + var geo = this.graph.getCellGeometry(cells[i]); + + if (geo != null && geo.relative) + { + return false; + } + } + } + + return mxGraphHandlerShouldRemoveCellsFromParent.apply(this, arguments); + }; + + // Overrides to ignore hotspot only for target terminal + var mxConnectionHandlerCreateMarker = mxConnectionHandler.prototype.createMarker; + mxConnectionHandler.prototype.createMarker = function() + { + var marker = mxConnectionHandlerCreateMarker.apply(this, arguments); + + marker.intersects = mxUtils.bind(this, function(state, evt) + { + if (this.isConnecting()) + { + return true; + } + + return mxCellMarker.prototype.intersects.apply(marker, arguments); + }); + + return marker; + }; + + // Creates background page shape + mxGraphView.prototype.createBackgroundPageShape = function(bounds) + { + return new mxRectangleShape(bounds, '#ffffff', this.graph.defaultPageBorderColor); + }; + + // Fits the number of background pages to the graph + mxGraphView.prototype.getBackgroundPageBounds = function() + { + var gb = this.getGraphBounds(); + + // Computes unscaled, untranslated graph bounds + var x = (gb.width > 0) ? gb.x / this.scale - this.translate.x : 0; + var y = (gb.height > 0) ? gb.y / this.scale - this.translate.y : 0; + var w = gb.width / this.scale; + var h = gb.height / this.scale; + + var fmt = this.graph.pageFormat; + var ps = this.graph.pageScale; + + var pw = fmt.width * ps; + var ph = fmt.height * ps; + + var x0 = Math.floor(Math.min(0, x) / pw); + var y0 = Math.floor(Math.min(0, y) / ph); + var xe = Math.ceil(Math.max(1, x + w) / pw); + var ye = Math.ceil(Math.max(1, y + h) / ph); + + var rows = xe - x0; + var cols = ye - y0; + + var bounds = new mxRectangle(this.scale * (this.translate.x + x0 * pw), this.scale * + (this.translate.y + y0 * ph), this.scale * rows * pw, this.scale * cols * ph); + + return bounds; + }; + + // Add panning for background page in VML + var graphPanGraph = mxGraph.prototype.panGraph; + mxGraph.prototype.panGraph = function(dx, dy) + { + graphPanGraph.apply(this, arguments); + + if ((this.dialect != mxConstants.DIALECT_SVG && this.view.backgroundPageShape != null) && + (!this.useScrollbarsForPanning || !mxUtils.hasScrollbars(this.container))) + { + this.view.backgroundPageShape.node.style.marginLeft = dx + 'px'; + this.view.backgroundPageShape.node.style.marginTop = dy + 'px'; + } + }; + + /** + * Consumes click events for disabled menu items. + */ + var mxPopupMenuAddItem = mxPopupMenu.prototype.addItem; + mxPopupMenu.prototype.addItem = function(title, image, funct, parent, iconCls, enabled) + { + var result = mxPopupMenuAddItem.apply(this, arguments); + + if (enabled != null && !enabled) + { + mxEvent.addListener(result, 'mousedown', function(evt) + { + mxEvent.consume(evt); + }); + } + + return result; + }; + + // Selects ancestors before descendants + var graphHandlerGetInitialCellForEvent = mxGraphHandler.prototype.getInitialCellForEvent; + mxGraphHandler.prototype.getInitialCellForEvent = function(me) + { + var model = this.graph.getModel(); + var psel = model.getParent(this.graph.getSelectionCell()); + var cell = graphHandlerGetInitialCellForEvent.apply(this, arguments); + var parent = model.getParent(cell); + + if (psel == null || (psel != cell && psel != parent)) + { + while (!this.graph.isCellSelected(cell) && !this.graph.isCellSelected(parent) && + model.isVertex(parent) && !this.graph.isContainer(parent)) + { + cell = parent; + parent = this.graph.getModel().getParent(cell); + } + } + + return cell; + }; + + // Selection is delayed to mouseup if ancestor is selected + var graphHandlerIsDelayedSelection = mxGraphHandler.prototype.isDelayedSelection; + mxGraphHandler.prototype.isDelayedSelection = function(cell, me) + { + var result = graphHandlerIsDelayedSelection.apply(this, arguments); + + if (!result) + { + var model = this.graph.getModel(); + var parent = model.getParent(cell); + + while (parent != null) + { + // Inconsistency for unselected parent swimlane is intended for easier moving + // of stack layouts where the container title section is too far away + if (this.graph.isCellSelected(parent) && model.isVertex(parent)) + { + result = true; + break; + } + + parent = model.getParent(parent); + } + } + + return result; + }; + + // Delayed selection of parent group + mxGraphHandler.prototype.selectDelayed = function(me) + { + if (!this.graph.popupMenuHandler.isPopupTrigger(me)) + { + var cell = me.getCell(); + + if (cell == null) + { + cell = this.cell; + } + + // Selects folded cell for hit on folding icon + var state = this.graph.view.getState(cell) + + if (state != null && me.isSource(state.control)) + { + this.graph.selectCellForEvent(cell, me.getEvent()); + } + else + { + var model = this.graph.getModel(); + var parent = model.getParent(cell); + + while (!this.graph.isCellSelected(parent) && model.isVertex(parent)) + { + cell = parent; + parent = model.getParent(cell); + } + + this.graph.selectCellForEvent(cell, me.getEvent()); + } + } + }; + + // Returns last selected ancestor + mxPopupMenuHandler.prototype.getCellForPopupEvent = function(me) + { + var cell = me.getCell(); + var model = this.graph.getModel(); + var parent = model.getParent(cell); + + while (model.isVertex(parent) && !this.graph.isContainer(parent)) + { + if (this.graph.isCellSelected(parent)) + { + cell = parent; + } + + parent = model.getParent(parent); + } + + return cell; + }; + +})(); diff --git a/media/grapheditor/js/EditorUi.js b/media/grapheditor/js/EditorUi.js new file mode 100644 index 0000000000..9e3eb711f3 --- /dev/null +++ b/media/grapheditor/js/EditorUi.js @@ -0,0 +1,4216 @@ +/** + * Copyright (c) 2006-2012, JGraph Ltd + */ +/** + * Constructs a new graph editor + */ +EditorUi = function(editor, container, lightbox) +{ + mxEventSource.call(this); + + this.destroyFunctions = []; + this.editor = editor || new Editor(); + this.container = container || document.body; + + var graph = this.editor.graph; + graph.lightbox = lightbox; + + // Faster scrollwheel zoom is possible with CSS transforms + if (graph.useCssTransforms) + { + this.lazyZoomDelay = 0; + } + + // Pre-fetches submenu image or replaces with embedded image if supported + if (mxClient.IS_SVG) + { + mxPopupMenu.prototype.submenuImage = 'data:image/gif;base64,R0lGODlhCQAJAIAAAP///zMzMyH5BAEAAAAALAAAAAAJAAkAAAIPhI8WebHsHopSOVgb26AAADs='; + } + else + { + new Image().src = mxPopupMenu.prototype.submenuImage; + } + + // Pre-fetches connect image + if (!mxClient.IS_SVG && mxConnectionHandler.prototype.connectImage != null) + { + new Image().src = mxConnectionHandler.prototype.connectImage.src; + } + + // Disables graph and forced panning in chromeless mode + if (this.editor.chromeless && !this.editor.editable) + { + this.footerHeight = 0; + graph.isEnabled = function() { return false; }; + graph.panningHandler.isForcePanningEvent = function(me) + { + return !mxEvent.isPopupTrigger(me.getEvent()); + }; + } + + // Creates the user interface + this.actions = new Actions(this); + this.menus = this.createMenus(); + this.createDivs(); + this.createUi(); + this.refresh(); + + // Disables HTML and text selection + var textEditing = mxUtils.bind(this, function(evt) + { + if (evt == null) + { + evt = window.event; + } + + return (this.isSelectionAllowed(evt) || graph.isEditing()); + }); + + // Disables text selection while not editing and no dialog visible + if (this.container == document.body) + { + this.menubarContainer.onselectstart = textEditing; + this.menubarContainer.onmousedown = textEditing; + this.toolbarContainer.onselectstart = textEditing; + this.toolbarContainer.onmousedown = textEditing; + this.diagramContainer.onselectstart = textEditing; + this.diagramContainer.onmousedown = textEditing; + this.sidebarContainer.onselectstart = textEditing; + this.sidebarContainer.onmousedown = textEditing; + this.formatContainer.onselectstart = textEditing; + this.formatContainer.onmousedown = textEditing; + this.footerContainer.onselectstart = textEditing; + this.footerContainer.onmousedown = textEditing; + + if (this.tabContainer != null) + { + // Mouse down is needed for drag and drop + this.tabContainer.onselectstart = textEditing; + } + } + + // And uses built-in context menu while editing + if (!this.editor.chromeless || this.editor.editable) + { + // Allows context menu for links in hints + var linkHandler = function(evt) + { + var source = mxEvent.getSource(evt); + + if (source.nodeName == 'A') + { + while (source != null) + { + if (source.className == 'geHint') + { + return true; + } + + source = source.parentNode; + } + } + + return textEditing(evt); + }; + + if (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9)) + { + mxEvent.addListener(this.diagramContainer, 'contextmenu', linkHandler); + } + else + { + // Allows browser context menu outside of diagram and sidebar + this.diagramContainer.oncontextmenu = linkHandler; + } + } + else + { + graph.panningHandler.usePopupTrigger = false; + } + + // Contains the main graph instance inside the given panel + graph.init(this.diagramContainer); + + // Improves line wrapping for in-place editor + if (mxClient.IS_SVG && graph.view.getDrawPane() != null) + { + var root = graph.view.getDrawPane().ownerSVGElement; + + if (root != null) + { + root.style.position = 'absolute'; + } + } + + // Creates hover icons + this.hoverIcons = this.createHoverIcons(); + + // Adds tooltip when mouse is over scrollbars to show space-drag panning option + mxEvent.addListener(this.diagramContainer, 'mousemove', mxUtils.bind(this, function(evt) + { + var off = mxUtils.getOffset(this.diagramContainer); + + if (mxEvent.getClientX(evt) - off.x - this.diagramContainer.clientWidth > 0 || + mxEvent.getClientY(evt) - off.y - this.diagramContainer.clientHeight > 0) + { + this.diagramContainer.setAttribute('title', mxResources.get('panTooltip')); + } + else + { + this.diagramContainer.removeAttribute('title'); + } + })); + + // Escape key hides dialogs, adds space+drag panning + var spaceKeyPressed = false; + + // Overrides hovericons to disable while space key is pressed + var hoverIconsIsResetEvent = this.hoverIcons.isResetEvent; + + this.hoverIcons.isResetEvent = function(evt, allowShift) + { + return spaceKeyPressed || hoverIconsIsResetEvent.apply(this, arguments); + }; + + this.keydownHandler = mxUtils.bind(this, function(evt) + { + if (evt.which == 32 /* Space */) + { + spaceKeyPressed = true; + this.hoverIcons.reset(); + graph.container.style.cursor = 'move'; + + // Disables scroll after space keystroke with scrollbars + if (!graph.isEditing() && mxEvent.getSource(evt) == graph.container) + { + mxEvent.consume(evt); + } + } + else if (!mxEvent.isConsumed(evt) && evt.keyCode == 27 /* Escape */) + { + this.hideDialog(); + } + }); + + mxEvent.addListener(document, 'keydown', this.keydownHandler); + + this.keyupHandler = mxUtils.bind(this, function(evt) + { + graph.container.style.cursor = ''; + spaceKeyPressed = false; + }); + + mxEvent.addListener(document, 'keyup', this.keyupHandler); + + // Forces panning for middle and right mouse buttons + var panningHandlerIsForcePanningEvent = graph.panningHandler.isForcePanningEvent; + graph.panningHandler.isForcePanningEvent = function(me) + { + // Ctrl+left button is reported as right button in FF on Mac + return panningHandlerIsForcePanningEvent.apply(this, arguments) || + spaceKeyPressed || (mxEvent.isMouseEvent(me.getEvent()) && + (this.usePopupTrigger || !mxEvent.isPopupTrigger(me.getEvent())) && + ((!mxEvent.isControlDown(me.getEvent()) && + mxEvent.isRightMouseButton(me.getEvent())) || + mxEvent.isMiddleMouseButton(me.getEvent()))); + }; + + // Ctrl/Cmd+Enter applies editing value except in Safari where Ctrl+Enter creates + // a new line (while Enter creates a new paragraph and Shift+Enter stops) + var cellEditorIsStopEditingEvent = graph.cellEditor.isStopEditingEvent; + graph.cellEditor.isStopEditingEvent = function(evt) + { + return cellEditorIsStopEditingEvent.apply(this, arguments) || + (evt.keyCode == 13 && ((!mxClient.IS_SF && mxEvent.isControlDown(evt)) || + (mxClient.IS_MAC && mxEvent.isMetaDown(evt)) || + (mxClient.IS_SF && mxEvent.isShiftDown(evt)))); + }; + + // Switches toolbar for text editing + var textMode = false; + var fontMenu = null; + var sizeMenu = null; + var nodes = null; + + var updateToolbar = mxUtils.bind(this, function() + { + if (this.toolbar != null && textMode != graph.cellEditor.isContentEditing()) + { + var node = this.toolbar.container.firstChild; + var newNodes = []; + + while (node != null) + { + var tmp = node.nextSibling; + + if (mxUtils.indexOf(this.toolbar.staticElements, node) < 0) + { + node.parentNode.removeChild(node); + newNodes.push(node); + } + + node = tmp; + } + + // Saves references to special items + var tmp1 = this.toolbar.fontMenu; + var tmp2 = this.toolbar.sizeMenu; + + if (nodes == null) + { + this.toolbar.createTextToolbar(); + } + else + { + for (var i = 0; i < nodes.length; i++) + { + this.toolbar.container.appendChild(nodes[i]); + } + + // Restores references to special items + this.toolbar.fontMenu = fontMenu; + this.toolbar.sizeMenu = sizeMenu; + } + + textMode = graph.cellEditor.isContentEditing(); + fontMenu = tmp1; + sizeMenu = tmp2; + nodes = newNodes; + } + }); + + var ui = this; + + // Overrides cell editor to update toolbar + var cellEditorStartEditing = graph.cellEditor.startEditing; + graph.cellEditor.startEditing = function() + { + cellEditorStartEditing.apply(this, arguments); + updateToolbar(); + + if (graph.cellEditor.isContentEditing()) + { + var updating = false; + + var updateCssHandler = function() + { + if (!updating) + { + updating = true; + + window.setTimeout(function() + { + var selectedElement = graph.getSelectedElement(); + var node = selectedElement; + + while (node != null && node.nodeType != mxConstants.NODETYPE_ELEMENT) + { + node = node.parentNode; + } + + if (node != null) + { + var css = mxUtils.getCurrentStyle(node); + + if (css != null && ui.toolbar != null) + { + // Strips leading and trailing quotes + var ff = css.fontFamily; + + if (ff.charAt(0) == '\'') + { + ff = ff.substring(1); + } + + if (ff.charAt(ff.length - 1) == '\'') + { + ff = ff.substring(0, ff.length - 1); + } + + ui.toolbar.setFontName(ff); + ui.toolbar.setFontSize(parseInt(css.fontSize)); + } + } + + updating = false; + }, 0); + } + }; + + mxEvent.addListener(graph.cellEditor.textarea, 'input', updateCssHandler) + mxEvent.addListener(graph.cellEditor.textarea, 'touchend', updateCssHandler); + mxEvent.addListener(graph.cellEditor.textarea, 'mouseup', updateCssHandler); + mxEvent.addListener(graph.cellEditor.textarea, 'keyup', updateCssHandler); + updateCssHandler(); + } + }; + + var cellEditorStopEditing = graph.cellEditor.stopEditing; + graph.cellEditor.stopEditing = function(cell, trigger) + { + cellEditorStopEditing.apply(this, arguments); + updateToolbar(); + }; + + // Enables scrollbars and sets cursor style for the container + graph.container.setAttribute('tabindex', '0'); + graph.container.style.cursor = 'default'; + + // Workaround for page scroll if embedded via iframe + if (window.self === window.top && graph.container.parentNode != null) + { + try + { + graph.container.focus(); + } + catch (e) + { + // ignores error in old versions of IE + } + } + + // Keeps graph container focused on mouse down + var graphFireMouseEvent = graph.fireMouseEvent; + graph.fireMouseEvent = function(evtName, me, sender) + { + if (evtName == mxEvent.MOUSE_DOWN) + { + this.container.focus(); + } + + graphFireMouseEvent.apply(this, arguments); + }; + + // Configures automatic expand on mouseover + graph.popupMenuHandler.autoExpand = true; + + // Installs context menu + if (this.menus != null) + { + graph.popupMenuHandler.factoryMethod = mxUtils.bind(this, function(menu, cell, evt) + { + this.menus.createPopupMenu(menu, cell, evt); + }); + } + + // Hides context menu + mxEvent.addGestureListeners(document, mxUtils.bind(this, function(evt) + { + graph.popupMenuHandler.hideMenu(); + })); + + // Create handler for key events + this.keyHandler = this.createKeyHandler(editor); + + // Getter for key handler + this.getKeyHandler = function() + { + return keyHandler; + }; + + // Stores the current style and assigns it to new cells + var styles = ['rounded', 'shadow', 'glass', 'dashed', 'dashPattern', 'comic', 'labelBackgroundColor']; + var connectStyles = ['shape', 'edgeStyle', 'curved', 'rounded', 'elbow', 'comic', 'jumpStyle', 'jumpSize']; + + // Note: Everything that is not in styles is ignored (styles is augmented below) + this.setDefaultStyle = function(cell) + { + var state = graph.view.getState(cell); + + if (state != null) + { + // Ignores default styles + var clone = cell.clone(); + clone.style = '' + var defaultStyle = graph.getCellStyle(clone); + var values = []; + var keys = []; + + for (var key in state.style) + { + if (defaultStyle[key] != state.style[key]) + { + values.push(state.style[key]); + keys.push(key); + } + } + + // Handles special case for value "none" + var cellStyle = graph.getModel().getStyle(state.cell); + var tokens = (cellStyle != null) ? cellStyle.split(';') : []; + + for (var i = 0; i < tokens.length; i++) + { + var tmp = tokens[i]; + var pos = tmp.indexOf('='); + + if (pos >= 0) + { + var key = tmp.substring(0, pos); + var value = tmp.substring(pos + 1); + + if (defaultStyle[key] != null && value == 'none') + { + values.push(value); + keys.push(key); + } + } + } + + // Resets current style + if (graph.getModel().isEdge(state.cell)) + { + graph.currentEdgeStyle = {}; + } + else + { + graph.currentVertexStyle = {} + } + + this.fireEvent(new mxEventObject('styleChanged', 'keys', keys, 'values', values, 'cells', [state.cell])); + } + }; + + this.clearDefaultStyle = function() + { + graph.currentEdgeStyle = mxUtils.clone(graph.defaultEdgeStyle); + graph.currentVertexStyle = mxUtils.clone(graph.defaultVertexStyle); + + // Updates UI + this.fireEvent(new mxEventObject('styleChanged', 'keys', [], 'values', [], 'cells', [])); + }; + + // Keys that should be ignored if the cell has a value (known: new default for all cells is html=1 so + // for the html key this effecticely only works for edges inserted via the connection handler) + var valueStyles = ['fontFamily', 'fontSize', 'fontColor']; + + // Keys that always update the current edge style regardless of selection + var alwaysEdgeStyles = ['edgeStyle', 'startArrow', 'startFill', 'startSize', 'endArrow', + 'endFill', 'endSize', 'jettySize', 'orthogonalLoop']; + + // Keys that are ignored together (if one appears all are ignored) + var keyGroups = [['startArrow', 'startFill', 'startSize', 'endArrow', 'endFill', 'endSize', 'jettySize', 'orthogonalLoop'], + ['strokeColor', 'strokeWidth'], + ['fillColor', 'gradientColor'], + valueStyles, + ['opacity'], + ['align'], + ['html']]; + + // Adds all keys used above to the styles array + for (var i = 0; i < keyGroups.length; i++) + { + for (var j = 0; j < keyGroups[i].length; j++) + { + styles.push(keyGroups[i][j]); + } + } + + for (var i = 0; i < connectStyles.length; i++) + { + if (mxUtils.indexOf(styles, connectStyles[i]) < 0) + { + styles.push(connectStyles[i]); + } + } + + // Implements a global current style for edges and vertices that is applied to new cells + var insertHandler = function(cells, asText) + { + var model = graph.getModel(); + + model.beginUpdate(); + try + { + // Applies only basic text styles + if (asText) + { + var edge = model.isEdge(cell); + var current = (edge) ? graph.currentEdgeStyle : graph.currentVertexStyle; + var textStyles = ['fontSize', 'fontFamily', 'fontColor']; + + for (var j = 0; j < textStyles.length; j++) + { + var value = current[textStyles[j]]; + + if (value != null) + { + graph.setCellStyles(textStyles[j], value, cells); + } + } + } + else + { + for (var i = 0; i < cells.length; i++) + { + var cell = cells[i]; + + // Removes styles defined in the cell style from the styles to be applied + var cellStyle = model.getStyle(cell); + var tokens = (cellStyle != null) ? cellStyle.split(';') : []; + var appliedStyles = styles.slice(); + + for (var j = 0; j < tokens.length; j++) + { + var tmp = tokens[j]; + var pos = tmp.indexOf('='); + + if (pos >= 0) + { + var key = tmp.substring(0, pos); + var index = mxUtils.indexOf(appliedStyles, key); + + if (index >= 0) + { + appliedStyles.splice(index, 1); + } + + // Handles special cases where one defined style ignores other styles + for (var k = 0; k < keyGroups.length; k++) + { + var group = keyGroups[k]; + + if (mxUtils.indexOf(group, key) >= 0) + { + for (var l = 0; l < group.length; l++) + { + var index2 = mxUtils.indexOf(appliedStyles, group[l]); + + if (index2 >= 0) + { + appliedStyles.splice(index2, 1); + } + } + } + } + } + } + + // Applies the current style to the cell + var edge = model.isEdge(cell); + var current = (edge) ? graph.currentEdgeStyle : graph.currentVertexStyle; + var newStyle = model.getStyle(cell); + + for (var j = 0; j < appliedStyles.length; j++) + { + var key = appliedStyles[j]; + var styleValue = current[key]; + + if (styleValue != null && (key != 'shape' || edge)) + { + // Special case: Connect styles are not applied here but in the connection handler + if (!edge || mxUtils.indexOf(connectStyles, key) < 0) + { + newStyle = mxUtils.setStyle(newStyle, key, styleValue); + } + } + } + + model.setStyle(cell, newStyle); + } + } + } + finally + { + model.endUpdate(); + } + }; + + graph.addListener('cellsInserted', function(sender, evt) + { + insertHandler(evt.getProperty('cells')); + }); + + graph.addListener('textInserted', function(sender, evt) + { + insertHandler(evt.getProperty('cells'), true); + }); + + graph.connectionHandler.addListener(mxEvent.CONNECT, function(sender, evt) + { + var cells = [evt.getProperty('cell')]; + + if (evt.getProperty('terminalInserted')) + { + cells.push(evt.getProperty('terminal')); + } + + insertHandler(cells); + }); + + this.addListener('styleChanged', mxUtils.bind(this, function(sender, evt) + { + // Checks if edges and/or vertices were modified + var cells = evt.getProperty('cells'); + var vertex = false; + var edge = false; + + if (cells.length > 0) + { + for (var i = 0; i < cells.length; i++) + { + vertex = graph.getModel().isVertex(cells[i]) || vertex; + edge = graph.getModel().isEdge(cells[i]) || edge; + + if (edge && vertex) + { + break; + } + } + } + else + { + vertex = true; + edge = true; + } + + var keys = evt.getProperty('keys'); + var values = evt.getProperty('values'); + + for (var i = 0; i < keys.length; i++) + { + var common = mxUtils.indexOf(valueStyles, keys[i]) >= 0; + + // Ignores transparent stroke colors + if (keys[i] != 'strokeColor' || (values[i] != null && values[i] != 'none')) + { + // Special case: Edge style and shape + if (mxUtils.indexOf(connectStyles, keys[i]) >= 0) + { + if (edge || mxUtils.indexOf(alwaysEdgeStyles, keys[i]) >= 0) + { + if (values[i] == null) + { + delete graph.currentEdgeStyle[keys[i]]; + } + else + { + graph.currentEdgeStyle[keys[i]] = values[i]; + } + } + // Uses style for vertex if defined in styles + else if (vertex && mxUtils.indexOf(styles, keys[i]) >= 0) + { + if (values[i] == null) + { + delete graph.currentVertexStyle[keys[i]]; + } + else + { + graph.currentVertexStyle[keys[i]] = values[i]; + } + } + } + else if (mxUtils.indexOf(styles, keys[i]) >= 0) + { + if (vertex || common) + { + if (values[i] == null) + { + delete graph.currentVertexStyle[keys[i]]; + } + else + { + graph.currentVertexStyle[keys[i]] = values[i]; + } + } + + if (edge || common || mxUtils.indexOf(alwaysEdgeStyles, keys[i]) >= 0) + { + if (values[i] == null) + { + delete graph.currentEdgeStyle[keys[i]]; + } + else + { + graph.currentEdgeStyle[keys[i]] = values[i]; + } + } + } + } + } + + if (this.toolbar != null) + { + this.toolbar.setFontName(graph.currentVertexStyle['fontFamily'] || Menus.prototype.defaultFont); + this.toolbar.setFontSize(graph.currentVertexStyle['fontSize'] || Menus.prototype.defaultFontSize); + + if (this.toolbar.edgeStyleMenu != null) + { + // Updates toolbar icon for edge style + var edgeStyleDiv = this.toolbar.edgeStyleMenu.getElementsByTagName('div')[0]; + + if (graph.currentEdgeStyle['edgeStyle'] == 'orthogonalEdgeStyle' && graph.currentEdgeStyle['curved'] == '1') + { + edgeStyleDiv.className = 'geSprite geSprite-curved'; + } + else if (graph.currentEdgeStyle['edgeStyle'] == 'straight' || graph.currentEdgeStyle['edgeStyle'] == 'none' || + graph.currentEdgeStyle['edgeStyle'] == null) + { + edgeStyleDiv.className = 'geSprite geSprite-straight'; + } + else if (graph.currentEdgeStyle['edgeStyle'] == 'entityRelationEdgeStyle') + { + edgeStyleDiv.className = 'geSprite geSprite-entity'; + } + else if (graph.currentEdgeStyle['edgeStyle'] == 'elbowEdgeStyle') + { + edgeStyleDiv.className = 'geSprite geSprite-' + ((graph.currentEdgeStyle['elbow'] == 'vertical') ? + 'verticalelbow' : 'horizontalelbow'); + } + else if (graph.currentEdgeStyle['edgeStyle'] == 'isometricEdgeStyle') + { + edgeStyleDiv.className = 'geSprite geSprite-' + ((graph.currentEdgeStyle['elbow'] == 'vertical') ? + 'verticalisometric' : 'horizontalisometric'); + } + else + { + edgeStyleDiv.className = 'geSprite geSprite-orthogonal'; + } + } + + if (this.toolbar.edgeShapeMenu != null) + { + // Updates icon for edge shape + var edgeShapeDiv = this.toolbar.edgeShapeMenu.getElementsByTagName('div')[0]; + + if (graph.currentEdgeStyle['shape'] == 'link') + { + edgeShapeDiv.className = 'geSprite geSprite-linkedge'; + } + else if (graph.currentEdgeStyle['shape'] == 'flexArrow') + { + edgeShapeDiv.className = 'geSprite geSprite-arrow'; + } + else if (graph.currentEdgeStyle['shape'] == 'arrow') + { + edgeShapeDiv.className = 'geSprite geSprite-simplearrow'; + } + else + { + edgeShapeDiv.className = 'geSprite geSprite-connection'; + } + } + + // Updates icon for optinal line start shape + if (this.toolbar.lineStartMenu != null) + { + var lineStartDiv = this.toolbar.lineStartMenu.getElementsByTagName('div')[0]; + + lineStartDiv.className = this.getCssClassForMarker('start', + graph.currentEdgeStyle['shape'], graph.currentEdgeStyle[mxConstants.STYLE_STARTARROW], + mxUtils.getValue(graph.currentEdgeStyle, 'startFill', '1')); + } + + // Updates icon for optinal line end shape + if (this.toolbar.lineEndMenu != null) + { + var lineEndDiv = this.toolbar.lineEndMenu.getElementsByTagName('div')[0]; + + lineEndDiv.className = this.getCssClassForMarker('end', + graph.currentEdgeStyle['shape'], graph.currentEdgeStyle[mxConstants.STYLE_ENDARROW], + mxUtils.getValue(graph.currentEdgeStyle, 'endFill', '1')); + } + } + })); + + // Update font size and font family labels + if (this.toolbar != null) + { + var update = mxUtils.bind(this, function() + { + var ff = graph.currentVertexStyle['fontFamily'] || 'Helvetica'; + var fs = String(graph.currentVertexStyle['fontSize'] || '12'); + var state = graph.getView().getState(graph.getSelectionCell()); + + if (state != null) + { + ff = state.style[mxConstants.STYLE_FONTFAMILY] || ff; + fs = state.style[mxConstants.STYLE_FONTSIZE] || fs; + + if (ff.length > 10) + { + ff = ff.substring(0, 8) + '...'; + } + } + + this.toolbar.setFontName(ff); + this.toolbar.setFontSize(fs); + }); + + graph.getSelectionModel().addListener(mxEvent.CHANGE, update); + graph.getModel().addListener(mxEvent.CHANGE, update); + } + + // Makes sure the current layer is visible when cells are added + graph.addListener(mxEvent.CELLS_ADDED, function(sender, evt) + { + var cells = evt.getProperty('cells'); + var parent = evt.getProperty('parent'); + + if (graph.getModel().isLayer(parent) && !graph.isCellVisible(parent) && cells != null && cells.length > 0) + { + graph.getModel().setVisible(parent, true); + } + }); + + // Global handler to hide the current menu + this.gestureHandler = mxUtils.bind(this, function(evt) + { + if (this.currentMenu != null && mxEvent.getSource(evt) != this.currentMenu.div) + { + this.hideCurrentMenu(); + } + }); + + mxEvent.addGestureListeners(document, this.gestureHandler); + + // Updates the editor UI after the window has been resized or the orientation changes + // Timeout is workaround for old IE versions which have a delay for DOM client sizes. + // Should not use delay > 0 to avoid handle multiple repaints during window resize + this.resizeHandler = mxUtils.bind(this, function() + { + window.setTimeout(mxUtils.bind(this, function() + { + if (this.editor.graph != null) + { + this.refresh(); + } + }), 0); + }); + + mxEvent.addListener(window, 'resize', this.resizeHandler); + + this.orientationChangeHandler = mxUtils.bind(this, function() + { + this.refresh(); + }); + + mxEvent.addListener(window, 'orientationchange', this.orientationChangeHandler); + + // Workaround for bug on iOS see + // http://stackoverflow.com/questions/19012135/ios-7-ipad-safari-landscape-innerheight-outerheight-layout-issue + if (mxClient.IS_IOS && !window.navigator.standalone) + { + this.scrollHandler = mxUtils.bind(this, function() + { + window.scrollTo(0, 0); + }); + + mxEvent.addListener(window, 'scroll', this.scrollHandler); + } + + /** + * Sets the initial scrollbar locations after a file was loaded. + */ + this.editor.addListener('resetGraphView', mxUtils.bind(this, function() + { + this.resetScrollbars(); + })); + + /** + * Repaints the grid. + */ + this.addListener('gridEnabledChanged', mxUtils.bind(this, function() + { + graph.view.validateBackground(); + })); + + this.addListener('backgroundColorChanged', mxUtils.bind(this, function() + { + graph.view.validateBackground(); + })); + + /** + * Repaints the grid. + */ + graph.addListener('gridSizeChanged', mxUtils.bind(this, function() + { + if (graph.isGridEnabled()) + { + graph.view.validateBackground(); + } + })); + + // Resets UI, updates action and menu states + this.editor.resetGraph(); + this.init(); + this.open(); +}; + +// Extends mxEventSource +mxUtils.extend(EditorUi, mxEventSource); + +/** + * Global config that specifies if the compact UI elements should be used. + */ +EditorUi.compactUi = true; + +/** + * Specifies the size of the split bar. + */ +EditorUi.prototype.splitSize = (mxClient.IS_TOUCH || mxClient.IS_POINTER) ? 12 : 8; + +/** + * Specifies the height of the menubar. Default is 34. + */ +EditorUi.prototype.menubarHeight = 30; + +/** + * Specifies the width of the format panel should be enabled. Default is true. + */ +EditorUi.prototype.formatEnabled = true; + +/** + * Specifies the width of the format panel. Default is 240. + */ +EditorUi.prototype.formatWidth = 240; + +/** + * Specifies the height of the toolbar. Default is 36. + */ +EditorUi.prototype.toolbarHeight = 34; + +/** + * Specifies the height of the footer. Default is 28. + */ +EditorUi.prototype.footerHeight = 28; + +/** + * Specifies the height of the optional sidebarFooterContainer. Default is 34. + */ +EditorUi.prototype.sidebarFooterHeight = 34; + +/** + * Specifies the position of the horizontal split bar. Default is 208 or 118 for + * screen widths <= 640px. + */ +EditorUi.prototype.hsplitPosition = (screen.width <= 640) ? 118 : 208; + +/** + * Specifies if animations are allowed in . Default is true. + */ +EditorUi.prototype.allowAnimation = true; + +/** + * Specifies if animations are allowed in . Default is true. + */ +EditorUi.prototype.lightboxMaxFitScale = 2; + +/** + * Specifies if animations are allowed in . Default is true. + */ +EditorUi.prototype.lightboxVerticalDivider = 4; + +/** + * Specifies if single click on horizontal split should collapse sidebar. Default is false. + */ +EditorUi.prototype.hsplitClickEnabled = false; + +/** + * Installs the listeners to update the action states. + */ +EditorUi.prototype.init = function() +{ + /** + * Keypress starts immediate editing on selection cell + */ + var graph = this.editor.graph; + + mxEvent.addListener(graph.container, 'keydown', mxUtils.bind(this, function(evt) + { + this.onKeyDown(evt); + })); + mxEvent.addListener(graph.container, 'keypress', mxUtils.bind(this, function(evt) + { + this.onKeyPress(evt); + })); + + // Updates action states + this.addUndoListener(); + this.addBeforeUnloadListener(); + + graph.getSelectionModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function() + { + this.updateActionStates(); + })); + + graph.getModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function() + { + this.updateActionStates(); + })); + + // Changes action states after change of default parent + var graphSetDefaultParent = graph.setDefaultParent; + var ui = this; + + this.editor.graph.setDefaultParent = function() + { + graphSetDefaultParent.apply(this, arguments); + ui.updateActionStates(); + }; + + // Hack to make editLink available in vertex handler + graph.editLink = ui.actions.get('editLink').funct; + + this.updateActionStates(); + this.initClipboard(); + this.initCanvas(); + + if (this.format != null) + { + this.format.init(); + } +}; + +/** + * Returns true if the given event should start editing. This implementation returns true. + */ +EditorUi.prototype.onKeyDown = function(evt) +{ + var graph = this.editor.graph; + + // Tab selects next cell + if (evt.which == 9 && graph.isEnabled() && !mxEvent.isAltDown(evt)) + { + if (graph.isEditing()) + { + graph.stopEditing(false); + } + else + { + graph.selectCell(!mxEvent.isShiftDown(evt)); + } + + mxEvent.consume(evt); + } +}; + +/** + * Returns true if the given event should start editing. This implementation returns true. + */ +EditorUi.prototype.onKeyPress = function(evt) +{ + var graph = this.editor.graph; + + // KNOWN: Focus does not work if label is empty in quirks mode + if (this.isImmediateEditingEvent(evt) && !graph.isEditing() && !graph.isSelectionEmpty() && evt.which !== 0 && + !mxEvent.isAltDown(evt) && !mxEvent.isControlDown(evt) && !mxEvent.isMetaDown(evt)) + { + graph.escape(); + graph.startEditing(); + + // Workaround for FF where char is lost if cursor is placed before char + if (mxClient.IS_FF) + { + var ce = graph.cellEditor; + ce.textarea.innerHTML = String.fromCharCode(evt.which); + + // Moves cursor to end of textarea + var range = document.createRange(); + range.selectNodeContents(ce.textarea); + range.collapse(false); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + } +}; + +/** + * Returns true if the given event should start editing. This implementation returns true. + */ +EditorUi.prototype.isImmediateEditingEvent = function(evt) +{ + return true; +}; + +/** + * Private helper method. + */ +EditorUi.prototype.getCssClassForMarker = function(prefix, shape, marker, fill) +{ + var result = ''; + + if (shape == 'flexArrow') + { + result = (marker != null && marker != mxConstants.NONE) ? + 'geSprite geSprite-' + prefix + 'blocktrans' : 'geSprite geSprite-noarrow'; + } + else + { + if (marker == mxConstants.ARROW_CLASSIC) + { + result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'classic' : 'geSprite geSprite-' + prefix + 'classictrans'; + } + else if (marker == mxConstants.ARROW_CLASSIC_THIN) + { + result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'classicthin' : 'geSprite geSprite-' + prefix + 'classicthintrans'; + } + else if (marker == mxConstants.ARROW_OPEN) + { + result = 'geSprite geSprite-' + prefix + 'open'; + } + else if (marker == mxConstants.ARROW_OPEN_THIN) + { + result = 'geSprite geSprite-' + prefix + 'openthin'; + } + else if (marker == mxConstants.ARROW_BLOCK) + { + result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'block' : 'geSprite geSprite-' + prefix + 'blocktrans'; + } + else if (marker == mxConstants.ARROW_BLOCK_THIN) + { + result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'blockthin' : 'geSprite geSprite-' + prefix + 'blockthintrans'; + } + else if (marker == mxConstants.ARROW_OVAL) + { + result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'oval' : 'geSprite geSprite-' + prefix + 'ovaltrans'; + } + else if (marker == mxConstants.ARROW_DIAMOND) + { + result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'diamond' : 'geSprite geSprite-' + prefix + 'diamondtrans'; + } + else if (marker == mxConstants.ARROW_DIAMOND_THIN) + { + result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'thindiamond' : 'geSprite geSprite-' + prefix + 'thindiamondtrans'; + } + else if (marker == 'openAsync') + { + result = 'geSprite geSprite-' + prefix + 'openasync'; + } + else if (marker == 'dash') + { + result = 'geSprite geSprite-' + prefix + 'dash'; + } + else if (marker == 'cross') + { + result = 'geSprite geSprite-' + prefix + 'cross'; + } + else if (marker == 'async') + { + result = (fill == '1') ? 'geSprite geSprite-' + prefix + 'async' : 'geSprite geSprite-' + prefix + 'asynctrans'; + } + else if (marker == 'circle' || marker == 'circlePlus') + { + result = (fill == '1' || marker == 'circle') ? 'geSprite geSprite-' + prefix + 'circle' : 'geSprite geSprite-' + prefix + 'circleplus'; + } + else if (marker == 'ERone') + { + result = 'geSprite geSprite-' + prefix + 'erone'; + } + else if (marker == 'ERmandOne') + { + result = 'geSprite geSprite-' + prefix + 'eronetoone'; + } + else if (marker == 'ERmany') + { + result = 'geSprite geSprite-' + prefix + 'ermany'; + } + else if (marker == 'ERoneToMany') + { + result = 'geSprite geSprite-' + prefix + 'eronetomany'; + } + else if (marker == 'ERzeroToOne') + { + result = 'geSprite geSprite-' + prefix + 'eroneopt'; + } + else if (marker == 'ERzeroToMany') + { + result = 'geSprite geSprite-' + prefix + 'ermanyopt'; + } + else + { + result = 'geSprite geSprite-noarrow'; + } + } + + return result; +}; + +/** + * Overridden in Menus.js + */ +EditorUi.prototype.createMenus = function() +{ + return null; +}; + +/** + * Hook for allowing selection and context menu for certain events. + */ +EditorUi.prototype.updatePasteActionStates = function() +{ + var graph = this.editor.graph; + var paste = this.actions.get('paste'); + var pasteHere = this.actions.get('pasteHere'); + + paste.setEnabled(this.editor.graph.cellEditor.isContentEditing() || (!mxClipboard.isEmpty() && + graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()))); + pasteHere.setEnabled(paste.isEnabled()); +}; + +/** + * Hook for allowing selection and context menu for certain events. + */ +EditorUi.prototype.initClipboard = function() +{ + var ui = this; + + var mxClipboardCut = mxClipboard.cut; + mxClipboard.cut = function(graph) + { + if (graph.cellEditor.isContentEditing()) + { + document.execCommand('cut', false, null); + } + else + { + mxClipboardCut.apply(this, arguments); + } + + ui.updatePasteActionStates(); + }; + + var mxClipboardCopy = mxClipboard.copy; + mxClipboard.copy = function(graph) + { + if (graph.cellEditor.isContentEditing()) + { + document.execCommand('copy', false, null); + } + else + { + mxClipboardCopy.apply(this, arguments); + } + + ui.updatePasteActionStates(); + }; + + var mxClipboardPaste = mxClipboard.paste; + mxClipboard.paste = function(graph) + { + var result = null; + + if (graph.cellEditor.isContentEditing()) + { + document.execCommand('paste', false, null); + } + else + { + result = mxClipboardPaste.apply(this, arguments); + } + + ui.updatePasteActionStates(); + + return result; + }; + + // Overrides cell editor to update paste action state + var cellEditorStartEditing = this.editor.graph.cellEditor.startEditing; + + this.editor.graph.cellEditor.startEditing = function() + { + cellEditorStartEditing.apply(this, arguments); + ui.updatePasteActionStates(); + }; + + var cellEditorStopEditing = this.editor.graph.cellEditor.stopEditing; + + this.editor.graph.cellEditor.stopEditing = function(cell, trigger) + { + cellEditorStopEditing.apply(this, arguments); + ui.updatePasteActionStates(); + }; + + this.updatePasteActionStates(); +}; + +/** + * Initializes the infinite canvas. + */ +EditorUi.prototype.lazyZoomDelay = 20; + +/** + * Initializes the infinite canvas. + */ +EditorUi.prototype.initCanvas = function() +{ + // Initial page layout view, scrollBuffer and timer-based scrolling + var graph = this.editor.graph; + graph.timerAutoScroll = true; + + /** + * Returns the padding for pages in page view with scrollbars. + */ + graph.getPagePadding = function() + { + return new mxPoint(Math.max(0, Math.round((graph.container.offsetWidth - 34) / graph.view.scale)), + Math.max(0, Math.round((graph.container.offsetHeight - 34) / graph.view.scale))); + }; + + // Fits the number of background pages to the graph + graph.view.getBackgroundPageBounds = function() + { + var layout = this.graph.getPageLayout(); + var page = this.graph.getPageSize(); + + return new mxRectangle(this.scale * (this.translate.x + layout.x * page.width), + this.scale * (this.translate.y + layout.y * page.height), + this.scale * layout.width * page.width, + this.scale * layout.height * page.height); + }; + + graph.getPreferredPageSize = function(bounds, width, height) + { + var pages = this.getPageLayout(); + var size = this.getPageSize(); + + return new mxRectangle(0, 0, pages.width * size.width, pages.height * size.height); + }; + + // Scales pages/graph to fit available size + var resize = null; + var ui = this; + + if (this.editor.isChromelessView()) + { + resize = mxUtils.bind(this, function(autoscale, maxScale, cx, cy) + { + if (graph.container != null) + { + cx = (cx != null) ? cx : 0; + cy = (cy != null) ? cy : 0; + + var bds = (graph.pageVisible) ? graph.view.getBackgroundPageBounds() : graph.getGraphBounds(); + var scroll = mxUtils.hasScrollbars(graph.container); + var tr = graph.view.translate; + var s = graph.view.scale; + + // Normalizes the bounds + var b = mxRectangle.fromRectangle(bds); + b.x = b.x / s - tr.x; + b.y = b.y / s - tr.y; + b.width /= s; + b.height /= s; + + var st = graph.container.scrollTop; + var sl = graph.container.scrollLeft; + var sb = (mxClient.IS_QUIRKS || document.documentMode >= 8) ? 20 : 14; + + if (document.documentMode == 8 || document.documentMode == 9) + { + sb += 3; + } + + var cw = graph.container.offsetWidth - sb; + var ch = graph.container.offsetHeight - sb; + + var ns = (autoscale) ? Math.max(0.3, Math.min(maxScale || 1, cw / b.width)) : s; + var dx = ((cw - ns * b.width) / 2) / ns; + var dy = (this.lightboxVerticalDivider == 0) ? 0 : ((ch - ns * b.height) / this.lightboxVerticalDivider) / ns; + + if (scroll) + { + dx = Math.max(dx, 0); + dy = Math.max(dy, 0); + } + + if (scroll || bds.width < cw || bds.height < ch) + { + graph.view.scaleAndTranslate(ns, Math.floor(dx - b.x), Math.floor(dy - b.y)); + graph.container.scrollTop = st * ns / s; + graph.container.scrollLeft = sl * ns / s; + } + else if (cx != 0 || cy != 0) + { + var t = graph.view.translate; + graph.view.setTranslate(Math.floor(t.x + cx / s), Math.floor(t.y + cy / s)); + } + } + }); + + // Hack to make function available to subclassers + this.chromelessResize = resize; + + // Hook for subclassers for override + this.chromelessWindowResize = mxUtils.bind(this, function() + { + this.chromelessResize(false); + }); + + // Removable resize listener + var autoscaleResize = mxUtils.bind(this, function() + { + this.chromelessWindowResize(false); + }); + + mxEvent.addListener(window, 'resize', autoscaleResize); + + this.destroyFunctions.push(function() + { + mxEvent.removeListener(window, 'resize', autoscaleResize); + }); + + this.editor.addListener('resetGraphView', mxUtils.bind(this, function() + { + this.chromelessResize(true); + })); + + this.actions.get('zoomIn').funct = mxUtils.bind(this, function(evt) + { + graph.zoomIn(); + this.chromelessResize(false); + }); + this.actions.get('zoomOut').funct = mxUtils.bind(this, function(evt) + { + graph.zoomOut(); + this.chromelessResize(false); + }); + + // Creates toolbar for viewer - do not use CSS here + // as this may be used in a viewer that has no CSS + if (urlParams['toolbar'] != '0') + { + this.chromelessToolbar = document.createElement('div'); + this.chromelessToolbar.style.position = 'fixed'; + this.chromelessToolbar.style.overflow = 'hidden'; + this.chromelessToolbar.style.boxSizing = 'border-box'; + this.chromelessToolbar.style.whiteSpace = 'nowrap'; + this.chromelessToolbar.style.backgroundColor = '#000000'; + this.chromelessToolbar.style.padding = '10px 10px 8px 10px'; + this.chromelessToolbar.style.left = '50%'; + + if (!mxClient.IS_VML) + { + mxUtils.setPrefixedStyle(this.chromelessToolbar.style, 'borderRadius', '20px'); + mxUtils.setPrefixedStyle(this.chromelessToolbar.style, 'transition', 'opacity 600ms ease-in-out'); + } + + var updateChromelessToolbarPosition = mxUtils.bind(this, function() + { + var css = mxUtils.getCurrentStyle(graph.container); + this.chromelessToolbar.style.bottom = ((css != null) ? parseInt(css['margin-bottom'] || 0) : 0) + + ((this.tabContainer != null) ? (20 + parseInt(this.tabContainer.style.height)) : 20) + 'px'; + }); + + this.editor.addListener('resetGraphView', updateChromelessToolbarPosition); + updateChromelessToolbarPosition(); + + var btnCount = 0; + + var addButton = mxUtils.bind(this, function(fn, imgSrc, tip) + { + btnCount++; + + var a = document.createElement('span'); + a.style.paddingLeft = '8px'; + a.style.paddingRight = '8px'; + a.style.cursor = 'pointer'; + mxEvent.addListener(a, 'click', fn); + + if (tip != null) + { + a.setAttribute('title', tip); + } + + var img = document.createElement('img'); + img.setAttribute('border', '0'); + img.setAttribute('src', imgSrc); + + a.appendChild(img); + this.chromelessToolbar.appendChild(a); + + return a; + }); + + var prevButton = addButton(mxUtils.bind(this, function(evt) + { + this.actions.get('previousPage').funct(); + mxEvent.consume(evt); + }), Editor.previousLargeImage, mxResources.get('previousPage')); + + var pageInfo = document.createElement('div'); + pageInfo.style.display = 'inline-block'; + pageInfo.style.verticalAlign = 'top'; + pageInfo.style.fontFamily = 'Helvetica,Arial'; + pageInfo.style.marginTop = '8px'; + pageInfo.style.fontSize = '14px'; + pageInfo.style.color = '#ffffff'; + this.chromelessToolbar.appendChild(pageInfo); + + var nextButton = addButton(mxUtils.bind(this, function(evt) + { + this.actions.get('nextPage').funct(); + mxEvent.consume(evt); + }), Editor.nextLargeImage, mxResources.get('nextPage')); + + var updatePageInfo = mxUtils.bind(this, function() + { + if (this.pages != null && this.pages.length > 1 && this.currentPage != null) + { + pageInfo.innerHTML = ''; + mxUtils.write(pageInfo, (mxUtils.indexOf(this.pages, this.currentPage) + 1) + ' / ' + this.pages.length); + } + }); + + prevButton.style.paddingLeft = '0px'; + prevButton.style.paddingRight = '4px'; + nextButton.style.paddingLeft = '4px'; + nextButton.style.paddingRight = '0px'; + + var updatePageButtons = mxUtils.bind(this, function() + { + if (this.pages != null && this.pages.length > 1 && this.currentPage != null) + { + nextButton.style.display = ''; + prevButton.style.display = ''; + pageInfo.style.display = 'inline-block'; + } + else + { + nextButton.style.display = 'none'; + prevButton.style.display = 'none'; + pageInfo.style.display = 'none'; + } + + updatePageInfo(); + }); + + this.editor.addListener('resetGraphView', updatePageButtons); + this.editor.addListener('pageSelected', updatePageInfo); + + addButton(mxUtils.bind(this, function(evt) + { + this.actions.get('zoomOut').funct(); + mxEvent.consume(evt); + }), Editor.zoomOutLargeImage, mxResources.get('zoomOut') + ' (Alt+Mousewheel)'); + + addButton(mxUtils.bind(this, function(evt) + { + this.actions.get('zoomIn').funct(); + mxEvent.consume(evt); + }), Editor.zoomInLargeImage, mxResources.get('zoomIn') + ' (Alt+Mousewheel)'); + + addButton(mxUtils.bind(this, function(evt) + { + if (graph.isLightboxView()) + { + if (graph.view.scale == 1) + { + this.lightboxFit(); + } + else + { + graph.zoomTo(1); + } + + this.chromelessResize(false); + } + else + { + this.chromelessResize(true); + } + + mxEvent.consume(evt); + }), Editor.actualSizeLargeImage, mxResources.get('fit')); + + // Changes toolbar opacity on hover + var fadeThread = null; + var fadeThread2 = null; + + var fadeOut = mxUtils.bind(this, function(delay) + { + if (fadeThread != null) + { + window.clearTimeout(fadeThread); + fadeThead = null; + } + + if (fadeThread2 != null) + { + window.clearTimeout(fadeThread2); + fadeThead2 = null; + } + + fadeThread = window.setTimeout(mxUtils.bind(this, function() + { + mxUtils.setOpacity(this.chromelessToolbar, 0); + fadeThread = null; + + fadeThread2 = window.setTimeout(mxUtils.bind(this, function() + { + this.chromelessToolbar.style.display = 'none'; + fadeThread2 = null; + }), 600); + }), delay || 200); + }); + + var fadeIn = mxUtils.bind(this, function(opacity) + { + if (fadeThread != null) + { + window.clearTimeout(fadeThread); + fadeThead = null; + } + + if (fadeThread2 != null) + { + window.clearTimeout(fadeThread2); + fadeThead2 = null; + } + + this.chromelessToolbar.style.display = ''; + mxUtils.setOpacity(this.chromelessToolbar, opacity || 30); + }); + + if (urlParams['layers'] == '1') + { + this.layersDialog = null; + + var layersButton = addButton(mxUtils.bind(this, function(evt) + { + if (this.layersDialog != null) + { + this.layersDialog.parentNode.removeChild(this.layersDialog); + this.layersDialog = null; + } + else + { + this.layersDialog = graph.createLayersDialog(); + + mxEvent.addListener(this.layersDialog, 'mouseleave', mxUtils.bind(this, function() + { + this.layersDialog.parentNode.removeChild(this.layersDialog); + this.layersDialog = null; + })); + + var r = layersButton.getBoundingClientRect(); + + mxUtils.setPrefixedStyle(this.layersDialog.style, 'borderRadius', '5px'); + this.layersDialog.style.position = 'fixed'; + this.layersDialog.style.fontFamily = 'Helvetica,Arial'; + this.layersDialog.style.backgroundColor = '#000000'; + this.layersDialog.style.width = '160px'; + this.layersDialog.style.padding = '4px 2px 4px 2px'; + this.layersDialog.style.color = '#ffffff'; + mxUtils.setOpacity(this.layersDialog, 70); + this.layersDialog.style.left = r.left + 'px'; + this.layersDialog.style.bottom = parseInt(this.chromelessToolbar.style.bottom) + + this.chromelessToolbar.offsetHeight + 4 + 'px'; + + // Puts the dialog on top of the container z-index + var style = mxUtils.getCurrentStyle(this.editor.graph.container); + this.layersDialog.style.zIndex = style.zIndex; + + document.body.appendChild(this.layersDialog); + } + + mxEvent.consume(evt); + }), Editor.layersLargeImage, mxResources.get('layers')); + + // Shows/hides layers button depending on content + var model = graph.getModel(); + + model.addListener(mxEvent.CHANGE, function() + { + layersButton.style.display = (model.getChildCount(model.root) > 1) ? '' : 'none'; + }); + } + + this.addChromelessToolbarItems(addButton); + + if (this.editor.editButtonLink != null || this.editor.editButtonFunc != null) + { + addButton(mxUtils.bind(this, function(evt) + { + if (this.editor.editButtonFunc != null) + { + this.editor.editButtonFunc(); + } + else if (this.editor.editButtonLink == '_blank') + { + this.editor.editAsNew(this.getEditBlankXml()); + } + else + { + graph.openLink(this.editor.editButtonLink, 'editWindow'); + } + + mxEvent.consume(evt); + }), Editor.editLargeImage, mxResources.get('edit')); + } + + if (graph.lightbox && (urlParams['close'] == '1' || this.container != document.body)) + { + addButton(mxUtils.bind(this, function(evt) + { + if (urlParams['close'] == '1') + { + window.close(); + } + else + { + this.destroy(); + mxEvent.consume(evt); + } + }), Editor.closeLargeImage, mxResources.get('close') + ' (Escape)'); + } + + // Initial state invisible + this.chromelessToolbar.style.display = 'none'; + mxUtils.setPrefixedStyle(this.chromelessToolbar.style, 'transform', 'translate(-50%,0)'); + graph.container.appendChild(this.chromelessToolbar); + + mxEvent.addListener(graph.container, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', mxUtils.bind(this, function(evt) + { + if (!mxEvent.isTouchEvent(evt)) + { + if (!mxEvent.isShiftDown(evt)) + { + fadeIn(30); + } + + fadeOut(); + } + })); + + mxEvent.addListener(this.chromelessToolbar, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', function(evt) + { + mxEvent.consume(evt); + }); + + mxEvent.addListener(this.chromelessToolbar, 'mouseenter', mxUtils.bind(this, function(evt) + { + if (!mxEvent.isShiftDown(evt)) + { + fadeIn(100); + } + else + { + fadeOut(); + } + })); + + mxEvent.addListener(this.chromelessToolbar, 'mousemove', mxUtils.bind(this, function(evt) + { + if (!mxEvent.isShiftDown(evt)) + { + fadeIn(100); + } + else + { + fadeOut(); + } + + mxEvent.consume(evt); + })); + + mxEvent.addListener(this.chromelessToolbar, 'mouseleave', mxUtils.bind(this, function(evt) + { + if (!mxEvent.isTouchEvent(evt)) + { + fadeIn(30); + } + })); + + // Shows/hides toolbar for touch devices + var tol = graph.getTolerance(); + + graph.addMouseListener( + { + startX: 0, + startY: 0, + scrollLeft: 0, + scrollTop: 0, + mouseDown: function(sender, me) + { + this.startX = me.getGraphX(); + this.startY = me.getGraphY(); + this.scrollLeft = graph.container.scrollLeft; + this.scrollTop = graph.container.scrollTop; + }, + mouseMove: function(sender, me) {}, + mouseUp: function(sender, me) + { + if (mxEvent.isTouchEvent(me.getEvent())) + { + if ((Math.abs(this.scrollLeft - graph.container.scrollLeft) < tol && + Math.abs(this.scrollTop - graph.container.scrollTop) < tol) && + (Math.abs(this.startX - me.getGraphX()) < tol && + Math.abs(this.startY - me.getGraphY()) < tol)) + { + if (parseFloat(ui.chromelessToolbar.style.opacity || 0) > 0) + { + fadeOut(); + } + else + { + fadeIn(30); + } + } + } + } + }); + } // end if toolbar + + // Installs handling of highlight and handling links to relative links and anchors + if (!this.editor.editable) + { + this.addChromelessClickHandler(); + } + } + else if (this.editor.extendCanvas) + { + /** + * Guesses autoTranslate to avoid another repaint (see below). + * Works if only the scale of the graph changes or if pages + * are visible and the visible pages do not change. + */ + var graphViewValidate = graph.view.validate; + graph.view.validate = function() + { + if (this.graph.container != null && mxUtils.hasScrollbars(this.graph.container)) + { + var pad = this.graph.getPagePadding(); + var size = this.graph.getPageSize(); + + // Updating scrollbars here causes flickering in quirks and is not needed + // if zoom method is always used to set the current scale on the graph. + var tx = this.translate.x; + var ty = this.translate.y; + this.translate.x = pad.x - (this.x0 || 0) * size.width; + this.translate.y = pad.y - (this.y0 || 0) * size.height; + } + + graphViewValidate.apply(this, arguments); + }; + + var graphSizeDidChange = graph.sizeDidChange; + graph.sizeDidChange = function() + { + if (this.container != null && mxUtils.hasScrollbars(this.container)) + { + var pages = this.getPageLayout(); + var pad = this.getPagePadding(); + var size = this.getPageSize(); + + // Updates the minimum graph size + var minw = Math.ceil(2 * pad.x + pages.width * size.width); + var minh = Math.ceil(2 * pad.y + pages.height * size.height); + + var min = graph.minimumGraphSize; + + // LATER: Fix flicker of scrollbar size in IE quirks mode + // after delayed call in window.resize event handler + if (min == null || min.width != minw || min.height != minh) + { + graph.minimumGraphSize = new mxRectangle(0, 0, minw, minh); + } + + // Updates auto-translate to include padding and graph size + var dx = pad.x - pages.x * size.width; + var dy = pad.y - pages.y * size.height; + + if (!this.autoTranslate && (this.view.translate.x != dx || this.view.translate.y != dy)) + { + this.autoTranslate = true; + this.view.x0 = pages.x; + this.view.y0 = pages.y; + + // NOTE: THIS INVOKES THIS METHOD AGAIN. UNFORTUNATELY THERE IS NO WAY AROUND THIS SINCE THE + // BOUNDS ARE KNOWN AFTER THE VALIDATION AND SETTING THE TRANSLATE TRIGGERS A REVALIDATION. + // SHOULD MOVE TRANSLATE/SCALE TO VIEW. + var tx = graph.view.translate.x; + var ty = graph.view.translate.y; + graph.view.setTranslate(dx, dy); + + // LATER: Fix rounding errors for small zoom + graph.container.scrollLeft += Math.round((dx - tx) * graph.view.scale); + graph.container.scrollTop += Math.round((dy - ty) * graph.view.scale); + + this.autoTranslate = false; + + return; + } + + graphSizeDidChange.apply(this, arguments); + } + }; + } + + // Accumulates the zoom factor while the rendering is taking place + // so that not the complete sequence of zoom steps must be painted + graph.updateZoomTimeout = null; + graph.cumulativeZoomFactor = 1; + + var cursorPosition = null; + + graph.lazyZoom = function(zoomIn) + { + if (this.updateZoomTimeout != null) + { + window.clearTimeout(this.updateZoomTimeout); + } + + // Switches to 1% zoom steps below 15% + // Lower bound depdends on rounding below + if (zoomIn) + { + if (this.view.scale * this.cumulativeZoomFactor < 0.15) + { + this.cumulativeZoomFactor = (this.view.scale + 0.01) / this.view.scale; + } + else + { + // Uses to 5% zoom steps for better grid rendering in webkit + // and to avoid rounding errors for zoom steps + this.cumulativeZoomFactor *= this.zoomFactor; + this.cumulativeZoomFactor = Math.round(this.view.scale * this.cumulativeZoomFactor * 20) / 20 / this.view.scale; + } + } + else + { + if (this.view.scale * this.cumulativeZoomFactor <= 0.15) + { + this.cumulativeZoomFactor = (this.view.scale - 0.01) / this.view.scale; + } + else + { + // Uses to 5% zoom steps for better grid rendering in webkit + // and to avoid rounding errors for zoom steps + this.cumulativeZoomFactor /= this.zoomFactor; + this.cumulativeZoomFactor = Math.round(this.view.scale * this.cumulativeZoomFactor * 20) / 20 / this.view.scale; + } + } + + this.cumulativeZoomFactor = Math.max(0.01, Math.min(this.view.scale * this.cumulativeZoomFactor, 160) / this.view.scale); + + this.updateZoomTimeout = window.setTimeout(mxUtils.bind(this, function() + { + var offset = mxUtils.getOffset(graph.container); + var dx = 0; + var dy = 0; + + if (cursorPosition != null) + { + dx = graph.container.offsetWidth / 2 - cursorPosition.x + offset.x; + dy = graph.container.offsetHeight / 2 - cursorPosition.y + offset.y; + } + + var prev = this.view.scale; + this.zoom(this.cumulativeZoomFactor); + var s = this.view.scale; + + if (s != prev) + { + if (resize != null) + { + ui.chromelessResize(false, null, dx * (this.cumulativeZoomFactor - 1), + dy * (this.cumulativeZoomFactor - 1)); + } + + if (mxUtils.hasScrollbars(graph.container) && (dx != 0 || dy != 0)) + { + graph.container.scrollLeft -= dx * (this.cumulativeZoomFactor - 1); + graph.container.scrollTop -= dy * (this.cumulativeZoomFactor - 1); + } + } + + this.cumulativeZoomFactor = 1; + this.updateZoomTimeout = null; + }), this.lazyZoomDelay); + }; + + mxEvent.addMouseWheelListener(mxUtils.bind(this, function(evt, up) + { + // Ctrl+wheel (or pinch on touchpad) is a native browser zoom event is OS X + // LATER: Add support for zoom via pinch on trackpad for Chrome in OS X + if ((this.dialogs == null || this.dialogs.length == 0) && graph.isZoomWheelEvent(evt)) + { + var source = mxEvent.getSource(evt); + + while (source != null) + { + if (source == graph.container) + { + cursorPosition = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + graph.lazyZoom(up); + mxEvent.consume(evt); + + return; + } + + source = source.parentNode; + } + } + })); +}; + +/** + * Creates a temporary graph instance for rendering off-screen content. + */ +EditorUi.prototype.addChromelessToolbarItems = function(addButton) +{ + addButton(mxUtils.bind(this, function(evt) + { + this.actions.get('print').funct(); + mxEvent.consume(evt); + }), Editor.printLargeImage, mxResources.get('print')); +}; + +/** + * Creates a temporary graph instance for rendering off-screen content. + */ +EditorUi.prototype.createTemporaryGraph = function(stylesheet) +{ + var graph = new Graph(document.createElement('div'), null, null, stylesheet); + graph.resetViewOnRootChange = false; + graph.setConnectable(false); + graph.gridEnabled = false; + graph.autoScroll = false; + graph.setTooltips(false); + graph.setEnabled(false); + + // Container must be in the DOM for correct HTML rendering + graph.container.style.visibility = 'hidden'; + graph.container.style.position = 'absolute'; + graph.container.style.overflow = 'hidden'; + graph.container.style.height = '1px'; + graph.container.style.width = '1px'; + + return graph; +}; + +/** + * + */ +EditorUi.prototype.addChromelessClickHandler = function() +{ + var hl = urlParams['highlight']; + + // Adds leading # for highlight color code + if (hl != null && hl.length > 0) + { + hl = '#' + hl; + } + + this.editor.graph.addClickHandler(hl); +}; + +/** + * + */ +EditorUi.prototype.toggleFormatPanel = function(forceHide) +{ + this.formatWidth = (forceHide || this.formatWidth > 0) ? 0 : 240; + this.formatContainer.style.display = (forceHide || this.formatWidth > 0) ? '' : 'none'; + this.refresh(); + this.format.refresh(); + this.fireEvent(new mxEventObject('formatWidthChanged')); +}; + +/** + * Adds support for placeholders in labels. + */ +EditorUi.prototype.lightboxFit = function(maxHeight) +{ + if (this.isDiagramEmpty()) + { + this.editor.graph.view.setScale(1); + } + else + { + var p = urlParams['border']; + var border = 60; + + if (p != null) + { + border = parseInt(p); + } + + // LATER: Use initial graph bounds to avoid rounding errors + this.editor.graph.maxFitScale = this.lightboxMaxFitScale; + this.editor.graph.fit(border, null, null, null, null, null, maxHeight); + this.editor.graph.maxFitScale = null; + } +}; + +/** + * Translates this point by the given vector. + * + * @param {number} dx X-coordinate of the translation. + * @param {number} dy Y-coordinate of the translation. + */ +EditorUi.prototype.isDiagramEmpty = function() +{ + var model = this.editor.graph.getModel(); + + return model.getChildCount(model.root) == 1 && model.getChildCount(model.getChildAt(model.root, 0)) == 0; +}; + +/** + * Hook for allowing selection and context menu for certain events. + */ +EditorUi.prototype.isSelectionAllowed = function(evt) +{ + return mxEvent.getSource(evt).nodeName == 'SELECT' || (mxEvent.getSource(evt).nodeName == 'INPUT' && + mxUtils.isAncestorNode(this.formatContainer, mxEvent.getSource(evt))); +}; + +/** + * Installs dialog if browser window is closed without saving + * This must be disabled during save and image export. + */ +EditorUi.prototype.addBeforeUnloadListener = function() +{ + // Installs dialog if browser window is closed without saving + // This must be disabled during save and image export + window.onbeforeunload = mxUtils.bind(this, function() + { + if (!this.editor.isChromelessView()) + { + return this.onBeforeUnload(); + } + }); +}; + +/** + * Sets the onbeforeunload for the application + */ +EditorUi.prototype.onBeforeUnload = function() +{ + if (this.editor.modified) + { + return mxResources.get('allChangesLost'); + } +}; + +/** + * Opens the current diagram via the window.opener if one exists. + */ +EditorUi.prototype.open = function() +{ + // Cross-domain window access is not allowed in FF, so if we + // were opened from another domain then this will fail. + try + { + if (window.opener != null && window.opener.openFile != null) + { + window.opener.openFile.setConsumer(mxUtils.bind(this, function(xml, filename) + { + try + { + var doc = mxUtils.parseXml(xml); + this.editor.setGraphXml(doc.documentElement); + this.editor.setModified(false); + this.editor.undoManager.clear(); + + if (filename != null) + { + this.editor.setFilename(filename); + this.updateDocumentTitle(); + } + + return; + } + catch (e) + { + mxUtils.alert(mxResources.get('invalidOrMissingFile') + ': ' + e.message); + } + })); + } + } + catch(e) + { + // ignore + } + + // Fires as the last step if no file was loaded + this.editor.graph.view.validate(); + + // Required only in special cases where an initial file is opened + // and the minimumGraphSize changes and CSS must be updated. + this.editor.graph.sizeDidChange(); + this.editor.fireEvent(new mxEventObject('resetGraphView')); +}; + +/** + * Sets the current menu and element. + */ +EditorUi.prototype.setCurrentMenu = function(menu, elt) +{ + this.currentMenuElt = elt; + this.currentMenu = menu; +}; + +/** + * Resets the current menu and element. + */ +EditorUi.prototype.resetCurrentMenu = function() +{ + this.currentMenuElt = null; + this.currentMenu = null; +}; + +/** + * Hides and destroys the current menu. + */ +EditorUi.prototype.hideCurrentMenu = function() +{ + if (this.currentMenu != null) + { + this.currentMenu.hideMenu(); + this.resetCurrentMenu(); + } +}; + +/** + * Updates the document title. + */ +EditorUi.prototype.updateDocumentTitle = function() +{ + var title = this.editor.getOrCreateFilename(); + + if (this.editor.appName != null) + { + title += ' - ' + this.editor.appName; + } + + document.title = title; +}; + +/** + * Updates the document title. + */ +EditorUi.prototype.createHoverIcons = function() +{ + return new HoverIcons(this.editor.graph); +}; + +/** + * Returns the URL for a copy of this editor with no state. + */ +EditorUi.prototype.redo = function() +{ + try + { + var graph = this.editor.graph; + + if (graph.isEditing()) + { + document.execCommand('redo', false, null); + } + else + { + this.editor.undoManager.redo(); + } + } + catch (e) + { + // ignore all errors + } +}; + +/** + * Returns the URL for a copy of this editor with no state. + */ +EditorUi.prototype.undo = function() +{ + try + { + var graph = this.editor.graph; + + if (graph.isEditing()) + { + // Stops editing and executes undo on graph if native undo + // does not affect current editing value + var value = graph.cellEditor.textarea.innerHTML; + document.execCommand('undo', false, null); + + if (value == graph.cellEditor.textarea.innerHTML) + { + graph.stopEditing(true); + this.editor.undoManager.undo(); + } + } + else + { + this.editor.undoManager.undo(); + } + } + catch (e) + { + // ignore all errors + } +}; + +/** + * Returns the URL for a copy of this editor with no state. + */ +EditorUi.prototype.canRedo = function() +{ + return this.editor.graph.isEditing() || this.editor.undoManager.canRedo(); +}; + +/** + * Returns the URL for a copy of this editor with no state. + */ +EditorUi.prototype.canUndo = function() +{ + return this.editor.graph.isEditing() || this.editor.undoManager.canUndo(); +}; + +/** + * + */ +EditorUi.prototype.getEditBlankXml = function() +{ + return mxUtils.getXml(this.editor.getGraphXml()); +}; + +/** + * Returns the URL for a copy of this editor with no state. + */ +EditorUi.prototype.getUrl = function(pathname) +{ + var href = (pathname != null) ? pathname : window.location.pathname; + var parms = (href.indexOf('?') > 0) ? 1 : 0; + + // Removes template URL parameter for new blank diagram + for (var key in urlParams) + { + if (parms == 0) + { + href += '?'; + } + else + { + href += '&'; + } + + href += key + '=' + urlParams[key]; + parms++; + } + + return href; +}; + +/** + * Specifies if the graph has scrollbars. + */ +EditorUi.prototype.setScrollbars = function(value) +{ + var graph = this.editor.graph; + var prev = graph.container.style.overflow; + graph.scrollbars = value; + this.editor.updateGraphComponents(); + + if (prev != graph.container.style.overflow) + { + if (graph.container.style.overflow == 'hidden') + { + var t = graph.view.translate; + graph.view.setTranslate(t.x - graph.container.scrollLeft / graph.view.scale, t.y - graph.container.scrollTop / graph.view.scale); + graph.container.scrollLeft = 0; + graph.container.scrollTop = 0; + graph.minimumGraphSize = null; + graph.sizeDidChange(); + } + else + { + var dx = graph.view.translate.x; + var dy = graph.view.translate.y; + + graph.view.translate.x = 0; + graph.view.translate.y = 0; + graph.sizeDidChange(); + graph.container.scrollLeft -= Math.round(dx * graph.view.scale); + graph.container.scrollTop -= Math.round(dy * graph.view.scale); + } + } + + this.fireEvent(new mxEventObject('scrollbarsChanged')); +}; + +/** + * Returns true if the graph has scrollbars. + */ +EditorUi.prototype.hasScrollbars = function() +{ + return this.editor.graph.scrollbars; +}; + +/** + * Resets the state of the scrollbars. + */ +EditorUi.prototype.resetScrollbars = function() +{ + var graph = this.editor.graph; + + if (!this.editor.extendCanvas) + { + graph.container.scrollTop = 0; + graph.container.scrollLeft = 0; + + if (!mxUtils.hasScrollbars(graph.container)) + { + graph.view.setTranslate(0, 0); + } + } + else if (!this.editor.isChromelessView()) + { + if (mxUtils.hasScrollbars(graph.container)) + { + if (graph.pageVisible) + { + var pad = graph.getPagePadding(); + graph.container.scrollTop = Math.floor(pad.y - this.editor.initialTopSpacing) - 1; + graph.container.scrollLeft = Math.floor(Math.min(pad.x, + (graph.container.scrollWidth - graph.container.clientWidth) / 2)) - 1; + + // Scrolls graph to visible area + var bounds = graph.getGraphBounds(); + + if (bounds.width > 0 && bounds.height > 0) + { + if (bounds.x > graph.container.scrollLeft + graph.container.clientWidth * 0.9) + { + graph.container.scrollLeft = Math.min(bounds.x + bounds.width - graph.container.clientWidth, bounds.x - 10); + } + + if (bounds.y > graph.container.scrollTop + graph.container.clientHeight * 0.9) + { + graph.container.scrollTop = Math.min(bounds.y + bounds.height - graph.container.clientHeight, bounds.y - 10); + } + } + } + else + { + var bounds = graph.getGraphBounds(); + var width = Math.max(bounds.width, graph.scrollTileSize.width * graph.view.scale); + var height = Math.max(bounds.height, graph.scrollTileSize.height * graph.view.scale); + graph.container.scrollTop = Math.floor(Math.max(0, bounds.y - Math.max(20, (graph.container.clientHeight - height) / 4))); + graph.container.scrollLeft = Math.floor(Math.max(0, bounds.x - Math.max(0, (graph.container.clientWidth - width) / 2))); + } + } + else + { + // This code is not actively used since the default for scrollbars is always true + if (graph.pageVisible) + { + var b = graph.view.getBackgroundPageBounds(); + graph.view.setTranslate(Math.floor(Math.max(0, (graph.container.clientWidth - b.width) / 2) - b.x), + Math.floor(Math.max(0, (graph.container.clientHeight - b.height) / 2) - b.y)); + } + else + { + var bounds = graph.getGraphBounds(); + graph.view.setTranslate(Math.floor(Math.max(0, Math.max(0, (graph.container.clientWidth - bounds.width) / 2) - bounds.x)), + Math.floor(Math.max(0, Math.max(20, (graph.container.clientHeight - bounds.height) / 4)) - bounds.y)); + } + } + } +}; + +/** + * Loads the stylesheet for this graph. + */ +EditorUi.prototype.setPageVisible = function(value) +{ + var graph = this.editor.graph; + var hasScrollbars = mxUtils.hasScrollbars(graph.container); + var tx = 0; + var ty = 0; + + if (hasScrollbars) + { + tx = graph.view.translate.x * graph.view.scale - graph.container.scrollLeft; + ty = graph.view.translate.y * graph.view.scale - graph.container.scrollTop; + } + + graph.pageVisible = value; + graph.pageBreaksVisible = value; + graph.preferPageSize = value; + graph.view.validateBackground(); + + // Workaround for possible handle offset + if (hasScrollbars) + { + var cells = graph.getSelectionCells(); + graph.clearSelection(); + graph.setSelectionCells(cells); + } + + // Calls updatePageBreaks + graph.sizeDidChange(); + + if (hasScrollbars) + { + graph.container.scrollLeft = graph.view.translate.x * graph.view.scale - tx; + graph.container.scrollTop = graph.view.translate.y * graph.view.scale - ty; + } + + this.fireEvent(new mxEventObject('pageViewChanged')); +}; + +/** + * Change types + */ +function ChangePageSetup(ui, color, image, format) +{ + this.ui = ui; + this.color = color; + this.previousColor = color; + this.image = image; + this.previousImage = image; + this.format = format; + this.previousFormat = format; + + // Needed since null are valid values for color and image + this.ignoreColor = false; + this.ignoreImage = false; +} + +/** + * Implementation of the undoable page rename. + */ +ChangePageSetup.prototype.execute = function() +{ + var graph = this.ui.editor.graph; + + if (!this.ignoreColor) + { + this.color = this.previousColor; + var tmp = graph.background; + this.ui.setBackgroundColor(this.previousColor); + this.previousColor = tmp; + } + + if (!this.ignoreImage) + { + this.image = this.previousImage; + var tmp = graph.backgroundImage; + this.ui.setBackgroundImage(this.previousImage); + this.previousImage = tmp; + } + + if (this.previousFormat != null) + { + this.format = this.previousFormat; + var tmp = graph.pageFormat; + + if (this.previousFormat.width != tmp.width || + this.previousFormat.height != tmp.height) + { + this.ui.setPageFormat(this.previousFormat); + this.previousFormat = tmp; + } + } + + if (this.foldingEnabled != null && this.foldingEnabled != this.ui.editor.graph.foldingEnabled) + { + this.ui.setFoldingEnabled(this.foldingEnabled); + this.foldingEnabled = !this.foldingEnabled; + } +}; + +// Registers codec for ChangePageSetup +(function() +{ + var codec = new mxObjectCodec(new ChangePageSetup(), ['ui', 'previousColor', 'previousImage', 'previousFormat']); + + codec.afterDecode = function(dec, node, obj) + { + obj.previousColor = obj.color; + obj.previousImage = obj.image; + obj.previousFormat = obj.format; + + if (obj.foldingEnabled != null) + { + obj.foldingEnabled = !obj.foldingEnabled; + } + + return obj; + }; + + mxCodecRegistry.register(codec); +})(); + +/** + * Loads the stylesheet for this graph. + */ +EditorUi.prototype.setBackgroundColor = function(value) +{ + this.editor.graph.background = value; + this.editor.graph.view.validateBackground(); + + this.fireEvent(new mxEventObject('backgroundColorChanged')); +}; + +/** + * Loads the stylesheet for this graph. + */ +EditorUi.prototype.setFoldingEnabled = function(value) +{ + this.editor.graph.foldingEnabled = value; + this.editor.graph.view.revalidate(); + + this.fireEvent(new mxEventObject('foldingEnabledChanged')); +}; + +/** + * Loads the stylesheet for this graph. + */ +EditorUi.prototype.setPageFormat = function(value) +{ + this.editor.graph.pageFormat = value; + + if (!this.editor.graph.pageVisible) + { + this.actions.get('pageView').funct(); + } + else + { + this.editor.graph.view.validateBackground(); + this.editor.graph.sizeDidChange(); + } + + this.fireEvent(new mxEventObject('pageFormatChanged')); +}; + +/** + * Loads the stylesheet for this graph. + */ +EditorUi.prototype.setPageScale = function(value) +{ + this.editor.graph.pageScale = value; + + if (!this.editor.graph.pageVisible) + { + this.actions.get('pageView').funct(); + } + else + { + this.editor.graph.view.validateBackground(); + this.editor.graph.sizeDidChange(); + } + + this.fireEvent(new mxEventObject('pageScaleChanged')); +}; + +/** + * Loads the stylesheet for this graph. + */ +EditorUi.prototype.setGridColor = function(value) +{ + this.editor.graph.view.gridColor = value; + this.editor.graph.view.validateBackground(); + this.fireEvent(new mxEventObject('gridColorChanged')); +}; + +/** + * Updates the states of the given undo/redo items. + */ +EditorUi.prototype.addUndoListener = function() +{ + var undo = this.actions.get('undo'); + var redo = this.actions.get('redo'); + + var undoMgr = this.editor.undoManager; + + var undoListener = mxUtils.bind(this, function() + { + undo.setEnabled(this.canUndo()); + redo.setEnabled(this.canRedo()); + }); + + undoMgr.addListener(mxEvent.ADD, undoListener); + undoMgr.addListener(mxEvent.UNDO, undoListener); + undoMgr.addListener(mxEvent.REDO, undoListener); + undoMgr.addListener(mxEvent.CLEAR, undoListener); + + // Overrides cell editor to update action states + var cellEditorStartEditing = this.editor.graph.cellEditor.startEditing; + + this.editor.graph.cellEditor.startEditing = function() + { + cellEditorStartEditing.apply(this, arguments); + undoListener(); + }; + + var cellEditorStopEditing = this.editor.graph.cellEditor.stopEditing; + + this.editor.graph.cellEditor.stopEditing = function(cell, trigger) + { + cellEditorStopEditing.apply(this, arguments); + undoListener(); + }; + + // Updates the button states once + undoListener(); +}; + +/** +* Updates the states of the given toolbar items based on the selection. +*/ +EditorUi.prototype.updateActionStates = function() +{ + var graph = this.editor.graph; + var selected = !graph.isSelectionEmpty(); + var vertexSelected = false; + var edgeSelected = false; + + var cells = graph.getSelectionCells(); + + if (cells != null) + { + for (var i = 0; i < cells.length; i++) + { + var cell = cells[i]; + + if (graph.getModel().isEdge(cell)) + { + edgeSelected = true; + } + + if (graph.getModel().isVertex(cell)) + { + vertexSelected = true; + } + + if (edgeSelected && vertexSelected) + { + break; + } + } + } + + // Updates action states + var actions = ['cut', 'copy', 'bold', 'italic', 'underline', 'delete', 'duplicate', + 'editStyle', 'editTooltip', 'editLink', 'backgroundColor', 'borderColor', + 'edit', 'toFront', 'toBack', 'lockUnlock', 'solid', 'dashed', 'pasteSize', + 'dotted', 'fillColor', 'gradientColor', 'shadow', 'fontColor', + 'formattedText', 'rounded', 'toggleRounded', 'sharp', 'strokeColor']; + + for (var i = 0; i < actions.length; i++) + { + this.actions.get(actions[i]).setEnabled(selected); + } + + this.actions.get('setAsDefaultStyle').setEnabled(graph.getSelectionCount() == 1); + this.actions.get('clearWaypoints').setEnabled(!graph.isSelectionEmpty()); + this.actions.get('copySize').setEnabled(graph.getSelectionCount() == 1); + this.actions.get('turn').setEnabled(!graph.isSelectionEmpty()); + this.actions.get('curved').setEnabled(edgeSelected); + this.actions.get('rotation').setEnabled(vertexSelected); + this.actions.get('wordWrap').setEnabled(vertexSelected); + this.actions.get('autosize').setEnabled(vertexSelected); + var oneVertexSelected = vertexSelected && graph.getSelectionCount() == 1; + this.actions.get('group').setEnabled(graph.getSelectionCount() > 1 || + (oneVertexSelected && !graph.isContainer(graph.getSelectionCell()))); + this.actions.get('ungroup').setEnabled(graph.getSelectionCount() == 1 && + (graph.getModel().getChildCount(graph.getSelectionCell()) > 0 || + (oneVertexSelected && graph.isContainer(graph.getSelectionCell())))); + this.actions.get('removeFromGroup').setEnabled(oneVertexSelected && + graph.getModel().isVertex(graph.getModel().getParent(graph.getSelectionCell()))); + + // Updates menu states + var state = graph.view.getState(graph.getSelectionCell()); + this.menus.get('navigation').setEnabled(selected || graph.view.currentRoot != null); + this.actions.get('collapsible').setEnabled(vertexSelected && + (graph.isContainer(graph.getSelectionCell()) || graph.model.getChildCount(graph.getSelectionCell()) > 0)); + this.actions.get('home').setEnabled(graph.view.currentRoot != null); + this.actions.get('exitGroup').setEnabled(graph.view.currentRoot != null); + this.actions.get('enterGroup').setEnabled(graph.getSelectionCount() == 1 && graph.isValidRoot(graph.getSelectionCell())); + var foldable = graph.getSelectionCount() == 1 && graph.isCellFoldable(graph.getSelectionCell()); + this.actions.get('expand').setEnabled(foldable); + this.actions.get('collapse').setEnabled(foldable); + this.actions.get('editLink').setEnabled(graph.getSelectionCount() == 1); + this.actions.get('openLink').setEnabled(graph.getSelectionCount() == 1 && + graph.getLinkForCell(graph.getSelectionCell()) != null); + this.actions.get('guides').setEnabled(graph.isEnabled()); + this.actions.get('grid').setEnabled(!this.editor.chromeless || this.editor.editable); + + var unlocked = graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent()); + this.menus.get('layout').setEnabled(unlocked); + this.menus.get('insert').setEnabled(unlocked); + this.menus.get('direction').setEnabled(unlocked && vertexSelected); + this.menus.get('align').setEnabled(unlocked && vertexSelected && graph.getSelectionCount() > 1); + this.menus.get('distribute').setEnabled(unlocked && vertexSelected && graph.getSelectionCount() > 1); + this.actions.get('selectVertices').setEnabled(unlocked); + this.actions.get('selectEdges').setEnabled(unlocked); + this.actions.get('selectAll').setEnabled(unlocked); + this.actions.get('selectNone').setEnabled(unlocked); + + this.updatePasteActionStates(); +}; + +/** + * Refreshes the viewport. + */ +EditorUi.prototype.refresh = function(sizeDidChange) +{ + sizeDidChange = (sizeDidChange != null) ? sizeDidChange : true; + + var quirks = mxClient.IS_IE && (document.documentMode == null || document.documentMode == 5); + var w = this.container.clientWidth; + var h = this.container.clientHeight; + + if (this.container == document.body) + { + w = document.body.clientWidth || document.documentElement.clientWidth; + h = (quirks) ? document.body.clientHeight || document.documentElement.clientHeight : document.documentElement.clientHeight; + } + + // Workaround for bug on iOS see + // http://stackoverflow.com/questions/19012135/ios-7-ipad-safari-landscape-innerheight-outerheight-layout-issue + // FIXME: Fix if footer visible + var off = 0; + + if (mxClient.IS_IOS && !window.navigator.standalone) + { + if (window.innerHeight != document.documentElement.clientHeight) + { + off = document.documentElement.clientHeight - window.innerHeight; + window.scrollTo(0, 0); + } + } + + var effHsplitPosition = Math.max(0, Math.min(this.hsplitPosition, w - this.splitSize - 20)); + + var tmp = 0; + + if (this.menubar != null) + { + this.menubarContainer.style.height = this.menubarHeight + 'px'; + tmp += this.menubarHeight; + } + + if (this.toolbar != null) + { + this.toolbarContainer.style.top = this.menubarHeight + 'px'; + this.toolbarContainer.style.height = this.toolbarHeight + 'px'; + tmp += this.toolbarHeight; + } + + if (tmp > 0 && !mxClient.IS_QUIRKS) + { + tmp += 1; + } + + var sidebarFooterHeight = 0; + + if (this.sidebarFooterContainer != null) + { + var bottom = this.footerHeight + off; + sidebarFooterHeight = Math.max(0, Math.min(h - tmp - bottom, this.sidebarFooterHeight)); + this.sidebarFooterContainer.style.width = effHsplitPosition + 'px'; + this.sidebarFooterContainer.style.height = sidebarFooterHeight + 'px'; + this.sidebarFooterContainer.style.bottom = bottom + 'px'; + } + + var fw = (this.format != null) ? this.formatWidth : 0; + this.sidebarContainer.style.top = tmp + 'px'; + this.sidebarContainer.style.width = effHsplitPosition + 'px'; + this.formatContainer.style.top = tmp + 'px'; + this.formatContainer.style.width = fw + 'px'; + this.formatContainer.style.display = (this.format != null) ? '' : 'none'; + + this.diagramContainer.style.left = (this.hsplit.parentNode != null) ? (effHsplitPosition + this.splitSize) + 'px' : '0px'; + this.diagramContainer.style.top = this.sidebarContainer.style.top; + this.footerContainer.style.height = this.footerHeight + 'px'; + this.hsplit.style.top = this.sidebarContainer.style.top; + this.hsplit.style.bottom = (this.footerHeight + off) + 'px'; + this.hsplit.style.left = effHsplitPosition + 'px'; + + if (this.tabContainer != null) + { + this.tabContainer.style.left = this.diagramContainer.style.left; + } + + if (quirks) + { + this.menubarContainer.style.width = w + 'px'; + this.toolbarContainer.style.width = this.menubarContainer.style.width; + var sidebarHeight = Math.max(0, h - this.footerHeight - this.menubarHeight - this.toolbarHeight); + this.sidebarContainer.style.height = (sidebarHeight - sidebarFooterHeight) + 'px'; + this.formatContainer.style.height = sidebarHeight + 'px'; + this.diagramContainer.style.width = (this.hsplit.parentNode != null) ? Math.max(0, w - effHsplitPosition - this.splitSize - fw) + 'px' : w + 'px'; + this.footerContainer.style.width = this.menubarContainer.style.width; + var diagramHeight = Math.max(0, h - this.footerHeight - this.menubarHeight - this.toolbarHeight); + + if (this.tabContainer != null) + { + this.tabContainer.style.width = this.diagramContainer.style.width; + this.tabContainer.style.bottom = (this.footerHeight + off) + 'px'; + diagramHeight -= this.tabContainer.clientHeight; + } + + this.diagramContainer.style.height = diagramHeight + 'px'; + this.hsplit.style.height = diagramHeight + 'px'; + } + else + { + if (this.footerHeight > 0) + { + this.footerContainer.style.bottom = off + 'px'; + } + + this.diagramContainer.style.right = fw + 'px'; + var th = 0; + + if (this.tabContainer != null) + { + this.tabContainer.style.bottom = (this.footerHeight + off) + 'px'; + this.tabContainer.style.right = this.diagramContainer.style.right; + th = this.tabContainer.clientHeight; + } + + this.sidebarContainer.style.bottom = (this.footerHeight + sidebarFooterHeight + off) + 'px'; + this.formatContainer.style.bottom = (this.footerHeight + off) + 'px'; + this.diagramContainer.style.bottom = (this.footerHeight + off + th) + 'px'; + } + + if (sizeDidChange) + { + this.editor.graph.sizeDidChange(); + } +}; + +/** + * Creates the required containers. + */ +EditorUi.prototype.createTabContainer = function() +{ + return null; +}; + +/** + * Creates the required containers. + */ +EditorUi.prototype.createDivs = function() +{ + this.menubarContainer = this.createDiv('geMenubarContainer'); + this.toolbarContainer = this.createDiv('geToolbarContainer'); + this.sidebarContainer = this.createDiv('geSidebarContainer'); + this.formatContainer = this.createDiv('geSidebarContainer geFormatContainer'); + this.diagramContainer = this.createDiv('geDiagramContainer'); + this.footerContainer = this.createDiv('geFooterContainer'); + this.hsplit = this.createDiv('geHsplit'); + this.hsplit.setAttribute('title', mxResources.get('collapseExpand')); + + // Sets static style for containers + this.menubarContainer.style.top = '0px'; + this.menubarContainer.style.left = '0px'; + this.menubarContainer.style.right = '0px'; + this.toolbarContainer.style.left = '0px'; + this.toolbarContainer.style.right = '0px'; + this.sidebarContainer.style.left = '0px'; + this.formatContainer.style.right = '0px'; + this.formatContainer.style.zIndex = '1'; + this.diagramContainer.style.right = ((this.format != null) ? this.formatWidth : 0) + 'px'; + this.footerContainer.style.left = '0px'; + this.footerContainer.style.right = '0px'; + this.footerContainer.style.bottom = '0px'; + this.footerContainer.style.zIndex = mxPopupMenu.prototype.zIndex - 2; + this.hsplit.style.width = this.splitSize + 'px'; + this.sidebarFooterContainer = this.createSidebarFooterContainer(); + + if (this.sidebarFooterContainer) + { + this.sidebarFooterContainer.style.left = '0px'; + } + + if (!this.editor.chromeless) + { + this.tabContainer = this.createTabContainer(); + } + else + { + this.diagramContainer.style.border = 'none'; + } +}; + +/** + * Hook for sidebar footer container. This implementation returns null. + */ +EditorUi.prototype.createSidebarFooterContainer = function() +{ + return null; +}; + +/** + * Creates the required containers. + */ +EditorUi.prototype.createUi = function() +{ + // Creates menubar + this.menubar = (this.editor.chromeless) ? null : this.menus.createMenubar(this.createDiv('geMenubar')); + + if (this.menubar != null) + { + this.menubarContainer.appendChild(this.menubar.container); + } + + // Adds status bar in menubar + if (this.menubar != null) + { + this.statusContainer = this.createStatusContainer(); + + // Connects the status bar to the editor status + this.editor.addListener('statusChanged', mxUtils.bind(this, function() + { + this.setStatusText(this.editor.getStatus()); + })); + + this.setStatusText(this.editor.getStatus()); + this.menubar.container.appendChild(this.statusContainer); + + // Inserts into DOM + this.container.appendChild(this.menubarContainer); + } + + // Creates the sidebar + this.sidebar = (this.editor.chromeless) ? null : this.createSidebar(this.sidebarContainer); + + if (this.sidebar != null) + { + this.container.appendChild(this.sidebarContainer); + } + + // Creates the format sidebar + this.format = (this.editor.chromeless || !this.formatEnabled) ? null : this.createFormat(this.formatContainer); + + if (this.format != null) + { + this.container.appendChild(this.formatContainer); + } + + // Creates the footer + var footer = (this.editor.chromeless) ? null : this.createFooter(); + + if (footer != null) + { + this.footerContainer.appendChild(footer); + this.container.appendChild(this.footerContainer); + } + + if (this.sidebar != null && this.sidebarFooterContainer) + { + this.container.appendChild(this.sidebarFooterContainer); + } + + this.container.appendChild(this.diagramContainer); + + if (this.container != null && this.tabContainer != null) + { + this.container.appendChild(this.tabContainer); + } + + // Creates toolbar + this.toolbar = (this.editor.chromeless) ? null : this.createToolbar(this.createDiv('geToolbar')); + + if (this.toolbar != null) + { + this.toolbarContainer.appendChild(this.toolbar.container); + this.container.appendChild(this.toolbarContainer); + } + + // HSplit + if (this.sidebar != null) + { + this.container.appendChild(this.hsplit); + + this.addSplitHandler(this.hsplit, true, 0, mxUtils.bind(this, function(value) + { + this.hsplitPosition = value; + this.refresh(); + })); + } +}; + +/** + * Creates a new toolbar for the given container. + */ +EditorUi.prototype.createStatusContainer = function() +{ + var container = document.createElement('a'); + container.className = 'geItem geStatus'; + + if (screen.width < 420) + { + container.style.maxWidth = Math.max(20, screen.width - 320) + 'px'; + container.style.overflow = 'hidden'; + } + + return container; +}; + +/** + * Creates a new toolbar for the given container. + */ +EditorUi.prototype.setStatusText = function(value) +{ + this.statusContainer.innerHTML = value; +}; + +/** + * Creates a new toolbar for the given container. + */ +EditorUi.prototype.createToolbar = function(container) +{ + return new Toolbar(this, container); +}; + +/** + * Creates a new sidebar for the given container. + */ +EditorUi.prototype.createSidebar = function(container) +{ + return new Sidebar(this, container); +}; + +/** + * Creates a new sidebar for the given container. + */ +EditorUi.prototype.createFormat = function(container) +{ + return new Format(this, container); +}; + +/** + * Creates and returns a new footer. + */ +EditorUi.prototype.createFooter = function() +{ + return this.createDiv('geFooter'); +}; + +/** + * Creates the actual toolbar for the toolbar container. + */ +EditorUi.prototype.createDiv = function(classname) +{ + var elt = document.createElement('div'); + elt.className = classname; + + return elt; +}; + +/** + * Updates the states of the given undo/redo items. + */ +EditorUi.prototype.addSplitHandler = function(elt, horizontal, dx, onChange) +{ + var start = null; + var initial = null; + var ignoreClick = true; + var last = null; + + // Disables built-in pan and zoom in IE10 and later + if (mxClient.IS_POINTER) + { + elt.style.touchAction = 'none'; + } + + var getValue = mxUtils.bind(this, function() + { + var result = parseInt(((horizontal) ? elt.style.left : elt.style.bottom)); + + // Takes into account hidden footer + if (!horizontal) + { + result = result + dx - this.footerHeight; + } + + return result; + }); + + function moveHandler(evt) + { + if (start != null) + { + var pt = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + onChange(Math.max(0, initial + ((horizontal) ? (pt.x - start.x) : (start.y - pt.y)) - dx)); + mxEvent.consume(evt); + + if (initial != getValue()) + { + ignoreClick = true; + last = null; + } + } + }; + + function dropHandler(evt) + { + moveHandler(evt); + initial = null; + start = null; + }; + + mxEvent.addGestureListeners(elt, function(evt) + { + start = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + initial = getValue(); + ignoreClick = false; + mxEvent.consume(evt); + }); + + mxEvent.addListener(elt, 'click', mxUtils.bind(this, function(evt) + { + if (!ignoreClick && this.hsplitClickEnabled) + { + var next = (last != null) ? last - dx : 0; + last = getValue(); + onChange(next); + mxEvent.consume(evt); + } + })); + + mxEvent.addGestureListeners(document, null, moveHandler, dropHandler); + + this.destroyFunctions.push(function() + { + mxEvent.removeGestureListeners(document, null, moveHandler, dropHandler); + }); +}; + +/** + * Displays a print dialog. + */ +EditorUi.prototype.showDialog = function(elt, w, h, modal, closable, onClose, noScroll, trasparent) +{ + this.editor.graph.tooltipHandler.hideTooltip(); + + if (this.dialogs == null) + { + this.dialogs = []; + } + + this.dialog = new Dialog(this, elt, w, h, modal, closable, onClose, noScroll, trasparent); + this.dialogs.push(this.dialog); +}; + +/** + * Displays a print dialog. + */ +EditorUi.prototype.hideDialog = function(cancel) +{ + if (this.dialogs != null && this.dialogs.length > 0) + { + var dlg = this.dialogs.pop(); + dlg.close(cancel); + + this.dialog = (this.dialogs.length > 0) ? this.dialogs[this.dialogs.length - 1] : null; + + if (this.dialog == null && this.editor.graph.container.style.visibility != 'hidden') + { + this.editor.graph.container.focus(); + } + + mxUtils.clearSelection(); + this.editor.fireEvent(new mxEventObject('hideDialog')); + } +}; + +/** + * Display a color dialog. + */ +EditorUi.prototype.pickColor = function(color, apply) +{ + var graph = this.editor.graph; + var selState = graph.cellEditor.saveSelection(); + + var dlg = new ColorDialog(this, color || 'none', function(color) + { + graph.cellEditor.restoreSelection(selState); + apply(color); + }, function() + { + graph.cellEditor.restoreSelection(selState); + }); + this.showDialog(dlg.container, 230, 430, true, false); + dlg.init(); +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +EditorUi.prototype.openFile = function() +{ + // Closes dialog after open + window.openFile = new OpenFile(mxUtils.bind(this, function(cancel) + { + this.hideDialog(cancel); + })); + + // Removes openFile if dialog is closed + this.showDialog(new OpenDialog(this).container, (Editor.useLocalStorage) ? 640 : 320, + (Editor.useLocalStorage) ? 480 : 220, true, true, function() + { + window.openFile = null; + }); +}; + +/** + * Extracs the graph model from the given HTML data from a data transfer event. + */ +EditorUi.prototype.extractGraphModelFromHtml = function(data) +{ + var result = null; + + try + { + var idx = data.indexOf('<mxGraphModel '); + + if (idx >= 0) + { + var idx2 = data.lastIndexOf('</mxGraphModel>'); + + if (idx2 > idx) + { + result = data.substring(idx, idx2 + 21).replace(/>/g, '>'). + replace(/</g, '<').replace(/\\"/g, '"').replace(/\n/g, ''); + } + } + } + catch (e) + { + // ignore + } + + return result; +}; + +/** + * Opens the given files in the editor. + */ +EditorUi.prototype.extractGraphModelFromEvent = function(evt) +{ + var result = null; + var data = null; + + if (evt != null) + { + var provider = (evt.dataTransfer != null) ? evt.dataTransfer : evt.clipboardData; + + if (provider != null) + { + if (document.documentMode == 10 || document.documentMode == 11) + { + data = provider.getData('Text'); + } + else + { + data = (mxUtils.indexOf(provider.types, 'text/html') >= 0) ? provider.getData('text/html') : null; + + if (mxUtils.indexOf(provider.types, 'text/plain' && (data == null || data.length == 0))) + { + data = provider.getData('text/plain'); + } + } + + if (data != null) + { + data = this.editor.graph.zapGremlins(mxUtils.trim(data)); + + // Tries parsing as HTML document with embedded XML + var xml = this.extractGraphModelFromHtml(data); + + if (xml != null) + { + data = xml; + } + } + } + } + + if (data != null && this.isCompatibleString(data)) + { + result = data; + } + + return result; +}; + +/** + * Hook for subclassers to return true if event data is a supported format. + * This implementation always returns false. + */ +EditorUi.prototype.isCompatibleString = function(data) +{ + return false; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +EditorUi.prototype.saveFile = function(forceDialog) +{ + if (!forceDialog && this.editor.filename != null) + { + this.save(this.editor.getOrCreateFilename()); + } + else + { + var dlg = new FilenameDialog(this, this.editor.getOrCreateFilename(), mxResources.get('save'), mxUtils.bind(this, function(name) + { + this.save(name); + }), null, mxUtils.bind(this, function(name) + { + if (name != null && name.length > 0) + { + return true; + } + + mxUtils.confirm(mxResources.get('invalidName')); + + return false; + })); + this.showDialog(dlg.container, 300, 100, true, true); + dlg.init(); + } +}; + +/** + * Saves the current graph under the given filename. + */ +EditorUi.prototype.save = function(name) +{ + if (name != null) + { + if (this.editor.graph.isEditing()) + { + this.editor.graph.stopEditing(); + } + + var xml = mxUtils.getXml(this.editor.getGraphXml()); + + try + { + if (Editor.useLocalStorage) + { + if (localStorage.getItem(name) != null && + !mxUtils.confirm(mxResources.get('replaceIt', [name]))) + { + return; + } + + localStorage.setItem(name, xml); + this.editor.setStatus(mxUtils.htmlEntities(mxResources.get('saved')) + ' ' + new Date()); + } + else + { + if (xml.length < MAX_REQUEST_SIZE) + { + new mxXmlRequest(SAVE_URL, 'filename=' + encodeURIComponent(name) + + '&xml=' + encodeURIComponent(xml)).simulate(document, '_blank'); + } + else + { + mxUtils.alert(mxResources.get('drawingTooLarge')); + mxUtils.popup(xml); + + return; + } + } + + this.editor.setModified(false); + this.editor.setFilename(name); + this.updateDocumentTitle(); + } + catch (e) + { + this.editor.setStatus(mxUtils.htmlEntities(mxResources.get('errorSavingFile'))); + } + } +}; + +/** + * Executes the given layout. + */ +EditorUi.prototype.executeLayout = function(exec, animate, post) +{ + var graph = this.editor.graph; + + if (graph.isEnabled()) + { + graph.getModel().beginUpdate(); + try + { + exec(); + } + catch (e) + { + throw e; + } + finally + { + // Animates the changes in the graph model except + // for Camino, where animation is too slow + if (this.allowAnimation && animate && navigator.userAgent.indexOf('Camino') < 0) + { + // New API for animating graph layout results asynchronously + var morph = new mxMorphing(graph); + morph.addListener(mxEvent.DONE, mxUtils.bind(this, function() + { + graph.getModel().endUpdate(); + + if (post != null) + { + post(); + } + })); + + morph.startAnimation(); + } + else + { + graph.getModel().endUpdate(); + + if (post != null) + { + post(); + } + } + } + } +}; + +/** + * Hides the current menu. + */ +EditorUi.prototype.showImageDialog = function(title, value, fn, ignoreExisting) +{ + var cellEditor = this.editor.graph.cellEditor; + var selState = cellEditor.saveSelection(); + var newValue = mxUtils.prompt(title, value); + cellEditor.restoreSelection(selState); + + if (newValue != null && newValue.length > 0) + { + var img = new Image(); + + img.onload = function() + { + fn(newValue, img.width, img.height); + }; + img.onerror = function() + { + fn(null); + mxUtils.alert(mxResources.get('fileNotFound')); + }; + + img.src = newValue; + } + else + { + fn(null); + } +}; + +/** + * Hides the current menu. + */ +EditorUi.prototype.showLinkDialog = function(value, btnLabel, fn) +{ + var dlg = new LinkDialog(this, value, btnLabel, fn); + this.showDialog(dlg.container, 420, 90, true, true); + dlg.init(); +}; + +/** + * Hides the current menu. + */ +EditorUi.prototype.showDataDialog = function(cell) +{ + if (cell != null) + { + var dlg = new EditDataDialog(this, cell); + this.showDialog(dlg.container, 340, 340, true, false, null, false); + dlg.init(); + } +}; + +/** + * Hides the current menu. + */ +EditorUi.prototype.showBackgroundImageDialog = function(apply) +{ + apply = (apply != null) ? apply : mxUtils.bind(this, function(image) + { + var change = new ChangePageSetup(this, null, image); + change.ignoreColor = true; + + this.editor.graph.model.execute(change); + }); + + var newValue = mxUtils.prompt(mxResources.get('backgroundImage'), ''); + + if (newValue != null && newValue.length > 0) + { + var img = new Image(); + + img.onload = function() + { + apply(new mxImage(newValue, img.width, img.height)); + }; + img.onerror = function() + { + apply(null); + mxUtils.alert(mxResources.get('fileNotFound')); + }; + + img.src = newValue; + } + else + { + apply(null); + } +}; + +/** + * Loads the stylesheet for this graph. + */ +EditorUi.prototype.setBackgroundImage = function(image) +{ + this.editor.graph.setBackgroundImage(image); + this.editor.graph.view.validateBackgroundImage(); + + this.fireEvent(new mxEventObject('backgroundImageChanged')); +}; + +/** + * Creates the keyboard event handler for the current graph and history. + */ +EditorUi.prototype.confirm = function(msg, okFn, cancelFn) +{ + if (mxUtils.confirm(msg)) + { + if (okFn != null) + { + okFn(); + } + } + else if (cancelFn != null) + { + cancelFn(); + } +}; + +/** + * Creates the keyboard event handler for the current graph and history. + */ +EditorUi.prototype.createOutline = function(wnd) +{ + var outline = new mxOutline(this.editor.graph); + outline.border = 20; + + mxEvent.addListener(window, 'resize', function() + { + outline.update(); + }); + + this.addListener('pageFormatChanged', function() + { + outline.update(); + }); + + return outline; +}; + +/** + * Creates the keyboard event handler for the current graph and history. + */ +EditorUi.prototype.createKeyHandler = function(editor) +{ + var editorUi = this; + var graph = this.editor.graph; + var keyHandler = new mxKeyHandler(graph); + + var isEventIgnored = keyHandler.isEventIgnored; + keyHandler.isEventIgnored = function(evt) + { + // Handles undo/redo/ctrl+./,/u via action and allows ctrl+b/i only if editing value is HTML (except for FF and Safari) + return (!this.isControlDown(evt) || mxEvent.isShiftDown(evt) || (evt.keyCode != 90 && evt.keyCode != 89 && + evt.keyCode != 188 && evt.keyCode != 190 && evt.keyCode != 85)) && ((evt.keyCode != 66 && evt.keyCode != 73) || + !this.isControlDown(evt) || (this.graph.cellEditor.isContentEditing() && !mxClient.IS_FF && !mxClient.IS_SF)) && + isEventIgnored.apply(this, arguments); + }; + + // Ignores graph enabled state but not chromeless state + keyHandler.isEnabledForEvent = function(evt) + { + return (!mxEvent.isConsumed(evt) && this.isGraphEvent(evt) && this.isEnabled() && + (editorUi.dialogs == null || editorUi.dialogs.length == 0)); + }; + + // Routes command-key to control-key on Mac + keyHandler.isControlDown = function(evt) + { + return mxEvent.isControlDown(evt) || (mxClient.IS_MAC && evt.metaKey); + }; + + var queue = []; + var thread = null; + + // Helper function to move cells with the cursor keys + function nudge(keyCode, stepSize, resize) + { + queue.push(function() + { + if (!graph.isSelectionEmpty() && graph.isEnabled()) + { + stepSize = (stepSize != null) ? stepSize : 1; + + if (resize) + { + // Resizes all selected vertices + graph.getModel().beginUpdate(); + try + { + var cells = graph.getSelectionCells(); + + for (var i = 0; i < cells.length; i++) + { + if (graph.getModel().isVertex(cells[i]) && graph.isCellResizable(cells[i])) + { + var geo = graph.getCellGeometry(cells[i]); + + if (geo != null) + { + geo = geo.clone(); + + if (keyCode == 37) + { + geo.width = Math.max(0, geo.width - stepSize); + } + else if (keyCode == 38) + { + geo.height = Math.max(0, geo.height - stepSize); + } + else if (keyCode == 39) + { + geo.width += stepSize; + } + else if (keyCode == 40) + { + geo.height += stepSize; + } + + graph.getModel().setGeometry(cells[i], geo); + } + } + } + } + finally + { + graph.getModel().endUpdate(); + } + } + else + { + // Moves vertices up/down in a stack layout + var cell = graph.getSelectionCell(); + var parent = graph.model.getParent(cell); + var layout = null; + + if (graph.getSelectionCount() == 1 && graph.model.isVertex(cell) && + graph.layoutManager != null && !graph.isCellLocked(cell)) + { + layout = graph.layoutManager.getLayout(parent); + } + + if (layout != null && layout.constructor == mxStackLayout) + { + var index = parent.getIndex(cell); + + if (keyCode == 37 || keyCode == 38) + { + graph.model.add(parent, cell, Math.max(0, index - 1)); + } + else if (keyCode == 39 ||keyCode == 40) + { + graph.model.add(parent, cell, Math.min(graph.model.getChildCount(parent), index + 1)); + } + } + else + { + var dx = 0; + var dy = 0; + + if (keyCode == 37) + { + dx = -stepSize; + } + else if (keyCode == 38) + { + dy = -stepSize; + } + else if (keyCode == 39) + { + dx = stepSize; + } + else if (keyCode == 40) + { + dy = stepSize; + } + + graph.moveCells(graph.getMovableCells(graph.getSelectionCells()), dx, dy); + } + } + } + }); + + if (thread != null) + { + window.clearTimeout(thread); + } + + thread = window.setTimeout(function() + { + if (queue.length > 0) + { + graph.getModel().beginUpdate(); + try + { + for (var i = 0; i < queue.length; i++) + { + queue[i](); + } + + queue = []; + } + finally + { + graph.getModel().endUpdate(); + } + graph.scrollCellToVisible(graph.getSelectionCell()); + } + }, 200); + }; + + // Overridden to handle special alt+shift+cursor keyboard shortcuts + var directions = {37: mxConstants.DIRECTION_WEST, 38: mxConstants.DIRECTION_NORTH, + 39: mxConstants.DIRECTION_EAST, 40: mxConstants.DIRECTION_SOUTH}; + + var keyHandlerGetFunction = keyHandler.getFunction; + + // Alt+Shift+Keycode mapping to action + var altShiftActions = {67: this.actions.get('clearWaypoints'), // Alt+Shift+C + 65: this.actions.get('connectionArrows'), // Alt+Shift+A + 76: this.actions.get('editLink'), // Alt+Shift+L + 80: this.actions.get('connectionPoints'), // Alt+Shift+P + 84: this.actions.get('editTooltip'), // Alt+Shift+T + 86: this.actions.get('pasteSize'), // Alt+Shift+V + 88: this.actions.get('copySize') // Alt+Shift+X + }; + + mxKeyHandler.prototype.getFunction = function(evt) + { + if (graph.isEnabled()) + { + // TODO: Add alt modified state in core API, here are some specific cases + if (mxEvent.isShiftDown(evt) && mxEvent.isAltDown(evt)) + { + var action = altShiftActions[evt.keyCode]; + + if (action != null) + { + return action.funct; + } + } + + if (evt.keyCode == 9 && mxEvent.isAltDown(evt)) + { + if (mxEvent.isShiftDown(evt)) + { + // Alt+Shift+Tab + return function() + { + graph.selectParentCell(); + }; + } + else + { + // Alt+Tab + return function() + { + graph.selectChildCell(); + }; + } + } + else if (directions[evt.keyCode] != null && !graph.isSelectionEmpty()) + { + if (mxEvent.isShiftDown(evt) && mxEvent.isAltDown(evt)) + { + if (graph.model.isVertex(graph.getSelectionCell())) + { + return function() + { + var cells = graph.connectVertex(graph.getSelectionCell(), directions[evt.keyCode], + graph.defaultEdgeLength, evt, true); + + if (cells != null && cells.length > 0) + { + if (cells.length == 1 && graph.model.isEdge(cells[0])) + { + graph.setSelectionCell(graph.model.getTerminal(cells[0], false)); + } + else + { + graph.setSelectionCell(cells[cells.length - 1]); + } + + graph.scrollCellToVisible(graph.getSelectionCell()); + + if (editorUi.hoverIcons != null) + { + editorUi.hoverIcons.update(graph.view.getState(graph.getSelectionCell())); + } + } + }; + } + } + else + { + // Avoids consuming event if no vertex is selected by returning null below + // Cursor keys move and resize (ctrl) cells + if (this.isControlDown(evt)) + { + return function() + { + nudge(evt.keyCode, (mxEvent.isShiftDown(evt)) ? graph.gridSize : null, true); + }; + } + else + { + return function() + { + nudge(evt.keyCode, (mxEvent.isShiftDown(evt)) ? graph.gridSize : null); + }; + } + } + } + } + + return keyHandlerGetFunction.apply(this, arguments); + }; + + // Binds keystrokes to actions + keyHandler.bindAction = mxUtils.bind(this, function(code, control, key, shift) + { + var action = this.actions.get(key); + + if (action != null) + { + var f = function() + { + if (action.isEnabled()) + { + action.funct(); + } + }; + + if (control) + { + if (shift) + { + keyHandler.bindControlShiftKey(code, f); + } + else + { + keyHandler.bindControlKey(code, f); + } + } + else + { + if (shift) + { + keyHandler.bindShiftKey(code, f); + } + else + { + keyHandler.bindKey(code, f); + } + } + } + }); + + var ui = this; + var keyHandlerEscape = keyHandler.escape; + keyHandler.escape = function(evt) + { + keyHandlerEscape.apply(this, arguments); + }; + + // Ignores enter keystroke. Remove this line if you want the + // enter keystroke to stop editing. N, W, T are reserved. + keyHandler.enter = function() {}; + + keyHandler.bindControlShiftKey(36, function() { graph.exitGroup(); }); // Ctrl+Shift+Home + keyHandler.bindControlShiftKey(35, function() { graph.enterGroup(); }); // Ctrl+Shift+End + keyHandler.bindKey(36, function() { graph.home(); }); // Home + keyHandler.bindKey(35, function() { graph.refresh(); }); // End + keyHandler.bindAction(107, true, 'zoomIn'); // Ctrl+Plus + keyHandler.bindAction(109, true, 'zoomOut'); // Ctrl+Minus + keyHandler.bindAction(80, true, 'print'); // Ctrl+P + keyHandler.bindAction(79, true, 'outline', true); // Ctrl+Shift+O + keyHandler.bindAction(112, false, 'about'); // F1 + + if (!this.editor.chromeless || this.editor.editable) + { + keyHandler.bindControlKey(36, function() { if (graph.isEnabled()) { graph.foldCells(true); }}); // Ctrl+Home + keyHandler.bindControlKey(35, function() { if (graph.isEnabled()) { graph.foldCells(false); }}); // Ctrl+End + keyHandler.bindControlKey(13, function() { if (graph.isEnabled()) { graph.setSelectionCells(graph.duplicateCells(graph.getSelectionCells(), false)); }}); // Ctrl+Enter + keyHandler.bindAction(8, false, 'delete'); // Backspace + keyHandler.bindAction(8, true, 'deleteAll'); // Backspace + keyHandler.bindAction(46, false, 'delete'); // Delete + keyHandler.bindAction(46, true, 'deleteAll'); // Ctrl+Delete + keyHandler.bindAction(72, true, 'resetView'); // Ctrl+H + keyHandler.bindAction(72, true, 'fitWindow', true); // Ctrl+Shift+H + keyHandler.bindAction(74, true, 'fitPage'); // Ctrl+J + keyHandler.bindAction(74, true, 'fitTwoPages', true); // Ctrl+Shift+J + keyHandler.bindAction(48, true, 'customZoom'); // Ctrl+0 + keyHandler.bindAction(82, true, 'turn'); // Ctrl+R + keyHandler.bindAction(82, true, 'clearDefaultStyle', true); // Ctrl+Shift+R + keyHandler.bindAction(83, true, 'save'); // Ctrl+S + keyHandler.bindAction(83, true, 'saveAs', true); // Ctrl+Shift+S + keyHandler.bindAction(65, true, 'selectAll'); // Ctrl+A + keyHandler.bindAction(65, true, 'selectNone', true); // Ctrl+A + keyHandler.bindAction(73, true, 'selectVertices', true); // Ctrl+Shift+I + keyHandler.bindAction(69, true, 'selectEdges', true); // Ctrl+Shift+E + keyHandler.bindAction(69, true, 'editStyle'); // Ctrl+E + keyHandler.bindAction(66, true, 'bold'); // Ctrl+B + keyHandler.bindAction(66, true, 'toBack', true); // Ctrl+Shift+B + keyHandler.bindAction(70, true, 'toFront', true); // Ctrl+Shift+F + keyHandler.bindAction(68, true, 'duplicate'); // Ctrl+D + keyHandler.bindAction(68, true, 'setAsDefaultStyle', true); // Ctrl+Shift+D + keyHandler.bindAction(90, true, 'undo'); // Ctrl+Z + keyHandler.bindAction(89, true, 'autosize', true); // Ctrl+Shift+Y + keyHandler.bindAction(88, true, 'cut'); // Ctrl+X + keyHandler.bindAction(67, true, 'copy'); // Ctrl+C + keyHandler.bindAction(86, true, 'paste'); // Ctrl+V + keyHandler.bindAction(71, true, 'group'); // Ctrl+G + keyHandler.bindAction(77, true, 'editData'); // Ctrl+M + keyHandler.bindAction(71, true, 'grid', true); // Ctrl+Shift+G + keyHandler.bindAction(73, true, 'italic'); // Ctrl+I + keyHandler.bindAction(76, true, 'lockUnlock'); // Ctrl+L + keyHandler.bindAction(76, true, 'layers', true); // Ctrl+Shift+L + keyHandler.bindAction(80, true, 'formatPanel', true); // Ctrl+Shift+P + keyHandler.bindAction(85, true, 'underline'); // Ctrl+U + keyHandler.bindAction(85, true, 'ungroup', true); // Ctrl+Shift+U + keyHandler.bindAction(190, true, 'superscript'); // Ctrl+. + keyHandler.bindAction(188, true, 'subscript'); // Ctrl+, + keyHandler.bindKey(13, function() { if (graph.isEnabled()) { graph.startEditingAtCell(); }}); // Enter + keyHandler.bindKey(113, function() { if (graph.isEnabled()) { graph.startEditingAtCell(); }}); // F2 + } + + if (!mxClient.IS_WIN) + { + keyHandler.bindAction(90, true, 'redo', true); // Ctrl+Shift+Z + } + else + { + keyHandler.bindAction(89, true, 'redo'); // Ctrl+Y + } + + return keyHandler; +}; + +/** + * Creates the keyboard event handler for the current graph and history. + */ +EditorUi.prototype.destroy = function() +{ + if (this.editor != null) + { + this.editor.destroy(); + this.editor = null; + } + + if (this.menubar != null) + { + this.menubar.destroy(); + this.menubar = null; + } + + if (this.toolbar != null) + { + this.toolbar.destroy(); + this.toolbar = null; + } + + if (this.sidebar != null) + { + this.sidebar.destroy(); + this.sidebar = null; + } + + if (this.keyHandler != null) + { + this.keyHandler.destroy(); + this.keyHandler = null; + } + + if (this.keydownHandler != null) + { + mxEvent.removeListener(document, 'keydown', this.keydownHandler); + this.keydownHandler = null; + } + + if (this.keyupHandler != null) + { + mxEvent.removeListener(document, 'keyup', this.keyupHandler); + this.keyupHandler = null; + } + + if (this.resizeHandler != null) + { + mxEvent.removeListener(window, 'resize', this.resizeHandler); + this.resizeHandler = null; + } + + if (this.gestureHandler != null) + { + mxEvent.removeGestureListeners(document, this.gestureHandler); + this.gestureHandler = null; + } + + if (this.orientationChangeHandler != null) + { + mxEvent.removeListener(window, 'orientationchange', this.orientationChangeHandler); + this.orientationChangeHandler = null; + } + + if (this.scrollHandler != null) + { + mxEvent.removeListener(window, 'scroll', this.scrollHandler); + this.scrollHandler = null; + } + + if (this.destroyFunctions != null) + { + for (var i = 0; i < this.destroyFunctions.length; i++) + { + this.destroyFunctions[i](); + } + + this.destroyFunctions = null; + } + + var c = [this.menubarContainer, this.toolbarContainer, this.sidebarContainer, + this.formatContainer, this.diagramContainer, this.footerContainer, + this.chromelessToolbar, this.hsplit, this.sidebarFooterContainer, + this.layersDialog]; + + for (var i = 0; i < c.length; i++) + { + if (c[i] != null && c[i].parentNode != null) + { + c[i].parentNode.removeChild(c[i]); + } + } +}; diff --git a/media/grapheditor/js/Format.js b/media/grapheditor/js/Format.js new file mode 100644 index 0000000000..87eac59531 --- /dev/null +++ b/media/grapheditor/js/Format.js @@ -0,0 +1,5497 @@ +/** + * Copyright (c) 2006-2012, JGraph Ltd + */ +Format = function(editorUi, container) +{ + this.editorUi = editorUi; + this.container = container; +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.labelIndex = 0; + +/** + * Returns information about the current selection. + */ +Format.prototype.currentIndex = 0; + +/** + * Returns information about the current selection. + */ +Format.prototype.showCloseButton = true; + +/** + * Background color for inactive tabs. + */ +Format.prototype.inactiveTabBackgroundColor = '#d7d7d7'; + +/** + * Background color for inactive tabs. + */ +Format.prototype.roundableShapes = ['label', 'rectangle', 'internalStorage', 'corner', + 'parallelogram', 'swimlane', 'triangle', 'trapezoid', + 'ext', 'step', 'tee', 'process', 'link', + 'rhombus', 'offPageConnector', 'loopLimit', 'hexagon', + 'manualInput', 'curlyBracket', 'singleArrow', 'callout', + 'doubleArrow', 'flexArrow', 'card', 'umlLifeline']; + +/** + * Adds the label menu items to the given menu and parent. + */ +Format.prototype.init = function() +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + + this.update = mxUtils.bind(this, function(sender, evt) + { + this.clearSelectionState(); + this.refresh(); + }); + + graph.getSelectionModel().addListener(mxEvent.CHANGE, this.update); + graph.addListener(mxEvent.EDITING_STARTED, this.update); + graph.addListener(mxEvent.EDITING_STOPPED, this.update); + graph.getModel().addListener(mxEvent.CHANGE, this.update); + graph.addListener(mxEvent.ROOT, mxUtils.bind(this, function() + { + this.refresh(); + })); + + editor.addListener('autosaveChanged', mxUtils.bind(this, function() + { + this.refresh(); + })); + + this.refresh(); +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.clearSelectionState = function() +{ + this.selectionState = null; +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.getSelectionState = function() +{ + if (this.selectionState == null) + { + this.selectionState = this.createSelectionState(); + } + + return this.selectionState; +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.createSelectionState = function() +{ + var cells = this.editorUi.editor.graph.getSelectionCells(); + var result = this.initSelectionState(); + + for (var i = 0; i < cells.length; i++) + { + this.updateSelectionStateForCell(result, cells[i], cells); + } + + return result; +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.initSelectionState = function() +{ + return {vertices: [], edges: [], x: null, y: null, width: null, height: null, style: {}, + containsImage: false, containsLabel: false, fill: true, glass: true, rounded: true, + comic: true, autoSize: false, image: true, shadow: true, lineJumps: true}; +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.updateSelectionStateForCell = function(result, cell, cells) +{ + var graph = this.editorUi.editor.graph; + + if (graph.getModel().isVertex(cell)) + { + result.vertices.push(cell); + var geo = graph.getCellGeometry(cell); + + if (geo != null) + { + if (geo.width > 0) + { + if (result.width == null) + { + result.width = geo.width; + } + else if (result.width != geo.width) + { + result.width = ''; + } + } + else + { + result.containsLabel = true; + } + + if (geo.height > 0) + { + if (result.height == null) + { + result.height = geo.height; + } + else if (result.height != geo.height) + { + result.height = ''; + } + } + else + { + result.containsLabel = true; + } + + if (!geo.relative || geo.offset != null) + { + var x = (geo.relative) ? geo.offset.x : geo.x; + var y = (geo.relative) ? geo.offset.y : geo.y; + + if (result.x == null) + { + result.x = x; + } + else if (result.x != x) + { + result.x = ''; + } + + if (result.y == null) + { + result.y = y; + } + else if (result.y != y) + { + result.y = ''; + } + } + } + } + else if (graph.getModel().isEdge(cell)) + { + result.edges.push(cell); + } + + var state = graph.view.getState(cell); + + if (state != null) + { + result.autoSize = result.autoSize || this.isAutoSizeState(state); + result.glass = result.glass && this.isGlassState(state); + result.rounded = result.rounded && this.isRoundedState(state); + result.lineJumps = result.lineJumps && this.isLineJumpState(state); + result.comic = result.comic && this.isComicState(state); + result.image = result.image && this.isImageState(state); + result.shadow = result.shadow && this.isShadowState(state); + result.fill = result.fill && this.isFillState(state); + + var shape = mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null); + result.containsImage = result.containsImage || shape == 'image'; + + for (var key in state.style) + { + var value = state.style[key]; + + if (value != null) + { + if (result.style[key] == null) + { + result.style[key] = value; + } + else if (result.style[key] != value) + { + result.style[key] = ''; + } + } + } + } +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.isFillState = function(state) +{ + return state.view.graph.model.isVertex(state.cell) || + mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null) == 'arrow' || + mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null) == 'filledEdge' || + mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null) == 'flexArrow'; +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.isGlassState = function(state) +{ + var shape = mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null); + + return (shape == 'label' || shape == 'rectangle' || shape == 'internalStorage' || + shape == 'ext' || shape == 'umlLifeline' || shape == 'swimlane' || + shape == 'process'); +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.isRoundedState = function(state) +{ + return (state.shape != null) ? state.shape.isRoundable() : + mxUtils.indexOf(this.roundableShapes, mxUtils.getValue(state.style, + mxConstants.STYLE_SHAPE, null)) >= 0; +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.isLineJumpState = function(state) +{ + var shape = mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null); + var curved = mxUtils.getValue(state.style, mxConstants.STYLE_CURVED, false); + + return !curved && (shape == 'connector' || shape == 'filledEdge'); +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.isComicState = function(state) +{ + var shape = mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null); + + return mxUtils.indexOf(['label', 'rectangle', 'internalStorage', 'corner', 'parallelogram', 'note', 'collate', + 'swimlane', 'triangle', 'trapezoid', 'ext', 'step', 'tee', 'process', 'link', 'rhombus', + 'offPageConnector', 'loopLimit', 'hexagon', 'manualInput', 'singleArrow', 'doubleArrow', + 'flexArrow', 'filledEdge', 'card', 'umlLifeline', 'connector', 'folder', 'component', 'sortShape', + 'cross', 'umlFrame', 'cube', 'isoCube', 'isoRectangle', 'partialRectangle'], shape) >= 0; +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.isAutoSizeState = function(state) +{ + return mxUtils.getValue(state.style, mxConstants.STYLE_AUTOSIZE, null) == '1'; +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.isImageState = function(state) +{ + var shape = mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null); + + return (shape == 'label' || shape == 'image'); +}; + +/** + * Returns information about the current selection. + */ +Format.prototype.isShadowState = function(state) +{ + var shape = mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null); + + return (shape != 'image'); +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +Format.prototype.clear = function() +{ + this.container.innerHTML = ''; + + // Destroy existing panels + if (this.panels != null) + { + for (var i = 0; i < this.panels.length; i++) + { + this.panels[i].destroy(); + } + } + + this.panels = []; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +Format.prototype.refresh = function() +{ + // Performance tweak: No refresh needed if not visible + if (this.container.style.width == '0px') + { + return; + } + + this.clear(); + var ui = this.editorUi; + var graph = ui.editor.graph; + + var div = document.createElement('div'); + div.style.whiteSpace = 'nowrap'; + div.style.color = 'rgb(112, 112, 112)'; + div.style.textAlign = 'left'; + div.style.cursor = 'default'; + + var label = document.createElement('div'); + label.style.border = '1px solid #c0c0c0'; + label.style.borderWidth = '0px 0px 1px 0px'; + label.style.textAlign = 'center'; + label.style.fontWeight = 'bold'; + label.style.overflow = 'hidden'; + label.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; + label.style.paddingTop = '8px'; + label.style.height = (mxClient.IS_QUIRKS) ? '34px' : '25px'; + label.style.width = '100%'; + this.container.appendChild(div); + + if (graph.isSelectionEmpty()) + { + mxUtils.write(label, mxResources.get('diagram')); + + // Adds button to hide the format panel since + // people don't seem to find the toolbar button + // and the menu item in the format menu + if (this.showCloseButton) + { + var img = document.createElement('img'); + img.setAttribute('border', '0'); + img.setAttribute('src', Dialog.prototype.closeImage); + img.setAttribute('title', mxResources.get('hide')); + img.style.position = 'absolute'; + img.style.display = 'block'; + img.style.right = '0px'; + img.style.top = '8px'; + img.style.cursor = 'pointer'; + img.style.marginTop = '1px'; + img.style.marginRight = '17px'; + img.style.border = '1px solid transparent'; + img.style.padding = '1px'; + img.style.opacity = 0.5; + label.appendChild(img) + + mxEvent.addListener(img, 'click', function() + { + ui.actions.get('formatPanel').funct(); + }); + } + + div.appendChild(label); + this.panels.push(new DiagramFormatPanel(this, ui, div)); + } + else if (graph.isEditing()) + { + mxUtils.write(label, mxResources.get('text')); + div.appendChild(label); + this.panels.push(new TextFormatPanel(this, ui, div)); + } + else + { + var containsLabel = this.getSelectionState().containsLabel; + var currentLabel = null; + var currentPanel = null; + + var addClickHandler = mxUtils.bind(this, function(elt, panel, index) + { + var clickHandler = mxUtils.bind(this, function(evt) + { + if (currentLabel != elt) + { + if (containsLabel) + { + this.labelIndex = index; + } + else + { + this.currentIndex = index; + } + + if (currentLabel != null) + { + currentLabel.style.backgroundColor = this.inactiveTabBackgroundColor; + currentLabel.style.borderBottomWidth = '1px'; + } + + currentLabel = elt; + currentLabel.style.backgroundColor = ''; + currentLabel.style.borderBottomWidth = '0px'; + + if (currentPanel != panel) + { + if (currentPanel != null) + { + currentPanel.style.display = 'none'; + } + + currentPanel = panel; + currentPanel.style.display = ''; + } + } + }); + + mxEvent.addListener(elt, 'click', clickHandler); + + if (index == ((containsLabel) ? this.labelIndex : this.currentIndex)) + { + // Invokes handler directly as a workaround for no click on DIV in KHTML. + clickHandler(); + } + }); + + var idx = 0; + + label.style.backgroundColor = this.inactiveTabBackgroundColor; + label.style.borderLeftWidth = '1px'; + label.style.width = (containsLabel) ? '50%' : '33.3%'; + label.style.width = (containsLabel) ? '50%' : '33.3%'; + var label2 = label.cloneNode(false); + var label3 = label2.cloneNode(false); + + // Workaround for ignored background in IE + label2.style.backgroundColor = this.inactiveTabBackgroundColor; + label3.style.backgroundColor = this.inactiveTabBackgroundColor; + + // Style + if (containsLabel) + { + label2.style.borderLeftWidth = '0px'; + } + else + { + label.style.borderLeftWidth = '0px'; + mxUtils.write(label, mxResources.get('style')); + div.appendChild(label); + + var stylePanel = div.cloneNode(false); + stylePanel.style.display = 'none'; + this.panels.push(new StyleFormatPanel(this, ui, stylePanel)); + this.container.appendChild(stylePanel); + + addClickHandler(label, stylePanel, idx++); + } + + // Text + mxUtils.write(label2, mxResources.get('text')); + div.appendChild(label2); + + var textPanel = div.cloneNode(false); + textPanel.style.display = 'none'; + this.panels.push(new TextFormatPanel(this, ui, textPanel)); + this.container.appendChild(textPanel); + + // Arrange + mxUtils.write(label3, mxResources.get('arrange')); + div.appendChild(label3); + + var arrangePanel = div.cloneNode(false); + arrangePanel.style.display = 'none'; + this.panels.push(new ArrangePanel(this, ui, arrangePanel)); + this.container.appendChild(arrangePanel); + + addClickHandler(label2, textPanel, idx++); + addClickHandler(label3, arrangePanel, idx++); + } +}; + +/** + * Base class for format panels. + */ +BaseFormatPanel = function(format, editorUi, container) +{ + this.format = format; + this.editorUi = editorUi; + this.container = container; + this.listeners = []; +}; + +/** + * + */ +BaseFormatPanel.prototype.buttonBackgroundColor = 'white'; + +/** + * Adds the given color option. + */ +BaseFormatPanel.prototype.getSelectionState = function() +{ + var graph = this.editorUi.editor.graph; + var cells = graph.getSelectionCells(); + var shape = null; + + for (var i = 0; i < cells.length; i++) + { + var state = graph.view.getState(cells[i]); + + if (state != null) + { + var tmp = mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null); + + if (tmp != null) + { + if (shape == null) + { + shape = tmp; + } + else if (shape != tmp) + { + return null; + } + } + + } + } + + return shape; +}; + +/** + * Install input handler. + */ +BaseFormatPanel.prototype.installInputHandler = function(input, key, defaultValue, min, max, unit, textEditFallback, isFloat) +{ + unit = (unit != null) ? unit : ''; + isFloat = (isFloat != null) ? isFloat : false; + + var ui = this.editorUi; + var graph = ui.editor.graph; + + min = (min != null) ? min : 1; + max = (max != null) ? max : 999; + + var selState = null; + var updating = false; + + var update = mxUtils.bind(this, function(evt) + { + var value = (isFloat) ? parseFloat(input.value) : parseInt(input.value); + + // Special case: angle mod 360 + if (!isNaN(value) && key == mxConstants.STYLE_ROTATION) + { + // Workaround for decimal rounding errors in floats is to + // use integer and round all numbers to two decimal point + value = mxUtils.mod(Math.round(value * 100), 36000) / 100; + } + + value = Math.min(max, Math.max(min, (isNaN(value)) ? defaultValue : value)); + + if (graph.cellEditor.isContentEditing() && textEditFallback) + { + if (!updating) + { + updating = true; + + if (selState != null) + { + graph.cellEditor.restoreSelection(selState); + selState = null; + } + + textEditFallback(value); + input.value = value + unit; + + // Restore focus and selection in input + updating = false; + } + } + else if (value != mxUtils.getValue(this.format.getSelectionState().style, key, defaultValue)) + { + if (graph.isEditing()) + { + graph.stopEditing(true); + } + + graph.getModel().beginUpdate(); + try + { + graph.setCellStyles(key, value, graph.getSelectionCells()); + + // Handles special case for fontSize where HTML labels are parsed and updated + if (key == mxConstants.STYLE_FONTSIZE) + { + graph.updateLabelElements(graph.getSelectionCells(), function(elt) + { + elt.style.fontSize = value + 'px'; + elt.removeAttribute('size'); + }); + } + + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [key], + 'values', [value], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + } + + input.value = value + unit; + mxEvent.consume(evt); + }); + + if (textEditFallback && graph.cellEditor.isContentEditing()) + { + // KNOWN: Arrow up/down clear selection text in quirks/IE 8 + // Text size via arrow button limits to 16 in IE11. Why? + mxEvent.addListener(input, 'mousedown', function() + { + if (document.activeElement == graph.cellEditor.textarea) + { + selState = graph.cellEditor.saveSelection(); + } + }); + + mxEvent.addListener(input, 'touchstart', function() + { + if (document.activeElement == graph.cellEditor.textarea) + { + selState = graph.cellEditor.saveSelection(); + } + }); + } + + mxEvent.addListener(input, 'change', update); + mxEvent.addListener(input, 'blur', update); + + return update; +}; + +/** + * Adds the given option. + */ +BaseFormatPanel.prototype.createPanel = function() +{ + var div = document.createElement('div'); + div.style.padding = '12px 0px 12px 18px'; + div.style.borderBottom = '1px solid #c0c0c0'; + + return div; +}; + +/** + * Adds the given option. + */ +BaseFormatPanel.prototype.createTitle = function(title) +{ + var div = document.createElement('div'); + div.style.padding = '0px 0px 6px 0px'; + div.style.whiteSpace = 'nowrap'; + div.style.overflow = 'hidden'; + div.style.width = '200px'; + div.style.fontWeight = 'bold'; + mxUtils.write(div, title); + + return div; +}; + +/** + * + */ +BaseFormatPanel.prototype.createStepper = function(input, update, step, height, disableFocus, defaultValue) +{ + step = (step != null) ? step : 1; + height = (height != null) ? height : 8; + + if (mxClient.IS_QUIRKS) + { + height = height - 2; + } + else if (mxClient.IS_MT || document.documentMode >= 8) + { + height = height + 1; + } + + var stepper = document.createElement('div'); + mxUtils.setPrefixedStyle(stepper.style, 'borderRadius', '3px'); + stepper.style.border = '1px solid rgb(192, 192, 192)'; + stepper.style.position = 'absolute'; + + var up = document.createElement('div'); + up.style.borderBottom = '1px solid rgb(192, 192, 192)'; + up.style.position = 'relative'; + up.style.height = height + 'px'; + up.style.width = '10px'; + up.className = 'geBtnUp'; + stepper.appendChild(up); + + var down = up.cloneNode(false); + down.style.border = 'none'; + down.style.height = height + 'px'; + down.className = 'geBtnDown'; + stepper.appendChild(down); + + mxEvent.addListener(down, 'click', function(evt) + { + if (input.value == '') + { + input.value = defaultValue || '2'; + } + + var val = parseInt(input.value); + + if (!isNaN(val)) + { + input.value = val - step; + + if (update != null) + { + update(evt); + } + } + + mxEvent.consume(evt); + }); + + mxEvent.addListener(up, 'click', function(evt) + { + if (input.value == '') + { + input.value = defaultValue || '0'; + } + + var val = parseInt(input.value); + + if (!isNaN(val)) + { + input.value = val + step; + + if (update != null) + { + update(evt); + } + } + + mxEvent.consume(evt); + }); + + // Disables transfer of focus to DIV but also :active CSS + // so it's only used for fontSize where the focus should + // stay on the selected text, but not for any other input. + if (disableFocus) + { + var currentSelection = null; + + mxEvent.addGestureListeners(stepper, + function(evt) + { + // Workaround for lost current selection in page because of focus in IE + if (mxClient.IS_QUIRKS || document.documentMode == 8) + { + currentSelection = document.selection.createRange(); + } + + mxEvent.consume(evt); + }, + null, + function(evt) + { + // Workaround for lost current selection in page because of focus in IE + if (currentSelection != null) + { + try + { + currentSelection.select(); + } + catch (e) + { + // ignore + } + + currentSelection = null; + mxEvent.consume(evt); + } + } + ); + } + + return stepper; +}; + +/** + * Adds the given option. + */ +BaseFormatPanel.prototype.createOption = function(label, isCheckedFn, setCheckedFn, listener) +{ + var div = document.createElement('div'); + div.style.padding = '6px 0px 1px 0px'; + div.style.whiteSpace = 'nowrap'; + div.style.overflow = 'hidden'; + div.style.width = '200px'; + div.style.height = (mxClient.IS_QUIRKS) ? '27px' : '18px'; + + var cb = document.createElement('input'); + cb.setAttribute('type', 'checkbox'); + cb.style.margin = '0px 6px 0px 0px'; + div.appendChild(cb); + + var span = document.createElement('span'); + mxUtils.write(span, label); + div.appendChild(span); + + var applying = false; + var value = isCheckedFn(); + + var apply = function(newValue) + { + if (!applying) + { + applying = true; + + if (newValue) + { + cb.setAttribute('checked', 'checked'); + cb.defaultChecked = true; + cb.checked = true; + } + else + { + cb.removeAttribute('checked'); + cb.defaultChecked = false; + cb.checked = false; + } + + if (value != newValue) + { + value = newValue; + + // Checks if the color value needs to be updated in the model + if (isCheckedFn() != value) + { + setCheckedFn(value); + } + } + + applying = false; + } + }; + + mxEvent.addListener(div, 'click', function(evt) + { + if (cb.getAttribute('disabled') != 'disabled') + { + // Toggles checkbox state for click on label + var source = mxEvent.getSource(evt); + + if (source == div || source == span) + { + cb.checked = !cb.checked; + } + + apply(cb.checked); + } + }); + + apply(value); + + if (listener != null) + { + listener.install(apply); + this.listeners.push(listener); + } + + return div; +}; + +/** + * The string 'null' means use null in values. + */ +BaseFormatPanel.prototype.createCellOption = function(label, key, defaultValue, enabledValue, disabledValue, fn, action, stopEditing) +{ + enabledValue = (enabledValue != null) ? ((enabledValue == 'null') ? null : enabledValue) : '1'; + disabledValue = (disabledValue != null) ? ((disabledValue == 'null') ? null : disabledValue) : '0'; + + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + + return this.createOption(label, function() + { + // Seems to be null sometimes, not sure why... + var state = graph.view.getState(graph.getSelectionCell()); + + if (state != null) + { + return mxUtils.getValue(state.style, key, defaultValue) != disabledValue; + } + + return null; + }, function(checked) + { + if (stopEditing) + { + graph.stopEditing(); + } + + if (action != null) + { + action.funct(); + } + else + { + graph.getModel().beginUpdate(); + try + { + var value = (checked) ? enabledValue : disabledValue; + graph.setCellStyles(key, value, graph.getSelectionCells()); + + if (fn != null) + { + fn(graph.getSelectionCells(), value); + } + + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [key], + 'values', [value], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + } + }, + { + install: function(apply) + { + this.listener = function() + { + // Seems to be null sometimes, not sure why... + var state = graph.view.getState(graph.getSelectionCell()); + + if (state != null) + { + apply(mxUtils.getValue(state.style, key, defaultValue) != disabledValue); + } + }; + + graph.getModel().addListener(mxEvent.CHANGE, this.listener); + }, + destroy: function() + { + graph.getModel().removeListener(this.listener); + } + }); +}; + +/** + * Adds the given color option. + */ +BaseFormatPanel.prototype.createColorOption = function(label, getColorFn, setColorFn, defaultColor, listener, callbackFn, hideCheckbox) +{ + var div = document.createElement('div'); + div.style.padding = '6px 0px 1px 0px'; + div.style.whiteSpace = 'nowrap'; + div.style.overflow = 'hidden'; + div.style.width = '200px'; + div.style.height = (mxClient.IS_QUIRKS) ? '27px' : '18px'; + + var cb = document.createElement('input'); + cb.setAttribute('type', 'checkbox'); + cb.style.margin = '0px 6px 0px 0px'; + + if (!hideCheckbox) + { + div.appendChild(cb); + } + + var span = document.createElement('span'); + mxUtils.write(span, label); + div.appendChild(span); + + var applying = false; + var value = getColorFn(); + + var btn = null; + + var apply = function(color, disableUpdate, forceUpdate) + { + if (!applying) + { + applying = true; + btn.innerHTML = '
'; + + // Fine-tuning in Firefox, quirks mode and IE8 standards + if (mxClient.IS_QUIRKS || document.documentMode == 8) + { + btn.firstChild.style.margin = '0px'; + } + + if (color != null && color != mxConstants.NONE) + { + cb.setAttribute('checked', 'checked'); + cb.defaultChecked = true; + cb.checked = true; + } + else + { + cb.removeAttribute('checked'); + cb.defaultChecked = false; + cb.checked = false; + } + + btn.style.display = (cb.checked || hideCheckbox) ? '' : 'none'; + + if (callbackFn != null) + { + callbackFn(color); + } + + if (!disableUpdate) + { + value = color; + + // Checks if the color value needs to be updated in the model + if (forceUpdate || hideCheckbox || getColorFn() != value) + { + setColorFn(value); + } + } + + applying = false; + } + }; + + btn = mxUtils.button('', mxUtils.bind(this, function(evt) + { + this.editorUi.pickColor(value, function(color) + { + apply(color, null, true); + }); + mxEvent.consume(evt); + })); + + btn.style.position = 'absolute'; + btn.style.marginTop = '-4px'; + btn.style.right = (mxClient.IS_QUIRKS) ? '0px' : '20px'; + btn.style.height = '22px'; + btn.className = 'geColorBtn'; + btn.style.display = (cb.checked || hideCheckbox) ? '' : 'none'; + div.appendChild(btn); + + mxEvent.addListener(div, 'click', function(evt) + { + var source = mxEvent.getSource(evt); + + if (source == cb || source.nodeName != 'INPUT') + { + // Toggles checkbox state for click on label + if (source != cb) + { + cb.checked = !cb.checked; + } + + // Overrides default value with current value to make it easier + // to restore previous value if the checkbox is clicked twice + if (!cb.checked && value != null && value != mxConstants.NONE && + defaultColor != mxConstants.NONE) + { + defaultColor = value; + } + + apply((cb.checked) ? defaultColor : mxConstants.NONE); + } + }); + + apply(value, true); + + if (listener != null) + { + listener.install(apply); + this.listeners.push(listener); + } + + return div; +}; + +/** + * + */ +BaseFormatPanel.prototype.createCellColorOption = function(label, colorKey, defaultColor, callbackFn, setStyleFn) +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + + return this.createColorOption(label, function() + { + // Seems to be null sometimes, not sure why... + var state = graph.view.getState(graph.getSelectionCell()); + + if (state != null) + { + return mxUtils.getValue(state.style, colorKey, null); + } + + return null; + }, function(color) + { + graph.getModel().beginUpdate(); + try + { + if (setStyleFn != null) + { + setStyleFn(color); + } + + graph.setCellStyles(colorKey, color, graph.getSelectionCells()); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [colorKey], + 'values', [color], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + }, defaultColor || mxConstants.NONE, + { + install: function(apply) + { + this.listener = function() + { + // Seems to be null sometimes, not sure why... + var state = graph.view.getState(graph.getSelectionCell()); + + if (state != null) + { + apply(mxUtils.getValue(state.style, colorKey, null)); + } + }; + + graph.getModel().addListener(mxEvent.CHANGE, this.listener); + }, + destroy: function() + { + graph.getModel().removeListener(this.listener); + } + }, callbackFn); +}; + +/** + * + */ +BaseFormatPanel.prototype.addArrow = function(elt, height) +{ + height = (height != null) ? height : 10; + + var arrow = document.createElement('div'); + arrow.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; + arrow.style.padding = '6px'; + arrow.style.paddingRight = '4px'; + + var m = (10 - height); + + if (m == 2) + { + arrow.style.paddingTop = 6 + 'px'; + } + else if (m > 0) + { + arrow.style.paddingTop = (6 - m) + 'px'; + } + else + { + arrow.style.marginTop = '-2px'; + } + + arrow.style.height = height + 'px'; + arrow.style.borderLeft = '1px solid #a0a0a0'; + arrow.innerHTML = ''; + mxUtils.setOpacity(arrow, 70); + + var symbol = elt.getElementsByTagName('div')[0]; + + if (symbol != null) + { + symbol.style.paddingRight = '6px'; + symbol.style.marginLeft = '4px'; + symbol.style.marginTop = '-1px'; + symbol.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; + mxUtils.setOpacity(symbol, 60); + } + + mxUtils.setOpacity(elt, 100); + elt.style.border = '1px solid #a0a0a0'; + elt.style.backgroundColor = this.buttonBackgroundColor; + elt.style.backgroundImage = 'none'; + elt.style.width = 'auto'; + elt.className += ' geColorBtn'; + mxUtils.setPrefixedStyle(elt.style, 'borderRadius', '3px'); + + elt.appendChild(arrow); + + return symbol; +}; + +/** + * + */ +BaseFormatPanel.prototype.addUnitInput = function(container, unit, right, width, update, step, marginTop, disableFocus) +{ + marginTop = (marginTop != null) ? marginTop : 0; + + var input = document.createElement('input'); + input.style.position = 'absolute'; + input.style.textAlign = 'right'; + input.style.marginTop = '-2px'; + input.style.right = (right + 12) + 'px'; + input.style.width = width + 'px'; + container.appendChild(input); + + var stepper = this.createStepper(input, update, step, null, disableFocus); + stepper.style.marginTop = (marginTop - 2) + 'px'; + stepper.style.right = right + 'px'; + container.appendChild(stepper); + + return input; +}; + +/** + * + */ +BaseFormatPanel.prototype.createRelativeOption = function(label, key, width, handler, init) +{ + width = (width != null) ? width : 44; + + var graph = this.editorUi.editor.graph; + var div = this.createPanel(); + div.style.paddingTop = '10px'; + div.style.paddingBottom = '10px'; + mxUtils.write(div, label); + div.style.fontWeight = 'bold'; + + var update = mxUtils.bind(this, function(evt) + { + if (handler != null) + { + handler(input); + } + else + { + var value = parseInt(input.value); + value = Math.min(100, Math.max(0, (isNaN(value)) ? 100 : value)); + var state = graph.view.getState(graph.getSelectionCell()); + + if (state != null && value != mxUtils.getValue(state.style, key, 100)) + { + // Removes entry in style (assumes 100 is default for relative values) + if (value == 100) + { + value = null; + } + + graph.setCellStyles(key, value, graph.getSelectionCells()); + this.editorUi.fireEvent(new mxEventObject('styleChanged', 'keys', [key], + 'values', [value], 'cells', graph.getSelectionCells())); + } + + input.value = ((value != null) ? value : '100') + ' %'; + } + + mxEvent.consume(evt); + }); + + var input = this.addUnitInput(div, '%', 20, width, update, 10, -15, handler != null); + + if (key != null) + { + var listener = mxUtils.bind(this, function(sender, evt, force) + { + if (force || input != document.activeElement) + { + var ss = this.format.getSelectionState(); + var tmp = parseInt(mxUtils.getValue(ss.style, key, 100)); + input.value = (isNaN(tmp)) ? '' : tmp + ' %'; + } + }); + + mxEvent.addListener(input, 'keydown', function(e) + { + if (e.keyCode == 13) + { + graph.container.focus(); + mxEvent.consume(e); + } + else if (e.keyCode == 27) + { + listener(null, null, true); + graph.container.focus(); + mxEvent.consume(e); + } + }); + + graph.getModel().addListener(mxEvent.CHANGE, listener); + this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }}); + listener(); + } + + mxEvent.addListener(input, 'blur', update); + mxEvent.addListener(input, 'change', update); + + if (init != null) + { + init(input); + } + + return div; +}; + +/** + * + */ +BaseFormatPanel.prototype.addLabel = function(div, title, right, width) +{ + width = (width != null) ? width : 61; + + var label = document.createElement('div'); + mxUtils.write(label, title); + label.style.position = 'absolute'; + label.style.right = right + 'px'; + label.style.width = width + 'px'; + label.style.marginTop = '6px'; + label.style.textAlign = 'center'; + div.appendChild(label); +}; + +/** + * + */ +BaseFormatPanel.prototype.addKeyHandler = function(input, listener) +{ + mxEvent.addListener(input, 'keydown', mxUtils.bind(this, function(e) + { + if (e.keyCode == 13) + { + this.editorUi.editor.graph.container.focus(); + mxEvent.consume(e); + } + else if (e.keyCode == 27) + { + if (listener != null) + { + listener(null, null, true); + } + + this.editorUi.editor.graph.container.focus(); + mxEvent.consume(e); + } + })); +}; + +/** + * + */ +BaseFormatPanel.prototype.styleButtons = function(elts) +{ + for (var i = 0; i < elts.length; i++) + { + mxUtils.setPrefixedStyle(elts[i].style, 'borderRadius', '3px'); + mxUtils.setOpacity(elts[i], 100); + elts[i].style.border = '1px solid #a0a0a0'; + elts[i].style.padding = '4px'; + elts[i].style.paddingTop = '3px'; + elts[i].style.paddingRight = '1px'; + elts[i].style.margin = '1px'; + elts[i].style.width = '24px'; + elts[i].style.height = '20px'; + elts[i].className += ' geColorBtn'; + } +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +BaseFormatPanel.prototype.destroy = function() +{ + if (this.listeners != null) + { + for (var i = 0; i < this.listeners.length; i++) + { + this.listeners[i].destroy(); + } + + this.listeners = null; + } +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +ArrangePanel = function(format, editorUi, container) +{ + BaseFormatPanel.call(this, format, editorUi, container); + this.init(); +}; + +mxUtils.extend(ArrangePanel, BaseFormatPanel); + +/** + * Adds the label menu items to the given menu and parent. + */ +ArrangePanel.prototype.init = function() +{ + var graph = this.editorUi.editor.graph; + var ss = this.format.getSelectionState(); + + this.container.appendChild(this.addLayerOps(this.createPanel())); + // Special case that adds two panels + this.addGeometry(this.container); + this.addEdgeGeometry(this.container); + + if (!ss.containsLabel || ss.edges.length == 0) + { + this.container.appendChild(this.addAngle(this.createPanel())); + } + + if (!ss.containsLabel && ss.edges.length == 0) + { + this.container.appendChild(this.addFlip(this.createPanel())); + } + + if (ss.vertices.length > 1) + { + this.container.appendChild(this.addAlign(this.createPanel())); + this.container.appendChild(this.addDistribute(this.createPanel())); + } + + this.container.appendChild(this.addGroupOps(this.createPanel())); +}; + +/** + * + */ +ArrangePanel.prototype.addLayerOps = function(div) +{ + var ui = this.editorUi; + + var btn = mxUtils.button(mxResources.get('toFront'), function(evt) + { + ui.actions.get('toFront').funct(); + }) + + btn.setAttribute('title', mxResources.get('toFront') + ' (' + this.editorUi.actions.get('toFront').shortcut + ')'); + btn.style.width = '100px'; + btn.style.marginRight = '2px'; + div.appendChild(btn); + + var btn = mxUtils.button(mxResources.get('toBack'), function(evt) + { + ui.actions.get('toBack').funct(); + }) + + btn.setAttribute('title', mxResources.get('toBack') + ' (' + this.editorUi.actions.get('toBack').shortcut + ')'); + btn.style.width = '100px'; + div.appendChild(btn); + + return div; +}; + +/** + * + */ +ArrangePanel.prototype.addGroupOps = function(div) +{ + var ui = this.editorUi; + var graph = ui.editor.graph; + var cell = graph.getSelectionCell(); + var ss = this.format.getSelectionState(); + var count = 0; + var btn = null; + + div.style.paddingTop = '8px'; + div.style.paddingBottom = '6px'; + + if (graph.getSelectionCount() > 1) + { + btn = mxUtils.button(mxResources.get('group'), function(evt) + { + ui.actions.get('group').funct(); + }) + + btn.setAttribute('title', mxResources.get('group') + ' (' + this.editorUi.actions.get('group').shortcut + ')'); + btn.style.width = '202px'; + btn.style.marginBottom = '2px'; + div.appendChild(btn); + count++; + } + else if (graph.getSelectionCount() == 1 && !graph.getModel().isEdge(cell) && !graph.isSwimlane(cell) && + graph.getModel().getChildCount(cell) > 0) + { + btn = mxUtils.button(mxResources.get('ungroup'), function(evt) + { + ui.actions.get('ungroup').funct(); + }) + + btn.setAttribute('title', mxResources.get('ungroup') + ' (' + + this.editorUi.actions.get('ungroup').shortcut + ')'); + btn.style.width = '202px'; + btn.style.marginBottom = '2px'; + div.appendChild(btn); + count++; + } + + if (ss.vertices.length > 0) + { + if (count > 0) + { + mxUtils.br(div); + count = 0; + } + + var btn = mxUtils.button(mxResources.get('copySize'), function(evt) + { + ui.actions.get('copySize').funct(); + }); + + btn.setAttribute('title', mxResources.get('copySize') + ' (' + + this.editorUi.actions.get('copySize').shortcut + ')'); + btn.style.width = '202px'; + btn.style.marginBottom = '2px'; + + div.appendChild(btn); + count++; + + if (ui.copiedSize != null) + { + var btn2 = mxUtils.button(mxResources.get('pasteSize'), function(evt) + { + ui.actions.get('pasteSize').funct(); + }); + + btn2.setAttribute('title', mxResources.get('pasteSize') + ' (' + + this.editorUi.actions.get('pasteSize').shortcut + ')'); + + div.appendChild(btn2); + count++; + + btn.style.width = '100px'; + btn.style.marginBottom = '2px'; + btn2.style.width = '100px'; + btn2.style.marginBottom = '2px'; + } + } + + if (graph.getSelectionCount() == 1 && graph.getModel().isVertex(cell) && + graph.getModel().isVertex(graph.getModel().getParent(cell))) + { + if (count > 0) + { + mxUtils.br(div); + } + + btn = mxUtils.button(mxResources.get('removeFromGroup'), function(evt) + { + ui.actions.get('removeFromGroup').funct(); + }) + + btn.setAttribute('title', mxResources.get('removeFromGroup')); + btn.style.width = '202px'; + btn.style.marginBottom = '2px'; + div.appendChild(btn); + count++; + } + else if (graph.getSelectionCount() > 0) + { + if (count > 0) + { + mxUtils.br(div); + } + + btn = mxUtils.button(mxResources.get('clearWaypoints'), mxUtils.bind(this, function(evt) + { + this.editorUi.actions.get('clearWaypoints').funct(); + })); + + btn.setAttribute('title', mxResources.get('clearWaypoints') + ' (' + this.editorUi.actions.get('clearWaypoints').shortcut + ')'); + btn.style.width = '202px'; + btn.style.marginBottom = '2px'; + div.appendChild(btn); + + count++; + } + + if (graph.getSelectionCount() == 1) + { + if (count > 0) + { + mxUtils.br(div); + } + + btn = mxUtils.button(mxResources.get('editData'), mxUtils.bind(this, function(evt) + { + this.editorUi.actions.get('editData').funct(); + })); + + btn.setAttribute('title', mxResources.get('editData') + ' (' + this.editorUi.actions.get('editData').shortcut + ')'); + btn.style.width = '100px'; + btn.style.marginBottom = '2px'; + div.appendChild(btn); + count++; + + btn = mxUtils.button(mxResources.get('editLink'), mxUtils.bind(this, function(evt) + { + this.editorUi.actions.get('editLink').funct(); + })); + + btn.setAttribute('title', mxResources.get('editLink')); + btn.style.width = '100px'; + btn.style.marginLeft = '2px'; + btn.style.marginBottom = '2px'; + div.appendChild(btn); + count++; + } + + if (count == 0) + { + div.style.display = 'none'; + } + + return div; +}; + +/** + * + */ +ArrangePanel.prototype.addAlign = function(div) +{ + var graph = this.editorUi.editor.graph; + div.style.paddingTop = '6px'; + div.style.paddingBottom = '12px'; + div.appendChild(this.createTitle(mxResources.get('align'))); + + var stylePanel = document.createElement('div'); + stylePanel.style.position = 'relative'; + stylePanel.style.paddingLeft = '0px'; + stylePanel.style.borderWidth = '0px'; + stylePanel.className = 'geToolbarContainer'; + + if (mxClient.IS_QUIRKS) + { + div.style.height = '60px'; + } + + var left = this.editorUi.toolbar.addButton('geSprite-alignleft', mxResources.get('left'), + function() { graph.alignCells(mxConstants.ALIGN_LEFT); }, stylePanel); + var center = this.editorUi.toolbar.addButton('geSprite-aligncenter', mxResources.get('center'), + function() { graph.alignCells(mxConstants.ALIGN_CENTER); }, stylePanel); + var right = this.editorUi.toolbar.addButton('geSprite-alignright', mxResources.get('right'), + function() { graph.alignCells(mxConstants.ALIGN_RIGHT); }, stylePanel); + + var top = this.editorUi.toolbar.addButton('geSprite-aligntop', mxResources.get('top'), + function() { graph.alignCells(mxConstants.ALIGN_TOP); }, stylePanel); + var middle = this.editorUi.toolbar.addButton('geSprite-alignmiddle', mxResources.get('middle'), + function() { graph.alignCells(mxConstants.ALIGN_MIDDLE); }, stylePanel); + var bottom = this.editorUi.toolbar.addButton('geSprite-alignbottom', mxResources.get('bottom'), + function() { graph.alignCells(mxConstants.ALIGN_BOTTOM); }, stylePanel); + + this.styleButtons([left, center, right, top, middle, bottom]); + right.style.marginRight = '6px'; + div.appendChild(stylePanel); + + return div; +}; + +/** + * + */ +ArrangePanel.prototype.addFlip = function(div) +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + div.style.paddingTop = '6px'; + div.style.paddingBottom = '10px'; + + var span = document.createElement('div'); + span.style.marginTop = '2px'; + span.style.marginBottom = '8px'; + span.style.fontWeight = 'bold'; + mxUtils.write(span, mxResources.get('flip')); + div.appendChild(span); + + var btn = mxUtils.button(mxResources.get('horizontal'), function(evt) + { + graph.toggleCellStyles(mxConstants.STYLE_FLIPH, false); + }) + + btn.setAttribute('title', mxResources.get('horizontal')); + btn.style.width = '100px'; + btn.style.marginRight = '2px'; + div.appendChild(btn); + + var btn = mxUtils.button(mxResources.get('vertical'), function(evt) + { + graph.toggleCellStyles(mxConstants.STYLE_FLIPV, false); + }) + + btn.setAttribute('title', mxResources.get('vertical')); + btn.style.width = '100px'; + div.appendChild(btn); + + return div; +}; + +/** + * + */ +ArrangePanel.prototype.addDistribute = function(div) +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + div.style.paddingTop = '6px'; + div.style.paddingBottom = '12px'; + + div.appendChild(this.createTitle(mxResources.get('distribute'))); + + var btn = mxUtils.button(mxResources.get('horizontal'), function(evt) + { + graph.distributeCells(true); + }) + + btn.setAttribute('title', mxResources.get('horizontal')); + btn.style.width = '100px'; + btn.style.marginRight = '2px'; + div.appendChild(btn); + + var btn = mxUtils.button(mxResources.get('vertical'), function(evt) + { + graph.distributeCells(false); + }) + + btn.setAttribute('title', mxResources.get('vertical')); + btn.style.width = '100px'; + div.appendChild(btn); + + return div; +}; + +/** + * + */ +ArrangePanel.prototype.addAngle = function(div) +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + var ss = this.format.getSelectionState(); + + div.style.paddingBottom = '8px'; + + var span = document.createElement('div'); + span.style.position = 'absolute'; + span.style.width = '70px'; + span.style.marginTop = '0px'; + span.style.fontWeight = 'bold'; + + var input = null; + var update = null; + var btn = null; + + if (ss.edges.length == 0) + { + mxUtils.write(span, mxResources.get('angle')); + div.appendChild(span); + + input = this.addUnitInput(div, '°', 20, 44, function() + { + update.apply(this, arguments); + }); + + mxUtils.br(div); + div.style.paddingTop = '10px'; + } + else + { + div.style.paddingTop = '8px'; + } + + if (!ss.containsLabel) + { + var label = mxResources.get('reverse'); + + if (ss.vertices.length > 0 && ss.edges.length > 0) + { + label = mxResources.get('turn') + ' / ' + label; + } + else if (ss.vertices.length > 0) + { + label = mxResources.get('turn'); + } + + btn = mxUtils.button(label, function(evt) + { + ui.actions.get('turn').funct(); + }) + + btn.setAttribute('title', label + ' (' + this.editorUi.actions.get('turn').shortcut + ')'); + btn.style.width = '202px'; + div.appendChild(btn); + + if (input != null) + { + btn.style.marginTop = '8px'; + } + } + + if (input != null) + { + var listener = mxUtils.bind(this, function(sender, evt, force) + { + if (force || document.activeElement != input) + { + ss = this.format.getSelectionState(); + var tmp = parseFloat(mxUtils.getValue(ss.style, mxConstants.STYLE_ROTATION, 0)); + input.value = (isNaN(tmp)) ? '' : tmp + '°'; + } + }); + + update = this.installInputHandler(input, mxConstants.STYLE_ROTATION, 0, 0, 360, '°', null, true); + this.addKeyHandler(input, listener); + + graph.getModel().addListener(mxEvent.CHANGE, listener); + this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }}); + listener(); + } + + return div; +}; + +/** + * + */ +ArrangePanel.prototype.addGeometry = function(container) +{ + var ui = this.editorUi; + var graph = ui.editor.graph; + var rect = this.format.getSelectionState(); + + var div = this.createPanel(); + div.style.paddingBottom = '8px'; + + var span = document.createElement('div'); + span.style.position = 'absolute'; + span.style.width = '50px'; + span.style.marginTop = '0px'; + span.style.fontWeight = 'bold'; + mxUtils.write(span, mxResources.get('size')); + div.appendChild(span); + + var widthUpdate, heightUpdate, leftUpdate, topUpdate; + var width = this.addUnitInput(div, 'pt', 84, 44, function() + { + widthUpdate.apply(this, arguments); + }); + var height = this.addUnitInput(div, 'pt', 20, 44, function() + { + heightUpdate.apply(this, arguments); + }); + + var autosizeBtn = document.createElement('div'); + autosizeBtn.className = 'geSprite geSprite-fit'; + autosizeBtn.setAttribute('title', mxResources.get('autosize') + ' (' + this.editorUi.actions.get('autosize').shortcut + ')'); + autosizeBtn.style.position = 'relative'; + autosizeBtn.style.cursor = 'pointer'; + autosizeBtn.style.marginTop = '-3px'; + autosizeBtn.style.border = '0px'; + autosizeBtn.style.left = '52px'; + mxUtils.setOpacity(autosizeBtn, 50); + + mxEvent.addListener(autosizeBtn, 'mouseenter', function() + { + mxUtils.setOpacity(autosizeBtn, 100); + }); + + mxEvent.addListener(autosizeBtn, 'mouseleave', function() + { + mxUtils.setOpacity(autosizeBtn, 50); + }); + + mxEvent.addListener(autosizeBtn, 'click', function() + { + ui.actions.get('autosize').funct(); + }); + + div.appendChild(autosizeBtn); + this.addLabel(div, mxResources.get('width'), 84); + this.addLabel(div, mxResources.get('height'), 20); + mxUtils.br(div); + + var wrapper = document.createElement('div'); + wrapper.style.paddingTop = '8px'; + wrapper.style.paddingRight = '20px'; + wrapper.style.whiteSpace = 'nowrap'; + wrapper.style.textAlign = 'right'; + var opt = this.createCellOption(mxResources.get('constrainProportions'), + mxConstants.STYLE_ASPECT, null, 'fixed', 'null'); + opt.style.width = '100%'; + wrapper.appendChild(opt); + div.appendChild(wrapper); + + var constrainCheckbox = opt.getElementsByTagName('input')[0]; + this.addKeyHandler(width, listener); + this.addKeyHandler(height, listener); + + widthUpdate = this.addGeometryHandler(width, function(geo, value) + { + if (geo.width > 0) + { + var value = Math.max(1, value); + + if (constrainCheckbox.checked) + { + geo.height = Math.round((geo.height * value * 100) / geo.width) / 100; + } + + geo.width = value; + } + }); + heightUpdate = this.addGeometryHandler(height, function(geo, value) + { + if (geo.height > 0) + { + var value = Math.max(1, value); + + if (constrainCheckbox.checked) + { + geo.width = Math.round((geo.width * value * 100) / geo.height) / 100; + } + + geo.height = value; + } + }); + + container.appendChild(div); + + var div2 = this.createPanel(); + div2.style.paddingBottom = '30px'; + + var span = document.createElement('div'); + span.style.position = 'absolute'; + span.style.width = '70px'; + span.style.marginTop = '0px'; + span.style.fontWeight = 'bold'; + mxUtils.write(span, mxResources.get('position')); + div2.appendChild(span); + + var left = this.addUnitInput(div2, 'pt', 84, 44, function() + { + leftUpdate.apply(this, arguments); + }); + var top = this.addUnitInput(div2, 'pt', 20, 44, function() + { + topUpdate.apply(this, arguments); + }); + + mxUtils.br(div2); + this.addLabel(div2, mxResources.get('left'), 84); + this.addLabel(div2, mxResources.get('top'), 20); + + var listener = mxUtils.bind(this, function(sender, evt, force) + { + rect = this.format.getSelectionState(); + + if (!rect.containsLabel && rect.vertices.length == graph.getSelectionCount() && + rect.width != null && rect.height != null) + { + div.style.display = ''; + + if (force || document.activeElement != width) + { + width.value = rect.width + ((rect.width == '') ? '' : ' pt'); + } + + if (force || document.activeElement != height) + { + height.value = rect.height + ((rect.height == '') ? '' : ' pt'); + } + } + else + { + div.style.display = 'none'; + } + + if (rect.vertices.length == graph.getSelectionCount() && + rect.x != null && rect.y != null) + { + div2.style.display = ''; + + if (force || document.activeElement != left) + { + left.value = rect.x + ((rect.x == '') ? '' : ' pt'); + } + + if (force || document.activeElement != top) + { + top.value = rect.y + ((rect.y == '') ? '' : ' pt'); + } + } + else + { + div2.style.display = 'none'; + } + }); + + this.addKeyHandler(left, listener); + this.addKeyHandler(top, listener); + + graph.getModel().addListener(mxEvent.CHANGE, listener); + this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }}); + listener(); + + leftUpdate = this.addGeometryHandler(left, function(geo, value) + { + if (geo.relative) + { + geo.offset.x = value; + } + else + { + geo.x = value; + } + }); + topUpdate = this.addGeometryHandler(top, function(geo, value) + { + if (geo.relative) + { + geo.offset.y = value; + } + else + { + geo.y = value; + } + }); + + container.appendChild(div2); +}; + +/** + * + */ +ArrangePanel.prototype.addGeometryHandler = function(input, fn) +{ + var ui = this.editorUi; + var graph = ui.editor.graph; + var initialValue = null; + + function update(evt) + { + if (input.value != '') + { + var value = parseFloat(input.value); + + if (value != initialValue) + { + graph.getModel().beginUpdate(); + try + { + var cells = graph.getSelectionCells(); + + for (var i = 0; i < cells.length; i++) + { + if (graph.getModel().isVertex(cells[i])) + { + var geo = graph.getCellGeometry(cells[i]); + + if (geo != null) + { + geo = geo.clone(); + fn(geo, value); + + graph.getModel().setGeometry(cells[i], geo); + } + } + } + } + finally + { + graph.getModel().endUpdate(); + } + + initialValue = value; + input.value = value + ' pt'; + } + else if (isNaN(value)) + { + input.value = initialValue + ' pt'; + } + } + + mxEvent.consume(evt); + }; + + mxEvent.addListener(input, 'blur', update); + mxEvent.addListener(input, 'change', update); + mxEvent.addListener(input, 'focus', function() + { + initialValue = input.value; + }); + + return update; +}; + +ArrangePanel.prototype.addEdgeGeometryHandler = function(input, fn) +{ + var ui = this.editorUi; + var graph = ui.editor.graph; + var initialValue = null; + + function update(evt) + { + if (input.value != '') + { + var value = parseFloat(input.value); + + if (isNaN(value)) + { + input.value = initialValue + ' pt'; + } + else if (value != initialValue) + { + graph.getModel().beginUpdate(); + try + { + var cells = graph.getSelectionCells(); + + for (var i = 0; i < cells.length; i++) + { + if (graph.getModel().isEdge(cells[i])) + { + var geo = graph.getCellGeometry(cells[i]); + + if (geo != null) + { + geo = geo.clone(); + fn(geo, value); + + graph.getModel().setGeometry(cells[i], geo); + } + } + } + } + finally + { + graph.getModel().endUpdate(); + } + + initialValue = value; + input.value = value + ' pt'; + } + } + + mxEvent.consume(evt); + }; + + mxEvent.addListener(input, 'blur', update); + mxEvent.addListener(input, 'change', update); + mxEvent.addListener(input, 'focus', function() + { + initialValue = input.value; + }); + + return update; +}; + +/** + * + */ +ArrangePanel.prototype.addEdgeGeometry = function(container) +{ + var ui = this.editorUi; + var graph = ui.editor.graph; + var rect = this.format.getSelectionState(); + + var div = this.createPanel(); + + var span = document.createElement('div'); + span.style.position = 'absolute'; + span.style.width = '70px'; + span.style.marginTop = '0px'; + span.style.fontWeight = 'bold'; + mxUtils.write(span, mxResources.get('width')); + div.appendChild(span); + + var widthUpdate, xtUpdate, ytUpdate, xsUpdate, ysUpdate; + var width = this.addUnitInput(div, 'pt', 20, 44, function() + { + widthUpdate.apply(this, arguments); + }); + + mxUtils.br(div); + this.addKeyHandler(width, listener); + + function widthUpdate(evt) + { + // Maximum stroke width is 999 + var value = parseInt(width.value); + value = Math.min(999, Math.max(1, (isNaN(value)) ? 1 : value)); + + if (value != mxUtils.getValue(rect.style, 'width', mxCellRenderer.defaultShapes['flexArrow'].prototype.defaultWidth)) + { + graph.setCellStyles('width', value, graph.getSelectionCells()); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', ['width'], + 'values', [value], 'cells', graph.getSelectionCells())); + } + + width.value = value + ' pt'; + mxEvent.consume(evt); + }; + + mxEvent.addListener(width, 'blur', widthUpdate); + mxEvent.addListener(width, 'change', widthUpdate); + + container.appendChild(div); + + var divs = this.createPanel(); + divs.style.paddingBottom = '30px'; + + var span = document.createElement('div'); + span.style.position = 'absolute'; + span.style.width = '70px'; + span.style.marginTop = '0px'; + span.style.fontWeight = 'bold'; + mxUtils.write(span, 'Start'); + divs.appendChild(span); + + var xs = this.addUnitInput(divs, 'pt', 84, 44, function() + { + xsUpdate.apply(this, arguments); + }); + var ys = this.addUnitInput(divs, 'pt', 20, 44, function() + { + ysUpdate.apply(this, arguments); + }); + + mxUtils.br(divs); + this.addLabel(divs, mxResources.get('left'), 84); + this.addLabel(divs, mxResources.get('top'), 20); + container.appendChild(divs); + this.addKeyHandler(xs, listener); + this.addKeyHandler(ys, listener); + + var divt = this.createPanel(); + divt.style.paddingBottom = '30px'; + + var span = document.createElement('div'); + span.style.position = 'absolute'; + span.style.width = '70px'; + span.style.marginTop = '0px'; + span.style.fontWeight = 'bold'; + mxUtils.write(span, 'End'); + divt.appendChild(span); + + var xt = this.addUnitInput(divt, 'pt', 84, 44, function() + { + xtUpdate.apply(this, arguments); + }); + var yt = this.addUnitInput(divt, 'pt', 20, 44, function() + { + ytUpdate.apply(this, arguments); + }); + + mxUtils.br(divt); + this.addLabel(divt, mxResources.get('left'), 84); + this.addLabel(divt, mxResources.get('top'), 20); + container.appendChild(divt); + this.addKeyHandler(xt, listener); + this.addKeyHandler(yt, listener); + + var listener = mxUtils.bind(this, function(sender, evt, force) + { + rect = this.format.getSelectionState(); + var cell = graph.getSelectionCell(); + + if (rect.style.shape == 'link' || rect.style.shape == 'flexArrow') + { + div.style.display = ''; + + if (force || document.activeElement != width) + { + var value = mxUtils.getValue(rect.style, 'width', + mxCellRenderer.defaultShapes['flexArrow'].prototype.defaultWidth); + width.value = value + ' pt'; + } + } + else + { + div.style.display = 'none'; + } + + if (graph.getSelectionCount() == 1 && graph.model.isEdge(cell)) + { + var geo = graph.model.getGeometry(cell); + + if (geo.sourcePoint != null && graph.model.getTerminal(cell, true) == null) + { + xs.value = geo.sourcePoint.x; + ys.value = geo.sourcePoint.y; + } + else + { + divs.style.display = 'none'; + } + + if (geo.targetPoint != null && graph.model.getTerminal(cell, false) == null) + { + xt.value = geo.targetPoint.x; + yt.value = geo.targetPoint.y; + } + else + { + divt.style.display = 'none'; + } + } + else + { + divs.style.display = 'none'; + divt.style.display = 'none'; + } + }); + + xsUpdate = this.addEdgeGeometryHandler(xs, function(geo, value) + { + geo.sourcePoint.x = value; + }); + + ysUpdate = this.addEdgeGeometryHandler(ys, function(geo, value) + { + geo.sourcePoint.y = value; + }); + + xtUpdate = this.addEdgeGeometryHandler(xt, function(geo, value) + { + geo.targetPoint.x = value; + }); + + ytUpdate = this.addEdgeGeometryHandler(yt, function(geo, value) + { + geo.targetPoint.y = value; + }); + + graph.getModel().addListener(mxEvent.CHANGE, listener); + this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }}); + listener(); +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +TextFormatPanel = function(format, editorUi, container) +{ + BaseFormatPanel.call(this, format, editorUi, container); + this.init(); +}; + +mxUtils.extend(TextFormatPanel, BaseFormatPanel); + +/** + * Adds the label menu items to the given menu and parent. + */ +TextFormatPanel.prototype.init = function() +{ + this.container.style.borderBottom = 'none'; + this.addFont(this.container); +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +TextFormatPanel.prototype.addFont = function(container) +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + var ss = this.format.getSelectionState(); + + var title = this.createTitle(mxResources.get('font')); + title.style.paddingLeft = '18px'; + title.style.paddingTop = '10px'; + title.style.paddingBottom = '6px'; + container.appendChild(title); + + var stylePanel = this.createPanel(); + stylePanel.style.paddingTop = '2px'; + stylePanel.style.paddingBottom = '2px'; + stylePanel.style.position = 'relative'; + stylePanel.style.marginLeft = '-2px'; + stylePanel.style.borderWidth = '0px'; + stylePanel.className = 'geToolbarContainer'; + + if (mxClient.IS_QUIRKS) + { + stylePanel.style.display = 'block'; + } + + if (graph.cellEditor.isContentEditing()) + { + var cssPanel = stylePanel.cloneNode(); + + var cssMenu = this.editorUi.toolbar.addMenu(mxResources.get('style'), + mxResources.get('style'), true, 'formatBlock', cssPanel, null, true); + cssMenu.style.color = 'rgb(112, 112, 112)'; + cssMenu.style.whiteSpace = 'nowrap'; + cssMenu.style.overflow = 'hidden'; + cssMenu.style.margin = '0px'; + this.addArrow(cssMenu); + cssMenu.style.width = '192px'; + cssMenu.style.height = '15px'; + + var arrow = cssMenu.getElementsByTagName('div')[0]; + arrow.style.cssFloat = 'right'; + container.appendChild(cssPanel); + + // Workaround for offset in FF + if (mxClient.IS_FF) + { + cssMenu.getElementsByTagName('div')[0].style.marginTop = '-18px'; + } + } + + container.appendChild(stylePanel); + + var colorPanel = this.createPanel(); + colorPanel.style.marginTop = '8px'; + colorPanel.style.borderTop = '1px solid #c0c0c0'; + colorPanel.style.paddingTop = '6px'; + colorPanel.style.paddingBottom = '6px'; + + var fontMenu = this.editorUi.toolbar.addMenu('Helvetica', mxResources.get('fontFamily'), + true, 'fontFamily', stylePanel, null, true); + fontMenu.style.color = 'rgb(112, 112, 112)'; + fontMenu.style.whiteSpace = 'nowrap'; + fontMenu.style.overflow = 'hidden'; + fontMenu.style.margin = '0px'; + + this.addArrow(fontMenu); + fontMenu.style.width = '192px'; + fontMenu.style.height = '15px'; + + // Workaround for offset in FF + if (mxClient.IS_FF) + { + fontMenu.getElementsByTagName('div')[0].style.marginTop = '-18px'; + } + + var stylePanel2 = stylePanel.cloneNode(false); + stylePanel2.style.marginLeft = '-3px'; + var fontStyleItems = this.editorUi.toolbar.addItems(['bold', 'italic', 'underline'], stylePanel2, true); + fontStyleItems[0].setAttribute('title', mxResources.get('bold') + ' (' + this.editorUi.actions.get('bold').shortcut + ')'); + fontStyleItems[1].setAttribute('title', mxResources.get('italic') + ' (' + this.editorUi.actions.get('italic').shortcut + ')'); + fontStyleItems[2].setAttribute('title', mxResources.get('underline') + ' (' + this.editorUi.actions.get('underline').shortcut + ')'); + + var verticalItem = this.editorUi.toolbar.addItems(['vertical'], stylePanel2, true)[0]; + + if (mxClient.IS_QUIRKS) + { + mxUtils.br(container); + } + + container.appendChild(stylePanel2); + + this.styleButtons(fontStyleItems); + this.styleButtons([verticalItem]); + + var stylePanel3 = stylePanel.cloneNode(false); + stylePanel3.style.marginLeft = '-3px'; + stylePanel3.style.paddingBottom = '0px'; + + // Helper function to return a wrapper function does not pass any arguments + var callFn = function(fn) + { + return function() + { + return fn(); + }; + }; + + var left = this.editorUi.toolbar.addButton('geSprite-left', mxResources.get('left'), + (graph.cellEditor.isContentEditing()) ? + function() + { + document.execCommand('justifyleft', false, null); + } : callFn(this.editorUi.menus.createStyleChangeFunction([mxConstants.STYLE_ALIGN], [mxConstants.ALIGN_LEFT])), stylePanel3); + var center = this.editorUi.toolbar.addButton('geSprite-center', mxResources.get('center'), + (graph.cellEditor.isContentEditing()) ? + function() + { + document.execCommand('justifycenter', false, null); + } : callFn(this.editorUi.menus.createStyleChangeFunction([mxConstants.STYLE_ALIGN], [mxConstants.ALIGN_CENTER])), stylePanel3); + var right = this.editorUi.toolbar.addButton('geSprite-right', mxResources.get('right'), + (graph.cellEditor.isContentEditing()) ? + function() + { + document.execCommand('justifyright', false, null); + } : callFn(this.editorUi.menus.createStyleChangeFunction([mxConstants.STYLE_ALIGN], [mxConstants.ALIGN_RIGHT])), stylePanel3); + + this.styleButtons([left, center, right]); + + if (graph.cellEditor.isContentEditing()) + { + var clear = this.editorUi.toolbar.addButton('geSprite-removeformat', mxResources.get('removeFormat'), + function() + { + document.execCommand('removeformat', false, null); + }, stylePanel2); + this.styleButtons([clear]); + } + + var top = this.editorUi.toolbar.addButton('geSprite-top', mxResources.get('top'), + callFn(this.editorUi.menus.createStyleChangeFunction([mxConstants.STYLE_VERTICAL_ALIGN], [mxConstants.ALIGN_TOP])), stylePanel3); + var middle = this.editorUi.toolbar.addButton('geSprite-middle', mxResources.get('middle'), + callFn(this.editorUi.menus.createStyleChangeFunction([mxConstants.STYLE_VERTICAL_ALIGN], [mxConstants.ALIGN_MIDDLE])), stylePanel3); + var bottom = this.editorUi.toolbar.addButton('geSprite-bottom', mxResources.get('bottom'), + callFn(this.editorUi.menus.createStyleChangeFunction([mxConstants.STYLE_VERTICAL_ALIGN], [mxConstants.ALIGN_BOTTOM])), stylePanel3); + + this.styleButtons([top, middle, bottom]); + + if (mxClient.IS_QUIRKS) + { + mxUtils.br(container); + } + + container.appendChild(stylePanel3); + + // Hack for updating UI state below based on current text selection + // currentTable is the current selected DOM table updated below + var sub, sup, full, tableWrapper, currentTable, tableCell, tableRow; + + if (graph.cellEditor.isContentEditing()) + { + top.style.display = 'none'; + middle.style.display = 'none'; + bottom.style.display = 'none'; + verticalItem.style.display = 'none'; + + full = this.editorUi.toolbar.addButton('geSprite-justifyfull', null, + function() + { + document.execCommand('justifyfull', false, null); + }, stylePanel3); + this.styleButtons([full, + sub = this.editorUi.toolbar.addButton('geSprite-subscript', + mxResources.get('subscript') + ' (' + Editor.ctrlKey + '+,)', + function() + { + document.execCommand('subscript', false, null); + }, stylePanel3), sup = this.editorUi.toolbar.addButton('geSprite-superscript', + mxResources.get('superscript') + ' (' + Editor.ctrlKey + '+.)', + function() + { + document.execCommand('superscript', false, null); + }, stylePanel3)]); + full.style.marginRight = '9px'; + + var tmp = stylePanel3.cloneNode(false); + tmp.style.paddingTop = '4px'; + var btns = [this.editorUi.toolbar.addButton('geSprite-orderedlist', mxResources.get('numberedList'), + function() + { + document.execCommand('insertorderedlist', false, null); + }, tmp), + this.editorUi.toolbar.addButton('geSprite-unorderedlist', mxResources.get('bulletedList'), + function() + { + document.execCommand('insertunorderedlist', false, null); + }, tmp), + this.editorUi.toolbar.addButton('geSprite-outdent', mxResources.get('decreaseIndent'), + function() + { + document.execCommand('outdent', false, null); + }, tmp), + this.editorUi.toolbar.addButton('geSprite-indent', mxResources.get('increaseIndent'), + function() + { + document.execCommand('indent', false, null); + }, tmp), + this.editorUi.toolbar.addButton('geSprite-code', mxResources.get('html'), + function() + { + graph.cellEditor.toggleViewMode(); + }, tmp)]; + this.styleButtons(btns); + btns[btns.length - 1].style.marginLeft = '9px'; + + if (mxClient.IS_QUIRKS) + { + mxUtils.br(container); + tmp.style.height = '40'; + } + + container.appendChild(tmp); + } + else + { + fontStyleItems[2].style.marginRight = '9px'; + right.style.marginRight = '9px'; + } + + // Label position + var stylePanel4 = stylePanel.cloneNode(false); + stylePanel4.style.marginLeft = '0px'; + stylePanel4.style.paddingTop = '8px'; + stylePanel4.style.paddingBottom = '4px'; + stylePanel4.style.fontWeight = 'normal'; + + mxUtils.write(stylePanel4, mxResources.get('position')); + + // Adds label position options + var positionSelect = document.createElement('select'); + positionSelect.style.position = 'absolute'; + positionSelect.style.right = '20px'; + positionSelect.style.width = '97px'; + positionSelect.style.marginTop = '-2px'; + + var directions = ['topLeft', 'top', 'topRight', 'left', 'center', 'right', 'bottomLeft', 'bottom', 'bottomRight']; + var lset = {'topLeft': [mxConstants.ALIGN_LEFT, mxConstants.ALIGN_TOP, mxConstants.ALIGN_RIGHT, mxConstants.ALIGN_BOTTOM], + 'top': [mxConstants.ALIGN_CENTER, mxConstants.ALIGN_TOP, mxConstants.ALIGN_CENTER, mxConstants.ALIGN_BOTTOM], + 'topRight': [mxConstants.ALIGN_RIGHT, mxConstants.ALIGN_TOP, mxConstants.ALIGN_LEFT, mxConstants.ALIGN_BOTTOM], + 'left': [mxConstants.ALIGN_LEFT, mxConstants.ALIGN_MIDDLE, mxConstants.ALIGN_RIGHT, mxConstants.ALIGN_MIDDLE], + 'center': [mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE, mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE], + 'right': [mxConstants.ALIGN_RIGHT, mxConstants.ALIGN_MIDDLE, mxConstants.ALIGN_LEFT, mxConstants.ALIGN_MIDDLE], + 'bottomLeft': [mxConstants.ALIGN_LEFT, mxConstants.ALIGN_BOTTOM, mxConstants.ALIGN_RIGHT, mxConstants.ALIGN_TOP], + 'bottom': [mxConstants.ALIGN_CENTER, mxConstants.ALIGN_BOTTOM, mxConstants.ALIGN_CENTER, mxConstants.ALIGN_TOP], + 'bottomRight': [mxConstants.ALIGN_RIGHT, mxConstants.ALIGN_BOTTOM, mxConstants.ALIGN_LEFT, mxConstants.ALIGN_TOP]}; + + for (var i = 0; i < directions.length; i++) + { + var positionOption = document.createElement('option'); + positionOption.setAttribute('value', directions[i]); + mxUtils.write(positionOption, mxResources.get(directions[i])); + positionSelect.appendChild(positionOption); + } + + stylePanel4.appendChild(positionSelect); + + // Writing direction + var stylePanel5 = stylePanel.cloneNode(false); + stylePanel5.style.marginLeft = '0px'; + stylePanel5.style.paddingTop = '4px'; + stylePanel5.style.paddingBottom = '4px'; + stylePanel5.style.fontWeight = 'normal'; + + mxUtils.write(stylePanel5, mxResources.get('writingDirection')); + + // Adds writing direction options + // LATER: Handle reselect of same option in all selects (change event + // is not fired for same option so have opened state on click) and + // handle multiple different styles for current selection + var dirSelect = document.createElement('select'); + dirSelect.style.position = 'absolute'; + dirSelect.style.right = '20px'; + dirSelect.style.width = '97px'; + dirSelect.style.marginTop = '-2px'; + + // NOTE: For automatic we use the value null since automatic + // requires the text to be non formatted and non-wrapped + var dirs = ['automatic', 'leftToRight', 'rightToLeft']; + var dirSet = {'automatic': null, + 'leftToRight': mxConstants.TEXT_DIRECTION_LTR, + 'rightToLeft': mxConstants.TEXT_DIRECTION_RTL}; + + for (var i = 0; i < dirs.length; i++) + { + var dirOption = document.createElement('option'); + dirOption.setAttribute('value', dirs[i]); + mxUtils.write(dirOption, mxResources.get(dirs[i])); + dirSelect.appendChild(dirOption); + } + + stylePanel5.appendChild(dirSelect); + + if (!graph.isEditing()) + { + container.appendChild(stylePanel4); + + mxEvent.addListener(positionSelect, 'change', function(evt) + { + graph.getModel().beginUpdate(); + try + { + var vals = lset[positionSelect.value]; + + if (vals != null) + { + graph.setCellStyles(mxConstants.STYLE_LABEL_POSITION, vals[0], graph.getSelectionCells()); + graph.setCellStyles(mxConstants.STYLE_VERTICAL_LABEL_POSITION, vals[1], graph.getSelectionCells()); + graph.setCellStyles(mxConstants.STYLE_ALIGN, vals[2], graph.getSelectionCells()); + graph.setCellStyles(mxConstants.STYLE_VERTICAL_ALIGN, vals[3], graph.getSelectionCells()); + } + } + finally + { + graph.getModel().endUpdate(); + } + + mxEvent.consume(evt); + }); + + // LATER: Update dir in text editor while editing and update style with label + // NOTE: The tricky part is handling and passing on the auto value + container.appendChild(stylePanel5); + + mxEvent.addListener(dirSelect, 'change', function(evt) + { + graph.setCellStyles(mxConstants.STYLE_TEXT_DIRECTION, dirSet[dirSelect.value], graph.getSelectionCells()); + mxEvent.consume(evt); + }); + } + + // Font size + var input = document.createElement('input'); + input.style.textAlign = 'right'; + input.style.marginTop = '4px'; + + if (!mxClient.IS_QUIRKS) + { + input.style.position = 'absolute'; + input.style.right = '32px'; + } + + input.style.width = '46px'; + input.style.height = (mxClient.IS_QUIRKS) ? '21px' : '17px'; + stylePanel2.appendChild(input); + + // Workaround for font size 4 if no text is selected is update font size below + // after first character was entered (as the font element is lazy created) + var pendingFontSize = null; + + var inputUpdate = this.installInputHandler(input, mxConstants.STYLE_FONTSIZE, Menus.prototype.defaultFontSize, 1, 999, ' pt', + function(fontSize) + { + // IE does not support containsNode + // KNOWN: Fixes font size issues but bypasses undo + if (window.getSelection && !mxClient.IS_IE && !mxClient.IS_IE11) + { + var selection = window.getSelection(); + var container = (selection.rangeCount > 0) ? selection.getRangeAt(0).commonAncestorContainer : + graph.cellEditor.textarea; + + function updateSize(elt, ignoreContains) + { + if (elt != graph.cellEditor.textarea && graph.cellEditor.textarea.contains(elt) && + (ignoreContains || selection.containsNode(elt, true))) + { + if (elt.nodeName == 'FONT') + { + elt.removeAttribute('size'); + elt.style.fontSize = fontSize + 'px'; + } + else + { + var css = mxUtils.getCurrentStyle(elt); + + if (css.fontSize != fontSize + 'px') + { + if (mxUtils.getCurrentStyle(elt.parentNode).fontSize != fontSize + 'px') + { + elt.style.fontSize = fontSize + 'px'; + } + else + { + elt.style.fontSize = ''; + } + } + } + } + }; + + // Wraps text node or mixed selection with leading text in a font element + if (container == graph.cellEditor.textarea || + container.nodeType != mxConstants.NODETYPE_ELEMENT) + { + document.execCommand('fontSize', false, '1'); + } + + if (container != graph.cellEditor.textarea) + { + container = container.parentNode; + } + + if (container.nodeType == mxConstants.NODETYPE_ELEMENT) + { + var elts = container.getElementsByTagName('*'); + updateSize(container); + + for (var i = 0; i < elts.length; i++) + { + updateSize(elts[i]); + } + } + + input.value = fontSize + ' pt'; + } + else if (window.getSelection || document.selection) + { + // Checks selection + var par = null; + + if (document.selection) + { + par = document.selection.createRange().parentElement(); + } + else + { + var selection = window.getSelection(); + + if (selection.rangeCount > 0) + { + par = selection.getRangeAt(0).commonAncestorContainer; + } + } + + // Node.contains does not work for text nodes in IE11 + function isOrContains(container, node) + { + while (node != null) + { + if (node === container) + { + return true; + } + + node = node.parentNode; + } + + return false; + }; + + if (par != null && isOrContains(graph.cellEditor.textarea, par)) + { + pendingFontSize = fontSize; + + // Workaround for can't set font size in px is to change font size afterwards + document.execCommand('fontSize', false, '4'); + var elts = graph.cellEditor.textarea.getElementsByTagName('font'); + + for (var i = 0; i < elts.length; i++) + { + if (elts[i].getAttribute('size') == '4') + { + elts[i].removeAttribute('size'); + elts[i].style.fontSize = pendingFontSize + 'px'; + + // Overrides fontSize in input with the one just assigned as a workaround + // for potential fontSize values of parent elements that don't match + window.setTimeout(function() + { + input.value = pendingFontSize + ' pt'; + pendingFontSize = null; + }, 0); + + break; + } + } + } + } + }, true); + + var stepper = this.createStepper(input, inputUpdate, 1, 10, true, Menus.prototype.defaultFontSize); + stepper.style.display = input.style.display; + stepper.style.marginTop = '4px'; + + if (!mxClient.IS_QUIRKS) + { + stepper.style.right = '20px'; + } + + stylePanel2.appendChild(stepper); + + var arrow = fontMenu.getElementsByTagName('div')[0]; + arrow.style.cssFloat = 'right'; + + var bgColorApply = null; + var currentBgColor = '#ffffff'; + + var fontColorApply = null; + var currentFontColor = '#000000'; + + var bgPanel = (graph.cellEditor.isContentEditing()) ? this.createColorOption(mxResources.get('backgroundColor'), function() + { + return currentBgColor; + }, function(color) + { + document.execCommand('backcolor', false, (color != mxConstants.NONE) ? color : 'transparent'); + }, '#ffffff', + { + install: function(apply) { bgColorApply = apply; }, + destroy: function() { bgColorApply = null; } + }, null, true) : this.createCellColorOption(mxResources.get('backgroundColor'), mxConstants.STYLE_LABEL_BACKGROUNDCOLOR, '#ffffff', null, function(color) + { + graph.updateLabelElements(graph.getSelectionCells(), function(elt) + { + elt.style.backgroundColor = null; + }); + }); + bgPanel.style.fontWeight = 'bold'; + + var borderPanel = this.createCellColorOption(mxResources.get('borderColor'), mxConstants.STYLE_LABEL_BORDERCOLOR, '#000000'); + borderPanel.style.fontWeight = 'bold'; + + var panel = (graph.cellEditor.isContentEditing()) ? this.createColorOption(mxResources.get('fontColor'), function() + { + return currentFontColor; + }, function(color) + { + document.execCommand('forecolor', false, (color != mxConstants.NONE) ? color : 'transparent'); + }, '#000000', + { + install: function(apply) { fontColorApply = apply; }, + destroy: function() { fontColorApply = null; } + }, null, true) : this.createCellColorOption(mxResources.get('fontColor'), mxConstants.STYLE_FONTCOLOR, '#000000', function(color) + { + if (color == null || color == mxConstants.NONE) + { + bgPanel.style.display = 'none'; + } + else + { + bgPanel.style.display = ''; + } + + borderPanel.style.display = bgPanel.style.display; + }, function(color) + { + if (color == null || color == mxConstants.NONE) + { + graph.setCellStyles(mxConstants.STYLE_NOLABEL, '1', graph.getSelectionCells()); + } + else + { + graph.setCellStyles(mxConstants.STYLE_NOLABEL, null, graph.getSelectionCells()); + } + + graph.updateLabelElements(graph.getSelectionCells(), function(elt) + { + elt.removeAttribute('color'); + elt.style.color = null; + }); + }); + panel.style.fontWeight = 'bold'; + + colorPanel.appendChild(panel); + colorPanel.appendChild(bgPanel); + + if (!graph.cellEditor.isContentEditing()) + { + colorPanel.appendChild(borderPanel); + } + + container.appendChild(colorPanel); + + var extraPanel = this.createPanel(); + extraPanel.style.paddingTop = '2px'; + extraPanel.style.paddingBottom = '4px'; + + // LATER: Fix toggle using '' instead of 'null' + var wwOpt = this.createCellOption(mxResources.get('wordWrap'), mxConstants.STYLE_WHITE_SPACE, null, 'wrap', 'null', null, null, true); + wwOpt.style.fontWeight = 'bold'; + + // Word wrap in edge labels only supported via labelWidth style + if (!ss.containsLabel && !ss.autoSize && ss.edges.length == 0) + { + extraPanel.appendChild(wwOpt); + } + + // Delegates switch of style to formattedText action as it also convertes newlines + var htmlOpt = this.createCellOption(mxResources.get('formattedText'), 'html', '0', + null, null, null, ui.actions.get('formattedText')); + htmlOpt.style.fontWeight = 'bold'; + extraPanel.appendChild(htmlOpt); + + var spacingPanel = this.createPanel(); + spacingPanel.style.paddingTop = '10px'; + spacingPanel.style.paddingBottom = '28px'; + spacingPanel.style.fontWeight = 'normal'; + + var span = document.createElement('div'); + span.style.position = 'absolute'; + span.style.width = '70px'; + span.style.marginTop = '0px'; + span.style.fontWeight = 'bold'; + mxUtils.write(span, mxResources.get('spacing')); + spacingPanel.appendChild(span); + + var topUpdate, globalUpdate, leftUpdate, bottomUpdate, rightUpdate; + var topSpacing = this.addUnitInput(spacingPanel, 'pt', 91, 44, function() + { + topUpdate.apply(this, arguments); + }); + var globalSpacing = this.addUnitInput(spacingPanel, 'pt', 20, 44, function() + { + globalUpdate.apply(this, arguments); + }); + + mxUtils.br(spacingPanel); + this.addLabel(spacingPanel, mxResources.get('top'), 91); + this.addLabel(spacingPanel, mxResources.get('global'), 20); + mxUtils.br(spacingPanel); + mxUtils.br(spacingPanel); + + var leftSpacing = this.addUnitInput(spacingPanel, 'pt', 162, 44, function() + { + leftUpdate.apply(this, arguments); + }); + var bottomSpacing = this.addUnitInput(spacingPanel, 'pt', 91, 44, function() + { + bottomUpdate.apply(this, arguments); + }); + var rightSpacing = this.addUnitInput(spacingPanel, 'pt', 20, 44, function() + { + rightUpdate.apply(this, arguments); + }); + + mxUtils.br(spacingPanel); + this.addLabel(spacingPanel, mxResources.get('left'), 162); + this.addLabel(spacingPanel, mxResources.get('bottom'), 91); + this.addLabel(spacingPanel, mxResources.get('right'), 20); + + if (!graph.cellEditor.isContentEditing()) + { + container.appendChild(extraPanel); + container.appendChild(this.createRelativeOption(mxResources.get('opacity'), mxConstants.STYLE_TEXT_OPACITY)); + container.appendChild(spacingPanel); + } + else + { + var selState = null; + var lineHeightInput = null; + + container.appendChild(this.createRelativeOption(mxResources.get('lineheight'), null, null, function(input) + { + var value = (input.value == '') ? 120 : parseInt(input.value); + value = Math.max(0, (isNaN(value)) ? 120 : value); + + if (selState != null) + { + graph.cellEditor.restoreSelection(selState); + selState = null; + } + + var selectedElement = graph.getSelectedElement(); + var node = selectedElement; + + while (node != null && node.nodeType != mxConstants.NODETYPE_ELEMENT) + { + node = node.parentNode; + } + + if (node != null && node == graph.cellEditor.textarea && graph.cellEditor.textarea.firstChild != null) + { + if (graph.cellEditor.textarea.firstChild.nodeName != 'P') + { + graph.cellEditor.textarea.innerHTML = '

' + graph.cellEditor.textarea.innerHTML + '

'; + } + + node = graph.cellEditor.textarea.firstChild; + } + + if (node != null && node != graph.cellEditor.textarea && graph.cellEditor.textarea.contains(node)) + { + node.style.lineHeight = value + '%'; + } + + input.value = value + ' %'; + }, function(input) + { + // Used in CSS handler to update current value + lineHeightInput = input; + + // KNOWN: Arrow up/down clear selection text in quirks/IE 8 + // Text size via arrow button limits to 16 in IE11. Why? + mxEvent.addListener(input, 'mousedown', function() + { + if (document.activeElement == graph.cellEditor.textarea) + { + selState = graph.cellEditor.saveSelection(); + } + }); + + mxEvent.addListener(input, 'touchstart', function() + { + if (document.activeElement == graph.cellEditor.textarea) + { + selState = graph.cellEditor.saveSelection(); + } + }); + + input.value = '120 %'; + })); + + var insertPanel = stylePanel.cloneNode(false); + insertPanel.style.paddingLeft = '0px'; + var insertBtns = this.editorUi.toolbar.addItems(['link', 'image'], insertPanel, true); + + var btns = [ + this.editorUi.toolbar.addButton('geSprite-horizontalrule', mxResources.get('insertHorizontalRule'), + function() + { + document.execCommand('inserthorizontalrule', false); + }, insertPanel), + this.editorUi.toolbar.addMenuFunctionInContainer(insertPanel, 'geSprite-table', mxResources.get('table'), false, mxUtils.bind(this, function(menu) + { + this.editorUi.menus.addInsertTableItem(menu); + }))]; + this.styleButtons(insertBtns); + this.styleButtons(btns); + + var wrapper2 = this.createPanel(); + wrapper2.style.paddingTop = '10px'; + wrapper2.style.paddingBottom = '10px'; + wrapper2.appendChild(this.createTitle(mxResources.get('insert'))); + wrapper2.appendChild(insertPanel); + container.appendChild(wrapper2); + + if (mxClient.IS_QUIRKS) + { + wrapper2.style.height = '70'; + } + + var tablePanel = stylePanel.cloneNode(false); + tablePanel.style.paddingLeft = '0px'; + + var btns = [ + this.editorUi.toolbar.addButton('geSprite-insertcolumnbefore', mxResources.get('insertColumnBefore'), + mxUtils.bind(this, function() + { + try + { + if (currentTable != null) + { + graph.selectNode(graph.insertColumn(currentTable, (tableCell != null) ? tableCell.cellIndex : 0)); + } + } + catch (e) + { + this.editorUi.handleError(e); + } + }), tablePanel), + this.editorUi.toolbar.addButton('geSprite-insertcolumnafter', mxResources.get('insertColumnAfter'), + mxUtils.bind(this, function() + { + try + { + if (currentTable != null) + { + graph.selectNode(graph.insertColumn(currentTable, (tableCell != null) ? tableCell.cellIndex + 1 : -1)); + } + } + catch (e) + { + this.editorUi.handleError(e); + } + }), tablePanel), + this.editorUi.toolbar.addButton('geSprite-deletecolumn', mxResources.get('deleteColumn'), + mxUtils.bind(this, function() + { + try + { + if (currentTable != null && tableCell != null) + { + graph.deleteColumn(currentTable, tableCell.cellIndex); + } + } + catch (e) + { + this.editorUi.handleError(e); + } + }), tablePanel), + this.editorUi.toolbar.addButton('geSprite-insertrowbefore', mxResources.get('insertRowBefore'), + mxUtils.bind(this, function() + { + try + { + if (currentTable != null && tableRow != null) + { + graph.selectNode(graph.insertRow(currentTable, tableRow.sectionRowIndex)); + } + } + catch (e) + { + this.editorUi.handleError(e); + } + }), tablePanel), + this.editorUi.toolbar.addButton('geSprite-insertrowafter', mxResources.get('insertRowAfter'), + mxUtils.bind(this, function() + { + try + { + if (currentTable != null && tableRow != null) + { + graph.selectNode(graph.insertRow(currentTable, tableRow.sectionRowIndex + 1)); + } + } + catch (e) + { + this.editorUi.handleError(e); + } + }), tablePanel), + this.editorUi.toolbar.addButton('geSprite-deleterow', mxResources.get('deleteRow'), + mxUtils.bind(this, function() + { + try + { + if (currentTable != null && tableRow != null) + { + graph.deleteRow(currentTable, tableRow.sectionRowIndex); + } + } + catch (e) + { + this.editorUi.handleError(e); + } + }), tablePanel)]; + this.styleButtons(btns); + btns[2].style.marginRight = '9px'; + + var wrapper3 = this.createPanel(); + wrapper3.style.paddingTop = '10px'; + wrapper3.style.paddingBottom = '10px'; + wrapper3.appendChild(this.createTitle(mxResources.get('table'))); + wrapper3.appendChild(tablePanel); + + if (mxClient.IS_QUIRKS) + { + mxUtils.br(container); + wrapper3.style.height = '70'; + } + + var tablePanel2 = stylePanel.cloneNode(false); + tablePanel2.style.paddingLeft = '0px'; + + var btns = [ + this.editorUi.toolbar.addButton('geSprite-strokecolor', mxResources.get('borderColor'), + mxUtils.bind(this, function() + { + if (currentTable != null) + { + // Converts rgb(r,g,b) values + var color = currentTable.style.borderColor.replace( + /\brgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/g, + function($0, $1, $2, $3) { + return "#" + ("0"+Number($1).toString(16)).substr(-2) + ("0"+Number($2).toString(16)).substr(-2) + ("0"+Number($3).toString(16)).substr(-2); + }); + this.editorUi.pickColor(color, function(newColor) + { + if (newColor == null || newColor == mxConstants.NONE) + { + currentTable.removeAttribute('border'); + currentTable.style.border = ''; + currentTable.style.borderCollapse = ''; + } + else + { + currentTable.setAttribute('border', '1'); + currentTable.style.border = '1px solid ' + newColor; + currentTable.style.borderCollapse = 'collapse'; + } + }); + } + }), tablePanel2), + this.editorUi.toolbar.addButton('geSprite-fillcolor', mxResources.get('backgroundColor'), + mxUtils.bind(this, function() + { + // Converts rgb(r,g,b) values + if (currentTable != null) + { + var color = currentTable.style.backgroundColor.replace( + /\brgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/g, + function($0, $1, $2, $3) { + return "#" + ("0"+Number($1).toString(16)).substr(-2) + ("0"+Number($2).toString(16)).substr(-2) + ("0"+Number($3).toString(16)).substr(-2); + }); + this.editorUi.pickColor(color, function(newColor) + { + if (newColor == null || newColor == mxConstants.NONE) + { + currentTable.style.backgroundColor = ''; + } + else + { + currentTable.style.backgroundColor = newColor; + } + }); + } + }), tablePanel2), + this.editorUi.toolbar.addButton('geSprite-fit', mxResources.get('spacing'), + function() + { + if (currentTable != null) + { + var value = currentTable.getAttribute('cellPadding') || 0; + + var dlg = new FilenameDialog(ui, value, mxResources.get('apply'), mxUtils.bind(this, function(newValue) + { + if (newValue != null && newValue.length > 0) + { + currentTable.setAttribute('cellPadding', newValue); + } + else + { + currentTable.removeAttribute('cellPadding'); + } + }), mxResources.get('spacing')); + ui.showDialog(dlg.container, 300, 80, true, true); + dlg.init(); + } + }, tablePanel2), + this.editorUi.toolbar.addButton('geSprite-left', mxResources.get('left'), + function() + { + if (currentTable != null) + { + currentTable.setAttribute('align', 'left'); + } + }, tablePanel2), + this.editorUi.toolbar.addButton('geSprite-center', mxResources.get('center'), + function() + { + if (currentTable != null) + { + currentTable.setAttribute('align', 'center'); + } + }, tablePanel2), + this.editorUi.toolbar.addButton('geSprite-right', mxResources.get('right'), + function() + { + if (currentTable != null) + { + currentTable.setAttribute('align', 'right'); + } + }, tablePanel2)]; + this.styleButtons(btns); + btns[2].style.marginRight = '9px'; + + if (mxClient.IS_QUIRKS) + { + mxUtils.br(wrapper3); + mxUtils.br(wrapper3); + } + + wrapper3.appendChild(tablePanel2); + container.appendChild(wrapper3); + + tableWrapper = wrapper3; + } + + function setSelected(elt, selected) + { + if (mxClient.IS_IE && (mxClient.IS_QUIRKS || document.documentMode < 10)) + { + elt.style.filter = (selected) ? 'progid:DXImageTransform.Microsoft.Gradient('+ + 'StartColorStr=\'#c5ecff\', EndColorStr=\'#87d4fb\', GradientType=0)' : ''; + } + else + { + elt.style.backgroundImage = (selected) ? 'linear-gradient(#c5ecff 0px,#87d4fb 100%)' : ''; + } + }; + + var listener = mxUtils.bind(this, function(sender, evt, force) + { + ss = this.format.getSelectionState(); + var fontStyle = mxUtils.getValue(ss.style, mxConstants.STYLE_FONTSTYLE, 0); + setSelected(fontStyleItems[0], (fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD); + setSelected(fontStyleItems[1], (fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC); + setSelected(fontStyleItems[2], (fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE); + fontMenu.firstChild.nodeValue = mxUtils.htmlEntities(mxUtils.getValue(ss.style, mxConstants.STYLE_FONTFAMILY, Menus.prototype.defaultFont)); + + setSelected(verticalItem, mxUtils.getValue(ss.style, mxConstants.STYLE_HORIZONTAL, '1') == '0'); + + if (force || document.activeElement != input) + { + var tmp = parseFloat(mxUtils.getValue(ss.style, mxConstants.STYLE_FONTSIZE, Menus.prototype.defaultFontSize)); + input.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + var align = mxUtils.getValue(ss.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER); + setSelected(left, align == mxConstants.ALIGN_LEFT); + setSelected(center, align == mxConstants.ALIGN_CENTER); + setSelected(right, align == mxConstants.ALIGN_RIGHT); + + var valign = mxUtils.getValue(ss.style, mxConstants.STYLE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE); + setSelected(top, valign == mxConstants.ALIGN_TOP); + setSelected(middle, valign == mxConstants.ALIGN_MIDDLE); + setSelected(bottom, valign == mxConstants.ALIGN_BOTTOM); + + var pos = mxUtils.getValue(ss.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER); + var vpos = mxUtils.getValue(ss.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE); + + if (pos == mxConstants.ALIGN_LEFT && vpos == mxConstants.ALIGN_TOP) + { + positionSelect.value = 'topLeft'; + } + else if (pos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_TOP) + { + positionSelect.value = 'top'; + } + else if (pos == mxConstants.ALIGN_RIGHT && vpos == mxConstants.ALIGN_TOP) + { + positionSelect.value = 'topRight'; + } + else if (pos == mxConstants.ALIGN_LEFT && vpos == mxConstants.ALIGN_BOTTOM) + { + positionSelect.value = 'bottomLeft'; + } + else if (pos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_BOTTOM) + { + positionSelect.value = 'bottom'; + } + else if (pos == mxConstants.ALIGN_RIGHT && vpos == mxConstants.ALIGN_BOTTOM) + { + positionSelect.value = 'bottomRight'; + } + else if (pos == mxConstants.ALIGN_LEFT) + { + positionSelect.value = 'left'; + } + else if (pos == mxConstants.ALIGN_RIGHT) + { + positionSelect.value = 'right'; + } + else + { + positionSelect.value = 'center'; + } + + var dir = mxUtils.getValue(ss.style, mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION); + + if (dir == mxConstants.TEXT_DIRECTION_RTL) + { + dirSelect.value = 'rightToLeft'; + } + else if (dir == mxConstants.TEXT_DIRECTION_LTR) + { + dirSelect.value = 'leftToRight'; + } + else if (dir == mxConstants.TEXT_DIRECTION_AUTO) + { + dirSelect.value = 'automatic'; + } + + if (force || document.activeElement != globalSpacing) + { + var tmp = parseFloat(mxUtils.getValue(ss.style, mxConstants.STYLE_SPACING, 2)); + globalSpacing.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + if (force || document.activeElement != topSpacing) + { + var tmp = parseFloat(mxUtils.getValue(ss.style, mxConstants.STYLE_SPACING_TOP, 0)); + topSpacing.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + if (force || document.activeElement != rightSpacing) + { + var tmp = parseFloat(mxUtils.getValue(ss.style, mxConstants.STYLE_SPACING_RIGHT, 0)); + rightSpacing.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + if (force || document.activeElement != bottomSpacing) + { + var tmp = parseFloat(mxUtils.getValue(ss.style, mxConstants.STYLE_SPACING_BOTTOM, 0)); + bottomSpacing.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + if (force || document.activeElement != leftSpacing) + { + var tmp = parseFloat(mxUtils.getValue(ss.style, mxConstants.STYLE_SPACING_LEFT, 0)); + leftSpacing.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + }); + + globalUpdate = this.installInputHandler(globalSpacing, mxConstants.STYLE_SPACING, 2, -999, 999, ' pt'); + topUpdate = this.installInputHandler(topSpacing, mxConstants.STYLE_SPACING_TOP, 0, -999, 999, ' pt'); + rightUpdate = this.installInputHandler(rightSpacing, mxConstants.STYLE_SPACING_RIGHT, 0, -999, 999, ' pt'); + bottomUpdate = this.installInputHandler(bottomSpacing, mxConstants.STYLE_SPACING_BOTTOM, 0, -999, 999, ' pt'); + leftUpdate = this.installInputHandler(leftSpacing, mxConstants.STYLE_SPACING_LEFT, 0, -999, 999, ' pt'); + + this.addKeyHandler(input, listener); + this.addKeyHandler(globalSpacing, listener); + this.addKeyHandler(topSpacing, listener); + this.addKeyHandler(rightSpacing, listener); + this.addKeyHandler(bottomSpacing, listener); + this.addKeyHandler(leftSpacing, listener); + + graph.getModel().addListener(mxEvent.CHANGE, listener); + this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }}); + listener(); + + if (graph.cellEditor.isContentEditing()) + { + var updating = false; + + var updateCssHandler = function() + { + if (!updating) + { + updating = true; + + window.setTimeout(function() + { + var selectedElement = graph.getSelectedElement(); + var node = selectedElement; + + while (node != null && node.nodeType != mxConstants.NODETYPE_ELEMENT) + { + node = node.parentNode; + } + + if (node != null) + { + // Workaround for commonAncestor on range in IE11 returning parent of common ancestor + if (node == graph.cellEditor.textarea && graph.cellEditor.textarea.children.length == 1 && + graph.cellEditor.textarea.firstChild.nodeType == mxConstants.NODETYPE_ELEMENT) + { + node = graph.cellEditor.textarea.firstChild; + } + + function getRelativeLineHeight(fontSize, lineHeight, elt) + { + if (elt.style.lineHeight.substring(elt.style.lineHeight.length - 1) == '%') + { + return parseInt(elt.style.lineHeight) / 100; + } + else + { + return (lineHeight.substring(lineHeight.length - 2) == 'px') ? + parseFloat(lineHeight) / fontSize : parseInt(lineHeight); + } + }; + + function getAbsoluteFontSize(fontSize) + { + if (fontSize.substring(fontSize.length - 2) == 'px') + { + return parseFloat(fontSize); + } + else + { + return mxConstants.DEFAULT_FONTSIZE; + } + } + + //var realCss = mxUtils.getCurrentStyle(selectedElement); + var css = mxUtils.getCurrentStyle(node); + var fontSize = getAbsoluteFontSize(css.fontSize); + var lineHeight = getRelativeLineHeight(fontSize, css.lineHeight, node); + + // Finds common font size + var elts = node.getElementsByTagName('*'); + + // IE does not support containsNode + if (elts.length > 0 && window.getSelection && !mxClient.IS_IE && !mxClient.IS_IE11) + { + var selection = window.getSelection(); + + for (var i = 0; i < elts.length; i++) + { + if (selection.containsNode(elts[i], true)) + { + temp = mxUtils.getCurrentStyle(elts[i]); + fontSize = Math.max(getAbsoluteFontSize(temp.fontSize), fontSize); + var lh = getRelativeLineHeight(fontSize, temp.lineHeight, elts[i]); + + if (lh != lineHeight || isNaN(lh)) + { + lineHeight = ''; + } + } + } + } + + if (css != null) + { + setSelected(fontStyleItems[0], css.fontWeight == 'bold' || graph.getParentByName(node, 'B', graph.cellEditor.textarea) != null); + setSelected(fontStyleItems[1], css.fontStyle == 'italic' || graph.getParentByName(node, 'I', graph.cellEditor.textarea) != null); + setSelected(fontStyleItems[2], graph.getParentByName(node, 'U', graph.cellEditor.textarea) != null); + setSelected(left, css.textAlign == 'left'); + setSelected(center, css.textAlign == 'center'); + setSelected(right, css.textAlign == 'right'); + setSelected(full, css.textAlign == 'justify'); + setSelected(sup, graph.getParentByName(node, 'SUP', graph.cellEditor.textarea) != null); + setSelected(sub, graph.getParentByName(node, 'SUB', graph.cellEditor.textarea) != null); + + currentTable = graph.getParentByName(node, 'TABLE', graph.cellEditor.textarea); + tableRow = (currentTable == null) ? null : graph.getParentByName(node, 'TR', currentTable); + tableCell = (currentTable == null) ? null : graph.getParentByName(node, 'TD', currentTable); + tableWrapper.style.display = (currentTable != null) ? '' : 'none'; + + if (document.activeElement != input) + { + if (node.nodeName == 'FONT' && node.getAttribute('size') == '4' && + pendingFontSize != null) + { + node.removeAttribute('size'); + node.style.fontSize = pendingFontSize + ' pt'; + pendingFontSize = null; + } + else + { + input.value = (isNaN(fontSize)) ? '' : fontSize + ' pt'; + } + + var lh = parseFloat(lineHeight); + + if (!isNaN(lh)) + { + lineHeightInput.value = Math.round(lh * 100) + ' %'; + } + else + { + lineHeightInput.value = '100 %'; + } + } + + // Converts rgb(r,g,b) values + var color = css.color.replace( + /\brgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/g, + function($0, $1, $2, $3) { + return "#" + ("0"+Number($1).toString(16)).substr(-2) + ("0"+Number($2).toString(16)).substr(-2) + ("0"+Number($3).toString(16)).substr(-2); + }); + var color2 = css.backgroundColor.replace( + /\brgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/g, + function($0, $1, $2, $3) { + return "#" + ("0"+Number($1).toString(16)).substr(-2) + ("0"+Number($2).toString(16)).substr(-2) + ("0"+Number($3).toString(16)).substr(-2); + }); + + // Updates the color picker for the current font + if (fontColorApply != null) + { + if (color.charAt(0) == '#') + { + currentFontColor = color; + } + else + { + currentFontColor = '#000000'; + } + + fontColorApply(currentFontColor, true); + } + + if (bgColorApply != null) + { + if (color2.charAt(0) == '#') + { + currentBgColor = color2; + } + else + { + currentBgColor = null; + } + + bgColorApply(currentBgColor, true); + } + + // Workaround for firstChild is null or not an object + // in the log which seems to be IE8- only / 29.01.15 + if (fontMenu.firstChild != null) + { + // Strips leading and trailing quotes + var ff = css.fontFamily; + + if (ff.charAt(0) == '\'') + { + ff = ff.substring(1); + } + + if (ff.charAt(ff.length - 1) == '\'') + { + ff = ff.substring(0, ff.length - 1); + } + + if (ff.charAt(0) == '"') + { + ff = ff.substring(1); + } + + if (ff.charAt(ff.length - 1) == '"') + { + ff = ff.substring(0, ff.length - 1); + } + + fontMenu.firstChild.nodeValue = ff; + } + } + } + + updating = false; + }, 0); + } + }; + + mxEvent.addListener(graph.cellEditor.textarea, 'input', updateCssHandler) + mxEvent.addListener(graph.cellEditor.textarea, 'touchend', updateCssHandler); + mxEvent.addListener(graph.cellEditor.textarea, 'mouseup', updateCssHandler); + mxEvent.addListener(graph.cellEditor.textarea, 'keyup', updateCssHandler); + this.listeners.push({destroy: function() + { + // No need to remove listener since textarea is destroyed after edit + }}); + updateCssHandler(); + } + + return container; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +StyleFormatPanel = function(format, editorUi, container) +{ + BaseFormatPanel.call(this, format, editorUi, container); + this.init(); +}; + +mxUtils.extend(StyleFormatPanel, BaseFormatPanel); + +/** + * + */ +StyleFormatPanel.prototype.defaultStrokeColor = 'black'; + +/** + * Adds the label menu items to the given menu and parent. + */ +StyleFormatPanel.prototype.init = function() +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + var ss = this.format.getSelectionState(); + + if (ss.containsImage && ss.vertices.length == 1 && ss.style.shape == 'image' && + ss.style.image != null && ss.style.image.substring(0, 19) == 'data:image/svg+xml;') + { + this.container.appendChild(this.addSvgStyles(this.createPanel())); + } + + if (!ss.containsImage || ss.style.shape == 'image') + { + this.container.appendChild(this.addFill(this.createPanel())); + } + + this.container.appendChild(this.addStroke(this.createPanel())); + this.container.appendChild(this.addLineJumps(this.createPanel())); + var opacityPanel = this.createRelativeOption(mxResources.get('opacity'), mxConstants.STYLE_OPACITY, 41); + opacityPanel.style.paddingTop = '8px'; + opacityPanel.style.paddingBottom = '8px'; + this.container.appendChild(opacityPanel); + this.container.appendChild(this.addEffects(this.createPanel())); + var opsPanel = this.addEditOps(this.createPanel()); + + if (opsPanel.firstChild != null) + { + mxUtils.br(opsPanel); + } + + this.container.appendChild(this.addStyleOps(opsPanel)); +}; + +/** + * Use browser for parsing CSS. + */ +StyleFormatPanel.prototype.getCssRules = function(css) +{ + var doc = document.implementation.createHTMLDocument(''); + var styleElement = document.createElement('style'); + + mxUtils.setTextContent(styleElement, css); + doc.body.appendChild(styleElement); + + return styleElement.sheet.cssRules; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +StyleFormatPanel.prototype.addSvgStyles = function(container) +{ + var ui = this.editorUi; + var graph = ui.editor.graph; + var ss = this.format.getSelectionState(); + container.style.paddingTop = '6px'; + container.style.paddingBottom = '6px'; + container.style.fontWeight = 'bold'; + container.style.display = 'none'; + + try + { + var exp = ss.style.editableCssRules; + + if (exp != null) + { + var regex = new RegExp(exp); + + var data = ss.style.image.substring(ss.style.image.indexOf(',') + 1); + var xml = (window.atob) ? atob(data) : Base64.decode(data, true); + var svg = mxUtils.parseXml(xml); + + if (svg != null) + { + var styles = svg.getElementsByTagName('style'); + + for (var i = 0; i < styles.length; i++) + { + var rules = this.getCssRules(mxUtils.getTextContent(styles[i])); + + for (var j = 0; j < rules.length; j++) + { + this.addSvgRule(container, rules[j], svg, styles[i], rules, j, regex); + } + } + } + } + } + catch (e) + { + // ignore + } + + return container; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +StyleFormatPanel.prototype.addSvgRule = function(container, rule, svg, styleElem, rules, ruleIndex, regex) +{ + var ui = this.editorUi; + var graph = ui.editor.graph; + + if (regex.test(rule.selectorText)) + { + function rgb2hex(rgb) + { + rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); + + return (rgb && rgb.length === 4) ? "#" + + ("0" + parseInt(rgb[1],10).toString(16)).slice(-2) + + ("0" + parseInt(rgb[2],10).toString(16)).slice(-2) + + ("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : ''; + }; + + var addStyleRule = mxUtils.bind(this, function(rule, key, label) + { + if (rule.style[key] != '') + { + var option = this.createColorOption(label + ' ' + rule.selectorText, function() + { + return rgb2hex(rule.style[key]); + }, function(color) + { + rules[ruleIndex].style[key] = color; + var cssTxt = ''; + + for (var i = 0; i < rules.length; i++) + { + cssTxt += rules[i].cssText + ' '; + } + + styleElem.textContent = cssTxt; + var xml = mxUtils.getXml(svg.documentElement); + + graph.setCellStyles(mxConstants.STYLE_IMAGE, 'data:image/svg+xml,' + + ((window.btoa) ? btoa(xml) : Base64.encode(xml, true)), + graph.getSelectionCells()); + }, '#ffffff', + { + install: function(apply) + { + // ignore + }, + destroy: function() + { + // ignore + } + }); + + container.appendChild(option); + + // Shows container if rules are added + container.style.display = ''; + } + }); + + addStyleRule(rule, 'fill', mxResources.get('fill')); + addStyleRule(rule, 'stroke', mxResources.get('line')); + } +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +StyleFormatPanel.prototype.addEditOps = function(div) +{ + var ss = this.format.getSelectionState(); + var btn = null; + + if (this.editorUi.editor.graph.getSelectionCount() == 1) + { + btn = mxUtils.button(mxResources.get('editStyle'), mxUtils.bind(this, function(evt) + { + this.editorUi.actions.get('editStyle').funct(); + })); + + btn.setAttribute('title', mxResources.get('editStyle') + ' (' + this.editorUi.actions.get('editStyle').shortcut + ')'); + btn.style.width = '202px'; + btn.style.marginBottom = '2px'; + + div.appendChild(btn); + } + + if (ss.image) + { + var btn2 = mxUtils.button(mxResources.get('editImage'), mxUtils.bind(this, function(evt) + { + this.editorUi.actions.get('image').funct(); + })); + + btn2.setAttribute('title', mxResources.get('editImage')); + btn2.style.marginBottom = '2px'; + + if (btn == null) + { + btn2.style.width = '202px'; + } + else + { + btn.style.width = '100px'; + btn2.style.width = '100px'; + btn2.style.marginLeft = '2px'; + } + + div.appendChild(btn2); + } + + return div; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +StyleFormatPanel.prototype.addFill = function(container) +{ + var ui = this.editorUi; + var graph = ui.editor.graph; + var ss = this.format.getSelectionState(); + container.style.paddingTop = '6px'; + container.style.paddingBottom = '6px'; + + // Adds gradient direction option + var gradientSelect = document.createElement('select'); + gradientSelect.style.position = 'absolute'; + gradientSelect.style.marginTop = '-2px'; + gradientSelect.style.right = (mxClient.IS_QUIRKS) ? '52px' : '72px'; + gradientSelect.style.width = '70px'; + + // Stops events from bubbling to color option event handler + mxEvent.addListener(gradientSelect, 'click', function(evt) + { + mxEvent.consume(evt); + }); + + var gradientPanel = this.createCellColorOption(mxResources.get('gradient'), mxConstants.STYLE_GRADIENTCOLOR, '#ffffff', function(color) + { + if (color == null || color == mxConstants.NONE) + { + gradientSelect.style.display = 'none'; + } + else + { + gradientSelect.style.display = ''; + } + }); + + var fillKey = (ss.style.shape == 'image') ? mxConstants.STYLE_IMAGE_BACKGROUND : mxConstants.STYLE_FILLCOLOR; + var label = (ss.style.shape == 'image') ? mxResources.get('background') : mxResources.get('fill'); + + var fillPanel = this.createCellColorOption(label, fillKey, '#ffffff'); + fillPanel.style.fontWeight = 'bold'; + + var tmpColor = mxUtils.getValue(ss.style, fillKey, null); + gradientPanel.style.display = (tmpColor != null && tmpColor != mxConstants.NONE && + ss.fill && ss.style.shape != 'image') ? '' : 'none'; + + var directions = [mxConstants.DIRECTION_NORTH, mxConstants.DIRECTION_EAST, + mxConstants.DIRECTION_SOUTH, mxConstants.DIRECTION_WEST]; + + for (var i = 0; i < directions.length; i++) + { + var gradientOption = document.createElement('option'); + gradientOption.setAttribute('value', directions[i]); + mxUtils.write(gradientOption, mxResources.get(directions[i])); + gradientSelect.appendChild(gradientOption); + } + + gradientPanel.appendChild(gradientSelect); + + var listener = mxUtils.bind(this, function() + { + ss = this.format.getSelectionState(); + var value = mxUtils.getValue(ss.style, mxConstants.STYLE_GRADIENT_DIRECTION, mxConstants.DIRECTION_SOUTH); + + // Handles empty string which is not allowed as a value + if (value == '') + { + value = mxConstants.DIRECTION_SOUTH; + } + + gradientSelect.value = value; + container.style.display = (ss.fill) ? '' : 'none'; + + var fillColor = mxUtils.getValue(ss.style, mxConstants.STYLE_FILLCOLOR, null); + + if (!ss.fill || ss.containsImage || fillColor == null || fillColor == mxConstants.NONE || ss.style.shape == 'filledEdge') + { + gradientPanel.style.display = 'none'; + } + else + { + gradientPanel.style.display = ''; + } + }); + + graph.getModel().addListener(mxEvent.CHANGE, listener); + this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }}); + listener(); + + mxEvent.addListener(gradientSelect, 'change', function(evt) + { + graph.setCellStyles(mxConstants.STYLE_GRADIENT_DIRECTION, gradientSelect.value, graph.getSelectionCells()); + mxEvent.consume(evt); + }); + + container.appendChild(fillPanel); + container.appendChild(gradientPanel); + + // Adds custom colors + var custom = this.getCustomColors(); + + for (var i = 0; i < custom.length; i++) + { + container.appendChild(this.createCellColorOption(custom[i].title, custom[i].key, custom[i].defaultValue)); + } + + return container; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +StyleFormatPanel.prototype.getCustomColors = function() +{ + var ss = this.format.getSelectionState(); + var result = []; + + if (ss.style.shape == 'swimlane') + { + result.push({title: mxResources.get('laneColor'), key: 'swimlaneFillColor', defaultValue: '#ffffff'}); + } + + return result; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +StyleFormatPanel.prototype.addStroke = function(container) +{ + var ui = this.editorUi; + var graph = ui.editor.graph; + var ss = this.format.getSelectionState(); + + container.style.paddingTop = '4px'; + container.style.paddingBottom = '4px'; + container.style.whiteSpace = 'normal'; + + var colorPanel = document.createElement('div'); + colorPanel.style.fontWeight = 'bold'; + + // Adds gradient direction option + var styleSelect = document.createElement('select'); + styleSelect.style.position = 'absolute'; + styleSelect.style.marginTop = '-2px'; + styleSelect.style.right = '72px'; + styleSelect.style.width = '80px'; + + var styles = ['sharp', 'rounded', 'curved']; + + for (var i = 0; i < styles.length; i++) + { + var styleOption = document.createElement('option'); + styleOption.setAttribute('value', styles[i]); + mxUtils.write(styleOption, mxResources.get(styles[i])); + styleSelect.appendChild(styleOption); + } + + mxEvent.addListener(styleSelect, 'change', function(evt) + { + graph.getModel().beginUpdate(); + try + { + var keys = [mxConstants.STYLE_ROUNDED, mxConstants.STYLE_CURVED]; + // Default for rounded is 1 + var values = ['0', null]; + + if (styleSelect.value == 'rounded') + { + values = ['1', null]; + } + else if (styleSelect.value == 'curved') + { + values = [null, '1']; + } + + for (var i = 0; i < keys.length; i++) + { + graph.setCellStyles(keys[i], values[i], graph.getSelectionCells()); + } + + ui.fireEvent(new mxEventObject('styleChanged', 'keys', keys, + 'values', values, 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + + mxEvent.consume(evt); + }); + + // Stops events from bubbling to color option event handler + mxEvent.addListener(styleSelect, 'click', function(evt) + { + mxEvent.consume(evt); + }); + + var strokeKey = (ss.style.shape == 'image') ? mxConstants.STYLE_IMAGE_BORDER : mxConstants.STYLE_STROKECOLOR; + var label = (ss.style.shape == 'image') ? mxResources.get('border') : mxResources.get('line'); + + var lineColor = this.createCellColorOption(label, strokeKey, '#000000'); + lineColor.appendChild(styleSelect); + colorPanel.appendChild(lineColor); + + // Used if only edges selected + var stylePanel = colorPanel.cloneNode(false); + stylePanel.style.fontWeight = 'normal'; + stylePanel.style.whiteSpace = 'nowrap'; + stylePanel.style.position = 'relative'; + stylePanel.style.paddingLeft = '16px' + stylePanel.style.marginBottom = '2px'; + stylePanel.style.marginTop = '2px'; + stylePanel.className = 'geToolbarContainer'; + + var addItem = mxUtils.bind(this, function(menu, width, cssName, keys, values) + { + var item = this.editorUi.menus.styleChange(menu, '', keys, values, 'geIcon', null); + + var pat = document.createElement('div'); + pat.style.width = width + 'px'; + pat.style.height = '1px'; + pat.style.borderBottom = '1px ' + cssName + ' ' + this.defaultStrokeColor; + pat.style.paddingTop = '6px'; + + item.firstChild.firstChild.style.padding = '0px 4px 0px 4px'; + item.firstChild.firstChild.style.width = width + 'px'; + item.firstChild.firstChild.appendChild(pat); + + return item; + }); + + var pattern = this.editorUi.toolbar.addMenuFunctionInContainer(stylePanel, 'geSprite-orthogonal', mxResources.get('pattern'), false, mxUtils.bind(this, function(menu) + { + addItem(menu, 75, 'solid', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], [null, null]).setAttribute('title', mxResources.get('solid')); + addItem(menu, 75, 'dashed', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], ['1', null]).setAttribute('title', mxResources.get('dashed')); + addItem(menu, 75, 'dotted', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], ['1', '1 1']).setAttribute('title', mxResources.get('dotted') + ' (1)'); + addItem(menu, 75, 'dotted', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], ['1', '1 2']).setAttribute('title', mxResources.get('dotted') + ' (2)'); + addItem(menu, 75, 'dotted', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], ['1', '1 4']).setAttribute('title', mxResources.get('dotted') + ' (3)'); + })); + + // Used for mixed selection (vertices and edges) + var altStylePanel = stylePanel.cloneNode(false); + + var edgeShape = this.editorUi.toolbar.addMenuFunctionInContainer(altStylePanel, 'geSprite-connection', mxResources.get('connection'), false, mxUtils.bind(this, function(menu) + { + this.editorUi.menus.styleChange(menu, '', [mxConstants.STYLE_SHAPE, mxConstants.STYLE_STARTSIZE, mxConstants.STYLE_ENDSIZE, 'width'], [null, null, null, null], 'geIcon geSprite geSprite-connection', null, true).setAttribute('title', mxResources.get('line')); + this.editorUi.menus.styleChange(menu, '', [mxConstants.STYLE_SHAPE, mxConstants.STYLE_STARTSIZE, mxConstants.STYLE_ENDSIZE, 'width'], ['link', null, null, null], 'geIcon geSprite geSprite-linkedge', null, true).setAttribute('title', mxResources.get('link')); + this.editorUi.menus.styleChange(menu, '', [mxConstants.STYLE_SHAPE, mxConstants.STYLE_STARTSIZE, mxConstants.STYLE_ENDSIZE, 'width'], ['flexArrow', null, null, null], 'geIcon geSprite geSprite-arrow', null, true).setAttribute('title', mxResources.get('arrow')); + this.editorUi.menus.styleChange(menu, '', [mxConstants.STYLE_SHAPE, mxConstants.STYLE_STARTSIZE, mxConstants.STYLE_ENDSIZE, 'width'], ['arrow', null, null, null], 'geIcon geSprite geSprite-simplearrow', null, true).setAttribute('title', mxResources.get('simpleArrow')); + })); + + var altPattern = this.editorUi.toolbar.addMenuFunctionInContainer(altStylePanel, 'geSprite-orthogonal', mxResources.get('pattern'), false, mxUtils.bind(this, function(menu) + { + addItem(menu, 33, 'solid', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], [null, null]).setAttribute('title', mxResources.get('solid')); + addItem(menu, 33, 'dashed', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], ['1', null]).setAttribute('title', mxResources.get('dashed')); + addItem(menu, 33, 'dotted', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], ['1', '1 1']).setAttribute('title', mxResources.get('dotted') + ' (1)'); + addItem(menu, 33, 'dotted', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], ['1', '1 2']).setAttribute('title', mxResources.get('dotted') + ' (2)'); + addItem(menu, 33, 'dotted', [mxConstants.STYLE_DASHED, mxConstants.STYLE_DASH_PATTERN], ['1', '1 4']).setAttribute('title', mxResources.get('dotted') + ' (3)'); + })); + + var stylePanel2 = stylePanel.cloneNode(false); + + // Stroke width + var input = document.createElement('input'); + input.style.textAlign = 'right'; + input.style.marginTop = '2px'; + input.style.width = '41px'; + input.setAttribute('title', mxResources.get('linewidth')); + + stylePanel.appendChild(input); + + var altInput = input.cloneNode(true); + altStylePanel.appendChild(altInput); + + function update(evt) + { + // Maximum stroke width is 999 + var value = parseInt(input.value); + value = Math.min(999, Math.max(1, (isNaN(value)) ? 1 : value)); + + if (value != mxUtils.getValue(ss.style, mxConstants.STYLE_STROKEWIDTH, 1)) + { + graph.setCellStyles(mxConstants.STYLE_STROKEWIDTH, value, graph.getSelectionCells()); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_STROKEWIDTH], + 'values', [value], 'cells', graph.getSelectionCells())); + } + + input.value = value + ' pt'; + mxEvent.consume(evt); + }; + + function altUpdate(evt) + { + // Maximum stroke width is 999 + var value = parseInt(altInput.value); + value = Math.min(999, Math.max(1, (isNaN(value)) ? 1 : value)); + + if (value != mxUtils.getValue(ss.style, mxConstants.STYLE_STROKEWIDTH, 1)) + { + graph.setCellStyles(mxConstants.STYLE_STROKEWIDTH, value, graph.getSelectionCells()); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_STROKEWIDTH], + 'values', [value], 'cells', graph.getSelectionCells())); + } + + altInput.value = value + ' pt'; + mxEvent.consume(evt); + }; + + var stepper = this.createStepper(input, update, 1, 9); + stepper.style.display = input.style.display; + stepper.style.marginTop = '2px'; + stylePanel.appendChild(stepper); + + var altStepper = this.createStepper(altInput, altUpdate, 1, 9); + altStepper.style.display = altInput.style.display; + altStepper.style.marginTop = '2px'; + altStylePanel.appendChild(altStepper); + + if (!mxClient.IS_QUIRKS) + { + input.style.position = 'absolute'; + input.style.right = '32px'; + input.style.height = '15px'; + stepper.style.right = '20px'; + + altInput.style.position = 'absolute'; + altInput.style.right = '32px'; + altInput.style.height = '15px'; + altStepper.style.right = '20px'; + } + else + { + input.style.height = '17px'; + altInput.style.height = '17px'; + } + + mxEvent.addListener(input, 'blur', update); + mxEvent.addListener(input, 'change', update); + + mxEvent.addListener(altInput, 'blur', altUpdate); + mxEvent.addListener(altInput, 'change', altUpdate); + + if (mxClient.IS_QUIRKS) + { + mxUtils.br(stylePanel2); + mxUtils.br(stylePanel2); + } + + var edgeStyle = this.editorUi.toolbar.addMenuFunctionInContainer(stylePanel2, 'geSprite-orthogonal', mxResources.get('waypoints'), false, mxUtils.bind(this, function(menu) + { + if (ss.style.shape != 'arrow') + { + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], [null, null, null], 'geIcon geSprite geSprite-straight', null, true).setAttribute('title', mxResources.get('straight')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['orthogonalEdgeStyle', null, null], 'geIcon geSprite geSprite-orthogonal', null, true).setAttribute('title', mxResources.get('orthogonal')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_ELBOW, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['elbowEdgeStyle', null, null, null], 'geIcon geSprite geSprite-horizontalelbow', null, true).setAttribute('title', mxResources.get('simple')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_ELBOW, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['elbowEdgeStyle', 'vertical', null, null], 'geIcon geSprite geSprite-verticalelbow', null, true).setAttribute('title', mxResources.get('simple')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_ELBOW, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['isometricEdgeStyle', null, null, null], 'geIcon geSprite geSprite-horizontalisometric', null, true).setAttribute('title', mxResources.get('isometric')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_ELBOW, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['isometricEdgeStyle', 'vertical', null, null], 'geIcon geSprite geSprite-verticalisometric', null, true).setAttribute('title', mxResources.get('isometric')); + + if (ss.style.shape == 'connector') + { + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['orthogonalEdgeStyle', '1', null], 'geIcon geSprite geSprite-curved', null, true).setAttribute('title', mxResources.get('curved')); + } + + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['entityRelationEdgeStyle', null, null], 'geIcon geSprite geSprite-entity', null, true).setAttribute('title', mxResources.get('entityRelation')); + } + })); + + var lineStart = this.editorUi.toolbar.addMenuFunctionInContainer(stylePanel2, 'geSprite-startclassic', mxResources.get('linestart'), false, mxUtils.bind(this, function(menu) + { + if (ss.style.shape == 'connector' || ss.style.shape == 'flexArrow' || ss.style.shape == 'filledEdge') + { + var item = this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.NONE, 0], 'geIcon', null, false); + item.setAttribute('title', mxResources.get('none')); + item.firstChild.firstChild.innerHTML = '' + mxUtils.htmlEntities(mxResources.get('none')) + ''; + + if (ss.style.shape == 'connector' || ss.style.shape == 'filledEdge') + { + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_CLASSIC, 1], 'geIcon geSprite geSprite-startclassic', null, false).setAttribute('title', mxResources.get('classic')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_CLASSIC_THIN, 1], 'geIcon geSprite geSprite-startclassicthin', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_OPEN, 0], 'geIcon geSprite geSprite-startopen', null, false).setAttribute('title', mxResources.get('openArrow')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_OPEN_THIN, 0], 'geIcon geSprite geSprite-startopenthin', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['openAsync', 0], 'geIcon geSprite geSprite-startopenasync', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_BLOCK, 1], 'geIcon geSprite geSprite-startblock', null, false).setAttribute('title', mxResources.get('block')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_BLOCK_THIN, 1], 'geIcon geSprite geSprite-startblockthin', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['async', 1], 'geIcon geSprite geSprite-startasync', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_OVAL, 1], 'geIcon geSprite geSprite-startoval', null, false).setAttribute('title', mxResources.get('oval')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_DIAMOND, 1], 'geIcon geSprite geSprite-startdiamond', null, false).setAttribute('title', mxResources.get('diamond')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_DIAMOND_THIN, 1], 'geIcon geSprite geSprite-startthindiamond', null, false).setAttribute('title', mxResources.get('diamondThin')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_CLASSIC, 0], 'geIcon geSprite geSprite-startclassictrans', null, false).setAttribute('title', mxResources.get('classic')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_CLASSIC_THIN, 0], 'geIcon geSprite geSprite-startclassicthintrans', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_BLOCK, 0], 'geIcon geSprite geSprite-startblocktrans', null, false).setAttribute('title', mxResources.get('block')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_BLOCK_THIN, 0], 'geIcon geSprite geSprite-startblockthintrans', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['async', 0], 'geIcon geSprite geSprite-startasynctrans', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_OVAL, 0], 'geIcon geSprite geSprite-startovaltrans', null, false).setAttribute('title', mxResources.get('oval')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_DIAMOND, 0], 'geIcon geSprite geSprite-startdiamondtrans', null, false).setAttribute('title', mxResources.get('diamond')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], [mxConstants.ARROW_DIAMOND_THIN, 0], 'geIcon geSprite geSprite-startthindiamondtrans', null, false).setAttribute('title', mxResources.get('diamondThin')); + + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['dash', 0], 'geIcon geSprite geSprite-startdash', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['cross', 0], 'geIcon geSprite geSprite-startcross', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['circlePlus', 0], 'geIcon geSprite geSprite-startcircleplus', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['circle', 1], 'geIcon geSprite geSprite-startcircle', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['ERone', 0], 'geIcon geSprite geSprite-starterone', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['ERmandOne', 0], 'geIcon geSprite geSprite-starteronetoone', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['ERmany', 0], 'geIcon geSprite geSprite-startermany', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['ERoneToMany', 0], 'geIcon geSprite geSprite-starteronetomany', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['ERzeroToOne', 1], 'geIcon geSprite geSprite-starteroneopt', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW, 'startFill'], ['ERzeroToMany', 1], 'geIcon geSprite geSprite-startermanyopt', null, false); + } + else + { + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_STARTARROW], [mxConstants.ARROW_BLOCK], 'geIcon geSprite geSprite-startblocktrans', null, false).setAttribute('title', mxResources.get('block')); + } + } + })); + + var lineEnd = this.editorUi.toolbar.addMenuFunctionInContainer(stylePanel2, 'geSprite-endclassic', mxResources.get('lineend'), false, mxUtils.bind(this, function(menu) + { + if (ss.style.shape == 'connector' || ss.style.shape == 'flexArrow' || ss.style.shape == 'filledEdge') + { + var item = this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.NONE, 0], 'geIcon', null, false); + item.setAttribute('title', mxResources.get('none')); + item.firstChild.firstChild.innerHTML = '' + mxUtils.htmlEntities(mxResources.get('none')) + ''; + + if (ss.style.shape == 'connector' || ss.style.shape == 'filledEdge') + { + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_CLASSIC, 1], 'geIcon geSprite geSprite-endclassic', null, false).setAttribute('title', mxResources.get('classic')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_CLASSIC_THIN, 1], 'geIcon geSprite geSprite-endclassicthin', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_OPEN, 0], 'geIcon geSprite geSprite-endopen', null, false).setAttribute('title', mxResources.get('openArrow')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_OPEN_THIN, 0], 'geIcon geSprite geSprite-endopenthin', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['openAsync', 0], 'geIcon geSprite geSprite-endopenasync', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_BLOCK, 1], 'geIcon geSprite geSprite-endblock', null, false).setAttribute('title', mxResources.get('block')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_BLOCK_THIN, 1], 'geIcon geSprite geSprite-endblockthin', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['async', 1], 'geIcon geSprite geSprite-endasync', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_OVAL, 1], 'geIcon geSprite geSprite-endoval', null, false).setAttribute('title', mxResources.get('oval')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_DIAMOND, 1], 'geIcon geSprite geSprite-enddiamond', null, false).setAttribute('title', mxResources.get('diamond')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_DIAMOND_THIN, 1], 'geIcon geSprite geSprite-endthindiamond', null, false).setAttribute('title', mxResources.get('diamondThin')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_CLASSIC, 0], 'geIcon geSprite geSprite-endclassictrans', null, false).setAttribute('title', mxResources.get('classic')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_CLASSIC_THIN, 0], 'geIcon geSprite geSprite-endclassicthintrans', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_BLOCK, 0], 'geIcon geSprite geSprite-endblocktrans', null, false).setAttribute('title', mxResources.get('block')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_BLOCK_THIN, 0], 'geIcon geSprite geSprite-endblockthintrans', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['async', 0], 'geIcon geSprite geSprite-endasynctrans', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_OVAL, 0], 'geIcon geSprite geSprite-endovaltrans', null, false).setAttribute('title', mxResources.get('oval')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_DIAMOND, 0], 'geIcon geSprite geSprite-enddiamondtrans', null, false).setAttribute('title', mxResources.get('diamond')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], [mxConstants.ARROW_DIAMOND_THIN, 0], 'geIcon geSprite geSprite-endthindiamondtrans', null, false).setAttribute('title', mxResources.get('diamondThin')); + + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['dash', 0], 'geIcon geSprite geSprite-enddash', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['cross', 0], 'geIcon geSprite geSprite-endcross', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['circlePlus', 0], 'geIcon geSprite geSprite-endcircleplus', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['circle', 1], 'geIcon geSprite geSprite-endcircle', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['ERone', 0], 'geIcon geSprite geSprite-enderone', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['ERmandOne', 0], 'geIcon geSprite geSprite-enderonetoone', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['ERmany', 0], 'geIcon geSprite geSprite-endermany', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['ERoneToMany', 0], 'geIcon geSprite geSprite-enderonetomany', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['ERzeroToOne', 1], 'geIcon geSprite geSprite-enderoneopt', null, false); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW, 'endFill'], ['ERzeroToMany', 1], 'geIcon geSprite geSprite-endermanyopt', null, false); + } + else + { + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_ENDARROW], [mxConstants.ARROW_BLOCK], 'geIcon geSprite geSprite-endblocktrans', null, false).setAttribute('title', mxResources.get('block')); + } + } + })); + + this.addArrow(edgeShape, 8); + this.addArrow(edgeStyle); + this.addArrow(lineStart); + this.addArrow(lineEnd); + + var symbol = this.addArrow(pattern, 9); + symbol.className = 'geIcon'; + symbol.style.width = '84px'; + + var altSymbol = this.addArrow(altPattern, 9); + altSymbol.className = 'geIcon'; + altSymbol.style.width = '22px'; + + var solid = document.createElement('div'); + solid.style.width = '85px'; + solid.style.height = '1px'; + solid.style.borderBottom = '1px solid ' + this.defaultStrokeColor; + solid.style.marginBottom = '9px'; + symbol.appendChild(solid); + + var altSolid = document.createElement('div'); + altSolid.style.width = '23px'; + altSolid.style.height = '1px'; + altSolid.style.borderBottom = '1px solid ' + this.defaultStrokeColor; + altSolid.style.marginBottom = '9px'; + altSymbol.appendChild(altSolid); + + pattern.style.height = '15px'; + altPattern.style.height = '15px'; + edgeShape.style.height = '15px'; + edgeStyle.style.height = '17px'; + lineStart.style.marginLeft = '3px'; + lineStart.style.height = '17px'; + lineEnd.style.marginLeft = '3px'; + lineEnd.style.height = '17px'; + + container.appendChild(colorPanel); + container.appendChild(altStylePanel); + container.appendChild(stylePanel); + + var arrowPanel = stylePanel.cloneNode(false); + arrowPanel.style.paddingBottom = '6px'; + arrowPanel.style.paddingTop = '4px'; + arrowPanel.style.fontWeight = 'normal'; + + var span = document.createElement('div'); + span.style.position = 'absolute'; + span.style.marginLeft = '3px'; + span.style.marginBottom = '12px'; + span.style.marginTop = '2px'; + span.style.fontWeight = 'normal'; + span.style.width = '76px'; + + mxUtils.write(span, mxResources.get('lineend')); + arrowPanel.appendChild(span); + + var endSpacingUpdate, endSizeUpdate; + var endSpacing = this.addUnitInput(arrowPanel, 'pt', 74, 33, function() + { + endSpacingUpdate.apply(this, arguments); + }); + var endSize = this.addUnitInput(arrowPanel, 'pt', 20, 33, function() + { + endSizeUpdate.apply(this, arguments); + }); + + mxUtils.br(arrowPanel); + + var spacer = document.createElement('div'); + spacer.style.height = '8px'; + arrowPanel.appendChild(spacer); + + span = span.cloneNode(false); + mxUtils.write(span, mxResources.get('linestart')); + arrowPanel.appendChild(span); + + var startSpacingUpdate, startSizeUpdate; + var startSpacing = this.addUnitInput(arrowPanel, 'pt', 74, 33, function() + { + startSpacingUpdate.apply(this, arguments); + }); + var startSize = this.addUnitInput(arrowPanel, 'pt', 20, 33, function() + { + startSizeUpdate.apply(this, arguments); + }); + + mxUtils.br(arrowPanel); + this.addLabel(arrowPanel, mxResources.get('spacing'), 74, 50); + this.addLabel(arrowPanel, mxResources.get('size'), 20, 50); + mxUtils.br(arrowPanel); + + var perimeterPanel = colorPanel.cloneNode(false); + perimeterPanel.style.fontWeight = 'normal'; + perimeterPanel.style.position = 'relative'; + perimeterPanel.style.paddingLeft = '16px' + perimeterPanel.style.marginBottom = '2px'; + perimeterPanel.style.marginTop = '6px'; + perimeterPanel.style.borderWidth = '0px'; + perimeterPanel.style.paddingBottom = '18px'; + + var span = document.createElement('div'); + span.style.position = 'absolute'; + span.style.marginLeft = '3px'; + span.style.marginBottom = '12px'; + span.style.marginTop = '1px'; + span.style.fontWeight = 'normal'; + span.style.width = '120px'; + mxUtils.write(span, mxResources.get('perimeter')); + perimeterPanel.appendChild(span); + + var perimeterUpdate; + var perimeterSpacing = this.addUnitInput(perimeterPanel, 'pt', 20, 41, function() + { + perimeterUpdate.apply(this, arguments); + }); + + if (ss.edges.length == graph.getSelectionCount()) + { + container.appendChild(stylePanel2); + + if (mxClient.IS_QUIRKS) + { + mxUtils.br(container); + mxUtils.br(container); + } + + container.appendChild(arrowPanel); + } + else if (ss.vertices.length == graph.getSelectionCount()) + { + if (mxClient.IS_QUIRKS) + { + mxUtils.br(container); + } + + container.appendChild(perimeterPanel); + } + + var listener = mxUtils.bind(this, function(sender, evt, force) + { + ss = this.format.getSelectionState(); + var color = mxUtils.getValue(ss.style, strokeKey, null); + + if (force || document.activeElement != input) + { + var tmp = parseInt(mxUtils.getValue(ss.style, mxConstants.STYLE_STROKEWIDTH, 1)); + input.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + if (force || document.activeElement != altInput) + { + var tmp = parseInt(mxUtils.getValue(ss.style, mxConstants.STYLE_STROKEWIDTH, 1)); + altInput.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + styleSelect.style.visibility = (ss.style.shape == 'connector' || ss.style.shape == 'filledEdge') ? '' : 'hidden'; + + if (mxUtils.getValue(ss.style, mxConstants.STYLE_CURVED, null) == '1') + { + styleSelect.value = 'curved'; + } + else if (mxUtils.getValue(ss.style, mxConstants.STYLE_ROUNDED, null) == '1') + { + styleSelect.value = 'rounded'; + } + + if (mxUtils.getValue(ss.style, mxConstants.STYLE_DASHED, null) == '1') + { + if (mxUtils.getValue(ss.style, mxConstants.STYLE_DASH_PATTERN, null) == null) + { + solid.style.borderBottom = '1px dashed ' + this.defaultStrokeColor; + } + else + { + solid.style.borderBottom = '1px dotted ' + this.defaultStrokeColor; + } + } + else + { + solid.style.borderBottom = '1px solid ' + this.defaultStrokeColor; + } + + altSolid.style.borderBottom = solid.style.borderBottom; + + // Updates toolbar icon for edge style + var edgeStyleDiv = edgeStyle.getElementsByTagName('div')[0]; + var es = mxUtils.getValue(ss.style, mxConstants.STYLE_EDGE, null); + + if (mxUtils.getValue(ss.style, mxConstants.STYLE_NOEDGESTYLE, null) == '1') + { + es = null; + } + + if (es == 'orthogonalEdgeStyle' && mxUtils.getValue(ss.style, mxConstants.STYLE_CURVED, null) == '1') + { + edgeStyleDiv.className = 'geSprite geSprite-curved'; + } + else if (es == 'straight' || es == 'none' || es == null) + { + edgeStyleDiv.className = 'geSprite geSprite-straight'; + } + else if (es == 'entityRelationEdgeStyle') + { + edgeStyleDiv.className = 'geSprite geSprite-entity'; + } + else if (es == 'elbowEdgeStyle') + { + edgeStyleDiv.className = 'geSprite ' + ((mxUtils.getValue(ss.style, + mxConstants.STYLE_ELBOW, null) == 'vertical') ? + 'geSprite-verticalelbow' : 'geSprite-horizontalelbow'); + } + else if (es == 'isometricEdgeStyle') + { + edgeStyleDiv.className = 'geSprite ' + ((mxUtils.getValue(ss.style, + mxConstants.STYLE_ELBOW, null) == 'vertical') ? + 'geSprite-verticalisometric' : 'geSprite-horizontalisometric'); + } + else + { + edgeStyleDiv.className = 'geSprite geSprite-orthogonal'; + } + + // Updates icon for edge shape + var edgeShapeDiv = edgeShape.getElementsByTagName('div')[0]; + + if (ss.style.shape == 'link') + { + edgeShapeDiv.className = 'geSprite geSprite-linkedge'; + } + else if (ss.style.shape == 'flexArrow') + { + edgeShapeDiv.className = 'geSprite geSprite-arrow'; + } + else if (ss.style.shape == 'arrow') + { + edgeShapeDiv.className = 'geSprite geSprite-simplearrow'; + } + else + { + edgeShapeDiv.className = 'geSprite geSprite-connection'; + } + + if (ss.edges.length == graph.getSelectionCount()) + { + altStylePanel.style.display = ''; + stylePanel.style.display = 'none'; + } + else + { + altStylePanel.style.display = 'none'; + stylePanel.style.display = ''; + } + + function updateArrow(marker, fill, elt, prefix) + { + var markerDiv = elt.getElementsByTagName('div')[0]; + + markerDiv.className = ui.getCssClassForMarker(prefix, ss.style.shape, marker, fill); + + if (markerDiv.className == 'geSprite geSprite-noarrow') + { + markerDiv.innerHTML = mxUtils.htmlEntities(mxResources.get('none')); + markerDiv.style.backgroundImage = 'none'; + markerDiv.style.verticalAlign = 'top'; + markerDiv.style.marginTop = '5px'; + markerDiv.style.fontSize = '10px'; + markerDiv.style.filter = 'none'; + markerDiv.style.color = this.defaultStrokeColor; + markerDiv.nextSibling.style.marginTop = '0px'; + } + + return markerDiv; + }; + + var sourceDiv = updateArrow(mxUtils.getValue(ss.style, mxConstants.STYLE_STARTARROW, null), + mxUtils.getValue(ss.style, 'startFill', '1'), lineStart, 'start'); + var targetDiv = updateArrow(mxUtils.getValue(ss.style, mxConstants.STYLE_ENDARROW, null), + mxUtils.getValue(ss.style, 'endFill', '1'), lineEnd, 'end'); + + // Special cases for markers + if (ss.style.shape == 'arrow') + { + sourceDiv.className = 'geSprite geSprite-noarrow'; + targetDiv.className = 'geSprite geSprite-endblocktrans'; + } + else if (ss.style.shape == 'link') + { + sourceDiv.className = 'geSprite geSprite-noarrow'; + targetDiv.className = 'geSprite geSprite-noarrow'; + } + + mxUtils.setOpacity(edgeStyle, (ss.style.shape == 'arrow') ? 30 : 100); + + if (ss.style.shape != 'connector' && ss.style.shape != 'flexArrow' && ss.style.shape != 'filledEdge') + { + mxUtils.setOpacity(lineStart, 30); + mxUtils.setOpacity(lineEnd, 30); + } + else + { + mxUtils.setOpacity(lineStart, 100); + mxUtils.setOpacity(lineEnd, 100); + } + + if (force || document.activeElement != startSize) + { + var tmp = parseInt(mxUtils.getValue(ss.style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_MARKERSIZE)); + startSize.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + if (force || document.activeElement != startSpacing) + { + var tmp = parseInt(mxUtils.getValue(ss.style, mxConstants.STYLE_SOURCE_PERIMETER_SPACING, 0)); + startSpacing.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + if (force || document.activeElement != endSize) + { + var tmp = parseInt(mxUtils.getValue(ss.style, mxConstants.STYLE_ENDSIZE, mxConstants.DEFAULT_MARKERSIZE)); + endSize.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + if (force || document.activeElement != startSpacing) + { + var tmp = parseInt(mxUtils.getValue(ss.style, mxConstants.STYLE_TARGET_PERIMETER_SPACING, 0)); + endSpacing.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + + if (force || document.activeElement != perimeterSpacing) + { + var tmp = parseInt(mxUtils.getValue(ss.style, mxConstants.STYLE_PERIMETER_SPACING, 0)); + perimeterSpacing.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + }); + + startSizeUpdate = this.installInputHandler(startSize, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_MARKERSIZE, 0, 999, ' pt'); + startSpacingUpdate = this.installInputHandler(startSpacing, mxConstants.STYLE_SOURCE_PERIMETER_SPACING, 0, -999, 999, ' pt'); + endSizeUpdate = this.installInputHandler(endSize, mxConstants.STYLE_ENDSIZE, mxConstants.DEFAULT_MARKERSIZE, 0, 999, ' pt'); + endSpacingUpdate = this.installInputHandler(endSpacing, mxConstants.STYLE_TARGET_PERIMETER_SPACING, 0, -999, 999, ' pt'); + perimeterUpdate = this.installInputHandler(perimeterSpacing, mxConstants.STYLE_PERIMETER_SPACING, 0, 0, 999, ' pt'); + + this.addKeyHandler(input, listener); + this.addKeyHandler(startSize, listener); + this.addKeyHandler(startSpacing, listener); + this.addKeyHandler(endSize, listener); + this.addKeyHandler(endSpacing, listener); + this.addKeyHandler(perimeterSpacing, listener); + + graph.getModel().addListener(mxEvent.CHANGE, listener); + this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }}); + listener(); + + return container; +}; + +/** + * Adds UI for configuring line jumps. + */ +StyleFormatPanel.prototype.addLineJumps = function(container) +{ + var ss = this.format.getSelectionState(); + + if (Graph.lineJumpsEnabled && ss.edges.length > 0 && + ss.vertices.length == 0 && ss.lineJumps) + { + container.style.padding = '8px 0px 24px 18px'; + + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + + var span = document.createElement('div'); + span.style.position = 'absolute'; + span.style.fontWeight = 'bold'; + span.style.width = '80px'; + + mxUtils.write(span, mxResources.get('lineJumps')); + container.appendChild(span); + + var styleSelect = document.createElement('select'); + styleSelect.style.position = 'absolute'; + styleSelect.style.marginTop = '-2px'; + styleSelect.style.right = '76px'; + styleSelect.style.width = '62px'; + + var styles = ['none', 'arc', 'gap', 'sharp']; + + for (var i = 0; i < styles.length; i++) + { + var styleOption = document.createElement('option'); + styleOption.setAttribute('value', styles[i]); + mxUtils.write(styleOption, mxResources.get(styles[i])); + styleSelect.appendChild(styleOption); + } + + mxEvent.addListener(styleSelect, 'change', function(evt) + { + graph.getModel().beginUpdate(); + try + { + graph.setCellStyles('jumpStyle', styleSelect.value, graph.getSelectionCells()); + ui.fireEvent(new mxEventObject('styleChanged', 'keys', ['jumpStyle'], + 'values', [styleSelect.value], 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + + mxEvent.consume(evt); + }); + + // Stops events from bubbling to color option event handler + mxEvent.addListener(styleSelect, 'click', function(evt) + { + mxEvent.consume(evt); + }); + + container.appendChild(styleSelect); + + var jumpSizeUpdate; + + var jumpSize = this.addUnitInput(container, 'pt', 22, 33, function() + { + jumpSizeUpdate.apply(this, arguments); + }); + + jumpSizeUpdate = this.installInputHandler(jumpSize, 'jumpSize', + Graph.defaultJumpSize, 0, 999, ' pt'); + + var listener = mxUtils.bind(this, function(sender, evt, force) + { + ss = this.format.getSelectionState(); + styleSelect.value = mxUtils.getValue(ss.style, 'jumpStyle', 'none'); + + if (force || document.activeElement != jumpSize) + { + var tmp = parseInt(mxUtils.getValue(ss.style, 'jumpSize', Graph.defaultJumpSize)); + jumpSize.value = (isNaN(tmp)) ? '' : tmp + ' pt'; + } + }); + + this.addKeyHandler(jumpSize, listener); + + graph.getModel().addListener(mxEvent.CHANGE, listener); + this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }}); + listener(); + } + else + { + container.style.display = 'none'; + } + + return container; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +StyleFormatPanel.prototype.addEffects = function(div) +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + var ss = this.format.getSelectionState(); + + div.style.paddingTop = '0px'; + div.style.paddingBottom = '2px'; + + var table = document.createElement('table'); + + if (mxClient.IS_QUIRKS) + { + table.style.fontSize = '1em'; + } + + table.style.width = '100%'; + table.style.fontWeight = 'bold'; + table.style.paddingRight = '20px'; + var tbody = document.createElement('tbody'); + var row = document.createElement('tr'); + row.style.padding = '0px'; + var left = document.createElement('td'); + left.style.padding = '0px'; + left.style.width = '50%'; + left.setAttribute('valign', 'top'); + + var right = left.cloneNode(true); + right.style.paddingLeft = '8px'; + row.appendChild(left); + row.appendChild(right); + tbody.appendChild(row); + table.appendChild(tbody); + div.appendChild(table); + + var current = left; + var count = 0; + + var addOption = mxUtils.bind(this, function(label, key, defaultValue) + { + var opt = this.createCellOption(label, key, defaultValue); + opt.style.width = '100%'; + current.appendChild(opt); + current = (current == left) ? right : left; + count++; + }); + + var listener = mxUtils.bind(this, function(sender, evt, force) + { + ss = this.format.getSelectionState(); + + left.innerHTML = ''; + right.innerHTML = ''; + current = left; + + if (ss.rounded) + { + addOption(mxResources.get('rounded'), mxConstants.STYLE_ROUNDED, 0); + } + + if (ss.style.shape == 'swimlane') + { + addOption(mxResources.get('divider'), 'swimlaneLine', 1); + } + + if (!ss.containsImage) + { + addOption(mxResources.get('shadow'), mxConstants.STYLE_SHADOW, 0); + } + + if (ss.glass) + { + addOption(mxResources.get('glass'), mxConstants.STYLE_GLASS, 0); + } + + if (ss.comic) + { + addOption(mxResources.get('comic'), 'comic', 0); + } + + if (count == 0) + { + div.style.display = 'none'; + } + }); + + graph.getModel().addListener(mxEvent.CHANGE, listener); + this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }}); + listener(); + + return div; +} + +/** + * Adds the label menu items to the given menu and parent. + */ +StyleFormatPanel.prototype.addStyleOps = function(div) +{ + div.style.paddingTop = '10px'; + div.style.paddingBottom = '10px'; + + var btn = mxUtils.button(mxResources.get('setAsDefaultStyle'), mxUtils.bind(this, function(evt) + { + this.editorUi.actions.get('setAsDefaultStyle').funct(); + })); + + btn.setAttribute('title', mxResources.get('setAsDefaultStyle') + ' (' + this.editorUi.actions.get('setAsDefaultStyle').shortcut + ')'); + btn.style.width = '202px'; + div.appendChild(btn); + + return div; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +DiagramFormatPanel = function(format, editorUi, container) +{ + BaseFormatPanel.call(this, format, editorUi, container); + this.init(); +}; + +mxUtils.extend(DiagramFormatPanel, BaseFormatPanel); + +/** + * Switch to disable page view. + */ +DiagramFormatPanel.showPageView = true; + +/** + * Specifies if the background image option should be shown. Default is true. + */ +DiagramFormatPanel.prototype.showBackgroundImageOption = true; + +/** + * Adds the label menu items to the given menu and parent. + */ +DiagramFormatPanel.prototype.init = function() +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + + this.container.appendChild(this.addView(this.createPanel())); + + if (graph.isEnabled()) + { + this.container.appendChild(this.addOptions(this.createPanel())); + this.container.appendChild(this.addPaperSize(this.createPanel())); + this.container.appendChild(this.addStyleOps(this.createPanel())); + } +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +DiagramFormatPanel.prototype.addView = function(div) +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + + div.appendChild(this.createTitle(mxResources.get('view'))); + + // Grid + this.addGridOption(div); + + if (graph.isEnabled()) + { + // Page View + if (DiagramFormatPanel.showPageView) + { + div.appendChild(this.createOption(mxResources.get('pageView'), function() + { + return graph.pageVisible; + }, function(checked) + { + ui.actions.get('pageView').funct(); + }, + { + install: function(apply) + { + this.listener = function() + { + apply(graph.pageVisible); + }; + + ui.addListener('pageViewChanged', this.listener); + }, + destroy: function() + { + ui.removeListener(this.listener); + } + })); + } + + // Background + var bg = this.createColorOption(mxResources.get('background'), function() + { + return graph.background; + }, function(color) + { + var change = new ChangePageSetup(ui, color); + change.ignoreImage = true; + + graph.model.execute(change); + }, '#ffffff', + { + install: function(apply) + { + this.listener = function() + { + apply(graph.background); + }; + + ui.addListener('backgroundColorChanged', this.listener); + }, + destroy: function() + { + ui.removeListener(this.listener); + } + }); + + if (this.showBackgroundImageOption) + { + var btn = mxUtils.button(mxResources.get('image'), function(evt) + { + ui.showBackgroundImageDialog(); + mxEvent.consume(evt); + }) + + btn.style.position = 'absolute'; + btn.className = 'geColorBtn'; + btn.style.marginTop = '-4px'; + btn.style.paddingBottom = (document.documentMode == 11 || mxClient.IS_MT) ? '0px' : '2px'; + btn.style.height = '22px'; + btn.style.right = (mxClient.IS_QUIRKS) ? '52px' : '72px'; + btn.style.width = '56px'; + + bg.appendChild(btn); + } + + div.appendChild(bg); + } + + return div; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +DiagramFormatPanel.prototype.addOptions = function(div) +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + + div.appendChild(this.createTitle(mxResources.get('options'))); + + if (graph.isEnabled()) + { + // Connection arrows + div.appendChild(this.createOption(mxResources.get('connectionArrows'), function() + { + return graph.connectionArrowsEnabled; + }, function(checked) + { + ui.actions.get('connectionArrows').funct(); + }, + { + install: function(apply) + { + this.listener = function() + { + apply(graph.connectionArrowsEnabled); + }; + + ui.addListener('connectionArrowsChanged', this.listener); + }, + destroy: function() + { + ui.removeListener(this.listener); + } + })); + + // Connection points + div.appendChild(this.createOption(mxResources.get('connectionPoints'), function() + { + return graph.connectionHandler.isEnabled(); + }, function(checked) + { + ui.actions.get('connectionPoints').funct(); + }, + { + install: function(apply) + { + this.listener = function() + { + apply(graph.connectionHandler.isEnabled()); + }; + + ui.addListener('connectionPointsChanged', this.listener); + }, + destroy: function() + { + ui.removeListener(this.listener); + } + })); + + // Guides + div.appendChild(this.createOption(mxResources.get('guides'), function() + { + return graph.graphHandler.guidesEnabled; + }, function(checked) + { + ui.actions.get('guides').funct(); + }, + { + install: function(apply) + { + this.listener = function() + { + apply(graph.graphHandler.guidesEnabled); + }; + + ui.addListener('guidesEnabledChanged', this.listener); + }, + destroy: function() + { + ui.removeListener(this.listener); + } + })); + } + + return div; +}; + +/** + * + */ +DiagramFormatPanel.prototype.addGridOption = function(container) +{ + var ui = this.editorUi; + var graph = ui.editor.graph; + + var input = document.createElement('input'); + input.style.position = 'absolute'; + input.style.textAlign = 'right'; + input.style.width = '38px'; + input.value = graph.getGridSize() + ' pt'; + + var stepper = this.createStepper(input, update); + input.style.display = (graph.isGridEnabled()) ? '' : 'none'; + stepper.style.display = input.style.display; + + mxEvent.addListener(input, 'keydown', function(e) + { + if (e.keyCode == 13) + { + graph.container.focus(); + mxEvent.consume(e); + } + else if (e.keyCode == 27) + { + input.value = graph.getGridSize(); + graph.container.focus(); + mxEvent.consume(e); + } + }); + + function update(evt) + { + var value = parseInt(input.value); + value = Math.max(1, (isNaN(value)) ? 10 : value); + + if (value != graph.getGridSize()) + { + graph.setGridSize(value) + } + + input.value = value + ' pt'; + mxEvent.consume(evt); + }; + + mxEvent.addListener(input, 'blur', update); + mxEvent.addListener(input, 'change', update); + + if (mxClient.IS_SVG) + { + input.style.marginTop = '-2px'; + input.style.right = '84px'; + stepper.style.marginTop = '-16px'; + stepper.style.right = '72px'; + + var panel = this.createColorOption(mxResources.get('grid'), function() + { + var color = graph.view.gridColor; + + return (graph.isGridEnabled()) ? color : null; + }, function(color) + { + if (color == mxConstants.NONE) + { + graph.setGridEnabled(false); + } + else + { + graph.setGridEnabled(true); + ui.setGridColor(color); + } + + input.style.display = (graph.isGridEnabled()) ? '' : 'none'; + stepper.style.display = input.style.display; + ui.fireEvent(new mxEventObject('gridEnabledChanged')); + }, '#e0e0e0', + { + install: function(apply) + { + this.listener = function() + { + apply((graph.isGridEnabled()) ? graph.view.gridColor : null); + }; + + ui.addListener('gridColorChanged', this.listener); + ui.addListener('gridEnabledChanged', this.listener); + }, + destroy: function() + { + ui.removeListener(this.listener); + } + }); + + panel.appendChild(input); + panel.appendChild(stepper); + container.appendChild(panel); + } + else + { + input.style.marginTop = '2px'; + input.style.right = '32px'; + stepper.style.marginTop = '2px'; + stepper.style.right = '20px'; + + container.appendChild(input); + container.appendChild(stepper); + + container.appendChild(this.createOption(mxResources.get('grid'), function() + { + return graph.isGridEnabled(); + }, function(checked) + { + graph.setGridEnabled(checked); + + if (graph.isGridEnabled()) + { + graph.view.gridColor = '#e0e0e0'; + } + + ui.fireEvent(new mxEventObject('gridEnabledChanged')); + }, + { + install: function(apply) + { + this.listener = function() + { + input.style.display = (graph.isGridEnabled()) ? '' : 'none'; + stepper.style.display = input.style.display; + + apply(graph.isGridEnabled()); + }; + + ui.addListener('gridEnabledChanged', this.listener); + }, + destroy: function() + { + ui.removeListener(this.listener); + } + })); + } +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +DiagramFormatPanel.prototype.addDocumentProperties = function(div) +{ + // Hook for subclassers + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + + div.appendChild(this.createTitle(mxResources.get('options'))); + + return div; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +DiagramFormatPanel.prototype.addPaperSize = function(div) +{ + var ui = this.editorUi; + var editor = ui.editor; + var graph = editor.graph; + + div.appendChild(this.createTitle(mxResources.get('paperSize'))); + + var accessor = PageSetupDialog.addPageFormatPanel(div, 'formatpanel', graph.pageFormat, function(pageFormat) + { + if (graph.pageFormat == null || graph.pageFormat.width != pageFormat.width || + graph.pageFormat.height != pageFormat.height) + { + var change = new ChangePageSetup(ui, null, null, pageFormat); + change.ignoreColor = true; + change.ignoreImage = true; + + graph.model.execute(change); + } + }); + + this.addKeyHandler(accessor.widthInput, function() + { + accessor.set(graph.pageFormat); + }); + this.addKeyHandler(accessor.heightInput, function() + { + accessor.set(graph.pageFormat); + }); + + var listener = function() + { + accessor.set(graph.pageFormat); + }; + + ui.addListener('pageFormatChanged', listener); + this.listeners.push({destroy: function() { ui.removeListener(listener); }}); + + graph.getModel().addListener(mxEvent.CHANGE, listener); + this.listeners.push({destroy: function() { graph.getModel().removeListener(listener); }}); + + return div; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +DiagramFormatPanel.prototype.addStyleOps = function(div) +{ + var btn = mxUtils.button(mxResources.get('editData'), mxUtils.bind(this, function(evt) + { + this.editorUi.actions.get('editData').funct(); + })); + + btn.setAttribute('title', mxResources.get('editData') + ' (' + this.editorUi.actions.get('editData').shortcut + ')'); + btn.style.width = '202px'; + btn.style.marginBottom = '2px'; + div.appendChild(btn); + + mxUtils.br(div); + + btn = mxUtils.button(mxResources.get('clearDefaultStyle'), mxUtils.bind(this, function(evt) + { + this.editorUi.actions.get('clearDefaultStyle').funct(); + })); + + btn.setAttribute('title', mxResources.get('clearDefaultStyle') + ' (' + this.editorUi.actions.get('clearDefaultStyle').shortcut + ')'); + btn.style.width = '202px'; + div.appendChild(btn); + + return div; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +DiagramFormatPanel.prototype.destroy = function() +{ + BaseFormatPanel.prototype.destroy.apply(this, arguments); + + if (this.gridEnabledListener) + { + this.editorUi.removeListener(this.gridEnabledListener); + this.gridEnabledListener = null; + } +}; diff --git a/media/grapheditor/js/Graph.js b/media/grapheditor/js/Graph.js new file mode 100644 index 0000000000..1241b9dda5 --- /dev/null +++ b/media/grapheditor/js/Graph.js @@ -0,0 +1,8440 @@ +/** + * Copyright (c) 2006-2012, JGraph Ltd + */ +// Workaround for allowing target="_blank" in HTML sanitizer +// see https://code.google.com/p/google-caja/issues/detail?can=2&q=&colspec=ID%20Type%20Status%20Priority%20Owner%20Summary&groupby=&sort=&id=1296 +if (typeof html4 !== 'undefined') +{ + html4.ATTRIBS["a::target"] = 0; + html4.ATTRIBS["source::src"] = 0; + html4.ATTRIBS["video::src"] = 0; + // Would be nice for tooltips but probably a security risk... + //html4.ATTRIBS["video::autoplay"] = 0; + //html4.ATTRIBS["video::autobuffer"] = 0; +} + +/** + * Sets global constants. + */ +// Changes default colors +mxConstants.SHADOW_OPACITY = 0.25; +mxConstants.SHADOWCOLOR = '#000000'; +mxConstants.VML_SHADOWCOLOR = '#d0d0d0'; +mxGraph.prototype.pageBreakColor = '#c0c0c0'; +mxGraph.prototype.pageScale = 1; + +// Letter page format is default in US, Canada and Mexico +(function() +{ + try + { + if (navigator != null && navigator.language != null) + { + var lang = navigator.language.toLowerCase(); + mxGraph.prototype.pageFormat = (lang === 'en-us' || lang === 'en-ca' || lang === 'es-mx') ? + mxConstants.PAGE_FORMAT_LETTER_PORTRAIT : mxConstants.PAGE_FORMAT_A4_PORTRAIT; + } + } + catch (e) + { + // ignore + } +})(); + +// Matches label positions of mxGraph 1.x +mxText.prototype.baseSpacingTop = 5; +mxText.prototype.baseSpacingBottom = 1; + +// Keeps edges between relative child cells inside parent +mxGraphModel.prototype.ignoreRelativeEdgeParent = false; + +// Defines grid properties +mxGraphView.prototype.gridImage = (mxClient.IS_SVG) ? 'data:image/gif;base64,R0lGODlhCgAKAJEAAAAAAP///8zMzP///yH5BAEAAAMALAAAAAAKAAoAAAIJ1I6py+0Po2wFADs=' : + IMAGE_PATH + '/grid.gif'; +mxGraphView.prototype.gridSteps = 4; +mxGraphView.prototype.minGridSize = 4; + +// UrlParams is null in embed mode +mxGraphView.prototype.gridColor = '#e0e0e0'; + +// Alternative text for unsupported foreignObjects +mxSvgCanvas2D.prototype.foAltText = '[Not supported by viewer]'; + +/** + * Constructs a new graph instance. Note that the constructor does not take a + * container because the graph instance is needed for creating the UI, which + * in turn will create the container for the graph. Hence, the container is + * assigned later in EditorUi. + */ +/** + * Defines graph class. + */ +Graph = function(container, model, renderHint, stylesheet, themes) +{ + mxGraph.call(this, container, model, renderHint, stylesheet); + + this.themes = themes || this.defaultThemes; + this.currentEdgeStyle = mxUtils.clone(this.defaultEdgeStyle); + this.currentVertexStyle = mxUtils.clone(this.defaultVertexStyle); + + // Sets the base domain URL and domain path URL for relative links. + var b = this.baseUrl; + var p = b.indexOf('//'); + this.domainUrl = ''; + this.domainPathUrl = ''; + + if (p > 0) + { + var d = b.indexOf('/', p + 2); + + if (d > 0) + { + this.domainUrl = b.substring(0, d); + } + + d = b.lastIndexOf('/'); + + if (d > 0) + { + this.domainPathUrl = b.substring(0, d + 1); + } + } + + // Adds support for HTML labels via style. Note: Currently, only the Java + // backend supports HTML labels but CSS support is limited to the following: + // http://docs.oracle.com/javase/6/docs/api/index.html?javax/swing/text/html/CSS.html + // TODO: Wrap should not affect isHtmlLabel output (should be handled later) + this.isHtmlLabel = function(cell) + { + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return style['html'] == '1' || style[mxConstants.STYLE_WHITE_SPACE] == 'wrap'; + }; + + // Implements a listener for hover and click handling on edges + if (this.edgeMode) + { + var start = { + point: null, + event: null, + state: null, + handle: null, + selected: false + }; + + // Uses this event to process mouseDown to check the selection state before it is changed + this.addListener(mxEvent.FIRE_MOUSE_EVENT, mxUtils.bind(this, function(sender, evt) + { + if (evt.getProperty('eventName') == 'mouseDown' && this.isEnabled()) + { + var me = evt.getProperty('event'); + + if (!mxEvent.isControlDown(me.getEvent()) && !mxEvent.isShiftDown(me.getEvent())) + { + var state = me.getState(); + + if (state != null) + { + // Checks if state was removed in call to stopEditing above + if (this.model.isEdge(state.cell)) + { + start.point = new mxPoint(me.getGraphX(), me.getGraphY()); + start.selected = this.isCellSelected(state.cell); + start.state = state; + start.event = me; + + if (state.text != null && state.text.boundingBox != null && + mxUtils.contains(state.text.boundingBox, me.getGraphX(), me.getGraphY())) + { + start.handle = mxEvent.LABEL_HANDLE; + } + else + { + var handler = this.selectionCellsHandler.getHandler(state.cell); + + if (handler != null && handler.bends != null && handler.bends.length > 0) + { + start.handle = handler.getHandleForEvent(me); + } + } + } + } + } + } + })); + + var mouseDown = null; + + this.addMouseListener( + { + mouseDown: function(sender, me) {}, + mouseMove: mxUtils.bind(this, function(sender, me) + { + // Checks if any other handler is active + var handlerMap = this.selectionCellsHandler.handlers.map; + + for (var key in handlerMap) + { + if (handlerMap[key].index != null) + { + return; + } + } + + if (this.isEnabled() && !this.panningHandler.isActive() && !mxEvent.isControlDown(me.getEvent()) && + !mxEvent.isShiftDown(me.getEvent()) && !mxEvent.isAltDown(me.getEvent())) + { + var tol = this.tolerance; + + if (start.point != null && start.state != null && start.event != null) + { + var state = start.state; + + if (Math.abs(start.point.x - me.getGraphX()) > tol || + Math.abs(start.point.y - me.getGraphY()) > tol) + { + // Lazy selection for edges inside groups + if (!this.isCellSelected(state.cell)) + { + this.setSelectionCell(state.cell); + } + + var handler = this.selectionCellsHandler.getHandler(state.cell); + + if (handler != null && handler.bends != null && handler.bends.length > 0) + { + var handle = handler.getHandleForEvent(start.event); + var edgeStyle = this.view.getEdgeStyle(state); + var entity = edgeStyle == mxEdgeStyle.EntityRelation; + + // Handles special case where label was clicked on unselected edge in which + // case the label will be moved regardless of the handle that is returned + if (!start.selected && start.handle == mxEvent.LABEL_HANDLE) + { + handle = start.handle; + } + + if (!entity || handle == 0 || handle == handler.bends.length - 1 || handle == mxEvent.LABEL_HANDLE) + { + // Source or target handle or connected for direct handle access or orthogonal line + // with just two points where the central handle is moved regardless of mouse position + if (handle == mxEvent.LABEL_HANDLE || handle == 0 || state.visibleSourceState != null || + handle == handler.bends.length - 1 || state.visibleTargetState != null) + { + if (!entity && handle != mxEvent.LABEL_HANDLE) + { + var pts = state.absolutePoints; + + // Default case where handles are at corner points handles + // drag of corner as drag of existing point + if (pts != null && ((edgeStyle == null && handle == null) || + edgeStyle == mxEdgeStyle.OrthConnector)) + { + // Does not use handles if they were not initially visible + handle = start.handle; + + if (handle == null) + { + var box = new mxRectangle(start.point.x, start.point.y); + box.grow(mxEdgeHandler.prototype.handleImage.width / 2); + + if (mxUtils.contains(box, pts[0].x, pts[0].y)) + { + // Moves source terminal handle + handle = 0; + } + else if (mxUtils.contains(box, pts[pts.length - 1].x, pts[pts.length - 1].y)) + { + // Moves target terminal handle + handle = handler.bends.length - 1; + } + else + { + // Checks if edge has no bends + var nobends = edgeStyle != null && (pts.length == 2 || (pts.length == 3 && + ((Math.round(pts[0].x - pts[1].x) == 0 && Math.round(pts[1].x - pts[2].x) == 0) || + (Math.round(pts[0].y - pts[1].y) == 0 && Math.round(pts[1].y - pts[2].y) == 0)))); + + if (nobends) + { + // Moves central handle for straight orthogonal edges + handle = 2; + } + else + { + // Finds and moves vertical or horizontal segment + handle = mxUtils.findNearestSegment(state, start.point.x, start.point.y); + + // Converts segment to virtual handle index + if (edgeStyle == null) + { + handle = mxEvent.VIRTUAL_HANDLE - handle; + } + // Maps segment to handle + else + { + handle += 1; + } + } + } + } + } + + // Creates a new waypoint and starts moving it + if (handle == null) + { + handle = mxEvent.VIRTUAL_HANDLE; + } + } + + handler.start(me.getGraphX(), me.getGraphX(), handle); + start.state = null; + start.event = null; + start.point = null; + start.handle = null; + start.selected = false; + me.consume(); + + // Removes preview rectangle in graph handler + this.graphHandler.reset(); + } + } + else if (entity && (state.visibleSourceState != null || state.visibleTargetState != null)) + { + // Disables moves on entity to make it consistent + this.graphHandler.reset(); + me.consume(); + } + } + } + } + else + { + // Updates cursor for unselected edges under the mouse + var state = me.getState(); + + if (state != null) + { + // Checks if state was removed in call to stopEditing above + if (this.model.isEdge(state.cell)) + { + var cursor = null; + var pts = state.absolutePoints; + + if (pts != null) + { + var box = new mxRectangle(me.getGraphX(), me.getGraphY()); + box.grow(mxEdgeHandler.prototype.handleImage.width / 2); + + if (state.text != null && state.text.boundingBox != null && + mxUtils.contains(state.text.boundingBox, me.getGraphX(), me.getGraphY())) + { + cursor = 'move'; + } + else if (mxUtils.contains(box, pts[0].x, pts[0].y) || + mxUtils.contains(box, pts[pts.length - 1].x, pts[pts.length - 1].y)) + { + cursor = 'pointer'; + } + else if (state.visibleSourceState != null || state.visibleTargetState != null) + { + // Moving is not allowed for entity relation but still indicate hover state + var tmp = this.view.getEdgeStyle(state); + cursor = 'crosshair'; + + if (tmp != mxEdgeStyle.EntityRelation && this.isOrthogonal(state)) + { + var idx = mxUtils.findNearestSegment(state, me.getGraphX(), me.getGraphY()); + + if (idx < pts.length - 1 && idx >= 0) + { + cursor = (Math.round(pts[idx].x - pts[idx + 1].x) == 0) ? + 'col-resize' : 'row-resize'; + } + } + } + } + + if (cursor != null) + { + state.setCursor(cursor); + } + } + } + } + } + }), + mouseUp: mxUtils.bind(this, function(sender, me) + { + start.state = null; + start.event = null; + start.point = null; + start.handle = null; + }) + }); + } + + // HTML entities are displayed as plain text in wrapped plain text labels + this.cellRenderer.getLabelValue = function(state) + { + var result = mxCellRenderer.prototype.getLabelValue.apply(this, arguments); + + if (state.view.graph.isHtmlLabel(state.cell)) + { + if (state.style['html'] != 1) + { + result = mxUtils.htmlEntities(result, false); + } + else + { + result = state.view.graph.sanitizeHtml(result); + } + } + + return result; + }; + + // All code below not available and not needed in embed mode + if (typeof mxVertexHandler !== 'undefined') + { + this.setConnectable(true); + this.setDropEnabled(true); + this.setPanning(true); + this.setTooltips(true); + this.setAllowLoops(true); + this.allowAutoPanning = true; + this.resetEdgesOnConnect = false; + this.constrainChildren = false; + this.constrainRelativeChildren = true; + + // Do not scroll after moving cells + this.graphHandler.scrollOnMove = false; + this.graphHandler.scaleGrid = true; + + // Disables cloning of connection sources by default + this.connectionHandler.setCreateTarget(false); + this.connectionHandler.insertBeforeSource = true; + + // Disables built-in connection starts + this.connectionHandler.isValidSource = function(cell, me) + { + return false; + }; + + // Sets the style to be used when an elbow edge is double clicked + this.alternateEdgeStyle = 'vertical'; + + if (stylesheet == null) + { + this.loadStylesheet(); + } + + // Adds page centers to the guides for moving cells + var graphHandlerGetGuideStates = this.graphHandler.getGuideStates; + this.graphHandler.getGuideStates = function() + { + var result = graphHandlerGetGuideStates.apply(this, arguments); + + // Create virtual cell state for page centers + if (this.graph.pageVisible) + { + var guides = []; + + var pf = this.graph.pageFormat; + var ps = this.graph.pageScale; + var pw = pf.width * ps; + var ph = pf.height * ps; + var t = this.graph.view.translate; + var s = this.graph.view.scale; + + var layout = this.graph.getPageLayout(); + + for (var i = 0; i < layout.width; i++) + { + guides.push(new mxRectangle(((layout.x + i) * pw + t.x) * s, + (layout.y * ph + t.y) * s, pw * s, ph * s)); + } + + for (var j = 0; j < layout.height; j++) + { + guides.push(new mxRectangle((layout.x * pw + t.x) * s, + ((layout.y + j) * ph + t.y) * s, pw * s, ph * s)); + } + + // Page center guides have predence over normal guides + result = guides.concat(result); + } + + return result; + }; + + // Overrides zIndex for dragElement + mxDragSource.prototype.dragElementZIndex = mxPopupMenu.prototype.zIndex; + + // Overrides color for virtual guides for page centers + mxGuide.prototype.getGuideColor = function(state, horizontal) + { + return (state.cell == null) ? '#ffa500' /* orange */ : mxConstants.GUIDE_COLOR; + }; + + // Changes color of move preview for black backgrounds + this.graphHandler.createPreviewShape = function(bounds) + { + this.previewColor = (this.graph.background == '#000000') ? '#ffffff' : mxGraphHandler.prototype.previewColor; + + return mxGraphHandler.prototype.createPreviewShape.apply(this, arguments); + }; + + // Handles parts of cells by checking if part=1 is in the style and returning the parent + // if the parent is not already in the list of cells. container style is used to disable + // step into swimlanes and dropTarget style is used to disable acting as a drop target. + // LATER: Handle recursive parts + this.graphHandler.getCells = function(initialCell) + { + var cells = mxGraphHandler.prototype.getCells.apply(this, arguments); + var newCells = []; + + for (var i = 0; i < cells.length; i++) + { + var state = this.graph.view.getState(cells[i]); + var style = (state != null) ? state.style : this.graph.getCellStyle(cells[i]); + + if (mxUtils.getValue(style, 'part', '0') == '1') + { + var parent = this.graph.model.getParent(cells[i]); + + if (this.graph.model.isVertex(parent) && mxUtils.indexOf(cells, parent) < 0) + { + newCells.push(parent); + } + } + else + { + newCells.push(cells[i]); + } + } + + return newCells; + }; + + // Handles parts of cells when cloning the source for new connections + this.connectionHandler.createTargetVertex = function(evt, source) + { + var state = this.graph.view.getState(source); + var style = (state != null) ? state.style : this.graph.getCellStyle(source); + + if (mxUtils.getValue(style, 'part', false)) + { + var parent = this.graph.model.getParent(source); + + if (this.graph.model.isVertex(parent)) + { + source = parent; + } + } + + return mxConnectionHandler.prototype.createTargetVertex.apply(this, arguments); + }; + + var rubberband = new mxRubberband(this); + + this.getRubberband = function() + { + return rubberband; + }; + + // Timer-based activation of outline connect in connection handler + var startTime = new Date().getTime(); + var timeOnTarget = 0; + + var connectionHandlerMouseMove = this.connectionHandler.mouseMove; + + this.connectionHandler.mouseMove = function() + { + var prev = this.currentState; + connectionHandlerMouseMove.apply(this, arguments); + + if (prev != this.currentState) + { + startTime = new Date().getTime(); + timeOnTarget = 0; + } + else + { + timeOnTarget = new Date().getTime() - startTime; + } + }; + + // Activates outline connect after 1500ms with touch event or if alt is pressed inside the shape + // outlineConnect=0 is a custom style that means do not connect to strokes inside the shape, + // or in other words, connect to the shape's perimeter if the highlight is under the mouse + // (the name is because the highlight, including all strokes, is called outline in the code) + var connectionHandleIsOutlineConnectEvent = this.connectionHandler.isOutlineConnectEvent; + + this.connectionHandler.isOutlineConnectEvent = function(me) + { + return (this.currentState != null && me.getState() == this.currentState && timeOnTarget > 2000) || + ((this.currentState == null || mxUtils.getValue(this.currentState.style, 'outlineConnect', '1') != '0') && + connectionHandleIsOutlineConnectEvent.apply(this, arguments)); + }; + + // Adds shift+click to toggle selection state + var isToggleEvent = this.isToggleEvent; + this.isToggleEvent = function(evt) + { + return isToggleEvent.apply(this, arguments) || mxEvent.isShiftDown(evt); + }; + + // Workaround for Firefox where first mouse down is received + // after tap and hold if scrollbars are visible, which means + // start rubberband immediately if no cell is under mouse. + var isForceRubberBandEvent = rubberband.isForceRubberbandEvent; + rubberband.isForceRubberbandEvent = function(me) + { + return isForceRubberBandEvent.apply(this, arguments) || + (mxUtils.hasScrollbars(this.graph.container) && mxClient.IS_FF && + mxClient.IS_WIN && me.getState() == null && mxEvent.isTouchEvent(me.getEvent())); + }; + + // Shows hand cursor while panning + var prevCursor = null; + + this.panningHandler.addListener(mxEvent.PAN_START, mxUtils.bind(this, function() + { + if (this.isEnabled()) + { + prevCursor = this.container.style.cursor; + this.container.style.cursor = 'move'; + } + })); + + this.panningHandler.addListener(mxEvent.PAN_END, mxUtils.bind(this, function() + { + if (this.isEnabled()) + { + this.container.style.cursor = prevCursor; + } + })); + + this.popupMenuHandler.autoExpand = true; + + this.popupMenuHandler.isSelectOnPopup = function(me) + { + return mxEvent.isMouseEvent(me.getEvent()); + }; + + // Handles links if graph is read-only or cell is locked + var click = this.click; + this.click = function(me) + { + var locked = me.state == null && me.sourceState != null && this.isCellLocked(me.sourceState.cell); + + if ((!this.isEnabled() || locked) && !me.isConsumed()) + { + var cell = (locked) ? me.sourceState.cell : me.getCell(); + + if (cell != null) + { + var link = this.getLinkForCell(cell); + + if (link != null) + { + if (this.isCustomLink(link)) + { + this.customLinkClicked(link); + } + else + { + this.openLink(link); + } + } + } + } + else + { + return click.apply(this, arguments); + } + }; + + // Redirects tooltips for locked cells + this.tooltipHandler.getStateForEvent = function(me) + { + return me.sourceState; + }; + + // Redirects cursor for locked cells + var getCursorForMouseEvent = this.getCursorForMouseEvent; + this.getCursorForMouseEvent = function(me) + { + var locked = me.state == null && me.sourceState != null && this.isCellLocked(me.sourceState.cell); + + return this.getCursorForCell((locked) ? me.sourceState.cell : me.getCell()); + }; + + // Shows pointer cursor for clickable cells with links + // ie. if the graph is disabled and cells cannot be selected + var getCursorForCell = this.getCursorForCell; + this.getCursorForCell = function(cell) + { + if (!this.isEnabled() || this.isCellLocked(cell)) + { + var link = this.getLinkForCell(cell); + + if (link != null) + { + return 'pointer'; + } + else if (this.isCellLocked(cell)) + { + return 'default'; + } + } + + return getCursorForCell.apply(this, arguments); + }; + + // Changes rubberband selection to be recursive + this.selectRegion = function(rect, evt) + { + var cells = this.getAllCells(rect.x, rect.y, rect.width, rect.height); + this.selectCellsForEvent(cells, evt); + + return cells; + }; + + // Recursive implementation for rubberband selection + this.getAllCells = function(x, y, width, height, parent, result) + { + result = (result != null) ? result : []; + + if (width > 0 || height > 0) + { + var model = this.getModel(); + var right = x + width; + var bottom = y + height; + + if (parent == null) + { + parent = this.getCurrentRoot(); + + if (parent == null) + { + parent = model.getRoot(); + } + } + + if (parent != null) + { + var childCount = model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var cell = model.getChildAt(parent, i); + var state = this.view.getState(cell); + + if (state != null && this.isCellVisible(cell) && mxUtils.getValue(state.style, 'locked', '0') != '1') + { + var deg = mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION) || 0; + var box = state; + + if (deg != 0) + { + box = mxUtils.getBoundingBox(box, deg); + } + + if ((model.isEdge(cell) || model.isVertex(cell)) && + box.x >= x && box.y + box.height <= bottom && + box.y >= y && box.x + box.width <= right) + { + result.push(cell); + } + + this.getAllCells(x, y, width, height, cell, result); + } + } + } + } + + return result; + }; + + // Never removes cells from parents that are being moved + var graphHandlerShouldRemoveCellsFromParent = this.graphHandler.shouldRemoveCellsFromParent; + this.graphHandler.shouldRemoveCellsFromParent = function(parent, cells, evt) + { + if (this.graph.isCellSelected(parent)) + { + return false; + } + + return graphHandlerShouldRemoveCellsFromParent.apply(this, arguments); + }; + + // Unlocks all cells + this.isCellLocked = function(cell) + { + var pState = this.view.getState(cell); + + while (pState != null) + { + if (mxUtils.getValue(pState.style, 'locked', '0') == '1') + { + return true; + } + + pState = this.view.getState(this.model.getParent(pState.cell)); + } + + return false; + }; + + var tapAndHoldSelection = null; + + // Uses this event to process mouseDown to check the selection state before it is changed + this.addListener(mxEvent.FIRE_MOUSE_EVENT, mxUtils.bind(this, function(sender, evt) + { + if (evt.getProperty('eventName') == 'mouseDown') + { + var me = evt.getProperty('event'); + var state = me.getState(); + + if (state != null && !this.isSelectionEmpty() && !this.isCellSelected(state.cell)) + { + tapAndHoldSelection = this.getSelectionCells(); + } + else + { + tapAndHoldSelection = null; + } + } + })); + + // Tap and hold on background starts rubberband for multiple selected + // cells the cell associated with the event is deselected + this.addListener(mxEvent.TAP_AND_HOLD, mxUtils.bind(this, function(sender, evt) + { + if (!mxEvent.isMultiTouchEvent(evt)) + { + var me = evt.getProperty('event'); + var cell = evt.getProperty('cell'); + + if (cell == null) + { + var pt = mxUtils.convertPoint(this.container, + mxEvent.getClientX(me), mxEvent.getClientY(me)); + rubberband.start(pt.x, pt.y); + } + else if (tapAndHoldSelection != null) + { + this.addSelectionCells(tapAndHoldSelection); + } + else if (this.getSelectionCount() > 1 && this.isCellSelected(cell)) + { + this.removeSelectionCell(cell); + } + + // Blocks further processing of the event + tapAndHoldSelection = null; + evt.consume(); + } + })); + + // On connect the target is selected and we clone the cell of the preview edge for insert + this.connectionHandler.selectCells = function(edge, target) + { + this.graph.setSelectionCell(target || edge); + }; + + // Shows connection points only if cell not selected + this.connectionHandler.constraintHandler.isStateIgnored = function(state, source) + { + return source && state.view.graph.isCellSelected(state.cell); + }; + + // Updates constraint handler if the selection changes + this.selectionModel.addListener(mxEvent.CHANGE, mxUtils.bind(this, function() + { + var ch = this.connectionHandler.constraintHandler; + + if (ch.currentFocus != null && ch.isStateIgnored(ch.currentFocus, true)) + { + ch.currentFocus = null; + ch.constraints = null; + ch.destroyIcons(); + } + + ch.destroyFocusHighlight(); + })); + + // Initializes touch interface + if (Graph.touchStyle) + { + this.initTouch(); + } + + /** + * Adds locking + */ + var graphUpdateMouseEvent = this.updateMouseEvent; + this.updateMouseEvent = function(me) + { + me = graphUpdateMouseEvent.apply(this, arguments); + + if (me.state != null && this.isCellLocked(me.getCell())) + { + me.state = null; + } + + return me; + }; + } + + //Create a unique offset object for each graph instance. + this.currentTranslate = new mxPoint(0, 0); +}; + +/** + * Specifies if the touch UI should be used (cannot detect touch in FF so always on for Windows/Linux) + */ +Graph.touchStyle = mxClient.IS_TOUCH || (mxClient.IS_FF && mxClient.IS_WIN) || navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 || window.urlParams == null || urlParams['touch'] == '1'; + +/** + * Shortcut for capability check. + */ +Graph.fileSupport = window.File != null && window.FileReader != null && window.FileList != null && + (window.urlParams == null || urlParams['filesupport'] != '0'); + +/** + * Default size for line jumps. + */ +Graph.lineJumpsEnabled = true; + +/** + * Default size for line jumps. + */ +Graph.defaultJumpSize = 6; + +/** + * Helper function (requires atob). + */ +Graph.createSvgImage = function(w, h, data) +{ + var tmp = unescape(encodeURIComponent( + '' + + '' + data + '')); + + return new mxImage('data:image/svg+xml;base64,' + ((window.btoa) ? btoa(tmp) : Base64.encode(tmp, true)), w, h) +}; + +/** + * Graph inherits from mxGraph. + */ +mxUtils.extend(Graph, mxGraph); + +/** + * Allows all values in fit. + */ +Graph.prototype.minFitScale = null; + +/** + * Allows all values in fit. + */ +Graph.prototype.maxFitScale = null; + +/** + * Sets the policy for links. Possible values are "self" to replace any framesets, + * "blank" to load the URL in and "auto" (default). + */ +Graph.prototype.linkPolicy = (urlParams['target'] == 'frame') ? 'blank' : (urlParams['target'] || 'auto'); + +/** + * Target for links that open in a new window. Default is _blank. + */ +Graph.prototype.linkTarget = (urlParams['target'] == 'frame') ? '_self' : '_blank'; + +/** + * Scrollbars are enabled on non-touch devices (not including Firefox because touch events + * cannot be detected in Firefox, see above). + */ +Graph.prototype.defaultScrollbars = !mxClient.IS_IOS; + +/** + * Specifies if the page should be visible for new files. Default is true. + */ +Graph.prototype.defaultPageVisible = true; + +/** + * Specifies if the app should run in chromeless mode. Default is false. + * This default is only used if the contructor argument is null. + */ +Graph.prototype.lightbox = false; + +/** + * + */ +Graph.prototype.defaultPageBackgroundColor = '#ffffff'; + +/** + * + */ +Graph.prototype.defaultPageBorderColor = '#ffffff'; + +/** + * + */ +Graph.prototype.defaultGraphBackground = '#ffffff'; + +/** + * Specifies the size of the size for "tiles" to be used for a graph with + * scrollbars but no visible background page. A good value is large + * enough to reduce the number of repaints that is caused for auto- + * translation, which depends on this value, and small enough to give + * a small empty buffer around the graph. Default is 400x400. + */ +Graph.prototype.scrollTileSize = new mxRectangle(0, 0, 400, 400); + +/** + * Overrides the background color and paints a transparent background. + */ +Graph.prototype.transparentBackground = true; + +/** + * Sets the default target for all links in cells. + */ +Graph.prototype.defaultEdgeLength = 80; + +/** + * Disables move of bends/segments without selecting. + */ +Graph.prototype.edgeMode = false; + +/** + * Allows all values in fit. + */ +Graph.prototype.connectionArrowsEnabled = true; + +/** + * Specifies the regular expression for matching placeholders. + */ +Graph.prototype.placeholderPattern = new RegExp('%(date\{.*\}|[^%^\{^\}]+)%', 'g'); + +/** + * Specifies the regular expression for matching placeholders. + */ +Graph.prototype.absoluteUrlPattern = new RegExp('^(?:[a-z]+:)?//', 'i'); + +/** + * Specifies the default name for the theme. Default is 'default'. + */ +Graph.prototype.defaultThemeName = 'default'; + +/** + * Specifies the default name for the theme. Default is 'default'. + */ +Graph.prototype.defaultThemes = {}; + +/** + * Base URL for relative links. + */ +Graph.prototype.baseUrl = (urlParams['base'] != null) ? + decodeURIComponent(urlParams['base']) : + (((window != window.top) ? document.referrer : + document.location.toString()).split('#')[0]); + +/** + * Specifies if the label should be edited after an insert. + */ +Graph.prototype.editAfterInsert = false; + +/** + * Defines the built-in properties to be ignored in tooltips. + */ +Graph.prototype.builtInProperties = ['label', 'tooltip', 'placeholders', 'placeholder']; + +/** + * Installs child layout styles. + */ +Graph.prototype.init = function(container) +{ + mxGraph.prototype.init.apply(this, arguments); + + // Intercepts links with no target attribute and opens in new window + this.cellRenderer.initializeLabel = function(state, shape) + { + mxCellRenderer.prototype.initializeLabel.apply(this, arguments); + + // Checks tolerance for clicks on links + var tol = state.view.graph.tolerance; + var handleClick = true; + var first = null; + + var down = mxUtils.bind(this, function(evt) + { + handleClick = true; + first = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + }); + + var move = mxUtils.bind(this, function(evt) + { + handleClick = handleClick && first != null && + Math.abs(first.x - mxEvent.getClientX(evt)) < tol && + Math.abs(first.y - mxEvent.getClientY(evt)) < tol; + }); + + var up = mxUtils.bind(this, function(evt) + { + if (handleClick) + { + var elt = mxEvent.getSource(evt) + + while (elt != null && elt != shape.node) + { + if (elt.nodeName.toLowerCase() == 'a') + { + state.view.graph.labelLinkClicked(state, elt, evt); + break; + } + + elt = elt.parentNode; + } + } + }); + + mxEvent.addGestureListeners(shape.node, down, move, up); + mxEvent.addListener(shape.node, 'click', function(evt) + { + mxEvent.consume(evt); + }); + }; + + this.initLayoutManager(); +}; + +/** + * Implements zoom and offset via CSS transforms. This is currently only used + * in read-only as there are fewer issues with the mxCellState not being scaled + * and translated. + * + * KNOWN ISSUES TO FIX: + * - Apply CSS transforms to HTML labels in IE11 + */ +(function() +{ + /** + * Uses CSS transforms for scale and translate. + */ + Graph.prototype.useCssTransforms = false; + + /** + * Contains the scale. + */ + Graph.prototype.currentScale = 1; + + /** + * Contains the offset. + */ + Graph.prototype.currentTranslate = new mxPoint(0, 0); + + /** + * Only foreignObject supported for now (no IE11). + */ + Graph.prototype.isCssTransformsSupported = function() + { + return this.dialect == mxConstants.DIALECT_SVG && !mxClient.NO_FO; + }; + + /** + * Function: getCellAt + * + * Needs to modify original method for recursive call. + */ + Graph.prototype.getCellAt = function(x, y, parent, vertices, edges, ignoreFn) + { + if (this.useCssTransforms) + { + x = x / this.currentScale - this.currentTranslate.x; + y = y / this.currentScale - this.currentTranslate.y; + } + + return this.getScaledCellAt.apply(this, arguments); + }; + + /** + * Function: getScaledCellAt + * + * Overridden for recursion. + */ + Graph.prototype.getScaledCellAt = function(x, y, parent, vertices, edges, ignoreFn) + { + vertices = (vertices != null) ? vertices : true; + edges = (edges != null) ? edges : true; + + if (parent == null) + { + parent = this.getCurrentRoot(); + + if (parent == null) + { + parent = this.getModel().getRoot(); + } + } + + if (parent != null) + { + var childCount = this.model.getChildCount(parent); + + for (var i = childCount - 1; i >= 0; i--) + { + var cell = this.model.getChildAt(parent, i); + var result = this.getScaledCellAt(x, y, cell, vertices, edges, ignoreFn); + + if (result != null) + { + return result; + } + else if (this.isCellVisible(cell) && (edges && this.model.isEdge(cell) || + vertices && this.model.isVertex(cell))) + { + var state = this.view.getState(cell); + + if (state != null && (ignoreFn == null || !ignoreFn(state, x, y)) && + this.intersects(state, x, y)) + { + return cell; + } + } + } + } + + return null; + }; + + + /** + * Function: repaint + * + * Updates the highlight after a change of the model or view. + */ + mxCellHighlight.prototype.getStrokeWidth = function(state) + { + var s = this.strokeWidth; + + if (this.graph.useCssTransforms) + { + s /= this.graph.currentScale; + } + + return s; + }; + + /** + * Function: getGraphBounds + * + * Overrides getGraphBounds to use bounding box from SVG. + */ + mxGraphView.prototype.getGraphBounds = function() + { + var b = this.graphBounds; + + if (this.graph.useCssTransforms) + { + var t = this.graph.currentTranslate; + var s = this.graph.currentScale; + + b = new mxRectangle( + (b.x + t.x) * s, (b.y + t.y) * s, + b.width * s, b.height * s); + } + + return b; + }; + + /** + * Function: viewStateChanged + * + * Overrides to bypass full cell tree validation. + * TODO: Check if this improves performance + */ + mxGraphView.prototype.viewStateChanged = function() + { + if (this.graph.useCssTransforms) + { + this.validate(); + this.graph.sizeDidChange(); + } + else + { + this.revalidate(); + this.graph.sizeDidChange(); + } + }; + + /** + * Function: validate + * + * Overrides validate to normalize validation view state and pass + * current state to CSS transform. + */ + var graphViewValidate = mxGraphView.prototype.validate; + + mxGraphView.prototype.validate = function(cell) + { + if (this.graph.useCssTransforms) + { + this.graph.currentScale = this.scale; + this.graph.currentTranslate.x = this.translate.x; + this.graph.currentTranslate.y = this.translate.y; + + this.scale = 1; + this.translate.x = 0; + this.translate.y = 0; + } + + graphViewValidate.apply(this, arguments); + + if (this.graph.useCssTransforms) + { + this.graph.updateCssTransform(); + + this.scale = this.graph.currentScale; + this.translate.x = this.graph.currentTranslate.x; + this.translate.y = this.graph.currentTranslate.y; + } + }; + + /** + * Function: updateCssTransform + * + * Zooms out of the graph by . + */ + Graph.prototype.updateCssTransform = function() + { + var temp = this.view.getDrawPane(); + + if (temp != null) + { + var g = temp.parentNode; + + if (!this.useCssTransforms) + { + g.removeAttribute('transformOrigin'); + g.removeAttribute('transform'); + } + else + { + var prev = g.getAttribute('transform'); + g.setAttribute('transformOrigin', '0 0'); + g.setAttribute('transform', 'scale(' + this.currentScale + ',' + this.currentScale + ')' + + 'translate(' + this.currentTranslate.x + ',' + this.currentTranslate.y + ')'); + + // Applies workarounds only if translate has changed + if (prev != g.getAttribute('transform')) + { + try + { + // Applies transform to labels outside of the SVG DOM + // Excluded via isCssTransformsSupported + // if (mxClient.NO_FO) + // { + // var transform = 'scale(' + this.currentScale + ')' + 'translate(' + + // this.currentTranslate.x + 'px,' + this.currentTranslate.y + 'px)'; + // + // this.view.states.visit(mxUtils.bind(this, function(cell, state) + // { + // if (state.text != null && state.text.node != null) + // { + // // Stores initial CSS transform that is used for the label alignment + // if (state.text.originalTransform == null) + // { + // state.text.originalTransform = state.text.node.style.transform; + // } + // + // state.text.node.style.transform = transform + state.text.originalTransform; + // } + // })); + // } + // Workaround for https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4320441/ + if (mxClient.IS_EDGE) + { + // Recommended workaround is to do this on all + // foreignObjects, but this seems to be faster + var val = g.style.display; + g.style.display = 'none'; + g.getBBox(); + g.style.display = val; + } + } + catch (e) + { + // ignore + } + } + } + } + }; + + var graphViewValidateBackgroundPage = mxGraphView.prototype.validateBackgroundPage; + + mxGraphView.prototype.validateBackgroundPage = function() + { + var useCssTranforms = this.graph.useCssTransforms, scale = this.scale, + translate = this.translate; + + if (useCssTranforms) + { + this.scale = this.graph.currentScale; + this.translate = this.graph.currentTranslate; + } + + graphViewValidateBackgroundPage.apply(this, arguments); + + if (useCssTranforms) + { + this.scale = scale; + this.translate = translate; + } + }; + + var graphUpdatePageBreaks = mxGraph.prototype.updatePageBreaks; + + mxGraph.prototype.updatePageBreaks = function(visible, width, height) + { + var useCssTranforms = this.useCssTransforms, scale = this.view.scale, + translate = this.view.translate; + + if (useCssTranforms) + { + this.view.scale = 1; + this.view.translate = new mxPoint(0, 0); + this.useCssTransforms = false; + } + + graphUpdatePageBreaks.apply(this, arguments); + + if (useCssTranforms) + { + this.view.scale = scale; + this.view.translate = translate; + this.useCssTransforms = true; + } + }; + +})(); + +/** + * Sets the XML node for the current diagram. + */ +Graph.prototype.isLightboxView = function() +{ + return this.lightbox; +}; + +/** + * Installs automatic layout via styles + */ +Graph.prototype.labelLinkClicked = function(state, elt, evt) +{ + var href = elt.getAttribute('href'); + + if (href != null && !this.isCustomLink(href) && (mxEvent.isLeftMouseButton(evt) && + !mxEvent.isPopupTrigger(evt)) || mxEvent.isTouchEvent(evt)) + { + if (!this.isEnabled() || this.isCellLocked(state.cell)) + { + var target = this.isBlankLink(href) ? this.linkTarget : '_top'; + this.openLink(this.getAbsoluteUrl(href), target); + } + + mxEvent.consume(evt); + } +}; + +/** + * Returns the size of the page format scaled with the page size. + */ +Graph.prototype.openLink = function(href, target, allowOpener) +{ + var result = window; + + // Workaround for blocking in same iframe + if (target == '_self' && window != window.top) + { + window.location.href = href; + } + else + { + // Avoids page reload for anchors (workaround for IE but used everywhere) + if (href.substring(0, this.baseUrl.length) == this.baseUrl && + href.charAt(this.baseUrl.length) == '#' && + target == '_top' && window == window.top) + { + var hash = href.split('#')[1]; + + // Forces navigation if on same hash + if (window.location.hash == '#' + hash) + { + window.location.hash = ''; + } + + window.location.hash = hash; + } + else + { + result = window.open(href, target); + + if (result != null && !allowOpener) + { + result.opener = null; + } + } + } + + return result; +}; + +/** + * Adds support for page links. + */ +Graph.prototype.getLinkTitle = function(href) +{ + return href.substring(href.lastIndexOf('/') + 1); +}; + +/** + * Adds support for page links. + */ +Graph.prototype.isCustomLink = function(href) +{ + return href.substring(0, 5) == 'data:'; +}; + +/** + * Adds support for page links. + */ +Graph.prototype.customLinkClicked = function(link) +{ + return false; +}; + +/** + * Returns true if the fiven href references an external protocol that + * should never open in a new window. Default returns true for mailto. + */ +Graph.prototype.isExternalProtocol = function(href) +{ + return href.substring(0, 7) === 'mailto:'; +}; + +/** + * Hook for links to open in same window. Default returns true for anchors, + * links to same domain or if target == 'self' in the config. + */ +Graph.prototype.isBlankLink = function(href) +{ + return !this.isExternalProtocol(href) && + (this.linkPolicy === 'blank' || + (this.linkPolicy !== 'self' && + !this.isRelativeUrl(href) && + href.substring(0, this.domainUrl.length) !== this.domainUrl)); +}; + +/** + * + */ +Graph.prototype.isRelativeUrl = function(url) +{ + return url != null && !this.absoluteUrlPattern.test(url) && + url.substring(0, 5) !== 'data:' && + !this.isExternalProtocol(url); +}; + +/** + * Installs automatic layout via styles + */ +Graph.prototype.initLayoutManager = function() +{ + this.layoutManager = new mxLayoutManager(this); + + this.layoutManager.getLayout = function(cell) + { + var state = this.graph.view.getState(cell); + var style = (state != null) ? state.style : this.graph.getCellStyle(cell); + + if (style['childLayout'] == 'stackLayout') + { + var stackLayout = new mxStackLayout(this.graph, true); + stackLayout.resizeParentMax = mxUtils.getValue(style, 'resizeParentMax', '1') == '1'; + stackLayout.horizontal = mxUtils.getValue(style, 'horizontalStack', '1') == '1'; + stackLayout.resizeParent = mxUtils.getValue(style, 'resizeParent', '1') == '1'; + stackLayout.resizeLast = mxUtils.getValue(style, 'resizeLast', '0') == '1'; + stackLayout.spacing = style['stackSpacing'] || stackLayout.spacing; + stackLayout.border = style['stackBorder'] || stackLayout.border; + stackLayout.marginLeft = style['marginLeft'] || 0; + stackLayout.marginRight = style['marginRight'] || 0; + stackLayout.marginTop = style['marginTop'] || 0; + stackLayout.marginBottom = style['marginBottom'] || 0; + stackLayout.fill = true; + + return stackLayout; + } + else if (style['childLayout'] == 'treeLayout') + { + var treeLayout = new mxCompactTreeLayout(this.graph); + treeLayout.horizontal = mxUtils.getValue(style, 'horizontalTree', '1') == '1'; + treeLayout.resizeParent = mxUtils.getValue(style, 'resizeParent', '1') == '1'; + treeLayout.groupPadding = mxUtils.getValue(style, 'parentPadding', 20); + treeLayout.levelDistance = mxUtils.getValue(style, 'treeLevelDistance', 30); + treeLayout.maintainParentLocation = true; + treeLayout.edgeRouting = false; + treeLayout.resetEdges = false; + + return treeLayout; + } + else if (style['childLayout'] == 'flowLayout') + { + var flowLayout = new mxHierarchicalLayout(this.graph, mxUtils.getValue(style, + 'flowOrientation', mxConstants.DIRECTION_EAST)); + flowLayout.resizeParent = mxUtils.getValue(style, 'resizeParent', '1') == '1'; + flowLayout.parentBorder = mxUtils.getValue(style, 'parentPadding', 20); + flowLayout.maintainParentLocation = true; + + // Special undocumented styles for changing the hierarchical + flowLayout.intraCellSpacing = mxUtils.getValue(style, 'intraCellSpacing', mxHierarchicalLayout.prototype.intraCellSpacing); + flowLayout.interRankCellSpacing = mxUtils.getValue(style, 'interRankCellSpacing', mxHierarchicalLayout.prototype.interRankCellSpacing); + flowLayout.interHierarchySpacing = mxUtils.getValue(style, 'interHierarchySpacing', mxHierarchicalLayout.prototype.interHierarchySpacing); + flowLayout.parallelEdgeSpacing = mxUtils.getValue(style, 'parallelEdgeSpacing', mxHierarchicalLayout.prototype.parallelEdgeSpacing); + + return flowLayout; + } + + return null; + }; +}; + + /** + * Returns the size of the page format scaled with the page size. + */ +Graph.prototype.getPageSize = function() +{ + return (this.pageVisible) ? new mxRectangle(0, 0, this.pageFormat.width * this.pageScale, + this.pageFormat.height * this.pageScale) : this.scrollTileSize; +}; + +/** + * Returns a rectangle describing the position and count of the + * background pages, where x and y are the position of the top, + * left page and width and height are the vertical and horizontal + * page count. + */ +Graph.prototype.getPageLayout = function() +{ + var size = this.getPageSize(); + var bounds = this.getGraphBounds(); + + if (bounds.width == 0 || bounds.height == 0) + { + return new mxRectangle(0, 0, 1, 1); + } + else + { + // Computes untransformed graph bounds + var x = Math.ceil(bounds.x / this.view.scale - this.view.translate.x); + var y = Math.ceil(bounds.y / this.view.scale - this.view.translate.y); + var w = Math.floor(bounds.width / this.view.scale); + var h = Math.floor(bounds.height / this.view.scale); + + var x0 = Math.floor(x / size.width); + var y0 = Math.floor(y / size.height); + var w0 = Math.ceil((x + w) / size.width) - x0; + var h0 = Math.ceil((y + h) / size.height) - y0; + + return new mxRectangle(x0, y0, w0, h0); + } +}; + +/** + * Sanitizes the given HTML markup. + */ +Graph.prototype.sanitizeHtml = function(value, editing) +{ + // Uses https://code.google.com/p/google-caja/wiki/JsHtmlSanitizer + // NOTE: Original minimized sanitizer was modified to support + // data URIs for images, mailto and special data:-links. + // LATER: Add MathML to whitelisted tags + function urlX(link) + { + if (link != null && link.toString().toLowerCase().substring(0, 11) !== 'javascript:') + { + return link; + } + + return null; + }; + function idX(id) { return id }; + + return html_sanitize(value, urlX, idX); +}; + +/** + * Revalidates all cells with placeholders in the current graph model. + */ +Graph.prototype.updatePlaceholders = function() +{ + var model = this.model; + var validate = false; + + for (var key in this.model.cells) + { + var cell = this.model.cells[key]; + + if (this.isReplacePlaceholders(cell)) + { + this.view.invalidate(cell, false, false); + validate = true; + } + } + + if (validate) + { + this.view.validate(); + } +}; + +/** + * Adds support for placeholders in labels. + */ +Graph.prototype.isReplacePlaceholders = function(cell) +{ + return cell.value != null && typeof(cell.value) == 'object' && + cell.value.getAttribute('placeholders') == '1'; +}; + +/** + * Returns true if the given mouse wheel event should be used for zooming. This + * is invoked if no dialogs are showing and returns true with Alt or Control + * (except macOS) is pressed. + */ +Graph.prototype.isZoomWheelEvent = function(evt) +{ + return mxEvent.isAltDown(evt) || (mxEvent.isMetaDown(evt) && mxClient.IS_MAC) || + (mxEvent.isControlDown(evt) && !mxClient.IS_MAC); +}; + +/** + * Adds Alt+click to select cells behind cells. + */ +Graph.prototype.isTransparentClickEvent = function(evt) +{ + return mxEvent.isAltDown(evt); +}; + +/** + * Adds ctrl+shift+connect to disable connections. + */ +Graph.prototype.isIgnoreTerminalEvent = function(evt) +{ + return mxEvent.isShiftDown(evt) && mxEvent.isControlDown(evt); +}; + +/** + * Adds support for placeholders in labels. + */ +Graph.prototype.isSplitTarget = function(target, cells, evt) +{ + return !this.model.isEdge(cells[0]) && + !mxEvent.isAltDown(evt) && !mxEvent.isShiftDown(evt) && + mxGraph.prototype.isSplitTarget.apply(this, arguments); +}; + +/** + * Adds support for placeholders in labels. + */ +Graph.prototype.getLabel = function(cell) +{ + var result = mxGraph.prototype.getLabel.apply(this, arguments); + + if (result != null && this.isReplacePlaceholders(cell) && cell.getAttribute('placeholder') == null) + { + result = this.replacePlaceholders(cell, result); + } + + return result; +}; + +/** + * Adds labelMovable style. + */ +Graph.prototype.isLabelMovable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return !this.isCellLocked(cell) && + ((this.model.isEdge(cell) && this.edgeLabelsMovable) || + (this.model.isVertex(cell) && (this.vertexLabelsMovable || + mxUtils.getValue(style, 'labelMovable', '0') == '1'))); +}; + +/** + * Adds event if grid size is changed. + */ +Graph.prototype.setGridSize = function(value) +{ + this.gridSize = value; + this.fireEvent(new mxEventObject('gridSizeChanged')); +}; + +/** + * Private helper method. + */ +Graph.prototype.getGlobalVariable = function(name) +{ + var val = null; + + if (name == 'date') + { + val = new Date().toLocaleDateString(); + } + else if (name == 'time') + { + val = new Date().toLocaleTimeString(); + } + else if (name == 'timestamp') + { + val = new Date().toLocaleString(); + } + else if (name.substring(0, 5) == 'date{') + { + var fmt = name.substring(5, name.length - 1); + val = this.formatDate(new Date(), fmt); + } + + return val; +}; + +/** + * Formats a date, see http://blog.stevenlevithan.com/archives/date-time-format + */ +Graph.prototype.formatDate = function(date, mask, utc) +{ + // LATER: Cache regexs + if (this.dateFormatCache == null) + { + this.dateFormatCache = { + i18n: { + dayNames: [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ], + monthNames: [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ] + }, + + masks: { + "default": "ddd mmm dd yyyy HH:MM:ss", + shortDate: "m/d/yy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" + } + }; + } + + var dF = this.dateFormatCache; + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, + timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, + timezoneClip = /[^-+\dA-Z]/g, + pad = function (val, len) { + val = String(val); + len = len || 2; + while (val.length < len) val = "0" + val; + return val; + }; + + // You can't provide utc if you skip other args (use the "UTC:" mask prefix) + if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { + mask = date; + date = undefined; + } + + // Passing date through Date applies Date.parse, if necessary + date = date ? new Date(date) : new Date; + if (isNaN(date)) throw SyntaxError("invalid date"); + + mask = String(dF.masks[mask] || mask || dF.masks["default"]); + + // Allow setting the utc argument via the mask + if (mask.slice(0, 4) == "UTC:") { + mask = mask.slice(4); + utc = true; + } + + var _ = utc ? "getUTC" : "get", + d = date[_ + "Date"](), + D = date[_ + "Day"](), + m = date[_ + "Month"](), + y = date[_ + "FullYear"](), + H = date[_ + "Hours"](), + M = date[_ + "Minutes"](), + s = date[_ + "Seconds"](), + L = date[_ + "Milliseconds"](), + o = utc ? 0 : date.getTimezoneOffset(), + flags = { + d: d, + dd: pad(d), + ddd: dF.i18n.dayNames[D], + dddd: dF.i18n.dayNames[D + 7], + m: m + 1, + mm: pad(m + 1), + mmm: dF.i18n.monthNames[m], + mmmm: dF.i18n.monthNames[m + 12], + yy: String(y).slice(2), + yyyy: y, + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? "a" : "p", + tt: H < 12 ? "am" : "pm", + T: H < 12 ? "A" : "P", + TT: H < 12 ? "AM" : "PM", + Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), + o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] + }; + + return mask.replace(token, function ($0) + { + return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); + }); +}; + +/** + * + */ +Graph.prototype.createLayersDialog = function() +{ + var div = document.createElement('div'); + div.style.position = 'absolute'; + + var model = this.getModel(); + var childCount = model.getChildCount(model.root); + + for (var i = 0; i < childCount; i++) + { + (mxUtils.bind(this, function(layer) + { + var span = document.createElement('div'); + span.style.overflow = 'hidden'; + span.style.textOverflow = 'ellipsis'; + span.style.padding = '2px'; + span.style.whiteSpace = 'nowrap'; + + var cb = document.createElement('input'); + cb.style.display = 'inline-block'; + cb.setAttribute('type', 'checkbox'); + + if (model.isVisible(layer)) + { + cb.setAttribute('checked', 'checked'); + cb.defaultChecked = true; + } + + span.appendChild(cb); + + var title = this.convertValueToString(layer) || (mxResources.get('background') || 'Background'); + span.setAttribute('title', title); + mxUtils.write(span, title); + div.appendChild(span); + + mxEvent.addListener(cb, 'click', function() + { + if (cb.getAttribute('checked') != null) + { + cb.removeAttribute('checked'); + } + else + { + cb.setAttribute('checked', 'checked'); + } + + model.setVisible(layer, cb.checked); + }); + })(model.getChildAt(model.root, i))); + } + + return div; +}; + +/** + * Private helper method. + */ +Graph.prototype.replacePlaceholders = function(cell, str) +{ + var result = []; + + if (str != null) + { + var last = 0; + + while (match = this.placeholderPattern.exec(str)) + { + var val = match[0]; + + if (val.length > 2 && val != '%label%' && val != '%tooltip%') + { + var tmp = null; + + if (match.index > last && str.charAt(match.index - 1) == '%') + { + tmp = val.substring(1); + } + else + { + var name = val.substring(1, val.length - 1); + + // Workaround for invalid char for getting attribute in older versions of IE + if (name.indexOf('{') < 0) + { + var current = cell; + + while (tmp == null && current != null) + { + if (current.value != null && typeof(current.value) == 'object') + { + tmp = (current.hasAttribute(name)) ? ((current.getAttribute(name) != null) ? + current.getAttribute(name) : '') : null; + } + + current = this.model.getParent(current); + } + } + + if (tmp == null) + { + tmp = this.getGlobalVariable(name); + } + } + + result.push(str.substring(last, match.index) + ((tmp != null) ? tmp : val)); + last = match.index + val.length; + } + } + + result.push(str.substring(last)); + } + + return result.join(''); +}; + +/** + * Selects cells for connect vertex return value. + */ +Graph.prototype.selectCellsForConnectVertex = function(cells, evt, hoverIcons) +{ + // Selects only target vertex if one exists + if (cells.length == 2 && this.model.isVertex(cells[1])) + { + this.setSelectionCell(cells[1]); + + if (hoverIcons != null) + { + // Adds hover icons to new target vertex for touch devices + if (mxEvent.isTouchEvent(evt)) + { + hoverIcons.update(hoverIcons.getState(this.view.getState(cells[1]))); + } + else + { + // Hides hover icons after click with mouse + hoverIcons.reset(); + } + } + + this.scrollCellToVisible(cells[1]); + } + else + { + this.setSelectionCells(cells); + } +}; + +/** + * Adds a connection to the given vertex. + */ +Graph.prototype.connectVertex = function(source, direction, length, evt, forceClone, ignoreCellAt) +{ + ignoreCellAt = (ignoreCellAt) ? ignoreCellAt : false; + + var pt = (source.geometry.relative && source.parent.geometry != null) ? + new mxPoint(source.parent.geometry.width * source.geometry.x, source.parent.geometry.height * source.geometry.y) : + new mxPoint(source.geometry.x, source.geometry.y); + + if (direction == mxConstants.DIRECTION_NORTH) + { + pt.x += source.geometry.width / 2; + pt.y -= length ; + } + else if (direction == mxConstants.DIRECTION_SOUTH) + { + pt.x += source.geometry.width / 2; + pt.y += source.geometry.height + length; + } + else if (direction == mxConstants.DIRECTION_WEST) + { + pt.x -= length; + pt.y += source.geometry.height / 2; + } + else + { + pt.x += source.geometry.width + length; + pt.y += source.geometry.height / 2; + } + + var parentState = this.view.getState(this.model.getParent(source)); + var s = this.view.scale; + var t = this.view.translate; + var dx = t.x * s; + var dy = t.y * s; + + if (this.model.isVertex(parentState.cell)) + { + dx = parentState.x; + dy = parentState.y; + } + + // Workaround for relative child cells + if (this.model.isVertex(source.parent) && source.geometry.relative) + { + pt.x += source.parent.geometry.x; + pt.y += source.parent.geometry.y; + } + + // Checks actual end point of edge for target cell + var target = (ignoreCellAt || (mxEvent.isControlDown(evt) && !forceClone)) ? + null : this.getCellAt(dx + pt.x * s, dy + pt.y * s); + + if (this.model.isAncestor(target, source)) + { + target = null; + } + + // Checks if target or ancestor is locked + var temp = target; + + while (temp != null) + { + if (this.isCellLocked(temp)) + { + target = null; + break; + } + + temp = this.model.getParent(temp); + } + + // Checks if source and target intersect + if (target != null) + { + var sourceState = this.view.getState(source); + var targetState = this.view.getState(target); + + if (sourceState != null && targetState != null && mxUtils.intersects(sourceState, targetState)) + { + target = null; + } + } + + var duplicate = !mxEvent.isShiftDown(evt) || forceClone; + + if (duplicate) + { + if (direction == mxConstants.DIRECTION_NORTH) + { + pt.y -= source.geometry.height / 2; + } + else if (direction == mxConstants.DIRECTION_SOUTH) + { + pt.y += source.geometry.height / 2; + } + else if (direction == mxConstants.DIRECTION_WEST) + { + pt.x -= source.geometry.width / 2; + } + else + { + pt.x += source.geometry.width / 2; + } + } + + // Uses connectable parent vertex if one exists + if (target != null && !this.isCellConnectable(target)) + { + var parent = this.getModel().getParent(target); + + if (this.getModel().isVertex(parent) && this.isCellConnectable(parent)) + { + target = parent; + } + } + + if (target == source || this.model.isEdge(target) || !this.isCellConnectable(target)) + { + target = null; + } + + var result = []; + + this.model.beginUpdate(); + try + { + var realTarget = target; + + if (realTarget == null && duplicate) + { + // Handles relative children + var cellToClone = source; + var geo = this.getCellGeometry(source); + + while (geo != null && geo.relative) + { + cellToClone = this.getModel().getParent(cellToClone); + geo = this.getCellGeometry(cellToClone); + } + + // Handle consistuents for cloning + var state = this.view.getState(cellToClone); + var style = (state != null) ? state.style : this.getCellStyle(cellToClone); + + if (mxUtils.getValue(style, 'part', false)) + { + var tmpParent = this.model.getParent(cellToClone); + + if (this.model.isVertex(tmpParent)) + { + cellToClone = tmpParent; + } + } + + realTarget = this.duplicateCells([cellToClone], false)[0]; + + var geo = this.getCellGeometry(realTarget); + + if (geo != null) + { + geo.x = pt.x - geo.width / 2; + geo.y = pt.y - geo.height / 2; + } + } + + // Never connects children in stack layouts + var layout = null; + + if (this.layoutManager != null) + { + layout = this.layoutManager.getLayout(this.model.getParent(source)); + } + + var edge = ((mxEvent.isControlDown(evt) && duplicate) || (target == null && layout != null && layout.constructor == mxStackLayout)) ? null : + this.insertEdge(this.model.getParent(source), null, '', source, realTarget, this.createCurrentEdgeStyle()); + + // Inserts edge before source + if (edge != null && this.connectionHandler.insertBeforeSource) + { + var index = null; + var tmp = source; + + while (tmp.parent != null && tmp.geometry != null && + tmp.geometry.relative && tmp.parent != edge.parent) + { + tmp = this.model.getParent(tmp); + } + + if (tmp != null && tmp.parent != null && tmp.parent == edge.parent) + { + var index = tmp.parent.getIndex(tmp); + this.model.add(tmp.parent, edge, index); + } + } + + // Special case: Click on west icon puts clone before cell + if (target == null && realTarget != null && layout != null && source.parent != null && + layout.constructor == mxStackLayout && direction == mxConstants.DIRECTION_WEST) + { + var index = source.parent.getIndex(source); + this.model.add(source.parent, realTarget, index); + } + + if (edge != null) + { + result.push(edge); + } + + if (target == null && realTarget != null) + { + result.push(realTarget); + } + + if (realTarget == null && edge != null) + { + edge.geometry.setTerminalPoint(pt, false); + } + + if (edge != null) + { + this.fireEvent(new mxEventObject('cellsInserted', 'cells', [edge])); + } + } + finally + { + this.model.endUpdate(); + } + + return result; +}; + +/** + * Returns all labels in the diagram as a string. + */ +Graph.prototype.getIndexableText = function() +{ + var tmp = document.createElement('div'); + var labels = []; + var label = ''; + + for (var key in this.model.cells) + { + var cell = this.model.cells[key]; + + if (this.model.isVertex(cell) || this.model.isEdge(cell)) + { + if (this.isHtmlLabel(cell)) + { + tmp.innerHTML = this.getLabel(cell); + label = mxUtils.extractTextWithWhitespace([tmp]); + } + else + { + label = this.getLabel(cell); + } + + label = mxUtils.trim(label.replace(/[\x00-\x1F\x7F-\x9F]|\s+/g, ' ')); + + if (label.length > 0) + { + labels.push(label); + } + } + } + + return labels.join(' '); +}; + +/** + * Returns the label for the given cell. + */ +Graph.prototype.convertValueToString = function(cell) +{ + if (cell.value != null && typeof(cell.value) == 'object') + { + if (this.isReplacePlaceholders(cell) && cell.getAttribute('placeholder') != null) + { + var name = cell.getAttribute('placeholder'); + var current = cell; + var result = null; + + while (result == null && current != null) + { + if (current.value != null && typeof(current.value) == 'object') + { + result = (current.hasAttribute(name)) ? ((current.getAttribute(name) != null) ? + current.getAttribute(name) : '') : null; + } + + current = this.model.getParent(current); + } + + return result || ''; + } + else + { + return cell.value.getAttribute('label') || ''; + } + } + + return mxGraph.prototype.convertValueToString.apply(this, arguments); +}; + +/** + * Returns the link for the given cell. + */ +Graph.prototype.getLinksForState = function(state) +{ + if (state != null && state.text != null && state.text.node != null) + { + return state.text.node.getElementsByTagName('a'); + } + + return null; +}; + +/** + * Returns the link for the given cell. + */ +Graph.prototype.getLinkForCell = function(cell) +{ + if (cell.value != null && typeof(cell.value) == 'object') + { + var link = cell.value.getAttribute('link'); + + // Removes links with leading javascript: protocol + // TODO: Check more possible attack vectors + if (link != null && link.toLowerCase().substring(0, 11) === 'javascript:') + { + link = link.substring(11); + } + + return link; + } + + return null; +}; + +/** + * Overrides label orientation for collapsed swimlanes inside stack. + */ +Graph.prototype.getCellStyle = function(cell) +{ + var style = mxGraph.prototype.getCellStyle.apply(this, arguments); + + if (cell != null && this.layoutManager != null) + { + var parent = this.model.getParent(cell); + + if (this.model.isVertex(parent) && this.isCellCollapsed(cell)) + { + var layout = this.layoutManager.getLayout(parent); + + if (layout != null && layout.constructor == mxStackLayout) + { + style[mxConstants.STYLE_HORIZONTAL] = !layout.horizontal; + } + } + } + + return style; +}; + +/** + * Disables alternate width persistence for stack layout parents + */ +Graph.prototype.updateAlternateBounds = function(cell, geo, willCollapse) +{ + if (cell != null && geo != null && this.layoutManager != null && geo.alternateBounds != null) + { + var layout = this.layoutManager.getLayout(this.model.getParent(cell)); + + if (layout != null && layout.constructor == mxStackLayout) + { + if (layout.horizontal) + { + geo.alternateBounds.height = 0; + } + else + { + geo.alternateBounds.width = 0; + } + } + } + + mxGraph.prototype.updateAlternateBounds.apply(this, arguments); +}; + +/** + * Adds Shift+collapse/expand and size management for folding inside stack + */ +Graph.prototype.isMoveCellsEvent = function(evt) +{ + return mxEvent.isShiftDown(evt); +}; + +/** + * Adds Shift+collapse/expand and size management for folding inside stack + */ +Graph.prototype.foldCells = function(collapse, recurse, cells, checkFoldable, evt) +{ + recurse = (recurse != null) ? recurse : false; + + if (cells == null) + { + cells = this.getFoldableCells(this.getSelectionCells(), collapse); + } + + if (cells != null) + { + this.model.beginUpdate(); + + try + { + mxGraph.prototype.foldCells.apply(this, arguments); + + // Resizes all parent stacks if alt is not pressed + if (this.layoutManager != null) + { + for (var i = 0; i < cells.length; i++) + { + var state = this.view.getState(cells[i]); + var geo = this.getCellGeometry(cells[i]); + + if (state != null && geo != null) + { + var dx = Math.round(geo.width - state.width / this.view.scale); + var dy = Math.round(geo.height - state.height / this.view.scale); + + if (dy != 0 || dx != 0) + { + var parent = this.model.getParent(cells[i]); + var layout = this.layoutManager.getLayout(parent); + + if (layout == null) + { + // Moves cells to the right and down after collapse/expand + if (evt != null && this.isMoveCellsEvent(evt)) + { + this.moveSiblings(state, parent, dx, dy); + } + } + else if ((evt == null || !mxEvent.isAltDown(evt)) && layout.constructor == mxStackLayout && !layout.resizeLast) + { + this.resizeParentStacks(parent, layout, dx, dy); + } + } + } + } + } + } + finally + { + this.model.endUpdate(); + } + + // Selects cells after folding + if (this.isEnabled()) + { + this.setSelectionCells(cells); + } + } +}; + +/** + * Overrides label orientation for collapsed swimlanes inside stack. + */ +Graph.prototype.moveSiblings = function(state, parent, dx, dy) +{ + this.model.beginUpdate(); + try + { + var cells = this.getCellsBeyond(state.x, state.y, parent, true, true); + + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != state.cell) + { + var tmp = this.view.getState(cells[i]); + var geo = this.getCellGeometry(cells[i]); + + if (tmp != null && geo != null) + { + geo = geo.clone(); + geo.translate(Math.round(dx * Math.max(0, Math.min(1, (tmp.x - state.x) / state.width))), + Math.round(dy * Math.max(0, Math.min(1, (tmp.y - state.y) / state.height)))); + this.model.setGeometry(cells[i], geo); + } + } + } + } + finally + { + this.model.endUpdate(); + } +}; + +/** + * Overrides label orientation for collapsed swimlanes inside stack. + */ +Graph.prototype.resizeParentStacks = function(parent, layout, dx, dy) +{ + if (this.layoutManager != null && layout != null && layout.constructor == mxStackLayout && !layout.resizeLast) + { + this.model.beginUpdate(); + try + { + var dir = layout.horizontal; + + // Bubble resize up for all parent stack layouts with same orientation + while (parent != null && layout != null && layout.constructor == mxStackLayout && + layout.horizontal == dir && !layout.resizeLast) + { + var pgeo = this.getCellGeometry(parent); + var pstate = this.view.getState(parent); + + if (pstate != null && pgeo != null) + { + pgeo = pgeo.clone(); + + if (layout.horizontal) + { + pgeo.width += dx + Math.min(0, pstate.width / this.view.scale - pgeo.width); + } + else + { + pgeo.height += dy + Math.min(0, pstate.height / this.view.scale - pgeo.height); + } + + this.model.setGeometry(parent, pgeo); + } + + parent = this.model.getParent(parent); + layout = this.layoutManager.getLayout(parent); + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Disables drill-down for non-swimlanes. + */ +Graph.prototype.isContainer = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + if (this.isSwimlane(cell)) + { + return style['container'] != '0'; + } + else + { + return style['container'] == '1'; + } +}; + +/** + * Adds a connectable style. + */ +Graph.prototype.isCellConnectable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return (style['connectable'] != null) ? style['connectable'] != '0' : + mxGraph.prototype.isCellConnectable.apply(this, arguments); +}; + +/** + * Function: selectAll + * + * Selects all children of the given parent cell or the children of the + * default parent if no parent is specified. To select leaf vertices and/or + * edges use . + * + * Parameters: + * + * parent - Optional whose children should be selected. + * Default is . + */ +Graph.prototype.selectAll = function(parent) +{ + parent = parent || this.getDefaultParent(); + + if (!this.isCellLocked(parent)) + { + mxGraph.prototype.selectAll.apply(this, arguments); + } +}; + +/** + * Function: selectCells + * + * Selects all vertices and/or edges depending on the given boolean + * arguments recursively, starting at the given parent or the default + * parent if no parent is specified. Use to select all cells. + * For vertices, only cells with no children are selected. + * + * Parameters: + * + * vertices - Boolean indicating if vertices should be selected. + * edges - Boolean indicating if edges should be selected. + * parent - Optional that acts as the root of the recursion. + * Default is . + */ +Graph.prototype.selectCells = function(vertices, edges, parent) +{ + parent = parent || this.getDefaultParent(); + + if (!this.isCellLocked(parent)) + { + mxGraph.prototype.selectCells.apply(this, arguments); + } +}; + +/** + * Function: getSwimlaneAt + * + * Returns the bottom-most swimlane that intersects the given point (x, y) + * in the cell hierarchy that starts at the given parent. + * + * Parameters: + * + * x - X-coordinate of the location to be checked. + * y - Y-coordinate of the location to be checked. + * parent - that should be used as the root of the recursion. + * Default is . + */ +Graph.prototype.getSwimlaneAt = function (x, y, parent) +{ + parent = parent || this.getDefaultParent(); + + if (!this.isCellLocked(parent)) + { + return mxGraph.prototype.getSwimlaneAt.apply(this, arguments); + } + + return null; +}; + +/** + * Disables folding for non-swimlanes. + */ +Graph.prototype.isCellFoldable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.foldingEnabled && !this.isCellLocked(cell) && + ((this.isContainer(cell) && style['collapsible'] != '0') || + (!this.isContainer(cell) && style['collapsible'] == '1')); +}; + +/** + * Stops all interactions and clears the selection. + */ +Graph.prototype.reset = function() +{ + if (this.isEditing()) + { + this.stopEditing(true); + } + + this.escape(); + + if (!this.isSelectionEmpty()) + { + this.clearSelection(); + } +}; + +/** + * Overridden to limit zoom to 1% - 16.000%. + */ +Graph.prototype.zoom = function(factor, center) +{ + factor = Math.max(0.01, Math.min(this.view.scale * factor, 160)) / this.view.scale; + + mxGraph.prototype.zoom.apply(this, arguments); +}; + +/** + * Function: zoomIn + * + * Zooms into the graph by . + */ +Graph.prototype.zoomIn = function() +{ + // Switches to 1% zoom steps below 15% + if (this.view.scale < 0.15) + { + this.zoom((this.view.scale + 0.01) / this.view.scale); + } + else + { + // Uses to 5% zoom steps for better grid rendering in webkit + // and to avoid rounding errors for zoom steps + this.zoom((Math.round(this.view.scale * this.zoomFactor * 20) / 20) / this.view.scale); + } +}; + +/** + * Function: zoomOut + * + * Zooms out of the graph by . + */ +Graph.prototype.zoomOut = function() +{ + // Switches to 1% zoom steps below 15% + if (this.view.scale <= 0.15) + { + this.zoom((this.view.scale - 0.01) / this.view.scale); + } + else + { + // Uses to 5% zoom steps for better grid rendering in webkit + // and to avoid rounding errors for zoom steps + this.zoom((Math.round(this.view.scale * (1 / this.zoomFactor) * 20) / 20) / this.view.scale); + } +}; + +/** + * Overrides tooltips to show custom tooltip or metadata. + */ +Graph.prototype.getTooltipForCell = function(cell) +{ + var tip = ''; + + if (mxUtils.isNode(cell.value)) + { + var tmp = cell.value.getAttribute('tooltip'); + + if (tmp != null) + { + if (tmp != null && this.isReplacePlaceholders(cell)) + { + tmp = this.replacePlaceholders(cell, tmp); + } + + tip = this.sanitizeHtml(tmp); + } + else + { + var ignored = this.builtInProperties; + var attrs = cell.value.attributes; + var temp = []; + + // Hides links in edit mode + if (this.isEnabled()) + { + ignored.push('link'); + } + + for (var i = 0; i < attrs.length; i++) + { + if (mxUtils.indexOf(ignored, attrs[i].nodeName) < 0 && attrs[i].nodeValue.length > 0) + { + temp.push({name: attrs[i].nodeName, value: attrs[i].nodeValue}); + } + } + + // Sorts by name + temp.sort(function(a, b) + { + if (a.name < b.name) + { + return -1; + } + else if (a.name > b.name) + { + return 1; + } + else + { + return 0; + } + }); + + for (var i = 0; i < temp.length; i++) + { + if (temp[i].name != 'link' || !this.isCustomLink(temp[i].value)) + { + tip += ((temp[i].name != 'link') ? '' + temp[i].name + ': ' : '') + + mxUtils.htmlEntities(temp[i].value) + '\n'; + } + } + + if (tip.length > 0) + { + tip = tip.substring(0, tip.length - 1); + + if (mxClient.IS_SVG) + { + tip = '
' + tip + '
'; + } + } + } + } + + return tip; +}; + +/** + * Turns the given string into an array. + */ +Graph.prototype.stringToBytes = function(str) +{ + var arr = new Array(str.length); + + for (var i = 0; i < str.length; i++) + { + arr[i] = str.charCodeAt(i); + } + + return arr; +}; + +/** + * Turns the given array into a string. + */ +Graph.prototype.bytesToString = function(arr) +{ + var result = new Array(arr.length); + + for (var i = 0; i < arr.length; i++) + { + result[i] = String.fromCharCode(arr[i]); + } + + return result.join(''); +}; + +/** + * Returns a base64 encoded version of the compressed string. + */ +Graph.prototype.compress = function(data) +{ + if (data == null || data.length == 0 || typeof(pako) === 'undefined') + { + return data; + } + else + { + var tmp = this.bytesToString(pako.deflateRaw(encodeURIComponent(data))); + + return (window.btoa) ? btoa(tmp) : Base64.encode(tmp, true); + } +}; + +/** + * Returns a decompressed version of the base64 encoded string. + */ +Graph.prototype.decompress = function(data) +{ + if (data == null || data.length == 0 || typeof(pako) === 'undefined') + { + return data; + } + else + { + var tmp = (window.atob) ? atob(data) : Base64.decode(data, true); + + return this.zapGremlins(decodeURIComponent( + this.bytesToString(pako.inflateRaw(tmp)))); + } +}; + +/** + * Removes all illegal control characters with ASCII code <32 except TAB, LF + * and CR. + */ +Graph.prototype.zapGremlins = function(text) +{ + var checked = []; + + for (var i = 0; i < text.length; i++) + { + var code = text.charCodeAt(i); + + // Removes all control chars except TAB, LF and CR + if (code >= 32 || code == 9 || code == 10 || code == 13) + { + checked.push(text.charAt(i)); + } + } + + return checked.join(''); +}; + +/** + * Hover icons are used for hover, vertex handler and drag from sidebar. + */ +HoverIcons = function(graph) +{ + this.graph = graph; + this.init(); +}; + +/** + * Up arrow. + */ +HoverIcons.prototype.arrowSpacing = 2; + +/** + * Delay to switch to another state for overlapping bbox. Default is 500ms. + */ +HoverIcons.prototype.updateDelay = 500; + +/** + * Delay to switch between states. Default is 140ms. + */ +HoverIcons.prototype.activationDelay = 140; + +/** + * Up arrow. + */ +HoverIcons.prototype.currentState = null; + +/** + * Up arrow. + */ +HoverIcons.prototype.activeArrow = null; + +/** + * Up arrow. + */ +HoverIcons.prototype.inactiveOpacity = 15; + +/** + * Up arrow. + */ +HoverIcons.prototype.cssCursor = 'copy'; + +/** + * Whether to hide arrows that collide with vertices. + * LATER: Add keyboard override, touch support. + */ +HoverIcons.prototype.checkCollisions = true; + +/** + * Up arrow. + */ +HoverIcons.prototype.arrowFill = '#29b6f2'; + +/** + * Up arrow. + */ +HoverIcons.prototype.triangleUp = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/triangle-up.png', 26, 14) : + Graph.createSvgImage(18, 28, ''); + +/** + * Right arrow. + */ +HoverIcons.prototype.triangleRight = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/triangle-right.png', 14, 26) : + Graph.createSvgImage(26, 18, ''); + +/** + * Down arrow. + */ +HoverIcons.prototype.triangleDown = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/triangle-down.png', 26, 14) : + Graph.createSvgImage(18, 26, ''); + +/** + * Left arrow. + */ +HoverIcons.prototype.triangleLeft = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/triangle-left.png', 14, 26) : + Graph.createSvgImage(28, 18, ''); + +/** + * Round target. + */ +HoverIcons.prototype.roundDrop = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/round-drop.png', 26, 26) : + Graph.createSvgImage(26, 26, ''); + +/** + * Refresh target. + */ +HoverIcons.prototype.refreshTarget = new mxImage((mxClient.IS_SVG) ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAmCAYAAACoPemuAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDQxNERDRTU1QjY1MTFFNDkzNTRFQTVEMTdGMTdBQjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDQxNERDRTY1QjY1MTFFNDkzNTRFQTVEMTdGMTdBQjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0NDE0RENFMzVCNjUxMUU0OTM1NEVBNUQxN0YxN0FCNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0NDE0RENFNDVCNjUxMUU0OTM1NEVBNUQxN0YxN0FCNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsvuX50AAANaSURBVHja7FjRZ1tRGD9ZJ1NCyIQSwrivI4Q8hCpjlFDyFEoYfSp9Ko1QWnmo0If+BSXkIfo0QirTMUpeGo2EPfWllFYjZMLKLDJn53d3biU337m5J223bPbxk5t7v+/c3/2+73znO8fDOWezKM/YjMpz68Lj8ejY+QTeCCwLxOS9qPxtyN+6wAeBTwJ31CCO0cJDjXBGBN4LfIepSwykTUT1bgpuib0SONIgo8KRHOtRiCFcvUcgZeGrHPNBxLIyFPyRgTGz0xLbegJCdmzpElue5KlAIMDX19d5uVzm5+fnfDAYmMA17uEZdOx2Yvb/sHlu2S0xwymn5ufneTab5b1ej08S6EAXNrDd2dnhiUTim21MvMtwQ6yiIrWwsMDPzs64rsBmf3/fvM7n89TYlUnEllSkQqEQv7q64g+Vk5MTVXosORErU0Zer5f0FEIlw2N6MxwO82QyaXql2+2SxDqdjopYWUUsqEp45IldqtWq6UWVh/1+P7+8vCTJ4QMUJSRIEXuneoH96w8PDyeWAnhSJfCqwm6NIlaklFdXV0cGhRcQ2mlJQXK5nMq2YPEZbnteU1U2lUqN/D84OGD9fl+5fgnSrFarsUwmw0qlEru4uBjTicViTk3Cr27HSnxR+Doyz0ZE1CAWiUTusbu7y9rttlZv5fP5WDQavYfIMba4uEipfhF8XtqJoZXx/uH+sC/4vPg7OljZZQbsCmLtYzc3N6zRaJhotVrmfx0xDINtbm6athYUeXpHdbBNaqZUKpWxWXV7e2vex+xaWVnhc3NzjrPUXgexyCt0m67LBV7uJMITjqRE4o8tZeg8FPpFitgapYxiOC0poFgsji1jKNo6BZZckrAGUtJsNk1vqAihCBcKhTE7hNWhqw2qFnGy5UFOUYJVIJ1OjzSE+BCEilon0URavRmBqnbbQ00AXbm+vnZc9O1tj72OnQoc2+cwygRkb2+P1et17ZoEm3g87lRmjgWZ00kbXkNuse6/Bu2wlegIxfb2tuvWGroO4bO2c4bbzUh60mxDXm1sbJhhxkQYnhS4h2fUZoRAWnf7lv8N27f8P7Xhnekjgpk+VKGOoQbsiY+hhhtF3YO7twIJ+ULvUGv+GQ2fQEvWxI/THNx5/p/BaspPAQYAqStgiSQwCDoAAAAASUVORK5CYII=' : + IMAGE_PATH + '/refresh.png', 38, 38); + +/** + * Tolerance for hover icon clicks. + */ +HoverIcons.prototype.tolerance = (mxClient.IS_TOUCH) ? 6 : 0; + +/** + * + */ +HoverIcons.prototype.init = function() +{ + this.arrowUp = this.createArrow(this.triangleUp, mxResources.get('plusTooltip')); + this.arrowRight = this.createArrow(this.triangleRight, mxResources.get('plusTooltip')); + this.arrowDown = this.createArrow(this.triangleDown, mxResources.get('plusTooltip')); + this.arrowLeft = this.createArrow(this.triangleLeft, mxResources.get('plusTooltip')); + + this.elts = [this.arrowUp, this.arrowRight, this.arrowDown, this.arrowLeft]; + + this.repaintHandler = mxUtils.bind(this, function() + { + this.repaint(); + }); + + this.graph.selectionModel.addListener(mxEvent.CHANGE, this.repaintHandler); + this.graph.model.addListener(mxEvent.CHANGE, this.repaintHandler); + this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.repaintHandler); + this.graph.view.addListener(mxEvent.TRANSLATE, this.repaintHandler); + this.graph.view.addListener(mxEvent.SCALE, this.repaintHandler); + this.graph.view.addListener(mxEvent.DOWN, this.repaintHandler); + this.graph.view.addListener(mxEvent.UP, this.repaintHandler); + this.graph.addListener(mxEvent.ROOT, this.repaintHandler); + + // Resets the mouse point on escape + this.graph.addListener(mxEvent.ESCAPE, mxUtils.bind(this, function() + { + this.mouseDownPoint = null; + })); + + // Removes hover icons if mouse leaves the container + mxEvent.addListener(this.graph.container, 'mouseleave', mxUtils.bind(this, function(evt) + { + // Workaround for IE11 firing mouseleave for touch in diagram + if (evt.relatedTarget != null && mxEvent.getSource(evt) == this.graph.container) + { + this.setDisplay('none'); + } + })); + + // Resets current state when in-place editor starts + this.graph.addListener(mxEvent.START_EDITING, mxUtils.bind(this, function(evt) + { + this.reset(); + })); + + // Resets current state after update of selection state for touch events + var graphClick = this.graph.click; + this.graph.click = mxUtils.bind(this, function(me) + { + graphClick.apply(this.graph, arguments); + + if (this.currentState != null && !this.graph.isCellSelected(this.currentState.cell) && + mxEvent.isTouchEvent(me.getEvent()) && !this.graph.model.isVertex(me.getCell())) + { + this.reset(); + } + }); + + // Checks if connection handler was active in mouse move + // as workaround for possible double connection inserted + var connectionHandlerActive = false; + + // Implements a listener for hover and click handling + this.graph.addMouseListener( + { + mouseDown: mxUtils.bind(this, function(sender, me) + { + connectionHandlerActive = false; + var evt = me.getEvent(); + + if (this.isResetEvent(evt)) + { + this.reset(); + } + else if (!this.isActive()) + { + var state = this.getState(me.getState()); + + if (state != null || !mxEvent.isTouchEvent(evt)) + { + this.update(state); + } + } + + this.setDisplay('none'); + }), + mouseMove: mxUtils.bind(this, function(sender, me) + { + var evt = me.getEvent(); + + if (this.isResetEvent(evt)) + { + this.reset(); + } + else if (!this.graph.isMouseDown && !mxEvent.isTouchEvent(evt)) + { + this.update(this.getState(me.getState()), + me.getGraphX(), me.getGraphY()); + } + + if (this.graph.connectionHandler != null && + this.graph.connectionHandler.shape != null) + { + connectionHandlerActive = true; + } + }), + mouseUp: mxUtils.bind(this, function(sender, me) + { + var evt = me.getEvent(); + var pt = mxUtils.convertPoint(this.graph.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)) + + if (this.isResetEvent(evt)) + { + this.reset(); + } + else if (this.isActive() && !connectionHandlerActive && + this.mouseDownPoint != null) + { + this.click(this.currentState, this.getDirection(), me); + } + else if (this.isActive()) + { + // Selects target vertex after drag and clone if not only new edge was inserted + if (this.graph.getSelectionCount() != 1 || !this.graph.model.isEdge( + this.graph.getSelectionCell())) + { + this.update(this.getState(this.graph.view.getState( + this.graph.getCellAt(me.getGraphX(), me.getGraphY())))); + } + else + { + this.reset(); + } + } + else if (mxEvent.isTouchEvent(evt) || (this.bbox != null && + mxUtils.contains(this.bbox, me.getGraphX(), me.getGraphY()))) + { + // Shows existing hover icons if inside bounding box + this.setDisplay(''); + this.repaint(); + } + else if (!mxEvent.isTouchEvent(evt)) + { + this.reset(); + } + + connectionHandlerActive = false; + this.resetActiveArrow(); + }) + }); +}; + +/** + * + */ +HoverIcons.prototype.isResetEvent = function(evt, allowShift) +{ + return mxEvent.isAltDown(evt) || (this.activeArrow == null && mxEvent.isShiftDown(evt)) || + mxEvent.isMetaDown(evt) || (mxEvent.isPopupTrigger(evt) && !mxEvent.isControlDown(evt)); +}; + +/** + * + */ +HoverIcons.prototype.createArrow = function(img, tooltip) +{ + var arrow = null; + + if (mxClient.IS_IE && !mxClient.IS_SVG) + { + // Workaround for PNG images in IE6 + if (mxClient.IS_IE6 && document.compatMode != 'CSS1Compat') + { + arrow = document.createElement(mxClient.VML_PREFIX + ':image'); + arrow.setAttribute('src', img.src); + arrow.style.borderStyle = 'none'; + } + else + { + arrow = document.createElement('div'); + arrow.style.backgroundImage = 'url(' + img.src + ')'; + arrow.style.backgroundPosition = 'center'; + arrow.style.backgroundRepeat = 'no-repeat'; + } + + arrow.style.width = (img.width + 4) + 'px'; + arrow.style.height = (img.height + 4) + 'px'; + arrow.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; + } + else + { + arrow = mxUtils.createImage(img.src); + arrow.style.width = img.width + 'px'; + arrow.style.height = img.height + 'px'; + arrow.style.padding = this.tolerance + 'px'; + } + + if (tooltip != null) + { + arrow.setAttribute('title', tooltip); + } + + arrow.style.position = 'absolute'; + arrow.style.cursor = this.cssCursor; + + mxEvent.addGestureListeners(arrow, mxUtils.bind(this, function(evt) + { + if (this.currentState != null && !this.isResetEvent(evt)) + { + this.mouseDownPoint = mxUtils.convertPoint(this.graph.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + this.drag(evt, this.mouseDownPoint.x, this.mouseDownPoint.y); + this.activeArrow = arrow; + this.setDisplay('none'); + mxEvent.consume(evt); + } + })); + + // Captures mouse events as events on graph + mxEvent.redirectMouseEvents(arrow, this.graph, this.currentState); + + mxEvent.addListener(arrow, 'mouseenter', mxUtils.bind(this, function(evt) + { + // Workaround for Firefox firing mouseenter on touchend + if (mxEvent.isMouseEvent(evt)) + { + if (this.activeArrow != null && this.activeArrow != arrow) + { + mxUtils.setOpacity(this.activeArrow, this.inactiveOpacity); + } + + this.graph.connectionHandler.constraintHandler.reset(); + mxUtils.setOpacity(arrow, 100); + this.activeArrow = arrow; + } + })); + + mxEvent.addListener(arrow, 'mouseleave', mxUtils.bind(this, function(evt) + { + // Workaround for IE11 firing this event on touch + if (!this.graph.isMouseDown) + { + this.resetActiveArrow(); + } + })); + + return arrow; +}; + +/** + * + */ +HoverIcons.prototype.resetActiveArrow = function() +{ + if (this.activeArrow != null) + { + mxUtils.setOpacity(this.activeArrow, this.inactiveOpacity); + this.activeArrow = null; + } +}; + +/** + * + */ +HoverIcons.prototype.getDirection = function() +{ + var dir = mxConstants.DIRECTION_EAST; + + if (this.activeArrow == this.arrowUp) + { + dir = mxConstants.DIRECTION_NORTH; + } + else if (this.activeArrow == this.arrowDown) + { + dir = mxConstants.DIRECTION_SOUTH; + } + else if (this.activeArrow == this.arrowLeft) + { + dir = mxConstants.DIRECTION_WEST; + } + + return dir; +}; + +/** + * + */ +HoverIcons.prototype.visitNodes = function(visitor) +{ + for (var i = 0; i < this.elts.length; i++) + { + if (this.elts[i] != null) + { + visitor(this.elts[i]); + } + } +}; + +/** + * + */ +HoverIcons.prototype.removeNodes = function() +{ + this.visitNodes(function(elt) + { + if (elt.parentNode != null) + { + elt.parentNode.removeChild(elt); + } + }); +}; + +/** + * + */ +HoverIcons.prototype.setDisplay = function(display) +{ + this.visitNodes(function(elt) + { + elt.style.display = display; + }); +}; + +/** + * + */ +HoverIcons.prototype.isActive = function() +{ + return this.activeArrow != null && this.currentState != null; +}; + +/** + * + */ +HoverIcons.prototype.drag = function(evt, x, y) +{ + this.graph.popupMenuHandler.hideMenu(); + this.graph.stopEditing(false); + + // Checks if state was removed in call to stopEditing above + if (this.currentState != null) + { + this.graph.connectionHandler.start(this.currentState, x, y); + this.graph.isMouseTrigger = mxEvent.isMouseEvent(evt); + this.graph.isMouseDown = true; + + // Hides handles for selection cell + var handler = this.graph.selectionCellsHandler.getHandler(this.currentState.cell); + + if (handler != null) + { + handler.setHandlesVisible(false); + } + + // Ctrl+shift drag sets source constraint + var es = this.graph.connectionHandler.edgeState; + + if (evt != null && mxEvent.isShiftDown(evt) && mxEvent.isControlDown(evt) && es != null && + mxUtils.getValue(es.style, mxConstants.STYLE_EDGE, null) === 'orthogonalEdgeStyle') + { + var direction = this.getDirection(); + es.cell.style = mxUtils.setStyle(es.cell.style, 'sourcePortConstraint', direction); + es.style['sourcePortConstraint'] = direction; + } + } +}; + +/** + * + */ +HoverIcons.prototype.getStateAt = function(state, x, y) +{ + return this.graph.view.getState(this.graph.getCellAt(x, y)); +}; + +/** + * + */ +HoverIcons.prototype.click = function(state, dir, me) +{ + var evt = me.getEvent(); + var x = me.getGraphX(); + var y = me.getGraphY(); + + var tmp = this.getStateAt(state, x, y); + + if (tmp != null && this.graph.model.isEdge(tmp.cell) && !mxEvent.isControlDown(evt) && + (tmp.getVisibleTerminalState(true) == state || tmp.getVisibleTerminalState(false) == state)) + { + this.graph.setSelectionCell(tmp.cell); + this.reset(); + } + else if (state != null) + { + var cells = this.graph.connectVertex(state.cell, dir, this.graph.defaultEdgeLength, evt); + this.graph.selectCellsForConnectVertex(cells, evt, this); + + // Selects only target vertex if one exists + if (cells.length == 2 && this.graph.model.isVertex(cells[1])) + { + this.graph.setSelectionCell(cells[1]); + + // Adds hover icons to new target vertex for touch devices + if (mxEvent.isTouchEvent(evt)) + { + this.update(this.getState(this.graph.view.getState(cells[1]))); + } + else + { + // Hides hover icons after click with mouse + this.reset(); + } + + this.graph.scrollCellToVisible(cells[1]); + } + else + { + this.graph.setSelectionCells(cells); + } + } + + me.consume(); +}; + +/** + * + */ +HoverIcons.prototype.reset = function(clearTimeout) +{ + clearTimeout = (clearTimeout == null) ? true : clearTimeout; + + if (clearTimeout && this.updateThread != null) + { + window.clearTimeout(this.updateThread); + } + + this.mouseDownPoint = null; + this.currentState = null; + this.activeArrow = null; + this.removeNodes(); + this.bbox = null; +}; + +/** + * + */ +HoverIcons.prototype.repaint = function() +{ + this.bbox = null; + + if (this.currentState != null) + { + // Checks if cell was deleted + this.currentState = this.getState(this.currentState); + + // Cell was deleted + if (this.currentState != null && + this.graph.model.isVertex(this.currentState.cell) && + this.graph.isCellConnectable(this.currentState.cell)) + { + var bds = mxRectangle.fromRectangle(this.currentState); + + // Uses outer bounding box to take rotation into account + if (this.currentState.shape != null && this.currentState.shape.boundingBox != null) + { + bds = mxRectangle.fromRectangle(this.currentState.shape.boundingBox); + } + + bds.grow(this.graph.tolerance); + bds.grow(this.arrowSpacing); + + var handler = this.graph.selectionCellsHandler.getHandler(this.currentState.cell); + + if (handler != null) + { + bds.x -= handler.horizontalOffset / 2; + bds.y -= handler.verticalOffset / 2; + bds.width += handler.horizontalOffset; + bds.height += handler.verticalOffset; + + // Adds bounding box of rotation handle to avoid overlap + if (handler.rotationShape != null && handler.rotationShape.node != null && + handler.rotationShape.node.style.visibility != 'hidden' && + handler.rotationShape.node.style.display != 'none' && + handler.rotationShape.boundingBox != null) + { + bds.add(handler.rotationShape.boundingBox); + } + } + + this.arrowUp.style.left = Math.round(this.currentState.getCenterX() - this.triangleUp.width / 2 - this.tolerance) + 'px'; + this.arrowUp.style.top = Math.round(bds.y - this.triangleUp.height - this.tolerance) + 'px'; + mxUtils.setOpacity(this.arrowUp, this.inactiveOpacity); + + this.arrowRight.style.left = Math.round(bds.x + bds.width - this.tolerance) + 'px'; + this.arrowRight.style.top = Math.round(this.currentState.getCenterY() - this.triangleRight.height / 2 - this.tolerance) + 'px'; + mxUtils.setOpacity(this.arrowRight, this.inactiveOpacity); + + this.arrowDown.style.left = this.arrowUp.style.left; + this.arrowDown.style.top = Math.round(bds.y + bds.height - this.tolerance) + 'px'; + mxUtils.setOpacity(this.arrowDown, this.inactiveOpacity); + + this.arrowLeft.style.left = Math.round(bds.x - this.triangleLeft.width - this.tolerance) + 'px'; + this.arrowLeft.style.top = this.arrowRight.style.top; + mxUtils.setOpacity(this.arrowLeft, this.inactiveOpacity); + + if (this.checkCollisions) + { + var right = this.graph.getCellAt(bds.x + bds.width + + this.triangleRight.width / 2, this.currentState.getCenterY()); + var left = this.graph.getCellAt(bds.x - this.triangleLeft.width / 2, this.currentState.getCenterY()); + var top = this.graph.getCellAt(this.currentState.getCenterX(), bds.y - this.triangleUp.height / 2); + var bottom = this.graph.getCellAt(this.currentState.getCenterX(), bds.y + bds.height + this.triangleDown.height / 2); + + // Shows hover icons large cell is behind all directions of current cell + if (right != null && right == left && left == top && top == bottom) + { + right = null; + left = null; + top = null; + bottom = null; + } + + var currentGeo = this.graph.getCellGeometry(this.currentState.cell); + + var checkCollision = mxUtils.bind(this, function(cell, arrow) + { + var geo = this.graph.model.isVertex(cell) && this.graph.getCellGeometry(cell); + + // Ignores collision if vertex is more than 3 times the size of this vertex + if (cell != null && !this.graph.model.isAncestor(cell, this.currentState.cell) && + (geo == null || currentGeo == null || (geo.height < 6 * currentGeo.height && + geo.width < 6 * currentGeo.width))) + { + arrow.style.visibility = 'hidden'; + } + else + { + arrow.style.visibility = 'visible'; + } + }); + + checkCollision(right, this.arrowRight); + checkCollision(left, this.arrowLeft); + checkCollision(top, this.arrowUp); + checkCollision(bottom, this.arrowDown); + } + else + { + this.arrowLeft.style.visibility = 'visible'; + this.arrowRight.style.visibility = 'visible'; + this.arrowUp.style.visibility = 'visible'; + this.arrowDown.style.visibility = 'visible'; + } + + if (this.graph.tooltipHandler.isEnabled()) + { + this.arrowLeft.setAttribute('title', mxResources.get('plusTooltip')); + this.arrowRight.setAttribute('title', mxResources.get('plusTooltip')); + this.arrowUp.setAttribute('title', mxResources.get('plusTooltip')); + this.arrowDown.setAttribute('title', mxResources.get('plusTooltip')); + } + else + { + this.arrowLeft.removeAttribute('title'); + this.arrowRight.removeAttribute('title'); + this.arrowUp.removeAttribute('title'); + this.arrowDown.removeAttribute('title'); + } + } + else + { + this.reset(); + } + + // Updates bounding box + if (this.currentState != null) + { + this.bbox = this.computeBoundingBox(); + + // Adds tolerance for hover + if (this.bbox != null) + { + this.bbox.grow(10); + } + } + } +}; + +/** + * + */ +HoverIcons.prototype.computeBoundingBox = function() +{ + var bbox = (!this.graph.model.isEdge(this.currentState.cell)) ? mxRectangle.fromRectangle(this.currentState) : null; + + this.visitNodes(function(elt) + { + if (elt.parentNode != null) + { + var tmp = new mxRectangle(elt.offsetLeft, elt.offsetTop, elt.offsetWidth, elt.offsetHeight); + + if (bbox == null) + { + bbox = tmp; + } + else + { + bbox.add(tmp); + } + } + }); + + return bbox; +}; + +/** + * + */ +HoverIcons.prototype.getState = function(state) +{ + if (state != null) + { + var cell = state.cell; + + // Uses connectable parent vertex if child is not connectable + if (this.graph.getModel().isVertex(cell) && !this.graph.isCellConnectable(cell)) + { + var parent = this.graph.getModel().getParent(cell); + + if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent)) + { + cell = parent; + } + } + + // Ignores locked cells and edges + if (this.graph.isCellLocked(cell) || this.graph.model.isEdge(cell)) + { + cell = null; + } + + state = this.graph.view.getState(cell); + } + + return state; +}; + +/** + * + */ +HoverIcons.prototype.update = function(state, x, y) +{ + if (!this.graph.connectionArrowsEnabled) + { + this.reset(); + } + else + { + var timeOnTarget = null; + + // Time on target + if (this.prev != state || this.isActive()) + { + this.startTime = new Date().getTime(); + this.prev = state; + timeOnTarget = 0; + + if (this.updateThread != null) + { + window.clearTimeout(this.updateThread); + } + + if (state != null) + { + // Starts timer to update current state with no mouse events + this.updateThread = window.setTimeout(mxUtils.bind(this, function() + { + if (!this.isActive() && !this.graph.isMouseDown && + !this.graph.panningHandler.isActive()) + { + this.prev = state; + this.update(state, x, y); + } + }), this.updateDelay + 10); + } + } + else if (this.startTime != null) + { + timeOnTarget = new Date().getTime() - this.startTime; + } + + this.setDisplay(''); + + if (this.currentState != null && this.currentState != state && timeOnTarget < this.activationDelay && + this.bbox != null && !mxUtils.contains(this.bbox, x, y)) + { + this.reset(false); + } + else if (this.currentState != null || timeOnTarget > this.activationDelay) + { + if (this.currentState != state && ((timeOnTarget > this.updateDelay && state != null) || + this.bbox == null || x == null || y == null || !mxUtils.contains(this.bbox, x, y))) + { + if (state != null && this.graph.isEnabled()) + { + this.removeNodes(); + this.setCurrentState(state); + this.repaint(); + + // Resets connection points on other focused cells + if (this.graph.connectionHandler.constraintHandler.currentFocus != state) + { + this.graph.connectionHandler.constraintHandler.reset(); + } + } + else + { + this.reset(); + } + } + } + } +}; + +/** + * + */ +HoverIcons.prototype.setCurrentState = function(state) +{ + if (state.style['portConstraint'] != 'eastwest') + { + this.graph.container.appendChild(this.arrowUp); + this.graph.container.appendChild(this.arrowDown); + } + + this.graph.container.appendChild(this.arrowRight); + this.graph.container.appendChild(this.arrowLeft); + this.currentState = state; +}; + +(function() +{ + + /** + * Reset the list of processed edges. + */ + var mxGraphViewResetValidationState = mxGraphView.prototype.resetValidationState; + + mxGraphView.prototype.resetValidationState = function() + { + mxGraphViewResetValidationState.apply(this, arguments); + + this.validEdges = []; + }; + + /** + * Updates jumps for valid edges and repaints if needed. + */ + var mxGraphViewValidateCellState = mxGraphView.prototype.validateCellState; + + mxGraphView.prototype.validateCellState = function(cell, recurse) + { + var state = this.getState(cell); + + // Forces repaint if jumps change on a valid edge + if (state != null && this.graph.model.isEdge(state.cell) && + state.style != null && state.style[mxConstants.STYLE_CURVED] != 1 && + !state.invalid && this.updateLineJumps(state)) + { + this.graph.cellRenderer.redraw(state, false, this.isRendering()); + } + + state = mxGraphViewValidateCellState.apply(this, arguments); + + // Adds to the list of edges that may intersect with later edges + if (state != null && this.graph.model.isEdge(state.cell) && + state.style[mxConstants.STYLE_CURVED] != 1) + { + // LATER: Reuse jumps for valid edges + this.validEdges.push(state); + } + + return state; + }; + + /** + * Forces repaint if routed points have changed. + */ + var mxCellRendererIsShapeInvalid = mxCellRenderer.prototype.isShapeInvalid; + + mxCellRenderer.prototype.isShapeInvalid = function(state, shape) + { + return mxCellRendererIsShapeInvalid.apply(this, arguments) || + (state.routedPoints != null && shape.routedPoints != null && + !mxUtils.equalPoints(shape.routedPoints, state.routedPoints)) + }; + + + /** + * Updates jumps for invalid edges. + */ + var mxGraphViewUpdateCellState = mxGraphView.prototype.updateCellState; + + mxGraphView.prototype.updateCellState = function(state) + { + mxGraphViewUpdateCellState.apply(this, arguments); + + // Updates jumps on invalid edge before repaint + if (this.graph.model.isEdge(state.cell) && + state.style[mxConstants.STYLE_CURVED] != 1) + { + this.updateLineJumps(state); + } + }; + + /** + * Updates the jumps between given state and processed edges. + */ + mxGraphView.prototype.updateLineJumps = function(state) + { + var pts = state.absolutePoints; + + if (Graph.lineJumpsEnabled) + { + var changed = state.routedPoints != null; + var actual = null; + + if (pts != null && this.validEdges != null && + mxUtils.getValue(state.style, 'jumpStyle', 'none') !== 'none') + { + var thresh = 0.5 * this.scale; + changed = false; + actual = []; + + // Type 0 means normal waypoint, 1 means jump + function addPoint(type, x, y) + { + var rpt = new mxPoint(x, y); + rpt.type = type; + + actual.push(rpt); + var curr = (state.routedPoints != null) ? state.routedPoints[actual.length - 1] : null; + + return curr == null || curr.type != type || curr.x != x || curr.y != y; + }; + + for (var i = 0; i < pts.length - 1; i++) + { + var p1 = pts[i + 1]; + var p0 = pts[i]; + var list = []; + + // Ignores waypoints on straight segments + var pn = pts[i + 2]; + + while (i < pts.length - 2 && + mxUtils.ptSegDistSq(p0.x, p0.y, pn.x, pn.y, + p1.x, p1.y) < 1 * this.scale * this.scale) + { + p1 = pn; + i++; + pn = pts[i + 2]; + } + + changed = addPoint(0, p0.x, p0.y) || changed; + + // Processes all previous edges + for (var e = 0; e < this.validEdges.length; e++) + { + var state2 = this.validEdges[e]; + var pts2 = state2.absolutePoints; + + if (pts2 != null && mxUtils.intersects(state, state2) && state2.style['noJump'] != '1') + { + // Compares each segment of the edge with the current segment + for (var j = 0; j < pts2.length - 1; j++) + { + var p3 = pts2[j + 1]; + var p2 = pts2[j]; + + // Ignores waypoints on straight segments + pn = pts2[j + 2]; + + while (j < pts2.length - 2 && + mxUtils.ptSegDistSq(p2.x, p2.y, pn.x, pn.y, + p3.x, p3.y) < 1 * this.scale * this.scale) + { + p3 = pn; + j++; + pn = pts2[j + 2]; + } + + var pt = mxUtils.intersection(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + + // Handles intersection between two segments + if (pt != null && (Math.abs(pt.x - p2.x) > thresh || + Math.abs(pt.y - p2.y) > thresh) && + (Math.abs(pt.x - p3.x) > thresh || + Math.abs(pt.y - p3.y) > thresh)) + { + var dx = pt.x - p0.x; + var dy = pt.y - p0.y; + var temp = {distSq: dx * dx + dy * dy, x: pt.x, y: pt.y}; + + // Intersections must be ordered by distance from start of segment + for (var t = 0; t < list.length; t++) + { + if (list[t].distSq > temp.distSq) + { + list.splice(t, 0, temp); + temp = null; + + break; + } + } + + // Ignores multiple intersections at segment joint + if (temp != null && (list.length == 0 || + list[list.length - 1].x !== temp.x || + list[list.length - 1].y !== temp.y)) + { + list.push(temp); + } + } + } + } + } + + // Adds ordered intersections to routed points + for (var j = 0; j < list.length; j++) + { + changed = addPoint(1, list[j].x, list[j].y) || changed; + } + } + + var pt = pts[pts.length - 1]; + changed = addPoint(0, pt.x, pt.y) || changed; + } + + state.routedPoints = actual; + + return changed; + } + else + { + return false; + } + }; + + /** + * Overrides painting the actual shape for taking into account jump style. + */ + var mxConnectorPaintLine = mxConnector.prototype.paintLine; + + mxConnector.prototype.paintLine = function (c, absPts, rounded) + { + // Required for checking dirty state + this.routedPoints = (this.state != null) ? this.state.routedPoints : null; + + if (this.outline || this.state == null || this.style == null || + this.state.routedPoints == null || this.state.routedPoints.length == 0) + { + mxConnectorPaintLine.apply(this, arguments); + } + else + { + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, + mxConstants.LINE_ARCSIZE) / 2; + var size = (parseInt(mxUtils.getValue(this.style, 'jumpSize', + Graph.defaultJumpSize)) - 2) / 2 + this.strokewidth; + var style = mxUtils.getValue(this.style, 'jumpStyle', 'none'); + var f = Editor.jumpSizeRatio; + var moveTo = true; + var last = null; + var len = null; + var pts = []; + var n = null; + c.begin(); + + for (var i = 0; i < this.state.routedPoints.length; i++) + { + var rpt = this.state.routedPoints[i]; + var pt = new mxPoint(rpt.x / this.scale, rpt.y / this.scale); + + // Takes first and last point from passed-in array + if (i == 0) + { + pt = absPts[0]; + } + else if (i == this.state.routedPoints.length - 1) + { + pt = absPts[absPts.length - 1]; + } + + var done = false; + + // Type 1 is an intersection + if (last != null && rpt.type == 1) + { + // Checks if next/previous points are too close + var next = this.state.routedPoints[i + 1]; + var dx = next.x / this.scale - pt.x; + var dy = next.y / this.scale - pt.y; + var dist = dx * dx + dy * dy; + + if (n == null) + { + n = new mxPoint(pt.x - last.x, pt.y - last.y); + len = Math.sqrt(n.x * n.x + n.y * n.y); + n.x = n.x * size / len; + n.y = n.y * size / len; + } + + if (dist > size * size && len > 0) + { + var dx = last.x - pt.x; + var dy = last.y - pt.y; + var dist = dx * dx + dy * dy; + + if (dist > size * size) + { + var p0 = new mxPoint(pt.x - n.x, pt.y - n.y); + var p1 = new mxPoint(pt.x + n.x, pt.y + n.y); + pts.push(p0); + + this.addPoints(c, pts, rounded, arcSize, false, null, moveTo); + + var f = (Math.round(n.x) < 0 || (Math.round(n.x) == 0 + && Math.round(n.y) <= 0)) ? 1 : -1; + moveTo = false; + + if (style == 'sharp') + { + c.lineTo(p0.x - n.y * f, p0.y + n.x * f); + c.lineTo(p1.x - n.y * f, p1.y + n.x * f); + c.lineTo(p1.x, p1.y); + } + else if (style == 'arc') + { + f *= 1.3; + c.curveTo(p0.x - n.y * f, p0.y + n.x * f, + p1.x - n.y * f, p1.y + n.x * f, + p1.x, p1.y); + } + else + { + c.moveTo(p1.x, p1.y); + moveTo = true; + } + + pts = [p1]; + done = true; + } + } + } + else + { + n = null; + } + + if (!done) + { + pts.push(pt); + last = pt; + } + } + + this.addPoints(c, pts, rounded, arcSize, false, null, moveTo); + c.stroke(); + } + }; + + /** + * Adds support for snapToPoint style. + */ + var mxGraphViewUpdateFloatingTerminalPoint = mxGraphView.prototype.updateFloatingTerminalPoint; + + mxGraphView.prototype.updateFloatingTerminalPoint = function(edge, start, end, source) + { + if (start != null && edge != null && + (start.style['snapToPoint'] == '1' || + edge.style['snapToPoint'] == '1')) + { + start = this.getTerminalPort(edge, start, source); + var next = this.getNextPoint(edge, end, source); + + var orth = this.graph.isOrthogonal(edge); + var alpha = mxUtils.toRadians(Number(start.style[mxConstants.STYLE_ROTATION] || '0')); + var center = new mxPoint(start.getCenterX(), start.getCenterY()); + + if (alpha != 0) + { + var cos = Math.cos(-alpha); + var sin = Math.sin(-alpha); + next = mxUtils.getRotatedPoint(next, cos, sin, center); + } + + var border = parseFloat(edge.style[mxConstants.STYLE_PERIMETER_SPACING] || 0); + border += parseFloat(edge.style[(source) ? + mxConstants.STYLE_SOURCE_PERIMETER_SPACING : + mxConstants.STYLE_TARGET_PERIMETER_SPACING] || 0); + var pt = this.getPerimeterPoint(start, next, alpha == 0 && orth, border); + + if (alpha != 0) + { + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + pt = mxUtils.getRotatedPoint(pt, cos, sin, center); + } + + edge.setAbsoluteTerminalPoint(this.snapToAnchorPoint(edge, start, end, source, pt), source); + } + else + { + mxGraphViewUpdateFloatingTerminalPoint.apply(this, arguments); + } + }; + + mxGraphView.prototype.snapToAnchorPoint = function(edge, start, end, source, pt) + { + if (start != null && edge != null) + { + var constraints = this.graph.getAllConnectionConstraints(start) + var nearest = null; + var dist = null; + + if (constraints != null) + { + for (var i = 0; i < constraints.length; i++) + { + var cp = this.graph.getConnectionPoint(start, constraints[i]); + + if (cp != null) + { + var tmp = (cp.x - pt.x) * (cp.x - pt.x) + (cp.y - pt.y) * (cp.y - pt.y); + + if (dist == null || tmp < dist) + { + nearest = cp; + dist = tmp; + } + } + } + } + + if (nearest != null) + { + pt = nearest; + } + } + + return pt; + }; + + /** + * Adds support for placeholders in text elements of shapes. + */ + var mxStencilEvaluateTextAttribute = mxStencil.prototype.evaluateTextAttribute; + + mxStencil.prototype.evaluateTextAttribute = function(node, attribute, shape) + { + var result = mxStencilEvaluateTextAttribute.apply(this, arguments); + var placeholders = node.getAttribute('placeholders'); + + if (placeholders == '1' && shape.state != null) + { + result = shape.state.view.graph.replacePlaceholders(shape.state.cell, result); + } + + return result; + }; + + /** + * Adds custom stencils defined via shape=stencil(value) style. The value is a base64 encoded, compressed and + * URL encoded XML definition of the shape according to the stencil definition language of mxGraph. + * + * Needs to be in this file to make sure its part of the embed client code. Also the check for ZLib is + * different than for the Editor code. + */ + var mxCellRendererCreateShape = mxCellRenderer.prototype.createShape; + mxCellRenderer.prototype.createShape = function(state) + { + if (state.style != null && typeof(pako) !== 'undefined') + { + var shape = mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null); + + // Extracts and decodes stencil XML if shape has the form shape=stencil(value) + if (shape != null && shape.substring(0, 8) == 'stencil(') + { + try + { + var stencil = shape.substring(8, shape.length - 1); + var doc = mxUtils.parseXml(state.view.graph.decompress(stencil)); + + return new mxShape(new mxStencil(doc.documentElement)); + } + catch (e) + { + if (window.console != null) + { + console.log('Error in shape: ' + e); + } + } + } + } + + return mxCellRendererCreateShape.apply(this, arguments); + }; +})(); + +/** + * Overrides stencil registry for dynamic loading of stencils. + */ +/** + * Maps from library names to an array of Javascript filenames, + * which are synchronously loaded. Currently only stencil files + * (.xml) and JS files (.js) are supported. + * IMPORTANT: For embedded diagrams to work entries must also + * be added in EmbedServlet.java. + */ +mxStencilRegistry.libraries = {}; + +/** + * Global switch to disable dynamic loading. + */ +mxStencilRegistry.dynamicLoading = true; + +/** + * Global switch to disable eval for JS (preload all JS instead). + */ +mxStencilRegistry.allowEval = true; + +/** + * Stores all package names that have been dynamically loaded. + * Each package is only loaded once. + */ +mxStencilRegistry.packages = []; + +// Extends the default stencil registry to add dynamic loading +mxStencilRegistry.getStencil = function(name) +{ + var result = mxStencilRegistry.stencils[name]; + + if (result == null && mxCellRenderer.defaultShapes[name] == null && mxStencilRegistry.dynamicLoading) + { + var basename = mxStencilRegistry.getBasenameForStencil(name); + + // Loads stencil files and tries again + if (basename != null) + { + var libs = mxStencilRegistry.libraries[basename]; + + if (libs != null) + { + if (mxStencilRegistry.packages[basename] == null) + { + for (var i = 0; i < libs.length; i++) + { + var fname = libs[i]; + + if (fname.toLowerCase().substring(fname.length - 4, fname.length) == '.xml') + { + mxStencilRegistry.loadStencilSet(fname, null); + } + else if (fname.toLowerCase().substring(fname.length - 3, fname.length) == '.js') + { + try + { + if (mxStencilRegistry.allowEval) + { + var req = mxUtils.load(fname); + + if (req != null && req.getStatus() >= 200 && req.getStatus() <= 299) + { + eval.call(window, req.getText()); + } + } + } + catch (e) + { + if (window.console != null) + { + console.log('error in getStencil:', fname, e); + } + } + } + else + { + // FIXME: This does not yet work as the loading is triggered after + // the shape was used in the graph, at which point the keys have + // typically been translated in the calling method. + //mxResources.add(fname); + } + } + + mxStencilRegistry.packages[basename] = 1; + } + } + else + { + // Replaces '_-_' with '_' + basename = basename.replace('_-_', '_'); + mxStencilRegistry.loadStencilSet(STENCIL_PATH + '/' + basename + '.xml', null); + } + + result = mxStencilRegistry.stencils[name]; + } + } + + return result; +}; + +// Returns the basename for the given stencil or null if no file must be +// loaded to render the given stencil. +mxStencilRegistry.getBasenameForStencil = function(name) +{ + var tmp = null; + + if (name != null) + { + var parts = name.split('.'); + + if (parts.length > 0 && parts[0] == 'mxgraph') + { + tmp = parts[1]; + + for (var i = 2; i < parts.length - 1; i++) + { + tmp += '/' + parts[i]; + } + } + } + + return tmp; +}; + +// Loads the given stencil set +mxStencilRegistry.loadStencilSet = function(stencilFile, postStencilLoad, force, async) +{ + force = (force != null) ? force : false; + + // Uses additional cache for detecting previous load attempts + var xmlDoc = mxStencilRegistry.packages[stencilFile]; + + if (force || xmlDoc == null) + { + var install = false; + + if (xmlDoc == null) + { + try + { + if (async) + { + mxStencilRegistry.loadStencil(stencilFile, mxUtils.bind(this, function(xmlDoc2) + { + if (xmlDoc2 != null && xmlDoc2.documentElement != null) + { + mxStencilRegistry.packages[stencilFile] = xmlDoc2; + install = true; + mxStencilRegistry.parseStencilSet(xmlDoc2.documentElement, postStencilLoad, install); + } + })); + + return; + } + else + { + xmlDoc = mxStencilRegistry.loadStencil(stencilFile); + mxStencilRegistry.packages[stencilFile] = xmlDoc; + install = true; + } + } + catch (e) + { + if (window.console != null) + { + console.log('error in loadStencilSet:', stencilFile, e); + } + } + } + + if (xmlDoc != null && xmlDoc.documentElement != null) + { + mxStencilRegistry.parseStencilSet(xmlDoc.documentElement, postStencilLoad, install); + } + } +}; + +// Loads the given stencil XML file. +mxStencilRegistry.loadStencil = function(filename, fn) +{ + if (fn != null) + { + var req = mxUtils.get(filename, mxUtils.bind(this, function(req) + { + fn((req.getStatus() >= 200 && req.getStatus() <= 299) ? req.getXml() : null); + })); + } + else + { + return mxUtils.load(filename).getXml(); + } +}; + +// Takes array of strings +mxStencilRegistry.parseStencilSets = function(stencils) +{ + for (var i = 0; i < stencils.length; i++) + { + mxStencilRegistry.parseStencilSet(mxUtils.parseXml(stencils[i]).documentElement); + } +}; + +// Parses the given stencil set +mxStencilRegistry.parseStencilSet = function(root, postStencilLoad, install) +{ + if (root.nodeName == 'stencils') + { + var shapes = root.firstChild; + + while (shapes != null) + { + if (shapes.nodeName == 'shapes') + { + mxStencilRegistry.parseStencilSet(shapes, postStencilLoad, install); + } + + shapes = shapes.nextSibling; + } + } + else + { + install = (install != null) ? install : true; + var shape = root.firstChild; + var packageName = ''; + var name = root.getAttribute('name'); + + if (name != null) + { + packageName = name + '.'; + } + + while (shape != null) + { + if (shape.nodeType == mxConstants.NODETYPE_ELEMENT) + { + name = shape.getAttribute('name'); + + if (name != null) + { + packageName = packageName.toLowerCase(); + var stencilName = name.replace(/ /g,"_"); + + if (install) + { + mxStencilRegistry.addStencil(packageName + stencilName.toLowerCase(), new mxStencil(shape)); + } + + if (postStencilLoad != null) + { + var w = shape.getAttribute('w'); + var h = shape.getAttribute('h'); + + w = (w == null) ? 80 : parseInt(w, 10); + h = (h == null) ? 80 : parseInt(h, 10); + + postStencilLoad(packageName, stencilName, name, w, h); + } + } + } + + shape = shape.nextSibling; + } + } +}; + +/** + * These overrides are only added if mxVertexHandler is defined (ie. not in embedded graph) + */ +if (typeof mxVertexHandler != 'undefined') +{ + (function() + { + // Sets colors for handles + mxConstants.HANDLE_FILLCOLOR = '#29b6f2'; + mxConstants.HANDLE_STROKECOLOR = '#0088cf'; + mxConstants.VERTEX_SELECTION_COLOR = '#00a8ff'; + mxConstants.OUTLINE_COLOR = '#00a8ff'; + mxConstants.OUTLINE_HANDLE_FILLCOLOR = '#99ccff'; + mxConstants.OUTLINE_HANDLE_STROKECOLOR = '#00a8ff'; + mxConstants.CONNECT_HANDLE_FILLCOLOR = '#cee7ff'; + mxConstants.EDGE_SELECTION_COLOR = '#00a8ff'; + mxConstants.DEFAULT_VALID_COLOR = '#00a8ff'; + mxConstants.LABEL_HANDLE_FILLCOLOR = '#cee7ff'; + mxConstants.GUIDE_COLOR = '#0088cf'; + mxConstants.HIGHLIGHT_OPACITY = 30; + mxConstants.HIGHLIGHT_SIZE = 5; + + // Enables snapping to off-grid terminals for edge waypoints + mxEdgeHandler.prototype.snapToTerminals = true; + + // Enables guides + mxGraphHandler.prototype.guidesEnabled = true; + + // Enables fading of rubberband + mxRubberband.prototype.fadeOut = true; + + // Alt-move disables guides + mxGuide.prototype.isEnabledForEvent = function(evt) + { + return !mxEvent.isAltDown(evt); + }; + + // Extends connection handler to enable ctrl+drag for cloning source cell + // since copyOnConnect is now disabled by default + var mxConnectionHandlerCreateTarget = mxConnectionHandler.prototype.isCreateTarget; + mxConnectionHandler.prototype.isCreateTarget = function(evt) + { + return mxEvent.isControlDown(evt) || mxConnectionHandlerCreateTarget.apply(this, arguments); + }; + + // Overrides highlight shape for connection points + mxConstraintHandler.prototype.createHighlightShape = function() + { + var hl = new mxEllipse(null, this.highlightColor, this.highlightColor, 0); + hl.opacity = mxConstants.HIGHLIGHT_OPACITY; + + return hl; + }; + + // Overrides edge preview to use current edge shape and default style + mxConnectionHandler.prototype.livePreview = true; + mxConnectionHandler.prototype.cursor = 'crosshair'; + + // Uses current edge style for connect preview + mxConnectionHandler.prototype.createEdgeState = function(me) + { + var style = this.graph.createCurrentEdgeStyle(); + var edge = this.graph.createEdge(null, null, null, null, null, style); + var state = new mxCellState(this.graph.view, edge, this.graph.getCellStyle(edge)); + + for (var key in this.graph.currentEdgeStyle) + { + state.style[key] = this.graph.currentEdgeStyle[key]; + } + + return state; + }; + + // Overrides dashed state with current edge style + var connectionHandlerCreateShape = mxConnectionHandler.prototype.createShape; + mxConnectionHandler.prototype.createShape = function() + { + var shape = connectionHandlerCreateShape.apply(this, arguments); + + shape.isDashed = this.graph.currentEdgeStyle[mxConstants.STYLE_DASHED] == '1'; + + return shape; + } + + // Overrides live preview to keep current style + mxConnectionHandler.prototype.updatePreview = function(valid) + { + // do not change color of preview + }; + + // Overrides connection handler to ignore edges instead of not allowing connections + var mxConnectionHandlerCreateMarker = mxConnectionHandler.prototype.createMarker; + mxConnectionHandler.prototype.createMarker = function() + { + var marker = mxConnectionHandlerCreateMarker.apply(this, arguments); + + var markerGetCell = marker.getCell; + marker.getCell = mxUtils.bind(this, function(me) + { + var result = markerGetCell.apply(this, arguments); + + this.error = null; + + return result; + }); + + return marker; + }; + + /** + * Function: isCellLocked + * + * Returns true if the given cell does not allow new connections to be created. + * This implementation returns false. + */ + mxConnectionHandler.prototype.isCellEnabled = function(cell) + { + return !this.graph.isCellLocked(cell); + }; + + /** + * + */ + Graph.prototype.defaultVertexStyle = {}; + + /** + * Contains the default style for edges. + */ + Graph.prototype.defaultEdgeStyle = {'edgeStyle': 'orthogonalEdgeStyle', 'rounded': '0', + 'jettySize': 'auto', 'orthogonalLoop': '1'}; + + /** + * Returns the current edge style as a string. + */ + Graph.prototype.createCurrentEdgeStyle = function() + { + var style = 'edgeStyle=' + (this.currentEdgeStyle['edgeStyle'] || 'none') + ';'; + + if (this.currentEdgeStyle['shape'] != null) + { + style += 'shape=' + this.currentEdgeStyle['shape'] + ';'; + } + + if (this.currentEdgeStyle['curved'] != null) + { + style += 'curved=' + this.currentEdgeStyle['curved'] + ';'; + } + + if (this.currentEdgeStyle['rounded'] != null) + { + style += 'rounded=' + this.currentEdgeStyle['rounded'] + ';'; + } + + if (this.currentEdgeStyle['comic'] != null) + { + style += 'comic=' + this.currentEdgeStyle['comic'] + ';'; + } + + if (this.currentEdgeStyle['jumpStyle'] != null) + { + style += 'jumpStyle=' + this.currentEdgeStyle['jumpStyle'] + ';'; + } + + if (this.currentEdgeStyle['jumpSize'] != null) + { + style += 'jumpSize=' + this.currentEdgeStyle['jumpSize'] + ';'; + } + + // Special logic for custom property of elbowEdgeStyle + if (this.currentEdgeStyle['edgeStyle'] == 'elbowEdgeStyle' && this.currentEdgeStyle['elbow'] != null) + { + style += 'elbow=' + this.currentEdgeStyle['elbow'] + ';'; + } + + if (this.currentEdgeStyle['html'] != null) + { + style += 'html=' + this.currentEdgeStyle['html'] + ';'; + } + else + { + style += 'html=1;'; + } + + return style; + }; + + /** + * Hook for subclassers. + */ + Graph.prototype.getPagePadding = function() + { + return new mxPoint(0, 0); + }; + + /** + * Loads the stylesheet for this graph. + */ + Graph.prototype.loadStylesheet = function() + { + var node = (this.themes != null) ? this.themes[this.defaultThemeName] : + (!mxStyleRegistry.dynamicLoading) ? null : + mxUtils.load(STYLE_PATH + '/default.xml').getDocumentElement(); + + if (node != null) + { + var dec = new mxCodec(node.ownerDocument); + dec.decode(node, this.getStylesheet()); + } + }; + + /** + * + */ + Graph.prototype.importGraphModel = function(node, dx, dy, crop) + { + dx = (dx != null) ? dx : 0; + dy = (dy != null) ? dy : 0; + + var cells = [] + var model = new mxGraphModel(); + var codec = new mxCodec(node.ownerDocument); + codec.decode(node, model); + + var childCount = model.getChildCount(model.getRoot()); + var targetChildCount = this.model.getChildCount(this.model.getRoot()); + + // Merges into active layer if one layer is pasted + this.model.beginUpdate(); + try + { + // Mapping for multiple calls to cloneCells with the same set of cells + var mapping = new Object(); + + for (var i = 0; i < childCount; i++) + { + var parent = model.getChildAt(model.getRoot(), i); + + // Adds cells to existing layer if not locked + if (childCount == 1 && !this.isCellLocked(this.getDefaultParent())) + { + var children = model.getChildren(parent); + cells = cells.concat(this.importCells(children, dx, dy, this.getDefaultParent(), null, mapping)); + } + else + { + // Delta is non cascading, needs separate move for layers + parent = this.importCells([parent], 0, 0, this.model.getRoot(), null, mapping)[0]; + var children = this.model.getChildren(parent); + this.moveCells(children, dx, dy); + cells = cells.concat(children); + } + } + + if (crop) + { + if (this.isGridEnabled()) + { + dx = this.snap(dx); + dy = this.snap(dy); + } + + var bounds = this.getBoundingBoxFromGeometry(cells, true); + + if (bounds != null) + { + this.moveCells(cells, dx - bounds.x, dy - bounds.y); + } + } + } + finally + { + this.model.endUpdate(); + } + + return cells; + } + + /** + * Overrides method to provide connection constraints for shapes. + */ + Graph.prototype.getAllConnectionConstraints = function(terminal, source) + { + if (terminal != null) + { + var constraints = mxUtils.getValue(terminal.style, 'points', null); + + if (constraints != null) + { + // Requires an array of arrays with x, y (0..1) and an optional + // perimeter (0 or 1), eg. points=[[0,0,1],[0,1,0],[1,1]] + var result = []; + + try + { + var c = JSON.parse(constraints); + + for (var i = 0; i < c.length; i++) + { + var tmp = c[i]; + result.push(new mxConnectionConstraint(new mxPoint(tmp[0], tmp[1]), (tmp.length > 2) ? tmp[2] != '0' : true)); + } + } + catch (e) + { + // ignore + } + + return result; + } + else + { + if (terminal.shape != null) + { + if (terminal.shape.stencil != null) + { + if (terminal.shape.stencil != null) + { + return terminal.shape.stencil.constraints; + } + } + else if (terminal.shape.constraints != null) + { + return terminal.shape.constraints; + } + } + } + } + + return null; + }; + + /** + * Inverts the elbow edge style without removing existing styles. + */ + Graph.prototype.flipEdge = function(edge) + { + if (edge != null) + { + var state = this.view.getState(edge); + var style = (state != null) ? state.style : this.getCellStyle(edge); + + if (style != null) + { + var elbow = mxUtils.getValue(style, mxConstants.STYLE_ELBOW, + mxConstants.ELBOW_HORIZONTAL); + var value = (elbow == mxConstants.ELBOW_HORIZONTAL) ? + mxConstants.ELBOW_VERTICAL : mxConstants.ELBOW_HORIZONTAL; + this.setCellStyles(mxConstants.STYLE_ELBOW, value, [edge]); + } + } + }; + + /** + * Disables drill-down for non-swimlanes. + */ + Graph.prototype.isValidRoot = function(cell) + { + // Counts non-relative children + var childCount = this.model.getChildCount(cell); + var realChildCount = 0; + + for (var i = 0; i < childCount; i++) + { + var child = this.model.getChildAt(cell, i); + + if (this.model.isVertex(child)) + { + var geometry = this.getCellGeometry(child); + + if (geometry != null && !geometry.relative) + { + realChildCount++; + } + } + } + + return realChildCount > 0 || this.isContainer(cell); + }; + + /** + * Disables drill-down for non-swimlanes. + */ + Graph.prototype.isValidDropTarget = function(cell) + { + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return mxUtils.getValue(style, 'part', '0') != '1' && (this.isContainer(cell) || + (mxGraph.prototype.isValidDropTarget.apply(this, arguments) && + mxUtils.getValue(style, 'dropTarget', '1') != '0')); + }; + + /** + * Overrides createGroupCell to set the group style for new groups to 'group'. + */ + Graph.prototype.createGroupCell = function() + { + var group = mxGraph.prototype.createGroupCell.apply(this, arguments); + group.setStyle('group'); + + return group; + }; + + /** + * Disables extending parents with stack layouts on add + */ + Graph.prototype.isExtendParentsOnAdd = function(cell) + { + var result = mxGraph.prototype.isExtendParentsOnAdd.apply(this, arguments); + + if (result && cell != null && this.layoutManager != null) + { + var parent = this.model.getParent(cell); + + if (parent != null) + { + var layout = this.layoutManager.getLayout(parent); + + if (layout != null && layout.constructor == mxStackLayout) + { + result = false; + } + } + } + + return result; + }; + + /** + * Overrides autosize to add a border. + */ + Graph.prototype.getPreferredSizeForCell = function(cell) + { + var result = mxGraph.prototype.getPreferredSizeForCell.apply(this, arguments); + + // Adds buffer + if (result != null) + { + result.width += 10; + result.height += 4; + + if (this.gridEnabled) + { + result.width = this.snap(result.width); + result.height = this.snap(result.height); + } + } + + return result; + } + + /** + * Turns the given cells and returns the changed cells. + */ + Graph.prototype.turnShapes = function(cells) + { + var model = this.getModel(); + var select = []; + + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var cell = cells[i]; + + if (model.isEdge(cell)) + { + var src = model.getTerminal(cell, true); + var trg = model.getTerminal(cell, false); + + model.setTerminal(cell, trg, true); + model.setTerminal(cell, src, false); + + var geo = model.getGeometry(cell); + + if (geo != null) + { + geo = geo.clone(); + + if (geo.points != null) + { + geo.points.reverse(); + } + + var sp = geo.getTerminalPoint(true); + var tp = geo.getTerminalPoint(false) + + geo.setTerminalPoint(sp, false); + geo.setTerminalPoint(tp, true); + model.setGeometry(cell, geo); + + // Inverts constraints + var edgeState = this.view.getState(cell); + var sourceState = this.view.getState(src); + var targetState = this.view.getState(trg); + + if (edgeState != null) + { + var sc = (sourceState != null) ? this.getConnectionConstraint(edgeState, sourceState, true) : null; + var tc = (targetState != null) ? this.getConnectionConstraint(edgeState, targetState, false) : null; + + this.setConnectionConstraint(cell, src, true, tc); + this.setConnectionConstraint(cell, trg, false, sc); + } + + select.push(cell); + } + } + else if (model.isVertex(cell)) + { + var geo = this.getCellGeometry(cell); + + if (geo != null) + { + // Rotates the size and position in the geometry + geo = geo.clone(); + geo.x += geo.width / 2 - geo.height / 2; + geo.y += geo.height / 2 - geo.width / 2; + var tmp = geo.width; + geo.width = geo.height; + geo.height = tmp; + model.setGeometry(cell, geo); + + // Reads the current direction and advances by 90 degrees + var state = this.view.getState(cell); + + if (state != null) + { + var dir = state.style[mxConstants.STYLE_DIRECTION] || 'east'/*default*/; + + if (dir == 'east') + { + dir = 'south'; + } + else if (dir == 'south') + { + dir = 'west'; + } + else if (dir == 'west') + { + dir = 'north'; + } + else if (dir == 'north') + { + dir = 'east'; + } + + this.setCellStyles(mxConstants.STYLE_DIRECTION, dir, [cell]); + } + + select.push(cell); + } + } + } + } + finally + { + model.endUpdate(); + } + + return select; + }; + + /** + * Returns true if the given stencil contains any placeholder text. + */ + Graph.prototype.stencilHasPlaceholders = function(stencil) + { + if (stencil != null && stencil.fgNode != null) + { + var node = stencil.fgNode.firstChild; + + while (node != null) + { + if (node.nodeName == 'text' && node.getAttribute('placeholders') == '1') + { + return true; + } + + node = node.nextSibling; + } + } + + return false; + }; + + /** + * Updates the child cells with placeholders if metadata of a cell has changed. + */ + Graph.prototype.processChange = function(change) + { + mxGraph.prototype.processChange.apply(this, arguments); + + if (change instanceof mxValueChange && change.cell != null && + change.cell.value != null && typeof(change.cell.value) == 'object') + { + // Invalidates all descendants with placeholders + var desc = this.model.getDescendants(change.cell); + + // LATER: Check if only label or tooltip have changed + if (desc.length > 0) + { + for (var i = 0; i < desc.length; i++) + { + var state = this.view.getState(desc[i]); + + if (state != null && state.shape != null && state.shape.stencil != null && + this.stencilHasPlaceholders(state.shape.stencil)) + { + this.removeStateForCell(desc[i]); + } + else if (this.isReplacePlaceholders(desc[i])) + { + this.view.invalidate(desc[i], false, false); + } + } + } + } + }; + + /** + * Replaces the given element with a span. + */ + Graph.prototype.replaceElement = function(elt, tagName) + { + var span = elt.ownerDocument.createElement((tagName != null) ? tagName : 'span'); + var attributes = Array.prototype.slice.call(elt.attributes); + + while (attr = attributes.pop()) + { + span.setAttribute(attr.nodeName, attr.nodeValue); + } + + span.innerHTML = elt.innerHTML; + elt.parentNode.replaceChild(span, elt); + }; + + /** + * Handles label changes for XML user objects. + */ + Graph.prototype.updateLabelElements = function(cells, fn, tagName) + { + cells = (cells != null) ? cells : this.getSelectionCells(); + var div = document.createElement('div'); + + for (var i = 0; i < cells.length; i++) + { + // Changes font tags inside HTML labels + if (this.isHtmlLabel(cells[i])) + { + var label = this.convertValueToString(cells[i]); + + if (label != null && label.length > 0) + { + div.innerHTML = label; + var elts = div.getElementsByTagName((tagName != null) ? tagName : '*'); + + for (var j = 0; j < elts.length; j++) + { + fn(elts[j]); + } + + if (div.innerHTML != label) + { + this.cellLabelChanged(cells[i], div.innerHTML); + } + } + } + } + }; + + /** + * Handles label changes for XML user objects. + */ + Graph.prototype.cellLabelChanged = function(cell, value, autoSize) + { + // Removes all illegal control characters in user input + value = this.zapGremlins(value); + + this.model.beginUpdate(); + try + { + if (cell.value != null && typeof cell.value == 'object') + { + if (this.isReplacePlaceholders(cell) && + cell.getAttribute('placeholder') != null) + { + // LATER: Handle delete, name change + var name = cell.getAttribute('placeholder'); + var current = cell; + + while (current != null) + { + if (current == this.model.getRoot() || (current.value != null && + typeof(current.value) == 'object' && current.hasAttribute(name))) + { + this.setAttributeForCell(current, name, value); + + break; + } + + current = this.model.getParent(current); + } + } + + var tmp = cell.value.cloneNode(true); + tmp.setAttribute('label', value); + value = tmp; + } + + mxGraph.prototype.cellLabelChanged.apply(this, arguments); + } + finally + { + this.model.endUpdate(); + } + }; + + /** + * Removes transparent empty groups if all children are removed. + */ + Graph.prototype.cellsRemoved = function(cells) + { + if (cells != null) + { + var dict = new mxDictionary(); + + for (var i = 0; i < cells.length; i++) + { + dict.put(cells[i], true); + } + + // LATER: Recurse up the cell hierarchy + var parents = []; + + for (var i = 0; i < cells.length; i++) + { + var parent = this.model.getParent(cells[i]); + + if (parent != null && !dict.get(parent)) + { + dict.put(parent, true); + parents.push(parent); + } + } + + for (var i = 0; i < parents.length; i++) + { + var state = this.view.getState(parents[i]); + + if (state != null && (this.model.isEdge(state.cell) || this.model.isVertex(state.cell)) && this.isCellDeletable(state.cell)) + { + var stroke = mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE); + var fill = mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, mxConstants.NONE); + + if (stroke == mxConstants.NONE && fill == mxConstants.NONE) + { + var allChildren = true; + + for (var j = 0; j < this.model.getChildCount(state.cell) && allChildren; j++) + { + if (!dict.get(this.model.getChildAt(state.cell, j))) + { + allChildren = false; + } + } + + if (allChildren) + { + cells.push(state.cell); + } + } + } + } + } + + mxGraph.prototype.cellsRemoved.apply(this, arguments); + }; + + /** + * Overrides ungroup to check if group should be removed. + */ + Graph.prototype.removeCellsAfterUngroup = function(cells) + { + var cellsToRemove = []; + + for (var i = 0; i < cells.length; i++) + { + if (this.isCellDeletable(cells[i])) + { + var state = this.view.getState(cells[i]); + + if (state != null) + { + var stroke = mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE); + var fill = mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, mxConstants.NONE); + + if (stroke == mxConstants.NONE && fill == mxConstants.NONE) + { + cellsToRemove.push(cells[i]); + } + } + } + } + + cells = cellsToRemove; + + mxGraph.prototype.removeCellsAfterUngroup.apply(this, arguments); + }; + + /** + * Sets the link for the given cell. + */ + Graph.prototype.setLinkForCell = function(cell, link) + { + this.setAttributeForCell(cell, 'link', link); + }; + + /** + * Sets the link for the given cell. + */ + Graph.prototype.setTooltipForCell = function(cell, link) + { + this.setAttributeForCell(cell, 'tooltip', link); + }; + + /** + * Sets the link for the given cell. + */ + Graph.prototype.setAttributeForCell = function(cell, attributeName, attributeValue) + { + var value = null; + + if (cell.value != null && typeof(cell.value) == 'object') + { + value = cell.value.cloneNode(true); + } + else + { + var doc = mxUtils.createXmlDocument(); + + value = doc.createElement('UserObject'); + value.setAttribute('label', cell.value || ''); + } + + if (attributeValue != null && attributeValue.length > 0) + { + value.setAttribute(attributeName, attributeValue); + } + else + { + value.removeAttribute(attributeName); + } + + this.model.setValue(cell, value); + }; + + /** + * Overridden to stop moving edge labels between cells. + */ + Graph.prototype.getDropTarget = function(cells, evt, cell, clone) + { + var model = this.getModel(); + + // Disables drop into group if alt is pressed + if (mxEvent.isAltDown(evt)) + { + return null; + } + + // Disables dragging edge labels out of edges + for (var i = 0; i < cells.length; i++) + { + if (this.model.isEdge(this.model.getParent(cells[i]))) + { + return null; + } + } + + return mxGraph.prototype.getDropTarget.apply(this, arguments); + }; + + /** + * Overrides double click handling to avoid accidental inserts of new labels in dblClick below. + */ + Graph.prototype.click = function(me) + { + mxGraph.prototype.click.call(this, me); + + // Stores state and source for checking in dblClick + this.firstClickState = me.getState(); + this.firstClickSource = me.getSource(); + }; + + /** + * Overrides double click handling to add the tolerance and inserting text. + */ + Graph.prototype.dblClick = function(evt, cell) + { + if (this.isEnabled()) + { + var pt = mxUtils.convertPoint(this.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + + // Automatically adds new child cells to edges on double click + if (evt != null && !this.model.isVertex(cell)) + { + var state = (this.model.isEdge(cell)) ? this.view.getState(cell) : null; + var src = mxEvent.getSource(evt); + + if (this.firstClickState == state && this.firstClickSource == src) + { + if (state == null || (state.text == null || state.text.node == null || + (!mxUtils.contains(state.text.boundingBox, pt.x, pt.y) && + !mxUtils.isAncestorNode(state.text.node, mxEvent.getSource(evt))))) + { + if ((state == null && !this.isCellLocked(this.getDefaultParent())) || + (state != null && !this.isCellLocked(state.cell))) + { + // Avoids accidental inserts on background + if (state != null || (mxClient.IS_VML && src == this.view.getCanvas()) || + (mxClient.IS_SVG && src == this.view.getCanvas().ownerSVGElement)) + { + cell = this.addText(pt.x, pt.y, state); + } + } + } + } + } + + mxGraph.prototype.dblClick.call(this, evt, cell); + } + }; + + /** + * Returns a point that specifies the location for inserting cells. + */ + Graph.prototype.getInsertPoint = function() + { + var gs = this.getGridSize(); + var dx = this.container.scrollLeft / this.view.scale - this.view.translate.x; + var dy = this.container.scrollTop / this.view.scale - this.view.translate.y; + + if (this.pageVisible) + { + var layout = this.getPageLayout(); + var page = this.getPageSize(); + dx = Math.max(dx, layout.x * page.width); + dy = Math.max(dy, layout.y * page.height); + } + + return new mxPoint(this.snap(dx + gs), this.snap(dy + gs)); + }; + + /** + * + */ + Graph.prototype.getFreeInsertPoint = function() + { + var view = this.view; + var bds = this.getGraphBounds(); + var pt = this.getInsertPoint(); + + // Places at same x-coord and 2 grid sizes below existing graph + var x = this.snap(Math.round(Math.max(pt.x, bds.x / view.scale - view.translate.x + + ((bds.width == 0) ? 2 * this.gridSize : 0)))); + var y = this.snap(Math.round(Math.max(pt.y, (bds.y + bds.height) / view.scale - view.translate.y + + 2 * this.gridSize))); + + return new mxPoint(x, y); + }; + + /** + * Hook for subclassers to return true if the current insert point was defined + * using a mouse hover event. + */ + Graph.prototype.isMouseInsertPoint = function() + { + return false; + }; + + /** + * Adds a new label at the given position and returns the new cell. State is + * an optional edge state to be used as the parent for the label. Vertices + * are not allowed currently as states. + */ + Graph.prototype.addText = function(x, y, state) + { + // Creates a new edge label with a predefined text + var label = new mxCell(); + label.value = 'Text'; + label.style = 'text;html=1;resizable=0;points=[];' + label.geometry = new mxGeometry(0, 0, 0, 0); + label.vertex = true; + + if (state != null) + { + label.style += 'align=center;verticalAlign=middle;labelBackgroundColor=#ffffff;' + label.geometry.relative = true; + label.connectable = false; + + // Resets the relative location stored inside the geometry + var pt2 = this.view.getRelativePoint(state, x, y); + label.geometry.x = Math.round(pt2.x * 10000) / 10000; + label.geometry.y = Math.round(pt2.y); + + // Resets the offset inside the geometry to find the offset from the resulting point + label.geometry.offset = new mxPoint(0, 0); + pt2 = this.view.getPoint(state, label.geometry); + + var scale = this.view.scale; + label.geometry.offset = new mxPoint(Math.round((x - pt2.x) / scale), Math.round((y - pt2.y) / scale)); + } + else + { + label.style += 'autosize=1;align=left;verticalAlign=top;spacingTop=-4;' + + var tr = this.view.translate; + label.geometry.width = 40; + label.geometry.height = 20; + label.geometry.x = Math.round(x / this.view.scale) - tr.x; + label.geometry.y = Math.round(y / this.view.scale) - tr.y; + } + + this.getModel().beginUpdate(); + try + { + this.addCells([label], (state != null) ? state.cell : null); + this.fireEvent(new mxEventObject('textInserted', 'cells', [label])); + // Updates size of text after possible change of style via event + this.autoSizeCell(label); + } + finally + { + this.getModel().endUpdate(); + } + + return label; + }; + + /** + * + */ + Graph.prototype.getAbsoluteUrl = function(url) + { + if (url != null && this.isRelativeUrl(url)) + { + if (url.charAt(0) == '#') + { + url = this.baseUrl + url; + } + else if (url.charAt(0) == '/') + { + url = this.domainUrl + url; + } + else + { + url = this.domainPathUrl + url; + } + } + + return url; + }; + + /** + * Adds a handler for clicking on shapes with links. This replaces all links in labels. + */ + Graph.prototype.addClickHandler = function(highlight, beforeClick, onClick) + { + // Replaces links in labels for consistent right-clicks + var checkLinks = mxUtils.bind(this, function() + { + var links = this.container.getElementsByTagName('a'); + + if (links != null) + { + for (var i = 0; i < links.length; i++) + { + var href = this.getAbsoluteUrl(links[i].getAttribute('href')); + + if (href != null) + { + links[i].setAttribute('rel', 'nofollow noopener noreferrer'); + links[i].setAttribute('href', href); + + if (beforeClick != null) + { + mxEvent.addGestureListeners(links[i], null, null, beforeClick); + } + } + } + } + }); + + this.model.addListener(mxEvent.CHANGE, checkLinks); + checkLinks(); + + var cursor = this.container.style.cursor; + var tol = this.getTolerance(); + var graph = this; + + var mouseListener = + { + currentState: null, + currentLink: null, + highlight: (highlight != null && highlight != '' && highlight != mxConstants.NONE) ? + new mxCellHighlight(graph, highlight, 4) : null, + startX: 0, + startY: 0, + scrollLeft: 0, + scrollTop: 0, + updateCurrentState: function(me) + { + var tmp = me.sourceState; + + // Gets topmost intersecting cell with link + if (tmp == null || graph.getLinkForCell(tmp.cell) == null) + { + var cell = graph.getCellAt(me.getGraphX(), me.getGraphY(), null, null, null, function(state, x, y) + { + return graph.getLinkForCell(state.cell) == null; + }); + + tmp = graph.view.getState(cell); + } + + if (tmp != this.currentState) + { + if (this.currentState != null) + { + this.clear(); + } + + this.currentState = tmp; + + if (this.currentState != null) + { + this.activate(this.currentState); + } + } + }, + mouseDown: function(sender, me) + { + this.startX = me.getGraphX(); + this.startY = me.getGraphY(); + this.scrollLeft = graph.container.scrollLeft; + this.scrollTop = graph.container.scrollTop; + + if (this.currentLink == null && graph.container.style.overflow == 'auto') + { + graph.container.style.cursor = 'move'; + } + + this.updateCurrentState(me); + }, + mouseMove: function(sender, me) + { + if (graph.isMouseDown) + { + if (this.currentLink != null) + { + var dx = Math.abs(this.startX - me.getGraphX()); + var dy = Math.abs(this.startY - me.getGraphY()); + + if (dx > tol || dy > tol) + { + this.clear(); + } + } + } + else + { + // Checks for parent link + var linkNode = me.getSource(); + + while (linkNode != null && linkNode.nodeName.toLowerCase() != 'a') + { + linkNode = linkNode.parentNode; + } + + if (linkNode != null) + { + this.clear(); + } + else + { + if (graph.tooltipHandler != null && this.currentLink != null && this.currentState != null) + { + graph.tooltipHandler.reset(me, true, this.currentState); + } + + if (this.currentState != null && (me.getState() == this.currentState || me.sourceState == null) && + graph.intersects(this.currentState, me.getGraphX(), me.getGraphY())) + { + return; + } + + this.updateCurrentState(me); + } + } + }, + mouseUp: function(sender, me) + { + var source = me.getSource(); + var evt = me.getEvent(); + + // Checks for parent link + var linkNode = source; + + while (linkNode != null && linkNode.nodeName.toLowerCase() != 'a') + { + linkNode = linkNode.parentNode; + } + + // Ignores clicks on links and collapse/expand icon + if (linkNode == null && + (((Math.abs(this.scrollLeft - graph.container.scrollLeft) < tol && + Math.abs(this.scrollTop - graph.container.scrollTop) < tol) && + (me.sourceState == null || !me.isSource(me.sourceState.control))) && + (((mxEvent.isLeftMouseButton(evt) || mxEvent.isMiddleMouseButton(evt)) && + !mxEvent.isPopupTrigger(evt)) || mxEvent.isTouchEvent(evt)))) + { + if (this.currentLink != null) + { + var blank = graph.isBlankLink(this.currentLink); + + if ((this.currentLink.substring(0, 5) === 'data:' || + !blank) && beforeClick != null) + { + beforeClick(evt, this.currentLink); + } + + if (!mxEvent.isConsumed(evt)) + { + var target = (mxEvent.isMiddleMouseButton(evt)) ? '_blank' : + ((blank) ? graph.linkTarget : '_top'); + graph.openLink(this.currentLink, target); + me.consume(); + } + } + else if (onClick != null && !me.isConsumed() && + (Math.abs(this.scrollLeft - graph.container.scrollLeft) < tol && + Math.abs(this.scrollTop - graph.container.scrollTop) < tol) && + (Math.abs(this.startX - me.getGraphX()) < tol && + Math.abs(this.startY - me.getGraphY()) < tol)) + { + onClick(me.getEvent()); + } + } + + this.clear(); + }, + activate: function(state) + { + this.currentLink = graph.getAbsoluteUrl(graph.getLinkForCell(state.cell)); + + if (this.currentLink != null) + { + graph.container.style.cursor = 'pointer'; + + if (this.highlight != null) + { + this.highlight.highlight(state); + } + } + }, + clear: function() + { + if (graph.container != null) + { + graph.container.style.cursor = cursor; + } + + this.currentState = null; + this.currentLink = null; + + if (this.highlight != null) + { + this.highlight.hide(); + } + + if (graph.tooltipHandler != null) + { + graph.tooltipHandler.hide(); + } + } + }; + + // Ignores built-in click handling + graph.click = function(me) {}; + graph.addMouseListener(mouseListener); + + mxEvent.addListener(document, 'mouseleave', function(evt) + { + mouseListener.clear(); + }); + }; + + /** + * Duplicates the given cells and returns the duplicates. + */ + Graph.prototype.duplicateCells = function(cells, append) + { + cells = (cells != null) ? cells : this.getSelectionCells(); + append = (append != null) ? append : true; + + cells = this.model.getTopmostCells(cells); + + var model = this.getModel(); + var s = this.gridSize; + var select = []; + + model.beginUpdate(); + try + { + var clones = this.cloneCells(cells, false); + + for (var i = 0; i < cells.length; i++) + { + var parent = model.getParent(cells[i]); + var child = this.moveCells([clones[i]], s, s, false)[0]; + select.push(child); + + if (append) + { + model.add(parent, clones[i]); + } + else + { + // Maintains child index by inserting after clone in parent + var index = parent.getIndex(cells[i]); + model.add(parent, clones[i], index + 1); + } + } + } + finally + { + model.endUpdate(); + } + + return select; + }; + + /** + * Inserts the given image at the cursor in a content editable text box using + * the insertimage command on the document instance. + */ + Graph.prototype.insertImage = function(newValue, w, h) + { + // To find the new image, we create a list of all existing links first + if (newValue != null) + { + var tmp = this.cellEditor.textarea.getElementsByTagName('img'); + var oldImages = []; + + for (var i = 0; i < tmp.length; i++) + { + oldImages.push(tmp[i]); + } + + // LATER: Fix inserting link/image in IE8/quirks after focus lost + document.execCommand('insertimage', false, newValue); + + // Sets size of new image + var newImages = this.cellEditor.textarea.getElementsByTagName('img'); + + if (newImages.length == oldImages.length + 1) + { + // Inverse order in favor of appended images + for (var i = newImages.length - 1; i >= 0; i--) + { + if (i == 0 || newImages[i] != oldImages[i - 1]) + { + // Workaround for lost styles during undo and redo is using attributes + newImages[i].setAttribute('width', w); + newImages[i].setAttribute('height', h); + + break; + } + } + } + } + }; + + /** + * Inserts the given image at the cursor in a content editable text box using + * the insertimage command on the document instance. + */ + Graph.prototype.insertLink = function(value) + { + if (value.length == 0) + { + document.execCommand('unlink', false); + } + else if (mxClient.IS_FF) + { + // Workaround for Firefox that adds a new link and removes + // the href from the inner link if its parent is a span is + // to remove all inner links inside the new outer link + var tmp = this.cellEditor.textarea.getElementsByTagName('a'); + var oldLinks = []; + + for (var i = 0; i < tmp.length; i++) + { + oldLinks.push(tmp[i]); + } + + document.execCommand('createlink', false, mxUtils.trim(value)); + + // Finds the new link element + var newLinks = this.cellEditor.textarea.getElementsByTagName('a'); + + if (newLinks.length == oldLinks.length + 1) + { + // Inverse order in favor of appended links + for (var i = newLinks.length - 1; i >= 0; i--) + { + if (newLinks[i] != oldLinks[i - 1]) + { + // Removes all inner links from the new link and + // moves the children to the inner link parent + var tmp = newLinks[i].getElementsByTagName('a'); + + while (tmp.length > 0) + { + var parent = tmp[0].parentNode; + + while (tmp[0].firstChild != null) + { + parent.insertBefore(tmp[0].firstChild, tmp[0]); + } + + parent.removeChild(tmp[0]); + } + + break; + } + } + } + } + else + { + // LATER: Fix inserting link/image in IE8/quirks after focus lost + document.execCommand('createlink', false, mxUtils.trim(value)); + } + }; + + /** + * + * @param cell + * @returns {Boolean} + */ + Graph.prototype.isCellResizable = function(cell) + { + var result = mxGraph.prototype.isCellResizable.apply(this, arguments); + + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return result || (mxUtils.getValue(style, mxConstants.STYLE_RESIZABLE, '1') != '0' && + style[mxConstants.STYLE_WHITE_SPACE] == 'wrap'); + }; + + /** + * Function: distributeCells + * + * Distribuets the centers of the given cells equally along the available + * horizontal or vertical space. + * + * Parameters: + * + * horizontal - Boolean that specifies the direction of the distribution. + * cells - Optional array of to be distributed. Edges are ignored. + */ + Graph.prototype.distributeCells = function(horizontal, cells) + { + if (cells == null) + { + cells = this.getSelectionCells(); + } + + if (cells != null && cells.length > 1) + { + var vertices = []; + var max = null; + var min = null; + + for (var i = 0; i < cells.length; i++) + { + if (this.getModel().isVertex(cells[i])) + { + var state = this.view.getState(cells[i]); + + if (state != null) + { + var tmp = (horizontal) ? state.getCenterX() : state.getCenterY(); + max = (max != null) ? Math.max(max, tmp) : tmp; + min = (min != null) ? Math.min(min, tmp) : tmp; + + vertices.push(state); + } + } + } + + if (vertices.length > 2) + { + vertices.sort(function(a, b) + { + return (horizontal) ? a.x - b.x : a.y - b.y; + }); + + var t = this.view.translate; + var s = this.view.scale; + + min = min / s - ((horizontal) ? t.x : t.y); + max = max / s - ((horizontal) ? t.x : t.y); + + this.getModel().beginUpdate(); + try + { + var dt = (max - min) / (vertices.length - 1); + var t0 = min; + + for (var i = 1; i < vertices.length - 1; i++) + { + var pstate = this.view.getState(this.model.getParent(vertices[i].cell)); + var geo = this.getCellGeometry(vertices[i].cell); + t0 += dt; + + if (geo != null && pstate != null) + { + geo = geo.clone(); + + if (horizontal) + { + geo.x = Math.round(t0 - geo.width / 2) - pstate.origin.x; + } + else + { + geo.y = Math.round(t0 - geo.height / 2) - pstate.origin.y; + } + + this.getModel().setGeometry(vertices[i].cell, geo); + } + } + } + finally + { + this.getModel().endUpdate(); + } + } + } + + return cells; + }; + + /** + * Adds meta-drag an Mac. + * @param evt + * @returns + */ + Graph.prototype.isCloneEvent = function(evt) + { + return (mxClient.IS_MAC && mxEvent.isMetaDown(evt)) || mxEvent.isControlDown(evt); + }; + + /** + * Translates this point by the given vector. + * + * @param {number} dx X-coordinate of the translation. + * @param {number} dy Y-coordinate of the translation. + */ + Graph.prototype.encodeCells = function(cells) + { + var clones = this.cloneCells(cells); + + // Creates a dictionary for fast lookups + var dict = new mxDictionary(); + + for (var i = 0; i < cells.length; i++) + { + dict.put(cells[i], true); + } + + // Checks for orphaned relative children and makes absolute + for (var i = 0; i < clones.length; i++) + { + var state = this.view.getState(cells[i]); + + if (state != null) + { + var geo = this.getCellGeometry(clones[i]); + + if (geo != null && geo.relative && !this.model.isEdge(cells[i]) && + !dict.get(this.model.getParent(cells[i]))) + { + geo.relative = false; + geo.x = state.x / state.view.scale - state.view.translate.x; + geo.y = state.y / state.view.scale - state.view.translate.y; + } + } + } + + var codec = new mxCodec(); + var model = new mxGraphModel(); + var parent = model.getChildAt(model.getRoot(), 0); + + for (var i = 0; i < cells.length; i++) + { + model.add(parent, clones[i]); + } + + return codec.encode(model); + }; + + /** + * Translates this point by the given vector. + * + * @param {number} dx X-coordinate of the translation. + * @param {number} dy Y-coordinate of the translation. + */ + Graph.prototype.createSvgImageExport = function() + { + var exp = new mxImageExport(); + + // Adds hyperlinks (experimental) + exp.getLinkForCellState = mxUtils.bind(this, function(state, canvas) + { + return this.getLinkForCell(state.cell); + }); + + return exp; + }; + + /** + * Translates this point by the given vector. + * + * @param {number} dx X-coordinate of the translation. + * @param {number} dy Y-coordinate of the translation. + */ + Graph.prototype.getSvg = function(background, scale, border, nocrop, crisp, ignoreSelection, showText, imgExport) + { + //Disable Css Transforms if it is used + var origUseCssTrans = this.useCssTransforms; + + if (origUseCssTrans) + { + this.useCssTransforms = false; + this.view.revalidate(); + this.sizeDidChange(); + } + + try + { + scale = (scale != null) ? scale : 1; + border = (border != null) ? border : 0; + crisp = (crisp != null) ? crisp : true; + ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; + showText = (showText != null) ? showText : true; + + var bounds = (ignoreSelection || nocrop) ? + this.getGraphBounds() : this.getBoundingBox(this.getSelectionCells()); + + if (bounds == null) + { + throw Error(mxResources.get('drawingEmpty')); + } + + var vs = this.view.scale; + + // Prepares SVG document that holds the output + var svgDoc = mxUtils.createXmlDocument(); + var root = (svgDoc.createElementNS != null) ? + svgDoc.createElementNS(mxConstants.NS_SVG, 'svg') : svgDoc.createElement('svg'); + + if (background != null) + { + if (root.style != null) + { + root.style.backgroundColor = background; + } + else + { + root.setAttribute('style', 'background-color:' + background); + } + } + + if (svgDoc.createElementNS == null) + { + root.setAttribute('xmlns', mxConstants.NS_SVG); + root.setAttribute('xmlns:xlink', mxConstants.NS_XLINK); + } + else + { + // KNOWN: Ignored in IE9-11, adds namespace for each image element instead. No workaround. + root.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', mxConstants.NS_XLINK); + } + + var s = scale / vs; + root.setAttribute('width', Math.max(1, Math.ceil(bounds.width * s) + 2 * border) + 'px'); + root.setAttribute('height', Math.max(1, Math.ceil(bounds.height * s) + 2 * border) + 'px'); + root.setAttribute('version', '1.1'); + + // Adds group for anti-aliasing via transform + var node = root; + + if (crisp) + { + var group = (svgDoc.createElementNS != null) ? + svgDoc.createElementNS(mxConstants.NS_SVG, 'g') : svgDoc.createElement('g'); + group.setAttribute('transform', 'translate(0.5,0.5)'); + root.appendChild(group); + svgDoc.appendChild(root); + node = group; + } + else + { + svgDoc.appendChild(root); + } + + // Renders graph. Offset will be multiplied with state's scale when painting state. + // TextOffset only seems to affect FF output but used everywhere for consistency. + var svgCanvas = this.createSvgCanvas(node); + svgCanvas.foOffset = (crisp) ? -0.5 : 0; + svgCanvas.textOffset = (crisp) ? -0.5 : 0; + svgCanvas.imageOffset = (crisp) ? -0.5 : 0; + svgCanvas.translate(Math.floor((border / scale - bounds.x) / vs), Math.floor((border / scale - bounds.y) / vs)); + + // Convert HTML entities + var htmlConverter = document.createElement('textarea'); + + // Adds simple text fallback for viewers with no support for foreignObjects + var createAlternateContent = svgCanvas.createAlternateContent; + svgCanvas.createAlternateContent = function(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation) + { + var s = this.state; + + // Assumes a max character width of 0.2em + if (this.foAltText != null && (w == 0 || (s.fontSize != 0 && str.length < (w * 5) / s.fontSize))) + { + var alt = this.createElement('text'); + alt.setAttribute('x', Math.round(w / 2)); + alt.setAttribute('y', Math.round((h + s.fontSize) / 2)); + alt.setAttribute('fill', s.fontColor || 'black'); + alt.setAttribute('text-anchor', 'middle'); + alt.setAttribute('font-size', Math.round(s.fontSize) + 'px'); + alt.setAttribute('font-family', s.fontFamily); + + if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + alt.setAttribute('font-weight', 'bold'); + } + + if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + alt.setAttribute('font-style', 'italic'); + } + + if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) + { + alt.setAttribute('text-decoration', 'underline'); + } + + try + { + htmlConverter.innerHTML = str; + alt.textContent = htmlConverter.value; + + return alt; + } + catch (e) + { + return createAlternateContent.apply(this, arguments); + } + } + else + { + return createAlternateContent.apply(this, arguments); + } + }; + + // Paints background image + var bgImg = this.backgroundImage; + + if (bgImg != null) + { + var s2 = vs / scale; + var tr = this.view.translate; + var tmp = new mxRectangle(tr.x * s2, tr.y * s2, bgImg.width * s2, bgImg.height * s2); + + // Checks if visible + if (mxUtils.intersects(bounds, tmp)) + { + svgCanvas.image(tr.x, tr.y, bgImg.width, bgImg.height, bgImg.src, true); + } + } + + svgCanvas.scale(s); + svgCanvas.textEnabled = showText; + + imgExport = (imgExport != null) ? imgExport : this.createSvgImageExport(); + var imgExportDrawCellState = imgExport.drawCellState; + + // Implements ignoreSelection flag + imgExport.drawCellState = function(state, canvas) + { + var graph = state.view.graph; + var selected = graph.isCellSelected(state.cell); + var parent = graph.model.getParent(state.cell); + + // Checks if parent cell is selected + while (!ignoreSelection && !selected && parent != null) + { + selected = graph.isCellSelected(parent); + parent = graph.model.getParent(parent); + } + + if (ignoreSelection || selected) + { + imgExportDrawCellState.apply(this, arguments); + } + }; + + imgExport.drawState(this.getView().getState(this.model.root), svgCanvas); + + return root; + } + finally + { + if (origUseCssTrans) + { + this.useCssTransforms = true; + this.view.revalidate(); + this.sizeDidChange(); + } + } + }; + + /** + * Hook for creating the canvas used in getSvg. + */ + Graph.prototype.createSvgCanvas = function(node) + { + return new mxSvgCanvas2D(node); + }; + + /** + * Returns the first ancestor of the current selection with the given name. + */ + Graph.prototype.getSelectedElement = function() + { + var node = null; + + if (window.getSelection) + { + var sel = window.getSelection(); + + if (sel.getRangeAt && sel.rangeCount) + { + var range = sel.getRangeAt(0); + node = range.commonAncestorContainer; + } + } + else if (document.selection) + { + node = document.selection.createRange().parentElement(); + } + + return node; + }; + + /** + * Returns the first ancestor of the current selection with the given name. + */ + Graph.prototype.getParentByName = function(node, name, stopAt) + { + while (node != null) + { + if (node.nodeName == name) + { + return node; + } + + if (node == stopAt) + { + return null; + } + + node = node.parentNode; + } + + return node; + }; + + /** + * Selects the given node. + */ + Graph.prototype.selectNode = function(node) + { + var sel = null; + + // IE9 and non-IE + if (window.getSelection) + { + sel = window.getSelection(); + + if (sel.getRangeAt && sel.rangeCount) + { + var range = document.createRange(); + range.selectNode(node); + sel.removeAllRanges(); + sel.addRange(range); + } + } + // IE < 9 + else if ((sel = document.selection) && sel.type != 'Control') + { + var originalRange = sel.createRange(); + originalRange.collapse(true); + var range = sel.createRange(); + range.setEndPoint('StartToStart', originalRange); + range.select(); + } + }; + + /** + * Inserts a new row into the given table. + */ + Graph.prototype.insertRow = function(table, index) + { + var bd = table.tBodies[0]; + var cells = bd.rows[0].cells; + var cols = 0; + + // Counts columns including colspans + for (var i = 0; i < cells.length; i++) + { + var colspan = cells[i].getAttribute('colspan'); + cols += (colspan != null) ? parseInt(colspan) : 1; + } + + var row = bd.insertRow(index); + + for (var i = 0; i < cols; i++) + { + mxUtils.br(row.insertCell(-1)); + } + + return row.cells[0]; + }; + + /** + * Deletes the given column. + */ + Graph.prototype.deleteRow = function(table, index) + { + table.tBodies[0].deleteRow(index); + }; + + /** + * Deletes the given column. + */ + Graph.prototype.insertColumn = function(table, index) + { + var hd = table.tHead; + + if (hd != null) + { + // TODO: use colIndex + for (var h = 0; h < hd.rows.length; h++) + { + var th = document.createElement('th'); + hd.rows[h].appendChild(th); + mxUtils.br(th); + } + } + + var bd = table.tBodies[0]; + + for (var i = 0; i < bd.rows.length; i++) + { + var cell = bd.rows[i].insertCell(index); + mxUtils.br(cell); + } + + return bd.rows[0].cells[(index >= 0) ? index : bd.rows[0].cells.length - 1]; + }; + + /** + * Deletes the given column. + */ + Graph.prototype.deleteColumn = function(table, index) + { + if (index >= 0) + { + var bd = table.tBodies[0]; + var rows = bd.rows; + + for (var i = 0; i < rows.length; i++) + { + if (rows[i].cells.length > index) + { + rows[i].deleteCell(index); + } + } + } + }; + + /** + * Inserts the given HTML at the caret position (no undo). + */ + Graph.prototype.pasteHtmlAtCaret = function(html) + { + var sel, range; + + // IE9 and non-IE + if (window.getSelection) + { + sel = window.getSelection(); + + if (sel.getRangeAt && sel.rangeCount) + { + range = sel.getRangeAt(0); + range.deleteContents(); + + // Range.createContextualFragment() would be useful here but is + // only relatively recently standardized and is not supported in + // some browsers (IE9, for one) + var el = document.createElement("div"); + el.innerHTML = html; + var frag = document.createDocumentFragment(), node; + + while ((node = el.firstChild)) + { + lastNode = frag.appendChild(node); + } + + range.insertNode(frag); + } + } + // IE < 9 + else if ((sel = document.selection) && sel.type != "Control") + { + // FIXME: Does not work if selection is empty + sel.createRange().pasteHTML(html); + } + }; + + /** + * Creates an anchor elements for handling the given link in the + * hint that is shown when the cell is selected. + */ + Graph.prototype.createLinkForHint = function(link, label) + { + link = (link != null) ? link : 'javascript:void(0);'; + + if (label == null || label.length == 0) + { + if (this.isCustomLink(link)) + { + label = this.getLinkTitle(link); + } + else + { + label = link; + } + } + + // Helper function to shorten strings + function short(str, max) + { + if (str.length > max) + { + str = str.substring(0, Math.round(max / 2)) + '...' + + str.substring(str.length - Math.round(max / 4)); + } + + return str; + }; + + var a = document.createElement('a'); + a.setAttribute('rel', 'nofollow noopener noreferrer'); + a.setAttribute('href', this.getAbsoluteUrl(link)); + a.setAttribute('title', short((this.isCustomLink(link)) ? + this.getLinkTitle(link) : link, 80)); + + if (this.linkTarget != null) + { + a.setAttribute('target', this.linkTarget); + } + + // Adds shortened label to link + mxUtils.write(a, short(label, 40)); + + // Handles custom links + if (this.isCustomLink(link)) + { + mxEvent.addListener(a, 'click', mxUtils.bind(this, function(evt) + { + this.customLinkClicked(link); + mxEvent.consume(evt); + })); + } + + return a; + }; + + /** + * Customized graph for touch devices. + */ + Graph.prototype.initTouch = function() + { + // Disables new connections via "hotspot" + this.connectionHandler.marker.isEnabled = function() + { + return this.graph.connectionHandler.first != null; + }; + + // Hides menu when editing starts + this.addListener(mxEvent.START_EDITING, function(sender, evt) + { + this.popupMenuHandler.hideMenu(); + }); + + // Adds custom hit detection if native hit detection found no cell + var graphUpdateMouseEvent = this.updateMouseEvent; + this.updateMouseEvent = function(me) + { + me = graphUpdateMouseEvent.apply(this, arguments); + + if (mxEvent.isTouchEvent(me.getEvent()) && me.getState() == null) + { + var cell = this.getCellAt(me.graphX, me.graphY); + + if (cell != null && this.isSwimlane(cell) && this.hitsSwimlaneContent(cell, me.graphX, me.graphY)) + { + cell = null; + } + else + { + me.state = this.view.getState(cell); + + if (me.state != null && me.state.shape != null) + { + this.container.style.cursor = me.state.shape.node.style.cursor; + } + } + } + + if (me.getState() == null && this.isEnabled()) + { + this.container.style.cursor = 'default'; + } + + return me; + }; + + // Context menu trigger implementation depending on current selection state + // combined with support for normal popup trigger. + var cellSelected = false; + var selectionEmpty = false; + var menuShowing = false; + + var oldFireMouseEvent = this.fireMouseEvent; + + this.fireMouseEvent = function(evtName, me, sender) + { + if (evtName == mxEvent.MOUSE_DOWN) + { + // For hit detection on edges + me = this.updateMouseEvent(me); + + cellSelected = this.isCellSelected(me.getCell()); + selectionEmpty = this.isSelectionEmpty(); + menuShowing = this.popupMenuHandler.isMenuShowing(); + } + + oldFireMouseEvent.apply(this, arguments); + }; + + // Shows popup menu if cell was selected or selection was empty and background was clicked + // FIXME: Conflicts with mxPopupMenuHandler.prototype.getCellForPopupEvent in Editor.js by + // selecting parent for selected children in groups before this check can be made. + this.popupMenuHandler.mouseUp = mxUtils.bind(this, function(sender, me) + { + this.popupMenuHandler.popupTrigger = !this.isEditing() && this.isEnabled() && + (me.getState() == null || !me.isSource(me.getState().control)) && + (this.popupMenuHandler.popupTrigger || (!menuShowing && !mxEvent.isMouseEvent(me.getEvent()) && + ((selectionEmpty && me.getCell() == null && this.isSelectionEmpty()) || + (cellSelected && this.isCellSelected(me.getCell()))))); + mxPopupMenuHandler.prototype.mouseUp.apply(this.popupMenuHandler, arguments); + }); + }; + + /** + * HTML in-place editor + */ + mxCellEditor.prototype.isContentEditing = function() + { + var state = this.graph.view.getState(this.editingCell); + + return state != null && state.style['html'] == 1; + }; + + /** + * Creates the keyboard event handler for the current graph and history. + */ + mxCellEditor.prototype.saveSelection = function() + { + if (window.getSelection) + { + var sel = window.getSelection(); + + if (sel.getRangeAt && sel.rangeCount) + { + var ranges = []; + + for (var i = 0, len = sel.rangeCount; i < len; ++i) + { + ranges.push(sel.getRangeAt(i)); + } + + return ranges; + } + } + else if (document.selection && document.selection.createRange) + { + return document.selection.createRange(); + } + + return null; + }; + + /** + * Creates the keyboard event handler for the current graph and history. + */ + mxCellEditor.prototype.restoreSelection = function(savedSel) + { + try + { + if (savedSel) + { + if (window.getSelection) + { + sel = window.getSelection(); + sel.removeAllRanges(); + + for (var i = 0, len = savedSel.length; i < len; ++i) + { + sel.addRange(savedSel[i]); + } + } + else if (document.selection && savedSel.select) + { + savedSel.select(); + } + } + } + catch (e) + { + // ignore + } + }; + + /** + * Handling of special nl2Br style for not converting newlines to breaks in HTML labels. + * NOTE: Since it's easier to set this when the label is created we assume that it does + * not change during the lifetime of the mxText instance. + */ + var mxCellRendererInitializeLabel = mxCellRenderer.prototype.initializeLabel; + mxCellRenderer.prototype.initializeLabel = function(state) + { + if (state.text != null) + { + state.text.replaceLinefeeds = mxUtils.getValue(state.style, 'nl2Br', '1') != '0'; + } + + mxCellRendererInitializeLabel.apply(this, arguments); + }; + + var mxConstraintHandlerUpdate = mxConstraintHandler.prototype.update; + mxConstraintHandler.prototype.update = function(me, source) + { + if (this.isKeepFocusEvent(me) || !mxEvent.isAltDown(me.getEvent())) + { + mxConstraintHandlerUpdate.apply(this, arguments); + } + else + { + this.reset(); + } + }; + + /** + * No dashed shapes. + */ + mxGuide.prototype.createGuideShape = function(horizontal) + { + var guide = new mxPolyline([], mxConstants.GUIDE_COLOR, mxConstants.GUIDE_STROKEWIDTH); + + return guide; + }; + + /** + * HTML in-place editor + */ + mxCellEditor.prototype.escapeCancelsEditing = false; + + var mxCellEditorStartEditing = mxCellEditor.prototype.startEditing; + mxCellEditor.prototype.startEditing = function(cell, trigger) + { + mxCellEditorStartEditing.apply(this, arguments); + + // Overrides class in case of HTML content to add + // dashed borders for divs and table cells + var state = this.graph.view.getState(cell); + + if (state != null && state.style['html'] == 1) + { + this.textarea.className = 'mxCellEditor geContentEditable'; + } + else + { + this.textarea.className = 'mxCellEditor mxPlainTextEditor'; + } + + // Toggles markup vs wysiwyg mode + this.codeViewMode = false; + + // Stores current selection range when switching between markup and code + this.switchSelectionState = null; + + // Selects editing cell + this.graph.setSelectionCell(cell); + + // Enables focus outline for edges and edge labels + var parent = this.graph.getModel().getParent(cell); + var geo = this.graph.getCellGeometry(cell); + + if ((this.graph.getModel().isEdge(parent) && geo != null && geo.relative) || + this.graph.getModel().isEdge(cell)) + { + // Quirks does not support outline at all so use border instead + if (mxClient.IS_QUIRKS) + { + this.textarea.style.border = 'gray dotted 1px'; + } + // IE>8 and FF on Windows uses outline default of none + else if (mxClient.IS_IE || mxClient.IS_IE11 || (mxClient.IS_FF && mxClient.IS_WIN)) + { + this.textarea.style.outline = 'gray dotted 1px'; + } + else + { + this.textarea.style.outline = ''; + } + } + else if (mxClient.IS_QUIRKS) + { + this.textarea.style.outline = 'none'; + this.textarea.style.border = ''; + } + } + + /** + * HTML in-place editor + */ + var cellEditorInstallListeners = mxCellEditor.prototype.installListeners; + mxCellEditor.prototype.installListeners = function(elt) + { + cellEditorInstallListeners.apply(this, arguments); + + // Adds a reference from the clone to the original node, recursively + function reference(node, clone) + { + clone.originalNode = node; + + node = node.firstChild; + var child = clone.firstChild; + + while (node != null && child != null) + { + reference(node, child); + node = node.nextSibling; + child = child.nextSibling; + } + + return clone; + }; + + // Checks the given node for new nodes, recursively + function checkNode(node, clone) + { + if (node != null) + { + if (clone.originalNode != node) + { + cleanNode(node); + } + else + { + node = node.firstChild; + clone = clone.firstChild; + + while (node != null) + { + var nextNode = node.nextSibling; + + if (clone == null) + { + cleanNode(node); + } + else + { + checkNode(node, clone); + clone = clone.nextSibling; + } + + node = nextNode; + } + } + } + }; + + // Removes unused DOM nodes and attributes, recursively + function cleanNode(node) + { + var child = node.firstChild; + + while (child != null) + { + var next = child.nextSibling; + cleanNode(child); + child = next; + } + + if ((node.nodeType != 1 || (node.nodeName !== 'BR' && node.firstChild == null)) && + (node.nodeType != 3 || mxUtils.trim(mxUtils.getTextContent(node)).length == 0)) + { + node.parentNode.removeChild(node); + } + else + { + // Removes linefeeds + if (node.nodeType == 3) + { + mxUtils.setTextContent(node, mxUtils.getTextContent(node).replace(/\n|\r/g, '')); + } + + // Removes CSS classes and styles (for Word and Excel) + if (node.nodeType == 1) + { + node.removeAttribute('style'); + node.removeAttribute('class'); + node.removeAttribute('width'); + node.removeAttribute('cellpadding'); + node.removeAttribute('cellspacing'); + node.removeAttribute('border'); + } + } + }; + + // Handles paste from Word, Excel etc by removing styles, classnames and unused nodes + // LATER: Fix undo/redo for paste + if (!mxClient.IS_QUIRKS && document.documentMode !== 7 && document.documentMode !== 8) + { + mxEvent.addListener(this.textarea, 'paste', mxUtils.bind(this, function(evt) + { + var clone = reference(this.textarea, this.textarea.cloneNode(true)); + + window.setTimeout(mxUtils.bind(this, function() + { + checkNode(this.textarea, clone); + }), 0); + })); + } + }; + + mxCellEditor.prototype.toggleViewMode = function() + { + var state = this.graph.view.getState(this.editingCell); + var nl2Br = state != null && mxUtils.getValue(state.style, 'nl2Br', '1') != '0'; + var tmp = this.saveSelection(); + + if (!this.codeViewMode) + { + // Clears the initial empty label on the first keystroke + if (this.clearOnChange && this.textarea.innerHTML == this.getEmptyLabelText()) + { + this.clearOnChange = false; + this.textarea.innerHTML = ''; + } + + // Removes newlines from HTML and converts breaks to newlines + // to match the HTML output in plain text + var content = mxUtils.htmlEntities(this.textarea.innerHTML); + + // Workaround for trailing line breaks being ignored in the editor + if (!mxClient.IS_QUIRKS && document.documentMode != 8) + { + content = mxUtils.replaceTrailingNewlines(content, '

'); + } + + content = this.graph.sanitizeHtml((nl2Br) ? content.replace(/\n/g, '').replace(/<br\s*.?>/g, '
') : content, true); + this.textarea.className = 'mxCellEditor mxPlainTextEditor'; + + var size = mxConstants.DEFAULT_FONTSIZE; + + this.textarea.style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? Math.round(size * mxConstants.LINE_HEIGHT) + 'px' : mxConstants.LINE_HEIGHT; + this.textarea.style.fontSize = Math.round(size) + 'px'; + this.textarea.style.textDecoration = ''; + this.textarea.style.fontWeight = 'normal'; + this.textarea.style.fontStyle = ''; + this.textarea.style.fontFamily = mxConstants.DEFAULT_FONTFAMILY; + this.textarea.style.textAlign = 'left'; + + // Adds padding to make cursor visible with borders + this.textarea.style.padding = '2px'; + + if (this.textarea.innerHTML != content) + { + this.textarea.innerHTML = content; + } + + this.codeViewMode = true; + } + else + { + var content = mxUtils.extractTextWithWhitespace(this.textarea.childNodes); + + // Strips trailing line break + if (content.length > 0 && content.charAt(content.length - 1) == '\n') + { + content = content.substring(0, content.length - 1); + } + + content = this.graph.sanitizeHtml((nl2Br) ? content.replace(/\n/g, '
') : content, true) + this.textarea.className = 'mxCellEditor geContentEditable'; + + var size = mxUtils.getValue(state.style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE); + var family = mxUtils.getValue(state.style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY); + var align = mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT); + var bold = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) & + mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD; + var italic = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) & + mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC; + var uline = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) & + mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE; + + this.textarea.style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? Math.round(size * mxConstants.LINE_HEIGHT) + 'px' : mxConstants.LINE_HEIGHT; + this.textarea.style.fontSize = Math.round(size) + 'px'; + this.textarea.style.textDecoration = (uline) ? 'underline' : ''; + this.textarea.style.fontWeight = (bold) ? 'bold' : 'normal'; + this.textarea.style.fontStyle = (italic) ? 'italic' : ''; + this.textarea.style.fontFamily = family; + this.textarea.style.textAlign = align; + this.textarea.style.padding = '0px'; + + if (this.textarea.innerHTML != content) + { + this.textarea.innerHTML = content; + + if (this.textarea.innerHTML.length == 0) + { + this.textarea.innerHTML = this.getEmptyLabelText(); + this.clearOnChange = this.textarea.innerHTML.length > 0; + } + } + + this.codeViewMode = false; + } + + this.textarea.focus(); + + if (this.switchSelectionState != null) + { + this.restoreSelection(this.switchSelectionState); + } + + this.switchSelectionState = tmp; + this.resize(); + }; + + var mxCellEditorResize = mxCellEditor.prototype.resize; + mxCellEditor.prototype.resize = function(state, trigger) + { + if (this.textarea != null) + { + var state = this.graph.getView().getState(this.editingCell); + + if (this.codeViewMode && state != null) + { + var scale = state.view.scale; + this.bounds = mxRectangle.fromRectangle(state); + + // General placement of code editor if cell has no size + // LATER: Fix HTML editor bounds for edge labels + if (this.bounds.width == 0 && this.bounds.height == 0) + { + this.bounds.width = 160 * scale; + this.bounds.height = 60 * scale; + + var m = (state.text != null) ? state.text.margin : null; + + if (m == null) + { + m = mxUtils.getAlignmentAsPoint(mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER), + mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE)); + } + + this.bounds.x += m.x * this.bounds.width; + this.bounds.y += m.y * this.bounds.height; + } + + this.textarea.style.width = Math.round((this.bounds.width - 4) / scale) + 'px'; + this.textarea.style.height = Math.round((this.bounds.height - 4) / scale) + 'px'; + this.textarea.style.overflow = 'auto'; + + // Adds scrollbar offset if visible + if (this.textarea.clientHeight < this.textarea.offsetHeight) + { + this.textarea.style.height = Math.round((this.bounds.height / scale)) + (this.textarea.offsetHeight - this.textarea.clientHeight) + 'px'; + this.bounds.height = parseInt(this.textarea.style.height) * scale; + } + + if (this.textarea.clientWidth < this.textarea.offsetWidth) + { + this.textarea.style.width = Math.round((this.bounds.width / scale)) + (this.textarea.offsetWidth - this.textarea.clientWidth) + 'px'; + this.bounds.width = parseInt(this.textarea.style.width) * scale; + } + + this.textarea.style.left = Math.round(this.bounds.x) + 'px'; + this.textarea.style.top = Math.round(this.bounds.y) + 'px'; + + if (mxClient.IS_VML) + { + this.textarea.style.zoom = scale; + } + else + { + mxUtils.setPrefixedStyle(this.textarea.style, 'transform', 'scale(' + scale + ',' + scale + ')'); + } + } + else + { + this.textarea.style.height = ''; + this.textarea.style.overflow = ''; + mxCellEditorResize.apply(this, arguments); + } + } + }; + + mxCellEditorGetInitialValue = mxCellEditor.prototype.getInitialValue; + mxCellEditor.prototype.getInitialValue = function(state, trigger) + { + if (mxUtils.getValue(state.style, 'html', '0') == '0') + { + return mxCellEditorGetInitialValue.apply(this, arguments); + } + else + { + var result = this.graph.getEditingValue(state.cell, trigger) + + if (mxUtils.getValue(state.style, 'nl2Br', '1') == '1') + { + result = result.replace(/\n/g, '
'); + } + + result = this.graph.sanitizeHtml(result, true); + + return result; + } + }; + + mxCellEditorGetCurrentValue = mxCellEditor.prototype.getCurrentValue; + mxCellEditor.prototype.getCurrentValue = function(state) + { + if (mxUtils.getValue(state.style, 'html', '0') == '0') + { + return mxCellEditorGetCurrentValue.apply(this, arguments); + } + else + { + var result = this.graph.sanitizeHtml(this.textarea.innerHTML, true); + + if (mxUtils.getValue(state.style, 'nl2Br', '1') == '1') + { + result = result.replace(/\r\n/g, '
').replace(/\n/g, '
'); + } + else + { + result = result.replace(/\r\n/g, '').replace(/\n/g, ''); + } + + return result; + } + }; + + var mxCellEditorStopEditing = mxCellEditor.prototype.stopEditing; + mxCellEditor.prototype.stopEditing = function(cancel) + { + // Restores default view mode before applying value + if (this.codeViewMode) + { + this.toggleViewMode(); + } + + mxCellEditorStopEditing.apply(this, arguments); + + // Tries to move focus back to container after editing if possible + this.focusContainer(); + }; + + mxCellEditor.prototype.focusContainer = function() + { + try + { + this.graph.container.focus(); + } + catch (e) + { + // ignore + } + }; + + var mxCellEditorApplyValue = mxCellEditor.prototype.applyValue; + mxCellEditor.prototype.applyValue = function(state, value) + { + // Removes empty relative child labels in edges + this.graph.getModel().beginUpdate(); + + try + { + mxCellEditorApplyValue.apply(this, arguments); + + if (this.graph.isCellDeletable(state.cell) && this.graph.model.getChildCount(state.cell) == 0) + { + var stroke = mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE); + var fill = mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, mxConstants.NONE); + + if (value == '' && stroke == mxConstants.NONE && fill == mxConstants.NONE) + { + this.graph.removeCells([state.cell], false); + } + } + } + finally + { + this.graph.getModel().endUpdate(); + } + }; + + /** + * Returns the background color to be used for the editing box. This returns + * the label background for edge labels and null for all other cases. + */ + mxCellEditor.prototype.getBackgroundColor = function(state) + { + var color = null; + + if (this.graph.getModel().isEdge(state.cell) || this.graph.getModel().isEdge(this.graph.getModel().getParent(state.cell))) + { + var color = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_BACKGROUNDCOLOR, null); + + if (color == mxConstants.NONE) + { + color = null; + } + } + + return color; + }; + + mxCellEditor.prototype.getMinimumSize = function(state) + { + var scale = this.graph.getView().scale; + + return new mxRectangle(0, 0, (state.text == null) ? 30 : state.text.size * scale + 20, 30); + }; + + // Hold alt to ignore drop target + var mxGraphHandlerMoveCells = mxGraphHandler.prototype.moveCells; + + mxGraphHandler.prototype.moveCells = function(cells, dx, dy, clone, target, evt) + { + if (mxEvent.isAltDown(evt)) + { + target = null; + } + + mxGraphHandlerMoveCells.apply(this, arguments); + }; + + /** + * Hints on handlers + */ + function createHint() + { + var hint = document.createElement('div'); + hint.className = 'geHint'; + hint.style.whiteSpace = 'nowrap'; + hint.style.position = 'absolute'; + + return hint; + }; + + /** + * Updates the hint for the current operation. + */ + mxGraphHandler.prototype.updateHint = function(me) + { + if (this.shape != null) + { + if (this.hint == null) + { + this.hint = createHint(); + this.graph.container.appendChild(this.hint); + } + + var t = this.graph.view.translate; + var s = this.graph.view.scale; + var x = this.roundLength((this.bounds.x + this.currentDx) / s - t.x); + var y = this.roundLength((this.bounds.y + this.currentDy) / s - t.y); + + this.hint.innerHTML = x + ', ' + y; + + this.hint.style.left = (this.shape.bounds.x + Math.round((this.shape.bounds.width - this.hint.clientWidth) / 2)) + 'px'; + this.hint.style.top = (this.shape.bounds.y + this.shape.bounds.height + 12) + 'px'; + } + }; + + /** + * Updates the hint for the current operation. + */ + mxGraphHandler.prototype.removeHint = function() + { + if (this.hint != null) + { + this.hint.parentNode.removeChild(this.hint); + this.hint = null; + } + }; + + /** + * Enables recursive resize for groups. + */ + mxVertexHandler.prototype.isRecursiveResize = function(state, me) + { + return !this.graph.isSwimlane(state.cell) && this.graph.model.getChildCount(state.cell) > 0 && + !mxEvent.isControlDown(me.getEvent()) && !this.graph.isCellCollapsed(state.cell) && + mxUtils.getValue(state.style, 'recursiveResize', '1') == '1' && + mxUtils.getValue(state.style, 'childLayout', null) == null; + }; + + /** + * Enables centered resize events. + */ + mxVertexHandler.prototype.isCenteredEvent = function(state, me) + { + return (!(!this.graph.isSwimlane(state.cell) && this.graph.model.getChildCount(state.cell) > 0 && + !this.graph.isCellCollapsed(state.cell) && + mxUtils.getValue(state.style, 'recursiveResize', '1') == '1' && + mxUtils.getValue(state.style, 'childLayout', null) == null) && + mxEvent.isControlDown(me.getEvent())) || + mxEvent.isMetaDown(me.getEvent()); + }; + + var vertexHandlerGetHandlePadding = mxVertexHandler.prototype.getHandlePadding; + mxVertexHandler.prototype.getHandlePadding = function() + { + var result = new mxPoint(0, 0); + var tol = this.tolerance; + + if (this.graph.cellEditor.getEditingCell() == this.state.cell && + this.sizers != null && this.sizers.length > 0 && this.sizers[0] != null) + { + tol /= 2; + + result.x = this.sizers[0].bounds.width + tol; + result.y = this.sizers[0].bounds.height + tol; + } + else + { + result = vertexHandlerGetHandlePadding.apply(this, arguments); + } + + return result; + }; + + /** + * Updates the hint for the current operation. + */ + mxVertexHandler.prototype.updateHint = function(me) + { + if (this.index != mxEvent.LABEL_HANDLE) + { + if (this.hint == null) + { + this.hint = createHint(); + this.state.view.graph.container.appendChild(this.hint); + } + + if (this.index == mxEvent.ROTATION_HANDLE) + { + this.hint.innerHTML = this.currentAlpha + '°'; + } + else + { + var s = this.state.view.scale; + this.hint.innerHTML = this.roundLength(this.bounds.width / s) + ' x ' + this.roundLength(this.bounds.height / s); + } + + var rot = (this.currentAlpha != null) ? this.currentAlpha : this.state.style[mxConstants.STYLE_ROTATION] || '0'; + var bb = mxUtils.getBoundingBox(this.bounds, rot); + + if (bb == null) + { + bb = this.bounds; + } + + this.hint.style.left = bb.x + Math.round((bb.width - this.hint.clientWidth) / 2) + 'px'; + this.hint.style.top = (bb.y + bb.height + 12) + 'px'; + + if (this.linkHint != null) + { + this.linkHint.style.display = 'none'; + } + } + }; + + /** + * Updates the hint for the current operation. + */ + mxVertexHandler.prototype.removeHint = function() + { + mxGraphHandler.prototype.removeHint.apply(this, arguments); + + if (this.linkHint != null) + { + this.linkHint.style.display = ''; + } + }; + + /** + * Updates the hint for the current operation. + */ + mxEdgeHandler.prototype.updateHint = function(me, point) + { + if (this.hint == null) + { + this.hint = createHint(); + this.state.view.graph.container.appendChild(this.hint); + } + + var t = this.graph.view.translate; + var s = this.graph.view.scale; + var x = this.roundLength(point.x / s - t.x); + var y = this.roundLength(point.y / s - t.y); + + this.hint.innerHTML = x + ', ' + y; + this.hint.style.visibility = 'visible'; + + if (this.isSource || this.isTarget) + { + if (this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null) + { + var pt = this.constraintHandler.currentConstraint.point; + this.hint.innerHTML = '[' + Math.round(pt.x * 100) + '%, '+ Math.round(pt.y * 100) + '%]'; + } + else if (this.marker.hasValidState()) + { + this.hint.style.visibility = 'hidden'; + } + } + + this.hint.style.left = Math.round(me.getGraphX() - this.hint.clientWidth / 2) + 'px'; + this.hint.style.top = (Math.max(me.getGraphY(), point.y) + this.state.view.graph.gridSize) + 'px'; + + if (this.linkHint != null) + { + this.linkHint.style.display = 'none'; + } + }; + + /** + * Updates the hint for the current operation. + */ + mxEdgeHandler.prototype.removeHint = mxVertexHandler.prototype.removeHint; + + /** + * Defines the handles for the UI. Uses data-URIs to speed-up loading time where supported. + */ + // TODO: Increase handle padding + HoverIcons.prototype.mainHandle = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/handle-main.png', 17, 17) : + Graph.createSvgImage(18, 18, ''); + HoverIcons.prototype.secondaryHandle = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/handle-secondary.png', 17, 17) : + Graph.createSvgImage(16, 16, ''); + HoverIcons.prototype.fixedHandle = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/handle-fixed.png', 17, 17) : + Graph.createSvgImage(18, 18, ''); + HoverIcons.prototype.terminalHandle = (!mxClient.IS_SVG) ? new mxImage(IMAGE_PATH + '/handle-terminal.png', 17, 17) : + Graph.createSvgImage(18, 18, ''); + HoverIcons.prototype.rotationHandle = new mxImage((mxClient.IS_SVG) ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAVCAYAAACkCdXRAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAA6ZJREFUeNqM001IY1cUB/D/fYmm2sbR2lC1zYlgoRG6MpEyBlpxM9iFIGKFIm3s0lCKjOByhCLZCFqLBF1YFVJdSRbdFHRhBbULtRuFVBTzYRpJgo2mY5OX5N9Fo2TG+eiFA/dd3vvd8+65ByTxshARTdf1JySp6/oTEdFe9T5eg5lIcnBwkCSZyWS+exX40oyur68/KxaLf5Okw+H4X+A9JBaLfUySZ2dnnJqaosPhIAACeC34DJRKpb7IZrMcHx+nwWCgUopGo/EOKwf9fn/1CzERUevr6+9ls1mOjIwQAH0+H4PBIKPR6D2ofAQCgToRUeVYJUkuLy8TANfW1kiS8/PzCy84Mw4MDBAAZ2dnmc/nub+/X0MSEBF1cHDwMJVKsaGhgV6vl+l0mqOjo1+KyKfl1dze3l4NBoM/PZ+diFSLiIKIGBOJxA9bW1sEwNXVVSaTyQMRaRaRxrOzs+9J8ujoaE5EPhQRq67rcZ/PRwD0+/3Udf03EdEgIqZisZibnJykwWDg4eEhd3Z2xkXELCJvPpdBrYjUiEhL+Xo4HH4sIhUaAKNSqiIcDsNkMqG+vh6RSOQQQM7tdhsAQCkFAHC73UUATxcWFqypVApmsxnDw8OwWq2TADQNgAYAFosF+XweyWQSdru9BUBxcXFRB/4rEgDcPouIIx6P4+bmBi0tLSCpAzBqAIqnp6c/dnZ2IpfLYXNzE62traMADACKNputpr+/v8lms9UAKAAwiMjXe3t7KBQKqKurQy6Xi6K0i2l6evpROp1mbW0t29vbGY/Hb8/IVIqq2zlJXl1dsaOjg2azmefn5wwEAl+JSBVExCgi75PkzMwMlVJsbGxkIpFgPp8PX15ePopEIs3JZPITXdf/iEajbGpqolKKExMT1HWdHo/nIxGpgIgoEXnQ3d39kCTHxsYIgC6Xi3NzcwyHw8xkMozFYlxaWmJbWxuVUuzt7WUul6PX6/1cRN4WEe2uA0SkaWVl5XGpRVhdXU0A1DSNlZWVdz3qdDrZ09PDWCzG4+Pjn0XEWvp9KJKw2WwKwBsA3gHQHAqFfr24uMDGxgZ2d3cRiUQAAHa7HU6nE319fTg5Ofmlq6vrGwB/AngaCoWK6rbsNptNA1AJoA7Aux6Pp3NoaMhjsVg+QNmIRqO/u1yubwFEASRKUAEA7rASqABUAKgC8KAUb5XWCOAfAFcA/gJwDSB7C93DylCtdM8qABhLc5TumV6KQigUeubjfwcAHkQJ94ndWeYAAAAASUVORK5CYII=' : + IMAGE_PATH + '/handle-rotate.png', 19, 21); + + if (mxClient.IS_SVG) + { + mxConstraintHandler.prototype.pointImage = Graph.createSvgImage(5, 5, ''); + } + + mxVertexHandler.prototype.handleImage = HoverIcons.prototype.mainHandle; + mxVertexHandler.prototype.secondaryHandleImage = HoverIcons.prototype.secondaryHandle; + mxEdgeHandler.prototype.handleImage = HoverIcons.prototype.mainHandle; + mxEdgeHandler.prototype.terminalHandleImage = HoverIcons.prototype.terminalHandle; + mxEdgeHandler.prototype.fixedHandleImage = HoverIcons.prototype.fixedHandle; + mxEdgeHandler.prototype.labelHandleImage = HoverIcons.prototype.secondaryHandle; + mxOutline.prototype.sizerImage = HoverIcons.prototype.mainHandle; + + if (window.Sidebar != null) + { + Sidebar.prototype.triangleUp = HoverIcons.prototype.triangleUp; + Sidebar.prototype.triangleRight = HoverIcons.prototype.triangleRight; + Sidebar.prototype.triangleDown = HoverIcons.prototype.triangleDown; + Sidebar.prototype.triangleLeft = HoverIcons.prototype.triangleLeft; + Sidebar.prototype.refreshTarget = HoverIcons.prototype.refreshTarget; + Sidebar.prototype.roundDrop = HoverIcons.prototype.roundDrop; + } + + // Pre-fetches images (only needed for non data-uris) + if (!mxClient.IS_SVG) + { + new Image().src = HoverIcons.prototype.mainHandle.src; + new Image().src = HoverIcons.prototype.fixedHandle.src; + new Image().src = HoverIcons.prototype.terminalHandle.src; + new Image().src = HoverIcons.prototype.secondaryHandle.src; + new Image().src = HoverIcons.prototype.rotationHandle.src; + + new Image().src = HoverIcons.prototype.triangleUp.src; + new Image().src = HoverIcons.prototype.triangleRight.src; + new Image().src = HoverIcons.prototype.triangleDown.src; + new Image().src = HoverIcons.prototype.triangleLeft.src; + new Image().src = HoverIcons.prototype.refreshTarget.src; + new Image().src = HoverIcons.prototype.roundDrop.src; + } + + // Adds rotation handle and live preview + mxVertexHandler.prototype.rotationEnabled = true; + mxVertexHandler.prototype.manageSizers = true; + mxVertexHandler.prototype.livePreview = true; + + // Increases default rubberband opacity (default is 20) + mxRubberband.prototype.defaultOpacity = 30; + + // Enables connections along the outline, virtual waypoints, parent highlight etc + mxConnectionHandler.prototype.outlineConnect = true; + mxCellHighlight.prototype.keepOnTop = true; + mxVertexHandler.prototype.parentHighlightEnabled = true; + mxVertexHandler.prototype.rotationHandleVSpacing = -20; + + mxEdgeHandler.prototype.parentHighlightEnabled = true; + mxEdgeHandler.prototype.dblClickRemoveEnabled = true; + mxEdgeHandler.prototype.straightRemoveEnabled = true; + mxEdgeHandler.prototype.virtualBendsEnabled = true; + mxEdgeHandler.prototype.mergeRemoveEnabled = true; + mxEdgeHandler.prototype.manageLabelHandle = true; + mxEdgeHandler.prototype.outlineConnect = true; + + // Disables adding waypoints if shift is pressed + mxEdgeHandler.prototype.isAddVirtualBendEvent = function(me) + { + return !mxEvent.isShiftDown(me.getEvent()); + }; + + // Disables custom handles if shift is pressed + mxEdgeHandler.prototype.isCustomHandleEvent = function(me) + { + return !mxEvent.isShiftDown(me.getEvent()); + }; + + /** + * Implements touch style + */ + if (Graph.touchStyle) + { + // Larger tolerance for real touch devices + if (mxClient.IS_TOUCH || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0) + { + mxShape.prototype.svgStrokeTolerance = 18; + mxVertexHandler.prototype.tolerance = 12; + mxEdgeHandler.prototype.tolerance = 12; + Graph.prototype.tolerance = 12; + + mxVertexHandler.prototype.rotationHandleVSpacing = -24; + + // Implements a smaller tolerance for mouse events and a larger tolerance for touch + // events on touch devices. The default tolerance (4px) is used for mouse events. + mxConstraintHandler.prototype.getTolerance = function(me) + { + return (mxEvent.isMouseEvent(me.getEvent())) ? 4 : this.graph.getTolerance(); + }; + } + + // One finger pans (no rubberband selection) must start regardless of mouse button + mxPanningHandler.prototype.isPanningTrigger = function(me) + { + var evt = me.getEvent(); + + return (me.getState() == null && !mxEvent.isMouseEvent(evt)) || + (mxEvent.isPopupTrigger(evt) && (me.getState() == null || + mxEvent.isControlDown(evt) || mxEvent.isShiftDown(evt))); + }; + + // Don't clear selection if multiple cells selected + var graphHandlerMouseDown = mxGraphHandler.prototype.mouseDown; + mxGraphHandler.prototype.mouseDown = function(sender, me) + { + graphHandlerMouseDown.apply(this, arguments); + + if (mxEvent.isTouchEvent(me.getEvent()) && this.graph.isCellSelected(me.getCell()) && + this.graph.getSelectionCount() > 1) + { + this.delayedSelection = false; + } + }; + } + else + { + // Removes ctrl+shift as panning trigger for space splitting + mxPanningHandler.prototype.isPanningTrigger = function(me) + { + var evt = me.getEvent(); + + return (mxEvent.isLeftMouseButton(evt) && ((this.useLeftButtonForPanning && + me.getState() == null) || (mxEvent.isControlDown(evt) && + !mxEvent.isShiftDown(evt)))) || (this.usePopupTrigger && + mxEvent.isPopupTrigger(evt)); + }; + } + + // Overrides/extends rubberband for space handling with Ctrl+Shift(+Alt) drag ("scissors tool") + mxRubberband.prototype.isSpaceEvent = function(me) + { + return this.graph.isEnabled() && !this.graph.isCellLocked(this.graph.getDefaultParent()) && + mxEvent.isControlDown(me.getEvent()) && mxEvent.isShiftDown(me.getEvent()); + }; + + // Handles moving of cells in both half panes + mxRubberband.prototype.mouseUp = function(sender, me) + { + var execute = this.div != null && this.div.style.display != 'none'; + + var x0 = null; + var y0 = null; + var dx = null; + var dy = null; + + if (this.first != null && this.currentX != null && this.currentY != null) + { + x0 = this.first.x; + y0 = this.first.y; + dx = (this.currentX - x0) / this.graph.view.scale; + dy = (this.currentY - y0) / this.graph.view.scale; + + if (!mxEvent.isAltDown(me.getEvent())) + { + dx = this.graph.snap(dx); + dy = this.graph.snap(dy); + + if (!this.graph.isGridEnabled()) + { + if (Math.abs(dx) < this.graph.tolerance) + { + dx = 0; + } + + if (Math.abs(dy) < this.graph.tolerance) + { + dy = 0; + } + } + } + } + + this.reset(); + + if (execute) + { + if (mxEvent.isAltDown(me.getEvent()) && this.graph.isToggleEvent(me.getEvent())) + { + var rect = new mxRectangle(this.x, this.y, this.width, this.height); + var cells = this.graph.getCells(rect.x, rect.y, rect.width, rect.height); + + this.graph.removeSelectionCells(cells); + } + else if (this.isSpaceEvent(me)) + { + this.graph.model.beginUpdate(); + try + { + var cells = this.graph.getCellsBeyond(x0, y0, this.graph.getDefaultParent(), true, true); + + for (var i = 0; i < cells.length; i++) + { + if (this.graph.isCellMovable(cells[i])) + { + var tmp = this.graph.view.getState(cells[i]); + var geo = this.graph.getCellGeometry(cells[i]); + + if (tmp != null && geo != null) + { + geo = geo.clone(); + geo.translate(dx, dy); + this.graph.model.setGeometry(cells[i], geo); + } + } + } + } + finally + { + this.graph.model.endUpdate(); + } + } + else + { + var rect = new mxRectangle(this.x, this.y, this.width, this.height); + this.graph.selectRegion(rect, me.getEvent()); + } + + me.consume(); + } + }; + + // Handles preview for creating/removing space in diagram + mxRubberband.prototype.mouseMove = function(sender, me) + { + if (!me.isConsumed() && this.first != null) + { + var origin = mxUtils.getScrollOrigin(this.graph.container); + var offset = mxUtils.getOffset(this.graph.container); + origin.x -= offset.x; + origin.y -= offset.y; + var x = me.getX() + origin.x; + var y = me.getY() + origin.y; + var dx = this.first.x - x; + var dy = this.first.y - y; + var tol = this.graph.tolerance; + + if (this.div != null || Math.abs(dx) > tol || Math.abs(dy) > tol) + { + if (this.div == null) + { + this.div = this.createShape(); + } + + // Clears selection while rubberbanding. This is required because + // the event is not consumed in mouseDown. + mxUtils.clearSelection(); + this.update(x, y); + + if (this.isSpaceEvent(me)) + { + var right = this.x + this.width; + var bottom = this.y + this.height; + var scale = this.graph.view.scale; + + if (!mxEvent.isAltDown(me.getEvent())) + { + this.width = this.graph.snap(this.width / scale) * scale; + this.height = this.graph.snap(this.height / scale) * scale; + + if (!this.graph.isGridEnabled()) + { + if (this.width < this.graph.tolerance) + { + this.width = 0; + } + + if (this.height < this.graph.tolerance) + { + this.height = 0; + } + } + + if (this.x < this.first.x) + { + this.x = right - this.width; + } + + if (this.y < this.first.y) + { + this.y = bottom - this.height; + } + } + + this.div.style.borderStyle = 'dashed'; + this.div.style.backgroundColor = 'white'; + this.div.style.left = this.x + 'px'; + this.div.style.top = this.y + 'px'; + this.div.style.width = Math.max(0, this.width) + 'px'; + this.div.style.height = this.graph.container.clientHeight + 'px'; + this.div.style.borderWidth = (this.width <= 0) ? '0px 1px 0px 0px' : '0px 1px 0px 1px'; + + if (this.secondDiv == null) + { + this.secondDiv = this.div.cloneNode(true); + this.div.parentNode.appendChild(this.secondDiv); + } + + this.secondDiv.style.left = this.x + 'px'; + this.secondDiv.style.top = this.y + 'px'; + this.secondDiv.style.width = this.graph.container.clientWidth + 'px'; + this.secondDiv.style.height = Math.max(0, this.height) + 'px'; + this.secondDiv.style.borderWidth = (this.height <= 0) ? '1px 0px 0px 0px' : '1px 0px 1px 0px'; + } + else + { + // Hides second div and restores style + this.div.style.backgroundColor = ''; + this.div.style.borderWidth = ''; + this.div.style.borderStyle = ''; + + if (this.secondDiv != null) + { + this.secondDiv.parentNode.removeChild(this.secondDiv); + this.secondDiv = null; + } + } + + me.consume(); + } + } + }; + + // Removes preview + var mxRubberbandReset = mxRubberband.prototype.reset; + mxRubberband.prototype.reset = function() + { + if (this.secondDiv != null) + { + this.secondDiv.parentNode.removeChild(this.secondDiv); + this.secondDiv = null; + } + + mxRubberbandReset.apply(this, arguments); + }; + + // Timer-based activation of outline connect in connection handler + var startTime = new Date().getTime(); + var timeOnTarget = 0; + + var mxEdgeHandlerUpdatePreviewState = mxEdgeHandler.prototype.updatePreviewState; + + mxEdgeHandler.prototype.updatePreviewState = function(edge, point, terminalState, me) + { + mxEdgeHandlerUpdatePreviewState.apply(this, arguments); + + if (terminalState != this.currentTerminalState) + { + startTime = new Date().getTime(); + timeOnTarget = 0; + } + else + { + timeOnTarget = new Date().getTime() - startTime; + } + + this.currentTerminalState = terminalState; + }; + + // Timer-based outline connect + var mxEdgeHandlerIsOutlineConnectEvent = mxEdgeHandler.prototype.isOutlineConnectEvent; + + mxEdgeHandler.prototype.isOutlineConnectEvent = function(me) + { + return (this.currentTerminalState != null && me.getState() == this.currentTerminalState && timeOnTarget > 2000) || + ((this.currentTerminalState == null || mxUtils.getValue(this.currentTerminalState.style, 'outlineConnect', '1') != '0') && + mxEdgeHandlerIsOutlineConnectEvent.apply(this, arguments)); + }; + + // Disables custom handles if shift is pressed + mxVertexHandler.prototype.isCustomHandleEvent = function(me) + { + return !mxEvent.isShiftDown(me.getEvent()); + }; + + // Shows secondary handle for fixed connection points + mxEdgeHandler.prototype.createHandleShape = function(index, virtual) + { + var source = index != null && index == 0; + var terminalState = this.state.getVisibleTerminalState(source); + var c = (index != null && (index == 0 || index >= this.state.absolutePoints.length - 1 || + (this.constructor == mxElbowEdgeHandler && index == 2))) ? + this.graph.getConnectionConstraint(this.state, terminalState, source) : null; + var pt = (c != null) ? this.graph.getConnectionPoint(this.state.getVisibleTerminalState(source), c) : null; + var img = (pt != null) ? this.fixedHandleImage : ((c != null && terminalState != null) ? + this.terminalHandleImage : this.handleImage); + + if (img != null) + { + var shape = new mxImageShape(new mxRectangle(0, 0, img.width, img.height), img.src); + + // Allows HTML rendering of the images + shape.preserveImageAspect = false; + + return shape; + } + else + { + var s = mxConstants.HANDLE_SIZE; + + if (this.preferHtml) + { + s -= 1; + } + + return new mxRectangleShape(new mxRectangle(0, 0, s, s), mxConstants.HANDLE_FILLCOLOR, mxConstants.HANDLE_STROKECOLOR); + } + }; + + var vertexHandlerCreateSizerShape = mxVertexHandler.prototype.createSizerShape; + mxVertexHandler.prototype.createSizerShape = function(bounds, index, fillColor) + { + this.handleImage = (index == mxEvent.ROTATION_HANDLE) ? HoverIcons.prototype.rotationHandle : (index == mxEvent.LABEL_HANDLE) ? this.secondaryHandleImage : this.handleImage; + + return vertexHandlerCreateSizerShape.apply(this, arguments); + }; + + // Special case for single edge label handle moving in which case the text bounding box is used + var mxGraphHandlerGetBoundingBox = mxGraphHandler.prototype.getBoundingBox; + mxGraphHandler.prototype.getBoundingBox = function(cells) + { + if (cells != null && cells.length == 1) + { + var model = this.graph.getModel(); + var parent = model.getParent(cells[0]); + var geo = this.graph.getCellGeometry(cells[0]); + + if (model.isEdge(parent) && geo != null && geo.relative) + { + var state = this.graph.view.getState(cells[0]); + + if (state != null && state.width < 2 && state.height < 2 && state.text != null && state.text.boundingBox != null) + { + return mxRectangle.fromRectangle(state.text.boundingBox); + } + } + } + + return mxGraphHandlerGetBoundingBox.apply(this, arguments); + }; + + // Uses text bounding box for edge labels + var mxVertexHandlerGetSelectionBounds = mxVertexHandler.prototype.getSelectionBounds; + mxVertexHandler.prototype.getSelectionBounds = function(state) + { + var model = this.graph.getModel(); + var parent = model.getParent(state.cell); + var geo = this.graph.getCellGeometry(state.cell); + + if (model.isEdge(parent) && geo != null && geo.relative && state.width < 2 && state.height < 2 && state.text != null && state.text.boundingBox != null) + { + var bbox = state.text.unrotatedBoundingBox || state.text.boundingBox; + + return new mxRectangle(Math.round(bbox.x), Math.round(bbox.y), Math.round(bbox.width), Math.round(bbox.height)); + } + else + { + return mxVertexHandlerGetSelectionBounds.apply(this, arguments); + } + }; + + // Redirects moving of edge labels to mxGraphHandler by not starting here. + // This will use the move preview of mxGraphHandler (see above). + var mxVertexHandlerMouseDown = mxVertexHandler.prototype.mouseDown; + mxVertexHandler.prototype.mouseDown = function(sender, me) + { + var model = this.graph.getModel(); + var parent = model.getParent(this.state.cell); + var geo = this.graph.getCellGeometry(this.state.cell); + + // Lets rotation events through + var handle = this.getHandleForEvent(me); + + if (handle == mxEvent.ROTATION_HANDLE || !model.isEdge(parent) || geo == null || !geo.relative || + this.state == null || this.state.width >= 2 || this.state.height >= 2) + { + mxVertexHandlerMouseDown.apply(this, arguments); + } + }; + + // Shows rotation handle for edge labels. + mxVertexHandler.prototype.isRotationHandleVisible = function() + { + return this.graph.isEnabled() && this.rotationEnabled && this.graph.isCellRotatable(this.state.cell) && + (mxGraphHandler.prototype.maxCells <= 0 || this.graph.getSelectionCount() < mxGraphHandler.prototype.maxCells); + }; + + // Invokes turn on single click on rotation handle + mxVertexHandler.prototype.rotateClick = function() + { + this.state.view.graph.turnShapes([this.state.cell]); + }; + + var vertexHandlerMouseMove = mxVertexHandler.prototype.mouseMove; + + // Workaround for "isConsumed not defined" in MS Edge is to use arguments + mxVertexHandler.prototype.mouseMove = function(sender, me) + { + vertexHandlerMouseMove.apply(this, arguments); + + if (this.graph.graphHandler.first != null) + { + if (this.rotationShape != null && this.rotationShape.node != null) + { + this.rotationShape.node.style.display = 'none'; + } + } + }; + + var vertexHandlerMouseUp = mxVertexHandler.prototype.mouseUp; + mxVertexHandler.prototype.mouseUp = function(sender, me) + { + vertexHandlerMouseUp.apply(this, arguments); + + // Shows rotation handle only if one vertex is selected + if (this.rotationShape != null && this.rotationShape.node != null) + { + this.rotationShape.node.style.display = (this.graph.getSelectionCount() == 1) ? '' : 'none'; + } + }; + + var vertexHandlerInit = mxVertexHandler.prototype.init; + mxVertexHandler.prototype.init = function() + { + vertexHandlerInit.apply(this, arguments); + var redraw = false; + + if (this.rotationShape != null) + { + this.rotationShape.node.setAttribute('title', mxResources.get('rotateTooltip')); + } + + var update = mxUtils.bind(this, function() + { + // Shows rotation handle only if one vertex is selected + if (this.rotationShape != null && this.rotationShape.node != null) + { + this.rotationShape.node.style.display = (this.graph.getSelectionCount() == 1) ? '' : 'none'; + } + + if (this.specialHandle != null) + { + this.specialHandle.node.style.display = (this.graph.isEnabled() && this.graph.getSelectionCount() < this.graph.graphHandler.maxCells) ? '' : 'none'; + } + + this.redrawHandles(); + }); + + this.selectionHandler = mxUtils.bind(this, function(sender, evt) + { + update(); + }); + + this.graph.getSelectionModel().addListener(mxEvent.CHANGE, this.selectionHandler); + + this.changeHandler = mxUtils.bind(this, function(sender, evt) + { + this.updateLinkHint(this.graph.getLinkForCell(this.state.cell), + this.graph.getLinksForState(this.state)); + update(); + }); + + this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler); + + // Repaint needed when editing stops and no change event is fired + this.editingHandler = mxUtils.bind(this, function(sender, evt) + { + this.redrawHandles(); + }); + + this.graph.addListener(mxEvent.EDITING_STOPPED, this.editingHandler); + + var link = this.graph.getLinkForCell(this.state.cell); + var links = this.graph.getLinksForState(this.state); + this.updateLinkHint(link, links); + + if (link != null || (links != null && links.length > 0)) + { + redraw = true; + } + + if (redraw) + { + this.redrawHandles(); + } + }; + + mxVertexHandler.prototype.updateLinkHint = function(link, links) + { + if ((link == null && (links == null || links.length == 0)) || + this.graph.getSelectionCount() > 1) + { + if (this.linkHint != null) + { + this.linkHint.parentNode.removeChild(this.linkHint); + this.linkHint = null; + } + } + else if (link != null || (links != null && links.length > 0)) + { + if (this.linkHint == null) + { + this.linkHint = createHint(); + this.linkHint.style.padding = '6px 8px 6px 8px'; + this.linkHint.style.opacity = '1'; + this.linkHint.style.filter = ''; + + this.graph.container.appendChild(this.linkHint); + } + + this.linkHint.innerHTML = ''; + + if (link != null) + { + this.linkHint.appendChild(this.graph.createLinkForHint(link)); + + if (this.graph.isEnabled() && typeof this.graph.editLink === 'function') + { + var changeLink = document.createElement('img'); + changeLink.setAttribute('src', Editor.editImage); + changeLink.setAttribute('title', mxResources.get('editLink')); + changeLink.setAttribute('width', '11'); + changeLink.setAttribute('height', '11'); + changeLink.style.marginLeft = '10px'; + changeLink.style.marginBottom = '-1px'; + changeLink.style.cursor = 'pointer'; + this.linkHint.appendChild(changeLink); + + mxEvent.addListener(changeLink, 'click', mxUtils.bind(this, function(evt) + { + this.graph.setSelectionCell(this.state.cell); + this.graph.editLink(); + mxEvent.consume(evt); + })); + + var removeLink = document.createElement('img'); + removeLink.setAttribute('src', Dialog.prototype.clearImage); + removeLink.setAttribute('title', mxResources.get('removeIt', [mxResources.get('link')])); + removeLink.setAttribute('width', '13'); + removeLink.setAttribute('height', '10'); + removeLink.style.marginLeft = '4px'; + removeLink.style.marginBottom = '-1px'; + removeLink.style.cursor = 'pointer'; + this.linkHint.appendChild(removeLink); + + mxEvent.addListener(removeLink, 'click', mxUtils.bind(this, function(evt) + { + this.graph.setLinkForCell(this.state.cell, null); + mxEvent.consume(evt); + })); + } + } + + if (links != null) + { + for (var i = 0; i < links.length; i++) + { + var div = document.createElement('div'); + div.style.marginTop = (link != null || i > 0) ? '6px' : '0px'; + div.appendChild(this.graph.createLinkForHint( + links[i].getAttribute('href'), + mxUtils.getTextContent(links[i]))); + + this.linkHint.appendChild(div); + } + } + } + }; + + mxEdgeHandler.prototype.updateLinkHint = mxVertexHandler.prototype.updateLinkHint; + + var edgeHandlerInit = mxEdgeHandler.prototype.init; + mxEdgeHandler.prototype.init = function() + { + edgeHandlerInit.apply(this, arguments); + + // Disables connection points + this.constraintHandler.isEnabled = mxUtils.bind(this, function() + { + return this.state.view.graph.connectionHandler.isEnabled(); + }); + + var update = mxUtils.bind(this, function() + { + if (this.linkHint != null) + { + this.linkHint.style.display = (this.graph.getSelectionCount() == 1) ? '' : 'none'; + } + + if (this.labelShape != null) + { + this.labelShape.node.style.display = (this.graph.isEnabled() && this.graph.getSelectionCount() < this.graph.graphHandler.maxCells) ? '' : 'none'; + } + }); + + this.selectionHandler = mxUtils.bind(this, function(sender, evt) + { + update(); + }); + + this.graph.getSelectionModel().addListener(mxEvent.CHANGE, this.selectionHandler); + + this.changeHandler = mxUtils.bind(this, function(sender, evt) + { + this.updateLinkHint(this.graph.getLinkForCell(this.state.cell), + this.graph.getLinksForState(this.state)); + update(); + this.redrawHandles(); + }); + + this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler); + + var link = this.graph.getLinkForCell(this.state.cell); + var links = this.graph.getLinksForState(this.state); + + if (link != null || (links != null && links.length > 0)) + { + this.updateLinkHint(link, links); + this.redrawHandles(); + } + }; + + // Disables connection points + var connectionHandlerInit = mxConnectionHandler.prototype.init; + + mxConnectionHandler.prototype.init = function() + { + connectionHandlerInit.apply(this, arguments); + + this.constraintHandler.isEnabled = mxUtils.bind(this, function() + { + return this.graph.connectionHandler.isEnabled(); + }); + }; + + var vertexHandlerRedrawHandles = mxVertexHandler.prototype.redrawHandles; + mxVertexHandler.prototype.redrawHandles = function() + { + vertexHandlerRedrawHandles.apply(this); + + if (this.state != null && this.linkHint != null) + { + var c = new mxPoint(this.state.getCenterX(), this.state.getCenterY()); + var tmp = new mxRectangle(this.state.x, this.state.y - 22, this.state.width + 24, this.state.height + 22); + var bb = mxUtils.getBoundingBox(tmp, this.state.style[mxConstants.STYLE_ROTATION] || '0', c); + var rs = (bb != null) ? mxUtils.getBoundingBox(this.state, + this.state.style[mxConstants.STYLE_ROTATION] || '0') : this.state; + var tb = (this.state.text != null) ? this.state.text.boundingBox : null; + + if (bb == null) + { + bb = this.state; + } + + var b = bb.y + bb.height; + + if (tb != null) + { + b = Math.max(b, tb.y + tb.height); + } + + this.linkHint.style.left = Math.max(0, Math.round(rs.x + (rs.width - this.linkHint.clientWidth) / 2)) + 'px'; + this.linkHint.style.top = Math.round(b + this.verticalOffset / 2 + 6 + + this.state.view.graph.tolerance) + 'px'; + } + }; + + + var vertexHandlerReset = mxVertexHandler.prototype.reset; + mxVertexHandler.prototype.reset = function() + { + vertexHandlerReset.apply(this, arguments); + + // Shows rotation handle only if one vertex is selected + if (this.rotationShape != null && this.rotationShape.node != null) + { + this.rotationShape.node.style.display = (this.graph.getSelectionCount() == 1) ? '' : 'none'; + } + }; + + var vertexHandlerDestroy = mxVertexHandler.prototype.destroy; + mxVertexHandler.prototype.destroy = function() + { + vertexHandlerDestroy.apply(this, arguments); + + if (this.linkHint != null) + { + this.linkHint.parentNode.removeChild(this.linkHint); + this.linkHint = null; + } + + if (this.selectionHandler != null) + { + this.graph.getSelectionModel().removeListener(this.selectionHandler); + this.selectionHandler = null; + } + + if (this.changeHandler != null) + { + this.graph.getModel().removeListener(this.changeHandler); + this.changeHandler = null; + } + + if (this.editingHandler != null) + { + this.graph.removeListener(this.editingHandler); + this.editingHandler = null; + } + }; + + var edgeHandlerRedrawHandles = mxEdgeHandler.prototype.redrawHandles; + mxEdgeHandler.prototype.redrawHandles = function() + { + // Workaround for special case where handler + // is reset before this which leads to a NPE + if (this.marker != null) + { + edgeHandlerRedrawHandles.apply(this); + + if (this.state != null && this.linkHint != null) + { + var b = this.state; + + if (this.state.text != null && this.state.text.bounds != null) + { + b = new mxRectangle(b.x, b.y, b.width, b.height); + b.add(this.state.text.bounds); + } + + this.linkHint.style.left = Math.max(0, Math.round(b.x + (b.width - this.linkHint.clientWidth) / 2)) + 'px'; + this.linkHint.style.top = Math.round(b.y + b.height + 6 + this.state.view.graph.tolerance) + 'px'; + } + } + }; + + var edgeHandlerReset = mxEdgeHandler.prototype.reset; + mxEdgeHandler.prototype.reset = function() + { + edgeHandlerReset.apply(this, arguments); + + if (this.linkHint != null) + { + this.linkHint.style.visibility = ''; + } + }; + + var edgeHandlerDestroy = mxEdgeHandler.prototype.destroy; + mxEdgeHandler.prototype.destroy = function() + { + edgeHandlerDestroy.apply(this, arguments); + + if (this.linkHint != null) + { + this.linkHint.parentNode.removeChild(this.linkHint); + this.linkHint = null; + } + + if (this.selectionHandler != null) + { + this.graph.getSelectionModel().removeListener(this.selectionHandler); + this.selectionHandler = null; + } + + if (this.changeHandler != null) + { + this.graph.getModel().removeListener(this.changeHandler); + this.changeHandler = null; + } + }; + })(); +} diff --git a/media/grapheditor/js/Init.js b/media/grapheditor/js/Init.js new file mode 100644 index 0000000000..32ab10b9ac --- /dev/null +++ b/media/grapheditor/js/Init.js @@ -0,0 +1,29 @@ +// urlParams is null when used for embedding +window.urlParams = window.urlParams || {}; + +// Public global variables +window.MAX_REQUEST_SIZE = window.MAX_REQUEST_SIZE || 10485760; +window.MAX_AREA = window.MAX_AREA || 15000 * 15000; + +// URLs for save and export +window.EXPORT_URL = window.EXPORT_URL || '/export'; +window.SAVE_URL = window.SAVE_URL || '/save'; +window.OPEN_URL = window.OPEN_URL || '/open'; +window.RESOURCES_PATH = window.RESOURCES_PATH || 'resources'; +window.RESOURCE_BASE = window.RESOURCE_BASE || window.RESOURCES_PATH + '/grapheditor'; +window.STENCIL_PATH = window.STENCIL_PATH || 'stencils'; +window.IMAGE_PATH = window.IMAGE_PATH || 'images'; +window.STYLE_PATH = window.STYLE_PATH || 'styles'; +window.CSS_PATH = window.CSS_PATH || 'styles'; +window.OPEN_FORM = window.OPEN_FORM || 'open.html'; + +// Sets the base path, the UI language via URL param and configures the +// supported languages to avoid 404s. The loading of all core language +// resources is disabled as all required resources are in grapheditor. +// properties. Note that in this example the loading of two resource +// files (the special bundle and the default bundle) is disabled to +// save a GET request. This requires that all resources be present in +// each properties file since only one file is loaded. +window.mxBasePath = window.mxBasePath || '../../../src'; +window.mxLanguage = window.mxLanguage || urlParams['lang']; +window.mxLanguages = window.mxLanguages || ['de']; diff --git a/media/grapheditor/js/Menus.js b/media/grapheditor/js/Menus.js new file mode 100644 index 0000000000..9b0ac24e04 --- /dev/null +++ b/media/grapheditor/js/Menus.js @@ -0,0 +1,1315 @@ +/** + * Copyright (c) 2006-2012, JGraph Ltd + */ +/** + * Constructs a new graph editor + */ +Menus = function(editorUi) +{ + this.editorUi = editorUi; + this.menus = new Object(); + this.init(); + + // Pre-fetches checkmark image + if (!mxClient.IS_SVG) + { + new Image().src = this.checkmarkImage; + } +}; + +/** + * Sets the default font family. + */ +Menus.prototype.defaultFont = 'Helvetica'; + +/** + * Sets the default font size. + */ +Menus.prototype.defaultFontSize = '12'; + +/** + * Sets the default font size. + */ +Menus.prototype.defaultMenuItems = ['file', 'edit', 'view', 'arrange', 'extras', 'help']; + +/** + * Adds the label menu items to the given menu and parent. + */ +Menus.prototype.defaultFonts = ['Helvetica', 'Verdana', 'Times New Roman', 'Garamond', 'Comic Sans MS', + 'Courier New', 'Georgia', 'Lucida Console', 'Tahoma']; + +/** + * Adds the label menu items to the given menu and parent. + */ +Menus.prototype.init = function() +{ + var graph = this.editorUi.editor.graph; + var isGraphEnabled = mxUtils.bind(graph, graph.isEnabled); + + this.customFonts = []; + this.customFontSizes = []; + + this.put('fontFamily', new Menu(mxUtils.bind(this, function(menu, parent) + { + var addItem = mxUtils.bind(this, function(fontname) + { + var tr = this.styleChange(menu, fontname, [mxConstants.STYLE_FONTFAMILY], [fontname], null, parent, function() + { + document.execCommand('fontname', false, fontname); + }, function() + { + graph.updateLabelElements(graph.getSelectionCells(), function(elt) + { + elt.removeAttribute('face'); + elt.style.fontFamily = null; + + if (elt.nodeName == 'PRE') + { + graph.replaceElement(elt, 'div'); + } + }); + }); + tr.firstChild.nextSibling.style.fontFamily = fontname; + }); + + for (var i = 0; i < this.defaultFonts.length; i++) + { + addItem(this.defaultFonts[i]); + } + + menu.addSeparator(parent); + + if (this.customFonts.length > 0) + { + for (var i = 0; i < this.customFonts.length; i++) + { + addItem(this.customFonts[i]); + } + + menu.addSeparator(parent); + + menu.addItem(mxResources.get('reset'), null, mxUtils.bind(this, function() + { + this.customFonts = []; + }), parent); + + menu.addSeparator(parent); + } + + this.promptChange(menu, mxResources.get('custom') + '...', '', mxConstants.DEFAULT_FONTFAMILY, mxConstants.STYLE_FONTFAMILY, parent, true, mxUtils.bind(this, function(newValue) + { + this.customFonts.push(newValue); + })); + }))); + this.put('formatBlock', new Menu(mxUtils.bind(this, function(menu, parent) + { + function addItem(label, tag) + { + return menu.addItem(label, null, mxUtils.bind(this, function() + { + // TODO: Check if visible + graph.cellEditor.textarea.focus(); + document.execCommand('formatBlock', false, '<' + tag + '>'); + }), parent); + }; + + addItem(mxResources.get('normal'), 'p'); + + addItem('', 'h1').firstChild.nextSibling.innerHTML = '

' + mxResources.get('heading') + ' 1

'; + addItem('', 'h2').firstChild.nextSibling.innerHTML = '

' + mxResources.get('heading') + ' 2

'; + addItem('', 'h3').firstChild.nextSibling.innerHTML = '

' + mxResources.get('heading') + ' 3

'; + addItem('', 'h4').firstChild.nextSibling.innerHTML = '

' + mxResources.get('heading') + ' 4

'; + addItem('', 'h5').firstChild.nextSibling.innerHTML = '
' + mxResources.get('heading') + ' 5
'; + addItem('', 'h6').firstChild.nextSibling.innerHTML = '
' + mxResources.get('heading') + ' 6
'; + + addItem('', 'pre').firstChild.nextSibling.innerHTML = '
' + mxResources.get('formatted') + '
'; + addItem('', 'blockquote').firstChild.nextSibling.innerHTML = '
' + mxResources.get('blockquote') + '
'; + }))); + this.put('fontSize', new Menu(mxUtils.bind(this, function(menu, parent) + { + var sizes = [6, 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 72]; + + var addItem = mxUtils.bind(this, function(fontsize) + { + this.styleChange(menu, fontsize, [mxConstants.STYLE_FONTSIZE], [fontsize], null, parent, function() + { + // Creates an element with arbitrary size 3 + document.execCommand('fontSize', false, '3'); + + // Changes the css font size of the first font element inside the in-place editor with size 3 + // hopefully the above element that we've just created. LATER: Check for new element using + // previous result of getElementsByTagName (see other actions) + var elts = graph.cellEditor.textarea.getElementsByTagName('font'); + + for (var i = 0; i < elts.length; i++) + { + if (elts[i].getAttribute('size') == '3') + { + elts[i].removeAttribute('size'); + elts[i].style.fontSize = fontsize + 'px'; + + break; + } + } + }); + }); + + for (var i = 0; i < sizes.length; i++) + { + addItem(sizes[i]); + } + + menu.addSeparator(parent); + + if (this.customFontSizes.length > 0) + { + for (var i = 0; i < this.customFontSizes.length; i++) + { + addItem(this.customFontSizes[i]); + } + + menu.addSeparator(parent); + + menu.addItem(mxResources.get('reset'), null, mxUtils.bind(this, function() + { + this.customFontSizes = []; + }), parent); + + menu.addSeparator(parent); + } + + this.promptChange(menu, mxResources.get('custom') + '...', '(pt)', '12', mxConstants.STYLE_FONTSIZE, parent, true, mxUtils.bind(this, function(newValue) + { + this.customFontSizes.push(newValue); + })); + }))); + this.put('direction', new Menu(mxUtils.bind(this, function(menu, parent) + { + menu.addItem(mxResources.get('flipH'), null, function() { graph.toggleCellStyles(mxConstants.STYLE_FLIPH, false); }, parent); + menu.addItem(mxResources.get('flipV'), null, function() { graph.toggleCellStyles(mxConstants.STYLE_FLIPV, false); }, parent); + this.addMenuItems(menu, ['-', 'rotation'], parent); + }))); + this.put('align', new Menu(mxUtils.bind(this, function(menu, parent) + { + menu.addItem(mxResources.get('leftAlign'), null, function() { graph.alignCells(mxConstants.ALIGN_LEFT); }, parent); + menu.addItem(mxResources.get('center'), null, function() { graph.alignCells(mxConstants.ALIGN_CENTER); }, parent); + menu.addItem(mxResources.get('rightAlign'), null, function() { graph.alignCells(mxConstants.ALIGN_RIGHT); }, parent); + menu.addSeparator(parent); + menu.addItem(mxResources.get('topAlign'), null, function() { graph.alignCells(mxConstants.ALIGN_TOP); }, parent); + menu.addItem(mxResources.get('middle'), null, function() { graph.alignCells(mxConstants.ALIGN_MIDDLE); }, parent); + menu.addItem(mxResources.get('bottomAlign'), null, function() { graph.alignCells(mxConstants.ALIGN_BOTTOM); }, parent); + }))); + this.put('distribute', new Menu(mxUtils.bind(this, function(menu, parent) + { + menu.addItem(mxResources.get('horizontal'), null, function() { graph.distributeCells(true); }, parent); + menu.addItem(mxResources.get('vertical'), null, function() { graph.distributeCells(false); }, parent); + }))); + this.put('layout', new Menu(mxUtils.bind(this, function(menu, parent) + { + var promptSpacing = mxUtils.bind(this, function(defaultValue, fn) + { + var dlg = new FilenameDialog(this.editorUi, defaultValue, mxResources.get('apply'), function(newValue) + { + fn(parseFloat(newValue)); + }, mxResources.get('spacing')); + this.editorUi.showDialog(dlg.container, 300, 80, true, true); + dlg.init(); + }); + + menu.addItem(mxResources.get('horizontalFlow'), null, mxUtils.bind(this, function() + { + var layout = new mxHierarchicalLayout(graph, mxConstants.DIRECTION_WEST); + + this.editorUi.executeLayout(function() + { + var selectionCells = graph.getSelectionCells(); + layout.execute(graph.getDefaultParent(), selectionCells.length == 0 ? null : selectionCells); + }, true); + }), parent); + menu.addItem(mxResources.get('verticalFlow'), null, mxUtils.bind(this, function() + { + var layout = new mxHierarchicalLayout(graph, mxConstants.DIRECTION_NORTH); + + this.editorUi.executeLayout(function() + { + var selectionCells = graph.getSelectionCells(); + layout.execute(graph.getDefaultParent(), selectionCells.length == 0 ? null : selectionCells); + }, true); + }), parent); + menu.addSeparator(parent); + menu.addItem(mxResources.get('horizontalTree'), null, mxUtils.bind(this, function() + { + var tmp = graph.getSelectionCell(); + var roots = null; + + if (tmp == null || graph.getModel().getChildCount(tmp) == 0) + { + if (graph.getModel().getEdgeCount(tmp) == 0) + { + roots = graph.findTreeRoots(graph.getDefaultParent()); + } + } + else + { + roots = graph.findTreeRoots(tmp); + } + + if (roots != null && roots.length > 0) + { + tmp = roots[0]; + } + + if (tmp != null) + { + var layout = new mxCompactTreeLayout(graph, true); + layout.edgeRouting = false; + layout.levelDistance = 30; + + promptSpacing(layout.levelDistance, mxUtils.bind(this, function(newValue) + { + layout.levelDistance = newValue; + + this.editorUi.executeLayout(function() + { + layout.execute(graph.getDefaultParent(), tmp); + }, true); + })); + } + }), parent); + menu.addItem(mxResources.get('verticalTree'), null, mxUtils.bind(this, function() + { + var tmp = graph.getSelectionCell(); + var roots = null; + + if (tmp == null || graph.getModel().getChildCount(tmp) == 0) + { + if (graph.getModel().getEdgeCount(tmp) == 0) + { + roots = graph.findTreeRoots(graph.getDefaultParent()); + } + } + else + { + roots = graph.findTreeRoots(tmp); + } + + if (roots != null && roots.length > 0) + { + tmp = roots[0]; + } + + if (tmp != null) + { + var layout = new mxCompactTreeLayout(graph, false); + layout.edgeRouting = false; + layout.levelDistance = 30; + + promptSpacing(layout.levelDistance, mxUtils.bind(this, function(newValue) + { + layout.levelDistance = newValue; + + this.editorUi.executeLayout(function() + { + layout.execute(graph.getDefaultParent(), tmp); + }, true); + })); + } + }), parent); + menu.addItem(mxResources.get('radialTree'), null, mxUtils.bind(this, function() + { + var tmp = graph.getSelectionCell(); + var roots = null; + + if (tmp == null || graph.getModel().getChildCount(tmp) == 0) + { + if (graph.getModel().getEdgeCount(tmp) == 0) + { + roots = graph.findTreeRoots(graph.getDefaultParent()); + } + } + else + { + roots = graph.findTreeRoots(tmp); + } + + if (roots != null && roots.length > 0) + { + tmp = roots[0]; + } + + if (tmp != null) + { + var layout = new mxRadialTreeLayout(graph, false); + layout.levelDistance = 80; + layout.autoRadius = true; + + promptSpacing(layout.levelDistance, mxUtils.bind(this, function(newValue) + { + layout.levelDistance = newValue; + + this.editorUi.executeLayout(function() + { + layout.execute(graph.getDefaultParent(), tmp); + + if (!graph.isSelectionEmpty()) + { + tmp = graph.getModel().getParent(tmp); + + if (graph.getModel().isVertex(tmp)) + { + graph.updateGroupBounds([tmp], graph.gridSize * 2, true); + } + } + }, true); + })); + } + }), parent); + menu.addSeparator(parent); + menu.addItem(mxResources.get('organic'), null, mxUtils.bind(this, function() + { + var layout = new mxFastOrganicLayout(graph); + + promptSpacing(layout.forceConstant, mxUtils.bind(this, function(newValue) + { + layout.forceConstant = newValue; + + this.editorUi.executeLayout(function() + { + var tmp = graph.getSelectionCell(); + + if (tmp == null || graph.getModel().getChildCount(tmp) == 0) + { + tmp = graph.getDefaultParent(); + } + + layout.execute(tmp); + + if (graph.getModel().isVertex(tmp)) + { + graph.updateGroupBounds([tmp], graph.gridSize * 2, true); + } + }, true); + })); + }), parent); + menu.addItem(mxResources.get('circle'), null, mxUtils.bind(this, function() + { + var layout = new mxCircleLayout(graph); + + this.editorUi.executeLayout(function() + { + var tmp = graph.getSelectionCell(); + + if (tmp == null || graph.getModel().getChildCount(tmp) == 0) + { + tmp = graph.getDefaultParent(); + } + + layout.execute(tmp); + + if (graph.getModel().isVertex(tmp)) + { + graph.updateGroupBounds([tmp], graph.gridSize * 2, true); + } + }, true); + }), parent); + }))); + this.put('navigation', new Menu(mxUtils.bind(this, function(menu, parent) + { + this.addMenuItems(menu, ['home', '-', 'exitGroup', 'enterGroup', '-', 'expand', 'collapse', '-', 'collapsible'], parent); + }))); + this.put('arrange', new Menu(mxUtils.bind(this, function(menu, parent) + { + this.addMenuItems(menu, ['toFront', 'toBack', '-'], parent); + this.addSubmenu('direction', menu, parent); + this.addMenuItems(menu, ['turn', '-'], parent); + this.addSubmenu('align', menu, parent); + this.addSubmenu('distribute', menu, parent); + menu.addSeparator(parent); + this.addSubmenu('navigation', menu, parent); + this.addSubmenu('insert', menu, parent); + this.addSubmenu('layout', menu, parent); + this.addMenuItems(menu, ['-', 'group', 'ungroup', 'removeFromGroup', '-', 'clearWaypoints', 'autosize'], parent); + }))).isEnabled = isGraphEnabled; + this.put('insert', new Menu(mxUtils.bind(this, function(menu, parent) + { + this.addMenuItems(menu, ['insertLink', 'insertImage'], parent); + }))); + this.put('view', new Menu(mxUtils.bind(this, function(menu, parent) + { + this.addMenuItems(menu, ((this.editorUi.format != null) ? ['formatPanel'] : []). + concat(['outline', 'layers', '-', 'pageView', 'pageScale', '-', 'scrollbars', 'tooltips', '-', + 'grid', 'guides', '-', 'connectionArrows', 'connectionPoints', '-', + 'resetView', 'zoomIn', 'zoomOut'], parent)); + }))); + // Two special dropdowns that are only used in the toolbar + this.put('viewPanels', new Menu(mxUtils.bind(this, function(menu, parent) + { + if (this.editorUi.format != null) + { + this.addMenuItems(menu, ['formatPanel'], parent); + } + + this.addMenuItems(menu, ['outline', 'layers'], parent); + }))); + this.put('viewZoom', new Menu(mxUtils.bind(this, function(menu, parent) + { + this.addMenuItems(menu, ['resetView', '-'], parent); + var scales = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4]; + + for (var i = 0; i < scales.length; i++) + { + (function(scale) + { + menu.addItem((scale * 100) + '%', null, function() + { + graph.zoomTo(scale); + }, parent); + })(scales[i]); + } + + this.addMenuItems(menu, ['-', 'fitWindow', 'fitPageWidth', 'fitPage', 'fitTwoPages', '-', 'customZoom'], parent); + }))); + this.put('file', new Menu(mxUtils.bind(this, function(menu, parent) + { + this.addMenuItems(menu, ['new', 'open', '-', 'save', 'saveAs', '-', 'import', 'export', '-', 'pageSetup', 'print'], parent); + }))); + this.put('edit', new Menu(mxUtils.bind(this, function(menu, parent) + { + this.addMenuItems(menu, ['undo', 'redo', '-', 'cut', 'copy', 'paste', 'delete', '-', 'duplicate', '-', + 'editData', 'editTooltip', 'editStyle', '-', 'edit', '-', 'editLink', 'openLink', '-', + 'selectVertices', 'selectEdges', 'selectAll', 'selectNone', '-', 'lockUnlock']); + }))); + this.put('extras', new Menu(mxUtils.bind(this, function(menu, parent) + { + this.addMenuItems(menu, ['copyConnect', 'collapseExpand', '-', 'editDiagram']); + }))); + this.put('help', new Menu(mxUtils.bind(this, function(menu, parent) + { + this.addMenuItems(menu, ['help', '-', 'about']); + }))); +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +Menus.prototype.put = function(name, menu) +{ + this.menus[name] = menu; + + return menu; +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +Menus.prototype.get = function(name) +{ + return this.menus[name]; +}; + +/** + * Adds the given submenu. + */ +Menus.prototype.addSubmenu = function(name, menu, parent, label) +{ + var entry = this.get(name); + + if (entry != null) + { + var enabled = entry.isEnabled(); + + if (menu.showDisabled || enabled) + { + var submenu = menu.addItem(label || mxResources.get(name), null, null, parent, null, enabled); + this.addMenu(name, menu, submenu); + } + } +}; + +/** + * Adds the label menu items to the given menu and parent. + */ +Menus.prototype.addMenu = function(name, popupMenu, parent) +{ + var menu = this.get(name); + + if (menu != null && (popupMenu.showDisabled || menu.isEnabled())) + { + this.get(name).execute(popupMenu, parent); + } +}; + +/** + * Adds a menu item to insert a table. + */ +Menus.prototype.addInsertTableItem = function(menu) +{ + // KNOWN: Does not work in IE8 standards and quirks + var graph = this.editorUi.editor.graph; + + function createTable(rows, cols) + { + var html = ['']; + + for (var i = 0; i < rows; i++) + { + html.push(''); + + for (var j = 0; j < cols; j++) + { + html.push(''); + } + + html.push(''); + } + + html.push('

'); + + return html.join(''); + }; + + // Show table size dialog + var elt2 = menu.addItem('', null, mxUtils.bind(this, function(evt) + { + var td = graph.getParentByName(mxEvent.getSource(evt), 'TD'); + + if (td != null) + { + var row2 = graph.getParentByName(td, 'TR'); + + // To find the new link, we create a list of all existing links first + // LATER: Refactor for reuse with code for finding inserted image below + var tmp = graph.cellEditor.textarea.getElementsByTagName('table'); + var oldTables = []; + + for (var i = 0; i < tmp.length; i++) + { + oldTables.push(tmp[i]); + } + + // Finding the new table will work with insertHTML, but IE does not support that + graph.container.focus(); + graph.pasteHtmlAtCaret(createTable(row2.sectionRowIndex + 1, td.cellIndex + 1)); + + // Moves cursor to first table cell + var newTables = graph.cellEditor.textarea.getElementsByTagName('table'); + + if (newTables.length == oldTables.length + 1) + { + // Inverse order in favor of appended tables + for (var i = newTables.length - 1; i >= 0; i--) + { + if (i == 0 || newTables[i] != oldTables[i - 1]) + { + graph.selectNode(newTables[i].rows[0].cells[0]); + break; + } + } + } + } + })); + + // Quirks mode does not add cell padding if cell is empty, needs good old spacer solution + var quirksCellHtml = ''; + + function createPicker(rows, cols) + { + var table2 = document.createElement('table'); + table2.setAttribute('border', '1'); + table2.style.borderCollapse = 'collapse'; + + if (!mxClient.IS_QUIRKS) + { + table2.setAttribute('cellPadding', '8'); + } + + for (var i = 0; i < rows; i++) + { + var row = table2.insertRow(i); + + for (var j = 0; j < cols; j++) + { + var cell = row.insertCell(-1); + + if (mxClient.IS_QUIRKS) + { + cell.innerHTML = quirksCellHtml; + } + } + } + + return table2; + }; + + function extendPicker(picker, rows, cols) + { + for (var i = picker.rows.length; i < rows; i++) + { + var row = picker.insertRow(i); + + for (var j = 0; j < picker.rows[0].cells.length; j++) + { + var cell = row.insertCell(-1); + + if (mxClient.IS_QUIRKS) + { + cell.innerHTML = quirksCellHtml; + } + } + } + + for (var i = 0; i < picker.rows.length; i++) + { + var row = picker.rows[i]; + + for (var j = row.cells.length; j < cols; j++) + { + var cell = row.insertCell(-1); + + if (mxClient.IS_QUIRKS) + { + cell.innerHTML = quirksCellHtml; + } + } + } + }; + + elt2.firstChild.innerHTML = ''; + var picker = createPicker(5, 5); + elt2.firstChild.appendChild(picker); + + var label = document.createElement('div'); + label.style.padding = '4px'; + label.style.fontSize = Menus.prototype.defaultFontSize + 'px'; + label.innerHTML = '1x1'; + elt2.firstChild.appendChild(label); + + mxEvent.addListener(picker, 'mouseover', function(e) + { + var td = graph.getParentByName(mxEvent.getSource(e), 'TD'); + + if (td != null) + { + var row2 = graph.getParentByName(td, 'TR'); + extendPicker(picker, Math.min(20, row2.sectionRowIndex + 2), Math.min(20, td.cellIndex + 2)); + label.innerHTML = (td.cellIndex + 1) + 'x' + (row2.sectionRowIndex + 1); + + for (var i = 0; i < picker.rows.length; i++) + { + var r = picker.rows[i]; + + for (var j = 0; j < r.cells.length; j++) + { + var cell = r.cells[j]; + + if (i <= row2.sectionRowIndex && j <= td.cellIndex) + { + cell.style.backgroundColor = 'blue'; + } + else + { + cell.style.backgroundColor = 'white'; + } + } + } + + mxEvent.consume(e); + } + }); +}; + +/** + * Adds a style change item to the given menu. + */ +Menus.prototype.edgeStyleChange = function(menu, label, keys, values, sprite, parent, reset) +{ + return menu.addItem(label, null, mxUtils.bind(this, function() + { + var graph = this.editorUi.editor.graph; + graph.stopEditing(false); + + graph.getModel().beginUpdate(); + try + { + var cells = graph.getSelectionCells(); + var edges = []; + + for (var i = 0; i < cells.length; i++) + { + var cell = cells[i]; + + if (graph.getModel().isEdge(cell)) + { + if (reset) + { + var geo = graph.getCellGeometry(cell); + + // Resets all edge points + if (geo != null) + { + geo = geo.clone(); + geo.points = null; + graph.getModel().setGeometry(cell, geo); + } + } + + for (var j = 0; j < keys.length; j++) + { + graph.setCellStyles(keys[j], values[j], [cell]); + } + + edges.push(cell); + } + } + + this.editorUi.fireEvent(new mxEventObject('styleChanged', 'keys', keys, + 'values', values, 'cells', edges)); + } + finally + { + graph.getModel().endUpdate(); + } + }), parent, sprite); +}; + +/** + * Adds a style change item to the given menu. + */ +Menus.prototype.styleChange = function(menu, label, keys, values, sprite, parent, fn, post) +{ + var apply = this.createStyleChangeFunction(keys, values); + + return menu.addItem(label, null, mxUtils.bind(this, function() + { + var graph = this.editorUi.editor.graph; + + if (fn != null && graph.cellEditor.isContentEditing()) + { + fn(); + } + else + { + apply(post); + } + }), parent, sprite); +}; + +/** + * + */ +Menus.prototype.createStyleChangeFunction = function(keys, values) +{ + return mxUtils.bind(this, function(post) + { + var graph = this.editorUi.editor.graph; + graph.stopEditing(false); + + graph.getModel().beginUpdate(); + try + { + for (var i = 0; i < keys.length; i++) + { + graph.setCellStyles(keys[i], values[i]); + } + + if (post != null) + { + post(); + } + + this.editorUi.fireEvent(new mxEventObject('styleChanged', 'keys', keys, 'values', values, + 'cells', graph.getSelectionCells())); + } + finally + { + graph.getModel().endUpdate(); + } + }); +}; + +/** + * Adds a style change item with a prompt to the given menu. + */ +Menus.prototype.promptChange = function(menu, label, hint, defaultValue, key, parent, enabled, fn, sprite) +{ + return menu.addItem(label, null, mxUtils.bind(this, function() + { + var graph = this.editorUi.editor.graph; + var value = defaultValue; + var state = graph.getView().getState(graph.getSelectionCell()); + + if (state != null) + { + value = state.style[key] || value; + } + + var dlg = new FilenameDialog(this.editorUi, value, mxResources.get('apply'), mxUtils.bind(this, function(newValue) + { + if (newValue != null && newValue.length > 0) + { + graph.getModel().beginUpdate(); + try + { + graph.stopEditing(false); + graph.setCellStyles(key, newValue); + } + finally + { + graph.getModel().endUpdate(); + } + + if (fn != null) + { + fn(newValue); + } + } + }), mxResources.get('enterValue') + ((hint.length > 0) ? (' ' + hint) : '')); + this.editorUi.showDialog(dlg.container, 300, 80, true, true); + dlg.init(); + }), parent, sprite, enabled); +}; + +/** + * Adds a handler for showing a menu in the given element. + */ +Menus.prototype.pickColor = function(key, cmd, defaultValue) +{ + var graph = this.editorUi.editor.graph; + + if (cmd != null && graph.cellEditor.isContentEditing()) + { + // Saves and restores text selection for in-place editor + var selState = graph.cellEditor.saveSelection(); + + var dlg = new ColorDialog(this.editorUi, defaultValue || '000000', mxUtils.bind(this, function(color) + { + graph.cellEditor.restoreSelection(selState); + document.execCommand(cmd, false, (color != mxConstants.NONE) ? color : 'transparent'); + }), function() + { + graph.cellEditor.restoreSelection(selState); + }); + this.editorUi.showDialog(dlg.container, 230, 430, true, true); + dlg.init(); + } + else + { + if (this.colorDialog == null) + { + this.colorDialog = new ColorDialog(this.editorUi); + } + + this.colorDialog.currentColorKey = key; + var state = graph.getView().getState(graph.getSelectionCell()); + var color = 'none'; + + if (state != null) + { + color = state.style[key] || color; + } + + if (color == 'none') + { + color = 'ffffff'; + this.colorDialog.picker.fromString('ffffff'); + this.colorDialog.colorInput.value = 'none'; + } + else + { + this.colorDialog.picker.fromString(color); + } + + this.editorUi.showDialog(this.colorDialog.container, 230, 430, true, true); + this.colorDialog.init(); + } +}; + +/** + * Adds a handler for showing a menu in the given element. + */ +Menus.prototype.toggleStyle = function(key, defaultValue) +{ + var graph = this.editorUi.editor.graph; + var value = graph.toggleCellStyles(key, defaultValue); + this.editorUi.fireEvent(new mxEventObject('styleChanged', 'keys', [key], 'values', [value], + 'cells', graph.getSelectionCells())); +}; + +/** + * Creates the keyboard event handler for the current graph and history. + */ +Menus.prototype.addMenuItem = function(menu, key, parent, trigger, sprite, label) +{ + var action = this.editorUi.actions.get(key); + + if (action != null && (menu.showDisabled || action.isEnabled()) && action.visible) + { + var item = menu.addItem(label || action.label, null, function() + { + action.funct(trigger); + }, parent, sprite, action.isEnabled()); + + // Adds checkmark image + if (action.toggleAction && action.isSelected()) + { + menu.addCheckmark(item, Editor.checkmarkImage); + } + + this.addShortcut(item, action); + + return item; + } + + return null; +}; + +/** + * Adds a checkmark to the given menuitem. + */ +Menus.prototype.addShortcut = function(item, action) +{ + if (action.shortcut != null) + { + var td = item.firstChild.nextSibling.nextSibling; + var span = document.createElement('span'); + span.style.color = 'gray'; + mxUtils.write(span, action.shortcut); + td.appendChild(span); + } +}; + +/** + * Creates the keyboard event handler for the current graph and history. + */ +Menus.prototype.addMenuItems = function(menu, keys, parent, trigger, sprites) +{ + for (var i = 0; i < keys.length; i++) + { + if (keys[i] == '-') + { + menu.addSeparator(parent); + } + else + { + this.addMenuItem(menu, keys[i], parent, trigger, (sprites != null) ? sprites[i] : null); + } + } +}; + +/** + * Creates the keyboard event handler for the current graph and history. + */ +Menus.prototype.createPopupMenu = function(menu, cell, evt) +{ + var graph = this.editorUi.editor.graph; + menu.smartSeparators = true; + + if (graph.isSelectionEmpty()) + { + this.addMenuItems(menu, ['undo', 'redo', 'pasteHere'], null, evt); + } + else + { + this.addMenuItems(menu, ['delete', '-', 'cut', 'copy', '-', 'duplicate'], null, evt); + } + + if (!graph.isSelectionEmpty()) + { + if (graph.getSelectionCount() == 1) + { + this.addMenuItems(menu, ['setAsDefaultStyle'], null, evt); + } + + menu.addSeparator(); + + cell = graph.getSelectionCell(); + var state = graph.view.getState(cell); + + if (state != null) + { + var hasWaypoints = false; + this.addMenuItems(menu, ['toFront', 'toBack', '-'], null, evt); + + if (graph.getModel().isEdge(cell) && mxUtils.getValue(state.style, mxConstants.STYLE_EDGE, null) != 'entityRelationEdgeStyle' && + mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE, null) != 'arrow') + { + var handler = graph.selectionCellsHandler.getHandler(cell); + var isWaypoint = false; + + if (handler instanceof mxEdgeHandler && handler.bends != null && handler.bends.length > 2) + { + var index = handler.getHandleForEvent(graph.updateMouseEvent(new mxMouseEvent(evt))); + + // Configures removeWaypoint action before execution + // Using trigger parameter is cleaner but have to find waypoint here anyway. + var rmWaypointAction = this.editorUi.actions.get('removeWaypoint'); + rmWaypointAction.handler = handler; + rmWaypointAction.index = index; + + isWaypoint = index > 0 && index < handler.bends.length - 1; + } + + menu.addSeparator(); + this.addMenuItem(menu, 'turn', null, evt, null, mxResources.get('reverse')); + this.addMenuItems(menu, [(isWaypoint) ? 'removeWaypoint' : 'addWaypoint'], null, evt); + + // Adds reset waypoints option if waypoints exist + var geo = graph.getModel().getGeometry(cell); + hasWaypoints = geo != null && geo.points != null && geo.points.length > 0; + } + + if (graph.getSelectionCount() == 1 && (hasWaypoints || (graph.getModel().isVertex(cell) && + graph.getModel().getEdgeCount(cell) > 0))) + { + this.addMenuItems(menu, ['clearWaypoints'], null, evt); + } + + if (graph.getSelectionCount() > 1) + { + menu.addSeparator(); + this.addMenuItems(menu, ['group'], null, evt); + } + else if (graph.getSelectionCount() == 1 && !graph.getModel().isEdge(cell) && !graph.isSwimlane(cell) && + graph.getModel().getChildCount(cell) > 0) + { + menu.addSeparator(); + this.addMenuItems(menu, ['ungroup'], null, evt); + } + + if (graph.getSelectionCount() == 1) + { + menu.addSeparator(); + this.addMenuItems(menu, ['editData', 'editLink'], null, evt); + + // Shows edit image action if there is an image in the style + if (graph.getModel().isVertex(cell) && mxUtils.getValue(state.style, mxConstants.STYLE_IMAGE, null) != null) + { + menu.addSeparator(); + this.addMenuItem(menu, 'image', null, evt).firstChild.nextSibling.innerHTML = mxResources.get('editImage') + '...'; + } + } + } + } + else + { + this.addMenuItems(menu, ['-', 'selectVertices', 'selectEdges', + 'selectAll', '-', 'clearDefaultStyle'], null, evt); + } +}; + +/** + * Creates the keyboard event handler for the current graph and history. + */ +Menus.prototype.createMenubar = function(container) +{ + var menubar = new Menubar(this.editorUi, container); + var menus = this.defaultMenuItems; + + for (var i = 0; i < menus.length; i++) + { + (mxUtils.bind(this, function(menu) + { + var elt = menubar.addMenu(mxResources.get(menus[i]), mxUtils.bind(this, function() + { + // Allows extensions of menu.funct + menu.funct.apply(this, arguments); + })); + + this.menuCreated(menu, elt); + }))(this.get(menus[i])); + } + + return menubar; +}; + +/** + * Creates the keyboard event handler for the current graph and history. + */ +Menus.prototype.menuCreated = function(menu, elt, className) +{ + if (elt != null) + { + className = (className != null) ? className : 'geItem'; + + menu.addListener('stateChanged', function() + { + elt.enabled = menu.enabled; + + if (!menu.enabled) + { + elt.className = className + ' mxDisabled'; + + if (document.documentMode == 8) + { + elt.style.color = '#c3c3c3'; + } + } + else + { + elt.className = className; + + if (document.documentMode == 8) + { + elt.style.color = ''; + } + } + }); + } +}; + +/** + * Construcs a new menubar for the given editor. + */ +function Menubar(editorUi, container) +{ + this.editorUi = editorUi; + this.container = container; +}; + +/** + * Adds the menubar elements. + */ +Menubar.prototype.hideMenu = function() +{ + this.editorUi.hideCurrentMenu(); +}; + +/** + * Adds a submenu to this menubar. + */ +Menubar.prototype.addMenu = function(label, funct, before) +{ + var elt = document.createElement('a'); + elt.setAttribute('href', 'javascript:void(0);'); + elt.className = 'geItem'; + mxUtils.write(elt, label); + this.addMenuHandler(elt, funct); + + if (before != null) + { + this.container.insertBefore(elt, before); + } + else + { + this.container.appendChild(elt); + } + + return elt; +}; + +/** + * Adds a handler for showing a menu in the given element. + */ +Menubar.prototype.addMenuHandler = function(elt, funct) +{ + if (funct != null) + { + var show = true; + + var clickHandler = mxUtils.bind(this, function(evt) + { + if (show && elt.enabled == null || elt.enabled) + { + this.editorUi.editor.graph.popupMenuHandler.hideMenu(); + var menu = new mxPopupMenu(funct); + menu.div.className += ' geMenubarMenu'; + menu.smartSeparators = true; + menu.showDisabled = true; + menu.autoExpand = true; + + // Disables autoexpand and destroys menu when hidden + menu.hideMenu = mxUtils.bind(this, function() + { + mxPopupMenu.prototype.hideMenu.apply(menu, arguments); + this.editorUi.resetCurrentMenu(); + menu.destroy(); + }); + + var offset = mxUtils.getOffset(elt); + menu.popup(offset.x, offset.y + elt.offsetHeight, null, evt); + this.editorUi.setCurrentMenu(menu, elt); + } + + mxEvent.consume(evt); + }); + + // Shows menu automatically while in expanded state + mxEvent.addListener(elt, 'mousemove', mxUtils.bind(this, function(evt) + { + if (this.editorUi.currentMenu != null && this.editorUi.currentMenuElt != elt) + { + this.editorUi.hideCurrentMenu(); + clickHandler(evt); + } + })); + + // Hides menu if already showing + mxEvent.addListener(elt, 'mousedown', mxUtils.bind(this, function() + { + show = this.currentElt != elt; + })); + + mxEvent.addListener(elt, 'click', mxUtils.bind(this, function(evt) + { + clickHandler(evt); + show = true; + })); + } +}; + +/** + * Creates the keyboard event handler for the current graph and history. + */ +Menubar.prototype.destroy = function() +{ + // do nothing +}; + +/** + * Constructs a new action for the given parameters. + */ +function Menu(funct, enabled) +{ + mxEventSource.call(this); + this.funct = funct; + this.enabled = (enabled != null) ? enabled : true; +}; + +// Menu inherits from mxEventSource +mxUtils.extend(Menu, mxEventSource); + +/** + * Sets the enabled state of the action and fires a stateChanged event. + */ +Menu.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Sets the enabled state of the action and fires a stateChanged event. + */ +Menu.prototype.setEnabled = function(value) +{ + if (this.enabled != value) + { + this.enabled = value; + this.fireEvent(new mxEventObject('stateChanged')); + } +}; + +/** + * Sets the enabled state of the action and fires a stateChanged event. + */ +Menu.prototype.execute = function(menu, parent) +{ + this.funct(menu, parent); +}; + +/** + * "Installs" menus in EditorUi. + */ +EditorUi.prototype.createMenus = function() +{ + return new Menus(this); +}; diff --git a/media/grapheditor/js/Shapes.js b/media/grapheditor/js/Shapes.js new file mode 100644 index 0000000000..2427fa3522 --- /dev/null +++ b/media/grapheditor/js/Shapes.js @@ -0,0 +1,3965 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + */ + +/** + * Registers shapes. + */ +(function() +{ + // Cube Shape, supports size style + function CubeShape() + { + mxCylinder.call(this); + }; + mxUtils.extend(CubeShape, mxCylinder); + CubeShape.prototype.size = 20; + CubeShape.prototype.redrawPath = function(path, x, y, w, h, isForeground) + { + var s = Math.max(0, Math.min(w, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'size', this.size))))); + + if (isForeground) + { + path.moveTo(s, h); + path.lineTo(s, s); + path.lineTo(0, 0); + path.moveTo(s, s); + path.lineTo(w, s); + path.end(); + } + else + { + path.moveTo(0, 0); + path.lineTo(w - s, 0); + path.lineTo(w, s); + path.lineTo(w, h); + path.lineTo(s, h); + path.lineTo(0, h - s); + path.lineTo(0, 0); + path.close(); + path.end(); + } + }; + CubeShape.prototype.getLabelMargins = function(rect) + { + if (mxUtils.getValue(this.style, 'boundedLbl', false)) + { + var s = parseFloat(mxUtils.getValue(this.style, 'size', this.size)) * this.scale; + + return new mxRectangle(s, s, 0, 0); + } + + return null; + }; + + mxCellRenderer.registerShape('cube', CubeShape); + + var tan30 = Math.tan(mxUtils.toRadians(30)); + var tan30Dx = (0.5 - tan30) / 2; + + // Cube Shape, supports size style + function IsoRectangleShape() + { + mxActor.call(this); + }; + mxUtils.extend(IsoRectangleShape, mxActor); + IsoRectangleShape.prototype.size = 20; + IsoRectangleShape.prototype.redrawPath = function(path, x, y, w, h) + { + var m = Math.min(w, h / tan30); + + path.translate((w - m) / 2, (h - m) / 2 + m / 4); + path.moveTo(0, 0.25 * m); + path.lineTo(0.5 * m, m * tan30Dx); + path.lineTo(m, 0.25 * m); + path.lineTo(0.5 * m, (0.5 - tan30Dx) * m); + path.lineTo(0, 0.25 * m); + path.close(); + path.end(); + }; + + mxCellRenderer.registerShape('isoRectangle', IsoRectangleShape); + + // Cube Shape, supports size style + function IsoCubeShape() + { + mxCylinder.call(this); + }; + mxUtils.extend(IsoCubeShape, mxCylinder); + IsoCubeShape.prototype.size = 20; + IsoCubeShape.prototype.redrawPath = function(path, x, y, w, h, isForeground) + { + var m = Math.min(w, h / (0.5 + tan30)); + + if (isForeground) + { + path.moveTo(0, 0.25 * m); + path.lineTo(0.5 * m, (0.5 - tan30Dx) * m); + path.lineTo(m, 0.25 * m); + path.moveTo(0.5 * m, (0.5 - tan30Dx) * m); + path.lineTo(0.5 * m, (1 - tan30Dx) * m); + path.end(); + } + else + { + path.translate((w - m) / 2, (h - m) / 2); + path.moveTo(0, 0.25 * m); + path.lineTo(0.5 * m, m * tan30Dx); + path.lineTo(m, 0.25 * m); + path.lineTo(m, 0.75 * m); + path.lineTo(0.5 * m, (1 - tan30Dx) * m); + path.lineTo(0, 0.75 * m); + path.close(); + path.end(); + } + }; + + mxCellRenderer.registerShape('isoCube', IsoCubeShape); + + // DataStore Shape, supports size style + function DataStoreShape() + { + mxCylinder.call(this); + }; + mxUtils.extend(DataStoreShape, mxCylinder); + + DataStoreShape.prototype.redrawPath = function(c, x, y, w, h, isForeground) + { + var dy = Math.min(h / 2, Math.round(h / 8) + this.strokewidth - 1); + + if ((isForeground && this.fill != null) || (!isForeground && this.fill == null)) + { + c.moveTo(0, dy); + c.curveTo(0, 2 * dy, w, 2 * dy, w, dy); + + // Needs separate shapes for correct hit-detection + if (!isForeground) + { + c.stroke(); + c.begin(); + } + + c.translate(0, dy / 2); + c.moveTo(0, dy); + c.curveTo(0, 2 * dy, w, 2 * dy, w, dy); + + // Needs separate shapes for correct hit-detection + if (!isForeground) + { + c.stroke(); + c.begin(); + } + + c.translate(0, dy / 2); + c.moveTo(0, dy); + c.curveTo(0, 2 * dy, w, 2 * dy, w, dy); + + // Needs separate shapes for correct hit-detection + if (!isForeground) + { + c.stroke(); + c.begin(); + } + + c.translate(0, -dy); + } + + if (!isForeground) + { + c.moveTo(0, dy); + c.curveTo(0, -dy / 3, w, -dy / 3, w, dy); + c.lineTo(w, h - dy); + c.curveTo(w, h + dy / 3, 0, h + dy / 3, 0, h - dy); + c.close(); + } + }; + DataStoreShape.prototype.getLabelMargins = function(rect) + { + return new mxRectangle(0, 2.5 * Math.min(rect.height / 2, Math.round(rect.height / 8) + + this.strokewidth - 1) * this.scale, 0, 0); + } + + mxCellRenderer.registerShape('datastore', DataStoreShape); + + // Note Shape, supports size style + function NoteShape() + { + mxCylinder.call(this); + }; + mxUtils.extend(NoteShape, mxCylinder); + NoteShape.prototype.size = 30; + NoteShape.prototype.redrawPath = function(path, x, y, w, h, isForeground) + { + var s = Math.max(0, Math.min(w, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'size', this.size))))); + + if (isForeground) + { + path.moveTo(w - s, 0); + path.lineTo(w - s, s); + path.lineTo(w, s); + path.end(); + } + else + { + path.moveTo(0, 0); + path.lineTo(w - s, 0); + path.lineTo(w, s); + path.lineTo(w, h); + path.lineTo(0, h); + path.lineTo(0, 0); + path.close(); + path.end(); + } + }; + + mxCellRenderer.registerShape('note', NoteShape); + + // Note Shape, supports size style + function SwitchShape() + { + mxActor.call(this); + }; + mxUtils.extend(SwitchShape, mxActor); + SwitchShape.prototype.redrawPath = function(c, x, y, w, h) + { + var curve = 0.5; + c.moveTo(0, 0); + c.quadTo(w / 2, h * curve, w, 0); + c.quadTo(w * (1 - curve), h / 2, w, h); + c.quadTo(w / 2, h * (1 - curve), 0, h); + c.quadTo(w * curve, h / 2, 0, 0); + c.end(); + }; + + mxCellRenderer.registerShape('switch', SwitchShape); + + // Folder Shape, supports tabWidth, tabHeight styles + function FolderShape() + { + mxCylinder.call(this); + }; + mxUtils.extend(FolderShape, mxCylinder); + FolderShape.prototype.tabWidth = 60; + FolderShape.prototype.tabHeight = 20; + FolderShape.prototype.tabPosition = 'right'; + FolderShape.prototype.redrawPath = function(path, x, y, w, h, isForeground) + { + var dx = Math.max(0, Math.min(w, parseFloat(mxUtils.getValue(this.style, 'tabWidth', this.tabWidth)))); + var dy = Math.max(0, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'tabHeight', this.tabHeight)))); + var tp = mxUtils.getValue(this.style, 'tabPosition', this.tabPosition); + + if (isForeground) + { + if (tp == 'left') + { + path.moveTo(0, dy); + path.lineTo(dx, dy); + } + // Right is default + else + { + path.moveTo(w - dx, dy); + path.lineTo(w, dy); + } + + path.end(); + } + else + { + if (tp == 'left') + { + path.moveTo(0, 0); + path.lineTo(dx, 0); + path.lineTo(dx, dy); + path.lineTo(w, dy); + } + // Right is default + else + { + path.moveTo(0, dy); + path.lineTo(w - dx, dy); + path.lineTo(w - dx, 0); + path.lineTo(w, 0); + } + + path.lineTo(w, h); + path.lineTo(0, h); + path.lineTo(0, dy); + path.close(); + path.end(); + } + }; + + mxCellRenderer.registerShape('folder', FolderShape); + + // Card shape + function CardShape() + { + mxActor.call(this); + }; + mxUtils.extend(CardShape, mxActor); + CardShape.prototype.size = 30; + CardShape.prototype.isRoundable = function() + { + return true; + }; + CardShape.prototype.redrawPath = function(c, x, y, w, h) + { + var s = Math.max(0, Math.min(w, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'size', this.size))))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(s, 0), new mxPoint(w, 0), new mxPoint(w, h), new mxPoint(0, h), new mxPoint(0, s)], + this.isRounded, arcSize, true); + c.end(); + }; + + mxCellRenderer.registerShape('card', CardShape); + + // Tape shape + function TapeShape() + { + mxActor.call(this); + }; + mxUtils.extend(TapeShape, mxActor); + TapeShape.prototype.size = 0.4; + TapeShape.prototype.redrawPath = function(c, x, y, w, h) + { + var dy = h * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var fy = 1.4; + + c.moveTo(0, dy / 2); + c.quadTo(w / 4, dy * fy, w / 2, dy / 2); + c.quadTo(w * 3 / 4, dy * (1 - fy), w, dy / 2); + c.lineTo(w, h - dy / 2); + c.quadTo(w * 3 / 4, h - dy * fy, w / 2, h - dy / 2); + c.quadTo(w / 4, h - dy * (1 - fy), 0, h - dy / 2); + c.lineTo(0, dy / 2); + c.close(); + c.end(); + }; + + TapeShape.prototype.getLabelBounds = function(rect) + { + if (mxUtils.getValue(this.style, 'boundedLbl', false)) + { + var size = mxUtils.getValue(this.style, 'size', this.size); + var w = rect.width; + var h = rect.height; + + if (this.direction == null || + this.direction == mxConstants.DIRECTION_EAST || + this.direction == mxConstants.DIRECTION_WEST) + { + var dy = h * size; + + return new mxRectangle(rect.x, rect.y + dy, w, h - 2 * dy); + } + else + { + var dx = w * size; + + return new mxRectangle(rect.x + dx, rect.y, w - 2 * dx, h); + } + } + + return rect; + }; + + mxCellRenderer.registerShape('tape', TapeShape); + + // Document shape + function DocumentShape() + { + mxActor.call(this); + }; + mxUtils.extend(DocumentShape, mxActor); + DocumentShape.prototype.size = 0.3; + DocumentShape.prototype.getLabelMargins = function(rect) + { + if (mxUtils.getValue(this.style, 'boundedLbl', false)) + { + return new mxRectangle(0, 0, 0, parseFloat(mxUtils.getValue( + this.style, 'size', this.size)) * rect.height); + } + + return null; + }; + DocumentShape.prototype.redrawPath = function(c, x, y, w, h) + { + var dy = h * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var fy = 1.4; + + c.moveTo(0, 0); + c.lineTo(w, 0); + c.lineTo(w, h - dy / 2); + c.quadTo(w * 3 / 4, h - dy * fy, w / 2, h - dy / 2); + c.quadTo(w / 4, h - dy * (1 - fy), 0, h - dy / 2); + c.lineTo(0, dy / 2); + c.close(); + c.end(); + }; + + mxCellRenderer.registerShape('document', DocumentShape); + + var cylinderGetCylinderSize = mxCylinder.prototype.getCylinderSize; + + mxCylinder.prototype.getCylinderSize = function(x, y, w, h) + { + var size = mxUtils.getValue(this.style, 'size'); + + if (size != null) + { + return h * Math.max(0, Math.min(1, size)); + } + else + { + return cylinderGetCylinderSize.apply(this, arguments); + } + }; + + mxCylinder.prototype.getLabelMargins = function(rect) + { + if (mxUtils.getValue(this.style, 'boundedLbl', false)) + { + var size = mxUtils.getValue(this.style, 'size', 0.15) * 2; + + return new mxRectangle(0, Math.min(this.maxHeight * this.scale, rect.height * size), 0, 0); + } + + return null; + }; + + // Parallelogram shape + function ParallelogramShape() + { + mxActor.call(this); + }; + mxUtils.extend(ParallelogramShape, mxActor); + ParallelogramShape.prototype.size = 0.2; + ParallelogramShape.prototype.isRoundable = function() + { + return true; + }; + ParallelogramShape.prototype.redrawPath = function(c, x, y, w, h) + { + var dx = w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(0, h), new mxPoint(dx, 0), new mxPoint(w, 0), new mxPoint(w - dx, h)], + this.isRounded, arcSize, true); + c.end(); + }; + + mxCellRenderer.registerShape('parallelogram', ParallelogramShape); + + // Trapezoid shape + function TrapezoidShape() + { + mxActor.call(this); + }; + mxUtils.extend(TrapezoidShape, mxActor); + TrapezoidShape.prototype.size = 0.2; + TrapezoidShape.prototype.isRoundable = function() + { + return true; + }; + TrapezoidShape.prototype.redrawPath = function(c, x, y, w, h) + { + var dx = w * Math.max(0, Math.min(0.5, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(0, h), new mxPoint(dx, 0), new mxPoint(w - dx, 0), new mxPoint(w, h)], + this.isRounded, arcSize, true); + }; + + mxCellRenderer.registerShape('trapezoid', TrapezoidShape); + + // Curly Bracket shape + function CurlyBracketShape() + { + mxActor.call(this); + }; + mxUtils.extend(CurlyBracketShape, mxActor); + CurlyBracketShape.prototype.size = 0.5; + CurlyBracketShape.prototype.redrawPath = function(c, x, y, w, h) + { + c.setFillColor(null); + var s = w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(w, 0), new mxPoint(s, 0), new mxPoint(s, h / 2), + new mxPoint(0, h / 2), new mxPoint(s, h / 2), new mxPoint(s, h), + new mxPoint(w, h)], this.isRounded, arcSize, false); + c.end(); + }; + + mxCellRenderer.registerShape('curlyBracket', CurlyBracketShape); + + // Parallel marker shape + function ParallelMarkerShape() + { + mxActor.call(this); + }; + mxUtils.extend(ParallelMarkerShape, mxActor); + ParallelMarkerShape.prototype.redrawPath = function(c, x, y, w, h) + { + c.setStrokeWidth(1); + c.setFillColor(this.stroke); + var w2 = w / 5; + c.rect(0, 0, w2, h); + c.fillAndStroke(); + c.rect(2 * w2, 0, w2, h); + c.fillAndStroke(); + c.rect(4 * w2, 0, w2, h); + c.fillAndStroke(); + }; + + mxCellRenderer.registerShape('parallelMarker', ParallelMarkerShape); + + /** + * Adds handJiggle style (jiggle=n sets jiggle) + */ + function HandJiggle(canvas, defaultVariation) + { + this.canvas = canvas; + + // Avoids "spikes" in the output + this.canvas.setLineJoin('round'); + this.canvas.setLineCap('round'); + + this.defaultVariation = defaultVariation; + + this.originalLineTo = this.canvas.lineTo; + this.canvas.lineTo = mxUtils.bind(this, HandJiggle.prototype.lineTo); + + this.originalMoveTo = this.canvas.moveTo; + this.canvas.moveTo = mxUtils.bind(this, HandJiggle.prototype.moveTo); + + this.originalClose = this.canvas.close; + this.canvas.close = mxUtils.bind(this, HandJiggle.prototype.close); + + this.originalQuadTo = this.canvas.quadTo; + this.canvas.quadTo = mxUtils.bind(this, HandJiggle.prototype.quadTo); + + this.originalCurveTo = this.canvas.curveTo; + this.canvas.curveTo = mxUtils.bind(this, HandJiggle.prototype.curveTo); + + this.originalArcTo = this.canvas.arcTo; + this.canvas.arcTo = mxUtils.bind(this, HandJiggle.prototype.arcTo); + }; + + HandJiggle.prototype.moveTo = function(endX, endY) + { + this.originalMoveTo.apply(this.canvas, arguments); + this.lastX = endX; + this.lastY = endY; + this.firstX = endX; + this.firstY = endY; + }; + + HandJiggle.prototype.close = function() + { + if (this.firstX != null && this.firstY != null) + { + this.lineTo(this.firstX, this.firstY); + this.originalClose.apply(this.canvas, arguments); + } + + this.originalClose.apply(this.canvas, arguments); + }; + + HandJiggle.prototype.quadTo = function(x1, y1, x2, y2) + { + this.originalQuadTo.apply(this.canvas, arguments); + this.lastX = x2; + this.lastY = y2; + }; + + HandJiggle.prototype.curveTo = function(x1, y1, x2, y2, x3, y3) + { + this.originalCurveTo.apply(this.canvas, arguments); + this.lastX = x3; + this.lastY = y3; + }; + + HandJiggle.prototype.arcTo = function(rx, ry, angle, largeArcFlag, sweepFlag, x, y) + { + this.originalArcTo.apply(this.canvas, arguments); + this.lastX = x; + this.lastY = y; + }; + + HandJiggle.prototype.lineTo = function(endX, endY) + { + // LATER: Check why this.canvas.lastX cannot be used + if (this.lastX != null && this.lastY != null) + { + var dx = Math.abs(endX - this.lastX); + var dy = Math.abs(endY - this.lastY); + var dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 2) + { + this.originalLineTo.apply(this.canvas, arguments); + this.lastX = endX; + this.lastY = endY; + + return; + } + + var segs = Math.round(dist / 10); + var variation = this.defaultVariation; + + if (segs < 5) + { + segs = 5; + variation /= 3; + } + + function sign(x) + { + return typeof x === 'number' ? x ? x < 0 ? -1 : 1 : x === x ? 0 : NaN : NaN; + } + + var stepX = sign(endX - this.lastX) * dx / segs; + var stepY = sign(endY - this.lastY) * dy / segs; + + var fx = dx / dist; + var fy = dy / dist; + + for (var s = 0; s < segs; s++) + { + var x = stepX * s + this.lastX; + var y = stepY * s + this.lastY; + + var offset = (Math.random() - 0.5) * variation; + this.originalLineTo.call(this.canvas, x - offset * fy, y - offset * fx); + } + + this.originalLineTo.call(this.canvas, endX, endY); + this.lastX = endX; + this.lastY = endY; + } + else + { + this.originalLineTo.apply(this.canvas, arguments); + this.lastX = endX; + this.lastY = endY; + } + }; + + HandJiggle.prototype.destroy = function() + { + this.canvas.lineTo = this.originalLineTo; + this.canvas.moveTo = this.originalMoveTo; + this.canvas.close = this.originalClose; + this.canvas.quadTo = this.originalQuadTo; + this.canvas.curveTo = this.originalCurveTo; + this.canvas.arcTo = this.originalArcTo; + }; + + // Installs hand jiggle in all shapes + var mxShapePaint0 = mxShape.prototype.paint; + mxShape.prototype.defaultJiggle = 1.5; + mxShape.prototype.paint = function(c) + { + // NOTE: getValue does not return a boolean value so !('0') would return true here and below + if (this.style != null && mxUtils.getValue(this.style, 'comic', '0') != '0' && c.handHiggle == null) + { + c.handJiggle = new HandJiggle(c, mxUtils.getValue(this.style, 'jiggle', this.defaultJiggle)); + } + + mxShapePaint0.apply(this, arguments); + + if (c.handJiggle != null) + { + c.handJiggle.destroy(); + delete c.handJiggle; + } + }; + + // Sets default jiggle for diamond + mxRhombus.prototype.defaultJiggle = 2; + + /** + * Overrides to avoid call to rect + */ + var mxRectangleShapeIsHtmlAllowed0 = mxRectangleShape.prototype.isHtmlAllowed; + mxRectangleShape.prototype.isHtmlAllowed = function() + { + return (this.style == null || mxUtils.getValue(this.style, 'comic', '0') == '0') && + mxRectangleShapeIsHtmlAllowed0.apply(this, arguments); + }; + + var mxRectangleShapePaintBackground0 = mxRectangleShape.prototype.paintBackground; + mxRectangleShape.prototype.paintBackground = function(c, x, y, w, h) + { + if (c.handJiggle == null) + { + mxRectangleShapePaintBackground0.apply(this, arguments); + } + else + { + var events = true; + + if (this.style != null) + { + events = mxUtils.getValue(this.style, mxConstants.STYLE_POINTER_EVENTS, '1') == '1'; + } + + if (events || (this.fill != null && this.fill != mxConstants.NONE) || + (this.stroke != null && this.stroke != mxConstants.NONE)) + { + if (!events && (this.fill == null || this.fill == mxConstants.NONE)) + { + c.pointerEvents = false; + } + + c.begin(); + + if (this.isRounded) + { + var r = 0; + + if (mxUtils.getValue(this.style, mxConstants.STYLE_ABSOLUTE_ARCSIZE, 0) == '1') + { + r = Math.min(w / 2, Math.min(h / 2, mxUtils.getValue(this.style, + mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2)); + } + else + { + var f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, + mxConstants.RECTANGLE_ROUNDING_FACTOR * 100) / 100; + r = Math.min(w * f, h * f); + } + + c.moveTo(x + r, y); + c.lineTo(x + w - r, y); + c.quadTo(x + w, y, x + w, y + r); + c.lineTo(x + w, y + h - r); + c.quadTo(x + w, y + h, x + w - r, y + h); + c.lineTo(x + r, y + h); + c.quadTo(x, y + h, x, y + h - r); + c.lineTo(x, y + r); + c.quadTo(x, y, x + r, y); + } + else + { + + c.moveTo(x, y); + c.lineTo(x + w, y); + c.lineTo(x + w, y + h); + c.lineTo(x, y + h); + c.lineTo(x, y); + } + + // LATER: Check if close is needed here + c.close(); + c.end(); + + c.fillAndStroke(); + } + } + }; + + /** + * Disables glass effect with hand jiggle. + */ + var mxRectangleShapePaintForeground0 = mxRectangleShape.prototype.paintForeground; + mxRectangleShape.prototype.paintForeground = function(c, x, y, w, h) + { + if (c.handJiggle == null) + { + mxRectangleShapePaintForeground0.apply(this, arguments); + } + }; + + // End of hand jiggle integration + + // Process Shape + function ProcessShape() + { + mxRectangleShape.call(this); + }; + mxUtils.extend(ProcessShape, mxRectangleShape); + ProcessShape.prototype.size = 0.1; + ProcessShape.prototype.isHtmlAllowed = function() + { + return false; + }; + ProcessShape.prototype.getLabelBounds = function(rect) + { + if (mxUtils.getValue(this.state.style, mxConstants.STYLE_HORIZONTAL, true) == + (this.direction == null || + this.direction == mxConstants.DIRECTION_EAST || + this.direction == mxConstants.DIRECTION_WEST)) + { + var w = rect.width; + var h = rect.height; + var r = new mxRectangle(rect.x, rect.y, w, h); + + var inset = w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + + if (this.isRounded) + { + var f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, + mxConstants.RECTANGLE_ROUNDING_FACTOR * 100) / 100; + inset = Math.max(inset, Math.min(w * f, h * f)); + } + + r.x += Math.round(inset); + r.width -= Math.round(2 * inset); + + return r; + } + + return rect; + }; + ProcessShape.prototype.paintForeground = function(c, x, y, w, h) + { + var inset = w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + + if (this.isRounded) + { + var f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, + mxConstants.RECTANGLE_ROUNDING_FACTOR * 100) / 100; + inset = Math.max(inset, Math.min(w * f, h * f)); + } + + // Crisp rendering of inner lines + inset = Math.round(inset); + + c.begin(); + c.moveTo(x + inset, y); + c.lineTo(x + inset, y + h); + c.moveTo(x + w - inset, y); + c.lineTo(x + w - inset, y + h); + c.end(); + c.stroke(); + mxRectangleShape.prototype.paintForeground.apply(this, arguments); + }; + + mxCellRenderer.registerShape('process', ProcessShape); + + // Transparent Shape + function TransparentShape() + { + mxRectangleShape.call(this); + }; + mxUtils.extend(TransparentShape, mxRectangleShape); + TransparentShape.prototype.paintBackground = function(c, x, y, w, h) + { + c.setFillColor(mxConstants.NONE); + c.rect(x, y, w, h); + c.fill(); + }; + TransparentShape.prototype.paintForeground = function(c, x, y, w, h) { }; + + mxCellRenderer.registerShape('transparent', TransparentShape); + + // Callout shape + function CalloutShape() + { + mxActor.call(this); + }; + mxUtils.extend(CalloutShape, mxHexagon); + CalloutShape.prototype.size = 30; + CalloutShape.prototype.position = 0.5; + CalloutShape.prototype.position2 = 0.5; + CalloutShape.prototype.base = 20; + CalloutShape.prototype.getLabelMargins = function() + { + return new mxRectangle(0, 0, 0, parseFloat(mxUtils.getValue( + this.style, 'size', this.size)) * this.scale); + }; + CalloutShape.prototype.isRoundable = function() + { + return true; + }; + CalloutShape.prototype.redrawPath = function(c, x, y, w, h) + { + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + var s = Math.max(0, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var dx = w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'position', this.position)))); + var dx2 = w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'position2', this.position2)))); + var base = Math.max(0, Math.min(w, parseFloat(mxUtils.getValue(this.style, 'base', this.base)))); + + this.addPoints(c, [new mxPoint(0, 0), new mxPoint(w, 0), new mxPoint(w, h - s), + new mxPoint(Math.min(w, dx + base), h - s), new mxPoint(dx2, h), + new mxPoint(Math.max(0, dx), h - s), new mxPoint(0, h - s)], + this.isRounded, arcSize, true, [4]); + }; + + mxCellRenderer.registerShape('callout', CalloutShape); + + // Step shape + function StepShape() + { + mxActor.call(this); + }; + mxUtils.extend(StepShape, mxActor); + StepShape.prototype.size = 0.2; + StepShape.prototype.fixedSize = 20; + StepShape.prototype.isRoundable = function() + { + return true; + }; + StepShape.prototype.redrawPath = function(c, x, y, w, h) + { + var fixed = mxUtils.getValue(this.style, 'fixedSize', '0') != '0'; + var s = (fixed) ? Math.max(0, Math.min(w, parseFloat(mxUtils.getValue(this.style, 'size', this.fixedSize)))) : + w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(0, 0), new mxPoint(w - s, 0), new mxPoint(w, h / 2), new mxPoint(w - s, h), + new mxPoint(0, h), new mxPoint(s, h / 2)], this.isRounded, arcSize, true); + c.end(); + }; + + mxCellRenderer.registerShape('step', StepShape); + + // Hexagon shape + function HexagonShape() + { + mxActor.call(this); + }; + mxUtils.extend(HexagonShape, mxHexagon); + HexagonShape.prototype.size = 0.25; + HexagonShape.prototype.isRoundable = function() + { + return true; + }; + HexagonShape.prototype.redrawPath = function(c, x, y, w, h) + { + var s = w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(s, 0), new mxPoint(w - s, 0), new mxPoint(w, 0.5 * h), new mxPoint(w - s, h), + new mxPoint(s, h), new mxPoint(0, 0.5 * h)], this.isRounded, arcSize, true); + }; + + mxCellRenderer.registerShape('hexagon', HexagonShape); + + // Plus Shape + function PlusShape() + { + mxRectangleShape.call(this); + }; + mxUtils.extend(PlusShape, mxRectangleShape); + PlusShape.prototype.isHtmlAllowed = function() + { + return false; + }; + PlusShape.prototype.paintForeground = function(c, x, y, w, h) + { + var border = Math.min(w / 5, h / 5) + 1; + + c.begin(); + c.moveTo(x + w / 2, y + border); + c.lineTo(x + w / 2, y + h - border); + c.moveTo(x + border, y + h / 2); + c.lineTo(x + w - border, y + h / 2); + c.end(); + c.stroke(); + mxRectangleShape.prototype.paintForeground.apply(this, arguments); + }; + + mxCellRenderer.registerShape('plus', PlusShape); + + // Overrides painting of rhombus shape to allow for double style + var mxRhombusPaintVertexShape = mxRhombus.prototype.paintVertexShape; + mxRhombus.prototype.getLabelBounds = function(rect) + { + if (this.style['double'] == 1) + { + var margin = (Math.max(2, this.strokewidth + 1) * 2 + parseFloat( + this.style[mxConstants.STYLE_MARGIN] || 0)) * this.scale; + + return new mxRectangle(rect.x + margin, rect.y + margin, + rect.width - 2 * margin, rect.height - 2 * margin); + } + + return rect; + }; + mxRhombus.prototype.paintVertexShape = function(c, x, y, w, h) + { + mxRhombusPaintVertexShape.apply(this, arguments); + + if (!this.outline && this.style['double'] == 1) + { + var margin = Math.max(2, this.strokewidth + 1) * 2 + + parseFloat(this.style[mxConstants.STYLE_MARGIN] || 0); + x += margin; + y += margin; + w -= 2 * margin; + h -= 2 * margin; + + if (w > 0 && h > 0) + { + c.setShadow(false); + + // Workaround for closure compiler bug where the lines with x and y above + // are removed if arguments is used as second argument in call below. + mxRhombusPaintVertexShape.apply(this, [c, x, y, w, h]); + } + } + }; + + // CompositeShape + function ExtendedShape() + { + mxRectangleShape.call(this); + }; + mxUtils.extend(ExtendedShape, mxRectangleShape); + ExtendedShape.prototype.isHtmlAllowed = function() + { + return false; + }; + ExtendedShape.prototype.getLabelBounds = function(rect) + { + if (this.style['double'] == 1) + { + var margin = (Math.max(2, this.strokewidth + 1) + parseFloat( + this.style[mxConstants.STYLE_MARGIN] || 0)) * this.scale; + + return new mxRectangle(rect.x + margin, rect.y + margin, + rect.width - 2 * margin, rect.height - 2 * margin); + } + + return rect; + }; + + ExtendedShape.prototype.paintForeground = function(c, x, y, w, h) + { + if (this.style != null) + { + if (!this.outline && this.style['double'] == 1) + { + var margin = Math.max(2, this.strokewidth + 1) + parseFloat(this.style[mxConstants.STYLE_MARGIN] || 0); + x += margin; + y += margin; + w -= 2 * margin; + h -= 2 * margin; + + if (w > 0 && h > 0) + { + mxRectangleShape.prototype.paintBackground.apply(this, arguments); + } + } + + c.setDashed(false); + + // Draws the symbols defined in the style. The symbols are + // numbered from 1...n. Possible postfixes are align, + // verticalAlign, spacing, arcSpacing, width, height + var counter = 0; + var shape = null; + + do + { + shape = mxCellRenderer.defaultShapes[this.style['symbol' + counter]]; + + if (shape != null) + { + var align = this.style['symbol' + counter + 'Align']; + var valign = this.style['symbol' + counter + 'VerticalAlign']; + var width = this.style['symbol' + counter + 'Width']; + var height = this.style['symbol' + counter + 'Height']; + var spacing = this.style['symbol' + counter + 'Spacing'] || 0; + var vspacing = this.style['symbol' + counter + 'VSpacing'] || spacing; + var arcspacing = this.style['symbol' + counter + 'ArcSpacing']; + + if (arcspacing != null) + { + var arcSize = this.getArcSize(w + this.strokewidth, h + this.strokewidth) * arcspacing; + spacing += arcSize; + vspacing += arcSize; + } + + var x2 = x; + var y2 = y; + + if (align == mxConstants.ALIGN_CENTER) + { + x2 += (w - width) / 2; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + x2 += w - width - spacing; + } + else + { + x2 += spacing; + } + + if (valign == mxConstants.ALIGN_MIDDLE) + { + y2 += (h - height) / 2; + } + else if (valign == mxConstants.ALIGN_BOTTOM) + { + y2 += h - height - vspacing; + } + else + { + y2 += vspacing; + } + + c.save(); + + // Small hack to pass style along into subshape + var tmp = new shape(); + // TODO: Clone style and override settings (eg. strokewidth) + tmp.style = this.style; + shape.prototype.paintVertexShape.call(tmp, c, x2, y2, width, height); + c.restore(); + } + + counter++; + } + while (shape != null); + } + + // Paints glass effect + mxRectangleShape.prototype.paintForeground.apply(this, arguments); + }; + + mxCellRenderer.registerShape('ext', ExtendedShape); + + // Tape Shape, supports size style + function MessageShape() + { + mxCylinder.call(this); + }; + mxUtils.extend(MessageShape, mxCylinder); + MessageShape.prototype.redrawPath = function(path, x, y, w, h, isForeground) + { + if (isForeground) + { + path.moveTo(0, 0); + path.lineTo(w / 2, h / 2); + path.lineTo(w, 0); + path.end(); + } + else + { + path.moveTo(0, 0); + path.lineTo(w, 0); + path.lineTo(w, h); + path.lineTo(0, h); + path.close(); + } + }; + + mxCellRenderer.registerShape('message', MessageShape); + + // UML Actor Shape + function UmlActorShape() + { + mxShape.call(this); + }; + mxUtils.extend(UmlActorShape, mxShape); + UmlActorShape.prototype.paintBackground = function(c, x, y, w, h) + { + c.translate(x, y); + + // Head + c.ellipse(w / 4, 0, w / 2, h / 4); + c.fillAndStroke(); + + c.begin(); + c.moveTo(w / 2, h / 4); + c.lineTo(w / 2, 2 * h / 3); + + // Arms + c.moveTo(w / 2, h / 3); + c.lineTo(0, h / 3); + c.moveTo(w / 2, h / 3); + c.lineTo(w, h / 3); + + // Legs + c.moveTo(w / 2, 2 * h / 3); + c.lineTo(0, h); + c.moveTo(w / 2, 2 * h / 3); + c.lineTo(w, h); + c.end(); + + c.stroke(); + }; + + // Replaces existing actor shape + mxCellRenderer.registerShape('umlActor', UmlActorShape); + + // UML Boundary Shape + function UmlBoundaryShape() + { + mxShape.call(this); + }; + mxUtils.extend(UmlBoundaryShape, mxShape); + UmlBoundaryShape.prototype.getLabelMargins = function(rect) + { + return new mxRectangle(rect.width / 6, 0, 0, 0); + }; + UmlBoundaryShape.prototype.paintBackground = function(c, x, y, w, h) + { + c.translate(x, y); + + // Base line + c.begin(); + c.moveTo(0, h / 4); + c.lineTo(0, h * 3 / 4); + c.end(); + c.stroke(); + + // Horizontal line + c.begin(); + c.moveTo(0, h / 2); + c.lineTo(w / 6, h / 2); + c.end(); + c.stroke(); + + // Circle + c.ellipse(w / 6, 0, w * 5 / 6, h); + c.fillAndStroke(); + }; + + // Replaces existing actor shape + mxCellRenderer.registerShape('umlBoundary', UmlBoundaryShape); + + // UML Entity Shape + function UmlEntityShape() + { + mxEllipse.call(this); + }; + mxUtils.extend(UmlEntityShape, mxEllipse); + UmlEntityShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + mxEllipse.prototype.paintVertexShape.apply(this, arguments); + + c.begin(); + c.moveTo(x + w / 8, y + h); + c.lineTo(x + w * 7 / 8, y + h); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('umlEntity', UmlEntityShape); + + // UML Destroy Shape + function UmlDestroyShape() + { + mxShape.call(this); + }; + mxUtils.extend(UmlDestroyShape, mxShape); + UmlDestroyShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + c.translate(x, y); + + c.begin(); + c.moveTo(w, 0); + c.lineTo(0, h); + c.moveTo(0, 0); + c.lineTo(w, h); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('umlDestroy', UmlDestroyShape); + + // UML Control Shape + function UmlControlShape() + { + mxShape.call(this); + }; + mxUtils.extend(UmlControlShape, mxShape); + UmlControlShape.prototype.getLabelBounds = function(rect) + { + return new mxRectangle(rect.x, rect.y + rect.height / 8, rect.width, rect.height * 7 / 8); + }; + UmlControlShape.prototype.paintBackground = function(c, x, y, w, h) + { + c.translate(x, y); + + // Upper line + c.begin(); + c.moveTo(w * 3 / 8, h / 8 * 1.1); + c.lineTo(w * 5 / 8, 0); + c.end(); + c.stroke(); + + // Circle + c.ellipse(0, h / 8, w, h * 7 / 8); + c.fillAndStroke(); + }; + UmlControlShape.prototype.paintForeground = function(c, x, y, w, h) + { + // Lower line + c.begin(); + c.moveTo(w * 3 / 8, h / 8 * 1.1); + c.lineTo(w * 5 / 8, h / 4); + c.end(); + c.stroke(); + }; + + // Replaces existing actor shape + mxCellRenderer.registerShape('umlControl', UmlControlShape); + + // UML Lifeline Shape + function UmlLifeline() + { + mxRectangleShape.call(this); + }; + mxUtils.extend(UmlLifeline, mxRectangleShape); + UmlLifeline.prototype.size = 40; + UmlLifeline.prototype.isHtmlAllowed = function() + { + return false; + }; + UmlLifeline.prototype.getLabelBounds = function(rect) + { + var size = Math.max(0, Math.min(rect.height, parseFloat( + mxUtils.getValue(this.style, 'size', this.size)) * this.scale)); + + return new mxRectangle(rect.x, rect.y, rect.width, size); + }; + UmlLifeline.prototype.paintBackground = function(c, x, y, w, h) + { + var size = Math.max(0, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var participant = mxUtils.getValue(this.style, 'participant'); + + if (participant == null || this.state == null) + { + mxRectangleShape.prototype.paintBackground.call(this, c, x, y, w, size); + } + else + { + var ctor = this.state.view.graph.cellRenderer.getShape(participant); + + if (ctor != null && ctor != UmlLifeline) + { + var shape = new ctor(); + shape.apply(this.state); + c.save(); + shape.paintVertexShape(c, x, y, w, size); + c.restore(); + } + } + + if (size < h) + { + c.setDashed(true); + c.begin(); + c.moveTo(x + w / 2, y + size); + c.lineTo(x + w / 2, y + h); + c.end(); + c.stroke(); + } + }; + UmlLifeline.prototype.paintForeground = function(c, x, y, w, h) + { + var size = Math.max(0, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + mxRectangleShape.prototype.paintForeground.call(this, c, x, y, w, Math.min(h, size)); + }; + + mxCellRenderer.registerShape('umlLifeline', UmlLifeline); + + // UML Frame Shape + function UmlFrame() + { + mxShape.call(this); + }; + mxUtils.extend(UmlFrame, mxShape); + UmlFrame.prototype.width = 60; + UmlFrame.prototype.height = 30; + UmlFrame.prototype.corner = 10; + UmlFrame.prototype.getLabelMargins = function(rect) + { + return new mxRectangle(0, 0, + rect.width - (parseFloat(mxUtils.getValue(this.style, 'width', this.width) * this.scale)), + rect.height - (parseFloat(mxUtils.getValue(this.style, 'height', this.height) * this.scale))); + }; + UmlFrame.prototype.paintBackground = function(c, x, y, w, h) + { + var co = this.corner; + var w0 = Math.min(w, Math.max(co, parseFloat(mxUtils.getValue(this.style, 'width', this.width)))); + var h0 = Math.min(h, Math.max(co * 1.5, parseFloat(mxUtils.getValue(this.style, 'height', this.height)))); + var bg = mxUtils.getValue(this.style, mxConstants.STYLE_SWIMLANE_FILLCOLOR, mxConstants.NONE); + + if (bg != mxConstants.NONE) + { + c.setFillColor(bg); + c.rect(x, y, w, h); + c.fill(); + } + + if (this.fill != null && this.fill != mxConstants.NONE && this.gradient && this.gradient != mxConstants.NONE) + { + var b = this.getGradientBounds(c, x, y, w, h); + c.setGradient(this.fill, this.gradient, x, y, w, h, this.gradientDirection); + } + else + { + c.setFillColor(this.fill); + } + + c.begin(); + c.moveTo(x, y); + c.lineTo(x + w0, y); + c.lineTo(x + w0, y + Math.max(0, h0 - co * 1.5)); + c.lineTo(x + Math.max(0, w0 - co), y + h0); + c.lineTo(x, y + h0); + c.close(); + c.fillAndStroke(); + + c.begin(); + c.moveTo(x + w0, y); + c.lineTo(x + w, y); + c.lineTo(x + w, y + h); + c.lineTo(x, y + h); + c.lineTo(x, y + h0); + c.stroke(); + }; + + mxCellRenderer.registerShape('umlFrame', UmlFrame); + + mxPerimeter.LifelinePerimeter = function (bounds, vertex, next, orthogonal) + { + var size = UmlLifeline.prototype.size; + + if (vertex != null) + { + size = mxUtils.getValue(vertex.style, 'size', size) * vertex.view.scale; + } + + var sw = (parseFloat(vertex.style[mxConstants.STYLE_STROKEWIDTH] || 1) * vertex.view.scale / 2) - 1; + + if (next.x < bounds.getCenterX()) + { + sw += 1; + sw *= -1; + } + + return new mxPoint(bounds.getCenterX() + sw, Math.min(bounds.y + bounds.height, + Math.max(bounds.y + size, next.y))); + }; + + mxStyleRegistry.putValue('lifelinePerimeter', mxPerimeter.LifelinePerimeter); + + mxPerimeter.OrthogonalPerimeter = function (bounds, vertex, next, orthogonal) + { + orthogonal = true; + + return mxPerimeter.RectanglePerimeter.apply(this, arguments); + }; + + mxStyleRegistry.putValue('orthogonalPerimeter', mxPerimeter.OrthogonalPerimeter); + + mxPerimeter.BackbonePerimeter = function (bounds, vertex, next, orthogonal) + { + var sw = (parseFloat(vertex.style[mxConstants.STYLE_STROKEWIDTH] || 1) * vertex.view.scale / 2) - 1; + + if (vertex.style['backboneSize'] != null) + { + sw += (parseFloat(vertex.style['backboneSize']) * vertex.view.scale / 2) - 1; + } + + if (vertex.style[mxConstants.STYLE_DIRECTION] == 'south' || + vertex.style[mxConstants.STYLE_DIRECTION] == 'north') + { + if (next.x < bounds.getCenterX()) + { + sw += 1; + sw *= -1; + } + + return new mxPoint(bounds.getCenterX() + sw, Math.min(bounds.y + bounds.height, + Math.max(bounds.y, next.y))); + } + else + { + if (next.y < bounds.getCenterY()) + { + sw += 1; + sw *= -1; + } + + return new mxPoint(Math.min(bounds.x + bounds.width, Math.max(bounds.x, next.x)), + bounds.getCenterY() + sw); + } + }; + + mxStyleRegistry.putValue('backbonePerimeter', mxPerimeter.BackbonePerimeter); + + // Callout Perimeter + mxPerimeter.CalloutPerimeter = function (bounds, vertex, next, orthogonal) + { + return mxPerimeter.RectanglePerimeter(mxUtils.getDirectedBounds(bounds, new mxRectangle(0, 0, 0, + Math.max(0, Math.min(bounds.height, parseFloat(mxUtils.getValue(vertex.style, 'size', + CalloutShape.prototype.size)) * vertex.view.scale))), + vertex.style), vertex, next, orthogonal); + }; + + mxStyleRegistry.putValue('calloutPerimeter', mxPerimeter.CalloutPerimeter); + + // Parallelogram Perimeter + mxPerimeter.ParallelogramPerimeter = function (bounds, vertex, next, orthogonal) + { + var size = ParallelogramShape.prototype.size; + + if (vertex != null) + { + size = mxUtils.getValue(vertex.style, 'size', size); + } + + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + var direction = (vertex != null) ? mxUtils.getValue( + vertex.style, mxConstants.STYLE_DIRECTION, + mxConstants.DIRECTION_EAST) : mxConstants.DIRECTION_EAST; + var vertical = direction == mxConstants.DIRECTION_NORTH || + direction == mxConstants.DIRECTION_SOUTH; + var points; + + if (vertical) + { + var dy = h * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x, y), new mxPoint(x + w, y + dy), + new mxPoint(x + w, y + h), new mxPoint(x, y + h - dy), new mxPoint(x, y)]; + } + else + { + var dx = w * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x + dx, y), new mxPoint(x + w, y), + new mxPoint(x + w - dx, y + h), new mxPoint(x, y + h), new mxPoint(x + dx, y)]; + } + + var cx = bounds.getCenterX(); + var cy = bounds.getCenterY(); + + var p1 = new mxPoint(cx, cy); + + if (orthogonal) + { + if (next.x < x || next.x > x + w) + { + p1.y = next.y; + } + else + { + p1.x = next.x; + } + } + + return mxUtils.getPerimeterPoint(points, p1, next); + }; + + mxStyleRegistry.putValue('parallelogramPerimeter', mxPerimeter.ParallelogramPerimeter); + + // Trapezoid Perimeter + mxPerimeter.TrapezoidPerimeter = function (bounds, vertex, next, orthogonal) + { + var size = TrapezoidShape.prototype.size; + + if (vertex != null) + { + size = mxUtils.getValue(vertex.style, 'size', size); + } + + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + var direction = (vertex != null) ? mxUtils.getValue( + vertex.style, mxConstants.STYLE_DIRECTION, + mxConstants.DIRECTION_EAST) : mxConstants.DIRECTION_EAST; + var points; + + if (direction == mxConstants.DIRECTION_EAST) + { + var dx = w * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x + dx, y), new mxPoint(x + w - dx, y), + new mxPoint(x + w, y + h), new mxPoint(x, y + h), new mxPoint(x + dx, y)]; + } + else if (direction == mxConstants.DIRECTION_WEST) + { + var dx = w * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x, y), new mxPoint(x + w, y), + new mxPoint(x + w - dx, y + h), new mxPoint(x + dx, y + h), new mxPoint(x, y)]; + } + else if (direction == mxConstants.DIRECTION_NORTH) + { + var dy = h * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x, y + dy), new mxPoint(x + w, y), + new mxPoint(x + w, y + h), new mxPoint(x, y + h - dy), new mxPoint(x, y + dy)]; + } + else + { + var dy = h * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x, y), new mxPoint(x + w, y + dy), + new mxPoint(x + w, y + h - dy), new mxPoint(x, y + h), new mxPoint(x, y)]; + } + + var cx = bounds.getCenterX(); + var cy = bounds.getCenterY(); + + var p1 = new mxPoint(cx, cy); + + if (orthogonal) + { + if (next.x < x || next.x > x + w) + { + p1.y = next.y; + } + else + { + p1.x = next.x; + } + } + + return mxUtils.getPerimeterPoint(points, p1, next); + }; + + mxStyleRegistry.putValue('trapezoidPerimeter', mxPerimeter.TrapezoidPerimeter); + + // Step Perimeter + mxPerimeter.StepPerimeter = function (bounds, vertex, next, orthogonal) + { + var fixed = mxUtils.getValue(vertex.style, 'fixedSize', '0') != '0'; + var size = (fixed) ? StepShape.prototype.fixedSize : StepShape.prototype.size; + + if (vertex != null) + { + size = mxUtils.getValue(vertex.style, 'size', size); + } + + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + var cx = bounds.getCenterX(); + var cy = bounds.getCenterY(); + + var direction = (vertex != null) ? mxUtils.getValue( + vertex.style, mxConstants.STYLE_DIRECTION, + mxConstants.DIRECTION_EAST) : mxConstants.DIRECTION_EAST; + var points; + + if (direction == mxConstants.DIRECTION_EAST) + { + var dx = (fixed) ? Math.max(0, Math.min(w, size)) : w * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x, y), new mxPoint(x + w - dx, y), new mxPoint(x + w, cy), + new mxPoint(x + w - dx, y + h), new mxPoint(x, y + h), + new mxPoint(x + dx, cy), new mxPoint(x, y)]; + } + else if (direction == mxConstants.DIRECTION_WEST) + { + var dx = (fixed) ? Math.max(0, Math.min(w, size)) : w * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x + dx, y), new mxPoint(x + w, y), new mxPoint(x + w - dx, cy), + new mxPoint(x + w, y + h), new mxPoint(x + dx, y + h), + new mxPoint(x, cy), new mxPoint(x + dx, y)]; + } + else if (direction == mxConstants.DIRECTION_NORTH) + { + var dy = (fixed) ? Math.max(0, Math.min(h, size)) : h * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x, y + dy), new mxPoint(cx, y), new mxPoint(x + w, y + dy), + new mxPoint(x + w, y + h), new mxPoint(cx, y + h - dy), + new mxPoint(x, y + h), new mxPoint(x, y + dy)]; + } + else + { + var dy = (fixed) ? Math.max(0, Math.min(h, size)) : h * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x, y), new mxPoint(cx, y + dy), new mxPoint(x + w, y), + new mxPoint(x + w, y + h - dy), new mxPoint(cx, y + h), + new mxPoint(x, y + h - dy), new mxPoint(x, y)]; + } + + var p1 = new mxPoint(cx, cy); + + if (orthogonal) + { + if (next.x < x || next.x > x + w) + { + p1.y = next.y; + } + else + { + p1.x = next.x; + } + } + + return mxUtils.getPerimeterPoint(points, p1, next); + }; + + mxStyleRegistry.putValue('stepPerimeter', mxPerimeter.StepPerimeter); + + // Hexagon Perimeter 2 (keep existing one) + mxPerimeter.HexagonPerimeter2 = function (bounds, vertex, next, orthogonal) + { + var size = HexagonShape.prototype.size; + + if (vertex != null) + { + size = mxUtils.getValue(vertex.style, 'size', size); + } + + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + var cx = bounds.getCenterX(); + var cy = bounds.getCenterY(); + + var direction = (vertex != null) ? mxUtils.getValue( + vertex.style, mxConstants.STYLE_DIRECTION, + mxConstants.DIRECTION_EAST) : mxConstants.DIRECTION_EAST; + var vertical = direction == mxConstants.DIRECTION_NORTH || + direction == mxConstants.DIRECTION_SOUTH; + var points; + + if (vertical) + { + var dy = h * Math.max(0, Math.min(1, size)); + points = [new mxPoint(cx, y), new mxPoint(x + w, y + dy), new mxPoint(x + w, y + h - dy), + new mxPoint(cx, y + h), new mxPoint(x, y + h - dy), + new mxPoint(x, y + dy), new mxPoint(cx, y)]; + } + else + { + var dx = w * Math.max(0, Math.min(1, size)); + points = [new mxPoint(x + dx, y), new mxPoint(x + w - dx, y), new mxPoint(x + w, cy), + new mxPoint(x + w - dx, y + h), new mxPoint(x + dx, y + h), + new mxPoint(x, cy), new mxPoint(x + dx, y)]; + } + + var p1 = new mxPoint(cx, cy); + + if (orthogonal) + { + if (next.x < x || next.x > x + w) + { + p1.y = next.y; + } + else + { + p1.x = next.x; + } + } + + return mxUtils.getPerimeterPoint(points, p1, next); + }; + + mxStyleRegistry.putValue('hexagonPerimeter2', mxPerimeter.HexagonPerimeter2); + + // Provided Interface Shape (aka Lollipop) + function LollipopShape() + { + mxShape.call(this); + }; + mxUtils.extend(LollipopShape, mxShape); + LollipopShape.prototype.size = 10; + LollipopShape.prototype.paintBackground = function(c, x, y, w, h) + { + var sz = parseFloat(mxUtils.getValue(this.style, 'size', this.size)); + c.translate(x, y); + + c.ellipse((w - sz) / 2, 0, sz, sz); + c.fillAndStroke(); + + c.begin(); + c.moveTo(w / 2, sz); + c.lineTo(w / 2, h); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('lollipop', LollipopShape); + + // Required Interface Shape + function RequiresShape() + { + mxShape.call(this); + }; + mxUtils.extend(RequiresShape, mxShape); + RequiresShape.prototype.size = 10; + RequiresShape.prototype.inset = 2; + RequiresShape.prototype.paintBackground = function(c, x, y, w, h) + { + var sz = parseFloat(mxUtils.getValue(this.style, 'size', this.size)); + var inset = parseFloat(mxUtils.getValue(this.style, 'inset', this.inset)) + this.strokewidth; + c.translate(x, y); + + c.begin(); + c.moveTo(w / 2, sz + inset); + c.lineTo(w / 2, h); + c.end(); + c.stroke(); + + c.begin(); + c.moveTo((w - sz) / 2 - inset, sz / 2); + c.quadTo((w - sz) / 2 - inset, sz + inset, w / 2, sz + inset); + c.quadTo((w + sz) / 2 + inset, sz + inset, (w + sz) / 2 + inset, sz / 2); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('requires', RequiresShape); + + // Required Interface Shape + function RequiredInterfaceShape() + { + mxShape.call(this); + }; + mxUtils.extend(RequiredInterfaceShape, mxShape); + + RequiredInterfaceShape.prototype.paintBackground = function(c, x, y, w, h) + { + c.translate(x, y); + + c.begin(); + c.moveTo(0, 0); + c.quadTo(w, 0, w, h / 2); + c.quadTo(w, h, 0, h); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('requiredInterface', RequiredInterfaceShape); + + // Provided and Required Interface Shape + function ProvidedRequiredInterfaceShape() + { + mxShape.call(this); + }; + mxUtils.extend(ProvidedRequiredInterfaceShape, mxShape); + ProvidedRequiredInterfaceShape.prototype.inset = 2; + ProvidedRequiredInterfaceShape.prototype.paintBackground = function(c, x, y, w, h) + { + var inset = parseFloat(mxUtils.getValue(this.style, 'inset', this.inset)) + this.strokewidth; + c.translate(x, y); + + c.ellipse(0, inset, w - 2 * inset, h - 2 * inset); + c.fillAndStroke(); + + c.begin(); + c.moveTo(w / 2, 0); + c.quadTo(w, 0, w, h / 2); + c.quadTo(w, h, w / 2, h); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('providedRequiredInterface', ProvidedRequiredInterfaceShape); + + // Component shape + function ComponentShape() + { + mxCylinder.call(this); + }; + mxUtils.extend(ComponentShape, mxCylinder); + ComponentShape.prototype.jettyWidth = 32; + ComponentShape.prototype.jettyHeight = 12; + ComponentShape.prototype.redrawPath = function(path, x, y, w, h, isForeground) + { + var dx = parseFloat(mxUtils.getValue(this.style, 'jettyWidth', this.jettyWidth)); + var dy = parseFloat(mxUtils.getValue(this.style, 'jettyHeight', this.jettyHeight)); + var x0 = dx / 2; + var x1 = x0 + dx / 2; + var y0 = 0.3 * h - dy / 2; + var y1 = 0.7 * h - dy / 2; + + if (isForeground) + { + path.moveTo(x0, y0); + path.lineTo(x1, y0); + path.lineTo(x1, y0 + dy); + path.lineTo(x0, y0 + dy); + path.moveTo(x0, y1); + path.lineTo(x1, y1); + path.lineTo(x1, y1 + dy); + path.lineTo(x0, y1 + dy); + path.end(); + } + else + { + path.moveTo(x0, 0); + path.lineTo(w, 0); + path.lineTo(w, h); + path.lineTo(x0, h); + path.lineTo(x0, y1 + dy); + path.lineTo(0, y1 + dy); + path.lineTo(0, y1); + path.lineTo(x0, y1); + path.lineTo(x0, y0 + dy); + path.lineTo(0, y0 + dy); + path.lineTo(0, y0); + path.lineTo(x0, y0); + path.close(); + path.end(); + } + }; + + mxCellRenderer.registerShape('component', ComponentShape); + + // State Shapes derives from double ellipse + function StateShape() + { + mxDoubleEllipse.call(this); + }; + mxUtils.extend(StateShape, mxDoubleEllipse); + StateShape.prototype.outerStroke = true; + StateShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + var inset = Math.min(4, Math.min(w / 5, h / 5)); + + if (w > 0 && h > 0) + { + c.ellipse(x + inset, y + inset, w - 2 * inset, h - 2 * inset); + c.fillAndStroke(); + } + + c.setShadow(false); + + if (this.outerStroke) + { + c.ellipse(x, y, w, h); + c.stroke(); + } + }; + + mxCellRenderer.registerShape('endState', StateShape); + + function StartStateShape() + { + StateShape.call(this); + }; + mxUtils.extend(StartStateShape, StateShape); + StartStateShape.prototype.outerStroke = false; + + mxCellRenderer.registerShape('startState', StartStateShape); + + // Link shape + function LinkShape() + { + mxArrowConnector.call(this); + this.spacing = 0; + }; + mxUtils.extend(LinkShape, mxArrowConnector); + LinkShape.prototype.defaultWidth = 4; + + LinkShape.prototype.isOpenEnded = function() + { + return true; + }; + + LinkShape.prototype.getEdgeWidth = function() + { + return mxUtils.getNumber(this.style, 'width', this.defaultWidth) + Math.max(0, this.strokewidth - 1); + }; + + LinkShape.prototype.isArrowRounded = function() + { + return this.isRounded; + }; + + // Registers the link shape + mxCellRenderer.registerShape('link', LinkShape); + + // Generic arrow + function FlexArrowShape() + { + mxArrowConnector.call(this); + this.spacing = 0; + }; + mxUtils.extend(FlexArrowShape, mxArrowConnector); + FlexArrowShape.prototype.defaultWidth = 10; + FlexArrowShape.prototype.defaultArrowWidth = 20; + + FlexArrowShape.prototype.getStartArrowWidth = function() + { + return this.getEdgeWidth() + mxUtils.getNumber(this.style, 'startWidth', this.defaultArrowWidth); + }; + + FlexArrowShape.prototype.getEndArrowWidth = function() + { + return this.getEdgeWidth() + mxUtils.getNumber(this.style, 'endWidth', this.defaultArrowWidth);; + }; + + FlexArrowShape.prototype.getEdgeWidth = function() + { + return mxUtils.getNumber(this.style, 'width', this.defaultWidth) + Math.max(0, this.strokewidth - 1); + }; + + // Registers the link shape + mxCellRenderer.registerShape('flexArrow', FlexArrowShape); + + // Manual Input shape + function ManualInputShape() + { + mxActor.call(this); + }; + mxUtils.extend(ManualInputShape, mxActor); + ManualInputShape.prototype.size = 30; + ManualInputShape.prototype.isRoundable = function() + { + return true; + }; + ManualInputShape.prototype.redrawPath = function(c, x, y, w, h) + { + var s = Math.min(h, parseFloat(mxUtils.getValue(this.style, 'size', this.size))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(0, h), new mxPoint(0, s), new mxPoint(w, 0), new mxPoint(w, h)], + this.isRounded, arcSize, true); + c.end(); + }; + + mxCellRenderer.registerShape('manualInput', ManualInputShape); + + // Internal storage + function InternalStorageShape() + { + mxRectangleShape.call(this); + }; + mxUtils.extend(InternalStorageShape, mxRectangleShape); + InternalStorageShape.prototype.dx = 20; + InternalStorageShape.prototype.dy = 20; + InternalStorageShape.prototype.isHtmlAllowed = function() + { + return false; + }; + InternalStorageShape.prototype.paintForeground = function(c, x, y, w, h) + { + mxRectangleShape.prototype.paintForeground.apply(this, arguments); + var inset = 0; + + if (this.isRounded) + { + var f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, + mxConstants.RECTANGLE_ROUNDING_FACTOR * 100) / 100; + inset = Math.max(inset, Math.min(w * f, h * f)); + } + + var dx = Math.max(inset, Math.min(w, parseFloat(mxUtils.getValue(this.style, 'dx', this.dx)))); + var dy = Math.max(inset, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'dy', this.dy)))); + + c.begin(); + c.moveTo(x, y + dy); + c.lineTo(x + w, y + dy); + c.end(); + c.stroke(); + + c.begin(); + c.moveTo(x + dx, y); + c.lineTo(x + dx, y + h); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('internalStorage', InternalStorageShape); + + // Internal storage + function CornerShape() + { + mxActor.call(this); + }; + mxUtils.extend(CornerShape, mxActor); + CornerShape.prototype.dx = 20; + CornerShape.prototype.dy = 20; + + // Corner + CornerShape.prototype.redrawPath = function(c, x, y, w, h) + { + var dx = Math.max(0, Math.min(w, parseFloat(mxUtils.getValue(this.style, 'dx', this.dx)))); + var dy = Math.max(0, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'dy', this.dy)))); + + var s = Math.min(w / 2, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(0, 0), new mxPoint(w, 0), new mxPoint(w, dy), new mxPoint(dx, dy), + new mxPoint(dx, h), new mxPoint(0, h)], this.isRounded, arcSize, true); + c.end(); + }; + + mxCellRenderer.registerShape('corner', CornerShape); + + // Crossbar shape + function CrossbarShape() + { + mxActor.call(this); + }; + mxUtils.extend(CrossbarShape, mxActor); + + CrossbarShape.prototype.redrawPath = function(c, x, y, w, h) + { + c.moveTo(0, 0); + c.lineTo(0, h); + c.end(); + + c.moveTo(w, 0); + c.lineTo(w, h); + c.end(); + + c.moveTo(0, h / 2); + c.lineTo(w, h / 2); + c.end(); + }; + + mxCellRenderer.registerShape('crossbar', CrossbarShape); + + // Internal storage + function TeeShape() + { + mxActor.call(this); + }; + mxUtils.extend(TeeShape, mxActor); + TeeShape.prototype.dx = 20; + TeeShape.prototype.dy = 20; + + // Corner + TeeShape.prototype.redrawPath = function(c, x, y, w, h) + { + var dx = Math.max(0, Math.min(w, parseFloat(mxUtils.getValue(this.style, 'dx', this.dx)))); + var dy = Math.max(0, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'dy', this.dy)))); + var w2 = Math.abs(w - dx) / 2; + + var s = Math.min(w / 2, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(0, 0), new mxPoint(w, 0), new mxPoint(w, dy), new mxPoint((w + dx) / 2, dy), + new mxPoint((w + dx) / 2, h), new mxPoint((w - dx) / 2, h), new mxPoint((w - dx) / 2, dy), + new mxPoint(0, dy)], this.isRounded, arcSize, true); + c.end(); + }; + + mxCellRenderer.registerShape('tee', TeeShape); + + // Arrow + function SingleArrowShape() + { + mxActor.call(this); + }; + mxUtils.extend(SingleArrowShape, mxActor); + SingleArrowShape.prototype.arrowWidth = 0.3; + SingleArrowShape.prototype.arrowSize = 0.2; + SingleArrowShape.prototype.redrawPath = function(c, x, y, w, h) + { + var aw = h * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'arrowWidth', this.arrowWidth)))); + var as = w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'arrowSize', this.arrowSize)))); + var at = (h - aw) / 2; + var ab = at + aw; + + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(0, at), new mxPoint(w - as, at), new mxPoint(w - as, 0), new mxPoint(w, h / 2), + new mxPoint(w - as, h), new mxPoint(w - as, ab), new mxPoint(0, ab)], + this.isRounded, arcSize, true); + c.end(); + }; + + mxCellRenderer.registerShape('singleArrow', SingleArrowShape); + + // Arrow + function DoubleArrowShape() + { + mxActor.call(this); + }; + mxUtils.extend(DoubleArrowShape, mxActor); + DoubleArrowShape.prototype.redrawPath = function(c, x, y, w, h) + { + var aw = h * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'arrowWidth', SingleArrowShape.prototype.arrowWidth)))); + var as = w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'arrowSize', SingleArrowShape.prototype.arrowSize)))); + var at = (h - aw) / 2; + var ab = at + aw; + + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(0, h / 2), new mxPoint(as, 0), new mxPoint(as, at), new mxPoint(w - as, at), + new mxPoint(w - as, 0), new mxPoint(w, h / 2), new mxPoint(w - as, h), + new mxPoint(w - as, ab), new mxPoint(as, ab), new mxPoint(as, h)], + this.isRounded, arcSize, true); + c.end(); + }; + + mxCellRenderer.registerShape('doubleArrow', DoubleArrowShape); + + // Data storage + function DataStorageShape() + { + mxActor.call(this); + }; + mxUtils.extend(DataStorageShape, mxActor); + DataStorageShape.prototype.size = 0.1; + DataStorageShape.prototype.redrawPath = function(c, x, y, w, h) + { + var s = w * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + + c.moveTo(s, 0); + c.lineTo(w, 0); + c.quadTo(w - s * 2, h / 2, w, h); + c.lineTo(s, h); + c.quadTo(s - s * 2, h / 2, s, 0); + c.close(); + c.end(); + }; + + mxCellRenderer.registerShape('dataStorage', DataStorageShape); + + // Or + function OrShape() + { + mxActor.call(this); + }; + mxUtils.extend(OrShape, mxActor); + OrShape.prototype.redrawPath = function(c, x, y, w, h) + { + c.moveTo(0, 0); + c.quadTo(w, 0, w, h / 2); + c.quadTo(w, h, 0, h); + c.close(); + c.end(); + }; + + mxCellRenderer.registerShape('or', OrShape); + + // Xor + function XorShape() + { + mxActor.call(this); + }; + mxUtils.extend(XorShape, mxActor); + XorShape.prototype.redrawPath = function(c, x, y, w, h) + { + c.moveTo(0, 0); + c.quadTo(w, 0, w, h / 2); + c.quadTo(w, h, 0, h); + c.quadTo(w / 2, h / 2, 0, 0); + c.close(); + c.end(); + }; + + mxCellRenderer.registerShape('xor', XorShape); + + // Loop limit + function LoopLimitShape() + { + mxActor.call(this); + }; + mxUtils.extend(LoopLimitShape, mxActor); + LoopLimitShape.prototype.size = 20; + LoopLimitShape.prototype.isRoundable = function() + { + return true; + }; + LoopLimitShape.prototype.redrawPath = function(c, x, y, w, h) + { + var s = Math.min(w / 2, Math.min(h, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(s, 0), new mxPoint(w - s, 0), new mxPoint(w, s * 0.8), new mxPoint(w, h), + new mxPoint(0, h), new mxPoint(0, s * 0.8)], this.isRounded, arcSize, true); + c.end(); + }; + + mxCellRenderer.registerShape('loopLimit', LoopLimitShape); + + // Off page connector + function OffPageConnectorShape() + { + mxActor.call(this); + }; + mxUtils.extend(OffPageConnectorShape, mxActor); + OffPageConnectorShape.prototype.size = 3 / 8; + OffPageConnectorShape.prototype.isRoundable = function() + { + return true; + }; + OffPageConnectorShape.prototype.redrawPath = function(c, x, y, w, h) + { + var s = h * Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var arcSize = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + this.addPoints(c, [new mxPoint(0, 0), new mxPoint(w, 0), new mxPoint(w, h - s), new mxPoint(w / 2, h), + new mxPoint(0, h - s)], this.isRounded, arcSize, true); + c.end(); + }; + + mxCellRenderer.registerShape('offPageConnector', OffPageConnectorShape); + + // Internal storage + function TapeDataShape() + { + mxEllipse.call(this); + }; + mxUtils.extend(TapeDataShape, mxEllipse); + TapeDataShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + mxEllipse.prototype.paintVertexShape.apply(this, arguments); + + c.begin(); + c.moveTo(x + w / 2, y + h); + c.lineTo(x + w, y + h); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('tapeData', TapeDataShape); + + // OrEllipseShape + function OrEllipseShape() + { + mxEllipse.call(this); + }; + mxUtils.extend(OrEllipseShape, mxEllipse); + OrEllipseShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + mxEllipse.prototype.paintVertexShape.apply(this, arguments); + + c.setShadow(false); + c.begin(); + c.moveTo(x, y + h / 2); + c.lineTo(x + w, y + h / 2); + c.end(); + c.stroke(); + + c.begin(); + c.moveTo(x + w / 2, y); + c.lineTo(x + w / 2, y + h); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('orEllipse', OrEllipseShape); + + // SumEllipseShape + function SumEllipseShape() + { + mxEllipse.call(this); + }; + mxUtils.extend(SumEllipseShape, mxEllipse); + SumEllipseShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + mxEllipse.prototype.paintVertexShape.apply(this, arguments); + var s2 = 0.145; + + c.setShadow(false); + c.begin(); + c.moveTo(x + w * s2, y + h * s2); + c.lineTo(x + w * (1 - s2), y + h * (1 - s2)); + c.end(); + c.stroke(); + + c.begin(); + c.moveTo(x + w * (1 - s2), y + h * s2); + c.lineTo(x + w * s2, y + h * (1 - s2)); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('sumEllipse', SumEllipseShape); + + // SortShape + function SortShape() + { + mxRhombus.call(this); + }; + mxUtils.extend(SortShape, mxRhombus); + SortShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + mxRhombus.prototype.paintVertexShape.apply(this, arguments); + + c.setShadow(false); + c.begin(); + c.moveTo(x, y + h / 2); + c.lineTo(x + w, y + h / 2); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('sortShape', SortShape); + + // CollateShape + function CollateShape() + { + mxEllipse.call(this); + }; + mxUtils.extend(CollateShape, mxEllipse); + CollateShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + c.begin(); + c.moveTo(x, y); + c.lineTo(x + w, y); + c.lineTo(x + w / 2, y + h / 2); + c.close(); + c.fillAndStroke(); + + c.begin(); + c.moveTo(x, y + h); + c.lineTo(x + w, y + h); + c.lineTo(x + w / 2, y + h / 2); + c.close(); + c.fillAndStroke(); + }; + + mxCellRenderer.registerShape('collate', CollateShape); + + // DimensionShape + function DimensionShape() + { + mxEllipse.call(this); + }; + mxUtils.extend(DimensionShape, mxEllipse); + DimensionShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + // Arrow size + var al = 10; + var cy = y + h - al / 2; + + c.begin(); + c.moveTo(x, y); + c.lineTo(x, y + h); + c.moveTo(x, cy); + c.lineTo(x + al, cy - al / 2); + c.moveTo(x, cy); + c.lineTo(x + al, cy + al / 2); + c.moveTo(x, cy); + c.lineTo(x + w, cy); + + // Opposite side + c.moveTo(x + w, y); + c.lineTo(x + w, y + h); + c.moveTo(x + w, cy); + c.lineTo(x + w - al, cy - al / 2); + c.moveTo(x + w, cy); + c.lineTo(x + w - al, cy + al / 2); + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('dimension', DimensionShape); + + // PartialRectangleShape + function PartialRectangleShape() + { + mxEllipse.call(this); + }; + mxUtils.extend(PartialRectangleShape, mxEllipse); + PartialRectangleShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + if (!this.outline) + { + c.setStrokeColor(null); + } + + mxRectangleShape.prototype.paintBackground.apply(this, arguments); + + if (this.style != null) + { + c.setStrokeColor(this.stroke); + c.rect(x, y, w, h); + c.fill(); + + c.begin(); + c.moveTo(x, y); + + if (mxUtils.getValue(this.style, 'top', '1') == '1') + { + c.lineTo(x + w, y); + } + else + { + c.moveTo(x + w, y); + } + + if (mxUtils.getValue(this.style, 'right', '1') == '1') + { + c.lineTo(x + w, y + h); + } + else + { + c.moveTo(x + w, y + h); + } + + if (mxUtils.getValue(this.style, 'bottom', '1') == '1') + { + c.lineTo(x, y + h); + } + else + { + c.moveTo(x, y + h); + } + + if (mxUtils.getValue(this.style, 'left', '1') == '1') + { + c.lineTo(x, y - this.strokewidth / 2); + } + + c.end(); + c.stroke(); + } + }; + + mxCellRenderer.registerShape('partialRectangle', PartialRectangleShape); + + // LineEllipseShape + function LineEllipseShape() + { + mxEllipse.call(this); + }; + mxUtils.extend(LineEllipseShape, mxEllipse); + LineEllipseShape.prototype.paintVertexShape = function(c, x, y, w, h) + { + mxEllipse.prototype.paintVertexShape.apply(this, arguments); + + c.setShadow(false); + c.begin(); + + if (mxUtils.getValue(this.style, 'line') == 'vertical') + { + c.moveTo(x + w / 2, y); + c.lineTo(x + w / 2, y + h); + } + else + { + c.moveTo(x, y + h / 2); + c.lineTo(x + w, y + h / 2); + } + + c.end(); + c.stroke(); + }; + + mxCellRenderer.registerShape('lineEllipse', LineEllipseShape); + + // Delay + function DelayShape() + { + mxActor.call(this); + }; + mxUtils.extend(DelayShape, mxActor); + DelayShape.prototype.redrawPath = function(c, x, y, w, h) + { + var dx = Math.min(w, h / 2); + c.moveTo(0, 0); + c.lineTo(w - dx, 0); + c.quadTo(w, 0, w, h / 2); + c.quadTo(w, h, w - dx, h); + c.lineTo(0, h); + c.close(); + c.end(); + }; + + mxCellRenderer.registerShape('delay', DelayShape); + + // Cross Shape + function CrossShape() + { + mxActor.call(this); + }; + mxUtils.extend(CrossShape, mxActor); + CrossShape.prototype.size = 0.2; + CrossShape.prototype.redrawPath = function(c, x, y, w, h) + { + var m = Math.min(h, w); + var size = Math.max(0, Math.min(m, m * parseFloat(mxUtils.getValue(this.style, 'size', this.size)))); + var t = (h - size) / 2; + var b = t + size; + var l = (w - size) / 2; + var r = l + size; + + c.moveTo(0, t); + c.lineTo(l, t); + c.lineTo(l, 0); + c.lineTo(r, 0); + c.lineTo(r, t); + c.lineTo(w, t); + c.lineTo(w, b); + c.lineTo(r, b); + c.lineTo(r, h); + c.lineTo(l, h); + c.lineTo(l, b); + c.lineTo(0, b); + c.close(); + c.end(); + }; + + mxCellRenderer.registerShape('cross', CrossShape); + + // Display + function DisplayShape() + { + mxActor.call(this); + }; + mxUtils.extend(DisplayShape, mxActor); + DisplayShape.prototype.size = 0.25; + DisplayShape.prototype.redrawPath = function(c, x, y, w, h) + { + var dx = Math.min(w, h / 2); + var s = Math.min(w - dx, Math.max(0, parseFloat(mxUtils.getValue(this.style, 'size', this.size))) * w); + + c.moveTo(0, h / 2); + c.lineTo(s, 0); + c.lineTo(w - dx, 0); + c.quadTo(w, 0, w, h / 2); + c.quadTo(w, h, w - dx, h); + c.lineTo(s, h); + c.close(); + c.end(); + }; + + mxCellRenderer.registerShape('display', DisplayShape); + + // FilledEdge shape + function FilledEdge() + { + mxConnector.call(this); + }; + mxUtils.extend(FilledEdge, mxConnector); + + FilledEdge.prototype.origPaintEdgeShape = FilledEdge.prototype.paintEdgeShape; + FilledEdge.prototype.paintEdgeShape = function(c, pts, rounded) + { + // Markers modify incoming points array + var temp = []; + + for (var i = 0; i < pts.length; i++) + { + temp.push(mxUtils.clone(pts[i])); + } + + // paintEdgeShape resets dashed to false + var dashed = c.state.dashed; + var fixDash = c.state.fixDash; + FilledEdge.prototype.origPaintEdgeShape.apply(this, [c, temp, rounded]); + + if (c.state.strokeWidth >= 3) + { + var fillClr = mxUtils.getValue(this.style, 'fillColor', null); + + if (fillClr != null) + { + c.setStrokeColor(fillClr); + c.setStrokeWidth(c.state.strokeWidth - 2); + c.setDashed(dashed, fixDash); + + FilledEdge.prototype.origPaintEdgeShape.apply(this, [c, pts, rounded]); + } + } + }; + + // Registers the link shape + mxCellRenderer.registerShape('filledEdge', FilledEdge); + + // Implements custom colors for shapes + if (typeof StyleFormatPanel !== 'undefined') + { + (function() + { + var styleFormatPanelGetCustomColors = StyleFormatPanel.prototype.getCustomColors; + + StyleFormatPanel.prototype.getCustomColors = function() + { + var ss = this.format.getSelectionState(); + var result = styleFormatPanelGetCustomColors.apply(this, arguments); + + if (ss.style.shape == 'umlFrame') + { + result.push({title: mxResources.get('laneColor'), key: 'swimlaneFillColor', defaultValue: '#ffffff'}); + } + + return result; + }; + })(); + } + + // Registers and defines the custom marker + mxMarker.addMarker('dash', function(c, shape, type, pe, unitX, unitY, size, source, sw, filled) + { + var nx = unitX * (size + sw + 1); + var ny = unitY * (size + sw + 1); + + return function() + { + c.begin(); + c.moveTo(pe.x - nx / 2 - ny / 2, pe.y - ny / 2 + nx / 2); + c.lineTo(pe.x + ny / 2 - 3 * nx / 2, pe.y - 3 * ny / 2 - nx / 2); + c.stroke(); + }; + }); + + // Registers and defines the custom marker + mxMarker.addMarker('cross', function(c, shape, type, pe, unitX, unitY, size, source, sw, filled) + { + var nx = unitX * (size + sw + 1); + var ny = unitY * (size + sw + 1); + + return function() + { + c.begin(); + c.moveTo(pe.x - nx / 2 - ny / 2, pe.y - ny / 2 + nx / 2); + c.lineTo(pe.x + ny / 2 - 3 * nx / 2, pe.y - 3 * ny / 2 - nx / 2); + c.moveTo(pe.x - nx / 2 + ny / 2, pe.y - ny / 2 - nx / 2); + c.lineTo(pe.x - ny / 2 - 3 * nx / 2, pe.y - 3 * ny / 2 + nx / 2); + c.stroke(); + }; + }); + + function circleMarker(c, shape, type, pe, unitX, unitY, size, source, sw, filled) + { + var a = size / 2; + var size = size + sw; + + var pt = pe.clone(); + + pe.x -= unitX * (2 * size + sw); + pe.y -= unitY * (2 * size + sw); + + unitX = unitX * (size + sw); + unitY = unitY * (size + sw); + + return function() + { + c.ellipse(pt.x - unitX - size, pt.y - unitY - size, 2 * size, 2 * size); + + if (filled) + { + c.fillAndStroke(); + } + else + { + c.stroke(); + } + }; + }; + + mxMarker.addMarker('circle', circleMarker); + mxMarker.addMarker('circlePlus', function(c, shape, type, pe, unitX, unitY, size, source, sw, filled) + { + var pt = pe.clone(); + var fn = circleMarker.apply(this, arguments); + var nx = unitX * (size + 2 * sw); // (size + sw + 1); + var ny = unitY * (size + 2 * sw); //(size + sw + 1); + + return function() + { + fn.apply(this, arguments); + + c.begin(); + c.moveTo(pt.x - unitX * (sw), pt.y - unitY * (sw)); + c.lineTo(pt.x - 2 * nx + unitX * (sw), pt.y - 2 * ny + unitY * (sw)); + c.moveTo(pt.x - nx - ny + unitY * sw, pt.y - ny + nx - unitX * sw); + c.lineTo(pt.x + ny - nx - unitY * sw, pt.y - ny - nx + unitX * sw); + c.stroke(); + }; + }); + + mxMarker.addMarker('async', function(c, shape, type, pe, unitX, unitY, size, source, sw, filled) + { + // The angle of the forward facing arrow sides against the x axis is + // 26.565 degrees, 1/sin(26.565) = 2.236 / 2 = 1.118 ( / 2 allows for + // only half the strokewidth is processed ). + var endOffsetX = unitX * sw * 1.118; + var endOffsetY = unitY * sw * 1.118; + + unitX = unitX * (size + sw); + unitY = unitY * (size + sw); + + var pt = pe.clone(); + pt.x -= endOffsetX; + pt.y -= endOffsetY; + + var f = 1; + pe.x += -unitX * f - endOffsetX; + pe.y += -unitY * f - endOffsetY; + + return function() + { + c.begin(); + c.moveTo(pt.x, pt.y); + + if (source) + { + c.lineTo(pt.x - unitX - unitY / 2, pt.y - unitY + unitX / 2); + } + else + { + c.lineTo(pt.x + unitY / 2 - unitX, pt.y - unitY - unitX / 2); + } + + c.lineTo(pt.x - unitX, pt.y - unitY); + c.close(); + + if (filled) + { + c.fillAndStroke(); + } + else + { + c.stroke(); + } + }; + }); + + function createOpenAsyncArrow(widthFactor) + { + widthFactor = (widthFactor != null) ? widthFactor : 2; + + return function(c, shape, type, pe, unitX, unitY, size, source, sw, filled) + { + unitX = unitX * (size + sw); + unitY = unitY * (size + sw); + + var pt = pe.clone(); + + return function() + { + c.begin(); + c.moveTo(pt.x, pt.y); + + if (source) + { + c.lineTo(pt.x - unitX - unitY / widthFactor, pt.y - unitY + unitX / widthFactor); + } + else + { + c.lineTo(pt.x + unitY / widthFactor - unitX, pt.y - unitY - unitX / widthFactor); + } + + c.stroke(); + }; + } + }; + + mxMarker.addMarker('openAsync', createOpenAsyncArrow(2)); + + function arrow(canvas, shape, type, pe, unitX, unitY, size, source, sw, filled) + { + // The angle of the forward facing arrow sides against the x axis is + // 26.565 degrees, 1/sin(26.565) = 2.236 / 2 = 1.118 ( / 2 allows for + // only half the strokewidth is processed ). + var endOffsetX = unitX * sw * 1.118; + var endOffsetY = unitY * sw * 1.118; + + unitX = unitX * (size + sw); + unitY = unitY * (size + sw); + + var pt = pe.clone(); + pt.x -= endOffsetX; + pt.y -= endOffsetY; + + var f = (type != mxConstants.ARROW_CLASSIC && type != mxConstants.ARROW_CLASSIC_THIN) ? 1 : 3 / 4; + pe.x += -unitX * f - endOffsetX; + pe.y += -unitY * f - endOffsetY; + + return function() + { + canvas.begin(); + canvas.moveTo(pt.x, pt.y); + canvas.lineTo(pt.x - unitX - unitY / widthFactor, pt.y - unitY + unitX / widthFactor); + + if (type == mxConstants.ARROW_CLASSIC || type == mxConstants.ARROW_CLASSIC_THIN) + { + canvas.lineTo(pt.x - unitX * 3 / 4, pt.y - unitY * 3 / 4); + } + + canvas.lineTo(pt.x + unitY / widthFactor - unitX, pt.y - unitY - unitX / widthFactor); + canvas.close(); + + if (filled) + { + canvas.fillAndStroke(); + } + else + { + canvas.stroke(); + } + }; + } + + // Handlers are only added if mxVertexHandler is defined (ie. not in embedded graph) + if (typeof mxVertexHandler !== 'undefined') + { + function createHandle(state, keys, getPositionFn, setPositionFn, ignoreGrid, redrawEdges) + { + var handle = new mxHandle(state, null, mxVertexHandler.prototype.secondaryHandleImage); + + handle.execute = function() + { + for (var i = 0; i < keys.length; i++) + { + this.copyStyle(keys[i]); + } + }; + + handle.getPosition = getPositionFn; + handle.setPosition = setPositionFn; + handle.ignoreGrid = (ignoreGrid != null) ? ignoreGrid : true; + + // Overridden to update connected edges + if (redrawEdges) + { + var positionChanged = handle.positionChanged; + + handle.positionChanged = function() + { + positionChanged.apply(this, arguments); + + // Redraws connected edges TODO: Include child edges + state.view.invalidate(this.state.cell); + state.view.validate(); + }; + } + + return handle; + }; + + function createArcHandle(state, yOffset) + { + return createHandle(state, [mxConstants.STYLE_ARCSIZE], function(bounds) + { + var tmp = (yOffset != null) ? yOffset : bounds.height / 8; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_ABSOLUTE_ARCSIZE, 0) == '1') + { + var arcSize = mxUtils.getValue(state.style, mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2; + + return new mxPoint(bounds.x + bounds.width - Math.min(bounds.width / 2, arcSize), bounds.y + tmp); + } + else + { + var arcSize = Math.max(0, parseFloat(mxUtils.getValue(state.style, + mxConstants.STYLE_ARCSIZE, mxConstants.RECTANGLE_ROUNDING_FACTOR * 100))) / 100; + + return new mxPoint(bounds.x + bounds.width - Math.min(Math.max(bounds.width / 2, bounds.height / 2), + Math.min(bounds.width, bounds.height) * arcSize), bounds.y + tmp); + } + }, function(bounds, pt, me) + { + if (mxUtils.getValue(state.style, mxConstants.STYLE_ABSOLUTE_ARCSIZE, 0) == '1') + { + this.state.style[mxConstants.STYLE_ARCSIZE] = Math.round(Math.max(0, Math.min(bounds.width, + (bounds.x + bounds.width - pt.x) * 2))); + } + else + { + var f = Math.min(50, Math.max(0, (bounds.width - pt.x + bounds.x) * 100 / + Math.min(bounds.width, bounds.height))); + this.state.style[mxConstants.STYLE_ARCSIZE] = Math.round(f); + } + }); + } + + function createArcHandleFunction() + { + return function(state) + { + var handles = []; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false)) + { + handles.push(createArcHandle(state)); + } + + return handles; + }; + }; + + function createTrapezoidHandleFunction(max) + { + return function(state) + { + var handles = [createHandle(state, ['size'], function(bounds) + { + var size = Math.max(0, Math.min(max, parseFloat(mxUtils.getValue(this.state.style, 'size', TrapezoidShape.prototype.size)))); + + return new mxPoint(bounds.x + size * bounds.width * 0.75, bounds.y + bounds.height / 4); + }, function(bounds, pt) + { + this.state.style['size'] = Math.max(0, Math.min(max, (pt.x - bounds.x) / (bounds.width * 0.75))); + }, null, true)]; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false)) + { + handles.push(createArcHandle(state)); + } + + return handles; + }; + }; + + function createDisplayHandleFunction(defaultValue, allowArcHandle, max, redrawEdges, fixedDefaultValue) + { + max = (max != null) ? max : 1; + + return function(state) + { + var handles = [createHandle(state, ['size'], function(bounds) + { + var fixed = (fixedDefaultValue != null) ? mxUtils.getValue(this.state.style, 'fixedSize', '0') != '0' : null; + var size = parseFloat(mxUtils.getValue(this.state.style, 'size', (fixed) ? fixedDefaultValue : defaultValue)); + + return new mxPoint(bounds.x + Math.max(0, Math.min(bounds.width, size * ((fixed) ? 1 : bounds.width))), bounds.getCenterY()); + }, function(bounds, pt, me) + { + var fixed = (fixedDefaultValue != null) ? mxUtils.getValue(this.state.style, 'fixedSize', '0') != '0' : null; + var size = (fixed) ? (pt.x - bounds.x) : Math.max(0, Math.min(max, (pt.x - bounds.x) / bounds.width)); + + if (fixed && !mxEvent.isAltDown(me.getEvent())) + { + size = state.view.graph.snap(size); + } + + this.state.style['size'] = size; + }, null, redrawEdges)]; + + if (allowArcHandle && mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false)) + { + handles.push(createArcHandle(state)); + } + + return handles; + }; + }; + + function createCubeHandleFunction(factor, defaultValue, allowArcHandle) + { + return function(state) + { + var handles = [createHandle(state, ['size'], function(bounds) + { + var size = Math.max(0, Math.min(bounds.width, Math.min(bounds.height, parseFloat( + mxUtils.getValue(this.state.style, 'size', defaultValue))))) * factor; + + return new mxPoint(bounds.x + size, bounds.y + size); + }, function(bounds, pt) + { + this.state.style['size'] = Math.round(Math.max(0, Math.min(Math.min(bounds.width, pt.x - bounds.x), + Math.min(bounds.height, pt.y - bounds.y))) / factor); + })]; + + if (allowArcHandle && mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false)) + { + handles.push(createArcHandle(state)); + } + + return handles; + }; + }; + + function createArrowHandleFunction(maxSize) + { + return function(state) + { + return [createHandle(state, ['arrowWidth', 'arrowSize'], function(bounds) + { + var aw = Math.max(0, Math.min(1, mxUtils.getValue(this.state.style, 'arrowWidth', SingleArrowShape.prototype.arrowWidth))); + var as = Math.max(0, Math.min(maxSize, mxUtils.getValue(this.state.style, 'arrowSize', SingleArrowShape.prototype.arrowSize))); + + return new mxPoint(bounds.x + (1 - as) * bounds.width, bounds.y + (1 - aw) * bounds.height / 2); + }, function(bounds, pt) + { + this.state.style['arrowWidth'] = Math.max(0, Math.min(1, Math.abs(bounds.y + bounds.height / 2 - pt.y) / bounds.height * 2)); + this.state.style['arrowSize'] = Math.max(0, Math.min(maxSize, (bounds.x + bounds.width - pt.x) / (bounds.width))); + })]; + }; + }; + + function createEdgeHandle(state, keys, start, getPosition, setPosition) + { + return createHandle(state, keys, function(bounds) + { + var pts = state.absolutePoints; + var n = pts.length - 1; + + var tr = state.view.translate; + var s = state.view.scale; + + var p0 = (start) ? pts[0] : pts[n]; + var p1 = (start) ? pts[1] : pts[n - 1]; + var dx = (start) ? p1.x - p0.x : p1.x - p0.x; + var dy = (start) ? p1.y - p0.y : p1.y - p0.y; + + var dist = Math.sqrt(dx * dx + dy * dy); + + var pt = getPosition.call(this, dist, dx / dist, dy / dist, p0, p1); + + return new mxPoint(pt.x / s - tr.x, pt.y / s - tr.y); + }, function(bounds, pt, me) + { + var pts = state.absolutePoints; + var n = pts.length - 1; + + var tr = state.view.translate; + var s = state.view.scale; + + var p0 = (start) ? pts[0] : pts[n]; + var p1 = (start) ? pts[1] : pts[n - 1]; + var dx = (start) ? p1.x - p0.x : p1.x - p0.x; + var dy = (start) ? p1.y - p0.y : p1.y - p0.y; + + var dist = Math.sqrt(dx * dx + dy * dy); + pt.x = (pt.x + tr.x) * s; + pt.y = (pt.y + tr.y) * s; + + setPosition.call(this, dist, dx / dist, dy / dist, p0, p1, pt, me); + }); + }; + + function createEdgeWidthHandle(state, start, spacing) + { + return createEdgeHandle(state, ['width'], start, function(dist, nx, ny, p0, p1) + { + var w = state.shape.getEdgeWidth() * state.view.scale + spacing; + + return new mxPoint(p0.x + nx * dist / 4 + ny * w / 2, p0.y + ny * dist / 4 - nx * w / 2); + }, function(dist, nx, ny, p0, p1, pt) + { + var w = Math.sqrt(mxUtils.ptSegDistSq(p0.x, p0.y, p1.x, p1.y, pt.x, pt.y)); + state.style['width'] = Math.round(w * 2) / state.view.scale - spacing; + }); + }; + + function ptLineDistance(x1, y1, x2, y2, x0, y0) + { + return Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / Math.sqrt((y2 - y1) * (y2 - y1) + (x2 - x1) * (x2 - x1)); + } + + var handleFactory = { + 'link': function(state) + { + var spacing = 10; + + return [createEdgeWidthHandle(state, true, spacing), createEdgeWidthHandle(state, false, spacing)]; + }, + 'flexArrow': function(state) + { + // Do not use state.shape.startSize/endSize since it is cached + var tol = state.view.graph.gridSize / state.view.scale; + var handles = []; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_STARTARROW, mxConstants.NONE) != mxConstants.NONE) + { + handles.push(createEdgeHandle(state, ['width', mxConstants.STYLE_STARTSIZE, mxConstants.STYLE_ENDSIZE], true, function(dist, nx, ny, p0, p1) + { + var w = (state.shape.getEdgeWidth() - state.shape.strokewidth) * state.view.scale; + var l = mxUtils.getNumber(state.style, mxConstants.STYLE_STARTSIZE, mxConstants.ARROW_SIZE / 5) * 3 * state.view.scale; + + return new mxPoint(p0.x + nx * (l + state.shape.strokewidth * state.view.scale) + ny * w / 2, + p0.y + ny * (l + state.shape.strokewidth * state.view.scale) - nx * w / 2); + }, function(dist, nx, ny, p0, p1, pt, me) + { + var w = Math.sqrt(mxUtils.ptSegDistSq(p0.x, p0.y, p1.x, p1.y, pt.x, pt.y)); + var l = mxUtils.ptLineDist(p0.x, p0.y, p0.x + ny, p0.y - nx, pt.x, pt.y); + + state.style[mxConstants.STYLE_STARTSIZE] = Math.round((l - state.shape.strokewidth) * 100 / 3) / 100 / state.view.scale; + state.style['width'] = Math.round(w * 2) / state.view.scale; + + // Applies to opposite side + if (mxEvent.isControlDown(me.getEvent())) + { + state.style[mxConstants.STYLE_ENDSIZE] = state.style[mxConstants.STYLE_STARTSIZE]; + } + + // Snaps to end geometry + if (!mxEvent.isAltDown(me.getEvent())) + { + if (Math.abs(parseFloat(state.style[mxConstants.STYLE_STARTSIZE]) - parseFloat(state.style[mxConstants.STYLE_ENDSIZE])) < tol / 6) + { + state.style[mxConstants.STYLE_STARTSIZE] = state.style[mxConstants.STYLE_ENDSIZE]; + } + } + })); + + handles.push(createEdgeHandle(state, ['startWidth', 'endWidth', mxConstants.STYLE_STARTSIZE, mxConstants.STYLE_ENDSIZE], true, function(dist, nx, ny, p0, p1) + { + var w = (state.shape.getStartArrowWidth() - state.shape.strokewidth) * state.view.scale; + var l = mxUtils.getNumber(state.style, mxConstants.STYLE_STARTSIZE, mxConstants.ARROW_SIZE / 5) * 3 * state.view.scale; + + return new mxPoint(p0.x + nx * (l + state.shape.strokewidth * state.view.scale) + ny * w / 2, + p0.y + ny * (l + state.shape.strokewidth * state.view.scale) - nx * w / 2); + }, function(dist, nx, ny, p0, p1, pt, me) + { + var w = Math.sqrt(mxUtils.ptSegDistSq(p0.x, p0.y, p1.x, p1.y, pt.x, pt.y)); + var l = mxUtils.ptLineDist(p0.x, p0.y, p0.x + ny, p0.y - nx, pt.x, pt.y); + + state.style[mxConstants.STYLE_STARTSIZE] = Math.round((l - state.shape.strokewidth) * 100 / 3) / 100 / state.view.scale; + state.style['startWidth'] = Math.max(0, Math.round(w * 2) - state.shape.getEdgeWidth()) / state.view.scale; + + // Applies to opposite side + if (mxEvent.isControlDown(me.getEvent())) + { + state.style[mxConstants.STYLE_ENDSIZE] = state.style[mxConstants.STYLE_STARTSIZE]; + state.style['endWidth'] = state.style['startWidth']; + } + + // Snaps to endWidth + if (!mxEvent.isAltDown(me.getEvent())) + { + if (Math.abs(parseFloat(state.style[mxConstants.STYLE_STARTSIZE]) - parseFloat(state.style[mxConstants.STYLE_ENDSIZE])) < tol / 6) + { + state.style[mxConstants.STYLE_STARTSIZE] = state.style[mxConstants.STYLE_ENDSIZE]; + } + + if (Math.abs(parseFloat(state.style['startWidth']) - parseFloat(state.style['endWidth'])) < tol) + { + state.style['startWidth'] = state.style['endWidth']; + } + } + })); + } + + if (mxUtils.getValue(state.style, mxConstants.STYLE_ENDARROW, mxConstants.NONE) != mxConstants.NONE) + { + handles.push(createEdgeHandle(state, ['width', mxConstants.STYLE_STARTSIZE, mxConstants.STYLE_ENDSIZE], false, function(dist, nx, ny, p0, p1) + { + var w = (state.shape.getEdgeWidth() - state.shape.strokewidth) * state.view.scale; + var l = mxUtils.getNumber(state.style, mxConstants.STYLE_ENDSIZE, mxConstants.ARROW_SIZE / 5) * 3 * state.view.scale; + + return new mxPoint(p0.x + nx * (l + state.shape.strokewidth * state.view.scale) - ny * w / 2, + p0.y + ny * (l + state.shape.strokewidth * state.view.scale) + nx * w / 2); + }, function(dist, nx, ny, p0, p1, pt, me) + { + var w = Math.sqrt(mxUtils.ptSegDistSq(p0.x, p0.y, p1.x, p1.y, pt.x, pt.y)); + var l = mxUtils.ptLineDist(p0.x, p0.y, p0.x + ny, p0.y - nx, pt.x, pt.y); + + state.style[mxConstants.STYLE_ENDSIZE] = Math.round((l - state.shape.strokewidth) * 100 / 3) / 100 / state.view.scale; + state.style['width'] = Math.round(w * 2) / state.view.scale; + + // Applies to opposite side + if (mxEvent.isControlDown(me.getEvent())) + { + state.style[mxConstants.STYLE_STARTSIZE] = state.style[mxConstants.STYLE_ENDSIZE]; + } + + // Snaps to start geometry + if (!mxEvent.isAltDown(me.getEvent())) + { + if (Math.abs(parseFloat(state.style[mxConstants.STYLE_ENDSIZE]) - parseFloat(state.style[mxConstants.STYLE_STARTSIZE])) < tol / 6) + { + state.style[mxConstants.STYLE_ENDSIZE] = state.style[mxConstants.STYLE_STARTSIZE]; + } + } + })); + + handles.push(createEdgeHandle(state, ['startWidth', 'endWidth', mxConstants.STYLE_STARTSIZE, mxConstants.STYLE_ENDSIZE], false, function(dist, nx, ny, p0, p1) + { + var w = (state.shape.getEndArrowWidth() - state.shape.strokewidth) * state.view.scale; + var l = mxUtils.getNumber(state.style, mxConstants.STYLE_ENDSIZE, mxConstants.ARROW_SIZE / 5) * 3 * state.view.scale; + + return new mxPoint(p0.x + nx * (l + state.shape.strokewidth * state.view.scale) - ny * w / 2, + p0.y + ny * (l + state.shape.strokewidth * state.view.scale) + nx * w / 2); + }, function(dist, nx, ny, p0, p1, pt, me) + { + var w = Math.sqrt(mxUtils.ptSegDistSq(p0.x, p0.y, p1.x, p1.y, pt.x, pt.y)); + var l = mxUtils.ptLineDist(p0.x, p0.y, p0.x + ny, p0.y - nx, pt.x, pt.y); + + state.style[mxConstants.STYLE_ENDSIZE] = Math.round((l - state.shape.strokewidth) * 100 / 3) / 100 / state.view.scale; + state.style['endWidth'] = Math.max(0, Math.round(w * 2) - state.shape.getEdgeWidth()) / state.view.scale; + + // Applies to opposite side + if (mxEvent.isControlDown(me.getEvent())) + { + state.style[mxConstants.STYLE_STARTSIZE] = state.style[mxConstants.STYLE_ENDSIZE]; + state.style['startWidth'] = state.style['endWidth']; + } + + // Snaps to start geometry + if (!mxEvent.isAltDown(me.getEvent())) + { + if (Math.abs(parseFloat(state.style[mxConstants.STYLE_ENDSIZE]) - parseFloat(state.style[mxConstants.STYLE_STARTSIZE])) < tol / 6) + { + state.style[mxConstants.STYLE_ENDSIZE] = state.style[mxConstants.STYLE_STARTSIZE]; + } + + if (Math.abs(parseFloat(state.style['endWidth']) - parseFloat(state.style['startWidth'])) < tol) + { + state.style['endWidth'] = state.style['startWidth']; + } + } + })); + } + + return handles; + }, + 'swimlane': function(state) + { + var handles = [createHandle(state, [mxConstants.STYLE_STARTSIZE], function(bounds) + { + var size = parseFloat(mxUtils.getValue(state.style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE)); + + if (mxUtils.getValue(state.style, mxConstants.STYLE_HORIZONTAL, 1) == 1) + { + return new mxPoint(bounds.getCenterX(), bounds.y + Math.max(0, Math.min(bounds.height, size))); + } + else + { + return new mxPoint(bounds.x + Math.max(0, Math.min(bounds.width, size)), bounds.getCenterY()); + } + }, function(bounds, pt) + { + state.style[mxConstants.STYLE_STARTSIZE] = + (mxUtils.getValue(this.state.style, mxConstants.STYLE_HORIZONTAL, 1) == 1) ? + Math.round(Math.max(0, Math.min(bounds.height, pt.y - bounds.y))) : + Math.round(Math.max(0, Math.min(bounds.width, pt.x - bounds.x))); + })]; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED)) + { + var size = parseFloat(mxUtils.getValue(state.style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE)); + handles.push(createArcHandle(state, size / 2)); + } + + return handles; + }, + 'label': createArcHandleFunction(), + 'ext': createArcHandleFunction(), + 'rectangle': createArcHandleFunction(), + 'triangle': createArcHandleFunction(), + 'rhombus': createArcHandleFunction(), + 'umlLifeline': function(state) + { + return [createHandle(state, ['size'], function(bounds) + { + var size = Math.max(0, Math.min(bounds.height, parseFloat(mxUtils.getValue(this.state.style, 'size', UmlLifeline.prototype.size)))); + + return new mxPoint(bounds.getCenterX(), bounds.y + size); + }, function(bounds, pt) + { + this.state.style['size'] = Math.round(Math.max(0, Math.min(bounds.height, pt.y - bounds.y))); + }, false)]; + }, + 'umlFrame': function(state) + { + var handles = [createHandle(state, ['width', 'height'], function(bounds) + { + var w0 = Math.max(UmlFrame.prototype.corner, Math.min(bounds.width, mxUtils.getValue(this.state.style, 'width', UmlFrame.prototype.width))); + var h0 = Math.max(UmlFrame.prototype.corner * 1.5, Math.min(bounds.height, mxUtils.getValue(this.state.style, 'height', UmlFrame.prototype.height))); + + return new mxPoint(bounds.x + w0, bounds.y + h0); + }, function(bounds, pt) + { + this.state.style['width'] = Math.round(Math.max(UmlFrame.prototype.corner, Math.min(bounds.width, pt.x - bounds.x))); + this.state.style['height'] = Math.round(Math.max(UmlFrame.prototype.corner * 1.5, Math.min(bounds.height, pt.y - bounds.y))); + }, false)]; + + return handles; + }, + 'process': function(state) + { + var handles = [createHandle(state, ['size'], function(bounds) + { + var size = Math.max(0, Math.min(0.5, parseFloat(mxUtils.getValue(this.state.style, 'size', ProcessShape.prototype.size)))); + + return new mxPoint(bounds.x + bounds.width * size, bounds.y + bounds.height / 4); + }, function(bounds, pt) + { + this.state.style['size'] = Math.max(0, Math.min(0.5, (pt.x - bounds.x) / bounds.width)); + })]; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false)) + { + handles.push(createArcHandle(state)); + } + + return handles; + }, + 'cross': function(state) + { + return [createHandle(state, ['size'], function(bounds) + { + var m = Math.min(bounds.width, bounds.height); + var size = Math.max(0, Math.min(1, mxUtils.getValue(this.state.style, 'size', CrossShape.prototype.size))) * m / 2; + + return new mxPoint(bounds.getCenterX() - size, bounds.getCenterY() - size); + }, function(bounds, pt) + { + var m = Math.min(bounds.width, bounds.height); + this.state.style['size'] = Math.max(0, Math.min(1, Math.min((Math.max(0, bounds.getCenterY() - pt.y) / m) * 2, + (Math.max(0, bounds.getCenterX() - pt.x) / m) * 2))); + })]; + }, + 'note': function(state) + { + return [createHandle(state, ['size'], function(bounds) + { + var size = Math.max(0, Math.min(bounds.width, Math.min(bounds.height, parseFloat( + mxUtils.getValue(this.state.style, 'size', NoteShape.prototype.size))))); + + return new mxPoint(bounds.x + bounds.width - size, bounds.y + size); + }, function(bounds, pt) + { + this.state.style['size'] = Math.round(Math.max(0, Math.min(Math.min(bounds.width, bounds.x + bounds.width - pt.x), + Math.min(bounds.height, pt.y - bounds.y)))); + })]; + }, + 'manualInput': function(state) + { + var handles = [createHandle(state, ['size'], function(bounds) + { + var size = Math.max(0, Math.min(bounds.height, mxUtils.getValue(this.state.style, 'size', ManualInputShape.prototype.size))); + + return new mxPoint(bounds.x + bounds.width / 4, bounds.y + size * 3 / 4); + }, function(bounds, pt) + { + this.state.style['size'] = Math.round(Math.max(0, Math.min(bounds.height, (pt.y - bounds.y) * 4 / 3))); + })]; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false)) + { + handles.push(createArcHandle(state)); + } + + return handles; + }, + 'dataStorage': function(state) + { + return [createHandle(state, ['size'], function(bounds) + { + var size = Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.state.style, 'size', DataStorageShape.prototype.size)))); + + return new mxPoint(bounds.x + (1 - size) * bounds.width, bounds.getCenterY()); + }, function(bounds, pt) + { + this.state.style['size'] = Math.max(0, Math.min(1, (bounds.x + bounds.width - pt.x) / bounds.width)); + })]; + }, + 'callout': function(state) + { + var handles = [createHandle(state, ['size', 'position'], function(bounds) + { + var size = Math.max(0, Math.min(bounds.height, mxUtils.getValue(this.state.style, 'size', CalloutShape.prototype.size))); + var position = Math.max(0, Math.min(1, mxUtils.getValue(this.state.style, 'position', CalloutShape.prototype.position))); + var base = Math.max(0, Math.min(bounds.width, mxUtils.getValue(this.state.style, 'base', CalloutShape.prototype.base))); + + return new mxPoint(bounds.x + position * bounds.width, bounds.y + bounds.height - size); + }, function(bounds, pt) + { + var base = Math.max(0, Math.min(bounds.width, mxUtils.getValue(this.state.style, 'base', CalloutShape.prototype.base))); + this.state.style['size'] = Math.round(Math.max(0, Math.min(bounds.height, bounds.y + bounds.height - pt.y))); + this.state.style['position'] = Math.round(Math.max(0, Math.min(1, (pt.x - bounds.x) / bounds.width)) * 100) / 100; + }), createHandle(state, ['position2'], function(bounds) + { + var position2 = Math.max(0, Math.min(1, mxUtils.getValue(this.state.style, 'position2', CalloutShape.prototype.position2))); + + return new mxPoint(bounds.x + position2 * bounds.width, bounds.y + bounds.height); + }, function(bounds, pt) + { + this.state.style['position2'] = Math.round(Math.max(0, Math.min(1, (pt.x - bounds.x) / bounds.width)) * 100) / 100; + }), createHandle(state, ['base'], function(bounds) + { + var size = Math.max(0, Math.min(bounds.height, mxUtils.getValue(this.state.style, 'size', CalloutShape.prototype.size))); + var position = Math.max(0, Math.min(1, mxUtils.getValue(this.state.style, 'position', CalloutShape.prototype.position))); + var base = Math.max(0, Math.min(bounds.width, mxUtils.getValue(this.state.style, 'base', CalloutShape.prototype.base))); + + return new mxPoint(bounds.x + Math.min(bounds.width, position * bounds.width + base), bounds.y + bounds.height - size); + }, function(bounds, pt) + { + var position = Math.max(0, Math.min(1, mxUtils.getValue(this.state.style, 'position', CalloutShape.prototype.position))); + + this.state.style['base'] = Math.round(Math.max(0, Math.min(bounds.width, pt.x - bounds.x - position * bounds.width))); + })]; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false)) + { + handles.push(createArcHandle(state)); + } + + return handles; + }, + 'internalStorage': function(state) + { + var handles = [createHandle(state, ['dx', 'dy'], function(bounds) + { + var dx = Math.max(0, Math.min(bounds.width, mxUtils.getValue(this.state.style, 'dx', InternalStorageShape.prototype.dx))); + var dy = Math.max(0, Math.min(bounds.height, mxUtils.getValue(this.state.style, 'dy', InternalStorageShape.prototype.dy))); + + return new mxPoint(bounds.x + dx, bounds.y + dy); + }, function(bounds, pt) + { + this.state.style['dx'] = Math.round(Math.max(0, Math.min(bounds.width, pt.x - bounds.x))); + this.state.style['dy'] = Math.round(Math.max(0, Math.min(bounds.height, pt.y - bounds.y))); + })]; + + if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false)) + { + handles.push(createArcHandle(state)); + } + + return handles; + }, + 'corner': function(state) + { + return [createHandle(state, ['dx', 'dy'], function(bounds) + { + var dx = Math.max(0, Math.min(bounds.width, mxUtils.getValue(this.state.style, 'dx', CornerShape.prototype.dx))); + var dy = Math.max(0, Math.min(bounds.height, mxUtils.getValue(this.state.style, 'dy', CornerShape.prototype.dy))); + + return new mxPoint(bounds.x + dx, bounds.y + dy); + }, function(bounds, pt) + { + this.state.style['dx'] = Math.round(Math.max(0, Math.min(bounds.width, pt.x - bounds.x))); + this.state.style['dy'] = Math.round(Math.max(0, Math.min(bounds.height, pt.y - bounds.y))); + })]; + }, + 'tee': function(state) + { + return [createHandle(state, ['dx', 'dy'], function(bounds) + { + var dx = Math.max(0, Math.min(bounds.width, mxUtils.getValue(this.state.style, 'dx', TeeShape.prototype.dx))); + var dy = Math.max(0, Math.min(bounds.height, mxUtils.getValue(this.state.style, 'dy', TeeShape.prototype.dy))); + + return new mxPoint(bounds.x + (bounds.width + dx) / 2, bounds.y + dy); + }, function(bounds, pt) + { + this.state.style['dx'] = Math.round(Math.max(0, Math.min(bounds.width / 2, (pt.x - bounds.x - bounds.width / 2)) * 2)); + this.state.style['dy'] = Math.round(Math.max(0, Math.min(bounds.height, pt.y - bounds.y))); + })]; + }, + 'singleArrow': createArrowHandleFunction(1), + 'doubleArrow': createArrowHandleFunction(0.5), + 'folder': function(state) + { + return [createHandle(state, ['tabWidth', 'tabHeight'], function(bounds) + { + var tw = Math.max(0, Math.min(bounds.width, mxUtils.getValue(this.state.style, 'tabWidth', FolderShape.prototype.tabWidth))); + var th = Math.max(0, Math.min(bounds.height, mxUtils.getValue(this.state.style, 'tabHeight', FolderShape.prototype.tabHeight))); + + if (mxUtils.getValue(this.state.style, 'tabPosition', FolderShape.prototype.tabPosition) == mxConstants.ALIGN_RIGHT) + { + tw = bounds.width - tw; + } + + return new mxPoint(bounds.x + tw, bounds.y + th); + }, function(bounds, pt) + { + var tw = Math.max(0, Math.min(bounds.width, pt.x - bounds.x)); + + if (mxUtils.getValue(this.state.style, 'tabPosition', FolderShape.prototype.tabPosition) == mxConstants.ALIGN_RIGHT) + { + tw = bounds.width - tw; + } + + this.state.style['tabWidth'] = Math.round(tw); + this.state.style['tabHeight'] = Math.round(Math.max(0, Math.min(bounds.height, pt.y - bounds.y))); + })]; + }, + 'document': function(state) + { + return [createHandle(state, ['size'], function(bounds) + { + var size = Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.state.style, 'size', DocumentShape.prototype.size)))); + + return new mxPoint(bounds.x + 3 * bounds.width / 4, bounds.y + (1 - size) * bounds.height); + }, function(bounds, pt) + { + this.state.style['size'] = Math.max(0, Math.min(1, (bounds.y + bounds.height - pt.y) / bounds.height)); + })]; + }, + 'tape': function(state) + { + return [createHandle(state, ['size'], function(bounds) + { + var size = Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.state.style, 'size', TapeShape.prototype.size)))); + + return new mxPoint(bounds.getCenterX(), bounds.y + size * bounds.height / 2); + }, function(bounds, pt) + { + this.state.style['size'] = Math.max(0, Math.min(1, ((pt.y - bounds.y) / bounds.height) * 2)); + })]; + }, + 'offPageConnector': function(state) + { + return [createHandle(state, ['size'], function(bounds) + { + var size = Math.max(0, Math.min(1, parseFloat(mxUtils.getValue(this.state.style, 'size', OffPageConnectorShape.prototype.size)))); + + return new mxPoint(bounds.getCenterX(), bounds.y + (1 - size) * bounds.height); + }, function(bounds, pt) + { + this.state.style['size'] = Math.max(0, Math.min(1, (bounds.y + bounds.height - pt.y) / bounds.height)); + })]; + }, + 'step': createDisplayHandleFunction(StepShape.prototype.size, true, null, true, StepShape.prototype.fixedSize), + 'hexagon': createDisplayHandleFunction(HexagonShape.prototype.size, true, 0.5, true), + 'curlyBracket': createDisplayHandleFunction(CurlyBracketShape.prototype.size, false), + 'display': createDisplayHandleFunction(DisplayShape.prototype.size, false), + 'cube': createCubeHandleFunction(1, CubeShape.prototype.size, false), + 'card': createCubeHandleFunction(0.5, CardShape.prototype.size, true), + 'loopLimit': createCubeHandleFunction(0.5, LoopLimitShape.prototype.size, true), + 'trapezoid': createTrapezoidHandleFunction(0.5), + 'parallelogram': createTrapezoidHandleFunction(1) + }; + + // Exposes custom handles + Graph.createHandle = createHandle; + Graph.handleFactory = handleFactory; + + mxVertexHandler.prototype.createCustomHandles = function() + { + // Not rotatable means locked + if (this.state.view.graph.getSelectionCount() == 1) + { + if (this.graph.isCellRotatable(this.state.cell)) + // LATER: Make locked state independent of rotatable flag, fix toggle if default is false + //if (this.graph.isCellResizable(this.state.cell) || this.graph.isCellMovable(this.state.cell)) + { + var name = this.state.style['shape']; + + if (mxCellRenderer.defaultShapes[name] == null && + mxStencilRegistry.getStencil(name) == null) + { + name = mxConstants.SHAPE_RECTANGLE; + } + + var fn = handleFactory[name]; + + if (fn == null && this.state.shape != null && this.state.shape.isRoundable()) + { + fn = handleFactory[mxConstants.SHAPE_RECTANGLE]; + } + + if (fn != null) + { + return fn(this.state); + } + } + } + + return null; + }; + + mxEdgeHandler.prototype.createCustomHandles = function() + { + if (this.state.view.graph.getSelectionCount() == 1) + { + var name = this.state.style['shape']; + + if (mxCellRenderer.defaultShapes[name] == null && + mxStencilRegistry.getStencil(name) == null) + { + name = mxConstants.SHAPE_CONNECTOR; + } + + var fn = handleFactory[name]; + + if (fn != null) + { + return fn(this.state); + } + } + + return null; + } + } + else + { + // Dummy entries to avoid NPE in embed mode + Graph.createHandle = function() {}; + Graph.handleFactory = {}; + } + + var isoHVector = new mxPoint(1, 0); + var isoVVector = new mxPoint(1, 0); + + var alpha1 = mxUtils.toRadians(-30); + + var cos1 = Math.cos(alpha1); + var sin1 = Math.sin(alpha1); + + isoHVector = mxUtils.getRotatedPoint(isoHVector, cos1, sin1); + + var alpha2 = mxUtils.toRadians(-150); + + var cos2 = Math.cos(alpha2); + var sin2 = Math.sin(alpha2); + + isoVVector = mxUtils.getRotatedPoint(isoVVector, cos2, sin2); + + mxEdgeStyle.IsometricConnector = function (state, source, target, points, result) + { + var view = state.view; + var pt = (points != null && points.length > 0) ? points[0] : null; + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length-1]; + + if (pt != null) + { + pt = view.transformControlPoint(state, pt); + } + + if (p0 == null) + { + if (source != null) + { + p0 = new mxPoint(source.getCenterX(), source.getCenterY()); + } + } + + if (pe == null) + { + if (target != null) + { + pe = new mxPoint(target.getCenterX(), target.getCenterY()); + } + } + + var a1 = isoHVector.x; + var a2 = isoHVector.y; + + var b1 = isoVVector.x; + var b2 = isoVVector.y; + + var elbow = mxUtils.getValue(state.style, 'elbow', 'horizontal') == 'horizontal'; + + if (pe != null && p0 != null) + { + var last = p0; + + function isoLineTo(x, y, ignoreFirst) + { + var c1 = x - last.x; + var c2 = y - last.y; + + // Solves for isometric base vectors + var h = (b2 * c1 - b1 * c2) / (a1 * b2 - a2 * b1); + var v = (a2 * c1 - a1 * c2) / (a2 * b1 - a1 * b2); + + if (elbow) + { + if (ignoreFirst) + { + last = new mxPoint(last.x + a1 * h, last.y + a2 * h); + result.push(last); + } + + last = new mxPoint(last.x + b1 * v, last.y + b2 * v); + result.push(last); + } + else + { + if (ignoreFirst) + { + last = new mxPoint(last.x + b1 * v, last.y + b2 * v); + result.push(last); + } + + last = new mxPoint(last.x + a1 * h, last.y + a2 * h); + result.push(last); + } + }; + + if (pt == null) + { + pt = new mxPoint(p0.x + (pe.x - p0.x) / 2, p0.y + (pe.y - p0.y) / 2); + } + + isoLineTo(pt.x, pt.y, true); + isoLineTo(pe.x, pe.y, false); + } + }; + + mxStyleRegistry.putValue('isometricEdgeStyle', mxEdgeStyle.IsometricConnector); + + var graphCreateEdgeHandler = Graph.prototype.createEdgeHandler; + Graph.prototype.createEdgeHandler = function(state, edgeStyle) + { + if (edgeStyle == mxEdgeStyle.IsometricConnector) + { + var handler = new mxElbowEdgeHandler(state); + handler.snapToTerminals = false; + + return handler; + } + + return graphCreateEdgeHandler.apply(this, arguments); + }; + + // Defines connection points for all shapes + IsoRectangleShape.prototype.constraints = []; + IsoCubeShape.prototype.constraints = []; + CalloutShape.prototype.constraints = []; + mxRectangleShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.25, 0), true), + new mxConnectionConstraint(new mxPoint(0.5, 0), true), + new mxConnectionConstraint(new mxPoint(0.75, 0), true), + new mxConnectionConstraint(new mxPoint(0, 0.25), true), + new mxConnectionConstraint(new mxPoint(0, 0.5), true), + new mxConnectionConstraint(new mxPoint(0, 0.75), true), + new mxConnectionConstraint(new mxPoint(1, 0.25), true), + new mxConnectionConstraint(new mxPoint(1, 0.5), true), + new mxConnectionConstraint(new mxPoint(1, 0.75), true), + new mxConnectionConstraint(new mxPoint(0.25, 1), true), + new mxConnectionConstraint(new mxPoint(0.5, 1), true), + new mxConnectionConstraint(new mxPoint(0.75, 1), true)]; + mxEllipse.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0), true), new mxConnectionConstraint(new mxPoint(1, 0), true), + new mxConnectionConstraint(new mxPoint(0, 1), true), new mxConnectionConstraint(new mxPoint(1, 1), true), + new mxConnectionConstraint(new mxPoint(0.5, 0), true), new mxConnectionConstraint(new mxPoint(0.5, 1), true), + new mxConnectionConstraint(new mxPoint(0, 0.5), true), new mxConnectionConstraint(new mxPoint(1, 0.5))]; + mxLabel.prototype.constraints = mxRectangleShape.prototype.constraints; + mxImageShape.prototype.constraints = mxRectangleShape.prototype.constraints; + mxSwimlane.prototype.constraints = mxRectangleShape.prototype.constraints; + PlusShape.prototype.constraints = mxRectangleShape.prototype.constraints; + NoteShape.prototype.constraints = mxRectangleShape.prototype.constraints; + CardShape.prototype.constraints = mxRectangleShape.prototype.constraints; + CubeShape.prototype.constraints = mxRectangleShape.prototype.constraints; + FolderShape.prototype.constraints = mxRectangleShape.prototype.constraints; + InternalStorageShape.prototype.constraints = mxRectangleShape.prototype.constraints; + DataStorageShape.prototype.constraints = mxRectangleShape.prototype.constraints; + TapeDataShape.prototype.constraints = mxEllipse.prototype.constraints; + OrEllipseShape.prototype.constraints = mxEllipse.prototype.constraints; + SumEllipseShape.prototype.constraints = mxEllipse.prototype.constraints; + LineEllipseShape.prototype.constraints = mxEllipse.prototype.constraints; + ManualInputShape.prototype.constraints = mxRectangleShape.prototype.constraints; + DelayShape.prototype.constraints = mxRectangleShape.prototype.constraints; + DisplayShape.prototype.constraints = mxRectangleShape.prototype.constraints; + LoopLimitShape.prototype.constraints = mxRectangleShape.prototype.constraints; + OffPageConnectorShape.prototype.constraints = mxRectangleShape.prototype.constraints; + mxCylinder.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.15, 0.05), false), + new mxConnectionConstraint(new mxPoint(0.5, 0), true), + new mxConnectionConstraint(new mxPoint(0.85, 0.05), false), + new mxConnectionConstraint(new mxPoint(0, 0.3), true), + new mxConnectionConstraint(new mxPoint(0, 0.5), true), + new mxConnectionConstraint(new mxPoint(0, 0.7), true), + new mxConnectionConstraint(new mxPoint(1, 0.3), true), + new mxConnectionConstraint(new mxPoint(1, 0.5), true), + new mxConnectionConstraint(new mxPoint(1, 0.7), true), + new mxConnectionConstraint(new mxPoint(0.15, 0.95), false), + new mxConnectionConstraint(new mxPoint(0.5, 1), true), + new mxConnectionConstraint(new mxPoint(0.85, 0.95), false)]; + UmlActorShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.25, 0.1), false), + new mxConnectionConstraint(new mxPoint(0.5, 0), false), + new mxConnectionConstraint(new mxPoint(0.75, 0.1), false), + new mxConnectionConstraint(new mxPoint(0, 1/3), false), + new mxConnectionConstraint(new mxPoint(0, 1), false), + new mxConnectionConstraint(new mxPoint(1, 1/3), false), + new mxConnectionConstraint(new mxPoint(1, 1), false), + new mxConnectionConstraint(new mxPoint(0.5, 0.5), false)]; + ComponentShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.25, 0), true), + new mxConnectionConstraint(new mxPoint(0.5, 0), true), + new mxConnectionConstraint(new mxPoint(0.75, 0), true), + new mxConnectionConstraint(new mxPoint(0, 0.3), true), + new mxConnectionConstraint(new mxPoint(0, 0.7), true), + new mxConnectionConstraint(new mxPoint(1, 0.25), true), + new mxConnectionConstraint(new mxPoint(1, 0.5), true), + new mxConnectionConstraint(new mxPoint(1, 0.75), true), + new mxConnectionConstraint(new mxPoint(0.25, 1), true), + new mxConnectionConstraint(new mxPoint(0.5, 1), true), + new mxConnectionConstraint(new mxPoint(0.75, 1), true)]; + mxActor.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.5, 0), true), + new mxConnectionConstraint(new mxPoint(0.25, 0.2), false), + new mxConnectionConstraint(new mxPoint(0.1, 0.5), false), + new mxConnectionConstraint(new mxPoint(0, 0.75), true), + new mxConnectionConstraint(new mxPoint(0.75, 0.25), false), + new mxConnectionConstraint(new mxPoint(0.9, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 0.75), true), + new mxConnectionConstraint(new mxPoint(0.25, 1), true), + new mxConnectionConstraint(new mxPoint(0.5, 1), true), + new mxConnectionConstraint(new mxPoint(0.75, 1), true)]; + SwitchShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0), false), + new mxConnectionConstraint(new mxPoint(0.5, 0.25), false), + new mxConnectionConstraint(new mxPoint(1, 0), false), + new mxConnectionConstraint(new mxPoint(0.25, 0.5), false), + new mxConnectionConstraint(new mxPoint(0.75, 0.5), false), + new mxConnectionConstraint(new mxPoint(0, 1), false), + new mxConnectionConstraint(new mxPoint(0.5, 0.75), false), + new mxConnectionConstraint(new mxPoint(1, 1), false)]; + TapeShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0.35), false), + new mxConnectionConstraint(new mxPoint(0, 0.5), false), + new mxConnectionConstraint(new mxPoint(0, 0.65), false), + new mxConnectionConstraint(new mxPoint(1, 0.35), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 0.65), false), + new mxConnectionConstraint(new mxPoint(0.25, 1), false), + new mxConnectionConstraint(new mxPoint(0.75, 0), false)]; + StepShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.25, 0), true), + new mxConnectionConstraint(new mxPoint(0.5, 0), true), + new mxConnectionConstraint(new mxPoint(0.75, 0), true), + new mxConnectionConstraint(new mxPoint(0.25, 1), true), + new mxConnectionConstraint(new mxPoint(0.5, 1), true), + new mxConnectionConstraint(new mxPoint(0.75, 1), true), + new mxConnectionConstraint(new mxPoint(0, 0.25), true), + new mxConnectionConstraint(new mxPoint(0, 0.5), true), + new mxConnectionConstraint(new mxPoint(0, 0.75), true), + new mxConnectionConstraint(new mxPoint(1, 0.25), true), + new mxConnectionConstraint(new mxPoint(1, 0.5), true), + new mxConnectionConstraint(new mxPoint(1, 0.75), true)]; + mxLine.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0.5), false), + new mxConnectionConstraint(new mxPoint(0.25, 0.5), false), + new mxConnectionConstraint(new mxPoint(0.75, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false)]; + LollipopShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.5, 0), false), + new mxConnectionConstraint(new mxPoint(0.5, 1), false)]; + mxDoubleEllipse.prototype.constraints = mxEllipse.prototype.constraints; + mxRhombus.prototype.constraints = mxEllipse.prototype.constraints; + mxTriangle.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0.25), true), + new mxConnectionConstraint(new mxPoint(0, 0.5), true), + new mxConnectionConstraint(new mxPoint(0, 0.75), true), + new mxConnectionConstraint(new mxPoint(0.5, 0), true), + new mxConnectionConstraint(new mxPoint(0.5, 1), true), + new mxConnectionConstraint(new mxPoint(1, 0.5), true)]; + mxHexagon.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.375, 0), true), + new mxConnectionConstraint(new mxPoint(0.5, 0), true), + new mxConnectionConstraint(new mxPoint(0.625, 0), true), + new mxConnectionConstraint(new mxPoint(0, 0.25), true), + new mxConnectionConstraint(new mxPoint(0, 0.5), true), + new mxConnectionConstraint(new mxPoint(0, 0.75), true), + new mxConnectionConstraint(new mxPoint(1, 0.25), true), + new mxConnectionConstraint(new mxPoint(1, 0.5), true), + new mxConnectionConstraint(new mxPoint(1, 0.75), true), + new mxConnectionConstraint(new mxPoint(0.375, 1), true), + new mxConnectionConstraint(new mxPoint(0.5, 1), true), + new mxConnectionConstraint(new mxPoint(0.625, 1), true)]; + mxCloud.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.25, 0.25), false), + new mxConnectionConstraint(new mxPoint(0.4, 0.1), false), + new mxConnectionConstraint(new mxPoint(0.16, 0.55), false), + new mxConnectionConstraint(new mxPoint(0.07, 0.4), false), + new mxConnectionConstraint(new mxPoint(0.31, 0.8), false), + new mxConnectionConstraint(new mxPoint(0.13, 0.77), false), + new mxConnectionConstraint(new mxPoint(0.8, 0.8), false), + new mxConnectionConstraint(new mxPoint(0.55, 0.95), false), + new mxConnectionConstraint(new mxPoint(0.875, 0.5), false), + new mxConnectionConstraint(new mxPoint(0.96, 0.7), false), + new mxConnectionConstraint(new mxPoint(0.625, 0.2), false), + new mxConnectionConstraint(new mxPoint(0.88, 0.25), false)]; + ParallelogramShape.prototype.constraints = mxRectangleShape.prototype.constraints; + TrapezoidShape.prototype.constraints = mxRectangleShape.prototype.constraints; + DocumentShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.25, 0), true), + new mxConnectionConstraint(new mxPoint(0.5, 0), true), + new mxConnectionConstraint(new mxPoint(0.75, 0), true), + new mxConnectionConstraint(new mxPoint(0, 0.25), true), + new mxConnectionConstraint(new mxPoint(0, 0.5), true), + new mxConnectionConstraint(new mxPoint(0, 0.75), true), + new mxConnectionConstraint(new mxPoint(1, 0.25), true), + new mxConnectionConstraint(new mxPoint(1, 0.5), true), + new mxConnectionConstraint(new mxPoint(1, 0.75), true)]; + mxArrow.prototype.constraints = null; + TeeShape.prototype.constraints = null; + CornerShape.prototype.constraints = null; + CrossbarShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0), false), + new mxConnectionConstraint(new mxPoint(0, 0.5), false), + new mxConnectionConstraint(new mxPoint(0, 1), false), + new mxConnectionConstraint(new mxPoint(0.25, 0.5), false), + new mxConnectionConstraint(new mxPoint(0.5, 0.5), false), + new mxConnectionConstraint(new mxPoint(0.75, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 0), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 1), false)]; + + SingleArrowShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false)]; + DoubleArrowShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false)]; + CrossShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false), + new mxConnectionConstraint(new mxPoint(0.5, 0), false), + new mxConnectionConstraint(new mxPoint(0.5, 1), false)]; + UmlLifeline.prototype.constraints = null; + OrShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0.25), false), + new mxConnectionConstraint(new mxPoint(0, 0.5), false), + new mxConnectionConstraint(new mxPoint(0, 0.75), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false), + new mxConnectionConstraint(new mxPoint(0.7, 0.1), false), + new mxConnectionConstraint(new mxPoint(0.7, 0.9), false)]; + XorShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0.175, 0.25), false), + new mxConnectionConstraint(new mxPoint(0.25, 0.5), false), + new mxConnectionConstraint(new mxPoint(0.175, 0.75), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false), + new mxConnectionConstraint(new mxPoint(0.7, 0.1), false), + new mxConnectionConstraint(new mxPoint(0.7, 0.9), false)]; + RequiredInterfaceShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false)]; + ProvidedRequiredInterfaceShape.prototype.constraints = [new mxConnectionConstraint(new mxPoint(0, 0.5), false), + new mxConnectionConstraint(new mxPoint(1, 0.5), false)]; +})(); diff --git a/media/grapheditor/js/Sidebar.js b/media/grapheditor/js/Sidebar.js new file mode 100644 index 0000000000..9de847b183 --- /dev/null +++ b/media/grapheditor/js/Sidebar.js @@ -0,0 +1,3620 @@ +/** + * Copyright (c) 2006-2012, JGraph Ltd + */ +/** + * Construcs a new sidebar for the given editor. + */ +function Sidebar(editorUi, container) +{ + this.editorUi = editorUi; + this.container = container; + this.palettes = new Object(); + this.taglist = new Object(); + this.showTooltips = true; + this.graph = editorUi.createTemporaryGraph(this.editorUi.editor.graph.getStylesheet()); + this.graph.cellRenderer.antiAlias = false; + this.graph.foldingEnabled = false; + this.graph.container.style.visibility = 'hidden'; + document.body.appendChild(this.graph.container); + + this.pointerUpHandler = mxUtils.bind(this, function() + { + this.showTooltips = true; + }); + + mxEvent.addListener(document, (mxClient.IS_POINTER) ? 'pointerup' : 'mouseup', this.pointerUpHandler); + + this.pointerDownHandler = mxUtils.bind(this, function() + { + this.showTooltips = false; + this.hideTooltip(); + }); + + mxEvent.addListener(document, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown', this.pointerDownHandler); + + this.pointerMoveHandler = mxUtils.bind(this, function(evt) + { + var src = mxEvent.getSource(evt); + + while (src != null) + { + if (src == this.currentElt) + { + return; + } + + src = src.parentNode; + } + + this.hideTooltip(); + }); + + mxEvent.addListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.pointerMoveHandler); + + // Handles mouse leaving the window + this.pointerOutHandler = mxUtils.bind(this, function(evt) + { + if (evt.toElement == null && evt.relatedTarget == null) + { + this.hideTooltip(); + } + }); + + mxEvent.addListener(document, (mxClient.IS_POINTER) ? 'pointerout' : 'mouseout', this.pointerOutHandler); + + // Enables tooltips after scroll + mxEvent.addListener(container, 'scroll', mxUtils.bind(this, function() + { + this.showTooltips = true; + this.hideTooltip(); + })); + + this.init(); + + // Pre-fetches tooltip image + if (!mxClient.IS_SVG) + { + new Image().src = IMAGE_PATH + '/tooltip.png'; + } +}; + +/** + * Adds all palettes to the sidebar. + */ +Sidebar.prototype.init = function() +{ + var dir = STENCIL_PATH; + + this.addSearchPalette(true); + this.addGeneralPalette(true); + this.addMiscPalette(false); + this.addAdvancedPalette(false); + this.addBasicPalette(dir); + this.addStencilPalette('arrows', mxResources.get('arrows'), dir + '/arrows.xml', + ';whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#000000;strokeWidth=2'); + this.addUmlPalette(false); + this.addBpmnPalette(dir, false); + this.addStencilPalette('flowchart', 'Flowchart', dir + '/flowchart.xml', + ';whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#000000;strokeWidth=2'); + this.addImagePalette('clipart', mxResources.get('clipart'), dir + '/clipart/', '_128x128.png', + ['Earth_globe', 'Empty_Folder', 'Full_Folder', 'Gear', 'Lock', 'Software', 'Virus', 'Email', + 'Database', 'Router_Icon', 'iPad', 'iMac', 'Laptop', 'MacBook', 'Monitor_Tower', 'Printer', + 'Server_Tower', 'Workstation', 'Firewall_02', 'Wireless_Router_N', 'Credit_Card', + 'Piggy_Bank', 'Graph', 'Safe', 'Shopping_Cart', 'Suit1', 'Suit2', 'Suit3', 'Pilot1', + 'Worker1', 'Soldier1', 'Doctor1', 'Tech1', 'Security1', 'Telesales1'], null, + {'Wireless_Router_N': 'wireless router switch wap wifi access point wlan', + 'Router_Icon': 'router switch'}); +}; + +/** + * Sets the default font size. + */ +Sidebar.prototype.collapsedImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/collapsed.gif' : 'data:image/gif;base64,R0lGODlhDQANAIABAJmZmf///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjAgNjEuMTM0Nzc3LCAyMDEwLzAyLzEyLTE3OjMyOjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozNUQyRTJFNjZGNUYxMUU1QjZEOThCNDYxMDQ2MzNCQiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozNUQyRTJFNzZGNUYxMUU1QjZEOThCNDYxMDQ2MzNCQiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjFERjc3MEUxNkY1RjExRTVCNkQ5OEI0NjEwNDYzM0JCIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjFERjc3MEUyNkY1RjExRTVCNkQ5OEI0NjEwNDYzM0JCIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAAQAsAAAAAA0ADQAAAhSMj6lrwAjcC1GyahV+dcZJgeIIFgA7'; + +/** + * Sets the default font size. + */ +Sidebar.prototype.expandedImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/expanded.gif' : 'data:image/gif;base64,R0lGODlhDQANAIABAJmZmf///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjAgNjEuMTM0Nzc3LCAyMDEwLzAyLzEyLTE3OjMyOjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxREY3NzBERjZGNUYxMUU1QjZEOThCNDYxMDQ2MzNCQiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxREY3NzBFMDZGNUYxMUU1QjZEOThCNDYxMDQ2MzNCQiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjFERjc3MERENkY1RjExRTVCNkQ5OEI0NjEwNDYzM0JCIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjFERjc3MERFNkY1RjExRTVCNkQ5OEI0NjEwNDYzM0JCIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAAQAsAAAAAA0ADQAAAhGMj6nL3QAjVHIu6azbvPtWAAA7'; + +/** + * Sets the default font size. + */ +Sidebar.prototype.tooltipImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/tooltip.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAbCAMAAAB7jU7LAAAACVBMVEX///+ZmZn///9Y2COLAAAAA3RSTlP//wDXyg1BAAAAOElEQVR42mXQMQ4AMAgDsWv//+iutcJmIQSk+9dJpVKpVCqVSqVSqZTdncWzF8/NeP7FkxWenPEDOnUBiL3jWx0AAAAASUVORK5CYII='; + +/** + * + */ +Sidebar.prototype.searchImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/search.png' : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAEaSURBVHjabNGxS5VxFIfxz71XaWuQUJCG/gCHhgTD9VpEETg4aMOlQRp0EoezObgcd220KQiXmpretTAHQRBdojlQEJyukPdt+b1ywfvAGc7wnHP4nlZd1yKijQW8xzNc4Su+ZOYfQ3T6/f4YNvEJYzjELXp4VVXVz263+7cR2niBxAFeZ2YPi3iHR/gYERPDwhpOsd6sz8x/mfkNG3iOlWFhFj8y89J9KvzGXER0GuEaD42mgwHqUtoljbcRsTBCeINpfM/MgZLKPpaxFxGbOCqDXmILN7hoJrTKH+axhxmcYRxP0MIDnOBDZv5q1XUNIuJxifJp+UNV7t7BFM6xeic0RMQ4Bpl5W/ol7GISx/eEUUTECrbx+f8A8xhiZht9zsgAAAAASUVORK5CYII='; + +/** + * + */ +Sidebar.prototype.dragPreviewBorder = '1px dashed black'; + +/** + * Specifies if tooltips should be visible. Default is true. + */ +Sidebar.prototype.enableTooltips = true; + +/** + * Specifies the delay for the tooltip. Default is 16 px. + */ +Sidebar.prototype.tooltipBorder = 16; + +/** + * Specifies the delay for the tooltip. Default is 300 ms. + */ +Sidebar.prototype.tooltipDelay = 300; + +/** + * Specifies the delay for the drop target icons. Default is 200 ms. + */ +Sidebar.prototype.dropTargetDelay = 200; + +/** + * Specifies the URL of the gear image. + */ +Sidebar.prototype.gearImage = STENCIL_PATH + '/clipart/Gear_128x128.png'; + +/** + * Specifies the width of the thumbnails. + */ +Sidebar.prototype.thumbWidth = 36; + +/** + * Specifies the height of the thumbnails. + */ +Sidebar.prototype.thumbHeight = 36; + +/** + * Specifies the padding for the thumbnails. Default is 3. + */ +Sidebar.prototype.thumbPadding = (document.documentMode >= 5) ? 0 : 1; + +/** + * Specifies the delay for the tooltip. Default is 2 px. + */ +Sidebar.prototype.thumbBorder = 2; + +/** + * Specifies the size of the sidebar titles. + */ +Sidebar.prototype.sidebarTitleSize = 9; + +/** + * Specifies if titles in the sidebar should be enabled. + */ +Sidebar.prototype.sidebarTitles = false; + +/** + * Specifies if titles in the tooltips should be enabled. + */ +Sidebar.prototype.tooltipTitles = true; + +/** + * Specifies if titles in the tooltips should be enabled. + */ +Sidebar.prototype.maxTooltipWidth = 400; + +/** + * Specifies if titles in the tooltips should be enabled. + */ +Sidebar.prototype.maxTooltipHeight = 400; + +/** + * Specifies if stencil files should be loaded and added to the search index + * when stencil palettes are added. If this is false then the stencil files + * are lazy-loaded when the palette is shown. + */ +Sidebar.prototype.addStencilsToIndex = true; + +/** + * Specifies the width for clipart images. Default is 80. + */ +Sidebar.prototype.defaultImageWidth = 80; + +/** + * Specifies the height for clipart images. Default is 80. + */ +Sidebar.prototype.defaultImageHeight = 80; + +/** + * Adds all palettes to the sidebar. + */ +Sidebar.prototype.getTooltipOffset = function() +{ + return new mxPoint(0, 0); +}; + +/** + * Adds all palettes to the sidebar. + */ +Sidebar.prototype.showTooltip = function(elt, cells, w, h, title, showLabel) +{ + if (this.enableTooltips && this.showTooltips) + { + if (this.currentElt != elt) + { + if (this.thread != null) + { + window.clearTimeout(this.thread); + this.thread = null; + } + + var show = mxUtils.bind(this, function() + { + // Lazy creation of the DOM nodes and graph instance + if (this.tooltip == null) + { + this.tooltip = document.createElement('div'); + this.tooltip.className = 'geSidebarTooltip'; + this.tooltip.style.zIndex = mxPopupMenu.prototype.zIndex - 1; + document.body.appendChild(this.tooltip); + + this.graph2 = new Graph(this.tooltip, null, null, this.editorUi.editor.graph.getStylesheet()); + this.graph2.resetViewOnRootChange = false; + this.graph2.foldingEnabled = false; + this.graph2.gridEnabled = false; + this.graph2.autoScroll = false; + this.graph2.setTooltips(false); + this.graph2.setConnectable(false); + this.graph2.setEnabled(false); + + if (!mxClient.IS_SVG) + { + this.graph2.view.canvas.style.position = 'relative'; + } + + this.tooltipImage = mxUtils.createImage(this.tooltipImage); + this.tooltipImage.className = 'geSidebarTooltipImage'; + this.tooltipImage.style.zIndex = mxPopupMenu.prototype.zIndex - 1; + this.tooltipImage.style.position = 'absolute'; + this.tooltipImage.style.width = '14px'; + this.tooltipImage.style.height = '27px'; + + document.body.appendChild(this.tooltipImage); + } + + this.graph2.model.clear(); + this.graph2.view.setTranslate(this.tooltipBorder, this.tooltipBorder); + + if (w > this.maxTooltipWidth || h > this.maxTooltipHeight) + { + this.graph2.view.scale = Math.round(Math.min(this.maxTooltipWidth / w, this.maxTooltipHeight / h) * 100) / 100; + } + else + { + this.graph2.view.scale = 1; + } + + this.tooltip.style.display = 'block'; + this.graph2.labelsVisible = (showLabel == null || showLabel); + var fo = mxClient.NO_FO; + mxClient.NO_FO = Editor.prototype.originalNoForeignObject; + this.graph2.addCells(cells); + mxClient.NO_FO = fo; + + var bounds = this.graph2.getGraphBounds(); + var width = bounds.width + 2 * this.tooltipBorder + 4; + var height = bounds.height + 2 * this.tooltipBorder; + + if (mxClient.IS_QUIRKS) + { + height += 4; + this.tooltip.style.overflow = 'hidden'; + } + else + { + this.tooltip.style.overflow = 'visible'; + } + + this.tooltipImage.style.visibility = 'visible'; + this.tooltip.style.width = width + 'px'; + + // Adds title for entry + if (this.tooltipTitles && title != null && title.length > 0) + { + if (this.tooltipTitle == null) + { + this.tooltipTitle = document.createElement('div'); + this.tooltipTitle.style.borderTop = '1px solid gray'; + this.tooltipTitle.style.textAlign = 'center'; + this.tooltipTitle.style.width = '100%'; + + // Oversize titles are cut-off currently. Should make tooltip wider later. + this.tooltipTitle.style.overflow = 'hidden'; + this.tooltipTitle.style.position = 'absolute'; + this.tooltipTitle.style.paddingTop = '6px'; + this.tooltipTitle.style.bottom = '6px'; + + this.tooltip.appendChild(this.tooltipTitle); + } + else + { + this.tooltipTitle.innerHTML = ''; + } + + this.tooltipTitle.style.display = ''; + mxUtils.write(this.tooltipTitle, title); + + var ddy = this.tooltipTitle.offsetHeight + 10; + height += ddy; + + if (mxClient.IS_SVG) + { + this.tooltipTitle.style.marginTop = (2 - ddy) + 'px'; + } + else + { + height -= 6; + this.tooltipTitle.style.top = (height - ddy) + 'px'; + } + } + else if (this.tooltipTitle != null && this.tooltipTitle.parentNode != null) + { + this.tooltipTitle.style.display = 'none'; + } + + this.tooltip.style.height = height + 'px'; + var x0 = -Math.round(bounds.x - this.tooltipBorder); + var y0 = -Math.round(bounds.y - this.tooltipBorder); + + var b = document.body; + var d = document.documentElement; + var off = this.getTooltipOffset(); + var bottom = Math.max(b.clientHeight || 0, d.clientHeight); + var left = this.container.clientWidth + this.editorUi.splitSize + 3 + this.editorUi.container.offsetLeft + off.x; + var top = Math.min(bottom - height - 20 /*status bar*/, Math.max(0, (this.editorUi.container.offsetTop + + this.container.offsetTop + elt.offsetTop - this.container.scrollTop - height / 2 + 16))) + off.y; + + if (mxClient.IS_SVG) + { + if (x0 != 0 || y0 != 0) + { + this.graph2.view.canvas.setAttribute('transform', 'translate(' + x0 + ',' + y0 + ')'); + } + else + { + this.graph2.view.canvas.removeAttribute('transform'); + } + } + else + { + this.graph2.view.drawPane.style.left = x0 + 'px'; + this.graph2.view.drawPane.style.top = y0 + 'px'; + } + + // Workaround for ignored position CSS style in IE9 + // (changes to relative without the following line) + this.tooltip.style.position = 'absolute'; + this.tooltip.style.left = left + 'px'; + this.tooltip.style.top = top + 'px'; + this.tooltipImage.style.left = (left - 13) + 'px'; + this.tooltipImage.style.top = (top + height / 2 - 13) + 'px'; + }); + + if (this.tooltip != null && this.tooltip.style.display != 'none') + { + show(); + } + else + { + this.thread = window.setTimeout(show, this.tooltipDelay); + } + + this.currentElt = elt; + } + } +}; + +/** + * Hides the current tooltip. + */ +Sidebar.prototype.hideTooltip = function() +{ + if (this.thread != null) + { + window.clearTimeout(this.thread); + this.thread = null; + } + + if (this.tooltip != null) + { + this.tooltip.style.display = 'none'; + this.tooltipImage.style.visibility = 'hidden'; + this.currentElt = null; + } +}; + +/** + * Hides the current tooltip. + */ +Sidebar.prototype.addDataEntry = function(tags, width, height, title, data) +{ + return this.addEntry(tags, mxUtils.bind(this, function() + { + return this.createVertexTemplateFromData(data, width, height, title); + })); +}; + +/** + * Hides the current tooltip. + */ +Sidebar.prototype.addEntry = function(tags, fn) +{ + if (this.taglist != null && tags != null && tags.length > 0) + { + // Replaces special characters + var tmp = tags.toLowerCase().replace(/[\/\,\(\)]/g, ' ').split(' '); + + var doAddEntry = mxUtils.bind(this, function(tag) + { + if (tag.length > 1) + { + var entry = this.taglist[tag]; + + if (typeof entry !== 'object') + { + entry = {entries: [], dict: new mxDictionary()}; + this.taglist[tag] = entry; + } + + // Ignores duplicates + if (entry.dict.get(fn) == null) + { + entry.dict.put(fn, fn); + entry.entries.push(fn); + } + } + }); + + for (var i = 0; i < tmp.length; i++) + { + doAddEntry(tmp[i]); + + // Adds additional entry with removed trailing numbers + var normalized = tmp[i].replace(/\.*\d*$/, ''); + + if (normalized != tmp[i]) + { + doAddEntry(normalized); + } + } + } + + return fn; +}; + +/** + * Adds shape search UI. + */ +Sidebar.prototype.searchEntries = function(searchTerms, count, page, success, error) +{ + if (this.taglist != null && searchTerms != null) + { + var tmp = searchTerms.toLowerCase().split(' '); + var dict = new mxDictionary(); + var max = (page + 1) * count; + var results = []; + var index = 0; + + for (var i = 0; i < tmp.length; i++) + { + if (tmp[i].length > 0) + { + var entry = this.taglist[tmp[i]]; + var tmpDict = new mxDictionary(); + + if (entry != null) + { + var arr = entry.entries; + results = []; + + for (var j = 0; j < arr.length; j++) + { + var entry = arr[j]; + + // NOTE Array does not contain duplicates + if ((index == 0) == (dict.get(entry) == null)) + { + tmpDict.put(entry, entry); + results.push(entry); + + if (i == tmp.length - 1 && results.length == max) + { + success(results.slice(page * count, max), max, true, tmp); + + return; + } + } + } + } + else + { + results = []; + } + + dict = tmpDict; + index++; + } + } + + var len = results.length; + success(results.slice(page * count, (page + 1) * count), len, false, tmp); + } + else + { + success([], null, null, tmp); + } +}; + +/** + * Adds shape search UI. + */ +Sidebar.prototype.filterTags = function(tags) +{ + if (tags != null) + { + var arr = tags.split(' '); + var result = []; + var hash = {}; + + // Ignores tags with leading numbers, strips trailing numbers + for (var i = 0; i < arr.length; i++) + { + // Removes duplicates + if (hash[arr[i]] == null) + { + hash[arr[i]] = '1'; + result.push(arr[i]); + } + } + + return result.join(' '); + } + + return null; +}; + +/** + * Adds the general palette to the sidebar. + */ +Sidebar.prototype.cloneCell = function(cell, value) +{ + var clone = cell.clone(); + + if (value != null) + { + clone.value = value; + } + + return clone; +}; + +/** + * Adds shape search UI. + */ +Sidebar.prototype.addSearchPalette = function(expand) +{ + var elt = document.createElement('div'); + elt.style.visibility = 'hidden'; + this.container.appendChild(elt); + + var div = document.createElement('div'); + div.className = 'geSidebar'; + div.style.boxSizing = 'border-box'; + div.style.overflow = 'hidden'; + div.style.width = '100%'; + div.style.padding = '8px'; + div.style.paddingTop = '14px'; + div.style.paddingBottom = '0px'; + + if (!expand) + { + div.style.display = 'none'; + } + + var inner = document.createElement('div'); + inner.style.whiteSpace = 'nowrap'; + inner.style.textOverflow = 'clip'; + inner.style.paddingBottom = '8px'; + inner.style.cursor = 'default'; + + var input = document.createElement('input'); + input.setAttribute('placeholder', mxResources.get('searchShapes')); + input.setAttribute('type', 'text'); + input.style.fontSize = '12px'; + input.style.overflow = 'hidden'; + input.style.boxSizing = 'border-box'; + input.style.border = 'solid 1px #d5d5d5'; + input.style.borderRadius = '4px'; + input.style.width = '100%'; + input.style.outline = 'none'; + input.style.padding = '6px'; + inner.appendChild(input); + + var cross = document.createElement('img'); + cross.setAttribute('src', Sidebar.prototype.searchImage); + cross.setAttribute('title', mxResources.get('search')); + cross.style.position = 'relative'; + cross.style.left = '-18px'; + + if (mxClient.IS_QUIRKS) + { + input.style.height = '28px'; + cross.style.top = '-4px'; + } + else + { + cross.style.top = '1px'; + } + + // Needed to block event transparency in IE + cross.style.background = 'url(\'' + this.editorUi.editor.transparentImage + '\')'; + + var find; + + inner.appendChild(cross); + div.appendChild(inner); + + var center = document.createElement('center'); + var button = mxUtils.button(mxResources.get('moreResults'), function() + { + find(); + }); + button.style.display = 'none'; + + // Workaround for inherited line-height in quirks mode + button.style.lineHeight = 'normal'; + button.style.marginTop = '4px'; + button.style.marginBottom = '8px'; + center.style.paddingTop = '4px'; + center.style.paddingBottom = '8px'; + + center.appendChild(button); + div.appendChild(center); + + var searchTerm = ''; + var active = false; + var complete = false; + var page = 0; + var hash = new Object(); + + // Count is dynamically updated below + var count = 12; + + var clearDiv = mxUtils.bind(this, function() + { + active = false; + this.currentSearch = null; + var child = div.firstChild; + + while (child != null) + { + var next = child.nextSibling; + + if (child != inner && child != center) + { + child.parentNode.removeChild(child); + } + + child = next; + } + }); + + mxEvent.addListener(cross, 'click', function() + { + if (cross.getAttribute('src') == Dialog.prototype.closeImage) + { + cross.setAttribute('src', Sidebar.prototype.searchImage); + cross.setAttribute('title', mxResources.get('search')); + button.style.display = 'none'; + input.value = ''; + searchTerm = ''; + clearDiv(); + } + + input.focus(); + }); + + find = mxUtils.bind(this, function() + { + // Shows 4 rows (minimum 4 results) + count = 4 * Math.max(1, Math.floor(this.container.clientWidth / (this.thumbWidth + 10))); + this.hideTooltip(); + + if (input.value != '') + { + if (center.parentNode != null) + { + if (searchTerm != input.value) + { + clearDiv(); + searchTerm = input.value; + hash = new Object(); + complete = false; + page = 0; + } + + if (!active && !complete) + { + button.setAttribute('disabled', 'true'); + button.style.display = ''; + button.style.cursor = 'wait'; + button.innerHTML = mxResources.get('loading') + '...'; + active = true; + + // Ignores old results + var current = new Object(); + this.currentSearch = current; + + this.searchEntries(searchTerm, count, page, mxUtils.bind(this, function(results, len, more, terms) + { + if (this.currentSearch == current) + { + results = (results != null) ? results : []; + active = false; + page++; + center.parentNode.removeChild(center); + this.insertSearchHint(div, searchTerm, count, page, results, len, more, terms); + + for (var i = 0; i < results.length; i++) + { + var elt = results[i](); + + // Avoids duplicates in results + if (hash[elt.innerHTML] == null) + { + hash[elt.innerHTML] = '1'; + div.appendChild(results[i]()); + } + } + + if (more) + { + button.removeAttribute('disabled'); + button.innerHTML = mxResources.get('moreResults'); + } + else + { + button.innerHTML = mxResources.get('reset'); + button.style.display = 'none'; + complete = true; + } + + button.style.cursor = ''; + div.appendChild(center); + } + }), mxUtils.bind(this, function() + { + // TODO: Error handling + button.style.cursor = ''; + })); + } + } + } + else + { + clearDiv(); + input.value = ''; + searchTerm = ''; + hash = new Object(); + button.style.display = 'none'; + complete = false; + input.focus(); + } + }); + + mxEvent.addListener(input, 'keydown', mxUtils.bind(this, function(evt) + { + if (evt.keyCode == 13 /* Enter */) + { + find(); + mxEvent.consume(evt); + } + })); + + mxEvent.addListener(input, 'focus', function() + { + input.style.paddingRight = ''; + }); + + mxEvent.addListener(input, 'blur', function() + { + input.style.paddingRight = '20px'; + }); + + input.style.paddingRight = '20px'; + + mxEvent.addListener(input, 'keyup', mxUtils.bind(this, function(evt) + { + if (input.value == '') + { + cross.setAttribute('src', Sidebar.prototype.searchImage); + cross.setAttribute('title', mxResources.get('search')); + } + else + { + cross.setAttribute('src', Dialog.prototype.closeImage); + cross.setAttribute('title', mxResources.get('reset')); + } + + if (input.value == '') + { + complete = true; + button.style.display = 'none'; + } + else if (input.value != searchTerm) + { + button.style.display = 'none'; + complete = false; + } + else if (!active) + { + if (complete) + { + button.style.display = 'none'; + } + else + { + button.style.display = ''; + } + } + })); + + // Workaround for blocked text selection in Editor + mxEvent.addListener(input, 'mousedown', function(evt) + { + if (evt.stopPropagation) + { + evt.stopPropagation(); + } + + evt.cancelBubble = true; + }); + + // Workaround for blocked text selection in Editor + mxEvent.addListener(input, 'selectstart', function(evt) + { + if (evt.stopPropagation) + { + evt.stopPropagation(); + } + + evt.cancelBubble = true; + }); + + var outer = document.createElement('div'); + outer.appendChild(div); + this.container.appendChild(outer); + + // Keeps references to the DOM nodes + this.palettes['search'] = [elt, outer]; +}; + +/** + * Adds the general palette to the sidebar. + */ +Sidebar.prototype.insertSearchHint = function(div, searchTerm, count, page, results, len, more, terms) +{ + if (results.length == 0 && page == 1) + { + var err = document.createElement('div'); + err.className = 'geTitle'; + err.style.cssText = 'background-color:transparent;border-color:transparent;' + + 'color:gray;padding:6px 0px 0px 0px !important;margin:4px 8px 4px 8px;' + + 'text-align:center;cursor:default !important'; + + mxUtils.write(err, mxResources.get('noResultsFor', [searchTerm])); + div.appendChild(err); + } +}; + +/** + * Adds the general palette to the sidebar. + */ +Sidebar.prototype.addGeneralPalette = function(expand) +{ + var lineTags = 'line lines connector connectors connection connections arrow arrows '; + + var fns = [ + this.createVertexTemplateEntry('rounded=0;whiteSpace=wrap;html=1;', 120, 60, '', 'Rectangle', null, null, 'rect rectangle box'), + this.createVertexTemplateEntry('rounded=1;whiteSpace=wrap;html=1;', 120, 60, '', 'Rounded Rectangle', null, null, 'rounded rect rectangle box'), + // Explicit strokecolor/fillcolor=none is a workaround to maintain transparent background regardless of current style + this.createVertexTemplateEntry('text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;', + 40, 20, 'Text', 'Text', null, null, 'text textbox textarea label'), + this.createVertexTemplateEntry('text;html=1;strokeColor=none;fillColor=none;spacing=5;spacingTop=-20;whiteSpace=wrap;overflow=hidden;rounded=0;', 190, 120, + '

Heading

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

', + 'Textbox', null, null, 'text textbox textarea'), + this.createVertexTemplateEntry('ellipse;whiteSpace=wrap;html=1;', 120, 80, '', 'Ellipse', null, null, 'oval ellipse state'), + this.createVertexTemplateEntry('whiteSpace=wrap;html=1;aspect=fixed;', 80, 80, '', 'Square', null, null, 'square'), + this.createVertexTemplateEntry('ellipse;whiteSpace=wrap;html=1;aspect=fixed;', 80, 80, '', 'Circle', null, null, 'circle'), + this.createVertexTemplateEntry('shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;', 120, 60, '', 'Process', null, null, 'process task'), + this.createVertexTemplateEntry('rhombus;whiteSpace=wrap;html=1;', 80, 80, '', 'Diamond', null, null, 'diamond rhombus if condition decision conditional question test'), + this.createVertexTemplateEntry('shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;', 120, 60, '', 'Parallelogram'), + this.createVertexTemplateEntry('shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;', 120, 80, '', 'Hexagon', null, null, 'hexagon preparation'), + this.createVertexTemplateEntry('triangle;whiteSpace=wrap;html=1;', 60, 80, '', 'Triangle', null, null, 'triangle logic inverter buffer'), + this.createVertexTemplateEntry('shape=cylinder;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;', 60, 80, '', 'Cylinder', null, null, 'cylinder data database'), + this.createVertexTemplateEntry('ellipse;shape=cloud;whiteSpace=wrap;html=1;', 120, 80, '', 'Cloud', null, null, 'cloud network'), + this.createVertexTemplateEntry('shape=document;whiteSpace=wrap;html=1;boundedLbl=1;', 120, 80, '', 'Document'), + this.createVertexTemplateEntry('shape=internalStorage;whiteSpace=wrap;html=1;backgroundOutline=1;', 80, 80, '', 'Internal Storage'), + this.createVertexTemplateEntry('shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;', 120, 80, '', 'Cube'), + this.createVertexTemplateEntry('shape=step;perimeter=stepPerimeter;whiteSpace=wrap;html=1;fixedSize=1;', 120, 80, '', 'Step'), + this.createVertexTemplateEntry('shape=trapezoid;perimeter=trapezoidPerimeter;whiteSpace=wrap;html=1;', 120, 60, '', 'Trapezoid'), + this.createVertexTemplateEntry('shape=tape;whiteSpace=wrap;html=1;', 120, 100, '', 'Tape'), + this.createVertexTemplateEntry('shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;', 80, 100, '', 'Note'), + this.createVertexTemplateEntry('shape=card;whiteSpace=wrap;html=1;', 80, 100, '', 'Card'), + this.createVertexTemplateEntry('shape=callout;whiteSpace=wrap;html=1;perimeter=calloutPerimeter;', 120, 80, '', 'Callout', null, null, 'bubble chat thought speech message'), + this.createVertexTemplateEntry('shape=umlActor;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;html=1;outlineConnect=0;', 30, 60, 'Actor', 'Actor', false, null, 'user person human stickman'), + this.addEntry('curve', mxUtils.bind(this, function() + { + var cell = new mxCell('', new mxGeometry(0, 0, 50, 50), 'curved=1;endArrow=classic;html=1;'); + cell.geometry.setTerminalPoint(new mxPoint(0, 50), true); + cell.geometry.setTerminalPoint(new mxPoint(50, 0), false); + cell.geometry.points = [new mxPoint(50, 50), new mxPoint(0, 0)]; + cell.geometry.relative = true; + cell.edge = true; + + return this.createEdgeTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Curve'); + })), + this.createEdgeTemplateEntry('shape=flexArrow;endArrow=classic;startArrow=classic;html=1;fillColor=#ffffff;', 50, 50, '', 'Bidirectional Arrow', null, lineTags + 'bidirectional'), + this.createEdgeTemplateEntry('shape=flexArrow;endArrow=classic;html=1;fillColor=#ffffff;', 50, 50, '', 'Arrow', null, lineTags + 'directional directed'), + this.createEdgeTemplateEntry('shape=link;html=1;', 50, 50, '', 'Link', null, lineTags + 'link'), + this.createEdgeTemplateEntry('endArrow=none;dashed=1;html=1;', 50, 50, '', 'Dashed Line', null, lineTags + 'dashed undirected no'), + this.createEdgeTemplateEntry('endArrow=none;html=1;', 50, 50, '', 'Line', null, lineTags + 'simple undirected plain blank no'), + this.createEdgeTemplateEntry('endArrow=classic;startArrow=classic;html=1;', 50, 50, '', 'Bidirectional Connector', null, lineTags + 'bidirectional'), + this.createEdgeTemplateEntry('endArrow=classic;html=1;', 50, 50, '', 'Directional Connector', null, lineTags + 'directional directed') + ]; + + this.addPaletteFunctions('general', mxResources.get('general'), (expand != null) ? expand : true, fns); +}; + +/** + * Adds the general palette to the sidebar. + */ +Sidebar.prototype.addBasicPalette = function(dir) +{ + this.addStencilPalette('basic', mxResources.get('basic'), dir + '/basic.xml', + ';whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#000000;strokeWidth=2', + null, null, null, null, [ + this.createVertexTemplateEntry('shape=partialRectangle;whiteSpace=wrap;html=1;top=0;bottom=0;fillColor=none;', 120, 60, '', 'Partial Rectangle'), + this.createVertexTemplateEntry('shape=partialRectangle;whiteSpace=wrap;html=1;right=0;top=0;bottom=0;fillColor=none;routingCenterX=-0.5;', 120, 60, '', 'Partial Rectangle'), + this.createVertexTemplateEntry('shape=partialRectangle;whiteSpace=wrap;html=1;bottom=0;right=0;fillColor=none;', 120, 60, '', 'Partial Rectangle'), + this.createVertexTemplateEntry('shape=partialRectangle;whiteSpace=wrap;html=1;top=0;left=0;fillColor=none;', 120, 60, '', 'Partial Rectangle') + ]); +}; + +/** + * Adds the general palette to the sidebar. + */ +Sidebar.prototype.addMiscPalette = function(expand) +{ + var sb = this; + var lineTags = 'line lines connector connectors connection connections arrow arrows ' + + var fns = [ + this.createVertexTemplateEntry('text;strokeColor=none;fillColor=none;html=1;fontSize=24;fontStyle=1;verticalAlign=middle;align=center;', 100, 40, 'Title', 'Title', null, null, 'text heading title'), + this.createVertexTemplateEntry('text;strokeColor=none;fillColor=none;html=1;whiteSpace=wrap;verticalAlign=middle;overflow=hidden;', 100, 80, + '
  • Value 1
  • Value 2
  • Value 3
', 'Unordered List'), + this.createVertexTemplateEntry('text;strokeColor=none;fillColor=none;html=1;whiteSpace=wrap;verticalAlign=middle;overflow=hidden;', 100, 80, + '
  1. Value 1
  2. Value 2
  3. Value 3
', 'Ordered List'), + this.createVertexTemplateEntry('text;html=1;strokeColor=#c0c0c0;fillColor=#ffffff;overflow=fill;rounded=0;', 280, 160, + '' + + '' + + '' + + '' + + '' + + '
Title 1Title 2Title 3
Value 1Value 2Value 3
Value 4Value 5Value 6
Value 7Value 8Value 9
Value 10Value 11Value 12
', 'Table 1'), + this.createVertexTemplateEntry('text;html=1;strokeColor=#c0c0c0;fillColor=none;overflow=fill;', 180, 140, + '' + + '' + + '' + + '
Value 1Value 2Value 3
Value 4Value 5Value 6
Value 7Value 8Value 9
', 'Table 2'), + this.createVertexTemplateEntry('text;html=1;strokeColor=none;fillColor=none;overflow=fill;', 180, 140, + '' + + '' + + '' + + '
Value 1Value 2Value 3
Value 4Value 5Value 6
Value 7Value 8Value 9
', 'Table 3'), + this.createVertexTemplateEntry('text;html=1;strokeColor=none;fillColor=none;overflow=fill;', 160, 140, + '' + + '' + + '' + + '
Title
Section 1.1\nSection 1.2\nSection 1.3
Section 2.1\nSection 2.2\nSection 2.3
', 'Table 4'), + this.addEntry('link hyperlink', mxUtils.bind(this, function() + { + var cell = new mxCell('Link', new mxGeometry(0, 0, 60, 40), 'text;html=1;strokeColor=none;fillColor=none;whiteSpace=wrap;align=center;verticalAlign=middle;fontColor=#0000EE;fontStyle=4;'); + cell.vertex = true; + this.graph.setLinkForCell(cell, 'https://www.draw.io'); + + return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Link'); + })), + this.addEntry('timestamp date time text label', mxUtils.bind(this, function() + { + var cell = new mxCell('%date{ddd mmm dd yyyy HH:MM:ss}%', new mxGeometry(0, 0, 160, 20), 'text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;overflow=hidden;'); + cell.vertex = true; + this.graph.setAttributeForCell(cell, 'placeholders', '1'); + + return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Timestamp'); + })), + this.addEntry('variable placeholder metadata hello world text label', mxUtils.bind(this, function() + { + var cell = new mxCell('%name% Text', new mxGeometry(0, 0, 80, 20), 'text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;overflow=hidden;'); + cell.vertex = true; + this.graph.setAttributeForCell(cell, 'placeholders', '1'); + this.graph.setAttributeForCell(cell, 'name', 'Variable'); + + return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Variable'); + })), + this.createVertexTemplateEntry('shape=ext;double=1;rounded=0;whiteSpace=wrap;html=1;', 120, 80, '', 'Double Rectangle', null, null, 'rect rectangle box double'), + this.createVertexTemplateEntry('shape=ext;double=1;rounded=1;whiteSpace=wrap;html=1;', 120, 80, '', 'Double Rounded Rectangle', null, null, 'rounded rect rectangle box double'), + this.createVertexTemplateEntry('ellipse;shape=doubleEllipse;whiteSpace=wrap;html=1;', 100, 60, '', 'Double Ellipse', null, null, 'oval ellipse start end state double'), + this.createVertexTemplateEntry('shape=ext;double=1;whiteSpace=wrap;html=1;aspect=fixed;', 80, 80, '', 'Double Square', null, null, 'double square'), + this.createVertexTemplateEntry('ellipse;shape=doubleEllipse;whiteSpace=wrap;html=1;aspect=fixed;', 80, 80, '', 'Double Circle', null, null, 'double circle'), + this.createEdgeTemplateEntry('rounded=0;comic=1;strokeWidth=2;endArrow=blockThin;html=1;fontFamily=Comic Sans MS;fontStyle=1;', 50, 50, '', 'Comic Arrow'), + this.createVertexTemplateEntry('html=1;whiteSpace=wrap;comic=1;strokeWidth=2;fontFamily=Comic Sans MS;fontStyle=1;', 120, 60, 'RECTANGLE', 'Comic Rectangle', true, null, 'comic rectangle rect box text retro'), + this.createVertexTemplateEntry('rhombus;html=1;align=center;whiteSpace=wrap;comic=1;strokeWidth=2;fontFamily=Comic Sans MS;fontStyle=1;', 100, 100, 'DIAMOND', 'Comic Diamond', true, null, 'comic diamond rhombus if condition decision conditional question test retro'), + this.createVertexTemplateEntry('html=1;whiteSpace=wrap;aspect=fixed;shape=isoRectangle;', 150, 90, '', 'Isometric Square', true, null, 'rectangle rect box iso isometric'), + this.createVertexTemplateEntry('html=1;whiteSpace=wrap;aspect=fixed;shape=isoCube;backgroundOutline=1;', 90, 100, '', 'Isometric Cube', true, null, 'cube box iso isometric'), + this.createEdgeTemplateEntry('edgeStyle=isometricEdgeStyle;endArrow=none;html=1;', 50, 100, '', 'Isometric Edge 1'), + this.createEdgeTemplateEntry('edgeStyle=isometricEdgeStyle;endArrow=none;html=1;elbow=vertical;', 50, 100, '', 'Isometric Edge 2'), + this.createVertexTemplateEntry('shape=curlyBracket;whiteSpace=wrap;html=1;rounded=1;', 20, 120, '', 'Curly Bracket'), + this.createVertexTemplateEntry('line;strokeWidth=2;html=1;', 160, 10, '', 'Horizontal Line'), + this.createVertexTemplateEntry('line;strokeWidth=2;direction=south;html=1;', 10, 160, '', 'Vertical Line'), + this.createVertexTemplateEntry('line;strokeWidth=4;html=1;perimeter=backbonePerimeter;points=[];outlineConnect=0;', 160, 10, '', 'Horizontal Backbone', false, null, 'backbone bus network'), + this.createVertexTemplateEntry('line;strokeWidth=4;direction=south;html=1;perimeter=backbonePerimeter;points=[];outlineConnect=0;', 10, 160, '', 'Vertical Backbone', false, null, 'backbone bus network'), + this.createVertexTemplateEntry('shape=crossbar;whiteSpace=wrap;html=1;rounded=1;', 120, 20, '', 'Crossbar', false, null, 'crossbar distance measure dimension unit'), + this.createVertexTemplateEntry('shape=image;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;imageAspect=1;aspect=fixed;image=' + this.gearImage, 52, 61, '', 'Image (Fixed Aspect)', false, null, 'fixed image icon symbol'), + this.createVertexTemplateEntry('shape=image;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;imageAspect=0;image=' + this.gearImage, 50, 60, '', 'Image (Variable Aspect)', false, null, 'strechted image icon symbol'), + this.createVertexTemplateEntry('icon;html=1;image=' + this.gearImage, 60, 60, 'Icon', 'Icon', false, null, 'icon image symbol'), + this.createVertexTemplateEntry('label;whiteSpace=wrap;html=1;image=' + this.gearImage, 140, 60, 'Label', 'Label 1', null, null, 'label image icon symbol'), + this.createVertexTemplateEntry('label;whiteSpace=wrap;html=1;align=center;verticalAlign=bottom;spacingLeft=0;spacingBottom=4;imageAlign=center;imageVerticalAlign=top;image=' + this.gearImage, 120, 80, 'Label', 'Label 2', null, null, 'label image icon symbol'), + this.addEntry('shape group container', function() + { + var cell = new mxCell('Label', new mxGeometry(0, 0, 160, 70), + 'html=1;whiteSpace=wrap;container=1;recursiveResize=0;collapsible=0;'); + cell.vertex = true; + + var symbol = new mxCell('', new mxGeometry(20, 20, 20, 30), 'triangle;html=1;whiteSpace=wrap;'); + symbol.vertex = true; + cell.insert(symbol); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Shape Group'); + }), + this.createVertexTemplateEntry('shape=partialRectangle;whiteSpace=wrap;html=1;left=0;right=0;fillColor=none;', 120, 60, '', 'Partial Rectangle'), + this.createVertexTemplateEntry('shape=partialRectangle;whiteSpace=wrap;html=1;bottom=1;right=1;top=0;bottom=1;fillColor=none;routingCenterX=-0.5;', 120, 60, '', 'Partial Rectangle'), + this.createEdgeTemplateEntry('edgeStyle=segmentEdgeStyle;endArrow=classic;html=1;', 50, 50, '', 'Manual Line', null, lineTags + 'manual'), + this.createEdgeTemplateEntry('shape=filledEdge;rounded=0;fixDash=1;endArrow=none;strokeWidth=10;fillColor=#ffffff;edgeStyle=orthogonalEdgeStyle;', 60, 40, '', 'Filled Edge'), + this.createEdgeTemplateEntry('edgeStyle=elbowEdgeStyle;elbow=horizontal;endArrow=classic;html=1;', 50, 50, '', 'Horizontal Elbow', null, lineTags + 'elbow horizontal'), + this.createEdgeTemplateEntry('edgeStyle=elbowEdgeStyle;elbow=vertical;endArrow=classic;html=1;', 50, 50, '', 'Vertical Elbow', null, lineTags + 'elbow vertical') + ]; + + this.addPaletteFunctions('misc', mxResources.get('misc'), (expand != null) ? expand : true, fns); +}; +/** + * Adds the container palette to the sidebar. + */ +Sidebar.prototype.addAdvancedPalette = function(expand) +{ + this.addPaletteFunctions('advanced', mxResources.get('advanced'), (expand != null) ? expand : false, this.createAdvancedShapes()); +}; + +/** + * Adds the container palette to the sidebar. + */ +Sidebar.prototype.createAdvancedShapes = function() +{ + // Avoids having to bind all functions to "this" + var sb = this; + + // Reusable cells + var field = new mxCell('List Item', new mxGeometry(0, 0, 60, 26), 'text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;'); + field.vertex = true; + + return [ + this.createVertexTemplateEntry('shape=xor;whiteSpace=wrap;html=1;', 60, 80, '', 'Or', null, null, 'logic or'), + this.createVertexTemplateEntry('shape=or;whiteSpace=wrap;html=1;', 60, 80, '', 'And', null, null, 'logic and'), + this.createVertexTemplateEntry('shape=dataStorage;whiteSpace=wrap;html=1;', 100, 80, '', 'Data Storage'), + this.createVertexTemplateEntry('shape=tapeData;whiteSpace=wrap;html=1;perimeter=ellipsePerimeter;', 80, 80, '', 'Tape Data'), + this.createVertexTemplateEntry('shape=manualInput;whiteSpace=wrap;html=1;', 80, 80, '', 'Manual Input'), + this.createVertexTemplateEntry('shape=loopLimit;whiteSpace=wrap;html=1;', 100, 80, '', 'Loop Limit'), + this.createVertexTemplateEntry('shape=offPageConnector;whiteSpace=wrap;html=1;', 80, 80, '', 'Off Page Connector'), + this.createVertexTemplateEntry('shape=delay;whiteSpace=wrap;html=1;', 80, 40, '', 'Delay'), + this.createVertexTemplateEntry('shape=display;whiteSpace=wrap;html=1;', 80, 40, '', 'Display'), + this.createVertexTemplateEntry('shape=singleArrow;direction=west;whiteSpace=wrap;html=1;', 100, 60, '', 'Arrow Left'), + this.createVertexTemplateEntry('shape=singleArrow;whiteSpace=wrap;html=1;', 100, 60, '', 'Arrow Right'), + this.createVertexTemplateEntry('shape=singleArrow;direction=north;whiteSpace=wrap;html=1;', 60, 100, '', 'Arrow Up'), + this.createVertexTemplateEntry('shape=singleArrow;direction=south;whiteSpace=wrap;html=1;', 60, 100, '', 'Arrow Down'), + this.createVertexTemplateEntry('shape=doubleArrow;whiteSpace=wrap;html=1;', 100, 60, '', 'Double Arrow'), + this.createVertexTemplateEntry('shape=doubleArrow;direction=south;whiteSpace=wrap;html=1;', 60, 100, '', 'Double Arrow Vertical', null, null, 'double arrow'), + this.createVertexTemplateEntry('shape=actor;whiteSpace=wrap;html=1;', 40, 60, '', 'User', null, null, 'user person human'), + this.createVertexTemplateEntry('shape=cross;whiteSpace=wrap;html=1;', 80, 80, '', 'Cross'), + this.createVertexTemplateEntry('shape=corner;whiteSpace=wrap;html=1;', 80, 80, '', 'Corner'), + this.createVertexTemplateEntry('shape=tee;whiteSpace=wrap;html=1;', 80, 80, '', 'Tee'), + this.createVertexTemplateEntry('shape=datastore;whiteSpace=wrap;html=1;', 60, 60, '', 'Data Store', null, null, 'data store cylinder database'), + this.createVertexTemplateEntry('shape=orEllipse;perimeter=ellipsePerimeter;whiteSpace=wrap;html=1;backgroundOutline=1;', 80, 80, '', 'Or', null, null, 'or circle oval ellipse'), + this.createVertexTemplateEntry('shape=sumEllipse;perimeter=ellipsePerimeter;whiteSpace=wrap;html=1;backgroundOutline=1;', 80, 80, '', 'Sum', null, null, 'sum circle oval ellipse'), + this.createVertexTemplateEntry('shape=lineEllipse;perimeter=ellipsePerimeter;whiteSpace=wrap;html=1;backgroundOutline=1;', 80, 80, '', 'Ellipse with horizontal divider', null, null, 'circle oval ellipse'), + this.createVertexTemplateEntry('shape=lineEllipse;line=vertical;perimeter=ellipsePerimeter;whiteSpace=wrap;html=1;backgroundOutline=1;', 80, 80, '', 'Ellipse with vertical divider', null, null, 'circle oval ellipse'), + this.createVertexTemplateEntry('shape=sortShape;perimeter=rhombusPerimeter;whiteSpace=wrap;html=1;', 80, 80, '', 'Sort', null, null, 'sort'), + this.createVertexTemplateEntry('shape=collate;whiteSpace=wrap;html=1;', 80, 80, '', 'Collate', null, null, 'collate'), + this.createVertexTemplateEntry('shape=switch;whiteSpace=wrap;html=1;', 60, 60, '', 'Switch', null, null, 'switch router'), + this.addEntry('process bar', function() + { + return sb.createVertexTemplateFromData('zZXRaoMwFIafJpcDjbNrb2233rRQ8AkyPdPQaCRJV+3T7yTG2rUVBoOtgpDzn/xJzncCIdGyateKNeVW5iBI9EqipZLS9KOqXYIQhAY8J9GKUBrgT+jbRDZ02aBhCmrzEwPtDZ9MHKBXdkpmoDWKCVN9VptO+Kw+8kqwGqMkK7nIN6yTB7uTNizbD1FSSsVPsjYMC1qFKHxwIZZSSIVxLZ1/nJNar5+oQPMT7IYCrqUta1ENzuqGaeOFTArBGs3f3Vmtoo2Se7ja1h00kSoHK4bBIKUNy3hdoPYU0mF91i9mT8EEL2ocZ3gKa00ayWujLZY4IfHKFonVDLsRGgXuQ90zBmWgneyTk3yT1iArMKrDKUeem9L3ajHrbSXwohxsQd/ggOleKM7ese048J2/fwuim1uQGmhQCW8vQMkacP3GCQgBFMftHEsr7cYYe95CnmKTPMFbYD8CQ++DGQy+/M5X4ku5wHYmdIktfvk9tecpavThqS3m/0YtnqIWPTy1cD77K2wYjo+Ay317I74A', 296, 100, 'Process Bar'); + }), + this.createVertexTemplateEntry('swimlane;', 200, 200, 'Container', 'Container', null, null, 'container swimlane lane pool group'), + this.addEntry('list group erd table', function() + { + var cell = new mxCell('List', new mxGeometry(0, 0, 140, 110), + 'swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;' + + 'resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;'); + cell.vertex = true; + cell.insert(sb.cloneCell(field, 'Item 1')); + cell.insert(sb.cloneCell(field, 'Item 2')); + cell.insert(sb.cloneCell(field, 'Item 3')); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'List'); + }), + this.addEntry('list item entry value group erd table', function() + { + return sb.createVertexTemplateFromCells([sb.cloneCell(field, 'List Item')], field.geometry.width, field.geometry.height, 'List Item'); + }) + ]; +}; + +/** + * Adds the general palette to the sidebar. + */ +Sidebar.prototype.addUmlPalette = function(expand) +{ + // Avoids having to bind all functions to "this" + var sb = this; + + // Reusable cells + var field = new mxCell('+ field: type', new mxGeometry(0, 0, 100, 26), 'text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;'); + field.vertex = true; + + var divider = new mxCell('', new mxGeometry(0, 0, 40, 8), 'line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;'); + divider.vertex = true; + + // Default tags + var dt = 'uml static class '; + + var fns = [ + this.createVertexTemplateEntry('html=1;', 110, 50, 'Object', 'Object', null, null, dt + 'object instance'), + this.createVertexTemplateEntry('html=1;', 110, 50, '«interface»
Name', 'Interface', null, null, dt + 'interface object instance annotated annotation'), + this.addEntry(dt + 'object instance', function() + { + var cell = new mxCell('Classname', new mxGeometry(0, 0, 160, 90), + 'swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;'); + cell.vertex = true; + cell.insert(field.clone()); + cell.insert(divider.clone()); + cell.insert(sb.cloneCell(field, '+ method(type): type')); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Class'); + }), + this.addEntry(dt + 'section subsection', function() + { + var cell = new mxCell('Classname', new mxGeometry(0, 0, 140, 110), + 'swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;'); + cell.vertex = true; + cell.insert(field.clone()); + cell.insert(field.clone()); + cell.insert(field.clone()); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Class 2'); + }), + this.addEntry(dt + 'item member method function variable field attribute label', function() + { + return sb.createVertexTemplateFromCells([sb.cloneCell(field, '+ item: attribute')], field.geometry.width, field.geometry.height, 'Item 1'); + }), + this.addEntry(dt + 'item member method function variable field attribute label', function() + { + var cell = new mxCell('item: attribute', new mxGeometry(0, 0, 120, field.geometry.height), 'label;fontStyle=0;strokeColor=none;fillColor=none;align=left;verticalAlign=top;overflow=hidden;' + + 'spacingLeft=28;spacingRight=4;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;imageWidth=16;imageHeight=16;image=' + sb.gearImage); + cell.vertex = true; + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Item 2'); + }), + this.addEntry(dt + 'divider hline line separator', function() + { + return sb.createVertexTemplateFromCells([divider.clone()], divider.geometry.width, divider.geometry.height, 'Divider'); + }), + this.addEntry(dt + 'spacer space gap separator', function() + { + var cell = new mxCell('', new mxGeometry(0, 0, 20, 14), 'text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=4;spacingRight=4;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;'); + cell.vertex = true; + + return sb.createVertexTemplateFromCells([cell.clone()], cell.geometry.width, cell.geometry.height, 'Spacer'); + }), + this.createVertexTemplateEntry('text;align=center;fontStyle=1;verticalAlign=middle;spacingLeft=3;spacingRight=3;strokeColor=none;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;', + 80, 26, 'Title', 'Title', null, null, dt + 'title label'), + this.addEntry(dt + 'component', function() + { + var cell = new mxCell('«Annotation»
Component', new mxGeometry(0, 0, 180, 90), 'html=1;'); + cell.vertex = true; + + var symbol = new mxCell('', new mxGeometry(1, 0, 20, 20), 'shape=component;jettyWidth=8;jettyHeight=4;'); + symbol.vertex = true; + symbol.geometry.relative = true; + symbol.geometry.offset = new mxPoint(-27, 7); + cell.insert(symbol); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Component'); + }), + this.addEntry(dt + 'component', function() + { + var cell = new mxCell('

Component

' + + '

+ Attribute1: Type
+ Attribute2: Type

', new mxGeometry(0, 0, 180, 90), + 'align=left;overflow=fill;html=1;'); + cell.vertex = true; + + var symbol = new mxCell('', new mxGeometry(1, 0, 20, 20), 'shape=component;jettyWidth=8;jettyHeight=4;'); + symbol.vertex = true; + symbol.geometry.relative = true; + symbol.geometry.offset = new mxPoint(-24, 4); + cell.insert(symbol); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Component with Attributes'); + }), + this.createVertexTemplateEntry('verticalAlign=top;align=left;spacingTop=8;spacingLeft=2;spacingRight=12;shape=cube;size=10;direction=south;fontStyle=4;html=1;', + 180, 120, 'Block', 'Block', null, null, dt + 'block'), + this.createVertexTemplateEntry('shape=component;align=left;spacingLeft=36;', 120, 60, 'Module', 'Module', null, null, dt + 'module'), + this.createVertexTemplateEntry('shape=folder;fontStyle=1;spacingTop=10;tabWidth=40;tabHeight=14;tabPosition=left;html=1;', 70, 50, + 'package', 'Package', null, null, dt + 'package'), + this.createVertexTemplateEntry('verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;', + 160, 90, '

Object:Type


' + + '

field1 = value1
field2 = value2
field3 = value3

', 'Object', + null, null, dt + 'object instance'), + this.createVertexTemplateEntry('verticalAlign=top;align=left;overflow=fill;html=1;',180, 90, + '
Tablename
' + + '' + + '
PKuniqueId
FK1' + + 'foreignKey
fieldname
', 'Entity', null, null, 'er entity table'), + this.addEntry(dt + 'object instance', function() + { + var cell = new mxCell('

' + + 'Class

' + + '
', new mxGeometry(0, 0, 140, 60), + 'verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;'); + cell.vertex = true; + + return sb.createVertexTemplateFromCells([cell.clone()], cell.geometry.width, cell.geometry.height, 'Class 3'); + }), + this.addEntry(dt + 'object instance', function() + { + var cell = new mxCell('

' + + 'Class

' + + '

', new mxGeometry(0, 0, 140, 60), + 'verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;'); + cell.vertex = true; + + return sb.createVertexTemplateFromCells([cell.clone()], cell.geometry.width, cell.geometry.height, 'Class 4'); + }), + this.addEntry(dt + 'object instance', function() + { + var cell = new mxCell('

' + + 'Class

' + + '

+ field: Type


' + + '

+ method(): Type

', new mxGeometry(0, 0, 160, 90), + 'verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;'); + cell.vertex = true; + + return sb.createVertexTemplateFromCells([cell.clone()], cell.geometry.width, cell.geometry.height, 'Class 5'); + }), + this.addEntry(dt + 'object instance', function() + { + var cell = new mxCell('

' + + '<<Interface>>
Interface

' + + '

+ field1: Type
' + + '+ field2: Type

' + + '

' + + '+ method1(Type): Type
' + + '+ method2(Type, Type): Type

', new mxGeometry(0, 0, 190, 140), + 'verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;'); + cell.vertex = true; + + return sb.createVertexTemplateFromCells([cell.clone()], cell.geometry.width, cell.geometry.height, 'Interface 2'); + }), + this.createVertexTemplateEntry('shape=providedRequiredInterface;html=1;verticalLabelPosition=bottom;', 20, 20, '', 'Provided/Required Interface', null, null, dt + 'provided required interface'), + this.createVertexTemplateEntry('shape=requiredInterface;html=1;verticalLabelPosition=bottom;', 10, 20, '', 'Required Interface', null, null, dt + 'required interface'), + this.createVertexTemplateEntry('shape=umlBoundary;whiteSpace=wrap;html=1;', 100, 80, 'Boundary Object', 'Boundary Object', null, null, 'uml boundary object'), + this.createVertexTemplateEntry('ellipse;shape=umlEntity;whiteSpace=wrap;html=1;', 80, 80, 'Entity Object', 'Entity Object', null, null, 'uml entity object'), + this.createVertexTemplateEntry('ellipse;shape=umlControl;whiteSpace=wrap;html=1;', 70, 80, 'Control Object', 'Control Object', null, null, 'uml control object'), + this.createVertexTemplateEntry('shape=umlActor;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;html=1;', 30, 60, 'Actor', 'Actor', false, null, 'uml actor'), + this.createVertexTemplateEntry('ellipse;whiteSpace=wrap;html=1;', 140, 70, 'Use Case', 'Use Case', null, null, 'uml use case usecase'), + this.addEntry('uml activity state start', function() + { + var cell = new mxCell('', new mxGeometry(0, 0, 30, 30), + 'ellipse;html=1;shape=startState;fillColor=#000000;strokeColor=#ff0000;'); + cell.vertex = true; + + var edge = new mxCell('', new mxGeometry(0, 0, 0, 0), 'edgeStyle=orthogonalEdgeStyle;html=1;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#ff0000;'); + edge.geometry.setTerminalPoint(new mxPoint(15, 90), false); + edge.geometry.relative = true; + edge.edge = true; + + cell.insertEdge(edge, true); + + return sb.createVertexTemplateFromCells([cell, edge], 30, 90, 'Start'); + }), + this.addEntry('uml activity state', function() + { + var cell = new mxCell('Activity', new mxGeometry(0, 0, 120, 40), + 'rounded=1;whiteSpace=wrap;html=1;arcSize=40;fillColor=#ffffc0;strokeColor=#ff0000;'); + cell.vertex = true; + + var edge = new mxCell('', new mxGeometry(0, 0, 0, 0), 'edgeStyle=orthogonalEdgeStyle;html=1;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#ff0000;'); + edge.geometry.setTerminalPoint(new mxPoint(60, 100), false); + edge.geometry.relative = true; + edge.edge = true; + + cell.insertEdge(edge, true); + + return sb.createVertexTemplateFromCells([cell, edge], 120, 100, 'Activity'); + }), + this.addEntry('uml activity composite state', function() + { + var cell = new mxCell('Composite State', new mxGeometry(0, 0, 160, 60), + 'swimlane;html=1;fontStyle=1;align=center;verticalAlign=middle;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=0;resizeLast=1;container=0;collapsible=0;rounded=1;arcSize=30;strokeColor=#ff0000;fillColor=#ffffc0;swimlaneFillColor=#ffffc0;'); + cell.vertex = true; + + var cell1 = new mxCell('Subtitle', new mxGeometry(0, 0, 200, 26), 'text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;'); + cell1.vertex = true; + cell.insert(cell1); + + var edge = new mxCell('', new mxGeometry(0, 0, 0, 0), 'edgeStyle=orthogonalEdgeStyle;html=1;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#ff0000;'); + edge.geometry.setTerminalPoint(new mxPoint(80, 120), false); + edge.geometry.relative = true; + edge.edge = true; + + cell.insertEdge(edge, true); + + return sb.createVertexTemplateFromCells([cell, edge], 160, 120, 'Composite State'); + }), + this.addEntry('uml activity condition', function() + { + var cell = new mxCell('Condition', new mxGeometry(0, 0, 80, 40), 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffffc0;strokeColor=#ff0000;'); + cell.vertex = true; + + var edge1 = new mxCell('no', new mxGeometry(0, 0, 0, 0), 'edgeStyle=orthogonalEdgeStyle;html=1;align=left;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#ff0000;'); + edge1.geometry.setTerminalPoint(new mxPoint(180, 20), false); + edge1.geometry.relative = true; + edge1.geometry.x = -1; + edge1.edge = true; + + cell.insertEdge(edge1, true); + + var edge2 = new mxCell('yes', new mxGeometry(0, 0, 0, 0), 'edgeStyle=orthogonalEdgeStyle;html=1;align=left;verticalAlign=top;endArrow=open;endSize=8;strokeColor=#ff0000;'); + edge2.geometry.setTerminalPoint(new mxPoint(40, 100), false); + edge2.geometry.relative = true; + edge2.geometry.x = -1; + edge2.edge = true; + + cell.insertEdge(edge2, true); + + return sb.createVertexTemplateFromCells([cell, edge1, edge2], 180, 100, 'Condition'); + }), + this.addEntry('uml activity fork join', function() + { + var cell = new mxCell('', new mxGeometry(0, 0, 200, 10), 'shape=line;html=1;strokeWidth=6;strokeColor=#ff0000;'); + cell.vertex = true; + + var edge = new mxCell('', new mxGeometry(0, 0, 0, 0), 'edgeStyle=orthogonalEdgeStyle;html=1;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#ff0000;'); + edge.geometry.setTerminalPoint(new mxPoint(100, 80), false); + edge.geometry.relative = true; + edge.edge = true; + + cell.insertEdge(edge, true); + + return sb.createVertexTemplateFromCells([cell, edge], 200, 80, 'Fork/Join'); + }), + this.createVertexTemplateEntry('ellipse;html=1;shape=endState;fillColor=#000000;strokeColor=#ff0000;', 30, 30, '', 'End', null, null, 'uml activity state end'), + this.createVertexTemplateEntry('shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;collapsible=0;recursiveResize=0;outlineConnect=0;', 100, 300, ':Object', 'Lifeline', null, null, 'uml sequence participant lifeline'), + this.createVertexTemplateEntry('shape=umlLifeline;participant=umlActor;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;collapsible=0;recursiveResize=0;verticalAlign=top;spacingTop=36;labelBackgroundColor=#ffffff;outlineConnect=0;', + 20, 300, '', 'Actor Lifeline', null, null, 'uml sequence participant lifeline actor'), + this.createVertexTemplateEntry('shape=umlLifeline;participant=umlBoundary;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;collapsible=0;recursiveResize=0;verticalAlign=top;spacingTop=36;labelBackgroundColor=#ffffff;outlineConnect=0;', + 50, 300, '', 'Boundary Lifeline', null, null, 'uml sequence participant lifeline boundary'), + this.createVertexTemplateEntry('shape=umlLifeline;participant=umlEntity;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;collapsible=0;recursiveResize=0;verticalAlign=top;spacingTop=36;labelBackgroundColor=#ffffff;outlineConnect=0;', + 40, 300, '', 'Entity Lifeline', null, null, 'uml sequence participant lifeline entity'), + this.createVertexTemplateEntry('shape=umlLifeline;participant=umlControl;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;collapsible=0;recursiveResize=0;verticalAlign=top;spacingTop=36;labelBackgroundColor=#ffffff;outlineConnect=0;', + 40, 300, '', 'Control Lifeline', null, null, 'uml sequence participant lifeline control'), + this.createVertexTemplateEntry('shape=umlFrame;whiteSpace=wrap;html=1;', 300, 200, 'frame', 'Frame', null, null, 'uml sequence frame'), + this.createVertexTemplateEntry('shape=umlDestroy;whiteSpace=wrap;html=1;strokeWidth=3;', 30, 30, '', 'Destruction', null, null, 'uml sequence destruction destroy'), + this.createVertexTemplateEntry('shape=note;whiteSpace=wrap;html=1;size=14;verticalAlign=top;align=left;spacingTop=-6;', 100, 70, 'Note', 'Note', null, null, 'uml note'), + this.addEntry('uml sequence invoke invocation call activation', function() + { + var cell = new mxCell('', new mxGeometry(0, 0, 10, 80), 'html=1;points=[];perimeter=orthogonalPerimeter;'); + cell.vertex = true; + + var edge = new mxCell('dispatch', new mxGeometry(0, 0, 0, 0), 'html=1;verticalAlign=bottom;startArrow=oval;endArrow=block;startSize=8;'); + edge.geometry.setTerminalPoint(new mxPoint(-60, 0), true); + edge.geometry.relative = true; + edge.edge = true; + + cell.insertEdge(edge, false); + + return sb.createVertexTemplateFromCells([cell, edge], 10, 80, 'Found Message'); + }), + this.addEntry('uml sequence invoke call delegation synchronous invocation activation', function() + { + var cell = new mxCell('', new mxGeometry(0, 0, 10, 80), 'html=1;points=[];perimeter=orthogonalPerimeter;'); + cell.vertex = true; + + var edge1 = new mxCell('dispatch', new mxGeometry(0, 0, 0, 0), 'html=1;verticalAlign=bottom;endArrow=block;entryX=0;entryY=0;'); + edge1.geometry.setTerminalPoint(new mxPoint(-70, 0), true); + edge1.geometry.relative = true; + edge1.edge = true; + + cell.insertEdge(edge1, false); + + var edge2 = new mxCell('return', new mxGeometry(0, 0, 0, 0), 'html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;exitX=0;exitY=0.95;'); + edge2.geometry.setTerminalPoint(new mxPoint(-70, 76), false); + edge2.geometry.relative = true; + edge2.edge = true; + + cell.insertEdge(edge2, true); + + return sb.createVertexTemplateFromCells([cell, edge1, edge2], 10, 80, 'Synchronous Invocation'); + }), + this.addEntry('uml sequence self call recursion delegation activation', function() + { + var cell = new mxCell('', new mxGeometry(0, 20, 10, 40), 'html=1;points=[];perimeter=orthogonalPerimeter;'); + cell.vertex = true; + + var edge = new mxCell('self call', new mxGeometry(0, 0, 0, 0), 'edgeStyle=orthogonalEdgeStyle;html=1;align=left;spacingLeft=2;endArrow=block;rounded=0;entryX=1;entryY=0;'); + edge.geometry.setTerminalPoint(new mxPoint(5, 0), true); + edge.geometry.points = [new mxPoint(30, 0)]; + edge.geometry.relative = true; + edge.edge = true; + + cell.insertEdge(edge, false); + + return sb.createVertexTemplateFromCells([cell, edge], 10, 60, 'Self Call'); + }), + this.addEntry('uml sequence invoke call delegation callback activation', function() + { + // TODO: Check if more entries should be converted to compressed XML + return sb.createVertexTemplateFromData('xZRNT8MwDIZ/Ta6oaymD47rBTkiTuMAxW6wmIm0q19s6fj1OE3V0Y2iCA4dK8euP2I+riGxedUuUjX52CqzIHkU2R+conKpuDtaKNDFKZAuRpgl/In264J303qSRCDVdk5CGhJ20WwhKEFo62ChoqritxURkReNMTa2X80LkC68AmgoIkEWHpF3pamlXR7WIFwASdBeb7KXY4RIc5+KBQ/ZGkY4RYY5Egyl1zLqLmmyDXQ6Zx4n5EIf+HkB2BmAjrV3LzftPIPw4hgNn1pQ1a2tH5Cp2QK1miG7vNeu4iJe4pdeY2BtvbCQDGlAljMCQxBJotJ8rWCFYSWY3LvUdmZi68rvkkLiU6QnL1m1xAzHoBOdw61WEb88II9AW67/ydQ2wq1Cy1aAGvOrFfPh6997qDA3g+dxzv3nIL6MPU/8T+kMw8+m4QPgdfrEJNo8PSQj/+s58Ag==', + 10, 60, 'Callback'); + }), + this.createVertexTemplateEntry('html=1;points=[];perimeter=orthogonalPerimeter;', 10, 80, '', 'Activation', null, null, 'uml sequence activation'), + this.createEdgeTemplateEntry('html=1;verticalAlign=bottom;startArrow=oval;startFill=1;endArrow=block;startSize=8;', 60, 0, 'dispatch', 'Found Message 1', null, 'uml sequence message call invoke dispatch'), + this.createEdgeTemplateEntry('html=1;verticalAlign=bottom;startArrow=circle;startFill=1;endArrow=open;startSize=6;endSize=8;', 80, 0, 'dispatch', 'Found Message 2', null, 'uml sequence message call invoke dispatch'), + this.createEdgeTemplateEntry('html=1;verticalAlign=bottom;endArrow=block;', 80, 0, 'dispatch', 'Message', null, 'uml sequence message call invoke dispatch'), + this.addEntry('uml sequence return message', function() + { + var edge = new mxCell('return', new mxGeometry(0, 0, 0, 0), 'html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;'); + edge.geometry.setTerminalPoint(new mxPoint(80, 0), true); + edge.geometry.setTerminalPoint(new mxPoint(0, 0), false); + edge.geometry.relative = true; + edge.edge = true; + + return sb.createEdgeTemplateFromCells([edge], 80, 0, 'Return'); + }), + this.addEntry('uml relation', function() + { + var edge = new mxCell('name', new mxGeometry(0, 0, 0, 0), 'endArrow=block;endFill=1;html=1;edgeStyle=orthogonalEdgeStyle;align=left;verticalAlign=top;'); + edge.geometry.setTerminalPoint(new mxPoint(0, 0), true); + edge.geometry.setTerminalPoint(new mxPoint(160, 0), false); + edge.geometry.relative = true; + edge.geometry.x = -1; + edge.edge = true; + + var cell = new mxCell('1', new mxGeometry(-1, 0, 0, 0), 'resizable=0;html=1;align=left;verticalAlign=bottom;labelBackgroundColor=#ffffff;fontSize=10;'); + cell.geometry.relative = true; + cell.setConnectable(false); + cell.vertex = true; + edge.insert(cell); + + return sb.createEdgeTemplateFromCells([edge], 160, 0, 'Relation 1'); + }), + this.addEntry('uml association', function() + { + var edge = new mxCell('', new mxGeometry(0, 0, 0, 0), 'endArrow=none;html=1;edgeStyle=orthogonalEdgeStyle;'); + edge.geometry.setTerminalPoint(new mxPoint(0, 0), true); + edge.geometry.setTerminalPoint(new mxPoint(160, 0), false); + edge.geometry.relative = true; + edge.edge = true; + + var cell1 = new mxCell('parent', new mxGeometry(-1, 0, 0, 0), 'resizable=0;html=1;align=left;verticalAlign=bottom;labelBackgroundColor=#ffffff;fontSize=10;'); + cell1.geometry.relative = true; + cell1.setConnectable(false); + cell1.vertex = true; + edge.insert(cell1); + + var cell2 = new mxCell('child', new mxGeometry(1, 0, 0, 0), 'resizable=0;html=1;align=right;verticalAlign=bottom;labelBackgroundColor=#ffffff;fontSize=10;'); + cell2.geometry.relative = true; + cell2.setConnectable(false); + cell2.vertex = true; + edge.insert(cell2); + + return sb.createEdgeTemplateFromCells([edge], 160, 0, 'Association 1'); + }), + this.addEntry('uml aggregation', function() + { + var edge = new mxCell('1', new mxGeometry(0, 0, 0, 0), 'endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=0;edgeStyle=orthogonalEdgeStyle;align=left;verticalAlign=bottom;'); + edge.geometry.setTerminalPoint(new mxPoint(0, 0), true); + edge.geometry.setTerminalPoint(new mxPoint(160, 0), false); + edge.geometry.relative = true; + edge.geometry.x = -1; + edge.geometry.y = 3; + edge.edge = true; + + return sb.createEdgeTemplateFromCells([edge], 160, 0, 'Aggregation 1'); + }), + this.addEntry('uml composition', function() + { + var edge = new mxCell('1', new mxGeometry(0, 0, 0, 0), 'endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;align=left;verticalAlign=bottom;'); + edge.geometry.setTerminalPoint(new mxPoint(0, 0), true); + edge.geometry.setTerminalPoint(new mxPoint(160, 0), false); + edge.geometry.relative = true; + edge.geometry.x = -1; + edge.geometry.y = 3; + edge.edge = true; + + return sb.createEdgeTemplateFromCells([edge], 160, 0, 'Composition 1'); + }), + this.addEntry('uml relation', function() + { + var edge = new mxCell('Relation', new mxGeometry(0, 0, 0, 0), 'endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=0;edgeStyle=orthogonalEdgeStyle;'); + edge.geometry.setTerminalPoint(new mxPoint(0, 0), true); + edge.geometry.setTerminalPoint(new mxPoint(160, 0), false); + edge.geometry.relative = true; + edge.edge = true; + + var cell1 = new mxCell('0..n', new mxGeometry(-1, 0, 0, 0), 'resizable=0;html=1;align=left;verticalAlign=top;labelBackgroundColor=#ffffff;fontSize=10;'); + cell1.geometry.relative = true; + cell1.setConnectable(false); + cell1.vertex = true; + edge.insert(cell1); + + var cell2 = new mxCell('1', new mxGeometry(1, 0, 0, 0), 'resizable=0;html=1;align=right;verticalAlign=top;labelBackgroundColor=#ffffff;fontSize=10;'); + cell2.geometry.relative = true; + cell2.setConnectable(false); + cell2.vertex = true; + edge.insert(cell2); + + return sb.createEdgeTemplateFromCells([edge], 160, 0, 'Relation 2'); + }), + this.createEdgeTemplateEntry('endArrow=open;endSize=12;dashed=1;html=1;', 160, 0, 'Use', 'Dependency', null, 'uml dependency use'), + this.createEdgeTemplateEntry('endArrow=block;endSize=16;endFill=0;html=1;', 160, 0, 'Extends', 'Generalization', null, 'uml generalization extend'), + this.createEdgeTemplateEntry('endArrow=block;startArrow=block;endFill=1;startFill=1;html=1;', 160, 0, '', 'Association 2', null, 'uml association'), + this.createEdgeTemplateEntry('endArrow=open;startArrow=circlePlus;endFill=0;startFill=0;endSize=8;html=1;', 160, 0, '', 'Inner Class', null, 'inner class'), + this.createEdgeTemplateEntry('endArrow=open;startArrow=cross;endFill=0;startFill=0;endSize=8;startSize=10;html=1;', 160, 0, '', 'Terminate', null, 'terminate'), + this.createEdgeTemplateEntry('endArrow=block;dashed=1;endFill=0;endSize=12;html=1;', 160, 0, '', 'Implementation', null, 'realization implementation'), + this.createEdgeTemplateEntry('endArrow=diamondThin;endFill=0;endSize=24;html=1;', 160, 0, '', 'Aggregation 2', null, 'aggregation'), + this.createEdgeTemplateEntry('endArrow=diamondThin;endFill=1;endSize=24;html=1;', 160, 0, '', 'Composition 2', null, 'composition'), + this.createEdgeTemplateEntry('endArrow=open;endFill=1;endSize=12;html=1;', 160, 0, '', 'Association 3', null, 'association') + ]; + + this.addPaletteFunctions('uml', mxResources.get('uml'), expand || false, fns); +}; + +/** + * Adds the BPMN library to the sidebar. + */ +Sidebar.prototype.addBpmnPalette = function(dir, expand) +{ + // Avoids having to bind all functions to "this" + var sb = this; + + var fns = + [ + this.createVertexTemplateEntry('shape=ext;rounded=1;html=1;whiteSpace=wrap;', 120, 80, 'Task', 'Process', null, null, 'bpmn task process'), + this.createVertexTemplateEntry('shape=ext;rounded=1;html=1;whiteSpace=wrap;double=1;', 120, 80, 'Transaction', 'Transaction', null, null, 'bpmn transaction'), + this.createVertexTemplateEntry('shape=ext;rounded=1;html=1;whiteSpace=wrap;dashed=1;dashPattern=1 4;', 120, 80, 'Event\nSub-Process', 'Event Sub-Process', null, null, 'bpmn event subprocess sub process sub-process'), + this.createVertexTemplateEntry('shape=ext;rounded=1;html=1;whiteSpace=wrap;strokeWidth=3;', 120, 80, 'Call Activity', 'Call Activity', null, null, 'bpmn call activity'), + this.addEntry('bpmn subprocess sub process sub-process', function() + { + var cell = new mxCell('Sub-Process', new mxGeometry(0, 0, 120, 80), 'html=1;whiteSpace=wrap;rounded=1;'); + cell.vertex = true; + + var cell1 = new mxCell('', new mxGeometry(0.5, 1, 14, 14), 'html=1;shape=plus;outlineConnect=0;'); + cell1.vertex = true; + cell1.geometry.relative = true; + cell1.geometry.offset = new mxPoint(-7, -14); + cell.insert(cell1); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Sub-Process'); + }), + this.addEntry(this.getTagsForStencil('mxgraph.bpmn', 'loop', 'subprocess sub process sub-process looped').join(' '), function() + { + var cell = new mxCell('Looped\nSub-Process', new mxGeometry(0, 0, 120, 80), 'html=1;whiteSpace=wrap;rounded=1'); + cell.vertex = true; + + var cell1 = new mxCell('', new mxGeometry(0.5, 1, 14, 14), 'html=1;shape=mxgraph.bpmn.loop;outlineConnect=0;'); + cell1.vertex = true; + cell1.geometry.relative = true; + cell1.geometry.offset = new mxPoint(-15, -14); + cell.insert(cell1); + + var cell2 = new mxCell('', new mxGeometry(0.5, 1, 14, 14), 'html=1;shape=plus;'); + cell2.vertex = true; + cell2.geometry.relative = true; + cell2.geometry.offset = new mxPoint(1, -14); + cell.insert(cell2); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Looped Sub-Process'); + }), + this.addEntry('bpmn receive task', function() + { + var cell = new mxCell('Receive', new mxGeometry(0, 0, 120, 80), 'html=1;whiteSpace=wrap;rounded=1;'); + cell.vertex = true; + + var cell1 = new mxCell('', new mxGeometry(0, 0, 20, 14), 'html=1;shape=message;outlineConnect=0;'); + cell1.vertex = true; + cell1.geometry.relative = true; + cell1.geometry.offset = new mxPoint(7, 7); + cell.insert(cell1); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Receive Task'); + }), + this.addEntry(this.getTagsForStencil('mxgraph.bpmn', 'user_task').join(' '), function() + { + var cell = new mxCell('User', new mxGeometry(0, 0, 120, 80), 'html=1;whiteSpace=wrap;rounded=1;'); + cell.vertex = true; + + var cell1 = new mxCell('', new mxGeometry(0, 0, 14, 14), 'html=1;shape=mxgraph.bpmn.user_task;outlineConnect=0;'); + cell1.vertex = true; + cell1.geometry.relative = true; + cell1.geometry.offset = new mxPoint(7, 7); + cell.insert(cell1); + + var cell2 = new mxCell('', new mxGeometry(0.5, 1, 14, 14), 'html=1;shape=plus;outlineConnect=0;'); + cell2.vertex = true; + cell2.geometry.relative = true; + cell2.geometry.offset = new mxPoint(-7, -14); + cell.insert(cell2); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'User Task'); + }), + this.addEntry(this.getTagsForStencil('mxgraph.bpmn', 'timer_start', 'attached').join(' '), function() + { + var cell = new mxCell('Process', new mxGeometry(0, 0, 120, 80), 'html=1;whiteSpace=wrap;rounded=1;'); + cell.vertex = true; + + var cell1 = new mxCell('', new mxGeometry(1, 1, 30, 30), 'shape=mxgraph.bpmn.timer_start;perimeter=ellipsePerimeter;html=1;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;verticalAlign=top;outlineConnect=0;'); + cell1.vertex = true; + cell1.geometry.relative = true; + cell1.geometry.offset = new mxPoint(-40, -15); + cell.insert(cell1); + + return sb.createVertexTemplateFromCells([cell], 120, 95, 'Attached Timer Event 1'); + }), + this.addEntry(this.getTagsForStencil('mxgraph.bpmn', 'timer_start', 'attached').join(' '), function() + { + var cell = new mxCell('Process', new mxGeometry(0, 0, 120, 80), 'html=1;whiteSpace=wrap;rounded=1;'); + cell.vertex = true; + + var cell1 = new mxCell('', new mxGeometry(1, 0, 30, 30), 'shape=mxgraph.bpmn.timer_start;perimeter=ellipsePerimeter;html=1;labelPosition=right;labelBackgroundColor=#ffffff;align=left;outlineConnect=0;'); + cell1.vertex = true; + cell1.geometry.relative = true; + cell1.geometry.offset = new mxPoint(-15, 10); + cell.insert(cell1); + + return sb.createVertexTemplateFromCells([cell], 135, 80, 'Attached Timer Event 2'); + }), + this.createVertexTemplateEntry('swimlane;html=1;horizontal=0;startSize=20;', 320, 240, 'Pool', 'Pool', null, null, 'bpmn pool'), + this.createVertexTemplateEntry('swimlane;html=1;horizontal=0;swimlaneFillColor=white;swimlaneLine=0;', 300, 120, 'Lane', 'Lane', null, null, 'bpmn lane'), + this.createVertexTemplateEntry('shape=hexagon;html=1;whiteSpace=wrap;perimeter=hexagonPerimeter;rounded=0;', 60, 50, '', 'Conversation', null, null, 'bpmn conversation'), + this.createVertexTemplateEntry('shape=hexagon;html=1;whiteSpace=wrap;perimeter=hexagonPerimeter;strokeWidth=4;rounded=0;', 60, 50, '', 'Call Conversation', null, null, 'bpmn call conversation'), + this.addEntry('bpmn subconversation sub conversation sub-conversation', function() + { + var cell = new mxCell('', new mxGeometry(0, 0, 60, 50), 'shape=hexagon;whiteSpace=wrap;html=1;perimeter=hexagonPerimeter;rounded=0;'); + cell.vertex = true; + + var cell1 = new mxCell('', new mxGeometry(0.5, 1, 14, 14), 'html=1;shape=plus;'); + cell1.vertex = true; + cell1.geometry.relative = true; + cell1.geometry.offset = new mxPoint(-7, -14); + cell.insert(cell1); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Sub-Conversation'); + }), + this.addEntry('bpmn data object', function() + { + var cell = new mxCell('', new mxGeometry(0, 0, 40, 60), 'shape=note;whiteSpace=wrap;size=16;html=1;'); + cell.vertex = true; + + var cell1 = new mxCell('', new mxGeometry(0, 0, 14, 14), 'html=1;shape=singleArrow;arrowWidth=0.4;arrowSize=0.4;outlineConnect=0;'); + cell1.vertex = true; + cell1.geometry.relative = true; + cell1.geometry.offset = new mxPoint(2, 2); + cell.insert(cell1); + + var cell2 = new mxCell('', new mxGeometry(0.5, 1, 14, 14), 'html=1;whiteSpace=wrap;shape=parallelMarker;outlineConnect=0;'); + cell2.vertex = true; + cell2.geometry.relative = true; + cell2.geometry.offset = new mxPoint(-7, -14); + cell.insert(cell2); + + return sb.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, 'Data Object'); + }), + this.createVertexTemplateEntry('shape=datastore;whiteSpace=wrap;html=1;', 60, 60, '', 'Data Store', null, null, 'bpmn data store'), + this.createVertexTemplateEntry('shape=plus;html=1;outlineConnect=0;', 14, 14, '', 'Sub-Process Marker', null, null, 'bpmn subprocess sub process sub-process marker'), + this.createVertexTemplateEntry('shape=mxgraph.bpmn.loop;html=1;outlineConnect=0;', 14, 14, '', 'Loop Marker', null, null, 'bpmn loop marker'), + this.createVertexTemplateEntry('shape=parallelMarker;html=1;outlineConnect=0;', 14, 14, '', 'Parallel MI Marker', null, null, 'bpmn parallel mi marker'), + this.createVertexTemplateEntry('shape=parallelMarker;direction=south;html=1;outlineConnect=0;', 14, 14, '', 'Sequential MI Marker', null, null, 'bpmn sequential mi marker'), + this.createVertexTemplateEntry('shape=mxgraph.bpmn.ad_hoc;fillColor=#000000;html=1;outlineConnect=0;', 14, 14, '', 'Ad Hoc Marker', null, null, 'bpmn ad hoc marker'), + this.createVertexTemplateEntry('shape=mxgraph.bpmn.compensation;html=1;outlineConnect=0;', 14, 14, '', 'Compensation Marker', null, null, 'bpmn compensation marker'), + this.createVertexTemplateEntry('shape=message;whiteSpace=wrap;html=1;outlineConnect=0;fillColor=#000000;strokeColor=#ffffff;strokeWidth=2;', 40, 30, '', 'Send Task', null, null, 'bpmn send task'), + this.createVertexTemplateEntry('shape=message;whiteSpace=wrap;html=1;outlineConnect=0;', 40, 30, '', 'Receive Task', null, null, 'bpmn receive task'), + this.createVertexTemplateEntry('shape=mxgraph.bpmn.user_task;html=1;outlineConnect=0;', 14, 14, '', 'User Task', null, null, this.getTagsForStencil('mxgraph.bpmn', 'user_task').join(' ')), + this.createVertexTemplateEntry('shape=mxgraph.bpmn.manual_task;html=1;outlineConnect=0;', 14, 14, '', 'Manual Task', null, null, this.getTagsForStencil('mxgraph.bpmn', 'user_task').join(' ')), + this.createVertexTemplateEntry('shape=mxgraph.bpmn.business_rule_task;html=1;outlineConnect=0;', 14, 14, '', 'Business Rule Task', null, null, this.getTagsForStencil('mxgraph.bpmn', 'business_rule_task').join(' ')), + this.createVertexTemplateEntry('shape=mxgraph.bpmn.service_task;html=1;outlineConnect=0;', 14, 14, '', 'Service Task', null, null, this.getTagsForStencil('mxgraph.bpmn', 'service_task').join(' ')), + this.createVertexTemplateEntry('shape=mxgraph.bpmn.script_task;html=1;outlineConnect=0;', 14, 14, '', 'Script Task', null, null, this.getTagsForStencil('mxgraph.bpmn', 'script_task').join(' ')), + this.createVertexTemplateEntry('html=1;shape=mxgraph.flowchart.annotation_2;align=left;', 50, 100, '', 'Annotation', null, null, this.getTagsForStencil('bpmn', 'annotation_1', 'bpmn business process model ').join(' ')), + this.createVertexTemplateEntry('rounded=1;arcSize=10;dashed=1;strokeColor=#000000;fillColor=none;gradientColor=none;dashPattern=8 3 1 3;strokeWidth=2;', + 200, 200, '', 'Group', null, null, this.getTagsForStencil('bpmn', 'group', 'bpmn business process model ').join(' ')), + this.createEdgeTemplateEntry('endArrow=block;endFill=1;endSize=6;html=1;', 100, 0, '', 'Sequence Flow', null, 'bpmn sequence flow'), + this.createEdgeTemplateEntry('startArrow=dash;startSize=8;endArrow=block;endFill=1;endSize=6;html=1;', 100, 0, '', 'Default Flow', null, 'bpmn default flow'), + this.createEdgeTemplateEntry('startArrow=diamondThin;startFill=0;startSize=14;endArrow=block;endFill=1;endSize=6;html=1;', 100, 0, '', 'Conditional Flow', null, 'bpmn conditional flow'), + this.createEdgeTemplateEntry('startArrow=oval;startFill=0;startSize=7;endArrow=block;endFill=0;endSize=10;dashed=1;html=1;', 100, 0, '', 'Message Flow 1', null, 'bpmn message flow'), + this.addEntry('bpmn message flow', function() + { + var edge = new mxCell('', new mxGeometry(0, 0, 0, 0), 'startArrow=oval;startFill=0;startSize=7;endArrow=block;endFill=0;endSize=10;dashed=1;html=1;'); + edge.geometry.setTerminalPoint(new mxPoint(0, 0), true); + edge.geometry.setTerminalPoint(new mxPoint(100, 0), false); + edge.geometry.relative = true; + edge.edge = true; + + var cell = new mxCell('', new mxGeometry(0, 0, 20, 14), 'shape=message;html=1;outlineConnect=0;'); + cell.geometry.relative = true; + cell.vertex = true; + cell.geometry.offset = new mxPoint(-10, -7); + edge.insert(cell); + + return sb.createEdgeTemplateFromCells([edge], 100, 0, 'Message Flow 2'); + }), + this.createEdgeTemplateEntry('shape=link;html=1;', 100, 0, '', 'Link', null, 'bpmn link') + ]; + + this.addPaletteFunctions('bpmn', 'BPMN ' + mxResources.get('general'), false, fns); +}; + +/** + * Creates and returns the given title element. + */ +Sidebar.prototype.createTitle = function(label) +{ + var elt = document.createElement('a'); + elt.setAttribute('href', 'javascript:void(0);'); + elt.setAttribute('title', mxResources.get('sidebarTooltip')); + elt.className = 'geTitle'; + mxUtils.write(elt, label); + + return elt; +}; + +/** + * Creates a thumbnail for the given cells. + */ +Sidebar.prototype.createThumb = function(cells, width, height, parent, title, showLabel, showTitle, realWidth, realHeight) +{ + this.graph.labelsVisible = (showLabel == null || showLabel); + var fo = mxClient.NO_FO; + mxClient.NO_FO = Editor.prototype.originalNoForeignObject; + this.graph.view.scaleAndTranslate(1, 0, 0); + this.graph.addCells(cells); + var bounds = this.graph.getGraphBounds(); + var s = Math.floor(Math.min((width - 2 * this.thumbBorder) / bounds.width, + (height - 2 * this.thumbBorder) / bounds.height) * 100) / 100; + this.graph.view.scaleAndTranslate(s, Math.floor((width - bounds.width * s) / 2 / s - bounds.x), + Math.floor((height - bounds.height * s) / 2 / s - bounds.y)); + var node = null; + + // For supporting HTML labels in IE9 standards mode the container is cloned instead + if (this.graph.dialect == mxConstants.DIALECT_SVG && !mxClient.NO_FO) + { + node = this.graph.view.getCanvas().ownerSVGElement.cloneNode(true); + } + // LATER: Check if deep clone can be used for quirks if container in DOM + else + { + node = this.graph.container.cloneNode(false); + node.innerHTML = this.graph.container.innerHTML; + + // Workaround for clipping in older IE versions + if (mxClient.IS_QUIRKS || document.documentMode == 8) + { + node.firstChild.style.overflow = 'visible'; + } + } + + this.graph.getModel().clear(); + mxClient.NO_FO = fo; + + // Catch-all event handling + if (mxClient.IS_IE6) + { + parent.style.backgroundImage = 'url(' + this.editorUi.editor.transparentImage + ')'; + } + + node.style.position = 'relative'; + node.style.overflow = 'hidden'; + node.style.cursor = 'move'; + node.style.left = this.thumbBorder + 'px'; + node.style.top = this.thumbBorder + 'px'; + node.style.width = width + 'px'; + node.style.height = height + 'px'; + node.style.visibility = ''; + node.style.minWidth = ''; + node.style.minHeight = ''; + + parent.appendChild(node); + + // Adds title for sidebar entries + if (this.sidebarTitles && title != null && showTitle != false) + { + var border = (mxClient.IS_QUIRKS) ? 2 * this.thumbPadding + 2: 0; + parent.style.height = (this.thumbHeight + border + this.sidebarTitleSize + 8) + 'px'; + + var div = document.createElement('div'); + div.style.fontSize = this.sidebarTitleSize + 'px'; + div.style.color = '#303030'; + div.style.textAlign = 'center'; + div.style.whiteSpace = 'nowrap'; + + if (mxClient.IS_IE) + { + div.style.height = (this.sidebarTitleSize + 12) + 'px'; + } + + div.style.paddingTop = '4px'; + mxUtils.write(div, title); + parent.appendChild(div); + } + + return bounds; +}; + +/** + * Creates and returns a new palette item for the given image. + */ +Sidebar.prototype.createItem = function(cells, title, showLabel, showTitle, width, height, allowCellsInserted) +{ + var elt = document.createElement('a'); + elt.setAttribute('href', 'javascript:void(0);'); + elt.className = 'geItem'; + elt.style.overflow = 'hidden'; + var border = (mxClient.IS_QUIRKS) ? 8 + 2 * this.thumbPadding : 2 * this.thumbBorder; + elt.style.width = (this.thumbWidth + border) + 'px'; + elt.style.height = (this.thumbHeight + border) + 'px'; + elt.style.padding = this.thumbPadding + 'px'; + + if (mxClient.IS_IE6) + { + elt.style.border = 'none'; + } + + // Blocks default click action + mxEvent.addListener(elt, 'click', function(evt) + { + mxEvent.consume(evt); + }); + + this.createThumb(cells, this.thumbWidth, this.thumbHeight, elt, title, showLabel, showTitle, width, height); + var bounds = new mxRectangle(0, 0, width, height); + + if (cells.length > 1 || cells[0].vertex) + { + var ds = this.createDragSource(elt, this.createDropHandler(cells, true, allowCellsInserted, + bounds), this.createDragPreview(width, height), cells, bounds); + this.addClickHandler(elt, ds, cells); + + // Uses guides for vertices only if enabled in graph + ds.isGuidesEnabled = mxUtils.bind(this, function() + { + return this.editorUi.editor.graph.graphHandler.guidesEnabled; + }); + } + else if (cells[0] != null && cells[0].edge) + { + var ds = this.createDragSource(elt, this.createDropHandler(cells, false, allowCellsInserted, + bounds), this.createDragPreview(width, height), cells, bounds); + this.addClickHandler(elt, ds, cells); + } + + // Shows a tooltip with the rendered cell + if (!mxClient.IS_IOS) + { + mxEvent.addGestureListeners(elt, null, mxUtils.bind(this, function(evt) + { + if (mxEvent.isMouseEvent(evt)) + { + this.showTooltip(elt, cells, bounds.width, bounds.height, title, showLabel); + } + })); + } + + return elt; +}; + +/** + * Creates a drop handler for inserting the given cells. + */ +Sidebar.prototype.updateShapes = function(source, targets) +{ + var graph = this.editorUi.editor.graph; + var sourceCellStyle = graph.getCellStyle(source); + var result = []; + + graph.model.beginUpdate(); + try + { + var cellStyle = graph.getModel().getStyle(source); + + // Lists the styles to carry over from the existing shape + var styles = ['shadow', 'dashed', 'dashPattern', 'fontFamily', 'fontSize', 'fontColor', 'align', 'startFill', + 'startSize', 'endFill', 'endSize', 'strokeColor', 'strokeWidth', 'fillColor', 'gradientColor', + 'html', 'part', 'noEdgeStyle', 'edgeStyle', 'elbow', 'childLayout', 'recursiveResize', + 'container', 'collapsible', 'connectable']; + + for (var i = 0; i < targets.length; i++) + { + var targetCell = targets[i]; + + if ((graph.getModel().isVertex(targetCell) == graph.getModel().isVertex(source)) || + (graph.getModel().isEdge(targetCell) == graph.getModel().isEdge(source))) + { + var state = graph.view.getState(targetCell); + var style = (state != null) ? state.style : graph.getCellStyle(targets[i]); + graph.getModel().setStyle(targetCell, cellStyle); + + // Removes all children of composite cells + if (state != null && mxUtils.getValue(state.style, 'composite', '0') == '1') + { + var childCount = graph.model.getChildCount(targetCell); + + for (var j = childCount; j >= 0; j--) + { + graph.model.remove(graph.model.getChildAt(targetCell, j)); + } + } + + if (style != null) + { + // Replaces the participant style in the lifeline shape with the target shape + if (style[mxConstants.STYLE_SHAPE] == 'umlLifeline' && + sourceCellStyle[mxConstants.STYLE_SHAPE] != 'umlLifeline') + { + graph.setCellStyles(mxConstants.STYLE_SHAPE, 'umlLifeline', [targetCell]); + graph.setCellStyles('participant', sourceCellStyle[mxConstants.STYLE_SHAPE], [targetCell]); + } + + for (var j = 0; j < styles.length; j++) + { + var value = style[styles[j]]; + + if (value != null) + { + graph.setCellStyles(styles[j], value, [targetCell]); + } + } + } + + result.push(targetCell); + } + } + } + finally + { + graph.model.endUpdate(); + } + + return result; +}; + +/** + * Creates a drop handler for inserting the given cells. + */ +Sidebar.prototype.createDropHandler = function(cells, allowSplit, allowCellsInserted, bounds) +{ + allowCellsInserted = (allowCellsInserted != null) ? allowCellsInserted : true; + + return mxUtils.bind(this, function(graph, evt, target, x, y, force) + { + var elt = (force) ? null : ((mxEvent.isTouchEvent(evt) || mxEvent.isPenEvent(evt)) ? + document.elementFromPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt)) : + mxEvent.getSource(evt)); + + while (elt != null && elt != this.container) + { + elt = elt.parentNode; + } + + if (elt == null && graph.isEnabled()) + { + cells = graph.getImportableCells(cells); + + if (cells.length > 0) + { + graph.stopEditing(); + + // Holding alt while mouse is released ignores drop target + var validDropTarget = (target != null && !mxEvent.isAltDown(evt)) ? + graph.isValidDropTarget(target, cells, evt) : false; + var select = null; + + if (target != null && !validDropTarget) + { + target = null; + } + + if (!graph.isCellLocked(target || graph.getDefaultParent())) + { + graph.model.beginUpdate(); + try + { + x = Math.round(x); + y = Math.round(y); + + // Splits the target edge or inserts into target group + if (allowSplit && graph.isSplitTarget(target, cells, evt)) + { + var clones = graph.cloneCells(cells); + graph.splitEdge(target, clones, null, + x - bounds.width / 2, y - bounds.height / 2); + select = clones; + } + else if (cells.length > 0) + { + select = graph.importCells(cells, x, y, target); + } + + // Executes parent layout hooks for position/order + if (graph.layoutManager != null) + { + var layout = graph.layoutManager.getLayout(target); + + if (layout != null) + { + var s = graph.view.scale; + var tr = graph.view.translate; + var tx = (x + tr.x) * s; + var ty = (y + tr.y) * s; + + for (var i = 0; i < select.length; i++) + { + layout.moveCell(select[i], tx, ty); + } + } + } + + if (allowCellsInserted) + { + graph.fireEvent(new mxEventObject('cellsInserted', 'cells', select)); + } + } + catch (e) + { + this.editorUi.handleError(e); + } + finally + { + graph.model.endUpdate(); + } + + if (select != null && select.length > 0) + { + graph.scrollCellToVisible(select[0]); + graph.setSelectionCells(select); + } + + if (graph.editAfterInsert && evt != null && mxEvent.isMouseEvent(evt) && + select != null && select.length == 1) + { + window.setTimeout(function() + { + graph.startEditing(select[0]); + }, 0); + } + } + } + + mxEvent.consume(evt); + } + }); +}; + +/** + * Creates and returns a preview element for the given width and height. + */ +Sidebar.prototype.createDragPreview = function(width, height) +{ + var elt = document.createElement('div'); + elt.style.border = this.dragPreviewBorder; + elt.style.width = width + 'px'; + elt.style.height = height + 'px'; + + return elt; +}; + +/** + * Creates a drag source for the given element. + */ +Sidebar.prototype.dropAndConnect = function(source, targets, direction, dropCellIndex, evt) +{ + var geo = this.getDropAndConnectGeometry(source, targets[dropCellIndex], direction, targets); + + // Targets without the new edge for selection + var tmp = []; + + if (geo != null) + { + var graph = this.editorUi.editor.graph; + var editingCell = null; + + graph.model.beginUpdate(); + try + { + var sourceGeo = graph.getCellGeometry(source); + var geo2 = graph.getCellGeometry(targets[dropCellIndex]); + + // Handles special case where target should be ignored for stack layouts + var targetParent = graph.model.getParent(source); + var validLayout = true; + + // Ignores parent if it has a stack layout + if (graph.layoutManager != null) + { + var layout = graph.layoutManager.getLayout(targetParent); + + // LATER: Use parent of parent if valid layout + if (layout != null && layout.constructor == mxStackLayout) + { + validLayout = false; + + var tmp = graph.view.getState(targetParent); + + // Offsets by parent position + if (tmp != null) + { + var offset = new mxPoint((tmp.x / graph.view.scale - graph.view.translate.x), + (tmp.y / graph.view.scale - graph.view.translate.y)); + geo.x += offset.x; + geo.y += offset.y; + var pt = geo.getTerminalPoint(false); + + if (pt != null) + { + pt.x += offset.x; + pt.y += offset.y; + } + } + } + } + + var dx = geo2.x; + var dy = geo2.y; + + // Ignores geometry of edges + if (graph.model.isEdge(targets[dropCellIndex])) + { + dx = 0; + dy = 0; + } + + var useParent = graph.model.isEdge(source) || (sourceGeo != null && !sourceGeo.relative && validLayout); + targets = graph.importCells(targets, (geo.x - (useParent ? dx : 0)), + (geo.y - (useParent ? dy : 0)), (useParent) ? targetParent : null); + tmp = targets; + + if (graph.model.isEdge(source)) + { + // Adds new terminal to edge + // LATER: Push new terminal out radially from edge start point + graph.model.setTerminal(source, targets[dropCellIndex], direction == mxConstants.DIRECTION_NORTH); + } + else if (graph.model.isEdge(targets[dropCellIndex])) + { + // Adds new outgoing connection to vertex and clears points + graph.model.setTerminal(targets[dropCellIndex], source, true); + var geo3 = graph.getCellGeometry(targets[dropCellIndex]); + geo3.points = null; + + if (geo3.getTerminalPoint(false) != null) + { + geo3.setTerminalPoint(geo.getTerminalPoint(false), false); + } + else if (useParent && graph.model.isVertex(targetParent)) + { + // Adds parent offset to other nodes + var tmpState = graph.view.getState(targetParent); + var offset = (tmpState.cell != graph.view.currentRoot) ? + new mxPoint((tmpState.x / graph.view.scale - graph.view.translate.x), + (tmpState.y / graph.view.scale - graph.view.translate.y)) : new mxPoint(0, 0); + + graph.cellsMoved(targets, offset.x, offset.y, null, null, true); + } + } + else + { + geo2 = graph.getCellGeometry(targets[dropCellIndex]); + dx = geo.x - Math.round(geo2.x); + dy = geo.y - Math.round(geo2.y); + geo.x = Math.round(geo2.x); + geo.y = Math.round(geo2.y); + graph.model.setGeometry(targets[dropCellIndex], geo); + graph.cellsMoved(targets, dx, dy, null, null, true); + tmp = targets.slice(); + editingCell = (tmp.length == 1) ? tmp[0] : null; + targets.push(graph.insertEdge(null, null, '', source, targets[dropCellIndex], + graph.createCurrentEdgeStyle())); + } + + graph.fireEvent(new mxEventObject('cellsInserted', 'cells', targets)); + } + catch (e) + { + this.editorUi.handleError(e); + } + finally + { + graph.model.endUpdate(); + } + + if (graph.editAfterInsert && evt != null && mxEvent.isMouseEvent(evt) && + editingCell != null) + { + window.setTimeout(function() + { + graph.startEditing(editingCell); + }, 0); + } + } + + return tmp; +}; + +/** + * Creates a drag source for the given element. + */ +Sidebar.prototype.getDropAndConnectGeometry = function(source, target, direction, targets) +{ + var graph = this.editorUi.editor.graph; + var view = graph.view; + var keepSize = targets.length > 1; + var geo = graph.getCellGeometry(source); + var geo2 = graph.getCellGeometry(target); + + if (geo != null && geo2 != null) + { + geo2 = geo2.clone(); + + if (graph.model.isEdge(source)) + { + var state = graph.view.getState(source); + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length - 1]; + + if (direction == mxConstants.DIRECTION_NORTH) + { + geo2.x = p0.x / view.scale - view.translate.x - geo2.width / 2; + geo2.y = p0.y / view.scale - view.translate.y - geo2.height / 2; + } + else + { + geo2.x = pe.x / view.scale - view.translate.x - geo2.width / 2; + geo2.y = pe.y / view.scale - view.translate.y - geo2.height / 2; + } + } + else + { + if (geo.relative) + { + var state = graph.view.getState(source); + geo = geo.clone(); + geo.x = (state.x - view.translate.x) / view.scale; + geo.y = (state.y - view.translate.y) / view.scale; + } + + var length = graph.defaultEdgeLength; + + // Maintains edge length + if (graph.model.isEdge(target) && geo2.getTerminalPoint(true) != null && geo2.getTerminalPoint(false) != null) + { + var p0 = geo2.getTerminalPoint(true); + var pe = geo2.getTerminalPoint(false); + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + + length = Math.sqrt(dx * dx + dy * dy); + + geo2.x = geo.getCenterX(); + geo2.y = geo.getCenterY(); + geo2.width = 1; + geo2.height = 1; + + if (direction == mxConstants.DIRECTION_NORTH) + { + geo2.height = length + geo2.y = geo.y - length; + geo2.setTerminalPoint(new mxPoint(geo2.x, geo2.y), false); + } + else if (direction == mxConstants.DIRECTION_EAST) + { + geo2.width = length + geo2.x = geo.x + geo.width; + geo2.setTerminalPoint(new mxPoint(geo2.x + geo2.width, geo2.y), false); + } + else if (direction == mxConstants.DIRECTION_SOUTH) + { + geo2.height = length + geo2.y = geo.y + geo.height; + geo2.setTerminalPoint(new mxPoint(geo2.x, geo2.y + geo2.height), false); + } + else if (direction == mxConstants.DIRECTION_WEST) + { + geo2.width = length + geo2.x = geo.x - length; + geo2.setTerminalPoint(new mxPoint(geo2.x, geo2.y), false); + } + } + else + { + // Try match size or ignore if width or height < 45 which + // is considered special enough to be ignored here + if (!keepSize && geo2.width > 45 && geo2.height > 45 && + geo.width > 45 && geo.height > 45) + { + geo2.width = geo2.width * (geo.height / geo2.height); + geo2.height = geo.height; + } + + geo2.x = geo.x + geo.width / 2 - geo2.width / 2; + geo2.y = geo.y + geo.height / 2 - geo2.height / 2; + + if (direction == mxConstants.DIRECTION_NORTH) + { + geo2.y = geo2.y - geo.height / 2 - geo2.height / 2 - length; + } + else if (direction == mxConstants.DIRECTION_EAST) + { + geo2.x = geo2.x + geo.width / 2 + geo2.width / 2 + length; + } + else if (direction == mxConstants.DIRECTION_SOUTH) + { + geo2.y = geo2.y + geo.height / 2 + geo2.height / 2 + length; + } + else if (direction == mxConstants.DIRECTION_WEST) + { + geo2.x = geo2.x - geo.width / 2 - geo2.width / 2 - length; + } + + // Adds offset to match cells without connecting edge + if (graph.model.isEdge(target) && geo2.getTerminalPoint(true) != null && target.getTerminal(false) != null) + { + var targetGeo = graph.getCellGeometry(target.getTerminal(false)); + + if (targetGeo != null) + { + if (direction == mxConstants.DIRECTION_NORTH) + { + geo2.x -= targetGeo.getCenterX(); + geo2.y -= targetGeo.getCenterY() + targetGeo.height / 2; + } + else if (direction == mxConstants.DIRECTION_EAST) + { + geo2.x -= targetGeo.getCenterX() - targetGeo.width / 2; + geo2.y -= targetGeo.getCenterY(); + } + else if (direction == mxConstants.DIRECTION_SOUTH) + { + geo2.x -= targetGeo.getCenterX(); + geo2.y -= targetGeo.getCenterY() - targetGeo.height / 2; + } + else if (direction == mxConstants.DIRECTION_WEST) + { + geo2.x -= targetGeo.getCenterX() + targetGeo.width / 2; + geo2.y -= targetGeo.getCenterY(); + } + } + } + } + } + } + + return geo2; +}; + +/** + * Creates a drag source for the given element. + */ +Sidebar.prototype.createDragSource = function(elt, dropHandler, preview, cells, bounds) +{ + // Checks if the cells contain any vertices + var ui = this.editorUi; + var graph = ui.editor.graph; + var freeSourceEdge = null; + var firstVertex = null; + var sidebar = this; + + for (var i = 0; i < cells.length; i++) + { + if (firstVertex == null && this.editorUi.editor.graph.model.isVertex(cells[i])) + { + firstVertex = i; + } + else if (freeSourceEdge == null && this.editorUi.editor.graph.model.isEdge(cells[i]) && + this.editorUi.editor.graph.model.getTerminal(cells[i], true) == null) + { + freeSourceEdge = i; + } + + if (firstVertex != null && freeSourceEdge != null) + { + break; + } + } + + var dragSource = mxUtils.makeDraggable(elt, this.editorUi.editor.graph, mxUtils.bind(this, function(graph, evt, target, x, y) + { + if (this.updateThread != null) + { + window.clearTimeout(this.updateThread); + } + + if (cells != null && currentStyleTarget != null && activeArrow == styleTarget) + { + var tmp = graph.isCellSelected(currentStyleTarget.cell) ? graph.getSelectionCells() : [currentStyleTarget.cell]; + var updatedCells = this.updateShapes((graph.model.isEdge(currentStyleTarget.cell)) ? cells[0] : cells[firstVertex], tmp); + graph.setSelectionCells(updatedCells); + } + else if (cells != null && activeArrow != null && currentTargetState != null && activeArrow != styleTarget) + { + var index = (graph.model.isEdge(currentTargetState.cell) || freeSourceEdge == null) ? firstVertex : freeSourceEdge; + graph.setSelectionCells(this.dropAndConnect(currentTargetState.cell, cells, direction, index, evt)); + } + else + { + dropHandler.apply(this, arguments); + } + + if (this.editorUi.hoverIcons != null) + { + this.editorUi.hoverIcons.update(graph.view.getState(graph.getSelectionCell())); + } + }), preview, 0, 0, graph.autoscroll, true, true); + + // Stops dragging if cancel is pressed + graph.addListener(mxEvent.ESCAPE, function(sender, evt) + { + if (dragSource.isActive()) + { + dragSource.reset(); + } + }); + + // Overrides mouseDown to ignore popup triggers + var mouseDown = dragSource.mouseDown; + + dragSource.mouseDown = function(evt) + { + if (!mxEvent.isPopupTrigger(evt) && !mxEvent.isMultiTouchEvent(evt)) + { + graph.stopEditing(); + mouseDown.apply(this, arguments); + } + }; + + // Workaround for event redirection via image tag in quirks and IE8 + function createArrow(img, tooltip) + { + var arrow = null; + + if (mxClient.IS_IE && !mxClient.IS_SVG) + { + // Workaround for PNG images in IE6 + if (mxClient.IS_IE6 && document.compatMode != 'CSS1Compat') + { + arrow = document.createElement(mxClient.VML_PREFIX + ':image'); + arrow.setAttribute('src', img.src); + arrow.style.borderStyle = 'none'; + } + else + { + arrow = document.createElement('div'); + arrow.style.backgroundImage = 'url(' + img.src + ')'; + arrow.style.backgroundPosition = 'center'; + arrow.style.backgroundRepeat = 'no-repeat'; + } + + arrow.style.width = (img.width + 4) + 'px'; + arrow.style.height = (img.height + 4) + 'px'; + arrow.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; + } + else + { + arrow = mxUtils.createImage(img.src); + arrow.style.width = img.width + 'px'; + arrow.style.height = img.height + 'px'; + } + + if (tooltip != null) + { + arrow.setAttribute('title', tooltip); + } + + mxUtils.setOpacity(arrow, (img == this.refreshTarget) ? 30 : 20); + arrow.style.position = 'absolute'; + arrow.style.cursor = 'crosshair'; + + return arrow; + }; + + var currentTargetState = null; + var currentStateHandle = null; + var currentStyleTarget = null; + var activeTarget = false; + + var arrowUp = createArrow(this.triangleUp, mxResources.get('connect')); + var arrowRight = createArrow(this.triangleRight, mxResources.get('connect')); + var arrowDown = createArrow(this.triangleDown, mxResources.get('connect')); + var arrowLeft = createArrow(this.triangleLeft, mxResources.get('connect')); + var styleTarget = createArrow(this.refreshTarget, mxResources.get('replace')); + // Workaround for actual parentNode not being updated in old IE + var styleTargetParent = null; + var roundSource = createArrow(this.roundDrop); + var roundTarget = createArrow(this.roundDrop); + var direction = mxConstants.DIRECTION_NORTH; + var activeArrow = null; + + function checkArrow(x, y, bounds, arrow) + { + if (arrow.parentNode != null) + { + if (mxUtils.contains(bounds, x, y)) + { + mxUtils.setOpacity(arrow, 100); + activeArrow = arrow; + } + else + { + mxUtils.setOpacity(arrow, (arrow == styleTarget) ? 30 : 20); + } + } + + return bounds; + }; + + // Hides guides and preview if target is active + var dsCreatePreviewElement = dragSource.createPreviewElement; + + // Stores initial size of preview element + dragSource.createPreviewElement = function(graph) + { + var elt = dsCreatePreviewElement.apply(this, arguments); + + // Pass-through events required to tooltip on replace shape + if (mxClient.IS_SVG) + { + elt.style.pointerEvents = 'none'; + } + + this.previewElementWidth = elt.style.width; + this.previewElementHeight = elt.style.height; + + return elt; + }; + + // Shows/hides hover icons + var dragEnter = dragSource.dragEnter; + dragSource.dragEnter = function(graph, evt) + { + if (ui.hoverIcons != null) + { + ui.hoverIcons.setDisplay('none'); + } + + dragEnter.apply(this, arguments); + }; + + var dragExit = dragSource.dragExit; + dragSource.dragExit = function(graph, evt) + { + if (ui.hoverIcons != null) + { + ui.hoverIcons.setDisplay(''); + } + + dragExit.apply(this, arguments); + }; + + dragSource.dragOver = function(graph, evt) + { + mxDragSource.prototype.dragOver.apply(this, arguments); + + if (this.currentGuide != null && activeArrow != null) + { + this.currentGuide.hide(); + } + + if (this.previewElement != null) + { + var view = graph.view; + + if (currentStyleTarget != null && activeArrow == styleTarget) + { + this.previewElement.style.display = (graph.model.isEdge(currentStyleTarget.cell)) ? 'none' : ''; + + this.previewElement.style.left = currentStyleTarget.x + 'px'; + this.previewElement.style.top = currentStyleTarget.y + 'px'; + this.previewElement.style.width = currentStyleTarget.width + 'px'; + this.previewElement.style.height = currentStyleTarget.height + 'px'; + } + else if (currentTargetState != null && activeArrow != null) + { + var index = (graph.model.isEdge(currentTargetState.cell) || freeSourceEdge == null) ? firstVertex : freeSourceEdge; + var geo = sidebar.getDropAndConnectGeometry(currentTargetState.cell, cells[index], direction, cells); + var geo2 = (!graph.model.isEdge(currentTargetState.cell)) ? graph.getCellGeometry(currentTargetState.cell) : null; + var geo3 = graph.getCellGeometry(cells[index]); + var parent = graph.model.getParent(currentTargetState.cell); + var dx = view.translate.x * view.scale; + var dy = view.translate.y * view.scale; + + if (geo2 != null && !geo2.relative && graph.model.isVertex(parent) && parent != view.currentRoot) + { + var pState = view.getState(parent); + + dx = pState.x; + dy = pState.y; + } + + var dx2 = geo3.x; + var dy2 = geo3.y; + + // Ignores geometry of edges + if (graph.model.isEdge(cells[index])) + { + dx2 = 0; + dy2 = 0; + } + + // Shows preview at drop location + this.previewElement.style.left = ((geo.x - dx2) * view.scale + dx) + 'px'; + this.previewElement.style.top = ((geo.y - dy2) * view.scale + dy) + 'px'; + + if (cells.length == 1) + { + this.previewElement.style.width = (geo.width * view.scale) + 'px'; + this.previewElement.style.height = (geo.height * view.scale) + 'px'; + } + + this.previewElement.style.display = ''; + } + else if (dragSource.currentHighlight.state != null && + graph.model.isEdge(dragSource.currentHighlight.state.cell)) + { + // Centers drop cells when splitting edges + this.previewElement.style.left = Math.round(parseInt(this.previewElement.style.left) - + bounds.width * view.scale / 2) + 'px'; + this.previewElement.style.top = Math.round(parseInt(this.previewElement.style.top) - + bounds.height * view.scale / 2) + 'px'; + } + else + { + this.previewElement.style.width = this.previewElementWidth; + this.previewElement.style.height = this.previewElementHeight; + this.previewElement.style.display = ''; + } + } + }; + + var startTime = new Date().getTime(); + var timeOnTarget = 0; + var prev = null; + + // Gets source cell style to compare shape below + var sourceCellStyle = this.editorUi.editor.graph.getCellStyle(cells[0]); + + // Allows drop into cell only if target is a valid root + dragSource.getDropTarget = mxUtils.bind(this, function(graph, x, y, evt) + { + // Alt means no targets at all + // LATER: Show preview where result will go + var cell = (!mxEvent.isAltDown(evt) && cells != null) ? graph.getCellAt(x, y) : null; + + // Uses connectable parent vertex if one exists + if (cell != null && !this.graph.isCellConnectable(cell)) + { + var parent = this.graph.getModel().getParent(cell); + + if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent)) + { + cell = parent; + } + } + + // Ignores locked cells + if (graph.isCellLocked(cell)) + { + cell = null; + } + + var state = graph.view.getState(cell); + activeArrow = null; + var bbox = null; + + // Time on target + if (prev != state) + { + prev = state; + startTime = new Date().getTime(); + timeOnTarget = 0; + + if (this.updateThread != null) + { + window.clearTimeout(this.updateThread); + } + + if (state != null) + { + this.updateThread = window.setTimeout(function() + { + if (activeArrow == null) + { + prev = state; + dragSource.getDropTarget(graph, x, y, evt); + } + }, this.dropTargetDelay + 10); + } + } + else + { + timeOnTarget = new Date().getTime() - startTime; + } + + // Shift means disabled, delayed on cells with children, shows after this.dropTargetDelay, hides after 2500ms + if (timeOnTarget < 2500 && state != null && !mxEvent.isShiftDown(evt) && + // If shape is equal or target has no stroke, fill and gradient then use longer delay except for images + (((mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE) != mxUtils.getValue(sourceCellStyle, mxConstants.STYLE_SHAPE) && + (mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE) != mxConstants.NONE || + mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, mxConstants.NONE) != mxConstants.NONE || + mxUtils.getValue(state.style, mxConstants.STYLE_GRADIENTCOLOR, mxConstants.NONE) != mxConstants.NONE)) || + mxUtils.getValue(sourceCellStyle, mxConstants.STYLE_SHAPE) == 'image') || + timeOnTarget > 1500 || graph.model.isEdge(state.cell)) && (timeOnTarget > this.dropTargetDelay) && + ((graph.model.isVertex(state.cell) && firstVertex != null) || + (graph.model.isEdge(state.cell) && graph.model.isEdge(cells[0])))) + { + currentStyleTarget = state; + var tmp = (graph.model.isEdge(state.cell)) ? graph.view.getPoint(state) : + new mxPoint(state.getCenterX(), state.getCenterY()); + tmp = new mxRectangle(tmp.x - this.refreshTarget.width / 2, tmp.y - this.refreshTarget.height / 2, + this.refreshTarget.width, this.refreshTarget.height); + + styleTarget.style.left = Math.floor(tmp.x) + 'px'; + styleTarget.style.top = Math.floor(tmp.y) + 'px'; + + if (styleTargetParent == null) + { + graph.container.appendChild(styleTarget); + styleTargetParent = styleTarget.parentNode; + } + + checkArrow(x, y, tmp, styleTarget); + } + // Does not reset on ignored edges + else if (currentStyleTarget == null || !mxUtils.contains(currentStyleTarget, x, y) || + (timeOnTarget > 1500 && !mxEvent.isShiftDown(evt))) + { + currentStyleTarget = null; + + if (styleTargetParent != null) + { + styleTarget.parentNode.removeChild(styleTarget); + styleTargetParent = null; + } + } + else if (currentStyleTarget != null && styleTargetParent != null) + { + // Sets active Arrow as side effect + var tmp = (graph.model.isEdge(currentStyleTarget.cell)) ? graph.view.getPoint(currentStyleTarget) : new mxPoint(currentStyleTarget.getCenterX(), currentStyleTarget.getCenterY()); + tmp = new mxRectangle(tmp.x - this.refreshTarget.width / 2, tmp.y - this.refreshTarget.height / 2, + this.refreshTarget.width, this.refreshTarget.height); + checkArrow(x, y, tmp, styleTarget); + } + + // Checks if inside bounds + if (activeTarget && currentTargetState != null && !mxEvent.isAltDown(evt) && activeArrow == null) + { + // LATER: Use hit-detection for edges + bbox = mxRectangle.fromRectangle(currentTargetState); + + if (graph.model.isEdge(currentTargetState.cell)) + { + var pts = currentTargetState.absolutePoints; + + if (roundSource.parentNode != null) + { + var p0 = pts[0]; + bbox.add(checkArrow(x, y, new mxRectangle(p0.x - this.roundDrop.width / 2, + p0.y - this.roundDrop.height / 2, this.roundDrop.width, this.roundDrop.height), roundSource)); + } + + if (roundTarget.parentNode != null) + { + var pe = pts[pts.length - 1]; + bbox.add(checkArrow(x, y, new mxRectangle(pe.x - this.roundDrop.width / 2, + pe.y - this.roundDrop.height / 2, + this.roundDrop.width, this.roundDrop.height), roundTarget)); + } + } + else + { + var bds = mxRectangle.fromRectangle(currentTargetState); + + // Uses outer bounding box to take rotation into account + if (currentTargetState.shape != null && currentTargetState.shape.boundingBox != null) + { + bds = mxRectangle.fromRectangle(currentTargetState.shape.boundingBox); + } + + bds.grow(this.graph.tolerance); + bds.grow(HoverIcons.prototype.arrowSpacing); + + var handler = this.graph.selectionCellsHandler.getHandler(currentTargetState.cell); + + if (handler != null) + { + bds.x -= handler.horizontalOffset / 2; + bds.y -= handler.verticalOffset / 2; + bds.width += handler.horizontalOffset; + bds.height += handler.verticalOffset; + + // Adds bounding box of rotation handle to avoid overlap + if (handler.rotationShape != null && handler.rotationShape.node != null && + handler.rotationShape.node.style.visibility != 'hidden' && + handler.rotationShape.node.style.display != 'none' && + handler.rotationShape.boundingBox != null) + { + bds.add(handler.rotationShape.boundingBox); + } + } + + bbox.add(checkArrow(x, y, new mxRectangle(currentTargetState.getCenterX() - this.triangleUp.width / 2, + bds.y - this.triangleUp.height, this.triangleUp.width, this.triangleUp.height), arrowUp)); + bbox.add(checkArrow(x, y, new mxRectangle(bds.x + bds.width, + currentTargetState.getCenterY() - this.triangleRight.height / 2, + this.triangleRight.width, this.triangleRight.height), arrowRight)); + bbox.add(checkArrow(x, y, new mxRectangle(currentTargetState.getCenterX() - this.triangleDown.width / 2, + bds.y + bds.height, this.triangleDown.width, this.triangleDown.height), arrowDown)); + bbox.add(checkArrow(x, y, new mxRectangle(bds.x - this.triangleLeft.width, + currentTargetState.getCenterY() - this.triangleLeft.height / 2, + this.triangleLeft.width, this.triangleLeft.height), arrowLeft)); + } + + // Adds tolerance + if (bbox != null) + { + bbox.grow(10); + } + } + + direction = mxConstants.DIRECTION_NORTH; + + if (activeArrow == arrowRight) + { + direction = mxConstants.DIRECTION_EAST; + } + else if (activeArrow == arrowDown || activeArrow == roundTarget) + { + direction = mxConstants.DIRECTION_SOUTH; + } + else if (activeArrow == arrowLeft) + { + direction = mxConstants.DIRECTION_WEST; + } + + if (currentStyleTarget != null && activeArrow == styleTarget) + { + state = currentStyleTarget; + } + + var validTarget = (firstVertex == null || graph.isCellConnectable(cells[firstVertex])) && + ((graph.model.isEdge(cell) && firstVertex != null) || + (graph.model.isVertex(cell) && graph.isCellConnectable(cell))); + + // Drop arrows shown after this.dropTargetDelay, hidden after 5 secs, switches arrows after 500ms + if ((currentTargetState != null && timeOnTarget >= 5000) || + (currentTargetState != state && + (bbox == null || !mxUtils.contains(bbox, x, y) || + (timeOnTarget > 500 && activeArrow == null && validTarget)))) + { + activeTarget = false; + currentTargetState = ((timeOnTarget < 5000 && timeOnTarget > this.dropTargetDelay) || graph.model.isEdge(cell)) ? state : null; + + if (currentTargetState != null && validTarget) + { + var elts = [roundSource, roundTarget, arrowUp, arrowRight, arrowDown, arrowLeft]; + + for (var i = 0; i < elts.length; i++) + { + if (elts[i].parentNode != null) + { + elts[i].parentNode.removeChild(elts[i]); + } + } + + if (graph.model.isEdge(cell)) + { + var pts = state.absolutePoints; + + if (pts != null) + { + var p0 = pts[0]; + var pe = pts[pts.length - 1]; + var tol = graph.tolerance; + var box = new mxRectangle(x - tol, y - tol, 2 * tol, 2 * tol); + + roundSource.style.left = Math.floor(p0.x - this.roundDrop.width / 2) + 'px'; + roundSource.style.top = Math.floor(p0.y - this.roundDrop.height / 2) + 'px'; + + roundTarget.style.left = Math.floor(pe.x - this.roundDrop.width / 2) + 'px'; + roundTarget.style.top = Math.floor(pe.y - this.roundDrop.height / 2) + 'px'; + + if (graph.model.getTerminal(cell, true) == null) + { + graph.container.appendChild(roundSource); + } + + if (graph.model.getTerminal(cell, false) == null) + { + graph.container.appendChild(roundTarget); + } + } + } + else + { + var bds = mxRectangle.fromRectangle(state); + + // Uses outer bounding box to take rotation into account + if (state.shape != null && state.shape.boundingBox != null) + { + bds = mxRectangle.fromRectangle(state.shape.boundingBox); + } + + bds.grow(this.graph.tolerance); + bds.grow(HoverIcons.prototype.arrowSpacing); + + var handler = this.graph.selectionCellsHandler.getHandler(state.cell); + + if (handler != null) + { + bds.x -= handler.horizontalOffset / 2; + bds.y -= handler.verticalOffset / 2; + bds.width += handler.horizontalOffset; + bds.height += handler.verticalOffset; + + // Adds bounding box of rotation handle to avoid overlap + if (handler.rotationShape != null && handler.rotationShape.node != null && + handler.rotationShape.node.style.visibility != 'hidden' && + handler.rotationShape.node.style.display != 'none' && + handler.rotationShape.boundingBox != null) + { + bds.add(handler.rotationShape.boundingBox); + } + } + + arrowUp.style.left = Math.floor(state.getCenterX() - this.triangleUp.width / 2) + 'px'; + arrowUp.style.top = Math.floor(bds.y - this.triangleUp.height) + 'px'; + + arrowRight.style.left = Math.floor(bds.x + bds.width) + 'px'; + arrowRight.style.top = Math.floor(state.getCenterY() - this.triangleRight.height / 2) + 'px'; + + arrowDown.style.left = arrowUp.style.left + arrowDown.style.top = Math.floor(bds.y + bds.height) + 'px'; + + arrowLeft.style.left = Math.floor(bds.x - this.triangleLeft.width) + 'px'; + arrowLeft.style.top = arrowRight.style.top; + + if (state.style['portConstraint'] != 'eastwest') + { + graph.container.appendChild(arrowUp); + graph.container.appendChild(arrowDown); + } + + graph.container.appendChild(arrowRight); + graph.container.appendChild(arrowLeft); + } + + // Hides handle for cell under mouse + if (state != null) + { + currentStateHandle = graph.selectionCellsHandler.getHandler(state.cell); + + if (currentStateHandle != null && currentStateHandle.setHandlesVisible != null) + { + currentStateHandle.setHandlesVisible(false); + } + } + + activeTarget = true; + } + else + { + var elts = [roundSource, roundTarget, arrowUp, arrowRight, arrowDown, arrowLeft]; + + for (var i = 0; i < elts.length; i++) + { + if (elts[i].parentNode != null) + { + elts[i].parentNode.removeChild(elts[i]); + } + } + } + } + + if (!activeTarget && currentStateHandle != null) + { + currentStateHandle.setHandlesVisible(true); + } + + // Handles drop target + var target = ((!mxEvent.isAltDown(evt) || mxEvent.isShiftDown(evt)) && + !(currentStyleTarget != null && activeArrow == styleTarget)) ? + mxDragSource.prototype.getDropTarget.apply(this, arguments) : null; + var model = graph.getModel(); + + if (target != null) + { + if (activeArrow != null || !graph.isSplitTarget(target, cells, evt)) + { + // Selects parent group as drop target + while (target != null && !graph.isValidDropTarget(target, cells, evt) && model.isVertex(model.getParent(target))) + { + target = model.getParent(target); + } + + if (graph.view.currentRoot == target || (!graph.isValidRoot(target) && + graph.getModel().getChildCount(target) == 0) || + graph.isCellLocked(target) || model.isEdge(target)) + { + target = null; + } + } + } + + return target; + }); + + dragSource.stopDrag = function() + { + mxDragSource.prototype.stopDrag.apply(this, arguments); + + var elts = [roundSource, roundTarget, styleTarget, arrowUp, arrowRight, arrowDown, arrowLeft]; + + for (var i = 0; i < elts.length; i++) + { + if (elts[i].parentNode != null) + { + elts[i].parentNode.removeChild(elts[i]); + } + } + + if (currentTargetState != null && currentStateHandle != null) + { + currentStateHandle.reset(); + } + + currentStateHandle = null; + currentTargetState = null; + currentStyleTarget = null; + styleTargetParent = null; + activeArrow = null; + }; + + return dragSource; +}; + +/** + * Adds a handler for inserting the cell with a single click. + */ +Sidebar.prototype.itemClicked = function(cells, ds, evt, elt) +{ + var graph = this.editorUi.editor.graph; + graph.container.focus(); + + // Alt+Click inserts and connects + if (mxEvent.isAltDown(evt)) + { + if (graph.getSelectionCount() == 1 && graph.model.isVertex(graph.getSelectionCell())) + { + var firstVertex = null; + + for (var i = 0; i < cells.length && firstVertex == null; i++) + { + if (graph.model.isVertex(cells[i])) + { + firstVertex = i; + } + } + + if (firstVertex != null) + { + graph.setSelectionCells(this.dropAndConnect(graph.getSelectionCell(), cells, (mxEvent.isMetaDown(evt) || mxEvent.isControlDown(evt)) ? + (mxEvent.isShiftDown(evt) ? mxConstants.DIRECTION_WEST : mxConstants.DIRECTION_NORTH) : + (mxEvent.isShiftDown(evt) ? mxConstants.DIRECTION_EAST : mxConstants.DIRECTION_SOUTH), + firstVertex, evt)); + graph.scrollCellToVisible(graph.getSelectionCell()); + } + } + } + // Shift+Click updates shape + else if (mxEvent.isShiftDown(evt) && !graph.isSelectionEmpty()) + { + this.updateShapes(cells[0], graph.getSelectionCells()); + graph.scrollCellToVisible(graph.getSelectionCell()); + } + else + { + var pt = graph.getFreeInsertPoint(); + + if (mxEvent.isShiftDown(evt)) + { + var bounds = graph.getGraphBounds(); + var tr = graph.view.translate; + var s = graph.view.scale; + pt.x = bounds.x / s - tr.x + bounds.width / s + graph.gridSize; + pt.y = bounds.y / s - tr.y; + } + + ds.drop(graph, evt, null, pt.x, pt.y, true); + + if (this.editorUi.hoverIcons != null && (mxEvent.isTouchEvent(evt) || mxEvent.isPenEvent(evt))) + { + this.editorUi.hoverIcons.update(graph.view.getState(graph.getSelectionCell())); + } + } +}; + +/** + * Adds a handler for inserting the cell with a single click. + */ +Sidebar.prototype.addClickHandler = function(elt, ds, cells) +{ + var graph = this.editorUi.editor.graph; + var oldMouseDown = ds.mouseDown; + var oldMouseMove = ds.mouseMove; + var oldMouseUp = ds.mouseUp; + var tol = graph.tolerance; + var first = null; + var sb = this; + + ds.mouseDown =function(evt) + { + oldMouseDown.apply(this, arguments); + first = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + + if (this.dragElement != null) + { + this.dragElement.style.display = 'none'; + mxUtils.setOpacity(elt, 50); + } + }; + + ds.mouseMove = function(evt) + { + if (this.dragElement != null && this.dragElement.style.display == 'none' && + first != null && (Math.abs(first.x - mxEvent.getClientX(evt)) > tol || + Math.abs(first.y - mxEvent.getClientY(evt)) > tol)) + { + this.dragElement.style.display = ''; + mxUtils.setOpacity(elt, 100); + } + + oldMouseMove.apply(this, arguments); + }; + + ds.mouseUp = function(evt) + { + if (!mxEvent.isPopupTrigger(evt) && this.currentGraph == null && + this.dragElement != null && this.dragElement.style.display == 'none') + { + sb.itemClicked(cells, ds, evt, elt); + } + + oldMouseUp.apply(ds, arguments); + mxUtils.setOpacity(elt, 100); + first = null; + + // Blocks tooltips on this element after single click + sb.currentElt = elt; + }; +}; + +/** + * Creates a drop handler for inserting the given cells. + */ +Sidebar.prototype.createVertexTemplateEntry = function(style, width, height, value, title, showLabel, showTitle, tags) +{ + tags = (tags != null && tags.length > 0) ? tags : title.toLowerCase(); + + return this.addEntry(tags, mxUtils.bind(this, function() + { + return this.createVertexTemplate(style, width, height, value, title, showLabel, showTitle); + })); +} + +/** + * Creates a drop handler for inserting the given cells. + */ +Sidebar.prototype.createVertexTemplate = function(style, width, height, value, title, showLabel, showTitle, allowCellsInserted) +{ + var cells = [new mxCell((value != null) ? value : '', new mxGeometry(0, 0, width, height), style)]; + cells[0].vertex = true; + + return this.createVertexTemplateFromCells(cells, width, height, title, showLabel, showTitle, allowCellsInserted); +}; + +/** + * Creates a drop handler for inserting the given cells. + */ +Sidebar.prototype.createVertexTemplateFromData = function(data, width, height, title, showLabel, showTitle, allowCellsInserted) +{ + var doc = mxUtils.parseXml(this.graph.decompress(data)); + var codec = new mxCodec(doc); + + var model = new mxGraphModel(); + codec.decode(doc.documentElement, model); + + var cells = this.graph.cloneCells(model.root.getChildAt(0).children); + + return this.createVertexTemplateFromCells(cells, width, height, title, showLabel, showTitle, allowCellsInserted); +}; + +/** + * Creates a drop handler for inserting the given cells. + */ +Sidebar.prototype.createVertexTemplateFromCells = function(cells, width, height, title, showLabel, showTitle, allowCellsInserted) +{ + // Use this line to convert calls to this function with lots of boilerplate code for creating cells + //console.trace('xml', this.graph.compress(mxUtils.getXml(this.graph.encodeCells(cells))), cells); + return this.createItem(cells, title, showLabel, showTitle, width, height, allowCellsInserted); +}; + +/** + * + */ +Sidebar.prototype.createEdgeTemplateEntry = function(style, width, height, value, title, showLabel, tags, allowCellsInserted) +{ + tags = (tags != null && tags.length > 0) ? tags : title.toLowerCase(); + + return this.addEntry(tags, mxUtils.bind(this, function() + { + return this.createEdgeTemplate(style, width, height, value, title, showLabel, allowCellsInserted); + })); +}; + +/** + * Creates a drop handler for inserting the given cells. + */ +Sidebar.prototype.createEdgeTemplate = function(style, width, height, value, title, showLabel, allowCellsInserted) +{ + var cell = new mxCell((value != null) ? value : '', new mxGeometry(0, 0, width, height), style); + cell.geometry.setTerminalPoint(new mxPoint(0, height), true); + cell.geometry.setTerminalPoint(new mxPoint(width, 0), false); + cell.geometry.relative = true; + cell.edge = true; + + return this.createEdgeTemplateFromCells([cell], width, height, title, showLabel, allowCellsInserted); +}; + +/** + * Creates a drop handler for inserting the given cells. + */ +Sidebar.prototype.createEdgeTemplateFromCells = function(cells, width, height, title, showLabel, allowCellsInserted) +{ + return this.createItem(cells, title, showLabel, true, width, height, allowCellsInserted); +}; + +/** + * Adds the given palette. + */ +Sidebar.prototype.addPaletteFunctions = function(id, title, expanded, fns) +{ + this.addPalette(id, title, expanded, mxUtils.bind(this, function(content) + { + for (var i = 0; i < fns.length; i++) + { + content.appendChild(fns[i](content)); + } + })); +}; + +/** + * Adds the given palette. + */ +Sidebar.prototype.addPalette = function(id, title, expanded, onInit) +{ + var elt = this.createTitle(title); + this.container.appendChild(elt); + + var div = document.createElement('div'); + div.className = 'geSidebar'; + + // Disables built-in pan and zoom in IE10 and later + if (mxClient.IS_POINTER) + { + div.style.touchAction = 'none'; + } + + if (expanded) + { + onInit(div); + onInit = null; + } + else + { + div.style.display = 'none'; + } + + this.addFoldingHandler(elt, div, onInit); + + var outer = document.createElement('div'); + outer.appendChild(div); + this.container.appendChild(outer); + + // Keeps references to the DOM nodes + if (id != null) + { + this.palettes[id] = [elt, outer]; + } + + return div; +}; + +/** + * Create the given title element. + */ +Sidebar.prototype.addFoldingHandler = function(title, content, funct) +{ + var initialized = false; + + // Avoids mixed content warning in IE6-8 + if (!mxClient.IS_IE || document.documentMode >= 8) + { + title.style.backgroundImage = (content.style.display == 'none') ? + 'url(\'' + this.collapsedImage + '\')' : 'url(\'' + this.expandedImage + '\')'; + } + + title.style.backgroundRepeat = 'no-repeat'; + title.style.backgroundPosition = '0% 50%'; + + mxEvent.addListener(title, 'click', mxUtils.bind(this, function(evt) + { + if (content.style.display == 'none') + { + if (!initialized) + { + initialized = true; + + if (funct != null) + { + // Wait cursor does not show up on Mac + title.style.cursor = 'wait'; + var prev = title.innerHTML; + title.innerHTML = mxResources.get('loading') + '...'; + + window.setTimeout(function() + { + var fo = mxClient.NO_FO; + mxClient.NO_FO = Editor.prototype.originalNoForeignObject; + funct(content); + mxClient.NO_FO = fo; + content.style.display = 'block'; + title.style.cursor = ''; + title.innerHTML = prev; + }, 0); + } + else + { + content.style.display = 'block'; + } + } + else + { + content.style.display = 'block'; + } + + title.style.backgroundImage = 'url(\'' + this.expandedImage + '\')'; + } + else + { + title.style.backgroundImage = 'url(\'' + this.collapsedImage + '\')'; + content.style.display = 'none'; + } + + mxEvent.consume(evt); + })); +}; + +/** + * Removes the palette for the given ID. + */ +Sidebar.prototype.removePalette = function(id) +{ + var elts = this.palettes[id]; + + if (elts != null) + { + this.palettes[id] = null; + + for (var i = 0; i < elts.length; i++) + { + this.container.removeChild(elts[i]); + } + + return true; + } + + return false; +}; + +/** + * Adds the given image palette. + */ +Sidebar.prototype.addImagePalette = function(id, title, prefix, postfix, items, titles, tags) +{ + var showTitles = titles != null; + var fns = []; + + for (var i = 0; i < items.length; i++) + { + (mxUtils.bind(this, function(item, title, tmpTags) + { + if (tmpTags == null) + { + var slash = item.lastIndexOf('/'); + var dot = item.lastIndexOf('.'); + tmpTags = item.substring((slash >= 0) ? slash + 1 : 0, (dot >= 0) ? dot : item.length).replace(/[-_]/g, ' '); + } + + fns.push(this.createVertexTemplateEntry('image;html=1;labelBackgroundColor=#ffffff;image=' + prefix + item + postfix, + this.defaultImageWidth, this.defaultImageHeight, '', title, title != null, null, this.filterTags(tmpTags))); + }))(items[i], (titles != null) ? titles[i] : null, (tags != null) ? tags[items[i]] : null); + } + + this.addPaletteFunctions(id, title, false, fns); +}; + +/** + * Creates the array of tags for the given stencil. Duplicates are allowed and will be filtered out later. + */ +Sidebar.prototype.getTagsForStencil = function(packageName, stencilName, moreTags) +{ + var tags = packageName.split('.'); + + for (var i = 1; i < tags.length; i++) + { + tags[i] = tags[i].replace(/_/g, ' ') + } + + tags.push(stencilName.replace(/_/g, ' ')); + + if (moreTags != null) + { + tags.push(moreTags); + } + + return tags.slice(1, tags.length); +}; + +/** + * Adds the given stencil palette. + */ +Sidebar.prototype.addStencilPalette = function(id, title, stencilFile, style, ignore, onInit, scale, tags, customFns) +{ + scale = (scale != null) ? scale : 1; + + if (this.addStencilsToIndex) + { + // LATER: Handle asynchronous loading dependency + var fns = []; + + if (customFns != null) + { + for (var i = 0; i < customFns.length; i++) + { + fns.push(customFns[i]); + } + } + + mxStencilRegistry.loadStencilSet(stencilFile, mxUtils.bind(this, function(packageName, stencilName, displayName, w, h) + { + if (ignore == null || mxUtils.indexOf(ignore, stencilName) < 0) + { + var tmp = this.getTagsForStencil(packageName, stencilName); + var tmpTags = (tags != null) ? tags[stencilName] : null; + + if (tmpTags != null) + { + tmp.push(tmpTags); + } + + fns.push(this.createVertexTemplateEntry('shape=' + packageName + stencilName.toLowerCase() + style, + Math.round(w * scale), Math.round(h * scale), '', stencilName.replace(/_/g, ' '), null, null, + this.filterTags(tmp.join(' ')))); + } + }), true, true); + + this.addPaletteFunctions(id, title, false, fns); + } + else + { + this.addPalette(id, title, false, mxUtils.bind(this, function(content) + { + if (style == null) + { + style = ''; + } + + if (onInit != null) + { + onInit.call(this, content); + } + + if (customFns != null) + { + for (var i = 0; i < customFns.length; i++) + { + customFns[i](content); + } + } + + mxStencilRegistry.loadStencilSet(stencilFile, mxUtils.bind(this, function(packageName, stencilName, displayName, w, h) + { + if (ignore == null || mxUtils.indexOf(ignore, stencilName) < 0) + { + content.appendChild(this.createVertexTemplate('shape=' + packageName + stencilName.toLowerCase() + style, + Math.round(w * scale), Math.round(h * scale), '', stencilName.replace(/_/g, ' '), true)); + } + }), true); + })); + } +}; + +/** + * Adds the given stencil palette. + */ +Sidebar.prototype.destroy = function() +{ + if (this.graph != null) + { + if (this.graph.container != null && this.graph.container.parentNode != null) + { + this.graph.container.parentNode.removeChild(this.graph.container); + } + + this.graph.destroy(); + this.graph = null; + } + + if (this.pointerUpHandler != null) + { + mxEvent.removeListener(document, (mxClient.IS_POINTER) ? 'pointerup' : 'mouseup', this.pointerUpHandler); + this.pointerUpHandler = null; + } + + if (this.pointerDownHandler != null) + { + mxEvent.removeListener(document, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown', this.pointerDownHandler); + this.pointerDownHandler = null; + } + + if (this.pointerMoveHandler != null) + { + mxEvent.removeListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.pointerMoveHandler); + this.pointerMoveHandler = null; + } + + if (this.pointerOutHandler != null) + { + mxEvent.removeListener(document, (mxClient.IS_POINTER) ? 'pointerout' : 'mouseout', this.pointerOutHandler); + this.pointerOutHandler = null; + } +}; diff --git a/media/grapheditor/js/Toolbar.js b/media/grapheditor/js/Toolbar.js new file mode 100644 index 0000000000..be50f3bbaf --- /dev/null +++ b/media/grapheditor/js/Toolbar.js @@ -0,0 +1,957 @@ +/** + * Copyright (c) 2006-2012, JGraph Ltd + */ +/** + * Construcs a new toolbar for the given editor. + */ +function Toolbar(editorUi, container) +{ + this.editorUi = editorUi; + this.container = container; + this.staticElements = []; + this.init(); + + // Global handler to hide the current menu + this.gestureHandler = mxUtils.bind(this, function(evt) + { + if (this.editorUi.currentMenu != null && mxEvent.getSource(evt) != this.editorUi.currentMenu.div) + { + this.hideMenu(); + } + }); + + mxEvent.addGestureListeners(document, this.gestureHandler); +}; + +/** + * Image for the dropdown arrow. + */ +Toolbar.prototype.dropdownImage = (!mxClient.IS_SVG) ? IMAGE_PATH + '/dropdown.gif' : 'data:image/gif;base64,R0lGODlhDQANAIABAHt7e////yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjAgNjEuMTM0Nzc3LCAyMDEwLzAyLzEyLTE3OjMyOjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCREM1NkJFMjE0NEMxMUU1ODk1Q0M5MjQ0MTA4QjNDMSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCREM1NkJFMzE0NEMxMUU1ODk1Q0M5MjQ0MTA4QjNDMSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkQzOUMzMjZCMTQ0QjExRTU4OTVDQzkyNDQxMDhCM0MxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkQzOUMzMjZDMTQ0QjExRTU4OTVDQzkyNDQxMDhCM0MxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAQAAAQAsAAAAAA0ADQAAAhGMj6nL3QAjVHIu6azbvPtWAAA7'; + +/** + * Image element for the dropdown arrow. + */ +Toolbar.prototype.dropdownImageHtml = ''; + +/** + * Defines the background for selected buttons. + */ +Toolbar.prototype.selectedBackground = '#d0d0d0'; + +/** + * Defines the background for selected buttons. + */ +Toolbar.prototype.unselectedBackground = 'none'; + +/** + * Array that contains the DOM nodes that should never be removed. + */ +Toolbar.prototype.staticElements = null; + +/** + * Adds the toolbar elements. + */ +Toolbar.prototype.init = function() +{ + var sw = screen.width; + + // Takes into account initial compact mode + sw -= (screen.height > 740) ? 56 : 0; + + if (sw >= 700) + { + var formatMenu = this.addMenu('', mxResources.get('view') + ' (' + mxResources.get('panTooltip') + ')', true, 'viewPanels', null, true); + this.addDropDownArrow(formatMenu, 'geSprite-formatpanel', 38, 50, -4, -3, 36, -8); + this.addSeparator(); + } + + var viewMenu = this.addMenu('', mxResources.get('zoom') + ' (Alt+Mousewheel)', true, 'viewZoom', null, true); + viewMenu.showDisabled = true; + viewMenu.style.whiteSpace = 'nowrap'; + viewMenu.style.position = 'relative'; + viewMenu.style.overflow = 'hidden'; + + if (EditorUi.compactUi) + { + viewMenu.style.width = (mxClient.IS_QUIRKS) ? '58px' : '50px'; + } + else + { + viewMenu.style.width = (mxClient.IS_QUIRKS) ? '62px' : '36px'; + } + + if (sw >= 420) + { + this.addSeparator(); + var elts = this.addItems(['zoomIn', 'zoomOut']); + elts[0].setAttribute('title', mxResources.get('zoomIn') + ' (' + this.editorUi.actions.get('zoomIn').shortcut + ')'); + elts[1].setAttribute('title', mxResources.get('zoomOut') + ' (' + this.editorUi.actions.get('zoomOut').shortcut + ')'); + } + + // Updates the label if the scale changes + this.updateZoom = mxUtils.bind(this, function() + { + viewMenu.innerHTML = Math.round(this.editorUi.editor.graph.view.scale * 100) + '%' + + this.dropdownImageHtml; + + if (EditorUi.compactUi) + { + viewMenu.getElementsByTagName('img')[0].style.right = '1px'; + viewMenu.getElementsByTagName('img')[0].style.top = '5px'; + } + }); + + this.editorUi.editor.graph.view.addListener(mxEvent.EVENT_SCALE, this.updateZoom); + this.editorUi.editor.addListener('resetGraphView', this.updateZoom); + + var elts = this.addItems(['-', 'undo', 'redo']); + elts[1].setAttribute('title', mxResources.get('undo') + ' (' + this.editorUi.actions.get('undo').shortcut + ')'); + elts[2].setAttribute('title', mxResources.get('redo') + ' (' + this.editorUi.actions.get('redo').shortcut + ')'); + + if (sw >= 320) + { + var elts = this.addItems(['-', 'delete']); + elts[1].setAttribute('title', mxResources.get('delete') + ' (' + this.editorUi.actions.get('delete').shortcut + ')'); + } + + if (sw >= 550) + { + this.addItems(['-', 'toFront', 'toBack']); + } + + if (sw >= 740) + { + this.addItems(['-', 'fillColor']); + + if (sw >= 780) + { + this.addItems(['strokeColor']); + + if (sw >= 820) + { + this.addItems(['shadow']); + } + } + } + + if (sw >= 400) + { + this.addSeparator(); + + if (sw >= 440) + { + this.edgeShapeMenu = this.addMenuFunction('', mxResources.get('connection'), false, mxUtils.bind(this, function(menu) + { + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_SHAPE, 'width'], [null, null], 'geIcon geSprite geSprite-connection', null, true).setAttribute('title', mxResources.get('line')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_SHAPE, 'width'], ['link', null], 'geIcon geSprite geSprite-linkedge', null, true).setAttribute('title', mxResources.get('link')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_SHAPE, 'width'], ['flexArrow', null], 'geIcon geSprite geSprite-arrow', null, true).setAttribute('title', mxResources.get('arrow')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_SHAPE, 'width'], ['arrow', null], 'geIcon geSprite geSprite-simplearrow', null, true).setAttribute('title', mxResources.get('simpleArrow')); + })); + + this.addDropDownArrow(this.edgeShapeMenu, 'geSprite-connection', 44, 50, 0, 0, 22, -4); + } + + this.edgeStyleMenu = this.addMenuFunction('geSprite-orthogonal', mxResources.get('waypoints'), false, mxUtils.bind(this, function(menu) + { + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], [null, null, null], 'geIcon geSprite geSprite-straight', null, true).setAttribute('title', mxResources.get('straight')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['orthogonalEdgeStyle', null, null], 'geIcon geSprite geSprite-orthogonal', null, true).setAttribute('title', mxResources.get('orthogonal')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_ELBOW, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['elbowEdgeStyle', null, null, null], 'geIcon geSprite geSprite-horizontalelbow', null, true).setAttribute('title', mxResources.get('simple')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_ELBOW, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['elbowEdgeStyle', 'vertical', null, null], 'geIcon geSprite geSprite-verticalelbow', null, true).setAttribute('title', mxResources.get('simple')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_ELBOW, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['isometricEdgeStyle', null, null, null], 'geIcon geSprite geSprite-horizontalisometric', null, true).setAttribute('title', mxResources.get('isometric')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_ELBOW, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['isometricEdgeStyle', 'vertical', null, null], 'geIcon geSprite geSprite-verticalisometric', null, true).setAttribute('title', mxResources.get('isometric')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['orthogonalEdgeStyle', '1', null], 'geIcon geSprite geSprite-curved', null, true).setAttribute('title', mxResources.get('curved')); + this.editorUi.menus.edgeStyleChange(menu, '', [mxConstants.STYLE_EDGE, mxConstants.STYLE_CURVED, mxConstants.STYLE_NOEDGESTYLE], ['entityRelationEdgeStyle', null, null], 'geIcon geSprite geSprite-entity', null, true).setAttribute('title', mxResources.get('entityRelation')); + })); + + this.addDropDownArrow(this.edgeStyleMenu, 'geSprite-orthogonal', 44, 50, 0, 0, 22, -4); + } + + this.addSeparator(); + + var insertMenu = this.addMenu('', mxResources.get('insert') + ' (' + mxResources.get('doubleClickTooltip') + ')', true, 'insert', null, true); + this.addDropDownArrow(insertMenu, 'geSprite-plus', 38, 48, -4, -3, 36, -8); +}; + +/** + * Adds the toolbar elements. + */ +Toolbar.prototype.addDropDownArrow = function(menu, sprite, width, atlasWidth, left, top, atlasDelta, atlasLeft) +{ + atlasDelta = (atlasDelta != null) ? atlasDelta : 32; + left = (EditorUi.compactUi) ? left : atlasLeft; + + menu.style.whiteSpace = 'nowrap'; + menu.style.overflow = 'hidden'; + menu.style.position = 'relative'; + menu.innerHTML = '
' + + this.dropdownImageHtml; + menu.style.width = (mxClient.IS_QUIRKS) ? atlasWidth + 'px' : (atlasWidth - atlasDelta) + 'px'; + + if (mxClient.IS_QUIRKS) + { + menu.style.height = (EditorUi.compactUi) ? '24px' : '26px'; + } + + // Fix for item size in kennedy theme + if (EditorUi.compactUi) + { + menu.getElementsByTagName('img')[0].style.left = '24px'; + menu.getElementsByTagName('img')[0].style.top = '5px'; + menu.style.width = (mxClient.IS_QUIRKS) ? width + 'px' : (width - 10) + 'px'; + } +}; + +/** + * Sets the current font name. + */ +Toolbar.prototype.setFontName = function(value) +{ + if (this.fontMenu != null) + { + this.fontMenu.innerHTML = '
' + + mxUtils.htmlEntities(value) + '
' + this.dropdownImageHtml; + } +}; + +/** + * Sets the current font name. + */ +Toolbar.prototype.setFontSize = function(value) +{ + if (this.sizeMenu != null) + { + this.sizeMenu.innerHTML = '
' + + value + '
' + this.dropdownImageHtml; + } +}; + +/** + * Hides the current menu. + */ +Toolbar.prototype.createTextToolbar = function() +{ + var graph = this.editorUi.editor.graph; + + var styleElt = this.addMenu('', mxResources.get('style'), true, 'formatBlock'); + styleElt.style.position = 'relative'; + styleElt.style.whiteSpace = 'nowrap'; + styleElt.style.overflow = 'hidden'; + styleElt.innerHTML = mxResources.get('style') + this.dropdownImageHtml; + + if (EditorUi.compactUi) + { + styleElt.style.paddingRight = '18px'; + styleElt.getElementsByTagName('img')[0].style.right = '1px'; + styleElt.getElementsByTagName('img')[0].style.top = '5px'; + } + + this.addSeparator(); + + this.fontMenu = this.addMenu('', mxResources.get('fontFamily'), true, 'fontFamily'); + this.fontMenu.style.position = 'relative'; + this.fontMenu.style.whiteSpace = 'nowrap'; + this.fontMenu.style.overflow = 'hidden'; + this.fontMenu.style.width = (mxClient.IS_QUIRKS) ? '80px' : '60px'; + + this.setFontName(Menus.prototype.defaultFont); + + if (EditorUi.compactUi) + { + this.fontMenu.style.paddingRight = '18px'; + this.fontMenu.getElementsByTagName('img')[0].style.right = '1px'; + this.fontMenu.getElementsByTagName('img')[0].style.top = '5px'; + } + + this.addSeparator(); + + this.sizeMenu = this.addMenu(Menus.prototype.defaultFontSize, mxResources.get('fontSize'), true, 'fontSize'); + this.sizeMenu.style.position = 'relative'; + this.sizeMenu.style.whiteSpace = 'nowrap'; + this.sizeMenu.style.overflow = 'hidden'; + this.sizeMenu.style.width = (mxClient.IS_QUIRKS) ? '44px' : '24px'; + + this.setFontSize(Menus.prototype.defaultFontSize); + + if (EditorUi.compactUi) + { + this.sizeMenu.style.paddingRight = '18px'; + this.sizeMenu.getElementsByTagName('img')[0].style.right = '1px'; + this.sizeMenu.getElementsByTagName('img')[0].style.top = '5px'; + } + + var elts = this.addItems(['-', 'undo', 'redo','-', 'bold', 'italic', 'underline']); + elts[1].setAttribute('title', mxResources.get('undo') + ' (' + this.editorUi.actions.get('undo').shortcut + ')'); + elts[2].setAttribute('title', mxResources.get('redo') + ' (' + this.editorUi.actions.get('redo').shortcut + ')'); + elts[4].setAttribute('title', mxResources.get('bold') + ' (' + this.editorUi.actions.get('bold').shortcut + ')'); + elts[5].setAttribute('title', mxResources.get('italic') + ' (' + this.editorUi.actions.get('italic').shortcut + ')'); + elts[6].setAttribute('title', mxResources.get('underline') + ' (' + this.editorUi.actions.get('underline').shortcut + ')'); + + // KNOWN: Lost focus after click on submenu with text (not icon) in quirks and IE8. This is because the TD seems + // to catch the focus on click in these browsers. NOTE: Workaround in mxPopupMenu for icon items (without text). + var alignMenu = this.addMenuFunction('', mxResources.get('align'), false, mxUtils.bind(this, function(menu) + { + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + document.execCommand('justifyleft', false, null); + }), null, 'geIcon geSprite geSprite-left'); + elt.setAttribute('title', mxResources.get('left')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + document.execCommand('justifycenter', false, null); + }), null, 'geIcon geSprite geSprite-center'); + elt.setAttribute('title', mxResources.get('center')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + document.execCommand('justifyright', false, null); + }), null, 'geIcon geSprite geSprite-right'); + elt.setAttribute('title', mxResources.get('right')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + document.execCommand('justifyfull', false, null); + }), null, 'geIcon geSprite geSprite-justifyfull'); + elt.setAttribute('title', mxResources.get('justifyfull')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + document.execCommand('insertorderedlist', false, null); + }), null, 'geIcon geSprite geSprite-orderedlist'); + elt.setAttribute('title', mxResources.get('numberedList')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + document.execCommand('insertunorderedlist', false, null); + }), null, 'geIcon geSprite geSprite-unorderedlist'); + elt.setAttribute('title', mxResources.get('bulletedList')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + document.execCommand('outdent', false, null); + }), null, 'geIcon geSprite geSprite-outdent'); + elt.setAttribute('title', mxResources.get('decreaseIndent')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + document.execCommand('indent', false, null); + }), null, 'geIcon geSprite geSprite-indent'); + elt.setAttribute('title', mxResources.get('increaseIndent')); + })); + + alignMenu.style.position = 'relative'; + alignMenu.style.whiteSpace = 'nowrap'; + alignMenu.style.overflow = 'hidden'; + alignMenu.innerHTML = '
' + this.dropdownImageHtml; + alignMenu.style.width = (mxClient.IS_QUIRKS) ? '50px' : '30px'; + + if (EditorUi.compactUi) + { + alignMenu.getElementsByTagName('img')[0].style.left = '22px'; + alignMenu.getElementsByTagName('img')[0].style.top = '5px'; + } + + var formatMenu = this.addMenuFunction('', mxResources.get('format'), false, mxUtils.bind(this, function(menu) + { + elt = menu.addItem('', null, this.editorUi.actions.get('subscript').funct, + null, 'geIcon geSprite geSprite-subscript'); + elt.setAttribute('title', mxResources.get('subscript') + ' (' + Editor.ctrlKey + '+,)'); + + elt = menu.addItem('', null, this.editorUi.actions.get('superscript').funct, + null, 'geIcon geSprite geSprite-superscript'); + elt.setAttribute('title', mxResources.get('superscript') + ' (' + Editor.ctrlKey + '+.)'); + + // KNOWN: IE+FF don't return keyboard focus after color dialog (calling focus doesn't help) + elt = menu.addItem('', null, this.editorUi.actions.get('fontColor').funct, + null, 'geIcon geSprite geSprite-fontcolor'); + elt.setAttribute('title', mxResources.get('fontColor')); + + elt = menu.addItem('', null, this.editorUi.actions.get('backgroundColor').funct, + null, 'geIcon geSprite geSprite-fontbackground'); + elt.setAttribute('title', mxResources.get('backgroundColor')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + document.execCommand('removeformat', false, null); + }), null, 'geIcon geSprite geSprite-removeformat'); + elt.setAttribute('title', mxResources.get('removeFormat')); + })); + + formatMenu.style.position = 'relative'; + formatMenu.style.whiteSpace = 'nowrap'; + formatMenu.style.overflow = 'hidden'; + formatMenu.innerHTML = '
' + + this.dropdownImageHtml; + formatMenu.style.width = (mxClient.IS_QUIRKS) ? '50px' : '30px'; + + if (EditorUi.compactUi) + { + formatMenu.getElementsByTagName('img')[0].style.left = '22px'; + formatMenu.getElementsByTagName('img')[0].style.top = '5px'; + } + + this.addSeparator(); + + this.addButton('geIcon geSprite geSprite-code', mxResources.get('html'), function() + { + graph.cellEditor.toggleViewMode(); + + if (graph.cellEditor.textarea.innerHTML.length > 0 && (graph.cellEditor.textarea.innerHTML != ' ' || !graph.cellEditor.clearOnChange)) + { + window.setTimeout(function() + { + document.execCommand('selectAll', false, null); + }); + } + }); + + this.addSeparator(); + + // FIXME: Uses geButton here and geLabel in main menu + var insertMenu = this.addMenuFunction('', mxResources.get('insert'), true, mxUtils.bind(this, function(menu) + { + menu.addItem(mxResources.get('insertLink'), null, mxUtils.bind(this, function() + { + this.editorUi.actions.get('link').funct(); + })); + + menu.addItem(mxResources.get('insertImage'), null, mxUtils.bind(this, function() + { + this.editorUi.actions.get('image').funct(); + })); + + menu.addItem(mxResources.get('insertHorizontalRule'), null, mxUtils.bind(this, function() + { + document.execCommand('inserthorizontalrule', false, null); + })); + })); + + insertMenu.style.whiteSpace = 'nowrap'; + insertMenu.style.overflow = 'hidden'; + insertMenu.style.position = 'relative'; + insertMenu.innerHTML = '
' + + this.dropdownImageHtml; + insertMenu.style.width = (mxClient.IS_QUIRKS) ? '36px' : '16px'; + + // Fix for item size in kennedy theme + if (EditorUi.compactUi) + { + insertMenu.getElementsByTagName('img')[0].style.left = '24px'; + insertMenu.getElementsByTagName('img')[0].style.top = '5px'; + insertMenu.style.width = (mxClient.IS_QUIRKS) ? '50px' : '30px'; + } + + this.addSeparator(); + + // KNOWN: All table stuff does not work with undo/redo + // KNOWN: Lost focus after click on submenu with text (not icon) in quirks and IE8. This is because the TD seems + // to catch the focus on click in these browsers. NOTE: Workaround in mxPopupMenu for icon items (without text). + var elt = this.addMenuFunction('geIcon geSprite geSprite-table', mxResources.get('table'), false, mxUtils.bind(this, function(menu) + { + var elt = graph.getSelectedElement(); + var cell = graph.getParentByName(elt, 'TD', graph.cellEditor.text2); + var row = graph.getParentByName(elt, 'TR', graph.cellEditor.text2); + + if (row == null) + { + this.editorUi.menus.addInsertTableItem(menu); + } + else + { + var table = graph.getParentByName(row, 'TABLE', graph.cellEditor.text2); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + try + { + graph.selectNode(graph.insertColumn(table, (cell != null) ? cell.cellIndex : 0)); + } + catch (e) + { + mxUtils.alert(mxResources.get('error') + ': ' + e.message); + } + }), null, 'geIcon geSprite geSprite-insertcolumnbefore'); + elt.setAttribute('title', mxResources.get('insertColumnBefore')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + try + { + graph.selectNode(graph.insertColumn(table, (cell != null) ? cell.cellIndex + 1 : -1)); + } + catch (e) + { + mxUtils.alert(mxResources.get('error') + ': ' + e.message); + } + }), null, 'geIcon geSprite geSprite-insertcolumnafter'); + elt.setAttribute('title', mxResources.get('insertColumnAfter')); + + elt = menu.addItem('Delete column', null, mxUtils.bind(this, function() + { + if (cell != null) + { + try + { + graph.deleteColumn(table, cell.cellIndex); + } + catch (e) + { + mxUtils.alert(mxResources.get('error') + ': ' + e.message); + } + } + }), null, 'geIcon geSprite geSprite-deletecolumn'); + elt.setAttribute('title', mxResources.get('deleteColumn')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + try + { + graph.selectNode(graph.insertRow(table, row.sectionRowIndex)); + } + catch (e) + { + mxUtils.alert(mxResources.get('error') + ': ' + e.message); + } + }), null, 'geIcon geSprite geSprite-insertrowbefore'); + elt.setAttribute('title', mxResources.get('insertRowBefore')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + try + { + graph.selectNode(graph.insertRow(table, row.sectionRowIndex + 1)); + } + catch (e) + { + mxUtils.alert(mxResources.get('error') + ': ' + e.message); + } + }), null, 'geIcon geSprite geSprite-insertrowafter'); + elt.setAttribute('title', mxResources.get('insertRowAfter')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + try + { + graph.deleteRow(table, row.sectionRowIndex); + } + catch (e) + { + mxUtils.alert(mxResources.get('error') + ': ' + e.message); + } + }), null, 'geIcon geSprite geSprite-deleterow'); + elt.setAttribute('title', mxResources.get('deleteRow')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + // Converts rgb(r,g,b) values + var color = table.style.borderColor.replace( + /\brgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/g, + function($0, $1, $2, $3) { + return "#" + ("0"+Number($1).toString(16)).substr(-2) + ("0"+Number($2).toString(16)).substr(-2) + ("0"+Number($3).toString(16)).substr(-2); + }); + this.editorUi.pickColor(color, function(newColor) + { + if (newColor == null || newColor == mxConstants.NONE) + { + table.removeAttribute('border'); + table.style.border = ''; + table.style.borderCollapse = ''; + } + else + { + table.setAttribute('border', '1'); + table.style.border = '1px solid ' + newColor; + table.style.borderCollapse = 'collapse'; + } + }); + }), null, 'geIcon geSprite geSprite-strokecolor'); + elt.setAttribute('title', mxResources.get('borderColor')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + // Converts rgb(r,g,b) values + var color = table.style.backgroundColor.replace( + /\brgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/g, + function($0, $1, $2, $3) { + return "#" + ("0"+Number($1).toString(16)).substr(-2) + ("0"+Number($2).toString(16)).substr(-2) + ("0"+Number($3).toString(16)).substr(-2); + }); + this.editorUi.pickColor(color, function(newColor) + { + if (newColor == null || newColor == mxConstants.NONE) + { + table.style.backgroundColor = ''; + } + else + { + table.style.backgroundColor = newColor; + } + }); + }), null, 'geIcon geSprite geSprite-fillcolor'); + elt.setAttribute('title', mxResources.get('backgroundColor')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + var value = table.getAttribute('cellPadding') || 0; + + var dlg = new FilenameDialog(this.editorUi, value, mxResources.get('apply'), mxUtils.bind(this, function(newValue) + { + if (newValue != null && newValue.length > 0) + { + table.setAttribute('cellPadding', newValue); + } + else + { + table.removeAttribute('cellPadding'); + } + }), mxResources.get('spacing')); + this.editorUi.showDialog(dlg.container, 300, 80, true, true); + dlg.init(); + }), null, 'geIcon geSprite geSprite-fit'); + elt.setAttribute('title', mxResources.get('spacing')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + table.setAttribute('align', 'left'); + }), null, 'geIcon geSprite geSprite-left'); + elt.setAttribute('title', mxResources.get('left')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + table.setAttribute('align', 'center'); + }), null, 'geIcon geSprite geSprite-center'); + elt.setAttribute('title', mxResources.get('center')); + + elt = menu.addItem('', null, mxUtils.bind(this, function() + { + table.setAttribute('align', 'right'); + }), null, 'geIcon geSprite geSprite-right'); + elt.setAttribute('title', mxResources.get('right')); + } + })); + + elt.style.position = 'relative'; + elt.style.whiteSpace = 'nowrap'; + elt.style.overflow = 'hidden'; + elt.innerHTML = '
' + this.dropdownImageHtml; + elt.style.width = (mxClient.IS_QUIRKS) ? '50px' : '30px'; + + // Fix for item size in kennedy theme + if (EditorUi.compactUi) + { + elt.getElementsByTagName('img')[0].style.left = '22px'; + elt.getElementsByTagName('img')[0].style.top = '5px'; + } +}; + +/** + * Hides the current menu. + */ +Toolbar.prototype.hideMenu = function() +{ + this.editorUi.hideCurrentMenu(); +}; + +/** + * Adds a label to the toolbar. + */ +Toolbar.prototype.addMenu = function(label, tooltip, showLabels, name, c, showAll, ignoreState) +{ + var menu = this.editorUi.menus.get(name); + var elt = this.addMenuFunction(label, tooltip, showLabels, function() + { + menu.funct.apply(menu, arguments); + }, c, showAll); + + if (!ignoreState) + { + menu.addListener('stateChanged', function() + { + elt.setEnabled(menu.enabled); + }); + } + + return elt; +}; + +/** + * Adds a label to the toolbar. + */ +Toolbar.prototype.addMenuFunction = function(label, tooltip, showLabels, funct, c, showAll) +{ + return this.addMenuFunctionInContainer((c != null) ? c : this.container, label, tooltip, showLabels, funct, showAll); +}; + +/** + * Adds a label to the toolbar. + */ +Toolbar.prototype.addMenuFunctionInContainer = function(container, label, tooltip, showLabels, funct, showAll) +{ + var elt = (showLabels) ? this.createLabel(label) : this.createButton(label); + this.initElement(elt, tooltip); + this.addMenuHandler(elt, showLabels, funct, showAll); + container.appendChild(elt); + + return elt; +}; + +/** + * Adds a separator to the separator. + */ +Toolbar.prototype.addSeparator = function(c) +{ + c = (c != null) ? c : this.container; + var elt = document.createElement('div'); + elt.className = 'geSeparator'; + c.appendChild(elt); + + return elt; +}; + +/** + * Adds given action item + */ +Toolbar.prototype.addItems = function(keys, c, ignoreDisabled) +{ + var items = []; + + for (var i = 0; i < keys.length; i++) + { + var key = keys[i]; + + if (key == '-') + { + items.push(this.addSeparator(c)); + } + else + { + items.push(this.addItem('geSprite-' + key.toLowerCase(), key, c, ignoreDisabled)); + } + } + + return items; +}; + +/** + * Adds given action item + */ +Toolbar.prototype.addItem = function(sprite, key, c, ignoreDisabled) +{ + var action = this.editorUi.actions.get(key); + var elt = null; + + if (action != null) + { + var tooltip = action.label; + + if (action.shortcut != null) + { + tooltip += ' (' + action.shortcut + ')'; + } + + elt = this.addButton(sprite, tooltip, action.funct, c); + + if (!ignoreDisabled) + { + elt.setEnabled(action.enabled); + + action.addListener('stateChanged', function() + { + elt.setEnabled(action.enabled); + }); + } + } + + return elt; +}; + +/** + * Adds a button to the toolbar. + */ +Toolbar.prototype.addButton = function(classname, tooltip, funct, c) +{ + var elt = this.createButton(classname); + c = (c != null) ? c : this.container; + + this.initElement(elt, tooltip); + this.addClickHandler(elt, funct); + c.appendChild(elt); + + return elt; +}; + +/** + * Initializes the given toolbar element. + */ +Toolbar.prototype.initElement = function(elt, tooltip) +{ + // Adds tooltip + if (tooltip != null) + { + elt.setAttribute('title', tooltip); + } + + this.addEnabledState(elt); +}; + +/** + * Adds enabled state with setter to DOM node (avoids JS wrapper). + */ +Toolbar.prototype.addEnabledState = function(elt) +{ + var classname = elt.className; + + elt.setEnabled = function(value) + { + elt.enabled = value; + + if (value) + { + elt.className = classname; + } + else + { + elt.className = classname + ' mxDisabled'; + } + }; + + elt.setEnabled(true); +}; + +/** + * Adds enabled state with setter to DOM node (avoids JS wrapper). + */ +Toolbar.prototype.addClickHandler = function(elt, funct) +{ + if (funct != null) + { + mxEvent.addListener(elt, 'click', function(evt) + { + if (elt.enabled) + { + funct(evt); + } + + mxEvent.consume(evt); + }); + + if (document.documentMode != null && document.documentMode >= 9) + { + // Prevents focus + mxEvent.addListener(elt, 'mousedown', function(evt) + { + evt.preventDefault(); + }); + } + } +}; + +/** + * Creates and returns a new button. + */ +Toolbar.prototype.createButton = function(classname) +{ + var elt = document.createElement('a'); + elt.setAttribute('href', 'javascript:void(0);'); + elt.className = 'geButton'; + + var inner = document.createElement('div'); + + if (classname != null) + { + inner.className = 'geSprite ' + classname; + } + + elt.appendChild(inner); + + return elt; +}; + +/** + * Creates and returns a new button. + */ +Toolbar.prototype.createLabel = function(label, tooltip) +{ + var elt = document.createElement('a'); + elt.setAttribute('href', 'javascript:void(0);'); + elt.className = 'geLabel'; + mxUtils.write(elt, label); + + return elt; +}; + +/** + * Adds a handler for showing a menu in the given element. + */ +Toolbar.prototype.addMenuHandler = function(elt, showLabels, funct, showAll) +{ + if (funct != null) + { + var graph = this.editorUi.editor.graph; + var menu = null; + var show = true; + + mxEvent.addListener(elt, 'click', mxUtils.bind(this, function(evt) + { + if (show && (elt.enabled == null || elt.enabled)) + { + graph.popupMenuHandler.hideMenu(); + menu = new mxPopupMenu(funct); + menu.div.className += ' geToolbarMenu'; + menu.showDisabled = showAll; + menu.labels = showLabels; + menu.autoExpand = true; + + var offset = mxUtils.getOffset(elt); + menu.popup(offset.x, offset.y + elt.offsetHeight, null, evt); + this.editorUi.setCurrentMenu(menu, elt); + + // Workaround for scrollbar hiding menu items + if (!showLabels && menu.div.scrollHeight > menu.div.clientHeight) + { + menu.div.style.width = '40px'; + } + + menu.hideMenu = mxUtils.bind(this, function() + { + mxPopupMenu.prototype.hideMenu.apply(menu, arguments); + this.editorUi.resetCurrentMenu(); + menu.destroy(); + }); + + // Extends destroy to reset global state + menu.addListener(mxEvent.EVENT_HIDE, mxUtils.bind(this, function() + { + this.currentElt = null; + })); + } + + show = true; + mxEvent.consume(evt); + })); + + // Hides menu if already showing + mxEvent.addListener(elt, 'mousedown', mxUtils.bind(this, function(evt) + { + show = this.currentElt != elt; + + // Prevents focus + if (document.documentMode != null && document.documentMode >= 9) + { + evt.preventDefault(); + } + })); + } +}; + +/** + * Adds a handler for showing a menu in the given element. + */ +Toolbar.prototype.destroy = function() +{ + if (this.gestureHandler != null) + { + mxEvent.removeGestureListeners(document, this.gestureHandler); + this.gestureHandler = null; + } +}; diff --git a/media/grapheditor/jscolor/arrow.gif b/media/grapheditor/jscolor/arrow.gif new file mode 100644 index 0000000000..246478a864 Binary files /dev/null and b/media/grapheditor/jscolor/arrow.gif differ diff --git a/media/grapheditor/jscolor/cross.gif b/media/grapheditor/jscolor/cross.gif new file mode 100644 index 0000000000..0ee9c7ac51 Binary files /dev/null and b/media/grapheditor/jscolor/cross.gif differ diff --git a/media/grapheditor/jscolor/hs.png b/media/grapheditor/jscolor/hs.png new file mode 100644 index 0000000000..3d94486ced Binary files /dev/null and b/media/grapheditor/jscolor/hs.png differ diff --git a/media/grapheditor/jscolor/hv.png b/media/grapheditor/jscolor/hv.png new file mode 100644 index 0000000000..1c5e01f8bc Binary files /dev/null and b/media/grapheditor/jscolor/hv.png differ diff --git a/media/grapheditor/jscolor/jscolor.js b/media/grapheditor/jscolor/jscolor.js new file mode 100644 index 0000000000..b8093d88c7 --- /dev/null +++ b/media/grapheditor/jscolor/jscolor.js @@ -0,0 +1,913 @@ +/** + * jscolor, JavaScript Color Picker + * + * @version 1.3.13 + * @license GNU Lesser General Public License, http://www.gnu.org/copyleft/lesser.html + * @author Jan Odvarko, http://odvarko.cz + * @created 2008-06-15 + * @updated 2012-01-19 + * @link http://jscolor.com + */ + + +var jscolor = { + + + dir : '', // location of jscolor directory (leave empty to autodetect) + bindClass : 'color', // class name + binding : true, // automatic binding via + preloading : true, // use image preloading? + + + install : function() { + //jscolor.addEvent(window, 'load', jscolor.init); + }, + + + init : function() { + if(jscolor.preloading) { + jscolor.preload(); + } + }, + + + getDir : function() { + if(!jscolor.dir) { + var detected = jscolor.detectDir(); + jscolor.dir = detected!==false ? detected : 'jscolor/'; + } + return jscolor.dir; + }, + + + detectDir : function() { + var base = location.href; + + var e = document.getElementsByTagName('base'); + for(var i=0; i vs[a] ? + (-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) : + tp[a], + -vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ? + (-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) : + (tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c) + ]; + } + drawPicker(0, 0); + } + }; + + + this.importColor = function() { + if(!valueElement) { + this.exportColor(); + } else { + if(!this.adjust) { + if(!this.fromString(valueElement.value, leaveValue)) { + styleElement.style.backgroundImage = styleElement.jscStyle.backgroundImage; + styleElement.style.backgroundColor = styleElement.jscStyle.backgroundColor; + styleElement.style.color = styleElement.jscStyle.color; + this.exportColor(leaveValue | leaveStyle); + } + } else if(!this.required && /^\s*$/.test(valueElement.value)) { + valueElement.value = ''; + styleElement.style.backgroundImage = styleElement.jscStyle.backgroundImage; + styleElement.style.backgroundColor = styleElement.jscStyle.backgroundColor; + styleElement.style.color = styleElement.jscStyle.color; + this.exportColor(leaveValue | leaveStyle); + + } else if(this.fromString(valueElement.value)) { + // OK + } else { + this.exportColor(); + } + } + }; + + + this.exportColor = function(flags) { + if(!(flags & leaveValue) && valueElement) { + var value = this.toString(); + if(this.caps) { value = value.toUpperCase(); } + if(this.hash) { value = '#'+value; } + valueElement.value = value; + } + if(!(flags & leaveStyle) && styleElement) { + styleElement.style.backgroundImage = "none"; + styleElement.style.backgroundColor = + '#'+this.toString(); + styleElement.style.color = + 0.213 * this.rgb[0] + + 0.715 * this.rgb[1] + + 0.072 * this.rgb[2] + < 0.5 ? '#FFF' : '#000'; + } + if(!(flags & leavePad) && isPickerOwner()) { + redrawPad(); + } + if(!(flags & leaveSld) && isPickerOwner()) { + redrawSld(); + } + }; + + + this.fromHSV = function(h, s, v, flags) { // null = don't change + h<0 && (h=0) || h>6 && (h=6); + s<0 && (s=0) || s>1 && (s=1); + v<0 && (v=0) || v>1 && (v=1); + this.rgb = HSV_RGB( + h===null ? this.hsv[0] : (this.hsv[0]=h), + s===null ? this.hsv[1] : (this.hsv[1]=s), + v===null ? this.hsv[2] : (this.hsv[2]=v) + ); + this.exportColor(flags); + }; + + + this.fromRGB = function(r, g, b, flags) { // null = don't change + r<0 && (r=0) || r>1 && (r=1); + g<0 && (g=0) || g>1 && (g=1); + b<0 && (b=0) || b>1 && (b=1); + var hsv = RGB_HSV( + r===null ? this.rgb[0] : (this.rgb[0]=r), + g===null ? this.rgb[1] : (this.rgb[1]=g), + b===null ? this.rgb[2] : (this.rgb[2]=b) + ); + if(hsv[0] !== null) { + this.hsv[0] = hsv[0]; + } + if(hsv[2] !== 0) { + this.hsv[1] = hsv[1]; + } + this.hsv[2] = hsv[2]; + this.exportColor(flags); + }; + + + this.fromString = function(hex, flags) { + var m = hex.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i); + if(!m) { + return false; + } else { + if(m[1].length === 6) { // 6-char notation + this.fromRGB( + parseInt(m[1].substr(0,2),16) / 255, + parseInt(m[1].substr(2,2),16) / 255, + parseInt(m[1].substr(4,2),16) / 255, + flags + ); + } else { // 3-char notation + this.fromRGB( + parseInt(m[1].charAt(0)+m[1].charAt(0),16) / 255, + parseInt(m[1].charAt(1)+m[1].charAt(1),16) / 255, + parseInt(m[1].charAt(2)+m[1].charAt(2),16) / 255, + flags + ); + } + return true; + } + }; + + + this.toString = function() { + return ( + (0x100 | Math.round(255*this.rgb[0])).toString(16).substr(1) + + (0x100 | Math.round(255*this.rgb[1])).toString(16).substr(1) + + (0x100 | Math.round(255*this.rgb[2])).toString(16).substr(1) + ); + }; + + + function RGB_HSV(r, g, b) { + var n = Math.min(Math.min(r,g),b); + var v = Math.max(Math.max(r,g),b); + var m = v - n; + if(m === 0) { return [ null, 0, v ]; } + var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m); + return [ h===6?0:h, m/v, v ]; + } + + + function HSV_RGB(h, s, v) { + if(h === null) { return [ v, v, v ]; } + var i = Math.floor(h); + var f = i%2 ? h-i : 1-(h-i); + var m = v * (1 - s); + var n = v * (1 - s*f); + switch(i) { + case 6: + case 0: return [v,n,m]; + case 1: return [n,v,m]; + case 2: return [m,v,n]; + case 3: return [m,n,v]; + case 4: return [n,m,v]; + case 5: return [v,m,n]; + } + } + + + function removePicker() { + delete jscolor.picker.owner; + document.getElementsByTagName('body')[0].removeChild(jscolor.picker.boxB); + } + + + function drawPicker(x, y) { + if(!jscolor.picker) { + jscolor.picker = { + box : document.createElement('div'), + boxB : document.createElement('div'), + pad : document.createElement('div'), + padB : document.createElement('div'), + padM : document.createElement('div'), + sld : document.createElement('div'), + sldB : document.createElement('div'), + sldM : document.createElement('div'), + btn : document.createElement('div'), + btnS : document.createElement('span'), + btnT : document.createTextNode(THIS.pickerCloseText) + }; + for(var i=0,segSize=4; i + * to detect IE 11. + */ + IS_IE: navigator.userAgent.indexOf('MSIE') >= 0, + + /** + * Variable: IS_IE6 + * + * True if the current browser is Internet Explorer 6.x. + */ + IS_IE6: navigator.userAgent.indexOf('MSIE 6') >= 0, + + /** + * Variable: IS_IE11 + * + * True if the current browser is Internet Explorer 11.x. + */ + IS_IE11: !!navigator.userAgent.match(/Trident\/7\./), + + /** + * Variable: IS_EDGE + * + * True if the current browser is Microsoft Edge. + */ + IS_EDGE: !!navigator.userAgent.match(/Edge\//), + + /** + * Variable: IS_QUIRKS + * + * True if the current browser is Internet Explorer and it is in quirks mode. + */ + IS_QUIRKS: navigator.userAgent.indexOf('MSIE') >= 0 && (document.documentMode == null || document.documentMode == 5), + + /** + * Variable: IS_EM + * + * True if the browser is IE11 in enterprise mode (IE8 standards mode). + */ + IS_EM: 'spellcheck' in document.createElement('textarea') && document.documentMode == 8, + + /** + * Variable: VML_PREFIX + * + * Prefix for VML namespace in node names. Default is 'v'. + */ + VML_PREFIX: 'v', + + /** + * Variable: OFFICE_PREFIX + * + * Prefix for VML office namespace in node names. Default is 'o'. + */ + OFFICE_PREFIX: 'o', + + /** + * Variable: IS_NS + * + * True if the current browser is Netscape (including Firefox). + */ + IS_NS: navigator.userAgent.indexOf('Mozilla/') >= 0 && + navigator.userAgent.indexOf('MSIE') < 0 && + navigator.userAgent.indexOf('Edge/') < 0, + + /** + * Variable: IS_OP + * + * True if the current browser is Opera. + */ + IS_OP: navigator.userAgent.indexOf('Opera/') >= 0 || + navigator.userAgent.indexOf('OPR/') >= 0, + + /** + * Variable: IS_OT + * + * True if -o-transform is available as a CSS style, ie for Opera browsers + * based on a Presto engine with version 2.5 or later. + */ + IS_OT: navigator.userAgent.indexOf('Presto/') >= 0 && + navigator.userAgent.indexOf('Presto/2.4.') < 0 && + navigator.userAgent.indexOf('Presto/2.3.') < 0 && + navigator.userAgent.indexOf('Presto/2.2.') < 0 && + navigator.userAgent.indexOf('Presto/2.1.') < 0 && + navigator.userAgent.indexOf('Presto/2.0.') < 0 && + navigator.userAgent.indexOf('Presto/1.') < 0, + + /** + * Variable: IS_SF + * + * True if the current browser is Safari. + */ + IS_SF: navigator.userAgent.indexOf('AppleWebKit/') >= 0 && + navigator.userAgent.indexOf('Chrome/') < 0 && + navigator.userAgent.indexOf('Edge/') < 0, + + /** + * Variable: IS_IOS + * + * Returns true if the user agent is an iPad, iPhone or iPod. + */ + IS_IOS: (navigator.userAgent.match(/(iPad|iPhone|iPod)/g) ? true : false), + + /** + * Variable: IS_GC + * + * True if the current browser is Google Chrome. + */ + IS_GC: navigator.userAgent.indexOf('Chrome/') >= 0 && + navigator.userAgent.indexOf('Edge/') < 0, + + /** + * Variable: IS_CHROMEAPP + * + * True if the this is running inside a Chrome App. + */ + IS_CHROMEAPP: window.chrome != null && chrome.app != null && chrome.app.runtime != null, + + /** + * Variable: IS_FF + * + * True if the current browser is Firefox. + */ + IS_FF: navigator.userAgent.indexOf('Firefox/') >= 0, + + /** + * Variable: IS_MT + * + * True if -moz-transform is available as a CSS style. This is the case + * for all Firefox-based browsers newer than or equal 3, such as Camino, + * Iceweasel, Seamonkey and Iceape. + */ + IS_MT: (navigator.userAgent.indexOf('Firefox/') >= 0 && + navigator.userAgent.indexOf('Firefox/1.') < 0 && + navigator.userAgent.indexOf('Firefox/2.') < 0) || + (navigator.userAgent.indexOf('Iceweasel/') >= 0 && + navigator.userAgent.indexOf('Iceweasel/1.') < 0 && + navigator.userAgent.indexOf('Iceweasel/2.') < 0) || + (navigator.userAgent.indexOf('SeaMonkey/') >= 0 && + navigator.userAgent.indexOf('SeaMonkey/1.') < 0) || + (navigator.userAgent.indexOf('Iceape/') >= 0 && + navigator.userAgent.indexOf('Iceape/1.') < 0), + + /** + * Variable: IS_SVG + * + * True if the browser supports SVG. + */ + IS_SVG: navigator.userAgent.indexOf('Firefox/') >= 0 || // FF and Camino + navigator.userAgent.indexOf('Iceweasel/') >= 0 || // Firefox on Debian + navigator.userAgent.indexOf('Seamonkey/') >= 0 || // Firefox-based + navigator.userAgent.indexOf('Iceape/') >= 0 || // Seamonkey on Debian + navigator.userAgent.indexOf('Galeon/') >= 0 || // Gnome Browser (old) + navigator.userAgent.indexOf('Epiphany/') >= 0 || // Gnome Browser (new) + navigator.userAgent.indexOf('AppleWebKit/') >= 0 || // Safari/Google Chrome + navigator.userAgent.indexOf('Gecko/') >= 0 || // Netscape/Gecko + navigator.userAgent.indexOf('Opera/') >= 0 || // Opera + (document.documentMode != null && document.documentMode >= 9), // IE9+ + + /** + * Variable: NO_FO + * + * True if foreignObject support is not available. This is the case for + * Opera, older SVG-based browsers and all versions of IE. + */ + NO_FO: !document.createElementNS || document.createElementNS('http://www.w3.org/2000/svg', + 'foreignObject') != '[object SVGForeignObjectElement]' || navigator.userAgent.indexOf('Opera/') >= 0, + + /** + * Variable: IS_VML + * + * True if the browser supports VML. + */ + IS_VML: navigator.appName.toUpperCase() == 'MICROSOFT INTERNET EXPLORER', + + /** + * Variable: IS_WIN + * + * True if the client is a Windows. + */ + IS_WIN: navigator.appVersion.indexOf('Win') > 0, + + /** + * Variable: IS_MAC + * + * True if the client is a Mac. + */ + IS_MAC: navigator.appVersion.indexOf('Mac') > 0, + + /** + * Variable: IS_TOUCH + * + * True if this device supports touchstart/-move/-end events (Apple iOS, + * Android, Chromebook and Chrome Browser on touch-enabled devices). + */ + IS_TOUCH: 'ontouchstart' in document.documentElement, + + /** + * Variable: IS_POINTER + * + * True if this device supports Microsoft pointer events (always false on Macs). + */ + IS_POINTER: window.PointerEvent != null && !(navigator.appVersion.indexOf('Mac') > 0), + + /** + * Variable: IS_LOCAL + * + * True if the documents location does not start with http:// or https://. + */ + IS_LOCAL: document.location.href.indexOf('http://') < 0 && + document.location.href.indexOf('https://') < 0, + + /** + * Variable: defaultBundles + * + * Contains the base names of the default bundles if mxLoadResources is false. + */ + defaultBundles: [], + + /** + * Function: isBrowserSupported + * + * Returns true if the current browser is supported, that is, if + * or is true. + * + * Example: + * + * (code) + * if (!mxClient.isBrowserSupported()) + * { + * mxUtils.error('Browser is not supported!', 200, false); + * } + * (end) + */ + isBrowserSupported: function() + { + return mxClient.IS_VML || mxClient.IS_SVG; + }, + + /** + * Function: link + * + * Adds a link node to the head of the document. Use this + * to add a stylesheet to the page as follows: + * + * (code) + * mxClient.link('stylesheet', filename); + * (end) + * + * where filename is the (relative) URL of the stylesheet. The charset + * is hardcoded to ISO-8859-1 and the type is text/css. + * + * Parameters: + * + * rel - String that represents the rel attribute of the link node. + * href - String that represents the href attribute of the link node. + * doc - Optional parent document of the link node. + */ + link: function(rel, href, doc) + { + doc = doc || document; + + // Workaround for Operation Aborted in IE6 if base tag is used in head + if (mxClient.IS_IE6) + { + doc.write(''); + } + else + { + var link = doc.createElement('link'); + + link.setAttribute('rel', rel); + link.setAttribute('href', href); + link.setAttribute('charset', 'UTF-8'); + link.setAttribute('type', 'text/css'); + + var head = doc.getElementsByTagName('head')[0]; + head.appendChild(link); + } + }, + + /** + * Function: loadResources + * + * Helper method to load the default bundles if mxLoadResources is false. + * + * Parameters: + * + * fn - Function to call after all resources have been loaded. + * lan - Optional string to pass to . + */ + loadResources: function(fn, lan) + { + var pending = mxClient.defaultBundles.length; + + function callback() + { + if (--pending == 0) + { + fn(); + } + } + + for (var i = 0; i < mxClient.defaultBundles.length; i++) + { + mxResources.add(mxClient.defaultBundles[i], lan, callback); + } + }, + + /** + * Function: include + * + * Dynamically adds a script node to the document header. + * + * In production environments, the includes are resolved in the mxClient.js + * file to reduce the number of requests required for client startup. This + * function should only be used in development environments, but not in + * production systems. + */ + include: function(src) + { + document.write(''); + } +}; + +/** + * Variable: mxLoadResources + * + * Optional global config variable to toggle loading of the two resource files + * in and . Default is true. NOTE: This is a global variable, + * not a variable of mxClient. If this is false, you can use + * with its callback to load the default bundles asynchronously. + * + * (code) + * + * + * (end) + */ +if (typeof(mxLoadResources) == 'undefined') +{ + mxLoadResources = true; +} + +/** + * Variable: mxForceIncludes + * + * Optional global config variable to force loading the JavaScript files in + * development mode. Default is undefined. NOTE: This is a global variable, + * not a variable of mxClient. + * + * (code) + * + * + * (end) + */ +if (typeof(mxForceIncludes) == 'undefined') +{ + mxForceIncludes = false; +} + +/** + * Variable: mxResourceExtension + * + * Optional global config variable to specify the extension of resource files. + * Default is true. NOTE: This is a global variable, not a variable of mxClient. + * + * (code) + * + * + * (end) + */ +if (typeof(mxResourceExtension) == 'undefined') +{ + mxResourceExtension = '.txt'; +} + +/** + * Variable: mxLoadStylesheets + * + * Optional global config variable to toggle loading of the CSS files when + * the library is initialized. Default is true. NOTE: This is a global variable, + * not a variable of mxClient. + * + * (code) + * + * + * (end) + */ +if (typeof(mxLoadStylesheets) == 'undefined') +{ + mxLoadStylesheets = true; +} + +/** + * Variable: basePath + * + * Basepath for all URLs in the core without trailing slash. Default is '.'. + * Set mxBasePath prior to loading the mxClient library as follows to override + * this setting: + * + * (code) + * + * + * (end) + * + * When using a relative path, the path is relative to the URL of the page that + * contains the assignment. Trailing slashes are automatically removed. + */ +if (typeof(mxBasePath) != 'undefined' && mxBasePath.length > 0) +{ + // Adds a trailing slash if required + if (mxBasePath.substring(mxBasePath.length - 1) == '/') + { + mxBasePath = mxBasePath.substring(0, mxBasePath.length - 1); + } + + mxClient.basePath = mxBasePath; +} +else +{ + mxClient.basePath = '.'; +} + +/** + * Variable: imageBasePath + * + * Basepath for all images URLs in the core without trailing slash. Default is + * + '/images'. Set mxImageBasePath prior to loading the + * mxClient library as follows to override this setting: + * + * (code) + * + * + * (end) + * + * When using a relative path, the path is relative to the URL of the page that + * contains the assignment. Trailing slashes are automatically removed. + */ +if (typeof(mxImageBasePath) != 'undefined' && mxImageBasePath.length > 0) +{ + // Adds a trailing slash if required + if (mxImageBasePath.substring(mxImageBasePath.length - 1) == '/') + { + mxImageBasePath = mxImageBasePath.substring(0, mxImageBasePath.length - 1); + } + + mxClient.imageBasePath = mxImageBasePath; +} +else +{ + mxClient.imageBasePath = mxClient.basePath + '/images'; +} + +/** + * Variable: language + * + * Defines the language of the client, eg. en for english, de for german etc. + * The special value 'none' will disable all built-in internationalization and + * resource loading. See for handling identifiers + * with and without a dash. + * + * Set mxLanguage prior to loading the mxClient library as follows to override + * this setting: + * + * (code) + * + * + * (end) + * + * If internationalization is disabled, then the following variables should be + * overridden to reflect the current language of the system. These variables are + * cleared when i18n is disabled. + * , , + * , , + * , , , + * , , + * , , + * , , + * , , + * and + * . + */ +if (typeof(mxLanguage) != 'undefined' && mxLanguage != null) +{ + mxClient.language = mxLanguage; +} +else +{ + mxClient.language = (mxClient.IS_IE) ? navigator.userLanguage : navigator.language; +} + +/** + * Variable: defaultLanguage + * + * Defines the default language which is used in the common resource files. Any + * resources for this language will only load the common resource file, but not + * the language-specific resource file. Default is 'en'. + * + * Set mxDefaultLanguage prior to loading the mxClient library as follows to override + * this setting: + * + * (code) + * + * + * (end) + */ +if (typeof(mxDefaultLanguage) != 'undefined' && mxDefaultLanguage != null) +{ + mxClient.defaultLanguage = mxDefaultLanguage; +} +else +{ + mxClient.defaultLanguage = 'en'; +} + +// Adds all required stylesheets and namespaces +if (mxLoadStylesheets) +{ + mxClient.link('stylesheet', '/media/grapheditor/common.css'); +} + +/** + * Variable: languages + * + * Defines the optional array of all supported language extensions. The default + * language does not have to be part of this list. See + * . + * + * (code) + * + * + * (end) + * + * This is used to avoid unnecessary requests to language files, ie. if a 404 + * will be returned. + */ +if (typeof(mxLanguages) != 'undefined' && mxLanguages != null) +{ + mxClient.languages = mxLanguages; +} + +// Adds required namespaces, stylesheets and memory handling for older IE browsers +if (mxClient.IS_VML) +{ + if (mxClient.IS_SVG) + { + mxClient.IS_VML = false; + } + else + { + // Enables support for IE8 standards mode. Note that this requires all attributes for VML + // elements to be set using direct notation, ie. node.attr = value. The use of setAttribute + // is not possible. + if (document.documentMode == 8) + { + document.namespaces.add(mxClient.VML_PREFIX, 'urn:schemas-microsoft-com:vml', '#default#VML'); + document.namespaces.add(mxClient.OFFICE_PREFIX, 'urn:schemas-microsoft-com:office:office', '#default#VML'); + } + else + { + document.namespaces.add(mxClient.VML_PREFIX, 'urn:schemas-microsoft-com:vml'); + document.namespaces.add(mxClient.OFFICE_PREFIX, 'urn:schemas-microsoft-com:office:office'); + } + + // Workaround for limited number of stylesheets in IE (does not work in standards mode) + if (mxClient.IS_QUIRKS && document.styleSheets.length >= 30) + { + (function() + { + var node = document.createElement('style'); + node.type = 'text/css'; + node.styleSheet.cssText = mxClient.VML_PREFIX + '\\:*{behavior:url(#default#VML)}' + + mxClient.OFFICE_PREFIX + '\\:*{behavior:url(#default#VML)}'; + document.getElementsByTagName('head')[0].appendChild(node); + })(); + } + else + { + document.createStyleSheet().cssText = mxClient.VML_PREFIX + '\\:*{behavior:url(#default#VML)}' + + mxClient.OFFICE_PREFIX + '\\:*{behavior:url(#default#VML)}'; + } + + if (mxLoadStylesheets) + { + mxClient.link('stylesheet', mxClient.basePath + '/css/explorer.css'); + } + } +} + +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +var mxLog = +{ + /** + * Class: mxLog + * + * A singleton class that implements a simple console. + * + * Variable: consoleName + * + * Specifies the name of the console window. Default is 'Console'. + */ + consoleName: 'Console', + + /** + * Variable: TRACE + * + * Specified if the output for and should be visible in the + * console. Default is false. + */ + TRACE: false, + + /** + * Variable: DEBUG + * + * Specifies if the output for should be visible in the console. + * Default is true. + */ + DEBUG: true, + + /** + * Variable: WARN + * + * Specifies if the output for should be visible in the console. + * Default is true. + */ + WARN: true, + + /** + * Variable: buffer + * + * Buffer for pre-initialized content. + */ + buffer: '', + + /** + * Function: init + * + * Initializes the DOM node for the console. This requires document.body to + * point to a non-null value. This is called from within if the + * log has not yet been initialized. + */ + init: function() + { + if (mxLog.window == null && document.body != null) + { + var title = mxLog.consoleName + ' - mxGraph ' + mxClient.VERSION; + + // Creates a table that maintains the layout + var table = document.createElement('table'); + table.setAttribute('width', '100%'); + table.setAttribute('height', '100%'); + + var tbody = document.createElement('tbody'); + var tr = document.createElement('tr'); + var td = document.createElement('td'); + td.style.verticalAlign = 'top'; + + // Adds the actual console as a textarea + mxLog.textarea = document.createElement('textarea'); + mxLog.textarea.setAttribute('wrap', 'off'); + mxLog.textarea.setAttribute('readOnly', 'true'); + mxLog.textarea.style.height = '100%'; + mxLog.textarea.style.resize = 'none'; + mxLog.textarea.value = mxLog.buffer; + + // Workaround for wrong width in standards mode + if (mxClient.IS_NS && document.compatMode != 'BackCompat') + { + mxLog.textarea.style.width = '99%'; + } + else + { + mxLog.textarea.style.width = '100%'; + } + + td.appendChild(mxLog.textarea); + tr.appendChild(td); + tbody.appendChild(tr); + + // Creates the container div + tr = document.createElement('tr'); + mxLog.td = document.createElement('td'); + mxLog.td.style.verticalAlign = 'top'; + mxLog.td.setAttribute('height', '30px'); + + tr.appendChild(mxLog.td); + tbody.appendChild(tr); + table.appendChild(tbody); + + // Adds various debugging buttons + mxLog.addButton('Info', function (evt) + { + mxLog.info(); + }); + + mxLog.addButton('DOM', function (evt) + { + var content = mxUtils.getInnerHtml(document.body); + mxLog.debug(content); + }); + + mxLog.addButton('Trace', function (evt) + { + mxLog.TRACE = !mxLog.TRACE; + + if (mxLog.TRACE) + { + mxLog.debug('Tracing enabled'); + } + else + { + mxLog.debug('Tracing disabled'); + } + }); + + mxLog.addButton('Copy', function (evt) + { + try + { + mxUtils.copy(mxLog.textarea.value); + } + catch (err) + { + mxUtils.alert(err); + } + }); + + mxLog.addButton('Show', function (evt) + { + try + { + mxUtils.popup(mxLog.textarea.value); + } + catch (err) + { + mxUtils.alert(err); + } + }); + + mxLog.addButton('Clear', function (evt) + { + mxLog.textarea.value = ''; + }); + + // Cross-browser code to get window size + var h = 0; + var w = 0; + + if (typeof(window.innerWidth) === 'number') + { + h = window.innerHeight; + w = window.innerWidth; + } + else + { + h = (document.documentElement.clientHeight || document.body.clientHeight); + w = document.body.clientWidth; + } + + mxLog.window = new mxWindow(title, table, Math.max(0, w - 320), Math.max(0, h - 210), 300, 160); + mxLog.window.setMaximizable(true); + mxLog.window.setScrollable(false); + mxLog.window.setResizable(true); + mxLog.window.setClosable(true); + mxLog.window.destroyOnClose = false; + + // Workaround for ignored textarea height in various setups + if (((mxClient.IS_NS || mxClient.IS_IE) && !mxClient.IS_GC && + !mxClient.IS_SF && document.compatMode != 'BackCompat') || + document.documentMode == 11) + { + var elt = mxLog.window.getElement(); + + var resizeHandler = function(sender, evt) + { + mxLog.textarea.style.height = Math.max(0, elt.offsetHeight - 70) + 'px'; + }; + + mxLog.window.addListener(mxEvent.RESIZE_END, resizeHandler); + mxLog.window.addListener(mxEvent.MAXIMIZE, resizeHandler); + mxLog.window.addListener(mxEvent.NORMALIZE, resizeHandler); + + mxLog.textarea.style.height = '92px'; + } + } + }, + + /** + * Function: info + * + * Writes the current navigator information to the console. + */ + info: function() + { + mxLog.writeln(mxUtils.toString(navigator)); + }, + + /** + * Function: addButton + * + * Adds a button to the console using the given label and function. + */ + addButton: function(lab, funct) + { + var button = document.createElement('button'); + mxUtils.write(button, lab); + mxEvent.addListener(button, 'click', funct); + mxLog.td.appendChild(button); + }, + + /** + * Function: isVisible + * + * Returns true if the console is visible. + */ + isVisible: function() + { + if (mxLog.window != null) + { + return mxLog.window.isVisible(); + } + + return false; + }, + + + /** + * Function: show + * + * Shows the console. + */ + show: function() + { + mxLog.setVisible(true); + }, + + /** + * Function: setVisible + * + * Shows or hides the console. + */ + setVisible: function(visible) + { + if (mxLog.window == null) + { + mxLog.init(); + } + + if (mxLog.window != null) + { + mxLog.window.setVisible(visible); + } + }, + + /** + * Function: enter + * + * Writes the specified string to the console + * if is true and returns the current + * time in milliseconds. + * + * Example: + * + * (code) + * mxLog.show(); + * var t0 = mxLog.enter('Hello'); + * // Do something + * mxLog.leave('World!', t0); + * (end) + */ + enter: function(string) + { + if (mxLog.TRACE) + { + mxLog.writeln('Entering '+string); + + return new Date().getTime(); + } + }, + + /** + * Function: leave + * + * Writes the specified string to the console + * if is true and computes the difference + * between the current time and t0 in milliseconds. + * See for an example. + */ + leave: function(string, t0) + { + if (mxLog.TRACE) + { + var dt = (t0 != 0) ? ' ('+(new Date().getTime() - t0)+' ms)' : ''; + mxLog.writeln('Leaving '+string+dt); + } + }, + + /** + * Function: debug + * + * Adds all arguments to the console if is enabled. + * + * Example: + * + * (code) + * mxLog.show(); + * mxLog.debug('Hello, World!'); + * (end) + */ + debug: function() + { + if (mxLog.DEBUG) + { + mxLog.writeln.apply(this, arguments); + } + }, + + /** + * Function: warn + * + * Adds all arguments to the console if is enabled. + * + * Example: + * + * (code) + * mxLog.show(); + * mxLog.warn('Hello, World!'); + * (end) + */ + warn: function() + { + if (mxLog.WARN) + { + mxLog.writeln.apply(this, arguments); + } + }, + + /** + * Function: write + * + * Adds the specified strings to the console. + */ + write: function() + { + var string = ''; + + for (var i = 0; i < arguments.length; i++) + { + string += arguments[i]; + + if (i < arguments.length - 1) + { + string += ' '; + } + } + + if (mxLog.textarea != null) + { + mxLog.textarea.value = mxLog.textarea.value + string; + + // Workaround for no update in Presto 2.5.22 (Opera 10.5) + if (navigator.userAgent.indexOf('Presto/2.5') >= 0) + { + mxLog.textarea.style.visibility = 'hidden'; + mxLog.textarea.style.visibility = 'visible'; + } + + mxLog.textarea.scrollTop = mxLog.textarea.scrollHeight; + } + else + { + mxLog.buffer += string; + } + }, + + /** + * Function: writeln + * + * Adds the specified strings to the console, appending a linefeed at the + * end of each string. + */ + writeln: function() + { + var string = ''; + + for (var i = 0; i < arguments.length; i++) + { + string += arguments[i]; + + if (i < arguments.length - 1) + { + string += ' '; + } + } + + mxLog.write(string + '\n'); + } + +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +var mxObjectIdentity = +{ + /** + * Class: mxObjectIdentity + * + * Identity for JavaScript objects and functions. This is implemented using + * a simple incrementing counter which is stored in each object under + * . + * + * The identity for an object does not change during its lifecycle. + * + * Variable: FIELD_NAME + * + * Name of the field to be used to store the object ID. Default is + * mxObjectId. + */ + FIELD_NAME: 'mxObjectId', + + /** + * Variable: counter + * + * Current counter. + */ + counter: 0, + + /** + * Function: get + * + * Returns the ID for the given object or function or null if no object + * is specified. + */ + get: function(obj) + { + if (obj != null) + { + if (obj[mxObjectIdentity.FIELD_NAME] == null) + { + if (typeof obj === 'object') + { + var ctor = mxUtils.getFunctionName(obj.constructor); + obj[mxObjectIdentity.FIELD_NAME] = ctor + '#' + mxObjectIdentity.counter++; + } + else if (typeof obj === 'function') + { + obj[mxObjectIdentity.FIELD_NAME] = 'Function#' + mxObjectIdentity.counter++; + } + } + + return obj[mxObjectIdentity.FIELD_NAME]; + } + + return null; + }, + + /** + * Function: clear + * + * Deletes the ID from the given object or function. + */ + clear: function(obj) + { + if (typeof(obj) === 'object' || typeof obj === 'function') + { + delete obj[mxObjectIdentity.FIELD_NAME]; + } + } + +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxDictionary + * + * A wrapper class for an associative array with object keys. Note: This + * implementation uses to turn object keys into strings. + * + * Constructor: mxEventSource + * + * Constructs a new dictionary which allows object to be used as keys. + */ +function mxDictionary() +{ + this.clear(); +}; + +/** + * Function: map + * + * Stores the (key, value) pairs in this dictionary. + */ +mxDictionary.prototype.map = null; + +/** + * Function: clear + * + * Clears the dictionary. + */ +mxDictionary.prototype.clear = function() +{ + this.map = {}; +}; + +/** + * Function: get + * + * Returns the value for the given key. + */ +mxDictionary.prototype.get = function(key) +{ + var id = mxObjectIdentity.get(key); + + return this.map[id]; +}; + +/** + * Function: put + * + * Stores the value under the given key and returns the previous + * value for that key. + */ +mxDictionary.prototype.put = function(key, value) +{ + var id = mxObjectIdentity.get(key); + var previous = this.map[id]; + this.map[id] = value; + + return previous; +}; + +/** + * Function: remove + * + * Removes the value for the given key and returns the value that + * has been removed. + */ +mxDictionary.prototype.remove = function(key) +{ + var id = mxObjectIdentity.get(key); + var previous = this.map[id]; + delete this.map[id]; + + return previous; +}; + +/** + * Function: getKeys + * + * Returns all keys as an array. + */ +mxDictionary.prototype.getKeys = function() +{ + var result = []; + + for (var key in this.map) + { + result.push(key); + } + + return result; +}; + +/** + * Function: getValues + * + * Returns all values as an array. + */ +mxDictionary.prototype.getValues = function() +{ + var result = []; + + for (var key in this.map) + { + result.push(this.map[key]); + } + + return result; +}; + +/** + * Function: visit + * + * Visits all entries in the dictionary using the given function with the + * following signature: function(key, value) where key is a string and + * value is an object. + * + * Parameters: + * + * visitor - A function that takes the key and value as arguments. + */ +mxDictionary.prototype.visit = function(visitor) +{ + for (var key in this.map) + { + visitor(key, this.map[key]); + } +}; +/** + * Copyright (c) 2006-2016, JGraph Ltd + * Copyright (c) 2006-2016, Gaudenz Alder + */ +var mxResources = +{ + /** + * Class: mxResources + * + * Implements internationalization. You can provide any number of + * resource files on the server using the following format for the + * filename: name[-en].properties. The en stands for any lowercase + * 2-character language shortcut (eg. de for german, fr for french). + * + * If the optional language extension is omitted, then the file is used as a + * default resource which is loaded in all cases. If a properties file for a + * specific language exists, then it is used to override the settings in the + * default resource. All entries in the file are of the form key=value. The + * values may then be accessed in code via . Lines without + * equal signs in the properties files are ignored. + * + * Resource files may either be added programmatically using + * or via a resource tag in the UI section of the + * editor configuration file, eg: + * + * (code) + * + * + * + * (end) + * + * The above element will load examples/resources/mxWorkflow.properties as well + * as the language specific file for the current language, if it exists. + * + * Values may contain placeholders of the form {1}...{n} where each placeholder + * is replaced with the value of the corresponding array element in the params + * argument passed to . The placeholder {1} maps to the first + * element in the array (at index 0). + * + * See for more information on specifying the default + * language or disabling all loading of resources. + * + * Lines that start with a # sign will be ignored. + * + * Special characters + * + * To use unicode characters, use the standard notation (eg. \u8fd1) or %u as a + * prefix (eg. %u20AC will display a Euro sign). For normal hex encoded strings, + * use % as a prefix, eg. %F6 will display a "o umlaut" (ö). + * + * See to disable this. If you disable this, make sure that + * your files are UTF-8 encoded. + * + * Asynchronous loading + * + * By default, the core adds two resource files synchronously at load time. + * To load these files asynchronously, set to false + * before loading mxClient.js and use instead. + * + * Variable: resources + * + * Object that maps from keys to values. + */ + resources: {}, + + /** + * Variable: extension + * + * Specifies the extension used for language files. Default is . + */ + extension: mxResourceExtension, + + /** + * Variable: resourcesEncoded + * + * Specifies whether or not values in resource files are encoded with \u or + * percentage. Default is false. + */ + resourcesEncoded: false, + + /** + * Variable: loadDefaultBundle + * + * Specifies if the default file for a given basename should be loaded. + * Default is true. + */ + loadDefaultBundle: true, + + /** + * Variable: loadDefaultBundle + * + * Specifies if the specific language file file for a given basename should + * be loaded. Default is true. + */ + loadSpecialBundle: true, + + /** + * Function: isLanguageSupported + * + * Hook for subclassers to disable support for a given language. This + * implementation returns true if lan is in . + * + * Parameters: + * + * lan - The current language. + */ + isLanguageSupported: function(lan) + { + if (mxClient.languages != null) + { + return mxUtils.indexOf(mxClient.languages, lan) >= 0; + } + + return true; + }, + + /** + * Function: getDefaultBundle + * + * Hook for subclassers to return the URL for the special bundle. This + * implementation returns basename + or null if + * is false. + * + * Parameters: + * + * basename - The basename for which the file should be loaded. + * lan - The current language. + */ + getDefaultBundle: function(basename, lan) + { + if (mxResources.loadDefaultBundle || !mxResources.isLanguageSupported(lan)) + { + return basename + mxResources.extension; + } + else + { + return null; + } + }, + + /** + * Function: getSpecialBundle + * + * Hook for subclassers to return the URL for the special bundle. This + * implementation returns basename + '_' + lan + or null if + * is false or lan equals . + * + * If is not null and contains + * a dash, then this method checks if returns true + * for the full language (including the dash). If that returns false the + * first part of the language (up to the dash) will be tried as an extension. + * + * If is null then the first part of the language is + * used to maintain backwards compatibility. + * + * Parameters: + * + * basename - The basename for which the file should be loaded. + * lan - The language for which the file should be loaded. + */ + getSpecialBundle: function(basename, lan) + { + if (mxClient.languages == null || !this.isLanguageSupported(lan)) + { + var dash = lan.indexOf('-'); + + if (dash > 0) + { + lan = lan.substring(0, dash); + } + } + + if (mxResources.loadSpecialBundle && mxResources.isLanguageSupported(lan) && lan != mxClient.defaultLanguage) + { + return basename + '_' + lan + mxResources.extension; + } + else + { + return null; + } + }, + + /** + * Function: add + * + * Adds the default and current language properties file for the specified + * basename. Existing keys are overridden as new files are added. If no + * callback is used then the request is synchronous. + * + * Example: + * + * At application startup, additional resources may be + * added using the following code: + * + * (code) + * mxResources.add('resources/editor'); + * (end) + * + * Parameters: + * + * basename - The basename for which the file should be loaded. + * lan - The language for which the file should be loaded. + * callback - Optional callback for asynchronous loading. + */ + add: function(basename, lan, callback) + { + lan = (lan != null) ? lan : ((mxClient.language != null) ? + mxClient.language.toLowerCase() : mxConstants.NONE); + + if (lan != mxConstants.NONE) + { + var defaultBundle = mxResources.getDefaultBundle(basename, lan); + var specialBundle = mxResources.getSpecialBundle(basename, lan); + + var loadSpecialBundle = function() + { + if (specialBundle != null) + { + if (callback) + { + mxUtils.get(specialBundle, function(req) + { + mxResources.parse(req.getText()); + callback(); + }, function() + { + callback(); + }); + } + else + { + try + { + var req = mxUtils.load(specialBundle); + + if (req.isReady()) + { + mxResources.parse(req.getText()); + } + } + catch (e) + { + // ignore + } + } + } + else if (callback != null) + { + callback(); + } + } + + if (defaultBundle != null) + { + if (callback) + { + mxUtils.get(defaultBundle, function(req) + { + mxResources.parse(req.getText()); + loadSpecialBundle(); + }, function() + { + loadSpecialBundle(); + }); + } + else + { + try + { + var req = mxUtils.load(defaultBundle); + + if (req.isReady()) + { + mxResources.parse(req.getText()); + } + + loadSpecialBundle(); + } + catch (e) + { + // ignore + } + } + } + else + { + // Overlays the language specific file (_lan-extension) + loadSpecialBundle(); + } + } + }, + + /** + * Function: parse + * + * Parses the key, value pairs in the specified + * text and stores them as local resources. + */ + parse: function(text) + { + if (text != null) + { + var lines = text.split('\n'); + + for (var i = 0; i < lines.length; i++) + { + if (lines[i].charAt(0) != '#') + { + var index = lines[i].indexOf('='); + + if (index > 0) + { + var key = lines[i].substring(0, index); + var idx = lines[i].length; + + if (lines[i].charCodeAt(idx - 1) == 13) + { + idx--; + } + + var value = lines[i].substring(index + 1, idx); + + if (this.resourcesEncoded) + { + value = value.replace(/\\(?=u[a-fA-F\d]{4})/g,"%"); + mxResources.resources[key] = unescape(value); + } + else + { + mxResources.resources[key] = value; + } + } + } + } + } + }, + + /** + * Function: get + * + * Returns the value for the specified resource key. + * + * Example: + * To read the value for 'welomeMessage', use the following: + * (code) + * var result = mxResources.get('welcomeMessage') || ''; + * (end) + * + * This would require an entry of the following form in + * one of the English language resource files: + * (code) + * welcomeMessage=Welcome to mxGraph! + * (end) + * + * The part behind the || is the string value to be used if the given + * resource is not available. + * + * Parameters: + * + * key - String that represents the key of the resource to be returned. + * params - Array of the values for the placeholders of the form {1}...{n} + * to be replaced with in the resulting string. + * defaultValue - Optional string that specifies the default return value. + */ + get: function(key, params, defaultValue) + { + var value = mxResources.resources[key]; + + // Applies the default value if no resource was found + if (value == null) + { + value = defaultValue; + } + + // Replaces the placeholders with the values in the array + if (value != null && params != null) + { + value = mxResources.replacePlaceholders(value, params); + } + + return value; + }, + + /** + * Function: replacePlaceholders + * + * Replaces the given placeholders with the given parameters. + * + * Parameters: + * + * value - String that contains the placeholders. + * params - Array of the values for the placeholders of the form {1}...{n} + * to be replaced with in the resulting string. + */ + replacePlaceholders: function(value, params) + { + var result = []; + var index = null; + + for (var i = 0; i < value.length; i++) + { + var c = value.charAt(i); + + if (c == '{') + { + index = ''; + } + else if (index != null && c == '}') + { + index = parseInt(index)-1; + + if (index >= 0 && index < params.length) + { + result.push(params[index]); + } + + index = null; + } + else if (index != null) + { + index += c; + } + else + { + result.push(c); + } + } + + return result.join(''); + }, + + /** + * Function: loadResources + * + * Loads all required resources asynchronously. Use this to load the graph and + * editor resources if is false. + * + * Parameters: + * + * callback - Callback function for asynchronous loading. + */ + loadResources: function(callback) + { + mxResources.add(mxClient.basePath+'/resources/editor', null, function() + { + mxResources.add(mxClient.basePath+'/resources/graph', null, callback); + }); + } + +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxPoint + * + * Implements a 2-dimensional vector with double precision coordinates. + * + * Constructor: mxPoint + * + * Constructs a new point for the optional x and y coordinates. If no + * coordinates are given, then the default values for and are used. + */ +function mxPoint(x, y) +{ + this.x = (x != null) ? x : 0; + this.y = (y != null) ? y : 0; +}; + +/** + * Variable: x + * + * Holds the x-coordinate of the point. Default is 0. + */ +mxPoint.prototype.x = null; + +/** + * Variable: y + * + * Holds the y-coordinate of the point. Default is 0. + */ +mxPoint.prototype.y = null; + +/** + * Function: equals + * + * Returns true if the given object equals this point. + */ +mxPoint.prototype.equals = function(obj) +{ + return obj != null && obj.x == this.x && obj.y == this.y; +}; + +/** + * Function: clone + * + * Returns a clone of this . + */ +mxPoint.prototype.clone = function() +{ + // Handles subclasses as well + return mxUtils.clone(this); +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxRectangle + * + * Extends to implement a 2-dimensional rectangle with double + * precision coordinates. + * + * Constructor: mxRectangle + * + * Constructs a new rectangle for the optional parameters. If no parameters + * are given then the respective default values are used. + */ +function mxRectangle(x, y, width, height) +{ + mxPoint.call(this, x, y); + + this.width = (width != null) ? width : 0; + this.height = (height != null) ? height : 0; +}; + +/** + * Extends mxPoint. + */ +mxRectangle.prototype = new mxPoint(); +mxRectangle.prototype.constructor = mxRectangle; + +/** + * Variable: width + * + * Holds the width of the rectangle. Default is 0. + */ +mxRectangle.prototype.width = null; + +/** + * Variable: height + * + * Holds the height of the rectangle. Default is 0. + */ +mxRectangle.prototype.height = null; + +/** + * Function: setRect + * + * Sets this rectangle to the specified values + */ +mxRectangle.prototype.setRect = function(x, y, w, h) +{ + this.x = x; + this.y = y; + this.width = w; + this.height = h; +}; + +/** + * Function: getCenterX + * + * Returns the x-coordinate of the center point. + */ +mxRectangle.prototype.getCenterX = function () +{ + return this.x + this.width/2; +}; + +/** + * Function: getCenterY + * + * Returns the y-coordinate of the center point. + */ +mxRectangle.prototype.getCenterY = function () +{ + return this.y + this.height/2; +}; + +/** + * Function: add + * + * Adds the given rectangle to this rectangle. + */ +mxRectangle.prototype.add = function(rect) +{ + if (rect != null) + { + var minX = Math.min(this.x, rect.x); + var minY = Math.min(this.y, rect.y); + var maxX = Math.max(this.x + this.width, rect.x + rect.width); + var maxY = Math.max(this.y + this.height, rect.y + rect.height); + + this.x = minX; + this.y = minY; + this.width = maxX - minX; + this.height = maxY - minY; + } +}; + +/** + * Function: intersect + * + * Changes this rectangle to where it overlaps with the given rectangle. + */ +mxRectangle.prototype.intersect = function(rect) +{ + if (rect != null) + { + var r1 = this.x + this.width; + var r2 = rect.x + rect.width; + + var b1 = this.y + this.height; + var b2 = rect.y + rect.height; + + this.x = Math.max(this.x, rect.x); + this.y = Math.max(this.y, rect.y); + this.width = Math.min(r1, r2) - this.x; + this.height = Math.min(b1, b2) - this.y; + } +}; + +/** + * Function: grow + * + * Grows the rectangle by the given amount, that is, this method subtracts + * the given amount from the x- and y-coordinates and adds twice the amount + * to the width and height. + */ +mxRectangle.prototype.grow = function(amount) +{ + this.x -= amount; + this.y -= amount; + this.width += 2 * amount; + this.height += 2 * amount; +}; + +/** + * Function: getPoint + * + * Returns the top, left corner as a new . + */ +mxRectangle.prototype.getPoint = function() +{ + return new mxPoint(this.x, this.y); +}; + +/** + * Function: rotate90 + * + * Rotates this rectangle by 90 degree around its center point. + */ +mxRectangle.prototype.rotate90 = function() +{ + var t = (this.width - this.height) / 2; + this.x += t; + this.y -= t; + var tmp = this.width; + this.width = this.height; + this.height = tmp; +}; + +/** + * Function: equals + * + * Returns true if the given object equals this rectangle. + */ +mxRectangle.prototype.equals = function(obj) +{ + return obj != null && obj.x == this.x && obj.y == this.y && + obj.width == this.width && obj.height == this.height; +}; + +/** + * Function: fromRectangle + * + * Returns a new which is a copy of the given rectangle. + */ +mxRectangle.fromRectangle = function(rect) +{ + return new mxRectangle(rect.x, rect.y, rect.width, rect.height); +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +var mxEffects = +{ + + /** + * Class: mxEffects + * + * Provides animation effects. + */ + + /** + * Function: animateChanges + * + * Asynchronous animated move operation. See also: . + * + * Example: + * + * (code) + * graph.model.addListener(mxEvent.CHANGE, function(sender, evt) + * { + * var changes = evt.getProperty('edit').changes; + * + * if (changes.length < 10) + * { + * mxEffects.animateChanges(graph, changes); + * } + * }); + * (end) + * + * Parameters: + * + * graph - that received the changes. + * changes - Array of changes to be animated. + * done - Optional function argument that is invoked after the + * last step of the animation. + */ + animateChanges: function(graph, changes, done) + { + var maxStep = 10; + var step = 0; + + var animate = function() + { + var isRequired = false; + + for (var i = 0; i < changes.length; i++) + { + var change = changes[i]; + + if (change instanceof mxGeometryChange || + change instanceof mxTerminalChange || + change instanceof mxValueChange || + change instanceof mxChildChange || + change instanceof mxStyleChange) + { + var state = graph.getView().getState(change.cell || change.child, false); + + if (state != null) + { + isRequired = true; + + if (change.constructor != mxGeometryChange || graph.model.isEdge(change.cell)) + { + mxUtils.setOpacity(state.shape.node, 100 * step / maxStep); + } + else + { + var scale = graph.getView().scale; + + var dx = (change.geometry.x - change.previous.x) * scale; + var dy = (change.geometry.y - change.previous.y) * scale; + + var sx = (change.geometry.width - change.previous.width) * scale; + var sy = (change.geometry.height - change.previous.height) * scale; + + if (step == 0) + { + state.x -= dx; + state.y -= dy; + state.width -= sx; + state.height -= sy; + } + else + { + state.x += dx / maxStep; + state.y += dy / maxStep; + state.width += sx / maxStep; + state.height += sy / maxStep; + } + + graph.cellRenderer.redraw(state); + + // Fades all connected edges and children + mxEffects.cascadeOpacity(graph, change.cell, 100 * step / maxStep); + } + } + } + } + + if (step < maxStep && isRequired) + { + step++; + window.setTimeout(animate, delay); + } + else if (done != null) + { + done(); + } + }; + + var delay = 30; + animate(); + }, + + /** + * Function: cascadeOpacity + * + * Sets the opacity on the given cell and its descendants. + * + * Parameters: + * + * graph - that contains the cells. + * cell - to set the opacity for. + * opacity - New value for the opacity in %. + */ + cascadeOpacity: function(graph, cell, opacity) + { + // Fades all children + var childCount = graph.model.getChildCount(cell); + + for (var i=0; i 0) + { + window.setTimeout(f, delay); + } + else + { + node.style.visibility = 'hidden'; + + if (remove && node.parentNode) + { + node.parentNode.removeChild(node); + } + } + }; + window.setTimeout(f, delay); + } + else + { + node.style.visibility = 'hidden'; + + if (remove && node.parentNode) + { + node.parentNode.removeChild(node); + } + } + } + +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +var mxUtils = +{ + /** + * Class: mxUtils + * + * A singleton class that provides cross-browser helper methods. + * This is a global functionality. To access the functions in this + * class, use the global classname appended by the functionname. + * You may have to load chrome://global/content/contentAreaUtils.js + * to disable certain security restrictions in Mozilla for the , + * , and function. + * + * For example, the following code displays an error message: + * + * (code) + * mxUtils.error('Browser is not supported!', 200, false); + * (end) + * + * Variable: errorResource + * + * Specifies the resource key for the title of the error window. If the + * resource for this key does not exist then the value is used as + * the title. Default is 'error'. + */ + errorResource: (mxClient.language != 'none') ? 'error' : '', + + /** + * Variable: closeResource + * + * Specifies the resource key for the label of the close button. If the + * resource for this key does not exist then the value is used as + * the label. Default is 'close'. + */ + closeResource: (mxClient.language != 'none') ? 'close' : '', + + /** + * Variable: errorImage + * + * Defines the image used for error dialogs. + */ + errorImage: mxClient.imageBasePath + '/error.gif', + + /** + * Function: removeCursors + * + * Removes the cursors from the style of the given DOM node and its + * descendants. + * + * Parameters: + * + * element - DOM node to remove the cursor style from. + */ + removeCursors: function(element) + { + if (element.style != null) + { + element.style.cursor = ''; + } + + var children = element.childNodes; + + if (children != null) + { + var childCount = children.length; + + for (var i = 0; i < childCount; i += 1) + { + mxUtils.removeCursors(children[i]); + } + } + }, + + /** + * Function: getCurrentStyle + * + * Returns the current style of the specified element. + * + * Parameters: + * + * element - DOM node whose current style should be returned. + */ + getCurrentStyle: function() + { + if (mxClient.IS_IE && (document.documentMode == null || document.documentMode < 9)) + { + return function(element) + { + return (element != null) ? element.currentStyle : null; + }; + } + else + { + return function(element) + { + return (element != null) ? + window.getComputedStyle(element, '') : + null; + }; + } + }(), + + /** + * Function: parseCssNumber + * + * Parses the given CSS numeric value adding handling for the values thin, + * medium and thick (2, 4 and 6). + */ + parseCssNumber: function(value) + { + if (value == 'thin') + { + value = '2'; + } + else if (value == 'medium') + { + value = '4'; + } + else if (value == 'thick') + { + value = '6'; + } + + value = parseFloat(value); + + if (isNaN(value)) + { + value = 0; + } + + return value; + }, + + /** + * Function: setPrefixedStyle + * + * Adds the given style with the standard name and an optional vendor prefix for the current + * browser. + * + * (code) + * mxUtils.setPrefixedStyle(node.style, 'transformOrigin', '0% 0%'); + * (end) + */ + setPrefixedStyle: function() + { + var prefix = null; + + if (mxClient.IS_OT) + { + prefix = 'O'; + } + else if (mxClient.IS_SF || mxClient.IS_GC) + { + prefix = 'Webkit'; + } + else if (mxClient.IS_MT) + { + prefix = 'Moz'; + } + else if (mxClient.IS_IE && document.documentMode >= 9 && document.documentMode < 10) + { + prefix = 'ms'; + } + + return function(style, name, value) + { + style[name] = value; + + if (prefix != null && name.length > 0) + { + name = prefix + name.substring(0, 1).toUpperCase() + name.substring(1); + style[name] = value; + } + }; + }(), + + /** + * Function: hasScrollbars + * + * Returns true if the overflow CSS property of the given node is either + * scroll or auto. + * + * Parameters: + * + * node - DOM node whose style should be checked for scrollbars. + */ + hasScrollbars: function(node) + { + var style = mxUtils.getCurrentStyle(node); + + return style != null && (style.overflow == 'scroll' || style.overflow == 'auto'); + }, + + /** + * Function: bind + * + * Returns a wrapper function that locks the execution scope of the given + * function to the specified scope. Inside funct, the "this" keyword + * becomes a reference to that scope. + */ + bind: function(scope, funct) + { + return function() + { + return funct.apply(scope, arguments); + }; + }, + + /** + * Function: eval + * + * Evaluates the given expression using eval and returns the JavaScript + * object that represents the expression result. Supports evaluation of + * expressions that define functions and returns the function object for + * these expressions. + * + * Parameters: + * + * expr - A string that represents a JavaScript expression. + */ + eval: function(expr) + { + var result = null; + + if (expr.indexOf('function') >= 0) + { + try + { + eval('var _mxJavaScriptExpression='+expr); + result = _mxJavaScriptExpression; + // TODO: Use delete here? + _mxJavaScriptExpression = null; + } + catch (e) + { + mxLog.warn(e.message + ' while evaluating ' + expr); + } + } + else + { + try + { + result = eval(expr); + } + catch (e) + { + mxLog.warn(e.message + ' while evaluating ' + expr); + } + } + + return result; + }, + + /** + * Function: findNode + * + * Returns the first node where attr equals value. + * This implementation does not use XPath. + */ + findNode: function(node, attr, value) + { + if (node.nodeType == mxConstants.NODETYPE_ELEMENT) + { + var tmp = node.getAttribute(attr); + + if (tmp != null && tmp == value) + { + return node; + } + } + + node = node.firstChild; + + while (node != null) + { + var result = mxUtils.findNode(node, attr, value); + + if (result != null) + { + return result; + } + + node = node.nextSibling; + } + + return null; + }, + + /** + * Function: getFunctionName + * + * Returns the name for the given function. + * + * Parameters: + * + * f - JavaScript object that represents a function. + */ + getFunctionName: function(f) + { + var str = null; + + if (f != null) + { + if (f.name != null) + { + str = f.name; + } + else + { + str = mxUtils.trim(f.toString()); + + if (/^function\s/.test(str)) + { + str = mxUtils.ltrim(str.substring(9)); + var idx2 = str.indexOf('('); + + if (idx2 > 0) + { + str = str.substring(0, idx2); + } + } + } + } + + return str; + }, + + /** + * Function: indexOf + * + * Returns the index of obj in array or -1 if the array does not contain + * the given object. + * + * Parameters: + * + * array - Array to check for the given obj. + * obj - Object to find in the given array. + */ + indexOf: function(array, obj) + { + if (array != null && obj != null) + { + for (var i = 0; i < array.length; i++) + { + if (array[i] == obj) + { + return i; + } + } + } + + return -1; + }, + + /** + * Function: forEach + * + * Calls the given function for each element of the given array and returns + * the array. + * + * Parameters: + * + * array - Array that contains the elements. + * fn - Function to be called for each object. + */ + forEach: function(array, fn) + { + if (array != null && fn != null) + { + for (var i = 0; i < array.length; i++) + { + fn(array[i]); + } + } + + return array; + }, + + /** + * Function: remove + * + * Removes all occurrences of the given object in the given array or + * object. If there are multiple occurrences of the object, be they + * associative or as an array entry, all occurrences are removed from + * the array or deleted from the object. By removing the object from + * the array, all elements following the removed element are shifted + * by one step towards the beginning of the array. + * + * The length of arrays is not modified inside this function. + * + * Parameters: + * + * obj - Object to find in the given array. + * array - Array to check for the given obj. + */ + remove: function(obj, array) + { + var result = null; + + if (typeof(array) == 'object') + { + var index = mxUtils.indexOf(array, obj); + + while (index >= 0) + { + array.splice(index, 1); + result = obj; + index = mxUtils.indexOf(array, obj); + } + } + + for (var key in array) + { + if (array[key] == obj) + { + delete array[key]; + result = obj; + } + } + + return result; + }, + + /** + * Function: isNode + * + * Returns true if the given value is an XML node with the node name + * and if the optional attribute has the specified value. + * + * This implementation assumes that the given value is a DOM node if the + * nodeType property is numeric, that is, if isNaN returns false for + * value.nodeType. + * + * Parameters: + * + * value - Object that should be examined as a node. + * nodeName - String that specifies the node name. + * attributeName - Optional attribute name to check. + * attributeValue - Optional attribute value to check. + */ + isNode: function(value, nodeName, attributeName, attributeValue) + { + if (value != null && !isNaN(value.nodeType) && (nodeName == null || + value.nodeName.toLowerCase() == nodeName.toLowerCase())) + { + return attributeName == null || + value.getAttribute(attributeName) == attributeValue; + } + + return false; + }, + + /** + * Function: isAncestorNode + * + * Returns true if the given ancestor is an ancestor of the + * given DOM node in the DOM. This also returns true if the + * child is the ancestor. + * + * Parameters: + * + * ancestor - DOM node that represents the ancestor. + * child - DOM node that represents the child. + */ + isAncestorNode: function(ancestor, child) + { + var parent = child; + + while (parent != null) + { + if (parent == ancestor) + { + return true; + } + + parent = parent.parentNode; + } + + return false; + }, + + /** + * Function: getChildNodes + * + * Returns an array of child nodes that are of the given node type. + * + * Parameters: + * + * node - Parent DOM node to return the children from. + * nodeType - Optional node type to return. Default is + * . + */ + getChildNodes: function(node, nodeType) + { + nodeType = nodeType || mxConstants.NODETYPE_ELEMENT; + + var children = []; + var tmp = node.firstChild; + + while (tmp != null) + { + if (tmp.nodeType == nodeType) + { + children.push(tmp); + } + + tmp = tmp.nextSibling; + } + + return children; + }, + + /** + * Function: importNode + * + * Cross browser implementation for document.importNode. Uses document.importNode + * in all browsers but IE, where the node is cloned by creating a new node and + * copying all attributes and children into it using importNode, recursively. + * + * Parameters: + * + * doc - Document to import the node into. + * node - Node to be imported. + * allChildren - If all children should be imported. + */ + importNode: function(doc, node, allChildren) + { + if (mxClient.IS_IE && (document.documentMode == null || document.documentMode < 10)) + { + switch (node.nodeType) + { + case 1: /* element */ + { + var newNode = doc.createElement(node.nodeName); + + if (node.attributes && node.attributes.length > 0) + { + for (var i = 0; i < node.attributes.length; i++) + { + newNode.setAttribute(node.attributes[i].nodeName, + node.getAttribute(node.attributes[i].nodeName)); + } + + if (allChildren && node.childNodes && node.childNodes.length > 0) + { + for (var i = 0; i < node.childNodes.length; i++) + { + newNode.appendChild(mxUtils.importNode(doc, node.childNodes[i], allChildren)); + } + } + } + + return newNode; + break; + } + case 3: /* text */ + case 4: /* cdata-section */ + case 8: /* comment */ + { + return doc.createTextNode(node.value); + break; + } + }; + } + else + { + return doc.importNode(node, allChildren); + } + }, + + /** + * Function: createXmlDocument + * + * Returns a new, empty XML document. + */ + createXmlDocument: function() + { + var doc = null; + + if (document.implementation && document.implementation.createDocument) + { + doc = document.implementation.createDocument('', '', null); + } + else if (window.ActiveXObject) + { + doc = new ActiveXObject('Microsoft.XMLDOM'); + } + + return doc; + }, + + /** + * Function: parseXml + * + * Parses the specified XML string into a new XML document and returns the + * new document. + * + * Example: + * + * (code) + * var doc = mxUtils.parseXml( + * ''+ + * ''+ + * ''+ + * ''+ + * ''); + * (end) + * + * Parameters: + * + * xml - String that contains the XML data. + */ + parseXml: function() + { + if (window.DOMParser) + { + return function(xml) + { + var parser = new DOMParser(); + + return parser.parseFromString(xml, 'text/xml'); + }; + } + else // IE<=9 + { + return function(xml) + { + var result = mxUtils.createXmlDocument(); + result.async = false; + // Workaround for parsing errors with SVG DTD + result.validateOnParse = false; + result.resolveExternals = false; + result.loadXML(xml); + + return result; + }; + } + }(), + + /** + * Function: clearSelection + * + * Clears the current selection in the page. + */ + clearSelection: function() + { + if (document.selection) + { + return function() + { + document.selection.empty(); + }; + } + else if (window.getSelection) + { + return function() + { + if (window.getSelection().empty) + { + window.getSelection().empty(); + } + else if (window.getSelection().removeAllRanges) + { + window.getSelection().removeAllRanges(); + } + }; + } + else + { + return function() { }; + } + }(), + + /** + * Function: getPrettyXML + * + * Returns a pretty printed string that represents the XML tree for the + * given node. This method should only be used to print XML for reading, + * use instead to obtain a string for processing. + * + * Parameters: + * + * node - DOM node to return the XML for. + * tab - Optional string that specifies the indentation for one level. + * Default is two spaces. + * indent - Optional string that represents the current indentation. + * Default is an empty string. + */ + getPrettyXml: function(node, tab, indent) + { + var result = []; + + if (node != null) + { + tab = tab || ' '; + indent = indent || ''; + + if (node.nodeType == mxConstants.NODETYPE_TEXT) + { + var value = mxUtils.trim(mxUtils.getTextContent(node)); + + if (value.length > 0) + { + result.push(indent + mxUtils.htmlEntities(value) + '\n'); + } + } + else + { + result.push(indent + '<' + node.nodeName); + + // Creates the string with the node attributes + // and converts all HTML entities in the values + var attrs = node.attributes; + + if (attrs != null) + { + for (var i = 0; i < attrs.length; i++) + { + var val = mxUtils.htmlEntities(attrs[i].value); + result.push(' ' + attrs[i].nodeName + '="' + val + '"'); + } + } + + // Recursively creates the XML string for each + // child nodes and appends it here with an + // indentation + var tmp = node.firstChild; + + if (tmp != null) + { + result.push('>\n'); + + while (tmp != null) + { + result.push(mxUtils.getPrettyXml(tmp, tab, indent + tab)); + tmp = tmp.nextSibling; + } + + result.push(indent + '\n'); + } + else + { + result.push('/>\n'); + } + } + } + + return result.join(''); + }, + + /** + * Function: removeWhitespace + * + * Removes the sibling text nodes for the given node that only consists + * of tabs, newlines and spaces. + * + * Parameters: + * + * node - DOM node whose siblings should be removed. + * before - Optional boolean that specifies the direction of the traversal. + */ + removeWhitespace: function(node, before) + { + var tmp = (before) ? node.previousSibling : node.nextSibling; + + while (tmp != null && tmp.nodeType == mxConstants.NODETYPE_TEXT) + { + var next = (before) ? tmp.previousSibling : tmp.nextSibling; + var text = mxUtils.getTextContent(tmp); + + if (mxUtils.trim(text).length == 0) + { + tmp.parentNode.removeChild(tmp); + } + + tmp = next; + } + }, + + /** + * Function: htmlEntities + * + * Replaces characters (less than, greater than, newlines and quotes) with + * their HTML entities in the given string and returns the result. + * + * Parameters: + * + * s - String that contains the characters to be converted. + * newline - If newlines should be replaced. Default is true. + */ + htmlEntities: function(s, newline) + { + s = String(s || ''); + + s = s.replace(/&/g,'&'); // 38 26 + s = s.replace(/"/g,'"'); // 34 22 + s = s.replace(/\'/g,'''); // 39 27 + s = s.replace(//g,'>'); // 62 3E + + if (newline == null || newline) + { + s = s.replace(/\n/g, ' '); + } + + return s; + }, + + /** + * Function: isVml + * + * Returns true if the given node is in the VML namespace. + * + * Parameters: + * + * node - DOM node whose tag urn should be checked. + */ + isVml: function(node) + { + return node != null && node.tagUrn == 'urn:schemas-microsoft-com:vml'; + }, + + /** + * Function: getXml + * + * Returns the XML content of the specified node. For Internet Explorer, + * all \r\n\t[\t]* are removed from the XML string and the remaining \r\n + * are replaced by \n. All \n are then replaced with linefeed, or if + * no linefeed is defined. + * + * Parameters: + * + * node - DOM node to return the XML for. + * linefeed - Optional string that linefeeds are converted into. Default is + * + */ + getXml: function(node, linefeed) + { + var xml = ''; + + if (window.XMLSerializer != null) + { + var xmlSerializer = new XMLSerializer(); + xml = xmlSerializer.serializeToString(node); + } + else if (node.xml != null) + { + xml = node.xml.replace(/\r\n\t[\t]*/g, ''). + replace(/>\r\n/g, '>'). + replace(/\r\n/g, '\n'); + } + + // Replaces linefeeds with HTML Entities. + linefeed = linefeed || ' '; + xml = xml.replace(/\n/g, linefeed); + + return xml; + }, + + /** + * Function: extractTextWithWhitespace + * + * Returns the text content of the specified node. + * + * Parameters: + * + * elems - DOM nodes to return the text for. + */ + extractTextWithWhitespace: function(elems) + { + // Known block elements for handling linefeeds (list is not complete) + var blocks = ['BLOCKQUOTE', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'OL', 'P', 'PRE', 'TABLE', 'UL']; + var ret = []; + + function doExtract(elts) + { + // Single break should be ignored + if (elts.length == 1 && (elts[0].nodeName == 'BR' || + elts[0].innerHTML == '\n')) + { + return; + } + + for (var i = 0; i < elts.length; i++) + { + var elem = elts[i]; + + // DIV with a br or linefeed forces a linefeed + if (elem.nodeName == 'BR' || elem.innerHTML == '\n' || + ((elts.length == 1 || i == 0) && (elem.nodeName == 'DIV' && + elem.innerHTML.toLowerCase() == '
'))) + { + ret.push('\n'); + } + else + { + if (elem.nodeType === 3 || elem.nodeType === 4) + { + if (elem.nodeValue.length > 0) + { + ret.push(elem.nodeValue); + } + } + else if (elem.nodeType !== 8 && elem.childNodes.length > 0) + { + doExtract(elem.childNodes); + } + + if (i < elts.length - 1 && mxUtils.indexOf(blocks, elts[i + 1].nodeName) >= 0) + { + ret.push('\n'); + } + } + } + }; + + doExtract(elems); + + return ret.join(''); + }, + + /** + * Function: replaceTrailingNewlines + * + * Replaces each trailing newline with the given pattern. + */ + replaceTrailingNewlines: function(str, pattern) + { + // LATER: Check is this can be done with a regular expression + var postfix = ''; + + while (str.length > 0 && str.charAt(str.length - 1) == '\n') + { + str = str.substring(0, str.length - 1); + postfix += pattern; + } + + return str + postfix; + }, + + /** + * Function: getTextContent + * + * Returns the text content of the specified node. + * + * Parameters: + * + * node - DOM node to return the text content for. + */ + getTextContent: function(node) + { + // Only IE10- + if (mxClient.IS_IE && node.innerText !== undefined) + { + return node.innerText; + } + else + { + return (node != null) ? node[(node.textContent === undefined) ? 'text' : 'textContent'] : ''; + } + }, + + /** + * Function: setTextContent + * + * Sets the text content of the specified node. + * + * Parameters: + * + * node - DOM node to set the text content for. + * text - String that represents the text content. + */ + setTextContent: function(node, text) + { + if (node.innerText !== undefined) + { + node.innerText = text; + } + else + { + node[(node.textContent === undefined) ? 'text' : 'textContent'] = text; + } + }, + + /** + * Function: getInnerHtml + * + * Returns the inner HTML for the given node as a string or an empty string + * if no node was specified. The inner HTML is the text representing all + * children of the node, but not the node itself. + * + * Parameters: + * + * node - DOM node to return the inner HTML for. + */ + getInnerHtml: function() + { + if (mxClient.IS_IE) + { + return function(node) + { + if (node != null) + { + return node.innerHTML; + } + + return ''; + }; + } + else + { + return function(node) + { + if (node != null) + { + var serializer = new XMLSerializer(); + return serializer.serializeToString(node); + } + + return ''; + }; + } + }(), + + /** + * Function: getOuterHtml + * + * Returns the outer HTML for the given node as a string or an empty + * string if no node was specified. The outer HTML is the text representing + * all children of the node including the node itself. + * + * Parameters: + * + * node - DOM node to return the outer HTML for. + */ + getOuterHtml: function() + { + if (mxClient.IS_IE) + { + return function(node) + { + if (node != null) + { + if (node.outerHTML != null) + { + return node.outerHTML; + } + else + { + var tmp = []; + tmp.push('<'+node.nodeName); + + var attrs = node.attributes; + + if (attrs != null) + { + for (var i = 0; i < attrs.length; i++) + { + var value = attrs[i].value; + + if (value != null && value.length > 0) + { + tmp.push(' '); + tmp.push(attrs[i].nodeName); + tmp.push('="'); + tmp.push(value); + tmp.push('"'); + } + } + } + + if (node.innerHTML.length == 0) + { + tmp.push('/>'); + } + else + { + tmp.push('>'); + tmp.push(node.innerHTML); + tmp.push(''); + } + + return tmp.join(''); + } + } + + return ''; + }; + } + else + { + return function(node) + { + if (node != null) + { + var serializer = new XMLSerializer(); + return serializer.serializeToString(node); + } + + return ''; + }; + } + }(), + + /** + * Function: write + * + * Creates a text node for the given string and appends it to the given + * parent. Returns the text node. + * + * Parameters: + * + * parent - DOM node to append the text node to. + * text - String representing the text to be added. + */ + write: function(parent, text) + { + var doc = parent.ownerDocument; + var node = doc.createTextNode(text); + + if (parent != null) + { + parent.appendChild(node); + } + + return node; + }, + + /** + * Function: writeln + * + * Creates a text node for the given string and appends it to the given + * parent with an additional linefeed. Returns the text node. + * + * Parameters: + * + * parent - DOM node to append the text node to. + * text - String representing the text to be added. + */ + writeln: function(parent, text) + { + var doc = parent.ownerDocument; + var node = doc.createTextNode(text); + + if (parent != null) + { + parent.appendChild(node); + parent.appendChild(document.createElement('br')); + } + + return node; + }, + + /** + * Function: br + * + * Appends a linebreak to the given parent and returns the linebreak. + * + * Parameters: + * + * parent - DOM node to append the linebreak to. + */ + br: function(parent, count) + { + count = count || 1; + var br = null; + + for (var i = 0; i < count; i++) + { + if (parent != null) + { + br = parent.ownerDocument.createElement('br'); + parent.appendChild(br); + } + } + + return br; + }, + + /** + * Function: button + * + * Returns a new button with the given level and function as an onclick + * event handler. + * + * (code) + * document.body.appendChild(mxUtils.button('Test', function(evt) + * { + * alert('Hello, World!'); + * })); + * (end) + * + * Parameters: + * + * label - String that represents the label of the button. + * funct - Function to be called if the button is pressed. + * doc - Optional document to be used for creating the button. Default is the + * current document. + */ + button: function(label, funct, doc) + { + doc = (doc != null) ? doc : document; + + var button = doc.createElement('button'); + mxUtils.write(button, label); + + mxEvent.addListener(button, 'click', function(evt) + { + funct(evt); + }); + + return button; + }, + + /** + * Function: para + * + * Appends a new paragraph with the given text to the specified parent and + * returns the paragraph. + * + * Parameters: + * + * parent - DOM node to append the text node to. + * text - String representing the text for the new paragraph. + */ + para: function(parent, text) + { + var p = document.createElement('p'); + mxUtils.write(p, text); + + if (parent != null) + { + parent.appendChild(p); + } + + return p; + }, + + /** + * Function: addTransparentBackgroundFilter + * + * Adds a transparent background to the filter of the given node. This + * background can be used in IE8 standards mode (native IE8 only) to pass + * events through the node. + */ + addTransparentBackgroundFilter: function(node) + { + node.style.filter += 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\'' + + mxClient.imageBasePath + '/transparent.gif\', sizingMethod=\'scale\')'; + }, + + /** + * Function: linkAction + * + * Adds a hyperlink to the specified parent that invokes action on the + * specified editor. + * + * Parameters: + * + * parent - DOM node to contain the new link. + * text - String that is used as the link label. + * editor - that will execute the action. + * action - String that defines the name of the action to be executed. + * pad - Optional left-padding for the link. Default is 0. + */ + linkAction: function(parent, text, editor, action, pad) + { + return mxUtils.link(parent, text, function() + { + editor.execute(action); + }, pad); + }, + + /** + * Function: linkInvoke + * + * Adds a hyperlink to the specified parent that invokes the specified + * function on the editor passing along the specified argument. The + * function name is the name of a function of the editor instance, + * not an action name. + * + * Parameters: + * + * parent - DOM node to contain the new link. + * text - String that is used as the link label. + * editor - instance to execute the function on. + * functName - String that represents the name of the function. + * arg - Object that represents the argument to the function. + * pad - Optional left-padding for the link. Default is 0. + */ + linkInvoke: function(parent, text, editor, functName, arg, pad) + { + return mxUtils.link(parent, text, function() + { + editor[functName](arg); + }, pad); + }, + + /** + * Function: link + * + * Adds a hyperlink to the specified parent and invokes the given function + * when the link is clicked. + * + * Parameters: + * + * parent - DOM node to contain the new link. + * text - String that is used as the link label. + * funct - Function to execute when the link is clicked. + * pad - Optional left-padding for the link. Default is 0. + */ + link: function(parent, text, funct, pad) + { + var a = document.createElement('span'); + + a.style.color = 'blue'; + a.style.textDecoration = 'underline'; + a.style.cursor = 'pointer'; + + if (pad != null) + { + a.style.paddingLeft = pad+'px'; + } + + mxEvent.addListener(a, 'click', funct); + mxUtils.write(a, text); + + if (parent != null) + { + parent.appendChild(a); + } + + return a; + }, + + /** + * Function: fit + * + * Makes sure the given node is inside the visible area of the window. This + * is done by setting the left and top in the style. + */ + fit: function(node) + { + var left = parseInt(node.offsetLeft); + var width = parseInt(node.offsetWidth); + + var offset = mxUtils.getDocumentScrollOrigin(node.ownerDocument); + var sl = offset.x; + var st = offset.y; + + var b = document.body; + var d = document.documentElement; + var right = (sl) + (b.clientWidth || d.clientWidth); + + if (left + width > right) + { + node.style.left = Math.max(sl, right - width) + 'px'; + } + + var top = parseInt(node.offsetTop); + var height = parseInt(node.offsetHeight); + + var bottom = st + Math.max(b.clientHeight || 0, d.clientHeight); + + if (top + height > bottom) + { + node.style.top = Math.max(st, bottom - height) + 'px'; + } + }, + + /** + * Function: load + * + * Loads the specified URL *synchronously* and returns the . + * Throws an exception if the file cannot be loaded. See for + * an asynchronous implementation. + * + * Example: + * + * (code) + * try + * { + * var req = mxUtils.load(filename); + * var root = req.getDocumentElement(); + * // Process XML DOM... + * } + * catch (ex) + * { + * mxUtils.alert('Cannot load '+filename+': '+ex); + * } + * (end) + * + * Parameters: + * + * url - URL to get the data from. + */ + load: function(url) + { + var req = new mxXmlRequest(url, null, 'GET', false); + req.send(); + + return req; + }, + + /** + * Function: get + * + * Loads the specified URL *asynchronously* and invokes the given functions + * depending on the request status. Returns the in use. Both + * functions take the as the only parameter. See + * for a synchronous implementation. + * + * Example: + * + * (code) + * mxUtils.get(url, function(req) + * { + * var node = req.getDocumentElement(); + * // Process XML DOM... + * }); + * (end) + * + * So for example, to load a diagram into an existing graph model, the + * following code is used. + * + * (code) + * mxUtils.get(url, function(req) + * { + * var node = req.getDocumentElement(); + * var dec = new mxCodec(node.ownerDocument); + * dec.decode(node, graph.getModel()); + * }); + * (end) + * + * Parameters: + * + * url - URL to get the data from. + * onload - Optional function to execute for a successful response. + * onerror - Optional function to execute on error. + * binary - Optional boolean parameter that specifies if the request is + * binary. + * timeout - Optional timeout in ms before calling ontimeout. + * ontimeout - Optional function to execute on timeout. + */ + get: function(url, onload, onerror, binary, timeout, ontimeout) + { + var req = new mxXmlRequest(url, null, 'GET'); + + if (binary != null) + { + req.setBinary(binary); + } + + req.send(onload, onerror, timeout, ontimeout); + + return req; + }, + + /** + * Function: getAll + * + * Loads the URLs in the given array *asynchronously* and invokes the given function + * if all requests returned with a valid 2xx status. The error handler is invoked + * once on the first error or invalid response. + * + * Parameters: + * + * urls - Array of URLs to be loaded. + * onload - Callback with array of . + * onerror - Optional function to execute on error. + */ + getAll: function(urls, onload, onerror) + { + var remain = urls.length; + var result = []; + var errors = 0; + var err = function() + { + if (errors == 0 && onerror != null) + { + onerror(); + } + + errors++; + }; + + for (var i = 0; i < urls.length; i++) + { + (function(url, index) + { + mxUtils.get(url, function(req) + { + var status = req.getStatus(); + + if (status < 200 || status > 299) + { + err(); + } + else + { + result[index] = req; + remain--; + + if (remain == 0) + { + onload(result); + } + } + }, err); + })(urls[i], i); + } + + if (remain == 0) + { + onload(result); + } + }, + + /** + * Function: post + * + * Posts the specified params to the given URL *asynchronously* and invokes + * the given functions depending on the request status. Returns the + * in use. Both functions take the as the + * only parameter. Make sure to use encodeURIComponent for the parameter + * values. + * + * Example: + * + * (code) + * mxUtils.post(url, 'key=value', function(req) + * { + * mxUtils.alert('Ready: '+req.isReady()+' Status: '+req.getStatus()); + * // Process req.getDocumentElement() using DOM API if OK... + * }); + * (end) + * + * Parameters: + * + * url - URL to get the data from. + * params - Parameters for the post request. + * onload - Optional function to execute for a successful response. + * onerror - Optional function to execute on error. + */ + post: function(url, params, onload, onerror) + { + return new mxXmlRequest(url, params).send(onload, onerror); + }, + + /** + * Function: submit + * + * Submits the given parameters to the specified URL using + * and returns the . + * Make sure to use encodeURIComponent for the parameter + * values. + * + * Parameters: + * + * url - URL to get the data from. + * params - Parameters for the form. + * doc - Document to create the form in. + * target - Target to send the form result to. + */ + submit: function(url, params, doc, target) + { + return new mxXmlRequest(url, params).simulate(doc, target); + }, + + /** + * Function: loadInto + * + * Loads the specified URL *asynchronously* into the specified document, + * invoking onload after the document has been loaded. This implementation + * does not use , but the document.load method. + * + * Parameters: + * + * url - URL to get the data from. + * doc - The document to load the URL into. + * onload - Function to execute when the URL has been loaded. + */ + loadInto: function(url, doc, onload) + { + if (mxClient.IS_IE) + { + doc.onreadystatechange = function () + { + if (doc.readyState == 4) + { + onload(); + } + }; + } + else + { + doc.addEventListener('load', onload, false); + } + + doc.load(url); + }, + + /** + * Function: getValue + * + * Returns the value for the given key in the given associative array or + * the given default value if the value is null. + * + * Parameters: + * + * array - Associative array that contains the value for the key. + * key - Key whose value should be returned. + * defaultValue - Value to be returned if the value for the given + * key is null. + */ + getValue: function(array, key, defaultValue) + { + var value = (array != null) ? array[key] : null; + + if (value == null) + { + value = defaultValue; + } + + return value; + }, + + /** + * Function: getNumber + * + * Returns the numeric value for the given key in the given associative + * array or the given default value (or 0) if the value is null. The value + * is converted to a numeric value using the Number function. + * + * Parameters: + * + * array - Associative array that contains the value for the key. + * key - Key whose value should be returned. + * defaultValue - Value to be returned if the value for the given + * key is null. Default is 0. + */ + getNumber: function(array, key, defaultValue) + { + var value = (array != null) ? array[key] : null; + + if (value == null) + { + value = defaultValue || 0; + } + + return Number(value); + }, + + /** + * Function: getColor + * + * Returns the color value for the given key in the given associative + * array or the given default value if the value is null. If the value + * is then null is returned. + * + * Parameters: + * + * array - Associative array that contains the value for the key. + * key - Key whose value should be returned. + * defaultValue - Value to be returned if the value for the given + * key is null. Default is null. + */ + getColor: function(array, key, defaultValue) + { + var value = (array != null) ? array[key] : null; + + if (value == null) + { + value = defaultValue; + } + else if (value == mxConstants.NONE) + { + value = null; + } + + return value; + }, + + /** + * Function: clone + * + * Recursively clones the specified object ignoring all fieldnames in the + * given array of transient fields. is always + * ignored by this function. + * + * Parameters: + * + * obj - Object to be cloned. + * transients - Optional array of strings representing the fieldname to be + * ignored. + * shallow - Optional boolean argument to specify if a shallow clone should + * be created, that is, one where all object references are not cloned or, + * in other words, one where only atomic (strings, numbers) values are + * cloned. Default is false. + */ + clone: function(obj, transients, shallow) + { + shallow = (shallow != null) ? shallow : false; + var clone = null; + + if (obj != null && typeof(obj.constructor) == 'function') + { + clone = new obj.constructor(); + + for (var i in obj) + { + if (i != mxObjectIdentity.FIELD_NAME && (transients == null || + mxUtils.indexOf(transients, i) < 0)) + { + if (!shallow && typeof(obj[i]) == 'object') + { + clone[i] = mxUtils.clone(obj[i]); + } + else + { + clone[i] = obj[i]; + } + } + } + } + + return clone; + }, + + /** + * Function: equalPoints + * + * Compares all mxPoints in the given lists. + * + * Parameters: + * + * a - Array of to be compared. + * b - Array of to be compared. + */ + equalPoints: function(a, b) + { + if ((a == null && b != null) || (a != null && b == null) || + (a != null && b != null && a.length != b.length)) + { + return false; + } + else if (a != null && b != null) + { + for (var i = 0; i < a.length; i++) + { + if (a[i] == b[i] || (a[i] != null && !a[i].equals(b[i]))) + { + return false; + } + } + } + + return true; + }, + + /** + * Function: equalEntries + * + * Returns true if all properties of the given objects are equal. Values + * with NaN are equal to NaN and unequal to any other value. + * + * Parameters: + * + * a - First object to be compared. + * b - Second object to be compared. + */ + equalEntries: function(a, b) + { + if ((a == null && b != null) || (a != null && b == null) || + (a != null && b != null && a.length != b.length)) + { + return false; + } + else if (a != null && b != null) + { + // Counts keys in b to check if all values have been compared + var count = 0; + + for (var key in b) + { + count++; + } + + for (var key in a) + { + count-- + + if ((!mxUtils.isNaN(a[key]) || !mxUtils.isNaN(b[key])) && a[key] != b[key]) + { + return false; + } + } + } + + return count == 0; + }, + + /** + * Function: removeDuplicates + * + * Removes all duplicates from the given array. + */ + removeDuplicates: function(arr) + { + var dict = new mxDictionary(); + var result = []; + + for (var i = 0; i < arr.length; i++) + { + if (!dict.get(arr[i])) + { + result.push(arr[i]); + dict.put(arr[i], true); + } + } + + return result; + }, + + /** + * Function: isNaN + * + * Returns true if the given value is of type number and isNaN returns true. + */ + isNaN: function(value) + { + return typeof(value) == 'number' && isNaN(value); + }, + + /** + * Function: extend + * + * Assigns a copy of the superclass prototype to the subclass prototype. + * Note that this does not call the constructor of the superclass at this + * point, the superclass constructor should be called explicitely in the + * subclass constructor. Below is an example. + * + * (code) + * MyGraph = function(container, model, renderHint, stylesheet) + * { + * mxGraph.call(this, container, model, renderHint, stylesheet); + * } + * + * mxUtils.extend(MyGraph, mxGraph); + * (end) + * + * Parameters: + * + * ctor - Constructor of the subclass. + * superCtor - Constructor of the superclass. + */ + extend: function(ctor, superCtor) + { + var f = function() {}; + f.prototype = superCtor.prototype; + + ctor.prototype = new f(); + ctor.prototype.constructor = ctor; + }, + + /** + * Function: toString + * + * Returns a textual representation of the specified object. + * + * Parameters: + * + * obj - Object to return the string representation for. + */ + toString: function(obj) + { + var output = ''; + + for (var i in obj) + { + try + { + if (obj[i] == null) + { + output += i + ' = [null]\n'; + } + else if (typeof(obj[i]) == 'function') + { + output += i + ' => [Function]\n'; + } + else if (typeof(obj[i]) == 'object') + { + var ctor = mxUtils.getFunctionName(obj[i].constructor); + output += i + ' => [' + ctor + ']\n'; + } + else + { + output += i + ' = ' + obj[i] + '\n'; + } + } + catch (e) + { + output += i + '=' + e.message; + } + } + + return output; + }, + + /** + * Function: toRadians + * + * Converts the given degree to radians. + */ + toRadians: function(deg) + { + return Math.PI * deg / 180; + }, + + /** + * Function: toDegree + * + * Converts the given radians to degree. + */ + toDegree: function(rad) + { + return rad * 180 / Math.PI; + }, + + /** + * Function: arcToCurves + * + * Converts the given arc to a series of curves. + */ + arcToCurves: function(x0, y0, r1, r2, angle, largeArcFlag, sweepFlag, x, y) + { + x -= x0; + y -= y0; + + if (r1 === 0 || r2 === 0) + { + return result; + } + + var fS = sweepFlag; + var psai = angle; + r1 = Math.abs(r1); + r2 = Math.abs(r2); + var ctx = -x / 2; + var cty = -y / 2; + var cpsi = Math.cos(psai * Math.PI / 180); + var spsi = Math.sin(psai * Math.PI / 180); + var rxd = cpsi * ctx + spsi * cty; + var ryd = -1 * spsi * ctx + cpsi * cty; + var rxdd = rxd * rxd; + var rydd = ryd * ryd; + var r1x = r1 * r1; + var r2y = r2 * r2; + var lamda = rxdd / r1x + rydd / r2y; + var sds; + + if (lamda > 1) + { + r1 = Math.sqrt(lamda) * r1; + r2 = Math.sqrt(lamda) * r2; + sds = 0; + } + else + { + var seif = 1; + + if (largeArcFlag === fS) + { + seif = -1; + } + + sds = seif * Math.sqrt((r1x * r2y - r1x * rydd - r2y * rxdd) / (r1x * rydd + r2y * rxdd)); + } + + var txd = sds * r1 * ryd / r2; + var tyd = -1 * sds * r2 * rxd / r1; + var tx = cpsi * txd - spsi * tyd + x / 2; + var ty = spsi * txd + cpsi * tyd + y / 2; + var rad = Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1) - Math.atan2(0, 1); + var s1 = (rad >= 0) ? rad : 2 * Math.PI + rad; + rad = Math.atan2((-ryd - tyd) / r2, (-rxd - txd) / r1) - Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1); + var dr = (rad >= 0) ? rad : 2 * Math.PI + rad; + + if (fS == 0 && dr > 0) + { + dr -= 2 * Math.PI; + } + else if (fS != 0 && dr < 0) + { + dr += 2 * Math.PI; + } + + var sse = dr * 2 / Math.PI; + var seg = Math.ceil(sse < 0 ? -1 * sse : sse); + var segr = dr / seg; + var t = 8/3 * Math.sin(segr / 4) * Math.sin(segr / 4) / Math.sin(segr / 2); + var cpsir1 = cpsi * r1; + var cpsir2 = cpsi * r2; + var spsir1 = spsi * r1; + var spsir2 = spsi * r2; + var mc = Math.cos(s1); + var ms = Math.sin(s1); + var x2 = -t * (cpsir1 * ms + spsir2 * mc); + var y2 = -t * (spsir1 * ms - cpsir2 * mc); + var x3 = 0; + var y3 = 0; + + var result = []; + + for (var n = 0; n < seg; ++n) + { + s1 += segr; + mc = Math.cos(s1); + ms = Math.sin(s1); + + x3 = cpsir1 * mc - spsir2 * ms + tx; + y3 = spsir1 * mc + cpsir2 * ms + ty; + var dx = -t * (cpsir1 * ms + spsir2 * mc); + var dy = -t * (spsir1 * ms - cpsir2 * mc); + + // CurveTo updates x0, y0 so need to restore it + var index = n * 6; + result[index] = Number(x2 + x0); + result[index + 1] = Number(y2 + y0); + result[index + 2] = Number(x3 - dx + x0); + result[index + 3] = Number(y3 - dy + y0); + result[index + 4] = Number(x3 + x0); + result[index + 5] = Number(y3 + y0); + + x2 = x3 + dx; + y2 = y3 + dy; + } + + return result; + }, + + /** + * Function: getBoundingBox + * + * Returns the bounding box for the rotated rectangle. + * + * Parameters: + * + * rect - to be rotated. + * angle - Number that represents the angle (in degrees). + * cx - Optional that represents the rotation center. If no + * rotation center is given then the center of rect is used. + */ + getBoundingBox: function(rect, rotation, cx) + { + var result = null; + + if (rect != null && rotation != null && rotation != 0) + { + var rad = mxUtils.toRadians(rotation); + var cos = Math.cos(rad); + var sin = Math.sin(rad); + + cx = (cx != null) ? cx : new mxPoint(rect.x + rect.width / 2, rect.y + rect.height / 2); + + var p1 = new mxPoint(rect.x, rect.y); + var p2 = new mxPoint(rect.x + rect.width, rect.y); + var p3 = new mxPoint(p2.x, rect.y + rect.height); + var p4 = new mxPoint(rect.x, p3.y); + + p1 = mxUtils.getRotatedPoint(p1, cos, sin, cx); + p2 = mxUtils.getRotatedPoint(p2, cos, sin, cx); + p3 = mxUtils.getRotatedPoint(p3, cos, sin, cx); + p4 = mxUtils.getRotatedPoint(p4, cos, sin, cx); + + result = new mxRectangle(p1.x, p1.y, 0, 0); + result.add(new mxRectangle(p2.x, p2.y, 0, 0)); + result.add(new mxRectangle(p3.x, p3.y, 0, 0)); + result.add(new mxRectangle(p4.x, p4.y, 0, 0)); + } + + return result; + }, + + /** + * Function: getRotatedPoint + * + * Rotates the given point by the given cos and sin. + */ + getRotatedPoint: function(pt, cos, sin, c) + { + c = (c != null) ? c : new mxPoint(); + var x = pt.x - c.x; + var y = pt.y - c.y; + + var x1 = x * cos - y * sin; + var y1 = y * cos + x * sin; + + return new mxPoint(x1 + c.x, y1 + c.y); + }, + + /** + * Returns an integer mask of the port constraints of the given map + * @param dict the style map to determine the port constraints for + * @param defaultValue Default value to return if the key is undefined. + * @return the mask of port constraint directions + * + * Parameters: + * + * terminal - that represents the terminal. + * edge - that represents the edge. + * source - Boolean that specifies if the terminal is the source terminal. + * defaultValue - Default value to be returned. + */ + getPortConstraints: function(terminal, edge, source, defaultValue) + { + var value = mxUtils.getValue(terminal.style, mxConstants.STYLE_PORT_CONSTRAINT, + mxUtils.getValue(edge.style, (source) ? mxConstants.STYLE_SOURCE_PORT_CONSTRAINT : + mxConstants.STYLE_TARGET_PORT_CONSTRAINT, null)); + + if (value == null) + { + return defaultValue; + } + else + { + var directions = value.toString(); + var returnValue = mxConstants.DIRECTION_MASK_NONE; + var constraintRotationEnabled = mxUtils.getValue(terminal.style, mxConstants.STYLE_PORT_CONSTRAINT_ROTATION, 0); + var rotation = 0; + + if (constraintRotationEnabled == 1) + { + rotation = mxUtils.getValue(terminal.style, mxConstants.STYLE_ROTATION, 0); + } + + var quad = 0; + + if (rotation > 45) + { + quad = 1; + + if (rotation >= 135) + { + quad = 2; + } + } + else if (rotation < -45) + { + quad = 3; + + if (rotation <= -135) + { + quad = 2; + } + } + + if (directions.indexOf(mxConstants.DIRECTION_NORTH) >= 0) + { + switch (quad) + { + case 0: + returnValue |= mxConstants.DIRECTION_MASK_NORTH; + break; + case 1: + returnValue |= mxConstants.DIRECTION_MASK_EAST; + break; + case 2: + returnValue |= mxConstants.DIRECTION_MASK_SOUTH; + break; + case 3: + returnValue |= mxConstants.DIRECTION_MASK_WEST; + break; + } + } + if (directions.indexOf(mxConstants.DIRECTION_WEST) >= 0) + { + switch (quad) + { + case 0: + returnValue |= mxConstants.DIRECTION_MASK_WEST; + break; + case 1: + returnValue |= mxConstants.DIRECTION_MASK_NORTH; + break; + case 2: + returnValue |= mxConstants.DIRECTION_MASK_EAST; + break; + case 3: + returnValue |= mxConstants.DIRECTION_MASK_SOUTH; + break; + } + } + if (directions.indexOf(mxConstants.DIRECTION_SOUTH) >= 0) + { + switch (quad) + { + case 0: + returnValue |= mxConstants.DIRECTION_MASK_SOUTH; + break; + case 1: + returnValue |= mxConstants.DIRECTION_MASK_WEST; + break; + case 2: + returnValue |= mxConstants.DIRECTION_MASK_NORTH; + break; + case 3: + returnValue |= mxConstants.DIRECTION_MASK_EAST; + break; + } + } + if (directions.indexOf(mxConstants.DIRECTION_EAST) >= 0) + { + switch (quad) + { + case 0: + returnValue |= mxConstants.DIRECTION_MASK_EAST; + break; + case 1: + returnValue |= mxConstants.DIRECTION_MASK_SOUTH; + break; + case 2: + returnValue |= mxConstants.DIRECTION_MASK_WEST; + break; + case 3: + returnValue |= mxConstants.DIRECTION_MASK_NORTH; + break; + } + } + + return returnValue; + } + }, + + /** + * Function: reversePortConstraints + * + * Reverse the port constraint bitmask. For example, north | east + * becomes south | west + */ + reversePortConstraints: function(constraint) + { + var result = 0; + + result = (constraint & mxConstants.DIRECTION_MASK_WEST) << 3; + result |= (constraint & mxConstants.DIRECTION_MASK_NORTH) << 1; + result |= (constraint & mxConstants.DIRECTION_MASK_SOUTH) >> 1; + result |= (constraint & mxConstants.DIRECTION_MASK_EAST) >> 3; + + return result; + }, + + /** + * Function: findNearestSegment + * + * Finds the index of the nearest segment on the given cell state for + * the specified coordinate pair. + */ + findNearestSegment: function(state, x, y) + { + var index = -1; + + if (state.absolutePoints.length > 0) + { + var last = state.absolutePoints[0]; + var min = null; + + for (var i = 1; i < state.absolutePoints.length; i++) + { + var current = state.absolutePoints[i]; + var dist = mxUtils.ptSegDistSq(last.x, last.y, + current.x, current.y, x, y); + + if (min == null || dist < min) + { + min = dist; + index = i - 1; + } + + last = current; + } + } + + return index; + }, + + /** + * Function: getDirectedBounds + * + * Adds the given margins to the given rectangle and rotates and flips the + * rectangle according to the respective styles in style. + */ + getDirectedBounds: function (rect, m, style, flipH, flipV) + { + var d = mxUtils.getValue(style, mxConstants.STYLE_DIRECTION, mxConstants.DIRECTION_EAST); + flipH = (flipH != null) ? flipH : mxUtils.getValue(style, mxConstants.STYLE_FLIPH, false); + flipV = (flipV != null) ? flipV : mxUtils.getValue(style, mxConstants.STYLE_FLIPV, false); + + m.x = Math.round(Math.max(0, Math.min(rect.width, m.x))); + m.y = Math.round(Math.max(0, Math.min(rect.height, m.y))); + m.width = Math.round(Math.max(0, Math.min(rect.width, m.width))); + m.height = Math.round(Math.max(0, Math.min(rect.height, m.height))); + + if ((flipV && (d == mxConstants.DIRECTION_SOUTH || d == mxConstants.DIRECTION_NORTH)) || + (flipH && (d == mxConstants.DIRECTION_EAST || d == mxConstants.DIRECTION_WEST))) + { + var tmp = m.x; + m.x = m.width; + m.width = tmp; + } + + if ((flipH && (d == mxConstants.DIRECTION_SOUTH || d == mxConstants.DIRECTION_NORTH)) || + (flipV && (d == mxConstants.DIRECTION_EAST || d == mxConstants.DIRECTION_WEST))) + { + var tmp = m.y; + m.y = m.height; + m.height = tmp; + } + + var m2 = mxRectangle.fromRectangle(m); + + if (d == mxConstants.DIRECTION_SOUTH) + { + m2.y = m.x; + m2.x = m.height; + m2.width = m.y; + m2.height = m.width; + } + else if (d == mxConstants.DIRECTION_WEST) + { + m2.y = m.height; + m2.x = m.width; + m2.width = m.x; + m2.height = m.y; + } + else if (d == mxConstants.DIRECTION_NORTH) + { + m2.y = m.width; + m2.x = m.y; + m2.width = m.height; + m2.height = m.x; + } + + return new mxRectangle(rect.x + m2.x, rect.y + m2.y, rect.width - m2.width - m2.x, rect.height - m2.height - m2.y); + }, + + /** + * Function: getPerimeterPoint + * + * Returns the intersection between the polygon defined by the array of + * points and the line between center and point. + */ + getPerimeterPoint: function (pts, center, point) + { + var min = null; + + for (var i = 0; i < pts.length - 1; i++) + { + var pt = mxUtils.intersection(pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, + center.x, center.y, point.x, point.y); + + if (pt != null) + { + var dx = point.x - pt.x; + var dy = point.y - pt.y; + var ip = {p: pt, distSq: dy * dy + dx * dx}; + + if (ip != null && (min == null || min.distSq > ip.distSq)) + { + min = ip; + } + } + } + + return (min != null) ? min.p : null; + }, + + /** + * Function: rectangleIntersectsSegment + * + * Returns true if the given rectangle intersects the given segment. + * + * Parameters: + * + * bounds - that represents the rectangle. + * p1 - that represents the first point of the segment. + * p2 - that represents the second point of the segment. + */ + rectangleIntersectsSegment: function(bounds, p1, p2) + { + var top = bounds.y; + var left = bounds.x; + var bottom = top + bounds.height; + var right = left + bounds.width; + + // Find min and max X for the segment + var minX = p1.x; + var maxX = p2.x; + + if (p1.x > p2.x) + { + minX = p2.x; + maxX = p1.x; + } + + // Find the intersection of the segment's and rectangle's x-projections + if (maxX > right) + { + maxX = right; + } + + if (minX < left) + { + minX = left; + } + + if (minX > maxX) // If their projections do not intersect return false + { + return false; + } + + // Find corresponding min and max Y for min and max X we found before + var minY = p1.y; + var maxY = p2.y; + var dx = p2.x - p1.x; + + if (Math.abs(dx) > 0.0000001) + { + var a = (p2.y - p1.y) / dx; + var b = p1.y - a * p1.x; + minY = a * minX + b; + maxY = a * maxX + b; + } + + if (minY > maxY) + { + var tmp = maxY; + maxY = minY; + minY = tmp; + } + + // Find the intersection of the segment's and rectangle's y-projections + if (maxY > bottom) + { + maxY = bottom; + } + + if (minY < top) + { + minY = top; + } + + if (minY > maxY) // If Y-projections do not intersect return false + { + return false; + } + + return true; + }, + + /** + * Function: contains + * + * Returns true if the specified point (x, y) is contained in the given rectangle. + * + * Parameters: + * + * bounds - that represents the area. + * x - X-coordinate of the point. + * y - Y-coordinate of the point. + */ + contains: function(bounds, x, y) + { + return (bounds.x <= x && bounds.x + bounds.width >= x && + bounds.y <= y && bounds.y + bounds.height >= y); + }, + + /** + * Function: intersects + * + * Returns true if the two rectangles intersect. + * + * Parameters: + * + * a - to be checked for intersection. + * b - to be checked for intersection. + */ + intersects: function(a, b) + { + var tw = a.width; + var th = a.height; + var rw = b.width; + var rh = b.height; + + if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) + { + return false; + } + + var tx = a.x; + var ty = a.y; + var rx = b.x; + var ry = b.y; + + rw += rx; + rh += ry; + tw += tx; + th += ty; + + return ((rw < rx || rw > tx) && + (rh < ry || rh > ty) && + (tw < tx || tw > rx) && + (th < ty || th > ry)); + }, + + /** + * Function: intersects + * + * Returns true if the two rectangles intersect. + * + * Parameters: + * + * a - to be checked for intersection. + * b - to be checked for intersection. + */ + intersectsHotspot: function(state, x, y, hotspot, min, max) + { + hotspot = (hotspot != null) ? hotspot : 1; + min = (min != null) ? min : 0; + max = (max != null) ? max : 0; + + if (hotspot > 0) + { + var cx = state.getCenterX(); + var cy = state.getCenterY(); + var w = state.width; + var h = state.height; + + var start = mxUtils.getValue(state.style, mxConstants.STYLE_STARTSIZE) * state.view.scale; + + if (start > 0) + { + if (mxUtils.getValue(state.style, mxConstants.STYLE_HORIZONTAL, true)) + { + cy = state.y + start / 2; + h = start; + } + else + { + cx = state.x + start / 2; + w = start; + } + } + + w = Math.max(min, w * hotspot); + h = Math.max(min, h * hotspot); + + if (max > 0) + { + w = Math.min(w, max); + h = Math.min(h, max); + } + + var rect = new mxRectangle(cx - w / 2, cy - h / 2, w, h); + var alpha = mxUtils.toRadians(mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION) || 0); + + if (alpha != 0) + { + var cos = Math.cos(-alpha); + var sin = Math.sin(-alpha); + var cx = new mxPoint(state.getCenterX(), state.getCenterY()); + var pt = mxUtils.getRotatedPoint(new mxPoint(x, y), cos, sin, cx); + x = pt.x; + y = pt.y; + } + + return mxUtils.contains(rect, x, y); + } + + return true; + }, + + /** + * Function: getOffset + * + * Returns the offset for the specified container as an . The + * offset is the distance from the top left corner of the container to the + * top left corner of the document. + * + * Parameters: + * + * container - DOM node to return the offset for. + * scollOffset - Optional boolean to add the scroll offset of the document. + * Default is false. + */ + getOffset: function(container, scrollOffset) + { + var offsetLeft = 0; + var offsetTop = 0; + + // Ignores document scroll origin for fixed elements + var fixed = false; + var node = container; + var b = document.body; + var d = document.documentElement; + + while (node != null && node != b && node != d && !fixed) + { + var style = mxUtils.getCurrentStyle(node); + + if (style != null) + { + fixed = fixed || style.position == 'fixed'; + } + + node = node.parentNode; + } + + if (!scrollOffset && !fixed) + { + var offset = mxUtils.getDocumentScrollOrigin(container.ownerDocument); + offsetLeft += offset.x; + offsetTop += offset.y; + } + + var r = container.getBoundingClientRect(); + + if (r != null) + { + offsetLeft += r.left; + offsetTop += r.top; + } + + return new mxPoint(offsetLeft, offsetTop); + }, + + /** + * Function: getDocumentScrollOrigin + * + * Returns the scroll origin of the given document or the current document + * if no document is given. + */ + getDocumentScrollOrigin: function(doc) + { + if (mxClient.IS_QUIRKS) + { + return new mxPoint(doc.body.scrollLeft, doc.body.scrollTop); + } + else + { + var wnd = doc.defaultView || doc.parentWindow; + + var x = (wnd != null && window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft; + var y = (wnd != null && window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop; + + return new mxPoint(x, y); + } + }, + + /** + * Function: getScrollOrigin + * + * Returns the top, left corner of the viewrect as an . + * + * Parameters: + * + * node - DOM node whose scroll origin should be returned. + * includeAncestors - Whether the scroll origin of the ancestors should be + * included. Default is false. + * includeDocument - Whether the scroll origin of the document should be + * included. Default is true. + */ + getScrollOrigin: function(node, includeAncestors, includeDocument) + { + includeAncestors = (includeAncestors != null) ? includeAncestors : false; + includeDocument = (includeDocument != null) ? includeDocument : true; + + var doc = (node != null) ? node.ownerDocument : document; + var b = doc.body; + var d = doc.documentElement; + var result = new mxPoint(); + var fixed = false; + + while (node != null && node != b && node != d) + { + if (!isNaN(node.scrollLeft) && !isNaN(node.scrollTop)) + { + result.x += node.scrollLeft; + result.y += node.scrollTop; + } + + var style = mxUtils.getCurrentStyle(node); + + if (style != null) + { + fixed = fixed || style.position == 'fixed'; + } + + node = (includeAncestors) ? node.parentNode : null; + } + + if (!fixed && includeDocument) + { + var origin = mxUtils.getDocumentScrollOrigin(doc); + + result.x += origin.x; + result.y += origin.y; + } + + return result; + }, + + /** + * Function: convertPoint + * + * Converts the specified point (x, y) using the offset of the specified + * container and returns a new with the result. + * + * (code) + * var pt = mxUtils.convertPoint(graph.container, + * mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + * (end) + * + * Parameters: + * + * container - DOM node to use for the offset. + * x - X-coordinate of the point to be converted. + * y - Y-coordinate of the point to be converted. + */ + convertPoint: function(container, x, y) + { + var origin = mxUtils.getScrollOrigin(container, false); + var offset = mxUtils.getOffset(container); + + offset.x -= origin.x; + offset.y -= origin.y; + + return new mxPoint(x - offset.x, y - offset.y); + }, + + /** + * Function: ltrim + * + * Strips all whitespaces from the beginning of the string. Without the + * second parameter, this will trim these characters: + * + * - " " (ASCII 32 (0x20)), an ordinary space + * - "\t" (ASCII 9 (0x09)), a tab + * - "\n" (ASCII 10 (0x0A)), a new line (line feed) + * - "\r" (ASCII 13 (0x0D)), a carriage return + * - "\0" (ASCII 0 (0x00)), the NUL-byte + * - "\x0B" (ASCII 11 (0x0B)), a vertical tab + */ + ltrim: function(str, chars) + { + chars = chars || "\\s"; + + return (str != null) ? str.replace(new RegExp("^[" + chars + "]+", "g"), "") : null; + }, + + /** + * Function: rtrim + * + * Strips all whitespaces from the end of the string. Without the second + * parameter, this will trim these characters: + * + * - " " (ASCII 32 (0x20)), an ordinary space + * - "\t" (ASCII 9 (0x09)), a tab + * - "\n" (ASCII 10 (0x0A)), a new line (line feed) + * - "\r" (ASCII 13 (0x0D)), a carriage return + * - "\0" (ASCII 0 (0x00)), the NUL-byte + * - "\x0B" (ASCII 11 (0x0B)), a vertical tab + */ + rtrim: function(str, chars) + { + chars = chars || "\\s"; + + return (str != null) ? str.replace(new RegExp("[" + chars + "]+$", "g"), "") : null; + }, + + /** + * Function: trim + * + * Strips all whitespaces from both end of the string. + * Without the second parameter, Javascript function will trim these + * characters: + * + * - " " (ASCII 32 (0x20)), an ordinary space + * - "\t" (ASCII 9 (0x09)), a tab + * - "\n" (ASCII 10 (0x0A)), a new line (line feed) + * - "\r" (ASCII 13 (0x0D)), a carriage return + * - "\0" (ASCII 0 (0x00)), the NUL-byte + * - "\x0B" (ASCII 11 (0x0B)), a vertical tab + */ + trim: function(str, chars) + { + return mxUtils.ltrim(mxUtils.rtrim(str, chars), chars); + }, + + /** + * Function: isNumeric + * + * Returns true if the specified value is numeric, that is, if it is not + * null, not an empty string, not a HEX number and isNaN returns false. + * + * Parameters: + * + * n - String representing the possibly numeric value. + */ + isNumeric: function(n) + { + return !isNaN(parseFloat(n)) && isFinite(n) && (typeof(n) != 'string' || n.toLowerCase().indexOf('0x') < 0); + }, + + /** + * Function: isInteger + * + * Returns true if the given value is an valid integer number. + * + * Parameters: + * + * n - String representing the possibly numeric value. + */ + isInteger: function(n) + { + return String(parseInt(n)) === String(n); + }, + + /** + * Function: mod + * + * Returns the remainder of division of n by m. You should use this instead + * of the built-in operation as the built-in operation does not properly + * handle negative numbers. + */ + mod: function(n, m) + { + return ((n % m) + m) % m; + }, + + /** + * Function: intersection + * + * Returns the intersection of two lines as an . + * + * Parameters: + * + * x0 - X-coordinate of the first line's startpoint. + * y0 - X-coordinate of the first line's startpoint. + * x1 - X-coordinate of the first line's endpoint. + * y1 - Y-coordinate of the first line's endpoint. + * x2 - X-coordinate of the second line's startpoint. + * y2 - Y-coordinate of the second line's startpoint. + * x3 - X-coordinate of the second line's endpoint. + * y3 - Y-coordinate of the second line's endpoint. + */ + intersection: function (x0, y0, x1, y1, x2, y2, x3, y3) + { + var denom = ((y3 - y2) * (x1 - x0)) - ((x3 - x2) * (y1 - y0)); + var nume_a = ((x3 - x2) * (y0 - y2)) - ((y3 - y2) * (x0 - x2)); + var nume_b = ((x1 - x0) * (y0 - y2)) - ((y1 - y0) * (x0 - x2)); + + var ua = nume_a / denom; + var ub = nume_b / denom; + + if(ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0) + { + // Get the intersection point + var x = x0 + ua * (x1 - x0); + var y = y0 + ua * (y1 - y0); + + return new mxPoint(x, y); + } + + // No intersection + return null; + }, + + /** + * Function: ptSegDistSq + * + * Returns the square distance between a segment and a point. To get the + * distance between a point and a line (with infinite length) use + * . + * + * Parameters: + * + * x1 - X-coordinate of the startpoint of the segment. + * y1 - Y-coordinate of the startpoint of the segment. + * x2 - X-coordinate of the endpoint of the segment. + * y2 - Y-coordinate of the endpoint of the segment. + * px - X-coordinate of the point. + * py - Y-coordinate of the point. + */ + ptSegDistSq: function(x1, y1, x2, y2, px, py) + { + x2 -= x1; + y2 -= y1; + + px -= x1; + py -= y1; + + var dotprod = px * x2 + py * y2; + var projlenSq; + + if (dotprod <= 0.0) + { + projlenSq = 0.0; + } + else + { + px = x2 - px; + py = y2 - py; + dotprod = px * x2 + py * y2; + + if (dotprod <= 0.0) + { + projlenSq = 0.0; + } + else + { + projlenSq = dotprod * dotprod / (x2 * x2 + y2 * y2); + } + } + + var lenSq = px * px + py * py - projlenSq; + + if (lenSq < 0) + { + lenSq = 0; + } + + return lenSq; + }, + + /** + * Function: ptLineDist + * + * Returns the distance between a line defined by two points and a point. + * To get the distance between a point and a segment (with a specific + * length) use . + * + * Parameters: + * + * x1 - X-coordinate of point 1 of the line. + * y1 - Y-coordinate of point 1 of the line. + * x2 - X-coordinate of point 1 of the line. + * y2 - Y-coordinate of point 1 of the line. + * px - X-coordinate of the point. + * py - Y-coordinate of the point. + */ + ptLineDist: function(x1, y1, x2, y2, px, py) + { + return Math.abs((y2 - y1) * px - (x2 - x1) * py + x2 * y1 - y2 * x1) / + Math.sqrt((y2 - y1) * (y2 - y1) + (x2 - x1) * (x2 - x1)); + }, + + /** + * Function: relativeCcw + * + * Returns 1 if the given point on the right side of the segment, 0 if its + * on the segment, and -1 if the point is on the left side of the segment. + * + * Parameters: + * + * x1 - X-coordinate of the startpoint of the segment. + * y1 - Y-coordinate of the startpoint of the segment. + * x2 - X-coordinate of the endpoint of the segment. + * y2 - Y-coordinate of the endpoint of the segment. + * px - X-coordinate of the point. + * py - Y-coordinate of the point. + */ + relativeCcw: function(x1, y1, x2, y2, px, py) + { + x2 -= x1; + y2 -= y1; + px -= x1; + py -= y1; + var ccw = px * y2 - py * x2; + + if (ccw == 0.0) + { + ccw = px * x2 + py * y2; + + if (ccw > 0.0) + { + px -= x2; + py -= y2; + ccw = px * x2 + py * y2; + + if (ccw < 0.0) + { + ccw = 0.0; + } + } + } + + return (ccw < 0.0) ? -1 : ((ccw > 0.0) ? 1 : 0); + }, + + /** + * Function: animateChanges + * + * See . This is for backwards compatibility and + * will be removed later. + */ + animateChanges: function(graph, changes) + { + // LATER: Deprecated, remove this function + mxEffects.animateChanges.apply(this, arguments); + }, + + /** + * Function: cascadeOpacity + * + * See . This is for backwards compatibility and + * will be removed later. + */ + cascadeOpacity: function(graph, cell, opacity) + { + mxEffects.cascadeOpacity.apply(this, arguments); + }, + + /** + * Function: fadeOut + * + * See . This is for backwards compatibility and + * will be removed later. + */ + fadeOut: function(node, from, remove, step, delay, isEnabled) + { + mxEffects.fadeOut.apply(this, arguments); + }, + + /** + * Function: setOpacity + * + * Sets the opacity of the specified DOM node to the given value in %. + * + * Parameters: + * + * node - DOM node to set the opacity for. + * value - Opacity in %. Possible values are between 0 and 100. + */ + setOpacity: function(node, value) + { + if (mxUtils.isVml(node)) + { + if (value >= 100) + { + node.style.filter = ''; + } + else + { + // TODO: Why is the division by 5 needed in VML? + node.style.filter = 'alpha(opacity=' + (value/5) + ')'; + } + } + else if (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9)) + { + if (value >= 100) + { + node.style.filter = ''; + } + else + { + node.style.filter = 'alpha(opacity=' + value + ')'; + } + } + else + { + node.style.opacity = (value / 100); + } + }, + + /** + * Function: createImage + * + * Creates and returns an image (IMG node) or VML image (v:image) in IE6 in + * quirks mode. + * + * Parameters: + * + * src - URL that points to the image to be displayed. + */ + createImage: function(src) + { + var imageNode = null; + + if (mxClient.IS_IE6 && document.compatMode != 'CSS1Compat') + { + imageNode = document.createElement(mxClient.VML_PREFIX + ':image'); + imageNode.setAttribute('src', src); + imageNode.style.borderStyle = 'none'; + } + else + { + imageNode = document.createElement('img'); + imageNode.setAttribute('src', src); + imageNode.setAttribute('border', '0'); + } + + return imageNode; + }, + + /** + * Function: sortCells + * + * Sorts the given cells according to the order in the cell hierarchy. + * Ascending is optional and defaults to true. + */ + sortCells: function(cells, ascending) + { + ascending = (ascending != null) ? ascending : true; + var lookup = new mxDictionary(); + cells.sort(function(o1, o2) + { + var p1 = lookup.get(o1); + + if (p1 == null) + { + p1 = mxCellPath.create(o1).split(mxCellPath.PATH_SEPARATOR); + lookup.put(o1, p1); + } + + var p2 = lookup.get(o2); + + if (p2 == null) + { + p2 = mxCellPath.create(o2).split(mxCellPath.PATH_SEPARATOR); + lookup.put(o2, p2); + } + + var comp = mxCellPath.compare(p1, p2); + + return (comp == 0) ? 0 : (((comp > 0) == ascending) ? 1 : -1); + }); + + return cells; + }, + + /** + * Function: getStylename + * + * Returns the stylename in a style of the form [(stylename|key=value);] or + * an empty string if the given style does not contain a stylename. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + */ + getStylename: function(style) + { + if (style != null) + { + var pairs = style.split(';'); + var stylename = pairs[0]; + + if (stylename.indexOf('=') < 0) + { + return stylename; + } + } + + return ''; + }, + + /** + * Function: getStylenames + * + * Returns the stylenames in a style of the form [(stylename|key=value);] + * or an empty array if the given style does not contain any stylenames. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + */ + getStylenames: function(style) + { + var result = []; + + if (style != null) + { + var pairs = style.split(';'); + + for (var i = 0; i < pairs.length; i++) + { + if (pairs[i].indexOf('=') < 0) + { + result.push(pairs[i]); + } + } + } + + return result; + }, + + /** + * Function: indexOfStylename + * + * Returns the index of the given stylename in the given style. This + * returns -1 if the given stylename does not occur (as a stylename) in the + * given style, otherwise it returns the index of the first character. + */ + indexOfStylename: function(style, stylename) + { + if (style != null && stylename != null) + { + var tokens = style.split(';'); + var pos = 0; + + for (var i = 0; i < tokens.length; i++) + { + if (tokens[i] == stylename) + { + return pos; + } + + pos += tokens[i].length + 1; + } + } + + return -1; + }, + + /** + * Function: addStylename + * + * Adds the specified stylename to the given style if it does not already + * contain the stylename. + */ + addStylename: function(style, stylename) + { + if (mxUtils.indexOfStylename(style, stylename) < 0) + { + if (style == null) + { + style = ''; + } + else if (style.length > 0 && style.charAt(style.length - 1) != ';') + { + style += ';'; + } + + style += stylename; + } + + return style; + }, + + /** + * Function: removeStylename + * + * Removes all occurrences of the specified stylename in the given style + * and returns the updated style. Trailing semicolons are not preserved. + */ + removeStylename: function(style, stylename) + { + var result = []; + + if (style != null) + { + var tokens = style.split(';'); + + for (var i = 0; i < tokens.length; i++) + { + if (tokens[i] != stylename) + { + result.push(tokens[i]); + } + } + } + + return result.join(';'); + }, + + /** + * Function: removeAllStylenames + * + * Removes all stylenames from the given style and returns the updated + * style. + */ + removeAllStylenames: function(style) + { + var result = []; + + if (style != null) + { + var tokens = style.split(';'); + + for (var i = 0; i < tokens.length; i++) + { + // Keeps the key, value assignments + if (tokens[i].indexOf('=') >= 0) + { + result.push(tokens[i]); + } + } + } + + return result.join(';'); + }, + + /** + * Function: setCellStyles + * + * Assigns the value for the given key in the styles of the given cells, or + * removes the key from the styles if the value is null. + * + * Parameters: + * + * model - to execute the transaction in. + * cells - Array of to be updated. + * key - Key of the style to be changed. + * value - New value for the given key. + */ + setCellStyles: function(model, cells, key, value) + { + if (cells != null && cells.length > 0) + { + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != null) + { + var style = mxUtils.setStyle(model.getStyle(cells[i]), key, value); + model.setStyle(cells[i], style); + } + } + } + finally + { + model.endUpdate(); + } + } + }, + + /** + * Function: setStyle + * + * Adds or removes the given key, value pair to the style and returns the + * new style. If value is null or zero length then the key is removed from + * the style. This is for cell styles, not for CSS styles. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + * key - Key of the style to be changed. + * value - New value for the given key. + */ + setStyle: function(style, key, value) + { + var isValue = value != null && (typeof(value.length) == 'undefined' || value.length > 0); + + if (style == null || style.length == 0) + { + if (isValue) + { + style = key + '=' + value + ';'; + } + } + else + { + if (style.substring(0, key.length + 1) == key + '=') + { + var next = style.indexOf(';'); + + if (isValue) + { + style = key + '=' + value + ((next < 0) ? ';' : style.substring(next)); + } + else + { + style = (next < 0 || next == style.length - 1) ? '' : style.substring(next + 1); + } + } + else + { + var index = style.indexOf(';' + key + '='); + + if (index < 0) + { + if (isValue) + { + var sep = (style.charAt(style.length - 1) == ';') ? '' : ';'; + style = style + sep + key + '=' + value + ';'; + } + } + else + { + var next = style.indexOf(';', index + 1); + + if (isValue) + { + style = style.substring(0, index + 1) + key + '=' + value + ((next < 0) ? ';' : style.substring(next)); + } + else + { + style = style.substring(0, index) + ((next < 0) ? ';' : style.substring(next)); + } + } + } + } + + return style; + }, + + /** + * Function: setCellStyleFlags + * + * Sets or toggles the flag bit for the given key in the cell's styles. + * If value is null then the flag is toggled. + * + * Example: + * + * (code) + * var cells = graph.getSelectionCells(); + * mxUtils.setCellStyleFlags(graph.model, + * cells, + * mxConstants.STYLE_FONTSTYLE, + * mxConstants.FONT_BOLD); + * (end) + * + * Toggles the bold font style. + * + * Parameters: + * + * model - that contains the cells. + * cells - Array of to change the style for. + * key - Key of the style to be changed. + * flag - Integer for the bit to be changed. + * value - Optional boolean value for the flag. + */ + setCellStyleFlags: function(model, cells, key, flag, value) + { + if (cells != null && cells.length > 0) + { + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != null) + { + var style = mxUtils.setStyleFlag( + model.getStyle(cells[i]), + key, flag, value); + model.setStyle(cells[i], style); + } + } + } + finally + { + model.endUpdate(); + } + } + }, + + /** + * Function: setStyleFlag + * + * Sets or removes the given key from the specified style and returns the + * new style. If value is null then the flag is toggled. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + * key - Key of the style to be changed. + * flag - Integer for the bit to be changed. + * value - Optional boolean value for the given flag. + */ + setStyleFlag: function(style, key, flag, value) + { + if (style == null || style.length == 0) + { + if (value || value == null) + { + style = key+'='+flag; + } + else + { + style = key+'=0'; + } + } + else + { + var index = style.indexOf(key+'='); + + if (index < 0) + { + var sep = (style.charAt(style.length-1) == ';') ? '' : ';'; + + if (value || value == null) + { + style = style + sep + key + '=' + flag; + } + else + { + style = style + sep + key + '=0'; + } + } + else + { + var cont = style.indexOf(';', index); + var tmp = ''; + + if (cont < 0) + { + tmp = style.substring(index+key.length+1); + } + else + { + tmp = style.substring(index+key.length+1, cont); + } + + if (value == null) + { + tmp = parseInt(tmp) ^ flag; + } + else if (value) + { + tmp = parseInt(tmp) | flag; + } + else + { + tmp = parseInt(tmp) & ~flag; + } + + style = style.substring(0, index) + key + '=' + tmp + + ((cont >= 0) ? style.substring(cont) : ''); + } + } + + return style; + }, + + /** + * Function: getAlignmentAsPoint + * + * Returns an that represents the horizontal and vertical alignment + * for numeric computations. X is -0.5 for center, -1 for right and 0 for + * left alignment. Y is -0.5 for middle, -1 for bottom and 0 for top + * alignment. Default values for missing arguments is top, left. + */ + getAlignmentAsPoint: function(align, valign) + { + var dx = 0; + var dy = 0; + + // Horizontal alignment + if (align == mxConstants.ALIGN_CENTER) + { + dx = -0.5; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + dx = -1; + } + + // Vertical alignment + if (valign == mxConstants.ALIGN_MIDDLE) + { + dy = -0.5; + } + else if (valign == mxConstants.ALIGN_BOTTOM) + { + dy = -1; + } + + return new mxPoint(dx, dy); + }, + + /** + * Function: getSizeForString + * + * Returns an with the size (width and height in pixels) of + * the given string. The string may contain HTML markup. Newlines should be + * converted to
before calling this method. The caller is responsible + * for sanitizing the HTML markup. + * + * Example: + * + * (code) + * var label = graph.getLabel(cell).replace(/\n/g, "
"); + * var size = graph.getSizeForString(label); + * (end) + * + * Parameters: + * + * text - String whose size should be returned. + * fontSize - Integer that specifies the font size in pixels. Default is + * . + * fontFamily - String that specifies the name of the font family. Default + * is . + * textWidth - Optional width for text wrapping. + */ + getSizeForString: function(text, fontSize, fontFamily, textWidth) + { + fontSize = (fontSize != null) ? fontSize : mxConstants.DEFAULT_FONTSIZE; + fontFamily = (fontFamily != null) ? fontFamily : mxConstants.DEFAULT_FONTFAMILY; + var div = document.createElement('div'); + + // Sets the font size and family + div.style.fontFamily = fontFamily; + div.style.fontSize = Math.round(fontSize) + 'px'; + div.style.lineHeight = Math.round(fontSize * mxConstants.LINE_HEIGHT) + 'px'; + + // Disables block layout and outside wrapping and hides the div + div.style.position = 'absolute'; + div.style.visibility = 'hidden'; + div.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; + div.style.zoom = '1'; + + if (textWidth != null) + { + div.style.width = textWidth + 'px'; + div.style.whiteSpace = 'normal'; + } + else + { + div.style.whiteSpace = 'nowrap'; + } + + // Adds the text and inserts into DOM for updating of size + div.innerHTML = text; + document.body.appendChild(div); + + // Gets the size and removes from DOM + var size = new mxRectangle(0, 0, div.offsetWidth, div.offsetHeight); + document.body.removeChild(div); + + return size; + }, + + /** + * Function: getViewXml + */ + getViewXml: function(graph, scale, cells, x0, y0) + { + x0 = (x0 != null) ? x0 : 0; + y0 = (y0 != null) ? y0 : 0; + scale = (scale != null) ? scale : 1; + + if (cells == null) + { + var model = graph.getModel(); + cells = [model.getRoot()]; + } + + var view = graph.getView(); + var result = null; + + // Disables events on the view + var eventsEnabled = view.isEventsEnabled(); + view.setEventsEnabled(false); + + // Workaround for label bounds not taken into account for image export. + // Creates a temporary draw pane which is used for rendering the text. + // Text rendering is required for finding the bounds of the labels. + var drawPane = view.drawPane; + var overlayPane = view.overlayPane; + + if (graph.dialect == mxConstants.DIALECT_SVG) + { + view.drawPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + view.canvas.appendChild(view.drawPane); + + // Redirects cell overlays into temporary container + view.overlayPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + view.canvas.appendChild(view.overlayPane); + } + else + { + view.drawPane = view.drawPane.cloneNode(false); + view.canvas.appendChild(view.drawPane); + + // Redirects cell overlays into temporary container + view.overlayPane = view.overlayPane.cloneNode(false); + view.canvas.appendChild(view.overlayPane); + } + + // Resets the translation + var translate = view.getTranslate(); + view.translate = new mxPoint(x0, y0); + + // Creates the temporary cell states in the view + var temp = new mxTemporaryCellStates(graph.getView(), scale, cells); + + try + { + var enc = new mxCodec(); + result = enc.encode(graph.getView()); + } + finally + { + temp.destroy(); + view.translate = translate; + view.canvas.removeChild(view.drawPane); + view.canvas.removeChild(view.overlayPane); + view.drawPane = drawPane; + view.overlayPane = overlayPane; + view.setEventsEnabled(eventsEnabled); + } + + return result; + }, + + /** + * Function: getScaleForPageCount + * + * Returns the scale to be used for printing the graph with the given + * bounds across the specifies number of pages with the given format. The + * scale is always computed such that it given the given amount or fewer + * pages in the print output. See for an example. + * + * Parameters: + * + * pageCount - Specifies the number of pages in the print output. + * graph - that should be printed. + * pageFormat - Optional that specifies the page format. + * Default is . + * border - The border along each side of every page. + */ + getScaleForPageCount: function(pageCount, graph, pageFormat, border) + { + if (pageCount < 1) + { + // We can't work with less than 1 page, return no scale + // change + return 1; + } + + pageFormat = (pageFormat != null) ? pageFormat : mxConstants.PAGE_FORMAT_A4_PORTRAIT; + border = (border != null) ? border : 0; + + var availablePageWidth = pageFormat.width - (border * 2); + var availablePageHeight = pageFormat.height - (border * 2); + + // Work out the number of pages required if the + // graph is not scaled. + var graphBounds = graph.getGraphBounds().clone(); + var sc = graph.getView().getScale(); + graphBounds.width /= sc; + graphBounds.height /= sc; + var graphWidth = graphBounds.width; + var graphHeight = graphBounds.height; + + var scale = 1; + + // The ratio of the width/height for each printer page + var pageFormatAspectRatio = availablePageWidth / availablePageHeight; + // The ratio of the width/height for the graph to be printer + var graphAspectRatio = graphWidth / graphHeight; + + // The ratio of horizontal pages / vertical pages for this + // graph to maintain its aspect ratio on this page format + var pagesAspectRatio = graphAspectRatio / pageFormatAspectRatio; + + // Factor the square root of the page count up and down + // by the pages aspect ratio to obtain a horizontal and + // vertical page count that adds up to the page count + // and has the correct aspect ratio + var pageRoot = Math.sqrt(pageCount); + var pagesAspectRatioSqrt = Math.sqrt(pagesAspectRatio); + var numRowPages = pageRoot * pagesAspectRatioSqrt; + var numColumnPages = pageRoot / pagesAspectRatioSqrt; + + // These value are rarely more than 2 rounding downs away from + // a total that meets the page count. In cases of one being less + // than 1 page, the other value can be too high and take more iterations + // In this case, just change that value to be the page count, since + // we know the other value is 1 + if (numRowPages < 1 && numColumnPages > pageCount) + { + var scaleChange = numColumnPages / pageCount; + numColumnPages = pageCount; + numRowPages /= scaleChange; + } + + if (numColumnPages < 1 && numRowPages > pageCount) + { + var scaleChange = numRowPages / pageCount; + numRowPages = pageCount; + numColumnPages /= scaleChange; + } + + var currentTotalPages = Math.ceil(numRowPages) * Math.ceil(numColumnPages); + + var numLoops = 0; + + // Iterate through while the rounded up number of pages comes to + // a total greater than the required number + while (currentTotalPages > pageCount) + { + // Round down the page count (rows or columns) that is + // closest to its next integer down in percentage terms. + // i.e. Reduce the page total by reducing the total + // page area by the least possible amount + + var roundRowDownProportion = Math.floor(numRowPages) / numRowPages; + var roundColumnDownProportion = Math.floor(numColumnPages) / numColumnPages; + + // If the round down proportion is, work out the proportion to + // round down to 1 page less + if (roundRowDownProportion == 1) + { + roundRowDownProportion = Math.floor(numRowPages-1) / numRowPages; + } + if (roundColumnDownProportion == 1) + { + roundColumnDownProportion = Math.floor(numColumnPages-1) / numColumnPages; + } + + // Check which rounding down is smaller, but in the case of very small roundings + // try the other dimension instead + var scaleChange = 1; + + // Use the higher of the two values + if (roundRowDownProportion > roundColumnDownProportion) + { + scaleChange = roundRowDownProportion; + } + else + { + scaleChange = roundColumnDownProportion; + } + + numRowPages = numRowPages * scaleChange; + numColumnPages = numColumnPages * scaleChange; + currentTotalPages = Math.ceil(numRowPages) * Math.ceil(numColumnPages); + + numLoops++; + + if (numLoops > 10) + { + break; + } + } + + // Work out the scale from the number of row pages required + // The column pages will give the same value + var posterWidth = availablePageWidth * numRowPages; + scale = posterWidth / graphWidth; + + // Allow for rounding errors + return scale * 0.99999; + }, + + /** + * Function: show + * + * Copies the styles and the markup from the graph's container into the + * given document and removes all cursor styles. The document is returned. + * + * This function should be called from within the document with the graph. + * If you experience problems with missing stylesheets in IE then try adding + * the domain to the trusted sites. + * + * Parameters: + * + * graph - to be copied. + * doc - Document where the new graph is created. + * x0 - X-coordinate of the graph view origin. Default is 0. + * y0 - Y-coordinate of the graph view origin. Default is 0. + * w - Optional width of the graph view. + * h - Optional height of the graph view. + */ + show: function(graph, doc, x0, y0, w, h) + { + x0 = (x0 != null) ? x0 : 0; + y0 = (y0 != null) ? y0 : 0; + + if (doc == null) + { + var wnd = window.open(); + doc = wnd.document; + } + else + { + doc.open(); + } + + // Workaround for missing print output in IE9 standards + if (document.documentMode == 9) + { + doc.writeln(''); + } + + var bounds = graph.getGraphBounds(); + var dx = Math.ceil(x0 - bounds.x); + var dy = Math.ceil(y0 - bounds.y); + + if (w == null) + { + w = Math.ceil(bounds.width + x0) + Math.ceil(Math.ceil(bounds.x) - bounds.x); + } + + if (h == null) + { + h = Math.ceil(bounds.height + y0) + Math.ceil(Math.ceil(bounds.y) - bounds.y); + } + + // Needs a special way of creating the page so that no click is required + // to refresh the contents after the external CSS styles have been loaded. + // To avoid a click or programmatic refresh, the styleSheets[].cssText + // property is copied over from the original document. + if (mxClient.IS_IE || document.documentMode == 11) + { + var html = ''; + + var base = document.getElementsByTagName('base'); + + for (var i = 0; i < base.length; i++) + { + html += base[i].outerHTML; + } + + html += ''; + + // Copies the contents of the graph container + html += '
'; + html += graph.container.innerHTML; + html += '
'; + + doc.writeln(html); + doc.close(); + } + else + { + doc.writeln(''); + + var base = document.getElementsByTagName('base'); + + for (var i = 0; i < base.length; i++) + { + doc.writeln(mxUtils.getOuterHtml(base[i])); + } + + var links = document.getElementsByTagName('link'); + + for (var i = 0; i < links.length; i++) + { + doc.writeln(mxUtils.getOuterHtml(links[i])); + } + + var styles = document.getElementsByTagName('style'); + + for (var i = 0; i < styles.length; i++) + { + doc.writeln(mxUtils.getOuterHtml(styles[i])); + } + + doc.writeln(''); + doc.close(); + + var outer = doc.createElement('div'); + outer.position = 'absolute'; + outer.overflow = 'hidden'; + outer.style.width = w + 'px'; + outer.style.height = h + 'px'; + + // Required for HTML labels if foreignObjects are disabled + var div = doc.createElement('div'); + div.style.position = 'absolute'; + div.style.left = dx + 'px'; + div.style.top = dy + 'px'; + + var node = graph.container.firstChild; + var svg = null; + + while (node != null) + { + var clone = node.cloneNode(true); + + if (node == graph.view.drawPane.ownerSVGElement) + { + outer.appendChild(clone); + svg = clone; + } + else + { + div.appendChild(clone); + } + + node = node.nextSibling; + } + + doc.body.appendChild(outer); + + if (div.firstChild != null) + { + doc.body.appendChild(div); + } + + if (svg != null) + { + svg.style.minWidth = ''; + svg.style.minHeight = ''; + svg.firstChild.setAttribute('transform', 'translate(' + dx + ',' + dy + ')'); + } + } + + mxUtils.removeCursors(doc.body); + + return doc; + }, + + /** + * Function: printScreen + * + * Prints the specified graph using a new window and the built-in print + * dialog. + * + * This function should be called from within the document with the graph. + * + * Parameters: + * + * graph - to be printed. + */ + printScreen: function(graph) + { + var wnd = window.open(); + var bounds = graph.getGraphBounds(); + mxUtils.show(graph, wnd.document); + + var print = function() + { + wnd.focus(); + wnd.print(); + wnd.close(); + }; + + // Workaround for Google Chrome which needs a bit of a + // delay in order to render the SVG contents + if (mxClient.IS_GC) + { + wnd.setTimeout(print, 500); + } + else + { + print(); + } + }, + + /** + * Function: popup + * + * Shows the specified text content in a new or a new browser + * window if isInternalWindow is false. + * + * Parameters: + * + * content - String that specifies the text to be displayed. + * isInternalWindow - Optional boolean indicating if an mxWindow should be + * used instead of a new browser window. Default is false. + */ + popup: function(content, isInternalWindow) + { + if (isInternalWindow) + { + var div = document.createElement('div'); + + div.style.overflow = 'scroll'; + div.style.width = '636px'; + div.style.height = '460px'; + + var pre = document.createElement('pre'); + pre.innerHTML = mxUtils.htmlEntities(content, false). + replace(/\n/g,'
').replace(/ /g, ' '); + + div.appendChild(pre); + + var w = document.body.clientWidth; + var h = Math.max(document.body.clientHeight || 0, document.documentElement.clientHeight) + var wnd = new mxWindow('Popup Window', div, + w/2-320, h/2-240, 640, 480, false, true); + + wnd.setClosable(true); + wnd.setVisible(true); + } + else + { + // Wraps up the XML content in a textarea + if (mxClient.IS_NS) + { + var wnd = window.open(); + wnd.document.writeln('
'+mxUtils.htmlEntities(content)+'').replace(/ /g, ' ');
+			   	wnd.document.body.appendChild(pre);
+			}
+	   	}
+	},
+
+	/**
+	 * Function: alert
+	 *
+	 * Displayss the given alert in a new dialog. This implementation uses the
+	 * built-in alert function. This is used to display validation errors when
+	 * connections cannot be changed or created.
+	 *
+	 * Parameters:
+	 *
+	 * message - String specifying the message to be displayed.
+	 */
+	alert: function(message)
+	{
+		alert(message);
+	},
+
+	/**
+	 * Function: prompt
+	 *
+	 * Displays the given message in a prompt dialog. This implementation uses
+	 * the built-in prompt function.
+	 *
+	 * Parameters:
+	 *
+	 * message - String specifying the message to be displayed.
+	 * defaultValue - Optional string specifying the default value.
+	 */
+	prompt: function(message, defaultValue)
+	{
+		return prompt(message, (defaultValue != null) ? defaultValue : '');
+	},
+
+	/**
+	 * Function: confirm
+	 *
+	 * Displays the given message in a confirm dialog. This implementation uses
+	 * the built-in confirm function.
+	 *
+	 * Parameters:
+	 *
+	 * message - String specifying the message to be displayed.
+	 */
+	confirm: function(message)
+	{
+		return confirm(message);
+	},
+
+	/**
+	 * Function: error
+	 *
+	 * Displays the given error message in a new  of the given width.
+	 * If close is true then an additional close button is added to the window.
+	 * The optional icon specifies the icon to be used for the window. Default
+	 * is .
+	 *
+	 * Parameters:
+	 *
+	 * message - String specifying the message to be displayed.
+	 * width - Integer specifying the width of the window.
+	 * close - Optional boolean indicating whether to add a close button.
+	 * icon - Optional icon for the window decoration.
+	 */
+	error: function(message, width, close, icon)
+	{
+		var div = document.createElement('div');
+		div.style.padding = '20px';
+
+		var img = document.createElement('img');
+		img.setAttribute('src', icon || mxUtils.errorImage);
+		img.setAttribute('valign', 'bottom');
+		img.style.verticalAlign = 'middle';
+		div.appendChild(img);
+
+		div.appendChild(document.createTextNode('\u00a0')); //  
+		div.appendChild(document.createTextNode('\u00a0')); //  
+		div.appendChild(document.createTextNode('\u00a0')); //  
+		mxUtils.write(div, message);
+
+		var w = document.body.clientWidth;
+		var h = (document.body.clientHeight || document.documentElement.clientHeight);
+		var warn = new mxWindow(mxResources.get(mxUtils.errorResource) ||
+			mxUtils.errorResource, div, (w-width)/2, h/4, width, null,
+			false, true);
+
+		if (close)
+		{
+			mxUtils.br(div);
+
+			var tmp = document.createElement('p');
+			var button = document.createElement('button');
+
+			if (mxClient.IS_IE)
+			{
+				button.style.cssText = 'float:right';
+			}
+			else
+			{
+				button.setAttribute('style', 'float:right');
+			}
+
+			mxEvent.addListener(button, 'click', function(evt)
+			{
+				warn.destroy();
+			});
+
+			mxUtils.write(button, mxResources.get(mxUtils.closeResource) ||
+				mxUtils.closeResource);
+
+			tmp.appendChild(button);
+			div.appendChild(tmp);
+
+			mxUtils.br(div);
+
+			warn.setClosable(true);
+		}
+
+		warn.setVisible(true);
+
+		return warn;
+	},
+
+	/**
+	 * Function: makeDraggable
+	 *
+	 * Configures the given DOM element to act as a drag source for the
+	 * specified graph. Returns a a new . If
+	 *  is enabled then the x and y arguments must
+	 * be used in funct to match the preview location.
+	 *
+	 * Example:
+	 *
+	 * (code)
+	 * var funct = function(graph, evt, cell, x, y)
+	 * {
+	 *   if (graph.canImportCell(cell))
+	 *   {
+	 *     var parent = graph.getDefaultParent();
+	 *     var vertex = null;
+	 *
+	 *     graph.getModel().beginUpdate();
+	 *     try
+	 *     {
+	 * 	     vertex = graph.insertVertex(parent, null, 'Hello', x, y, 80, 30);
+	 *     }
+	 *     finally
+	 *     {
+	 *       graph.getModel().endUpdate();
+	 *     }
+	 *
+	 *     graph.setSelectionCell(vertex);
+	 *   }
+	 * }
+	 *
+	 * var img = document.createElement('img');
+	 * img.setAttribute('src', 'editors/images/rectangle.gif');
+	 * img.style.position = 'absolute';
+	 * img.style.left = '0px';
+	 * img.style.top = '0px';
+	 * img.style.width = '16px';
+	 * img.style.height = '16px';
+	 *
+	 * var dragImage = img.cloneNode(true);
+	 * dragImage.style.width = '32px';
+	 * dragImage.style.height = '32px';
+	 * mxUtils.makeDraggable(img, graph, funct, dragImage);
+	 * document.body.appendChild(img);
+	 * (end)
+	 *
+	 * Parameters:
+	 *
+	 * element - DOM element to make draggable.
+	 * graphF -  that acts as the drop target or a function that takes a
+	 * mouse event and returns the current .
+	 * funct - Function to execute on a successful drop.
+	 * dragElement - Optional DOM node to be used for the drag preview.
+	 * dx - Optional horizontal offset between the cursor and the drag
+	 * preview.
+	 * dy - Optional vertical offset between the cursor and the drag
+	 * preview.
+	 * autoscroll - Optional boolean that specifies if autoscroll should be
+	 * used. Default is mxGraph.autoscroll.
+	 * scalePreview - Optional boolean that specifies if the preview element
+	 * should be scaled according to the graph scale. If this is true, then
+	 * the offsets will also be scaled. Default is false.
+	 * highlightDropTargets - Optional boolean that specifies if dropTargets
+	 * should be highlighted. Default is true.
+	 * getDropTarget - Optional function to return the drop target for a given
+	 * location (x, y). Default is mxGraph.getCellAt.
+	 */
+	makeDraggable: function(element, graphF, funct, dragElement, dx, dy, autoscroll,
+			scalePreview, highlightDropTargets, getDropTarget)
+	{
+		var dragSource = new mxDragSource(element, funct);
+		dragSource.dragOffset = new mxPoint((dx != null) ? dx : 0,
+			(dy != null) ? dy : mxConstants.TOOLTIP_VERTICAL_OFFSET);
+		dragSource.autoscroll = autoscroll;
+
+		// Cannot enable this by default. This needs to be enabled in the caller
+		// if the funct argument uses the new x- and y-arguments.
+		dragSource.setGuidesEnabled(false);
+
+		if (highlightDropTargets != null)
+		{
+			dragSource.highlightDropTargets = highlightDropTargets;
+		}
+
+		// Overrides function to find drop target cell
+		if (getDropTarget != null)
+		{
+			dragSource.getDropTarget = getDropTarget;
+		}
+
+		// Overrides function to get current graph
+		dragSource.getGraphForEvent = function(evt)
+		{
+			return (typeof(graphF) == 'function') ? graphF(evt) : graphF;
+		};
+
+		// Translates switches into dragSource customizations
+		if (dragElement != null)
+		{
+			dragSource.createDragElement = function()
+			{
+				return dragElement.cloneNode(true);
+			};
+
+			if (scalePreview)
+			{
+				dragSource.createPreviewElement = function(graph)
+				{
+					var elt = dragElement.cloneNode(true);
+
+					var w = parseInt(elt.style.width);
+					var h = parseInt(elt.style.height);
+					elt.style.width = Math.round(w * graph.view.scale) + 'px';
+					elt.style.height = Math.round(h * graph.view.scale) + 'px';
+
+					return elt;
+				};
+			}
+		}
+
+		return dragSource;
+	}
+
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+ var mxConstants =
+ {
+	/**
+	 * Class: mxConstants
+	 *
+	 * Defines various global constants.
+	 *
+	 * Variable: DEFAULT_HOTSPOT
+	 *
+	 * Defines the portion of the cell which is to be used as a connectable
+	 * region. Default is 0.3. Possible values are 0 < x <= 1.
+	 */
+	DEFAULT_HOTSPOT: 0.3,
+
+	/**
+	 * Variable: MIN_HOTSPOT_SIZE
+	 *
+	 * Defines the minimum size in pixels of the portion of the cell which is
+	 * to be used as a connectable region. Default is 8.
+	 */
+	MIN_HOTSPOT_SIZE: 8,
+
+	/**
+	 * Variable: MAX_HOTSPOT_SIZE
+	 *
+	 * Defines the maximum size in pixels of the portion of the cell which is
+	 * to be used as a connectable region. Use 0 for no maximum. Default is 0.
+	 */
+	MAX_HOTSPOT_SIZE: 0,
+
+	/**
+	 * Variable: RENDERING_HINT_EXACT
+	 *
+	 * Defines the exact rendering hint.
+	 */
+	RENDERING_HINT_EXACT: 'exact',
+
+	/**
+	 * Variable: RENDERING_HINT_FASTER
+	 *
+	 * Defines the faster rendering hint.
+	 */
+	RENDERING_HINT_FASTER: 'faster',
+
+	/**
+	 * Variable: RENDERING_HINT_FASTEST
+	 *
+	 * Defines the fastest rendering hint.
+	 */
+	RENDERING_HINT_FASTEST: 'fastest',
+
+	/**
+	 * Variable: DIALECT_SVG
+	 *
+	 * Defines the SVG display dialect name.
+	 */
+	DIALECT_SVG: 'svg',
+
+	/**
+	 * Variable: DIALECT_VML
+	 *
+	 * Defines the VML display dialect name.
+	 */
+	DIALECT_VML: 'vml',
+
+	/**
+	 * Variable: DIALECT_MIXEDHTML
+	 *
+	 * Defines the mixed HTML display dialect name.
+	 */
+	DIALECT_MIXEDHTML: 'mixedHtml',
+
+	/**
+	 * Variable: DIALECT_PREFERHTML
+	 *
+	 * Defines the preferred HTML display dialect name.
+	 */
+	DIALECT_PREFERHTML: 'preferHtml',
+
+	/**
+	 * Variable: DIALECT_STRICTHTML
+	 *
+	 * Defines the strict HTML display dialect.
+	 */
+	DIALECT_STRICTHTML: 'strictHtml',
+
+	/**
+	 * Variable: NS_SVG
+	 *
+	 * Defines the SVG namespace.
+	 */
+	NS_SVG: 'http://www.w3.org/2000/svg',
+
+	/**
+	 * Variable: NS_XHTML
+	 *
+	 * Defines the XHTML namespace.
+	 */
+	NS_XHTML: 'http://www.w3.org/1999/xhtml',
+
+	/**
+	 * Variable: NS_XLINK
+	 *
+	 * Defines the XLink namespace.
+	 */
+	NS_XLINK: 'http://www.w3.org/1999/xlink',
+
+	/**
+	 * Variable: SHADOWCOLOR
+	 *
+	 * Defines the color to be used to draw shadows in shapes and windows.
+	 * Default is gray.
+	 */
+	SHADOWCOLOR: 'gray',
+
+	/**
+	 * Variable: VML_SHADOWCOLOR
+	 *
+	 * Used for shadow color in filters where transparency is not supported
+	 * (Microsoft Internet Explorer). Default is gray.
+	 */
+	VML_SHADOWCOLOR: 'gray',
+
+	/**
+	 * Variable: SHADOW_OFFSET_X
+	 *
+	 * Specifies the x-offset of the shadow. Default is 2.
+	 */
+	SHADOW_OFFSET_X: 2,
+
+	/**
+	 * Variable: SHADOW_OFFSET_Y
+	 *
+	 * Specifies the y-offset of the shadow. Default is 3.
+	 */
+	SHADOW_OFFSET_Y: 3,
+
+	/**
+	 * Variable: SHADOW_OPACITY
+	 *
+	 * Defines the opacity for shadows. Default is 1.
+	 */
+	SHADOW_OPACITY: 1,
+
+	/**
+	 * Variable: NODETYPE_ELEMENT
+	 *
+	 * DOM node of type ELEMENT.
+	 */
+	NODETYPE_ELEMENT: 1,
+
+	/**
+	 * Variable: NODETYPE_ATTRIBUTE
+	 *
+	 * DOM node of type ATTRIBUTE.
+	 */
+	NODETYPE_ATTRIBUTE: 2,
+
+	/**
+	 * Variable: NODETYPE_TEXT
+	 *
+	 * DOM node of type TEXT.
+	 */
+	NODETYPE_TEXT: 3,
+
+	/**
+	 * Variable: NODETYPE_CDATA
+	 *
+	 * DOM node of type CDATA.
+	 */
+	NODETYPE_CDATA: 4,
+
+	/**
+	 * Variable: NODETYPE_ENTITY_REFERENCE
+	 *
+	 * DOM node of type ENTITY_REFERENCE.
+	 */
+	NODETYPE_ENTITY_REFERENCE: 5,
+
+	/**
+	 * Variable: NODETYPE_ENTITY
+	 *
+	 * DOM node of type ENTITY.
+	 */
+	NODETYPE_ENTITY: 6,
+
+	/**
+	 * Variable: NODETYPE_PROCESSING_INSTRUCTION
+	 *
+	 * DOM node of type PROCESSING_INSTRUCTION.
+	 */
+	NODETYPE_PROCESSING_INSTRUCTION: 7,
+
+	/**
+	 * Variable: NODETYPE_COMMENT
+	 *
+	 * DOM node of type COMMENT.
+	 */
+	NODETYPE_COMMENT: 8,
+
+	/**
+	 * Variable: NODETYPE_DOCUMENT
+	 *
+	 * DOM node of type DOCUMENT.
+	 */
+	NODETYPE_DOCUMENT: 9,
+
+	/**
+	 * Variable: NODETYPE_DOCUMENTTYPE
+	 *
+	 * DOM node of type DOCUMENTTYPE.
+	 */
+	NODETYPE_DOCUMENTTYPE: 10,
+
+	/**
+	 * Variable: NODETYPE_DOCUMENT_FRAGMENT
+	 *
+	 * DOM node of type DOCUMENT_FRAGMENT.
+	 */
+	NODETYPE_DOCUMENT_FRAGMENT: 11,
+
+	/**
+	 * Variable: NODETYPE_NOTATION
+	 *
+	 * DOM node of type NOTATION.
+	 */
+	NODETYPE_NOTATION: 12,
+
+	/**
+	 * Variable: TOOLTIP_VERTICAL_OFFSET
+	 *
+	 * Defines the vertical offset for the tooltip.
+	 * Default is 16.
+	 */
+	TOOLTIP_VERTICAL_OFFSET: 16,
+
+	/**
+	 * Variable: DEFAULT_VALID_COLOR
+	 *
+	 * Specifies the default valid color. Default is #0000FF.
+	 */
+	DEFAULT_VALID_COLOR: '#00FF00',
+
+	/**
+	 * Variable: DEFAULT_INVALID_COLOR
+	 *
+	 * Specifies the default invalid color. Default is #FF0000.
+	 */
+	DEFAULT_INVALID_COLOR: '#FF0000',
+
+	/**
+	 * Variable: OUTLINE_HIGHLIGHT_COLOR
+	 *
+	 * Specifies the default highlight color for shape outlines.
+	 * Default is #0000FF. This is used in .
+	 */
+	OUTLINE_HIGHLIGHT_COLOR: '#00FF00',
+
+	/**
+	 * Variable: OUTLINE_HIGHLIGHT_COLOR
+	 *
+	 * Defines the strokewidth to be used for shape outlines.
+	 * Default is 5. This is used in .
+	 */
+	OUTLINE_HIGHLIGHT_STROKEWIDTH: 5,
+
+	/**
+	 * Variable: HIGHLIGHT_STROKEWIDTH
+	 *
+	 * Defines the strokewidth to be used for the highlights.
+	 * Default is 3.
+	 */
+	HIGHLIGHT_STROKEWIDTH: 3,
+
+	/**
+	 * Variable: CONSTRAINT_HIGHLIGHT_SIZE
+	 *
+	 * Size of the constraint highlight (in px). Default is 2.
+	 */
+	HIGHLIGHT_SIZE: 2,
+
+	/**
+	 * Variable: HIGHLIGHT_OPACITY
+	 *
+	 * Opacity (in %) used for the highlights (including outline).
+	 * Default is 100.
+	 */
+	HIGHLIGHT_OPACITY: 100,
+
+	/**
+	 * Variable: CURSOR_MOVABLE_VERTEX
+	 *
+	 * Defines the cursor for a movable vertex. Default is 'move'.
+	 */
+	CURSOR_MOVABLE_VERTEX: 'move',
+
+	/**
+	 * Variable: CURSOR_MOVABLE_EDGE
+	 *
+	 * Defines the cursor for a movable edge. Default is 'move'.
+	 */
+	CURSOR_MOVABLE_EDGE: 'move',
+
+	/**
+	 * Variable: CURSOR_LABEL_HANDLE
+	 *
+	 * Defines the cursor for a movable label. Default is 'default'.
+	 */
+	CURSOR_LABEL_HANDLE: 'default',
+
+	/**
+	 * Variable: CURSOR_TERMINAL_HANDLE
+	 *
+	 * Defines the cursor for a terminal handle. Default is 'pointer'.
+	 */
+	CURSOR_TERMINAL_HANDLE: 'pointer',
+
+	/**
+	 * Variable: CURSOR_BEND_HANDLE
+	 *
+	 * Defines the cursor for a movable bend. Default is 'crosshair'.
+	 */
+	CURSOR_BEND_HANDLE: 'crosshair',
+
+	/**
+	 * Variable: CURSOR_VIRTUAL_BEND_HANDLE
+	 *
+	 * Defines the cursor for a movable bend. Default is 'crosshair'.
+	 */
+	CURSOR_VIRTUAL_BEND_HANDLE: 'crosshair',
+
+	/**
+	 * Variable: CURSOR_CONNECT
+	 *
+	 * Defines the cursor for a connectable state. Default is 'pointer'.
+	 */
+	CURSOR_CONNECT: 'pointer',
+
+	/**
+	 * Variable: HIGHLIGHT_COLOR
+	 *
+	 * Defines the color to be used for the cell highlighting.
+	 * Use 'none' for no color. Default is #00FF00.
+	 */
+	HIGHLIGHT_COLOR: '#00FF00',
+
+	/**
+	 * Variable: TARGET_HIGHLIGHT_COLOR
+	 *
+	 * Defines the color to be used for highlighting a target cell for a new
+	 * or changed connection. Note that this may be either a source or
+	 * target terminal in the graph. Use 'none' for no color.
+	 * Default is #0000FF.
+	 */
+	CONNECT_TARGET_COLOR: '#0000FF',
+
+	/**
+	 * Variable: INVALID_CONNECT_TARGET_COLOR
+	 *
+	 * Defines the color to be used for highlighting a invalid target cells
+	 * for a new or changed connections. Note that this may be either a source
+	 * or target terminal in the graph. Use 'none' for no color. Default is
+	 * #FF0000.
+	 */
+	INVALID_CONNECT_TARGET_COLOR: '#FF0000',
+
+	/**
+	 * Variable: DROP_TARGET_COLOR
+	 *
+	 * Defines the color to be used for the highlighting target parent cells
+	 * (for drag and drop). Use 'none' for no color. Default is #0000FF.
+	 */
+	DROP_TARGET_COLOR: '#0000FF',
+
+	/**
+	 * Variable: VALID_COLOR
+	 *
+	 * Defines the color to be used for the coloring valid connection
+	 * previews. Use 'none' for no color. Default is #FF0000.
+	 */
+	VALID_COLOR: '#00FF00',
+
+	/**
+	 * Variable: INVALID_COLOR
+	 *
+	 * Defines the color to be used for the coloring invalid connection
+	 * previews. Use 'none' for no color. Default is #FF0000.
+	 */
+	INVALID_COLOR: '#FF0000',
+
+	/**
+	 * Variable: EDGE_SELECTION_COLOR
+	 *
+	 * Defines the color to be used for the selection border of edges. Use
+	 * 'none' for no color. Default is #00FF00.
+	 */
+	EDGE_SELECTION_COLOR: '#00FF00',
+
+	/**
+	 * Variable: VERTEX_SELECTION_COLOR
+	 *
+	 * Defines the color to be used for the selection border of vertices. Use
+	 * 'none' for no color. Default is #00FF00.
+	 */
+	VERTEX_SELECTION_COLOR: '#00FF00',
+
+	/**
+	 * Variable: VERTEX_SELECTION_STROKEWIDTH
+	 *
+	 * Defines the strokewidth to be used for vertex selections.
+	 * Default is 1.
+	 */
+	VERTEX_SELECTION_STROKEWIDTH: 1,
+
+	/**
+	 * Variable: EDGE_SELECTION_STROKEWIDTH
+	 *
+	 * Defines the strokewidth to be used for edge selections.
+	 * Default is 1.
+	 */
+	EDGE_SELECTION_STROKEWIDTH: 1,
+
+	/**
+	 * Variable: SELECTION_DASHED
+	 *
+	 * Defines the dashed state to be used for the vertex selection
+	 * border. Default is true.
+	 */
+	VERTEX_SELECTION_DASHED: true,
+
+	/**
+	 * Variable: SELECTION_DASHED
+	 *
+	 * Defines the dashed state to be used for the edge selection
+	 * border. Default is true.
+	 */
+	EDGE_SELECTION_DASHED: true,
+
+	/**
+	 * Variable: GUIDE_COLOR
+	 *
+	 * Defines the color to be used for the guidelines in mxGraphHandler.
+	 * Default is #FF0000.
+	 */
+	GUIDE_COLOR: '#FF0000',
+
+	/**
+	 * Variable: GUIDE_STROKEWIDTH
+	 *
+	 * Defines the strokewidth to be used for the guidelines in mxGraphHandler.
+	 * Default is 1.
+	 */
+	GUIDE_STROKEWIDTH: 1,
+
+	/**
+	 * Variable: OUTLINE_COLOR
+	 *
+	 * Defines the color to be used for the outline rectangle
+	 * border.  Use 'none' for no color. Default is #0099FF.
+	 */
+	OUTLINE_COLOR: '#0099FF',
+
+	/**
+	 * Variable: OUTLINE_STROKEWIDTH
+	 *
+	 * Defines the strokewidth to be used for the outline rectangle
+	 * stroke width. Default is 3.
+	 */
+	OUTLINE_STROKEWIDTH: (mxClient.IS_IE) ? 2 : 3,
+
+	/**
+	 * Variable: HANDLE_SIZE
+	 *
+	 * Defines the default size for handles. Default is 6.
+	 */
+	HANDLE_SIZE: 6,
+
+	/**
+	 * Variable: LABEL_HANDLE_SIZE
+	 *
+	 * Defines the default size for label handles. Default is 4.
+	 */
+	LABEL_HANDLE_SIZE: 4,
+
+	/**
+	 * Variable: HANDLE_FILLCOLOR
+	 *
+	 * Defines the color to be used for the handle fill color. Use 'none' for
+	 * no color. Default is #00FF00 (green).
+	 */
+	HANDLE_FILLCOLOR: '#00FF00',
+
+	/**
+	 * Variable: HANDLE_STROKECOLOR
+	 *
+	 * Defines the color to be used for the handle stroke color. Use 'none' for
+	 * no color. Default is black.
+	 */
+	HANDLE_STROKECOLOR: 'black',
+
+	/**
+	 * Variable: LABEL_HANDLE_FILLCOLOR
+	 *
+	 * Defines the color to be used for the label handle fill color. Use 'none'
+	 * for no color. Default is yellow.
+	 */
+	LABEL_HANDLE_FILLCOLOR: 'yellow',
+
+	/**
+	 * Variable: CONNECT_HANDLE_FILLCOLOR
+	 *
+	 * Defines the color to be used for the connect handle fill color. Use
+	 * 'none' for no color. Default is #0000FF (blue).
+	 */
+	CONNECT_HANDLE_FILLCOLOR: '#0000FF',
+
+	/**
+	 * Variable: LOCKED_HANDLE_FILLCOLOR
+	 *
+	 * Defines the color to be used for the locked handle fill color. Use
+	 * 'none' for no color. Default is #FF0000 (red).
+	 */
+	LOCKED_HANDLE_FILLCOLOR: '#FF0000',
+
+	/**
+	 * Variable: OUTLINE_HANDLE_FILLCOLOR
+	 *
+	 * Defines the color to be used for the outline sizer fill color. Use
+	 * 'none' for no color. Default is #00FFFF.
+	 */
+	OUTLINE_HANDLE_FILLCOLOR: '#00FFFF',
+
+	/**
+	 * Variable: OUTLINE_HANDLE_STROKECOLOR
+	 *
+	 * Defines the color to be used for the outline sizer stroke color. Use
+	 * 'none' for no color. Default is #0033FF.
+	 */
+	OUTLINE_HANDLE_STROKECOLOR: '#0033FF',
+
+	/**
+	 * Variable: DEFAULT_FONTFAMILY
+	 *
+	 * Defines the default family for all fonts. Default is Arial,Helvetica.
+	 */
+	DEFAULT_FONTFAMILY: 'Arial,Helvetica',
+
+	/**
+	 * Variable: DEFAULT_FONTSIZE
+	 *
+	 * Defines the default size (in px). Default is 11.
+	 */
+	DEFAULT_FONTSIZE: 11,
+
+	/**
+	 * Variable: DEFAULT_TEXT_DIRECTION
+	 *
+	 * Defines the default value for the  if no value is
+	 * defined for it in the style. Default value is an empty string which means
+	 * the default system setting is used and no direction is set.
+	 */
+	DEFAULT_TEXT_DIRECTION: '',
+
+	/**
+	 * Variable: LINE_HEIGHT
+	 *
+	 * Defines the default line height for text labels. Default is 1.2.
+	 */
+	LINE_HEIGHT: 1.2,
+
+	/**
+	 * Variable: WORD_WRAP
+	 *
+	 * Defines the CSS value for the word-wrap property. Default is "normal".
+	 * Change this to "break-word" to allow long words to be able to be broken
+	 * and wrap onto the next line.
+	 */
+	WORD_WRAP: 'normal',
+
+	/**
+	 * Variable: ABSOLUTE_LINE_HEIGHT
+	 *
+	 * Specifies if absolute line heights should be used (px) in CSS. Default
+	 * is false. Set this to true for backwards compatibility.
+	 */
+	ABSOLUTE_LINE_HEIGHT: false,
+
+	/**
+	 * Variable: DEFAULT_FONTSTYLE
+	 *
+	 * Defines the default style for all fonts. Default is 0. This can be set
+	 * to any combination of font styles as follows.
+	 *
+	 * (code)
+	 * mxConstants.DEFAULT_FONTSTYLE = mxConstants.FONT_BOLD | mxConstants.FONT_ITALIC;
+	 * (end)
+	 */
+	DEFAULT_FONTSTYLE: 0,
+
+	/**
+	 * Variable: DEFAULT_STARTSIZE
+	 *
+	 * Defines the default start size for swimlanes. Default is 40.
+	 */
+	DEFAULT_STARTSIZE: 40,
+
+	/**
+	 * Variable: DEFAULT_MARKERSIZE
+	 *
+	 * Defines the default size for all markers. Default is 6.
+	 */
+	DEFAULT_MARKERSIZE: 6,
+
+	/**
+	 * Variable: DEFAULT_IMAGESIZE
+	 *
+	 * Defines the default width and height for images used in the
+	 * label shape. Default is 24.
+	 */
+	DEFAULT_IMAGESIZE: 24,
+
+	/**
+	 * Variable: ENTITY_SEGMENT
+	 *
+	 * Defines the length of the horizontal segment of an Entity Relation.
+	 * This can be overridden using  style.
+	 * Default is 30.
+	 */
+	ENTITY_SEGMENT: 30,
+
+	/**
+	 * Variable: RECTANGLE_ROUNDING_FACTOR
+	 *
+	 * Defines the rounding factor for rounded rectangles in percent between
+	 * 0 and 1. Values should be smaller than 0.5. Default is 0.15.
+	 */
+	RECTANGLE_ROUNDING_FACTOR: 0.15,
+
+	/**
+	 * Variable: LINE_ARCSIZE
+	 *
+	 * Defines the size of the arcs for rounded edges. Default is 20.
+	 */
+	LINE_ARCSIZE: 20,
+
+	/**
+	 * Variable: ARROW_SPACING
+	 *
+	 * Defines the spacing between the arrow shape and its terminals. Default is 0.
+	 */
+	ARROW_SPACING: 0,
+
+	/**
+	 * Variable: ARROW_WIDTH
+	 *
+	 * Defines the width of the arrow shape. Default is 30.
+	 */
+	ARROW_WIDTH: 30,
+
+	/**
+	 * Variable: ARROW_SIZE
+	 *
+	 * Defines the size of the arrowhead in the arrow shape. Default is 30.
+	 */
+	ARROW_SIZE: 30,
+
+	/**
+	 * Variable: PAGE_FORMAT_A4_PORTRAIT
+	 *
+	 * Defines the rectangle for the A4 portrait page format. The dimensions
+	 * of this page format are 826x1169 pixels.
+	 */
+	PAGE_FORMAT_A4_PORTRAIT: new mxRectangle(0, 0, 827, 1169),
+
+	/**
+	 * Variable: PAGE_FORMAT_A4_PORTRAIT
+	 *
+	 * Defines the rectangle for the A4 portrait page format. The dimensions
+	 * of this page format are 826x1169 pixels.
+	 */
+	PAGE_FORMAT_A4_LANDSCAPE: new mxRectangle(0, 0, 1169, 827),
+
+	/**
+	 * Variable: PAGE_FORMAT_LETTER_PORTRAIT
+	 *
+	 * Defines the rectangle for the Letter portrait page format. The
+	 * dimensions of this page format are 850x1100 pixels.
+	 */
+	PAGE_FORMAT_LETTER_PORTRAIT: new mxRectangle(0, 0, 850, 1100),
+
+	/**
+	 * Variable: PAGE_FORMAT_LETTER_PORTRAIT
+	 *
+	 * Defines the rectangle for the Letter portrait page format. The dimensions
+	 * of this page format are 850x1100 pixels.
+	 */
+	PAGE_FORMAT_LETTER_LANDSCAPE: new mxRectangle(0, 0, 1100, 850),
+
+	/**
+	 * Variable: NONE
+	 *
+	 * Defines the value for none. Default is "none".
+	 */
+	NONE: 'none',
+
+	/**
+	 * Variable: STYLE_PERIMETER
+	 *
+	 * Defines the key for the perimeter style. This is a function that defines
+	 * the perimeter around a particular shape. Possible values are the
+	 * functions defined in . Alternatively, the constants in this
+	 * class that start with "PERIMETER_" may be used to access
+	 * perimeter styles in . Value is "perimeter".
+	 */
+	STYLE_PERIMETER: 'perimeter',
+
+	/**
+	 * Variable: STYLE_SOURCE_PORT
+	 *
+	 * Defines the ID of the cell that should be used for computing the
+	 * perimeter point of the source for an edge. This allows for graphically
+	 * connecting to a cell while keeping the actual terminal of the edge.
+	 * Value is "sourcePort".
+	 */
+	STYLE_SOURCE_PORT: 'sourcePort',
+
+	/**
+	 * Variable: STYLE_TARGET_PORT
+	 *
+	 * Defines the ID of the cell that should be used for computing the
+	 * perimeter point of the target for an edge. This allows for graphically
+	 * connecting to a cell while keeping the actual terminal of the edge.
+	 * Value is "targetPort".
+	 */
+	STYLE_TARGET_PORT: 'targetPort',
+
+	/**
+	 * Variable: STYLE_PORT_CONSTRAINT
+	 *
+	 * Defines the direction(s) that edges are allowed to connect to cells in.
+	 * Possible values are "DIRECTION_NORTH, DIRECTION_SOUTH,
+	 * DIRECTION_EAST" and "DIRECTION_WEST". Value is
+	 * "portConstraint".
+	 */
+	STYLE_PORT_CONSTRAINT: 'portConstraint',
+
+	/**
+	 * Variable: STYLE_PORT_CONSTRAINT_ROTATION
+	 *
+	 * Define whether port constraint directions are rotated with vertex
+	 * rotation. 0 (default) causes port constraints to remain absolute,
+	 * relative to the graph, 1 causes the constraints to rotate with
+	 * the vertex. Value is "portConstraintRotation".
+	 */
+	STYLE_PORT_CONSTRAINT_ROTATION: 'portConstraintRotation',
+
+	/**
+	 * Variable: STYLE_SOURCE_PORT_CONSTRAINT
+	 *
+	 * Defines the direction(s) that edges are allowed to connect to sources in.
+	 * Possible values are "DIRECTION_NORTH, DIRECTION_SOUTH, DIRECTION_EAST"
+	 * and "DIRECTION_WEST". Value is "sourcePortConstraint".
+	 */
+	STYLE_SOURCE_PORT_CONSTRAINT: 'sourcePortConstraint',
+
+	/**
+	 * Variable: STYLE_TARGET_PORT_CONSTRAINT
+	 *
+	 * Defines the direction(s) that edges are allowed to connect to targets in.
+	 * Possible values are "DIRECTION_NORTH, DIRECTION_SOUTH, DIRECTION_EAST"
+	 * and "DIRECTION_WEST". Value is "targetPortConstraint".
+	 */
+	STYLE_TARGET_PORT_CONSTRAINT: 'targetPortConstraint',
+
+	/**
+	 * Variable: STYLE_OPACITY
+	 *
+	 * Defines the key for the opacity style. The type of the value is
+	 * numeric and the possible range is 0-100. Value is "opacity".
+	 */
+	STYLE_OPACITY: 'opacity',
+
+	/**
+	 * Variable: STYLE_FILL_OPACITY
+	 *
+	 * Defines the key for the fill opacity style. The type of the value is
+	 * numeric and the possible range is 0-100. Value is "fillOpacity".
+	 */
+	STYLE_FILL_OPACITY: 'fillOpacity',
+
+	/**
+	 * Variable: STYLE_STROKE_OPACITY
+	 *
+	 * Defines the key for the stroke opacity style. The type of the value is
+	 * numeric and the possible range is 0-100. Value is "strokeOpacity".
+	 */
+	STYLE_STROKE_OPACITY: 'strokeOpacity',
+
+	/**
+	 * Variable: STYLE_TEXT_OPACITY
+	 *
+	 * Defines the key for the text opacity style. The type of the value is
+	 * numeric and the possible range is 0-100. Value is "textOpacity".
+	 */
+	STYLE_TEXT_OPACITY: 'textOpacity',
+
+	/**
+	 * Variable: STYLE_TEXT_DIRECTION
+	 *
+	 * Defines the key for the text direction style. Possible values are
+	 * "TEXT_DIRECTION_DEFAULT, TEXT_DIRECTION_AUTO, TEXT_DIRECTION_LTR"
+	 * and "TEXT_DIRECTION_RTL". Value is "textDirection".
+	 * The default value for the style is defined in .
+	 * It is used is no value is defined for this key in a given style. This is
+	 * an experimental style that is currently ignored in the backends.
+	 */
+	STYLE_TEXT_DIRECTION: 'textDirection',
+
+	/**
+	 * Variable: STYLE_OVERFLOW
+	 *
+	 * Defines the key for the overflow style. Possible values are 'visible',
+	 * 'hidden', 'fill' and 'width'. The default value is 'visible'. This value
+	 * specifies how overlapping vertex labels are handled. A value of
+	 * 'visible' will show the complete label. A value of 'hidden' will clip
+	 * the label so that it does not overlap the vertex bounds. A value of
+	 * 'fill' will use the vertex bounds and a value of 'width' will use the
+	 * the vertex width for the label. See . Note that
+	 * the vertical alignment is ignored for overflow fill and for horizontal
+	 * alignment, left should be used to avoid pixel offsets in Internet Explorer
+	 * 11 and earlier or if foreignObjects are disabled. Value is "overflow".
+	 */
+	STYLE_OVERFLOW: 'overflow',
+
+	/**
+	 * Variable: STYLE_ORTHOGONAL
+	 *
+	 * Defines if the connection points on either end of the edge should be
+	 * computed so that the edge is vertical or horizontal if possible and
+	 * if the point is not at a fixed location. Default is false. This is
+	 * used in , which also returns true if the edgeStyle
+	 * of the edge is an elbow or entity. Value is "orthogonal".
+	 */
+	STYLE_ORTHOGONAL: 'orthogonal',
+
+	/**
+	 * Variable: STYLE_EXIT_X
+	 *
+	 * Defines the key for the horizontal relative coordinate connection point
+	 * of an edge with its source terminal. Value is "exitX".
+	 */
+	STYLE_EXIT_X: 'exitX',
+
+	/**
+	 * Variable: STYLE_EXIT_Y
+	 *
+	 * Defines the key for the vertical relative coordinate connection point
+	 * of an edge with its source terminal. Value is "exitY".
+	 */
+	STYLE_EXIT_Y: 'exitY',
+
+	/**
+	 * Variable: STYLE_EXIT_PERIMETER
+	 *
+	 * Defines if the perimeter should be used to find the exact entry point
+	 * along the perimeter of the source. Possible values are 0 (false) and
+	 * 1 (true). Default is 1 (true). Value is "exitPerimeter".
+	 */
+	STYLE_EXIT_PERIMETER: 'exitPerimeter',
+
+	/**
+	 * Variable: STYLE_ENTRY_X
+	 *
+	 * Defines the key for the horizontal relative coordinate connection point
+	 * of an edge with its target terminal. Value is "entryX".
+	 */
+	STYLE_ENTRY_X: 'entryX',
+
+	/**
+	 * Variable: STYLE_ENTRY_Y
+	 *
+	 * Defines the key for the vertical relative coordinate connection point
+	 * of an edge with its target terminal. Value is "entryY".
+	 */
+	STYLE_ENTRY_Y: 'entryY',
+
+	/**
+	 * Variable: STYLE_ENTRY_PERIMETER
+	 *
+	 * Defines if the perimeter should be used to find the exact entry point
+	 * along the perimeter of the target. Possible values are 0 (false) and
+	 * 1 (true). Default is 1 (true). Value is "entryPerimeter".
+	 */
+	STYLE_ENTRY_PERIMETER: 'entryPerimeter',
+
+	/**
+	 * Variable: STYLE_WHITE_SPACE
+	 *
+	 * Defines the key for the white-space style. Possible values are 'nowrap'
+	 * and 'wrap'. The default value is 'nowrap'. This value specifies how
+	 * white-space inside a HTML vertex label should be handled. A value of
+	 * 'nowrap' means the text will never wrap to the next line until a
+	 * linefeed is encountered. A value of 'wrap' means text will wrap when
+	 * necessary. This style is only used for HTML labels.
+	 * See . Value is "whiteSpace".
+	 */
+	STYLE_WHITE_SPACE: 'whiteSpace',
+
+	/**
+	 * Variable: STYLE_ROTATION
+	 *
+	 * Defines the key for the rotation style. The type of the value is
+	 * numeric and the possible range is 0-360. Value is "rotation".
+	 */
+	STYLE_ROTATION: 'rotation',
+
+	/**
+	 * Variable: STYLE_FILLCOLOR
+	 *
+	 * Defines the key for the fill color. Possible values are all HTML color
+	 * names or HEX codes, as well as special keywords such as 'swimlane,
+	 * 'inherit' or 'indicated' to use the color code of a related cell or the
+	 * indicator shape. Value is "fillColor".
+	 */
+	STYLE_FILLCOLOR: 'fillColor',
+
+	/**
+	 * Variable: STYLE_POINTER_EVENTS
+	 *
+	 * Specifies if pointer events should be fired on transparent backgrounds.
+	 * This style is currently only supported in . Default
+	 * is true. Value is "pointerEvents". This is typically set to
+	 * false in groups where the transparent part should allow any underlying
+	 * cells to be clickable.
+	 */
+	STYLE_POINTER_EVENTS: 'pointerEvents',
+
+	/**
+	 * Variable: STYLE_SWIMLANE_FILLCOLOR
+	 *
+	 * Defines the key for the fill color of the swimlane background. Possible
+	 * values are all HTML color names or HEX codes. Default is no background.
+	 * Value is "swimlaneFillColor".
+	 */
+	STYLE_SWIMLANE_FILLCOLOR: 'swimlaneFillColor',
+
+	/**
+	 * Variable: STYLE_MARGIN
+	 *
+	 * Defines the key for the margin between the ellipses in the double ellipse shape.
+	 * Possible values are all positive numbers. Value is "margin".
+	 */
+	STYLE_MARGIN: 'margin',
+
+	/**
+	 * Variable: STYLE_GRADIENTCOLOR
+	 *
+	 * Defines the key for the gradient color. Possible values are all HTML color
+	 * names or HEX codes, as well as special keywords such as 'swimlane,
+	 * 'inherit' or 'indicated' to use the color code of a related cell or the
+	 * indicator shape. This is ignored if no fill color is defined. Value is
+	 * "gradientColor".
+	 */
+	STYLE_GRADIENTCOLOR: 'gradientColor',
+
+	/**
+	 * Variable: STYLE_GRADIENT_DIRECTION
+	 *
+	 * Defines the key for the gradient direction. Possible values are
+	 * , ,  and
+	 * . Default is . Generally, and by
+	 * default in mxGraph, gradient painting is done from the value of
+	 *  to the value of . Taking the
+	 * example of , this means  color at the
+	 * bottom of paint pattern and  at top, with a
+	 * gradient in-between. Value is "gradientDirection".
+	 */
+	STYLE_GRADIENT_DIRECTION: 'gradientDirection',
+
+	/**
+	 * Variable: STYLE_STROKECOLOR
+	 *
+	 * Defines the key for the strokeColor style. Possible values are all HTML
+	 * color names or HEX codes, as well as special keywords such as 'swimlane,
+	 * 'inherit', 'indicated' to use the color code of a related cell or the
+	 * indicator shape or 'none' for no color. Value is "strokeColor".
+	 */
+	STYLE_STROKECOLOR: 'strokeColor',
+
+	/**
+	 * Variable: STYLE_SEPARATORCOLOR
+	 *
+	 * Defines the key for the separatorColor style. Possible values are all
+	 * HTML color names or HEX codes. This style is only used for
+	 *  shapes. Value is "separatorColor".
+	 */
+	STYLE_SEPARATORCOLOR: 'separatorColor',
+
+	/**
+	 * Variable: STYLE_STROKEWIDTH
+	 *
+	 * Defines the key for the strokeWidth style. The type of the value is
+	 * numeric and the possible range is any non-negative value larger or equal
+	 * to 1. The value defines the stroke width in pixels. Note: To hide a
+	 * stroke use strokeColor none. Value is "strokeWidth".
+	 */
+	STYLE_STROKEWIDTH: 'strokeWidth',
+
+	/**
+	 * Variable: STYLE_ALIGN
+	 *
+	 * Defines the key for the align style. Possible values are ,
+	 *  and . This value defines how the lines of
+	 * the label are horizontally aligned.  mean label text lines
+	 * are aligned to left of the label bounds,  to the right of
+	 * the label bounds and  means the center of the text lines
+	 * are aligned in the center of the label bounds. Note this value doesn't
+	 * affect the positioning of the overall label bounds relative to the
+	 * vertex, to move the label bounds horizontally, use
+	 * . Value is "align".
+	 */
+	STYLE_ALIGN: 'align',
+
+	/**
+	 * Variable: STYLE_VERTICAL_ALIGN
+	 *
+	 * Defines the key for the verticalAlign style. Possible values are
+	 * ,  and . This value defines how
+	 * the lines of the label are vertically aligned.  means the
+	 * topmost label text line is aligned against the top of the label bounds,
+	 *  means the bottom-most label text line is aligned against
+	 * the bottom of the label bounds and  means there is equal
+	 * spacing between the topmost text label line and the top of the label
+	 * bounds and the bottom-most text label line and the bottom of the label
+	 * bounds. Note this value doesn't affect the positioning of the overall
+	 * label bounds relative to the vertex, to move the label bounds
+	 * vertically, use . Value is "verticalAlign".
+	 */
+	STYLE_VERTICAL_ALIGN: 'verticalAlign',
+
+	/**
+	 * Variable: STYLE_LABEL_WIDTH
+	 *
+	 * Defines the key for the width of the label if the label position is not
+	 * center. Value is "labelWidth".
+	 */
+	STYLE_LABEL_WIDTH: 'labelWidth',
+
+	/**
+	 * Variable: STYLE_LABEL_POSITION
+	 *
+	 * Defines the key for the horizontal label position of vertices. Possible
+	 * values are ,  and . Default is
+	 * . The label align defines the position of the label
+	 * relative to the cell.  means the entire label bounds is
+	 * placed completely just to the left of the vertex,  means
+	 * adjust to the right and  means the label bounds are
+	 * vertically aligned with the bounds of the vertex. Note this value
+	 * doesn't affect the positioning of label within the label bounds, to move
+	 * the label horizontally within the label bounds, use .
+	 * Value is "labelPosition".
+	 */
+	STYLE_LABEL_POSITION: 'labelPosition',
+
+	/**
+	 * Variable: STYLE_VERTICAL_LABEL_POSITION
+	 *
+	 * Defines the key for the vertical label position of vertices. Possible
+	 * values are ,  and . Default is
+	 * . The label align defines the position of the label
+	 * relative to the cell.  means the entire label bounds is
+	 * placed completely just on the top of the vertex,  means
+	 * adjust on the bottom and  means the label bounds are
+	 * horizontally aligned with the bounds of the vertex. Note this value
+	 * doesn't affect the positioning of label within the label bounds, to move
+	 * the label vertically within the label bounds, use
+	 * . Value is "verticalLabelPosition".
+	 */
+	STYLE_VERTICAL_LABEL_POSITION: 'verticalLabelPosition',
+
+	/**
+	 * Variable: STYLE_IMAGE_ASPECT
+	 *
+	 * Defines the key for the image aspect style. Possible values are 0 (do
+	 * not preserve aspect) or 1 (keep aspect). This is only used in
+	 * . Default is 1. Value is "imageAspect".
+	 */
+	STYLE_IMAGE_ASPECT: 'imageAspect',
+
+	/**
+	 * Variable: STYLE_IMAGE_ALIGN
+	 *
+	 * Defines the key for the align style. Possible values are ,
+	 *  and . The value defines how any image in the
+	 * vertex label is aligned horizontally within the label bounds of a
+	 *  shape. Value is "imageAlign".
+	 */
+	STYLE_IMAGE_ALIGN: 'imageAlign',
+
+	/**
+	 * Variable: STYLE_IMAGE_VERTICAL_ALIGN
+	 *
+	 * Defines the key for the verticalAlign style. Possible values are
+	 * ,  and . The value defines how
+	 * any image in the vertex label is aligned vertically within the label
+	 * bounds of a  shape. Value is "imageVerticalAlign".
+	 */
+	STYLE_IMAGE_VERTICAL_ALIGN: 'imageVerticalAlign',
+
+	/**
+	 * Variable: STYLE_GLASS
+	 *
+	 * Defines the key for the glass style. Possible values are 0 (disabled) and
+	 * 1(enabled). The default value is 0. This is used in . Value is
+	 * "glass".
+	 */
+	STYLE_GLASS: 'glass',
+
+	/**
+	 * Variable: STYLE_IMAGE
+	 *
+	 * Defines the key for the image style. Possible values are any image URL,
+	 * the type of the value is String. This is the path to the image that is
+	 * to be displayed within the label of a vertex. Data URLs should use the
+	 * following format: data:image/png,xyz where xyz is the base64 encoded
+	 * data (without the "base64"-prefix). Note that Data URLs are only
+	 * supported in modern browsers. Value is "image".
+	 */
+	STYLE_IMAGE: 'image',
+
+	/**
+	 * Variable: STYLE_IMAGE_WIDTH
+	 *
+	 * Defines the key for the imageWidth style. The type of this value is
+	 * int, the value is the image width in pixels and must be greater than 0.
+	 * Value is "imageWidth".
+	 */
+	STYLE_IMAGE_WIDTH: 'imageWidth',
+
+	/**
+	 * Variable: STYLE_IMAGE_HEIGHT
+	 *
+	 * Defines the key for the imageHeight style. The type of this value is
+	 * int, the value is the image height in pixels and must be greater than 0.
+	 * Value is "imageHeight".
+	 */
+	STYLE_IMAGE_HEIGHT: 'imageHeight',
+
+	/**
+	 * Variable: STYLE_IMAGE_BACKGROUND
+	 *
+	 * Defines the key for the image background color. This style is only used
+	 * in . Possible values are all HTML color names or HEX
+	 * codes. Value is "imageBackground".
+	 */
+	STYLE_IMAGE_BACKGROUND: 'imageBackground',
+
+	/**
+	 * Variable: STYLE_IMAGE_BORDER
+	 *
+	 * Defines the key for the image border color. This style is only used in
+	 * . Possible values are all HTML color names or HEX codes.
+	 * Value is "imageBorder".
+	 */
+	STYLE_IMAGE_BORDER: 'imageBorder',
+
+	/**
+	 * Variable: STYLE_FLIPH
+	 *
+	 * Defines the key for the horizontal image flip. This style is only used
+	 * in . Possible values are 0 and 1. Default is 0. Value is
+	 * "flipH".
+	 */
+	STYLE_FLIPH: 'flipH',
+
+	/**
+	 * Variable: STYLE_FLIPV
+	 *
+	 * Defines the key for the vertical flip. Possible values are 0 and 1.
+	 * Default is 0. Value is "flipV".
+	 */
+	STYLE_FLIPV: 'flipV',
+
+	/**
+	 * Variable: STYLE_NOLABEL
+	 *
+	 * Defines the key for the noLabel style. If this is true then no label is
+	 * visible for a given cell. Possible values are true or false (1 or 0).
+	 * Default is false. Value is "noLabel".
+	 */
+	STYLE_NOLABEL: 'noLabel',
+
+	/**
+	 * Variable: STYLE_NOEDGESTYLE
+	 *
+	 * Defines the key for the noEdgeStyle style. If this is true then no edge
+	 * style is applied for a given edge. Possible values are true or false
+	 * (1 or 0). Default is false. Value is "noEdgeStyle".
+	 */
+	STYLE_NOEDGESTYLE: 'noEdgeStyle',
+
+	/**
+	 * Variable: STYLE_LABEL_BACKGROUNDCOLOR
+	 *
+	 * Defines the key for the label background color. Possible values are all
+	 * HTML color names or HEX codes. Value is "labelBackgroundColor".
+	 */
+	STYLE_LABEL_BACKGROUNDCOLOR: 'labelBackgroundColor',
+
+	/**
+	 * Variable: STYLE_LABEL_BORDERCOLOR
+	 *
+	 * Defines the key for the label border color. Possible values are all
+	 * HTML color names or HEX codes. Value is "labelBorderColor".
+	 */
+	STYLE_LABEL_BORDERCOLOR: 'labelBorderColor',
+
+	/**
+	 * Variable: STYLE_LABEL_PADDING
+	 *
+	 * Defines the key for the label padding, ie. the space between the label
+	 * border and the label. Value is "labelPadding".
+	 */
+	STYLE_LABEL_PADDING: 'labelPadding',
+
+	/**
+	 * Variable: STYLE_INDICATOR_SHAPE
+	 *
+	 * Defines the key for the indicator shape used within an .
+	 * Possible values are all SHAPE_* constants or the names of any new
+	 * shapes. The indicatorShape has precedence over the indicatorImage.
+	 * Value is "indicatorShape".
+	 */
+	STYLE_INDICATOR_SHAPE: 'indicatorShape',
+
+	/**
+	 * Variable: STYLE_INDICATOR_IMAGE
+	 *
+	 * Defines the key for the indicator image used within an .
+	 * Possible values are all image URLs. The indicatorShape has
+	 * precedence over the indicatorImage. Value is "indicatorImage".
+	 */
+	STYLE_INDICATOR_IMAGE: 'indicatorImage',
+
+	/**
+	 * Variable: STYLE_INDICATOR_COLOR
+	 *
+	 * Defines the key for the indicatorColor style. Possible values are all
+	 * HTML color names or HEX codes, as well as the special 'swimlane' keyword
+	 * to refer to the color of the parent swimlane if one exists. Value is
+	 * "indicatorColor".
+	 */
+	STYLE_INDICATOR_COLOR: 'indicatorColor',
+
+	/**
+	 * Variable: STYLE_INDICATOR_STROKECOLOR
+	 *
+	 * Defines the key for the indicator stroke color in .
+	 * Possible values are all color codes. Value is "indicatorStrokeColor".
+	 */
+	STYLE_INDICATOR_STROKECOLOR: 'indicatorStrokeColor',
+
+	/**
+	 * Variable: STYLE_INDICATOR_GRADIENTCOLOR
+	 *
+	 * Defines the key for the indicatorGradientColor style. Possible values
+	 * are all HTML color names or HEX codes. This style is only supported in
+	 *  shapes. Value is "indicatorGradientColor".
+	 */
+	STYLE_INDICATOR_GRADIENTCOLOR: 'indicatorGradientColor',
+
+	/**
+	 * Variable: STYLE_INDICATOR_SPACING
+	 *
+	 * The defines the key for the spacing between the label and the
+	 * indicator in . Possible values are in pixels. Value is
+	 * "indicatorSpacing".
+	 */
+	STYLE_INDICATOR_SPACING: 'indicatorSpacing',
+
+	/**
+	 * Variable: STYLE_INDICATOR_WIDTH
+	 *
+	 * Defines the key for the indicator width. Possible values start at 0 (in
+	 * pixels). Value is "indicatorWidth".
+	 */
+	STYLE_INDICATOR_WIDTH: 'indicatorWidth',
+
+	/**
+	 * Variable: STYLE_INDICATOR_HEIGHT
+	 *
+	 * Defines the key for the indicator height. Possible values start at 0 (in
+	 * pixels). Value is "indicatorHeight".
+	 */
+	STYLE_INDICATOR_HEIGHT: 'indicatorHeight',
+
+	/**
+	 * Variable: STYLE_INDICATOR_DIRECTION
+	 *
+	 * Defines the key for the indicatorDirection style. The direction style is
+	 * used to specify the direction of certain shapes (eg. ).
+	 * Possible values are  (default), ,
+	 *  and . Value is "indicatorDirection".
+	 */
+	STYLE_INDICATOR_DIRECTION: 'indicatorDirection',
+
+	/**
+	 * Variable: STYLE_SHADOW
+	 *
+	 * Defines the key for the shadow style. The type of the value is Boolean.
+	 * Value is "shadow".
+	 */
+	STYLE_SHADOW: 'shadow',
+
+	/**
+	 * Variable: STYLE_SEGMENT
+	 *
+	 * Defines the key for the segment style. The type of this value is float
+	 * and the value represents the size of the horizontal segment of the
+	 * entity relation style. Default is ENTITY_SEGMENT. Value is "segment".
+	 */
+	STYLE_SEGMENT: 'segment',
+
+	/**
+	 * Variable: STYLE_ENDARROW
+	 *
+	 * Defines the key for the end arrow marker. Possible values are all
+	 * constants with an ARROW-prefix. This is only used in .
+	 * Value is "endArrow".
+	 *
+	 * Example:
+	 * (code)
+	 * style[mxConstants.STYLE_ENDARROW] = mxConstants.ARROW_CLASSIC;
+	 * (end)
+	 */
+	STYLE_ENDARROW: 'endArrow',
+
+	/**
+	 * Variable: STYLE_STARTARROW
+	 *
+	 * Defines the key for the start arrow marker. Possible values are all
+	 * constants with an ARROW-prefix. This is only used in .
+	 * See . Value is "startArrow".
+	 */
+	STYLE_STARTARROW: 'startArrow',
+
+	/**
+	 * Variable: STYLE_ENDSIZE
+	 *
+	 * Defines the key for the endSize style. The type of this value is numeric
+	 * and the value represents the size of the end marker in pixels. Value is
+	 * "endSize".
+	 */
+	STYLE_ENDSIZE: 'endSize',
+
+	/**
+	 * Variable: STYLE_STARTSIZE
+	 *
+	 * Defines the key for the startSize style. The type of this value is
+	 * numeric and the value represents the size of the start marker or the
+	 * size of the swimlane title region depending on the shape it is used for.
+	 * Value is "startSize".
+	 */
+	STYLE_STARTSIZE: 'startSize',
+
+	/**
+	 * Variable: STYLE_SWIMLANE_LINE
+	 *
+	 * Defines the key for the swimlaneLine style. This style specifies whether
+	 * the line between the title regio of a swimlane should be visible. Use 0
+	 * for hidden or 1 (default) for visible. Value is "swimlaneLine".
+	 */
+	STYLE_SWIMLANE_LINE: 'swimlaneLine',
+
+	/**
+	 * Variable: STYLE_ENDFILL
+	 *
+	 * Defines the key for the endFill style. Use 0 for no fill or 1 (default)
+	 * for fill. (This style is only exported via .) Value is
+	 * "endFill".
+	 */
+	STYLE_ENDFILL: 'endFill',
+
+	/**
+	 * Variable: STYLE_STARTFILL
+	 *
+	 * Defines the key for the startFill style. Use 0 for no fill or 1 (default)
+	 * for fill. (This style is only exported via .) Value is
+	 * "startFill".
+	 */
+	STYLE_STARTFILL: 'startFill',
+
+	/**
+	 * Variable: STYLE_DASHED
+	 *
+	 * Defines the key for the dashed style. Use 0 (default) for non-dashed or 1
+	 * for dashed. Value is "dashed".
+	 */
+	STYLE_DASHED: 'dashed',
+
+	/**
+	 * Defines the key for the dashed pattern style in SVG and image exports.
+	 * The type of this value is a space separated list of numbers that specify
+	 * a custom-defined dash pattern. Dash styles are defined in terms of the
+	 * length of the dash (the drawn part of the stroke) and the length of the
+	 * space between the dashes. The lengths are relative to the line width: a
+	 * length of "1" is equal to the line width. VML ignores this style and
+	 * uses dashStyle instead as defined in the VML specification. This style
+	 * is only used in the  shape. Value is "dashPattern".
+	 */
+	STYLE_DASH_PATTERN: 'dashPattern',
+
+	/**
+	 * Variable: STYLE_FIX_DASH
+	 *
+	 * Defines the key for the fixDash style. Use 0 (default) for dash patterns
+	 * that depend on the linewidth and 1 for dash patterns that ignore the
+	 * line width. Value is "fixDash".
+	 */
+	STYLE_FIX_DASH: 'fixDash',
+
+	/**
+	 * Variable: STYLE_ROUNDED
+	 *
+	 * Defines the key for the rounded style. The type of this value is
+	 * Boolean. For edges this determines whether or not joins between edges
+	 * segments are smoothed to a rounded finish. For vertices that have the
+	 * rectangle shape, this determines whether or not the rectangle is
+	 * rounded. Use 0 (default) for non-rounded or 1 for rounded. Value is
+	 * "rounded".
+	 */
+	STYLE_ROUNDED: 'rounded',
+
+	/**
+	 * Variable: STYLE_CURVED
+	 *
+	 * Defines the key for the curved style. The type of this value is
+	 * Boolean. It is only applicable for connector shapes. Use 0 (default)
+	 * for non-curved or 1 for curved. Value is "curved".
+	 */
+	STYLE_CURVED: 'curved',
+
+	/**
+	 * Variable: STYLE_ARCSIZE
+	 *
+	 * Defines the rounding factor for a rounded rectangle in percent (without
+	 * the percent sign). Possible values are between 0 and 100. If this value
+	 * is not specified then RECTANGLE_ROUNDING_FACTOR * 100 is used. For
+	 * edges, this defines the absolute size of rounded corners in pixels. If
+	 * this values is not specified then LINE_ARCSIZE is used.
+	 * (This style is only exported via .) Value is "arcSize".
+	 */
+	STYLE_ARCSIZE: 'arcSize',
+
+	/**
+	 * Variable: STYLE_ABSOLUTE_ARCSIZE
+	 *
+	 * Defines the key for the absolute arc size style. This specifies if
+	 * arcSize for rectangles is abolute or relative. Possible values are 1
+	 * and 0 (default). Value is "absoluteArcSize".
+	 */
+	STYLE_ABSOLUTE_ARCSIZE: 'absoluteArcSize',
+
+	/**
+	 * Variable: STYLE_SOURCE_PERIMETER_SPACING
+	 *
+	 * Defines the key for the source perimeter spacing. The type of this value
+	 * is numeric. This is the distance between the source connection point of
+	 * an edge and the perimeter of the source vertex in pixels. This style
+	 * only applies to edges. Value is "sourcePerimeterSpacing".
+	 */
+	STYLE_SOURCE_PERIMETER_SPACING: 'sourcePerimeterSpacing',
+
+	/**
+	 * Variable: STYLE_TARGET_PERIMETER_SPACING
+	 *
+	 * Defines the key for the target perimeter spacing. The type of this value
+	 * is numeric. This is the distance between the target connection point of
+	 * an edge and the perimeter of the target vertex in pixels. This style
+	 * only applies to edges. Value is "targetPerimeterSpacing".
+	 */
+	STYLE_TARGET_PERIMETER_SPACING: 'targetPerimeterSpacing',
+
+	/**
+	 * Variable: STYLE_PERIMETER_SPACING
+	 *
+	 * Defines the key for the perimeter spacing. This is the distance between
+	 * the connection point and the perimeter in pixels. When used in a vertex
+	 * style, this applies to all incoming edges to floating ports (edges that
+	 * terminate on the perimeter of the vertex). When used in an edge style,
+	 * this spacing applies to the source and target separately, if they
+	 * terminate in floating ports (on the perimeter of the vertex). Value is
+	 * "perimeterSpacing".
+	 */
+	STYLE_PERIMETER_SPACING: 'perimeterSpacing',
+
+	/**
+	 * Variable: STYLE_SPACING
+	 *
+	 * Defines the key for the spacing. The value represents the spacing, in
+	 * pixels, added to each side of a label in a vertex (style applies to
+	 * vertices only). Value is "spacing".
+	 */
+	STYLE_SPACING: 'spacing',
+
+	/**
+	 * Variable: STYLE_SPACING_TOP
+	 *
+	 * Defines the key for the spacingTop style. The value represents the
+	 * spacing, in pixels, added to the top side of a label in a vertex (style
+	 * applies to vertices only). Value is "spacingTop".
+	 */
+	STYLE_SPACING_TOP: 'spacingTop',
+
+	/**
+	 * Variable: STYLE_SPACING_LEFT
+	 *
+	 * Defines the key for the spacingLeft style. The value represents the
+	 * spacing, in pixels, added to the left side of a label in a vertex (style
+	 * applies to vertices only). Value is "spacingLeft".
+	 */
+	STYLE_SPACING_LEFT: 'spacingLeft',
+
+	/**
+	 * Variable: STYLE_SPACING_BOTTOM
+	 *
+	 * Defines the key for the spacingBottom style The value represents the
+	 * spacing, in pixels, added to the bottom side of a label in a vertex
+	 * (style applies to vertices only). Value is "spacingBottom".
+	 */
+	STYLE_SPACING_BOTTOM: 'spacingBottom',
+
+	/**
+	 * Variable: STYLE_SPACING_RIGHT
+	 *
+	 * Defines the key for the spacingRight style The value represents the
+	 * spacing, in pixels, added to the right side of a label in a vertex (style
+	 * applies to vertices only). Value is "spacingRight".
+	 */
+	STYLE_SPACING_RIGHT: 'spacingRight',
+
+	/**
+	 * Variable: STYLE_HORIZONTAL
+	 *
+	 * Defines the key for the horizontal style. Possible values are
+	 * true or false. This value only applies to vertices. If the 
+	 * is "SHAPE_SWIMLANE" a value of false indicates that the
+	 * swimlane should be drawn vertically, true indicates to draw it
+	 * horizontally. If the shape style does not indicate that this vertex is a
+	 * swimlane, this value affects only whether the label is drawn
+	 * horizontally or vertically. Value is "horizontal".
+	 */
+	STYLE_HORIZONTAL: 'horizontal',
+
+	/**
+	 * Variable: STYLE_DIRECTION
+	 *
+	 * Defines the key for the direction style. The direction style is used
+	 * to specify the direction of certain shapes (eg. ).
+	 * Possible values are  (default), ,
+	 *  and . Value is "direction".
+	 */
+	STYLE_DIRECTION: 'direction',
+
+	/**
+	 * Variable: STYLE_ANCHOR_POINT_DIRECTION
+	 *
+	 * Defines the key for the anchorPointDirection style. The defines if the
+	 * direction style should be taken into account when computing the fixed
+	 * point location for connected edges. Default is 1 (yes). Set this to 0
+	 * to ignore the direction style for fixed connection points. Value is
+	 * "anchorPointDirection".
+	 */
+	STYLE_ANCHOR_POINT_DIRECTION: 'anchorPointDirection',
+
+	/**
+	 * Variable: STYLE_ELBOW
+	 *
+	 * Defines the key for the elbow style. Possible values are
+	 *  and . Default is .
+	 * This defines how the three segment orthogonal edge style leaves its
+	 * terminal vertices. The vertical style leaves the terminal vertices at
+	 * the top and bottom sides. Value is "elbow".
+	 */
+	STYLE_ELBOW: 'elbow',
+
+	/**
+	 * Variable: STYLE_FONTCOLOR
+	 *
+	 * Defines the key for the fontColor style. Possible values are all HTML
+	 * color names or HEX codes. Value is "fontColor".
+	 */
+	STYLE_FONTCOLOR: 'fontColor',
+
+	/**
+	 * Variable: STYLE_FONTFAMILY
+	 *
+	 * Defines the key for the fontFamily style. Possible values are names such
+	 * as Arial; Dialog; Verdana; Times New Roman. The value is of type String.
+	 * Value is fontFamily.
+	 */
+	STYLE_FONTFAMILY: 'fontFamily',
+
+	/**
+	 * Variable: STYLE_FONTSIZE
+	 *
+	 * Defines the key for the fontSize style (in px). The type of the value
+	 * is int. Value is "fontSize".
+	 */
+	STYLE_FONTSIZE: 'fontSize',
+
+	/**
+	 * Variable: STYLE_FONTSTYLE
+	 *
+	 * Defines the key for the fontStyle style. Values may be any logical AND
+	 * (sum) of ,  and .
+	 * The type of the value is int. Value is "fontStyle".
+	 */
+	STYLE_FONTSTYLE: 'fontStyle',
+
+	/**
+	 * Variable: STYLE_ASPECT
+	 *
+	 * Defines the key for the aspect style. Possible values are empty or fixed.
+	 * If fixed is used then the aspect ratio of the cell will be maintained
+	 * when resizing. Default is empty. Value is "aspect".
+	 */
+	STYLE_ASPECT: 'aspect',
+
+	/**
+	 * Variable: STYLE_AUTOSIZE
+	 *
+	 * Defines the key for the autosize style. This specifies if a cell should be
+	 * resized automatically if the value has changed. Possible values are 0 or 1.
+	 * Default is 0. See . This is normally combined with
+	 *  to disable manual sizing. Value is "autosize".
+	 */
+	STYLE_AUTOSIZE: 'autosize',
+
+	/**
+	 * Variable: STYLE_FOLDABLE
+	 *
+	 * Defines the key for the foldable style. This specifies if a cell is foldable
+	 * using a folding icon. Possible values are 0 or 1. Default is 1. See
+	 * . Value is "foldable".
+	 */
+	STYLE_FOLDABLE: 'foldable',
+
+	/**
+	 * Variable: STYLE_EDITABLE
+	 *
+	 * Defines the key for the editable style. This specifies if the value of
+	 * a cell can be edited using the in-place editor. Possible values are 0 or
+	 * 1. Default is 1. See . Value is "editable".
+	 */
+	STYLE_EDITABLE: 'editable',
+
+	/**
+	 * Variable: STYLE_BACKGROUND_OUTLINE
+	 *
+	 * Defines the key for the backgroundOutline style. This specifies if a
+	 * only the background of a cell should be painted when it is highlighted.
+	 * Possible values are 0 or 1. Default is 0. Value is "backgroundOutline".
+	 */
+	STYLE_BACKGROUND_OUTLINE: 'backgroundOutline',
+
+	/**
+	 * Variable: STYLE_BENDABLE
+	 *
+	 * Defines the key for the bendable style. This specifies if the control
+	 * points of an edge can be moved. Possible values are 0 or 1. Default is
+	 * 1. See . Value is "bendable".
+	 */
+	STYLE_BENDABLE: 'bendable',
+
+	/**
+	 * Variable: STYLE_MOVABLE
+	 *
+	 * Defines the key for the movable style. This specifies if a cell can
+	 * be moved. Possible values are 0 or 1. Default is 1. See
+	 * . Value is "movable".
+	 */
+	STYLE_MOVABLE: 'movable',
+
+	/**
+	 * Variable: STYLE_RESIZABLE
+	 *
+	 * Defines the key for the resizable style. This specifies if a cell can
+	 * be resized. Possible values are 0 or 1. Default is 1. See
+	 * . Value is "resizable".
+	 */
+	STYLE_RESIZABLE: 'resizable',
+
+	/**
+	 * Variable: STYLE_RESIZE_WIDTH
+	 *
+	 * Defines the key for the resizeWidth style. This specifies if a cell's
+	 * width is resized if the parent is resized. If this is 1 then the width
+	 * will be resized even if the cell's geometry is relative. If this is 0
+	 * then the cell's width will not be resized. Default is not defined. Value
+	 * is "resizeWidth".
+	 */
+	STYLE_RESIZE_WIDTH: 'resizeWidth',
+
+	/**
+	 * Variable: STYLE_RESIZE_WIDTH
+	 *
+	 * Defines the key for the resizeHeight style. This specifies if a cell's
+	 * height if resize if the parent is resized. If this is 1 then the height
+	 * will be resized even if the cell's geometry is relative. If this is 0
+	 * then the cell's height will not be resized. Default is not defined. Value
+	 * is "resizeHeight".
+	 */
+	STYLE_RESIZE_HEIGHT: 'resizeHeight',
+
+	/**
+	 * Variable: STYLE_ROTATABLE
+	 *
+	 * Defines the key for the rotatable style. This specifies if a cell can
+	 * be rotated. Possible values are 0 or 1. Default is 1. See
+	 * . Value is "rotatable".
+	 */
+	STYLE_ROTATABLE: 'rotatable',
+
+	/**
+	 * Variable: STYLE_CLONEABLE
+	 *
+	 * Defines the key for the cloneable style. This specifies if a cell can
+	 * be cloned. Possible values are 0 or 1. Default is 1. See
+	 * . Value is "cloneable".
+	 */
+	STYLE_CLONEABLE: 'cloneable',
+
+	/**
+	 * Variable: STYLE_DELETABLE
+	 *
+	 * Defines the key for the deletable style. This specifies if a cell can be
+	 * deleted. Possible values are 0 or 1. Default is 1. See
+	 * . Value is "deletable".
+	 */
+	STYLE_DELETABLE: 'deletable',
+
+	/**
+	 * Variable: STYLE_SHAPE
+	 *
+	 * Defines the key for the shape. Possible values are all constants with
+	 * a SHAPE-prefix or any newly defined shape names. Value is "shape".
+	 */
+	STYLE_SHAPE: 'shape',
+
+	/**
+	 * Variable: STYLE_EDGE
+	 *
+	 * Defines the key for the edge style. Possible values are the functions
+	 * defined in . Value is "edgeStyle".
+	 */
+	STYLE_EDGE: 'edgeStyle',
+
+	/**
+	 * Variable: STYLE_JETTY_SIZE
+	 *
+	 * Defines the key for the jetty size in .
+	 * Default is 10. Possible values are all numeric values or "auto".
+	 * Value is "jettySize".
+	 */
+	STYLE_JETTY_SIZE: 'jettySize',
+
+	/**
+	 * Variable: STYLE_SOURCE_JETTY_SIZE
+	 *
+	 * Defines the key for the jetty size in .
+	 * Default is 10. Possible values are numeric values or "auto". This has
+	 * precedence over . Value is "sourceJettySize".
+	 */
+	STYLE_SOURCE_JETTY_SIZE: 'sourceJettySize',
+
+	/**
+	 * Variable: targetJettySize
+	 *
+	 * Defines the key for the jetty size in .
+	 * Default is 10. Possible values are numeric values or "auto". This has
+	 * precedence over . Value is "targetJettySize".
+	 */
+	STYLE_TARGET_JETTY_SIZE: 'targetJettySize',
+
+	/**
+	 * Variable: STYLE_LOOP
+	 *
+	 * Defines the key for the loop style. Possible values are the functions
+	 * defined in . Value is "loopStyle".
+	 */
+	STYLE_LOOP: 'loopStyle',
+
+	/**
+	 * Variable: STYLE_ORTHOGONAL_LOOP
+	 *
+	 * Defines the key for the orthogonal loop style. Possible values are 0 and
+	 * 1. Default is 0. Value is "orthogonalLoop". Use this style to specify
+	 * if loops should be routed using an orthogonal router. Currently, this
+	 * uses  but will be replaced with a dedicated
+	 * orthogonal loop router in later releases.
+	 */
+	STYLE_ORTHOGONAL_LOOP: 'orthogonalLoop',
+
+	/**
+	 * Variable: STYLE_ROUTING_CENTER_X
+	 *
+	 * Defines the key for the horizontal routing center. Possible values are
+	 * between -0.5 and 0.5. This is the relative offset from the center used
+	 * for connecting edges. The type of this value is numeric. Value is
+	 * "routingCenterX".
+	 */
+	STYLE_ROUTING_CENTER_X: 'routingCenterX',
+
+	/**
+	 * Variable: STYLE_ROUTING_CENTER_Y
+	 *
+	 * Defines the key for the vertical routing center. Possible values are
+	 * between -0.5 and 0.5. This is the relative offset from the center used
+	 * for connecting edges. The type of this value is numeric. Value is
+	 * "routingCenterY".
+	 */
+	STYLE_ROUTING_CENTER_Y: 'routingCenterY',
+
+	/**
+	 * Variable: FONT_BOLD
+	 *
+	 * Constant for bold fonts. Default is 1.
+	 */
+	FONT_BOLD: 1,
+
+	/**
+	 * Variable: FONT_ITALIC
+	 *
+	 * Constant for italic fonts. Default is 2.
+	 */
+	FONT_ITALIC: 2,
+
+	/**
+	 * Variable: FONT_UNDERLINE
+	 *
+	 * Constant for underlined fonts. Default is 4.
+	 */
+	FONT_UNDERLINE: 4,
+
+	/**
+	 * Variable: SHAPE_RECTANGLE
+	 *
+	 * Name under which  is registered in .
+	 * Default is rectangle.
+	 */
+	SHAPE_RECTANGLE: 'rectangle',
+
+	/**
+	 * Variable: SHAPE_ELLIPSE
+	 *
+	 * Name under which  is registered in .
+	 * Default is ellipse.
+	 */
+	SHAPE_ELLIPSE: 'ellipse',
+
+	/**
+	 * Variable: SHAPE_DOUBLE_ELLIPSE
+	 *
+	 * Name under which  is registered in .
+	 * Default is doubleEllipse.
+	 */
+	SHAPE_DOUBLE_ELLIPSE: 'doubleEllipse',
+
+	/**
+	 * Variable: SHAPE_RHOMBUS
+	 *
+	 * Name under which  is registered in .
+	 * Default is rhombus.
+	 */
+	SHAPE_RHOMBUS: 'rhombus',
+
+	/**
+	 * Variable: SHAPE_LINE
+	 *
+	 * Name under which  is registered in .
+	 * Default is line.
+	 */
+	SHAPE_LINE: 'line',
+
+	/**
+	 * Variable: SHAPE_IMAGE
+	 *
+	 * Name under which  is registered in .
+	 * Default is image.
+	 */
+	SHAPE_IMAGE: 'image',
+
+	/**
+	 * Variable: SHAPE_ARROW
+	 *
+	 * Name under which  is registered in .
+	 * Default is arrow.
+	 */
+	SHAPE_ARROW: 'arrow',
+
+	/**
+	 * Variable: SHAPE_ARROW_CONNECTOR
+	 *
+	 * Name under which  is registered in .
+	 * Default is arrowConnector.
+	 */
+	SHAPE_ARROW_CONNECTOR: 'arrowConnector',
+
+	/**
+	 * Variable: SHAPE_LABEL
+	 *
+	 * Name under which  is registered in .
+	 * Default is label.
+	 */
+	SHAPE_LABEL: 'label',
+
+	/**
+	 * Variable: SHAPE_CYLINDER
+	 *
+	 * Name under which  is registered in .
+	 * Default is cylinder.
+	 */
+	SHAPE_CYLINDER: 'cylinder',
+
+	/**
+	 * Variable: SHAPE_SWIMLANE
+	 *
+	 * Name under which  is registered in .
+	 * Default is swimlane.
+	 */
+	SHAPE_SWIMLANE: 'swimlane',
+
+	/**
+	 * Variable: SHAPE_CONNECTOR
+	 *
+	 * Name under which  is registered in .
+	 * Default is connector.
+	 */
+	SHAPE_CONNECTOR: 'connector',
+
+	/**
+	 * Variable: SHAPE_ACTOR
+	 *
+	 * Name under which  is registered in .
+	 * Default is actor.
+	 */
+	SHAPE_ACTOR: 'actor',
+
+	/**
+	 * Variable: SHAPE_CLOUD
+	 *
+	 * Name under which  is registered in .
+	 * Default is cloud.
+	 */
+	SHAPE_CLOUD: 'cloud',
+
+	/**
+	 * Variable: SHAPE_TRIANGLE
+	 *
+	 * Name under which  is registered in .
+	 * Default is triangle.
+	 */
+	SHAPE_TRIANGLE: 'triangle',
+
+	/**
+	 * Variable: SHAPE_HEXAGON
+	 *
+	 * Name under which  is registered in .
+	 * Default is hexagon.
+	 */
+	SHAPE_HEXAGON: 'hexagon',
+
+	/**
+	 * Variable: ARROW_CLASSIC
+	 *
+	 * Constant for classic arrow markers.
+	 */
+	ARROW_CLASSIC: 'classic',
+
+	/**
+	 * Variable: ARROW_CLASSIC_THIN
+	 *
+	 * Constant for thin classic arrow markers.
+	 */
+	ARROW_CLASSIC_THIN: 'classicThin',
+
+	/**
+	 * Variable: ARROW_BLOCK
+	 *
+	 * Constant for block arrow markers.
+	 */
+	ARROW_BLOCK: 'block',
+
+	/**
+	 * Variable: ARROW_BLOCK_THIN
+	 *
+	 * Constant for thin block arrow markers.
+	 */
+	ARROW_BLOCK_THIN: 'blockThin',
+
+	/**
+	 * Variable: ARROW_OPEN
+	 *
+	 * Constant for open arrow markers.
+	 */
+	ARROW_OPEN: 'open',
+
+	/**
+	 * Variable: ARROW_OPEN_THIN
+	 *
+	 * Constant for thin open arrow markers.
+	 */
+	ARROW_OPEN_THIN: 'openThin',
+
+	/**
+	 * Variable: ARROW_OVAL
+	 *
+	 * Constant for oval arrow markers.
+	 */
+	ARROW_OVAL: 'oval',
+
+	/**
+	 * Variable: ARROW_DIAMOND
+	 *
+	 * Constant for diamond arrow markers.
+	 */
+	ARROW_DIAMOND: 'diamond',
+
+	/**
+	 * Variable: ARROW_DIAMOND_THIN
+	 *
+	 * Constant for thin diamond arrow markers.
+	 */
+	ARROW_DIAMOND_THIN: 'diamondThin',
+
+	/**
+	 * Variable: ALIGN_LEFT
+	 *
+	 * Constant for left horizontal alignment. Default is left.
+	 */
+	ALIGN_LEFT: 'left',
+
+	/**
+	 * Variable: ALIGN_CENTER
+	 *
+	 * Constant for center horizontal alignment. Default is center.
+	 */
+	ALIGN_CENTER: 'center',
+
+	/**
+	 * Variable: ALIGN_RIGHT
+	 *
+	 * Constant for right horizontal alignment. Default is right.
+	 */
+	ALIGN_RIGHT: 'right',
+
+	/**
+	 * Variable: ALIGN_TOP
+	 *
+	 * Constant for top vertical alignment. Default is top.
+	 */
+	ALIGN_TOP: 'top',
+
+	/**
+	 * Variable: ALIGN_MIDDLE
+	 *
+	 * Constant for middle vertical alignment. Default is middle.
+	 */
+	ALIGN_MIDDLE: 'middle',
+
+	/**
+	 * Variable: ALIGN_BOTTOM
+	 *
+	 * Constant for bottom vertical alignment. Default is bottom.
+	 */
+	ALIGN_BOTTOM: 'bottom',
+
+	/**
+	 * Variable: DIRECTION_NORTH
+	 *
+	 * Constant for direction north. Default is north.
+	 */
+	DIRECTION_NORTH: 'north',
+
+	/**
+	 * Variable: DIRECTION_SOUTH
+	 *
+	 * Constant for direction south. Default is south.
+	 */
+	DIRECTION_SOUTH: 'south',
+
+	/**
+	 * Variable: DIRECTION_EAST
+	 *
+	 * Constant for direction east. Default is east.
+	 */
+	DIRECTION_EAST: 'east',
+
+	/**
+	 * Variable: DIRECTION_WEST
+	 *
+	 * Constant for direction west. Default is west.
+	 */
+	DIRECTION_WEST: 'west',
+
+	/**
+	 * Variable: TEXT_DIRECTION_DEFAULT
+	 *
+	 * Constant for text direction default. Default is an empty string. Use
+	 * this value to use the default text direction of the operating system.
+	 */
+	TEXT_DIRECTION_DEFAULT: '',
+
+	/**
+	 * Variable: TEXT_DIRECTION_AUTO
+	 *
+	 * Constant for text direction automatic. Default is auto. Use this value
+	 * to find the direction for a given text with .
+	 */
+	TEXT_DIRECTION_AUTO: 'auto',
+
+	/**
+	 * Variable: TEXT_DIRECTION_LTR
+	 *
+	 * Constant for text direction left to right. Default is ltr. Use this
+	 * value for left to right text direction.
+	 */
+	TEXT_DIRECTION_LTR: 'ltr',
+
+	/**
+	 * Variable: TEXT_DIRECTION_RTL
+	 *
+	 * Constant for text direction right to left. Default is rtl. Use this
+	 * value for right to left text direction.
+	 */
+	TEXT_DIRECTION_RTL: 'rtl',
+
+	/**
+	 * Variable: DIRECTION_MASK_NONE
+	 *
+	 * Constant for no direction.
+	 */
+	DIRECTION_MASK_NONE: 0,
+
+	/**
+	 * Variable: DIRECTION_MASK_WEST
+	 *
+	 * Bitwise mask for west direction.
+	 */
+	DIRECTION_MASK_WEST: 1,
+
+	/**
+	 * Variable: DIRECTION_MASK_NORTH
+	 *
+	 * Bitwise mask for north direction.
+	 */
+	DIRECTION_MASK_NORTH: 2,
+
+	/**
+	 * Variable: DIRECTION_MASK_SOUTH
+	 *
+	 * Bitwise mask for south direction.
+	 */
+	DIRECTION_MASK_SOUTH: 4,
+
+	/**
+	 * Variable: DIRECTION_MASK_EAST
+	 *
+	 * Bitwise mask for east direction.
+	 */
+	DIRECTION_MASK_EAST: 8,
+
+	/**
+	 * Variable: DIRECTION_MASK_ALL
+	 *
+	 * Bitwise mask for all directions.
+	 */
+	DIRECTION_MASK_ALL: 15,
+
+	/**
+	 * Variable: ELBOW_VERTICAL
+	 *
+	 * Constant for elbow vertical. Default is horizontal.
+	 */
+	ELBOW_VERTICAL: 'vertical',
+
+	/**
+	 * Variable: ELBOW_HORIZONTAL
+	 *
+	 * Constant for elbow horizontal. Default is horizontal.
+	 */
+	ELBOW_HORIZONTAL: 'horizontal',
+
+	/**
+	 * Variable: EDGESTYLE_ELBOW
+	 *
+	 * Name of the elbow edge style. Can be used as a string value
+	 * for the STYLE_EDGE style.
+	 */
+	EDGESTYLE_ELBOW: 'elbowEdgeStyle',
+
+	/**
+	 * Variable: EDGESTYLE_ENTITY_RELATION
+	 *
+	 * Name of the entity relation edge style. Can be used as a string value
+	 * for the STYLE_EDGE style.
+	 */
+	EDGESTYLE_ENTITY_RELATION: 'entityRelationEdgeStyle',
+
+	/**
+	 * Variable: EDGESTYLE_LOOP
+	 *
+	 * Name of the loop edge style. Can be used as a string value
+	 * for the STYLE_EDGE style.
+	 */
+	EDGESTYLE_LOOP: 'loopEdgeStyle',
+
+	/**
+	 * Variable: EDGESTYLE_SIDETOSIDE
+	 *
+	 * Name of the side to side edge style. Can be used as a string value
+	 * for the STYLE_EDGE style.
+	 */
+	EDGESTYLE_SIDETOSIDE: 'sideToSideEdgeStyle',
+
+	/**
+	 * Variable: EDGESTYLE_TOPTOBOTTOM
+	 *
+	 * Name of the top to bottom edge style. Can be used as a string value
+	 * for the STYLE_EDGE style.
+	 */
+	EDGESTYLE_TOPTOBOTTOM: 'topToBottomEdgeStyle',
+
+	/**
+	 * Variable: EDGESTYLE_ORTHOGONAL
+	 *
+	 * Name of the generic orthogonal edge style. Can be used as a string value
+	 * for the STYLE_EDGE style.
+	 */
+	EDGESTYLE_ORTHOGONAL: 'orthogonalEdgeStyle',
+
+	/**
+	 * Variable: EDGESTYLE_SEGMENT
+	 *
+	 * Name of the generic segment edge style. Can be used as a string value
+	 * for the STYLE_EDGE style.
+	 */
+	EDGESTYLE_SEGMENT: 'segmentEdgeStyle',
+
+	/**
+	 * Variable: PERIMETER_ELLIPSE
+	 *
+	 * Name of the ellipse perimeter. Can be used as a string value
+	 * for the STYLE_PERIMETER style.
+	 */
+	PERIMETER_ELLIPSE: 'ellipsePerimeter',
+
+	/**
+	 * Variable: PERIMETER_RECTANGLE
+	 *
+	 * Name of the rectangle perimeter. Can be used as a string value
+	 * for the STYLE_PERIMETER style.
+	 */
+	PERIMETER_RECTANGLE: 'rectanglePerimeter',
+
+	/**
+	 * Variable: PERIMETER_RHOMBUS
+	 *
+	 * Name of the rhombus perimeter. Can be used as a string value
+	 * for the STYLE_PERIMETER style.
+	 */
+	PERIMETER_RHOMBUS: 'rhombusPerimeter',
+
+	/**
+	 * Variable: PERIMETER_HEXAGON
+	 *
+	 * Name of the hexagon perimeter. Can be used as a string value
+	 * for the STYLE_PERIMETER style.
+	 */
+	PERIMETER_HEXAGON: 'hexagonPerimeter',
+
+	/**
+	 * Variable: PERIMETER_TRIANGLE
+	 *
+	 * Name of the triangle perimeter. Can be used as a string value
+	 * for the STYLE_PERIMETER style.
+	 */
+	PERIMETER_TRIANGLE: 'trianglePerimeter'
+
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxEventObject
+ *
+ * The mxEventObject is a wrapper for all properties of a single event.
+ * Additionally, it also offers functions to consume the event and check if it
+ * was consumed as follows:
+ *
+ * (code)
+ * evt.consume();
+ * INV: evt.isConsumed() == true
+ * (end)
+ *
+ * Constructor: mxEventObject
+ *
+ * Constructs a new event object with the specified name. An optional
+ * sequence of key, value pairs can be appended to define properties.
+ *
+ * Example:
+ *
+ * (code)
+ * new mxEventObject("eventName", key1, val1, .., keyN, valN)
+ * (end)
+ */
+function mxEventObject(name)
+{
+	this.name = name;
+	this.properties = [];
+
+	for (var i = 1; i < arguments.length; i += 2)
+	{
+		if (arguments[i + 1] != null)
+		{
+			this.properties[arguments[i]] = arguments[i + 1];
+		}
+	}
+};
+
+/**
+ * Variable: name
+ *
+ * Holds the name.
+ */
+mxEventObject.prototype.name = null;
+
+/**
+ * Variable: properties
+ *
+ * Holds the properties as an associative array.
+ */
+mxEventObject.prototype.properties = null;
+
+/**
+ * Variable: consumed
+ *
+ * Holds the consumed state. Default is false.
+ */
+mxEventObject.prototype.consumed = false;
+
+/**
+ * Function: getName
+ *
+ * Returns .
+ */
+mxEventObject.prototype.getName = function()
+{
+	return this.name;
+};
+
+/**
+ * Function: getProperties
+ *
+ * Returns .
+ */
+mxEventObject.prototype.getProperties = function()
+{
+	return this.properties;
+};
+
+/**
+ * Function: getProperty
+ *
+ * Returns the property for the given key.
+ */
+mxEventObject.prototype.getProperty = function(key)
+{
+	return this.properties[key];
+};
+
+/**
+ * Function: isConsumed
+ *
+ * Returns true if the event has been consumed.
+ */
+mxEventObject.prototype.isConsumed = function()
+{
+	return this.consumed;
+};
+
+/**
+ * Function: consume
+ *
+ * Consumes the event.
+ */
+mxEventObject.prototype.consume = function()
+{
+	this.consumed = true;
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxMouseEvent
+ *
+ * Base class for all mouse events in mxGraph. A listener for this event should
+ * implement the following methods:
+ *
+ * (code)
+ * graph.addMouseListener(
+ * {
+ *   mouseDown: function(sender, evt)
+ *   {
+ *     mxLog.debug('mouseDown');
+ *   },
+ *   mouseMove: function(sender, evt)
+ *   {
+ *     mxLog.debug('mouseMove');
+ *   },
+ *   mouseUp: function(sender, evt)
+ *   {
+ *     mxLog.debug('mouseUp');
+ *   }
+ * });
+ * (end)
+ *
+ * Constructor: mxMouseEvent
+ *
+ * Constructs a new event object for the given arguments.
+ *
+ * Parameters:
+ *
+ * evt - Native mouse event.
+ * state - Optional  under the mouse.
+ *
+ */
+function mxMouseEvent(evt, state)
+{
+	this.evt = evt;
+	this.state = state;
+	this.sourceState = state;
+};
+
+/**
+ * Variable: consumed
+ *
+ * Holds the consumed state of this event.
+ */
+mxMouseEvent.prototype.consumed = false;
+
+/**
+ * Variable: evt
+ *
+ * Holds the inner event object.
+ */
+mxMouseEvent.prototype.evt = null;
+
+/**
+ * Variable: graphX
+ *
+ * Holds the x-coordinate of the event in the graph. This value is set in
+ * .
+ */
+mxMouseEvent.prototype.graphX = null;
+
+/**
+ * Variable: graphY
+ *
+ * Holds the y-coordinate of the event in the graph. This value is set in
+ * .
+ */
+mxMouseEvent.prototype.graphY = null;
+
+/**
+ * Variable: state
+ *
+ * Holds the optional  associated with this event.
+ */
+mxMouseEvent.prototype.state = null;
+
+/**
+ * Variable: sourceState
+ *
+ * Holds the  that was passed to the constructor. This can be
+ * different from  depending on the result of .
+ */
+mxMouseEvent.prototype.sourceState = null;
+
+/**
+ * Function: getEvent
+ *
+ * Returns .
+ */
+mxMouseEvent.prototype.getEvent = function()
+{
+	return this.evt;
+};
+
+/**
+ * Function: getSource
+ *
+ * Returns the target DOM element using  for .
+ */
+mxMouseEvent.prototype.getSource = function()
+{
+	return mxEvent.getSource(this.evt);
+};
+
+/**
+ * Function: isSource
+ *
+ * Returns true if the given  is the source of .
+ */
+mxMouseEvent.prototype.isSource = function(shape)
+{
+	if (shape != null)
+	{
+		return mxUtils.isAncestorNode(shape.node, this.getSource());
+	}
+
+	return false;
+};
+
+/**
+ * Function: getX
+ *
+ * Returns .
+ */
+mxMouseEvent.prototype.getX = function()
+{
+	return mxEvent.getClientX(this.getEvent());
+};
+
+/**
+ * Function: getY
+ *
+ * Returns .
+ */
+mxMouseEvent.prototype.getY = function()
+{
+	return mxEvent.getClientY(this.getEvent());
+};
+
+/**
+ * Function: getGraphX
+ *
+ * Returns .
+ */
+mxMouseEvent.prototype.getGraphX = function()
+{
+	return this.graphX;
+};
+
+/**
+ * Function: getGraphY
+ *
+ * Returns .
+ */
+mxMouseEvent.prototype.getGraphY = function()
+{
+	return this.graphY;
+};
+
+/**
+ * Function: getState
+ *
+ * Returns .
+ */
+mxMouseEvent.prototype.getState = function()
+{
+	return this.state;
+};
+
+/**
+ * Function: getCell
+ *
+ * Returns the  in  is not null.
+ */
+mxMouseEvent.prototype.getCell = function()
+{
+	var state = this.getState();
+
+	if (state != null)
+	{
+		return state.cell;
+	}
+
+	return null;
+};
+
+/**
+ * Function: isPopupTrigger
+ *
+ * Returns true if the event is a popup trigger.
+ */
+mxMouseEvent.prototype.isPopupTrigger = function()
+{
+	return mxEvent.isPopupTrigger(this.getEvent());
+};
+
+/**
+ * Function: isConsumed
+ *
+ * Returns .
+ */
+mxMouseEvent.prototype.isConsumed = function()
+{
+	return this.consumed;
+};
+
+/**
+ * Function: consume
+ *
+ * Sets  to true and invokes preventDefault on the native event
+ * if such a method is defined. This is used mainly to avoid the cursor from
+ * being changed to a text cursor in Webkit. You can use the preventDefault
+ * flag to disable this functionality.
+ *
+ * Parameters:
+ *
+ * preventDefault - Specifies if the native event should be canceled. Default
+ * is true.
+ */
+mxMouseEvent.prototype.consume = function(preventDefault)
+{
+	preventDefault = (preventDefault != null) ? preventDefault : true;
+
+	if (preventDefault && this.evt.preventDefault)
+	{
+		this.evt.preventDefault();
+	}
+
+	// Workaround for images being dragged in IE
+	// Does not change returnValue in Opera
+	if (mxClient.IS_IE)
+	{
+		this.evt.returnValue = true;
+	}
+
+	// Sets local consumed state
+	this.consumed = true;
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxEventSource
+ *
+ * Base class for objects that dispatch named events. To create a subclass that
+ * inherits from mxEventSource, the following code is used.
+ *
+ * (code)
+ * function MyClass() { };
+ *
+ * MyClass.prototype = new mxEventSource();
+ * MyClass.prototype.constructor = MyClass;
+ * (end)
+ *
+ * Known Subclasses:
+ *
+ * , , , , ,
+ * , 
+ *
+ * Constructor: mxEventSource
+ *
+ * Constructs a new event source.
+ */
+function mxEventSource(eventSource)
+{
+	this.setEventSource(eventSource);
+};
+
+/**
+ * Variable: eventListeners
+ *
+ * Holds the event names and associated listeners in an array. The array
+ * contains the event name followed by the respective listener for each
+ * registered listener.
+ */
+mxEventSource.prototype.eventListeners = null;
+
+/**
+ * Variable: eventsEnabled
+ *
+ * Specifies if events can be fired. Default is true.
+ */
+mxEventSource.prototype.eventsEnabled = true;
+
+/**
+ * Variable: eventSource
+ *
+ * Optional source for events. Default is null.
+ */
+mxEventSource.prototype.eventSource = null;
+
+/**
+ * Function: isEventsEnabled
+ *
+ * Returns .
+ */
+mxEventSource.prototype.isEventsEnabled = function()
+{
+	return this.eventsEnabled;
+};
+
+/**
+ * Function: setEventsEnabled
+ *
+ * Sets .
+ */
+mxEventSource.prototype.setEventsEnabled = function(value)
+{
+	this.eventsEnabled = value;
+};
+
+/**
+ * Function: getEventSource
+ *
+ * Returns .
+ */
+mxEventSource.prototype.getEventSource = function()
+{
+	return this.eventSource;
+};
+
+/**
+ * Function: setEventSource
+ *
+ * Sets .
+ */
+mxEventSource.prototype.setEventSource = function(value)
+{
+	this.eventSource = value;
+};
+
+/**
+ * Function: addListener
+ *
+ * Binds the specified function to the given event name. If no event name
+ * is given, then the listener is registered for all events.
+ *
+ * The parameters of the listener are the sender and an .
+ */
+mxEventSource.prototype.addListener = function(name, funct)
+{
+	if (this.eventListeners == null)
+	{
+		this.eventListeners = [];
+	}
+
+	this.eventListeners.push(name);
+	this.eventListeners.push(funct);
+};
+
+/**
+ * Function: removeListener
+ *
+ * Removes all occurrences of the given listener from .
+ */
+mxEventSource.prototype.removeListener = function(funct)
+{
+	if (this.eventListeners != null)
+	{
+		var i = 0;
+
+		while (i < this.eventListeners.length)
+		{
+			if (this.eventListeners[i+1] == funct)
+			{
+				this.eventListeners.splice(i, 2);
+			}
+			else
+			{
+				i += 2;
+			}
+		}
+	}
+};
+
+/**
+ * Function: fireEvent
+ *
+ * Dispatches the given event to the listeners which are registered for
+ * the event. The sender argument is optional. The current execution scope
+ * ("this") is used for the listener invocation (see ).
+ *
+ * Example:
+ *
+ * (code)
+ * fireEvent(new mxEventObject("eventName", key1, val1, .., keyN, valN))
+ * (end)
+ *
+ * Parameters:
+ *
+ * evt -  that represents the event.
+ * sender - Optional sender to be passed to the listener. Default value is
+ * the return value of .
+ */
+mxEventSource.prototype.fireEvent = function(evt, sender)
+{
+	if (this.eventListeners != null && this.isEventsEnabled())
+	{
+		if (evt == null)
+		{
+			evt = new mxEventObject();
+		}
+
+		if (sender == null)
+		{
+			sender = this.getEventSource();
+		}
+
+		if (sender == null)
+		{
+			sender = this;
+		}
+
+		var args = [sender, evt];
+
+		for (var i = 0; i < this.eventListeners.length; i += 2)
+		{
+			var listen = this.eventListeners[i];
+
+			if (listen == null || listen == evt.getName())
+			{
+				this.eventListeners[i+1].apply(this, args);
+			}
+		}
+	}
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+var mxEvent =
+{
+
+	/**
+	 * Class: mxEvent
+	 *
+	 * Cross-browser DOM event support. For internal event handling,
+	 *  and the graph event dispatch loop in  are used.
+	 *
+	 * Memory Leaks:
+	 *
+	 * Use this class for adding and removing listeners to/from DOM nodes. The
+	 *  function is provided to remove all listeners that
+	 * have been added using . The function should be invoked when
+	 * the last reference is removed in the JavaScript code, typically when the
+	 * referenced DOM node is removed from the DOM.
+	 *
+	 * Function: addListener
+	 *
+	 * Binds the function to the specified event on the given element. Use
+	 *  in order to bind the "this" keyword inside the function
+	 * to a given execution scope.
+	 */
+	addListener: function()
+	{
+		var updateListenerList = function(element, eventName, funct)
+		{
+			if (element.mxListenerList == null)
+			{
+				element.mxListenerList = [];
+			}
+
+			var entry = {name: eventName, f: funct};
+			element.mxListenerList.push(entry);
+		};
+
+		if (window.addEventListener)
+		{
+			return function(element, eventName, funct)
+			{
+				element.addEventListener(eventName, funct, false);
+				updateListenerList(element, eventName, funct);
+			};
+		}
+		else
+		{
+			return function(element, eventName, funct)
+			{
+				element.attachEvent('on' + eventName, funct);
+				updateListenerList(element, eventName, funct);
+			};
+		}
+	}(),
+
+	/**
+	 * Function: removeListener
+	 *
+	 * Removes the specified listener from the given element.
+	 */
+	removeListener: function()
+	{
+		var updateListener = function(element, eventName, funct)
+		{
+			if (element.mxListenerList != null)
+			{
+				var listenerCount = element.mxListenerList.length;
+
+				for (var i = 0; i < listenerCount; i++)
+				{
+					var entry = element.mxListenerList[i];
+
+					if (entry.f == funct)
+					{
+						element.mxListenerList.splice(i, 1);
+						break;
+					}
+				}
+
+				if (element.mxListenerList.length == 0)
+				{
+					element.mxListenerList = null;
+				}
+			}
+		};
+
+		if (window.removeEventListener)
+		{
+			return function(element, eventName, funct)
+			{
+				element.removeEventListener(eventName, funct, false);
+				updateListener(element, eventName, funct);
+			};
+		}
+		else
+		{
+			return function(element, eventName, funct)
+			{
+				element.detachEvent('on' + eventName, funct);
+				updateListener(element, eventName, funct);
+			};
+		}
+	}(),
+
+	/**
+	 * Function: removeAllListeners
+	 *
+	 * Removes all listeners from the given element.
+	 */
+	removeAllListeners: function(element)
+	{
+		var list = element.mxListenerList;
+
+		if (list != null)
+		{
+			while (list.length > 0)
+			{
+				var entry = list[0];
+				mxEvent.removeListener(element, entry.name, entry.f);
+			}
+		}
+	},
+
+	/**
+	 * Function: addGestureListeners
+	 *
+	 * Adds the given listeners for touch, mouse and/or pointer events. If
+	 *  is true then pointer events will be registered,
+	 * else the respective mouse events will be registered. If 
+	 * is false and  is true then the respective touch events
+	 * will be registered as well as the mouse events.
+	 */
+	addGestureListeners: function(node, startListener, moveListener, endListener)
+	{
+		if (startListener != null)
+		{
+			mxEvent.addListener(node, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown', startListener);
+		}
+
+		if (moveListener != null)
+		{
+			mxEvent.addListener(node, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', moveListener);
+		}
+
+		if (endListener != null)
+		{
+			mxEvent.addListener(node, (mxClient.IS_POINTER) ? 'pointerup' : 'mouseup', endListener);
+		}
+
+		if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
+		{
+			if (startListener != null)
+			{
+				mxEvent.addListener(node, 'touchstart', startListener);
+			}
+
+			if (moveListener != null)
+			{
+				mxEvent.addListener(node, 'touchmove', moveListener);
+			}
+
+			if (endListener != null)
+			{
+				mxEvent.addListener(node, 'touchend', endListener);
+			}
+		}
+	},
+
+	/**
+	 * Function: removeGestureListeners
+	 *
+	 * Removes the given listeners from mousedown, mousemove, mouseup and the
+	 * respective touch events if  is true.
+	 */
+	removeGestureListeners: function(node, startListener, moveListener, endListener)
+	{
+		if (startListener != null)
+		{
+			mxEvent.removeListener(node, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown', startListener);
+		}
+
+		if (moveListener != null)
+		{
+			mxEvent.removeListener(node, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', moveListener);
+		}
+
+		if (endListener != null)
+		{
+			mxEvent.removeListener(node, (mxClient.IS_POINTER) ? 'pointerup' : 'mouseup', endListener);
+		}
+
+		if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
+		{
+			if (startListener != null)
+			{
+				mxEvent.removeListener(node, 'touchstart', startListener);
+			}
+
+			if (moveListener != null)
+			{
+				mxEvent.removeListener(node, 'touchmove', moveListener);
+			}
+
+			if (endListener != null)
+			{
+				mxEvent.removeListener(node, 'touchend', endListener);
+			}
+		}
+	},
+
+	/**
+	 * Function: redirectMouseEvents
+	 *
+	 * Redirects the mouse events from the given DOM node to the graph dispatch
+	 * loop using the event and given state as event arguments. State can
+	 * either be an instance of  or a function that returns an
+	 * . The down, move, up and dblClick arguments are optional
+	 * functions that take the trigger event as arguments and replace the
+	 * default behaviour.
+	 */
+	redirectMouseEvents: function(node, graph, state, down, move, up, dblClick)
+	{
+		var getState = function(evt)
+		{
+			return (typeof(state) == 'function') ? state(evt) : state;
+		};
+
+		mxEvent.addGestureListeners(node, function (evt)
+		{
+			if (down != null)
+			{
+				down(evt);
+			}
+			else if (!mxEvent.isConsumed(evt))
+			{
+				graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, getState(evt)));
+			}
+		},
+		function (evt)
+		{
+			if (move != null)
+			{
+				move(evt);
+			}
+			else if (!mxEvent.isConsumed(evt))
+			{
+				graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt)));
+			}
+		},
+		function (evt)
+		{
+			if (up != null)
+			{
+				up(evt);
+			}
+			else if (!mxEvent.isConsumed(evt))
+			{
+				graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, getState(evt)));
+			}
+		});
+
+		mxEvent.addListener(node, 'dblclick', function (evt)
+		{
+			if (dblClick != null)
+			{
+				dblClick(evt);
+			}
+			else if (!mxEvent.isConsumed(evt))
+			{
+				var tmp = getState(evt);
+				graph.dblClick(evt, (tmp != null) ? tmp.cell : null);
+			}
+		});
+	},
+
+	/**
+	 * Function: release
+	 *
+	 * Removes the known listeners from the given DOM node and its descendants.
+	 *
+	 * Parameters:
+	 *
+	 * element - DOM node to remove the listeners from.
+	 */
+	release: function(element)
+	{
+		if (element != null)
+		{
+			mxEvent.removeAllListeners(element);
+
+			var children = element.childNodes;
+
+			if (children != null)
+			{
+		        var childCount = children.length;
+
+		        for (var i = 0; i < childCount; i += 1)
+		        {
+		        	mxEvent.release(children[i]);
+		        }
+		    }
+		}
+	},
+
+	/**
+	 * Function: addMouseWheelListener
+	 *
+	 * Installs the given function as a handler for mouse wheel events. The
+	 * function has two arguments: the mouse event and a boolean that specifies
+	 * if the wheel was moved up or down.
+	 *
+	 * This has been tested with IE 6 and 7, Firefox (all versions), Opera and
+	 * Safari. It does currently not work on Safari for Mac.
+	 *
+	 * Example:
+	 *
+	 * (code)
+	 * mxEvent.addMouseWheelListener(function (evt, up)
+	 * {
+	 *   mxLog.show();
+	 *   mxLog.debug('mouseWheel: up='+up);
+	 * });
+	 *(end)
+	 *
+	 * Parameters:
+	 *
+	 * funct - Handler function that takes the event argument and a boolean up
+	 * argument for the mousewheel direction.
+	 */
+	addMouseWheelListener: function(funct)
+	{
+		if (funct != null)
+		{
+			var wheelHandler = function(evt)
+			{
+				// IE does not give an event object but the
+				// global event object is the mousewheel event
+				// at this point in time.
+				if (evt == null)
+				{
+					evt = window.event;
+				}
+
+				var delta = 0;
+
+				if (mxClient.IS_FF)
+				{
+					delta = -evt.detail / 2;
+				}
+				else
+				{
+					delta = evt.wheelDelta / 120;
+				}
+
+				// Handles the event using the given function
+				if (delta != 0)
+				{
+					funct(evt, delta > 0);
+				}
+			};
+
+			// Webkit has NS event API, but IE event name and details
+			if (mxClient.IS_NS && document.documentMode == null)
+			{
+				var eventName = (mxClient.IS_SF || 	mxClient.IS_GC) ? 'mousewheel' : 'DOMMouseScroll';
+				mxEvent.addListener(window, eventName, wheelHandler);
+			}
+			else
+			{
+				mxEvent.addListener(document, 'mousewheel', wheelHandler);
+			}
+		}
+	},
+
+	/**
+	 * Function: disableContextMenu
+	 *
+	 * Disables the context menu for the given element.
+	 */
+	disableContextMenu: function(element)
+	{
+		mxEvent.addListener(element, 'contextmenu', function(evt)
+		{
+			if (evt.preventDefault)
+			{
+				evt.preventDefault();
+			}
+
+			return false;
+		});
+	},
+
+	/**
+	 * Function: getSource
+	 *
+	 * Returns the event's target or srcElement depending on the browser.
+	 */
+	getSource: function(evt)
+	{
+		return (evt.srcElement != null) ? evt.srcElement : evt.target;
+	},
+
+	/**
+	 * Function: isConsumed
+	 *
+	 * Returns true if the event has been consumed using .
+	 */
+	isConsumed: function(evt)
+	{
+		return evt.isConsumed != null && evt.isConsumed;
+	},
+
+	/**
+	 * Function: isTouchEvent
+	 *
+	 * Returns true if the event was generated using a touch device (not a pen or mouse).
+	 */
+	isTouchEvent: function(evt)
+	{
+		return (evt.pointerType != null) ? (evt.pointerType == 'touch' || evt.pointerType ===
+			evt.MSPOINTER_TYPE_TOUCH) : ((evt.mozInputSource != null) ?
+					evt.mozInputSource == 5 : evt.type.indexOf('touch') == 0);
+	},
+
+	/**
+	 * Function: isPenEvent
+	 *
+	 * Returns true if the event was generated using a pen (not a touch device or mouse).
+	 */
+	isPenEvent: function(evt)
+	{
+		return (evt.pointerType != null) ? (evt.pointerType == 'pen' || evt.pointerType ===
+			evt.MSPOINTER_TYPE_PEN) : ((evt.mozInputSource != null) ?
+					evt.mozInputSource == 2 : evt.type.indexOf('pen') == 0);
+	},
+
+	/**
+	 * Function: isMultiTouchEvent
+	 *
+	 * Returns true if the event was generated using a touch device (not a pen or mouse).
+	 */
+	isMultiTouchEvent: function(evt)
+	{
+		return (evt.type != null && evt.type.indexOf('touch') == 0 && evt.touches != null && evt.touches.length > 1);
+	},
+
+	/**
+	 * Function: isMouseEvent
+	 *
+	 * Returns true if the event was generated using a mouse (not a pen or touch device).
+	 */
+	isMouseEvent: function(evt)
+	{
+		return (evt.pointerType != null) ? (evt.pointerType == 'mouse' || evt.pointerType ===
+			evt.MSPOINTER_TYPE_MOUSE) : ((evt.mozInputSource != null) ?
+				evt.mozInputSource == 1 : evt.type.indexOf('mouse') == 0);
+	},
+
+	/**
+	 * Function: isLeftMouseButton
+	 *
+	 * Returns true if the left mouse button is pressed for the given event.
+	 * To check if a button is pressed during a mouseMove you should use the
+	 *  property. Note that this returns true in Firefox
+	 * for control+left-click on the Mac.
+	 */
+	isLeftMouseButton: function(evt)
+	{
+		// Special case for mousemove and mousedown we check the buttons
+		// if it exists because which is 0 even if no button is pressed
+		if ('buttons' in evt && (evt.type == 'mousedown' || evt.type == 'mousemove'))
+		{
+			return evt.buttons == 1;
+		}
+		else if ('which' in evt)
+		{
+	        return evt.which === 1;
+	    }
+		else
+		{
+	        return evt.button === 1;
+	    }
+	},
+
+	/**
+	 * Function: isMiddleMouseButton
+	 *
+	 * Returns true if the middle mouse button is pressed for the given event.
+	 * To check if a button is pressed during a mouseMove you should use the
+	 *  property.
+	 */
+	isMiddleMouseButton: function(evt)
+	{
+		if ('which' in evt)
+		{
+	        return evt.which === 2;
+	    }
+		else
+		{
+	        return evt.button === 4;
+	    }
+	},
+
+	/**
+	 * Function: isRightMouseButton
+	 *
+	 * Returns true if the right mouse button was pressed. Note that this
+	 * button might not be available on some systems. For handling a popup
+	 * trigger  should be used.
+	 */
+	isRightMouseButton: function(evt)
+	{
+		if ('which' in evt)
+		{
+	        return evt.which === 3;
+	    }
+		else
+		{
+	        return evt.button === 2;
+	    }
+	},
+
+	/**
+	 * Function: isPopupTrigger
+	 *
+	 * Returns true if the event is a popup trigger. This implementation
+	 * returns true if the right button or the left button and control was
+	 * pressed on a Mac.
+	 */
+	isPopupTrigger: function(evt)
+	{
+		return mxEvent.isRightMouseButton(evt) || (mxClient.IS_MAC && mxEvent.isControlDown(evt) &&
+			!mxEvent.isShiftDown(evt) && !mxEvent.isMetaDown(evt) && !mxEvent.isAltDown(evt));
+	},
+
+	/**
+	 * Function: isShiftDown
+	 *
+	 * Returns true if the shift key is pressed for the given event.
+	 */
+	isShiftDown: function(evt)
+	{
+		return (evt != null) ? evt.shiftKey : false;
+	},
+
+	/**
+	 * Function: isAltDown
+	 *
+	 * Returns true if the alt key is pressed for the given event.
+	 */
+	isAltDown: function(evt)
+	{
+		return (evt != null) ? evt.altKey : false;
+	},
+
+	/**
+	 * Function: isControlDown
+	 *
+	 * Returns true if the control key is pressed for the given event.
+	 */
+	isControlDown: function(evt)
+	{
+		return (evt != null) ? evt.ctrlKey : false;
+	},
+
+	/**
+	 * Function: isMetaDown
+	 *
+	 * Returns true if the meta key is pressed for the given event.
+	 */
+	isMetaDown: function(evt)
+	{
+		return (evt != null) ? evt.metaKey : false;
+	},
+
+	/**
+	 * Function: getMainEvent
+	 *
+	 * Returns the touch or mouse event that contains the mouse coordinates.
+	 */
+	getMainEvent: function(e)
+	{
+		if ((e.type == 'touchstart' || e.type == 'touchmove') && e.touches != null && e.touches[0] != null)
+		{
+			e = e.touches[0];
+		}
+		else if (e.type == 'touchend' && e.changedTouches != null && e.changedTouches[0] != null)
+		{
+			e = e.changedTouches[0];
+		}
+
+		return e;
+	},
+
+	/**
+	 * Function: getClientX
+	 *
+	 * Returns true if the meta key is pressed for the given event.
+	 */
+	getClientX: function(e)
+	{
+		return mxEvent.getMainEvent(e).clientX;
+	},
+
+	/**
+	 * Function: getClientY
+	 *
+	 * Returns true if the meta key is pressed for the given event.
+	 */
+	getClientY: function(e)
+	{
+		return mxEvent.getMainEvent(e).clientY;
+	},
+
+	/**
+	 * Function: consume
+	 *
+	 * Consumes the given event.
+	 *
+	 * Parameters:
+	 *
+	 * evt - Native event to be consumed.
+	 * preventDefault - Optional boolean to prevent the default for the event.
+	 * Default is true.
+	 * stopPropagation - Option boolean to stop event propagation. Default is
+	 * true.
+	 */
+	consume: function(evt, preventDefault, stopPropagation)
+	{
+		preventDefault = (preventDefault != null) ? preventDefault : true;
+		stopPropagation = (stopPropagation != null) ? stopPropagation : true;
+
+		if (preventDefault)
+		{
+			if (evt.preventDefault)
+			{
+				if (stopPropagation)
+				{
+					evt.stopPropagation();
+				}
+
+				evt.preventDefault();
+			}
+			else if (stopPropagation)
+			{
+				evt.cancelBubble = true;
+			}
+		}
+
+		// Opera
+		evt.isConsumed = true;
+
+		// Other browsers
+		if (!evt.preventDefault)
+		{
+			evt.returnValue = false;
+		}
+	},
+
+	//
+	// Special handles in mouse events
+	//
+
+	/**
+	 * Variable: LABEL_HANDLE
+	 *
+	 * Index for the label handle in an mxMouseEvent. This should be a negative
+	 * value that does not interfere with any possible handle indices. Default
+	 * is -1.
+	 */
+	LABEL_HANDLE: -1,
+
+	/**
+	 * Variable: ROTATION_HANDLE
+	 *
+	 * Index for the rotation handle in an mxMouseEvent. This should be a
+	 * negative value that does not interfere with any possible handle indices.
+	 * Default is -2.
+	 */
+	ROTATION_HANDLE: -2,
+
+	/**
+	 * Variable: CUSTOM_HANDLE
+	 *
+	 * Start index for the custom handles in an mxMouseEvent. This should be a
+	 * negative value and is the start index which is decremented for each
+	 * custom handle. Default is -100.
+	 */
+	CUSTOM_HANDLE: -100,
+
+	/**
+	 * Variable: VIRTUAL_HANDLE
+	 *
+	 * Start index for the virtual handles in an mxMouseEvent. This should be a
+	 * negative value and is the start index which is decremented for each
+	 * virtual handle. Default is -100000. This assumes that there are no more
+	 * than VIRTUAL_HANDLE - CUSTOM_HANDLE custom handles.
+	 *
+	 */
+	VIRTUAL_HANDLE: -100000,
+
+	//
+	// Event names
+	//
+
+	/**
+	 * Variable: MOUSE_DOWN
+	 *
+	 * Specifies the event name for mouseDown.
+	 */
+	MOUSE_DOWN: 'mouseDown',
+
+	/**
+	 * Variable: MOUSE_MOVE
+	 *
+	 * Specifies the event name for mouseMove.
+	 */
+	MOUSE_MOVE: 'mouseMove',
+
+	/**
+	 * Variable: MOUSE_UP
+	 *
+	 * Specifies the event name for mouseUp.
+	 */
+	MOUSE_UP: 'mouseUp',
+
+	/**
+	 * Variable: ACTIVATE
+	 *
+	 * Specifies the event name for activate.
+	 */
+	ACTIVATE: 'activate',
+
+	/**
+	 * Variable: RESIZE_START
+	 *
+	 * Specifies the event name for resizeStart.
+	 */
+	RESIZE_START: 'resizeStart',
+
+	/**
+	 * Variable: RESIZE
+	 *
+	 * Specifies the event name for resize.
+	 */
+	RESIZE: 'resize',
+
+	/**
+	 * Variable: RESIZE_END
+	 *
+	 * Specifies the event name for resizeEnd.
+	 */
+	RESIZE_END: 'resizeEnd',
+
+	/**
+	 * Variable: MOVE_START
+	 *
+	 * Specifies the event name for moveStart.
+	 */
+	MOVE_START: 'moveStart',
+
+	/**
+	 * Variable: MOVE
+	 *
+	 * Specifies the event name for move.
+	 */
+	MOVE: 'move',
+
+	/**
+	 * Variable: MOVE_END
+	 *
+	 * Specifies the event name for moveEnd.
+	 */
+	MOVE_END: 'moveEnd',
+
+	/**
+	 * Variable: PAN_START
+	 *
+	 * Specifies the event name for panStart.
+	 */
+	PAN_START: 'panStart',
+
+	/**
+	 * Variable: PAN
+	 *
+	 * Specifies the event name for pan.
+	 */
+	PAN: 'pan',
+
+	/**
+	 * Variable: PAN_END
+	 *
+	 * Specifies the event name for panEnd.
+	 */
+	PAN_END: 'panEnd',
+
+	/**
+	 * Variable: MINIMIZE
+	 *
+	 * Specifies the event name for minimize.
+	 */
+	MINIMIZE: 'minimize',
+
+	/**
+	 * Variable: NORMALIZE
+	 *
+	 * Specifies the event name for normalize.
+	 */
+	NORMALIZE: 'normalize',
+
+	/**
+	 * Variable: MAXIMIZE
+	 *
+	 * Specifies the event name for maximize.
+	 */
+	MAXIMIZE: 'maximize',
+
+	/**
+	 * Variable: HIDE
+	 *
+	 * Specifies the event name for hide.
+	 */
+	HIDE: 'hide',
+
+	/**
+	 * Variable: SHOW
+	 *
+	 * Specifies the event name for show.
+	 */
+	SHOW: 'show',
+
+	/**
+	 * Variable: CLOSE
+	 *
+	 * Specifies the event name for close.
+	 */
+	CLOSE: 'close',
+
+	/**
+	 * Variable: DESTROY
+	 *
+	 * Specifies the event name for destroy.
+	 */
+	DESTROY: 'destroy',
+
+	/**
+	 * Variable: REFRESH
+	 *
+	 * Specifies the event name for refresh.
+	 */
+	REFRESH: 'refresh',
+
+	/**
+	 * Variable: SIZE
+	 *
+	 * Specifies the event name for size.
+	 */
+	SIZE: 'size',
+
+	/**
+	 * Variable: SELECT
+	 *
+	 * Specifies the event name for select.
+	 */
+	SELECT: 'select',
+
+	/**
+	 * Variable: FIRED
+	 *
+	 * Specifies the event name for fired.
+	 */
+	FIRED: 'fired',
+
+	/**
+	 * Variable: FIRE_MOUSE_EVENT
+	 *
+	 * Specifies the event name for fireMouseEvent.
+	 */
+	FIRE_MOUSE_EVENT: 'fireMouseEvent',
+
+	/**
+	 * Variable: GESTURE
+	 *
+	 * Specifies the event name for gesture.
+	 */
+	GESTURE: 'gesture',
+
+	/**
+	 * Variable: TAP_AND_HOLD
+	 *
+	 * Specifies the event name for tapAndHold.
+	 */
+	TAP_AND_HOLD: 'tapAndHold',
+
+	/**
+	 * Variable: GET
+	 *
+	 * Specifies the event name for get.
+	 */
+	GET: 'get',
+
+	/**
+	 * Variable: RECEIVE
+	 *
+	 * Specifies the event name for receive.
+	 */
+	RECEIVE: 'receive',
+
+	/**
+	 * Variable: CONNECT
+	 *
+	 * Specifies the event name for connect.
+	 */
+	CONNECT: 'connect',
+
+	/**
+	 * Variable: DISCONNECT
+	 *
+	 * Specifies the event name for disconnect.
+	 */
+	DISCONNECT: 'disconnect',
+
+	/**
+	 * Variable: SUSPEND
+	 *
+	 * Specifies the event name for suspend.
+	 */
+	SUSPEND: 'suspend',
+
+	/**
+	 * Variable: RESUME
+	 *
+	 * Specifies the event name for suspend.
+	 */
+	RESUME: 'resume',
+
+	/**
+	 * Variable: MARK
+	 *
+	 * Specifies the event name for mark.
+	 */
+	MARK: 'mark',
+
+	/**
+	 * Variable: ROOT
+	 *
+	 * Specifies the event name for root.
+	 */
+	ROOT: 'root',
+
+	/**
+	 * Variable: POST
+	 *
+	 * Specifies the event name for post.
+	 */
+	POST: 'post',
+
+	/**
+	 * Variable: OPEN
+	 *
+	 * Specifies the event name for open.
+	 */
+	OPEN: 'open',
+
+	/**
+	 * Variable: SAVE
+	 *
+	 * Specifies the event name for open.
+	 */
+	SAVE: 'save',
+
+	/**
+	 * Variable: BEFORE_ADD_VERTEX
+	 *
+	 * Specifies the event name for beforeAddVertex.
+	 */
+	BEFORE_ADD_VERTEX: 'beforeAddVertex',
+
+	/**
+	 * Variable: ADD_VERTEX
+	 *
+	 * Specifies the event name for addVertex.
+	 */
+	ADD_VERTEX: 'addVertex',
+
+	/**
+	 * Variable: AFTER_ADD_VERTEX
+	 *
+	 * Specifies the event name for afterAddVertex.
+	 */
+	AFTER_ADD_VERTEX: 'afterAddVertex',
+
+	/**
+	 * Variable: DONE
+	 *
+	 * Specifies the event name for done.
+	 */
+	DONE: 'done',
+
+	/**
+	 * Variable: EXECUTE
+	 *
+	 * Specifies the event name for execute.
+	 */
+	EXECUTE: 'execute',
+
+	/**
+	 * Variable: EXECUTED
+	 *
+	 * Specifies the event name for executed.
+	 */
+	EXECUTED: 'executed',
+
+	/**
+	 * Variable: BEGIN_UPDATE
+	 *
+	 * Specifies the event name for beginUpdate.
+	 */
+	BEGIN_UPDATE: 'beginUpdate',
+
+	/**
+	 * Variable: START_EDIT
+	 *
+	 * Specifies the event name for startEdit.
+	 */
+	START_EDIT: 'startEdit',
+
+	/**
+	 * Variable: END_UPDATE
+	 *
+	 * Specifies the event name for endUpdate.
+	 */
+	END_UPDATE: 'endUpdate',
+
+	/**
+	 * Variable: END_EDIT
+	 *
+	 * Specifies the event name for endEdit.
+	 */
+	END_EDIT: 'endEdit',
+
+	/**
+	 * Variable: BEFORE_UNDO
+	 *
+	 * Specifies the event name for beforeUndo.
+	 */
+	BEFORE_UNDO: 'beforeUndo',
+
+	/**
+	 * Variable: UNDO
+	 *
+	 * Specifies the event name for undo.
+	 */
+	UNDO: 'undo',
+
+	/**
+	 * Variable: REDO
+	 *
+	 * Specifies the event name for redo.
+	 */
+	REDO: 'redo',
+
+	/**
+	 * Variable: CHANGE
+	 *
+	 * Specifies the event name for change.
+	 */
+	CHANGE: 'change',
+
+	/**
+	 * Variable: NOTIFY
+	 *
+	 * Specifies the event name for notify.
+	 */
+	NOTIFY: 'notify',
+
+	/**
+	 * Variable: LAYOUT_CELLS
+	 *
+	 * Specifies the event name for layoutCells.
+	 */
+	LAYOUT_CELLS: 'layoutCells',
+
+	/**
+	 * Variable: CLICK
+	 *
+	 * Specifies the event name for click.
+	 */
+	CLICK: 'click',
+
+	/**
+	 * Variable: SCALE
+	 *
+	 * Specifies the event name for scale.
+	 */
+	SCALE: 'scale',
+
+	/**
+	 * Variable: TRANSLATE
+	 *
+	 * Specifies the event name for translate.
+	 */
+	TRANSLATE: 'translate',
+
+	/**
+	 * Variable: SCALE_AND_TRANSLATE
+	 *
+	 * Specifies the event name for scaleAndTranslate.
+	 */
+	SCALE_AND_TRANSLATE: 'scaleAndTranslate',
+
+	/**
+	 * Variable: UP
+	 *
+	 * Specifies the event name for up.
+	 */
+	UP: 'up',
+
+	/**
+	 * Variable: DOWN
+	 *
+	 * Specifies the event name for down.
+	 */
+	DOWN: 'down',
+
+	/**
+	 * Variable: ADD
+	 *
+	 * Specifies the event name for add.
+	 */
+	ADD: 'add',
+
+	/**
+	 * Variable: REMOVE
+	 *
+	 * Specifies the event name for remove.
+	 */
+	REMOVE: 'remove',
+
+	/**
+	 * Variable: CLEAR
+	 *
+	 * Specifies the event name for clear.
+	 */
+	CLEAR: 'clear',
+
+	/**
+	 * Variable: ADD_CELLS
+	 *
+	 * Specifies the event name for addCells.
+	 */
+	ADD_CELLS: 'addCells',
+
+	/**
+	 * Variable: CELLS_ADDED
+	 *
+	 * Specifies the event name for cellsAdded.
+	 */
+	CELLS_ADDED: 'cellsAdded',
+
+	/**
+	 * Variable: MOVE_CELLS
+	 *
+	 * Specifies the event name for moveCells.
+	 */
+	MOVE_CELLS: 'moveCells',
+
+	/**
+	 * Variable: CELLS_MOVED
+	 *
+	 * Specifies the event name for cellsMoved.
+	 */
+	CELLS_MOVED: 'cellsMoved',
+
+	/**
+	 * Variable: RESIZE_CELLS
+	 *
+	 * Specifies the event name for resizeCells.
+	 */
+	RESIZE_CELLS: 'resizeCells',
+
+	/**
+	 * Variable: CELLS_RESIZED
+	 *
+	 * Specifies the event name for cellsResized.
+	 */
+	CELLS_RESIZED: 'cellsResized',
+
+	/**
+	 * Variable: TOGGLE_CELLS
+	 *
+	 * Specifies the event name for toggleCells.
+	 */
+	TOGGLE_CELLS: 'toggleCells',
+
+	/**
+	 * Variable: CELLS_TOGGLED
+	 *
+	 * Specifies the event name for cellsToggled.
+	 */
+	CELLS_TOGGLED: 'cellsToggled',
+
+	/**
+	 * Variable: ORDER_CELLS
+	 *
+	 * Specifies the event name for orderCells.
+	 */
+	ORDER_CELLS: 'orderCells',
+
+	/**
+	 * Variable: CELLS_ORDERED
+	 *
+	 * Specifies the event name for cellsOrdered.
+	 */
+	CELLS_ORDERED: 'cellsOrdered',
+
+	/**
+	 * Variable: REMOVE_CELLS
+	 *
+	 * Specifies the event name for removeCells.
+	 */
+	REMOVE_CELLS: 'removeCells',
+
+	/**
+	 * Variable: CELLS_REMOVED
+	 *
+	 * Specifies the event name for cellsRemoved.
+	 */
+	CELLS_REMOVED: 'cellsRemoved',
+
+	/**
+	 * Variable: GROUP_CELLS
+	 *
+	 * Specifies the event name for groupCells.
+	 */
+	GROUP_CELLS: 'groupCells',
+
+	/**
+	 * Variable: UNGROUP_CELLS
+	 *
+	 * Specifies the event name for ungroupCells.
+	 */
+	UNGROUP_CELLS: 'ungroupCells',
+
+	/**
+	 * Variable: REMOVE_CELLS_FROM_PARENT
+	 *
+	 * Specifies the event name for removeCellsFromParent.
+	 */
+	REMOVE_CELLS_FROM_PARENT: 'removeCellsFromParent',
+
+	/**
+	 * Variable: FOLD_CELLS
+	 *
+	 * Specifies the event name for foldCells.
+	 */
+	FOLD_CELLS: 'foldCells',
+
+	/**
+	 * Variable: CELLS_FOLDED
+	 *
+	 * Specifies the event name for cellsFolded.
+	 */
+	CELLS_FOLDED: 'cellsFolded',
+
+	/**
+	 * Variable: ALIGN_CELLS
+	 *
+	 * Specifies the event name for alignCells.
+	 */
+	ALIGN_CELLS: 'alignCells',
+
+	/**
+	 * Variable: LABEL_CHANGED
+	 *
+	 * Specifies the event name for labelChanged.
+	 */
+	LABEL_CHANGED: 'labelChanged',
+
+	/**
+	 * Variable: CONNECT_CELL
+	 *
+	 * Specifies the event name for connectCell.
+	 */
+	CONNECT_CELL: 'connectCell',
+
+	/**
+	 * Variable: CELL_CONNECTED
+	 *
+	 * Specifies the event name for cellConnected.
+	 */
+	CELL_CONNECTED: 'cellConnected',
+
+	/**
+	 * Variable: SPLIT_EDGE
+	 *
+	 * Specifies the event name for splitEdge.
+	 */
+	SPLIT_EDGE: 'splitEdge',
+
+	/**
+	 * Variable: FLIP_EDGE
+	 *
+	 * Specifies the event name for flipEdge.
+	 */
+	FLIP_EDGE: 'flipEdge',
+
+	/**
+	 * Variable: START_EDITING
+	 *
+	 * Specifies the event name for startEditing.
+	 */
+	START_EDITING: 'startEditing',
+
+	/**
+	 * Variable: EDITING_STARTED
+	 *
+	 * Specifies the event name for editingStarted.
+	 */
+	EDITING_STARTED: 'editingStarted',
+
+	/**
+	 * Variable: EDITING_STOPPED
+	 *
+	 * Specifies the event name for editingStopped.
+	 */
+	EDITING_STOPPED: 'editingStopped',
+
+	/**
+	 * Variable: ADD_OVERLAY
+	 *
+	 * Specifies the event name for addOverlay.
+	 */
+	ADD_OVERLAY: 'addOverlay',
+
+	/**
+	 * Variable: REMOVE_OVERLAY
+	 *
+	 * Specifies the event name for removeOverlay.
+	 */
+	REMOVE_OVERLAY: 'removeOverlay',
+
+	/**
+	 * Variable: UPDATE_CELL_SIZE
+	 *
+	 * Specifies the event name for updateCellSize.
+	 */
+	UPDATE_CELL_SIZE: 'updateCellSize',
+
+	/**
+	 * Variable: ESCAPE
+	 *
+	 * Specifies the event name for escape.
+	 */
+	ESCAPE: 'escape',
+
+	/**
+	 * Variable: DOUBLE_CLICK
+	 *
+	 * Specifies the event name for doubleClick.
+	 */
+	DOUBLE_CLICK: 'doubleClick',
+
+	/**
+	 * Variable: START
+	 *
+	 * Specifies the event name for start.
+	 */
+	START: 'start',
+
+	/**
+	 * Variable: RESET
+	 *
+	 * Specifies the event name for reset.
+	 */
+	RESET: 'reset'
+
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxXmlRequest
+ *
+ * XML HTTP request wrapper. See also: ,  and
+ * . This class provides a cross-browser abstraction for Ajax
+ * requests.
+ *
+ * Encoding:
+ *
+ * For encoding parameter values, the built-in encodeURIComponent JavaScript
+ * method must be used. For automatic encoding of post data in  the
+ *  switch can be set to true (default). The encoding
+ * will be carried out using the conte type of the page. That is, the page
+ * containting the editor should contain a meta tag in the header, eg.
+ * 
+ *
+ * Example:
+ *
+ * (code)
+ * var onload = function(req)
+ * {
+ *   mxUtils.alert(req.getDocumentElement());
+ * }
+ *
+ * var onerror = function(req)
+ * {
+ *   mxUtils.alert('Error');
+ * }
+ * new mxXmlRequest(url, 'key=value').send(onload, onerror);
+ * (end)
+ *
+ * Sends an asynchronous POST request to the specified URL.
+ *
+ * Example:
+ *
+ * (code)
+ * var req = new mxXmlRequest(url, 'key=value', 'POST', false);
+ * req.send();
+ * mxUtils.alert(req.getDocumentElement());
+ * (end)
+ *
+ * Sends a synchronous POST request to the specified URL.
+ *
+ * Example:
+ *
+ * (code)
+ * var encoder = new mxCodec();
+ * var result = encoder.encode(graph.getModel());
+ * var xml = encodeURIComponent(mxUtils.getXml(result));
+ * new mxXmlRequest(url, 'xml='+xml).send();
+ * (end)
+ *
+ * Sends an encoded graph model to the specified URL using xml as the
+ * parameter name. The parameter can then be retrieved in C# as follows:
+ *
+ * (code)
+ * string xml = HttpUtility.UrlDecode(context.Request.Params["xml"]);
+ * (end)
+ *
+ * Or in Java as follows:
+ *
+ * (code)
+ * String xml = URLDecoder.decode(request.getParameter("xml"), "UTF-8").replace("\n", "
");
+ * (end)
+ *
+ * Note that the linefeeds should only be replaced if the XML is
+ * processed in Java, for example when creating an image.
+ *
+ * Constructor: mxXmlRequest
+ *
+ * Constructs an XML HTTP request.
+ *
+ * Parameters:
+ *
+ * url - Target URL of the request.
+ * params - Form encoded parameters to send with a POST request.
+ * method - String that specifies the request method. Possible values are
+ * POST and GET. Default is POST.
+ * async - Boolean specifying if an asynchronous request should be used.
+ * Default is true.
+ * username - String specifying the username to be used for the request.
+ * password - String specifying the password to be used for the request.
+ */
+function mxXmlRequest(url, params, method, async, username, password)
+{
+	this.url = url;
+	this.params = params;
+	this.method = method || 'POST';
+	this.async = (async != null) ? async : true;
+	this.username = username;
+	this.password = password;
+};
+
+/**
+ * Variable: url
+ *
+ * Holds the target URL of the request.
+ */
+mxXmlRequest.prototype.url = null;
+
+/**
+ * Variable: params
+ *
+ * Holds the form encoded data for the POST request.
+ */
+mxXmlRequest.prototype.params = null;
+
+/**
+ * Variable: method
+ *
+ * Specifies the request method. Possible values are POST and GET. Default
+ * is POST.
+ */
+mxXmlRequest.prototype.method = null;
+
+/**
+ * Variable: async
+ *
+ * Boolean indicating if the request is asynchronous.
+ */
+mxXmlRequest.prototype.async = null;
+
+/**
+ * Variable: binary
+ *
+ * Boolean indicating if the request is binary. This option is ignored in IE.
+ * In all other browsers the requested mime type is set to
+ * text/plain; charset=x-user-defined. Default is false.
+ */
+mxXmlRequest.prototype.binary = false;
+
+/**
+ * Variable: withCredentials
+ *
+ * Specifies if withCredentials should be used in HTML5-compliant browsers. Default is
+ * false.
+ */
+mxXmlRequest.prototype.withCredentials = false;
+
+/**
+ * Variable: username
+ *
+ * Specifies the username to be used for authentication.
+ */
+mxXmlRequest.prototype.username = null;
+
+/**
+ * Variable: password
+ *
+ * Specifies the password to be used for authentication.
+ */
+mxXmlRequest.prototype.password = null;
+
+/**
+ * Variable: request
+ *
+ * Holds the inner, browser-specific request object.
+ */
+mxXmlRequest.prototype.request = null;
+
+/**
+ * Variable: decodeSimulateValues
+ *
+ * Specifies if request values should be decoded as URIs before setting the
+ * textarea value in . Defaults to false for backwards compatibility,
+ * to avoid another decode on the server this should be set to true.
+ */
+mxXmlRequest.prototype.decodeSimulateValues = false;
+
+/**
+ * Function: isBinary
+ *
+ * Returns .
+ */
+mxXmlRequest.prototype.isBinary = function()
+{
+	return this.binary;
+};
+
+/**
+ * Function: setBinary
+ *
+ * Sets .
+ */
+mxXmlRequest.prototype.setBinary = function(value)
+{
+	this.binary = value;
+};
+
+/**
+ * Function: getText
+ *
+ * Returns the response as a string.
+ */
+mxXmlRequest.prototype.getText = function()
+{
+	return this.request.responseText;
+};
+
+/**
+ * Function: isReady
+ *
+ * Returns true if the response is ready.
+ */
+mxXmlRequest.prototype.isReady = function()
+{
+	return this.request.readyState == 4;
+};
+
+/**
+ * Function: getDocumentElement
+ *
+ * Returns the document element of the response XML document.
+ */
+mxXmlRequest.prototype.getDocumentElement = function()
+{
+	var doc = this.getXml();
+
+	if (doc != null)
+	{
+		return doc.documentElement;
+	}
+
+	return null;
+};
+
+/**
+ * Function: getXml
+ *
+ * Returns the response as an XML document. Use  to get
+ * the document element of the XML document.
+ */
+mxXmlRequest.prototype.getXml = function()
+{
+	var xml = this.request.responseXML;
+
+	// Handles missing response headers in IE, the first condition handles
+	// the case where responseXML is there, but using its nodes leads to
+	// type errors in the mxCellCodec when putting the nodes into a new
+	// document. This happens in IE9 standards mode and with XML user
+	// objects only, as they are used directly as values in cells.
+	if (document.documentMode >= 9 || xml == null || xml.documentElement == null)
+	{
+		xml = mxUtils.parseXml(this.request.responseText);
+	}
+
+	return xml;
+};
+
+/**
+ * Function: getText
+ *
+ * Returns the response as a string.
+ */
+mxXmlRequest.prototype.getText = function()
+{
+	return this.request.responseText;
+};
+
+/**
+ * Function: getStatus
+ *
+ * Returns the status as a number, eg. 404 for "Not found" or 200 for "OK".
+ * Note: The NS_ERROR_NOT_AVAILABLE for invalid responses cannot be cought.
+ */
+mxXmlRequest.prototype.getStatus = function()
+{
+	return this.request.status;
+};
+
+/**
+ * Function: create
+ *
+ * Creates and returns the inner  object.
+ */
+mxXmlRequest.prototype.create = function()
+{
+	if (window.XMLHttpRequest)
+	{
+		return function()
+		{
+			var req = new XMLHttpRequest();
+
+			// TODO: Check for overrideMimeType required here?
+			if (this.isBinary() && req.overrideMimeType)
+			{
+				req.overrideMimeType('text/plain; charset=x-user-defined');
+			}
+
+			return req;
+		};
+	}
+	else if (typeof(ActiveXObject) != 'undefined')
+	{
+		return function()
+		{
+			// TODO: Implement binary option
+			return new ActiveXObject('Microsoft.XMLHTTP');
+		};
+	}
+}();
+
+/**
+ * Function: send
+ *
+ * Send the  to the target URL using the specified functions to
+ * process the response asychronously.
+ *
+ * Note: Due to technical limitations, onerror is currently ignored.
+ *
+ * Parameters:
+ *
+ * onload - Function to be invoked if a successful response was received.
+ * onerror - Function to be called on any error.
+ * timeout - Optional timeout in ms before calling ontimeout.
+ * ontimeout - Optional function to execute on timeout.
+ */
+mxXmlRequest.prototype.send = function(onload, onerror, timeout, ontimeout)
+{
+	this.request = this.create();
+
+	if (this.request != null)
+	{
+		if (onload != null)
+		{
+			this.request.onreadystatechange = mxUtils.bind(this, function()
+			{
+				if (this.isReady())
+				{
+					onload(this);
+					this.request.onreadystatechaange = null;
+				}
+			});
+		}
+
+		this.request.open(this.method, this.url, this.async,
+			this.username, this.password);
+		this.setRequestHeaders(this.request, this.params);
+
+		if (window.XMLHttpRequest && this.withCredentials)
+		{
+			this.request.withCredentials = 'true';
+		}
+
+		if (!mxClient.IS_QUIRKS && (document.documentMode == null || document.documentMode > 9) &&
+			window.XMLHttpRequest && timeout != null && ontimeout != null)
+		{
+			this.request.timeout = timeout;
+			this.request.ontimeout = ontimeout;
+		}
+
+		this.request.send(this.params);
+	}
+};
+
+/**
+ * Function: setRequestHeaders
+ *
+ * Sets the headers for the given request and parameters. This sets the
+ * content-type to application/x-www-form-urlencoded if any params exist.
+ *
+ * Example:
+ *
+ * (code)
+ * request.setRequestHeaders = function(request, params)
+ * {
+ *   if (params != null)
+ *   {
+ *     request.setRequestHeader('Content-Type',
+ *             'multipart/form-data');
+ *     request.setRequestHeader('Content-Length',
+ *             params.length);
+ *   }
+ * };
+ * (end)
+ *
+ * Use the code above before calling  if you require a
+ * multipart/form-data request.
+ */
+mxXmlRequest.prototype.setRequestHeaders = function(request, params)
+{
+	if (params != null)
+	{
+		request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+	}
+};
+
+/**
+ * Function: simulate
+ *
+ * Creates and posts a request to the given target URL using a dynamically
+ * created form inside the given document.
+ *
+ * Parameters:
+ *
+ * docs - Document that contains the form element.
+ * target - Target to send the form result to.
+ */
+mxXmlRequest.prototype.simulate = function(doc, target)
+{
+	doc = doc || document;
+	var old = null;
+
+	if (doc == document)
+	{
+		old = window.onbeforeunload;
+		window.onbeforeunload = null;
+	}
+
+	var form = doc.createElement('form');
+	form.setAttribute('method', this.method);
+	form.setAttribute('action', this.url);
+
+	if (target != null)
+	{
+		form.setAttribute('target', target);
+	}
+
+	form.style.display = 'none';
+	form.style.visibility = 'hidden';
+
+	var pars = (this.params.indexOf('&') > 0) ?
+		this.params.split('&') :
+		this.params.split();
+
+	// Adds the parameters as textareas to the form
+	for (var i=0; i 0)
+		{
+			var name = pars[i].substring(0, pos);
+			var value = pars[i].substring(pos+1);
+
+			if (this.decodeSimulateValues)
+			{
+				value = decodeURIComponent(value);
+			}
+
+			var textarea = doc.createElement('textarea');
+			textarea.setAttribute('wrap', 'off');
+			textarea.setAttribute('name', name);
+			mxUtils.write(textarea, value);
+			form.appendChild(textarea);
+		}
+	}
+
+	doc.body.appendChild(form);
+	form.submit();
+
+	if (form.parentNode != null)
+	{
+		form.parentNode.removeChild(form);
+	}
+
+	if (old != null)
+	{
+		window.onbeforeunload = old;
+	}
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+var mxClipboard =
+{
+	/**
+	 * Class: mxClipboard
+	 *
+	 * Singleton that implements a clipboard for graph cells.
+	 *
+	 * Example:
+	 *
+	 * (code)
+	 * mxClipboard.copy(graph);
+	 * mxClipboard.paste(graph2);
+	 * (end)
+	 *
+	 * This copies the selection cells from the graph to the clipboard and
+	 * pastes them into graph2.
+	 *
+	 * For fine-grained control of the clipboard data the 
+	 * and  functions can be overridden.
+	 *
+	 * To restore previous parents for pasted cells, the implementation for
+	 *  and  can be changed as follows.
+	 *
+	 * (code)
+	 * mxClipboard.copy = function(graph, cells)
+	 * {
+	 *   cells = cells || graph.getSelectionCells();
+	 *   var result = graph.getExportableCells(cells);
+	 *
+	 *   mxClipboard.parents = new Object();
+	 *
+	 *   for (var i = 0; i < result.length; i++)
+	 *   {
+	 *     mxClipboard.parents[i] = graph.model.getParent(cells[i]);
+	 *   }
+	 *
+	 *   mxClipboard.insertCount = 1;
+	 *   mxClipboard.setCells(graph.cloneCells(result));
+	 *
+	 *   return result;
+	 * };
+	 *
+	 * mxClipboard.paste = function(graph)
+	 * {
+	 *   if (!mxClipboard.isEmpty())
+	 *   {
+	 *     var cells = graph.getImportableCells(mxClipboard.getCells());
+	 *     var delta = mxClipboard.insertCount * mxClipboard.STEPSIZE;
+	 *     var parent = graph.getDefaultParent();
+	 *
+	 *     graph.model.beginUpdate();
+	 *     try
+	 *     {
+	 *       for (var i = 0; i < cells.length; i++)
+	 *       {
+	 *         var tmp = (mxClipboard.parents != null && graph.model.contains(mxClipboard.parents[i])) ?
+	 *              mxClipboard.parents[i] : parent;
+	 *         cells[i] = graph.importCells([cells[i]], delta, delta, tmp)[0];
+	 *       }
+	 *     }
+	 *     finally
+	 *     {
+	 *       graph.model.endUpdate();
+	 *     }
+	 *
+	 *     // Increments the counter and selects the inserted cells
+	 *     mxClipboard.insertCount++;
+	 *     graph.setSelectionCells(cells);
+	 *   }
+	 * };
+	 * (end)
+	 *
+	 * Variable: STEPSIZE
+	 *
+	 * Defines the step size to offset the cells after each paste operation.
+	 * Default is 10.
+	 */
+	STEPSIZE: 10,
+
+	/**
+	 * Variable: insertCount
+	 *
+	 * Counts the number of times the clipboard data has been inserted.
+	 */
+	insertCount: 1,
+
+	/**
+	 * Variable: cells
+	 *
+	 * Holds the array of  currently in the clipboard.
+	 */
+	cells: null,
+
+	/**
+	 * Function: setCells
+	 *
+	 * Sets the cells in the clipboard. Fires a  event.
+	 */
+	setCells: function(cells)
+	{
+		mxClipboard.cells = cells;
+	},
+
+	/**
+	 * Function: getCells
+	 *
+	 * Returns  the cells in the clipboard.
+	 */
+	getCells: function()
+	{
+		return mxClipboard.cells;
+	},
+
+	/**
+	 * Function: isEmpty
+	 *
+	 * Returns true if the clipboard currently has not data stored.
+	 */
+	isEmpty: function()
+	{
+		return mxClipboard.getCells() == null;
+	},
+
+	/**
+	 * Function: cut
+	 *
+	 * Cuts the given array of  from the specified graph.
+	 * If cells is null then the selection cells of the graph will
+	 * be used. Returns the cells that have been cut from the graph.
+	 *
+	 * Parameters:
+	 *
+	 * graph -  that contains the cells to be cut.
+	 * cells - Optional array of  to be cut.
+	 */
+	cut: function(graph, cells)
+	{
+		cells = mxClipboard.copy(graph, cells);
+		mxClipboard.insertCount = 0;
+		mxClipboard.removeCells(graph, cells);
+
+		return cells;
+	},
+
+	/**
+	 * Function: removeCells
+	 *
+	 * Hook to remove the given cells from the given graph after
+	 * a cut operation.
+	 *
+	 * Parameters:
+	 *
+	 * graph -  that contains the cells to be cut.
+	 * cells - Array of  to be cut.
+	 */
+	removeCells: function(graph, cells)
+	{
+		graph.removeCells(cells);
+	},
+
+	/**
+	 * Function: copy
+	 *
+	 * Copies the given array of  from the specified
+	 * graph to . Returns the original array of cells that has
+	 * been cloned. Descendants of cells in the array are ignored.
+	 *
+	 * Parameters:
+	 *
+	 * graph -  that contains the cells to be copied.
+	 * cells - Optional array of  to be copied.
+	 */
+	copy: function(graph, cells)
+	{
+		cells = cells || graph.getSelectionCells();
+		var result = graph.getExportableCells(graph.model.getTopmostCells(cells));
+		mxClipboard.insertCount = 1;
+		mxClipboard.setCells(graph.cloneCells(result));
+
+		return result;
+	},
+
+	/**
+	 * Function: paste
+	 *
+	 * Pastes the  into the specified graph restoring
+	 * the relation to , if possible. If the parents
+	 * are no longer in the graph or invisible then the
+	 * cells are added to the graph's default or into the
+	 * swimlane under the cell's new location if one exists.
+	 * The cells are added to the graph using 
+	 * and returned.
+	 *
+	 * Parameters:
+	 *
+	 * graph -  to paste the  into.
+	 */
+	paste: function(graph)
+	{
+		var cells = null;
+
+		if (!mxClipboard.isEmpty())
+		{
+			cells = graph.getImportableCells(mxClipboard.getCells());
+			var delta = mxClipboard.insertCount * mxClipboard.STEPSIZE;
+			var parent = graph.getDefaultParent();
+			cells = graph.importCells(cells, delta, delta, parent);
+
+			// Increments the counter and selects the inserted cells
+			mxClipboard.insertCount++;
+			graph.setSelectionCells(cells);
+		}
+
+		return cells;
+	}
+
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxWindow
+ *
+ * Basic window inside a document.
+ *
+ * Examples:
+ *
+ * Creating a simple window.
+ *
+ * (code)
+ * var tb = document.createElement('div');
+ * var wnd = new mxWindow('Title', tb, 100, 100, 200, 200, true, true);
+ * wnd.setVisible(true);
+ * (end)
+ *
+ * Creating a window that contains an iframe.
+ *
+ * (code)
+ * var frame = document.createElement('iframe');
+ * frame.setAttribute('width', '192px');
+ * frame.setAttribute('height', '172px');
+ * frame.setAttribute('src', 'http://www.example.com/');
+ * frame.style.backgroundColor = 'white';
+ *
+ * var w = document.body.clientWidth;
+ * var h = (document.body.clientHeight || document.documentElement.clientHeight);
+ * var wnd = new mxWindow('Title', frame, (w-200)/2, (h-200)/3, 200, 200);
+ * wnd.setVisible(true);
+ * (end)
+ *
+ * To limit the movement of a window, eg. to keep it from being moved beyond
+ * the top, left corner the following method can be overridden (recommended):
+ *
+ * (code)
+ * wnd.setLocation = function(x, y)
+ * {
+ *   x = Math.max(0, x);
+ *   y = Math.max(0, y);
+ *   mxWindow.prototype.setLocation.apply(this, arguments);
+ * };
+ * (end)
+ *
+ * Or the following event handler can be used:
+ *
+ * (code)
+ * wnd.addListener(mxEvent.MOVE, function(e)
+ * {
+ *   wnd.setLocation(Math.max(0, wnd.getX()), Math.max(0, wnd.getY()));
+ * });
+ * (end)
+ *
+ * To keep a window inside the current window:
+ *
+ * (code)
+ * mxEvent.addListener(window, 'resize', mxUtils.bind(this, function()
+ * {
+ *   var iw = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
+ *   var ih = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
+ *
+ *   var x = this.window.getX();
+ *   var y = this.window.getY();
+ *
+ *   if (x + this.window.table.clientWidth > iw)
+ *   {
+ *     x = Math.max(0, iw - this.window.table.clientWidth);
+ *   }
+ *
+ *   if (y + this.window.table.clientHeight > ih)
+ *   {
+ *     y = Math.max(0, ih - this.window.table.clientHeight);
+ *   }
+ *
+ *   if (this.window.getX() != x || this.window.getY() != y)
+ *   {
+ *     this.window.setLocation(x, y);
+ *   }
+ * }));
+ * (end)
+ *
+ * Event: mxEvent.MOVE_START
+ *
+ * Fires before the window is moved. The event property contains
+ * the corresponding mouse event.
+ *
+ * Event: mxEvent.MOVE
+ *
+ * Fires while the window is being moved. The event property
+ * contains the corresponding mouse event.
+ *
+ * Event: mxEvent.MOVE_END
+ *
+ * Fires after the window is moved. The event property contains
+ * the corresponding mouse event.
+ *
+ * Event: mxEvent.RESIZE_START
+ *
+ * Fires before the window is resized. The event property contains
+ * the corresponding mouse event.
+ *
+ * Event: mxEvent.RESIZE
+ *
+ * Fires while the window is being resized. The event property
+ * contains the corresponding mouse event.
+ *
+ * Event: mxEvent.RESIZE_END
+ *
+ * Fires after the window is resized. The event property contains
+ * the corresponding mouse event.
+ *
+ * Event: mxEvent.MAXIMIZE
+ *
+ * Fires after the window is maximized. The event property
+ * contains the corresponding mouse event.
+ *
+ * Event: mxEvent.MINIMIZE
+ *
+ * Fires after the window is minimized. The event property
+ * contains the corresponding mouse event.
+ *
+ * Event: mxEvent.NORMALIZE
+ *
+ * Fires after the window is normalized, that is, it returned from
+ * maximized or minimized state. The event property contains the
+ * corresponding mouse event.
+ *
+ * Event: mxEvent.ACTIVATE
+ *
+ * Fires after a window is activated. The previousWindow property
+ * contains the previous window. The event sender is the active window.
+ *
+ * Event: mxEvent.SHOW
+ *
+ * Fires after the window is shown. This event has no properties.
+ *
+ * Event: mxEvent.HIDE
+ *
+ * Fires after the window is hidden. This event has no properties.
+ *
+ * Event: mxEvent.CLOSE
+ *
+ * Fires before the window is closed. The event property contains
+ * the corresponding mouse event.
+ *
+ * Event: mxEvent.DESTROY
+ *
+ * Fires before the window is destroyed. This event has no properties.
+ *
+ * Constructor: mxWindow
+ *
+ * Constructs a new window with the given dimension and title to display
+ * the specified content. The window elements use the given style as a
+ * prefix for the classnames of the respective window elements, namely,
+ * the window title and window pane. The respective postfixes are appended
+ * to the given stylename as follows:
+ *
+ *   style - Base style for the window.
+ *   style+Title - Style for the window title.
+ *   style+Pane - Style for the window pane.
+ *
+ * The default value for style is mxWindow, resulting in the following
+ * classnames for the window elements: mxWindow, mxWindowTitle and
+ * mxWindowPane.
+ *
+ * If replaceNode is given then the window replaces the given DOM node in
+ * the document.
+ *
+ * Parameters:
+ *
+ * title - String that represents the title of the new window.
+ * content - DOM node that is used as the window content.
+ * x - X-coordinate of the window location.
+ * y - Y-coordinate of the window location.
+ * width - Width of the window.
+ * height - Optional height of the window. Default is to match the height
+ * of the content at the specified width.
+ * minimizable - Optional boolean indicating if the window is minimizable.
+ * Default is true.
+ * movable - Optional boolean indicating if the window is movable. Default
+ * is true.
+ * replaceNode - Optional DOM node that the window should replace.
+ * style - Optional base classname for the window elements. Default is
+ * mxWindow.
+ */
+function mxWindow(title, content, x, y, width, height, minimizable, movable, replaceNode, style)
+{
+	if (content != null)
+	{
+		minimizable = (minimizable != null) ? minimizable : true;
+		this.content = content;
+		this.init(x, y, width, height, style);
+
+		this.installMaximizeHandler();
+		this.installMinimizeHandler();
+		this.installCloseHandler();
+		this.setMinimizable(minimizable);
+		this.setTitle(title);
+
+		if (movable == null || movable)
+		{
+			this.installMoveHandler();
+		}
+
+		if (replaceNode != null && replaceNode.parentNode != null)
+		{
+			replaceNode.parentNode.replaceChild(this.div, replaceNode);
+		}
+		else
+		{
+			document.body.appendChild(this.div);
+		}
+	}
+};
+
+/**
+ * Extends mxEventSource.
+ */
+mxWindow.prototype = new mxEventSource();
+mxWindow.prototype.constructor = mxWindow;
+
+/**
+ * Variable: closeImage
+ *
+ * URL of the image to be used for the close icon in the titlebar.
+ */
+mxWindow.prototype.closeImage = mxClient.imageBasePath + '/close.gif';
+
+/**
+ * Variable: minimizeImage
+ *
+ * URL of the image to be used for the minimize icon in the titlebar.
+ */
+mxWindow.prototype.minimizeImage = mxClient.imageBasePath + '/minimize.gif';
+
+/**
+ * Variable: normalizeImage
+ *
+ * URL of the image to be used for the normalize icon in the titlebar.
+ */
+mxWindow.prototype.normalizeImage = mxClient.imageBasePath + '/normalize.gif';
+
+/**
+ * Variable: maximizeImage
+ *
+ * URL of the image to be used for the maximize icon in the titlebar.
+ */
+mxWindow.prototype.maximizeImage = mxClient.imageBasePath + '/maximize.gif';
+
+/**
+ * Variable: normalizeImage
+ *
+ * URL of the image to be used for the resize icon.
+ */
+mxWindow.prototype.resizeImage = mxClient.imageBasePath + '/resize.gif';
+
+/**
+ * Variable: visible
+ *
+ * Boolean flag that represents the visible state of the window.
+ */
+mxWindow.prototype.visible = false;
+
+/**
+ * Variable: minimumSize
+ *
+ *  that specifies the minimum width and height of the window.
+ * Default is (50, 40).
+ */
+mxWindow.prototype.minimumSize = new mxRectangle(0, 0, 50, 40);
+
+/**
+ * Variable: destroyOnClose
+ *
+ * Specifies if the window should be destroyed when it is closed. If this
+ * is false then the window is hidden using . Default is true.
+ */
+mxWindow.prototype.destroyOnClose = true;
+
+/**
+ * Variable: contentHeightCorrection
+ *
+ * Defines the correction factor for computing the height of the contentWrapper.
+ * Default is 6 for IE 7/8 standards mode and 2 for all other browsers and modes.
+ */
+mxWindow.prototype.contentHeightCorrection = (document.documentMode == 8 || document.documentMode == 7) ? 6 : 2;
+
+/**
+ * Variable: title
+ *
+ * Reference to the DOM node (TD) that contains the title.
+ */
+mxWindow.prototype.title = null;
+
+/**
+ * Variable: content
+ *
+ * Reference to the DOM node that represents the window content.
+ */
+mxWindow.prototype.content = null;
+
+/**
+ * Function: init
+ *
+ * Initializes the DOM tree that represents the window.
+ */
+mxWindow.prototype.init = function(x, y, width, height, style)
+{
+	style = (style != null) ? style : 'mxWindow';
+
+	this.div = document.createElement('div');
+	this.div.className = style;
+
+	this.div.style.left = x + 'px';
+	this.div.style.top = y + 'px';
+	this.table = document.createElement('table');
+	this.table.className = style;
+
+	// Disables built-in pan and zoom in IE10 and later
+	if (mxClient.IS_POINTER)
+	{
+		this.div.style.touchAction = 'none';
+	}
+
+	// Workaround for table size problems in FF
+	if (width != null)
+	{
+		if (!mxClient.IS_QUIRKS)
+		{
+			this.div.style.width = width + 'px';
+		}
+
+		this.table.style.width = width + 'px';
+	}
+
+	if (height != null)
+	{
+		if (!mxClient.IS_QUIRKS)
+		{
+			this.div.style.height = height + 'px';
+		}
+
+		this.table.style.height = height + 'px';
+	}
+
+	// Creates title row
+	var tbody = document.createElement('tbody');
+	var tr = document.createElement('tr');
+
+	this.title = document.createElement('td');
+	this.title.className = style + 'Title';
+
+	this.buttons = document.createElement('div');
+	this.buttons.style.position = 'absolute';
+	this.buttons.style.display = 'inline-block';
+	this.buttons.style.right = '4px';
+	this.buttons.style.top = '5px';
+	this.title.appendChild(this.buttons);
+
+	tr.appendChild(this.title);
+	tbody.appendChild(tr);
+
+	// Creates content row and table cell
+	tr = document.createElement('tr');
+	this.td = document.createElement('td');
+	this.td.className = style + 'Pane';
+
+	if (document.documentMode == 7)
+	{
+		this.td.style.height = '100%';
+	}
+
+	this.contentWrapper = document.createElement('div');
+	this.contentWrapper.className = style + 'Pane';
+	this.contentWrapper.style.width = '100%';
+	this.contentWrapper.appendChild(this.content);
+
+	// Workaround for div around div restricts height
+	// of inner div if outerdiv has hidden overflow
+	if (mxClient.IS_QUIRKS || this.content.nodeName.toUpperCase() != 'DIV')
+	{
+		this.contentWrapper.style.height = '100%';
+	}
+
+	// Puts all content into the DOM
+	this.td.appendChild(this.contentWrapper);
+	tr.appendChild(this.td);
+	tbody.appendChild(tr);
+	this.table.appendChild(tbody);
+	this.div.appendChild(this.table);
+
+	// Puts the window on top of other windows when clicked
+	var activator = mxUtils.bind(this, function(evt)
+	{
+		this.activate();
+	});
+
+	mxEvent.addGestureListeners(this.title, activator);
+	mxEvent.addGestureListeners(this.table, activator);
+
+	this.hide();
+};
+
+/**
+ * Function: setTitle
+ *
+ * Sets the window title to the given string. HTML markup inside the title
+ * will be escaped.
+ */
+mxWindow.prototype.setTitle = function(title)
+{
+	// Removes all text content nodes (normally just one)
+	var child = this.title.firstChild;
+
+	while (child != null)
+	{
+		var next = child.nextSibling;
+
+		if (child.nodeType == mxConstants.NODETYPE_TEXT)
+		{
+			child.parentNode.removeChild(child);
+		}
+
+		child = next;
+	}
+
+	mxUtils.write(this.title, title || '');
+	this.title.appendChild(this.buttons);
+};
+
+/**
+ * Function: setScrollable
+ *
+ * Sets if the window contents should be scrollable.
+ */
+mxWindow.prototype.setScrollable = function(scrollable)
+{
+	// Workaround for hang in Presto 2.5.22 (Opera 10.5)
+	if (navigator.userAgent.indexOf('Presto/2.5') < 0)
+	{
+		if (scrollable)
+		{
+			this.contentWrapper.style.overflow = 'auto';
+		}
+		else
+		{
+			this.contentWrapper.style.overflow = 'hidden';
+		}
+	}
+};
+
+/**
+ * Function: activate
+ *
+ * Puts the window on top of all other windows.
+ */
+mxWindow.prototype.activate = function()
+{
+	if (mxWindow.activeWindow != this)
+	{
+		var style = mxUtils.getCurrentStyle(this.getElement());
+		var index = (style != null) ? style.zIndex : 3;
+
+		if (mxWindow.activeWindow)
+		{
+			var elt = mxWindow.activeWindow.getElement();
+
+			if (elt != null && elt.style != null)
+			{
+				elt.style.zIndex = index;
+			}
+		}
+
+		var previousWindow = mxWindow.activeWindow;
+		this.getElement().style.zIndex = parseInt(index) + 1;
+		mxWindow.activeWindow = this;
+
+		this.fireEvent(new mxEventObject(mxEvent.ACTIVATE, 'previousWindow', previousWindow));
+	}
+};
+
+/**
+ * Function: getElement
+ *
+ * Returuns the outermost DOM node that makes up the window.
+ */
+mxWindow.prototype.getElement = function()
+{
+	return this.div;
+};
+
+/**
+ * Function: fit
+ *
+ * Makes sure the window is inside the client area of the window.
+ */
+mxWindow.prototype.fit = function()
+{
+	mxUtils.fit(this.div);
+};
+
+/**
+ * Function: isResizable
+ *
+ * Returns true if the window is resizable.
+ */
+mxWindow.prototype.isResizable = function()
+{
+	if (this.resize != null)
+	{
+		return this.resize.style.display != 'none';
+	}
+
+	return false;
+};
+
+/**
+ * Function: setResizable
+ *
+ * Sets if the window should be resizable. To avoid interference with some
+ * built-in features of IE10 and later, the use of the following code is
+ * recommended if there are resizable s in the page:
+ *
+ * (code)
+ * if (mxClient.IS_POINTER)
+ * {
+ *   document.body.style.msTouchAction = 'none';
+ * }
+ * (end)
+ */
+mxWindow.prototype.setResizable = function(resizable)
+{
+	if (resizable)
+	{
+		if (this.resize == null)
+		{
+			this.resize = document.createElement('img');
+			this.resize.style.position = 'absolute';
+			this.resize.style.bottom = '2px';
+			this.resize.style.right = '2px';
+
+			this.resize.setAttribute('src', this.resizeImage);
+			this.resize.style.cursor = 'nw-resize';
+
+			var startX = null;
+			var startY = null;
+			var width = null;
+			var height = null;
+
+			var start = mxUtils.bind(this, function(evt)
+			{
+				// LATER: pointerdown starting on border of resize does start
+				// the drag operation but does not fire consecutive events via
+				// one of the listeners below (does pan instead).
+				// Workaround: document.body.style.msTouchAction = 'none'
+				this.activate();
+				startX = mxEvent.getClientX(evt);
+				startY = mxEvent.getClientY(evt);
+				width = this.div.offsetWidth;
+				height = this.div.offsetHeight;
+
+				mxEvent.addGestureListeners(document, null, dragHandler, dropHandler);
+				this.fireEvent(new mxEventObject(mxEvent.RESIZE_START, 'event', evt));
+				mxEvent.consume(evt);
+			});
+
+			// Adds a temporary pair of listeners to intercept
+			// the gesture event in the document
+			var dragHandler = mxUtils.bind(this, function(evt)
+			{
+				if (startX != null && startY != null)
+				{
+					var dx = mxEvent.getClientX(evt) - startX;
+					var dy = mxEvent.getClientY(evt) - startY;
+
+					this.setSize(width + dx, height + dy);
+
+					this.fireEvent(new mxEventObject(mxEvent.RESIZE, 'event', evt));
+					mxEvent.consume(evt);
+				}
+			});
+
+			var dropHandler = mxUtils.bind(this, function(evt)
+			{
+				if (startX != null && startY != null)
+				{
+					startX = null;
+					startY = null;
+					mxEvent.removeGestureListeners(document, null, dragHandler, dropHandler);
+					this.fireEvent(new mxEventObject(mxEvent.RESIZE_END, 'event', evt));
+					mxEvent.consume(evt);
+				}
+			});
+
+			mxEvent.addGestureListeners(this.resize, start, dragHandler, dropHandler);
+			this.div.appendChild(this.resize);
+		}
+		else
+		{
+			this.resize.style.display = 'inline';
+		}
+	}
+	else if (this.resize != null)
+	{
+		this.resize.style.display = 'none';
+	}
+};
+
+/**
+ * Function: setSize
+ *
+ * Sets the size of the window.
+ */
+mxWindow.prototype.setSize = function(width, height)
+{
+	width = Math.max(this.minimumSize.width, width);
+	height = Math.max(this.minimumSize.height, height);
+
+	// Workaround for table size problems in FF
+	if (!mxClient.IS_QUIRKS)
+	{
+		this.div.style.width =  width + 'px';
+		this.div.style.height = height + 'px';
+	}
+
+	this.table.style.width =  width + 'px';
+	this.table.style.height = height + 'px';
+
+	if (!mxClient.IS_QUIRKS)
+	{
+		this.contentWrapper.style.height = (this.div.offsetHeight -
+			this.title.offsetHeight - this.contentHeightCorrection) + 'px';
+	}
+};
+
+/**
+ * Function: setMinimizable
+ *
+ * Sets if the window is minimizable.
+ */
+mxWindow.prototype.setMinimizable = function(minimizable)
+{
+	this.minimize.style.display = (minimizable) ? '' : 'none';
+};
+
+/**
+ * Function: getMinimumSize
+ *
+ * Returns an  that specifies the size for the minimized window.
+ * A width or height of 0 means keep the existing width or height. This
+ * implementation returns the height of the window title and keeps the width.
+ */
+mxWindow.prototype.getMinimumSize = function()
+{
+	return new mxRectangle(0, 0, 0, this.title.offsetHeight);
+};
+
+/**
+ * Function: installMinimizeHandler
+ *
+ * Installs the event listeners required for minimizing the window.
+ */
+mxWindow.prototype.installMinimizeHandler = function()
+{
+	this.minimize = document.createElement('img');
+
+	this.minimize.setAttribute('src', this.minimizeImage);
+	this.minimize.setAttribute('title', 'Minimize');
+	this.minimize.style.cursor = 'pointer';
+	this.minimize.style.marginLeft = '2px';
+	this.minimize.style.display = 'none';
+
+	this.buttons.appendChild(this.minimize);
+
+	var minimized = false;
+	var maxDisplay = null;
+	var height = null;
+
+	var funct = mxUtils.bind(this, function(evt)
+	{
+		this.activate();
+
+		if (!minimized)
+		{
+			minimized = true;
+
+			this.minimize.setAttribute('src', this.normalizeImage);
+			this.minimize.setAttribute('title', 'Normalize');
+			this.contentWrapper.style.display = 'none';
+			maxDisplay = this.maximize.style.display;
+
+			this.maximize.style.display = 'none';
+			height = this.table.style.height;
+
+			var minSize = this.getMinimumSize();
+
+			if (minSize.height > 0)
+			{
+				if (!mxClient.IS_QUIRKS)
+				{
+					this.div.style.height = minSize.height + 'px';
+				}
+
+				this.table.style.height = minSize.height + 'px';
+			}
+
+			if (minSize.width > 0)
+			{
+				if (!mxClient.IS_QUIRKS)
+				{
+					this.div.style.width = minSize.width + 'px';
+				}
+
+				this.table.style.width = minSize.width + 'px';
+			}
+
+			if (this.resize != null)
+			{
+				this.resize.style.visibility = 'hidden';
+			}
+
+			this.fireEvent(new mxEventObject(mxEvent.MINIMIZE, 'event', evt));
+		}
+		else
+		{
+			minimized = false;
+
+			this.minimize.setAttribute('src', this.minimizeImage);
+			this.minimize.setAttribute('title', 'Minimize');
+			this.contentWrapper.style.display = ''; // default
+			this.maximize.style.display = maxDisplay;
+
+			if (!mxClient.IS_QUIRKS)
+			{
+				this.div.style.height = height;
+			}
+
+			this.table.style.height = height;
+
+			if (this.resize != null)
+			{
+				this.resize.style.visibility = '';
+			}
+
+			this.fireEvent(new mxEventObject(mxEvent.NORMALIZE, 'event', evt));
+		}
+
+		mxEvent.consume(evt);
+	});
+
+	mxEvent.addGestureListeners(this.minimize, funct);
+};
+
+/**
+ * Function: setMaximizable
+ *
+ * Sets if the window is maximizable.
+ */
+mxWindow.prototype.setMaximizable = function(maximizable)
+{
+	this.maximize.style.display = (maximizable) ? '' : 'none';
+};
+
+/**
+ * Function: installMaximizeHandler
+ *
+ * Installs the event listeners required for maximizing the window.
+ */
+mxWindow.prototype.installMaximizeHandler = function()
+{
+	this.maximize = document.createElement('img');
+
+	this.maximize.setAttribute('src', this.maximizeImage);
+	this.maximize.setAttribute('title', 'Maximize');
+	this.maximize.style.cursor = 'default';
+	this.maximize.style.marginLeft = '2px';
+	this.maximize.style.cursor = 'pointer';
+	this.maximize.style.display = 'none';
+
+	this.buttons.appendChild(this.maximize);
+
+	var maximized = false;
+	var x = null;
+	var y = null;
+	var height = null;
+	var width = null;
+	var minDisplay = null;
+
+	var funct = mxUtils.bind(this, function(evt)
+	{
+		this.activate();
+
+		if (this.maximize.style.display != 'none')
+		{
+			if (!maximized)
+			{
+				maximized = true;
+
+				this.maximize.setAttribute('src', this.normalizeImage);
+				this.maximize.setAttribute('title', 'Normalize');
+				this.contentWrapper.style.display = '';
+				minDisplay = this.minimize.style.display;
+				this.minimize.style.display = 'none';
+
+				// Saves window state
+				x = parseInt(this.div.style.left);
+				y = parseInt(this.div.style.top);
+				height = this.table.style.height;
+				width = this.table.style.width;
+
+				this.div.style.left = '0px';
+				this.div.style.top = '0px';
+				var docHeight = Math.max(document.body.clientHeight || 0, document.documentElement.clientHeight || 0);
+
+				if (!mxClient.IS_QUIRKS)
+				{
+					this.div.style.width = (document.body.clientWidth - 2) + 'px';
+					this.div.style.height = (docHeight - 2) + 'px';
+				}
+
+				this.table.style.width = (document.body.clientWidth - 2) + 'px';
+				this.table.style.height = (docHeight - 2) + 'px';
+
+				if (this.resize != null)
+				{
+					this.resize.style.visibility = 'hidden';
+				}
+
+				if (!mxClient.IS_QUIRKS)
+				{
+					var style = mxUtils.getCurrentStyle(this.contentWrapper);
+
+					if (style.overflow == 'auto' || this.resize != null)
+					{
+						this.contentWrapper.style.height = (this.div.offsetHeight -
+							this.title.offsetHeight - this.contentHeightCorrection) + 'px';
+					}
+				}
+
+				this.fireEvent(new mxEventObject(mxEvent.MAXIMIZE, 'event', evt));
+			}
+			else
+			{
+				maximized = false;
+
+				this.maximize.setAttribute('src', this.maximizeImage);
+				this.maximize.setAttribute('title', 'Maximize');
+				this.contentWrapper.style.display = '';
+				this.minimize.style.display = minDisplay;
+
+				// Restores window state
+				this.div.style.left = x+'px';
+				this.div.style.top = y+'px';
+
+				if (!mxClient.IS_QUIRKS)
+				{
+					this.div.style.height = height;
+					this.div.style.width = width;
+
+					var style = mxUtils.getCurrentStyle(this.contentWrapper);
+
+					if (style.overflow == 'auto' || this.resize != null)
+					{
+						this.contentWrapper.style.height = (this.div.offsetHeight -
+							this.title.offsetHeight - this.contentHeightCorrection) + 'px';
+					}
+				}
+
+				this.table.style.height = height;
+				this.table.style.width = width;
+
+				if (this.resize != null)
+				{
+					this.resize.style.visibility = '';
+				}
+
+				this.fireEvent(new mxEventObject(mxEvent.NORMALIZE, 'event', evt));
+			}
+
+			mxEvent.consume(evt);
+		}
+	});
+
+	mxEvent.addGestureListeners(this.maximize, funct);
+	mxEvent.addListener(this.title, 'dblclick', funct);
+};
+
+/**
+ * Function: installMoveHandler
+ *
+ * Installs the event listeners required for moving the window.
+ */
+mxWindow.prototype.installMoveHandler = function()
+{
+	this.title.style.cursor = 'move';
+
+	mxEvent.addGestureListeners(this.title,
+		mxUtils.bind(this, function(evt)
+		{
+			var startX = mxEvent.getClientX(evt);
+			var startY = mxEvent.getClientY(evt);
+			var x = this.getX();
+			var y = this.getY();
+
+			// Adds a temporary pair of listeners to intercept
+			// the gesture event in the document
+			var dragHandler = mxUtils.bind(this, function(evt)
+			{
+				var dx = mxEvent.getClientX(evt) - startX;
+				var dy = mxEvent.getClientY(evt) - startY;
+				this.setLocation(x + dx, y + dy);
+				this.fireEvent(new mxEventObject(mxEvent.MOVE, 'event', evt));
+				mxEvent.consume(evt);
+			});
+
+			var dropHandler = mxUtils.bind(this, function(evt)
+			{
+				mxEvent.removeGestureListeners(document, null, dragHandler, dropHandler);
+				this.fireEvent(new mxEventObject(mxEvent.MOVE_END, 'event', evt));
+				mxEvent.consume(evt);
+			});
+
+			mxEvent.addGestureListeners(document, null, dragHandler, dropHandler);
+			this.fireEvent(new mxEventObject(mxEvent.MOVE_START, 'event', evt));
+			mxEvent.consume(evt);
+		}));
+
+	// Disables built-in pan and zoom in IE10 and later
+	if (mxClient.IS_POINTER)
+	{
+		this.title.style.touchAction = 'none';
+	}
+};
+
+/**
+ * Function: setLocation
+ *
+ * Sets the upper, left corner of the window.
+ */
+ mxWindow.prototype.setLocation = function(x, y)
+ {
+	this.div.style.left = x + 'px';
+	this.div.style.top = y + 'px';
+ };
+
+/**
+ * Function: getX
+ *
+ * Returns the current position on the x-axis.
+ */
+mxWindow.prototype.getX = function()
+{
+	return parseInt(this.div.style.left);
+};
+
+/**
+ * Function: getY
+ *
+ * Returns the current position on the y-axis.
+ */
+mxWindow.prototype.getY = function()
+{
+	return parseInt(this.div.style.top);
+};
+
+/**
+ * Function: installCloseHandler
+ *
+ * Adds the  as a new image node in  and installs the
+ *  event.
+ */
+mxWindow.prototype.installCloseHandler = function()
+{
+	this.closeImg = document.createElement('img');
+
+	this.closeImg.setAttribute('src', this.closeImage);
+	this.closeImg.setAttribute('title', 'Close');
+	this.closeImg.style.marginLeft = '2px';
+	this.closeImg.style.cursor = 'pointer';
+	this.closeImg.style.display = 'none';
+
+	this.buttons.appendChild(this.closeImg);
+
+	mxEvent.addGestureListeners(this.closeImg,
+		mxUtils.bind(this, function(evt)
+		{
+			this.fireEvent(new mxEventObject(mxEvent.CLOSE, 'event', evt));
+
+			if (this.destroyOnClose)
+			{
+				this.destroy();
+			}
+			else
+			{
+				this.setVisible(false);
+			}
+
+			mxEvent.consume(evt);
+		}));
+};
+
+/**
+ * Function: setImage
+ *
+ * Sets the image associated with the window.
+ *
+ * Parameters:
+ *
+ * image - URL of the image to be used.
+ */
+mxWindow.prototype.setImage = function(image)
+{
+	this.image = document.createElement('img');
+	this.image.setAttribute('src', image);
+	this.image.setAttribute('align', 'left');
+	this.image.style.marginRight = '4px';
+	this.image.style.marginLeft = '0px';
+	this.image.style.marginTop = '-2px';
+
+	this.title.insertBefore(this.image, this.title.firstChild);
+};
+
+/**
+ * Function: setClosable
+ *
+ * Sets the image associated with the window.
+ *
+ * Parameters:
+ *
+ * closable - Boolean specifying if the window should be closable.
+ */
+mxWindow.prototype.setClosable = function(closable)
+{
+	this.closeImg.style.display = (closable) ? '' : 'none';
+};
+
+/**
+ * Function: isVisible
+ *
+ * Returns true if the window is visible.
+ */
+mxWindow.prototype.isVisible = function()
+{
+	if (this.div != null)
+	{
+		return this.div.style.display != 'none';
+	}
+
+	return false;
+};
+
+/**
+ * Function: setVisible
+ *
+ * Shows or hides the window depending on the given flag.
+ *
+ * Parameters:
+ *
+ * visible - Boolean indicating if the window should be made visible.
+ */
+mxWindow.prototype.setVisible = function(visible)
+{
+	if (this.div != null && this.isVisible() != visible)
+	{
+		if (visible)
+		{
+			this.show();
+		}
+		else
+		{
+			this.hide();
+		}
+	}
+};
+
+/**
+ * Function: show
+ *
+ * Shows the window.
+ */
+mxWindow.prototype.show = function()
+{
+	this.div.style.display = '';
+	this.activate();
+
+	var style = mxUtils.getCurrentStyle(this.contentWrapper);
+
+	if (!mxClient.IS_QUIRKS && (style.overflow == 'auto' || this.resize != null) &&
+		this.contentWrapper.style.display != 'none')
+	{
+		this.contentWrapper.style.height = (this.div.offsetHeight -
+				this.title.offsetHeight - this.contentHeightCorrection) + 'px';
+	}
+
+	this.fireEvent(new mxEventObject(mxEvent.SHOW));
+};
+
+/**
+ * Function: hide
+ *
+ * Hides the window.
+ */
+mxWindow.prototype.hide = function()
+{
+	this.div.style.display = 'none';
+	this.fireEvent(new mxEventObject(mxEvent.HIDE));
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the window and removes all associated resources. Fires a
+ *  event prior to destroying the window.
+ */
+mxWindow.prototype.destroy = function()
+{
+	this.fireEvent(new mxEventObject(mxEvent.DESTROY));
+
+	if (this.div != null)
+	{
+		mxEvent.release(this.div);
+		this.div.parentNode.removeChild(this.div);
+		this.div = null;
+	}
+
+	this.title = null;
+	this.content = null;
+	this.contentWrapper = null;
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxForm
+ *
+ * A simple class for creating HTML forms.
+ *
+ * Constructor: mxForm
+ *
+ * Creates a HTML table using the specified classname.
+ */
+function mxForm(className)
+{
+	this.table = document.createElement('table');
+	this.table.className = className;
+	this.body = document.createElement('tbody');
+
+	this.table.appendChild(this.body);
+};
+
+/**
+ * Variable: table
+ *
+ * Holds the DOM node that represents the table.
+ */
+mxForm.prototype.table = null;
+
+/**
+ * Variable: body
+ *
+ * Holds the DOM node that represents the tbody (table body). New rows
+ * can be added to this object using DOM API.
+ */
+mxForm.prototype.body = false;
+
+/**
+ * Function: getTable
+ *
+ * Returns the table that contains this form.
+ */
+mxForm.prototype.getTable = function()
+{
+	return this.table;
+};
+
+/**
+ * Function: addButtons
+ *
+ * Helper method to add an OK and Cancel button using the respective
+ * functions.
+ */
+mxForm.prototype.addButtons = function(okFunct, cancelFunct)
+{
+	var tr = document.createElement('tr');
+	var td = document.createElement('td');
+	tr.appendChild(td);
+	td = document.createElement('td');
+
+	// Adds the ok button
+	var button = document.createElement('button');
+	mxUtils.write(button, mxResources.get('ok') || 'OK');
+	td.appendChild(button);
+
+	mxEvent.addListener(button, 'click', function()
+	{
+		okFunct();
+	});
+
+	// Adds the cancel button
+	button = document.createElement('button');
+	mxUtils.write(button, mxResources.get('cancel') || 'Cancel');
+	td.appendChild(button);
+
+	mxEvent.addListener(button, 'click', function()
+	{
+		cancelFunct();
+	});
+
+	tr.appendChild(td);
+	this.body.appendChild(tr);
+};
+
+/**
+ * Function: addText
+ *
+ * Adds an input for the given name, type and value and returns it.
+ */
+mxForm.prototype.addText = function(name, value, type)
+{
+	var input = document.createElement('input');
+
+	input.setAttribute('type', type || 'text');
+	input.value = value;
+
+	return this.addField(name, input);
+};
+
+/**
+ * Function: addCheckbox
+ *
+ * Adds a checkbox for the given name and value and returns the textfield.
+ */
+mxForm.prototype.addCheckbox = function(name, value)
+{
+	var input = document.createElement('input');
+
+	input.setAttribute('type', 'checkbox');
+	this.addField(name, input);
+
+	// IE can only change the checked value if the input is inside the DOM
+	if (value)
+	{
+		input.checked = true;
+	}
+
+	return input;
+};
+
+/**
+ * Function: addTextarea
+ *
+ * Adds a textarea for the given name and value and returns the textarea.
+ */
+mxForm.prototype.addTextarea = function(name, value, rows)
+{
+	var input = document.createElement('textarea');
+
+	if (mxClient.IS_NS)
+	{
+		rows--;
+	}
+
+	input.setAttribute('rows', rows || 2);
+	input.value = value;
+
+	return this.addField(name, input);
+};
+
+/**
+ * Function: addCombo
+ *
+ * Adds a combo for the given name and returns the combo.
+ */
+mxForm.prototype.addCombo = function(name, isMultiSelect, size)
+{
+	var select = document.createElement('select');
+
+	if (size != null)
+	{
+		select.setAttribute('size', size);
+	}
+
+	if (isMultiSelect)
+	{
+		select.setAttribute('multiple', 'true');
+	}
+
+	return this.addField(name, select);
+};
+
+/**
+ * Function: addOption
+ *
+ * Adds an option for the given label to the specified combo.
+ */
+mxForm.prototype.addOption = function(combo, label, value, isSelected)
+{
+	var option = document.createElement('option');
+
+	mxUtils.writeln(option, label);
+	option.setAttribute('value', value);
+
+	if (isSelected)
+	{
+		option.setAttribute('selected', isSelected);
+	}
+
+	combo.appendChild(option);
+};
+
+/**
+ * Function: addField
+ *
+ * Adds a new row with the name and the input field in two columns and
+ * returns the given input.
+ */
+mxForm.prototype.addField = function(name, input)
+{
+	var tr = document.createElement('tr');
+	var td = document.createElement('td');
+	mxUtils.write(td, name);
+	tr.appendChild(td);
+
+	td = document.createElement('td');
+	td.appendChild(input);
+	tr.appendChild(td);
+	this.body.appendChild(tr);
+
+	return input;
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxImage
+ *
+ * Encapsulates the URL, width and height of an image.
+ *
+ * Constructor: mxImage
+ *
+ * Constructs a new image.
+ */
+function mxImage(src, width, height)
+{
+	this.src = src;
+	this.width = width;
+	this.height = height;
+};
+
+/**
+ * Variable: src
+ *
+ * String that specifies the URL of the image.
+ */
+mxImage.prototype.src = null;
+
+/**
+ * Variable: width
+ *
+ * Integer that specifies the width of the image.
+ */
+mxImage.prototype.width = null;
+
+/**
+ * Variable: height
+ *
+ * Integer that specifies the height of the image.
+ */
+mxImage.prototype.height = null;
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxDivResizer
+ *
+ * Maintains the size of a div element in Internet Explorer. This is a
+ * workaround for the right and bottom style being ignored in IE.
+ *
+ * If you need a div to cover the scrollwidth and -height of a document,
+ * then you can use this class as follows:
+ *
+ * (code)
+ * var resizer = new mxDivResizer(background);
+ * resizer.getDocumentHeight = function()
+ * {
+ *   return document.body.scrollHeight;
+ * }
+ * resizer.getDocumentWidth = function()
+ * {
+ *   return document.body.scrollWidth;
+ * }
+ * resizer.resize();
+ * (end)
+ *
+ * Constructor: mxDivResizer
+ *
+ * Constructs an object that maintains the size of a div
+ * element when the window is being resized. This is only
+ * required for Internet Explorer as it ignores the respective
+ * stylesheet information for DIV elements.
+ *
+ * Parameters:
+ *
+ * div - Reference to the DOM node whose size should be maintained.
+ * container - Optional Container that contains the div. Default is the
+ * window.
+ */
+function mxDivResizer(div, container)
+{
+	if (div.nodeName.toLowerCase() == 'div')
+	{
+		if (container == null)
+		{
+			container = window;
+		}
+
+		this.div = div;
+		var style = mxUtils.getCurrentStyle(div);
+
+		if (style != null)
+		{
+			this.resizeWidth = style.width == 'auto';
+			this.resizeHeight = style.height == 'auto';
+		}
+
+		mxEvent.addListener(container, 'resize',
+			mxUtils.bind(this, function(evt)
+			{
+				if (!this.handlingResize)
+				{
+					this.handlingResize = true;
+					this.resize();
+					this.handlingResize = false;
+				}
+			})
+		);
+
+		this.resize();
+	}
+};
+
+/**
+ * Function: resizeWidth
+ *
+ * Boolean specifying if the width should be updated.
+ */
+mxDivResizer.prototype.resizeWidth = true;
+
+/**
+ * Function: resizeHeight
+ *
+ * Boolean specifying if the height should be updated.
+ */
+mxDivResizer.prototype.resizeHeight = true;
+
+/**
+ * Function: handlingResize
+ *
+ * Boolean specifying if the width should be updated.
+ */
+mxDivResizer.prototype.handlingResize = false;
+
+/**
+ * Function: resize
+ *
+ * Updates the style of the DIV after the window has been resized.
+ */
+mxDivResizer.prototype.resize = function()
+{
+	var w = this.getDocumentWidth();
+	var h = this.getDocumentHeight();
+
+	var l = parseInt(this.div.style.left);
+	var r = parseInt(this.div.style.right);
+	var t = parseInt(this.div.style.top);
+	var b = parseInt(this.div.style.bottom);
+
+	if (this.resizeWidth &&
+		!isNaN(l) &&
+		!isNaN(r) &&
+		l >= 0 &&
+		r >= 0 &&
+		w - r - l > 0)
+	{
+		this.div.style.width = (w - r - l)+'px';
+	}
+
+	if (this.resizeHeight &&
+		!isNaN(t) &&
+		!isNaN(b) &&
+		t >= 0 &&
+		b >= 0 &&
+		h - t - b > 0)
+	{
+		this.div.style.height = (h - t - b)+'px';
+	}
+};
+
+/**
+ * Function: getDocumentWidth
+ *
+ * Hook for subclassers to return the width of the document (without
+ * scrollbars).
+ */
+mxDivResizer.prototype.getDocumentWidth = function()
+{
+	return document.body.clientWidth;
+};
+
+/**
+ * Function: getDocumentHeight
+ *
+ * Hook for subclassers to return the height of the document (without
+ * scrollbars).
+ */
+mxDivResizer.prototype.getDocumentHeight = function()
+{
+	return document.body.clientHeight;
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxDragSource
+ *
+ * Wrapper to create a drag source from a DOM element so that the element can
+ * be dragged over a graph and dropped into the graph as a new cell.
+ *
+ * Problem is that in the dropHandler the current preview location is not
+ * available, so the preview and the dropHandler must match.
+ *
+ * Constructor: mxDragSource
+ *
+ * Constructs a new drag source for the given element.
+ */
+function mxDragSource(element, dropHandler)
+{
+	this.element = element;
+	this.dropHandler = dropHandler;
+
+	// Handles a drag gesture on the element
+	mxEvent.addGestureListeners(element, mxUtils.bind(this, function(evt)
+	{
+		this.mouseDown(evt);
+	}));
+
+	// Prevents native drag and drop
+	mxEvent.addListener(element, 'dragstart', function(evt)
+	{
+		mxEvent.consume(evt);
+	});
+
+	this.eventConsumer = function(sender, evt)
+	{
+		var evtName = evt.getProperty('eventName');
+		var me = evt.getProperty('event');
+
+		if (evtName != mxEvent.MOUSE_DOWN)
+		{
+			me.consume();
+		}
+	};
+};
+
+/**
+ * Variable: element
+ *
+ * Reference to the DOM node which was made draggable.
+ */
+mxDragSource.prototype.element = null;
+
+/**
+ * Variable: dropHandler
+ *
+ * Holds the DOM node that is used to represent the drag preview. If this is
+ * null then the source element will be cloned and used for the drag preview.
+ */
+mxDragSource.prototype.dropHandler = null;
+
+/**
+ * Variable: dragOffset
+ *
+ *  that specifies the offset of the . Default is null.
+ */
+mxDragSource.prototype.dragOffset = null;
+
+/**
+ * Variable: dragElement
+ *
+ * Holds the DOM node that is used to represent the drag preview. If this is
+ * null then the source element will be cloned and used for the drag preview.
+ */
+mxDragSource.prototype.dragElement = null;
+
+/**
+ * Variable: previewElement
+ *
+ * Optional  that specifies the unscaled size of the preview.
+ */
+mxDragSource.prototype.previewElement = null;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if this drag source is enabled. Default is true.
+ */
+mxDragSource.prototype.enabled = true;
+
+/**
+ * Variable: currentGraph
+ *
+ * Reference to the  that is the current drop target.
+ */
+mxDragSource.prototype.currentGraph = null;
+
+/**
+ * Variable: currentDropTarget
+ *
+ * Holds the current drop target under the mouse.
+ */
+mxDragSource.prototype.currentDropTarget = null;
+
+/**
+ * Variable: currentPoint
+ *
+ * Holds the current drop location.
+ */
+mxDragSource.prototype.currentPoint = null;
+
+/**
+ * Variable: currentGuide
+ *
+ * Holds an  for the  if  is not null.
+ */
+mxDragSource.prototype.currentGuide = null;
+
+/**
+ * Variable: currentGuide
+ *
+ * Holds an  for the  if  is not null.
+ */
+mxDragSource.prototype.currentHighlight = null;
+
+/**
+ * Variable: autoscroll
+ *
+ * Specifies if the graph should scroll automatically. Default is true.
+ */
+mxDragSource.prototype.autoscroll = true;
+
+/**
+ * Variable: guidesEnabled
+ *
+ * Specifies if  should be enabled. Default is true.
+ */
+mxDragSource.prototype.guidesEnabled = true;
+
+/**
+ * Variable: gridEnabled
+ *
+ * Specifies if the grid should be allowed. Default is true.
+ */
+mxDragSource.prototype.gridEnabled = true;
+
+/**
+ * Variable: highlightDropTargets
+ *
+ * Specifies if drop targets should be highlighted. Default is true.
+ */
+mxDragSource.prototype.highlightDropTargets = true;
+
+/**
+ * Variable: dragElementZIndex
+ *
+ * ZIndex for the drag element. Default is 100.
+ */
+mxDragSource.prototype.dragElementZIndex = 100;
+
+/**
+ * Variable: dragElementOpacity
+ *
+ * Opacity of the drag element in %. Default is 70.
+ */
+mxDragSource.prototype.dragElementOpacity = 70;
+
+/**
+ * Variable: checkEventSource
+ *
+ * Whether the event source should be checked in . Default
+ * is true.
+ */
+mxDragSource.prototype.checkEventSource = true;
+
+/**
+ * Function: isEnabled
+ *
+ * Returns .
+ */
+mxDragSource.prototype.isEnabled = function()
+{
+	return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Sets .
+ */
+mxDragSource.prototype.setEnabled = function(value)
+{
+	this.enabled = value;
+};
+
+/**
+ * Function: isGuidesEnabled
+ *
+ * Returns .
+ */
+mxDragSource.prototype.isGuidesEnabled = function()
+{
+	return this.guidesEnabled;
+};
+
+/**
+ * Function: setGuidesEnabled
+ *
+ * Sets .
+ */
+mxDragSource.prototype.setGuidesEnabled = function(value)
+{
+	this.guidesEnabled = value;
+};
+
+/**
+ * Function: isGridEnabled
+ *
+ * Returns .
+ */
+mxDragSource.prototype.isGridEnabled = function()
+{
+	return this.gridEnabled;
+};
+
+/**
+ * Function: setGridEnabled
+ *
+ * Sets .
+ */
+mxDragSource.prototype.setGridEnabled = function(value)
+{
+	this.gridEnabled = value;
+};
+
+/**
+ * Function: getGraphForEvent
+ *
+ * Returns the graph for the given mouse event. This implementation returns
+ * null.
+ */
+mxDragSource.prototype.getGraphForEvent = function(evt)
+{
+	return null;
+};
+
+/**
+ * Function: getDropTarget
+ *
+ * Returns the drop target for the given graph and coordinates. This
+ * implementation uses .
+ */
+mxDragSource.prototype.getDropTarget = function(graph, x, y, evt)
+{
+	return graph.getCellAt(x, y);
+};
+
+/**
+ * Function: createDragElement
+ *
+ * Creates and returns a clone of the  or the 
+ * if the former is not defined.
+ */
+mxDragSource.prototype.createDragElement = function(evt)
+{
+	return this.element.cloneNode(true);
+};
+
+/**
+ * Function: createPreviewElement
+ *
+ * Creates and returns an element which can be used as a preview in the given
+ * graph.
+ */
+mxDragSource.prototype.createPreviewElement = function(graph)
+{
+	return null;
+};
+
+/**
+ * Function: isActive
+ *
+ * Returns true if this drag source is active.
+ */
+mxDragSource.prototype.isActive = function()
+{
+	return this.mouseMoveHandler != null;
+};
+
+/**
+ * Function: reset
+ *
+ * Stops and removes everything and restores the state of the object.
+ */
+mxDragSource.prototype.reset = function()
+{
+	if (this.currentGraph != null)
+	{
+		this.dragExit(this.currentGraph);
+		this.currentGraph = null;
+	}
+
+	this.removeDragElement();
+	this.removeListeners();
+	this.stopDrag();
+};
+
+/**
+ * Function: mouseDown
+ *
+ * Returns the drop target for the given graph and coordinates. This
+ * implementation uses .
+ *
+ * To ignore popup menu events for a drag source, this function can be
+ * overridden as follows.
+ *
+ * (code)
+ * var mouseDown = dragSource.mouseDown;
+ *
+ * dragSource.mouseDown = function(evt)
+ * {
+ *   if (!mxEvent.isPopupTrigger(evt))
+ *   {
+ *     mouseDown.apply(this, arguments);
+ *   }
+ * };
+ * (end)
+ */
+mxDragSource.prototype.mouseDown = function(evt)
+{
+	if (this.enabled && !mxEvent.isConsumed(evt) && this.mouseMoveHandler == null)
+	{
+		this.startDrag(evt);
+		this.mouseMoveHandler = mxUtils.bind(this, this.mouseMove);
+		this.mouseUpHandler = mxUtils.bind(this, this.mouseUp);
+		mxEvent.addGestureListeners(document, null, this.mouseMoveHandler, this.mouseUpHandler);
+
+		if (mxClient.IS_TOUCH && !mxEvent.isMouseEvent(evt))
+		{
+			this.eventSource = mxEvent.getSource(evt);
+			mxEvent.addGestureListeners(this.eventSource, null, this.mouseMoveHandler, this.mouseUpHandler);
+		}
+	}
+};
+
+/**
+ * Function: startDrag
+ *
+ * Creates the  using .
+ */
+mxDragSource.prototype.startDrag = function(evt)
+{
+	this.dragElement = this.createDragElement(evt);
+	this.dragElement.style.position = 'absolute';
+	this.dragElement.style.zIndex = this.dragElementZIndex;
+	mxUtils.setOpacity(this.dragElement, this.dragElementOpacity);
+
+	if (this.checkEventSource && mxClient.IS_SVG)
+	{
+		this.dragElement.style.pointerEvents = 'none';
+	}
+};
+
+/**
+ * Function: stopDrag
+ *
+ * Invokes .
+ */
+mxDragSource.prototype.stopDrag = function()
+{
+	// LATER: This used to have a mouse event. If that is still needed we need to add another
+	// final call to the DnD protocol to add a cleanup step in the case of escape press, which
+	// is not associated with a mouse event and which currently calles this method.
+	this.removeDragElement();
+};
+
+/**
+ * Function: removeDragElement
+ *
+ * Removes and destroys the .
+ */
+mxDragSource.prototype.removeDragElement = function()
+{
+	if (this.dragElement != null)
+	{
+		if (this.dragElement.parentNode != null)
+		{
+			this.dragElement.parentNode.removeChild(this.dragElement);
+		}
+
+		this.dragElement = null;
+	}
+};
+
+/**
+ * Function: getElementForEvent
+ *
+ * Returns the topmost element under the given event.
+ */
+mxDragSource.prototype.getElementForEvent = function(evt)
+{
+	return ((mxEvent.isTouchEvent(evt) || mxEvent.isPenEvent(evt)) ?
+			document.elementFromPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt)) :
+				mxEvent.getSource(evt));
+};
+
+/**
+ * Function: graphContainsEvent
+ *
+ * Returns true if the given graph contains the given event.
+ */
+mxDragSource.prototype.graphContainsEvent = function(graph, evt)
+{
+	var x = mxEvent.getClientX(evt);
+	var y = mxEvent.getClientY(evt);
+	var offset = mxUtils.getOffset(graph.container);
+	var origin = mxUtils.getScrollOrigin();
+	var elt = this.getElementForEvent(evt);
+
+	if (this.checkEventSource)
+	{
+		while (elt != null && elt != graph.container)
+		{
+			elt = elt.parentNode;
+		}
+	}
+
+	// Checks if event is inside the bounds of the graph container
+	return elt != null && x >= offset.x - origin.x && y >= offset.y - origin.y &&
+		x <= offset.x - origin.x + graph.container.offsetWidth &&
+		y <= offset.y - origin.y + graph.container.offsetHeight;
+};
+
+/**
+ * Function: mouseMove
+ *
+ * Gets the graph for the given event using , updates the
+ * , calling  and  on the new and old graph,
+ * respectively, and invokes  if  is not null.
+ */
+mxDragSource.prototype.mouseMove = function(evt)
+{
+	var graph = this.getGraphForEvent(evt);
+
+	// Checks if event is inside the bounds of the graph container
+	if (graph != null && !this.graphContainsEvent(graph, evt))
+	{
+		graph = null;
+	}
+
+	if (graph != this.currentGraph)
+	{
+		if (this.currentGraph != null)
+		{
+			this.dragExit(this.currentGraph, evt);
+		}
+
+		this.currentGraph = graph;
+
+		if (this.currentGraph != null)
+		{
+			this.dragEnter(this.currentGraph, evt);
+		}
+	}
+
+	if (this.currentGraph != null)
+	{
+		this.dragOver(this.currentGraph, evt);
+	}
+
+	if (this.dragElement != null && (this.previewElement == null || this.previewElement.style.visibility != 'visible'))
+	{
+		var x = mxEvent.getClientX(evt);
+		var y = mxEvent.getClientY(evt);
+
+		if (this.dragElement.parentNode == null)
+		{
+			document.body.appendChild(this.dragElement);
+		}
+
+		this.dragElement.style.visibility = 'visible';
+
+		if (this.dragOffset != null)
+		{
+			x += this.dragOffset.x;
+			y += this.dragOffset.y;
+		}
+
+		var offset = mxUtils.getDocumentScrollOrigin(document);
+
+		this.dragElement.style.left = (x + offset.x) + 'px';
+		this.dragElement.style.top = (y + offset.y) + 'px';
+	}
+	else if (this.dragElement != null)
+	{
+		this.dragElement.style.visibility = 'hidden';
+	}
+
+	mxEvent.consume(evt);
+};
+
+/**
+ * Function: mouseUp
+ *
+ * Processes the mouse up event and invokes ,  and 
+ * as required.
+ */
+mxDragSource.prototype.mouseUp = function(evt)
+{
+	if (this.currentGraph != null)
+	{
+		if (this.currentPoint != null && (this.previewElement == null ||
+			this.previewElement.style.visibility != 'hidden'))
+		{
+			var scale = this.currentGraph.view.scale;
+			var tr = this.currentGraph.view.translate;
+			var x = this.currentPoint.x / scale - tr.x;
+			var y = this.currentPoint.y / scale - tr.y;
+
+			this.drop(this.currentGraph, evt, this.currentDropTarget, x, y);
+		}
+
+		this.dragExit(this.currentGraph);
+		this.currentGraph = null;
+	}
+
+	this.stopDrag();
+	this.removeListeners();
+
+	mxEvent.consume(evt);
+};
+
+/**
+ * Function: removeListeners
+ *
+ * Actives the given graph as a drop target.
+ */
+mxDragSource.prototype.removeListeners = function()
+{
+	if (this.eventSource != null)
+	{
+		mxEvent.removeGestureListeners(this.eventSource, null, this.mouseMoveHandler, this.mouseUpHandler);
+		this.eventSource = null;
+	}
+
+	mxEvent.removeGestureListeners(document, null, this.mouseMoveHandler, this.mouseUpHandler);
+	this.mouseMoveHandler = null;
+	this.mouseUpHandler = null;
+};
+
+/**
+ * Function: dragEnter
+ *
+ * Actives the given graph as a drop target.
+ */
+mxDragSource.prototype.dragEnter = function(graph, evt)
+{
+	graph.isMouseDown = true;
+	graph.isMouseTrigger = mxEvent.isMouseEvent(evt);
+	this.previewElement = this.createPreviewElement(graph);
+
+	if (this.previewElement != null && this.checkEventSource && mxClient.IS_SVG)
+	{
+		this.previewElement.style.pointerEvents = 'none';
+	}
+
+	// Guide is only needed if preview element is used
+	if (this.isGuidesEnabled() && this.previewElement != null)
+	{
+		this.currentGuide = new mxGuide(graph, graph.graphHandler.getGuideStates());
+	}
+
+	if (this.highlightDropTargets)
+	{
+		this.currentHighlight = new mxCellHighlight(graph, mxConstants.DROP_TARGET_COLOR);
+	}
+
+	// Consumes all events in the current graph before they are fired
+	graph.addListener(mxEvent.FIRE_MOUSE_EVENT, this.eventConsumer);
+};
+
+/**
+ * Function: dragExit
+ *
+ * Deactivates the given graph as a drop target.
+ */
+mxDragSource.prototype.dragExit = function(graph, evt)
+{
+	this.currentDropTarget = null;
+	this.currentPoint = null;
+	graph.isMouseDown = false;
+
+	// Consumes all events in the current graph before they are fired
+	graph.removeListener(this.eventConsumer);
+
+	if (this.previewElement != null)
+	{
+		if (this.previewElement.parentNode != null)
+		{
+			this.previewElement.parentNode.removeChild(this.previewElement);
+		}
+
+		this.previewElement = null;
+	}
+
+	if (this.currentGuide != null)
+	{
+		this.currentGuide.destroy();
+		this.currentGuide = null;
+	}
+
+	if (this.currentHighlight != null)
+	{
+		this.currentHighlight.destroy();
+		this.currentHighlight = null;
+	}
+};
+
+/**
+ * Function: dragOver
+ *
+ * Implements autoscroll, updates the , highlights any drop
+ * targets and updates the preview.
+ */
+mxDragSource.prototype.dragOver = function(graph, evt)
+{
+	var offset = mxUtils.getOffset(graph.container);
+	var origin = mxUtils.getScrollOrigin(graph.container);
+	var x = mxEvent.getClientX(evt) - offset.x + origin.x - graph.panDx;
+	var y = mxEvent.getClientY(evt) - offset.y + origin.y - graph.panDy;
+
+	if (graph.autoScroll && (this.autoscroll == null || this.autoscroll))
+	{
+		graph.scrollPointToVisible(x, y, graph.autoExtend);
+	}
+
+	// Highlights the drop target under the mouse
+	if (this.currentHighlight != null && graph.isDropEnabled())
+	{
+		this.currentDropTarget = this.getDropTarget(graph, x, y, evt);
+		var state = graph.getView().getState(this.currentDropTarget);
+		this.currentHighlight.highlight(state);
+	}
+
+	// Updates the location of the preview
+	if (this.previewElement != null)
+	{
+		if (this.previewElement.parentNode == null)
+		{
+			graph.container.appendChild(this.previewElement);
+
+			this.previewElement.style.zIndex = '3';
+			this.previewElement.style.position = 'absolute';
+		}
+
+		var gridEnabled = this.isGridEnabled() && graph.isGridEnabledEvent(evt);
+		var hideGuide = true;
+
+		// Grid and guides
+		if (this.currentGuide != null && this.currentGuide.isEnabledForEvent(evt))
+		{
+			// LATER: HTML preview appears smaller than SVG preview
+			var w = parseInt(this.previewElement.style.width);
+			var h = parseInt(this.previewElement.style.height);
+			var bounds = new mxRectangle(0, 0, w, h);
+			var delta = new mxPoint(x, y);
+			delta = this.currentGuide.move(bounds, delta, gridEnabled);
+			hideGuide = false;
+			x = delta.x;
+			y = delta.y;
+		}
+		else if (gridEnabled)
+		{
+			var scale = graph.view.scale;
+			var tr = graph.view.translate;
+			var off = graph.gridSize / 2;
+			x = (graph.snap(x / scale - tr.x - off) + tr.x) * scale;
+			y = (graph.snap(y / scale - tr.y - off) + tr.y) * scale;
+		}
+
+		if (this.currentGuide != null && hideGuide)
+		{
+			this.currentGuide.hide();
+		}
+
+		if (this.previewOffset != null)
+		{
+			x += this.previewOffset.x;
+			y += this.previewOffset.y;
+		}
+
+		this.previewElement.style.left = Math.round(x) + 'px';
+		this.previewElement.style.top = Math.round(y) + 'px';
+		this.previewElement.style.visibility = 'visible';
+	}
+
+	this.currentPoint = new mxPoint(x, y);
+};
+
+/**
+ * Function: drop
+ *
+ * Returns the drop target for the given graph and coordinates. This
+ * implementation uses .
+ */
+mxDragSource.prototype.drop = function(graph, evt, dropTarget, x, y)
+{
+	this.dropHandler.apply(this, arguments);
+
+	// Had to move this to after the insert because it will
+	// affect the scrollbars of the window in IE to try and
+	// make the complete container visible.
+	// LATER: Should be made optional.
+	if (graph.container.style.visibility != 'hidden')
+	{
+		graph.container.focus();
+	}
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxToolbar
+ *
+ * Creates a toolbar inside a given DOM node. The toolbar may contain icons,
+ * buttons and combo boxes.
+ *
+ * Event: mxEvent.SELECT
+ *
+ * Fires when an item was selected in the toolbar. The function
+ * property contains the function that was selected in .
+ *
+ * Constructor: mxToolbar
+ *
+ * Constructs a toolbar in the specified container.
+ *
+ * Parameters:
+ *
+ * container - DOM node that contains the toolbar.
+ */
+function mxToolbar(container)
+{
+	this.container = container;
+};
+
+/**
+ * Extends mxEventSource.
+ */
+mxToolbar.prototype = new mxEventSource();
+mxToolbar.prototype.constructor = mxToolbar;
+
+/**
+ * Variable: container
+ *
+ * Reference to the DOM nodes that contains the toolbar.
+ */
+mxToolbar.prototype.container = null;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if events are handled. Default is true.
+ */
+mxToolbar.prototype.enabled = true;
+
+/**
+ * Variable: noReset
+ *
+ * Specifies if  requires a forced flag of true for resetting
+ * the current mode in the toolbar. Default is false. This is set to true
+ * if the toolbar item is double clicked to avoid a reset after a single
+ * use of the item.
+ */
+mxToolbar.prototype.noReset = false;
+
+/**
+ * Variable: updateDefaultMode
+ *
+ * Boolean indicating if the default mode should be the last selected
+ * switch mode or the first inserted switch mode. Default is true, that
+ * is the last selected switch mode is the default mode. The default mode
+ * is the mode to be selected after a reset of the toolbar. If this is
+ * false, then the default mode is the first inserted mode item regardless
+ * of what was last selected. Otherwise, the selected item after a reset is
+ * the previously selected item.
+ */
+mxToolbar.prototype.updateDefaultMode = true;
+
+/**
+ * Function: addItem
+ *
+ * Adds the given function as an image with the specified title and icon
+ * and returns the new image node.
+ *
+ * Parameters:
+ *
+ * title - Optional string that is used as the tooltip.
+ * icon - Optional URL of the image to be used. If no URL is given, then a
+ * button is created.
+ * funct - Function to execute on a mouse click.
+ * pressedIcon - Optional URL of the pressed image. Default is a gray
+ * background.
+ * style - Optional style classname. Default is mxToolbarItem.
+ * factoryMethod - Optional factory method for popup menu, eg.
+ * function(menu, evt, cell) { menu.addItem('Hello, World!'); }
+ */
+mxToolbar.prototype.addItem = function(title, icon, funct, pressedIcon, style, factoryMethod)
+{
+	var img = document.createElement((icon != null) ? 'img' : 'button');
+	var initialClassName = style || ((factoryMethod != null) ?
+			'mxToolbarMode' : 'mxToolbarItem');
+	img.className = initialClassName;
+	img.setAttribute('src', icon);
+
+	if (title != null)
+	{
+		if (icon != null)
+		{
+			img.setAttribute('title', title);
+		}
+		else
+		{
+			mxUtils.write(img, title);
+		}
+	}
+
+	this.container.appendChild(img);
+
+	// Invokes the function on a click on the toolbar item
+	if (funct != null)
+	{
+		mxEvent.addListener(img, 'click', funct);
+
+		if (mxClient.IS_TOUCH)
+		{
+			mxEvent.addListener(img, 'touchend', funct);
+		}
+	}
+
+	var mouseHandler = mxUtils.bind(this, function(evt)
+	{
+		if (pressedIcon != null)
+		{
+			img.setAttribute('src', icon);
+		}
+		else
+		{
+			img.style.backgroundColor = '';
+		}
+	});
+
+	// Highlights the toolbar item with a gray background
+	// while it is being clicked with the mouse
+	mxEvent.addGestureListeners(img, mxUtils.bind(this, function(evt)
+	{
+		if (pressedIcon != null)
+		{
+			img.setAttribute('src', pressedIcon);
+		}
+		else
+		{
+			img.style.backgroundColor = 'gray';
+		}
+
+		// Popup Menu
+		if (factoryMethod != null)
+		{
+			if (this.menu == null)
+			{
+				this.menu = new mxPopupMenu();
+				this.menu.init();
+			}
+
+			var last = this.currentImg;
+
+			if (this.menu.isMenuShowing())
+			{
+				this.menu.hideMenu();
+			}
+
+			if (last != img)
+			{
+				// Redirects factory method to local factory method
+				this.currentImg = img;
+				this.menu.factoryMethod = factoryMethod;
+
+				var point = new mxPoint(
+					img.offsetLeft,
+					img.offsetTop + img.offsetHeight);
+				this.menu.popup(point.x, point.y, null, evt);
+
+				// Sets and overrides to restore classname
+				if (this.menu.isMenuShowing())
+				{
+					img.className = initialClassName + 'Selected';
+
+					this.menu.hideMenu = function()
+					{
+						mxPopupMenu.prototype.hideMenu.apply(this);
+						img.className = initialClassName;
+						this.currentImg = null;
+					};
+				}
+			}
+		}
+	}), null, mouseHandler);
+
+	mxEvent.addListener(img, 'mouseout', mouseHandler);
+
+	return img;
+};
+
+/**
+ * Function: addCombo
+ *
+ * Adds and returns a new SELECT element using the given style. The element
+ * is placed inside a DIV with the mxToolbarComboContainer style classname.
+ *
+ * Parameters:
+ *
+ * style - Optional style classname. Default is mxToolbarCombo.
+ */
+mxToolbar.prototype.addCombo = function(style)
+{
+	var div = document.createElement('div');
+	div.style.display = 'inline';
+	div.className = 'mxToolbarComboContainer';
+
+	var select = document.createElement('select');
+	select.className = style || 'mxToolbarCombo';
+	div.appendChild(select);
+
+	this.container.appendChild(div);
+
+	return select;
+};
+
+/**
+ * Function: addCombo
+ *
+ * Adds and returns a new SELECT element using the given title as the
+ * default element. The selection is reset to this element after each
+ * change.
+ *
+ * Parameters:
+ *
+ * title - String that specifies the title of the default element.
+ * style - Optional style classname. Default is mxToolbarCombo.
+ */
+mxToolbar.prototype.addActionCombo = function(title, style)
+{
+	var select = document.createElement('select');
+	select.className = style || 'mxToolbarCombo';
+	this.addOption(select, title, null);
+
+	mxEvent.addListener(select, 'change', function(evt)
+	{
+		var value = select.options[select.selectedIndex];
+		select.selectedIndex = 0;
+
+		if (value.funct != null)
+		{
+			value.funct(evt);
+		}
+	});
+
+	this.container.appendChild(select);
+
+	return select;
+};
+
+/**
+ * Function: addOption
+ *
+ * Adds and returns a new OPTION element inside the given SELECT element.
+ * If the given value is a function then it is stored in the option's funct
+ * field.
+ *
+ * Parameters:
+ *
+ * combo - SELECT element that will contain the new entry.
+ * title - String that specifies the title of the option.
+ * value - Specifies the value associated with this option.
+ */
+mxToolbar.prototype.addOption = function(combo, title, value)
+{
+	var option = document.createElement('option');
+	mxUtils.writeln(option, title);
+
+	if (typeof(value) == 'function')
+	{
+		option.funct = value;
+	}
+	else
+	{
+		option.setAttribute('value', value);
+	}
+
+	combo.appendChild(option);
+
+	return option;
+};
+
+/**
+ * Function: addSwitchMode
+ *
+ * Adds a new selectable item to the toolbar. Only one switch mode item may
+ * be selected at a time. The currently selected item is the default item
+ * after a reset of the toolbar.
+ */
+mxToolbar.prototype.addSwitchMode = function(title, icon, funct, pressedIcon, style)
+{
+	var img = document.createElement('img');
+	img.initialClassName = style || 'mxToolbarMode';
+	img.className = img.initialClassName;
+	img.setAttribute('src', icon);
+	img.altIcon = pressedIcon;
+
+	if (title != null)
+	{
+		img.setAttribute('title', title);
+	}
+
+	mxEvent.addListener(img, 'click', mxUtils.bind(this, function(evt)
+	{
+		var tmp = this.selectedMode.altIcon;
+
+		if (tmp != null)
+		{
+			this.selectedMode.altIcon = this.selectedMode.getAttribute('src');
+			this.selectedMode.setAttribute('src', tmp);
+		}
+		else
+		{
+			this.selectedMode.className = this.selectedMode.initialClassName;
+		}
+
+		if (this.updateDefaultMode)
+		{
+			this.defaultMode = img;
+		}
+
+		this.selectedMode = img;
+
+		var tmp = img.altIcon;
+
+		if (tmp != null)
+		{
+			img.altIcon = img.getAttribute('src');
+			img.setAttribute('src', tmp);
+		}
+		else
+		{
+			img.className = img.initialClassName+'Selected';
+		}
+
+		this.fireEvent(new mxEventObject(mxEvent.SELECT));
+		funct();
+	}));
+
+	this.container.appendChild(img);
+
+	if (this.defaultMode == null)
+	{
+		this.defaultMode = img;
+
+		// Function should fire only once so
+		// do not pass it with the select event
+		this.selectMode(img);
+		funct();
+	}
+
+	return img;
+};
+
+/**
+ * Function: addMode
+ *
+ * Adds a new item to the toolbar. The selection is typically reset after
+ * the item has been consumed, for example by adding a new vertex to the
+ * graph. The reset is not carried out if the item is double clicked.
+ *
+ * The function argument uses the following signature: funct(evt, cell) where
+ * evt is the native mouse event and cell is the cell under the mouse.
+ */
+mxToolbar.prototype.addMode = function(title, icon, funct, pressedIcon, style, toggle)
+{
+	toggle = (toggle != null) ? toggle : true;
+	var img = document.createElement((icon != null) ? 'img' : 'button');
+
+	img.initialClassName = style || 'mxToolbarMode';
+	img.className = img.initialClassName;
+	img.setAttribute('src', icon);
+	img.altIcon = pressedIcon;
+
+	if (title != null)
+	{
+		img.setAttribute('title', title);
+	}
+
+	if (this.enabled && toggle)
+	{
+		mxEvent.addListener(img, 'click', mxUtils.bind(this, function(evt)
+		{
+			this.selectMode(img, funct);
+			this.noReset = false;
+		}));
+
+		mxEvent.addListener(img, 'dblclick', mxUtils.bind(this, function(evt)
+		{
+			this.selectMode(img, funct);
+			this.noReset = true;
+		}));
+
+		if (this.defaultMode == null)
+		{
+			this.defaultMode = img;
+			this.defaultFunction = funct;
+			this.selectMode(img, funct);
+		}
+	}
+
+	this.container.appendChild(img);
+
+	return img;
+};
+
+/**
+ * Function: selectMode
+ *
+ * Resets the state of the previously selected mode and displays the given
+ * DOM node as selected. This function fires a select event with the given
+ * function as a parameter.
+ */
+mxToolbar.prototype.selectMode = function(domNode, funct)
+{
+	if (this.selectedMode != domNode)
+	{
+		if (this.selectedMode != null)
+		{
+			var tmp = this.selectedMode.altIcon;
+
+			if (tmp != null)
+			{
+				this.selectedMode.altIcon = this.selectedMode.getAttribute('src');
+				this.selectedMode.setAttribute('src', tmp);
+			}
+			else
+			{
+				this.selectedMode.className = this.selectedMode.initialClassName;
+			}
+		}
+
+		this.selectedMode = domNode;
+		var tmp = this.selectedMode.altIcon;
+
+		if (tmp != null)
+		{
+			this.selectedMode.altIcon = this.selectedMode.getAttribute('src');
+			this.selectedMode.setAttribute('src', tmp);
+		}
+		else
+		{
+			this.selectedMode.className = this.selectedMode.initialClassName+'Selected';
+		}
+
+		this.fireEvent(new mxEventObject(mxEvent.SELECT, "function", funct));
+	}
+};
+
+/**
+ * Function: resetMode
+ *
+ * Selects the default mode and resets the state of the previously selected
+ * mode.
+ */
+mxToolbar.prototype.resetMode = function(forced)
+{
+	if ((forced || !this.noReset) && this.selectedMode != this.defaultMode)
+	{
+		// The last selected switch mode will be activated
+		// so the function was already executed and is
+		// no longer required here
+		this.selectMode(this.defaultMode, this.defaultFunction);
+	}
+};
+
+/**
+ * Function: addSeparator
+ *
+ * Adds the specifies image as a separator.
+ *
+ * Parameters:
+ *
+ * icon - URL of the separator icon.
+ */
+mxToolbar.prototype.addSeparator = function(icon)
+{
+	return this.addItem(null, icon, null);
+};
+
+/**
+ * Function: addBreak
+ *
+ * Adds a break to the container.
+ */
+mxToolbar.prototype.addBreak = function()
+{
+	mxUtils.br(this.container);
+};
+
+/**
+ * Function: addLine
+ *
+ * Adds a horizontal line to the container.
+ */
+mxToolbar.prototype.addLine = function()
+{
+	var hr = document.createElement('hr');
+
+	hr.style.marginRight = '6px';
+	hr.setAttribute('size', '1');
+
+	this.container.appendChild(hr);
+};
+
+/**
+ * Function: destroy
+ *
+ * Removes the toolbar and all its associated resources.
+ */
+mxToolbar.prototype.destroy = function ()
+{
+	mxEvent.release(this.container);
+	this.container = null;
+	this.defaultMode = null;
+	this.defaultFunction = null;
+	this.selectedMode = null;
+
+	if (this.menu != null)
+	{
+		this.menu.destroy();
+	}
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxUndoableEdit
+ *
+ * Implements a composite undoable edit. Here is an example for a custom change
+ * which gets executed via the model:
+ *
+ * (code)
+ * function CustomChange(model, name)
+ * {
+ *   this.model = model;
+ *   this.name = name;
+ *   this.previous = name;
+ * };
+ *
+ * CustomChange.prototype.execute = function()
+ * {
+ *   var tmp = this.model.name;
+ *   this.model.name = this.previous;
+ *   this.previous = tmp;
+ * };
+ *
+ * var name = prompt('Enter name');
+ * graph.model.execute(new CustomChange(graph.model, name));
+ * (end)
+ *
+ * Event: mxEvent.EXECUTED
+ *
+ * Fires between START_EDIT and END_EDIT after an atomic change was executed.
+ * The change property contains the change that was executed.
+ *
+ * Event: mxEvent.START_EDIT
+ *
+ * Fires before a set of changes will be executed in  or .
+ * This event contains no properties.
+ *
+ * Event: mxEvent.END_EDIT
+ *
+ * Fires after a set of changeswas executed in  or .
+ * This event contains no properties.
+ *
+ * Constructor: mxUndoableEdit
+ *
+ * Constructs a new undoable edit for the given source.
+ */
+function mxUndoableEdit(source, significant)
+{
+	this.source = source;
+	this.changes = [];
+	this.significant = (significant != null) ? significant : true;
+};
+
+/**
+ * Variable: source
+ *
+ * Specifies the source of the edit.
+ */
+mxUndoableEdit.prototype.source = null;
+
+/**
+ * Variable: changes
+ *
+ * Array that contains the changes that make up this edit. The changes are
+ * expected to either have an undo and redo function, or an execute
+ * function. Default is an empty array.
+ */
+mxUndoableEdit.prototype.changes = null;
+
+/**
+ * Variable: significant
+ *
+ * Specifies if the undoable change is significant.
+ * Default is true.
+ */
+mxUndoableEdit.prototype.significant = null;
+
+/**
+ * Variable: undone
+ *
+ * Specifies if this edit has been undone. Default is false.
+ */
+mxUndoableEdit.prototype.undone = false;
+
+/**
+ * Variable: redone
+ *
+ * Specifies if this edit has been redone. Default is false.
+ */
+mxUndoableEdit.prototype.redone = false;
+
+/**
+ * Function: isEmpty
+ *
+ * Returns true if the this edit contains no changes.
+ */
+mxUndoableEdit.prototype.isEmpty = function()
+{
+	return this.changes.length == 0;
+};
+
+/**
+ * Function: isSignificant
+ *
+ * Returns .
+ */
+mxUndoableEdit.prototype.isSignificant = function()
+{
+	return this.significant;
+};
+
+/**
+ * Function: add
+ *
+ * Adds the specified change to this edit. The change is an object that is
+ * expected to either have an undo and redo, or an execute function.
+ */
+mxUndoableEdit.prototype.add = function(change)
+{
+	this.changes.push(change);
+};
+
+/**
+ * Function: notify
+ *
+ * Hook to notify any listeners of the changes after an  or 
+ * has been carried out. This implementation is empty.
+ */
+mxUndoableEdit.prototype.notify = function() { };
+
+/**
+ * Function: die
+ *
+ * Hook to free resources after the edit has been removed from the command
+ * history. This implementation is empty.
+ */
+mxUndoableEdit.prototype.die = function() { };
+
+/**
+ * Function: undo
+ *
+ * Undoes all changes in this edit.
+ */
+mxUndoableEdit.prototype.undo = function()
+{
+	if (!this.undone)
+	{
+		this.source.fireEvent(new mxEventObject(mxEvent.START_EDIT));
+		var count = this.changes.length;
+
+		for (var i = count - 1; i >= 0; i--)
+		{
+			var change = this.changes[i];
+
+			if (change.execute != null)
+			{
+				change.execute();
+			}
+			else if (change.undo != null)
+			{
+				change.undo();
+			}
+
+			// New global executed event
+			this.source.fireEvent(new mxEventObject(mxEvent.EXECUTED, 'change', change));
+		}
+
+		this.undone = true;
+		this.redone = false;
+		this.source.fireEvent(new mxEventObject(mxEvent.END_EDIT));
+	}
+
+	this.notify();
+};
+
+/**
+ * Function: redo
+ *
+ * Redoes all changes in this edit.
+ */
+mxUndoableEdit.prototype.redo = function()
+{
+	if (!this.redone)
+	{
+		this.source.fireEvent(new mxEventObject(mxEvent.START_EDIT));
+		var count = this.changes.length;
+
+		for (var i = 0; i < count; i++)
+		{
+			var change = this.changes[i];
+
+			if (change.execute != null)
+			{
+				change.execute();
+			}
+			else if (change.redo != null)
+			{
+				change.redo();
+			}
+
+			// New global executed event
+			this.source.fireEvent(new mxEventObject(mxEvent.EXECUTED, 'change', change));
+		}
+
+		this.undone = false;
+		this.redone = true;
+		this.source.fireEvent(new mxEventObject(mxEvent.END_EDIT));
+	}
+
+	this.notify();
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxUndoManager
+ *
+ * Implements a command history. When changing the graph model, an
+ *  object is created at the start of the transaction (when
+ * model.beginUpdate is called). All atomic changes are then added to this
+ * object until the last model.endUpdate call, at which point the
+ *  is dispatched in an event, and added to the history inside
+ * . This is done by an event listener in
+ * .
+ *
+ * Each atomic change of the model is represented by an object (eg.
+ * , ,  etc) which contains the
+ * complete undo information. The  also listens to the
+ *  and stores it's changes to the current root as insignificant
+ * undoable changes, so that drilling (step into, step up) is undone.
+ *
+ * This means when you execute an atomic change on the model, then change the
+ * current root on the view and click undo, the change of the root will be
+ * undone together with the change of the model so that the display represents
+ * the state at which the model was changed. However, these changes are not
+ * transmitted for sharing as they do not represent a state change.
+ *
+ * Example:
+ *
+ * When adding an undo manager to a graph, make sure to add it
+ * to the model and the view as well to maintain a consistent
+ * display across multiple undo/redo steps.
+ *
+ * (code)
+ * var undoManager = new mxUndoManager();
+ * var listener = function(sender, evt)
+ * {
+ *   undoManager.undoableEditHappened(evt.getProperty('edit'));
+ * };
+ * graph.getModel().addListener(mxEvent.UNDO, listener);
+ * graph.getView().addListener(mxEvent.UNDO, listener);
+ * (end)
+ *
+ * The code creates a function that informs the undoManager
+ * of an undoable edit and binds it to the undo event of
+ *  and  using
+ * .
+ *
+ * Event: mxEvent.CLEAR
+ *
+ * Fires after  was invoked. This event has no properties.
+ *
+ * Event: mxEvent.UNDO
+ *
+ * Fires afer a significant edit was undone in . The edit
+ * property contains the  that was undone.
+ *
+ * Event: mxEvent.REDO
+ *
+ * Fires afer a significant edit was redone in . The edit
+ * property contains the  that was redone.
+ *
+ * Event: mxEvent.ADD
+ *
+ * Fires after an undoable edit was added to the history. The edit
+ * property contains the  that was added.
+ *
+ * Constructor: mxUndoManager
+ *
+ * Constructs a new undo manager with the given history size. If no history
+ * size is given, then a default size of 100 steps is used.
+ */
+function mxUndoManager(size)
+{
+	this.size = (size != null) ? size : 100;
+	this.clear();
+};
+
+/**
+ * Extends mxEventSource.
+ */
+mxUndoManager.prototype = new mxEventSource();
+mxUndoManager.prototype.constructor = mxUndoManager;
+
+/**
+ * Variable: size
+ *
+ * Maximum command history size. 0 means unlimited history. Default is
+ * 100.
+ */
+mxUndoManager.prototype.size = null;
+
+/**
+ * Variable: history
+ *
+ * Array that contains the steps of the command history.
+ */
+mxUndoManager.prototype.history = null;
+
+/**
+ * Variable: indexOfNextAdd
+ *
+ * Index of the element to be added next.
+ */
+mxUndoManager.prototype.indexOfNextAdd = 0;
+
+/**
+ * Function: isEmpty
+ *
+ * Returns true if the history is empty.
+ */
+mxUndoManager.prototype.isEmpty = function()
+{
+	return this.history.length == 0;
+};
+
+/**
+ * Function: clear
+ *
+ * Clears the command history.
+ */
+mxUndoManager.prototype.clear = function()
+{
+	this.history = [];
+	this.indexOfNextAdd = 0;
+	this.fireEvent(new mxEventObject(mxEvent.CLEAR));
+};
+
+/**
+ * Function: canUndo
+ *
+ * Returns true if an undo is possible.
+ */
+mxUndoManager.prototype.canUndo = function()
+{
+	return this.indexOfNextAdd > 0;
+};
+
+/**
+ * Function: undo
+ *
+ * Undoes the last change.
+ */
+mxUndoManager.prototype.undo = function()
+{
+    while (this.indexOfNextAdd > 0)
+    {
+        var edit = this.history[--this.indexOfNextAdd];
+        edit.undo();
+
+		if (edit.isSignificant())
+        {
+        	this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit));
+            break;
+        }
+    }
+};
+
+/**
+ * Function: canRedo
+ *
+ * Returns true if a redo is possible.
+ */
+mxUndoManager.prototype.canRedo = function()
+{
+	return this.indexOfNextAdd < this.history.length;
+};
+
+/**
+ * Function: redo
+ *
+ * Redoes the last change.
+ */
+mxUndoManager.prototype.redo = function()
+{
+    var n = this.history.length;
+
+    while (this.indexOfNextAdd < n)
+    {
+        var edit =  this.history[this.indexOfNextAdd++];
+        edit.redo();
+
+        if (edit.isSignificant())
+        {
+        	this.fireEvent(new mxEventObject(mxEvent.REDO, 'edit', edit));
+            break;
+        }
+    }
+};
+
+/**
+ * Function: undoableEditHappened
+ *
+ * Method to be called to add new undoable edits to the .
+ */
+mxUndoManager.prototype.undoableEditHappened = function(undoableEdit)
+{
+	this.trim();
+
+	if (this.size > 0 &&
+		this.size == this.history.length)
+	{
+		this.history.shift();
+	}
+
+	this.history.push(undoableEdit);
+	this.indexOfNextAdd = this.history.length;
+	this.fireEvent(new mxEventObject(mxEvent.ADD, 'edit', undoableEdit));
+};
+
+/**
+ * Function: trim
+ *
+ * Removes all pending steps after  from the history,
+ * invoking die on each edit. This is called from .
+ */
+mxUndoManager.prototype.trim = function()
+{
+	if (this.history.length > this.indexOfNextAdd)
+	{
+		var edits = this.history.splice(this.indexOfNextAdd,
+			this.history.length - this.indexOfNextAdd);
+
+		for (var i = 0; i < edits.length; i++)
+		{
+			edits[i].die();
+		}
+	}
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ *
+ * Class: mxUrlConverter
+ *
+ * Converts relative and absolute URLs to absolute URLs with protocol and domain.
+ */
+var mxUrlConverter = function()
+{
+	// Empty constructor
+};
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if the converter is enabled. Default is true.
+ */
+mxUrlConverter.prototype.enabled = true;
+
+/**
+ * Variable: baseUrl
+ *
+ * Specifies the base URL to be used as a prefix for relative URLs.
+ */
+mxUrlConverter.prototype.baseUrl = null;
+
+/**
+ * Variable: baseDomain
+ *
+ * Specifies the base domain to be used as a prefix for absolute URLs.
+ */
+mxUrlConverter.prototype.baseDomain = null;
+
+/**
+ * Function: updateBaseUrl
+ *
+ * Private helper function to update the base URL.
+ */
+mxUrlConverter.prototype.updateBaseUrl = function()
+{
+	this.baseDomain = location.protocol + '//' + location.host;
+	this.baseUrl = this.baseDomain + location.pathname;
+	var tmp = this.baseUrl.lastIndexOf('/');
+
+	// Strips filename etc
+	if (tmp > 0)
+	{
+		this.baseUrl = this.baseUrl.substring(0, tmp + 1);
+	}
+};
+
+/**
+ * Function: isEnabled
+ *
+ * Returns .
+ */
+mxUrlConverter.prototype.isEnabled = function()
+{
+	return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Sets .
+ */
+mxUrlConverter.prototype.setEnabled = function(value)
+{
+	this.enabled = value;
+};
+
+/**
+ * Function: getBaseUrl
+ *
+ * Returns .
+ */
+mxUrlConverter.prototype.getBaseUrl = function()
+{
+	return this.baseUrl;
+};
+
+/**
+ * Function: setBaseUrl
+ *
+ * Sets .
+ */
+mxUrlConverter.prototype.setBaseUrl = function(value)
+{
+	this.baseUrl = value;
+};
+
+/**
+ * Function: getBaseDomain
+ *
+ * Returns .
+ */
+mxUrlConverter.prototype.getBaseDomain = function()
+{
+	return this.baseDomain;
+},
+
+/**
+ * Function: setBaseDomain
+ *
+ * Sets .
+ */
+mxUrlConverter.prototype.setBaseDomain = function(value)
+{
+	this.baseDomain = value;
+},
+
+/**
+ * Function: isRelativeUrl
+ *
+ * Returns true if the given URL is relative.
+ */
+mxUrlConverter.prototype.isRelativeUrl = function(url)
+{
+	return url.substring(0, 2) != '//' && url.substring(0, 7) != 'http://' &&
+		url.substring(0, 8) != 'https://' && url.substring(0, 10) != 'data:image' &&
+		url.substring(0, 7) != 'file://';
+};
+
+/**
+ * Function: convert
+ *
+ * Converts the given URL to an absolute URL with protol and domain.
+ * Relative URLs are first converted to absolute URLs.
+ */
+mxUrlConverter.prototype.convert = function(url)
+{
+	if (this.isEnabled() && this.isRelativeUrl(url))
+	{
+		if (this.getBaseUrl() == null)
+		{
+			this.updateBaseUrl();
+		}
+
+		if (url.charAt(0) == '/')
+		{
+			url = this.getBaseDomain() + url;
+		}
+		else
+		{
+			url = this.getBaseUrl() + url;
+		}
+	}
+
+	return url;
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxPanningManager
+ *
+ * Implements a handler for panning.
+ */
+function mxPanningManager(graph)
+{
+	this.thread = null;
+	this.active = false;
+	this.tdx = 0;
+	this.tdy = 0;
+	this.t0x = 0;
+	this.t0y = 0;
+	this.dx = 0;
+	this.dy = 0;
+	this.scrollbars = false;
+	this.scrollLeft = 0;
+	this.scrollTop = 0;
+
+	this.mouseListener =
+	{
+	    mouseDown: function(sender, me) { },
+	    mouseMove: function(sender, me) { },
+	    mouseUp: mxUtils.bind(this, function(sender, me)
+	    {
+	    	if (this.active)
+	    	{
+	    		this.stop();
+	    	}
+	    })
+	};
+
+	graph.addMouseListener(this.mouseListener);
+
+	this.mouseUpListener = mxUtils.bind(this, function()
+	{
+	    	if (this.active)
+	    	{
+	    		this.stop();
+	    	}
+	});
+
+	// Stops scrolling on every mouseup anywhere in the document
+	mxEvent.addListener(document, 'mouseup', this.mouseUpListener);
+
+	var createThread = mxUtils.bind(this, function()
+	{
+	    	this.scrollbars = mxUtils.hasScrollbars(graph.container);
+	    	this.scrollLeft = graph.container.scrollLeft;
+	    	this.scrollTop = graph.container.scrollTop;
+
+	    	return window.setInterval(mxUtils.bind(this, function()
+		{
+			this.tdx -= this.dx;
+			this.tdy -= this.dy;
+
+			if (this.scrollbars)
+			{
+				var left = -graph.container.scrollLeft - Math.ceil(this.dx);
+				var top = -graph.container.scrollTop - Math.ceil(this.dy);
+				graph.panGraph(left, top);
+				graph.panDx = this.scrollLeft - graph.container.scrollLeft;
+				graph.panDy = this.scrollTop - graph.container.scrollTop;
+				graph.fireEvent(new mxEventObject(mxEvent.PAN));
+				// TODO: Implement graph.autoExtend
+			}
+			else
+			{
+				graph.panGraph(this.getDx(), this.getDy());
+			}
+		}), this.delay);
+	});
+
+	this.isActive = function()
+	{
+		return active;
+	};
+
+	this.getDx = function()
+	{
+		return Math.round(this.tdx);
+	};
+
+	this.getDy = function()
+	{
+		return Math.round(this.tdy);
+	};
+
+	this.start = function()
+	{
+		this.t0x = graph.view.translate.x;
+		this.t0y = graph.view.translate.y;
+		this.active = true;
+	};
+
+	this.panTo = function(x, y, w, h)
+	{
+		if (!this.active)
+		{
+			this.start();
+		}
+
+    	this.scrollLeft = graph.container.scrollLeft;
+    	this.scrollTop = graph.container.scrollTop;
+
+		w = (w != null) ? w : 0;
+		h = (h != null) ? h : 0;
+
+		var c = graph.container;
+		this.dx = x + w - c.scrollLeft - c.clientWidth;
+
+		if (this.dx < 0 && Math.abs(this.dx) < this.border)
+		{
+			this.dx = this.border + this.dx;
+		}
+		else if (this.handleMouseOut)
+		{
+			this.dx = Math.max(this.dx, 0);
+		}
+		else
+		{
+			this.dx = 0;
+		}
+
+		if (this.dx == 0)
+		{
+			this.dx = x - c.scrollLeft;
+
+			if (this.dx > 0 && this.dx < this.border)
+			{
+				this.dx = this.dx - this.border;
+			}
+			else if (this.handleMouseOut)
+			{
+				this.dx = Math.min(0, this.dx);
+			}
+			else
+			{
+				this.dx = 0;
+			}
+		}
+
+		this.dy = y + h - c.scrollTop - c.clientHeight;
+
+		if (this.dy < 0 && Math.abs(this.dy) < this.border)
+		{
+			this.dy = this.border + this.dy;
+		}
+		else if (this.handleMouseOut)
+		{
+			this.dy = Math.max(this.dy, 0);
+		}
+		else
+		{
+			this.dy = 0;
+		}
+
+		if (this.dy == 0)
+		{
+			this.dy = y - c.scrollTop;
+
+			if (this.dy > 0 && this.dy < this.border)
+			{
+				this.dy = this.dy - this.border;
+			}
+			else if (this.handleMouseOut)
+			{
+				this.dy = Math.min(0, this.dy);
+			}
+			else
+			{
+				this.dy = 0;
+			}
+		}
+
+		if (this.dx != 0 || this.dy != 0)
+		{
+			this.dx *= this.damper;
+			this.dy *= this.damper;
+
+			if (this.thread == null)
+			{
+				this.thread = createThread();
+			}
+		}
+		else if (this.thread != null)
+		{
+			window.clearInterval(this.thread);
+			this.thread = null;
+		}
+	};
+
+	this.stop = function()
+	{
+		if (this.active)
+		{
+			this.active = false;
+
+			if (this.thread != null)
+	    	{
+				window.clearInterval(this.thread);
+				this.thread = null;
+	    	}
+
+			this.tdx = 0;
+			this.tdy = 0;
+
+			if (!this.scrollbars)
+			{
+				var px = graph.panDx;
+				var py = graph.panDy;
+
+		    	if (px != 0 || py != 0)
+		    	{
+		    		graph.panGraph(0, 0);
+			    	graph.view.setTranslate(this.t0x + px / graph.view.scale, this.t0y + py / graph.view.scale);
+		    	}
+			}
+			else
+			{
+				graph.panDx = 0;
+				graph.panDy = 0;
+				graph.fireEvent(new mxEventObject(mxEvent.PAN));
+			}
+		}
+	};
+
+	this.destroy = function()
+	{
+		graph.removeMouseListener(this.mouseListener);
+		mxEvent.removeListener(document, 'mouseup', this.mouseUpListener);
+	};
+};
+
+/**
+ * Variable: damper
+ *
+ * Damper value for the panning. Default is 1/6.
+ */
+mxPanningManager.prototype.damper = 1/6;
+
+/**
+ * Variable: delay
+ *
+ * Delay in milliseconds for the panning. Default is 10.
+ */
+mxPanningManager.prototype.delay = 10;
+
+/**
+ * Variable: handleMouseOut
+ *
+ * Specifies if mouse events outside of the component should be handled. Default is true.
+ */
+mxPanningManager.prototype.handleMouseOut = true;
+
+/**
+ * Variable: border
+ *
+ * Border to handle automatic panning inside the component. Default is 0 (disabled).
+ */
+mxPanningManager.prototype.border = 0;
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxPopupMenu
+ *
+ * Basic popup menu. To add a vertical scrollbar to a given submenu, the
+ * following code can be used.
+ *
+ * (code)
+ * var mxPopupMenuShowMenu = mxPopupMenu.prototype.showMenu;
+ * mxPopupMenu.prototype.showMenu = function()
+ * {
+ *   mxPopupMenuShowMenu.apply(this, arguments);
+ *
+ *   this.div.style.overflowY = 'auto';
+ *   this.div.style.overflowX = 'hidden';
+ *   this.div.style.maxHeight = '160px';
+ * };
+ * (end)
+ *
+ * Constructor: mxPopupMenu
+ *
+ * Constructs a popupmenu.
+ *
+ * Event: mxEvent.SHOW
+ *
+ * Fires after the menu has been shown in .
+ */
+function mxPopupMenu(factoryMethod)
+{
+	this.factoryMethod = factoryMethod;
+
+	if (factoryMethod != null)
+	{
+		this.init();
+	}
+};
+
+/**
+ * Extends mxEventSource.
+ */
+mxPopupMenu.prototype = new mxEventSource();
+mxPopupMenu.prototype.constructor = mxPopupMenu;
+
+/**
+ * Variable: submenuImage
+ *
+ * URL of the image to be used for the submenu icon.
+ */
+mxPopupMenu.prototype.submenuImage = mxClient.imageBasePath + '/submenu.gif';
+
+/**
+ * Variable: zIndex
+ *
+ * Specifies the zIndex for the popupmenu and its shadow. Default is 1006.
+ */
+mxPopupMenu.prototype.zIndex = 10006;
+
+/**
+ * Variable: factoryMethod
+ *
+ * Function that is used to create the popup menu. The function takes the
+ * current panning handler, the  under the mouse and the mouse
+ * event that triggered the call as arguments.
+ */
+mxPopupMenu.prototype.factoryMethod = null;
+
+/**
+ * Variable: useLeftButtonForPopup
+ *
+ * Specifies if popupmenus should be activated by clicking the left mouse
+ * button. Default is false.
+ */
+mxPopupMenu.prototype.useLeftButtonForPopup = false;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if events are handled. Default is true.
+ */
+mxPopupMenu.prototype.enabled = true;
+
+/**
+ * Variable: itemCount
+ *
+ * Contains the number of times  has been called for a new menu.
+ */
+mxPopupMenu.prototype.itemCount = 0;
+
+/**
+ * Variable: autoExpand
+ *
+ * Specifies if submenus should be expanded on mouseover. Default is false.
+ */
+mxPopupMenu.prototype.autoExpand = false;
+
+/**
+ * Variable: smartSeparators
+ *
+ * Specifies if separators should only be added if a menu item follows them.
+ * Default is false.
+ */
+mxPopupMenu.prototype.smartSeparators = false;
+
+/**
+ * Variable: labels
+ *
+ * Specifies if any labels should be visible. Default is true.
+ */
+mxPopupMenu.prototype.labels = true;
+
+/**
+ * Function: init
+ *
+ * Initializes the shapes required for this vertex handler.
+ */
+mxPopupMenu.prototype.init = function()
+{
+	// Adds the inner table
+	this.table = document.createElement('table');
+	this.table.className = 'mxPopupMenu';
+
+	this.tbody = document.createElement('tbody');
+	this.table.appendChild(this.tbody);
+
+	// Adds the outer div
+	this.div = document.createElement('div');
+	this.div.className = 'mxPopupMenu';
+	this.div.style.display = 'inline';
+	this.div.style.zIndex = this.zIndex;
+	this.div.appendChild(this.table);
+
+	// Disables the context menu on the outer div
+	mxEvent.disableContextMenu(this.div);
+};
+
+/**
+ * Function: isEnabled
+ *
+ * Returns true if events are handled. This implementation
+ * returns .
+ */
+mxPopupMenu.prototype.isEnabled = function()
+{
+	return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Enables or disables event handling. This implementation
+ * updates .
+ */
+mxPopupMenu.prototype.setEnabled = function(enabled)
+{
+	this.enabled = enabled;
+};
+
+/**
+ * Function: isPopupTrigger
+ *
+ * Returns true if the given event is a popupmenu trigger for the optional
+ * given cell.
+ *
+ * Parameters:
+ *
+ * me -  that represents the mouse event.
+ */
+mxPopupMenu.prototype.isPopupTrigger = function(me)
+{
+	return me.isPopupTrigger() || (this.useLeftButtonForPopup && mxEvent.isLeftMouseButton(me.getEvent()));
+};
+
+/**
+ * Function: addItem
+ *
+ * Adds the given item to the given parent item. If no parent item is specified
+ * then the item is added to the top-level menu. The return value may be used
+ * as the parent argument, ie. as a submenu item. The return value is the table
+ * row that represents the item.
+ *
+ * Paramters:
+ *
+ * title - String that represents the title of the menu item.
+ * image - Optional URL for the image icon.
+ * funct - Function associated that takes a mouseup or touchend event.
+ * parent - Optional item returned by .
+ * iconCls - Optional string that represents the CSS class for the image icon.
+ * IconsCls is ignored if image is given.
+ * enabled - Optional boolean indicating if the item is enabled. Default is true.
+ * active - Optional boolean indicating if the menu should implement any event handling.
+ * Default is true.
+ */
+mxPopupMenu.prototype.addItem = function(title, image, funct, parent, iconCls, enabled, active)
+{
+	parent = parent || this;
+	this.itemCount++;
+
+	// Smart separators only added if element contains items
+	if (parent.willAddSeparator)
+	{
+		if (parent.containsItems)
+		{
+			this.addSeparator(parent, true);
+		}
+
+		parent.willAddSeparator = false;
+	}
+
+	parent.containsItems = true;
+	var tr = document.createElement('tr');
+	tr.className = 'mxPopupMenuItem';
+	var col1 = document.createElement('td');
+	col1.className = 'mxPopupMenuIcon';
+
+	// Adds the given image into the first column
+	if (image != null)
+	{
+		var img = document.createElement('img');
+		img.src = image;
+		col1.appendChild(img);
+	}
+	else if (iconCls != null)
+	{
+		var div = document.createElement('div');
+		div.className = iconCls;
+		col1.appendChild(div);
+	}
+
+	tr.appendChild(col1);
+
+	if (this.labels)
+	{
+		var col2 = document.createElement('td');
+		col2.className = 'mxPopupMenuItem' +
+			((enabled != null && !enabled) ? ' mxDisabled' : '');
+
+		mxUtils.write(col2, title);
+		col2.align = 'left';
+		tr.appendChild(col2);
+
+		var col3 = document.createElement('td');
+		col3.className = 'mxPopupMenuItem' +
+			((enabled != null && !enabled) ? ' mxDisabled' : '');
+		col3.style.paddingRight = '6px';
+		col3.style.textAlign = 'right';
+
+		tr.appendChild(col3);
+
+		if (parent.div == null)
+		{
+			this.createSubmenu(parent);
+		}
+	}
+
+	parent.tbody.appendChild(tr);
+
+	if (active != false && enabled != false)
+	{
+		var currentSelection = null;
+
+		mxEvent.addGestureListeners(tr,
+			mxUtils.bind(this, function(evt)
+			{
+				this.eventReceiver = tr;
+
+				if (parent.activeRow != tr && parent.activeRow != parent)
+				{
+					if (parent.activeRow != null && parent.activeRow.div.parentNode != null)
+					{
+						this.hideSubmenu(parent);
+					}
+
+					if (tr.div != null)
+					{
+						this.showSubmenu(parent, tr);
+						parent.activeRow = tr;
+					}
+				}
+
+				// Workaround for lost current selection in page because of focus in IE
+				if (mxClient.IS_QUIRKS || document.documentMode == 8)
+				{
+					currentSelection = document.selection.createRange();
+				}
+
+				mxEvent.consume(evt);
+			}),
+			mxUtils.bind(this, function(evt)
+			{
+				if (parent.activeRow != tr && parent.activeRow != parent)
+				{
+					if (parent.activeRow != null && parent.activeRow.div.parentNode != null)
+					{
+						this.hideSubmenu(parent);
+					}
+
+					if (this.autoExpand && tr.div != null)
+					{
+						this.showSubmenu(parent, tr);
+						parent.activeRow = tr;
+					}
+				}
+
+				// Sets hover style because TR in IE doesn't have hover
+				tr.className = 'mxPopupMenuItemHover';
+			}),
+			mxUtils.bind(this, function(evt)
+			{
+				// EventReceiver avoids clicks on a submenu item
+				// which has just been shown in the mousedown
+				if (this.eventReceiver == tr)
+				{
+					if (parent.activeRow != tr)
+					{
+						this.hideMenu();
+					}
+
+					// Workaround for lost current selection in page because of focus in IE
+					if (currentSelection != null)
+					{
+						// Workaround for "unspecified error" in IE8 standards
+						try
+						{
+							currentSelection.select();
+						}
+						catch (e)
+						{
+							// ignore
+						}
+
+						currentSelection = null;
+					}
+
+					if (funct != null)
+					{
+						funct(evt);
+					}
+				}
+
+				this.eventReceiver = null;
+				mxEvent.consume(evt);
+			})
+		);
+
+		// Resets hover style because TR in IE doesn't have hover
+		mxEvent.addListener(tr, 'mouseout',
+			mxUtils.bind(this, function(evt)
+			{
+				tr.className = 'mxPopupMenuItem';
+			})
+		);
+	}
+
+	return tr;
+};
+
+/**
+ * Adds a checkmark to the given menuitem.
+ */
+mxPopupMenu.prototype.addCheckmark = function(item, img)
+{
+	var td = item.firstChild.nextSibling;
+	td.style.backgroundImage = 'url(\'' + img + '\')';
+	td.style.backgroundRepeat = 'no-repeat';
+	td.style.backgroundPosition = '2px 50%';
+};
+
+/**
+ * Function: createSubmenu
+ *
+ * Creates the nodes required to add submenu items inside the given parent
+ * item. This is called in  if a parent item is used for the first
+ * time. This adds various DOM nodes and a  to the parent.
+ *
+ * Parameters:
+ *
+ * parent - An item returned by .
+ */
+mxPopupMenu.prototype.createSubmenu = function(parent)
+{
+	parent.table = document.createElement('table');
+	parent.table.className = 'mxPopupMenu';
+
+	parent.tbody = document.createElement('tbody');
+	parent.table.appendChild(parent.tbody);
+
+	parent.div = document.createElement('div');
+	parent.div.className = 'mxPopupMenu';
+
+	parent.div.style.position = 'absolute';
+	parent.div.style.display = 'inline';
+	parent.div.style.zIndex = this.zIndex;
+
+	parent.div.appendChild(parent.table);
+
+	var img = document.createElement('img');
+	img.setAttribute('src', this.submenuImage);
+
+	// Last column of the submenu item in the parent menu
+	td = parent.firstChild.nextSibling.nextSibling;
+	td.appendChild(img);
+};
+
+/**
+ * Function: showSubmenu
+ *
+ * Shows the submenu inside the given parent row.
+ */
+mxPopupMenu.prototype.showSubmenu = function(parent, row)
+{
+	if (row.div != null)
+	{
+		row.div.style.left = (parent.div.offsetLeft +
+			row.offsetLeft+row.offsetWidth - 1) + 'px';
+		row.div.style.top = (parent.div.offsetTop+row.offsetTop) + 'px';
+		document.body.appendChild(row.div);
+
+		// Moves the submenu to the left side if there is no space
+		var left = parseInt(row.div.offsetLeft);
+		var width = parseInt(row.div.offsetWidth);
+		var offset = mxUtils.getDocumentScrollOrigin(document);
+
+		var b = document.body;
+		var d = document.documentElement;
+
+		var right = offset.x + (b.clientWidth || d.clientWidth);
+
+		if (left + width > right)
+		{
+			row.div.style.left = Math.max(0, (parent.div.offsetLeft - width + ((mxClient.IS_IE) ? 6 : -6))) + 'px';
+		}
+
+		mxUtils.fit(row.div);
+	}
+};
+
+/**
+ * Function: addSeparator
+ *
+ * Adds a horizontal separator in the given parent item or the top-level menu
+ * if no parent is specified.
+ *
+ * Parameters:
+ *
+ * parent - Optional item returned by .
+ * force - Optional boolean to ignore . Default is false.
+ */
+mxPopupMenu.prototype.addSeparator = function(parent, force)
+{
+	parent = parent || this;
+
+	if (this.smartSeparators && !force)
+	{
+		parent.willAddSeparator = true;
+	}
+	else if (parent.tbody != null)
+	{
+		parent.willAddSeparator = false;
+		var tr = document.createElement('tr');
+
+		var col1 = document.createElement('td');
+		col1.className = 'mxPopupMenuIcon';
+		col1.style.padding = '0 0 0 0px';
+
+		tr.appendChild(col1);
+
+		var col2 = document.createElement('td');
+		col2.style.padding = '0 0 0 0px';
+		col2.setAttribute('colSpan', '2');
+
+		var hr = document.createElement('hr');
+		hr.setAttribute('size', '1');
+		col2.appendChild(hr);
+
+		tr.appendChild(col2);
+
+		parent.tbody.appendChild(tr);
+	}
+};
+
+/**
+ * Function: popup
+ *
+ * Shows the popup menu for the given event and cell.
+ *
+ * Example:
+ *
+ * (code)
+ * graph.panningHandler.popup = function(x, y, cell, evt)
+ * {
+ *   mxUtils.alert('Hello, World!');
+ * }
+ * (end)
+ */
+mxPopupMenu.prototype.popup = function(x, y, cell, evt)
+{
+	if (this.div != null && this.tbody != null && this.factoryMethod != null)
+	{
+		this.div.style.left = x + 'px';
+		this.div.style.top = y + 'px';
+
+		// Removes all child nodes from the existing menu
+		while (this.tbody.firstChild != null)
+		{
+			mxEvent.release(this.tbody.firstChild);
+			this.tbody.removeChild(this.tbody.firstChild);
+		}
+
+		this.itemCount = 0;
+		this.factoryMethod(this, cell, evt);
+
+		if (this.itemCount > 0)
+		{
+			this.showMenu();
+			this.fireEvent(new mxEventObject(mxEvent.SHOW));
+		}
+	}
+};
+
+/**
+ * Function: isMenuShowing
+ *
+ * Returns true if the menu is showing.
+ */
+mxPopupMenu.prototype.isMenuShowing = function()
+{
+	return this.div != null && this.div.parentNode == document.body;
+};
+
+/**
+ * Function: showMenu
+ *
+ * Shows the menu.
+ */
+mxPopupMenu.prototype.showMenu = function()
+{
+	// Disables filter-based shadow in IE9 standards mode
+	if (document.documentMode >= 9)
+	{
+		this.div.style.filter = 'none';
+	}
+
+	// Fits the div inside the viewport
+	document.body.appendChild(this.div);
+	mxUtils.fit(this.div);
+};
+
+/**
+ * Function: hideMenu
+ *
+ * Removes the menu and all submenus.
+ */
+mxPopupMenu.prototype.hideMenu = function()
+{
+	if (this.div != null)
+	{
+		if (this.div.parentNode != null)
+		{
+			this.div.parentNode.removeChild(this.div);
+		}
+
+		this.hideSubmenu(this);
+		this.containsItems = false;
+		this.fireEvent(new mxEventObject(mxEvent.HIDE));
+	}
+};
+
+/**
+ * Function: hideSubmenu
+ *
+ * Removes all submenus inside the given parent.
+ *
+ * Parameters:
+ *
+ * parent - An item returned by .
+ */
+mxPopupMenu.prototype.hideSubmenu = function(parent)
+{
+	if (parent.activeRow != null)
+	{
+		this.hideSubmenu(parent.activeRow);
+
+		if (parent.activeRow.div.parentNode != null)
+		{
+			parent.activeRow.div.parentNode.removeChild(parent.activeRow.div);
+		}
+
+		parent.activeRow = null;
+	}
+};
+
+/**
+ * Function: destroy
+ *
+ * Destroys the handler and all its resources and DOM nodes.
+ */
+mxPopupMenu.prototype.destroy = function()
+{
+	if (this.div != null)
+	{
+		mxEvent.release(this.div);
+
+		if (this.div.parentNode != null)
+		{
+			this.div.parentNode.removeChild(this.div);
+		}
+
+		this.div = null;
+	}
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxAutoSaveManager
+ *
+ * Manager for automatically saving diagrams. The  hook must be
+ * implemented.
+ *
+ * Example:
+ *
+ * (code)
+ * var mgr = new mxAutoSaveManager(editor.graph);
+ * mgr.save = function()
+ * {
+ *   mxLog.show();
+ *   mxLog.debug('save');
+ * };
+ * (end)
+ *
+ * Constructor: mxAutoSaveManager
+ *
+ * Constructs a new automatic layout for the given graph.
+ *
+ * Arguments:
+ *
+ * graph - Reference to the enclosing graph.
+ */
+function mxAutoSaveManager(graph)
+{
+	// Notifies the manager of a change
+	this.changeHandler = mxUtils.bind(this, function(sender, evt)
+	{
+		if (this.isEnabled())
+		{
+			this.graphModelChanged(evt.getProperty('edit').changes);
+		}
+	});
+
+	this.setGraph(graph);
+};
+
+/**
+ * Extends mxEventSource.
+ */
+mxAutoSaveManager.prototype = new mxEventSource();
+mxAutoSaveManager.prototype.constructor = mxAutoSaveManager;
+
+/**
+ * Variable: graph
+ *
+ * Reference to the enclosing .
+ */
+mxAutoSaveManager.prototype.graph = null;
+
+/**
+ * Variable: autoSaveDelay
+ *
+ * Minimum amount of seconds between two consecutive autosaves. Eg. a
+ * value of 1 (s) means the graph is not stored more than once per second.
+ * Default is 10.
+ */
+mxAutoSaveManager.prototype.autoSaveDelay = 10;
+
+/**
+ * Variable: autoSaveThrottle
+ *
+ * Minimum amount of seconds between two consecutive autosaves triggered by
+ * more than  changes within a timespan of less than
+ *  seconds. Eg. a value of 1 (s) means the graph is not
+ * stored more than once per second even if there are more than
+ *  changes within that timespan. Default is 2.
+ */
+mxAutoSaveManager.prototype.autoSaveThrottle = 2;
+
+/**
+ * Variable: autoSaveThreshold
+ *
+ * Minimum amount of ignored changes before an autosave. Eg. a value of 2
+ * means after 2 change of the graph model the autosave will trigger if the
+ * condition below is true. Default is 5.
+ */
+mxAutoSaveManager.prototype.autoSaveThreshold = 5;
+
+/**
+ * Variable: ignoredChanges
+ *
+ * Counter for ignored changes in autosave.
+ */
+mxAutoSaveManager.prototype.ignoredChanges = 0;
+
+/**
+ * Variable: lastSnapshot
+ *
+ * Used for autosaving. See .
+ */
+mxAutoSaveManager.prototype.lastSnapshot = 0;
+
+/**
+ * Variable: enabled
+ *
+ * Specifies if event handling is enabled. Default is true.
+ */
+mxAutoSaveManager.prototype.enabled = true;
+
+/**
+ * Variable: changeHandler
+ *
+ * Holds the function that handles graph model changes.
+ */
+mxAutoSaveManager.prototype.changeHandler = null;
+
+/**
+ * Function: isEnabled
+ *
+ * Returns true if events are handled. This implementation
+ * returns .
+ */
+mxAutoSaveManager.prototype.isEnabled = function()
+{
+	return this.enabled;
+};
+
+/**
+ * Function: setEnabled
+ *
+ * Enables or disables event handling. This implementation
+ * updates .
+ *
+ * Parameters:
+ *
+ * enabled - Boolean that specifies the new enabled state.
+ */
+mxAutoSaveManager.prototype.setEnabled = function(value)
+{
+	this.enabled = value;
+};
+
+/**
+ * Function: setGraph
+ *
+ * Sets the graph that the layouts operate on.
+ */
+mxAutoSaveManager.prototype.setGraph = function(graph)
+{
+	if (this.graph != null)
+	{
+		this.graph.getModel().removeListener(this.changeHandler);
+	}
+
+	this.graph = graph;
+
+	if (this.graph != null)
+	{
+		this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler);
+	}
+};
+
+/**
+ * Function: save
+ *
+ * Empty hook that is called if the graph should be saved.
+ */
+mxAutoSaveManager.prototype.save = function()
+{
+	// empty
+};
+
+/**
+ * Function: graphModelChanged
+ *
+ * Invoked when the graph model has changed.
+ */
+mxAutoSaveManager.prototype.graphModelChanged = function(changes)
+{
+	var now = new Date().getTime();
+	var dt = (now - this.lastSnapshot) / 1000;
+
+	if (dt > this.autoSaveDelay ||
+		(this.ignoredChanges >= this.autoSaveThreshold &&
+		 dt > this.autoSaveThrottle))
+	{
+		this.save();
+		this.reset();
+	}
+	else
+	{
+		// Increments the number of ignored changes
+		this.ignoredChanges++;
+	}
+};
+
+/**
+ * Function: reset
+ *
+ * Resets all counters.
+ */
+mxAutoSaveManager.prototype.reset = function()
+{
+	this.lastSnapshot = new Date().getTime();
+	this.ignoredChanges = 0;
+};
+
+/**
+ * Function: destroy
+ *
+ * Removes all handlers from the  and deletes the reference to it.
+ */
+mxAutoSaveManager.prototype.destroy = function()
+{
+	this.setGraph(null);
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ *
+ * Class: mxAnimation
+ *
+ * Implements a basic animation in JavaScript.
+ *
+ * Constructor: mxAnimation
+ *
+ * Constructs an animation.
+ *
+ * Parameters:
+ *
+ * graph - Reference to the enclosing .
+ */
+function mxAnimation(delay)
+{
+	this.delay = (delay != null) ? delay : 20;
+};
+
+/**
+ * Extends mxEventSource.
+ */
+mxAnimation.prototype = new mxEventSource();
+mxAnimation.prototype.constructor = mxAnimation;
+
+/**
+ * Variable: delay
+ *
+ * Specifies the delay between the animation steps. Defaul is 30ms.
+ */
+mxAnimation.prototype.delay = null;
+
+/**
+ * Variable: thread
+ *
+ * Reference to the thread while the animation is running.
+ */
+mxAnimation.prototype.thread = null;
+
+/**
+ * Function: isRunning
+ *
+ * Returns true if the animation is running.
+ */
+mxAnimation.prototype.isRunning = function()
+{
+	return this.thread != null;
+};
+
+/**
+ * Function: startAnimation
+ *
+ * Starts the animation by repeatedly invoking updateAnimation.
+ */
+mxAnimation.prototype.startAnimation = function()
+{
+	if (this.thread == null)
+	{
+		this.thread = window.setInterval(mxUtils.bind(this, this.updateAnimation), this.delay);
+	}
+};
+
+/**
+ * Function: updateAnimation
+ *
+ * Hook for subclassers to implement the animation. Invoke stopAnimation
+ * when finished, startAnimation to resume. This is called whenever the
+ * timer fires and fires an mxEvent.EXECUTE event with no properties.
+ */
+mxAnimation.prototype.updateAnimation = function()
+{
+	this.fireEvent(new mxEventObject(mxEvent.EXECUTE));
+};
+
+/**
+ * Function: stopAnimation
+ *
+ * Stops the animation by deleting the timer and fires an .
+ */
+mxAnimation.prototype.stopAnimation = function()
+{
+	if (this.thread != null)
+	{
+		window.clearInterval(this.thread);
+		this.thread = null;
+		this.fireEvent(new mxEventObject(mxEvent.DONE));
+	}
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ *
+ * Class: mxMorphing
+ *
+ * Implements animation for morphing cells. Here is an example of
+ * using this class for animating the result of a layout algorithm:
+ *
+ * (code)
+ * graph.getModel().beginUpdate();
+ * try
+ * {
+ *   var circleLayout = new mxCircleLayout(graph);
+ *   circleLayout.execute(graph.getDefaultParent());
+ * }
+ * finally
+ * {
+ *   var morph = new mxMorphing(graph);
+ *   morph.addListener(mxEvent.DONE, function()
+ *   {
+ *     graph.getModel().endUpdate();
+ *   });
+ *
+ *   morph.startAnimation();
+ * }
+ * (end)
+ *
+ * Constructor: mxMorphing
+ *
+ * Constructs an animation.
+ *
+ * Parameters:
+ *
+ * graph - Reference to the enclosing .
+ * steps - Optional number of steps in the morphing animation. Default is 6.
+ * ease - Optional easing constant for the animation. Default is 1.5.
+ * delay - Optional delay between the animation steps. Passed to .
+ */
+function mxMorphing(graph, steps, ease, delay)
+{
+	mxAnimation.call(this, delay);
+	this.graph = graph;
+	this.steps = (steps != null) ? steps : 6;
+	this.ease = (ease != null) ? ease : 1.5;
+};
+
+/**
+ * Extends mxEventSource.
+ */
+mxMorphing.prototype = new mxAnimation();
+mxMorphing.prototype.constructor = mxMorphing;
+
+/**
+ * Variable: graph
+ *
+ * Specifies the delay between the animation steps. Defaul is 30ms.
+ */
+mxMorphing.prototype.graph = null;
+
+/**
+ * Variable: steps
+ *
+ * Specifies the maximum number of steps for the morphing.
+ */
+mxMorphing.prototype.steps = null;
+
+/**
+ * Variable: step
+ *
+ * Contains the current step.
+ */
+mxMorphing.prototype.step = 0;
+
+/**
+ * Variable: ease
+ *
+ * Ease-off for movement towards the given vector. Larger values are
+ * slower and smoother. Default is 4.
+ */
+mxMorphing.prototype.ease = null;
+
+/**
+ * Variable: cells
+ *
+ * Optional array of cells to be animated. If this is not specified
+ * then all cells are checked and animated if they have been moved
+ * in the current transaction.
+ */
+mxMorphing.prototype.cells = null;
+
+/**
+ * Function: updateAnimation
+ *
+ * Animation step.
+ */
+mxMorphing.prototype.updateAnimation = function()
+{
+	mxAnimation.prototype.updateAnimation.apply(this, arguments);
+	var move = new mxCellStatePreview(this.graph);
+
+	if (this.cells != null)
+	{
+		// Animates the given cells individually without recursion
+		for (var i = 0; i < this.cells.length; i++)
+		{
+			this.animateCell(this.cells[i], move, false);
+		}
+	}
+	else
+	{
+		// Animates all changed cells by using recursion to find
+		// the changed cells but not for the animation itself
+		this.animateCell(this.graph.getModel().getRoot(), move, true);
+	}
+
+	this.show(move);
+
+	if (move.isEmpty() || this.step++ >= this.steps)
+	{
+		this.stopAnimation();
+	}
+};
+
+/**
+ * Function: show
+ *
+ * Shows the changes in the given .
+ */
+mxMorphing.prototype.show = function(move)
+{
+	move.show();
+};
+
+/**
+ * Function: animateCell
+ *
+ * Animates the given cell state using .
+ */
+mxMorphing.prototype.animateCell = function(cell, move, recurse)
+{
+	var state = this.graph.getView().getState(cell);
+	var delta = null;
+
+	if (state != null)
+	{
+		// Moves the animated state from where it will be after the model
+		// change by subtracting the given delta vector from that location
+		delta = this.getDelta(state);
+
+		if (this.graph.getModel().isVertex(cell) && (delta.x != 0 || delta.y != 0))
+		{
+			var translate = this.graph.view.getTranslate();
+			var scale = this.graph.view.getScale();
+
+			delta.x += translate.x * scale;
+			delta.y += translate.y * scale;
+
+			move.moveState(state, -delta.x / this.ease, -delta.y / this.ease);
+		}
+	}
+
+	if (recurse && !this.stopRecursion(state, delta))
+	{
+		var childCount = this.graph.getModel().getChildCount(cell);
+
+		for (var i = 0; i < childCount; i++)
+		{
+			this.animateCell(this.graph.getModel().getChildAt(cell, i), move, recurse);
+		}
+	}
+};
+
+/**
+ * Function: stopRecursion
+ *
+ * Returns true if the animation should not recursively find more
+ * deltas for children if the given parent state has been animated.
+ */
+mxMorphing.prototype.stopRecursion = function(state, delta)
+{
+	return delta != null && (delta.x != 0 || delta.y != 0);
+};
+
+/**
+ * Function: getDelta
+ *
+ * Returns the vector between the current rendered state and the future
+ * location of the state after the display will be updated.
+ */
+mxMorphing.prototype.getDelta = function(state)
+{
+	var origin = this.getOriginForCell(state.cell);
+	var translate = this.graph.getView().getTranslate();
+	var scale = this.graph.getView().getScale();
+	var x = state.x / scale - translate.x;
+	var y = state.y / scale - translate.y;
+
+	return new mxPoint((origin.x - x) * scale, (origin.y - y) * scale);
+};
+
+/**
+ * Function: getOriginForCell
+ *
+ * Returns the top, left corner of the given cell. TODO: Improve performance
+ * by using caching inside this method as the result per cell never changes
+ * during the lifecycle of this object.
+ */
+mxMorphing.prototype.getOriginForCell = function(cell)
+{
+	var result = null;
+
+	if (cell != null)
+	{
+		var parent = this.graph.getModel().getParent(cell);
+		var geo = this.graph.getCellGeometry(cell);
+		result = this.getOriginForCell(parent);
+
+		// TODO: Handle offsets
+		if (geo != null)
+		{
+			if (geo.relative)
+			{
+				var pgeo = this.graph.getCellGeometry(parent);
+
+				if (pgeo != null)
+				{
+					result.x += geo.x * pgeo.width;
+					result.y += geo.y * pgeo.height;
+				}
+			}
+			else
+			{
+				result.x += geo.x;
+				result.y += geo.y;
+			}
+		}
+	}
+
+	if (result == null)
+	{
+		var t = this.graph.view.getTranslate();
+		result = new mxPoint(-t.x, -t.y);
+	}
+
+	return result;
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxImageBundle
+ *
+ * Maps from keys to base64 encoded images or file locations. All values must
+ * be URLs or use the format data:image/format followed by a comma and the base64
+ * encoded image data, eg. "data:image/gif,XYZ", where XYZ is the base64 encoded
+ * image data.
+ *
+ * To add a new image bundle to an existing graph, the following code is used:
+ *
+ * (code)
+ * var bundle = new mxImageBundle(alt);
+ * bundle.putImage('myImage', 'data:image/gif,R0lGODlhEAAQAMIGAAAAAICAAICAgP' +
+ *   '//AOzp2O3r2////////yH+FUNyZWF0ZWQgd2l0aCBUaGUgR0lNUAAh+QQBCgAHACwAAAAA' +
+ *   'EAAQAAADTXi63AowynnAMDfjPUDlnAAJhmeBFxAEloliKltWmiYCQvfVr6lBPB1ggxN1hi' +
+ *   'laSSASFQpIV5HJBDyHpqK2ejVRm2AAgZCdmCGO9CIBADs=', fallback);
+ * bundle.putImage('mySvgImage', 'data:image/svg+xml,' + encodeURIComponent(
+ *   '' +
+ *   '' +
+ *   '' +
+ *   ''), fallback);
+ * graph.addImageBundle(bundle);
+ * (end);
+ *
+ * Alt is an optional boolean (default is false) that specifies if the value
+ * or the fallback should be returned in .
+ *
+ * The image can then be referenced in any cell style using image=myImage.
+ * If you are using mxOutline, you should use the same image bundles in the
+ * graph that renders the outline.
+ *
+ * The keys for images are resolved in  and
+ * turned into a data URI if the returned value has a short data URI format
+ * as specified above.
+ *
+ * A typical value for the fallback is a MTHML link as defined in RFC 2557.
+ * Note that this format requires a file to be dynamically created on the
+ * server-side, or the page that contains the graph to be modified to contain
+ * the resources, this can be done by adding a comment that contains the
+ * resource in the HEAD section of the page after the title tag.
+ *
+ * This type of fallback mechanism should be used in IE6 and IE7. IE8 does
+ * support data URIs, but the maximum size is limited to 32 KB, which means
+ * all data URIs should be limited to 32 KB.
+ */
+function mxImageBundle(alt)
+{
+	this.images = [];
+	this.alt = (alt != null) ? alt : false;
+};
+
+/**
+ * Variable: images
+ *
+ * Maps from keys to images.
+ */
+mxImageBundle.prototype.images = null;
+
+/**
+ * Variable: alt
+ *
+ * Specifies if the fallback representation should be returned.
+ */
+mxImageBundle.prototype.images = null;
+
+/**
+ * Function: putImage
+ *
+ * Adds the specified entry to the map. The entry is an object with a value and
+ * fallback property as specified in the arguments.
+ */
+mxImageBundle.prototype.putImage = function(key, value, fallback)
+{
+	this.images[key] = {value: value, fallback: fallback};
+};
+
+/**
+ * Function: getImage
+ *
+ * Returns the value for the given key. This returns the value
+ * or fallback, depending on . The fallback is returned if
+ *  is true, the value is returned otherwise.
+ */
+mxImageBundle.prototype.getImage = function(key)
+{
+	var result = null;
+
+	if (key != null)
+	{
+		var img = this.images[key];
+
+		if (img != null)
+		{
+			result = (this.alt) ? img.fallback : img.value;
+		}
+	}
+
+	return result;
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxImageExport
+ *
+ * Creates a new image export instance to be used with an export canvas. Here
+ * is an example that uses this class to create an image via a backend using
+ * .
+ *
+ * (code)
+ * var xmlDoc = mxUtils.createXmlDocument();
+ * var root = xmlDoc.createElement('output');
+ * xmlDoc.appendChild(root);
+ *
+ * var xmlCanvas = new mxXmlCanvas2D(root);
+ * var imgExport = new mxImageExport();
+ * imgExport.drawState(graph.getView().getState(graph.model.root), xmlCanvas);
+ *
+ * var bounds = graph.getGraphBounds();
+ * var w = Math.ceil(bounds.x + bounds.width);
+ * var h = Math.ceil(bounds.y + bounds.height);
+ *
+ * var xml = mxUtils.getXml(root);
+ * new mxXmlRequest('export', 'format=png&w=' + w +
+ * 		'&h=' + h + '&bg=#F9F7ED&xml=' + encodeURIComponent(xml))
+ * 		.simulate(document, '_blank');
+ * (end)
+ *
+ * Constructor: mxImageExport
+ *
+ * Constructs a new image export.
+ */
+function mxImageExport() { };
+
+/**
+ * Variable: includeOverlays
+ *
+ * Specifies if overlays should be included in the export. Default is false.
+ */
+mxImageExport.prototype.includeOverlays = false;
+
+/**
+ * Function: drawState
+ *
+ * Draws the given state and all its descendants to the given canvas.
+ */
+mxImageExport.prototype.drawState = function(state, canvas)
+{
+	if (state != null)
+	{
+		this.visitStatesRecursive(state, canvas, mxUtils.bind(this, function()
+		{
+			this.drawCellState.apply(this, arguments);
+		}));
+
+		// Paints the overlays
+		if (this.includeOverlays)
+		{
+			this.visitStatesRecursive(state, canvas, mxUtils.bind(this, function()
+			{
+				this.drawOverlays.apply(this, arguments);
+			}));
+		}
+	}
+};
+
+/**
+ * Function: drawState
+ *
+ * Draws the given state and all its descendants to the given canvas.
+ */
+mxImageExport.prototype.visitStatesRecursive = function(state, canvas, visitor)
+{
+	if (state != null)
+	{
+		visitor(state, canvas);
+
+		var graph = state.view.graph;
+		var childCount = graph.model.getChildCount(state.cell);
+
+		for (var i = 0; i < childCount; i++)
+		{
+			var childState = graph.view.getState(graph.model.getChildAt(state.cell, i));
+			this.visitStatesRecursive(childState, canvas, visitor);
+		}
+	}
+};
+
+/**
+ * Function: getLinkForCellState
+ *
+ * Returns the link for the given cell state and canvas. This returns null.
+ */
+mxImageExport.prototype.getLinkForCellState = function(state, canvas)
+{
+	return null;
+};
+
+/**
+ * Function: drawCellState
+ *
+ * Draws the given state to the given canvas.
+ */
+mxImageExport.prototype.drawCellState = function(state, canvas)
+{
+	// Experimental feature
+	var link = this.getLinkForCellState(state, canvas);
+
+	if (link != null)
+	{
+		canvas.setLink(link);
+	}
+
+	// Paints the shape and text
+	this.drawShape(state, canvas);
+	this.drawText(state, canvas);
+
+	if (link != null)
+	{
+		canvas.setLink(null);
+	}
+};
+
+/**
+ * Function: drawShape
+ *
+ * Draws the shape of the given state.
+ */
+mxImageExport.prototype.drawShape = function(state, canvas)
+{
+	if (state.shape instanceof mxShape && state.shape.checkBounds())
+	{
+		canvas.save();
+		state.shape.paint(canvas);
+		canvas.restore();
+	}
+};
+
+/**
+ * Function: drawText
+ *
+ * Draws the text of the given state.
+ */
+mxImageExport.prototype.drawText = function(state, canvas)
+{
+	if (state.text != null && state.text.checkBounds())
+	{
+		canvas.save();
+		state.text.paint(canvas);
+		canvas.restore();
+	}
+};
+
+/**
+ * Function: drawOverlays
+ *
+ * Draws the overlays for the given state. This is called if 
+ * is true.
+ */
+mxImageExport.prototype.drawOverlays = function(state, canvas)
+{
+	if (state.overlays != null)
+	{
+		state.overlays.visit(function(id, shape)
+		{
+			if (shape instanceof mxShape)
+			{
+				shape.paint(canvas);
+			}
+		});
+	}
+};
+
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxAbstractCanvas2D
+ *
+ * Base class for all canvases. A description of the public API is available in .
+ * All color values of  will be converted to null in the state.
+ *
+ * Constructor: mxAbstractCanvas2D
+ *
+ * Constructs a new abstract canvas.
+ */
+function mxAbstractCanvas2D()
+{
+	/**
+	 * Variable: converter
+	 *
+	 * Holds the  to convert image URLs.
+	 */
+	this.converter = this.createUrlConverter();
+
+	this.reset();
+};
+
+/**
+ * Variable: state
+ *
+ * Holds the current state.
+ */
+mxAbstractCanvas2D.prototype.state = null;
+
+/**
+ * Variable: states
+ *
+ * Stack of states.
+ */
+mxAbstractCanvas2D.prototype.states = null;
+
+/**
+ * Variable: path
+ *
+ * Holds the current path as an array.
+ */
+mxAbstractCanvas2D.prototype.path = null;
+
+/**
+ * Variable: rotateHtml
+ *
+ * Switch for rotation of HTML. Default is false.
+ */
+mxAbstractCanvas2D.prototype.rotateHtml = true;
+
+/**
+ * Variable: lastX
+ *
+ * Holds the last x coordinate.
+ */
+mxAbstractCanvas2D.prototype.lastX = 0;
+
+/**
+ * Variable: lastY
+ *
+ * Holds the last y coordinate.
+ */
+mxAbstractCanvas2D.prototype.lastY = 0;
+
+/**
+ * Variable: moveOp
+ *
+ * Contains the string used for moving in paths. Default is 'M'.
+ */
+mxAbstractCanvas2D.prototype.moveOp = 'M';
+
+/**
+ * Variable: lineOp
+ *
+ * Contains the string used for moving in paths. Default is 'L'.
+ */
+mxAbstractCanvas2D.prototype.lineOp = 'L';
+
+/**
+ * Variable: quadOp
+ *
+ * Contains the string used for quadratic paths. Default is 'Q'.
+ */
+mxAbstractCanvas2D.prototype.quadOp = 'Q';
+
+/**
+ * Variable: curveOp
+ *
+ * Contains the string used for bezier curves. Default is 'C'.
+ */
+mxAbstractCanvas2D.prototype.curveOp = 'C';
+
+/**
+ * Variable: closeOp
+ *
+ * Holds the operator for closing curves. Default is 'Z'.
+ */
+mxAbstractCanvas2D.prototype.closeOp = 'Z';
+
+/**
+ * Variable: pointerEvents
+ *
+ * Boolean value that specifies if events should be handled. Default is false.
+ */
+mxAbstractCanvas2D.prototype.pointerEvents = false;
+
+/**
+ * Function: createUrlConverter
+ *
+ * Create a new  and returns it.
+ */
+mxAbstractCanvas2D.prototype.createUrlConverter = function()
+{
+	return new mxUrlConverter();
+};
+
+/**
+ * Function: reset
+ *
+ * Resets the state of this canvas.
+ */
+mxAbstractCanvas2D.prototype.reset = function()
+{
+	this.state = this.createState();
+	this.states = [];
+};
+
+/**
+ * Function: createState
+ *
+ * Creates the state of the this canvas.
+ */
+mxAbstractCanvas2D.prototype.createState = function()
+{
+	return {
+		dx: 0,
+		dy: 0,
+		scale: 1,
+		alpha: 1,
+		fillAlpha: 1,
+		strokeAlpha: 1,
+		fillColor: null,
+		gradientFillAlpha: 1,
+		gradientColor: null,
+		gradientAlpha: 1,
+		gradientDirection: null,
+		strokeColor: null,
+		strokeWidth: 1,
+		dashed: false,
+		dashPattern: '3 3',
+		fixDash: false,
+		lineCap: 'flat',
+		lineJoin: 'miter',
+		miterLimit: 10,
+		fontColor: '#000000',
+		fontBackgroundColor: null,
+		fontBorderColor: null,
+		fontSize: mxConstants.DEFAULT_FONTSIZE,
+		fontFamily: mxConstants.DEFAULT_FONTFAMILY,
+		fontStyle: 0,
+		shadow: false,
+		shadowColor: mxConstants.SHADOWCOLOR,
+		shadowAlpha: mxConstants.SHADOW_OPACITY,
+		shadowDx: mxConstants.SHADOW_OFFSET_X,
+		shadowDy: mxConstants.SHADOW_OFFSET_Y,
+		rotation: 0,
+		rotationCx: 0,
+		rotationCy: 0
+	};
+};
+
+/**
+ * Function: format
+ *
+ * Rounds all numbers to integers.
+ */
+mxAbstractCanvas2D.prototype.format = function(value)
+{
+	return Math.round(parseFloat(value));
+};
+
+/**
+ * Function: addOp
+ *
+ * Adds the given operation to the path.
+ */
+mxAbstractCanvas2D.prototype.addOp = function()
+{
+	if (this.path != null)
+	{
+		this.path.push(arguments[0]);
+
+		if (arguments.length > 2)
+		{
+			var s = this.state;
+
+			for (var i = 2; i < arguments.length; i += 2)
+			{
+				this.lastX = arguments[i - 1];
+				this.lastY = arguments[i];
+
+				this.path.push(this.format((this.lastX + s.dx) * s.scale));
+				this.path.push(this.format((this.lastY + s.dy) * s.scale));
+			}
+		}
+	}
+};
+
+/**
+ * Function: rotatePoint
+ *
+ * Rotates the given point and returns the result as an .
+ */
+mxAbstractCanvas2D.prototype.rotatePoint = function(x, y, theta, cx, cy)
+{
+	var rad = theta * (Math.PI / 180);
+
+	return mxUtils.getRotatedPoint(new mxPoint(x, y), Math.cos(rad),
+		Math.sin(rad), new mxPoint(cx, cy));
+};
+
+/**
+ * Function: save
+ *
+ * Saves the current state.
+ */
+mxAbstractCanvas2D.prototype.save = function()
+{
+	this.states.push(this.state);
+	this.state = mxUtils.clone(this.state);
+};
+
+/**
+ * Function: restore
+ *
+ * Restores the current state.
+ */
+mxAbstractCanvas2D.prototype.restore = function()
+{
+	if (this.states.length > 0)
+	{
+		this.state = this.states.pop();
+	}
+};
+
+/**
+ * Function: setLink
+ *
+ * Sets the current link. Hook for subclassers.
+ */
+mxAbstractCanvas2D.prototype.setLink = function(link)
+{
+	// nop
+};
+
+/**
+ * Function: scale
+ *
+ * Scales the current state.
+ */
+mxAbstractCanvas2D.prototype.scale = function(value)
+{
+	this.state.scale *= value;
+	this.state.strokeWidth *= value;
+};
+
+/**
+ * Function: translate
+ *
+ * Translates the current state.
+ */
+mxAbstractCanvas2D.prototype.translate = function(dx, dy)
+{
+	this.state.dx += dx;
+	this.state.dy += dy;
+};
+
+/**
+ * Function: rotate
+ *
+ * Rotates the current state.
+ */
+mxAbstractCanvas2D.prototype.rotate = function(theta, flipH, flipV, cx, cy)
+{
+	// nop
+};
+
+/**
+ * Function: setAlpha
+ *
+ * Sets the current alpha.
+ */
+mxAbstractCanvas2D.prototype.setAlpha = function(value)
+{
+	this.state.alpha = value;
+};
+
+/**
+ * Function: setFillAlpha
+ *
+ * Sets the current solid fill alpha.
+ */
+mxAbstractCanvas2D.prototype.setFillAlpha = function(value)
+{
+	this.state.fillAlpha = value;
+};
+
+/**
+ * Function: setStrokeAlpha
+ *
+ * Sets the current stroke alpha.
+ */
+mxAbstractCanvas2D.prototype.setStrokeAlpha = function(value)
+{
+	this.state.strokeAlpha = value;
+};
+
+/**
+ * Function: setFillColor
+ *
+ * Sets the current fill color.
+ */
+mxAbstractCanvas2D.prototype.setFillColor = function(value)
+{
+	if (value == mxConstants.NONE)
+	{
+		value = null;
+	}
+
+	this.state.fillColor = value;
+	this.state.gradientColor = null;
+};
+
+/**
+ * Function: setGradient
+ *
+ * Sets the current gradient.
+ */
+mxAbstractCanvas2D.prototype.setGradient = function(color1, color2, x, y, w, h, direction, alpha1, alpha2)
+{
+	var s = this.state;
+	s.fillColor = color1;
+	s.gradientFillAlpha = (alpha1 != null) ? alpha1 : 1;
+	s.gradientColor = color2;
+	s.gradientAlpha = (alpha2 != null) ? alpha2 : 1;
+	s.gradientDirection = direction;
+};
+
+/**
+ * Function: setStrokeColor
+ *
+ * Sets the current stroke color.
+ */
+mxAbstractCanvas2D.prototype.setStrokeColor = function(value)
+{
+	if (value == mxConstants.NONE)
+	{
+		value = null;
+	}
+
+	this.state.strokeColor = value;
+};
+
+/**
+ * Function: setStrokeWidth
+ *
+ * Sets the current stroke width.
+ */
+mxAbstractCanvas2D.prototype.setStrokeWidth = function(value)
+{
+	this.state.strokeWidth = value;
+};
+
+/**
+ * Function: setDashed
+ *
+ * Enables or disables dashed lines.
+ */
+mxAbstractCanvas2D.prototype.setDashed = function(value, fixDash)
+{
+	this.state.dashed = value;
+	this.state.fixDash = fixDash;
+};
+
+/**
+ * Function: setDashPattern
+ *
+ * Sets the current dash pattern.
+ */
+mxAbstractCanvas2D.prototype.setDashPattern = function(value)
+{
+	this.state.dashPattern = value;
+};
+
+/**
+ * Function: setLineCap
+ *
+ * Sets the current line cap.
+ */
+mxAbstractCanvas2D.prototype.setLineCap = function(value)
+{
+	this.state.lineCap = value;
+};
+
+/**
+ * Function: setLineJoin
+ *
+ * Sets the current line join.
+ */
+mxAbstractCanvas2D.prototype.setLineJoin = function(value)
+{
+	this.state.lineJoin = value;
+};
+
+/**
+ * Function: setMiterLimit
+ *
+ * Sets the current miter limit.
+ */
+mxAbstractCanvas2D.prototype.setMiterLimit = function(value)
+{
+	this.state.miterLimit = value;
+};
+
+/**
+ * Function: setFontColor
+ *
+ * Sets the current font color.
+ */
+mxAbstractCanvas2D.prototype.setFontColor = function(value)
+{
+	if (value == mxConstants.NONE)
+	{
+		value = null;
+	}
+
+	this.state.fontColor = value;
+};
+
+/**
+ * Function: setFontColor
+ *
+ * Sets the current font color.
+ */
+mxAbstractCanvas2D.prototype.setFontBackgroundColor = function(value)
+{
+	if (value == mxConstants.NONE)
+	{
+		value = null;
+	}
+
+	this.state.fontBackgroundColor = value;
+};
+
+/**
+ * Function: setFontColor
+ *
+ * Sets the current font color.
+ */
+mxAbstractCanvas2D.prototype.setFontBorderColor = function(value)
+{
+	if (value == mxConstants.NONE)
+	{
+		value = null;
+	}
+
+	this.state.fontBorderColor = value;
+};
+
+/**
+ * Function: setFontSize
+ *
+ * Sets the current font size.
+ */
+mxAbstractCanvas2D.prototype.setFontSize = function(value)
+{
+	this.state.fontSize = parseFloat(value);
+};
+
+/**
+ * Function: setFontFamily
+ *
+ * Sets the current font family.
+ */
+mxAbstractCanvas2D.prototype.setFontFamily = function(value)
+{
+	this.state.fontFamily = value;
+};
+
+/**
+ * Function: setFontStyle
+ *
+ * Sets the current font style.
+ */
+mxAbstractCanvas2D.prototype.setFontStyle = function(value)
+{
+	if (value == null)
+	{
+		value = 0;
+	}
+
+	this.state.fontStyle = value;
+};
+
+/**
+ * Function: setShadow
+ *
+ * Enables or disables and configures the current shadow.
+ */
+mxAbstractCanvas2D.prototype.setShadow = function(enabled)
+{
+	this.state.shadow = enabled;
+};
+
+/**
+ * Function: setShadowColor
+ *
+ * Enables or disables and configures the current shadow.
+ */
+mxAbstractCanvas2D.prototype.setShadowColor = function(value)
+{
+	if (value == mxConstants.NONE)
+	{
+		value = null;
+	}
+
+	this.state.shadowColor = value;
+};
+
+/**
+ * Function: setShadowAlpha
+ *
+ * Enables or disables and configures the current shadow.
+ */
+mxAbstractCanvas2D.prototype.setShadowAlpha = function(value)
+{
+	this.state.shadowAlpha = value;
+};
+
+/**
+ * Function: setShadowOffset
+ *
+ * Enables or disables and configures the current shadow.
+ */
+mxAbstractCanvas2D.prototype.setShadowOffset = function(dx, dy)
+{
+	this.state.shadowDx = dx;
+	this.state.shadowDy = dy;
+};
+
+/**
+ * Function: begin
+ *
+ * Starts a new path.
+ */
+mxAbstractCanvas2D.prototype.begin = function()
+{
+	this.lastX = 0;
+	this.lastY = 0;
+	this.path = [];
+};
+
+/**
+ * Function: moveTo
+ *
+ *  Moves the current path the given coordinates.
+ */
+mxAbstractCanvas2D.prototype.moveTo = function(x, y)
+{
+	this.addOp(this.moveOp, x, y);
+};
+
+/**
+ * Function: lineTo
+ *
+ * Draws a line to the given coordinates. Uses moveTo with the op argument.
+ */
+mxAbstractCanvas2D.prototype.lineTo = function(x, y)
+{
+	this.addOp(this.lineOp, x, y);
+};
+
+/**
+ * Function: quadTo
+ *
+ * Adds a quadratic curve to the current path.
+ */
+mxAbstractCanvas2D.prototype.quadTo = function(x1, y1, x2, y2)
+{
+	this.addOp(this.quadOp, x1, y1, x2, y2);
+};
+
+/**
+ * Function: curveTo
+ *
+ * Adds a bezier curve to the current path.
+ */
+mxAbstractCanvas2D.prototype.curveTo = function(x1, y1, x2, y2, x3, y3)
+{
+	this.addOp(this.curveOp, x1, y1, x2, y2, x3, y3);
+};
+
+/**
+ * Function: arcTo
+ *
+ * Adds the given arc to the current path. This is a synthetic operation that
+ * is broken down into curves.
+ */
+mxAbstractCanvas2D.prototype.arcTo = function(rx, ry, angle, largeArcFlag, sweepFlag, x, y)
+{
+	var curves = mxUtils.arcToCurves(this.lastX, this.lastY, rx, ry, angle, largeArcFlag, sweepFlag, x, y);
+
+	if (curves != null)
+	{
+		for (var i = 0; i < curves.length; i += 6)
+		{
+			this.curveTo(curves[i], curves[i + 1], curves[i + 2],
+				curves[i + 3], curves[i + 4], curves[i + 5]);
+		}
+	}
+};
+
+/**
+ * Function: close
+ *
+ * Closes the current path.
+ */
+mxAbstractCanvas2D.prototype.close = function(x1, y1, x2, y2, x3, y3)
+{
+	this.addOp(this.closeOp);
+};
+
+/**
+ * Function: end
+ *
+ * Empty implementation for backwards compatibility. This will be removed.
+ */
+mxAbstractCanvas2D.prototype.end = function() { };
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxXmlCanvas2D
+ *
+ * Base class for all canvases. The following methods make up the public
+ * interface of the canvas 2D for all painting in mxGraph:
+ *
+ * - , 
+ * - , , 
+ * - , , , , ,
+ *   , , , , ,
+ *   , 
+ * - , , , ,
+ *   , 
+ * - , , , 
+ * - , , , , 
+ * - , , , , 
+ * - , , 
+ *
+ *  is an additional method for drawing paths. This is
+ * a synthetic method, meaning that it is turned into a sequence of curves by
+ * default. Subclassers may add native support for arcs.
+ *
+ * Constructor: mxXmlCanvas2D
+ *
+ * Constructs a new abstract canvas.
+ */
+function mxXmlCanvas2D(root)
+{
+	mxAbstractCanvas2D.call(this);
+
+	/**
+	 * Variable: root
+	 *
+	 * Reference to the container for the SVG content.
+	 */
+	this.root = root;
+
+	// Writes default settings;
+	this.writeDefaults();
+};
+
+/**
+ * Extends mxAbstractCanvas2D
+ */
+mxUtils.extend(mxXmlCanvas2D, mxAbstractCanvas2D);
+
+/**
+ * Variable: textEnabled
+ *
+ * Specifies if text output should be enabled. Default is true.
+ */
+mxXmlCanvas2D.prototype.textEnabled = true;
+
+/**
+ * Variable: compressed
+ *
+ * Specifies if the output should be compressed by removing redundant calls.
+ * Default is true.
+ */
+mxXmlCanvas2D.prototype.compressed = true;
+
+/**
+ * Function: writeDefaults
+ *
+ * Writes the rendering defaults to :
+ */
+mxXmlCanvas2D.prototype.writeDefaults = function()
+{
+	var elem;
+
+	// Writes font defaults
+	elem = this.createElement('fontfamily');
+	elem.setAttribute('family', mxConstants.DEFAULT_FONTFAMILY);
+	this.root.appendChild(elem);
+
+	elem = this.createElement('fontsize');
+	elem.setAttribute('size', mxConstants.DEFAULT_FONTSIZE);
+	this.root.appendChild(elem);
+
+	// Writes shadow defaults
+	elem = this.createElement('shadowcolor');
+	elem.setAttribute('color', mxConstants.SHADOWCOLOR);
+	this.root.appendChild(elem);
+
+	elem = this.createElement('shadowalpha');
+	elem.setAttribute('alpha', mxConstants.SHADOW_OPACITY);
+	this.root.appendChild(elem);
+
+	elem = this.createElement('shadowoffset');
+	elem.setAttribute('dx', mxConstants.SHADOW_OFFSET_X);
+	elem.setAttribute('dy', mxConstants.SHADOW_OFFSET_Y);
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: format
+ *
+ * Returns a formatted number with 2 decimal places.
+ */
+mxXmlCanvas2D.prototype.format = function(value)
+{
+	return parseFloat(parseFloat(value).toFixed(2));
+};
+
+/**
+ * Function: createElement
+ *
+ * Creates the given element using the owner document of .
+ */
+mxXmlCanvas2D.prototype.createElement = function(name)
+{
+	return this.root.ownerDocument.createElement(name);
+};
+
+/**
+ * Function: save
+ *
+ * Saves the drawing state.
+ */
+mxXmlCanvas2D.prototype.save = function()
+{
+	if (this.compressed)
+	{
+		mxAbstractCanvas2D.prototype.save.apply(this, arguments);
+	}
+
+	this.root.appendChild(this.createElement('save'));
+};
+
+/**
+ * Function: restore
+ *
+ * Restores the drawing state.
+ */
+mxXmlCanvas2D.prototype.restore = function()
+{
+	if (this.compressed)
+	{
+		mxAbstractCanvas2D.prototype.restore.apply(this, arguments);
+	}
+
+	this.root.appendChild(this.createElement('restore'));
+};
+
+/**
+ * Function: scale
+ *
+ * Scales the output.
+ *
+ * Parameters:
+ *
+ * scale - Number that represents the scale where 1 is equal to 100%.
+ */
+mxXmlCanvas2D.prototype.scale = function(value)
+{
+        var elem = this.createElement('scale');
+        elem.setAttribute('scale', value);
+        this.root.appendChild(elem);
+};
+
+/**
+ * Function: translate
+ *
+ * Translates the output.
+ *
+ * Parameters:
+ *
+ * dx - Number that specifies the horizontal translation.
+ * dy - Number that specifies the vertical translation.
+ */
+mxXmlCanvas2D.prototype.translate = function(dx, dy)
+{
+	var elem = this.createElement('translate');
+	elem.setAttribute('dx', this.format(dx));
+	elem.setAttribute('dy', this.format(dy));
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: rotate
+ *
+ * Rotates and/or flips the output around a given center. (Note: Due to
+ * limitations in VML, the rotation cannot be concatenated.)
+ *
+ * Parameters:
+ *
+ * theta - Number that represents the angle of the rotation (in degrees).
+ * flipH - Boolean indicating if the output should be flipped horizontally.
+ * flipV - Boolean indicating if the output should be flipped vertically.
+ * cx - Number that represents the x-coordinate of the rotation center.
+ * cy - Number that represents the y-coordinate of the rotation center.
+ */
+mxXmlCanvas2D.prototype.rotate = function(theta, flipH, flipV, cx, cy)
+{
+	var elem = this.createElement('rotate');
+
+	if (theta != 0 || flipH || flipV)
+	{
+		elem.setAttribute('theta', this.format(theta));
+		elem.setAttribute('flipH', (flipH) ? '1' : '0');
+		elem.setAttribute('flipV', (flipV) ? '1' : '0');
+		elem.setAttribute('cx', this.format(cx));
+		elem.setAttribute('cy', this.format(cy));
+		this.root.appendChild(elem);
+	}
+};
+
+/**
+ * Function: setAlpha
+ *
+ * Sets the current alpha.
+ *
+ * Parameters:
+ *
+ * value - Number that represents the new alpha. Possible values are between
+ * 1 (opaque) and 0 (transparent).
+ */
+mxXmlCanvas2D.prototype.setAlpha = function(value)
+{
+	if (this.compressed)
+	{
+		if (this.state.alpha == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setAlpha.apply(this, arguments);
+	}
+
+	var elem = this.createElement('alpha');
+	elem.setAttribute('alpha', this.format(value));
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setFillAlpha
+ *
+ * Sets the current fill alpha.
+ *
+ * Parameters:
+ *
+ * value - Number that represents the new fill alpha. Possible values are between
+ * 1 (opaque) and 0 (transparent).
+ */
+mxXmlCanvas2D.prototype.setFillAlpha = function(value)
+{
+	if (this.compressed)
+	{
+		if (this.state.fillAlpha == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setFillAlpha.apply(this, arguments);
+	}
+
+	var elem = this.createElement('fillalpha');
+	elem.setAttribute('alpha', this.format(value));
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setStrokeAlpha
+ *
+ * Sets the current stroke alpha.
+ *
+ * Parameters:
+ *
+ * value - Number that represents the new stroke alpha. Possible values are between
+ * 1 (opaque) and 0 (transparent).
+ */
+mxXmlCanvas2D.prototype.setStrokeAlpha = function(value)
+{
+	if (this.compressed)
+	{
+		if (this.state.strokeAlpha == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setStrokeAlpha.apply(this, arguments);
+	}
+
+	var elem = this.createElement('strokealpha');
+	elem.setAttribute('alpha', this.format(value));
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setFillColor
+ *
+ * Sets the current fill color.
+ *
+ * Parameters:
+ *
+ * value - Hexadecimal representation of the color or 'none'.
+ */
+mxXmlCanvas2D.prototype.setFillColor = function(value)
+{
+	if (value == mxConstants.NONE)
+	{
+		value = null;
+	}
+
+	if (this.compressed)
+	{
+		if (this.state.fillColor == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setFillColor.apply(this, arguments);
+	}
+
+	var elem = this.createElement('fillcolor');
+	elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setGradient
+ *
+ * Sets the gradient. Note that the coordinates may be ignored by some implementations.
+ *
+ * Parameters:
+ *
+ * color1 - Hexadecimal representation of the start color.
+ * color2 - Hexadecimal representation of the end color.
+ * x - X-coordinate of the gradient region.
+ * y - y-coordinate of the gradient region.
+ * w - Width of the gradient region.
+ * h - Height of the gradient region.
+ * direction - One of , ,
+ *  or .
+ * alpha1 - Optional alpha of the start color. Default is 1. Possible values
+ * are between 1 (opaque) and 0 (transparent).
+ * alpha2 - Optional alpha of the end color. Default is 1. Possible values
+ * are between 1 (opaque) and 0 (transparent).
+ */
+mxXmlCanvas2D.prototype.setGradient = function(color1, color2, x, y, w, h, direction, alpha1, alpha2)
+{
+	if (color1 != null && color2 != null)
+	{
+		mxAbstractCanvas2D.prototype.setGradient.apply(this, arguments);
+
+		var elem = this.createElement('gradient');
+		elem.setAttribute('c1', color1);
+		elem.setAttribute('c2', color2);
+		elem.setAttribute('x', this.format(x));
+		elem.setAttribute('y', this.format(y));
+		elem.setAttribute('w', this.format(w));
+		elem.setAttribute('h', this.format(h));
+
+		// Default direction is south
+		if (direction != null)
+		{
+			elem.setAttribute('direction', direction);
+		}
+
+		if (alpha1 != null)
+		{
+			elem.setAttribute('alpha1', alpha1);
+		}
+
+		if (alpha2 != null)
+		{
+			elem.setAttribute('alpha2', alpha2);
+		}
+
+		this.root.appendChild(elem);
+	}
+};
+
+/**
+ * Function: setStrokeColor
+ *
+ * Sets the current stroke color.
+ *
+ * Parameters:
+ *
+ * value - Hexadecimal representation of the color or 'none'.
+ */
+mxXmlCanvas2D.prototype.setStrokeColor = function(value)
+{
+	if (value == mxConstants.NONE)
+	{
+		value = null;
+	}
+
+	if (this.compressed)
+	{
+		if (this.state.strokeColor == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setStrokeColor.apply(this, arguments);
+	}
+
+	var elem = this.createElement('strokecolor');
+	elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setStrokeWidth
+ *
+ * Sets the current stroke width.
+ *
+ * Parameters:
+ *
+ * value - Numeric representation of the stroke width.
+ */
+mxXmlCanvas2D.prototype.setStrokeWidth = function(value)
+{
+	if (this.compressed)
+	{
+		if (this.state.strokeWidth == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setStrokeWidth.apply(this, arguments);
+	}
+
+	var elem = this.createElement('strokewidth');
+	elem.setAttribute('width', this.format(value));
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setDashed
+ *
+ * Enables or disables dashed lines.
+ *
+ * Parameters:
+ *
+ * value - Boolean that specifies if dashed lines should be enabled.
+ * value - Boolean that specifies if the stroke width should be ignored
+ * for the dash pattern. Default is false.
+ */
+mxXmlCanvas2D.prototype.setDashed = function(value, fixDash)
+{
+	if (this.compressed)
+	{
+		if (this.state.dashed == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setDashed.apply(this, arguments);
+	}
+
+	var elem = this.createElement('dashed');
+	elem.setAttribute('dashed', (value) ? '1' : '0');
+
+	if (fixDash != null)
+	{
+		elem.setAttribute('fixDash', (fixDash) ? '1' : '0');
+	}
+
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setDashPattern
+ *
+ * Sets the current dash pattern. Default is '3 3'.
+ *
+ * Parameters:
+ *
+ * value - String that represents the dash pattern, which is a sequence of
+ * numbers defining the length of the dashes and the length of the spaces
+ * between the dashes. The lengths are relative to the line width - a length
+ * of 1 is equals to the line width.
+ */
+mxXmlCanvas2D.prototype.setDashPattern = function(value)
+{
+	if (this.compressed)
+	{
+		if (this.state.dashPattern == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setDashPattern.apply(this, arguments);
+	}
+
+	var elem = this.createElement('dashpattern');
+	elem.setAttribute('pattern', value);
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setLineCap
+ *
+ * Sets the line cap. Default is 'flat' which corresponds to 'butt' in SVG.
+ *
+ * Parameters:
+ *
+ * value - String that represents the line cap. Possible values are flat, round
+ * and square.
+ */
+mxXmlCanvas2D.prototype.setLineCap = function(value)
+{
+	if (this.compressed)
+	{
+		if (this.state.lineCap == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setLineCap.apply(this, arguments);
+	}
+
+	var elem = this.createElement('linecap');
+	elem.setAttribute('cap', value);
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setLineJoin
+ *
+ * Sets the line join. Default is 'miter'.
+ *
+ * Parameters:
+ *
+ * value - String that represents the line join. Possible values are miter,
+ * round and bevel.
+ */
+mxXmlCanvas2D.prototype.setLineJoin = function(value)
+{
+	if (this.compressed)
+	{
+		if (this.state.lineJoin == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setLineJoin.apply(this, arguments);
+	}
+
+	var elem = this.createElement('linejoin');
+	elem.setAttribute('join', value);
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setMiterLimit
+ *
+ * Sets the miter limit. Default is 10.
+ *
+ * Parameters:
+ *
+ * value - Number that represents the miter limit.
+ */
+mxXmlCanvas2D.prototype.setMiterLimit = function(value)
+{
+	if (this.compressed)
+	{
+		if (this.state.miterLimit == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setMiterLimit.apply(this, arguments);
+	}
+
+	var elem = this.createElement('miterlimit');
+	elem.setAttribute('limit', value);
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setFontColor
+ *
+ * Sets the current font color. Default is '#000000'.
+ *
+ * Parameters:
+ *
+ * value - Hexadecimal representation of the color or 'none'.
+ */
+mxXmlCanvas2D.prototype.setFontColor = function(value)
+{
+	if (this.textEnabled)
+	{
+		if (value == mxConstants.NONE)
+		{
+			value = null;
+		}
+
+		if (this.compressed)
+		{
+			if (this.state.fontColor == value)
+			{
+				return;
+			}
+
+			mxAbstractCanvas2D.prototype.setFontColor.apply(this, arguments);
+		}
+
+		var elem = this.createElement('fontcolor');
+		elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
+		this.root.appendChild(elem);
+	}
+};
+
+/**
+ * Function: setFontBackgroundColor
+ *
+ * Sets the current font background color.
+ *
+ * Parameters:
+ *
+ * value - Hexadecimal representation of the color or 'none'.
+ */
+mxXmlCanvas2D.prototype.setFontBackgroundColor = function(value)
+{
+	if (this.textEnabled)
+	{
+		if (value == mxConstants.NONE)
+		{
+			value = null;
+		}
+
+		if (this.compressed)
+		{
+			if (this.state.fontBackgroundColor == value)
+			{
+				return;
+			}
+
+			mxAbstractCanvas2D.prototype.setFontBackgroundColor.apply(this, arguments);
+		}
+
+		var elem = this.createElement('fontbackgroundcolor');
+		elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
+		this.root.appendChild(elem);
+	}
+};
+
+/**
+ * Function: setFontBorderColor
+ *
+ * Sets the current font border color.
+ *
+ * Parameters:
+ *
+ * value - Hexadecimal representation of the color or 'none'.
+ */
+mxXmlCanvas2D.prototype.setFontBorderColor = function(value)
+{
+	if (this.textEnabled)
+	{
+		if (value == mxConstants.NONE)
+		{
+			value = null;
+		}
+
+		if (this.compressed)
+		{
+			if (this.state.fontBorderColor == value)
+			{
+				return;
+			}
+
+			mxAbstractCanvas2D.prototype.setFontBorderColor.apply(this, arguments);
+		}
+
+		var elem = this.createElement('fontbordercolor');
+		elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
+		this.root.appendChild(elem);
+	}
+};
+
+/**
+ * Function: setFontSize
+ *
+ * Sets the current font size. Default is .
+ *
+ * Parameters:
+ *
+ * value - Numeric representation of the font size.
+ */
+mxXmlCanvas2D.prototype.setFontSize = function(value)
+{
+	if (this.textEnabled)
+	{
+		if (this.compressed)
+		{
+			if (this.state.fontSize == value)
+			{
+				return;
+			}
+
+			mxAbstractCanvas2D.prototype.setFontSize.apply(this, arguments);
+		}
+
+		var elem = this.createElement('fontsize');
+		elem.setAttribute('size', value);
+		this.root.appendChild(elem);
+	}
+};
+
+/**
+ * Function: setFontFamily
+ *
+ * Sets the current font family. Default is .
+ *
+ * Parameters:
+ *
+ * value - String representation of the font family. This handles the same
+ * values as the CSS font-family property.
+ */
+mxXmlCanvas2D.prototype.setFontFamily = function(value)
+{
+	if (this.textEnabled)
+	{
+		if (this.compressed)
+		{
+			if (this.state.fontFamily == value)
+			{
+				return;
+			}
+
+			mxAbstractCanvas2D.prototype.setFontFamily.apply(this, arguments);
+		}
+
+		var elem = this.createElement('fontfamily');
+		elem.setAttribute('family', value);
+		this.root.appendChild(elem);
+	}
+};
+
+/**
+ * Function: setFontStyle
+ *
+ * Sets the current font style.
+ *
+ * Parameters:
+ *
+ * value - Numeric representation of the font family. This is the sum of the
+ * font styles from .
+ */
+mxXmlCanvas2D.prototype.setFontStyle = function(value)
+{
+	if (this.textEnabled)
+	{
+		if (value == null)
+		{
+			value = 0;
+		}
+
+		if (this.compressed)
+		{
+			if (this.state.fontStyle == value)
+			{
+				return;
+			}
+
+			mxAbstractCanvas2D.prototype.setFontStyle.apply(this, arguments);
+		}
+
+		var elem = this.createElement('fontstyle');
+		elem.setAttribute('style', value);
+		this.root.appendChild(elem);
+	}
+};
+
+/**
+ * Function: setShadow
+ *
+ * Enables or disables shadows.
+ *
+ * Parameters:
+ *
+ * value - Boolean that specifies if shadows should be enabled.
+ */
+mxXmlCanvas2D.prototype.setShadow = function(value)
+{
+	if (this.compressed)
+	{
+		if (this.state.shadow == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setShadow.apply(this, arguments);
+	}
+
+	var elem = this.createElement('shadow');
+	elem.setAttribute('enabled', (value) ? '1' : '0');
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setShadowColor
+ *
+ * Sets the current shadow color. Default is .
+ *
+ * Parameters:
+ *
+ * value - Hexadecimal representation of the color or 'none'.
+ */
+mxXmlCanvas2D.prototype.setShadowColor = function(value)
+{
+	if (this.compressed)
+	{
+		if (value == mxConstants.NONE)
+		{
+			value = null;
+		}
+
+		if (this.state.shadowColor == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setShadowColor.apply(this, arguments);
+	}
+
+	var elem = this.createElement('shadowcolor');
+	elem.setAttribute('color', (value != null) ? value : mxConstants.NONE);
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: setShadowAlpha
+ *
+ * Sets the current shadows alpha. Default is .
+ *
+ * Parameters:
+ *
+ * value - Number that represents the new alpha. Possible values are between
+ * 1 (opaque) and 0 (transparent).
+ */
+mxXmlCanvas2D.prototype.setShadowAlpha = function(value)
+{
+	if (this.compressed)
+	{
+		if (this.state.shadowAlpha == value)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setShadowAlpha.apply(this, arguments);
+	}
+
+	var elem = this.createElement('shadowalpha');
+	elem.setAttribute('alpha', value);
+	this.root.appendChild(elem);
+
+};
+
+/**
+ * Function: setShadowOffset
+ *
+ * Sets the current shadow offset.
+ *
+ * Parameters:
+ *
+ * dx - Number that represents the horizontal offset of the shadow.
+ * dy - Number that represents the vertical offset of the shadow.
+ */
+mxXmlCanvas2D.prototype.setShadowOffset = function(dx, dy)
+{
+	if (this.compressed)
+	{
+		if (this.state.shadowDx == dx && this.state.shadowDy == dy)
+		{
+			return;
+		}
+
+		mxAbstractCanvas2D.prototype.setShadowOffset.apply(this, arguments);
+	}
+
+	var elem = this.createElement('shadowoffset');
+	elem.setAttribute('dx', dx);
+	elem.setAttribute('dy', dy);
+	this.root.appendChild(elem);
+
+};
+
+/**
+ * Function: rect
+ *
+ * Puts a rectangle into the drawing buffer.
+ *
+ * Parameters:
+ *
+ * x - Number that represents the x-coordinate of the rectangle.
+ * y - Number that represents the y-coordinate of the rectangle.
+ * w - Number that represents the width of the rectangle.
+ * h - Number that represents the height of the rectangle.
+ */
+mxXmlCanvas2D.prototype.rect = function(x, y, w, h)
+{
+	var elem = this.createElement('rect');
+	elem.setAttribute('x', this.format(x));
+	elem.setAttribute('y', this.format(y));
+	elem.setAttribute('w', this.format(w));
+	elem.setAttribute('h', this.format(h));
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: roundrect
+ *
+ * Puts a rounded rectangle into the drawing buffer.
+ *
+ * Parameters:
+ *
+ * x - Number that represents the x-coordinate of the rectangle.
+ * y - Number that represents the y-coordinate of the rectangle.
+ * w - Number that represents the width of the rectangle.
+ * h - Number that represents the height of the rectangle.
+ * dx - Number that represents the horizontal rounding.
+ * dy - Number that represents the vertical rounding.
+ */
+mxXmlCanvas2D.prototype.roundrect = function(x, y, w, h, dx, dy)
+{
+	var elem = this.createElement('roundrect');
+	elem.setAttribute('x', this.format(x));
+	elem.setAttribute('y', this.format(y));
+	elem.setAttribute('w', this.format(w));
+	elem.setAttribute('h', this.format(h));
+	elem.setAttribute('dx', this.format(dx));
+	elem.setAttribute('dy', this.format(dy));
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: ellipse
+ *
+ * Puts an ellipse into the drawing buffer.
+ *
+ * Parameters:
+ *
+ * x - Number that represents the x-coordinate of the ellipse.
+ * y - Number that represents the y-coordinate of the ellipse.
+ * w - Number that represents the width of the ellipse.
+ * h - Number that represents the height of the ellipse.
+ */
+mxXmlCanvas2D.prototype.ellipse = function(x, y, w, h)
+{
+	var elem = this.createElement('ellipse');
+	elem.setAttribute('x', this.format(x));
+	elem.setAttribute('y', this.format(y));
+	elem.setAttribute('w', this.format(w));
+	elem.setAttribute('h', this.format(h));
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: image
+ *
+ * Paints an image.
+ *
+ * Parameters:
+ *
+ * x - Number that represents the x-coordinate of the image.
+ * y - Number that represents the y-coordinate of the image.
+ * w - Number that represents the width of the image.
+ * h - Number that represents the height of the image.
+ * src - String that specifies the URL of the image.
+ * aspect - Boolean indicating if the aspect of the image should be preserved.
+ * flipH - Boolean indicating if the image should be flipped horizontally.
+ * flipV - Boolean indicating if the image should be flipped vertically.
+ */
+mxXmlCanvas2D.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV)
+{
+	src = this.converter.convert(src);
+
+	// LATER: Add option for embedding images as base64.
+	var elem = this.createElement('image');
+	elem.setAttribute('x', this.format(x));
+	elem.setAttribute('y', this.format(y));
+	elem.setAttribute('w', this.format(w));
+	elem.setAttribute('h', this.format(h));
+	elem.setAttribute('src', src);
+	elem.setAttribute('aspect', (aspect) ? '1' : '0');
+	elem.setAttribute('flipH', (flipH) ? '1' : '0');
+	elem.setAttribute('flipV', (flipV) ? '1' : '0');
+	this.root.appendChild(elem);
+};
+
+/**
+ * Function: begin
+ *
+ * Starts a new path and puts it into the drawing buffer.
+ */
+mxXmlCanvas2D.prototype.begin = function()
+{
+	this.root.appendChild(this.createElement('begin'));
+	this.lastX = 0;
+	this.lastY = 0;
+};
+
+/**
+ * Function: moveTo
+ *
+ * Moves the current path the given point.
+ *
+ * Parameters:
+ *
+ * x - Number that represents the x-coordinate of the point.
+ * y - Number that represents the y-coordinate of the point.
+ */
+mxXmlCanvas2D.prototype.moveTo = function(x, y)
+{
+	var elem = this.createElement('move');
+	elem.setAttribute('x', this.format(x));
+	elem.setAttribute('y', this.format(y));
+	this.root.appendChild(elem);
+	this.lastX = x;
+	this.lastY = y;
+};
+
+/**
+ * Function: lineTo
+ *
+ * Draws a line to the given coordinates.
+ *
+ * Parameters:
+ *
+ * x - Number that represents the x-coordinate of the endpoint.
+ * y - Number that represents the y-coordinate of the endpoint.
+ */
+mxXmlCanvas2D.prototype.lineTo = function(x, y)
+{
+	var elem = this.createElement('line');
+	elem.setAttribute('x', this.format(x));
+	elem.setAttribute('y', this.format(y));
+	this.root.appendChild(elem);
+	this.lastX = x;
+	this.lastY = y;
+};
+
+/**
+ * Function: quadTo
+ *
+ * Adds a quadratic curve to the current path.
+ *
+ * Parameters:
+ *
+ * x1 - Number that represents the x-coordinate of the control point.
+ * y1 - Number that represents the y-coordinate of the control point.
+ * x2 - Number that represents the x-coordinate of the endpoint.
+ * y2 - Number that represents the y-coordinate of the endpoint.
+ */
+mxXmlCanvas2D.prototype.quadTo = function(x1, y1, x2, y2)
+{
+	var elem = this.createElement('quad');
+	elem.setAttribute('x1', this.format(x1));
+	elem.setAttribute('y1', this.format(y1));
+	elem.setAttribute('x2', this.format(x2));
+	elem.setAttribute('y2', this.format(y2));
+	this.root.appendChild(elem);
+	this.lastX = x2;
+	this.lastY = y2;
+};
+
+/**
+ * Function: curveTo
+ *
+ * Adds a bezier curve to the current path.
+ *
+ * Parameters:
+ *
+ * x1 - Number that represents the x-coordinate of the first control point.
+ * y1 - Number that represents the y-coordinate of the first control point.
+ * x2 - Number that represents the x-coordinate of the second control point.
+ * y2 - Number that represents the y-coordinate of the second control point.
+ * x3 - Number that represents the x-coordinate of the endpoint.
+ * y3 - Number that represents the y-coordinate of the endpoint.
+ */
+mxXmlCanvas2D.prototype.curveTo = function(x1, y1, x2, y2, x3, y3)
+{
+	var elem = this.createElement('curve');
+	elem.setAttribute('x1', this.format(x1));
+	elem.setAttribute('y1', this.format(y1));
+	elem.setAttribute('x2', this.format(x2));
+	elem.setAttribute('y2', this.format(y2));
+	elem.setAttribute('x3', this.format(x3));
+	elem.setAttribute('y3', this.format(y3));
+	this.root.appendChild(elem);
+	this.lastX = x3;
+	this.lastY = y3;
+};
+
+/**
+ * Function: close
+ *
+ * Closes the current path.
+ */
+mxXmlCanvas2D.prototype.close = function()
+{
+	this.root.appendChild(this.createElement('close'));
+};
+
+/**
+ * Function: text
+ *
+ * Paints the given text. Possible values for format are empty string for
+ * plain text and html for HTML markup. Background and border color as well
+ * as clipping is not available in plain text labels for VML. HTML labels
+ * are not available as part of shapes with no foreignObject support in SVG
+ * (eg. IE9, IE10).
+ *
+ * Parameters:
+ *
+ * x - Number that represents the x-coordinate of the text.
+ * y - Number that represents the y-coordinate of the text.
+ * w - Number that represents the available width for the text or 0 for automatic width.
+ * h - Number that represents the available height for the text or 0 for automatic height.
+ * str - String that specifies the text to be painted.
+ * align - String that represents the horizontal alignment.
+ * valign - String that represents the vertical alignment.
+ * wrap - Boolean that specifies if word-wrapping is enabled. Requires w > 0.
+ * format - Empty string for plain text or 'html' for HTML markup.
+ * overflow - Specifies the overflow behaviour of the label. Requires w > 0 and/or h > 0.
+ * clip - Boolean that specifies if the label should be clipped. Requires w > 0 and/or h > 0.
+ * rotation - Number that specifies the angle of the rotation around the anchor point of the text.
+ * dir - Optional string that specifies the text direction. Possible values are rtl and lrt.
+ */
+mxXmlCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir)
+{
+	if (this.textEnabled && str != null)
+	{
+		if (mxUtils.isNode(str))
+		{
+			str = mxUtils.getOuterHtml(str);
+		}
+
+		var elem = this.createElement('text');
+		elem.setAttribute('x', this.format(x));
+		elem.setAttribute('y', this.format(y));
+		elem.setAttribute('w', this.format(w));
+		elem.setAttribute('h', this.format(h));
+		elem.setAttribute('str', str);
+
+		if (align != null)
+		{
+			elem.setAttribute('align', align);
+		}
+
+		if (valign != null)
+		{
+			elem.setAttribute('valign', valign);
+		}
+
+		elem.setAttribute('wrap', (wrap) ? '1' : '0');
+
+		if (format == null)
+		{
+			format = '';
+		}
+
+		elem.setAttribute('format', format);
+
+		if (overflow != null)
+		{
+			elem.setAttribute('overflow', overflow);
+		}
+
+		if (clip != null)
+		{
+			elem.setAttribute('clip', (clip) ? '1' : '0');
+		}
+
+		if (rotation != null)
+		{
+			elem.setAttribute('rotation', rotation);
+		}
+
+		if (dir != null)
+		{
+			elem.setAttribute('dir', dir);
+		}
+
+		this.root.appendChild(elem);
+	}
+};
+
+/**
+ * Function: stroke
+ *
+ * Paints the outline of the current drawing buffer.
+ */
+mxXmlCanvas2D.prototype.stroke = function()
+{
+	this.root.appendChild(this.createElement('stroke'));
+};
+
+/**
+ * Function: fill
+ *
+ * Fills the current drawing buffer.
+ */
+mxXmlCanvas2D.prototype.fill = function()
+{
+	this.root.appendChild(this.createElement('fill'));
+};
+
+/**
+ * Function: fillAndStroke
+ *
+ * Fills the current drawing buffer and its outline.
+ */
+mxXmlCanvas2D.prototype.fillAndStroke = function()
+{
+	this.root.appendChild(this.createElement('fillstroke'));
+};
+/**
+ * Copyright (c) 2006-2015, JGraph Ltd
+ * Copyright (c) 2006-2015, Gaudenz Alder
+ */
+/**
+ * Class: mxSvgCanvas2D
+ *
+ * Extends  to implement a canvas for SVG. This canvas writes all
+ * calls as SVG output to the given SVG root node.
+ *
+ * (code)
+ * var svgDoc = mxUtils.createXmlDocument();
+ * var root = (svgDoc.createElementNS != null) ?
+ * 		svgDoc.createElementNS(mxConstants.NS_SVG, 'svg') : svgDoc.createElement('svg');
+ *
+ * if (svgDoc.createElementNS == null)
+ * {
+ *   root.setAttribute('xmlns', mxConstants.NS_SVG);
+ *   root.setAttribute('xmlns:xlink', mxConstants.NS_XLINK);
+ * }
+ * else
+ * {
+ *   root.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', mxConstants.NS_XLINK);
+ * }
+ *
+ * var bounds = graph.getGraphBounds();
+ * root.setAttribute('width', (bounds.x + bounds.width + 4) + 'px');
+ * root.setAttribute('height', (bounds.y + bounds.height + 4) + 'px');
+ * root.setAttribute('version', '1.1');
+ *
+ * svgDoc.appendChild(root);
+ *
+ * var svgCanvas = new mxSvgCanvas2D(root);
+ * (end)
+ *
+ * A description of the public API is available in .
+ *
+ * To disable anti-aliasing in the output, use the following code.
+ *
+ * (code)
+ * graph.view.canvas.ownerSVGElement.setAttribute('shape-rendering', 'crispEdges');
+ * (end)
+ *
+ * Or set the respective attribute in the SVG element directly.
+ *
+ * Constructor: mxSvgCanvas2D
+ *
+ * Constructs a new SVG canvas.
+ *
+ * Parameters:
+ *
+ * root - SVG container for the output.
+ * styleEnabled - Optional boolean that specifies if a style section should be
+ * added. The style section sets the default font-size, font-family and
+ * stroke-miterlimit globally. Default is false.
+ */
+function mxSvgCanvas2D(root, styleEnabled)
+{
+	mxAbstractCanvas2D.call(this);
+
+	/**
+	 * Variable: root
+	 *
+	 * Reference to the container for the SVG content.
+	 */
+	this.root = root;
+
+	/**
+	 * Variable: gradients
+	 *
+	 * Local cache of gradients for quick lookups.
+	 */
+	this.gradients = [];
+
+	/**
+	 * Variable: defs
+	 *
+	 * Reference to the defs section of the SVG document. Only for export.
+	 */
+	this.defs = null;
+
+	/**
+	 * Variable: styleEnabled
+	 *
+	 * Stores the value of styleEnabled passed to the constructor.
+	 */
+	this.styleEnabled = (styleEnabled != null) ? styleEnabled : false;
+
+	var svg = null;
+
+	// Adds optional defs section for export
+	if (root.ownerDocument != document)
+	{
+		var node = root;
+
+		// Finds owner SVG element in XML DOM
+		while (node != null && node.nodeName != 'svg')
+		{
+			node = node.parentNode;
+		}
+
+		svg = node;
+	}
+
+	if (svg != null)
+	{
+		// Tries to get existing defs section
+		var tmp = svg.getElementsByTagName('defs');
+
+		if (tmp.length > 0)
+		{
+			this.defs = svg.getElementsByTagName('defs')[0];
+		}
+
+		// Adds defs section if none exists
+		if (this.defs == null)
+		{
+			this.defs = this.createElement('defs');
+
+			if (svg.firstChild != null)
+			{
+				svg.insertBefore(this.defs, svg.firstChild);
+			}
+			else
+			{
+				svg.appendChild(this.defs);
+			}
+		}
+
+		// Adds stylesheet
+		if (this.styleEnabled)
+		{
+			this.defs.appendChild(this.createStyle());
+		}
+	}
+};
+
+/**
+ * Extends mxAbstractCanvas2D
+ */
+mxUtils.extend(mxSvgCanvas2D, mxAbstractCanvas2D);
+
+/**
+ * Capability check for DOM parser.
+ */
+(function()
+{
+	mxSvgCanvas2D.prototype.useDomParser = !mxClient.IS_IE && typeof DOMParser === 'function' && typeof XMLSerializer === 'function';
+
+	if (mxSvgCanvas2D.prototype.useDomParser)
+	{
+		// Checks using a generic test text if the parsing actually works. This is a workaround
+		// for older browsers where the capability check returns true but the parsing fails.
+		try
+		{
+			var doc = new DOMParser().parseFromString('test text', 'text/html');
+			mxSvgCanvas2D.prototype.useDomParser = doc != null;
+		}
+		catch (e)
+		{
+			mxSvgCanvas2D.prototype.useDomParser = false;
+		}
+	}
+})();
+
+/**
+ * Variable: path
+ *
+ * Holds the current DOM node.
+ */
+mxSvgCanvas2D.prototype.node = null;
+
+/**
+ * Variable: matchHtmlAlignment
+ *
+ * Specifies if plain text output should match the vertical HTML alignment.
+ * Defaul is true.
+ */
+mxSvgCanvas2D.prototype.matchHtmlAlignment = true;
+
+/**
+ * Variable: textEnabled
+ *
+ * Specifies if text output should be enabled. Default is true.
+ */
+mxSvgCanvas2D.prototype.textEnabled = true;
+
+/**
+ * Variable: foEnabled
+ *
+ * Specifies if use of foreignObject for HTML markup is allowed. Default is true.
+ */
+mxSvgCanvas2D.prototype.foEnabled = true;
+
+/**
+ * Variable: foAltText
+ *
+ * Specifies the fallback text for unsupported foreignObjects in exported
+ * documents. Default is '[Object]'. If this is set to null then no fallback
+ * text is added to the exported document.
+ */
+mxSvgCanvas2D.prototype.foAltText = '[Object]';
+
+/**
+ * Variable: foOffset
+ *
+ * Offset to be used for foreignObjects.
+ */
+mxSvgCanvas2D.prototype.foOffset = 0;
+
+/**
+ * Variable: textOffset
+ *
+ * Offset to be used for text elements.
+ */
+mxSvgCanvas2D.prototype.textOffset = 0;
+
+/**
+ * Variable: imageOffset
+ *
+ * Offset to be used for image elements.
+ */
+mxSvgCanvas2D.prototype.imageOffset = 0;
+
+/**
+ * Variable: strokeTolerance
+ *
+ * Adds transparent paths for strokes.
+ */
+mxSvgCanvas2D.prototype.strokeTolerance = 0;
+
+/**
+ * Variable: minStrokeWidth
+ *
+ * Minimum stroke width for output.
+ */
+mxSvgCanvas2D.prototype.minStrokeWidth = 1;
+
+/**
+ * Variable: refCount
+ *
+ * Local counter for references in SVG export.
+ */
+mxSvgCanvas2D.prototype.refCount = 0;
+
+/**
+ * Variable: blockImagePointerEvents
+ *
+ * Specifies if a transparent rectangle should be added on top of images to absorb
+ * all pointer events. Default is false. This is only needed in Firefox to disable
+ * control-clicks on images.
+ */
+mxSvgCanvas2D.prototype.blockImagePointerEvents = false;
+
+/**
+ * Variable: lineHeightCorrection
+ *
+ * Correction factor for  in HTML output. Default is 1.
+ */
+mxSvgCanvas2D.prototype.lineHeightCorrection = 1;
+
+/**
+ * Variable: pointerEventsValue
+ *
+ * Default value for active pointer events. Default is all.
+ */
+mxSvgCanvas2D.prototype.pointerEventsValue = 'all';
+
+/**
+ * Variable: fontMetricsPadding
+ *
+ * Padding to be added for text that is not wrapped to account for differences
+ * in font metrics on different platforms in pixels. Default is 10.
+ */
+mxSvgCanvas2D.prototype.fontMetricsPadding = 10;
+
+/**
+ * Variable: cacheOffsetSize
+ *
+ * Specifies if offsetWidth and offsetHeight should be cached. Default is true.
+ * This is used to speed up repaint of text in .
+ */
+mxSvgCanvas2D.prototype.cacheOffsetSize = true;
+
+/**
+ * Function: format
+ *
+ * Rounds all numbers to 2 decimal points.
+ */
+mxSvgCanvas2D.prototype.format = function(value)
+{
+	return parseFloat(parseFloat(value).toFixed(2));
+};
+
+/**
+ * Function: getBaseUrl
+ *
+ * Returns the URL of the page without the hash part. This needs to use href to
+ * include any search part with no params (ie question mark alone). This is a
+ * workaround for the fact that window.location.search is empty if there is
+ * no search string behind the question mark.
+ */
+mxSvgCanvas2D.prototype.getBaseUrl = function()
+{
+	var href = window.location.href;
+	var hash = href.lastIndexOf('#');
+
+	if (hash > 0)
+	{
+		href = href.substring(0, hash);
+	}
+
+	return href;
+};
+
+/**
+ * Function: reset
+ *
+ * Returns any offsets for rendering pixels.
+ */
+mxSvgCanvas2D.prototype.reset = function()
+{
+	mxAbstractCanvas2D.prototype.reset.apply(this, arguments);
+	this.gradients = [];
+};
+
+/**
+ * Function: createStyle
+ *
+ * Creates the optional style section.
+ */
+mxSvgCanvas2D.prototype.createStyle = function(x)
+{
+	var style = this.createElement('style');
+	style.setAttribute('type', 'text/css');
+	mxUtils.write(style, 'svg{font-family:' + mxConstants.DEFAULT_FONTFAMILY +
+			';font-size:' + mxConstants.DEFAULT_FONTSIZE +
+			';fill:none;stroke-miterlimit:10}');
+
+	return style;
+};
+
+/**
+ * Function: createElement
+ *
+ * Private helper function to create SVG elements
+ */
+mxSvgCanvas2D.prototype.createElement = function(tagName, namespace)
+{
+	if (this.root.ownerDocument.createElementNS != null)
+	{
+		return this.root.ownerDocument.createElementNS(namespace || mxConstants.NS_SVG, tagName);
+	}
+	else
+	{
+		var elt = this.root.ownerDocument.createElement(tagName);
+
+		if (namespace != null)
+		{
+			elt.setAttribute('xmlns', namespace);
+		}
+
+		return elt;
+	}
+};
+
+/**
+ * Function: getAlternateContent
+ *
+ * Returns the alternate content for the given foreignObject.
+ */
+mxSvgCanvas2D.prototype.createAlternateContent = function(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation)
+{
+	if (this.foAltText != null)
+	{
+		var s = this.state;
+		var alt = this.createElement('text');
+		alt.setAttribute('x', Math.round(w / 2));
+		alt.setAttribute('y', Math.round((h + s.fontSize) / 2));
+		alt.setAttribute('fill', s.fontColor || 'black');
+		alt.setAttribute('text-anchor', 'middle');
+		alt.setAttribute('font-size', s.fontSize + 'px');
+		alt.setAttribute('font-family', s.fontFamily);
+
+		if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
+		{
+			alt.setAttribute('font-weight', 'bold');
+		}
+
+		if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
+		{
+			alt.setAttribute('font-style', 'italic');
+		}
+
+		if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
+		{
+			alt.setAttribute('text-decoration', 'underline');
+		}
+
+		mxUtils.write(alt, this.foAltText);
+
+		return alt;
+	}
+	else
+	{
+		return null;
+	}
+};
+
+/**
+ * Function: createGradientId
+ *
+ * Private helper function to create SVG elements
+ */
+mxSvgCanvas2D.prototype.createGradientId = function(start, end, alpha1, alpha2, direction)
+{
+	// Removes illegal characters from gradient ID
+	if (start.charAt(0) == '#')
+	{
+		start = start.substring(1);
+	}
+
+	if (end.charAt(0) == '#')
+	{
+		end = end.substring(1);
+	}
+
+	// Workaround for gradient IDs not working in Safari 5 / Chrome 6
+	// if they contain uppercase characters
+	start = start.toLowerCase() + '-' + alpha1;
+	end = end.toLowerCase() + '-' + alpha2;
+
+	// Wrong gradient directions possible?
+	var dir = null;
+
+	if (direction == null || direction == mxConstants.DIRECTION_SOUTH)
+	{
+		dir = 's';
+	}
+	else if (direction == mxConstants.DIRECTION_EAST)
+	{
+		dir = 'e';
+	}
+	else
+	{
+		var tmp = start;
+		start = end;
+		end = tmp;
+
+		if (direction == mxConstants.DIRECTION_NORTH)
+		{
+			dir = 's';
+		}
+		else if (direction == mxConstants.DIRECTION_WEST)
+		{
+			dir = 'e';
+		}
+	}
+
+	return 'mx-gradient-' + start + '-' + end + '-' + dir;
+};
+
+/**
+ * Function: getSvgGradient
+ *
+ * Private helper function to create SVG elements
+ */
+mxSvgCanvas2D.prototype.getSvgGradient = function(start, end, alpha1, alpha2, direction)
+{
+	var id = this.createGradientId(start, end, alpha1, alpha2, direction);
+	var gradient = this.gradients[id];
+
+	if (gradient == null)
+	{
+		var svg = this.root.ownerSVGElement;
+
+		var counter = 0;
+		var tmpId = id + '-' + counter;
+
+		if (svg != null)
+		{
+			gradient = svg.ownerDocument.getElementById(tmpId);
+
+			while (gradient != null && gradient.ownerSVGElement != svg)
+			{
+				tmpId = id + '-' + counter++;
+				gradient = svg.ownerDocument.getElementById(tmpId);
+			}
+		}
+		else
+		{
+			// Uses shorter IDs for export
+			tmpId = 'id' + (++this.refCount);
+		}
+
+		if (gradient == null)
+		{
+			gradient = this.createSvgGradient(start, end, alpha1, alpha2, direction);
+			gradient.setAttribute('id', tmpId);
+
+			if (this.defs != null)
+			{
+				this.defs.appendChild(gradient);
+			}
+			else
+			{
+				svg.appendChild(gradient);
+			}
+		}
+
+		this.gradients[id] = gradient;
+	}
+
+	return gradient.getAttribute('id');
+};
+
+/**
+ * Function: createSvgGradient
+ *
+ * Creates the given SVG gradient.
+ */
+mxSvgCanvas2D.prototype.createSvgGradient = function(start, end, alpha1, alpha2, direction)
+{
+	var gradient = this.createElement('linearGradient');
+	gradient.setAttribute('x1', '0%');
+	gradient.setAttribute('y1', '0%');
+	gradient.setAttribute('x2', '0%');
+	gradient.setAttribute('y2', '0%');
+
+	if (direction == null || direction == mxConstants.DIRECTION_SOUTH)
+	{
+		gradient.setAttribute('y2', '100%');
+	}
+	else if (direction == mxConstants.DIRECTION_EAST)
+	{
+		gradient.setAttribute('x2', '100%');
+	}
+	else if (direction == mxConstants.DIRECTION_NORTH)
+	{
+		gradient.setAttribute('y1', '100%');
+	}
+	else if (direction == mxConstants.DIRECTION_WEST)
+	{
+		gradient.setAttribute('x1', '100%');
+	}
+
+	var op = (alpha1 < 1) ? ';stop-opacity:' + alpha1 : '';
+
+	var stop = this.createElement('stop');
+	stop.setAttribute('offset', '0%');
+	stop.setAttribute('style', 'stop-color:' + start + op);
+	gradient.appendChild(stop);
+
+	op = (alpha2 < 1) ? ';stop-opacity:' + alpha2 : '';
+
+	stop = this.createElement('stop');
+	stop.setAttribute('offset', '100%');
+	stop.setAttribute('style', 'stop-color:' + end + op);
+	gradient.appendChild(stop);
+
+	return gradient;
+};
+
+/**
+ * Function: addNode
+ *
+ * Private helper function to create SVG elements
+ */
+mxSvgCanvas2D.prototype.addNode = function(filled, stroked)
+{
+	var node = this.node;
+	var s = this.state;
+
+	if (node != null)
+	{
+		if (node.nodeName == 'path')
+		{
+			// Checks if the path is not empty
+			if (this.path != null && this.path.length > 0)
+			{
+				node.setAttribute('d', this.path.join(' '));
+			}
+			else
+			{
+				return;
+			}
+		}
+
+		if (filled && s.fillColor != null)
+		{
+			this.updateFill();
+		}
+		else if (!this.styleEnabled)
+		{
+			// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=814952
+			if (node.nodeName == 'ellipse' && mxClient.IS_FF)
+			{
+				node.setAttribute('fill', 'transparent');
+			}
+			else
+			{
+				node.setAttribute('fill', 'none');
+			}
+
+			// Sets the actual filled state for stroke tolerance
+			filled = false;
+		}
+
+		if (stroked && s.strokeColor != null)
+		{
+			this.updateStroke();
+		}
+		else if (!this.styleEnabled)
+		{
+			node.setAttribute('stroke', 'none');
+		}
+
+		if (s.transform != null && s.transform.length > 0)
+		{
+			node.setAttribute('transform', s.transform);
+		}
+
+		if (s.shadow)
+		{
+			this.root.appendChild(this.createShadow(node));
+		}
+
+		// Adds stroke tolerance
+		if (this.strokeTolerance > 0 && !filled)
+		{
+			this.root.appendChild(this.createTolerance(node));
+		}
+
+		// Adds pointer events
+		if (this.pointerEvents && (node.nodeName != 'path' ||
+			this.path[this.path.length - 1] == this.closeOp))
+		{
+			node.setAttribute('pointer-events', this.pointerEventsValue);
+		}
+		// Enables clicks for nodes inside a link element
+		else if (!this.pointerEvents && this.originalRoot == null)
+		{
+			node.setAttribute('pointer-events', 'none');
+		}
+
+		// Removes invisible nodes from output if they don't handle events
+		if ((node.nodeName != 'rect' && node.nodeName != 'path' && node.nodeName != 'ellipse') ||
+			(node.getAttribute('fill') != 'none' && node.getAttribute('fill') != 'transparent') ||
+			node.getAttribute('stroke') != 'none' || node.getAttribute('pointer-events') != 'none')
+		{
+			// LATER: Update existing DOM for performance
+			this.root.appendChild(node);
+		}
+
+		this.node = null;
+	}
+};
+
+/**
+ * Function: updateFill
+ *
+ * Transfers the stroke attributes from  to .
+ */
+mxSvgCanvas2D.prototype.updateFill = function()
+{
+	var s = this.state;
+
+	if (s.alpha < 1 || s.fillAlpha < 1)
+	{
+		this.node.setAttribute('fill-opacity', s.alpha * s.fillAlpha);
+	}
+
+	if (s.fillColor != null)
+	{
+		if (s.gradientColor != null)
+		{
+			var id = this.getSvgGradient(s.fillColor, s.gradientColor, s.gradientFillAlpha, s.gradientAlpha, s.gradientDirection);
+
+			if (!mxClient.IS_CHROME_APP && !mxClient.IS_IE && !mxClient.IS_IE11 &&
+				!mxClient.IS_EDGE && this.root.ownerDocument == document)
+			{
+				// Workaround for potential base tag and brackets must be escaped
+				var base = this.getBaseUrl().replace(/([\(\)])/g, '\\$1');
+				this.node.setAttribute('fill', 'url(' + base + '#' + id + ')');
+			}
+			else
+			{
+				this.node.setAttribute('fill', 'url(#' + id + ')');
+			}
+		}
+		else
+		{
+			this.node.setAttribute('fill', s.fillColor.toLowerCase());
+		}
+	}
+};
+
+/**
+ * Function: getCurrentStrokeWidth
+ *
+ * Returns the current stroke width (>= 1), ie. max(1, this.format(this.state.strokeWidth * this.state.scale)).
+ */
+mxSvgCanvas2D.prototype.getCurrentStrokeWidth = function()
+{
+	return Math.max(this.minStrokeWidth, Math.max(0.01, this.format(this.state.strokeWidth * this.state.scale)));
+};
+
+/**
+ * Function: updateStroke
+ *
+ * Transfers the stroke attributes from  to .
+ */
+mxSvgCanvas2D.prototype.updateStroke = function()
+{
+	var s = this.state;
+
+	this.node.setAttribute('stroke', s.strokeColor.toLowerCase());
+
+	if (s.alpha < 1 || s.strokeAlpha < 1)
+	{
+		this.node.setAttribute('stroke-opacity', s.alpha * s.strokeAlpha);
+	}
+
+	var sw = this.getCurrentStrokeWidth();
+
+	if (sw != 1)
+	{
+		this.node.setAttribute('stroke-width', sw);
+	}
+
+	if (this.node.nodeName == 'path')
+	{
+		this.updateStrokeAttributes();
+	}
+
+	if (s.dashed)
+	{
+		this.node.setAttribute('stroke-dasharray', this.createDashPattern(
+			((s.fixDash) ? 1 : s.strokeWidth) * s.scale));
+	}
+};
+
+/**
+ * Function: updateStrokeAttributes
+ *
+ * Transfers the stroke attributes from  to .
+ */
+mxSvgCanvas2D.prototype.updateStrokeAttributes = function()
+{
+	var s = this.state;
+
+	// Linejoin miter is default in SVG
+	if (s.lineJoin != null && s.lineJoin != 'miter')
+	{
+		this.node.setAttribute('stroke-linejoin', s.lineJoin);
+	}
+
+	if (s.lineCap != null)
+	{
+		// flat is called butt in SVG
+		var value = s.lineCap;
+
+		if (value == 'flat')
+		{
+			value = 'butt';
+		}
+
+		// Linecap butt is default in SVG
+		if (value != 'butt')
+		{
+			this.node.setAttribute('stroke-linecap', value);
+		}
+	}
+
+	// Miterlimit 10 is default in our document
+	if (s.miterLimit != null && (!this.styleEnabled || s.miterLimit != 10))
+	{
+		this.node.setAttribute('stroke-miterlimit', s.miterLimit);
+	}
+};
+
+/**
+ * Function: createDashPattern
+ *
+ * Creates the SVG dash pattern for the given state.
+ */
+mxSvgCanvas2D.prototype.createDashPattern = function(scale)
+{
+	var pat = [];
+
+	if (typeof(this.state.dashPattern) === 'string')
+	{
+		var dash = this.state.dashPattern.split(' ');
+
+		if (dash.length > 0)
+		{
+			for (var i = 0; i < dash.length; i++)
+			{
+				pat[i] = Number(dash[i]) * scale;
+			}
+		}
+	}
+
+	return pat.join(' ');
+};
+
+/**
+ * Function: createTolerance
+ *
+ * Creates a hit detection tolerance shape for the given node.
+ */
+mxSvgCanvas2D.prototype.createTolerance = function(node)
+{
+	var tol = node.cloneNode(true);
+	var sw = parseFloat(tol.getAttribute('stroke-width') || 1) + this.strokeTolerance;
+	tol.setAttribute('pointer-events', 'stroke');
+	tol.setAttribute('visibility', 'hidden');
+	tol.removeAttribute('stroke-dasharray');
+	tol.setAttribute('stroke-width', sw);
+	tol.setAttribute('fill', 'none');
+
+	// Workaround for Opera ignoring the visiblity attribute above while
+	// other browsers need a stroke color to perform the hit-detection but
+	// do not ignore the visibility attribute. Side-effect is that Opera's
+	// hit detection for horizontal/vertical edges seems to ignore the tol.
+	tol.setAttribute('stroke', (mxClient.IS_OT) ? 'none' : 'white');
+
+	return tol;
+};
+
+/**
+ * Function: createShadow
+ *
+ * Creates a shadow for the given node.
+ */
+mxSvgCanvas2D.prototype.createShadow = function(node)
+{
+	var shadow = node.cloneNode(true);
+	var s = this.state;
+
+	// Firefox uses transparent for no fill in ellipses
+	if (shadow.getAttribute('fill') != 'none' && (!mxClient.IS_FF || shadow.getAttribute('fill') != 'transparent'))
+	{
+		shadow.setAttribute('fill', s.shadowColor);
+	}
+
+	if (shadow.getAttribute('stroke') != 'none')
+	{
+		shadow.setAttribute('stroke', s.shadowColor);
+	}
+
+	shadow.setAttribute('transform', 'translate(' + this.format(s.shadowDx * s.scale) +
+		',' + this.format(s.shadowDy * s.scale) + ')' + (s.transform || ''));
+	shadow.setAttribute('opacity', s.shadowAlpha);
+
+	return shadow;
+};
+
+/**
+ * Function: setLink
+ *
+ * Experimental implementation for hyperlinks.
+ */
+mxSvgCanvas2D.prototype.setLink = function(link)
+{
+	if (link == null)
+	{
+		this.root = this.originalRoot;
+	}
+	else
+	{
+		this.originalRoot = this.root;
+
+		var node = this.createElement('a');
+
+		// Workaround for implicit namespace handling in HTML5 export, IE adds NS1 namespace so use code below
+		// in all IE versions except quirks mode. KNOWN: Adds xlink namespace to each image tag in output.
+		if (node.setAttributeNS == null || (this.root.ownerDocument != document && document.documentMode == null))
+		{
+			node.setAttribute('xlink:href', link);
+		}
+		else
+		{
+			node.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', link);
+		}
+
+		this.root.appendChild(node);
+		this.root = node;
+	}
+};
+
+/**
+ * Function: rotate
+ *
+ * Sets the rotation of the canvas. Note that rotation cannot be concatenated.
+ */
+mxSvgCanvas2D.prototype.rotate = function(theta, flipH, flipV, cx, cy)
+{
+	if (theta != 0 || flipH || flipV)
+	{
+		var s = this.state;
+		cx += s.dx;
+		cy += s.dy;
+
+		cx *= s.scale;
+		cy *= s.scale;
+
+		s.transform = s.transform || '';
+
+		// This implementation uses custom scale/translate and built-in rotation
+		// Rotation state is part of the AffineTransform in state.transform
+		if (flipH && flipV)
+		{
+			theta += 180;
+		}
+		else if (flipH != flipV)
+		{
+			var tx = (flipH) ? cx : 0;
+			var sx = (flipH) ? -1 : 1;
+
+			var ty = (flipV) ? cy : 0;
+			var sy = (flipV) ? -1 : 1;
+
+			s.transform += 'translate(' + this.format(tx) + ',' + this.format(ty) + ')' +
+				'scale(' + this.format(sx) + ',' + this.format(sy) + ')' +
+				'translate(' + this.format(-tx) + ',' + this.format(-ty) + ')';
+		}
+
+		if (flipH ? !flipV : flipV)
+		{
+			theta *= -1;
+		}
+
+		if (theta != 0)
+		{
+			s.transform += 'rotate(' + this.format(theta) + ',' + this.format(cx) + ',' + this.format(cy) + ')';
+		}
+
+		s.rotation = s.rotation + theta;
+		s.rotationCx = cx;
+		s.rotationCy = cy;
+	}
+};
+
+/**
+ * Function: begin
+ *
+ * Extends superclass to create path.
+ */
+mxSvgCanvas2D.prototype.begin = function()
+{
+	mxAbstractCanvas2D.prototype.begin.apply(this, arguments);
+	this.node = this.createElement('path');
+};
+
+/**
+ * Function: rect
+ *
+ * Private helper function to create SVG elements
+ */
+mxSvgCanvas2D.prototype.rect = function(x, y, w, h)
+{
+	var s = this.state;
+	var n = this.createElement('rect');
+	n.setAttribute('x', this.format((x + s.dx) * s.scale));
+	n.setAttribute('y', this.format((y + s.dy) * s.scale));
+	n.setAttribute('width', this.format(w * s.scale));
+	n.setAttribute('height', this.format(h * s.scale));
+
+	this.node = n;
+};
+
+/**
+ * Function: roundrect
+ *
+ * Private helper function to create SVG elements
+ */
+mxSvgCanvas2D.prototype.roundrect = function(x, y, w, h, dx, dy)
+{
+	this.rect(x, y, w, h);
+
+	if (dx > 0)
+	{
+		this.node.setAttribute('rx', this.format(dx * this.state.scale));
+	}
+
+	if (dy > 0)
+	{
+		this.node.setAttribute('ry', this.format(dy * this.state.scale));
+	}
+};
+
+/**
+ * Function: ellipse
+ *
+ * Private helper function to create SVG elements
+ */
+mxSvgCanvas2D.prototype.ellipse = function(x, y, w, h)
+{
+	var s = this.state;
+	var n = this.createElement('ellipse');
+	// No rounding for consistent output with 1.x
+	n.setAttribute('cx', Math.round((x + w / 2 + s.dx) * s.scale));
+	n.setAttribute('cy', Math.round((y + h / 2 + s.dy) * s.scale));
+	n.setAttribute('rx', w / 2 * s.scale);
+	n.setAttribute('ry', h / 2 * s.scale);
+	this.node = n;
+};
+
+/**
+ * Function: image
+ *
+ * Private helper function to create SVG elements
+ */
+mxSvgCanvas2D.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV)
+{
+	src = this.converter.convert(src);
+
+	// LATER: Add option for embedding images as base64.
+	aspect = (aspect != null) ? aspect : true;
+	flipH = (flipH != null) ? flipH : false;
+	flipV = (flipV != null) ? flipV : false;
+
+	var s = this.state;
+	x += s.dx;
+	y += s.dy;
+
+	var node = this.createElement('image');
+	node.setAttribute('x', this.format(x * s.scale) + this.imageOffset);
+	node.setAttribute('y', this.format(y * s.scale) + this.imageOffset);
+	node.setAttribute('width', this.format(w * s.scale));
+	node.setAttribute('height', this.format(h * s.scale));
+
+	// Workaround for missing namespace support
+	if (node.setAttributeNS == null)
+	{
+		node.setAttribute('xlink:href', src);
+	}
+	else
+	{
+		node.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', src);
+	}
+
+	if (!aspect)
+	{
+		node.setAttribute('preserveAspectRatio', 'none');
+	}
+
+	if (s.alpha < 1 || s.fillAlpha < 1)
+	{
+		node.setAttribute('opacity', s.alpha * s.fillAlpha);
+	}
+
+	var tr = this.state.transform || '';
+
+	if (flipH || flipV)
+	{
+		var sx = 1;
+		var sy = 1;
+		var dx = 0;
+		var dy = 0;
+
+		if (flipH)
+		{
+			sx = -1;
+			dx = -w - 2 * x;
+		}
+
+		if (flipV)
+		{
+			sy = -1;
+			dy = -h - 2 * y;
+		}
+
+		// Adds image tansformation to existing transform
+		tr += 'scale(' + sx + ',' + sy + ')translate(' + (dx * s.scale) + ',' + (dy * s.scale) + ')';
+	}
+
+	if (tr.length > 0)
+	{
+		node.setAttribute('transform', tr);
+	}
+
+	if (!this.pointerEvents)
+	{
+		node.setAttribute('pointer-events', 'none');
+	}
+
+	this.root.appendChild(node);
+
+	// Disables control-clicks on images in Firefox to open in new tab
+	// by putting a rect in the foreground that absorbs all events and
+	// disabling all pointer-events on the original image tag.
+	if (this.blockImagePointerEvents)
+	{
+		node.setAttribute('style', 'pointer-events:none');
+
+		node = this.createElement('rect');
+		node.setAttribute('visibility', 'hidden');
+		node.setAttribute('pointer-events', 'fill');
+		node.setAttribute('x', this.format(x * s.scale));
+		node.setAttribute('y', this.format(y * s.scale));
+		node.setAttribute('width', this.format(w * s.scale));
+		node.setAttribute('height', this.format(h * s.scale));
+		this.root.appendChild(node);
+	}
+};
+
+/**
+ * Function: convertHtml
+ *
+ * Converts the given HTML string to XHTML.
+ */
+mxSvgCanvas2D.prototype.convertHtml = function(val)
+{
+	if (this.useDomParser)
+	{
+		var doc = new DOMParser().parseFromString(val, 'text/html');
+
+		if (doc != null)
+		{
+			val = new XMLSerializer().serializeToString(doc.body);
+
+			// Extracts body content from DOM
+			if (val.substring(0, 5) == '', 5) + 1);
+			}
+
+			if (val.substring(val.length - 7, val.length) == '')
+			{
+				val = val.substring(0, val.length - 7);
+			}
+		}
+	}
+	else if (document.implementation != null && document.implementation.createDocument != null)
+	{
+		var xd = document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null);
+		var xb = xd.createElement('body');
+		xd.documentElement.appendChild(xb);
+
+		var div = document.createElement('div');
+		div.innerHTML = val;
+		var child = div.firstChild;
+
+		while (child != null)
+		{
+			var next = child.nextSibling;
+			xb.appendChild(xd.adoptNode(child));
+			child = next;
+		}
+
+		return xb.innerHTML;
+	}
+	else
+	{
+		var ta = document.createElement('textarea');
+
+		// Handles special HTML entities < and > and double escaping
+		// and converts unclosed br, hr and img tags to XHTML
+		// LATER: Convert all unclosed tags
+		ta.innerHTML = val.replace(/&/g, '&amp;').
+			replace(/</g, '&lt;').replace(/>/g, '&gt;').
+			replace(/</g, '&lt;').replace(/>/g, '&gt;').
+			replace(//g, '>');
+		val = ta.value.replace(/&/g, '&').replace(/&lt;/g, '<').
+			replace(/&gt;/g, '>').replace(/&amp;/g, '&').
+			replace(/
/g, '
').replace(/
/g, '
'). + replace(/(]+)>/gm, "$1 />"); + } + + return val; +}; + +/** + * Function: createDiv + * + * Private helper function to create SVG elements + */ +mxSvgCanvas2D.prototype.createDiv = function(str, align, valign, style, overflow) +{ + var s = this.state; + + // Inline block for rendering HTML background over SVG in Safari + var lh = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (s.fontSize * mxConstants.LINE_HEIGHT) + 'px' : + (mxConstants.LINE_HEIGHT * this.lineHeightCorrection); + + style = 'display:inline-block;font-size:' + s.fontSize + 'px;font-family:' + s.fontFamily + + ';color:' + s.fontColor + ';line-height:' + lh + ';' + style; + + if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + style += 'font-weight:bold;'; + } + + if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + style += 'font-style:italic;'; + } + + if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) + { + style += 'text-decoration:underline;'; + } + + if (align == mxConstants.ALIGN_CENTER) + { + style += 'text-align:center;'; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + style += 'text-align:right;'; + } + + var css = ''; + + if (s.fontBackgroundColor != null) + { + css += 'background-color:' + s.fontBackgroundColor + ';'; + } + + if (s.fontBorderColor != null) + { + css += 'border:1px solid ' + s.fontBorderColor + ';'; + } + + var val = str; + + if (!mxUtils.isNode(val)) + { + val = this.convertHtml(val); + + if (overflow != 'fill' && overflow != 'width') + { + // Inner div always needed to measure wrapped text + val = '
' + val + '
'; + } + else + { + style += css; + } + } + + // Uses DOM API where available. This cannot be used in IE to avoid + // an opening and two (!) closing TBODY tags being added to tables. + if (!mxClient.IS_IE && document.createElementNS) + { + var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); + div.setAttribute('style', style); + + if (mxUtils.isNode(val)) + { + // Creates a copy for export + if (this.root.ownerDocument != document) + { + div.appendChild(val.cloneNode(true)); + } + else + { + div.appendChild(val); + } + } + else + { + div.innerHTML = val; + } + + return div; + } + else + { + // Serializes for export + if (mxUtils.isNode(val) && this.root.ownerDocument != document) + { + val = val.outerHTML; + } + + // NOTE: FF 3.6 crashes if content CSS contains "height:100%" + return mxUtils.parseXml('
' + val + '
').documentElement; + } +}; + +/** + * Invalidates the cached offset size for the given node. + */ +mxSvgCanvas2D.prototype.invalidateCachedOffsetSize = function(node) +{ + delete node.firstChild.mxCachedOffsetWidth; + delete node.firstChild.mxCachedFinalOffsetWidth; + delete node.firstChild.mxCachedFinalOffsetHeight; +}; + +/** + * Updates existing DOM nodes for text rendering. LATER: Merge common parts with text function below. + */ +mxSvgCanvas2D.prototype.updateText = function(x, y, w, h, align, valign, wrap, overflow, clip, rotation, node) +{ + if (node != null && node.firstChild != null && node.firstChild.firstChild != null && + node.firstChild.firstChild.firstChild != null) + { + // Uses outer group for opacity and transforms to + // fix rendering order in Chrome + var group = node.firstChild; + var fo = group.firstChild; + var div = fo.firstChild; + + rotation = (rotation != null) ? rotation : 0; + + var s = this.state; + x += s.dx; + y += s.dy; + + if (clip) + { + div.style.maxHeight = Math.round(h) + 'px'; + div.style.maxWidth = Math.round(w) + 'px'; + } + else if (overflow == 'fill') + { + div.style.width = Math.round(w + 1) + 'px'; + div.style.height = Math.round(h + 1) + 'px'; + } + else if (overflow == 'width') + { + div.style.width = Math.round(w + 1) + 'px'; + + if (h > 0) + { + div.style.maxHeight = Math.round(h) + 'px'; + } + } + + if (wrap && w > 0) + { + div.style.width = Math.round(w + 1) + 'px'; + } + + // Code that depends on the size which is computed after + // the element was added to the DOM. + var ow = 0; + var oh = 0; + + // Padding avoids clipping on border and wrapping for differing font metrics on platforms + var padX = 0; + var padY = 2; + + var sizeDiv = div; + + if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV') + { + sizeDiv = sizeDiv.firstChild; + } + + var tmp = (group.mxCachedOffsetWidth != null) ? group.mxCachedOffsetWidth : sizeDiv.offsetWidth; + ow = tmp + padX; + + // Recomputes the height of the element for wrapped width + if (wrap && overflow != 'fill') + { + if (clip) + { + ow = Math.min(ow, w); + } + + div.style.width = Math.round(ow + 1) + 'px'; + } + + ow = (group.mxCachedFinalOffsetWidth != null) ? group.mxCachedFinalOffsetWidth : sizeDiv.offsetWidth; + oh = (group.mxCachedFinalOffsetHeight != null) ? group.mxCachedFinalOffsetHeight : sizeDiv.offsetHeight; + + if (this.cacheOffsetSize) + { + group.mxCachedOffsetWidth = tmp; + group.mxCachedFinalOffsetWidth = ow; + group.mxCachedFinalOffsetHeight = oh; + } + + ow += padX; + oh -= 2; + + if (clip) + { + oh = Math.min(oh, h); + ow = Math.min(ow, w); + } + + if (overflow == 'width') + { + h = oh; + } + else if (overflow != 'fill') + { + w = ow; + h = oh; + } + + var dx = 0; + var dy = 0; + + if (align == mxConstants.ALIGN_CENTER) + { + dx -= w / 2; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + dx -= w; + } + + x += dx; + + // FIXME: LINE_HEIGHT not ideal for all text sizes, fix for export + if (valign == mxConstants.ALIGN_MIDDLE) + { + dy -= h / 2; + } + else if (valign == mxConstants.ALIGN_BOTTOM) + { + dy -= h; + } + + // Workaround for rendering offsets + // TODO: Check if export needs these fixes, too + if (overflow != 'fill' && mxClient.IS_FF && mxClient.IS_WIN) + { + dy -= 2; + } + + y += dy; + + var tr = (s.scale != 1) ? 'scale(' + s.scale + ')' : ''; + + if (s.rotation != 0 && this.rotateHtml) + { + tr += 'rotate(' + (s.rotation) + ',' + (w / 2) + ',' + (h / 2) + ')'; + var pt = this.rotatePoint((x + w / 2) * s.scale, (y + h / 2) * s.scale, + s.rotation, s.rotationCx, s.rotationCy); + x = pt.x - w * s.scale / 2; + y = pt.y - h * s.scale / 2; + } + else + { + x *= s.scale; + y *= s.scale; + } + + if (rotation != 0) + { + tr += 'rotate(' + (rotation) + ',' + (-dx) + ',' + (-dy) + ')'; + } + + group.setAttribute('transform', 'translate(' + Math.round(x) + ',' + Math.round(y) + ')' + tr); + fo.setAttribute('width', Math.round(Math.max(1, w))); + fo.setAttribute('height', Math.round(Math.max(1, h))); + } +}; + +/** + * Function: text + * + * Paints the given text. Possible values for format are empty string for plain + * text and html for HTML markup. Note that HTML markup is only supported if + * foreignObject is supported and is true. (This means IE9 and later + * does currently not support HTML text as part of shapes.) + */ +mxSvgCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir) +{ + if (this.textEnabled && str != null) + { + rotation = (rotation != null) ? rotation : 0; + + var s = this.state; + x += s.dx; + y += s.dy; + + if (this.foEnabled && format == 'html') + { + var style = 'vertical-align:top;'; + + if (clip) + { + style += 'overflow:hidden;max-height:' + Math.round(h) + 'px;max-width:' + Math.round(w) + 'px;'; + } + else if (overflow == 'fill') + { + style += 'width:' + Math.round(w + 1) + 'px;height:' + Math.round(h + 1) + 'px;overflow:hidden;'; + } + else if (overflow == 'width') + { + style += 'width:' + Math.round(w + 1) + 'px;'; + + if (h > 0) + { + style += 'max-height:' + Math.round(h) + 'px;overflow:hidden;'; + } + } + + if (wrap && w > 0) + { + style += 'width:' + Math.round(w + 1) + 'px;white-space:normal;word-wrap:' + + mxConstants.WORD_WRAP + ';'; + } + else + { + style += 'white-space:nowrap;'; + } + + // Uses outer group for opacity and transforms to + // fix rendering order in Chrome + var group = this.createElement('g'); + + if (s.alpha < 1) + { + group.setAttribute('opacity', s.alpha); + } + + var fo = this.createElement('foreignObject'); + fo.setAttribute('style', 'overflow:visible;'); + fo.setAttribute('pointer-events', 'all'); + + var div = this.createDiv(str, align, valign, style, overflow); + + // Ignores invalid XHTML labels + if (div == null) + { + return; + } + else if (dir != null) + { + div.setAttribute('dir', dir); + } + + group.appendChild(fo); + this.root.appendChild(group); + + // Code that depends on the size which is computed after + // the element was added to the DOM. + var ow = 0; + var oh = 0; + + // Padding avoids clipping on border and wrapping for differing font metrics on platforms + var padX = 2; + var padY = 2; + + // NOTE: IE is always export as it does not support foreign objects + if (mxClient.IS_IE && (document.documentMode == 9 || !mxClient.IS_SVG)) + { + // Handles non-standard namespace for getting size in IE + var clone = document.createElement('div'); + + clone.style.cssText = div.getAttribute('style'); + clone.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; + clone.style.position = 'absolute'; + clone.style.visibility = 'hidden'; + + // Inner DIV is needed for text measuring + var div2 = document.createElement('div'); + div2.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; + div2.style.wordWrap = mxConstants.WORD_WRAP; + div2.innerHTML = (mxUtils.isNode(str)) ? str.outerHTML : str; + clone.appendChild(div2); + + document.body.appendChild(clone); + + // Workaround for different box models + if (document.documentMode != 8 && document.documentMode != 9 && s.fontBorderColor != null) + { + padX += 2; + padY += 2; + } + + if (wrap && w > 0) + { + var tmp = div2.offsetWidth; + + // Workaround for adding padding twice in IE8/IE9 standards mode if label is wrapped + padDx = 0; + + // For export, if no wrapping occurs, we add a large padding to make + // sure there is no wrapping even if the text metrics are different. + // This adds support for text metrics on different operating systems. + // Disables wrapping if text is not wrapped for given width + if (!clip && wrap && w > 0 && this.root.ownerDocument != document && overflow != 'fill') + { + var ws = clone.style.whiteSpace; + div2.style.whiteSpace = 'nowrap'; + + if (tmp < div2.offsetWidth) + { + clone.style.whiteSpace = ws; + } + } + + if (clip) + { + tmp = Math.min(tmp, w); + } + + clone.style.width = tmp + 'px'; + + // Padding avoids clipping on border + ow = div2.offsetWidth + padX + padDx; + oh = div2.offsetHeight + padY; + + // Overrides the width of the DIV via XML DOM by using the + // clone DOM style, getting the CSS text for that and + // then setting that on the DIV via setAttribute + clone.style.display = 'inline-block'; + clone.style.position = ''; + clone.style.visibility = ''; + clone.style.width = ow + 'px'; + + div.setAttribute('style', clone.style.cssText); + } + else + { + // Padding avoids clipping on border + ow = div2.offsetWidth + padX; + oh = div2.offsetHeight + padY; + } + + clone.parentNode.removeChild(clone); + fo.appendChild(div); + } + else + { + // Uses document for text measuring during export + if (this.root.ownerDocument != document) + { + div.style.visibility = 'hidden'; + document.body.appendChild(div); + } + else + { + fo.appendChild(div); + } + + var sizeDiv = div; + + if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV') + { + sizeDiv = sizeDiv.firstChild; + + if (wrap && div.style.wordWrap == 'break-word') + { + sizeDiv.style.width = '100%'; + } + } + + var tmp = sizeDiv.offsetWidth; + + // Workaround for text measuring in hidden containers + if (tmp == 0 && div.parentNode == fo) + { + div.style.visibility = 'hidden'; + document.body.appendChild(div); + + tmp = sizeDiv.offsetWidth; + } + + if (this.cacheOffsetSize) + { + group.mxCachedOffsetWidth = tmp; + } + + // Disables wrapping if text is not wrapped for given width + if (!clip && wrap && w > 0 && this.root.ownerDocument != document && + overflow != 'fill' && overflow != 'width') + { + var ws = div.style.whiteSpace; + div.style.whiteSpace = 'nowrap'; + + if (tmp < sizeDiv.offsetWidth) + { + div.style.whiteSpace = ws; + } + } + + ow = tmp + padX - 1; + + // Recomputes the height of the element for wrapped width + if (wrap && overflow != 'fill' && overflow != 'width') + { + if (clip) + { + ow = Math.min(ow, w); + } + + div.style.width = ow + 'px'; + } + + ow = sizeDiv.offsetWidth; + oh = sizeDiv.offsetHeight; + + if (this.cacheOffsetSize) + { + group.mxCachedFinalOffsetWidth = ow; + group.mxCachedFinalOffsetHeight = oh; + } + + oh -= padY; + + if (div.parentNode != fo) + { + fo.appendChild(div); + div.style.visibility = ''; + } + } + + if (clip) + { + oh = Math.min(oh, h); + ow = Math.min(ow, w); + } + + if (overflow == 'width') + { + h = oh; + } + else if (overflow != 'fill') + { + w = ow; + h = oh; + } + + if (s.alpha < 1) + { + group.setAttribute('opacity', s.alpha); + } + + var dx = 0; + var dy = 0; + + if (align == mxConstants.ALIGN_CENTER) + { + dx -= w / 2; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + dx -= w; + } + + x += dx; + + // FIXME: LINE_HEIGHT not ideal for all text sizes, fix for export + if (valign == mxConstants.ALIGN_MIDDLE) + { + dy -= h / 2; + } + else if (valign == mxConstants.ALIGN_BOTTOM) + { + dy -= h; + } + + // Workaround for rendering offsets + // TODO: Check if export needs these fixes, too + //if (this.root.ownerDocument == document) + if (overflow != 'fill' && mxClient.IS_FF && mxClient.IS_WIN) + { + dy -= 2; + } + + y += dy; + + var tr = (s.scale != 1) ? 'scale(' + s.scale + ')' : ''; + + if (s.rotation != 0 && this.rotateHtml) + { + tr += 'rotate(' + (s.rotation) + ',' + (w / 2) + ',' + (h / 2) + ')'; + var pt = this.rotatePoint((x + w / 2) * s.scale, (y + h / 2) * s.scale, + s.rotation, s.rotationCx, s.rotationCy); + x = pt.x - w * s.scale / 2; + y = pt.y - h * s.scale / 2; + } + else + { + x *= s.scale; + y *= s.scale; + } + + if (rotation != 0) + { + tr += 'rotate(' + (rotation) + ',' + (-dx) + ',' + (-dy) + ')'; + } + + group.setAttribute('transform', 'translate(' + (Math.round(x) + this.foOffset) + ',' + + (Math.round(y) + this.foOffset) + ')' + tr); + fo.setAttribute('width', Math.round(Math.max(1, w))); + fo.setAttribute('height', Math.round(Math.max(1, h))); + + // Adds alternate content if foreignObject not supported in viewer + if (this.root.ownerDocument != document) + { + var alt = this.createAlternateContent(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation); + + if (alt != null) + { + fo.setAttribute('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility'); + var sw = this.createElement('switch'); + sw.appendChild(fo); + sw.appendChild(alt); + group.appendChild(sw); + } + } + } + else + { + this.plainText(x, y, w, h, str, align, valign, wrap, overflow, clip, rotation, dir); + } + } +}; + +/** + * Function: createClip + * + * Creates a clip for the given coordinates. + */ +mxSvgCanvas2D.prototype.createClip = function(x, y, w, h) +{ + x = Math.round(x); + y = Math.round(y); + w = Math.round(w); + h = Math.round(h); + + var id = 'mx-clip-' + x + '-' + y + '-' + w + '-' + h; + + var counter = 0; + var tmp = id + '-' + counter; + + // Resolves ID conflicts + while (document.getElementById(tmp) != null) + { + tmp = id + '-' + (++counter); + } + + clip = this.createElement('clipPath'); + clip.setAttribute('id', tmp); + + var rect = this.createElement('rect'); + rect.setAttribute('x', x); + rect.setAttribute('y', y); + rect.setAttribute('width', w); + rect.setAttribute('height', h); + + clip.appendChild(rect); + + return clip; +}; + +/** + * Function: text + * + * Paints the given text. Possible values for format are empty string for + * plain text and html for HTML markup. + */ +mxSvgCanvas2D.prototype.plainText = function(x, y, w, h, str, align, valign, wrap, overflow, clip, rotation, dir) +{ + rotation = (rotation != null) ? rotation : 0; + var s = this.state; + var size = s.fontSize; + var node = this.createElement('g'); + var tr = s.transform || ''; + this.updateFont(node); + + // Non-rotated text + if (rotation != 0) + { + tr += 'rotate(' + rotation + ',' + this.format(x * s.scale) + ',' + this.format(y * s.scale) + ')'; + } + + if (dir != null) + { + node.setAttribute('direction', dir); + } + + if (clip && w > 0 && h > 0) + { + var cx = x; + var cy = y; + + if (align == mxConstants.ALIGN_CENTER) + { + cx -= w / 2; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + cx -= w; + } + + if (overflow != 'fill') + { + if (valign == mxConstants.ALIGN_MIDDLE) + { + cy -= h / 2; + } + else if (valign == mxConstants.ALIGN_BOTTOM) + { + cy -= h; + } + } + + // LATER: Remove spacing from clip rectangle + var c = this.createClip(cx * s.scale - 2, cy * s.scale - 2, w * s.scale + 4, h * s.scale + 4); + + if (this.defs != null) + { + this.defs.appendChild(c); + } + else + { + // Makes sure clip is removed with referencing node + this.root.appendChild(c); + } + + if (!mxClient.IS_CHROME_APP && !mxClient.IS_IE && !mxClient.IS_IE11 && + !mxClient.IS_EDGE && this.root.ownerDocument == document) + { + // Workaround for potential base tag + var base = this.getBaseUrl().replace(/([\(\)])/g, '\\$1'); + node.setAttribute('clip-path', 'url(' + base + '#' + c.getAttribute('id') + ')'); + } + else + { + node.setAttribute('clip-path', 'url(#' + c.getAttribute('id') + ')'); + } + } + + // Default is left + var anchor = (align == mxConstants.ALIGN_RIGHT) ? 'end' : + (align == mxConstants.ALIGN_CENTER) ? 'middle' : + 'start'; + + // Text-anchor start is default in SVG + if (anchor != 'start') + { + node.setAttribute('text-anchor', anchor); + } + + if (!this.styleEnabled || size != mxConstants.DEFAULT_FONTSIZE) + { + node.setAttribute('font-size', (size * s.scale) + 'px'); + } + + if (tr.length > 0) + { + node.setAttribute('transform', tr); + } + + if (s.alpha < 1) + { + node.setAttribute('opacity', s.alpha); + } + + var lines = str.split('\n'); + var lh = Math.round(size * mxConstants.LINE_HEIGHT); + var textHeight = size + (lines.length - 1) * lh; + + var cy = y + size - 1; + + if (valign == mxConstants.ALIGN_MIDDLE) + { + if (overflow == 'fill') + { + cy -= h / 2; + } + else + { + var dy = ((this.matchHtmlAlignment && clip && h > 0) ? Math.min(textHeight, h) : textHeight) / 2; + cy -= dy + 1; + } + } + else if (valign == mxConstants.ALIGN_BOTTOM) + { + if (overflow == 'fill') + { + cy -= h; + } + else + { + var dy = (this.matchHtmlAlignment && clip && h > 0) ? Math.min(textHeight, h) : textHeight; + cy -= dy + 2; + } + } + + for (var i = 0; i < lines.length; i++) + { + // Workaround for bounding box of empty lines and spaces + if (lines[i].length > 0 && mxUtils.trim(lines[i]).length > 0) + { + var text = this.createElement('text'); + // LATER: Match horizontal HTML alignment + text.setAttribute('x', this.format(x * s.scale) + this.textOffset); + text.setAttribute('y', this.format(cy * s.scale) + this.textOffset); + + mxUtils.write(text, lines[i]); + node.appendChild(text); + } + + cy += lh; + } + + this.root.appendChild(node); + this.addTextBackground(node, str, x, y, w, (overflow == 'fill') ? h : textHeight, align, valign, overflow); +}; + +/** + * Function: updateFont + * + * Updates the text properties for the given node. (NOTE: For this to work in + * IE, the given node must be a text or tspan element.) + */ +mxSvgCanvas2D.prototype.updateFont = function(node) +{ + var s = this.state; + + node.setAttribute('fill', s.fontColor); + + if (!this.styleEnabled || s.fontFamily != mxConstants.DEFAULT_FONTFAMILY) + { + node.setAttribute('font-family', s.fontFamily); + } + + if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + node.setAttribute('font-weight', 'bold'); + } + + if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + node.setAttribute('font-style', 'italic'); + } + + if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) + { + node.setAttribute('text-decoration', 'underline'); + } +}; + +/** + * Function: addTextBackground + * + * Background color and border + */ +mxSvgCanvas2D.prototype.addTextBackground = function(node, str, x, y, w, h, align, valign, overflow) +{ + var s = this.state; + + if (s.fontBackgroundColor != null || s.fontBorderColor != null) + { + var bbox = null; + + if (overflow == 'fill' || overflow == 'width') + { + if (align == mxConstants.ALIGN_CENTER) + { + x -= w / 2; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + x -= w; + } + + if (valign == mxConstants.ALIGN_MIDDLE) + { + y -= h / 2; + } + else if (valign == mxConstants.ALIGN_BOTTOM) + { + y -= h; + } + + bbox = new mxRectangle((x + 1) * s.scale, y * s.scale, (w - 2) * s.scale, (h + 2) * s.scale); + } + else if (node.getBBox != null && this.root.ownerDocument == document) + { + // Uses getBBox only if inside document for correct size + try + { + bbox = node.getBBox(); + var ie = mxClient.IS_IE && mxClient.IS_SVG; + bbox = new mxRectangle(bbox.x, bbox.y + ((ie) ? 0 : 1), bbox.width, bbox.height + ((ie) ? 1 : 0)); + } + catch (e) + { + // Ignores NS_ERROR_FAILURE in FF if container display is none. + } + } + else + { + // Computes size if not in document or no getBBox available + var div = document.createElement('div'); + + // Wrapping and clipping can be ignored here + div.style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (s.fontSize * mxConstants.LINE_HEIGHT) + 'px' : mxConstants.LINE_HEIGHT; + div.style.fontSize = s.fontSize + 'px'; + div.style.fontFamily = s.fontFamily; + div.style.whiteSpace = 'nowrap'; + div.style.position = 'absolute'; + div.style.visibility = 'hidden'; + div.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; + div.style.zoom = '1'; + + if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + div.style.fontWeight = 'bold'; + } + + if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + div.style.fontStyle = 'italic'; + } + + str = mxUtils.htmlEntities(str, false); + div.innerHTML = str.replace(/\n/g, '
'); + + document.body.appendChild(div); + var w = div.offsetWidth; + var h = div.offsetHeight; + div.parentNode.removeChild(div); + + if (align == mxConstants.ALIGN_CENTER) + { + x -= w / 2; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + x -= w; + } + + if (valign == mxConstants.ALIGN_MIDDLE) + { + y -= h / 2; + } + else if (valign == mxConstants.ALIGN_BOTTOM) + { + y -= h; + } + + bbox = new mxRectangle((x + 1) * s.scale, (y + 2) * s.scale, w * s.scale, (h + 1) * s.scale); + } + + if (bbox != null) + { + var n = this.createElement('rect'); + n.setAttribute('fill', s.fontBackgroundColor || 'none'); + n.setAttribute('stroke', s.fontBorderColor || 'none'); + n.setAttribute('x', Math.floor(bbox.x - 1)); + n.setAttribute('y', Math.floor(bbox.y - 1)); + n.setAttribute('width', Math.ceil(bbox.width + 2)); + n.setAttribute('height', Math.ceil(bbox.height)); + + var sw = (s.fontBorderColor != null) ? Math.max(1, this.format(s.scale)) : 0; + n.setAttribute('stroke-width', sw); + + // Workaround for crisp rendering - only required if not exporting + if (this.root.ownerDocument == document && mxUtils.mod(sw, 2) == 1) + { + n.setAttribute('transform', 'translate(0.5, 0.5)'); + } + + node.insertBefore(n, node.firstChild); + } + } +}; + +/** + * Function: stroke + * + * Paints the outline of the current path. + */ +mxSvgCanvas2D.prototype.stroke = function() +{ + this.addNode(false, true); +}; + +/** + * Function: fill + * + * Fills the current path. + */ +mxSvgCanvas2D.prototype.fill = function() +{ + this.addNode(true, false); +}; + +/** + * Function: fillAndStroke + * + * Fills and paints the outline of the current path. + */ +mxSvgCanvas2D.prototype.fillAndStroke = function() +{ + this.addNode(true, true); +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * + * Class: mxVmlCanvas2D + * + * Implements a canvas to be used for rendering VML. Here is an example of implementing a + * fallback for SVG images which are not supported in VML-based browsers. + * + * (code) + * var mxVmlCanvas2DImage = mxVmlCanvas2D.prototype.image; + * mxVmlCanvas2D.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV) + * { + * if (src.substring(src.length - 4, src.length) == '.svg') + * { + * src = 'http://www.jgraph.com/images/mxgraph.gif'; + * } + * + * mxVmlCanvas2DImage.apply(this, arguments); + * }; + * (end) + * + * To disable anti-aliasing in the output, use the following code. + * + * (code) + * document.createStyleSheet().cssText = mxClient.VML_PREFIX + '\\:*{antialias:false;)}'; + * (end) + * + * A description of the public API is available in . Note that + * there is a known issue in VML where gradients are painted using the outer + * bounding box of rotated shapes, not the actual bounds of the shape. See + * also for plain text label restrictions in shapes for VML. + */ +var mxVmlCanvas2D = function(root) +{ + mxAbstractCanvas2D.call(this); + + /** + * Variable: root + * + * Reference to the container for the SVG content. + */ + this.root = root; +}; + +/** + * Extends mxAbstractCanvas2D + */ +mxUtils.extend(mxVmlCanvas2D, mxAbstractCanvas2D); + +/** + * Variable: path + * + * Holds the current DOM node. + */ +mxVmlCanvas2D.prototype.node = null; + +/** + * Variable: textEnabled + * + * Specifies if text output should be enabledetB. Default is true. + */ +mxVmlCanvas2D.prototype.textEnabled = true; + +/** + * Variable: moveOp + * + * Contains the string used for moving in paths. Default is 'm'. + */ +mxVmlCanvas2D.prototype.moveOp = 'm'; + +/** + * Variable: lineOp + * + * Contains the string used for moving in paths. Default is 'l'. + */ +mxVmlCanvas2D.prototype.lineOp = 'l'; + +/** + * Variable: curveOp + * + * Contains the string used for bezier curves. Default is 'c'. + */ +mxVmlCanvas2D.prototype.curveOp = 'c'; + +/** + * Variable: closeOp + * + * Holds the operator for closing curves. Default is 'x e'. + */ +mxVmlCanvas2D.prototype.closeOp = 'x'; + +/** + * Variable: rotatedHtmlBackground + * + * Background color for rotated HTML. Default is ''. This can be set to eg. + * white to improve rendering of rotated text in VML for IE9. + */ +mxVmlCanvas2D.prototype.rotatedHtmlBackground = ''; + +/** + * Variable: vmlScale + * + * Specifies the scale used to draw VML shapes. + */ +mxVmlCanvas2D.prototype.vmlScale = 1; + +/** + * Function: createElement + * + * Creates the given element using the document. + */ +mxVmlCanvas2D.prototype.createElement = function(name) +{ + return document.createElement(name); +}; + +/** + * Function: createVmlElement + * + * Creates a new element using and prefixes the given name with + * . + */ +mxVmlCanvas2D.prototype.createVmlElement = function(name) +{ + return this.createElement(mxClient.VML_PREFIX + ':' + name); +}; + +/** + * Function: addNode + * + * Adds the current node to the . + */ +mxVmlCanvas2D.prototype.addNode = function(filled, stroked) +{ + var node = this.node; + var s = this.state; + + if (node != null) + { + if (node.nodeName == 'shape') + { + // Checks if the path is not empty + if (this.path != null && this.path.length > 0) + { + node.path = this.path.join(' ') + ' e'; + node.style.width = this.root.style.width; + node.style.height = this.root.style.height; + node.coordsize = parseInt(node.style.width) + ' ' + parseInt(node.style.height); + } + else + { + return; + } + } + + node.strokeweight = this.format(Math.max(1, s.strokeWidth * s.scale / this.vmlScale)) + 'px'; + + if (s.shadow) + { + this.root.appendChild(this.createShadow(node, + filled && s.fillColor != null, + stroked && s.strokeColor != null)); + } + + if (stroked && s.strokeColor != null) + { + node.stroked = 'true'; + node.strokecolor = s.strokeColor; + } + else + { + node.stroked = 'false'; + } + + node.appendChild(this.createStroke()); + + if (filled && s.fillColor != null) + { + node.appendChild(this.createFill()); + } + else if (this.pointerEvents && (node.nodeName != 'shape' || + this.path[this.path.length - 1] == this.closeOp)) + { + node.appendChild(this.createTransparentFill()); + } + else + { + node.filled = 'false'; + } + + // LATER: Update existing DOM for performance + this.root.appendChild(node); + } +}; + +/** + * Function: createTransparentFill + * + * Creates a transparent fill. + */ +mxVmlCanvas2D.prototype.createTransparentFill = function() +{ + var fill = this.createVmlElement('fill'); + fill.src = mxClient.imageBasePath + '/transparent.gif'; + fill.type = 'tile'; + + return fill; +}; + +/** + * Function: createFill + * + * Creates a fill for the current state. + */ +mxVmlCanvas2D.prototype.createFill = function() +{ + var s = this.state; + + // Gradients in foregrounds not supported because special gradients + // with bounds must be created for each element in graphics-canvases + var fill = this.createVmlElement('fill'); + fill.color = s.fillColor; + + if (s.gradientColor != null) + { + fill.type = 'gradient'; + fill.method = 'none'; + fill.color2 = s.gradientColor; + var angle = 180 - s.rotation; + + if (s.gradientDirection == mxConstants.DIRECTION_WEST) + { + angle -= 90 + ((this.root.style.flip == 'x') ? 180 : 0); + } + else if (s.gradientDirection == mxConstants.DIRECTION_EAST) + { + angle += 90 + ((this.root.style.flip == 'x') ? 180 : 0); + } + else if (s.gradientDirection == mxConstants.DIRECTION_NORTH) + { + angle -= 180 + ((this.root.style.flip == 'y') ? -180 : 0); + } + else + { + angle += ((this.root.style.flip == 'y') ? -180 : 0); + } + + if (this.root.style.flip == 'x' || this.root.style.flip == 'y') + { + angle *= -1; + } + + // LATER: Fix outer bounding box for rotated shapes used in VML. + fill.angle = mxUtils.mod(angle, 360); + fill.opacity = (s.alpha * s.gradientFillAlpha * 100) + '%'; + fill.setAttribute(mxClient.OFFICE_PREFIX + ':opacity2', (s.alpha * s.gradientAlpha * 100) + '%'); + } + else if (s.alpha < 1 || s.fillAlpha < 1) + { + fill.opacity = (s.alpha * s.fillAlpha * 100) + '%'; + } + + return fill; +}; +/** + * Function: createStroke + * + * Creates a fill for the current state. + */ +mxVmlCanvas2D.prototype.createStroke = function() +{ + var s = this.state; + var stroke = this.createVmlElement('stroke'); + stroke.endcap = s.lineCap || 'flat'; + stroke.joinstyle = s.lineJoin || 'miter'; + stroke.miterlimit = s.miterLimit || '10'; + + if (s.alpha < 1 || s.strokeAlpha < 1) + { + stroke.opacity = (s.alpha * s.strokeAlpha * 100) + '%'; + } + + if (s.dashed) + { + stroke.dashstyle = this.getVmlDashStyle(); + } + + return stroke; +}; + +/** + * Function: getVmlDashPattern + * + * Returns a VML dash pattern for the current dashPattern. + * See http://msdn.microsoft.com/en-us/library/bb264085(v=vs.85).aspx + */ +mxVmlCanvas2D.prototype.getVmlDashStyle = function() +{ + var result = 'dash'; + + if (typeof(this.state.dashPattern) === 'string') + { + var tok = this.state.dashPattern.split(' '); + + if (tok.length > 0 && tok[0] == 1) + { + result = '0 2'; + } + } + + return result; +}; + +/** + * Function: createShadow + * + * Creates a shadow for the given node. + */ +mxVmlCanvas2D.prototype.createShadow = function(node, filled, stroked) +{ + var s = this.state; + var rad = -s.rotation * (Math.PI / 180); + var cos = Math.cos(rad); + var sin = Math.sin(rad); + + var dx = s.shadowDx * s.scale; + var dy = s.shadowDy * s.scale; + + if (this.root.style.flip == 'x') + { + dx *= -1; + } + else if (this.root.style.flip == 'y') + { + dy *= -1; + } + + var shadow = node.cloneNode(true); + shadow.style.marginLeft = Math.round(dx * cos - dy * sin) + 'px'; + shadow.style.marginTop = Math.round(dx * sin + dy * cos) + 'px'; + + // Workaround for wrong cloning in IE8 standards mode + if (document.documentMode == 8) + { + shadow.strokeweight = node.strokeweight; + + if (node.nodeName == 'shape') + { + shadow.path = this.path.join(' ') + ' e'; + shadow.style.width = this.root.style.width; + shadow.style.height = this.root.style.height; + shadow.coordsize = parseInt(node.style.width) + ' ' + parseInt(node.style.height); + } + } + + if (stroked) + { + shadow.strokecolor = s.shadowColor; + shadow.appendChild(this.createShadowStroke()); + } + else + { + shadow.stroked = 'false'; + } + + if (filled) + { + shadow.appendChild(this.createShadowFill()); + } + else + { + shadow.filled = 'false'; + } + + return shadow; +}; + +/** + * Function: createShadowFill + * + * Creates the fill for the shadow. + */ +mxVmlCanvas2D.prototype.createShadowFill = function() +{ + var fill = this.createVmlElement('fill'); + fill.color = this.state.shadowColor; + fill.opacity = (this.state.alpha * this.state.shadowAlpha * 100) + '%'; + + return fill; +}; + +/** + * Function: createShadowStroke + * + * Creates the stroke for the shadow. + */ +mxVmlCanvas2D.prototype.createShadowStroke = function() +{ + var stroke = this.createStroke(); + stroke.opacity = (this.state.alpha * this.state.shadowAlpha * 100) + '%'; + + return stroke; +}; + +/** + * Function: rotate + * + * Sets the rotation of the canvas. Note that rotation cannot be concatenated. + */ +mxVmlCanvas2D.prototype.rotate = function(theta, flipH, flipV, cx, cy) +{ + if (flipH && flipV) + { + theta += 180; + } + else if (flipH) + { + this.root.style.flip = 'x'; + } + else if (flipV) + { + this.root.style.flip = 'y'; + } + + if (flipH ? !flipV : flipV) + { + theta *= -1; + } + + this.root.style.rotation = theta; + this.state.rotation = this.state.rotation + theta; + this.state.rotationCx = cx; + this.state.rotationCy = cy; +}; + +/** + * Function: begin + * + * Extends superclass to create path. + */ +mxVmlCanvas2D.prototype.begin = function() +{ + mxAbstractCanvas2D.prototype.begin.apply(this, arguments); + this.node = this.createVmlElement('shape'); + this.node.style.position = 'absolute'; +}; + +/** + * Function: quadTo + * + * Replaces quadratic curve with bezier curve in VML. + */ +mxVmlCanvas2D.prototype.quadTo = function(x1, y1, x2, y2) +{ + var s = this.state; + + var cpx0 = (this.lastX + s.dx) * s.scale; + var cpy0 = (this.lastY + s.dy) * s.scale; + var qpx1 = (x1 + s.dx) * s.scale; + var qpy1 = (y1 + s.dy) * s.scale; + var cpx3 = (x2 + s.dx) * s.scale; + var cpy3 = (y2 + s.dy) * s.scale; + + var cpx1 = cpx0 + 2/3 * (qpx1 - cpx0); + var cpy1 = cpy0 + 2/3 * (qpy1 - cpy0); + + var cpx2 = cpx3 + 2/3 * (qpx1 - cpx3); + var cpy2 = cpy3 + 2/3 * (qpy1 - cpy3); + + this.path.push('c ' + this.format(cpx1) + ' ' + this.format(cpy1) + + ' ' + this.format(cpx2) + ' ' + this.format(cpy2) + + ' ' + this.format(cpx3) + ' ' + this.format(cpy3)); + this.lastX = (cpx3 / s.scale) - s.dx; + this.lastY = (cpy3 / s.scale) - s.dy; + +}; + +/** + * Function: createRect + * + * Sets the glass gradient. + */ +mxVmlCanvas2D.prototype.createRect = function(nodeName, x, y, w, h) +{ + var s = this.state; + var n = this.createVmlElement(nodeName); + n.style.position = 'absolute'; + n.style.left = this.format((x + s.dx) * s.scale) + 'px'; + n.style.top = this.format((y + s.dy) * s.scale) + 'px'; + n.style.width = this.format(w * s.scale) + 'px'; + n.style.height = this.format(h * s.scale) + 'px'; + + return n; +}; + +/** + * Function: rect + * + * Sets the current path to a rectangle. + */ +mxVmlCanvas2D.prototype.rect = function(x, y, w, h) +{ + this.node = this.createRect('rect', x, y, w, h); +}; + +/** + * Function: roundrect + * + * Sets the current path to a rounded rectangle. + */ +mxVmlCanvas2D.prototype.roundrect = function(x, y, w, h, dx, dy) +{ + this.node = this.createRect('roundrect', x, y, w, h); + // SetAttribute needed here for IE8 + this.node.setAttribute('arcsize', Math.max(dx * 100 / w, dy * 100 / h) + '%'); +}; + +/** + * Function: ellipse + * + * Sets the current path to an ellipse. + */ +mxVmlCanvas2D.prototype.ellipse = function(x, y, w, h) +{ + this.node = this.createRect('oval', x, y, w, h); +}; + +/** + * Function: image + * + * Paints an image. + */ +mxVmlCanvas2D.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV) +{ + var node = null; + + if (!aspect) + { + node = this.createRect('image', x, y, w, h); + node.src = src; + } + else + { + // Uses fill with aspect to avoid asynchronous update of size + node = this.createRect('rect', x, y, w, h); + node.stroked = 'false'; + + // Handles image aspect via fill + var fill = this.createVmlElement('fill'); + fill.aspect = (aspect) ? 'atmost' : 'ignore'; + fill.rotate = 'true'; + fill.type = 'frame'; + fill.src = src; + + node.appendChild(fill); + } + + if (flipH && flipV) + { + node.style.rotation = '180'; + } + else if (flipH) + { + node.style.flip = 'x'; + } + else if (flipV) + { + node.style.flip = 'y'; + } + + if (this.state.alpha < 1 || this.state.fillAlpha < 1) + { + // KNOWN: Borders around transparent images in IE<9. Using fill.opacity + // fixes this problem by adding a white background in all IE versions. + node.style.filter += 'alpha(opacity=' + (this.state.alpha * this.state.fillAlpha * 100) + ')'; + } + + this.root.appendChild(node); +}; + +/** + * Function: createText + * + * Creates the innermost element that contains the HTML text. + */ +mxVmlCanvas2D.prototype.createDiv = function(str, align, valign, overflow) +{ + var div = this.createElement('div'); + var state = this.state; + + var css = ''; + + if (state.fontBackgroundColor != null) + { + css += 'background-color:' + state.fontBackgroundColor + ';'; + } + + if (state.fontBorderColor != null) + { + css += 'border:1px solid ' + state.fontBorderColor + ';'; + } + + if (mxUtils.isNode(str)) + { + div.appendChild(str); + } + else + { + if (overflow != 'fill' && overflow != 'width') + { + var div2 = this.createElement('div'); + div2.style.cssText = css; + div2.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; + div2.style.zoom = '1'; + div2.style.textDecoration = 'inherit'; + div2.innerHTML = str; + div.appendChild(div2); + } + else + { + div.style.cssText = css; + div.innerHTML = str; + } + } + + var style = div.style; + + style.fontSize = (state.fontSize / this.vmlScale) + 'px'; + style.fontFamily = state.fontFamily; + style.color = state.fontColor; + style.verticalAlign = 'top'; + style.textAlign = align || 'left'; + style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (state.fontSize * mxConstants.LINE_HEIGHT / this.vmlScale) + 'px' : mxConstants.LINE_HEIGHT; + + if ((state.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + style.fontWeight = 'bold'; + } + + if ((state.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + style.fontStyle = 'italic'; + } + + if ((state.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) + { + style.textDecoration = 'underline'; + } + + return div; +}; + +/** + * Function: text + * + * Paints the given text. Possible values for format are empty string for plain + * text and html for HTML markup. Clipping, text background and border are not + * supported for plain text in VML. + */ +mxVmlCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir) +{ + if (this.textEnabled && str != null) + { + var s = this.state; + + if (format == 'html') + { + if (s.rotation != null) + { + var pt = this.rotatePoint(x, y, s.rotation, s.rotationCx, s.rotationCy); + + x = pt.x; + y = pt.y; + } + + if (document.documentMode == 8 && !mxClient.IS_EM) + { + x += s.dx; + y += s.dy; + + // Workaround for rendering offsets + if (overflow != 'fill' && valign == mxConstants.ALIGN_TOP) + { + y -= 1; + } + } + else + { + x *= s.scale; + y *= s.scale; + } + + // Adds event transparency in IE8 standards without the transparent background + // filter which cannot be used due to bugs in the zoomed bounding box (too slow) + // FIXME: No event transparency if inside v:rect (ie part of shape) + // KNOWN: Offset wrong for rotated text with word that are longer than the wrapping + // width in IE8 because real width of text cannot be determined here. + // This should be fixed in mxText.updateBoundingBox by calling before this and + // passing the real width to this method if not clipped and wrapped. + var abs = (document.documentMode == 8 && !mxClient.IS_EM) ? this.createVmlElement('group') : this.createElement('div'); + abs.style.position = 'absolute'; + abs.style.display = 'inline'; + abs.style.left = this.format(x) + 'px'; + abs.style.top = this.format(y) + 'px'; + abs.style.zoom = s.scale; + + var box = this.createElement('div'); + box.style.position = 'relative'; + box.style.display = 'inline'; + + var margin = mxUtils.getAlignmentAsPoint(align, valign); + var dx = margin.x; + var dy = margin.y; + + var div = this.createDiv(str, align, valign, overflow); + var inner = this.createElement('div'); + + if (dir != null) + { + div.setAttribute('dir', dir); + } + + if (wrap && w > 0) + { + if (!clip) + { + div.style.width = Math.round(w) + 'px'; + } + + div.style.wordWrap = mxConstants.WORD_WRAP; + div.style.whiteSpace = 'normal'; + + // LATER: Check if other cases need to be handled + if (div.style.wordWrap == 'break-word') + { + var tmp = div; + + if (tmp.firstChild != null && tmp.firstChild.nodeName == 'DIV') + { + tmp.firstChild.style.width = '100%'; + } + } + } + else + { + div.style.whiteSpace = 'nowrap'; + } + + var rot = s.rotation + (rotation || 0); + + if (this.rotateHtml && rot != 0) + { + inner.style.display = 'inline'; + inner.style.zoom = '1'; + inner.appendChild(div); + + // Box not needed for rendering in IE8 standards + if (document.documentMode == 8 && !mxClient.IS_EM && this.root.nodeName != 'DIV') + { + box.appendChild(inner); + abs.appendChild(box); + } + else + { + abs.appendChild(inner); + } + } + else if (document.documentMode == 8 && !mxClient.IS_EM) + { + box.appendChild(div); + abs.appendChild(box); + } + else + { + div.style.display = 'inline'; + abs.appendChild(div); + } + + // Inserts the node into the DOM + if (this.root.nodeName != 'DIV') + { + // Rectangle to fix position in group + var rect = this.createVmlElement('rect'); + rect.stroked = 'false'; + rect.filled = 'false'; + + rect.appendChild(abs); + this.root.appendChild(rect); + } + else + { + this.root.appendChild(abs); + } + + if (clip) + { + div.style.overflow = 'hidden'; + div.style.width = Math.round(w) + 'px'; + + if (!mxClient.IS_QUIRKS) + { + div.style.maxHeight = Math.round(h) + 'px'; + } + } + else if (overflow == 'fill') + { + // KNOWN: Affects horizontal alignment in quirks + // but fill should only be used with align=left + div.style.overflow = 'hidden'; + div.style.width = (Math.max(0, w) + 1) + 'px'; + div.style.height = (Math.max(0, h) + 1) + 'px'; + } + else if (overflow == 'width') + { + // KNOWN: Affects horizontal alignment in quirks + // but fill should only be used with align=left + div.style.overflow = 'hidden'; + div.style.width = (Math.max(0, w) + 1) + 'px'; + div.style.maxHeight = (Math.max(0, h) + 1) + 'px'; + } + + if (this.rotateHtml && rot != 0) + { + var rad = rot * (Math.PI / 180); + + // Precalculate cos and sin for the rotation + var real_cos = parseFloat(parseFloat(Math.cos(rad)).toFixed(8)); + var real_sin = parseFloat(parseFloat(Math.sin(-rad)).toFixed(8)); + + rad %= 2 * Math.PI; + if (rad < 0) rad += 2 * Math.PI; + rad %= Math.PI; + if (rad > Math.PI / 2) rad = Math.PI - rad; + + var cos = Math.cos(rad); + var sin = Math.sin(rad); + + // Adds div to document to measure size + if (document.documentMode == 8 && !mxClient.IS_EM) + { + div.style.display = 'inline-block'; + inner.style.display = 'inline-block'; + box.style.display = 'inline-block'; + } + + div.style.visibility = 'hidden'; + div.style.position = 'absolute'; + document.body.appendChild(div); + + var sizeDiv = div; + + if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV') + { + sizeDiv = sizeDiv.firstChild; + } + + var tmp = sizeDiv.offsetWidth + 3; + var oh = sizeDiv.offsetHeight; + + if (clip) + { + w = Math.min(w, tmp); + oh = Math.min(oh, h); + } + else + { + w = tmp; + } + + // Handles words that are longer than the given wrapping width + if (wrap) + { + div.style.width = w + 'px'; + } + + // Simulates max-height in quirks + if (mxClient.IS_QUIRKS && (clip || overflow == 'width') && oh > h) + { + oh = h; + + // Quirks does not support maxHeight + div.style.height = oh + 'px'; + } + + h = oh; + + var top_fix = (h - h * cos + w * -sin) / 2 - real_sin * w * (dx + 0.5) + real_cos * h * (dy + 0.5); + var left_fix = (w - w * cos + h * -sin) / 2 + real_cos * w * (dx + 0.5) + real_sin * h * (dy + 0.5); + + if (abs.nodeName == 'group' && this.root.nodeName == 'DIV') + { + // Workaround for bug where group gets moved away if left and top are non-zero in IE8 standards + var pos = this.createElement('div'); + pos.style.display = 'inline-block'; + pos.style.position = 'absolute'; + pos.style.left = this.format(x + (left_fix - w / 2) * s.scale) + 'px'; + pos.style.top = this.format(y + (top_fix - h / 2) * s.scale) + 'px'; + + abs.parentNode.appendChild(pos); + pos.appendChild(abs); + } + else + { + var sc = (document.documentMode == 8 && !mxClient.IS_EM) ? 1 : s.scale; + + abs.style.left = this.format(x + (left_fix - w / 2) * sc) + 'px'; + abs.style.top = this.format(y + (top_fix - h / 2) * sc) + 'px'; + } + + // KNOWN: Rotated text rendering quality is bad for IE9 quirks + inner.style.filter = "progid:DXImageTransform.Microsoft.Matrix(M11="+real_cos+", M12="+ + real_sin+", M21="+(-real_sin)+", M22="+real_cos+", sizingMethod='auto expand')"; + inner.style.backgroundColor = this.rotatedHtmlBackground; + + if (this.state.alpha < 1) + { + inner.style.filter += 'alpha(opacity=' + (this.state.alpha * 100) + ')'; + } + + // Restore parent node for DIV + inner.appendChild(div); + div.style.position = ''; + div.style.visibility = ''; + } + else if (document.documentMode != 8 || mxClient.IS_EM) + { + div.style.verticalAlign = 'top'; + + if (this.state.alpha < 1) + { + abs.style.filter = 'alpha(opacity=' + (this.state.alpha * 100) + ')'; + } + + // Adds div to document to measure size + var divParent = div.parentNode; + div.style.visibility = 'hidden'; + document.body.appendChild(div); + + w = div.offsetWidth; + var oh = div.offsetHeight; + + // Simulates max-height in quirks + if (mxClient.IS_QUIRKS && clip && oh > h) + { + oh = h; + + // Quirks does not support maxHeight + div.style.height = oh + 'px'; + } + + h = oh; + + div.style.visibility = ''; + divParent.appendChild(div); + + abs.style.left = this.format(x + w * dx * this.state.scale) + 'px'; + abs.style.top = this.format(y + h * dy * this.state.scale) + 'px'; + } + else + { + if (this.state.alpha < 1) + { + div.style.filter = 'alpha(opacity=' + (this.state.alpha * 100) + ')'; + } + + // Faster rendering in IE8 without offsetWidth/Height + box.style.left = (dx * 100) + '%'; + box.style.top = (dy * 100) + '%'; + } + } + else + { + this.plainText(x, y, w, h, mxUtils.htmlEntities(str, false), align, valign, wrap, format, overflow, clip, rotation, dir); + } + } +}; + +/** + * Function: plainText + * + * Paints the outline of the current path. + */ +mxVmlCanvas2D.prototype.plainText = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir) +{ + // TextDirection is ignored since this code is not used (format is always HTML in the text function) + var s = this.state; + x = (x + s.dx) * s.scale; + y = (y + s.dy) * s.scale; + + var node = this.createVmlElement('shape'); + node.style.width = '1px'; + node.style.height = '1px'; + node.stroked = 'false'; + + var fill = this.createVmlElement('fill'); + fill.color = s.fontColor; + fill.opacity = (s.alpha * 100) + '%'; + node.appendChild(fill); + + var path = this.createVmlElement('path'); + path.textpathok = 'true'; + path.v = 'm ' + this.format(0) + ' ' + this.format(0) + ' l ' + this.format(1) + ' ' + this.format(0); + + node.appendChild(path); + + // KNOWN: Font family and text decoration ignored + var tp = this.createVmlElement('textpath'); + tp.style.cssText = 'v-text-align:' + align; + tp.style.align = align; + tp.style.fontFamily = s.fontFamily; + tp.string = str; + tp.on = 'true'; + + // Scale via fontsize instead of node.style.zoom for correct offsets in IE8 + var size = s.fontSize * s.scale / this.vmlScale; + tp.style.fontSize = size + 'px'; + + // Bold + if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) + { + tp.style.fontWeight = 'bold'; + } + + // Italic + if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) + { + tp.style.fontStyle = 'italic'; + } + + // Underline + if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) + { + tp.style.textDecoration = 'underline'; + } + + var lines = str.split('\n'); + var textHeight = size + (lines.length - 1) * size * mxConstants.LINE_HEIGHT; + var dx = 0; + var dy = 0; + + if (valign == mxConstants.ALIGN_BOTTOM) + { + dy = - textHeight / 2; + } + else if (valign != mxConstants.ALIGN_MIDDLE) // top + { + dy = textHeight / 2; + } + + if (rotation != null) + { + node.style.rotation = rotation; + var rad = rotation * (Math.PI / 180); + dx = Math.sin(rad) * dy; + dy = Math.cos(rad) * dy; + } + + // FIXME: Clipping is relative to bounding box + /*if (clip) + { + node.style.clip = 'rect(0px ' + this.format(w) + 'px ' + this.format(h) + 'px 0px)'; + }*/ + + node.appendChild(tp); + node.style.left = this.format(x - dx) + 'px'; + node.style.top = this.format(y + dy) + 'px'; + + this.root.appendChild(node); +}; + +/** + * Function: stroke + * + * Paints the outline of the current path. + */ +mxVmlCanvas2D.prototype.stroke = function() +{ + this.addNode(false, true); +}; + +/** + * Function: fill + * + * Fills the current path. + */ +mxVmlCanvas2D.prototype.fill = function() +{ + this.addNode(true, false); +}; + +/** + * Function: fillAndStroke + * + * Fills and paints the outline of the current path. + */ +mxVmlCanvas2D.prototype.fillAndStroke = function() +{ + this.addNode(true, true); +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxGuide + * + * Implements the alignment of selection cells to other cells in the graph. + * + * Constructor: mxGuide + * + * Constructs a new guide object. + */ +function mxGuide(graph, states) +{ + this.graph = graph; + this.setStates(states); +}; + +/** + * Variable: graph + * + * Reference to the enclosing instance. + */ +mxGuide.prototype.graph = null; + +/** + * Variable: states + * + * Contains the that are used for alignment. + */ +mxGuide.prototype.states = null; + +/** + * Variable: horizontal + * + * Specifies if horizontal guides are enabled. Default is true. + */ +mxGuide.prototype.horizontal = true; + +/** + * Variable: vertical + * + * Specifies if vertical guides are enabled. Default is true. + */ +mxGuide.prototype.vertical = true; + +/** + * Variable: vertical + * + * Holds the for the horizontal guide. + */ +mxGuide.prototype.guideX = null; + +/** + * Variable: vertical + * + * Holds the for the vertical guide. + */ +mxGuide.prototype.guideY = null; + +/** + * Function: setStates + * + * Sets the that should be used for alignment. + */ +mxGuide.prototype.setStates = function(states) +{ + this.states = states; +}; + +/** + * Function: isEnabledForEvent + * + * Returns true if the guide should be enabled for the given native event. This + * implementation always returns true. + */ +mxGuide.prototype.isEnabledForEvent = function(evt) +{ + return true; +}; + +/** + * Function: getGuideTolerance + * + * Returns the tolerance for the guides. Default value is gridSize / 2. + */ +mxGuide.prototype.getGuideTolerance = function() +{ + return this.graph.gridSize / 2; +}; + +/** + * Function: createGuideShape + * + * Returns the mxShape to be used for painting the respective guide. This + * implementation returns a new, dashed and crisp using + * and as the format. + * + * Parameters: + * + * horizontal - Boolean that specifies which guide should be created. + */ +mxGuide.prototype.createGuideShape = function(horizontal) +{ + var guide = new mxPolyline([], mxConstants.GUIDE_COLOR, mxConstants.GUIDE_STROKEWIDTH); + guide.isDashed = true; + + return guide; +}; + +/** + * Function: move + * + * Moves the by the given and returnt the snapped point. + */ +mxGuide.prototype.move = function(bounds, delta, gridEnabled) +{ + if (this.states != null && (this.horizontal || this.vertical) && bounds != null && delta != null) + { + var trx = this.graph.getView().translate; + var scale = this.graph.getView().scale; + var dx = delta.x; + var dy = delta.y; + + var overrideX = false; + var stateX = null; + var valueX = null; + var overrideY = false; + var stateY = null; + var valueY = null; + + var tt = this.getGuideTolerance(); + var ttX = tt; + var ttY = tt; + + var b = bounds.clone(); + b.x += delta.x; + b.y += delta.y; + + var left = b.x; + var right = b.x + b.width; + var center = b.getCenterX(); + var top = b.y; + var bottom = b.y + b.height; + var middle = b.getCenterY(); + + // Snaps the left, center and right to the given x-coordinate + function snapX(x, state) + { + x += this.graph.panDx; + var override = false; + + if (Math.abs(x - center) < ttX) + { + dx = x - bounds.getCenterX(); + ttX = Math.abs(x - center); + override = true; + } + else if (Math.abs(x - left) < ttX) + { + dx = x - bounds.x; + ttX = Math.abs(x - left); + override = true; + } + else if (Math.abs(x - right) < ttX) + { + dx = x - bounds.x - bounds.width; + ttX = Math.abs(x - right); + override = true; + } + + if (override) + { + stateX = state; + valueX = Math.round(x - this.graph.panDx); + + if (this.guideX == null) + { + this.guideX = this.createGuideShape(true); + + // Makes sure to use either VML or SVG shapes in order to implement + // event-transparency on the background area of the rectangle since + // HTML shapes do not let mouseevents through even when transparent + this.guideX.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.guideX.pointerEvents = false; + this.guideX.init(this.graph.getView().getOverlayPane()); + } + } + + overrideX = overrideX || override; + }; + + // Snaps the top, middle or bottom to the given y-coordinate + function snapY(y) + { + y += this.graph.panDy; + var override = false; + + if (Math.abs(y - middle) < ttY) + { + dy = y - bounds.getCenterY(); + ttY = Math.abs(y - middle); + override = true; + } + else if (Math.abs(y - top) < ttY) + { + dy = y - bounds.y; + ttY = Math.abs(y - top); + override = true; + } + else if (Math.abs(y - bottom) < ttY) + { + dy = y - bounds.y - bounds.height; + ttY = Math.abs(y - bottom); + override = true; + } + + if (override) + { + stateY = state; + valueY = Math.round(y - this.graph.panDy); + + if (this.guideY == null) + { + this.guideY = this.createGuideShape(false); + + // Makes sure to use either VML or SVG shapes in order to implement + // event-transparency on the background area of the rectangle since + // HTML shapes do not let mouseevents through even when transparent + this.guideY.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.guideY.pointerEvents = false; + this.guideY.init(this.graph.getView().getOverlayPane()); + } + } + + overrideY = overrideY || override; + }; + + for (var i = 0; i < this.states.length; i++) + { + var state = this.states[i]; + + if (state != null) + { + // Align x + if (this.horizontal) + { + snapX.call(this, state.getCenterX(), state); + snapX.call(this, state.x, state); + snapX.call(this, state.x + state.width, state); + } + + // Align y + if (this.vertical) + { + snapY.call(this, state.getCenterY(), state); + snapY.call(this, state.y, state); + snapY.call(this, state.y + state.height, state); + } + } + } + + // Moves cells that are off-grid back to the grid on move + if (gridEnabled) + { + if (!overrideX) + { + var tx = bounds.x - (this.graph.snap(bounds.x / + scale - trx.x) + trx.x) * scale; + dx = this.graph.snap(dx / scale) * scale - tx; + } + + if (!overrideY) + { + var ty = bounds.y - (this.graph.snap(bounds.y / + scale - trx.y) + trx.y) * scale; + dy = this.graph.snap(dy / scale) * scale - ty; + } + } + + // Redraws the guides + var c = this.graph.container; + + if (!overrideX && this.guideX != null) + { + this.guideX.node.style.visibility = 'hidden'; + } + else if (this.guideX != null) + { + if (stateX != null && bounds != null) + { + minY = Math.min(bounds.y + dy - this.graph.panDy, stateX.y); + maxY = Math.max(bounds.y + bounds.height + dy - this.graph.panDy, stateX.y + stateX.height); + } + + if (minY != null && maxY != null) + { + this.guideX.points = [new mxPoint(valueX, minY), new mxPoint(valueX, maxY)]; + } + else + { + this.guideX.points = [new mxPoint(valueX, -this.graph.panDy), new mxPoint(valueX, c.scrollHeight - 3 - this.graph.panDy)]; + } + + this.guideX.stroke = this.getGuideColor(stateX, true); + this.guideX.node.style.visibility = 'visible'; + this.guideX.redraw(); + } + + if (!overrideY && this.guideY != null) + { + this.guideY.node.style.visibility = 'hidden'; + } + else if (this.guideY != null) + { + if (stateY != null && bounds != null) + { + minX = Math.min(bounds.x + dx - this.graph.panDx, stateY.x); + maxX = Math.max(bounds.x + bounds.width + dx - this.graph.panDx, stateY.x + stateY.width); + } + + if (minX != null && maxX != null) + { + this.guideY.points = [new mxPoint(minX, valueY), new mxPoint(maxX, valueY)]; + } + else + { + this.guideY.points = [new mxPoint(-this.graph.panDx, valueY), new mxPoint(c.scrollWidth - 3 - this.graph.panDx, valueY)]; + } + + this.guideY.stroke = this.getGuideColor(stateY, false); + this.guideY.node.style.visibility = 'visible'; + this.guideY.redraw(); + } + + delta = new mxPoint(dx, dy); + } + + return delta; +}; + +/** + * Function: hide + * + * Hides all current guides. + */ +mxGuide.prototype.getGuideColor = function(state, horizontal) +{ + return mxConstants.GUIDE_COLOR; +}; + +/** + * Function: hide + * + * Hides all current guides. + */ +mxGuide.prototype.hide = function() +{ + this.setVisible(false); +}; + +/** + * Function: setVisible + * + * Shows or hides the current guides. + */ +mxGuide.prototype.setVisible = function(visible) +{ + if (this.guideX != null) + { + this.guideX.node.style.visibility = (visible) ? 'visible' : 'hidden'; + } + + if (this.guideY != null) + { + this.guideY.node.style.visibility = (visible) ? 'visible' : 'hidden'; + } +}; + +/** + * Function: destroy + * + * Destroys all resources that this object uses. + */ +mxGuide.prototype.destroy = function() +{ + if (this.guideX != null) + { + this.guideX.destroy(); + this.guideX = null; + } + + if (this.guideY != null) + { + this.guideY.destroy(); + this.guideY = null; + } +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxStencil + * + * Implements a generic shape which is based on a XML node as a description. + * + * shape: + * + * The outer element is *shape*, that has attributes: + * + * - "name", string, required. The stencil name that uniquely identifies the shape. + * - "w" and "h" are optional decimal view bounds. This defines your co-ordinate + * system for the graphics operations in the shape. The default is 100,100. + * - "aspect", optional string. Either "variable", the default, or "fixed". Fixed + * means always render the shape with the aspect ratio defined by the ratio w/h. + * Variable causes the ratio to match that of the geometry of the current vertex. + * - "strokewidth", optional string. Either an integer or the string "inherit". + * "inherit" indicates that the strokeWidth of the cell is only changed on scaling, + * not on resizing. Default is "1". + * If numeric values are used, the strokeWidth of the cell is changed on both + * scaling and resizing and the value defines the multiple that is applied to + * the width. + * + * connections: + * + * If you want to define specific fixed connection points on the shape use the + * *connections* element. Each *constraint* element within connections defines + * a fixed connection point on the shape. Constraints have attributes: + * + * - "perimeter", required. 1 or 0. 0 sets the connection point where specified + * by x,y. 1 Causes the position of the connection point to be extrapolated from + * the center of the shape, through x,y to the point of intersection with the + * perimeter of the shape. + * - "x" and "y" are the position of the fixed point relative to the bounds of + * the shape. They can be automatically adjusted if perimeter=1. So, (0,0) is top + * left, (0.5,0.5) the center, (1,0.5) the center of the right hand edge of the + * bounds, etc. Values may be less than 0 or greater than 1 to be positioned + * outside of the shape. + * - "name", optional string. A unique identifier for the port on the shape. + * + * background and foreground: + * + * The path of the graphics drawing is split into two elements, *foreground* and + * *background*. The split is to define which part any shadow applied to the shape + * is derived from (the background). This, generally, means the background is the + * line tracing of the outside of the shape, but not always. + * + * Any stroke, fill or fillstroke of a background must be the first element of the + * foreground element, they must not be used within *background*. If the background + * is empty, this is not required. + * + * Because the background cannot have any fill or stroke, it can contain only one + * *path*, *rect*, *roundrect* or *ellipse* element (or none). It can also not + * include *image*, *text* or *include-shape*. + * + * Note that the state, styling and drawing in mxGraph stencils is very close in + * design to that of HTML 5 canvas. Tutorials on this subject, if you're not + * familiar with the topic, will give a good high-level introduction to the + * concepts used. + * + * State: + * + * Rendering within the foreground and background elements has the concept of + * state. There are two types of operations other than state save/load, styling + * and drawing. The styling operations change the current state, so you can save + * the current state with and pull the last saved state from the state + * stack using . + * + * Styling: + * + * The elements that change colors within the current state all take a hash + * prefixed hex color code ("#FFEA80"). + * + * - *strokecolor*, this sets the color that drawing paths will be rendered in + * when a stroke or fillstroke command is issued. + * - *fillcolor*, this sets the color that the inside of closed paths will be + * rendered in when a fill or fillstroke command is issued. + * - *fontcolor*, this sets the color that fonts are rendered in when text is drawn. + * + * *alpha* defines the degree of transparency used between 1.0 for fully opaque + * and 0.0 for fully transparent. + * + * *strokewidth* defines the integer thickness of drawing elements rendered by + * stroking. Use fixed="1" to apply the value as-is, without scaling. + * + * *dashed* is "1" for dashing enabled and "0" for disabled. + * + * When *dashed* is enabled the current dash pattern, defined by *dashpattern*, + * is used on strokes. dashpattern is a sequence of space separated "on, off" + * lengths that define what distance to paint the stroke for, then what distance + * to paint nothing for, repeat... The default is "3 3". You could define a more + * complex pattern with "5 3 2 6", for example. Generally, it makes sense to have + * an even number of elements in the dashpattern, but that's not required. + * + * *linejoin*, *linecap* and *miterlimit* are best explained by the Mozilla page + * on Canvas styling (about halfway down). The values are all the same except we + * use "flat" for linecap, instead of Canvas' "butt". + * + * For font styling there are. + * + * - *fontsize*, an integer, + * - *fontstyle*, an ORed bit pattern of bold (1), italic (2) and underline (4), + * i.e bold underline is "5". + * - *fontfamily*, is a string defining the typeface to be used. + * + * Drawing: + * + * Most drawing is contained within a *path* element. Again, the graphic + * primitives are very similar to that of HTML 5 canvas. + * + * - *move* to attributes required decimals (x,y). + * - *line* to attributes required decimals (x,y). + * - *quad* to required decimals (x2,y2) via control point required decimals + * (x1,y1). + * - *curve* to required decimals (x3,y3), via control points required decimals + * (x1,y1) and (x2,y2). + * - *arc*, this doesn't follow the HTML Canvas signatures, instead it's a copy + * of the SVG arc command. The SVG specification documentation gives the best + * description of its behaviors. The attributes are named identically, they are + * decimals and all required. + * - *close* ends the current subpath and causes an automatic straight line to + * be drawn from the current point to the initial point of the current subpath. + * + * Complex drawing: + * + * In addition to the graphics primitive operations there are non-primitive + * operations. These provide an easy method to draw some basic shapes. + * + * - *rect*, attributes "x", "y", "w", "h", all required decimals + * - *roundrect*, attributes "x", "y", "w", "h", all required decimals. Also + * "arcsize" an optional decimal attribute defining how large, the corner curves + * are. + * - *ellipse*, attributes "x", "y", "w", "h", all required decimals. + * + * Note that these 3 shapes and all paths must be followed by either a fill, + * stroke, or fillstroke. + * + * Text: + * + * *text* elements have the following attributes. + * + * - "str", the text string to display, required. + * - "x" and "y", the decimal location (x,y) of the text element, required. + * - "align", the horizontal alignment of the text element, either "left", + * "center" or "right". Optional, default is "left". + * - "valign", the vertical alignment of the text element, either "top", "middle" + * or "bottom". Optional, default is "top". + * - "localized", 0 or 1, if 1 then the "str" actually contains a key to use to + * fetch the value out of mxResources. Optional, default is + * . + * - "vertical", 0 or 1, if 1 the label is rendered vertically (rotated by 90 + * degrees). Optional, default is 0. + * - "rotation", angle in degrees (0 to 360). The angle to rotate the text by. + * Optional, default is 0. + * - "align-shape", 0 or 1, if 0 ignore the rotation of the shape when setting + * the text rotation. Optional, default is 1. + * + * If is true, then the text content of the this element can define + * a function which is invoked with the shape as the only argument and returns + * the value for the text element (ignored if the str attribute is not null). + * + * Images: + * + * *image* elements can either be external URLs, or data URIs, where supported + * (not in IE 7-). Attributes are: + * + * - "src", required string. Either a data URI or URL. + * - "x", "y", required decimals. The (x,y) position of the image. + * - "w", "h", required decimals. The width and height of the image. + * - "flipH" and "flipV", optional 0 or 1. Whether to flip the image along the + * horizontal/vertical axis. Default is 0 for both. + * + * If is true, then the text content of the this element can define + * a function which is invoked with the shape as the only argument and returns + * the value for the image source (ignored if the src attribute is not null). + * + * Sub-shapes: + * + * *include-shape* allow stencils to be rendered within the current stencil by + * referencing the sub-stencil by name. Attributes are: + * + * - "name", required string. The unique shape name of the stencil. + * - "x", "y", "w", "h", required decimals. The (x,y) position of the sub-shape + * and its width and height. + * + * Constructor: mxStencil + * + * Constructs a new generic shape by setting to the given XML node and + * invoking and . + * + * Parameters: + * + * desc - XML node that contains the stencil description. + */ +function mxStencil(desc) +{ + this.desc = desc; + this.parseDescription(); + this.parseConstraints(); +}; + +/** + * Variable: defaultLocalized + * + * Static global variable that specifies the default value for the localized + * attribute of the text element. Default is false. + */ +mxStencil.defaultLocalized = false; + +/** + * Function: allowEval + * + * Static global switch that specifies if the use of eval is allowed for + * evaluating text content and images. Default is false. Set this to true + * if stencils can not contain user input. + */ +mxStencil.allowEval = false; + +/** + * Variable: desc + * + * Holds the XML node with the stencil description. + */ +mxStencil.prototype.desc = null; + +/** + * Variable: constraints + * + * Holds an array of as defined in the shape. + */ +mxStencil.prototype.constraints = null; + +/** + * Variable: aspect + * + * Holds the aspect of the shape. Default is 'auto'. + */ +mxStencil.prototype.aspect = null; + +/** + * Variable: w0 + * + * Holds the width of the shape. Default is 100. + */ +mxStencil.prototype.w0 = null; + +/** + * Variable: h0 + * + * Holds the height of the shape. Default is 100. + */ +mxStencil.prototype.h0 = null; + +/** + * Variable: bgNodes + * + * Holds the XML node with the stencil description. + */ +mxStencil.prototype.bgNode = null; + +/** + * Variable: fgNodes + * + * Holds the XML node with the stencil description. + */ +mxStencil.prototype.fgNode = null; + +/** + * Variable: strokewidth + * + * Holds the strokewidth direction from the description. + */ +mxStencil.prototype.strokewidth = null; + +/** + * Function: parseDescription + * + * Reads , , , and from . + */ +mxStencil.prototype.parseDescription = function() +{ + // LATER: Preprocess nodes for faster painting + this.fgNode = this.desc.getElementsByTagName('foreground')[0]; + this.bgNode = this.desc.getElementsByTagName('background')[0]; + this.w0 = Number(this.desc.getAttribute('w') || 100); + this.h0 = Number(this.desc.getAttribute('h') || 100); + + // Possible values for aspect are: variable and fixed where + // variable means fill the available space and fixed means + // use w0 and h0 to compute the aspect. + var aspect = this.desc.getAttribute('aspect'); + this.aspect = (aspect != null) ? aspect : 'variable'; + + // Possible values for strokewidth are all numbers and "inherit" + // where the inherit means take the value from the style (ie. the + // user-defined stroke-width). Note that the strokewidth is scaled + // by the minimum scaling that is used to draw the shape (sx, sy). + var sw = this.desc.getAttribute('strokewidth'); + this.strokewidth = (sw != null) ? sw : '1'; +}; + +/** + * Function: parseConstraints + * + * Reads the constraints from into using + * . + */ +mxStencil.prototype.parseConstraints = function() +{ + var conns = this.desc.getElementsByTagName('connections')[0]; + + if (conns != null) + { + var tmp = mxUtils.getChildNodes(conns); + + if (tmp != null && tmp.length > 0) + { + this.constraints = []; + + for (var i = 0; i < tmp.length; i++) + { + this.constraints.push(this.parseConstraint(tmp[i])); + } + } + } +}; + +/** + * Function: parseConstraint + * + * Parses the given XML node and returns its . + */ +mxStencil.prototype.parseConstraint = function(node) +{ + var x = Number(node.getAttribute('x')); + var y = Number(node.getAttribute('y')); + var perimeter = node.getAttribute('perimeter') == '1'; + var name = node.getAttribute('name'); + + return new mxConnectionConstraint(new mxPoint(x, y), perimeter, name); +}; + +/** + * Function: evaluateTextAttribute + * + * Gets the given attribute as a text. The return value from + * is used as a key to if the localized attribute in the text + * node is 1 or if is true. + */ +mxStencil.prototype.evaluateTextAttribute = function(node, attribute, shape) +{ + var result = this.evaluateAttribute(node, attribute, shape); + var loc = node.getAttribute('localized'); + + if ((mxStencil.defaultLocalized && loc == null) || loc == '1') + { + result = mxResources.get(result); + } + + return result; +}; + +/** + * Function: evaluateAttribute + * + * Gets the attribute for the given name from the given node. If the attribute + * does not exist then the text content of the node is evaluated and if it is + * a function it is invoked with as the only argument and the return + * value is used as the attribute value to be returned. + */ +mxStencil.prototype.evaluateAttribute = function(node, attribute, shape) +{ + var result = node.getAttribute(attribute); + + if (result == null) + { + var text = mxUtils.getTextContent(node); + + if (text != null && mxStencil.allowEval) + { + var funct = mxUtils.eval(text); + + if (typeof(funct) == 'function') + { + result = funct(shape); + } + } + } + + return result; +}; + +/** + * Function: drawShape + * + * Draws this stencil inside the given bounds. + */ +mxStencil.prototype.drawShape = function(canvas, shape, x, y, w, h) +{ + // TODO: Internal structure (array of special structs?), relative and absolute + // coordinates (eg. note shape, process vs star, actor etc.), text rendering + // and non-proportional scaling, how to implement pluggable edge shapes + // (start, segment, end blocks), pluggable markers, how to implement + // swimlanes (title area) with this API, add icon, horizontal/vertical + // label, indicator for all shapes, rotation + var direction = mxUtils.getValue(shape.style, mxConstants.STYLE_DIRECTION, null); + var aspect = this.computeAspect(shape.style, x, y, w, h, direction); + var minScale = Math.min(aspect.width, aspect.height); + var sw = (this.strokewidth == 'inherit') ? + Number(mxUtils.getNumber(shape.style, mxConstants.STYLE_STROKEWIDTH, 1)) : + Number(this.strokewidth) * minScale; + canvas.setStrokeWidth(sw); + + this.drawChildren(canvas, shape, x, y, w, h, this.bgNode, aspect, false, true); + this.drawChildren(canvas, shape, x, y, w, h, this.fgNode, aspect, true, + !shape.outline || shape.style == null || mxUtils.getValue( + shape.style, mxConstants.STYLE_BACKGROUND_OUTLINE, 0) == 0); +}; + +/** + * Function: drawChildren + * + * Draws this stencil inside the given bounds. + */ +mxStencil.prototype.drawChildren = function(canvas, shape, x, y, w, h, node, aspect, disableShadow, paint) +{ + if (node != null && w > 0 && h > 0) + { + var tmp = node.firstChild; + + while (tmp != null) + { + if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT) + { + this.drawNode(canvas, shape, tmp, aspect, disableShadow, paint); + } + + tmp = tmp.nextSibling; + } + } +}; + +/** + * Function: computeAspect + * + * Returns a rectangle that contains the offset in x and y and the horizontal + * and vertical scale in width and height used to draw this shape inside the + * given . + * + * Parameters: + * + * shape - to be drawn. + * bounds - that should contain the stencil. + * direction - Optional direction of the shape to be darwn. + */ +mxStencil.prototype.computeAspect = function(shape, x, y, w, h, direction) +{ + var x0 = x; + var y0 = y; + var sx = w / this.w0; + var sy = h / this.h0; + + var inverse = (direction == mxConstants.DIRECTION_NORTH || direction == mxConstants.DIRECTION_SOUTH); + + if (inverse) + { + sy = w / this.h0; + sx = h / this.w0; + + var delta = (w - h) / 2; + + x0 += delta; + y0 -= delta; + } + + if (this.aspect == 'fixed') + { + sy = Math.min(sx, sy); + sx = sy; + + // Centers the shape inside the available space + if (inverse) + { + x0 += (h - this.w0 * sx) / 2; + y0 += (w - this.h0 * sy) / 2; + } + else + { + x0 += (w - this.w0 * sx) / 2; + y0 += (h - this.h0 * sy) / 2; + } + } + + return new mxRectangle(x0, y0, sx, sy); +}; + +/** + * Function: drawNode + * + * Draws this stencil inside the given bounds. + */ +mxStencil.prototype.drawNode = function(canvas, shape, node, aspect, disableShadow, paint) +{ + var name = node.nodeName; + var x0 = aspect.x; + var y0 = aspect.y; + var sx = aspect.width; + var sy = aspect.height; + var minScale = Math.min(sx, sy); + + if (name == 'save') + { + canvas.save(); + } + else if (name == 'restore') + { + canvas.restore(); + } + else if (paint) + { + if (name == 'path') + { + canvas.begin(); + + // Renders the elements inside the given path + var childNode = node.firstChild; + + while (childNode != null) + { + if (childNode.nodeType == mxConstants.NODETYPE_ELEMENT) + { + this.drawNode(canvas, shape, childNode, aspect, disableShadow, paint); + } + + childNode = childNode.nextSibling; + } + } + else if (name == 'close') + { + canvas.close(); + } + else if (name == 'move') + { + canvas.moveTo(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy); + } + else if (name == 'line') + { + canvas.lineTo(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy); + } + else if (name == 'quad') + { + canvas.quadTo(x0 + Number(node.getAttribute('x1')) * sx, + y0 + Number(node.getAttribute('y1')) * sy, + x0 + Number(node.getAttribute('x2')) * sx, + y0 + Number(node.getAttribute('y2')) * sy); + } + else if (name == 'curve') + { + canvas.curveTo(x0 + Number(node.getAttribute('x1')) * sx, + y0 + Number(node.getAttribute('y1')) * sy, + x0 + Number(node.getAttribute('x2')) * sx, + y0 + Number(node.getAttribute('y2')) * sy, + x0 + Number(node.getAttribute('x3')) * sx, + y0 + Number(node.getAttribute('y3')) * sy); + } + else if (name == 'arc') + { + canvas.arcTo(Number(node.getAttribute('rx')) * sx, + Number(node.getAttribute('ry')) * sy, + Number(node.getAttribute('x-axis-rotation')), + Number(node.getAttribute('large-arc-flag')), + Number(node.getAttribute('sweep-flag')), + x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy); + } + else if (name == 'rect') + { + canvas.rect(x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy, + Number(node.getAttribute('w')) * sx, + Number(node.getAttribute('h')) * sy); + } + else if (name == 'roundrect') + { + var arcsize = Number(node.getAttribute('arcsize')); + + if (arcsize == 0) + { + arcsize = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100; + } + + var w = Number(node.getAttribute('w')) * sx; + var h = Number(node.getAttribute('h')) * sy; + var factor = Number(arcsize) / 100; + var r = Math.min(w * factor, h * factor); + + canvas.roundrect(x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy, + w, h, r, r); + } + else if (name == 'ellipse') + { + canvas.ellipse(x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy, + Number(node.getAttribute('w')) * sx, + Number(node.getAttribute('h')) * sy); + } + else if (name == 'image') + { + if (!shape.outline) + { + var src = this.evaluateAttribute(node, 'src', shape); + + canvas.image(x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy, + Number(node.getAttribute('w')) * sx, + Number(node.getAttribute('h')) * sy, + src, false, node.getAttribute('flipH') == '1', + node.getAttribute('flipV') == '1'); + } + } + else if (name == 'text') + { + if (!shape.outline) + { + var str = this.evaluateTextAttribute(node, 'str', shape); + var rotation = node.getAttribute('vertical') == '1' ? -90 : 0; + + if (node.getAttribute('align-shape') == '0') + { + var dr = shape.rotation; + + // Depends on flipping + var flipH = mxUtils.getValue(shape.style, mxConstants.STYLE_FLIPH, 0) == 1; + var flipV = mxUtils.getValue(shape.style, mxConstants.STYLE_FLIPV, 0) == 1; + + if (flipH && flipV) + { + rotation -= dr; + } + else if (flipH || flipV) + { + rotation += dr; + } + else + { + rotation -= dr; + } + } + + rotation -= node.getAttribute('rotation'); + + canvas.text(x0 + Number(node.getAttribute('x')) * sx, + y0 + Number(node.getAttribute('y')) * sy, + 0, 0, str, node.getAttribute('align') || 'left', + node.getAttribute('valign') || 'top', false, '', + null, false, rotation); + } + } + else if (name == 'include-shape') + { + var stencil = mxStencilRegistry.getStencil(node.getAttribute('name')); + + if (stencil != null) + { + var x = x0 + Number(node.getAttribute('x')) * sx; + var y = y0 + Number(node.getAttribute('y')) * sy; + var w = Number(node.getAttribute('w')) * sx; + var h = Number(node.getAttribute('h')) * sy; + + stencil.drawShape(canvas, shape, x, y, w, h); + } + } + else if (name == 'fillstroke') + { + canvas.fillAndStroke(); + } + else if (name == 'fill') + { + canvas.fill(); + } + else if (name == 'stroke') + { + canvas.stroke(); + } + else if (name == 'strokewidth') + { + var s = (node.getAttribute('fixed') == '1') ? 1 : minScale; + canvas.setStrokeWidth(Number(node.getAttribute('width')) * s); + } + else if (name == 'dashed') + { + canvas.setDashed(node.getAttribute('dashed') == '1'); + } + else if (name == 'dashpattern') + { + var value = node.getAttribute('pattern'); + + if (value != null) + { + var tmp = value.split(' '); + var pat = []; + + for (var i = 0; i < tmp.length; i++) + { + if (tmp[i].length > 0) + { + pat.push(Number(tmp[i]) * minScale); + } + } + + value = pat.join(' '); + canvas.setDashPattern(value); + } + } + else if (name == 'strokecolor') + { + canvas.setStrokeColor(node.getAttribute('color')); + } + else if (name == 'linecap') + { + canvas.setLineCap(node.getAttribute('cap')); + } + else if (name == 'linejoin') + { + canvas.setLineJoin(node.getAttribute('join')); + } + else if (name == 'miterlimit') + { + canvas.setMiterLimit(Number(node.getAttribute('limit'))); + } + else if (name == 'fillcolor') + { + canvas.setFillColor(node.getAttribute('color')); + } + else if (name == 'alpha') + { + canvas.setAlpha(node.getAttribute('alpha')); + } + else if (name == 'fontcolor') + { + canvas.setFontColor(node.getAttribute('color')); + } + else if (name == 'fontstyle') + { + canvas.setFontStyle(node.getAttribute('style')); + } + else if (name == 'fontfamily') + { + canvas.setFontFamily(node.getAttribute('family')); + } + else if (name == 'fontsize') + { + canvas.setFontSize(Number(node.getAttribute('size')) * minScale); + } + + if (disableShadow && (name == 'fillstroke' || name == 'fill' || name == 'stroke')) + { + disableShadow = false; + canvas.setShadow(false); + } + } +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxShape + * + * Base class for all shapes. A shape in mxGraph is a + * separate implementation for SVG, VML and HTML. Which + * implementation to use is controlled by the + * property which is assigned from within the + * when the shape is created. The dialect must be assigned + * for a shape, and it does normally depend on the browser and + * the confiuration of the graph (see rendering hint). + * + * For each supported shape in SVG and VML, a corresponding + * shape exists in mxGraph, namely for text, image, rectangle, + * rhombus, ellipse and polyline. The other shapes are a + * combination of these shapes (eg. label and swimlane) + * or they consist of one or more (filled) path objects + * (eg. actor and cylinder). The HTML implementation is + * optional but may be required for a HTML-only view of + * the graph. + * + * Custom Shapes: + * + * To extend from this class, the basic code looks as follows. + * In the special case where the custom shape consists only of + * one filled region or one filled region and an additional stroke + * the and should be subclassed, + * respectively. + * + * (code) + * function CustomShape() { } + * + * CustomShape.prototype = new mxShape(); + * CustomShape.prototype.constructor = CustomShape; + * (end) + * + * To register a custom shape in an existing graph instance, + * one must register the shape under a new name in the graph's + * cell renderer as follows: + * + * (code) + * mxCellRenderer.registerShape('customShape', CustomShape); + * (end) + * + * The second argument is the name of the constructor. + * + * In order to use the shape you can refer to the given name above + * in a stylesheet. For example, to change the shape for the default + * vertex style, the following code is used: + * + * (code) + * var style = graph.getStylesheet().getDefaultVertexStyle(); + * style[mxConstants.STYLE_SHAPE] = 'customShape'; + * (end) + * + * Constructor: mxShape + * + * Constructs a new shape. + */ +function mxShape(stencil) +{ + this.stencil = stencil; + this.initStyles(); +}; + +/** + * Variable: dialect + * + * Holds the dialect in which the shape is to be painted. + * This can be one of the DIALECT constants in . + */ +mxShape.prototype.dialect = null; + +/** + * Variable: scale + * + * Holds the scale in which the shape is being painted. + */ +mxShape.prototype.scale = 1; + +/** + * Variable: antiAlias + * + * Rendering hint for configuring the canvas. + */ +mxShape.prototype.antiAlias = true; + +/** + * Variable: minSvgStrokeWidth + * + * Minimum stroke width for SVG output. + */ +mxShape.prototype.minSvgStrokeWidth = 1; + +/** + * Variable: bounds + * + * Holds the that specifies the bounds of this shape. + */ +mxShape.prototype.bounds = null; + +/** + * Variable: points + * + * Holds the array of that specify the points of this shape. + */ +mxShape.prototype.points = null; + +/** + * Variable: node + * + * Holds the outermost DOM node that represents this shape. + */ +mxShape.prototype.node = null; + +/** + * Variable: state + * + * Optional reference to the corresponding . + */ +mxShape.prototype.state = null; + +/** + * Variable: style + * + * Optional reference to the style of the corresponding . + */ +mxShape.prototype.style = null; + +/** + * Variable: boundingBox + * + * Contains the bounding box of the shape, that is, the smallest rectangle + * that includes all pixels of the shape. + */ +mxShape.prototype.boundingBox = null; + +/** + * Variable: stencil + * + * Holds the that defines the shape. + */ +mxShape.prototype.stencil = null; + +/** + * Variable: svgStrokeTolerance + * + * Event-tolerance for SVG strokes (in px). Default is 8. This is only passed + * to the canvas in if is true. + */ +mxShape.prototype.svgStrokeTolerance = 8; + +/** + * Variable: pointerEvents + * + * Specifies if pointer events should be handled. Default is true. + */ +mxShape.prototype.pointerEvents = true; + +/** + * Variable: svgPointerEvents + * + * Specifies if pointer events should be handled. Default is true. + */ +mxShape.prototype.svgPointerEvents = 'all'; + +/** + * Variable: shapePointerEvents + * + * Specifies if pointer events outside of shape should be handled. Default + * is false. + */ +mxShape.prototype.shapePointerEvents = false; + +/** + * Variable: stencilPointerEvents + * + * Specifies if pointer events outside of stencils should be handled. Default + * is false. Set this to true for backwards compatibility with the 1.x branch. + */ +mxShape.prototype.stencilPointerEvents = false; + +/** + * Variable: vmlScale + * + * Scale for improving the precision of VML rendering. Default is 1. + */ +mxShape.prototype.vmlScale = 1; + +/** + * Variable: outline + * + * Specifies if the shape should be drawn as an outline. This disables all + * fill colors and can be used to disable other drawing states that should + * not be painted for outlines. Default is false. This should be set before + * calling . + */ +mxShape.prototype.outline = false; + +/** + * Variable: visible + * + * Specifies if the shape is visible. Default is true. + */ +mxShape.prototype.visible = true; + +/** + * Variable: useSvgBoundingBox + * + * Allows to use the SVG bounding box in SVG. Default is false for performance + * reasons. + */ +mxShape.prototype.useSvgBoundingBox = false; + +/** + * Function: init + * + * Initializes the shape by creaing the DOM node using + * and adding it into the given container. + * + * Parameters: + * + * container - DOM node that will contain the shape. + */ +mxShape.prototype.init = function(container) +{ + if (this.node == null) + { + this.node = this.create(container); + + if (container != null) + { + container.appendChild(this.node); + } + } +}; + +/** + * Function: initStyles + * + * Sets the styles to their default values. + */ +mxShape.prototype.initStyles = function(container) +{ + this.strokewidth = 1; + this.rotation = 0; + this.opacity = 100; + this.fillOpacity = 100; + this.strokeOpacity = 100; + this.flipH = false; + this.flipV = false; +}; + +/** + * Function: isParseVml + * + * Specifies if any VML should be added via insertAdjacentHtml to the DOM. This + * is only needed in IE8 and only if the shape contains VML markup. This method + * returns true. + */ +mxShape.prototype.isParseVml = function() +{ + return true; +}; + +/** + * Function: isHtmlAllowed + * + * Returns true if HTML is allowed for this shape. This implementation always + * returns false. + */ +mxShape.prototype.isHtmlAllowed = function() +{ + return false; +}; + +/** + * Function: getSvgScreenOffset + * + * Returns 0, or 0.5 if % 2 == 1. + */ +mxShape.prototype.getSvgScreenOffset = function() +{ + var sw = this.stencil && this.stencil.strokewidth != 'inherit' ? Number(this.stencil.strokewidth) : this.strokewidth; + + return (mxUtils.mod(Math.max(1, Math.round(sw * this.scale)), 2) == 1) ? 0.5 : 0; +}; + +/** + * Function: create + * + * Creates and returns the DOM node(s) for the shape in + * the given container. This implementation invokes + * , or depending + * on the and style settings. + * + * Parameters: + * + * container - DOM node that will contain the shape. + */ +mxShape.prototype.create = function(container) +{ + var node = null; + + if (container != null && container.ownerSVGElement != null) + { + node = this.createSvg(container); + } + else if (document.documentMode == 8 || !mxClient.IS_VML || + (this.dialect != mxConstants.DIALECT_VML && this.isHtmlAllowed())) + { + node = this.createHtml(container); + } + else + { + node = this.createVml(container); + } + + return node; +}; + +/** + * Function: createSvg + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxShape.prototype.createSvg = function() +{ + return document.createElementNS(mxConstants.NS_SVG, 'g'); +}; + +/** + * Function: createVml + * + * Creates and returns the VML node to represent this shape. + */ +mxShape.prototype.createVml = function() +{ + var node = document.createElement(mxClient.VML_PREFIX + ':group'); + node.style.position = 'absolute'; + + return node; +}; + +/** + * Function: createHtml + * + * Creates and returns the HTML DOM node(s) to represent + * this shape. This implementation falls back to + * so that the HTML creation is optional. + */ +mxShape.prototype.createHtml = function() +{ + var node = document.createElement('div'); + node.style.position = 'absolute'; + + return node; +}; + +/** + * Function: reconfigure + * + * Reconfigures this shape. This will update the colors etc in + * addition to the bounds or points. + */ +mxShape.prototype.reconfigure = function() +{ + this.redraw(); +}; + +/** + * Function: redraw + * + * Creates and returns the SVG node(s) to represent this shape. + */ +mxShape.prototype.redraw = function() +{ + this.updateBoundsFromPoints(); + + if (this.visible && this.checkBounds()) + { + this.node.style.visibility = 'visible'; + this.clear(); + + if (this.node.nodeName == 'DIV' && (this.isHtmlAllowed() || !mxClient.IS_VML)) + { + this.redrawHtmlShape(); + } + else + { + this.redrawShape(); + } + + this.updateBoundingBox(); + } + else + { + this.node.style.visibility = 'hidden'; + this.boundingBox = null; + } +}; + +/** + * Function: clear + * + * Removes all child nodes and resets all CSS. + */ +mxShape.prototype.clear = function() +{ + if (this.node.ownerSVGElement != null) + { + while (this.node.lastChild != null) + { + this.node.removeChild(this.node.lastChild); + } + } + else + { + this.node.style.cssText = 'position:absolute;' + ((this.cursor != null) ? + ('cursor:' + this.cursor + ';') : ''); + this.node.innerHTML = ''; + } +}; + +/** + * Function: updateBoundsFromPoints + * + * Updates the bounds based on the points. + */ +mxShape.prototype.updateBoundsFromPoints = function() +{ + var pts = this.points; + + if (pts != null && pts.length > 0 && pts[0] != null) + { + this.bounds = new mxRectangle(Number(pts[0].x), Number(pts[0].y), 1, 1); + + for (var i = 1; i < this.points.length; i++) + { + if (pts[i] != null) + { + this.bounds.add(new mxRectangle(Number(pts[i].x), Number(pts[i].y), 1, 1)); + } + } + } +}; + +/** + * Function: getLabelBounds + * + * Returns the for the label bounds of this shape, based on the + * given scaled and translated bounds of the shape. This method should not + * change the rectangle in-place. This implementation returns the given rect. + */ +mxShape.prototype.getLabelBounds = function(rect) +{ + var d = mxUtils.getValue(this.style, mxConstants.STYLE_DIRECTION, mxConstants.DIRECTION_EAST); + var bounds = rect; + + // Normalizes argument for getLabelMargins hook + if (d != mxConstants.DIRECTION_SOUTH && d != mxConstants.DIRECTION_NORTH && + this.state != null && this.state.text != null && + this.state.text.isPaintBoundsInverted()) + { + bounds = bounds.clone(); + var tmp = bounds.width; + bounds.width = bounds.height; + bounds.height = tmp; + } + + var m = this.getLabelMargins(bounds); + + if (m != null) + { + var flipH = mxUtils.getValue(this.style, mxConstants.STYLE_FLIPH, false) == '1'; + var flipV = mxUtils.getValue(this.style, mxConstants.STYLE_FLIPV, false) == '1'; + + // Handles special case for vertical labels + if (this.state != null && this.state.text != null && + this.state.text.isPaintBoundsInverted()) + { + var tmp = m.x; + m.x = m.height; + m.height = m.width; + m.width = m.y; + m.y = tmp; + + tmp = flipH; + flipH = flipV; + flipV = tmp; + } + + return mxUtils.getDirectedBounds(rect, m, this.style, flipH, flipV); + } + + return rect; +}; + +/** + * Function: getLabelMargins + * + * Returns the scaled top, left, bottom and right margin to be used for + * computing the label bounds as an , where the bottom and right + * margin are defined in the width and height of the rectangle, respectively. + */ +mxShape.prototype.getLabelMargins= function(rect) +{ + return null; +}; + +/** + * Function: checkBounds + * + * Returns true if the bounds are not null and all of its variables are numeric. + */ +mxShape.prototype.checkBounds = function() +{ + return (!isNaN(this.scale) && isFinite(this.scale) && this.scale > 0 && + this.bounds != null && !isNaN(this.bounds.x) && !isNaN(this.bounds.y) && + !isNaN(this.bounds.width) && !isNaN(this.bounds.height) && + this.bounds.width > 0 && this.bounds.height > 0); +}; + +/** + * Function: createVmlGroup + * + * Returns the temporary element used for rendering in IE8 standards mode. + */ +mxShape.prototype.createVmlGroup = function() +{ + var node = document.createElement(mxClient.VML_PREFIX + ':group'); + node.style.position = 'absolute'; + node.style.width = this.node.style.width; + node.style.height = this.node.style.height; + + return node; +}; + +/** + * Function: redrawShape + * + * Updates the SVG or VML shape. + */ +mxShape.prototype.redrawShape = function() +{ + var canvas = this.createCanvas(); + + if (canvas != null) + { + // Specifies if events should be handled + canvas.pointerEvents = this.pointerEvents; + + this.paint(canvas); + + if (this.node != canvas.root) + { + // Forces parsing in IE8 standards mode - slow! avoid + this.node.insertAdjacentHTML('beforeend', canvas.root.outerHTML); + } + + if (this.node.nodeName == 'DIV' && document.documentMode == 8) + { + // Makes DIV transparent to events for IE8 in IE8 standards + // mode (Note: Does not work for IE9 in IE8 standards mode + // and not for IE11 in enterprise mode) + this.node.style.filter = ''; + + // Adds event transparency in IE8 standards + mxUtils.addTransparentBackgroundFilter(this.node); + } + + this.destroyCanvas(canvas); + } +}; + +/** + * Function: createCanvas + * + * Creates a new canvas for drawing this shape. May return null. + */ +mxShape.prototype.createCanvas = function() +{ + var canvas = null; + + // LATER: Check if reusing existing DOM nodes improves performance + if (this.node.ownerSVGElement != null) + { + canvas = this.createSvgCanvas(); + } + else if (mxClient.IS_VML) + { + this.updateVmlContainer(); + canvas = this.createVmlCanvas(); + } + + if (canvas != null && this.outline) + { + canvas.setStrokeWidth(this.strokewidth); + canvas.setStrokeColor(this.stroke); + + if (this.isDashed != null) + { + canvas.setDashed(this.isDashed); + } + + canvas.setStrokeWidth = function() {}; + canvas.setStrokeColor = function() {}; + canvas.setFillColor = function() {}; + canvas.setGradient = function() {}; + canvas.setDashed = function() {}; + canvas.text = function() {}; + } + + return canvas; +}; + +/** + * Function: createSvgCanvas + * + * Creates and returns an for rendering this shape. + */ +mxShape.prototype.createSvgCanvas = function() +{ + var canvas = new mxSvgCanvas2D(this.node, false); + canvas.strokeTolerance = (this.pointerEvents) ? this.svgStrokeTolerance : 0; + canvas.pointerEventsValue = this.svgPointerEvents; + canvas.blockImagePointerEvents = mxClient.IS_FF; + var off = this.getSvgScreenOffset(); + + if (off != 0) + { + this.node.setAttribute('transform', 'translate(' + off + ',' + off + ')'); + } + else + { + this.node.removeAttribute('transform'); + } + + canvas.minStrokeWidth = this.minSvgStrokeWidth; + + if (!this.antiAlias) + { + // Rounds all numbers in the SVG output to integers + canvas.format = function(value) + { + return Math.round(parseFloat(value)); + }; + } + + return canvas; +}; + +/** + * Function: createVmlCanvas + * + * Creates and returns an for rendering this shape. + */ +mxShape.prototype.createVmlCanvas = function() +{ + // Workaround for VML rendering bug in IE8 standards mode + var node = (document.documentMode == 8 && this.isParseVml()) ? this.createVmlGroup() : this.node; + var canvas = new mxVmlCanvas2D(node, false); + + if (node.tagUrn != '') + { + var w = Math.max(1, Math.round(this.bounds.width)); + var h = Math.max(1, Math.round(this.bounds.height)); + node.coordsize = (w * this.vmlScale) + ',' + (h * this.vmlScale); + canvas.scale(this.vmlScale); + canvas.vmlScale = this.vmlScale; + } + + // Painting relative to top, left shape corner + var s = this.scale; + canvas.translate(-Math.round(this.bounds.x / s), -Math.round(this.bounds.y / s)); + + return canvas; +}; + +/** + * Function: updateVmlContainer + * + * Updates the bounds of the VML container. + */ +mxShape.prototype.updateVmlContainer = function() +{ + this.node.style.left = Math.round(this.bounds.x) + 'px'; + this.node.style.top = Math.round(this.bounds.y) + 'px'; + var w = Math.max(1, Math.round(this.bounds.width)); + var h = Math.max(1, Math.round(this.bounds.height)); + this.node.style.width = w + 'px'; + this.node.style.height = h + 'px'; + this.node.style.overflow = 'visible'; +}; + +/** + * Function: redrawHtml + * + * Allow optimization by replacing VML with HTML. + */ +mxShape.prototype.redrawHtmlShape = function() +{ + // LATER: Refactor methods + this.updateHtmlBounds(this.node); + this.updateHtmlFilters(this.node); + this.updateHtmlColors(this.node); +}; + +/** + * Function: updateHtmlFilters + * + * Allow optimization by replacing VML with HTML. + */ +mxShape.prototype.updateHtmlFilters = function(node) +{ + var f = ''; + + if (this.opacity < 100) + { + f += 'alpha(opacity=' + (this.opacity) + ')'; + } + + if (this.isShadow) + { + // FIXME: Cannot implement shadow transparency with filter + f += 'progid:DXImageTransform.Microsoft.dropShadow (' + + 'OffX=\'' + Math.round(mxConstants.SHADOW_OFFSET_X * this.scale) + '\', ' + + 'OffY=\'' + Math.round(mxConstants.SHADOW_OFFSET_Y * this.scale) + '\', ' + + 'Color=\'' + mxConstants.VML_SHADOWCOLOR + '\')'; + } + + if (this.fill != null && this.fill != mxConstants.NONE && this.gradient && this.gradient != mxConstants.NONE) + { + var start = this.fill; + var end = this.gradient; + var type = '0'; + + var lookup = {east:0,south:1,west:2,north:3}; + var dir = (this.direction != null) ? lookup[this.direction] : 0; + + if (this.gradientDirection != null) + { + dir = mxUtils.mod(dir + lookup[this.gradientDirection] - 1, 4); + } + + if (dir == 1) + { + type = '1'; + var tmp = start; + start = end; + end = tmp; + } + else if (dir == 2) + { + var tmp = start; + start = end; + end = tmp; + } + else if (dir == 3) + { + type = '1'; + } + + f += 'progid:DXImageTransform.Microsoft.gradient(' + + 'startColorStr=\'' + start + '\', endColorStr=\'' + end + + '\', gradientType=\'' + type + '\')'; + } + + node.style.filter = f; +}; + +/** + * Function: mixedModeHtml + * + * Allow optimization by replacing VML with HTML. + */ +mxShape.prototype.updateHtmlColors = function(node) +{ + var color = this.stroke; + + if (color != null && color != mxConstants.NONE) + { + node.style.borderColor = color; + + if (this.isDashed) + { + node.style.borderStyle = 'dashed'; + } + else if (this.strokewidth > 0) + { + node.style.borderStyle = 'solid'; + } + + node.style.borderWidth = Math.max(1, Math.ceil(this.strokewidth * this.scale)) + 'px'; + } + else + { + node.style.borderWidth = '0px'; + } + + color = (this.outline) ? null : this.fill; + + if (color != null && color != mxConstants.NONE) + { + node.style.backgroundColor = color; + node.style.backgroundImage = 'none'; + } + else if (this.pointerEvents) + { + node.style.backgroundColor = 'transparent'; + } + else if (document.documentMode == 8) + { + mxUtils.addTransparentBackgroundFilter(node); + } + else + { + this.setTransparentBackgroundImage(node); + } +}; + +/** + * Function: mixedModeHtml + * + * Allow optimization by replacing VML with HTML. + */ +mxShape.prototype.updateHtmlBounds = function(node) +{ + var sw = (document.documentMode >= 9) ? 0 : Math.ceil(this.strokewidth * this.scale); + node.style.borderWidth = Math.max(1, sw) + 'px'; + node.style.overflow = 'hidden'; + + node.style.left = Math.round(this.bounds.x - sw / 2) + 'px'; + node.style.top = Math.round(this.bounds.y - sw / 2) + 'px'; + + if (document.compatMode == 'CSS1Compat') + { + sw = -sw; + } + + node.style.width = Math.round(Math.max(0, this.bounds.width + sw)) + 'px'; + node.style.height = Math.round(Math.max(0, this.bounds.height + sw)) + 'px'; +}; + +/** + * Function: destroyCanvas + * + * Destroys the given canvas which was used for drawing. This implementation + * increments the reference counts on all shared gradients used in the canvas. + */ +mxShape.prototype.destroyCanvas = function(canvas) +{ + // Manages reference counts + if (canvas instanceof mxSvgCanvas2D) + { + // Increments ref counts + for (var key in canvas.gradients) + { + var gradient = canvas.gradients[key]; + + if (gradient != null) + { + gradient.mxRefCount = (gradient.mxRefCount || 0) + 1; + } + } + + this.releaseSvgGradients(this.oldGradients); + this.oldGradients = canvas.gradients; + } +}; + +/** + * Function: paint + * + * Generic rendering code. + */ +mxShape.prototype.paint = function(c) +{ + var strokeDrawn = false; + + if (c != null && this.outline) + { + var stroke = c.stroke; + + c.stroke = function() + { + strokeDrawn = true; + stroke.apply(this, arguments); + }; + + var fillAndStroke = c.fillAndStroke; + + c.fillAndStroke = function() + { + strokeDrawn = true; + fillAndStroke.apply(this, arguments); + }; + } + + // Scale is passed-through to canvas + var s = this.scale; + var x = this.bounds.x / s; + var y = this.bounds.y / s; + var w = this.bounds.width / s; + var h = this.bounds.height / s; + + if (this.isPaintBoundsInverted()) + { + var t = (w - h) / 2; + x += t; + y -= t; + var tmp = w; + w = h; + h = tmp; + } + + this.updateTransform(c, x, y, w, h); + this.configureCanvas(c, x, y, w, h); + + // Adds background rectangle to capture events + var bg = null; + + if ((this.stencil == null && this.points == null && this.shapePointerEvents) || + (this.stencil != null && this.stencilPointerEvents)) + { + var bb = this.createBoundingBox(); + + if (this.dialect == mxConstants.DIALECT_SVG) + { + bg = this.createTransparentSvgRectangle(bb.x, bb.y, bb.width, bb.height); + this.node.appendChild(bg); + } + else + { + var rect = c.createRect('rect', bb.x / s, bb.y / s, bb.width / s, bb.height / s); + rect.appendChild(c.createTransparentFill()); + rect.stroked = 'false'; + c.root.appendChild(rect); + } + } + + if (this.stencil != null) + { + this.stencil.drawShape(c, this, x, y, w, h); + } + else + { + // Stencils have separate strokewidth + c.setStrokeWidth(this.strokewidth); + + if (this.points != null) + { + // Paints edge shape + var pts = []; + + for (var i = 0; i < this.points.length; i++) + { + if (this.points[i] != null) + { + pts.push(new mxPoint(this.points[i].x / s, this.points[i].y / s)); + } + } + + this.paintEdgeShape(c, pts); + } + else + { + // Paints vertex shape + this.paintVertexShape(c, x, y, w, h); + } + } + + if (bg != null && c.state != null && c.state.transform != null) + { + bg.setAttribute('transform', c.state.transform); + } + + // Draws highlight rectangle if no stroke was used + if (c != null && this.outline && !strokeDrawn) + { + c.rect(x, y, w, h); + c.stroke(); + } +}; + +/** + * Function: configureCanvas + * + * Sets the state of the canvas for drawing the shape. + */ +mxShape.prototype.configureCanvas = function(c, x, y, w, h) +{ + var dash = null; + + if (this.style != null) + { + dash = this.style['dashPattern']; + } + + c.setAlpha(this.opacity / 100); + c.setFillAlpha(this.fillOpacity / 100); + c.setStrokeAlpha(this.strokeOpacity / 100); + + // Sets alpha, colors and gradients + if (this.isShadow != null) + { + c.setShadow(this.isShadow); + } + + // Dash pattern + if (this.isDashed != null) + { + c.setDashed(this.isDashed, (this.style != null) ? + mxUtils.getValue(this.style, mxConstants.STYLE_FIX_DASH, false) == 1 : false); + } + + if (dash != null) + { + c.setDashPattern(dash); + } + + if (this.fill != null && this.fill != mxConstants.NONE && this.gradient && this.gradient != mxConstants.NONE) + { + var b = this.getGradientBounds(c, x, y, w, h); + c.setGradient(this.fill, this.gradient, b.x, b.y, b.width, b.height, this.gradientDirection); + } + else + { + c.setFillColor(this.fill); + } + + c.setStrokeColor(this.stroke); +}; + +/** + * Function: getGradientBounds + * + * Returns the bounding box for the gradient box for this shape. + */ +mxShape.prototype.getGradientBounds = function(c, x, y, w, h) +{ + return new mxRectangle(x, y, w, h); +}; + +/** + * Function: updateTransform + * + * Sets the scale and rotation on the given canvas. + */ +mxShape.prototype.updateTransform = function(c, x, y, w, h) +{ + // NOTE: Currently, scale is implemented in state and canvas. This will + // move to canvas in a later version, so that the states are unscaled + // and untranslated and do not need an update after zooming or panning. + c.scale(this.scale); + c.rotate(this.getShapeRotation(), this.flipH, this.flipV, x + w / 2, y + h / 2); +}; + +/** + * Function: paintVertexShape + * + * Paints the vertex shape. + */ +mxShape.prototype.paintVertexShape = function(c, x, y, w, h) +{ + this.paintBackground(c, x, y, w, h); + + if (!this.outline || this.style == null || mxUtils.getValue( + this.style, mxConstants.STYLE_BACKGROUND_OUTLINE, 0) == 0) + { + c.setShadow(false); + this.paintForeground(c, x, y, w, h); + } +}; + +/** + * Function: paintBackground + * + * Hook for subclassers. This implementation is empty. + */ +mxShape.prototype.paintBackground = function(c, x, y, w, h) { }; + +/** + * Function: paintForeground + * + * Hook for subclassers. This implementation is empty. + */ +mxShape.prototype.paintForeground = function(c, x, y, w, h) { }; + +/** + * Function: paintEdgeShape + * + * Hook for subclassers. This implementation is empty. + */ +mxShape.prototype.paintEdgeShape = function(c, pts) { }; + +/** + * Function: getArcSize + * + * Returns the arc size for the given dimension. + */ +mxShape.prototype.getArcSize = function(w, h) +{ + var r = 0; + + if (mxUtils.getValue(this.style, mxConstants.STYLE_ABSOLUTE_ARCSIZE, 0) == '1') + { + r = Math.min(w / 2, Math.min(h / 2, mxUtils.getValue(this.style, + mxConstants.STYLE_ARCSIZE, mxConstants.LINE_ARCSIZE) / 2)); + } + else + { + var f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, + mxConstants.RECTANGLE_ROUNDING_FACTOR * 100) / 100; + r = Math.min(w * f, h * f); + } + + return r; +}; + +/** + * Function: paintGlassEffect + * + * Paints the glass gradient effect. + */ +mxShape.prototype.paintGlassEffect = function(c, x, y, w, h, arc) +{ + var sw = Math.ceil(this.strokewidth / 2); + var size = 0.4; + + c.setGradient('#ffffff', '#ffffff', x, y, w, h * 0.6, 'south', 0.9, 0.1); + c.begin(); + arc += 2 * sw; + + if (this.isRounded) + { + c.moveTo(x - sw + arc, y - sw); + c.quadTo(x - sw, y - sw, x - sw, y - sw + arc); + c.lineTo(x - sw, y + h * size); + c.quadTo(x + w * 0.5, y + h * 0.7, x + w + sw, y + h * size); + c.lineTo(x + w + sw, y - sw + arc); + c.quadTo(x + w + sw, y - sw, x + w + sw - arc, y - sw); + } + else + { + c.moveTo(x - sw, y - sw); + c.lineTo(x - sw, y + h * size); + c.quadTo(x + w * 0.5, y + h * 0.7, x + w + sw, y + h * size); + c.lineTo(x + w + sw, y - sw); + } + + c.close(); + c.fill(); +}; + +/** + * Function: addPoints + * + * Paints the given points with rounded corners. + */ +mxShape.prototype.addPoints = function(c, pts, rounded, arcSize, close, exclude, initialMove) +{ + if (pts != null && pts.length > 0) + { + initialMove = (initialMove != null) ? initialMove : true; + var pe = pts[pts.length - 1]; + + // Adds virtual waypoint in the center between start and end point + if (close && rounded) + { + pts = pts.slice(); + var p0 = pts[0]; + var wp = new mxPoint(pe.x + (p0.x - pe.x) / 2, pe.y + (p0.y - pe.y) / 2); + pts.splice(0, 0, wp); + } + + var pt = pts[0]; + var i = 1; + + // Draws the line segments + if (initialMove) + { + c.moveTo(pt.x, pt.y); + } + else + { + c.lineTo(pt.x, pt.y); + } + + while (i < ((close) ? pts.length : pts.length - 1)) + { + var tmp = pts[mxUtils.mod(i, pts.length)]; + var dx = pt.x - tmp.x; + var dy = pt.y - tmp.y; + + if (rounded && (dx != 0 || dy != 0) && (exclude == null || mxUtils.indexOf(exclude, i - 1) < 0)) + { + // Draws a line from the last point to the current + // point with a spacing of size off the current point + // into direction of the last point + var dist = Math.sqrt(dx * dx + dy * dy); + var nx1 = dx * Math.min(arcSize, dist / 2) / dist; + var ny1 = dy * Math.min(arcSize, dist / 2) / dist; + + var x1 = tmp.x + nx1; + var y1 = tmp.y + ny1; + c.lineTo(x1, y1); + + // Draws a curve from the last point to the current + // point with a spacing of size off the current point + // into direction of the next point + var next = pts[mxUtils.mod(i + 1, pts.length)]; + + // Uses next non-overlapping point + while (i < pts.length - 2 && Math.round(next.x - tmp.x) == 0 && Math.round(next.y - tmp.y) == 0) + { + next = pts[mxUtils.mod(i + 2, pts.length)]; + i++; + } + + dx = next.x - tmp.x; + dy = next.y - tmp.y; + + dist = Math.max(1, Math.sqrt(dx * dx + dy * dy)); + var nx2 = dx * Math.min(arcSize, dist / 2) / dist; + var ny2 = dy * Math.min(arcSize, dist / 2) / dist; + + var x2 = tmp.x + nx2; + var y2 = tmp.y + ny2; + + c.quadTo(tmp.x, tmp.y, x2, y2); + tmp = new mxPoint(x2, y2); + } + else + { + c.lineTo(tmp.x, tmp.y); + } + + pt = tmp; + i++; + } + + if (close) + { + c.close(); + } + else + { + c.lineTo(pe.x, pe.y); + } + } +}; + +/** + * Function: resetStyles + * + * Resets all styles. + */ +mxShape.prototype.resetStyles = function() +{ + this.initStyles(); + + this.spacing = 0; + + delete this.fill; + delete this.gradient; + delete this.gradientDirection; + delete this.stroke; + delete this.startSize; + delete this.endSize; + delete this.startArrow; + delete this.endArrow; + delete this.direction; + delete this.isShadow; + delete this.isDashed; + delete this.isRounded; + delete this.glass; +}; + +/** + * Function: apply + * + * Applies the style of the given to the shape. This + * implementation assigns the following styles to local fields: + * + * - => fill + * - => gradient + * - => gradientDirection + * - => opacity + * - => fillOpacity + * - => strokeOpacity + * - => stroke + * - => strokewidth + * - => isShadow + * - => isDashed + * - => spacing + * - => startSize + * - => endSize + * - => isRounded + * - => startArrow + * - => endArrow + * - => rotation + * - => direction + * - => glass + * + * This keeps a reference to the '); + * }; + * (end) + * + * Headers: + * + * Apart from setting the title argument in the mxPrintPreview constructor you + * can override as follows to add a header to any page: + * + * (code) + * var oldRenderPage = mxPrintPreview.prototype.renderPage; + * mxPrintPreview.prototype.renderPage = function(w, h, x, y, content, pageNumber) + * { + * var div = oldRenderPage.apply(this, arguments); + * + * var header = document.createElement('div'); + * header.style.position = 'absolute'; + * header.style.top = '0px'; + * header.style.width = '100%'; + * header.style.textAlign = 'right'; + * mxUtils.write(header, 'Your header here'); + * div.firstChild.appendChild(header); + * + * return div; + * }; + * (end) + * + * The pageNumber argument contains the number of the current page, starting at + * 1. To display a header on the first page only, check pageNumber and add a + * vertical offset in the constructor call for the height of the header. + * + * Page Format: + * + * For landscape printing, use as + * the pageFormat in and . + * Keep in mind that one can not set the defaults for the print dialog + * of the operating system from JavaScript so the user must manually choose + * a page format that matches this setting. + * + * You can try passing the following CSS directive to to set the + * page format in the print dialog to landscape. However, this CSS + * directive seems to be ignored in most major browsers, including IE. + * + * (code) + * @page { + * size: landscape; + * } + * (end) + * + * Note that the print preview behaves differently in IE when used from the + * filesystem or via HTTP so printing should always be tested via HTTP. + * + * If you are using a DOCTYPE in the source page you can override + * and provide the same DOCTYPE for the print preview if required. Here is + * an example for IE8 standards mode. + * + * (code) + * var preview = new mxPrintPreview(graph); + * preview.getDoctype = function() + * { + * return ''; + * }; + * preview.open(); + * (end) + * + * Constructor: mxPrintPreview + * + * Constructs a new print preview for the given parameters. + * + * Parameters: + * + * graph - to be previewed. + * scale - Optional scale of the output. Default is 1 / . + * border - Border in pixels along each side of every page. Note that the + * actual print function in the browser will add another border for + * printing. + * pageFormat - that specifies the page format (in pixels). + * This should match the page format of the printer. Default uses the + * of the given graph. + * x0 - Optional left offset of the output. Default is 0. + * y0 - Optional top offset of the output. Default is 0. + * borderColor - Optional color of the page border. Default is no border. + * Note that a border is sometimes useful to highlight the printed page + * border in the print preview of the browser. + * title - Optional string that is used for the window title. Default + * is 'Printer-friendly version'. + * pageSelector - Optional boolean that specifies if the page selector + * should appear in the window with the print preview. Default is true. + */ +function mxPrintPreview(graph, scale, pageFormat, border, x0, y0, borderColor, title, pageSelector) +{ + this.graph = graph; + this.scale = (scale != null) ? scale : 1 / graph.pageScale; + this.border = (border != null) ? border : 0; + this.pageFormat = mxRectangle.fromRectangle((pageFormat != null) ? pageFormat : graph.pageFormat); + this.title = (title != null) ? title : 'Printer-friendly version'; + this.x0 = (x0 != null) ? x0 : 0; + this.y0 = (y0 != null) ? y0 : 0; + this.borderColor = borderColor; + this.pageSelector = (pageSelector != null) ? pageSelector : true; +}; + +/** + * Variable: graph + * + * Reference to the that should be previewed. + */ +mxPrintPreview.prototype.graph = null; + +/** + * Variable: pageFormat + * + * Holds the that defines the page format. + */ +mxPrintPreview.prototype.pageFormat = null; + +/** + * Variable: scale + * + * Holds the scale of the print preview. + */ +mxPrintPreview.prototype.scale = null; + +/** + * Variable: border + * + * The border inset around each side of every page in the preview. This is set + * to 0 if autoOrigin is false. + */ +mxPrintPreview.prototype.border = 0; + +/** + * Variable: marginTop + * + * The margin at the top of the page (number). Default is 0. + */ +mxPrintPreview.prototype.marginTop = 0; + +/** + * Variable: marginBottom + * + * The margin at the bottom of the page (number). Default is 0. + */ +mxPrintPreview.prototype.marginBottom = 0; + +/** + * Variable: x0 + * + * Holds the horizontal offset of the output. + */ +mxPrintPreview.prototype.x0 = 0; + +/** + * Variable: y0 + * + * Holds the vertical offset of the output. + */ +mxPrintPreview.prototype.y0 = 0; + +/** + * Variable: autoOrigin + * + * Specifies if the origin should be automatically computed based on the top, + * left corner of the actual diagram contents. The required offset will be added + * to and in . Default is true. + */ +mxPrintPreview.prototype.autoOrigin = true; + +/** + * Variable: printOverlays + * + * Specifies if overlays should be printed. Default is false. + */ +mxPrintPreview.prototype.printOverlays = false; + +/** + * Variable: printControls + * + * Specifies if controls (such as folding icons) should be printed. Default is + * false. + */ +mxPrintPreview.prototype.printControls = false; + +/** + * Variable: printBackgroundImage + * + * Specifies if the background image should be printed. Default is false. + */ +mxPrintPreview.prototype.printBackgroundImage = false; + +/** + * Variable: backgroundColor + * + * Holds the color value for the page background color. Default is #ffffff. + */ +mxPrintPreview.prototype.backgroundColor = '#ffffff'; + +/** + * Variable: borderColor + * + * Holds the color value for the page border. + */ +mxPrintPreview.prototype.borderColor = null; + +/** + * Variable: title + * + * Holds the title of the preview window. + */ +mxPrintPreview.prototype.title = null; + +/** + * Variable: pageSelector + * + * Boolean that specifies if the page selector should be + * displayed. Default is true. + */ +mxPrintPreview.prototype.pageSelector = null; + +/** + * Variable: wnd + * + * Reference to the preview window. + */ +mxPrintPreview.prototype.wnd = null; + +/** + * Variable: targetWindow + * + * Assign any window here to redirect the rendering in . + */ +mxPrintPreview.prototype.targetWindow = null; + +/** + * Variable: pageCount + * + * Holds the actual number of pages in the preview. + */ +mxPrintPreview.prototype.pageCount = 0; + +/** + * Variable: clipping + * + * Specifies is clipping should be used to avoid creating too many cell states + * in large diagrams. The bounding box of the cells in the original diagram is + * used if this is enabled. Default is true. + */ +mxPrintPreview.prototype.clipping = true; + +/** + * Function: getWindow + * + * Returns . + */ +mxPrintPreview.prototype.getWindow = function() +{ + return this.wnd; +}; + +/** + * Function: getDocType + * + * Returns the string that should go before the HTML tag in the print preview + * page. This implementation returns an X-UA meta tag for IE5 in quirks mode, + * IE8 in IE8 standards mode and edge in IE9 standards mode. + */ +mxPrintPreview.prototype.getDoctype = function() +{ + var dt = ''; + + if (document.documentMode == 5) + { + dt = ''; + } + else if (document.documentMode == 8) + { + dt = ''; + } + else if (document.documentMode > 8) + { + // Comment needed to make standards doctype apply in IE + dt = ''; + } + + return dt; +}; + +/** + * Function: appendGraph + * + * Adds the given graph to the existing print preview. + * + * Parameters: + * + * css - Optional CSS string to be used in the head section. + * targetWindow - Optional window that should be used for rendering. If + * this is specified then no HEAD tag, CSS and BODY tag will be written. + */ +mxPrintPreview.prototype.appendGraph = function(graph, scale, x0, y0, forcePageBreaks, keepOpen) +{ + this.graph = graph; + this.scale = (scale != null) ? scale : 1 / graph.pageScale; + this.x0 = x0; + this.y0 = y0; + this.open(null, null, forcePageBreaks, keepOpen); +}; + +/** + * Function: open + * + * Shows the print preview window. The window is created here if it does + * not exist. + * + * Parameters: + * + * css - Optional CSS string to be used in the head section. + * targetWindow - Optional window that should be used for rendering. If + * this is specified then no HEAD tag, CSS and BODY tag will be written. + */ +mxPrintPreview.prototype.open = function(css, targetWindow, forcePageBreaks, keepOpen) +{ + // Closing the window while the page is being rendered may cause an + // exception in IE. This and any other exceptions are simply ignored. + var previousInitializeOverlay = this.graph.cellRenderer.initializeOverlay; + var div = null; + + try + { + // Temporarily overrides the method to redirect rendering of overlays + // to the draw pane so that they are visible in the printout + if (this.printOverlays) + { + this.graph.cellRenderer.initializeOverlay = function(state, overlay) + { + overlay.init(state.view.getDrawPane()); + }; + } + + if (this.printControls) + { + this.graph.cellRenderer.initControl = function(state, control, handleEvents, clickHandler) + { + control.dialect = state.view.graph.dialect; + control.init(state.view.getDrawPane()); + }; + } + + this.wnd = (targetWindow != null) ? targetWindow : this.wnd; + var isNewWindow = false; + + if (this.wnd == null) + { + isNewWindow = true; + this.wnd = window.open(); + } + + var doc = this.wnd.document; + + if (isNewWindow) + { + var dt = this.getDoctype(); + + if (dt != null && dt.length > 0) + { + doc.writeln(dt); + } + + if (mxClient.IS_VML) + { + doc.writeln(''); + } + else + { + if (document.compatMode === 'CSS1Compat') + { + doc.writeln(''); + } + + doc.writeln(''); + } + + doc.writeln(''); + this.writeHead(doc, css); + doc.writeln(''); + doc.writeln(''); + } + + // Computes the horizontal and vertical page count + var bounds = this.graph.getGraphBounds().clone(); + var currentScale = this.graph.getView().getScale(); + var sc = currentScale / this.scale; + var tr = this.graph.getView().getTranslate(); + + // Uses the absolute origin with no offset for all printing + if (!this.autoOrigin) + { + this.x0 -= tr.x * this.scale; + this.y0 -= tr.y * this.scale; + bounds.width += bounds.x; + bounds.height += bounds.y; + bounds.x = 0; + bounds.y = 0; + this.border = 0; + } + + // Store the available page area + var availableWidth = this.pageFormat.width - (this.border * 2); + var availableHeight = this.pageFormat.height - (this.border * 2); + + // Adds margins to page format + this.pageFormat.height += this.marginTop + this.marginBottom; + + // Compute the unscaled, untranslated bounds to find + // the number of vertical and horizontal pages + bounds.width /= sc; + bounds.height /= sc; + + var hpages = Math.max(1, Math.ceil((bounds.width + this.x0) / availableWidth)); + var vpages = Math.max(1, Math.ceil((bounds.height + this.y0) / availableHeight)); + this.pageCount = hpages * vpages; + + var writePageSelector = mxUtils.bind(this, function() + { + if (this.pageSelector && (vpages > 1 || hpages > 1)) + { + var table = this.createPageSelector(vpages, hpages); + doc.body.appendChild(table); + + // Implements position: fixed in IE quirks mode + if (mxClient.IS_IE && doc.documentMode == null || doc.documentMode == 5 || doc.documentMode == 8 || doc.documentMode == 7) + { + table.style.position = 'absolute'; + + var update = function() + { + table.style.top = ((doc.body.scrollTop || doc.documentElement.scrollTop) + 10) + 'px'; + }; + + mxEvent.addListener(this.wnd, 'scroll', function(evt) + { + update(); + }); + + mxEvent.addListener(this.wnd, 'resize', function(evt) + { + update(); + }); + } + } + }); + + var addPage = mxUtils.bind(this, function(div, addBreak) + { + // Border of the DIV (aka page) inside the document + if (this.borderColor != null) + { + div.style.borderColor = this.borderColor; + div.style.borderStyle = 'solid'; + div.style.borderWidth = '1px'; + } + + // Needs to be assigned directly because IE doesn't support + // child selectors, eg. body > div { background: white; } + div.style.background = this.backgroundColor; + + if (forcePageBreaks || addBreak) + { + div.style.pageBreakAfter = 'always'; + } + + // NOTE: We are dealing with cross-window DOM here, which + // is a problem in IE, so we copy the HTML markup instead. + // The underlying problem is that the graph display markup + // creation (in mxShape, mxGraphView) is hardwired to using + // document.createElement and hence we must use this document + // to create the complete page and then copy it over to the + // new window.document. This can be fixed later by using the + // ownerDocument of the container in mxShape and mxGraphView. + if (isNewWindow && (mxClient.IS_IE || document.documentMode >= 11 || mxClient.IS_EDGE)) + { + // For some obscure reason, removing the DIV from the + // parent before fetching its outerHTML has missing + // fillcolor properties and fill children, so the div + // must be removed afterwards to keep the fillcolors. + doc.writeln(div.outerHTML); + div.parentNode.removeChild(div); + } + else if (mxClient.IS_IE || document.documentMode >= 11 || mxClient.IS_EDGE) + { + var clone = doc.createElement('div'); + clone.innerHTML = div.outerHTML; + clone = clone.getElementsByTagName('div')[0]; + doc.body.appendChild(clone); + div.parentNode.removeChild(div); + } + else + { + div.parentNode.removeChild(div); + doc.body.appendChild(div); + } + + if (forcePageBreaks || addBreak) + { + this.addPageBreak(doc); + } + }); + + var cov = this.getCoverPages(this.pageFormat.width, this.pageFormat.height); + + if (cov != null) + { + for (var i = 0; i < cov.length; i++) + { + addPage(cov[i], true); + } + } + + var apx = this.getAppendices(this.pageFormat.width, this.pageFormat.height); + + // Appends each page to the page output for printing, making + // sure there will be a page break after each page (ie. div) + for (var i = 0; i < vpages; i++) + { + var dy = i * availableHeight / this.scale - this.y0 / this.scale + + (bounds.y - tr.y * currentScale) / currentScale; + + for (var j = 0; j < hpages; j++) + { + if (this.wnd == null) + { + return null; + } + + var dx = j * availableWidth / this.scale - this.x0 / this.scale + + (bounds.x - tr.x * currentScale) / currentScale; + var pageNum = i * hpages + j + 1; + var clip = new mxRectangle(dx, dy, availableWidth, availableHeight); + div = this.renderPage(this.pageFormat.width, this.pageFormat.height, 0, 0, mxUtils.bind(this, function(div) + { + this.addGraphFragment(-dx, -dy, this.scale, pageNum, div, clip); + + if (this.printBackgroundImage) + { + this.insertBackgroundImage(div, -dx, -dy); + } + }), pageNum); + + // Gives the page a unique ID for later accessing the page + div.setAttribute('id', 'mxPage-'+pageNum); + + addPage(div, apx != null || i < vpages - 1 || j < hpages - 1); + } + } + + if (apx != null) + { + for (var i = 0; i < apx.length; i++) + { + addPage(apx[i], i < apx.length - 1); + } + } + + if (isNewWindow && !keepOpen) + { + this.closeDocument(); + writePageSelector(); + } + + this.wnd.focus(); + } + catch (e) + { + // Removes the DIV from the document in case of an error + if (div != null && div.parentNode != null) + { + div.parentNode.removeChild(div); + } + } + finally + { + this.graph.cellRenderer.initializeOverlay = previousInitializeOverlay; + } + + return this.wnd; +}; + +/** + * Function: addPageBreak + * + * Adds a page break to the given document. + */ +mxPrintPreview.prototype.addPageBreak = function(doc) +{ + var hr = doc.createElement('hr'); + hr.className = 'mxPageBreak'; + doc.body.appendChild(hr); +}; + +/** + * Function: closeDocument + * + * Writes the closing tags for body and page after calling . + */ +mxPrintPreview.prototype.closeDocument = function() +{ + if (this.wnd != null && this.wnd.document != null) + { + var doc = this.wnd.document; + + this.writePostfix(doc); + doc.writeln(''); + doc.writeln(''); + doc.close(); + + // Removes all event handlers in the print output + mxEvent.release(doc.body); + } +}; + +/** + * Function: writeHead + * + * Writes the HEAD section into the given document, without the opening + * and closing HEAD tags. + */ +mxPrintPreview.prototype.writeHead = function(doc, css) +{ + if (this.title != null) + { + doc.writeln('' + this.title + ''); + } + + // Adds required namespaces + if (mxClient.IS_VML) + { + doc.writeln(''); + } + + // Adds all required stylesheets + mxClient.link('stylesheet', mxClient.basePath + '/css/common.css', doc); + + // Removes horizontal rules and page selector from print output + doc.writeln(''); +}; + +/** + * Function: writePostfix + * + * Called before closing the body of the page. This implementation is empty. + */ +mxPrintPreview.prototype.writePostfix = function(doc) +{ + // empty +}; + +/** + * Function: createPageSelector + * + * Creates the page selector table. + */ +mxPrintPreview.prototype.createPageSelector = function(vpages, hpages) +{ + var doc = this.wnd.document; + var table = doc.createElement('table'); + table.className = 'mxPageSelector'; + table.setAttribute('border', '0'); + + var tbody = doc.createElement('tbody'); + + for (var i = 0; i < vpages; i++) + { + var row = doc.createElement('tr'); + + for (var j = 0; j < hpages; j++) + { + var pageNum = i * hpages + j + 1; + var cell = doc.createElement('td'); + var a = doc.createElement('a'); + a.setAttribute('href', '#mxPage-' + pageNum); + + // Workaround for FF where the anchor is appended to the URL of the original document + if (mxClient.IS_NS && !mxClient.IS_SF && !mxClient.IS_GC) + { + var js = 'var page = document.getElementById(\'mxPage-' + pageNum + '\');page.scrollIntoView(true);event.preventDefault();'; + a.setAttribute('onclick', js); + } + + mxUtils.write(a, pageNum, doc); + cell.appendChild(a); + row.appendChild(cell); + } + + tbody.appendChild(row); + } + + table.appendChild(tbody); + + return table; +}; + +/** + * Function: renderPage + * + * Creates a DIV that prints a single page of the given + * graph using the given scale and returns the DIV that + * represents the page. + * + * Parameters: + * + * w - Width of the page in pixels. + * h - Height of the page in pixels. + * dx - Optional horizontal page offset in pixels (used internally). + * dy - Optional vertical page offset in pixels (used internally). + * content - Callback that adds the HTML content to the inner div of a page. + * Takes the inner div as the argument. + * pageNumber - Integer representing the page number. + */ +mxPrintPreview.prototype.renderPage = function(w, h, dx, dy, content, pageNumber) +{ + var doc = this.wnd.document; + var div = document.createElement('div'); + var arg = null; + + try + { + // Workaround for ignored clipping in IE 9 standards + // when printing with page breaks and HTML labels. + if (dx != 0 || dy != 0) + { + div.style.position = 'relative'; + div.style.width = w + 'px'; + div.style.height = h + 'px'; + div.style.pageBreakInside = 'avoid'; + + var innerDiv = document.createElement('div'); + innerDiv.style.position = 'relative'; + innerDiv.style.top = this.border + 'px'; + innerDiv.style.left = this.border + 'px'; + innerDiv.style.width = (w - 2 * this.border) + 'px'; + innerDiv.style.height = (h - 2 * this.border) + 'px'; + innerDiv.style.overflow = 'hidden'; + + var viewport = document.createElement('div'); + viewport.style.position = 'relative'; + viewport.style.marginLeft = dx + 'px'; + viewport.style.marginTop = dy + 'px'; + + // FIXME: IE8 standards output problems + if (doc.documentMode == 8) + { + innerDiv.style.position = 'absolute'; + viewport.style.position = 'absolute'; + } + + if (doc.documentMode == 10) + { + viewport.style.width = '100%'; + viewport.style.height = '100%'; + } + + innerDiv.appendChild(viewport); + div.appendChild(innerDiv); + document.body.appendChild(div); + arg = viewport; + } + // FIXME: IE10/11 too many pages + else + { + div.style.width = w + 'px'; + div.style.height = h + 'px'; + div.style.overflow = 'hidden'; + div.style.pageBreakInside = 'avoid'; + + // IE8 uses above branch currently + if (doc.documentMode == 8) + { + div.style.position = 'relative'; + } + + var innerDiv = document.createElement('div'); + innerDiv.style.width = (w - 2 * this.border) + 'px'; + innerDiv.style.height = (h - 2 * this.border) + 'px'; + innerDiv.style.overflow = 'hidden'; + + if (mxClient.IS_IE && (doc.documentMode == null || doc.documentMode == 5 || doc.documentMode == 8 || doc.documentMode == 7)) + { + innerDiv.style.marginTop = this.border + 'px'; + innerDiv.style.marginLeft = this.border + 'px'; + } + else + { + innerDiv.style.top = this.border + 'px'; + innerDiv.style.left = this.border + 'px'; + } + + if (this.graph.dialect == mxConstants.DIALECT_VML) + { + innerDiv.style.position = 'absolute'; + } + + div.appendChild(innerDiv); + document.body.appendChild(div); + arg = innerDiv; + } + } + catch (e) + { + div.parentNode.removeChild(div); + div = null; + + throw e; + } + + content(arg); + + return div; +}; + +/** + * Function: getRoot + * + * Returns the root cell for painting the graph. + */ +mxPrintPreview.prototype.getRoot = function() +{ + var root = this.graph.view.currentRoot; + + if (root == null) + { + root = this.graph.getModel().getRoot(); + } + + return root; +}; + +/** + * Function: addGraphFragment + * + * Adds a graph fragment to the given div. + * + * Parameters: + * + * dx - Horizontal translation for the diagram. + * dy - Vertical translation for the diagram. + * scale - Scale for the diagram. + * pageNumber - Number of the page to be rendered. + * div - Div that contains the output. + * clip - Contains the clipping rectangle as an . + */ +mxPrintPreview.prototype.addGraphFragment = function(dx, dy, scale, pageNumber, div, clip) +{ + var view = this.graph.getView(); + var previousContainer = this.graph.container; + this.graph.container = div; + + var canvas = view.getCanvas(); + var backgroundPane = view.getBackgroundPane(); + var drawPane = view.getDrawPane(); + var overlayPane = view.getOverlayPane(); + + if (this.graph.dialect == mxConstants.DIALECT_SVG) + { + view.createSvg(); + + // Uses CSS transform for scaling + if (!mxClient.NO_FO) + { + var g = view.getDrawPane().parentNode; + var prev = g.getAttribute('transform'); + g.setAttribute('transformOrigin', '0 0'); + g.setAttribute('transform', 'scale(' + scale + ',' + scale + ')' + + 'translate(' + dx + ',' + dy + ')'); + + scale = 1; + dx = 0; + dy = 0; + } + } + else if (this.graph.dialect == mxConstants.DIALECT_VML) + { + view.createVml(); + } + else + { + view.createHtml(); + } + + // Disables events on the view + var eventsEnabled = view.isEventsEnabled(); + view.setEventsEnabled(false); + + // Disables the graph to avoid cursors + var graphEnabled = this.graph.isEnabled(); + this.graph.setEnabled(false); + + // Resets the translation + var translate = view.getTranslate(); + view.translate = new mxPoint(dx, dy); + + // Redraws only states that intersect the clip + var redraw = this.graph.cellRenderer.redraw; + var states = view.states; + var s = view.scale; + + // Gets the transformed clip for intersection check below + if (this.clipping) + { + var tempClip = new mxRectangle((clip.x + translate.x) * s, (clip.y + translate.y) * s, + clip.width * s / scale, clip.height * s / scale); + + // Checks clipping rectangle for speedup + // Must create terminal states for edge clipping even if terminal outside of clip + this.graph.cellRenderer.redraw = function(state, force, rendering) + { + if (state != null) + { + // Gets original state from graph to find bounding box + var orig = states.get(state.cell); + + if (orig != null) + { + var bbox = view.getBoundingBox(orig, false); + + // Stops rendering if outside clip for speedup + if (bbox != null && !mxUtils.intersects(tempClip, bbox)) + { + //return; + } + } + } + + redraw.apply(this, arguments); + }; + } + + var temp = null; + + try + { + // Creates the temporary cell states in the view and + // draws them onto the temporary DOM nodes in the view + var cells = [this.getRoot()]; + temp = new mxTemporaryCellStates(view, scale, cells, null, mxUtils.bind(this, function(state) + { + return this.getLinkForCellState(state); + })); + } + finally + { + // Removes overlay pane with selection handles + // controls and icons from the print output + if (mxClient.IS_IE) + { + view.overlayPane.innerHTML = ''; + view.canvas.style.overflow = 'hidden'; + view.canvas.style.position = 'relative'; + view.canvas.style.top = this.marginTop + 'px'; + view.canvas.style.width = clip.width + 'px'; + view.canvas.style.height = clip.height + 'px'; + } + else + { + // Removes everything but the SVG node + var tmp = div.firstChild; + + while (tmp != null) + { + var next = tmp.nextSibling; + var name = tmp.nodeName.toLowerCase(); + + // Note: Width and height are required in FF 11 + if (name == 'svg') + { + tmp.style.overflow = 'hidden'; + tmp.style.position = 'relative'; + tmp.style.top = this.marginTop + 'px'; + tmp.setAttribute('width', clip.width); + tmp.setAttribute('height', clip.height); + tmp.style.width = ''; + tmp.style.height = ''; + } + // Tries to fetch all text labels and only text labels + else if (tmp.style.cursor != 'default' && name != 'div') + { + tmp.parentNode.removeChild(tmp); + } + + tmp = next; + } + } + + // Puts background image behind SVG output + if (this.printBackgroundImage) + { + var svgs = div.getElementsByTagName('svg'); + + if (svgs.length > 0) + { + svgs[0].style.position = 'absolute'; + } + } + + // Completely removes the overlay pane to remove more handles + view.overlayPane.parentNode.removeChild(view.overlayPane); + + // Restores the state of the view + this.graph.setEnabled(graphEnabled); + this.graph.container = previousContainer; + this.graph.cellRenderer.redraw = redraw; + view.canvas = canvas; + view.backgroundPane = backgroundPane; + view.drawPane = drawPane; + view.overlayPane = overlayPane; + view.translate = translate; + temp.destroy(); + view.setEventsEnabled(eventsEnabled); + } +}; + +/** + * Function: getLinkForCellState + * + * Returns the link for the given cell state. This returns null. + */ +mxPrintPreview.prototype.getLinkForCellState = function(state) +{ + return this.graph.getLinkForCell(state.cell); +}; + +/** + * Function: insertBackgroundImage + * + * Inserts the background image into the given div. + */ +mxPrintPreview.prototype.insertBackgroundImage = function(div, dx, dy) +{ + var bg = this.graph.backgroundImage; + + if (bg != null) + { + var img = document.createElement('img'); + img.style.position = 'absolute'; + img.style.marginLeft = Math.round(dx * this.scale) + 'px'; + img.style.marginTop = Math.round(dy * this.scale) + 'px'; + img.setAttribute('width', Math.round(this.scale * bg.width)); + img.setAttribute('height', Math.round(this.scale * bg.height)); + img.src = bg.src; + + div.insertBefore(img, div.firstChild); + } +}; + +/** + * Function: getCoverPages + * + * Returns the pages to be added before the print output. This returns null. + */ +mxPrintPreview.prototype.getCoverPages = function() +{ + return null; +}; + +/** + * Function: getAppendices + * + * Returns the pages to be added after the print output. This returns null. + */ +mxPrintPreview.prototype.getAppendices = function() +{ + return null; +}; + +/** + * Function: print + * + * Opens the print preview and shows the print dialog. + * + * Parameters: + * + * css - Optional CSS string to be used in the head section. + */ +mxPrintPreview.prototype.print = function(css) +{ + var wnd = this.open(css); + + if (wnd != null) + { + wnd.print(); + } +}; + +/** + * Function: close + * + * Closes the print preview window. + */ +mxPrintPreview.prototype.close = function() +{ + if (this.wnd != null) + { + this.wnd.close(); + this.wnd = null; + } +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxStylesheet + * + * Defines the appearance of the cells in a graph. See for an + * example of creating a new cell style. It is recommended to use objects, not + * arrays for holding cell styles. Existing styles can be cloned using + * and turned into a string for debugging using + * . + * + * Default Styles: + * + * The stylesheet contains two built-in styles, which are used if no style is + * defined for a cell: + * + * defaultVertex - Default style for vertices + * defaultEdge - Default style for edges + * + * Example: + * + * (code) + * var vertexStyle = stylesheet.getDefaultVertexStyle(); + * vertexStyle[mxConstants.ROUNDED] = true; + * var edgeStyle = stylesheet.getDefaultEdgeStyle(); + * edgeStyle[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation; + * (end) + * + * Modifies the built-in default styles. + * + * To avoid the default style for a cell, add a leading semicolon + * to the style definition, eg. + * + * (code) + * ;shadow=1 + * (end) + * + * Removing keys: + * + * For removing a key in a cell style of the form [stylename;|key=value;] the + * special value none can be used, eg. highlight;fillColor=none + * + * See also the helper methods in mxUtils to modify strings of this format, + * namely , , + * , , + * and . + * + * Constructor: mxStylesheet + * + * Constructs a new stylesheet and assigns default styles. + */ +function mxStylesheet() +{ + this.styles = new Object(); + + this.putDefaultVertexStyle(this.createDefaultVertexStyle()); + this.putDefaultEdgeStyle(this.createDefaultEdgeStyle()); +}; + +/** + * Function: styles + * + * Maps from names to cell styles. Each cell style is a map of key, + * value pairs. + */ +mxStylesheet.prototype.styles; + +/** + * Function: createDefaultVertexStyle + * + * Creates and returns the default vertex style. + */ +mxStylesheet.prototype.createDefaultVertexStyle = function() +{ + var style = new Object(); + + style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE; + style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter; + style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE; + style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER; + style[mxConstants.STYLE_FILLCOLOR] = '#C3D9FF'; + style[mxConstants.STYLE_STROKECOLOR] = '#6482B9'; + style[mxConstants.STYLE_FONTCOLOR] = '#774400'; + + return style; +}; + +/** + * Function: createDefaultEdgeStyle + * + * Creates and returns the default edge style. + */ +mxStylesheet.prototype.createDefaultEdgeStyle = function() +{ + var style = new Object(); + + style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_CONNECTOR; + style[mxConstants.STYLE_ENDARROW] = mxConstants.ARROW_CLASSIC; + style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE; + style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER; + style[mxConstants.STYLE_STROKECOLOR] = '#6482B9'; + style[mxConstants.STYLE_FONTCOLOR] = '#446299'; + + return style; +}; + +/** + * Function: putDefaultVertexStyle + * + * Sets the default style for vertices using defaultVertex as the + * stylename. + * + * Parameters: + * style - Key, value pairs that define the style. + */ +mxStylesheet.prototype.putDefaultVertexStyle = function(style) +{ + this.putCellStyle('defaultVertex', style); +}; + +/** + * Function: putDefaultEdgeStyle + * + * Sets the default style for edges using defaultEdge as the stylename. + */ +mxStylesheet.prototype.putDefaultEdgeStyle = function(style) +{ + this.putCellStyle('defaultEdge', style); +}; + +/** + * Function: getDefaultVertexStyle + * + * Returns the default style for vertices. + */ +mxStylesheet.prototype.getDefaultVertexStyle = function() +{ + return this.styles['defaultVertex']; +}; + +/** + * Function: getDefaultEdgeStyle + * + * Sets the default style for edges. + */ +mxStylesheet.prototype.getDefaultEdgeStyle = function() +{ + return this.styles['defaultEdge']; +}; + +/** + * Function: putCellStyle + * + * Stores the given map of key, value pairs under the given name in + * . + * + * Example: + * + * The following example adds a new style called 'rounded' into an + * existing stylesheet: + * + * (code) + * var style = new Object(); + * style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE; + * style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter; + * style[mxConstants.STYLE_ROUNDED] = true; + * graph.getStylesheet().putCellStyle('rounded', style); + * (end) + * + * In the above example, the new style is an object. The possible keys of + * the object are all the constants in that start with STYLE + * and the values are either JavaScript objects, such as + * (which is in fact a function) + * or expressions, such as true. Note that not all keys will be + * interpreted by all shapes (eg. the line shape ignores the fill color). + * The final call to this method associates the style with a name in the + * stylesheet. The style is used in a cell with the following code: + * + * (code) + * model.setStyle(cell, 'rounded'); + * (end) + * + * Parameters: + * + * name - Name for the style to be stored. + * style - Key, value pairs that define the style. + */ +mxStylesheet.prototype.putCellStyle = function(name, style) +{ + this.styles[name] = style; +}; + +/** + * Function: getCellStyle + * + * Returns the cell style for the specified stylename or the given + * defaultStyle if no style can be found for the given stylename. + * + * Parameters: + * + * name - String of the form [(stylename|key=value);] that represents the + * style. + * defaultStyle - Default style to be returned if no style can be found. + */ +mxStylesheet.prototype.getCellStyle = function(name, defaultStyle) +{ + var style = defaultStyle; + + if (name != null && name.length > 0) + { + var pairs = name.split(';'); + + if (style != null && + name.charAt(0) != ';') + { + style = mxUtils.clone(style); + } + else + { + style = new Object(); + } + + // Parses each key, value pair into the existing style + for (var i = 0; i < pairs.length; i++) + { + var tmp = pairs[i]; + var pos = tmp.indexOf('='); + + if (pos >= 0) + { + var key = tmp.substring(0, pos); + var value = tmp.substring(pos + 1); + + if (value == mxConstants.NONE) + { + delete style[key]; + } + else if (mxUtils.isNumeric(value)) + { + style[key] = parseFloat(value); + } + else + { + style[key] = value; + } + } + else + { + // Merges the entries from a named style + var tmpStyle = this.styles[tmp]; + + if (tmpStyle != null) + { + for (var key in tmpStyle) + { + style[key] = tmpStyle[key]; + } + } + } + } + } + + return style; +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxCellState + * + * Represents the current state of a cell in a given . + * + * For edges, the edge label position is stored in . + * + * The size for oversize labels can be retrieved using the boundingBox property + * of the field as shown below. + * + * (code) + * var bbox = (state.text != null) ? state.text.boundingBox : null; + * (end) + * + * Constructor: mxCellState + * + * Constructs a new object that represents the current state of the given + * cell in the specified view. + * + * Parameters: + * + * view - that contains the state. + * cell - that this state represents. + * style - Array of key, value pairs that constitute the style. + */ +function mxCellState(view, cell, style) +{ + this.view = view; + this.cell = cell; + this.style = style; + + this.origin = new mxPoint(); + this.absoluteOffset = new mxPoint(); +}; + +/** + * Extends mxRectangle. + */ +mxCellState.prototype = new mxRectangle(); +mxCellState.prototype.constructor = mxCellState; + +/** + * Variable: view + * + * Reference to the enclosing . + */ +mxCellState.prototype.view = null; + +/** + * Variable: cell + * + * Reference to the that is represented by this state. + */ +mxCellState.prototype.cell = null; + +/** + * Variable: style + * + * Contains an array of key, value pairs that represent the style of the + * cell. + */ +mxCellState.prototype.style = null; + +/** + * Variable: invalid + * + * Specifies if the state is invalid. Default is true. + */ +mxCellState.prototype.invalid = true; + +/** + * Variable: origin + * + * that holds the origin for all child cells. Default is a new + * empty . + */ +mxCellState.prototype.origin = null; + +/** + * Variable: absolutePoints + * + * Holds an array of that represent the absolute points of an + * edge. + */ +mxCellState.prototype.absolutePoints = null; + +/** + * Variable: absoluteOffset + * + * that holds the absolute offset. For edges, this is the + * absolute coordinates of the label position. For vertices, this is the + * offset of the label relative to the top, left corner of the vertex. + */ +mxCellState.prototype.absoluteOffset = null; + +/** + * Variable: visibleSourceState + * + * Caches the visible source terminal state. + */ +mxCellState.prototype.visibleSourceState = null; + +/** + * Variable: visibleTargetState + * + * Caches the visible target terminal state. + */ +mxCellState.prototype.visibleTargetState = null; + +/** + * Variable: terminalDistance + * + * Caches the distance between the end points for an edge. + */ +mxCellState.prototype.terminalDistance = 0; + +/** + * Variable: length + * + * Caches the length of an edge. + */ +mxCellState.prototype.length = 0; + +/** + * Variable: segments + * + * Array of numbers that represent the cached length of each segment of the + * edge. + */ +mxCellState.prototype.segments = null; + +/** + * Variable: shape + * + * Holds the that represents the cell graphically. + */ +mxCellState.prototype.shape = null; + +/** + * Variable: text + * + * Holds the that represents the label of the cell. Thi smay be + * null if the cell has no label. + */ +mxCellState.prototype.text = null; + +/** + * Variable: unscaledWidth + * + * Holds the unscaled width of the state. + */ +mxCellState.prototype.unscaledWidth = null; + +/** + * Function: getPerimeterBounds + * + * Returns the that should be used as the perimeter of the + * cell. + * + * Parameters: + * + * border - Optional border to be added around the perimeter bounds. + * bounds - Optional to be used as the initial bounds. + */ +mxCellState.prototype.getPerimeterBounds = function(border, bounds) +{ + border = border || 0; + bounds = (bounds != null) ? bounds : new mxRectangle(this.x, this.y, this.width, this.height); + + if (this.shape != null && this.shape.stencil != null && this.shape.stencil.aspect == 'fixed') + { + var aspect = this.shape.stencil.computeAspect(this.style, bounds.x, bounds.y, bounds.width, bounds.height); + + bounds.x = aspect.x; + bounds.y = aspect.y; + bounds.width = this.shape.stencil.w0 * aspect.width; + bounds.height = this.shape.stencil.h0 * aspect.height; + } + + if (border != 0) + { + bounds.grow(border); + } + + return bounds; +}; + +/** + * Function: setAbsoluteTerminalPoint + * + * Sets the first or last point in depending on isSource. + * + * Parameters: + * + * point - that represents the terminal point. + * isSource - Boolean that specifies if the first or last point should + * be assigned. + */ +mxCellState.prototype.setAbsoluteTerminalPoint = function(point, isSource) +{ + if (isSource) + { + if (this.absolutePoints == null) + { + this.absolutePoints = []; + } + + if (this.absolutePoints.length == 0) + { + this.absolutePoints.push(point); + } + else + { + this.absolutePoints[0] = point; + } + } + else + { + if (this.absolutePoints == null) + { + this.absolutePoints = []; + this.absolutePoints.push(null); + this.absolutePoints.push(point); + } + else if (this.absolutePoints.length == 1) + { + this.absolutePoints.push(point); + } + else + { + this.absolutePoints[this.absolutePoints.length - 1] = point; + } + } +}; + +/** + * Function: setCursor + * + * Sets the given cursor on the shape and text shape. + */ +mxCellState.prototype.setCursor = function(cursor) +{ + if (this.shape != null) + { + this.shape.setCursor(cursor); + } + + if (this.text != null) + { + this.text.setCursor(cursor); + } +}; + +/** + * Function: getVisibleTerminal + * + * Returns the visible source or target terminal cell. + * + * Parameters: + * + * source - Boolean that specifies if the source or target cell should be + * returned. + */ +mxCellState.prototype.getVisibleTerminal = function(source) +{ + var tmp = this.getVisibleTerminalState(source); + + return (tmp != null) ? tmp.cell : null; +}; + +/** + * Function: getVisibleTerminalState + * + * Returns the visible source or target terminal state. + * + * Parameters: + * + * source - Boolean that specifies if the source or target state should be + * returned. + */ +mxCellState.prototype.getVisibleTerminalState = function(source) +{ + return (source) ? this.visibleSourceState : this.visibleTargetState; +}; + +/** + * Function: setVisibleTerminalState + * + * Sets the visible source or target terminal state. + * + * Parameters: + * + * terminalState - that represents the terminal. + * source - Boolean that specifies if the source or target state should be set. + */ +mxCellState.prototype.setVisibleTerminalState = function(terminalState, source) +{ + if (source) + { + this.visibleSourceState = terminalState; + } + else + { + this.visibleTargetState = terminalState; + } +}; + +/** + * Function: getCellBounds + * + * Returns the unscaled, untranslated bounds. + */ +mxCellState.prototype.getCellBounds = function() +{ + return this.cellBounds; +}; + +/** + * Function: getPaintBounds + * + * Returns the unscaled, untranslated paint bounds. This is the same as + * but with a 90 degree rotation if the shape's + * isPaintBoundsInverted returns true. + */ +mxCellState.prototype.getPaintBounds = function() +{ + return this.paintBounds; +}; + +/** + * Function: updateCachedBounds + * + * Updates the cellBounds and paintBounds. + */ +mxCellState.prototype.updateCachedBounds = function() +{ + var tr = this.view.translate; + var s = this.view.scale; + this.cellBounds = new mxRectangle(this.x / s - tr.x, this.y / s - tr.y, this.width / s, this.height / s); + this.paintBounds = mxRectangle.fromRectangle(this.cellBounds); + + if (this.shape != null && this.shape.isPaintBoundsInverted()) + { + this.paintBounds.rotate90(); + } +}; + +/** + * Destructor: setState + * + * Copies all fields from the given state to this state. + */ +mxCellState.prototype.setState = function(state) +{ + this.view = state.view; + this.cell = state.cell; + this.style = state.style; + this.absolutePoints = state.absolutePoints; + this.origin = state.origin; + this.absoluteOffset = state.absoluteOffset; + this.boundingBox = state.boundingBox; + this.terminalDistance = state.terminalDistance; + this.segments = state.segments; + this.length = state.length; + this.x = state.x; + this.y = state.y; + this.width = state.width; + this.height = state.height; + this.unscaledWidth = state.unscaledWidth; +}; + +/** + * Function: clone + * + * Returns a clone of this . + */ +mxCellState.prototype.clone = function() +{ + var clone = new mxCellState(this.view, this.cell, this.style); + + // Clones the absolute points + if (this.absolutePoints != null) + { + clone.absolutePoints = []; + + for (var i = 0; i < this.absolutePoints.length; i++) + { + clone.absolutePoints[i] = this.absolutePoints[i].clone(); + } + } + + if (this.origin != null) + { + clone.origin = this.origin.clone(); + } + + if (this.absoluteOffset != null) + { + clone.absoluteOffset = this.absoluteOffset.clone(); + } + + if (this.boundingBox != null) + { + clone.boundingBox = this.boundingBox.clone(); + } + + clone.terminalDistance = this.terminalDistance; + clone.segments = this.segments; + clone.length = this.length; + clone.x = this.x; + clone.y = this.y; + clone.width = this.width; + clone.height = this.height; + clone.unscaledWidth = this.unscaledWidth; + + return clone; +}; + +/** + * Destructor: destroy + * + * Destroys the state and all associated resources. + */ +mxCellState.prototype.destroy = function() +{ + this.view.graph.cellRenderer.destroy(this); +}; +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxGraphSelectionModel + * + * Implements the selection model for a graph. Here is a listener that handles + * all removed selection cells. + * + * (code) + * graph.getSelectionModel().addListener(mxEvent.CHANGE, function(sender, evt) + * { + * var cells = evt.getProperty('added'); + * + * for (var i = 0; i < cells.length; i++) + * { + * // Handle cells[i]... + * } + * }); + * (end) + * + * Event: mxEvent.UNDO + * + * Fires after the selection was changed in . The + * edit property contains the which contains the + * . + * + * Event: mxEvent.CHANGE + * + * Fires after the selection changes by executing an . The + * added and removed properties contain arrays of + * cells that have been added to or removed from the selection, respectively. + * The names are inverted due to historic reasons. This cannot be changed. + * + * Constructor: mxGraphSelectionModel + * + * Constructs a new graph selection model for the given . + * + * Parameters: + * + * graph - Reference to the enclosing . + */ +function mxGraphSelectionModel(graph) +{ + this.graph = graph; + this.cells = []; +}; + +/** + * Extends mxEventSource. + */ +mxGraphSelectionModel.prototype = new mxEventSource(); +mxGraphSelectionModel.prototype.constructor = mxGraphSelectionModel; + +/** + * Variable: doneResource + * + * Specifies the resource key for the status message after a long operation. + * If the resource for this key does not exist then the value is used as + * the status message. Default is 'done'. + */ +mxGraphSelectionModel.prototype.doneResource = (mxClient.language != 'none') ? 'done' : ''; + +/** + * Variable: updatingSelectionResource + * + * Specifies the resource key for the status message while the selection is + * being updated. If the resource for this key does not exist then the + * value is used as the status message. Default is 'updatingSelection'. + */ +mxGraphSelectionModel.prototype.updatingSelectionResource = (mxClient.language != 'none') ? 'updatingSelection' : ''; + +/** + * Variable: graph + * + * Reference to the enclosing . + */ +mxGraphSelectionModel.prototype.graph = null; + +/** + * Variable: singleSelection + * + * Specifies if only one selected item at a time is allowed. + * Default is false. + */ +mxGraphSelectionModel.prototype.singleSelection = false; + +/** + * Function: isSingleSelection + * + * Returns as a boolean. + */ +mxGraphSelectionModel.prototype.isSingleSelection = function() +{ + return this.singleSelection; +}; + +/** + * Function: setSingleSelection + * + * Sets the flag. + * + * Parameters: + * + * singleSelection - Boolean that specifies the new value for + * . + */ +mxGraphSelectionModel.prototype.setSingleSelection = function(singleSelection) +{ + this.singleSelection = singleSelection; +}; + +/** + * Function: isSelected + * + * Returns true if the given is selected. + */ +mxGraphSelectionModel.prototype.isSelected = function(cell) +{ + if (cell != null) + { + return mxUtils.indexOf(this.cells, cell) >= 0; + } + + return false; +}; + +/** + * Function: isEmpty + * + * Returns true if no cells are currently selected. + */ +mxGraphSelectionModel.prototype.isEmpty = function() +{ + return this.cells.length == 0; +}; + +/** + * Function: clear + * + * Clears the selection and fires a event if the selection was not + * empty. + */ +mxGraphSelectionModel.prototype.clear = function() +{ + this.changeSelection(null, this.cells); +}; + +/** + * Function: setCell + * + * Selects the specified using . + * + * Parameters: + * + * cell - to be selected. + */ +mxGraphSelectionModel.prototype.setCell = function(cell) +{ + if (cell != null) + { + this.setCells([cell]); + } +}; + +/** + * Function: setCells + * + * Selects the given array of and fires a event. + * + * Parameters: + * + * cells - Array of to be selected. + */ +mxGraphSelectionModel.prototype.setCells = function(cells) +{ + if (cells != null) + { + if (this.singleSelection) + { + cells = [this.getFirstSelectableCell(cells)]; + } + + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + if (this.graph.isCellSelectable(cells[i])) + { + tmp.push(cells[i]); + } + } + + this.changeSelection(tmp, this.cells); + } +}; + +/** + * Function: getFirstSelectableCell + * + * Returns the first selectable cell in the given array of cells. + */ +mxGraphSelectionModel.prototype.getFirstSelectableCell = function(cells) +{ + if (cells != null) + { + for (var i = 0; i < cells.length; i++) + { + if (this.graph.isCellSelectable(cells[i])) + { + return cells[i]; + } + } + } + + return null; +}; + +/** + * Function: addCell + * + * Adds the given to the selection and fires a + * event. + * + * Parameters: + * + * cells - Array of to add to the selection. + */ +mxGraphSelectionModel.prototype.addCells = function(cells) +{ + if (cells != null) + { + var remove = null; + + if (this.singleSelection) + { + remove = this.cells; + cells = [this.getFirstSelectableCell(cells)]; + } + + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + if (!this.isSelected(cells[i]) && + this.graph.isCellSelectable(cells[i])) + { + tmp.push(cells[i]); + } + } + + this.changeSelection(tmp, remove); + } +}; + +/** + * Function: removeCell + * + * Removes the specified from the selection and fires a +
+ +
+ + + diff --git a/seahub/templates/view_file_draw.html b/seahub/templates/view_file_draw.html new file mode 100644 index 0000000000..70a08b7962 --- /dev/null +++ b/seahub/templates/view_file_draw.html @@ -0,0 +1,165 @@ +{% load seahub_tags i18n staticfiles %} +{% load render_bundle from webpack_loader %} + + + + +{{doc_title}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% render_bundle 'draw' %} + + + + + + diff --git a/seahub/templates/view_file_draw_read.html b/seahub/templates/view_file_draw_read.html new file mode 100644 index 0000000000..c39cd37e0d --- /dev/null +++ b/seahub/templates/view_file_draw_read.html @@ -0,0 +1,130 @@ +{% load seahub_tags i18n staticfiles %} +{% load render_bundle from webpack_loader %} + + + + +{{ site_title }} + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +{% render_bundle 'draw' %} + + + diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index a04af6223a..14c2182a80 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -101,6 +101,7 @@ PREVIEW_FILEEXT = { SPREADSHEET: ('xls', 'xlsx', 'ods', 'fods'), SVG: ('svg',), PDF: ('pdf',), + DRAW: ('draw',), MARKDOWN: ('markdown', 'md'), VIDEO: ('mp4', 'ogv', 'webm', 'mov'), AUDIO: ('mp3', 'oga', 'ogg'), diff --git a/seahub/utils/file_types.py b/seahub/utils/file_types.py index c2e78fee01..cb4ad7ef63 100644 --- a/seahub/utils/file_types.py +++ b/seahub/utils/file_types.py @@ -8,3 +8,4 @@ MARKDOWN = 'Markdown' VIDEO = 'Video' AUDIO = 'Audio' SPREADSHEET = 'SpreadSheet' +DRAW = 'Draw' diff --git a/seahub/views/file.py b/seahub/views/file.py index d1945e5b0a..5fa8c215a1 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -59,7 +59,7 @@ from seahub.utils import render_error, is_org_context, \ from seahub.utils.ip import get_remote_ip from seahub.utils.timeutils import utc_to_local from seahub.utils.file_types import (IMAGE, PDF, SVG, - DOCUMENT, SPREADSHEET, AUDIO, MARKDOWN, TEXT, VIDEO) + DOCUMENT, SPREADSHEET, AUDIO, MARKDOWN, TEXT, VIDEO, DRAW) from seahub.utils.star import is_file_starred from seahub.utils.http import json_response, \ BadRequestException, RequestForbbiddenException @@ -314,14 +314,17 @@ def can_preview_file(file_name, file_size, repo): filetype, fileext = get_file_type_and_ext(file_name) - # Seafile defines 9 kinds of filetype: - # TEXT, MARKDOWN, IMAGE, DOCUMENT, SPREADSHEET, VIDEO, AUDIO, PDF, SVG + # Seafile defines 10 kinds of filetype: + # TEXT, MARKDOWN, IMAGE, DOCUMENT, SPREADSHEET, VIDEO, AUDIO, PDF, SVG, DRAW if filetype in (TEXT, MARKDOWN, IMAGE) or fileext in get_conf_text_ext(): if file_size > FILE_PREVIEW_MAX_SIZE: error_msg = _(u'File size surpasses %s, can not be opened online.') % \ filesizeformat(FILE_PREVIEW_MAX_SIZE) return False, error_msg + elif filetype in (DRAW): + pass + elif filetype in (DOCUMENT, SPREADSHEET): if repo.encrypted: @@ -633,6 +636,14 @@ def view_lib_file(request, repo_id, path): return_dict['raw_path'] = raw_path return render(request, template, return_dict) + elif filetype == DRAW: + return_dict['raw_path'] = raw_path + if permission == 'r': + template = 'view_file_draw_read.html' + return render(request, template, return_dict) + else: + return render(request, template, return_dict) + elif filetype == IMAGE: if file_size > FILE_PREVIEW_MAX_SIZE: error_msg = _(u'File size surpasses %s, can not be opened online.') % \