UNPKG

drawio-offline

Version:
1,872 lines (1,617 loc) 411 kB
/** * Copyright (c) 2006-2017, JGraph Ltd * Copyright (c) 2006-2017, Gaudenz Alder */ (function() { /** * Version */ EditorUi.VERSION = '@DRAWIO-VERSION@'; /** * Overrides compact UI setting. */ EditorUi.compactUi = uiTheme != 'atlas'; /** * Overrides default grid color for dark mode */ if (Editor.isDarkMode()) { mxGraphView.prototype.gridColor = mxGraphView.prototype.defaultDarkGridColor; } /** * Switch to disable logging for mode and search terms. */ EditorUi.enableLogging = urlParams['stealth'] != '1' && urlParams['lockdown'] != '1' && (/.*\.draw\.io$/.test(window.location.hostname) || /.*\.diagrams\.net$/.test(window.location.hostname)) && window.location.hostname != 'support.draw.io'; /** * Protocol and hostname to use for embedded files. Default is https://www.draw.io */ EditorUi.drawHost = window.DRAWIO_BASE_URL; /** * Protocol and hostname to use for embedded files. Default is https://www.draw.io */ EditorUi.lightboxHost = window.DRAWIO_LIGHTBOX_URL; /** * Switch to disable logging for mode and search terms. */ EditorUi.lastErrorMessage = null; /** * Switch to disable logging for mode and search terms. */ EditorUi.ignoredAnonymizedChars = '\n\t`~!@#$%^&*()_+{}|:"<>?-=[]\;\'.\/,\n\t'; /** * Specifies the URL for the templates index file. */ EditorUi.templateFile = TEMPLATE_PATH + '/index.xml'; /** * Specifies the URL for the diffsync cache. */ EditorUi.cacheUrl = (urlParams['dev'] == '1') ? '/cache' : window.REALTIME_URL; if (EditorUi.cacheUrl == null && typeof DrawioFile !== 'undefined') { DrawioFile.SYNC = 'none'; //Disable real-time sync } /** * Cache timeout is 10 seconds. */ Editor.cacheTimeout = 10000; /** * Switch to enable PlantUML in the insert from text dialog. * NOTE: This must also be enabled on the server-side. */ EditorUi.enablePlantUml = EditorUi.enableLogging; /** * https://github.com/electron/electron/issues/2288 */ EditorUi.isElectronApp = window != null && window.process != null && window.process.versions != null && window.process.versions['electron'] != null; /** * Shortcut for capability check. */ EditorUi.nativeFileSupport = !mxClient.IS_OP && !EditorUi.isElectronApp && 'showSaveFilePicker' in window && 'showOpenFilePicker' in window; /** * Specifies if drafts should be saved in IndexedDB. */ EditorUi.enableDrafts = !mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp && isLocalStorage && urlParams['drafts'] != '0'; /** * Link for scratchpad help. */ EditorUi.scratchpadHelpLink = 'https://www.diagrams.net/doc/faq/scratchpad'; /** * Default Mermaid config without using foreign objects in flowcharts. */ EditorUi.defaultMermaidConfig = { theme:'neutral', arrowMarkerAbsolute:false, flowchart: { htmlLabels:false }, sequence: { diagramMarginX:50, diagramMarginY:10, actorMargin:50, width:150, height:65, boxMargin:10, boxTextMargin:5, noteMargin:10, messageMargin:35, mirrorActors:true, bottomMarginAdj:1, useMaxWidth:true, rightAngles:false, showSequenceNumbers:false }, gantt:{ titleTopMargin:25, barHeight:20, barGap:4, topPadding:50, leftPadding:75, gridLineStartPadding:35, fontSize:11, fontFamily:'"Open-Sans", "sans-serif"', numberSectionStyles:4, axisFormat:'%Y-%m-%d' } }; /** * Updates action states depending on the selection. */ EditorUi.logError = function(message, url, linenumber, colno, err, severity, quiet) { severity = ((severity != null) ? severity : (message.indexOf('NetworkError') >= 0 || message.indexOf('SecurityError') >= 0 || message.indexOf('NS_ERROR_FAILURE') >= 0 || message.indexOf('out of memory') >= 0) ? 'CONFIG' : 'SEVERE'); if (EditorUi.enableLogging && urlParams['dev'] != '1') { try { if (message == EditorUi.lastErrorMessage || (message != null && url != null && ((message.indexOf('Script error') != -1) || (message.indexOf('extension') != -1)))) { // TODO log external domain script failure "Script error." is // reported when the error occurs in a script that is hosted // on a domain other than the domain of the current page } // DocumentClosedError seems to be an FF bug an can be ignored for now else if (message != null && message.indexOf('DocumentClosedError') < 0) { EditorUi.lastErrorMessage = message; var logDomain = window.DRAWIO_LOG_URL != null ? window.DRAWIO_LOG_URL : ''; err = (err != null) ? err : new Error(message); var img = new Image(); img.src = logDomain + '/log?severity=' + severity + '&v=' + encodeURIComponent(EditorUi.VERSION) + '&msg=clientError:' + encodeURIComponent(message) + ':url:' + encodeURIComponent(window.location.href) + ':lnum:' + encodeURIComponent(linenumber) + ((colno != null) ? ':colno:' + encodeURIComponent(colno) : '') + ((err != null && err.stack != null) ? '&stack=' + encodeURIComponent(err.stack) : ''); } } catch (e) { // do nothing } } try { if (!quiet && window.console != null) { console.error(severity, message, url, linenumber, colno, err); } } catch (e) { // ignore } }; /** * Updates action states depending on the selection. */ EditorUi.logEvent = function(data) { if (urlParams['dev'] == '1') { EditorUi.debug('logEvent', data); } else if (EditorUi.enableLogging) { try { var logDomain = window.DRAWIO_LOG_URL != null ? window.DRAWIO_LOG_URL : ''; var img = new Image(); img.src = logDomain + '/images/1x1.png?' + 'v=' + encodeURIComponent(EditorUi.VERSION) + ((data != null) ? '&data=' + encodeURIComponent(JSON.stringify(data)) : ''); } catch (e) { // ignore } } }; /** * Sending error reports. */ EditorUi.sendReport = function(data, maxLength) { if (urlParams['dev'] == '1') { EditorUi.debug('sendReport', data); } else if (EditorUi.enableLogging) { try { maxLength = (maxLength != null) ? maxLength : 50000; if (data.length > maxLength) { data = data.substring(0, maxLength) + '\n...[SHORTENED]' } mxUtils.post('/email', 'version=' + encodeURIComponent(EditorUi.VERSION) + '&url=' + encodeURIComponent(window.location.href) + '&data=' + encodeURIComponent(data)); } catch (e) { // ignore } } }; /** * Adds the listener for automatically saving the diagram for local changes. */ EditorUi.debug = function() { try { if (window.console != null && urlParams['test'] == '1') { var args = [new Date().toISOString()]; for (var i = 0; i < arguments.length; i++) { if (arguments[i] != null) { args.push(arguments[i]); } } console.log.apply(console, args); } } catch (e) { // ignore } }; /** * Static method for pasing PNG files. */ EditorUi.parsePng = function(f, fn, error) { var pos = 0; function fread(d, count) { var start = pos; pos += count; return d.substring(start, pos); }; // Reads unsigned long 32 bit big endian function _freadint(d) { var bytes = fread(d, 4); return bytes.charCodeAt(3) + (bytes.charCodeAt(2) << 8) + (bytes.charCodeAt(1) << 16) + (bytes.charCodeAt(0) << 24); }; // Checks signature if (fread(f,8) != String.fromCharCode(137) + 'PNG' + String.fromCharCode(13, 10, 26, 10)) { if (error != null) { error(); } return; } // Reads header chunk fread(f,4); if (fread(f,4) != 'IHDR') { if (error != null) { error(); } return; } fread(f, 17); do { var n = _freadint(f); var type = fread(f,4); if (fn != null) { if (fn(pos - 8, type, n)) { break; } } value = fread(f,n); fread(f,4); if (type == 'IEND') { break; } } while (n); }; /** * Removes any values, styles and geometries from the given XML node. */ EditorUi.removeChildNodes = function(node) { while (node.firstChild != null) { node.removeChild(node.firstChild); } }; /** * Contains the default XML for an empty diagram. */ EditorUi.prototype.emptyDiagramXml = '<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel>'; /** * */ EditorUi.prototype.emptyLibraryXml = '<mxlibrary>[]</mxlibrary>'; /** * Sets the delay for autosave in milliseconds. Default is 2000. */ EditorUi.prototype.mode = null; /** * General timeout is 25 seconds. * LATER: Move to Editor */ EditorUi.prototype.timeout = Editor.prototype.timeout; /** * Allows for two buttons in the sidebar footer. */ EditorUi.prototype.sidebarFooterHeight = 38; /** * Specifies the default custom shape style. */ EditorUi.prototype.defaultCustomShapeStyle = 'shape=stencil(tZRtTsQgEEBPw1+DJR7AoN6DbWftpAgE0Ortd/jYRGq72R+YNE2YgTePloEJGWblgA18ZuKFDcMj5/Sm8boZq+BgjCX4pTyqk6ZlKROitwusOMXKQDODx5iy4pXxZ5qTHiFHawxB0JrQZH7lCabQ0Fr+XWC1/E8zcsT/gAi+Subo2/3Mh6d/oJb5nU1b5tW7r2knautaa3T+U32o7f7vZwpJkaNDLORJjcu7t59m2jXxqX9un+tt022acsfmoKaQZ+vhhswZtS6Ne/ThQGt0IV0N3Yyv6P3CeT9/tHO0XFI5cAE=);whiteSpace=wrap;html=1;'; /** * Defines the maximum size for images. */ EditorUi.prototype.maxBackgroundSize = 1600; /** * Defines the maximum size for images. */ EditorUi.prototype.maxImageSize = 520; /** * Defines the maximum width for pasted text. * Use 0 to disable check. */ EditorUi.prototype.maxTextWidth = 520; /** * Images above 100K should be resampled. */ EditorUi.prototype.resampleThreshold = 100000; /** * Maximum allowed size for images is 1 MB. */ EditorUi.prototype.maxImageBytes = 1000000; /** * Maximum size for background images is 2.5 MB. */ EditorUi.prototype.maxBackgroundBytes = 2500000; /** * Maximum size for text files in labels is 0.5 MB. */ EditorUi.prototype.maxTextBytes = 500000; /** * Holds the current file. */ EditorUi.prototype.currentFile = null; /** * Specifies if PDF export should be done via print dialog. Default is * false which uses the PhantomJS backend to create the PDF. */ EditorUi.prototype.printPdfExport = false; /** * Specifies if PDF export with pages is enabled. */ EditorUi.prototype.pdfPageExport = true; /** * Restores app defaults for UI */ EditorUi.prototype.formatEnabled = urlParams['format'] != '0'; /** * Whether template action should be shown in insert menu. */ EditorUi.prototype.insertTemplateEnabled = true; /** * Restores app defaults for UI */ EditorUi.prototype.closableScratchpad = true; /** * Capability check for canvas export */ (function() { EditorUi.prototype.useCanvasForExport = false; EditorUi.prototype.jpgSupported = false; // Checks if canvas is supported try { var cnv = document.createElement('canvas'); EditorUi.prototype.canvasSupported = !!(cnv.getContext && cnv.getContext('2d')); } catch (e) { // ignore } try { var canvas = document.createElement('canvas'); var img = new Image(); // LATER: Capability check should not be async img.onload = function() { try { var ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); // Works in Chrome, Firefox, Edge, Safari and Opera var result = canvas.toDataURL('image/png'); EditorUi.prototype.useCanvasForExport = result != null && result.length > 6; } catch (e) { // ignore } }; // Checks if SVG with foreignObject can be exported var svg = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1px" height="1px" version="1.1"><foreignObject pointer-events="all" width="1" height="1"><div xmlns="http://www.w3.org/1999/xhtml"></div></foreignObject></svg>'; img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg))); } catch (e) { // ignore } // Checks for client-side JPG support try { var canvas = document.createElement('canvas'); canvas.width = canvas.height = 1; var uri = canvas.toDataURL('image/jpeg'); EditorUi.prototype.jpgSupported = (uri.match('image/jpeg') !== null); } catch (e) { // ignore } })(); /** * Hook for subclassers. */ EditorUi.prototype.openLink = function(url, target, allowOpener) { // LATER: Replace this with direct calls to graph return this.editor.graph.openLink(url, target, allowOpener); }; /** * Hook for subclassers. */ EditorUi.prototype.showSplash = function(force) { }; /** * Abstraction for local storage access. */ EditorUi.prototype.getLocalData = function(key, fn) { fn(localStorage.getItem(key)); }; /** * Abstraction for local storage access. */ EditorUi.prototype.setLocalData = function(key, data, fn) { localStorage.setItem(key, data); if (fn != null) { fn(); } }; /** * Abstraction for local storage access. */ EditorUi.prototype.removeLocalData = function(key, fn) { localStorage.removeItem(key) fn(); }; EditorUi.prototype.setMathEnabled = function(value) { this.editor.graph.mathEnabled = value; this.editor.updateGraphComponents(); this.editor.graph.refresh(); this.fireEvent(new mxEventObject('mathEnabledChanged')); }; EditorUi.prototype.isMathEnabled = function(value) { return this.editor.graph.mathEnabled; }; /** * Returns true if offline app, which isn't a defined thing */ EditorUi.prototype.isOfflineApp = function() { return urlParams['offline'] == '1'; }; /** * Returns true if no external comms allowed or possible */ EditorUi.prototype.isOffline = function(ignoreStealth) { return this.isOfflineApp() || !navigator.onLine || (!ignoreStealth && (urlParams['stealth'] == '1' || urlParams['lockdown'] == '1')); }; /** * 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.createSpinner = function(x, y, size) { var autoPosition = (x == null || y == null); size = (size != null) ? size : 24; var spinner = new Spinner({ lines: 12, // The number of lines to draw length: size, // The length of each line width: Math.round(size / 3), // The line thickness radius: Math.round(size / 2), // The radius of the inner circle rotate: 0, // The rotation offset color: (Editor.isDarkMode()) ? '#c0c0c0' : '#000', // #rgb or #rrggbb speed: 1.5, // Rounds per second trail: 60, // Afterglow percentage shadow: false, // Whether to render a shadow hwaccel: false, // Whether to use hardware acceleration zIndex: 2e9 // The z-index (defaults to 2000000000) }); // Extends spin method to include an optional label var oldSpin = spinner.spin; spinner.spin = function(container, label) { var result = false; if (!this.active) { oldSpin.call(this, container); this.active = true; if (label != null) { if (autoPosition) { y = Math.max(document.body.clientHeight || 0, document.documentElement.clientHeight || 0) / 2; x = document.body.clientWidth / 2 - 2; } var status = document.createElement('div'); status.style.position = 'absolute'; status.style.whiteSpace = 'nowrap'; status.style.background = '#4B4243'; status.style.color = 'white'; status.style.fontFamily = 'Helvetica, Arial'; status.style.fontSize = '9pt'; status.style.padding = '6px'; status.style.paddingLeft = '10px'; status.style.paddingRight = '10px'; status.style.zIndex = 2e9; status.style.left = Math.max(0, x) + 'px'; status.style.top = Math.max(0, y + 70) + 'px'; mxUtils.setPrefixedStyle(status.style, 'borderRadius', '6px'); mxUtils.setPrefixedStyle(status.style, 'transform', 'translate(-50%,-50%)'); if (!Editor.isDarkMode()) { mxUtils.setPrefixedStyle(status.style, 'boxShadow', '2px 2px 3px 0px #ddd'); } if (label.substring(label.length - 3, label.length) != '...' && label.charAt(label.length - 1) != '!') { label = label + '...'; } status.innerHTML = label; container.appendChild(status); spinner.status = status; } // Pause returns a function to resume the spinner this.pause = mxUtils.bind(this, function() { var fn = function() { }; if (this.active) { fn = mxUtils.bind(this, function() { this.spin(container, label); }); } this.stop(); return fn; }); result = true; } return result; }; // Extends stop method to remove the optional label var oldStop = spinner.stop; spinner.stop = function() { oldStop.call(this); this.active = false; if (spinner.status != null && spinner.status.parentNode != null) { spinner.status.parentNode.removeChild(spinner.status); } spinner.status = null; }; spinner.pause = function() { return function() {}; }; return spinner; }; /** * Returns true if the given string contains a compatible graph model. */ EditorUi.prototype.isCompatibleString = function(data) { try { var doc = mxUtils.parseXml(data); var node = this.editor.extractGraphModel(doc.documentElement, true); return node != null && node.getElementsByTagName('parsererror').length == 0; } catch (e) { // ignore } return false; }; /** * Returns true if the given binary data is a Visio file. */ EditorUi.prototype.isVisioData = function(data) { return data.length > 8 && ((data.charCodeAt(0) == 0xD0 && data.charCodeAt(1) == 0xCF && data.charCodeAt(2) == 0x11 && data.charCodeAt(3) == 0xE0 && data.charCodeAt(4) == 0xA1 && data.charCodeAt(5) == 0xB1 && data.charCodeAt(6) == 0x1A && data.charCodeAt(7) == 0xE1) || (data.charCodeAt(0) == 0x50 && data.charCodeAt(1) == 0x4B && data.charCodeAt(2) == 0x03 && data.charCodeAt(3) == 0x04) || (data.charCodeAt(0) == 0x50 && data.charCodeAt(1) == 0x4B && data.charCodeAt(2) == 0x03 && data.charCodeAt(3) == 0x06)); }; /** * Returns true if the given binary data is a Visio file that requires remote conversion. * This code returns true for vss, vsd and vdx files. */ EditorUi.prototype.isRemoteVisioData = function(data) { return data.length > 8 && ((data.charCodeAt(0) == 0xD0 && data.charCodeAt(1) == 0xCF && data.charCodeAt(2) == 0x11 && data.charCodeAt(3) == 0xE0 && data.charCodeAt(4) == 0xA1 && data.charCodeAt(5) == 0xB1 && data.charCodeAt(6) == 0x1A && data.charCodeAt(7) == 0xE1) || (data.charCodeAt(0) == 0x3C && data.charCodeAt(1) == 0x3F && data.charCodeAt(2) == 0x78 && data.charCodeAt(3) == 0x6D && data.charCodeAt(3) == 0x6C)); }; /** * Returns true if the given binary data is a PNG file. */ EditorUi.prototype.isPngData = function(data) { return data.length > 8 && data.charCodeAt(0) == 137 && data.charCodeAt(1) == 80 && data.charCodeAt(2) == 78 && data.charCodeAt(3) == 71 && data.charCodeAt(4) == 13 && data.charCodeAt(5) == 10 && data.charCodeAt(6) == 26 && data.charCodeAt(7) == 10; }; /** * Adds keyboard shortcuts for page handling. */ var editorUiCreateKeyHandler = EditorUi.prototype.createKeyHandler; EditorUi.prototype.createKeyHandler = function(editor) { var keyHandler = editorUiCreateKeyHandler.apply(this, arguments); if (!this.editor.chromeless || this.editor.editable) { var keyHandlerGetFunction = keyHandler.getFunction; var graph = this.editor.graph; var ui = this; keyHandler.getFunction = function(evt) { if (graph.isSelectionEmpty() && ui.pages != null && ui.pages.length > 0) { var idx = ui.getSelectedPageIndex(); if (mxEvent.isShiftDown(evt)) { if (evt.keyCode == 37) { return function() { if (idx > 0) { ui.movePage(idx, idx - 1); } }; } else if (evt.keyCode == 38) { return function() { if (idx > 0) { ui.movePage(idx, 0); } }; } else if (evt.keyCode == 39) { return function() { if (idx < ui.pages.length - 1) { ui.movePage(idx, idx + 1); } }; } else if (evt.keyCode == 40) { return function() { if (idx < ui.pages.length - 1) { ui.movePage(idx, ui.pages.length - 1); } }; } } else if (mxEvent.isControlDown(evt) || (mxClient.IS_MAC && mxEvent.isMetaDown(evt))) { if (evt.keyCode == 37) { return function() { if (idx > 0) { ui.selectNextPage(false); } }; } else if (evt.keyCode == 38) { return function() { if (idx > 0) { ui.selectPage(ui.pages[0]); } }; } else if (evt.keyCode == 39) { return function() { if (idx < ui.pages.length - 1) { ui.selectNextPage(true); } }; } else if (evt.keyCode == 40) { return function() { if (idx < ui.pages.length - 1) { ui.selectPage(ui.pages[ui.pages.length - 1]); } }; } } } return keyHandlerGetFunction.apply(this, arguments); }; } return keyHandler; }; /** * Extracts the mxfile from the given HTML data from a data transfer event. */ var editorUiExtractGraphModelFromHtml = EditorUi.prototype.extractGraphModelFromHtml; EditorUi.prototype.extractGraphModelFromHtml = function(data) { var result = editorUiExtractGraphModelFromHtml.apply(this, arguments); if (result == null) { try { var idx = data.indexOf('&lt;mxfile '); if (idx >= 0) { var idx2 = data.lastIndexOf('&lt;/mxfile&gt;'); if (idx2 > idx) { result = data.substring(idx, idx2 + 15).replace(/&gt;/g, '>'). replace(/&lt;/g, '<').replace(/\\&quot;/g, '"').replace(/\n/g, ''); } } else { // Gets compressed data from mxgraph element in HTML document var doc = mxUtils.parseXml(data); var node = this.editor.extractGraphModel(doc.documentElement, this.pages != null || this.diagramContainer.style.visibility == 'hidden'); result = (node != null) ? mxUtils.getXml(node) : ''; } } catch (e) { // ignore } } return result; }; /** * Workaround for malformed xhtml meta element bug 07.08.16. The trailing slash was missing causing * reopen to fail trying to parse. Used in replaceFileData, setFileData and importFile. */ EditorUi.prototype.validateFileData = function(data) { if (data != null && data.length > 0) { var index = data.indexOf('<meta charset="utf-8">'); if (index >= 0) { var replaceString = '<meta charset="utf-8"/>'; var replaceStrLen = replaceString.length; data = data.slice(0, index) + replaceString + data.slice(index + replaceStrLen - 1, data.length); } data = Graph.zapGremlins(data); } return data; }; /** * */ EditorUi.prototype.replaceFileData = function(data) { data = this.validateFileData(data); var node = (data != null && data.length > 0) ? mxUtils.parseXml(data).documentElement : null; // Some nodes must be extracted here to find the mxfile node // LATER: Remove duplicate call to extractGraphModel in overridden setGraphXml var tmp = (node != null) ? this.editor.extractGraphModel(node, true) : null; if (tmp != null) { node = tmp; } if (node != null) { var graph = this.editor.graph; graph.model.beginUpdate(); try { var oldPages = (this.pages != null) ? this.pages.slice() : null; var nodes = node.getElementsByTagName('diagram'); if (urlParams['pages'] != '0' || nodes.length > 1 || (nodes.length == 1 && nodes[0].hasAttribute('name'))) { this.fileNode = node; this.pages = (this.pages != null) ? this.pages : []; // Wraps page nodes for (var i = nodes.length - 1; i >= 0; i--) { var page = this.updatePageRoot(new DiagramPage(nodes[i])); // Checks for invalid page names if (page.getName() == null) { page.setName(mxResources.get('pageWithNumber', [i + 1])); } graph.model.execute(new ChangePage(this, page, (i == 0) ? page : null, 0)); } } else { // Creates tabbed file structure if enforced by URL if (urlParams['pages'] != '0' && this.fileNode == null) { this.fileNode = node.ownerDocument.createElement('mxfile'); this.currentPage = new DiagramPage(node.ownerDocument.createElement('diagram')); this.currentPage.setName(mxResources.get('pageWithNumber', [1])); graph.model.execute(new ChangePage(this, this.currentPage, this.currentPage, 0)); } // Avoids scroll offset when switching page this.editor.setGraphXml(node); // Avoids duplicate parsing of the XML stored in the node if (this.currentPage != null) { this.currentPage.root = this.editor.graph.model.root; } } if (oldPages != null) { for (var i = 0; i < oldPages.length; i++) { graph.model.execute(new ChangePage(this, oldPages[i], null)); } } } finally { graph.model.endUpdate(); } } }; /** * 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.createFileData = function(node, graph, file, url, forceXml, forceSvg, forceHtml, embeddedCallback, ignoreSelection, compact, uncompressed) { graph = (graph != null) ? graph : this.editor.graph; forceXml = (forceXml != null) ? forceXml : false; ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; var editLink = null; var redirect = null; if (file == null || file.getMode() == App.MODE_DEVICE || file.getMode() == App.MODE_BROWSER) { editLink = '_blank'; } else { editLink = url; redirect = editLink; } if (node == null) { return ''; } else { var fileNode = node; // Ignores case for possible HTML or XML nodes if (fileNode.nodeName.toLowerCase() != 'mxfile') { if (uncompressed) { var diagramNode = node.ownerDocument.createElement('diagram'); diagramNode.setAttribute('id', Editor.guid()); diagramNode.appendChild(node); fileNode = node.ownerDocument.createElement('mxfile'); fileNode.appendChild(diagramNode); } else { // Removes control chars in input for correct roundtrip check var text = Graph.zapGremlins(mxUtils.getXml(node)); var data = Graph.compress(text); // Fallback to plain XML for invalid compression // TODO: Remove this fallback with active pages if (Graph.decompress(data) != text) { return text; } else { var diagramNode = node.ownerDocument.createElement('diagram'); diagramNode.setAttribute('id', Editor.guid()); mxUtils.setTextContent(diagramNode, data); fileNode = node.ownerDocument.createElement('mxfile'); fileNode.appendChild(diagramNode); } } } if (!compact) { // Removes old metadata fileNode.removeAttribute('userAgent'); fileNode.removeAttribute('version'); fileNode.removeAttribute('editor'); fileNode.removeAttribute('pages'); fileNode.removeAttribute('type'); if (mxClient.IS_CHROMEAPP) { fileNode.setAttribute('host', 'Chrome'); } else if (EditorUi.isElectronApp) { fileNode.setAttribute('host', 'Electron'); } else { fileNode.setAttribute('host', window.location.hostname); } // Adds new metadata fileNode.setAttribute('modified', new Date().toISOString()); fileNode.setAttribute('agent', navigator.appVersion); fileNode.setAttribute('version', EditorUi.VERSION); fileNode.setAttribute('etag', Editor.guid()); var md = (file != null) ? file.getMode() : this.mode; if (md != null) { fileNode.setAttribute('type', md); } if (fileNode.getElementsByTagName('diagram').length > 1 && this.pages != null) { fileNode.setAttribute('pages', this.pages.length); } } else { fileNode = fileNode.cloneNode(true); fileNode.removeAttribute('modified'); fileNode.removeAttribute('host'); fileNode.removeAttribute('agent'); fileNode.removeAttribute('etag'); fileNode.removeAttribute('userAgent'); fileNode.removeAttribute('version'); fileNode.removeAttribute('editor'); fileNode.removeAttribute('type'); } var xml = (uncompressed) ? mxUtils.getPrettyXml(fileNode) : mxUtils.getXml(fileNode); // Writes the file as an embedded HTML file if (!forceSvg && !forceXml && (forceHtml || (file != null && /(\.html)$/i.test(file.getTitle())))) { xml = this.getHtml2(mxUtils.getXml(fileNode), graph, (file != null) ? file.getTitle() : null, editLink, redirect); } // Maps the XML data to the content attribute in the SVG node else if (forceSvg || (!forceXml && file != null && /(\.svg)$/i.test(file.getTitle()))) { if (file != null && (file.getMode() == App.MODE_DEVICE || file.getMode() == App.MODE_BROWSER)) { url = null; } xml = this.getEmbeddedSvg(xml, graph, url, null, embeddedCallback, ignoreSelection, redirect); } return xml; } }; /** * 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.getXmlFileData = function(ignoreSelection, currentPage, uncompressed) { ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; currentPage = (currentPage != null) ? currentPage : false; uncompressed = (uncompressed != null) ? uncompressed : !Editor.compressXml; // Generats graph model XML node for single page export var node = this.editor.getGraphXml(ignoreSelection); if (ignoreSelection && this.fileNode != null && this.currentPage != null) { // Updates current page XML if selection is ignored EditorUi.removeChildNodes(this.currentPage.node); mxUtils.setTextContent(this.currentPage.node, Graph.compressNode(node)); // Creates a clone of the file node for processing node = this.fileNode.cloneNode(false); // Appends the node of the page and applies compression function appendPage(pageNode) { var models = pageNode.getElementsByTagName('mxGraphModel'); var modelNode = (models.length > 0) ? models[0] : null; var clone = pageNode; if (modelNode == null && uncompressed) { var text = mxUtils.trim(mxUtils.getTextContent(pageNode)); clone = pageNode.cloneNode(false); if (text.length > 0) { var tmp = Graph.decompress(text); if (tmp != null && tmp.length > 0) { clone.appendChild(mxUtils.parseXml(tmp).documentElement); } } } else if (modelNode != null && !uncompressed) { clone = pageNode.cloneNode(false); mxUtils.setTextContent(clone, Graph.compressNode(modelNode)); } else { clone = pageNode.cloneNode(true); } node.appendChild(clone); }; if (currentPage) { appendPage(this.currentPage.node); } else { // Restores order of pages for (var i = 0; i < this.pages.length; i++) { if (this.currentPage != this.pages[i]) { if (this.pages[i].needsUpdate) { var enc = new mxCodec(mxUtils.createXmlDocument()); var temp = enc.encode(new mxGraphModel(this.pages[i].root)); this.editor.graph.saveViewState(this.pages[i].viewState, temp); EditorUi.removeChildNodes(this.pages[i].node); mxUtils.setTextContent(this.pages[i].node, Graph.compressNode(temp)); // Marks the page as up-to-date delete this.pages[i].needsUpdate; } } appendPage(this.pages[i].node); } } } return node; }; /** * Removes any values, styles and geometries from the given XML node. */ EditorUi.prototype.anonymizeString = function(text, zeros) { var result = []; for (var i = 0; i < text.length; i++) { var c = text.charAt(i); if (EditorUi.ignoredAnonymizedChars.indexOf(c) >= 0) { result.push(c); } else if (!isNaN(parseInt(c))) { result.push((zeros) ? '0' : Math.round(Math.random() * 9)); } else if (c.toLowerCase() != c) { result.push(String.fromCharCode(65 + Math.round(Math.random() * 25))); } else if (c.toUpperCase() != c) { result.push(String.fromCharCode(97 + Math.round(Math.random() * 25))); } else if (/\s/.test(c)) { /* any whitespace */ result.push(' '); } else { result.push('?'); } } return result.join(''); }; /** * Removes any values, styles and geometries from the given XML node. */ EditorUi.prototype.anonymizePatch = function(patch) { if (patch[EditorUi.DIFF_INSERT] != null) { for (var i = 0; i < patch[EditorUi.DIFF_INSERT].length; i++) { try { var data = patch[EditorUi.DIFF_INSERT][i].data; var doc = mxUtils.parseXml(data); var clone = doc.documentElement.cloneNode(false); if (clone.getAttribute('name') != null) { clone.setAttribute('name', this.anonymizeString(clone.getAttribute('name'))); } patch[EditorUi.DIFF_INSERT][i].data = mxUtils.getXml(clone); } catch (e) { patch[EditorUi.DIFF_INSERT][i].data = e.message; } } } if (patch[EditorUi.DIFF_UPDATE] != null) { for (var pageId in patch[EditorUi.DIFF_UPDATE]) { var diff = patch[EditorUi.DIFF_UPDATE][pageId]; if (diff.name != null) { diff.name = this.anonymizeString(diff.name); } if (diff.cells != null) { var anonymizeCellDiffs = mxUtils.bind(this, function(key) { var cellDiffs = diff.cells[key]; if (cellDiffs != null) { for (var cellId in cellDiffs) { if (cellDiffs[cellId].value != null) { cellDiffs[cellId].value = '[' + cellDiffs[cellId].value.length + ']'; } if (cellDiffs[cellId].xmlValue != null) { cellDiffs[cellId].xmlValue = '[' + cellDiffs[cellId].xmlValue.length + ']'; } if (cellDiffs[cellId].style != null) { cellDiffs[cellId].style = '[' + cellDiffs[cellId].style.length + ']'; } if (Object.keys(cellDiffs[cellId]).length == 0) { delete cellDiffs[cellId]; } } if (Object.keys(cellDiffs).length == 0) { delete diff.cells[key]; } } }); anonymizeCellDiffs(EditorUi.DIFF_INSERT); anonymizeCellDiffs(EditorUi.DIFF_UPDATE); if (Object.keys(diff.cells).length == 0) { delete diff.cells; } } if (Object.keys(diff).length == 0) { delete patch[EditorUi.DIFF_UPDATE][pageId]; } } if (Object.keys(patch[EditorUi.DIFF_UPDATE]).length == 0) { delete patch[EditorUi.DIFF_UPDATE]; } } return patch; }; /** * Removes any values, styles and geometries from the given XML node. */ EditorUi.prototype.anonymizeAttributes = function(node, zeros) { if (node.attributes != null) { for (var i = 0; i < node.attributes.length; i++) { if (node.attributes[i].name != 'as') { node.setAttribute(node.attributes[i].name, this.anonymizeString(node.attributes[i].value, zeros)); } } } if (node.childNodes != null) { for (var i = 0; i < node.childNodes.length; i++) { this.anonymizeAttributes(node.childNodes[i], zeros); } } }; /** * Removes any values, styles and geometries from the given XML node. */ EditorUi.prototype.anonymizeNode = function(node, zeros) { var nodes = node.getElementsByTagName('mxCell'); for (var i = 0; i < nodes.length; i++) { if (nodes[i].getAttribute('value') != null) { nodes[i].setAttribute('value', '[' + nodes[i].getAttribute('value').length + ']'); } if (nodes[i].getAttribute('xmlValue') != null) { nodes[i].setAttribute('xmlValue', '[' + nodes[i].getAttribute('xmlValue').length + ']'); } if (nodes[i].getAttribute('style') != null) { nodes[i].setAttribute('style', '[' + nodes[i].getAttribute('style').length + ']'); } if (nodes[i].parentNode != null && nodes[i].parentNode.nodeName != 'root' && nodes[i].parentNode.parentNode != null) { nodes[i].setAttribute('id', nodes[i].parentNode.getAttribute('id')); nodes[i].parentNode.parentNode.replaceChild(nodes[i], nodes[i].parentNode); } } return node; }; /** * 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.synchronizeCurrentFile = function(forceReload) { var currentFile = this.getCurrentFile(); if (currentFile != null) { if (currentFile.savingFile) { this.handleError({message: mxResources.get('busy')}); } else if (!forceReload && currentFile.invalidChecksum) { currentFile.handleFileError(null, true); } else if (this.spinner.spin(document.body, mxResources.get('updatingDocument'))) { currentFile.clearAutosave(); this.editor.setStatus(''); if (forceReload) { currentFile.reloadFile(mxUtils.bind(this, function() { currentFile.handleFileSuccess(DrawioFile.SYNC == 'manual'); }), mxUtils.bind(this, function(err) { currentFile.handleFileError(err, true); })); } else { currentFile.synchronizeFile(mxUtils.bind(this, function() { currentFile.handleFileSuccess(DrawioFile.SYNC == 'manual'); }), mxUtils.bind(this, function(err) { currentFile.handleFileError(err, true); })); } } } }; /** * 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.getFileData = function(forceXml, forceSvg, forceHtml, embeddedCallback, ignoreSelection, currentPage, node, compact, file, uncompressed) { ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; currentPage = (currentPage != null) ? currentPage : false; var graph = this.editor.graph; // Forces compression of embedded XML if (forceSvg || (!forceXml && file != null && /(\.svg)$/i.test(file.getTitle()))) { uncompressed = false; var darkTheme = graph.themes != null && graph.defaultThemeName == 'darkTheme'; // Exports SVG for first page while other page is visible by creating a graph // LATER: Add caching for the graph or SVG while not on first page // Dark mode requires a refresh that would destroy all handlers // LATER: Use dark theme here to bypass refresh if (darkTheme || (this.pages != null && this.currentPage != this.pages[0])) { var graphGetGlobalVariable = graph.getGlobalVariable; graph = this.createTemporaryGraph(graph.getStylesheet()); var page = this.pages[0]; graph.getGlobalVariable = function(name) { if (name == 'page') { return page.getName(); } else if (name == 'pagenumber') { return 1; } return graphGetGlobalVariable.apply(this, arguments); }; document.body.appendChild(graph.container); graph.model.setRoot(page.root); } } node = (node != null) ? node : this.getXmlFileData(ignoreSelection, currentPage, uncompressed); file = (file != null) ? file : this.getCurrentFile(); var result = this.createFileData(node, graph, file, window.location.href, forceXml, forceSvg, forceHtml, embeddedCallback, ignoreSelection, compact, uncompressed); // Removes temporary graph from DOM if (graph != this.editor.graph) { graph.container.parentNode.removeChild(graph.container); } return result; }; /** * */ EditorUi.prototype.getHtml = function(node, graph, title, editLink, redirect, ignoreSelection) { ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; var bg = null; var js = EditorUi.drawHost + '/js/embed-static.min.js'; // LATER: Merge common code with EmbedDialog if (graph != null) { var bounds = (ignoreSelection) ? graph.getGraphBounds() : graph.getBoundingBox(graph.getSelectionCells()); var scale = graph.view.scale; var x0 = Math.floor(bounds.x / scale - graph.view.translate.x); var y0 = Math.floor(bounds.y / scale - graph.view.translate.y); bg = graph.background; // Embed script only used if no redirect if (redirect == null) { var s = this.getBasenames().join(';'); if (s.length > 0) { js = EditorUi.drawHost + '/embed.js?s=' + s; } } // Adds embed attributes node.setAttribute('x0', x0); node.setAttribute('y0', y0); } if (node != null) { node.setAttribute('pan', '1'); node.setAttribute('zoom', '1'); node.setAttribute('resize', '0'); node.setAttribute('fit', '0'); node.setAttribute('border', '20'); // Hidden attributes node.setAttribute('links', '1'); if (editLink != null) { node.setAttribute('edit', editLink); } } // Makes XHTML compatible if (redirect != null) { redirect = redirect.replace(/&/g, '&amp;'); } // Removes control chars in input for correct roundtrip check var text = (node != null) ? Graph.zapGremlins(mxUtils.getXml(node)) : ''; // Double compression for mxfile not fixed since it may cause imcompatibilites with // embed clients that rely on this format. HTML files and export use getHtml2. var data = Graph.compress(text); // Fallback to URI encoded XML for invalid compression if (Graph.decompress(data) != text) { data = encodeURIComponent(text); } var style = 'position:relative;overflow:auto;width:100%;'; return ((redirect == null) ? '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5,IE=9" ><![endif]-->\n' : '') + '<!DOCTYPE html>\n<html' + ((redirect != null) ? ' xmlns="http://www.w3.org/1999/xhtml">' : '>') + '\n<head>\n' + ((redirect == null) ? ((title != null) ? '<title>' + mxUtils.htmlEntities(title) + '</title>\n' : '') : '<title>diagrams.net</title>\n') + ((redirect != null) ? '<meta http-equiv="refresh" content="0;URL=\'' + redirect + '\'"/>\n' : '') + '</head>\n<body' + (((redirect == null && bg != null && bg != mxConstants.NONE) ? ' style="background-color:' + bg + ';">' : '>')) + '\n<div class="mxgraph" style="' + style + '">\n' + '<div style="width:1px;height:1px;overflow:hidden;">' + data + '</div>\n</div>\n' + ((redirect == null) ? '<script type="text/javascript" src="' + js + '"></script>' : '<a style="position:absolute;top:50%;left:50%;margin-top:-128px;margin-left:-64px;" ' + 'href="' + redirect + '" target="_blank"><img border="0" ' + 'src="' + EditorUi.drawHost + '/images/drawlogo128.png"/></a>') + '\n</body>\n</html>\n'; }; /** * Same as above but using the new embed code. */ EditorUi.prototype.getHtml2 = function(xml, graph, title, editLink, redirect) { var js = window.DRAWIO_VIEWER_URL || EditorUi.drawHost + '/js/viewer-static.min.js'; // Makes XHTML compatible if (redirect != null) { redirect = redirect.replace(/&/g, '&amp;'); } var data = {highlight: '#0000ff', nav: this.editor.graph.foldingEnabled, resize: true, xml: Graph.zapGremlins(xml), toolbar: 'pages zoom layers lightbox'}; if (this.pages != null && this.currentPage != null) { data.page = mxUtils.indexOf(this.pages, this.currentPage); } var style = 'max-width:100%;border:1px solid transparent;'; return ((redirect == null) ? '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5,IE=9" ><![endif]-->\n' : '') + '<!DOCTYPE html>\n<html' + ((redirect != null) ? ' xmlns="http://www.w3.org/1999/xhtml">' : '>') + '\n<head>\n' + ((redirect == null) ? ((title != null) ? '<title>' + mxUtils.htmlEntities(title) + '</title>\n' : '') : '<title>diagrams.net</title>\n') + ((redirect != null) ? '<meta http-equiv="refresh" content="0;URL=\'' + redirect + '\'"/>\n' : '') + '<meta charset="utf-8"/>\n</head>\n<body>' + '\n<div class="mxgraph" style="' + style + '" data-mxgraph="' + mxUtils.htmlEntities(JSON.stringify(data)) + '"></div>\n' + ((redirect == null) ? '<script type="text/javascript" src="' + js + '"></script>' : '<a style="position:absolute;top:50%;left:50%;margin-top:-128px;margin-left:-64px;" ' + 'href="' + redirect + '" target="_blank"><img border="0" ' + 'src="' + EditorUi.drawHost + '/images/drawlogo128.png"/></a>') + '\n</body>\n</html>\n'; }; /** * */ EditorUi.prototype.setFileData = function(data) { data = this.validateFileData(data); this.currentPage = null; this.fileNode = null; this.pages = null; var node = (data != null && data.length > 0) ? mxUtils.parseXml(data).documentElement : null; // Checks for parser errors var cause = Editor.extractParserError(node, mxResources.get('invalidOrMissingFile')); if (cause) { throw new Error(mxResources.get('notADiagramFile') + ' (' + cause + ')'); } else { // Some nodes must be extracted here to find the mxfile node // LATER: Remove duplicate call to extractGraphModel in overridden setGraphXml var tmp = (node != null) ? this.editor.extractGraphModel(node, true) : null; if (tmp != null) { node = tmp; } if (node != null && node.nodeName == 'mxfile') { var nodes = node.getElementsByTagName('diagram'); if (urlParams['pages'] != '0' || nodes.length > 1 || (nodes.length == 1 && nodes[0].hasAttribute('name'))) { var selectedPage = null; this.fileNode = node; this.pages = []; // Wraps page nodes for (var i = 0; i < nodes.length; i++) { // Adds page ID based on page order to match // remote IDs given if IDs are missing here if (nodes[i].getAttribute('id') == null) { nodes[i].setAttribute('id', i); } var page = new DiagramPage(nodes[i]); // Checks for invalid page names if (page.getName() == null) { page.setName(mxResources.get('pageWithNumber', [i + 1])); } this.pages.push(page); if (urlParams['page-id'] != null && page.getId() == urlParams['page-id']) { selectedPage = page; } } this.currentPage = (selectedPage != null) ? selectedPage : this.pages[Math.max(0, Math.min(this.pages.length - 1, urlParams['page'] || 0))]; node = this.currentPage.node; } } // Creates tabbed file structure if enforced by URL if (urlParams['pages'] != '0' && this.fileNode == null && node != null) { this.fileNode = node.ownerDocument.createElement('mxfile'); this.currentPage = new DiagramPage(node.ownerDocument.createElement('diagram')); this.currentPage.setName(mxResources.get('pageWithNumber', [1])); this.pages = [this.currentPage]; } // Avoids scroll offset when switching page this.editor.setGraphXml(node); // Avoids duplicate parsing of the XML stored in the node if (this.currentPage != null) { this.currentPage.root = this.editor.graph.model.root; } if (urlParams['layer-ids'] != null) { try { var layerIds = urlParams['layer-ids'].split(' '); var layerIdsMap = {}; for (var i = 0; i < layerIds.length; i++) { layerIdsMap[layerIds[i]] = true; } var model = this.editor.graph.getModel(); var children = model.getChildren(model.root); // handle layers visibility for (var i = 0; i < children.length; i++) { var child = children[i]; model.setVisible(child, layerIdsMap[child.id] || false); } } catch(e){} //ignore } } }; /** * 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.getBaseFilename = function(ignorePageName) { var file = this.getCurrentFile(); var basename = (file != null && file.getTitle() != null) ? file.getTitle() : this.defaultFilename; if (/(\.xml)$/i.test(basename) || /(\.html)$/i.test(basename) || /(\.svg)$/i.test(basename) || /(\.png)$/i.test(basename) || /(\.drawio)$/i.test(basename)) { basename = basename.substring(0, basename.lastIndexOf('.')); } if (!ignorePageName && this.pages != null && this.pages.length > 1 && this.currentPage != null && this.currentPage.node.getAttribute('name') != null && this.currentPage.getName().length > 0) { basename = basename + '-' + this.currentPage.getName(); } return basen