1/** 2 * Copyright (c) 2006-2017, JGraph Ltd 3 * Copyright (c) 2006-2017, Gaudenz Alder 4 */ 5(function() 6{ 7 /** 8 * Version 9 */ 10 EditorUi.VERSION = '@DRAWIO-VERSION@'; 11 12 /** 13 * Overrides compact UI setting. 14 */ 15 EditorUi.compactUi = uiTheme != 'atlas'; 16 17 /** 18 * Overrides default grid color for dark mode 19 */ 20 if (Editor.isDarkMode()) 21 { 22 mxGraphView.prototype.gridColor = mxGraphView.prototype.defaultDarkGridColor; 23 } 24 25 /** 26 * Switch to disable logging for mode and search terms. 27 */ 28 EditorUi.enableLogging = urlParams['stealth'] != '1' && urlParams['lockdown'] != '1' && 29 (/.*\.draw\.io$/.test(window.location.hostname) || 30 /.*\.diagrams\.net$/.test(window.location.hostname)) && 31 window.location.hostname != 'support.draw.io'; 32 33 /** 34 * Protocol and hostname to use for embedded files. Default is https://www.draw.io 35 */ 36 EditorUi.drawHost = window.DRAWIO_BASE_URL; 37 38 /** 39 * Protocol and hostname to use for embedded files. Default is https://www.draw.io 40 */ 41 EditorUi.lightboxHost = window.DRAWIO_LIGHTBOX_URL; 42 43 /** 44 * Switch to disable logging for mode and search terms. 45 */ 46 EditorUi.lastErrorMessage = null; 47 48 /** 49 * Switch to disable logging for mode and search terms. 50 */ 51 EditorUi.ignoredAnonymizedChars = '\n\t`~!@#$%^&*()_+{}|:"<>?-=[]\;\'.\/,\n\t'; 52 53 /** 54 * Specifies the URL for the templates index file. 55 */ 56 EditorUi.templateFile = TEMPLATE_PATH + '/index.xml'; 57 58 /** 59 * Specifies the URL for the diffsync cache. 60 */ 61 EditorUi.cacheUrl = (urlParams['dev'] == '1') ? '/cache' : window.REALTIME_URL; 62 63 if (EditorUi.cacheUrl == null && typeof DrawioFile !== 'undefined') 64 { 65 DrawioFile.SYNC = 'none'; //Disable real-time sync 66 } 67 68 /** 69 * Cache timeout is 10 seconds. 70 */ 71 Editor.cacheTimeout = 10000; 72 73 /** 74 * Switch to enable PlantUML in the insert from text dialog. 75 * NOTE: This must also be enabled on the server-side. 76 */ 77 EditorUi.enablePlantUml = EditorUi.enableLogging; 78 79 /** 80 * https://github.com/electron/electron/issues/2288 81 */ 82 EditorUi.isElectronApp = window != null && window.process != null && 83 window.process.versions != null && window.process.versions['electron'] != null; 84 85 /** 86 * Shortcut for capability check. 87 */ 88 EditorUi.nativeFileSupport = !mxClient.IS_OP && !EditorUi.isElectronApp && 89 urlParams['extAuth'] != '1' && 'showSaveFilePicker' in window && 90 'showOpenFilePicker' in window; 91 92 /** 93 * Specifies if drafts should be saved in IndexedDB. 94 */ 95 EditorUi.enableDrafts = !mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp && 96 isLocalStorage && urlParams['drafts'] != '0'; 97 98 /** 99 * Link for scratchpad help. 100 */ 101 EditorUi.scratchpadHelpLink = 'https://www.diagrams.net/doc/faq/scratchpad'; 102 103 /** 104 * Default Mermaid config without using foreign objects in flowcharts. 105 */ 106 EditorUi.defaultMermaidConfig = { 107 theme:'neutral', 108 arrowMarkerAbsolute:false, 109 flowchart: 110 { 111 htmlLabels:false 112 }, 113 sequence: 114 { 115 diagramMarginX:50, 116 diagramMarginY:10, 117 actorMargin:50, 118 width:150, 119 height:65, 120 boxMargin:10, 121 boxTextMargin:5, 122 noteMargin:10, 123 messageMargin:35, 124 mirrorActors:true, 125 bottomMarginAdj:1, 126 useMaxWidth:true, 127 rightAngles:false, 128 showSequenceNumbers:false 129 }, 130 gantt:{ 131 titleTopMargin:25, 132 barHeight:20, 133 barGap:4, 134 topPadding:50, 135 leftPadding:75, 136 gridLineStartPadding:35, 137 fontSize:11, 138 fontFamily:'"Open-Sans", "sans-serif"', 139 numberSectionStyles:4, 140 axisFormat:'%Y-%m-%d' 141 } 142 }; 143 144 /** 145 * Updates action states depending on the selection. 146 */ 147 EditorUi.logError = function(message, url, linenumber, colno, err, severity, quiet) 148 { 149 severity = ((severity != null) ? severity : (message.indexOf('NetworkError') >= 0 || 150 message.indexOf('SecurityError') >= 0 || message.indexOf('NS_ERROR_FAILURE') >= 0 || 151 message.indexOf('out of memory') >= 0) ? 'CONFIG' : 'SEVERE'); 152 153 if (EditorUi.enableLogging && urlParams['dev'] != '1') 154 { 155 try 156 { 157 if (message == EditorUi.lastErrorMessage || (message != null && url != null && 158 ((message.indexOf('Script error') != -1) || (message.indexOf('extension') != -1)))) 159 { 160 // TODO log external domain script failure "Script error." is 161 // reported when the error occurs in a script that is hosted 162 // on a domain other than the domain of the current page 163 } 164 // DocumentClosedError seems to be an FF bug an can be ignored for now 165 else if (message != null && message.indexOf('DocumentClosedError') < 0) 166 { 167 EditorUi.lastErrorMessage = message; 168 var logDomain = window.DRAWIO_LOG_URL != null ? window.DRAWIO_LOG_URL : ''; 169 err = (err != null) ? err : new Error(message); 170 171 var img = new Image(); 172 img.src = logDomain + '/log?severity=' + severity + '&v=' + encodeURIComponent(EditorUi.VERSION) + 173 '&msg=clientError:' + encodeURIComponent(message) + ':url:' + encodeURIComponent(window.location.href) + 174 ':lnum:' + encodeURIComponent(linenumber) + ((colno != null) ? ':colno:' + encodeURIComponent(colno) : '') + 175 ((err != null && err.stack != null) ? '&stack=' + encodeURIComponent(err.stack) : ''); 176 } 177 } 178 catch (e) 179 { 180 // do nothing 181 } 182 } 183 184 try 185 { 186 if (!quiet && window.console != null) 187 { 188 console.error(severity, message, url, linenumber, colno, err); 189 } 190 } 191 catch (e) 192 { 193 // ignore 194 } 195 }; 196 197 /** 198 * Updates action states depending on the selection. 199 */ 200 EditorUi.logEvent = function(data) 201 { 202 if (urlParams['dev'] == '1') 203 { 204 EditorUi.debug('logEvent', data); 205 } 206 else if (EditorUi.enableLogging) 207 { 208 try 209 { 210 var logDomain = window.DRAWIO_LOG_URL != null ? window.DRAWIO_LOG_URL : ''; 211 var img = new Image(); 212 img.src = logDomain + '/images/1x1.png?' + 213 'v=' + encodeURIComponent(EditorUi.VERSION) + 214 ((data != null) ? '&data=' + encodeURIComponent(JSON.stringify(data)) : ''); 215 } 216 catch (e) 217 { 218 // ignore 219 } 220 } 221 }; 222 223 /** 224 * Sending error reports. 225 */ 226 EditorUi.sendReport = function(data, maxLength) 227 { 228 if (urlParams['dev'] == '1') 229 { 230 EditorUi.debug('sendReport', data); 231 } 232 else if (EditorUi.enableLogging) 233 { 234 try 235 { 236 maxLength = (maxLength != null) ? maxLength : 50000; 237 238 if (data.length > maxLength) 239 { 240 data = data.substring(0, maxLength) + '\n...[SHORTENED]' 241 } 242 243 mxUtils.post('/email', 'version=' + encodeURIComponent(EditorUi.VERSION) + 244 '&url=' + encodeURIComponent(window.location.href) + 245 '&data=' + encodeURIComponent(data)); 246 } 247 catch (e) 248 { 249 // ignore 250 } 251 } 252 }; 253 254 /** 255 * Adds the listener for automatically saving the diagram for local changes. 256 */ 257 EditorUi.debug = function() 258 { 259 try 260 { 261 if (window.console != null && urlParams['test'] == '1') 262 { 263 var args = [new Date().toISOString()]; 264 265 for (var i = 0; i < arguments.length; i++) 266 { 267 if (arguments[i] != null) 268 { 269 args.push(arguments[i]); 270 } 271 } 272 273 console.log.apply(console, args); 274 } 275 } 276 catch (e) 277 { 278 // ignore 279 } 280 }; 281 282 /** 283 * Static method for pasing PNG files. 284 */ 285 EditorUi.parsePng = function(f, fn, error) 286 { 287 var pos = 0; 288 289 function fread(d, count) 290 { 291 var start = pos; 292 pos += count; 293 294 return d.substring(start, pos); 295 }; 296 297 // Reads unsigned long 32 bit big endian 298 function _freadint(d) 299 { 300 var bytes = fread(d, 4); 301 302 return bytes.charCodeAt(3) + (bytes.charCodeAt(2) << 8) + 303 (bytes.charCodeAt(1) << 16) + (bytes.charCodeAt(0) << 24); 304 }; 305 306 // Checks signature 307 if (fread(f,8) != String.fromCharCode(137) + 'PNG' + String.fromCharCode(13, 10, 26, 10)) 308 { 309 if (error != null) 310 { 311 error(); 312 } 313 314 return; 315 } 316 317 // Reads header chunk 318 fread(f,4); 319 320 if (fread(f,4) != 'IHDR') 321 { 322 if (error != null) 323 { 324 error(); 325 } 326 327 return; 328 } 329 330 fread(f, 17); 331 332 do 333 { 334 var n = _freadint(f); 335 var type = fread(f,4); 336 337 if (fn != null) 338 { 339 if (fn(pos - 8, type, n)) 340 { 341 break; 342 } 343 } 344 345 value = fread(f,n); 346 fread(f,4); 347 348 if (type == 'IEND') 349 { 350 break; 351 } 352 } 353 while (n); 354 }; 355 356 /** 357 * Removes any values, styles and geometries from the given XML node. 358 */ 359 EditorUi.removeChildNodes = function(node) 360 { 361 while (node.firstChild != null) 362 { 363 node.removeChild(node.firstChild); 364 } 365 }; 366 367 /** 368 * Contains the default XML for an empty diagram. 369 */ 370 EditorUi.prototype.emptyDiagramXml = '<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel>'; 371 372 /** 373 * 374 */ 375 EditorUi.prototype.emptyLibraryXml = '<mxlibrary>[]</mxlibrary>'; 376 377 /** 378 * Sets the delay for autosave in milliseconds. Default is 2000. 379 */ 380 EditorUi.prototype.mode = null; 381 382 /** 383 * General timeout is 25 seconds. 384 * LATER: Move to Editor 385 */ 386 EditorUi.prototype.timeout = Editor.prototype.timeout; 387 388 /** 389 * Allows for two buttons in the sidebar footer. 390 */ 391 EditorUi.prototype.sidebarFooterHeight = 38; 392 393 /** 394 * Specifies the default custom shape style. 395 */ 396 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;'; 397 398 /** 399 * Defines the maximum size for images. 400 */ 401 EditorUi.prototype.maxBackgroundSize = 1600; 402 403 /** 404 * Defines the maximum size for images. 405 */ 406 EditorUi.prototype.maxImageSize = 520; 407 408 /** 409 * Defines the maximum width for pasted text. 410 * Use 0 to disable check. 411 */ 412 EditorUi.prototype.maxTextWidth = 520; 413 414 /** 415 * Images above 100K should be resampled. 416 */ 417 EditorUi.prototype.resampleThreshold = 100000; 418 419 /** 420 * Maximum allowed size for images is 1 MB. 421 */ 422 EditorUi.prototype.maxImageBytes = 1000000; 423 424 /** 425 * Maximum size for background images is 2.5 MB. 426 */ 427 EditorUi.prototype.maxBackgroundBytes = 2500000; 428 429 /** 430 * Maximum size for text files in labels is 0.5 MB. 431 */ 432 EditorUi.prototype.maxTextBytes = 500000; 433 434 /** 435 * Holds the current file. 436 */ 437 EditorUi.prototype.currentFile = null; 438 439 /** 440 * Specifies if PDF export should be done via print dialog. Default is 441 * false which uses the PhantomJS backend to create the PDF. 442 */ 443 EditorUi.prototype.printPdfExport = false; 444 445 /** 446 * Specifies if PDF export with pages is enabled. 447 */ 448 EditorUi.prototype.pdfPageExport = true; 449 450 /** 451 * Restores app defaults for UI 452 */ 453 EditorUi.prototype.formatEnabled = urlParams['format'] != '0'; 454 455 /** 456 * Whether template action should be shown in insert menu. 457 */ 458 EditorUi.prototype.insertTemplateEnabled = true; 459 460 /** 461 * Restores app defaults for UI 462 */ 463 EditorUi.prototype.closableScratchpad = true; 464 465 /** 466 * Restores app defaults for UI 467 */ 468 EditorUi.prototype.embedExportBorder = 8; 469 470 /** 471 * Restores app defaults for UI 472 */ 473 EditorUi.prototype.embedExportBackground = null; 474 475 /** 476 * Capability check for canvas export 477 */ 478 (function() 479 { 480 EditorUi.prototype.useCanvasForExport = false; 481 EditorUi.prototype.jpgSupported = false; 482 483 // Checks if canvas is supported 484 try 485 { 486 var cnv = document.createElement('canvas'); 487 EditorUi.prototype.canvasSupported = !!(cnv.getContext && cnv.getContext('2d')); 488 } 489 catch (e) 490 { 491 // ignore 492 } 493 494 try 495 { 496 var canvas = document.createElement('canvas'); 497 var img = new Image(); 498 499 // LATER: Capability check should not be async 500 img.onload = function() 501 { 502 try 503 { 504 var ctx = canvas.getContext('2d'); 505 ctx.drawImage(img, 0, 0); 506 507 // Works in Chrome, Firefox, Edge, Safari and Opera 508 var result = canvas.toDataURL('image/png'); 509 EditorUi.prototype.useCanvasForExport = result != null && result.length > 6; 510 } 511 catch (e) 512 { 513 // ignore 514 } 515 }; 516 517 // Checks if SVG with foreignObject can be exported 518 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>'; 519 img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg))); 520 } 521 catch (e) 522 { 523 // ignore 524 } 525 526 // Checks for client-side JPG support 527 try 528 { 529 var canvas = document.createElement('canvas'); 530 canvas.width = canvas.height = 1; 531 var uri = canvas.toDataURL('image/jpeg'); 532 533 EditorUi.prototype.jpgSupported = (uri.match('image/jpeg') !== null); 534 } 535 catch (e) 536 { 537 // ignore 538 } 539 })(); 540 541 /** 542 * Hook for subclassers. 543 */ 544 EditorUi.prototype.openLink = function(url, target, allowOpener) 545 { 546 // LATER: Replace this with direct calls to graph 547 return this.editor.graph.openLink(url, target, allowOpener); 548 }; 549 550 /** 551 * Hook for subclassers. 552 */ 553 EditorUi.prototype.showSplash = function(force) { }; 554 555 /** 556 * Abstraction for local storage access. 557 */ 558 EditorUi.prototype.getLocalData = function(key, fn) 559 { 560 fn(localStorage.getItem(key)); 561 }; 562 563 /** 564 * Abstraction for local storage access. 565 */ 566 EditorUi.prototype.setLocalData = function(key, data, fn) 567 { 568 localStorage.setItem(key, data); 569 570 if (fn != null) 571 { 572 fn(); 573 } 574 }; 575 576 /** 577 * Abstraction for local storage access. 578 */ 579 EditorUi.prototype.removeLocalData = function(key, fn) 580 { 581 localStorage.removeItem(key) 582 fn(); 583 }; 584 585 EditorUi.prototype.setMathEnabled = function(value) 586 { 587 this.editor.graph.mathEnabled = value; 588 this.editor.updateGraphComponents(); 589 this.editor.graph.refresh(); 590 this.editor.graph.defaultMathEnabled = value; 591 592 this.fireEvent(new mxEventObject('mathEnabledChanged')); 593 }; 594 595 EditorUi.prototype.isMathEnabled = function(value) 596 { 597 return this.editor.graph.mathEnabled; 598 }; 599 600 /** 601 * Returns true if offline app, which isn't a defined thing 602 */ 603 EditorUi.prototype.isOfflineApp = function() 604 { 605 return urlParams['offline'] == '1'; 606 }; 607 608 /** 609 * Returns true if no external comms allowed or possible 610 */ 611 EditorUi.prototype.isOffline = function(ignoreStealth) 612 { 613 return this.isOfflineApp() || !navigator.onLine || (!ignoreStealth && (urlParams['stealth'] == '1' || urlParams['lockdown'] == '1')); 614 }; 615 616 /** 617 * Translates this point by the given vector. 618 * 619 * @param {number} dx X-coordinate of the translation. 620 * @param {number} dy Y-coordinate of the translation. 621 */ 622 EditorUi.prototype.createSpinner = function(x, y, size) 623 { 624 var autoPosition = (x == null || y == null); 625 size = (size != null) ? size : 24; 626 627 var spinner = new Spinner({ 628 lines: 12, // The number of lines to draw 629 length: size, // The length of each line 630 width: Math.round(size / 3), // The line thickness 631 radius: Math.round(size / 2), // The radius of the inner circle 632 rotate: 0, // The rotation offset 633 color: (Editor.isDarkMode()) ? '#c0c0c0' : '#000', // #rgb or #rrggbb 634 speed: 1.5, // Rounds per second 635 trail: 60, // Afterglow percentage 636 shadow: false, // Whether to render a shadow 637 hwaccel: false, // Whether to use hardware acceleration 638 zIndex: 2e9 // The z-index (defaults to 2000000000) 639 }); 640 641 // Extends spin method to include an optional label 642 var oldSpin = spinner.spin; 643 644 spinner.spin = function(container, label) 645 { 646 var result = false; 647 648 if (!this.active) 649 { 650 oldSpin.call(this, container); 651 this.active = true; 652 653 if (label != null) 654 { 655 if (autoPosition) 656 { 657 y = Math.max(document.body.clientHeight || 0, document.documentElement.clientHeight || 0) / 2; 658 x = document.body.clientWidth / 2 - 2; 659 } 660 661 var status = document.createElement('div'); 662 status.style.position = 'absolute'; 663 status.style.whiteSpace = 'nowrap'; 664 status.style.background = '#4B4243'; 665 status.style.color = 'white'; 666 status.style.fontFamily = Editor.defaultHtmlFont; 667 status.style.fontSize = '9pt'; 668 status.style.padding = '6px'; 669 status.style.paddingLeft = '10px'; 670 status.style.paddingRight = '10px'; 671 status.style.zIndex = 2e9; 672 status.style.left = Math.max(0, x) + 'px'; 673 status.style.top = Math.max(0, y + 70) + 'px'; 674 675 mxUtils.setPrefixedStyle(status.style, 'borderRadius', '6px'); 676 mxUtils.setPrefixedStyle(status.style, 'transform', 'translate(-50%,-50%)'); 677 678 if (!Editor.isDarkMode()) 679 { 680 mxUtils.setPrefixedStyle(status.style, 'boxShadow', '2px 2px 3px 0px #ddd'); 681 } 682 683 if (label.substring(label.length - 3, label.length) != '...' && 684 label.charAt(label.length - 1) != '!') 685 { 686 label = label + '...'; 687 } 688 689 status.innerHTML = label; 690 container.appendChild(status); 691 spinner.status = status; 692 } 693 694 // Pause returns a function to resume the spinner 695 this.pause = mxUtils.bind(this, function() 696 { 697 var fn = function() { }; 698 699 if (this.active) 700 { 701 fn = mxUtils.bind(this, function() 702 { 703 this.spin(container, label); 704 }); 705 } 706 707 this.stop(); 708 709 return fn; 710 }); 711 712 result = true; 713 } 714 715 return result; 716 }; 717 718 // Extends stop method to remove the optional label 719 var oldStop = spinner.stop; 720 721 spinner.stop = function() 722 { 723 oldStop.call(this); 724 this.active = false; 725 726 if (spinner.status != null && spinner.status.parentNode != null) 727 { 728 spinner.status.parentNode.removeChild(spinner.status); 729 } 730 731 spinner.status = null; 732 }; 733 734 spinner.pause = function() 735 { 736 return function() {}; 737 }; 738 739 return spinner; 740 }; 741 742 /** 743 * Returns true if the given string contains a compatible graph model. 744 */ 745 EditorUi.prototype.isCompatibleString = function(data) 746 { 747 try 748 { 749 var doc = mxUtils.parseXml(data); 750 var node = this.editor.extractGraphModel(doc.documentElement, true); 751 752 return node != null && node.getElementsByTagName('parsererror').length == 0; 753 } 754 catch (e) 755 { 756 // ignore 757 } 758 759 return false; 760 }; 761 762 /** 763 * Returns true if the given binary data is a Visio file. 764 */ 765 EditorUi.prototype.isVisioData = function(data) 766 { 767 return data.length > 8 && ((data.charCodeAt(0) == 0xD0 && data.charCodeAt(1) == 0xCF && 768 data.charCodeAt(2) == 0x11 && data.charCodeAt(3) == 0xE0 && data.charCodeAt(4) == 0xA1 && data.charCodeAt(5) == 0xB1 && 769 data.charCodeAt(6) == 0x1A && data.charCodeAt(7) == 0xE1) || (data.charCodeAt(0) == 0x50 && data.charCodeAt(1) == 0x4B && 770 data.charCodeAt(2) == 0x03 && data.charCodeAt(3) == 0x04) || (data.charCodeAt(0) == 0x50 && data.charCodeAt(1) == 0x4B && 771 data.charCodeAt(2) == 0x03 && data.charCodeAt(3) == 0x06)); 772 }; 773 774 /** 775 * Returns true if the given binary data is a Visio file that requires remote conversion. 776 * This code returns true for vss, vsd and vdx files. 777 */ 778 EditorUi.prototype.isRemoteVisioData = function(data) 779 { 780 return data.length > 8 && ((data.charCodeAt(0) == 0xD0 && data.charCodeAt(1) == 0xCF && 781 data.charCodeAt(2) == 0x11 && data.charCodeAt(3) == 0xE0 && data.charCodeAt(4) == 0xA1 && data.charCodeAt(5) == 0xB1 && 782 data.charCodeAt(6) == 0x1A && data.charCodeAt(7) == 0xE1) || (data.charCodeAt(0) == 0x3C && data.charCodeAt(1) == 0x3F && 783 data.charCodeAt(2) == 0x78 && data.charCodeAt(3) == 0x6D && data.charCodeAt(3) == 0x6C)); 784 }; 785 786 /** 787 * Returns true if the given binary data is a PNG file. 788 */ 789 EditorUi.prototype.isPngData = function(data) 790 { 791 return data.length > 8 && data.charCodeAt(0) == 137 && data.charCodeAt(1) == 80 && 792 data.charCodeAt(2) == 78 && data.charCodeAt(3) == 71 && data.charCodeAt(4) == 13 && 793 data.charCodeAt(5) == 10 && data.charCodeAt(6) == 26 && data.charCodeAt(7) == 10; 794 }; 795 796 /** 797 * Adds keyboard shortcuts for page handling. 798 */ 799 var editorUiCreateKeyHandler = EditorUi.prototype.createKeyHandler; 800 EditorUi.prototype.createKeyHandler = function(editor) 801 { 802 var keyHandler = editorUiCreateKeyHandler.apply(this, arguments); 803 804 if (!this.editor.chromeless || this.editor.editable) 805 { 806 var keyHandlerGetFunction = keyHandler.getFunction; 807 var graph = this.editor.graph; 808 var ui = this; 809 810 keyHandler.getFunction = function(evt) 811 { 812 if (graph.isSelectionEmpty() && ui.pages != null && ui.pages.length > 0) 813 { 814 var idx = ui.getSelectedPageIndex(); 815 816 if (mxEvent.isShiftDown(evt)) 817 { 818 if (evt.keyCode == 37) 819 { 820 return function() 821 { 822 if (idx > 0) 823 { 824 ui.movePage(idx, idx - 1); 825 } 826 }; 827 } 828 else if (evt.keyCode == 38) 829 { 830 return function() 831 { 832 if (idx > 0) 833 { 834 ui.movePage(idx, 0); 835 } 836 }; 837 } 838 else if (evt.keyCode == 39) 839 { 840 return function() 841 { 842 if (idx < ui.pages.length - 1) 843 { 844 ui.movePage(idx, idx + 1); 845 } 846 }; 847 } 848 else if (evt.keyCode == 40) 849 { 850 return function() 851 { 852 if (idx < ui.pages.length - 1) 853 { 854 ui.movePage(idx, ui.pages.length - 1); 855 } 856 }; 857 } 858 } 859 else if (mxEvent.isControlDown(evt) || (mxClient.IS_MAC && mxEvent.isMetaDown(evt))) 860 { 861 if (evt.keyCode == 37) 862 { 863 return function() 864 { 865 if (idx > 0) 866 { 867 ui.selectNextPage(false); 868 } 869 }; 870 } 871 else if (evt.keyCode == 38) 872 { 873 return function() 874 { 875 if (idx > 0) 876 { 877 ui.selectPage(ui.pages[0]); 878 } 879 }; 880 } 881 else if (evt.keyCode == 39) 882 { 883 return function() 884 { 885 if (idx < ui.pages.length - 1) 886 { 887 ui.selectNextPage(true); 888 } 889 }; 890 } 891 else if (evt.keyCode == 40) 892 { 893 return function() 894 { 895 if (idx < ui.pages.length - 1) 896 { 897 ui.selectPage(ui.pages[ui.pages.length - 1]); 898 } 899 }; 900 } 901 902 } 903 } 904 905 return keyHandlerGetFunction.apply(this, arguments); 906 }; 907 } 908 909 return keyHandler; 910 }; 911 912 /** 913 * Extracts the mxfile from the given HTML data from a data transfer event. 914 */ 915 var editorUiExtractGraphModelFromHtml = EditorUi.prototype.extractGraphModelFromHtml; 916 EditorUi.prototype.extractGraphModelFromHtml = function(data) 917 { 918 var result = editorUiExtractGraphModelFromHtml.apply(this, arguments); 919 920 if (result == null) 921 { 922 try 923 { 924 var idx = data.indexOf('<mxfile '); 925 926 if (idx >= 0) 927 { 928 var idx2 = data.lastIndexOf('</mxfile>'); 929 930 if (idx2 > idx) 931 { 932 result = data.substring(idx, idx2 + 15).replace(/>/g, '>'). 933 replace(/</g, '<').replace(/\\"/g, '"').replace(/\n/g, ''); 934 } 935 } 936 else 937 { 938 // Gets compressed data from mxgraph element in HTML document 939 var doc = mxUtils.parseXml(data); 940 var node = this.editor.extractGraphModel(doc.documentElement, this.pages != null || 941 this.diagramContainer.style.visibility == 'hidden'); 942 result = (node != null) ? mxUtils.getXml(node) : ''; 943 } 944 } 945 catch (e) 946 { 947 // ignore 948 } 949 } 950 951 return result; 952 }; 953 954 /** 955 * Workaround for malformed xhtml meta element bug 07.08.16. The trailing slash was missing causing 956 * reopen to fail trying to parse. Used in replaceFileData, setFileData and importFile. 957 */ 958 EditorUi.prototype.validateFileData = function(data) 959 { 960 if (data != null && data.length > 0) 961 { 962 var index = data.indexOf('<meta charset="utf-8">'); 963 964 if (index >= 0) 965 { 966 var replaceString = '<meta charset="utf-8"/>'; 967 var replaceStrLen = replaceString.length; 968 data = data.slice(0, index) + replaceString + data.slice(index + replaceStrLen - 1, data.length); 969 } 970 971 data = Graph.zapGremlins(data); 972 } 973 974 return data; 975 }; 976 977 /** 978 * 979 */ 980 EditorUi.prototype.replaceFileData = function(data) 981 { 982 data = this.validateFileData(data); 983 var node = (data != null && data.length > 0) ? mxUtils.parseXml(data).documentElement : null; 984 985 // Some nodes must be extracted here to find the mxfile node 986 // LATER: Remove duplicate call to extractGraphModel in overridden setGraphXml 987 var tmp = (node != null) ? this.editor.extractGraphModel(node, true) : null; 988 989 if (tmp != null) 990 { 991 node = tmp; 992 } 993 994 if (node != null) 995 { 996 var graph = this.editor.graph; 997 998 graph.model.beginUpdate(); 999 try 1000 { 1001 var oldPages = (this.pages != null) ? this.pages.slice() : null; 1002 var nodes = node.getElementsByTagName('diagram'); 1003 1004 if (urlParams['pages'] != '0' || nodes.length > 1 || 1005 (nodes.length == 1 && nodes[0].hasAttribute('name'))) 1006 { 1007 this.fileNode = node; 1008 this.pages = (this.pages != null) ? this.pages : []; 1009 1010 // Wraps page nodes 1011 for (var i = nodes.length - 1; i >= 0; i--) 1012 { 1013 var page = this.updatePageRoot(new DiagramPage(nodes[i])); 1014 1015 // Checks for invalid page names 1016 if (page.getName() == null) 1017 { 1018 page.setName(mxResources.get('pageWithNumber', [i + 1])); 1019 } 1020 1021 graph.model.execute(new ChangePage(this, page, (i == 0) ? page : null, 0)); 1022 } 1023 } 1024 else 1025 { 1026 // Creates tabbed file structure if enforced by URL 1027 if (urlParams['pages'] != '0' && this.fileNode == null) 1028 { 1029 this.fileNode = node.ownerDocument.createElement('mxfile'); 1030 this.currentPage = new DiagramPage(node.ownerDocument.createElement('diagram')); 1031 this.currentPage.setName(mxResources.get('pageWithNumber', [1])); 1032 graph.model.execute(new ChangePage(this, this.currentPage, this.currentPage, 0)); 1033 } 1034 1035 // Avoids scroll offset when switching page 1036 this.editor.setGraphXml(node); 1037 1038 // Avoids duplicate parsing of the XML stored in the node 1039 if (this.currentPage != null) 1040 { 1041 this.currentPage.root = this.editor.graph.model.root; 1042 } 1043 } 1044 1045 if (oldPages != null) 1046 { 1047 for (var i = 0; i < oldPages.length; i++) 1048 { 1049 graph.model.execute(new ChangePage(this, oldPages[i], null)); 1050 } 1051 } 1052 } 1053 finally 1054 { 1055 graph.model.endUpdate(); 1056 } 1057 } 1058 }; 1059 1060 /** 1061 * Translates this point by the given vector. 1062 * 1063 * @param {number} dx X-coordinate of the translation. 1064 * @param {number} dy Y-coordinate of the translation. 1065 */ 1066 EditorUi.prototype.createFileData = function(node, graph, file, url, forceXml, forceSvg, forceHtml, 1067 embeddedCallback, ignoreSelection, compact, uncompressed) 1068 { 1069 graph = (graph != null) ? graph : this.editor.graph; 1070 forceXml = (forceXml != null) ? forceXml : false; 1071 ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; 1072 1073 var editLink = null; 1074 var redirect = null; 1075 1076 if (file == null || file.getMode() == App.MODE_DEVICE || file.getMode() == App.MODE_BROWSER) 1077 { 1078 editLink = '_blank'; 1079 } 1080 else 1081 { 1082 editLink = url; 1083 redirect = editLink; 1084 } 1085 1086 if (node == null) 1087 { 1088 return ''; 1089 } 1090 else 1091 { 1092 var fileNode = node; 1093 1094 // Ignores case for possible HTML or XML nodes 1095 if (fileNode.nodeName.toLowerCase() != 'mxfile') 1096 { 1097 if (uncompressed) 1098 { 1099 var diagramNode = node.ownerDocument.createElement('diagram'); 1100 diagramNode.setAttribute('id', Editor.guid()); 1101 diagramNode.appendChild(node); 1102 1103 fileNode = node.ownerDocument.createElement('mxfile'); 1104 fileNode.appendChild(diagramNode); 1105 } 1106 else 1107 { 1108 // Removes control chars in input for correct roundtrip check 1109 var text = Graph.zapGremlins(mxUtils.getXml(node)); 1110 var data = Graph.compress(text); 1111 1112 // Fallback to plain XML for invalid compression 1113 // TODO: Remove this fallback with active pages 1114 if (Graph.decompress(data) != text) 1115 { 1116 return text; 1117 } 1118 else 1119 { 1120 var diagramNode = node.ownerDocument.createElement('diagram'); 1121 diagramNode.setAttribute('id', Editor.guid()); 1122 mxUtils.setTextContent(diagramNode, data); 1123 1124 fileNode = node.ownerDocument.createElement('mxfile'); 1125 fileNode.appendChild(diagramNode); 1126 } 1127 } 1128 } 1129 1130 if (!compact) 1131 { 1132 // Removes old metadata 1133 fileNode.removeAttribute('userAgent'); 1134 fileNode.removeAttribute('version'); 1135 fileNode.removeAttribute('editor'); 1136 fileNode.removeAttribute('pages'); 1137 fileNode.removeAttribute('type'); 1138 1139 if (mxClient.IS_CHROMEAPP) 1140 { 1141 fileNode.setAttribute('host', 'Chrome'); 1142 } 1143 else if (EditorUi.isElectronApp) 1144 { 1145 fileNode.setAttribute('host', 'Electron'); 1146 } 1147 else 1148 { 1149 fileNode.setAttribute('host', window.location.hostname); 1150 } 1151 1152 // Adds new metadata 1153 fileNode.setAttribute('modified', new Date().toISOString()); 1154 fileNode.setAttribute('agent', navigator.appVersion); 1155 fileNode.setAttribute('version', EditorUi.VERSION); 1156 fileNode.setAttribute('etag', Editor.guid()); 1157 1158 var md = (file != null) ? file.getMode() : this.mode; 1159 1160 if (md != null) 1161 { 1162 fileNode.setAttribute('type', md); 1163 } 1164 1165 if (fileNode.getElementsByTagName('diagram').length > 1 && this.pages != null) 1166 { 1167 fileNode.setAttribute('pages', this.pages.length); 1168 } 1169 } 1170 else 1171 { 1172 fileNode = fileNode.cloneNode(true); 1173 fileNode.removeAttribute('modified'); 1174 fileNode.removeAttribute('host'); 1175 fileNode.removeAttribute('agent'); 1176 fileNode.removeAttribute('etag'); 1177 fileNode.removeAttribute('userAgent'); 1178 fileNode.removeAttribute('version'); 1179 fileNode.removeAttribute('editor'); 1180 fileNode.removeAttribute('type'); 1181 } 1182 1183 var xml = (uncompressed) ? mxUtils.getPrettyXml(fileNode) : mxUtils.getXml(fileNode); 1184 1185 // Writes the file as an embedded HTML file 1186 if (!forceSvg && !forceXml && (forceHtml || (file != null && /(\.html)$/i.test(file.getTitle())))) 1187 { 1188 xml = this.getHtml2(mxUtils.getXml(fileNode), graph, (file != null) ? file.getTitle() : null, editLink, redirect); 1189 } 1190 // Maps the XML data to the content attribute in the SVG node 1191 else if (forceSvg || (!forceXml && file != null && /(\.svg)$/i.test(file.getTitle()))) 1192 { 1193 if (file != null && (file.getMode() == App.MODE_DEVICE || file.getMode() == App.MODE_BROWSER)) 1194 { 1195 url = null; 1196 } 1197 1198 xml = this.getEmbeddedSvg(xml, graph, url, null, embeddedCallback, ignoreSelection, redirect); 1199 } 1200 1201 return xml; 1202 } 1203 }; 1204 1205 /** 1206 * Translates this point by the given vector. 1207 * 1208 * @param {number} dx X-coordinate of the translation. 1209 * @param {number} dy Y-coordinate of the translation. 1210 */ 1211 EditorUi.prototype.getXmlFileData = function(ignoreSelection, currentPage, uncompressed, resolveReferences) 1212 { 1213 ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; 1214 currentPage = (currentPage != null) ? currentPage : false; 1215 uncompressed = (uncompressed != null) ? uncompressed : !Editor.compressXml; 1216 1217 // Generats graph model XML node for single page export 1218 var node = this.editor.getGraphXml(ignoreSelection, resolveReferences); 1219 1220 if (ignoreSelection && this.fileNode != null && this.currentPage != null) 1221 { 1222 // Updates current page XML if selection is ignored 1223 EditorUi.removeChildNodes(this.currentPage.node); 1224 mxUtils.setTextContent(this.currentPage.node, Graph.compressNode(node)); 1225 1226 // Creates a clone of the file node for processing 1227 node = this.fileNode.cloneNode(false); 1228 1229 // Appends the node of the page and applies compression 1230 function appendPage(pageNode) 1231 { 1232 var models = pageNode.getElementsByTagName('mxGraphModel'); 1233 var modelNode = (models.length > 0) ? models[0] : null; 1234 var clone = pageNode; 1235 1236 if (modelNode == null && uncompressed) 1237 { 1238 var text = mxUtils.trim(mxUtils.getTextContent(pageNode)); 1239 clone = pageNode.cloneNode(false); 1240 1241 if (text.length > 0) 1242 { 1243 var tmp = Graph.decompress(text); 1244 1245 if (tmp != null && tmp.length > 0) 1246 { 1247 clone.appendChild(mxUtils.parseXml(tmp).documentElement); 1248 } 1249 } 1250 } 1251 else if (modelNode != null && !uncompressed) 1252 { 1253 clone = pageNode.cloneNode(false); 1254 mxUtils.setTextContent(clone, Graph.compressNode(modelNode)); 1255 } 1256 else 1257 { 1258 clone = pageNode.cloneNode(true); 1259 } 1260 1261 node.appendChild(clone); 1262 }; 1263 1264 if (currentPage) 1265 { 1266 appendPage(this.currentPage.node); 1267 } 1268 else 1269 { 1270 // Restores order of pages 1271 for (var i = 0; i < this.pages.length; i++) 1272 { 1273 var page = this.pages[i]; 1274 var currNode = page.node; 1275 1276 if (page != this.currentPage) 1277 { 1278 if (page.needsUpdate) 1279 { 1280 var enc = new mxCodec(mxUtils.createXmlDocument()); 1281 var temp = enc.encode(new mxGraphModel(page.root)); 1282 this.editor.graph.saveViewState(page.viewState, 1283 temp, null, resolveReferences); 1284 EditorUi.removeChildNodes(currNode); 1285 mxUtils.setTextContent(currNode, Graph.compressNode(temp)); 1286 1287 // Marks the page as up-to-date 1288 delete page.needsUpdate; 1289 } 1290 else if (resolveReferences) 1291 { 1292 this.updatePageRoot(page); 1293 1294 // Forces update of background page image in offscreen page 1295 if (page.viewState.backgroundImage != null) 1296 { 1297 if (page.viewState.backgroundImage.originalSrc != null) 1298 { 1299 page.viewState.backgroundImage = this.createImageForPageLink( 1300 page.viewState.backgroundImage.originalSrc, page); 1301 } 1302 else if (Graph.isPageLink(page.viewState.backgroundImage.src)) 1303 { 1304 page.viewState.backgroundImage = this.createImageForPageLink( 1305 page.viewState.backgroundImage.src, page); 1306 } 1307 } 1308 1309 // Updates the page node 1310 if (page.viewState.backgroundImage != null && 1311 page.viewState.backgroundImage.originalSrc != null) 1312 { 1313 var enc = new mxCodec(mxUtils.createXmlDocument()); 1314 var temp = enc.encode(new mxGraphModel(page.root)); 1315 this.editor.graph.saveViewState(page.viewState, 1316 temp, null, resolveReferences); 1317 currNode = currNode.cloneNode(false); 1318 mxUtils.setTextContent(currNode, Graph.compressNode(temp)); 1319 } 1320 } 1321 } 1322 1323 appendPage(currNode); 1324 } 1325 } 1326 } 1327 1328 return node; 1329 }; 1330 1331 /** 1332 * Removes any values, styles and geometries from the given XML node. 1333 */ 1334 EditorUi.prototype.anonymizeString = function(text, zeros) 1335 { 1336 var result = []; 1337 1338 for (var i = 0; i < text.length; i++) 1339 { 1340 var c = text.charAt(i); 1341 1342 if (EditorUi.ignoredAnonymizedChars.indexOf(c) >= 0) 1343 { 1344 result.push(c); 1345 } 1346 else if (!isNaN(parseInt(c))) 1347 { 1348 result.push((zeros) ? '0' : Math.round(Math.random() * 9)); 1349 } 1350 else if (c.toLowerCase() != c) 1351 { 1352 result.push(String.fromCharCode(65 + Math.round(Math.random() * 25))); 1353 } 1354 else if (c.toUpperCase() != c) 1355 { 1356 result.push(String.fromCharCode(97 + Math.round(Math.random() * 25))); 1357 } 1358 else if (/\s/.test(c)) 1359 { 1360 /* any whitespace */ 1361 result.push(' '); 1362 } 1363 else 1364 { 1365 result.push('?'); 1366 } 1367 } 1368 1369 return result.join(''); 1370 }; 1371 1372 /** 1373 * Removes any values, styles and geometries from the given XML node. 1374 */ 1375 EditorUi.prototype.anonymizePatch = function(patch) 1376 { 1377 if (patch[EditorUi.DIFF_INSERT] != null) 1378 { 1379 for (var i = 0; i < patch[EditorUi.DIFF_INSERT].length; i++) 1380 { 1381 try 1382 { 1383 var data = patch[EditorUi.DIFF_INSERT][i].data; 1384 var doc = mxUtils.parseXml(data); 1385 var clone = doc.documentElement.cloneNode(false); 1386 1387 if (clone.getAttribute('name') != null) 1388 { 1389 clone.setAttribute('name', this.anonymizeString(clone.getAttribute('name'))); 1390 } 1391 1392 patch[EditorUi.DIFF_INSERT][i].data = mxUtils.getXml(clone); 1393 } 1394 catch (e) 1395 { 1396 patch[EditorUi.DIFF_INSERT][i].data = e.message; 1397 } 1398 } 1399 } 1400 1401 if (patch[EditorUi.DIFF_UPDATE] != null) 1402 { 1403 for (var pageId in patch[EditorUi.DIFF_UPDATE]) 1404 { 1405 var diff = patch[EditorUi.DIFF_UPDATE][pageId]; 1406 1407 if (diff.name != null) 1408 { 1409 diff.name = this.anonymizeString(diff.name); 1410 } 1411 1412 if (diff.cells != null) 1413 { 1414 var anonymizeCellDiffs = mxUtils.bind(this, function(key) 1415 { 1416 var cellDiffs = diff.cells[key]; 1417 1418 if (cellDiffs != null) 1419 { 1420 for (var cellId in cellDiffs) 1421 { 1422 if (cellDiffs[cellId].value != null) 1423 { 1424 cellDiffs[cellId].value = '[' + 1425 cellDiffs[cellId].value.length + ']'; 1426 } 1427 1428 if (cellDiffs[cellId].xmlValue != null) 1429 { 1430 cellDiffs[cellId].xmlValue = '[' + 1431 cellDiffs[cellId].xmlValue.length + ']'; 1432 } 1433 1434 if (cellDiffs[cellId].style != null) 1435 { 1436 cellDiffs[cellId].style = '[' + 1437 cellDiffs[cellId].style.length + ']'; 1438 } 1439 1440 if (Object.keys(cellDiffs[cellId]).length == 0) 1441 { 1442 delete cellDiffs[cellId]; 1443 } 1444 } 1445 1446 if (Object.keys(cellDiffs).length == 0) 1447 { 1448 delete diff.cells[key]; 1449 } 1450 } 1451 }); 1452 1453 anonymizeCellDiffs(EditorUi.DIFF_INSERT); 1454 anonymizeCellDiffs(EditorUi.DIFF_UPDATE); 1455 1456 if (Object.keys(diff.cells).length == 0) 1457 { 1458 delete diff.cells; 1459 } 1460 } 1461 1462 if (Object.keys(diff).length == 0) 1463 { 1464 delete patch[EditorUi.DIFF_UPDATE][pageId]; 1465 } 1466 } 1467 1468 if (Object.keys(patch[EditorUi.DIFF_UPDATE]).length == 0) 1469 { 1470 delete patch[EditorUi.DIFF_UPDATE]; 1471 } 1472 } 1473 1474 return patch; 1475 }; 1476 1477 /** 1478 * Removes any values, styles and geometries from the given XML node. 1479 */ 1480 EditorUi.prototype.anonymizeAttributes = function(node, zeros) 1481 { 1482 if (node.attributes != null) 1483 { 1484 for (var i = 0; i < node.attributes.length; i++) 1485 { 1486 if (node.attributes[i].name != 'as') 1487 { 1488 node.setAttribute(node.attributes[i].name, 1489 this.anonymizeString(node.attributes[i].value, zeros)); 1490 } 1491 } 1492 } 1493 1494 if (node.childNodes != null) 1495 { 1496 for (var i = 0; i < node.childNodes.length; i++) 1497 { 1498 this.anonymizeAttributes(node.childNodes[i], zeros); 1499 } 1500 } 1501 }; 1502 1503 /** 1504 * Removes any values, styles and geometries from the given XML node. 1505 */ 1506 EditorUi.prototype.anonymizeNode = function(node, zeros) 1507 { 1508 var nodes = node.getElementsByTagName('mxCell'); 1509 1510 for (var i = 0; i < nodes.length; i++) 1511 { 1512 if (nodes[i].getAttribute('value') != null) 1513 { 1514 nodes[i].setAttribute('value', '[' + nodes[i].getAttribute('value').length + ']'); 1515 } 1516 1517 if (nodes[i].getAttribute('xmlValue') != null) 1518 { 1519 nodes[i].setAttribute('xmlValue', '[' + nodes[i].getAttribute('xmlValue').length + ']'); 1520 } 1521 1522 if (nodes[i].getAttribute('style') != null) 1523 { 1524 nodes[i].setAttribute('style', '[' + nodes[i].getAttribute('style').length + ']'); 1525 } 1526 1527 if (nodes[i].parentNode != null && nodes[i].parentNode.nodeName != 'root' && 1528 nodes[i].parentNode.parentNode != null) 1529 { 1530 nodes[i].setAttribute('id', nodes[i].parentNode.getAttribute('id')); 1531 nodes[i].parentNode.parentNode.replaceChild(nodes[i], nodes[i].parentNode); 1532 } 1533 } 1534 1535 return node; 1536 }; 1537 1538 /** 1539 * Translates this point by the given vector. 1540 * 1541 * @param {number} dx X-coordinate of the translation. 1542 * @param {number} dy Y-coordinate of the translation. 1543 */ 1544 EditorUi.prototype.synchronizeCurrentFile = function(forceReload) 1545 { 1546 var currentFile = this.getCurrentFile(); 1547 1548 if (currentFile != null) 1549 { 1550 if (currentFile.savingFile) 1551 { 1552 this.handleError({message: mxResources.get('busy')}); 1553 } 1554 else if (!forceReload && currentFile.invalidChecksum) 1555 { 1556 currentFile.handleFileError(null, true); 1557 } 1558 else if (this.spinner.spin(document.body, mxResources.get('updatingDocument'))) 1559 { 1560 currentFile.clearAutosave(); 1561 this.editor.setStatus(''); 1562 1563 if (forceReload) 1564 { 1565 currentFile.reloadFile(mxUtils.bind(this, function() 1566 { 1567 currentFile.handleFileSuccess(DrawioFile.SYNC == 'manual'); 1568 }), mxUtils.bind(this, function(err) 1569 { 1570 currentFile.handleFileError(err, true); 1571 })); 1572 } 1573 else 1574 { 1575 currentFile.synchronizeFile(mxUtils.bind(this, function() 1576 { 1577 currentFile.handleFileSuccess(DrawioFile.SYNC == 'manual'); 1578 }), mxUtils.bind(this, function(err) 1579 { 1580 currentFile.handleFileError(err, true); 1581 })); 1582 } 1583 } 1584 } 1585 }; 1586 1587 /** 1588 * Translates this point by the given vector. 1589 * 1590 * @param {number} dx X-coordinate of the translation. 1591 * @param {number} dy Y-coordinate of the translation. 1592 */ 1593 EditorUi.prototype.getFileData = function(forceXml, forceSvg, forceHtml, embeddedCallback, 1594 ignoreSelection, currentPage, node, compact, file, uncompressed, resolveReferences) 1595 { 1596 ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; 1597 currentPage = (currentPage != null) ? currentPage : false; 1598 var graph = this.editor.graph; 1599 1600 // Forces compression of embedded XML 1601 if (forceSvg || (!forceXml && file != null && /(\.svg)$/i.test(file.getTitle()))) 1602 { 1603 var darkTheme = graph.themes != null && graph.defaultThemeName == 'darkTheme'; 1604 uncompressed = false; 1605 1606 // Exports SVG for first page while other page is visible by creating a graph 1607 // LATER: Add caching for the graph or SVG while not on first page 1608 // Dark mode requires a refresh that would destroy all handlers 1609 // LATER: Use dark theme here to bypass refresh 1610 if (darkTheme || (this.pages != null && this.currentPage != this.pages[0])) 1611 { 1612 var graphGetGlobalVariable = graph.getGlobalVariable; 1613 graph = this.createTemporaryGraph(darkTheme ? 1614 graph.getDefaultStylesheet() : graph.getStylesheet()); 1615 graph.setBackgroundImage = this.editor.graph.setBackgroundImage; 1616 var page = this.pages[0]; 1617 1618 if (this.currentPage == page) 1619 { 1620 graph.setBackgroundImage(this.editor.graph.backgroundImage); 1621 } 1622 else if (page.viewState != null && page.viewState != null) 1623 { 1624 graph.setBackgroundImage(page.viewState.backgroundImage); 1625 } 1626 1627 graph.getGlobalVariable = function(name) 1628 { 1629 if (name == 'page') 1630 { 1631 return page.getName(); 1632 } 1633 else if (name == 'pagenumber') 1634 { 1635 return 1; 1636 } 1637 1638 return graphGetGlobalVariable.apply(this, arguments); 1639 }; 1640 1641 document.body.appendChild(graph.container); 1642 graph.model.setRoot(page.root); 1643 } 1644 } 1645 1646 node = (node != null) ? node : this.getXmlFileData(ignoreSelection, 1647 currentPage, uncompressed, resolveReferences); 1648 file = (file != null) ? file : this.getCurrentFile(); 1649 1650 var result = this.createFileData(node, graph, file, window.location.href, 1651 forceXml, forceSvg, forceHtml, embeddedCallback, ignoreSelection, compact, 1652 uncompressed); 1653 1654 // Removes temporary graph from DOM 1655 if (graph != this.editor.graph) 1656 { 1657 graph.container.parentNode.removeChild(graph.container); 1658 } 1659 1660 return result; 1661 }; 1662 1663 /** 1664 * 1665 */ 1666 EditorUi.prototype.getHtml = function(node, graph, title, editLink, redirect, ignoreSelection) 1667 { 1668 ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true; 1669 var bg = null; 1670 var js = EditorUi.drawHost + '/js/embed-static.min.js'; 1671 1672 // LATER: Merge common code with EmbedDialog 1673 if (graph != null) 1674 { 1675 var bounds = (ignoreSelection) ? graph.getGraphBounds() : graph.getBoundingBox(graph.getSelectionCells()); 1676 var scale = graph.view.scale; 1677 var x0 = Math.floor(bounds.x / scale - graph.view.translate.x); 1678 var y0 = Math.floor(bounds.y / scale - graph.view.translate.y); 1679 bg = graph.background; 1680 1681 // Embed script only used if no redirect 1682 if (redirect == null) 1683 { 1684 var s = this.getBasenames().join(';'); 1685 1686 if (s.length > 0) 1687 { 1688 js = EditorUi.drawHost + '/embed.js?s=' + s; 1689 } 1690 } 1691 1692 // Adds embed attributes 1693 node.setAttribute('x0', x0); 1694 node.setAttribute('y0', y0); 1695 } 1696 1697 if (node != null) 1698 { 1699 node.setAttribute('pan', '1'); 1700 node.setAttribute('zoom', '1'); 1701 node.setAttribute('resize', '0'); 1702 node.setAttribute('fit', '0'); 1703 node.setAttribute('border', '20'); 1704 1705 // Hidden attributes 1706 node.setAttribute('links', '1'); 1707 1708 if (editLink != null) 1709 { 1710 node.setAttribute('edit', editLink); 1711 } 1712 } 1713 1714 // Makes XHTML compatible 1715 if (redirect != null) 1716 { 1717 redirect = redirect.replace(/&/g, '&'); 1718 } 1719 1720 // Removes control chars in input for correct roundtrip check 1721 var text = (node != null) ? Graph.zapGremlins(mxUtils.getXml(node)) : ''; 1722 1723 // Double compression for mxfile not fixed since it may cause imcompatibilites with 1724 // embed clients that rely on this format. HTML files and export use getHtml2. 1725 var data = Graph.compress(text); 1726 1727 // Fallback to URI encoded XML for invalid compression 1728 if (Graph.decompress(data) != text) 1729 { 1730 data = encodeURIComponent(text); 1731 } 1732 1733 var style = 'position:relative;overflow:auto;width:100%;'; 1734 1735 return ((redirect == null) ? '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5,IE=9" ><![endif]-->\n' : '') + 1736 '<!DOCTYPE html>\n<html' + ((redirect != null) ? ' xmlns="http://www.w3.org/1999/xhtml">' : '>') + 1737 '\n<head>\n' + ((redirect == null) ? ((title != null) ? '<title>' + mxUtils.htmlEntities(title) + 1738 '</title>\n' : '') : '<title>diagrams.net</title>\n') + 1739 ((redirect != null) ? '<meta http-equiv="refresh" content="0;URL=\'' + redirect + '\'"/>\n' : '') + 1740 '</head>\n<body' + 1741 (((redirect == null && bg != null && bg != mxConstants.NONE) ? ' style="background-color:' + bg + ';">' : '>')) + 1742 '\n<div class="mxgraph" style="' + style + '">\n' + 1743 '<div style="width:1px;height:1px;overflow:hidden;">' + data + '</div>\n</div>\n' + 1744 ((redirect == null) ? '<script type="text/javascript" src="' + js + '"></script>' : 1745 '<a style="position:absolute;top:50%;left:50%;margin-top:-128px;margin-left:-64px;" ' + 1746 'href="' + redirect + '" target="_blank"><img border="0" ' + 1747 'src="' + EditorUi.drawHost + '/images/drawlogo128.png"/></a>') + 1748 '\n</body>\n</html>\n'; 1749 }; 1750 1751 /** 1752 * Same as above but using the new embed code. 1753 */ 1754 EditorUi.prototype.getHtml2 = function(xml, graph, title, editLink, redirect) 1755 { 1756 var js = window.DRAWIO_VIEWER_URL || EditorUi.drawHost + '/js/viewer-static.min.js'; 1757 1758 // Makes XHTML compatible 1759 if (redirect != null) 1760 { 1761 redirect = redirect.replace(/&/g, '&'); 1762 } 1763 1764 var data = {highlight: '#0000ff', nav: this.editor.graph.foldingEnabled, resize: true, 1765 xml: Graph.zapGremlins(xml), toolbar: 'pages zoom layers lightbox'}; 1766 1767 if (this.pages != null && this.currentPage != null) 1768 { 1769 data.page = mxUtils.indexOf(this.pages, this.currentPage); 1770 } 1771 1772 var style = 'max-width:100%;border:1px solid transparent;'; 1773 1774 return ((redirect == null) ? '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5,IE=9" ><![endif]-->\n' : '') + 1775 '<!DOCTYPE html>\n<html' + ((redirect != null) ? ' xmlns="http://www.w3.org/1999/xhtml">' : '>') + 1776 '\n<head>\n' + ((redirect == null) ? ((title != null) ? '<title>' + mxUtils.htmlEntities(title) + 1777 '</title>\n' : '') : '<title>diagrams.net</title>\n') + 1778 ((redirect != null) ? '<meta http-equiv="refresh" content="0;URL=\'' + redirect + '\'"/>\n' : '') + 1779 '<meta charset="utf-8"/>\n</head>\n<body>' + 1780 '\n<div class="mxgraph" style="' + style + '" data-mxgraph="' + mxUtils.htmlEntities(JSON.stringify(data)) + '"></div>\n' + 1781 ((redirect == null) ? '<script type="text/javascript" src="' + js + '"></script>' : 1782 '<a style="position:absolute;top:50%;left:50%;margin-top:-128px;margin-left:-64px;" ' + 1783 'href="' + redirect + '" target="_blank"><img border="0" ' + 1784 'src="' + EditorUi.drawHost + '/images/drawlogo128.png"/></a>') + 1785 '\n</body>\n</html>\n'; 1786 }; 1787 1788 /** 1789 * 1790 */ 1791 EditorUi.prototype.setFileData = function(data) 1792 { 1793 data = this.validateFileData(data); 1794 this.currentPage = null; 1795 this.fileNode = null; 1796 this.pages = null; 1797 1798 var node = (data != null && data.length > 0) ? mxUtils.parseXml(data).documentElement : null; 1799 1800 // Checks for parser errors 1801 var cause = Editor.extractParserError(node, mxResources.get('invalidOrMissingFile')); 1802 1803 if (cause) 1804 { 1805 throw new Error(mxResources.get('notADiagramFile') + ' (' + cause + ')'); 1806 } 1807 else 1808 { 1809 // Some nodes must be extracted here to find the mxfile node 1810 // LATER: Remove duplicate call to extractGraphModel in overridden setGraphXml 1811 var tmp = (node != null) ? this.editor.extractGraphModel(node, true) : null; 1812 1813 if (tmp != null) 1814 { 1815 node = tmp; 1816 } 1817 1818 if (node != null && node.nodeName == 'mxfile') 1819 { 1820 var nodes = node.getElementsByTagName('diagram'); 1821 1822 if (urlParams['pages'] != '0' || nodes.length > 1 || 1823 (nodes.length == 1 && nodes[0].hasAttribute('name'))) 1824 { 1825 var selectedPage = null; 1826 this.fileNode = node; 1827 this.pages = []; 1828 1829 // Wraps page nodes 1830 for (var i = 0; i < nodes.length; i++) 1831 { 1832 // Adds page ID based on page order to match 1833 // remote IDs given if IDs are missing here 1834 if (nodes[i].getAttribute('id') == null) 1835 { 1836 nodes[i].setAttribute('id', i); 1837 } 1838 1839 var page = new DiagramPage(nodes[i]); 1840 1841 // Checks for invalid page names 1842 if (page.getName() == null) 1843 { 1844 page.setName(mxResources.get('pageWithNumber', [i + 1])); 1845 } 1846 1847 this.pages.push(page); 1848 1849 if (urlParams['page-id'] != null && page.getId() == urlParams['page-id']) 1850 { 1851 selectedPage = page; 1852 } 1853 } 1854 1855 this.currentPage = (selectedPage != null) ? selectedPage : 1856 this.pages[Math.max(0, Math.min(this.pages.length - 1, urlParams['page'] || 0))]; 1857 node = this.currentPage.node; 1858 } 1859 } 1860 1861 // Creates tabbed file structure if enforced by URL 1862 if (urlParams['pages'] != '0' && this.fileNode == null && node != null) 1863 { 1864 this.fileNode = node.ownerDocument.createElement('mxfile'); 1865 this.currentPage = new DiagramPage(node.ownerDocument.createElement('diagram')); 1866 this.currentPage.setName(mxResources.get('pageWithNumber', [1])); 1867 this.pages = [this.currentPage]; 1868 } 1869 1870 // Avoids scroll offset when switching page 1871 this.editor.setGraphXml(node); 1872 1873 // Avoids duplicate parsing of the XML stored in the node 1874 if (this.currentPage != null) 1875 { 1876 this.currentPage.root = this.editor.graph.model.root; 1877 } 1878 1879 if (urlParams['layer-ids'] != null) 1880 { 1881 try 1882 { 1883 var layerIds = urlParams['layer-ids'].split(' '); 1884 var layerIdsMap = {}; 1885 1886 for (var i = 0; i < layerIds.length; i++) 1887 { 1888 layerIdsMap[layerIds[i]] = true; 1889 } 1890 1891 var model = this.editor.graph.getModel(); 1892 var children = model.getChildren(model.root); 1893 1894 // handle layers visibility 1895 for (var i = 0; i < children.length; i++) 1896 { 1897 var child = children[i]; 1898 model.setVisible(child, layerIdsMap[child.id] || false); 1899 } 1900 } 1901 catch(e){} //ignore 1902 } 1903 } 1904 }; 1905 1906 /** 1907 * Translates this point by the given vector. 1908 * 1909 * @param {number} dx X-coordinate of the translation. 1910 * @param {number} dy Y-coordinate of the translation. 1911 */ 1912 EditorUi.prototype.getBaseFilename = function(ignorePageName) 1913 { 1914 var file = this.getCurrentFile(); 1915 var basename = (file != null && file.getTitle() != null) ? file.getTitle() : this.defaultFilename; 1916 1917 if (/(\.xml)$/i.test(basename) || /(\.html)$/i.test(basename) || 1918 /(\.svg)$/i.test(basename) || /(\.png)$/i.test(basename)) 1919 { 1920 basename = basename.substring(0, basename.lastIndexOf('.')); 1921 } 1922 1923 if (/(\.drawio)$/i.test(basename)) 1924 { 1925 basename = basename.substring(0, basename.lastIndexOf('.')); 1926 } 1927 1928 if (!ignorePageName && this.pages != null && this.pages.length > 1 && 1929 this.currentPage != null && this.currentPage.node.getAttribute('name') != null && 1930 this.currentPage.getName().length > 0) 1931 { 1932 basename = basename + '-' + this.currentPage.getName(); 1933 } 1934 1935 return basename; 1936 }; 1937 1938 /** 1939 * Translates this point by the given vector. 1940 * 1941 * @param {number} dx X-coordinate of the translation. 1942 * @param {number} dy Y-coordinate of the translation. 1943 */ 1944 EditorUi.prototype.downloadFile = function(format, uncompressed, addShadow, ignoreSelection, currentPage, 1945 pageVisible, transparent, scale, border, grid, includeXml, pageRange) 1946 { 1947 try 1948 { 1949 ignoreSelection = (ignoreSelection != null) ? ignoreSelection : this.editor.graph.isSelectionEmpty(); 1950 var basename = this.getBaseFilename(!currentPage); 1951 var filename = basename + ((format == 'xml' || (format == 'pdf' && 1952 includeXml)) ? '.drawio' : '') + '.' + format; 1953 1954 if (format == 'xml') 1955 { 1956 var data = Graph.xmlDeclaration +'\n' + 1957 this.getFileData(true, null, null, null, ignoreSelection, currentPage, 1958 null, null, null, uncompressed); 1959 1960 this.saveData(filename, format, data, 'text/xml'); 1961 } 1962 else if (format == 'html') 1963 { 1964 var data = this.getHtml2(this.getFileData(true), this.editor.graph, basename); 1965 this.saveData(filename, format, data, 'text/html'); 1966 } 1967 else if ((format == 'svg' || format == 'xmlsvg') && this.spinner.spin(document.body, mxResources.get('export'))) 1968 { 1969 var svg = null; 1970 1971 var saveSvg = mxUtils.bind(this, function(data) 1972 { 1973 if (data.length <= MAX_REQUEST_SIZE) 1974 { 1975 this.saveData(filename, 'svg', data, 'image/svg+xml'); 1976 } 1977 else 1978 { 1979 this.handleError({message: mxResources.get('drawingTooLarge')}, mxResources.get('error'), mxUtils.bind(this, function() 1980 { 1981 mxUtils.popup(svg); 1982 })); 1983 } 1984 }); 1985 1986 if (format == 'svg') 1987 { 1988 var bg = this.editor.graph.background; 1989 1990 if (transparent || bg == mxConstants.NONE) 1991 { 1992 bg = null; 1993 } 1994 1995 // Sets or disables alternate text for foreignObjects. Disabling is needed 1996 // because PhantomJS seems to ignore switch statements and paint all text. 1997 var svgRoot = this.editor.graph.getSvg(bg, null, null, null, null, ignoreSelection); 1998 1999 if (addShadow) 2000 { 2001 this.editor.graph.addSvgShadow(svgRoot); 2002 } 2003 2004 // Embeds the images in the SVG output (async) 2005 this.editor.convertImages(svgRoot, mxUtils.bind(this, mxUtils.bind(this, function(svgRoot2) 2006 { 2007 this.spinner.stop(); 2008 2009 saveSvg(Graph.xmlDeclaration + '\n' + Graph.svgDoctype + '\n' + mxUtils.getXml(svgRoot2)); 2010 }))); 2011 } 2012 else 2013 { 2014 filename = basename + '.svg'; 2015 2016 svg = this.getFileData(false, true, null, mxUtils.bind(this, function(svg) 2017 { 2018 this.spinner.stop(); 2019 saveSvg(svg); 2020 }), ignoreSelection); 2021 } 2022 } 2023 else 2024 { 2025 if (format == 'xmlpng') 2026 { 2027 filename = basename + '.png'; 2028 } 2029 else if (format == 'jpeg') 2030 { 2031 filename = basename + '.jpg'; 2032 } 2033 2034 this.saveRequest(filename, format, mxUtils.bind(this, function(newTitle, base64) 2035 { 2036 try 2037 { 2038 var prev = this.editor.graph.pageVisible; 2039 2040 if (pageVisible != null) 2041 { 2042 this.editor.graph.pageVisible = pageVisible; 2043 } 2044 2045 var req = this.createDownloadRequest(newTitle, format, ignoreSelection, base64, 2046 transparent, currentPage, scale, border, grid, includeXml, pageRange); 2047 this.editor.graph.pageVisible = prev; 2048 2049 return req; 2050 } 2051 catch (e) 2052 { 2053 this.handleError(e); 2054 } 2055 })); 2056 } 2057 } 2058 catch (e) 2059 { 2060 this.handleError(e); 2061 } 2062 }; 2063 2064 /** 2065 * Translates this point by the given vector. 2066 * 2067 * @param {number} dx X-coordinate of the translation. 2068 * @param {number} dy Y-coordinate of the translation. 2069 */ 2070 EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection, 2071 base64, transparent, currentPage, scale, border, grid, includeXml, pageRange) 2072 { 2073 var graph = this.editor.graph; 2074 var bounds = graph.getGraphBounds(); 2075 2076 // Exports only current page for images that does not contain file data, but for 2077 // the other formats with XML included or pdf with all pages, we need to send the complete data and use 2078 // the from/to URL parameters to specify the page to be exported. 2079 var data = this.getFileData(true, null, null, null, ignoreSelection, 2080 currentPage == false ? false : format != 'xmlpng', null, null, 2081 null, false, format == 'pdf'); 2082 var range = ''; 2083 var allPages = ''; 2084 2085 if (bounds.width * bounds.height > MAX_AREA || data.length > MAX_REQUEST_SIZE) 2086 { 2087 throw {message: mxResources.get('drawingTooLarge')}; 2088 } 2089 2090 var embed = (includeXml) ? '1' : '0'; 2091 2092 if (format == 'pdf') 2093 { 2094 if (pageRange != null) 2095 { 2096 allPages = '&from=' + pageRange.from + '&to=' + pageRange.to; 2097 } 2098 else if (currentPage == false) 2099 { 2100 allPages = '&allPages=1'; 2101 } 2102 } 2103 2104 if (format == 'xmlpng') 2105 { 2106 embed = '1'; 2107 format = 'png'; 2108 2109 // Finds the current page number 2110 if (this.pages != null && this.currentPage != null) 2111 { 2112 for (var i = 0; i < this.pages.length; i++) 2113 { 2114 if (this.pages[i] == this.currentPage) 2115 { 2116 range = '&from=' + i; 2117 break; 2118 } 2119 } 2120 } 2121 } 2122 2123 var bg = graph.background; 2124 2125 if ((format == 'png' || format == 'pdf') && transparent) 2126 { 2127 bg = mxConstants.NONE; 2128 } 2129 else if (!transparent && (bg == null || bg == mxConstants.NONE)) 2130 { 2131 bg = '#ffffff'; 2132 } 2133 2134 var extras = {globalVars: graph.getExportVariables()}; 2135 2136 if (grid) 2137 { 2138 extras.grid = { 2139 size: graph.gridSize, 2140 steps: graph.view.gridSteps, 2141 color: graph.view.gridColor 2142 }; 2143 } 2144 2145 if (Graph.translateDiagram) 2146 { 2147 extras.diagramLanguage = Graph.diagramLanguage; 2148 } 2149 2150 return new mxXmlRequest(EXPORT_URL, 'format=' + format + range + allPages + 2151 '&bg=' + ((bg != null) ? bg : mxConstants.NONE) + 2152 '&base64=' + base64 + '&embedXml=' + embed + '&xml=' + 2153 encodeURIComponent(data) + ((filename != null) ? 2154 '&filename=' + encodeURIComponent(filename) : '') + 2155 '&extras=' + encodeURIComponent(JSON.stringify(extras)) + 2156 (scale != null? '&scale=' + scale : '') + 2157 (border != null? '&border=' + border : '')); 2158 }; 2159 2160 /** 2161 * Translates this point by the given vector. 2162 * 2163 * @param {number} dx X-coordinate of the translation. 2164 * @param {number} dy Y-coordinate of the translation. 2165 */ 2166 EditorUi.prototype.setMode = function(mode, remember) 2167 { 2168 this.mode = mode; 2169 }; 2170 2171 /** 2172 * Loads the given file descriptor. The descriptor may define the following properties: 2173 * 2174 * - url: The url to load the data from (proxy is used if CORS is not enabled) 2175 * - data: The data to be inserted. If both, data and url are defined, then the data 2176 * is preprendended to the data returned from the given URL. 2177 * - format: Currently, only 'csv' is supported as an optional value. Default is XML. 2178 * - update: Optional URL to fetch updates from (POST request with the page XML). 2179 * - interval: Optional interval for fetching updates. Default is 60000 (60 seconds). 2180 */ 2181 EditorUi.prototype.loadDescriptor = function(desc, success, error) 2182 { 2183 var hash = window.location.hash; 2184 2185 var loadData = mxUtils.bind(this, function(data) 2186 { 2187 var realData = (desc.data != null) ? desc.data : ''; 2188 2189 if (data != null && data.length > 0) 2190 { 2191 if (realData.length > 0) 2192 { 2193 realData += '\n'; 2194 } 2195 2196 realData += data; 2197 } 2198 2199 var xml = (desc.format != 'csv' && realData.length > 0) ? realData : this.emptyDiagramXml; 2200 var tempFile = new LocalFile(this, xml, (urlParams['title'] != null) ? 2201 decodeURIComponent(urlParams['title']) : this.defaultFilename, true); 2202 tempFile.getHash = function() 2203 { 2204 return hash; 2205 }; 2206 this.fileLoaded(tempFile); 2207 2208 if (desc.format == 'csv') 2209 { 2210 this.importCsv(realData, mxUtils.bind(this, function(cells) 2211 { 2212 this.editor.undoManager.clear(); 2213 this.editor.setModified(false); 2214 this.editor.setStatus(''); 2215 })); 2216 } 2217 2218 // Installs updates 2219 if (desc.update != null) 2220 { 2221 var interval = (desc.interval != null) ? parseInt(desc.interval) : 60000; 2222 var currentThread = null; 2223 2224 var doUpdate = mxUtils.bind(this, function() 2225 { 2226 var page = this.currentPage; 2227 2228 mxUtils.post(desc.update, 'xml=' + encodeURIComponent( 2229 mxUtils.getXml(this.editor.getGraphXml())), 2230 mxUtils.bind(this, function(req) 2231 { 2232 if (page === this.currentPage) 2233 { 2234 if (req.getStatus() >= 200 && req.getStatus() <= 300) 2235 { 2236 var doc = this.updateDiagram(req.getText()); 2237 schedule(); 2238 } 2239 else 2240 { 2241 this.handleError({message: mxResources.get('error') + ' ' + req.getStatus()}); 2242 } 2243 } 2244 }), mxUtils.bind(this, function(err) 2245 { 2246 this.handleError(err); 2247 })); 2248 }); 2249 2250 var schedule = mxUtils.bind(this, function() 2251 { 2252 window.clearTimeout(currentThread); 2253 currentThread = window.setTimeout(doUpdate, interval); 2254 }); 2255 2256 this.editor.addListener('pageSelected', mxUtils.bind(this, function() 2257 { 2258 schedule(); 2259 doUpdate(); 2260 })); 2261 2262 schedule(); 2263 doUpdate(); 2264 } 2265 2266 if (success != null) 2267 { 2268 success(); 2269 } 2270 }); 2271 2272 if (desc.url != null && desc.url.length > 0) 2273 { 2274 // Cannot use proxy here as it will block unknown text content 2275 // LATER: Remove cache-control header 2276 this.editor.loadUrl(desc.url, mxUtils.bind(this, function(data) 2277 { 2278 loadData(data); 2279 }), mxUtils.bind(this, function(err) 2280 { 2281 if (error != null) 2282 { 2283 error(err) 2284 } 2285 })); 2286 } 2287 else 2288 { 2289 loadData(''); 2290 } 2291 }; 2292 2293 /** 2294 * Translates this point by the given vector. 2295 * 2296 * @param {number} dx X-coordinate of the translation. 2297 * @param {number} dy Y-coordinate of the translation. 2298 */ 2299 EditorUi.prototype.updateDiagram = function(xml) 2300 { 2301 var doc = null; 2302 var ui = this; 2303 2304 function createOverlay(desc) 2305 { 2306 var overlay = new mxCellOverlay(desc.image || graph.warningImage, 2307 desc.tooltip, desc.align, desc.valign, desc.offset); 2308 2309 // Installs a handler for clicks on the overlay 2310 overlay.addListener(mxEvent.CLICK, function(sender, evt) 2311 { 2312 ui.alert(desc.tooltip); 2313 }); 2314 2315 return overlay; 2316 }; 2317 2318 if (xml != null && xml.length > 0) 2319 { 2320 doc = mxUtils.parseXml(xml); 2321 var node = (doc != null) ? doc.documentElement : null; 2322 2323 if (node != null && node.nodeName == 'updates') 2324 { 2325 var graph = this.editor.graph; 2326 var model = graph.getModel(); 2327 model.beginUpdate(); 2328 var fit = null; 2329 2330 try 2331 { 2332 node = node.firstChild; 2333 2334 while (node != null) 2335 { 2336 if (node.nodeName == 'update') 2337 { 2338 // Resolves the cell ID 2339 var cell = model.getCell(node.getAttribute('id')); 2340 2341 if (cell != null) 2342 { 2343 // Changes the value 2344 try 2345 { 2346 var value = node.getAttribute('value'); 2347 2348 if (value != null) 2349 { 2350 var valueNode = mxUtils.parseXml(value).documentElement; 2351 2352 if (valueNode != null) 2353 { 2354 if (valueNode.getAttribute('replace-value') == '1') 2355 { 2356 model.setValue(cell, valueNode); 2357 } 2358 else 2359 { 2360 var attrs = valueNode.attributes; 2361 2362 for (var j = 0; j < attrs.length; j++) 2363 { 2364 graph.setAttributeForCell(cell, attrs[j].nodeName, 2365 (attrs[j].nodeValue.length > 0) ? attrs[j].nodeValue : null); 2366 } 2367 } 2368 } 2369 } 2370 } 2371 catch (e) 2372 { 2373 if (window.console != null) 2374 { 2375 console.log('Error in value for ' + cell.id + ': ' + e); 2376 } 2377 } 2378 2379 // Changes the style 2380 try 2381 { 2382 var style = node.getAttribute('style'); 2383 2384 if (style != null) 2385 { 2386 graph.model.setStyle(cell, style); 2387 } 2388 } 2389 catch (e) 2390 { 2391 if (window.console != null) 2392 { 2393 console.log('Error in style for ' + cell.id + ': ' + e); 2394 } 2395 } 2396 2397 // Adds or removes an overlay icon 2398 try 2399 { 2400 var icon = node.getAttribute('icon'); 2401 2402 if (icon != null) 2403 { 2404 var desc = (icon.length > 0) ? JSON.parse(icon) : null; 2405 2406 if (desc == null || !desc.append) 2407 { 2408 graph.removeCellOverlays(cell); 2409 } 2410 2411 if (desc != null) 2412 { 2413 graph.addCellOverlay(cell, createOverlay(desc)); 2414 } 2415 } 2416 } 2417 catch (e) 2418 { 2419 if (window.console != null) 2420 { 2421 console.log('Error in icon for ' + cell.id + ': ' + e); 2422 } 2423 } 2424 2425 // Replaces the geometry 2426 try 2427 { 2428 var geo = node.getAttribute('geometry'); 2429 2430 if (geo != null) 2431 { 2432 geo = JSON.parse(geo); 2433 var curr = graph.getCellGeometry(cell); 2434 2435 if (curr != null) 2436 { 2437 curr = curr.clone(); 2438 2439 // Partially overwrites geometry 2440 for (key in geo) 2441 { 2442 var val = parseFloat(geo[key]); 2443 2444 if (key == 'dx') 2445 { 2446 curr.x += val; 2447 } 2448 else if (key == 'dy') 2449 { 2450 curr.y += val; 2451 } 2452 else if (key == 'dw') 2453 { 2454 curr.width += val; 2455 } 2456 else if (key == 'dh') 2457 { 2458 curr.height += val; 2459 } 2460 else 2461 { 2462 curr[key] = parseFloat(geo[key]); 2463 } 2464 } 2465 2466 graph.model.setGeometry(cell, curr); 2467 } 2468 } 2469 } 2470 catch (e) 2471 { 2472 if (window.console != null) 2473 { 2474 console.log('Error in icon for ' + cell.id + ': ' + e); 2475 } 2476 } 2477 } // if cell != null 2478 } // if node.nodeName == 'update 2479 else if (node.nodeName == 'model') 2480 { 2481 // Finds first child element 2482 var dataNode = node.firstChild; 2483 2484 while (dataNode != null && dataNode.nodeType != mxConstants.NODETYPE_ELEMENT) 2485 { 2486 dataNode = dataNode.nextSibling; 2487 } 2488 2489 if (dataNode != null) 2490 { 2491 var dec = new mxCodec(node.firstChild); 2492 dec.decode(dataNode, model); 2493 } 2494 } 2495 else if (node.nodeName == 'view') 2496 { 2497 if (node.hasAttribute('scale')) 2498 { 2499 graph.view.scale = parseFloat(node.getAttribute('scale')); 2500 } 2501 2502 if (node.hasAttribute('dx') || node.hasAttribute('dy')) 2503 { 2504 graph.view.translate = new mxPoint(parseFloat(node.getAttribute('dx') || 0), 2505 parseFloat(node.getAttribute('dy') || 0)); 2506 } 2507 } 2508 else if (node.nodeName == 'fit') 2509 { 2510 if (node.hasAttribute('max-scale')) 2511 { 2512 fit = parseFloat(node.getAttribute('max-scale')); 2513 } 2514 else 2515 { 2516 fit = 1; 2517 } 2518 } 2519 2520 node = node.nextSibling; 2521 } // end of while 2522 } 2523 finally 2524 { 2525 model.endUpdate(); 2526 } 2527 2528 if (fit != null && this.chromelessResize) 2529 { 2530 this.chromelessResize(true, fit); 2531 } 2532 } 2533 } 2534 2535 return doc; 2536 }; 2537 2538 /** 2539 * Constructs a filename for a copy of the given file. 2540 */ 2541 EditorUi.prototype.getCopyFilename = function(file, timestamp) 2542 { 2543 var title = (file != null && file.getTitle() != null) ? 2544 file.getTitle() : this.defaultFilename; 2545 2546 // Handles extension 2547 var extension = ''; 2548 var dot = title.lastIndexOf('.'); 2549 2550 if (dot >= 0) 2551 { 2552 extension = title.substring(dot); 2553 title = title.substring(0, dot); 2554 } 2555 2556 if (timestamp) 2557 { 2558 function getFormattedTime() 2559 { 2560 var today = new Date(); 2561 var y = today.getFullYear(); 2562 // JavaScript months are 0-based. 2563 var m = today.getMonth() + 1; 2564 var d = today.getDate(); 2565 var h = today.getHours(); 2566 var mi = today.getMinutes(); 2567 var s = today.getSeconds(); 2568 2569 return y + "-" + m + "-" + d + "-" + h + "-" + mi + "-" + s; 2570 } 2571 2572 var ts = new Date(); 2573 title += ' ' + getFormattedTime(); 2574 } 2575 2576 title = mxResources.get('copyOf', [title]) + extension; 2577 2578 return title; 2579 }; 2580 2581 /** 2582 * Translates this point by the given vector. 2583 * 2584 * @param {number} dx X-coordinate of the translation. 2585 * @param {number} dy Y-coordinate of the translation. 2586 */ 2587 EditorUi.prototype.fileLoaded = function(file, noDialogs) 2588 { 2589 var oldFile = this.getCurrentFile(); 2590 this.fileLoadedError = null; 2591 this.fileEditable = null; 2592 this.setCurrentFile(null); 2593 var result = false; 2594 this.hideDialog(); 2595 2596 if (oldFile != null) 2597 { 2598 EditorUi.debug('File.closed', [oldFile]); 2599 oldFile.removeListener(this.descriptorChangedListener); 2600 oldFile.close(); 2601 } 2602 2603 this.editor.graph.model.clear(); 2604 this.editor.undoManager.clear(); 2605 2606 var noFile = mxUtils.bind(this, function() 2607 { 2608 this.setGraphEnabled(false); 2609 this.setCurrentFile(null); 2610 2611 // Keeps initial title if no file existed before 2612 if (oldFile != null) 2613 { 2614 this.updateDocumentTitle(); 2615 } 2616 2617 // File might have been loaded halfway 2618 this.editor.graph.model.clear(); 2619 this.editor.undoManager.clear(); 2620 this.setBackgroundImage(null); 2621 2622 // Avoids empty hash with no value 2623 if (!noDialogs && window.location.hash != null && window.location.hash.length > 0) 2624 { 2625 window.location.hash = ''; 2626 } 2627 2628 if (this.fname != null) 2629 { 2630 this.fnameWrapper.style.display = 'none'; 2631 this.fname.innerHTML = ''; 2632 this.fname.setAttribute('title', mxResources.get('rename')); 2633 } 2634 2635 this.editor.setStatus(''); 2636 this.updateUi(); 2637 2638 if (!noDialogs) 2639 { 2640 this.showSplash(); 2641 } 2642 }); 2643 2644 if (file != null) 2645 { 2646 try 2647 { 2648 // Workaround for delayed scroll repaint with min UI in Safari 2649 if (mxClient.IS_SF && uiTheme == 'min') 2650 { 2651 this.diagramContainer.style.visibility = ''; 2652 } 2653 2654 // Order is significant, current file needed for correct 2655 // file format for initial save after starting realtime 2656 this.openingFile = true; 2657 this.setCurrentFile(file); 2658 file.addListener('descriptorChanged', this.descriptorChangedListener); 2659 file.addListener('contentChanged', this.descriptorChangedListener); 2660 file.open(); 2661 delete this.openingFile; 2662 2663 // DescriptorChanged updates the enabled state of the graph 2664 this.setGraphEnabled(true); 2665 this.setMode(file.getMode()); 2666 this.editor.graph.model.prefix = Editor.guid() + '-'; 2667 this.editor.undoManager.clear(); 2668 this.descriptorChanged(); 2669 this.updateUi(); 2670 2671 // Realtime files have a valid status message 2672 if (!file.isEditable()) 2673 { 2674 this.editor.setStatus('<span class="geStatusAlert">' + 2675 mxUtils.htmlEntities(mxResources.get('readOnly')) + '</span>'); 2676 } 2677 // Handles modified state after error of loading new file 2678 else if (file.isModified()) 2679 { 2680 file.addUnsavedStatus(); 2681 2682 // Restores unsaved data 2683 if (file.backupPatch != null) 2684 { 2685 file.patch([file.backupPatch]); 2686 } 2687 } 2688 else 2689 { 2690 this.editor.setStatus(''); 2691 } 2692 2693 if (!this.editor.isChromelessView() || this.editor.editable) 2694 { 2695 this.editor.graph.selectUnlockedLayer(); 2696 this.showLayersDialog(); 2697 this.restoreLibraries(); 2698 2699 // Workaround for no initial focus in FF 2700 if (window.self !== window.top) 2701 { 2702 window.focus(); 2703 } 2704 } 2705 else if (this.editor.graph.isLightboxView()) 2706 { 2707 this.lightboxFit(); 2708 } 2709 2710 if (this.chromelessResize) 2711 { 2712 this.chromelessResize(); 2713 } 2714 2715 this.editor.fireEvent(new mxEventObject('fileLoaded')); 2716 result = true; 2717 2718 if (!this.isOffline() && file.getMode() != null) 2719 { 2720 EditorUi.logEvent({category: file.getMode().toUpperCase() + '-OPEN-FILE-' + file.getHash(), 2721 action: 'size_' + file.getSize(), 2722 label: 'autosave_' + ((this.editor.autosave) ? 'on' : 'off')}); 2723 } 2724 2725 EditorUi.debug('File.opened', [file]); 2726 2727 //Notify users that editing is disabled within mobile apps (mainly for MS Teams) 2728 if (urlParams['viewerOnlyMsg'] == '1') 2729 { 2730 this.showAlert(mxResources.get('viewerOnlyMsg')); 2731 } 2732 2733 if (this.editor.editable && this.mode == file.getMode() && 2734 file.getMode() != App.MODE_DEVICE && file.getMode() != null) 2735 { 2736 try 2737 { 2738 this.addRecent({id: file.getHash(), title: file.getTitle(), mode: file.getMode()}); 2739 } 2740 catch (e) 2741 { 2742 // ignore 2743 } 2744 } 2745 2746 try 2747 { 2748 mxSettings.setOpenCounter(mxSettings.getOpenCounter() + 1); 2749 mxSettings.save(); 2750 } 2751 catch (e) 2752 { 2753 // ignore 2754 } 2755 } 2756 catch (e) 2757 { 2758 this.fileLoadedError = e; 2759 2760 if (EditorUi.enableLogging && !this.isOffline()) 2761 { 2762 try 2763 { 2764 EditorUi.logEvent({category: 'ERROR-LOAD-FILE-' + 2765 ((file != null) ? file.getHash() : 'none'), 2766 action: 'message_' + e.message, 2767 label: 'stack_' + e.stack}); 2768 } 2769 catch (e) 2770 { 2771 // ignore 2772 } 2773 } 2774 2775 // Asynchronous handling of errors 2776 var fn = mxUtils.bind(this, function() 2777 { 2778 // Removes URL parameter and reloads the page 2779 if (urlParams['url'] != null && this.spinner.spin(document.body, mxResources.get('reconnecting'))) 2780 { 2781 window.location.search = this.getSearch(['url']); 2782 } 2783 else if (oldFile != null) 2784 { 2785 this.fileLoaded(oldFile); 2786 } 2787 else 2788 { 2789 noFile(); 2790 } 2791 }); 2792 2793 if (!noDialogs) 2794 { 2795 this.handleError(e, mxResources.get('errorLoadingFile'), fn, true, null, null, true); 2796 } 2797 else 2798 { 2799 fn(); 2800 } 2801 } 2802 } 2803 else 2804 { 2805 noFile(); 2806 } 2807 2808 return result; 2809 }; 2810 2811 /** 2812 * Creates a hash value for the current file. 2813 */ 2814 EditorUi.prototype.getHashValueForPages = function(pages, details) 2815 { 2816 // TODO: Avoid encoding to XML to make it faster 2817 var hash = 0; 2818 var model = new mxGraphModel(); 2819 var codec = new mxCodec(); 2820 2821 if (details != null) 2822 { 2823 details.byteCount = 0; 2824 details.attrCount = 0; 2825 details.eltCount = 0; 2826 details.nodeCount = 0; 2827 } 2828 2829 for (var i = 0; i < pages.length; i++) 2830 { 2831 this.updatePageRoot(pages[i]); 2832 var diagram = pages[i].node.cloneNode(false); 2833 2834 // FIXME: Check why names can be null in newer files 2835 // ignore in hash and do not diff null names for now 2836 diagram.removeAttribute('name'); 2837 2838 // Model is only a holder for the root 2839 model.root = pages[i].root; 2840 var xmlNode = codec.encode(model); 2841 this.editor.graph.saveViewState(pages[i].viewState, xmlNode, true); 2842 2843 // Local defaults may be different in files so ignore 2844 xmlNode.removeAttribute('pageWidth'); 2845 xmlNode.removeAttribute('pageHeight'); 2846 2847 diagram.appendChild(xmlNode); 2848 2849 if (details != null) 2850 { 2851 details.eltCount += diagram.getElementsByTagName('*').length; 2852 details.nodeCount += diagram.getElementsByTagName('mxCell').length; 2853 } 2854 2855 hash = ((hash << 5) - hash + this.hashValue(diagram, function(obj, key, value, isXml) 2856 { 2857 // Ignores JS machine rounding errors in known numeric attributes 2858 // eg. 412.33333333333326 (Webkit/FF) == 412.33333333333325 (Edge/IE11) 2859 if (isXml && (obj.nodeName == 'mxGeometry' || obj.nodeName == 'mxPoint') && 2860 (key == 'x' || key == 'y' || key == 'width' || key == 'height')) 2861 { 2862 return Math.round(value); 2863 } 2864 // Workaround for previous in patch written to mxCell in 10.0.23 2865 else if (isXml && obj.nodeName == 'mxCell' && key == 'previous') 2866 { 2867 return null; 2868 } 2869 else 2870 { 2871 return value; 2872 } 2873 }, details)) << 0; 2874 } 2875 2876 return hash; 2877 }; 2878 2879 /** 2880 * Creates a hash value for the given object. Replacer returns the value of the 2881 * property or attribute for the given object or XML node. 2882 */ 2883 EditorUi.prototype.hashValue = function(obj, replacer, details) 2884 { 2885 var hash = 0; 2886 2887 // Checks for XML nodes 2888 if (obj != null && typeof obj === 'object' && typeof obj.nodeType === 'number' && 2889 typeof obj.nodeName === 'string' && typeof obj.getAttribute === 'function') 2890 { 2891 if (obj.nodeName != null) 2892 { 2893 hash = hash ^ this.hashValue(obj.nodeName, replacer, details); 2894 } 2895 2896 if (obj.attributes != null) 2897 { 2898 if (details != null) 2899 { 2900 details.attrCount += obj.attributes.length; 2901 } 2902 2903 for (var i = 0; i < obj.attributes.length; i++) 2904 { 2905 var key = obj.attributes[i].name; 2906 var value = (replacer != null) ? replacer(obj, key, obj.attributes[i].value, true) : obj.attributes[i].value; 2907 2908 if (value != null) 2909 { 2910 hash = hash ^ (this.hashValue(key, replacer, details) + 2911 this.hashValue(value, replacer, details)); 2912 } 2913 } 2914 } 2915 2916 if (obj.childNodes != null) 2917 { 2918 for (var i = 0; i < obj.childNodes.length; i++) 2919 { 2920 hash = ((hash << 5) - hash + this.hashValue( 2921 obj.childNodes[i], replacer, details)) << 0; 2922 } 2923 } 2924 } 2925 else if (obj != null && typeof obj !== 'function') 2926 { 2927 var str = String(obj); 2928 var temp = 0; 2929 2930 if (details != null) 2931 { 2932 details.byteCount += str.length; 2933 } 2934 2935 for (var i = 0; i < str.length; i++) 2936 { 2937 temp = ((temp << 5) - temp + str.charCodeAt(i)) << 0; 2938 } 2939 2940 hash = hash ^ temp; 2941 } 2942 2943 return hash; 2944 }; 2945 2946 /** 2947 * Adds empty implementation 2948 */ 2949 EditorUi.prototype.descriptorChanged = function() 2950 { 2951 // empty 2952 }; 2953 2954 /** 2955 * Hook for subclassers. 2956 */ 2957 EditorUi.prototype.restoreLibraries = function() { }; 2958 2959 /** 2960 * Hook for subclassers. 2961 */ 2962 EditorUi.prototype.saveLibrary = function(name, images, file, mode, noSpin, noReload, fn) { }; 2963 2964 /** 2965 * 2966 */ 2967 EditorUi.prototype.isScratchpadEnabled = function() 2968 { 2969 return isLocalStorage || mxClient.IS_CHROMEAPP; 2970 }; 2971 2972 /** 2973 * Shows or hides the scratchpad library. 2974 */ 2975 EditorUi.prototype.toggleScratchpad = function() 2976 { 2977 if (this.isScratchpadEnabled()) 2978 { 2979 if (this.scratchpad == null) 2980 { 2981 StorageFile.getFileContent(this, '.scratchpad', mxUtils.bind(this, function(xml) 2982 { 2983 if (xml == null) 2984 { 2985 xml = this.emptyLibraryXml; 2986 } 2987 2988 this.loadLibrary(new StorageLibrary(this, xml, '.scratchpad')); 2989 })); 2990 } 2991 else 2992 { 2993 this.closeLibrary(this.scratchpad); 2994 } 2995 } 2996 }; 2997 2998 /** 2999 * Translates this point by the given vector. 3000 * 3001 * @param {number} dx X-coordinate of the translation. 3002 * @param {number} dy Y-coordinate of the translation. 3003 */ 3004 EditorUi.prototype.createLibraryDataFromImages = function(images) 3005 { 3006 var doc = mxUtils.createXmlDocument(); 3007 var library = doc.createElement('mxlibrary'); 3008 mxUtils.setTextContent(library, JSON.stringify(images)); 3009 doc.appendChild(library); 3010 3011 return mxUtils.getXml(doc); 3012 }; 3013 3014 /** 3015 * Translates this point by the given vector. 3016 * 3017 * @param {number} dx X-coordinate of the translation. 3018 * @param {number} dy Y-coordinate of the translation. 3019 */ 3020 EditorUi.prototype.closeLibrary = function(file) 3021 { 3022 if (file != null) 3023 { 3024 this.removeLibrarySidebar(file.getHash()); 3025 3026 if (file.constructor != LocalLibrary) 3027 { 3028 mxSettings.removeCustomLibrary(file.getHash()); 3029 } 3030 3031 if (file.title == '.scratchpad') 3032 { 3033 this.scratchpad = null; 3034 } 3035 } 3036 }; 3037 3038 /** 3039 * Translates this point by the given vector. 3040 * 3041 * @param {number} dx X-coordinate of the translation. 3042 * @param {number} dy Y-coordinate of the translation. 3043 */ 3044 EditorUi.prototype.removeLibrarySidebar = function(id) 3045 { 3046 var elts = this.sidebar.palettes[id]; 3047 3048 if (elts != null) 3049 { 3050 for (var i = 0; i < elts.length; i++) 3051 { 3052 elts[i].parentNode.removeChild(elts[i]); 3053 } 3054 3055 delete this.sidebar.palettes[id]; 3056 } 3057 }; 3058 3059 /** 3060 * Changes the position of the library in the sidebar 3061 */ 3062 EditorUi.prototype.repositionLibrary = function(nextChild) 3063 { 3064 var c = this.sidebar.container; 3065 3066 if (nextChild == null) 3067 { 3068 var elts = this.sidebar.palettes['L.scratchpad']; 3069 3070 if (elts == null) 3071 { 3072 elts = this.sidebar.palettes['search']; 3073 } 3074 3075 if (elts != null) 3076 { 3077 nextChild = elts[elts.length - 1].nextSibling; 3078 } 3079 } 3080 3081 nextChild = (nextChild != null) ? nextChild : c.firstChild.nextSibling.nextSibling; 3082 3083 var content = c.lastChild; 3084 var title = content.previousSibling; 3085 3086 c.insertBefore(content, nextChild); 3087 c.insertBefore(title, content); 3088 } 3089 3090 /** 3091 * Translates this point by the given vector. 3092 * 3093 * @param {number} dx X-coordinate of the translation. 3094 * @param {number} dy Y-coordinate of the translation. 3095 */ 3096 EditorUi.prototype.loadLibrary = function(file, expand) 3097 { 3098 var doc = mxUtils.parseXml(file.getData()); 3099 3100 if (doc.documentElement.nodeName == 'mxlibrary') 3101 { 3102 var images = JSON.parse(mxUtils.getTextContent(doc.documentElement)); 3103 this.libraryLoaded(file, images, doc.documentElement.getAttribute('title'), expand); 3104 } 3105 else 3106 { 3107 throw {message: mxResources.get('notALibraryFile')}; 3108 } 3109 }; 3110 3111 /** 3112 * Translates this point by the given vector. 3113 * 3114 * @param {number} dx X-coordinate of the translation. 3115 * @param {number} dy Y-coordinate of the translation. 3116 */ 3117 EditorUi.prototype.getLibraryStorageHint = function(file) 3118 { 3119 return ''; 3120 }; 3121 3122 /** 3123 * Translates this point by the given vector. 3124 * 3125 * @param {number} dx X-coordinate of the translation. 3126 * @param {number} dy Y-coordinate of the translation. 3127 */ 3128 EditorUi.prototype.libraryLoaded = function(file, images, optionalTitle, expand) 3129 { 3130 if (this.sidebar == null) 3131 { 3132 return; 3133 } 3134 3135 if (file.constructor != LocalLibrary) 3136 { 3137 mxSettings.addCustomLibrary(file.getHash()); 3138 } 3139 3140 if (file.title == '.scratchpad') 3141 { 3142 this.scratchpad = file; 3143 } 3144 3145 var elts = this.sidebar.palettes[file.getHash()]; 3146 var nextSibling = (elts != null) ? elts[elts.length - 1].nextSibling : null; 3147 3148 // Removes existing sidebar entry for this library 3149 this.removeLibrarySidebar(file.getHash()); 3150 var dropTarget = null; 3151 3152 var addImages = mxUtils.bind(this, function(imgs, content) 3153 { 3154 if (imgs.length == 0 && file.isEditable()) 3155 { 3156 if (dropTarget == null) 3157 { 3158 dropTarget = document.createElement('div'); 3159 dropTarget.className = 'geDropTarget'; 3160 mxUtils.write(dropTarget, mxResources.get('dragElementsHere')); 3161 } 3162 3163 content.appendChild(dropTarget); 3164 } 3165 else 3166 { 3167 this.addLibraryEntries(imgs, content); 3168 } 3169 }); 3170 3171 // Adds entries to search index 3172 // KNOWN: Existing entries are not replaced after edit of custom library 3173 if (this.sidebar != null && images != null) 3174 { 3175 this.sidebar.addEntries(images); 3176 } 3177 3178 // Adds new sidebar entry for this library 3179 var tmp = (optionalTitle != null && optionalTitle.length > 0) ? optionalTitle : file.getTitle(); 3180 var contentDiv = this.sidebar.addPalette(file.getHash(), tmp, 3181 (expand != null) ? expand : true, mxUtils.bind(this, function(content) 3182 { 3183 addImages(images, content); 3184 })); 3185 3186 this.repositionLibrary(nextSibling); 3187 3188 // Adds tooltip for backend 3189 var title = contentDiv.parentNode.previousSibling; 3190 var tip = title.getAttribute('title'); 3191 3192 if (tip != null && tip.length > 0 && file.title != '.scratchpad') 3193 { 3194 title.setAttribute('title', this.getLibraryStorageHint(file) + '\n' + tip); 3195 } 3196 3197 var buttons = document.createElement('div'); 3198 buttons.style.position = 'absolute'; 3199 buttons.style.right = '0px'; 3200 buttons.style.top = '0px'; 3201 buttons.style.padding = '8px' 3202 buttons.style.backgroundColor = 'inherit'; 3203 3204 title.style.position = 'relative'; 3205 3206 var btnWidth = 18; 3207 var btn = document.createElement('img'); 3208 btn.setAttribute('src', Editor.crossImage); 3209 btn.setAttribute('title', mxResources.get('close')); 3210 btn.setAttribute('valign', 'absmiddle'); 3211 btn.setAttribute('border', '0'); 3212 btn.style.position = 'relative'; 3213 btn.style.top = '2px'; 3214 btn.style.width = '14px'; 3215 btn.style.cursor = 'pointer'; 3216 btn.style.margin = '0 3px'; 3217 3218 if (Editor.isDarkMode()) 3219 { 3220 btn.style.filter = 'invert(100%)'; 3221 } 3222 3223 var saveBtn = null; 3224 3225 if (file.title != '.scratchpad' || this.closableScratchpad) 3226 { 3227 buttons.appendChild(btn); 3228 3229 mxEvent.addListener(btn, 'click', mxUtils.bind(this, function(evt) 3230 { 3231 // Workaround for close after any button click in IE8 3232 if (!mxEvent.isConsumed(evt)) 3233 { 3234 var fn = mxUtils.bind(this, function() 3235 { 3236 this.closeLibrary(file); 3237 }); 3238 3239 if (saveBtn != null) 3240 { 3241 this.confirm(mxResources.get('allChangesLost'), null, fn, 3242 mxResources.get('cancel'), mxResources.get('discardChanges')); 3243 } 3244 else 3245 { 3246 fn(); 3247 } 3248 3249 mxEvent.consume(evt); 3250 } 3251 })); 3252 } 3253 3254 if (file.isEditable()) 3255 { 3256 var graph = this.editor.graph; 3257 var spinBtn = null; 3258 3259 var editLibrary = mxUtils.bind(this, function(evt) 3260 { 3261 this.showLibraryDialog(file.getTitle(), contentDiv, images, file, file.getMode()); 3262 mxEvent.consume(evt); 3263 }); 3264 3265 var saveLibrary = mxUtils.bind(this, function(evt) 3266 { 3267 file.setModified(true); 3268 3269 if (file.isAutosave()) 3270 { 3271 if (spinBtn != null && spinBtn.parentNode != null) 3272 { 3273 spinBtn.parentNode.removeChild(spinBtn); 3274 } 3275 3276 spinBtn = btn.cloneNode(false); 3277 spinBtn.setAttribute('src', Editor.spinImage); 3278 spinBtn.setAttribute('title', mxResources.get('saving')); 3279 spinBtn.style.cursor = 'default'; 3280 spinBtn.style.marginRight = '2px'; 3281 spinBtn.style.marginTop = '-2px'; 3282 buttons.insertBefore(spinBtn, buttons.firstChild); 3283 title.style.paddingRight = (buttons.childNodes.length * btnWidth) + 'px'; 3284 3285 this.saveLibrary(file.getTitle(), images, file, file.getMode(), true, true, function() 3286 { 3287 if (spinBtn != null && spinBtn.parentNode != null) 3288 { 3289 spinBtn.parentNode.removeChild(spinBtn); 3290 title.style.paddingRight = (buttons.childNodes.length * btnWidth) + 'px'; 3291 } 3292 }); 3293 } 3294 else if (saveBtn == null) 3295 { 3296 saveBtn = btn.cloneNode(false); 3297 saveBtn.setAttribute('src', Editor.saveImage); 3298 saveBtn.setAttribute('title', mxResources.get('save')); 3299 buttons.insertBefore(saveBtn, buttons.firstChild); 3300 3301 mxEvent.addListener(saveBtn, 'click', mxUtils.bind(this, function(evt) 3302 { 3303 this.saveLibrary(file.getTitle(), images, file, file.getMode(), 3304 file.constructor == LocalLibrary, true, function() 3305 { 3306 if (saveBtn != null && !file.isModified()) 3307 { 3308 title.style.paddingRight = (buttons.childNodes.length * btnWidth) + 'px'; 3309 saveBtn.parentNode.removeChild(saveBtn); 3310 saveBtn = null; 3311 } 3312 }); 3313 3314 mxEvent.consume(evt); 3315 })); 3316 3317 title.style.paddingRight = (buttons.childNodes.length * btnWidth) + 'px'; 3318 } 3319 }); 3320 3321 var addCells = mxUtils.bind(this, function(cells, bounds, evt, title) 3322 { 3323 cells = graph.cloneCells(mxUtils.sortCells(graph.model.getTopmostCells(cells))); 3324 3325 // Translates cells to origin 3326 for (var i = 0; i < cells.length; i++) 3327 { 3328 var geo = graph.getCellGeometry(cells[i]); 3329 3330 if (geo != null) 3331 { 3332 geo.translate(-bounds.x, -bounds.y); 3333 } 3334 } 3335 3336 contentDiv.appendChild(this.sidebar.createVertexTemplateFromCells( 3337 cells, bounds.width, bounds.height, title || '', true, false, false)); 3338 3339 var xml = Graph.compress(mxUtils.getXml(this.editor.graph.encodeCells(cells))); 3340 var entry = {xml: xml, w: bounds.width, h: bounds.height}; 3341 3342 if (title != null) 3343 { 3344 entry.title = title; 3345 } 3346 3347 images.push(entry); 3348 saveLibrary(evt); 3349 3350 if (dropTarget != null && dropTarget.parentNode != null && images.length > 0) 3351 { 3352 dropTarget.parentNode.removeChild(dropTarget); 3353 dropTarget = null; 3354 } 3355 }); 3356 3357 var addSelection = mxUtils.bind(this, function(evt) 3358 { 3359 if (!graph.isSelectionEmpty()) 3360 { 3361 var cells = graph.getSelectionCells(); 3362 var bounds = graph.view.getBounds(cells); 3363 3364 var s = graph.view.scale; 3365 3366 bounds.x /= s; 3367 bounds.y /= s; 3368 bounds.width /= s; 3369 bounds.height /= s; 3370 3371 bounds.x -= graph.view.translate.x; 3372 bounds.y -= graph.view.translate.y; 3373 3374 addCells(cells, bounds); 3375 } 3376 else if (graph.getRubberband().isActive()) 3377 { 3378 graph.getRubberband().execute(evt); 3379 graph.getRubberband().reset(); 3380 } 3381 else 3382 { 3383 this.showError(mxResources.get('error'), mxResources.get('nothingIsSelected'), mxResources.get('ok')); 3384 } 3385 3386 mxEvent.consume(evt); 3387 }); 3388 3389 // Adds drop handler from graph 3390 mxEvent.addGestureListeners(contentDiv, function(){}, mxUtils.bind(this, function(evt) 3391 { 3392 if (graph.isMouseDown && graph.panningManager != null && graph.graphHandler.first != null) 3393 { 3394 graph.graphHandler.suspend(); 3395 3396 if (graph.graphHandler.hint != null) 3397 { 3398 graph.graphHandler.hint.style.visibility = 'hidden'; 3399 } 3400 3401 contentDiv.style.backgroundColor = '#f1f3f4'; 3402 contentDiv.style.cursor = 'copy'; 3403 graph.panningManager.stop(); 3404 graph.autoScroll = false; 3405 3406 mxEvent.consume(evt); 3407 } 3408 }), mxUtils.bind(this, function(evt) 3409 { 3410 if (graph.isMouseDown && graph.panningManager != null && graph.graphHandler != null) 3411 { 3412 contentDiv.style.backgroundColor = ''; 3413 contentDiv.style.cursor = 'default'; 3414 this.sidebar.showTooltips = true; 3415 graph.panningManager.stop(); 3416 3417 graph.graphHandler.reset(); 3418 graph.isMouseDown = false; 3419 graph.autoScroll = true; 3420 3421 addSelection(evt); 3422 mxEvent.consume(evt); 3423 } 3424 })); 3425 3426 // Handles mouse leaving the library and restoring move 3427 mxEvent.addListener(contentDiv, 'mouseleave', mxUtils.bind(this, function(evt) 3428 { 3429 if (graph.isMouseDown && graph.graphHandler.first != null) 3430 { 3431 graph.graphHandler.resume(); 3432 3433 if (graph.graphHandler.hint != null) 3434 { 3435 graph.graphHandler.hint.style.visibility = 'visible'; 3436 } 3437 3438 contentDiv.style.backgroundColor = ''; 3439 contentDiv.style.cursor = ''; 3440 graph.autoScroll = true; 3441 } 3442 })); 3443 3444 // Adds drop handler from filesystem 3445 if (Graph.fileSupport) 3446 { 3447 mxEvent.addListener(contentDiv, 'dragover', mxUtils.bind(this, function(evt) 3448 { 3449 contentDiv.style.backgroundColor = '#f1f3f4'; 3450 evt.dataTransfer.dropEffect = 'copy'; 3451 contentDiv.style.cursor = 'copy'; 3452 this.sidebar.hideTooltip(); 3453 evt.stopPropagation(); 3454 evt.preventDefault(); 3455 })); 3456 3457 mxEvent.addListener(contentDiv, 'drop', mxUtils.bind(this, function(evt) 3458 { 3459 contentDiv.style.cursor = ''; 3460 contentDiv.style.backgroundColor = ''; 3461 3462 if (evt.dataTransfer.files.length > 0) 3463 { 3464 this.importFiles(evt.dataTransfer.files, 0, 0, this.maxImageSize, mxUtils.bind(this, function(data, mimeType, x, y, w, h, img, doneFn, file) 3465 { 3466 if (data != null && mimeType.substring(0, 6) == 'image/') 3467 { 3468 var style = 'shape=image;verticalLabelPosition=bottom;verticalAlign=top;imageAspect=0;aspect=fixed;image=' + 3469 this.convertDataUri(data); 3470 var cells = [new mxCell('', new mxGeometry(0, 0, w, h), style)]; 3471 cells[0].vertex = true; 3472 3473 addCells(cells, new mxRectangle(0, 0, w, h), evt, (mxEvent.isAltDown(evt)) ? null : img.substring(0, img.lastIndexOf('.')).replace(/_/g, ' ')); 3474 3475 if (dropTarget != null && dropTarget.parentNode != null && images.length > 0) 3476 { 3477 dropTarget.parentNode.removeChild(dropTarget); 3478 dropTarget = null; 3479 } 3480 } 3481 else 3482 { 3483 var done = false; 3484 3485 var doImport = mxUtils.bind(this, function(theData, theMimeType) 3486 { 3487 if (theData != null && theMimeType == 'application/pdf') 3488 { 3489 var xml = Editor.extractGraphModelFromPdf(theData); 3490 3491 if (xml != null && xml.length > 0) 3492 { 3493 theMimeType = 'text/xml'; 3494 theData = xml; 3495 } 3496 } 3497 3498 if (theData != null) //Try to parse the file as xml (can be a library or mxfile). Otherwise, an error will be shown 3499 { 3500 var doc = mxUtils.parseXml(theData); 3501 3502 if (doc.documentElement.nodeName == 'mxlibrary') 3503 { 3504 try 3505 { 3506 var temp = JSON.parse(mxUtils.getTextContent(doc.documentElement)); 3507 addImages(temp, contentDiv); 3508 images = images.concat(temp); 3509 saveLibrary(evt); 3510 this.spinner.stop(); 3511 done = true; 3512 } 3513 catch (e) 3514 { 3515 // ignore 3516 } 3517 } 3518 else if (doc.documentElement.nodeName == 'mxfile') 3519 { 3520 try 3521 { 3522 var pages = doc.documentElement.getElementsByTagName('diagram'); 3523 3524 for (var i = 0; i < pages.length; i++) 3525 { 3526 var cells = this.stringToCells(Editor.getDiagramNodeXml(pages[i])); 3527 var size = this.editor.graph.getBoundingBoxFromGeometry(cells); 3528 addCells(cells, new mxRectangle(0, 0, size.width, size.height), evt); 3529 } 3530 3531 done = true; 3532 } 3533 catch (e) 3534 { 3535 if (window.console != null) 3536 { 3537 console.log('error in drop handler:', e); 3538 } 3539 } 3540 } 3541 } 3542 3543 if (!done) 3544 { 3545 this.spinner.stop(); 3546 this.handleError({message: mxResources.get('errorLoadingFile')}) 3547 } 3548 3549 if (dropTarget != null && dropTarget.parentNode != null && images.length > 0) 3550 { 3551 dropTarget.parentNode.removeChild(dropTarget); 3552 dropTarget = null; 3553 } 3554 }); 3555 3556 if (file != null && img != null && ((/(\.v(dx|sdx?))($|\?)/i.test(img)) || /(\.vs(x|sx?))($|\?)/i.test(img))) 3557 { 3558 this.importVisio(file, function(xml) 3559 { 3560 doImport(xml, 'text/xml'); 3561 }, null, img); 3562 } 3563 else if (!this.isOffline() && new XMLHttpRequest().upload && this.isRemoteFileFormat(data, img) && file != null) 3564 { 3565 this.parseFile(file, mxUtils.bind(this, function(xhr) 3566 { 3567 if (xhr.readyState == 4) 3568 { 3569 this.spinner.stop(); 3570 3571 if (xhr.status >= 200 && xhr.status <= 299) 3572 { 3573 doImport(xhr.responseText, 'text/xml'); 3574 } 3575 else 3576 { 3577 this.handleError({message: mxResources.get((xhr.status == 413) ? 3578 'drawingTooLarge' : 'invalidOrMissingFile')}, 3579 mxResources.get('errorLoadingFile')); 3580 } 3581 } 3582 })); 3583 } 3584 else 3585 { 3586 doImport(data, mimeType); 3587 } 3588 } 3589 })); 3590 } 3591 3592 evt.stopPropagation(); 3593 evt.preventDefault(); 3594 })); 3595 3596 mxEvent.addListener(contentDiv, 'dragleave', function(evt) 3597 { 3598 contentDiv.style.cursor = ''; 3599 contentDiv.style.backgroundColor = ''; 3600 evt.stopPropagation(); 3601 evt.preventDefault(); 3602 }); 3603 } 3604 3605 btn = btn.cloneNode(false); 3606 btn.setAttribute('src', Editor.editImage); 3607 btn.setAttribute('title', mxResources.get('edit')); 3608 buttons.insertBefore(btn, buttons.firstChild); 3609 3610 mxEvent.addListener(btn, 'click', editLibrary); 3611 mxEvent.addListener(contentDiv, 'dblclick', function(evt) 3612 { 3613 if (mxEvent.getSource(evt) == contentDiv) 3614 { 3615 editLibrary(evt); 3616 } 3617 }); 3618 3619 var btn2 = btn.cloneNode(false); 3620 btn2.setAttribute('src', Editor.plusImage); 3621 btn2.setAttribute('title', mxResources.get('add')); 3622 buttons.insertBefore(btn2, buttons.firstChild); 3623 mxEvent.addListener(btn2, 'click', addSelection); 3624 3625 if (!this.isOffline() && file.title == '.scratchpad' && EditorUi.scratchpadHelpLink != null) 3626 { 3627 var link = document.createElement('span'); 3628 link.setAttribute('title', mxResources.get('help')); 3629 link.style.cssText = 'color:#a3a3a3;text-decoration:none;margin-right:2px;cursor:pointer;'; 3630 mxUtils.write(link, '?'); 3631 3632 mxEvent.addGestureListeners(link, mxUtils.bind(this, function(evt) 3633 { 3634 this.openLink(EditorUi.scratchpadHelpLink); 3635 mxEvent.consume(evt); 3636 })); 3637 3638 buttons.insertBefore(link, buttons.firstChild); 3639 } 3640 } 3641 3642 title.appendChild(buttons); 3643 title.style.paddingRight = (buttons.childNodes.length * btnWidth) + 'px'; 3644 }; 3645 3646 /** 3647 * Adds the library entries to the given DOM node. 3648 */ 3649 EditorUi.prototype.addLibraryEntries = function(imgs, content) 3650 { 3651 for (var i = 0; i < imgs.length; i++) 3652 { 3653 var img = imgs[i]; 3654 var data = img.data; 3655 3656 if (data != null) 3657 { 3658 data = this.convertDataUri(data); 3659 var s = 'shape=image;verticalLabelPosition=bottom;verticalAlign=top;imageAspect=0;'; 3660 3661 if (img.aspect == 'fixed') 3662 { 3663 s += 'aspect=fixed;' 3664 } 3665 3666 content.appendChild(this.sidebar.createVertexTemplate(s + 'image=' + 3667 data, img.w, img.h, '', img.title || '', false, false, true)); 3668 } 3669 else if (img.xml != null) 3670 { 3671 var cells = this.stringToCells(Graph.decompress(img.xml)); 3672 3673 if (cells.length > 0) 3674 { 3675 content.appendChild(this.sidebar.createVertexTemplateFromCells( 3676 cells, img.w, img.h, img.title || '', true, false, true)); 3677 } 3678 } 3679 } 3680 }; 3681 3682 /** 3683 * Extracts the resource for the current language from the given multi language 3684 * resource object of the form {es: "...", de: "...", main: "..."} where the keys 3685 * are country codes and main defines the fallback if no resource for the current 3686 * country code exists. 3687 */ 3688 EditorUi.prototype.getResource = function(obj) 3689 { 3690 return (obj != null) ? (obj[mxLanguage] || obj.main) : null; 3691 }; 3692 3693 /** 3694 * EditorUi Overrides 3695 */ 3696 EditorUi.prototype.footerHeight = 0; 3697 3698 if (urlParams['savesidebar'] == '1') 3699 { 3700 Sidebar.prototype.thumbWidth = 64; 3701 Sidebar.prototype.thumbHeight = 64; 3702 } 3703 3704 /** 3705 * Programmatic settings for theme. 3706 */ 3707 EditorUi.initTheme = function() 3708 { 3709 if (uiTheme == 'atlas') 3710 { 3711 mxClient.link('stylesheet', STYLE_PATH + '/atlas.css'); 3712 3713 if (typeof Toolbar !== 'undefined') 3714 { 3715 Toolbar.prototype.unselectedBackground = 'linear-gradient(rgb(255, 255, 255) 0px, rgb(242, 242, 242) 100%)'; 3716 Toolbar.prototype.selectedBackground = 'rgb(242, 242, 242)'; 3717 } 3718 3719 Editor.prototype.initialTopSpacing = 3; 3720 EditorUi.prototype.menubarHeight = 41; 3721 EditorUi.prototype.toolbarHeight = 38; 3722 } 3723 else if (Editor.isDarkMode()) 3724 { 3725 mxClient.link('stylesheet', STYLE_PATH + '/dark.css'); 3726 3727 Dialog.backdropColor = Editor.darkColor; 3728 Format.inactiveTabBackgroundColor = 'black'; 3729 Graph.prototype.defaultThemeName = 'darkTheme'; 3730 Graph.prototype.shapeBackgroundColor = Editor.darkColor; 3731 Graph.prototype.shapeForegroundColor = Editor.lightColor; 3732 Graph.prototype.defaultPageBackgroundColor = Editor.darkColor; 3733 Graph.prototype.defaultPageBorderColor = '#505759'; 3734 BaseFormatPanel.prototype.buttonBackgroundColor = Editor.darkColor; 3735 mxGraphHandler.prototype.previewColor = '#cccccc'; 3736 StyleFormatPanel.prototype.defaultStrokeColor = '#cccccc'; 3737 mxConstants.DROP_TARGET_COLOR = '#00ff00'; 3738 } 3739 3740 Editor.sketchFontFamily = 'Architects Daughter'; 3741 Editor.sketchFontSource = 'https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter'; 3742 3743 // Implements the sketch-min UI 3744 if (urlParams['sketch'] == '1') 3745 { 3746 Graph.prototype.defaultVertexStyle = {'hachureGap': '4'}; 3747 Graph.prototype.defaultEdgeStyle = {'edgeStyle': 'none', 'rounded': '0', 'curved': '1', 3748 'jettySize': 'auto', 'orthogonalLoop': '1', 'endArrow': 'open', 'startSize': '14', 'endSize': '14', 3749 'sourcePerimeterSpacing': '8', 'targetPerimeterSpacing': '8'}; 3750 3751 Editor.configurationKey = '.sketch-configuration'; 3752 Editor.settingsKey = '.sketch-config'; 3753 Graph.prototype.defaultGridEnabled = urlParams['grid'] == '1'; 3754 Graph.prototype.defaultPageVisible = urlParams['pv'] == '1'; 3755 Graph.prototype.defaultEdgeLength = 120; 3756 Editor.fitWindowBorders = new mxRectangle(60, 30, 30, 30); 3757 } 3758 }; 3759 3760 EditorUi.initTheme(); 3761 3762 /** 3763 * Overrides image dialog to add image search and Google+. 3764 */ 3765 EditorUi.prototype.showImageDialog = function(title, value, fn, ignoreExisting, convertDataUri) 3766 { 3767 // KNOWN: IE+FF don't return keyboard focus after image dialog (calling focus doesn't help) 3768 var dlg = new ImageDialog(this, title, value, fn, ignoreExisting, convertDataUri); 3769 this.showDialog(dlg.container, (Graph.fileSupport) ? 480 : 360, (Graph.fileSupport) ? 200 : 90, true, true); 3770 dlg.init(); 3771 }; 3772 3773 /** 3774 * Hides the current menu. 3775 */ 3776 EditorUi.prototype.showBackgroundImageDialog = function(apply, img) 3777 { 3778 apply = (apply != null) ? apply : mxUtils.bind(this, function(image, failed) 3779 { 3780 if (!failed) 3781 { 3782 var change = new ChangePageSetup(this, null, image); 3783 change.ignoreColor = true; 3784 3785 this.editor.graph.model.execute(change); 3786 } 3787 }); 3788 var dlg = new BackgroundImageDialog(this, apply, img); 3789 this.showDialog(dlg.container, 360, 200, true, true); 3790 dlg.init(); 3791 }; 3792 3793 /** 3794 * Hides the current menu. 3795 */ 3796 EditorUi.prototype.showLibraryDialog = function(name, sidebar, images, file, mode) 3797 { 3798 var dlg = new LibraryDialog(this, name, sidebar, images, file, mode); 3799 3800 this.showDialog(dlg.container, 640, 440, true, false, mxUtils.bind(this, function(cancel) 3801 { 3802 if (cancel && this.getCurrentFile() == null && urlParams['embed'] != '1') 3803 { 3804 this.showSplash(); 3805 } 3806 })); 3807 3808 dlg.init(); 3809 }; 3810 3811 /** 3812 * Overridden to update after view state changes. 3813 */ 3814 var editorUiCreateFormat = EditorUi.prototype.createFormat; 3815 3816 EditorUi.prototype.createFormat = function(container) 3817 { 3818 var format = editorUiCreateFormat.apply(this, arguments); 3819 3820 this.editor.graph.addListener('viewStateChanged', mxUtils.bind(this, function(evt) 3821 { 3822 if (this.editor.graph.isSelectionEmpty()) 3823 { 3824 format.refresh(); 3825 } 3826 })); 3827 3828 return format; 3829 }; 3830 3831 /** 3832 * Hook for sidebar footer container. 3833 */ 3834 EditorUi.prototype.createSidebarFooterContainer = function() 3835 { 3836 var div = this.createDiv('geSidebarContainer geSidebarFooter'); 3837 div.style.position = 'absolute'; 3838 div.style.overflow = 'hidden'; 3839 3840 var elt2 = document.createElement('a'); 3841 elt2.className = 'geTitle'; 3842 elt2.style.color = '#DF6C0C'; 3843 elt2.style.fontWeight = 'bold'; 3844 elt2.style.height = '100%'; 3845 elt2.style.paddingTop = '9px'; 3846 elt2.innerHTML = '<span>+</span>'; 3847 3848 var span = elt2.getElementsByTagName('span')[0]; 3849 span.style.fontSize = '18px'; 3850 span.style.marginRight = '5px'; 3851 3852 mxUtils.write(elt2, mxResources.get('moreShapes') + '...'); 3853 3854 // Prevents focus 3855 mxEvent.addListener(elt2, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown', 3856 mxUtils.bind(this, function(evt) 3857 { 3858 evt.preventDefault(); 3859 })); 3860 3861 mxEvent.addListener(elt2, 'click', mxUtils.bind(this, function(evt) 3862 { 3863 this.actions.get('shapes').funct(); 3864 mxEvent.consume(evt); 3865 })); 3866 3867 div.appendChild(elt2); 3868 3869 return div; 3870 }; 3871 3872 /** 3873 * Translates this point by the given vector. 3874 * 3875 * @param {number} dx X-coordinate of the translation. 3876 * @param {number} dy Y-coordinate of the translation. 3877 */ 3878 EditorUi.prototype.handleError = function(resp, title, fn, invokeFnOnClose, notFoundMessage, fileHash, disableLogging) 3879 { 3880 var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {}; 3881 var e = (resp != null && resp.error != null) ? resp.error : resp; 3882 3883 // Logs errors and writes stack to console 3884 if (resp != null && resp.stack != null && resp.message != null) 3885 { 3886 try 3887 { 3888 if (!disableLogging) 3889 { 3890 EditorUi.logError('Caught: ' + 3891 (resp.message == '' && resp.name != null) ? resp.name : resp.message, 3892 resp.filename, resp.lineNumber, resp.columnNumber, resp, 'INFO'); 3893 } 3894 else 3895 { 3896 if (window.console != null) 3897 { 3898 console.error('EditorUi.handleError:', resp); 3899 } 3900 } 3901 } 3902 catch (e) 3903 { 3904 // ignore 3905 } 3906 } 3907 3908 if (e != null || title != null) 3909 { 3910 var msg = mxUtils.htmlEntities(mxResources.get('unknownError')); 3911 var btn = mxResources.get('ok'); 3912 var retry = null; 3913 title = (title != null) ? title : mxResources.get('error'); 3914 3915 if (e != null) 3916 { 3917 if (e.retry != null) 3918 { 3919 btn = mxResources.get('cancel'); 3920 retry = function() 3921 { 3922 resume(); 3923 e.retry(); 3924 }; 3925 } 3926 3927 if (e.code == 404 || e.status == 404 || e.code == 403) 3928 { 3929 if (e.code == 403) 3930 { 3931 if (e.message != null) 3932 { 3933 msg = mxUtils.htmlEntities(e.message); 3934 } 3935 else 3936 { 3937 msg = mxUtils.htmlEntities(mxResources.get('accessDenied')); 3938 } 3939 } 3940 else 3941 { 3942 msg = (notFoundMessage != null) ? notFoundMessage : 3943 mxUtils.htmlEntities(mxResources.get('fileNotFoundOrDenied') + 3944 ((this.drive != null && this.drive.user != null) ? ' (' + this.drive.user.displayName + 3945 ', ' + this.drive.user.email+ ')' : '')); 3946 } 3947 3948 var id = (notFoundMessage != null) ? null : ((fileHash != null) ? fileHash : window.location.hash); 3949 3950 // #U handles case where we tried to fallback to Google File and 3951 // hash property still shows the public URL we tried to load 3952 if (id != null && (id.substring(0, 2) == '#G' || 3953 id.substring(0, 45) == '#Uhttps%3A%2F%2Fdrive.google.com%2Fuc%3Fid%3D') && 3954 ((resp != null && resp.error != null && ((resp.error.errors != null && 3955 resp.error.errors.length > 0 && resp.error.errors[0].reason == 'fileAccess') || 3956 (resp.error.data != null && resp.error.data.length > 0 && 3957 resp.error.data[0].reason == 'fileAccess'))) || 3958 e.code == 404 || e.status == 404)) 3959 { 3960 id = (id.substring(0, 2) == '#U') ? id.substring(45, id.lastIndexOf('%26ex')) : id.substring(2); 3961 3962 // Special case where the button must have a different label and function 3963 this.showError(title, msg, mxResources.get('openInNewWindow'), mxUtils.bind(this, function() 3964 { 3965 this.editor.graph.openLink('https://drive.google.com/open?id=' + id); 3966 this.handleError(resp, title, fn, invokeFnOnClose, notFoundMessage) 3967 }), retry, mxResources.get('changeUser'), mxUtils.bind(this, function() 3968 { 3969 var driveUsers = this.drive.getUsersList(); 3970 3971 var div = document.createElement('div'); 3972 3973 var title = document.createElement('span'); 3974 title.style.marginTop = '6px'; 3975 mxUtils.write(title, mxResources.get('changeUser') + ': '); 3976 3977 div.appendChild(title); 3978 3979 var usersSelect = document.createElement('select'); 3980 usersSelect.style.width = '200px'; 3981 3982 //TODO This code is similar to Dialogs.js change user part in SplashDialog 3983 function fillUsersSelect() 3984 { 3985 usersSelect.innerHTML = ''; 3986 3987 for (var i = 0; i < driveUsers.length; i++) 3988 { 3989 var option = document.createElement('option'); 3990 mxUtils.write(option, driveUsers[i].displayName); 3991 option.value = i; 3992 usersSelect.appendChild(option); 3993 //More info (email) about the user in a disabled option 3994 option = document.createElement('option'); 3995 option.innerHTML = ' '; 3996 mxUtils.write(option, '<' + driveUsers[i].email + '>'); 3997 option.setAttribute('disabled', 'disabled'); 3998 usersSelect.appendChild(option); 3999 } 4000 4001 //Add account option 4002 var option = document.createElement('option'); 4003 mxUtils.write(option, mxResources.get('addAccount')); 4004 option.value = driveUsers.length; 4005 usersSelect.appendChild(option); 4006 } 4007 4008 fillUsersSelect(); 4009 4010 mxEvent.addListener(usersSelect, 'change', mxUtils.bind(this, function() 4011 { 4012 var userIndex = usersSelect.value; 4013 var existingAccount = driveUsers.length != userIndex; 4014 4015 if (existingAccount) 4016 { 4017 this.drive.setUser(driveUsers[userIndex]); 4018 } 4019 4020 this.drive.authorize(existingAccount, mxUtils.bind(this, function() 4021 { 4022 if (!existingAccount) 4023 { 4024 driveUsers = this.drive.getUsersList(); 4025 fillUsersSelect(); 4026 } 4027 }), mxUtils.bind(this, function(resp) 4028 { 4029 this.handleError(resp); 4030 }), true); 4031 })); 4032 4033 div.appendChild(usersSelect); 4034 4035 var dlg = new CustomDialog(this, div, mxUtils.bind(this, function() 4036 { 4037 this.loadFile(window.location.hash.substr(1), true); 4038 })); 4039 this.showDialog(dlg.container, 300, 100, true, true); 4040 }), mxResources.get('cancel'), mxUtils.bind(this, function() 4041 { 4042 this.hideDialog(); 4043 4044 if (fn != null) 4045 { 4046 fn(); 4047 } 4048 }), 480, 150); 4049 4050 return; 4051 } 4052 } 4053 4054 if (e.message != null) 4055 { 4056 if (e.message == '' && e.name != null) 4057 { 4058 msg = mxUtils.htmlEntities(e.name); 4059 } 4060 else 4061 { 4062 msg = mxUtils.htmlEntities(e.message); 4063 } 4064 } 4065 else if (e.response != null && e.response.error != null) 4066 { 4067 msg = mxUtils.htmlEntities(e.response.error); 4068 } 4069 else if (typeof window.App !== 'undefined') 4070 { 4071 if (e.code == App.ERROR_TIMEOUT) 4072 { 4073 msg = mxUtils.htmlEntities(mxResources.get('timeout')); 4074 } 4075 else if (e.code == App.ERROR_BUSY) 4076 { 4077 msg = mxUtils.htmlEntities(mxResources.get('busy')); 4078 } 4079 else if (typeof e === 'string' && e.length > 0) 4080 { 4081 msg = mxUtils.htmlEntities(e); 4082 } 4083 } 4084 } 4085 4086 var btn3 = null; 4087 var fn3 = null; 4088 4089 if (e != null && e.helpLink != null) 4090 { 4091 btn3 = mxResources.get('help'); 4092 4093 fn3 = mxUtils.bind(this, function() 4094 { 4095 return this.editor.graph.openLink(e.helpLink); 4096 }); 4097 } 4098 else if (e != null && e.ownerEmail != null) 4099 { 4100 btn3 = mxResources.get('contactOwner'); 4101 4102 msg += mxUtils.htmlEntities(' (' + btn3 + ': ' + e.ownerEmail + ')'); 4103 4104 fn3 = mxUtils.bind(this, function() 4105 { 4106 return this.openLink('mailto:' + mxUtils.htmlEntities(e.ownerEmail)); 4107 }); 4108 } 4109 4110 this.showError(title, msg, btn, fn, retry, null, null, btn3, fn3, 4111 null, null, null, (invokeFnOnClose) ? fn : null); 4112 } 4113 else if (fn != null) 4114 { 4115 fn(); 4116 } 4117 }; 4118 4119 /** 4120 * Translates this point by the given vector. 4121 * 4122 * @param {number} dx X-coordinate of the translation. 4123 * @param {number} dy Y-coordinate of the translation. 4124 */ 4125 EditorUi.prototype.alert = function(msg, fn, optionalWidth) 4126 { 4127 var dlg = new ErrorDialog(this, null, msg, mxResources.get('ok'), fn); 4128 this.showDialog(dlg.container, optionalWidth || 340, 100, true, false); 4129 dlg.init(); 4130 }; 4131 4132 /** 4133 * Translates this point by the given vector. 4134 * 4135 * @param {number} dx X-coordinate of the translation. 4136 * @param {number} dy Y-coordinate of the translation. 4137 */ 4138 EditorUi.prototype.confirm = function(msg, okFn, cancelFn, okLabel, cancelLabel, closable) 4139 { 4140 var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {}; 4141 var height = Math.min(200, Math.ceil(msg.length / 50) * 28); 4142 4143 var dlg = new ConfirmDialog(this, msg, function() 4144 { 4145 resume(); 4146 4147 if (okFn != null) 4148 { 4149 okFn(); 4150 } 4151 }, function() 4152 { 4153 resume(); 4154 4155 if (cancelFn != null) 4156 { 4157 cancelFn(); 4158 } 4159 }, okLabel, cancelLabel, null, null, null, null, height); 4160 4161 this.showDialog(dlg.container, 340, 46 + height, true, closable); 4162 dlg.init(); 4163 }; 4164 4165 /** 4166 * Creates a popup banner. 4167 */ 4168 EditorUi.prototype.showBanner = function(id, text, onclick, doNotShowAgainOnClose) 4169 { 4170 var result = false; 4171 4172 if (!this.bannerShowing && !this['hideBanner' + id] && 4173 (!isLocalStorage || mxSettings.settings == null || 4174 mxSettings.settings['close' + id] == null)) 4175 { 4176 var banner = document.createElement('div'); 4177 banner.style.cssText = 'position:absolute;bottom:10px;left:50%;max-width:90%;padding:18px 34px 12px 20px;' + 4178 'font-size:16px;font-weight:bold;white-space:nowrap;cursor:pointer;z-index:' + mxPopupMenu.prototype.zIndex + ';'; 4179 mxUtils.setPrefixedStyle(banner.style, 'box-shadow', '1px 1px 2px 0px #ddd'); 4180 mxUtils.setPrefixedStyle(banner.style, 'transform', 'translate(-50%,120%)'); 4181 mxUtils.setPrefixedStyle(banner.style, 'transition', 'all 1s ease'); 4182 banner.className = 'geBtn gePrimaryBtn'; 4183 4184 var logo = document.createElement('img'); 4185 logo.setAttribute('src', IMAGE_PATH + '/logo.png'); 4186 logo.setAttribute('border', '0'); 4187 logo.setAttribute('align', 'absmiddle'); 4188 logo.style.cssText = 'margin-top:-4px;margin-left:8px;margin-right:12px;width:26px;height:26px;'; 4189 banner.appendChild(logo); 4190 4191 var img = document.createElement('img'); 4192 img.setAttribute('src', Dialog.prototype.closeImage); 4193 img.setAttribute('title', mxResources.get((doNotShowAgainOnClose) ? 'doNotShowAgain' : 'close')); 4194 img.setAttribute('border', '0'); 4195 img.style.cssText = 'position:absolute;right:10px;top:12px;filter:invert(1);padding:6px;margin:-6px;cursor:default;'; 4196 banner.appendChild(img); 4197 4198 mxUtils.write(banner, text); 4199 document.body.appendChild(banner); 4200 this.bannerShowing = true; 4201 4202 var div = document.createElement('div'); 4203 div.style.cssText = 'font-size:11px;text-align:center;font-weight:normal;'; 4204 var chk = document.createElement('input'); 4205 chk.setAttribute('type', 'checkbox'); 4206 chk.setAttribute('id', 'geDoNotShowAgainCheckbox'); 4207 chk.style.marginRight = '6px'; 4208 4209 if (!doNotShowAgainOnClose) 4210 { 4211 div.appendChild(chk); 4212 4213 var label = document.createElement('label'); 4214 label.setAttribute('for', 'geDoNotShowAgainCheckbox'); 4215 mxUtils.write(label, mxResources.get('doNotShowAgain')); 4216 div.appendChild(label); 4217 banner.style.paddingBottom = '30px'; 4218 banner.appendChild(div); 4219 } 4220 4221 var onclose = mxUtils.bind(this, function() 4222 { 4223 if (banner.parentNode != null) 4224 { 4225 banner.parentNode.removeChild(banner); 4226 this.bannerShowing = false; 4227 4228 if (chk.checked || doNotShowAgainOnClose) 4229 { 4230 this['hideBanner' + id] = true; 4231 4232 if (isLocalStorage && mxSettings.settings != null) 4233 { 4234 mxSettings.settings['close' + id] = Date.now(); 4235 mxSettings.save(); 4236 } 4237 } 4238 } 4239 }); 4240 4241 mxEvent.addListener(img, 'click', mxUtils.bind(this, function(e) 4242 { 4243 mxEvent.consume(e); 4244 onclose(); 4245 })); 4246 4247 var hide = mxUtils.bind(this, function() 4248 { 4249 mxUtils.setPrefixedStyle(banner.style, 'transform', 'translate(-50%,120%)'); 4250 4251 window.setTimeout(mxUtils.bind(this, function() 4252 { 4253 onclose(); 4254 }), 1000); 4255 }); 4256 4257 mxEvent.addListener(banner, 'click', mxUtils.bind(this, function(e) 4258 { 4259 var source = mxEvent.getSource(e); 4260 4261 if (source != chk && source != label) 4262 { 4263 if (onclick != null) 4264 { 4265 onclick(); 4266 } 4267 4268 onclose(); 4269 mxEvent.consume(e); 4270 } 4271 else 4272 { 4273 hide(); 4274 } 4275 })); 4276 4277 window.setTimeout(mxUtils.bind(this, function() 4278 { 4279 mxUtils.setPrefixedStyle(banner.style, 'transform', 'translate(-50%,0%)'); 4280 }), 500); 4281 4282 window.setTimeout(hide, 30000); 4283 result = true; 4284 } 4285 4286 return result; 4287 }; 4288 4289 /** 4290 * Translates this point by the given vector. 4291 * 4292 * @param {number} dx X-coordinate of the translation. 4293 * @param {number} dy Y-coordinate of the translation. 4294 */ 4295 EditorUi.prototype.setCurrentFile = function(file) 4296 { 4297 if (file != null) 4298 { 4299 file.opened = new Date(); 4300 } 4301 4302 this.currentFile = file; 4303 }; 4304 4305 /** 4306 * Translates this point by the given vector. 4307 * 4308 * @param {number} dx X-coordinate of the translation. 4309 * @param {number} dy Y-coordinate of the translation. 4310 */ 4311 EditorUi.prototype.getCurrentFile = function() 4312 { 4313 return this.currentFile; 4314 }; 4315 4316 /** 4317 * Handling for canvas export. 4318 */ 4319 EditorUi.prototype.isExportToCanvas = function() 4320 { 4321 return this.editor.isExportToCanvas(); 4322 }; 4323 4324 /** 4325 * 4326 */ 4327 EditorUi.prototype.createImageDataUri = function(canvas, xml, format, dpi) 4328 { 4329 var data = canvas.toDataURL('image/' + format); 4330 4331 // Checks for valid output 4332 if (data != null && data.length > 6) 4333 { 4334 if (xml != null) 4335 { 4336 data = Editor.writeGraphModelToPng(data, 'tEXt', 'mxfile', encodeURIComponent(xml)); 4337 } 4338 4339 if (dpi > 0) 4340 { 4341 data = Editor.writeGraphModelToPng(data, 'pHYs', 'dpi', dpi); 4342 } 4343 } 4344 else 4345 { 4346 throw {message: mxResources.get('unknownError')}; 4347 } 4348 4349 return data; 4350 }; 4351 4352 /** 4353 * 4354 */ 4355 EditorUi.prototype.saveCanvas = function(canvas, xml, format, ignorePageName, dpi) 4356 { 4357 var ext = ((format == 'jpeg') ? 'jpg' : format); 4358 var filename = this.getBaseFilename(ignorePageName) + 4359 ((xml != null) ? '.drawio' : '') + '.' + ext; 4360 var data = this.createImageDataUri(canvas, xml, format, dpi); 4361 4362 this.saveData(filename, ext, data.substring(data.lastIndexOf(',') + 1), 'image/' + format, true); 4363 }; 4364 4365 /** 4366 * Returns true if files should be saved using <saveLocalFile>. 4367 */ 4368 EditorUi.prototype.isLocalFileSave = function() 4369 { 4370 return ((urlParams['save'] != 'remote' && (mxClient.IS_IE || 4371 (typeof window.Blob !== 'undefined' && typeof window.URL !== 'undefined')) && 4372 document.documentMode != 9 && document.documentMode != 8 && 4373 document.documentMode != 7) || 4374 this.isOfflineApp() || mxClient.IS_IOS); 4375 }; 4376 4377 /** 4378 * Translates this point by the given vector. 4379 * 4380 * @param {number} dx X-coordinate of the translation. 4381 * @param {number} dy Y-coordinate of the translation. 4382 */ 4383 EditorUi.prototype.showTextDialog = function(title, text) 4384 { 4385 var dlg = new TextareaDialog(this, title, text, null, null, mxResources.get('close')); 4386 dlg.textarea.style.width = '600px'; 4387 dlg.textarea.style.height = '380px'; 4388 this.showDialog(dlg.container, 620, 460, true, true, null, null, null, null, true); 4389 dlg.init(); 4390 document.execCommand('selectall', false, null); 4391 }; 4392 4393 /** 4394 * Translates this point by the given vector. 4395 * 4396 * @param {number} dx X-coordinate of the translation. 4397 * @param {number} dy Y-coordinate of the translation. 4398 */ 4399 EditorUi.prototype.doSaveLocalFile = function(data, filename, mimeType, base64Encoded, format, defaultExtension) 4400 { 4401 // Appends .drawio extension for XML files with no extension 4402 // to avoid the browser to automatically append .xml instead 4403 if (mimeType == 'text/xml' && 4404 !/(\.drawio)$/i.test(filename) && 4405 !/(\.xml)$/i.test(filename) && 4406 !/(\.svg)$/i.test(filename) && 4407 !/(\.html)$/i.test(filename)) 4408 { 4409 defaultExtension = (defaultExtension != null) ? defaultExtension : 'drawio'; 4410 filename = filename + '.' + defaultExtension; 4411 } 4412 4413 // Newer versions of IE 4414 if (window.Blob && navigator.msSaveOrOpenBlob) 4415 { 4416 var blob = (base64Encoded) ? 4417 this.base64ToBlob(data, mimeType) : 4418 new Blob([data], {type: mimeType}) 4419 navigator.msSaveOrOpenBlob(blob, filename); 4420 } 4421 // Older versions of IE (binary not supported) 4422 else if (mxClient.IS_IE) 4423 { 4424 var win = window.open('about:blank', '_blank'); 4425 4426 if (win == null) 4427 { 4428 mxUtils.popup(data, true); 4429 } 4430 else 4431 { 4432 win.document.write(data); 4433 win.document.close(); 4434 win.document.execCommand('SaveAs', true, filename); 4435 win.close(); 4436 } 4437 } 4438 else if (mxClient.IS_IOS && this.isOffline()) 4439 { 4440 // Workaround for "WebKitBlobResource error 1" in mobile Safari 4441 if (!navigator.standalone && mimeType != null && mimeType.substring(0, 6) == 'image/') 4442 { 4443 this.openInNewWindow(data, mimeType, base64Encoded); 4444 } 4445 else 4446 { 4447 this.showTextDialog(filename + ':', data); 4448 } 4449 } 4450 else 4451 { 4452 var a = document.createElement('a'); 4453 4454 // Workaround for mxXmlRequest.simulate no longer working in PaleMoon 4455 // if this is used (ie PNG export broken after XML export in PaleMoon) 4456 // and for "WebKitBlobResource error 1" for all browsers on iOS. 4457 var useDownload = (navigator.userAgent == null || 4458 navigator.userAgent.indexOf("PaleMoon/") < 0) && 4459 typeof a.download !== 'undefined'; 4460 4461 // Workaround for Chromium 65 cross-domain anchor download issue 4462 if (mxClient.IS_GC && navigator.userAgent != null) 4463 { 4464 var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) 4465 var vers = raw ? parseInt(raw[2], 10) : false; 4466 useDownload = vers == 65 ? false : useDownload; 4467 } 4468 4469 if (useDownload || this.isOffline()) 4470 { 4471 a.href = URL.createObjectURL((base64Encoded) ? 4472 this.base64ToBlob(data, mimeType) : 4473 new Blob([data], {type: mimeType})); 4474 4475 if (useDownload) 4476 { 4477 a.download = filename; 4478 } 4479 else 4480 { 4481 // Workaround for same window in Safari 4482 a.setAttribute('target', '_blank'); 4483 } 4484 4485 document.body.appendChild(a); 4486 4487 try 4488 { 4489 window.setTimeout(function() 4490 { 4491 URL.revokeObjectURL(a.href); 4492 }, 20000); 4493 4494 a.click(); 4495 a.parentNode.removeChild(a); 4496 } 4497 catch (e) 4498 { 4499 // ignore 4500 } 4501 } 4502 else 4503 { 4504 var req = this.createEchoRequest(data, filename, mimeType, base64Encoded, format); 4505 4506 req.simulate(document, '_blank'); 4507 } 4508 } 4509 }; 4510 4511 /** 4512 * Translates this point by the given vector. 4513 * 4514 * @param {number} dx X-coordinate of the translation. 4515 * @param {number} dy Y-coordinate of the translation. 4516 */ 4517 EditorUi.prototype.createEchoRequest = function(data, filename, mimeType, base64Encoded, format, base64Response) 4518 { 4519 var param = (typeof(pako) === 'undefined' || true) ? 'xml=' + encodeURIComponent(data) : 4520 'data=' + encodeURIComponent(Graph.compress(data)); 4521 4522 return new mxXmlRequest(SAVE_URL, param + 4523 ((mimeType != null) ? '&mime=' + mimeType : '') + 4524 ((format != null) ? '&format=' + format : '') + 4525 ((base64Response != null) ? '&base64=' + base64Response : '') + 4526 ((filename != null) ? '&filename=' + encodeURIComponent(filename) : '') + 4527 ((base64Encoded) ? '&binary=1' : '')); 4528 }; 4529 4530 /** 4531 * Translates this point by the given vector. 4532 * 4533 * @param {number} dx X-coordinate of the translation. 4534 * @param {number} dy Y-coordinate of the translation. 4535 */ 4536 EditorUi.prototype.base64ToBlob = function(base64Data, contentType) 4537 { 4538 contentType = contentType || ''; 4539 var sliceSize = 1024; 4540 var byteCharacters = atob(base64Data); 4541 var bytesLength = byteCharacters.length; 4542 var slicesCount = Math.ceil(bytesLength / sliceSize); 4543 var byteArrays = new Array(slicesCount); 4544 4545 for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) 4546 { 4547 var begin = sliceIndex * sliceSize; 4548 var end = Math.min(begin + sliceSize, bytesLength); 4549 4550 var bytes = new Array(end - begin); 4551 4552 for (var offset = begin, i = 0 ; offset < end; ++i, ++offset) 4553 { 4554 bytes[i] = byteCharacters[offset].charCodeAt(0); 4555 } 4556 4557 byteArrays[sliceIndex] = new Uint8Array(bytes); 4558 } 4559 4560 return new Blob(byteArrays, {type: contentType}); 4561 }; 4562 4563 /** 4564 * Translates this point by the given vector. 4565 * 4566 * @param {number} dx X-coordinate of the translation. 4567 * @param {number} dy Y-coordinate of the translation. 4568 */ 4569 EditorUi.prototype.saveLocalFile = function(data, filename, mimeType, base64Encoded, format, allowBrowser, allowTab, defaultExtension) 4570 { 4571 allowBrowser = (allowBrowser != null) ? allowBrowser : false; 4572 allowTab = (allowTab != null) ? allowTab : (format != 'vsdx') && (!mxClient.IS_IOS || !navigator.standalone); 4573 var count = this.getServiceCount(allowBrowser); 4574 4575 if (isLocalStorage) 4576 { 4577 count++; 4578 } 4579 4580 var rowLimit = (count <= 4) ? 2 : (count > 6 ? 4 : 3); 4581 4582 var dlg = new CreateDialog(this, filename, mxUtils.bind(this, function(newTitle, mode) 4583 { 4584 try 4585 { 4586 // Opens a new window 4587 if (mode == '_blank') 4588 { 4589 if (mimeType != null && mimeType.substring(0, 6) == 'image/') 4590 { 4591 this.openInNewWindow(data, mimeType, base64Encoded); 4592 } 4593 else if (mimeType != null && mimeType.substring(0, 9) == 'text/html') 4594 { 4595 var dlg = new EmbedDialog(this, data); 4596 this.showDialog(dlg.container, 450, 240, true, true); 4597 dlg.init(); 4598 } 4599 else 4600 { 4601 var win = window.open('about:blank'); 4602 4603 if (win == null) 4604 { 4605 mxUtils.popup(data, true); 4606 } 4607 else 4608 { 4609 win.document.write('<pre>' + mxUtils.htmlEntities(data, false) + '</pre>'); 4610 win.document.close(); 4611 } 4612 } 4613 } 4614 else if (mode == App.MODE_DEVICE || mode == 'download') 4615 { 4616 this.doSaveLocalFile(data, newTitle, mimeType, base64Encoded, null, defaultExtension); 4617 } 4618 else if (newTitle != null && newTitle.length > 0) 4619 { 4620 this.pickFolder(mode, mxUtils.bind(this, function(folderId) 4621 { 4622 try 4623 { 4624 this.exportFile(data, newTitle, mimeType, base64Encoded, mode, folderId); 4625 } 4626 catch (e) 4627 { 4628 this.handleError(e); 4629 } 4630 })); 4631 } 4632 } 4633 catch (e) 4634 { 4635 this.handleError(e); 4636 } 4637 }), mxUtils.bind(this, function() 4638 { 4639 this.hideDialog(); 4640 }), mxResources.get('saveAs'), mxResources.get('download'), false, allowBrowser, allowTab, 4641 null, count > 1, rowLimit, data, mimeType, base64Encoded); 4642 var height = (this.isServices(count)) ? ((count > rowLimit) ? 390 : 270) : 160; 4643 this.showDialog(dlg.container, 420, height, true, true); 4644 dlg.init(); 4645 }; 4646 4647 /** 4648 * 4649 */ 4650 EditorUi.prototype.openInNewWindow = function(data, mimeType, base64Encoded) 4651 { 4652 var win = window.open('about:blank'); 4653 4654 if (win == null || win.document == null) 4655 { 4656 mxUtils.popup(data, true); 4657 } 4658 else 4659 { 4660 if (mimeType == 'image/svg+xml' && !mxClient.IS_SVG) 4661 { 4662 // KNOWN: Output is scaled in Chrome on macOS 4663 win.document.write('<html><pre>' + mxUtils.htmlEntities(data, false) + '</pre></html>'); 4664 win.document.close(); 4665 } 4666 else 4667 { 4668 if (mimeType == 'image/svg+xml') 4669 { 4670 win.document.write('<html>'+ data + '</html>'); 4671 } 4672 else 4673 { 4674 var temp = (base64Encoded) ? data : btoa(unescape(encodeURIComponent(data))); 4675 4676 win.document.write('<html><img style="max-width:100%;" src="data:' + 4677 mimeType + ';base64,' + temp + '"/></html>'); 4678 } 4679 4680 win.document.close(); 4681 } 4682 } 4683 }; 4684 4685 var editoUiAddChromelessToolbarItems = EditorUi.prototype.addChromelessToolbarItems; 4686 4687 /** 4688 * Image export in viewer is only allowed for same domain or hosted environments but 4689 * but disabled to avoid cross domain image export in canvas which isn't allowed. 4690 */ 4691 EditorUi.prototype.isChromelessImageExportEnabled = function() 4692 { 4693 return this.getServiceName() != 'draw.io' || 4694 /.*\.draw\.io$/.test(window.location.hostname) || 4695 /.*\.diagrams\.net$/.test(window.location.hostname); 4696 }; 4697 4698 /** 4699 * Creates a temporary graph instance for rendering off-screen content. 4700 */ 4701 EditorUi.prototype.addChromelessToolbarItems = function(addButton) 4702 { 4703 if (urlParams['tags'] != null) 4704 { 4705 this.tagsComponent = null; 4706 this.tagsDialog = null; 4707 4708 var tagsButton = addButton(mxUtils.bind(this, function(evt) 4709 { 4710 if (this.tagsComponent == null) 4711 { 4712 this.tagsComponent = this.editor.graph.createTagsDialog(mxUtils.bind(this, function() 4713 { 4714 return this.tagsDialog != null; 4715 }), true); 4716 4717 this.tagsComponent.div.getElementsByTagName('div')[0].style.position = ''; 4718 mxUtils.setPrefixedStyle(this.tagsComponent.div.style, 'borderRadius', '5px'); 4719 this.tagsComponent.div.className = 'geScrollable'; 4720 this.tagsComponent.div.style.maxHeight = '160px'; 4721 this.tagsComponent.div.style.maxWidth = '120px'; 4722 this.tagsComponent.div.style.padding = '4px'; 4723 this.tagsComponent.div.style.overflow = 'auto'; 4724 this.tagsComponent.div.style.height = 'auto'; 4725 this.tagsComponent.div.style.position = 'fixed'; 4726 this.tagsComponent.div.style.fontFamily = Editor.defaultHtmlFont; 4727 4728 if (!mxClient.IS_IE && !mxClient.IS_IE11) 4729 { 4730 this.tagsComponent.div.style.backgroundColor = '#000000'; 4731 this.tagsComponent.div.style.color = '#ffffff'; 4732 mxUtils.setOpacity(this.tagsComponent.div, 80); 4733 } 4734 else 4735 { 4736 this.tagsComponent.div.style.backgroundColor = '#ffffff'; 4737 this.tagsComponent.div.style.border = '2px solid black'; 4738 this.tagsComponent.div.style.color = '#000000'; 4739 } 4740 } 4741 4742 if (this.tagsDialog != null) 4743 { 4744 this.tagsDialog.parentNode.removeChild(this.tagsDialog); 4745 this.tagsDialog = null; 4746 } 4747 else 4748 { 4749 this.tagsDialog = this.tagsComponent.div; 4750 4751 mxEvent.addListener(this.tagsDialog, 'mouseleave', mxUtils.bind(this, function() 4752 { 4753 if (this.tagsDialog != null) 4754 { 4755 this.tagsDialog.parentNode.removeChild(this.tagsDialog); 4756 this.tagsDialog = null; 4757 } 4758 })); 4759 4760 var r = tagsButton.getBoundingClientRect(); 4761 this.tagsDialog.style.left = r.left + 'px'; 4762 this.tagsDialog.style.bottom = parseInt(this.chromelessToolbar.style.bottom) + 4763 this.chromelessToolbar.offsetHeight + 4 + 'px'; 4764 4765 // Puts the dialog on top of the container z-index 4766 var style = mxUtils.getCurrentStyle(this.editor.graph.container); 4767 this.tagsDialog.style.zIndex = style.zIndex; 4768 document.body.appendChild(this.tagsDialog); 4769 4770 this.tagsComponent.refresh(); 4771 this.editor.fireEvent(new mxEventObject('tagsDialogShown')); 4772 } 4773 4774 mxEvent.consume(evt); 4775 }), Editor.tagsImage, mxResources.get('tags')); 4776 4777 // Shows/hides tags button depending on content 4778 var model = this.editor.graph.getModel(); 4779 4780 model.addListener(mxEvent.CHANGE, mxUtils.bind(this, function() 4781 { 4782 var tags = this.editor.graph.getAllTags(); 4783 tagsButton.style.display = (tags.length > 0) ? '' : 'none'; 4784 })); 4785 } 4786 4787 editoUiAddChromelessToolbarItems.apply(this, arguments); 4788 4789 this.editor.addListener('tagsDialogShown', mxUtils.bind(this, function() 4790 { 4791 if (this.layersDialog != null) 4792 { 4793 this.layersDialog.parentNode.removeChild(this.layersDialog); 4794 this.layersDialog = null; 4795 } 4796 })); 4797 4798 this.editor.addListener('layersDialogShown', mxUtils.bind(this, function() 4799 { 4800 if (this.tagsDialog != null) 4801 { 4802 this.tagsDialog.parentNode.removeChild(this.tagsDialog); 4803 this.tagsDialog = null; 4804 } 4805 })); 4806 4807 this.editor.addListener('pageSelected', mxUtils.bind(this, function() 4808 { 4809 if (this.tagsDialog != null) 4810 { 4811 this.tagsDialog.parentNode.removeChild(this.tagsDialog); 4812 this.tagsDialog = null; 4813 } 4814 4815 if (this.layersDialog != null) 4816 { 4817 this.layersDialog.parentNode.removeChild(this.layersDialog); 4818 this.layersDialog = null; 4819 } 4820 })); 4821 4822 mxEvent.addListener(this.editor.graph.container, 'click', mxUtils.bind(this, function() 4823 { 4824 if (this.tagsDialog != null) 4825 { 4826 this.tagsDialog.parentNode.removeChild(this.tagsDialog); 4827 this.tagsDialog = null; 4828 } 4829 4830 if (this.layersDialog != null) 4831 { 4832 this.layersDialog.parentNode.removeChild(this.layersDialog); 4833 this.layersDialog = null; 4834 } 4835 })); 4836 4837 if (this.isExportToCanvas() && this.isChromelessImageExportEnabled()) 4838 { 4839 this.exportDialog = null; 4840 4841 var exportButton = addButton(mxUtils.bind(this, function(evt) 4842 { 4843 var clickHandler = mxUtils.bind(this, function() 4844 { 4845 mxEvent.removeListener(this.editor.graph.container, 'click', clickHandler); 4846 4847 if (this.exportDialog != null) 4848 { 4849 this.exportDialog.parentNode.removeChild(this.exportDialog); 4850 this.exportDialog = null; 4851 } 4852 }); 4853 4854 if (this.exportDialog != null) 4855 { 4856 clickHandler.apply(this); 4857 } 4858 else 4859 { 4860 this.exportDialog = document.createElement('div'); 4861 var r = exportButton.getBoundingClientRect(); 4862 4863 mxUtils.setPrefixedStyle(this.exportDialog.style, 'borderRadius', '5px'); 4864 this.exportDialog.style.position = 'fixed'; 4865 this.exportDialog.style.textAlign = 'center'; 4866 this.exportDialog.style.fontFamily = Editor.defaultHtmlFont; 4867 this.exportDialog.style.backgroundColor = '#000000'; 4868 this.exportDialog.style.width = '50px'; 4869 this.exportDialog.style.height = '50px'; 4870 this.exportDialog.style.padding = '4px 2px 4px 2px'; 4871 this.exportDialog.style.color = '#ffffff'; 4872 mxUtils.setOpacity(this.exportDialog, 70); 4873 this.exportDialog.style.left = r.left + 'px'; 4874 this.exportDialog.style.bottom = parseInt(this.chromelessToolbar.style.bottom) + 4875 this.chromelessToolbar.offsetHeight + 4 + 'px'; 4876 4877 // Puts the dialog on top of the container z-index 4878 var style = mxUtils.getCurrentStyle(this.editor.graph.container); 4879 this.exportDialog.style.zIndex = style.zIndex; 4880 4881 var spinner = new Spinner({ 4882 lines: 8, // The number of lines to draw 4883 length: 6, // The length of each line 4884 width: 5, // The line thickness 4885 radius: 6, // The radius of the inner circle 4886 rotate: 0, // The rotation offset 4887 color: '#fff', // #rgb or #rrggbb 4888 speed: 1.5, // Rounds per second 4889 trail: 60, // Afterglow percentage 4890 shadow: false, // Whether to render a shadow 4891 hwaccel: false, // Whether to use hardware acceleration 4892 top: '28px', 4893 zIndex: 2e9 // The z-index (defaults to 2000000000) 4894 }); 4895 spinner.spin(this.exportDialog); 4896 4897 this.editor.exportToCanvas(mxUtils.bind(this, function(canvas) 4898 { 4899 spinner.stop(); 4900 4901 this.exportDialog.style.width = 'auto'; 4902 this.exportDialog.style.height = 'auto'; 4903 this.exportDialog.style.padding = '10px'; 4904 4905 var data = this.createImageDataUri(canvas, null, 'png'); 4906 var img = document.createElement('img'); 4907 4908 img.style.maxWidth = '140px'; 4909 img.style.maxHeight = '140px'; 4910 img.style.cursor = 'pointer'; 4911 img.style.backgroundColor = 'white'; 4912 4913 img.setAttribute('title', mxResources.get('openInNewWindow')); 4914 img.setAttribute('border', '0'); 4915 img.setAttribute('src', data); 4916 4917 this.exportDialog.appendChild(img); 4918 4919 mxEvent.addListener(img, 'click', mxUtils.bind(this, function() 4920 { 4921 this.openInNewWindow(data.substring(data.indexOf(',') + 1), 'image/png', true); 4922 clickHandler.apply(this, arguments); 4923 })); 4924 }), null, this.thumbImageCache, null, mxUtils.bind(this, function(e) 4925 { 4926 this.spinner.stop(); 4927 this.handleError(e); 4928 }), null, null, null, null, null, null, null, Editor.defaultBorder); 4929 4930 mxEvent.addListener(this.editor.graph.container, 'click', clickHandler); 4931 document.body.appendChild(this.exportDialog); 4932 } 4933 4934 mxEvent.consume(evt); 4935 }), Editor.cameraImage, mxResources.get('export')); 4936 } 4937 }; 4938 4939 /** 4940 * Translates this point by the given vector. 4941 * 4942 * @param {number} dx X-coordinate of the translation. 4943 * @param {number} dy Y-coordinate of the translation. 4944 */ 4945 EditorUi.prototype.saveData = function(filename, format, data, mime, base64Encoded) 4946 { 4947 if (this.isLocalFileSave()) 4948 { 4949 this.saveLocalFile(data, filename, mime, base64Encoded, format); 4950 } 4951 else 4952 { 4953 this.saveRequest(filename, format, mxUtils.bind(this, function(newTitle, base64) 4954 { 4955 return this.createEchoRequest(data, newTitle, mime, base64Encoded, format, base64); 4956 }), data, base64Encoded, mime); 4957 } 4958 }; 4959 4960 /** 4961 * Translates this point by the given vector. 4962 * 4963 * Last 3 argument are optional and must only be used if the data can be stored as is on the client 4964 * side without requiring a server roundtrip. 4965 * 4966 * @param {number} dx X-coordinate of the translation. 4967 * @param {number} dy Y-coordinate of the translation. 4968 */ 4969 EditorUi.prototype.saveRequest = function(filename, format, fn, data, base64Encoded, mimeType, allowTab) 4970 { 4971 allowTab = (allowTab != null) ? allowTab : !mxClient.IS_IOS || !navigator.standalone; 4972 var count = this.getServiceCount(false); 4973 4974 if (isLocalStorage) 4975 { 4976 count++; 4977 } 4978 4979 var rowLimit = (count <= 4) ? 2 : (count > 6 ? 4 : 3); 4980 4981 var dlg = new CreateDialog(this, filename, mxUtils.bind(this, function(newTitle, mode) 4982 { 4983 if (mode == '_blank' || newTitle != null && newTitle.length > 0) 4984 { 4985 var base64 = (mode == App.MODE_DEVICE || mode == 'download' || mode == null || mode == '_blank') ? '0' : '1'; 4986 var xhr = fn((mode == '_blank') ? null : newTitle, base64); 4987 4988 if (xhr != null) 4989 { 4990 if (mode == App.MODE_DEVICE || mode == 'download' || mode == '_blank') 4991 { 4992 xhr.simulate(document, '_blank'); 4993 } 4994 else 4995 { 4996 this.pickFolder(mode, mxUtils.bind(this, function(folderId) 4997 { 4998 mimeType = (mimeType != null) ? mimeType : ((format == 'pdf') ? 4999 'application/pdf' : 'image/' + format); 5000 5001 // Workaround for no roundtrip required if data is available on client-side 5002 // TODO: Refactor the saveData/saveRequest call chain for local data 5003 if (data != null) 5004 { 5005 try 5006 { 5007 this.exportFile(data, newTitle, mimeType, true, mode, folderId); 5008 } 5009 catch (e) 5010 { 5011 this.handleError(e); 5012 } 5013 } 5014 else if (this.spinner.spin(document.body, mxResources.get('saving'))) 5015 { 5016 // LATER: Catch possible mixed content error 5017 // see http://stackoverflow.com/questions/30646417/catching-mixed-content-error 5018 xhr.send(mxUtils.bind(this, function() 5019 { 5020 this.spinner.stop(); 5021 5022 if (xhr.getStatus() >= 200 && xhr.getStatus() <= 299) 5023 { 5024 try 5025 { 5026 this.exportFile(xhr.getText(), newTitle, mimeType, true, mode, folderId); 5027 } 5028 catch (e) 5029 { 5030 this.handleError(e); 5031 } 5032 } 5033 else 5034 { 5035 this.handleError({message: mxResources.get('errorSavingFile')}); 5036 } 5037 }), function(resp) 5038 { 5039 this.spinner.stop(); 5040 this.handleError(resp); 5041 }); 5042 } 5043 })); 5044 } 5045 } 5046 } 5047 }), mxUtils.bind(this, function() 5048 { 5049 this.hideDialog(); 5050 }), mxResources.get('saveAs'), mxResources.get('download'), false, false, allowTab, 5051 null, count > 1, rowLimit, data, mimeType, base64Encoded); 5052 5053 var height = (this.isServices(count)) ? ((count > 4) ? 390 : 270) : 160; 5054 this.showDialog(dlg.container, 420, height, true, true); 5055 dlg.init(); 5056 }; 5057 5058 /** 5059 * Returns whether or not any services should be shown in dialogs 5060 */ 5061 EditorUi.prototype.isServices = function(count) 5062 { 5063 var noServices = 1; //(mxClient.IS_IOS) ? 0 : 1; 5064 return count != noServices; 5065 }; 5066 5067 /** 5068 * 5069 */ 5070 EditorUi.prototype.getEditBlankXml = function() 5071 { 5072 return this.getFileData(true); 5073 }; 5074 5075 /** 5076 * Hook for subclassers. 5077 */ 5078 EditorUi.prototype.exportFile = function(data, filename, mimeType, base64Encoded, mode, folderId) 5079 { 5080 // do nothing 5081 }; 5082 5083 /** 5084 * Hook for subclassers. 5085 */ 5086 EditorUi.prototype.pickFolder = function(mode, fn, enabled) 5087 { 5088 fn(null); 5089 }; 5090 5091 /** 5092 * 5093 */ 5094 EditorUi.prototype.exportSvg = function(scale, transparentBackground, ignoreSelection, addShadow, 5095 editable, embedImages, border, noCrop, currentPage, linkTarget, keepTheme, exportType, 5096 embedFonts, saveFn) 5097 { 5098 if (this.spinner.spin(document.body, mxResources.get('export'))) 5099 { 5100 try 5101 { 5102 var selectionEmpty = this.editor.graph.isSelectionEmpty(); 5103 ignoreSelection = (ignoreSelection != null) ? ignoreSelection : selectionEmpty; 5104 var bg = (transparentBackground) ? null : this.editor.graph.background; 5105 5106 if (bg == mxConstants.NONE) 5107 { 5108 bg = null; 5109 } 5110 5111 // Handles special case where background is null but transparent is false 5112 if (bg == null && transparentBackground == false) 5113 { 5114 bg = (keepTheme) ? this.editor.graph.defaultPageBackgroundColor : '#ffffff'; 5115 } 5116 5117 // Sets or disables alternate text for foreignObjects. Disabling is needed 5118 // because PhantomJS seems to ignore switch statements and paint all text. 5119 var svgRoot = this.editor.graph.getSvg(bg, scale, border, noCrop, 5120 null, ignoreSelection, null, null, (linkTarget == 'blank') ? '_blank' : 5121 ((linkTarget == 'self') ? '_top' : null), null, true, keepTheme, 5122 exportType); 5123 5124 if (addShadow) 5125 { 5126 this.editor.graph.addSvgShadow(svgRoot); 5127 } 5128 5129 var filename = this.getBaseFilename() + ((editable) ? '.drawio' : '') + '.svg'; 5130 5131 saveFn = (saveFn != null) ? saveFn : mxUtils.bind(this, function(svg) 5132 { 5133 if (this.isLocalFileSave() || svg.length <= MAX_REQUEST_SIZE) 5134 { 5135 this.saveData(filename, 'svg', svg, 'image/svg+xml'); 5136 } 5137 else 5138 { 5139 this.handleError({message: mxResources.get('drawingTooLarge')}, mxResources.get('error'), mxUtils.bind(this, function() 5140 { 5141 mxUtils.popup(svg); 5142 })); 5143 } 5144 }); 5145 5146 var doSave = mxUtils.bind(this, function(svgRoot) 5147 { 5148 this.spinner.stop(); 5149 5150 if (editable) 5151 { 5152 svgRoot.setAttribute('content', this.getFileData(true, null, null, null, ignoreSelection, 5153 currentPage, null, null, null, false)); 5154 } 5155 5156 saveFn(Graph.xmlDeclaration + '\n' + ((editable) ? Graph.svgFileComment + '\n' : '') + 5157 Graph.svgDoctype + '\n' + mxUtils.getXml(svgRoot)); 5158 }); 5159 5160 // Adds CSS 5161 if (this.editor.graph.mathEnabled) 5162 { 5163 this.editor.addMathCss(svgRoot); 5164 } 5165 5166 var done = mxUtils.bind(this, function(svgRoot) 5167 { 5168 if (embedImages) 5169 { 5170 // Caches images 5171 if (this.thumbImageCache == null) 5172 { 5173 this.thumbImageCache = new Object(); 5174 } 5175 5176 this.editor.convertImages(svgRoot, doSave, this.thumbImageCache); 5177 } 5178 else 5179 { 5180 doSave(svgRoot); 5181 } 5182 }); 5183 5184 if (embedFonts) 5185 { 5186 this.embedFonts(svgRoot, done); 5187 } 5188 else 5189 { 5190 this.editor.addFontCss(svgRoot); 5191 done(svgRoot); 5192 } 5193 } 5194 catch (e) 5195 { 5196 this.handleError(e); 5197 } 5198 } 5199 }; 5200 5201 /** 5202 * 5203 */ 5204 EditorUi.prototype.addRadiobox = function(div, radioGroupName, label, checked, disabled, disableNewline, visible) 5205 { 5206 return this.addCheckbox(div, label, checked, disabled, disableNewline, visible, true, radioGroupName); 5207 }; 5208 5209 /** 5210 * 5211 */ 5212 EditorUi.prototype.addCheckbox = function(div, label, checked, disabled, disableNewline, visible, asRadio, radioGroupName) 5213 { 5214 visible = (visible != null) ? visible : true; 5215 5216 var cb = document.createElement('input'); 5217 cb.style.marginRight = '8px'; 5218 cb.style.marginTop = '16px'; 5219 cb.setAttribute('type', asRadio? 'radio' : 'checkbox'); 5220 var id = 'geCheckbox-' + Editor.guid(); 5221 cb.id = id; 5222 5223 if (radioGroupName != null) 5224 { 5225 cb.setAttribute('name', radioGroupName); 5226 } 5227 5228 if (checked) 5229 { 5230 cb.setAttribute('checked', 'checked'); 5231 cb.defaultChecked = true; 5232 } 5233 5234 if (disabled) 5235 { 5236 cb.setAttribute('disabled', 'disabled'); 5237 } 5238 5239 if (visible) 5240 { 5241 div.appendChild(cb); 5242 5243 var lbl = document.createElement('label'); 5244 mxUtils.write(lbl, label); 5245 lbl.setAttribute('for', id); 5246 div.appendChild(lbl); 5247 5248 if (!disableNewline) 5249 { 5250 mxUtils.br(div); 5251 } 5252 } 5253 5254 return cb; 5255 }; 5256 5257 /** 5258 * 5259 */ 5260 EditorUi.prototype.addEditButton = function(div, lightbox) 5261 { 5262 var edit = this.addCheckbox(div, mxResources.get('edit') + ':', true, null, true); 5263 edit.style.marginLeft = '24px'; 5264 5265 var file = this.getCurrentFile(); 5266 var editUrl = ''; 5267 5268 if (file != null && file.getMode() != App.MODE_DEVICE && file.getMode() != App.MODE_BROWSER) 5269 { 5270 editUrl = window.location.href; 5271 } 5272 5273 var editSelect = document.createElement('select'); 5274 editSelect.style.width = '120px'; 5275 editSelect.style.marginLeft = '8px'; 5276 editSelect.style.marginRight = '10px'; 5277 editSelect.className = 'geBtn'; 5278 5279 var blankOption = document.createElement('option'); 5280 blankOption.setAttribute('value', 'blank'); 5281 mxUtils.write(blankOption, mxResources.get('makeCopy')); 5282 editSelect.appendChild(blankOption); 5283 5284 var customOption = document.createElement('option'); 5285 customOption.setAttribute('value', 'custom'); 5286 mxUtils.write(customOption, mxResources.get('custom') + '...'); 5287 editSelect.appendChild(customOption); 5288 5289 div.appendChild(editSelect); 5290 5291 mxEvent.addListener(editSelect, 'change', mxUtils.bind(this, function() 5292 { 5293 if (editSelect.value == 'custom') 5294 { 5295 var dlg2 = new FilenameDialog(this, editUrl, mxResources.get('ok'), function(value) 5296 { 5297 if (value != null) 5298 { 5299 editUrl = value; 5300 } 5301 else 5302 { 5303 editSelect.value = 'blank'; 5304 } 5305 }, mxResources.get('url'), null, null, null, null, function() 5306 { 5307 editSelect.value = 'blank'; 5308 }); 5309 this.showDialog(dlg2.container, 300, 80, true, false); 5310 dlg2.init(); 5311 } 5312 })); 5313 5314 mxEvent.addListener(edit, 'change', mxUtils.bind(this, function() 5315 { 5316 if (edit.checked && (lightbox == null || lightbox.checked)) 5317 { 5318 editSelect.removeAttribute('disabled'); 5319 } 5320 else 5321 { 5322 editSelect.setAttribute('disabled', 'disabled'); 5323 } 5324 })); 5325 5326 mxUtils.br(div); 5327 5328 return { 5329 getLink: function() 5330 { 5331 return (edit.checked) ? ((editSelect.value === 'blank') ? '_blank' : editUrl) : null; 5332 }, 5333 getEditInput: function() 5334 { 5335 return edit; 5336 }, 5337 getEditSelect: function() 5338 { 5339 return editSelect; 5340 } 5341 }; 5342 } 5343 5344 /** 5345 * 5346 */ 5347 EditorUi.prototype.addLinkSection = function(div, showFrameOption) 5348 { 5349 mxUtils.write(div, mxResources.get('links') + ':'); 5350 5351 var linkSelect = document.createElement('select'); 5352 linkSelect.style.width = '100px'; 5353 linkSelect.style.marginLeft = '8px'; 5354 linkSelect.style.marginRight = '10px'; 5355 linkSelect.className = 'geBtn'; 5356 5357 var autoOption = document.createElement('option'); 5358 autoOption.setAttribute('value', 'auto'); 5359 mxUtils.write(autoOption, mxResources.get('automatic')); 5360 linkSelect.appendChild(autoOption); 5361 5362 var blankOption = document.createElement('option'); 5363 blankOption.setAttribute('value', 'blank'); 5364 mxUtils.write(blankOption, mxResources.get('openInNewWindow')); 5365 linkSelect.appendChild(blankOption); 5366 5367 var selfOption = document.createElement('option'); 5368 selfOption.setAttribute('value', 'self'); 5369 mxUtils.write(selfOption, mxResources.get('openInThisWindow')); 5370 linkSelect.appendChild(selfOption); 5371 5372 if (showFrameOption) 5373 { 5374 var frameOption = document.createElement('option'); 5375 frameOption.setAttribute('value', 'frame'); 5376 mxUtils.write(frameOption, mxResources.get('openInThisWindow') + 5377 ' (' + mxResources.get('iframe') + ')'); 5378 linkSelect.appendChild(frameOption); 5379 } 5380 5381 div.appendChild(linkSelect); 5382 5383 mxUtils.write(div, mxResources.get('borderColor') + ':'); 5384 var linkColor = '#0000ff'; 5385 var linkButton = null; 5386 5387 function updateLinkColor() 5388 { 5389 linkButton.innerHTML = '<div style="width:100%;height:100%;box-sizing:border-box;' + 5390 ((linkColor != null && linkColor != mxConstants.NONE) ? 5391 'border:1px solid black;background-color:' + linkColor : 5392 'background-position:center center;background-repeat:no-repeat;' + 5393 'background-image:url(\'' + Dialog.prototype.closeImage + '\')') + ';"></div>'; 5394 }; 5395 5396 linkButton = mxUtils.button('', mxUtils.bind(this, function(evt) 5397 { 5398 this.pickColor(linkColor || 'none', function(color) 5399 { 5400 linkColor = color; 5401 updateLinkColor(); 5402 }); 5403 5404 mxEvent.consume(evt); 5405 })); 5406 5407 updateLinkColor(); 5408 linkButton.style.padding = (mxClient.IS_FF) ? '4px 2px 4px 2px' : '4px'; 5409 linkButton.style.marginLeft = '4px'; 5410 linkButton.style.height = '22px'; 5411 linkButton.style.width = '22px'; 5412 linkButton.style.position = 'relative'; 5413 linkButton.style.top = (mxClient.IS_IE || mxClient.IS_IE11 || mxClient.IS_EDGE) ? '6px' : '1px'; 5414 linkButton.className = 'geColorBtn'; 5415 div.appendChild(linkButton); 5416 mxUtils.br(div); 5417 5418 return { 5419 getColor: function() 5420 { 5421 return linkColor; 5422 }, 5423 getTarget: function() 5424 { 5425 return linkSelect.value; 5426 }, 5427 focus: function() 5428 { 5429 linkSelect.focus(); 5430 } 5431 }; 5432 } 5433 5434 /** 5435 * 5436 */ 5437 EditorUi.prototype.createUrlParameters = function(linkTarget, linkColor, allPages, lightbox, editLink, layers, params) 5438 { 5439 params = (params != null) ? params : []; 5440 5441 if (lightbox) 5442 { 5443 if (EditorUi.lightboxHost != 'https://viewer.diagrams.net' || urlParams['dev'] == '1') 5444 { 5445 params.push('lightbox=1'); 5446 } 5447 5448 if (linkTarget != 'auto') 5449 { 5450 params.push('target=' + linkTarget); 5451 } 5452 5453 if (linkColor != null && linkColor != mxConstants.NONE) 5454 { 5455 params.push('highlight=' + ((linkColor.charAt(0) == '#') ? 5456 linkColor.substring(1) : linkColor)); 5457 } 5458 5459 if (editLink != null && editLink.length > 0) 5460 { 5461 params.push('edit=' + encodeURIComponent(editLink)); 5462 } 5463 5464 if (layers) 5465 { 5466 params.push('layers=1'); 5467 } 5468 5469 if (this.editor.graph.foldingEnabled) 5470 { 5471 params.push('nav=1'); 5472 } 5473 } 5474 5475 if (allPages && this.currentPage != null && this.pages != null && 5476 this.currentPage != this.pages[0]) 5477 { 5478 params.push('page-id=' + this.currentPage.getId()); 5479 } 5480 5481 return params; 5482 }; 5483 5484 /** 5485 * 5486 */ 5487 EditorUi.prototype.createLink = function(linkTarget, linkColor, allPages, lightbox, editLink, layers, url, ignoreFile, params, useOpenParameter) 5488 { 5489 params = this.createUrlParameters(linkTarget, linkColor, allPages, lightbox, editLink, layers, params); 5490 var file = this.getCurrentFile(); 5491 var addTitle = true; 5492 var data = ''; 5493 5494 if (url != null) 5495 { 5496 data = '#U' + encodeURIComponent(url); 5497 } 5498 else 5499 { 5500 var file = this.getCurrentFile(); 5501 5502 // Fallback to non-public URL for Drive files 5503 if (!ignoreFile && file != null && file.constructor == window.DriveFile) 5504 { 5505 data = '#' + file.getHash(); 5506 addTitle = false; 5507 } 5508 else 5509 { 5510 data = '#R' + encodeURIComponent((allPages) ? 5511 this.getFileData(true, null, null, null, null, null, null, true, null, false) : 5512 Graph.compress(mxUtils.getXml(this.editor.getGraphXml()))) 5513 } 5514 } 5515 5516 if (addTitle && file != null && file.getTitle() != null && file.getTitle() != this.defaultFilename) 5517 { 5518 params.push('title=' + encodeURIComponent(file.getTitle())); 5519 } 5520 5521 if (useOpenParameter && data.length > 1) 5522 { 5523 params.push('open=' + data.substring(1)); 5524 data = ''; 5525 } 5526 5527 return ((lightbox && urlParams['dev'] != '1') ? EditorUi.lightboxHost : 5528 (((mxClient.IS_CHROMEAPP || EditorUi.isElectronApp || 5529 !(/.*\.draw\.io$/.test(window.location.hostname))) ? 5530 EditorUi.drawHost : 'https://' + window.location.host))) + '/' + 5531 ((params.length > 0) ? '?' + params.join('&') : '') + data; 5532 }; 5533 5534 /** 5535 * 5536 */ 5537 EditorUi.prototype.createHtml = function(publicUrl, zoomEnabled, initialZoom, linkTarget, 5538 linkColor, fit, allPages, layers, tags, lightbox, editLink, fn) 5539 { 5540 var s = this.getBasenames(); 5541 var data = {}; 5542 5543 if (linkColor != '' && linkColor != mxConstants.NONE) 5544 { 5545 data.highlight = linkColor; 5546 } 5547 5548 if (linkTarget !== 'auto') 5549 { 5550 data.target = linkTarget; 5551 } 5552 5553 if (!lightbox) 5554 { 5555 data.lightbox = false; 5556 } 5557 5558 data.nav = this.editor.graph.foldingEnabled; 5559 var zoom = parseInt(initialZoom); 5560 5561 if (!isNaN(zoom) && zoom != 100) 5562 { 5563 data.zoom = zoom / 100; 5564 } 5565 5566 var tb = []; 5567 5568 if (allPages) 5569 { 5570 tb.push('pages'); 5571 data.resize = true; 5572 5573 if (this.pages != null && this.currentPage != null) 5574 { 5575 data.page = mxUtils.indexOf(this.pages, this.currentPage); 5576 } 5577 } 5578 5579 if (zoomEnabled) 5580 { 5581 tb.push('zoom'); 5582 data.resize = true; 5583 } 5584 5585 if (layers) 5586 { 5587 tb.push('layers'); 5588 } 5589 5590 if (tags) 5591 { 5592 tb.push('tags'); 5593 } 5594 5595 if (tb.length > 0) 5596 { 5597 if (lightbox) 5598 { 5599 tb.push('lightbox'); 5600 } 5601 5602 data.toolbar = tb.join(' '); 5603 } 5604 5605 if (editLink != null && editLink.length > 0) 5606 { 5607 data.edit = editLink; 5608 } 5609 5610 if (publicUrl != null) 5611 { 5612 data.url = publicUrl; 5613 } 5614 else 5615 { 5616 data.xml = this.getFileData(true, null, null, null, null, !allPages); 5617 } 5618 5619 var value = '<div class="mxgraph" style="' + 5620 ((fit) ? 'max-width:100%;' : '') + 5621 ((tb != '') ? 'border:1px solid transparent;' : '') + 5622 '" data-mxgraph="' + mxUtils.htmlEntities(JSON.stringify(data)) + '"></div>'; 5623 5624 var fetchParam = (publicUrl != null) ? '&fetch=' + encodeURIComponent(publicUrl) : ''; 5625 var s2 = (fetchParam.length > 0) ? (((urlParams['dev'] == '1') ? 5626 'https://test.draw.io/embed2.js?dev=1' : EditorUi.lightboxHost + '/embed2.js?')) + fetchParam : 5627 (((urlParams['dev'] == '1') ? 'https://test.draw.io/js/viewer-static.min.js' : 5628 window.DRAWIO_VIEWER_URL ? window.DRAWIO_VIEWER_URL : EditorUi.lightboxHost + '/js/viewer-static.min.js')); 5629 var src = '<script type="text/javascript" src="' + s2 + '"></script>'; 5630 5631 fn(value, src); 5632 }; 5633 5634 /** 5635 * 5636 */ 5637 EditorUi.prototype.showHtmlDialog = function(btnLabel, helpLink, publicUrl, fn) 5638 { 5639 var div = document.createElement('div'); 5640 div.style.whiteSpace = 'nowrap'; 5641 var graph = this.editor.graph; 5642 5643 var hd = document.createElement('h3'); 5644 mxUtils.write(hd, mxResources.get('html')); 5645 hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px'; 5646 div.appendChild(hd); 5647 5648 var radioSection = document.createElement('div'); 5649 radioSection.style.cssText = 'border-bottom:1px solid lightGray;padding-bottom:8px;margin-bottom:12px;'; 5650 5651 var publicUrlRadio = document.createElement('input'); 5652 publicUrlRadio.style.cssText = 'margin-right:8px;margin-top:8px;margin-bottom:8px;'; 5653 publicUrlRadio.setAttribute('value', 'url'); 5654 publicUrlRadio.setAttribute('type', 'radio'); 5655 publicUrlRadio.setAttribute('name', 'type-embedhtmldialog'); 5656 5657 var copyRadio = publicUrlRadio.cloneNode(true); 5658 copyRadio.setAttribute('value', 'copy'); 5659 radioSection.appendChild(copyRadio); 5660 5661 var span = document.createElement('span'); 5662 mxUtils.write(span, mxResources.get('includeCopyOfMyDiagram')); 5663 radioSection.appendChild(span); 5664 5665 mxUtils.br(radioSection); 5666 radioSection.appendChild(publicUrlRadio); 5667 5668 var span = document.createElement('span'); 5669 mxUtils.write(span, mxResources.get('publicDiagramUrl')); 5670 radioSection.appendChild(span); 5671 5672 var file = this.getCurrentFile(); 5673 5674 if (publicUrl == null && file != null && file.constructor == window.DriveFile) 5675 { 5676 var testLink = document.createElement('a'); 5677 testLink.style.paddingLeft = '12px'; 5678 testLink.style.color = 'gray'; 5679 testLink.style.cursor = 'pointer'; 5680 mxUtils.write(testLink, mxResources.get('share')); 5681 radioSection.appendChild(testLink); 5682 5683 mxEvent.addListener(testLink, 'click', mxUtils.bind(this, function() 5684 { 5685 this.hideDialog(); 5686 this.drive.showPermissions(file.getId()); 5687 })); 5688 } 5689 5690 copyRadio.setAttribute('checked', 'checked'); 5691 5692 if (publicUrl == null) 5693 { 5694 publicUrlRadio.setAttribute('disabled', 'disabled'); 5695 } 5696 5697 div.appendChild(radioSection); 5698 5699 var linkSection = this.addLinkSection(div); 5700 var zoom = this.addCheckbox(div, mxResources.get('zoom'), true, null, true); 5701 mxUtils.write(div, ':'); 5702 5703 var zoomInput = document.createElement('input'); 5704 zoomInput.setAttribute('type', 'text'); 5705 zoomInput.style.marginRight = '16px'; 5706 zoomInput.style.width = '60px'; 5707 zoomInput.style.marginLeft = '4px'; 5708 zoomInput.style.marginRight = '12px'; 5709 zoomInput.value = '100%'; 5710 5711 div.appendChild(zoomInput); 5712 5713 var fit = this.addCheckbox(div, mxResources.get('fit'), true); 5714 var hasPages = this.pages != null && this.pages.length > 1; 5715 var allPages = allPages = this.addCheckbox(div, mxResources.get('allPages'), hasPages, !hasPages); 5716 var layers = this.addCheckbox(div, mxResources.get('layers'), true); 5717 var tags = this.addCheckbox(div, mxResources.get('tags'), true); 5718 var lightbox = this.addCheckbox(div, mxResources.get('lightbox'), true); 5719 5720 var editSection = this.addEditButton(div, lightbox); 5721 var edit = editSection.getEditInput(); 5722 edit.style.marginBottom = '16px'; 5723 5724 mxEvent.addListener(lightbox, 'change', function() 5725 { 5726 if (lightbox.checked) 5727 { 5728 edit.removeAttribute('disabled'); 5729 } 5730 else 5731 { 5732 edit.setAttribute('disabled', 'disabled'); 5733 } 5734 5735 if (edit.checked && lightbox.checked) 5736 { 5737 editSection.getEditSelect().removeAttribute('disabled'); 5738 } 5739 else 5740 { 5741 editSection.getEditSelect().setAttribute('disabled', 'disabled'); 5742 } 5743 }); 5744 5745 var dlg = new CustomDialog(this, div, mxUtils.bind(this, function() 5746 { 5747 fn((publicUrlRadio.checked) ? publicUrl : null, zoom.checked, zoomInput.value, linkSection.getTarget(), 5748 linkSection.getColor(), fit.checked, allPages.checked, layers.checked, tags.checked, 5749 lightbox.checked, editSection.getLink()); 5750 }), null, btnLabel, helpLink); 5751 this.showDialog(dlg.container, 340, 430, true, true); 5752 copyRadio.focus(); 5753 }; 5754 5755 /** 5756 * 5757 */ 5758 EditorUi.prototype.showPublishLinkDialog = function(title, hideShare, width, height, fn, showFrameOption, helpLink) 5759 { 5760 var div = document.createElement('div'); 5761 div.style.whiteSpace = 'nowrap'; 5762 var graph = this.editor.graph; 5763 5764 var hd = document.createElement('h3'); 5765 mxUtils.write(hd, title || mxResources.get('link')); 5766 hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px'; 5767 div.appendChild(hd); 5768 5769 var file = this.getCurrentFile(); 5770 var dy = 0; 5771 5772 if (file != null && file.constructor == window.DriveFile && !hideShare) 5773 { 5774 dy = 80; 5775 helpLink = (helpLink != null) ? helpLink : 'https://www.diagrams.net/doc/faq/google-drive-publicly-publish-diagram'; 5776 var hintSection = document.createElement('div'); 5777 hintSection.style.cssText = 'border-bottom:1px solid lightGray;padding-bottom:14px;padding-top:6px;margin-bottom:14px;text-align:center;'; 5778 5779 var text = document.createElement('div'); 5780 text.style.whiteSpace = 'normal'; 5781 mxUtils.write(text, mxResources.get('linkAccountRequired')); 5782 hintSection.appendChild(text); 5783 5784 var shareBtn = mxUtils.button(mxResources.get('share'), mxUtils.bind(this, function() 5785 { 5786 this.drive.showPermissions(file.getId()); 5787 })); 5788 shareBtn.style.marginTop = '12px'; 5789 shareBtn.className = 'geBtn'; 5790 hintSection.appendChild(shareBtn); 5791 div.appendChild(hintSection); 5792 5793 var testLink = document.createElement('a'); 5794 testLink.style.paddingLeft = '12px'; 5795 testLink.style.color = 'gray'; 5796 testLink.style.fontSize = '11px'; 5797 testLink.style.cursor = 'pointer'; 5798 mxUtils.write(testLink, mxResources.get('check')); 5799 hintSection.appendChild(testLink); 5800 5801 mxEvent.addListener(testLink, 'click', mxUtils.bind(this, function() 5802 { 5803 if (this.spinner.spin(document.body, mxResources.get('loading'))) 5804 { 5805 this.getPublicUrl(this.getCurrentFile(), mxUtils.bind(this, function(url) 5806 { 5807 this.spinner.stop(); 5808 5809 var dlg = new ErrorDialog(this, null, mxResources.get((url != null) ? 5810 'diagramIsPublic' : 'diagramIsNotPublic'), mxResources.get('ok')); 5811 this.showDialog(dlg.container, 300, 80, true, false); 5812 dlg.init(); 5813 })); 5814 } 5815 })); 5816 } 5817 else 5818 { 5819 helpLink = (helpLink != null) ? helpLink : 'https://www.diagrams.net/doc/faq/publish-diagram-as-link'; 5820 } 5821 5822 var widthInput = null; 5823 var heightInput = null; 5824 5825 if (width != null || height != null) 5826 { 5827 dy += 30; 5828 mxUtils.write(div, mxResources.get('width') + ':'); 5829 5830 widthInput = document.createElement('input'); 5831 widthInput.setAttribute('type', 'text'); 5832 widthInput.style.marginRight = '16px'; 5833 widthInput.style.width = '50px'; 5834 widthInput.style.marginLeft = '6px'; 5835 widthInput.style.marginRight = '16px'; 5836 widthInput.style.marginBottom = '10px'; 5837 widthInput.value = '100%'; 5838 5839 div.appendChild(widthInput); 5840 5841 mxUtils.write(div, mxResources.get('height') + ':'); 5842 5843 heightInput = document.createElement('input'); 5844 heightInput.setAttribute('type', 'text'); 5845 heightInput.style.width = '50px'; 5846 heightInput.style.marginLeft = '6px'; 5847 heightInput.style.marginBottom = '10px'; 5848 heightInput.value = height + 'px'; 5849 5850 div.appendChild(heightInput); 5851 mxUtils.br(div); 5852 } 5853 5854 var linkSection = this.addLinkSection(div, showFrameOption); 5855 var hasPages = this.pages != null && this.pages.length > 1; 5856 var allPages = null; 5857 5858 if (file == null || file.constructor != window.DriveFile || hideShare) 5859 { 5860 allPages = this.addCheckbox(div, mxResources.get('allPages'), hasPages, !hasPages); 5861 } 5862 5863 var lightbox = this.addCheckbox(div, mxResources.get('lightbox'), true, null, null, !showFrameOption); 5864 var editSection = this.addEditButton(div, lightbox); 5865 var edit = editSection.getEditInput(); 5866 5867 // Cannot disable lightbox in iframes 5868 if (showFrameOption) 5869 { 5870 edit.style.marginLeft = lightbox.style.marginLeft; 5871 lightbox.style.display = 'none'; 5872 dy -= 20; 5873 } 5874 5875 var layers = this.addCheckbox(div, mxResources.get('layers'), true); 5876 layers.style.marginLeft = edit.style.marginLeft; 5877 layers.style.marginTop = '8px'; 5878 5879 var tags = this.addCheckbox(div, mxResources.get('tags'), true); 5880 tags.style.marginLeft = edit.style.marginLeft; 5881 tags.style.marginBottom = '16px'; 5882 tags.style.marginTop = '16px'; 5883 5884 mxEvent.addListener(lightbox, 'change', function() 5885 { 5886 if (lightbox.checked) 5887 { 5888 layers.removeAttribute('disabled'); 5889 edit.removeAttribute('disabled'); 5890 } 5891 else 5892 { 5893 layers.setAttribute('disabled', 'disabled'); 5894 edit.setAttribute('disabled', 'disabled'); 5895 } 5896 5897 if (edit.checked && lightbox.checked) 5898 { 5899 editSection.getEditSelect().removeAttribute('disabled'); 5900 } 5901 else 5902 { 5903 editSection.getEditSelect().setAttribute('disabled', 'disabled'); 5904 } 5905 }); 5906 5907 var dlg = new CustomDialog(this, div, mxUtils.bind(this, function() 5908 { 5909 fn(linkSection.getTarget(), linkSection.getColor(), 5910 (allPages == null) ? true : allPages.checked, 5911 lightbox.checked, editSection.getLink(), 5912 layers.checked, (widthInput != null) ? widthInput.value : null, 5913 (heightInput != null) ? heightInput.value : null, tags.checked); 5914 }), null, mxResources.get('create'), helpLink); 5915 this.showDialog(dlg.container, 340, 300 + dy, true, true); 5916 5917 if (widthInput != null) 5918 { 5919 widthInput.focus(); 5920 5921 if (mxClient.IS_GC || mxClient.IS_FF || document.documentMode >= 5) 5922 { 5923 widthInput.select(); 5924 } 5925 else 5926 { 5927 document.execCommand('selectAll', false, null); 5928 } 5929 } 5930 else 5931 { 5932 linkSection.focus(); 5933 } 5934 }; 5935 5936 /** 5937 * 5938 */ 5939 EditorUi.prototype.showRemoteExportDialog = function(btnLabel, helpLink, callback, hideInclude, showZoomBorder) 5940 { 5941 var div = document.createElement('div'); 5942 div.style.whiteSpace = 'nowrap'; 5943 5944 var hd = document.createElement('h3'); 5945 mxUtils.write(hd, mxResources.get('image')); 5946 hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:' + (showZoomBorder? '10' : '4') +'px'; 5947 div.appendChild(hd); 5948 5949 if (showZoomBorder) 5950 { 5951 mxUtils.write(div, mxResources.get('zoom') + ':'); 5952 var zoomInput = document.createElement('input'); 5953 zoomInput.setAttribute('type', 'text'); 5954 zoomInput.style.marginRight = '16px'; 5955 zoomInput.style.width = '60px'; 5956 zoomInput.style.marginLeft = '4px'; 5957 zoomInput.style.marginRight = '12px'; 5958 zoomInput.value = this.lastExportZoom || '100%'; 5959 div.appendChild(zoomInput); 5960 5961 mxUtils.write(div, mxResources.get('borderWidth') + ':'); 5962 var borderInput = document.createElement('input'); 5963 borderInput.setAttribute('type', 'text'); 5964 borderInput.style.marginRight = '16px'; 5965 borderInput.style.width = '60px'; 5966 borderInput.style.marginLeft = '4px'; 5967 borderInput.value = this.lastExportBorder || '0'; 5968 div.appendChild(borderInput); 5969 mxUtils.br(div); 5970 } 5971 5972 var selection = this.addCheckbox(div, mxResources.get('selectionOnly'), false, 5973 this.editor.graph.isSelectionEmpty()); 5974 var include = (hideInclude) ? null : this.addCheckbox(div, mxResources.get('includeCopyOfMyDiagram'), 5975 Editor.defaultIncludeDiagram); 5976 5977 var graph = this.editor.graph; 5978 var transparent = (hideInclude) ? null : this.addCheckbox(div, mxResources.get('transparentBackground'), 5979 graph.background == mxConstants.NONE || graph.background == null); 5980 5981 if (transparent != null) 5982 { 5983 transparent.style.marginBottom = '16px'; 5984 } 5985 5986 var dlg = new CustomDialog(this, div, mxUtils.bind(this, function() 5987 { 5988 var scale = parseInt(zoomInput.value) / 100 || 1; 5989 var border = parseInt(borderInput.value) || 0; 5990 5991 callback(!selection.checked, (include != null) ? include.checked : false, 5992 (transparent != null) ? transparent.checked : false, scale, border); 5993 }), null, btnLabel, helpLink); 5994 this.showDialog(dlg.container, 300, (showZoomBorder? 25 : 0) + (hideInclude ? 125 : 210), true, true); 5995 }; 5996 5997 /** 5998 * 5999 */ 6000 EditorUi.prototype.showExportDialog = function(title, embedOption, btnLabel, helpLink, callback, 6001 cropOption, defaultInclude, format, exportOption) 6002 { 6003 defaultInclude = (defaultInclude != null) ? defaultInclude : Editor.defaultIncludeDiagram; 6004 6005 var div = document.createElement('div'); 6006 div.style.whiteSpace = 'nowrap'; 6007 var graph = this.editor.graph; 6008 var height = (format == 'jpeg') ? 220 : 300; 6009 6010 var hd = document.createElement('h3'); 6011 mxUtils.write(hd, title); 6012 hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:10px'; 6013 div.appendChild(hd); 6014 6015 mxUtils.write(div, mxResources.get('zoom') + ':'); 6016 var zoomInput = document.createElement('input'); 6017 zoomInput.setAttribute('type', 'text'); 6018 zoomInput.style.marginRight = '16px'; 6019 zoomInput.style.width = '60px'; 6020 zoomInput.style.marginLeft = '4px'; 6021 zoomInput.style.marginRight = '12px'; 6022 zoomInput.value = this.lastExportZoom || '100%'; 6023 div.appendChild(zoomInput); 6024 6025 mxUtils.write(div, mxResources.get('borderWidth') + ':'); 6026 var borderInput = document.createElement('input'); 6027 borderInput.setAttribute('type', 'text'); 6028 borderInput.style.marginRight = '16px'; 6029 borderInput.style.width = '60px'; 6030 borderInput.style.marginLeft = '4px'; 6031 borderInput.value = this.lastExportBorder || '0'; 6032 div.appendChild(borderInput); 6033 mxUtils.br(div); 6034 6035 var selection = this.addCheckbox(div, mxResources.get('selectionOnly'), 6036 false, graph.isSelectionEmpty()); 6037 6038 var cb6 = document.createElement('input'); 6039 cb6.style.marginTop = '16px'; 6040 cb6.style.marginRight = '8px'; 6041 cb6.style.marginLeft = '24px'; 6042 cb6.setAttribute('disabled', 'disabled'); 6043 cb6.setAttribute('type', 'checkbox'); 6044 6045 var exportSelect = document.createElement('select'); 6046 exportSelect.style.marginTop = '16px'; 6047 exportSelect.style.marginLeft = '8px'; 6048 6049 var sizes = ['selectionOnly', 'diagram', 'page']; 6050 6051 for (var i = 0; i < sizes.length; i++) 6052 { 6053 if (!graph.isSelectionEmpty() || sizes[i] != 'selectionOnly') 6054 { 6055 var opt = document.createElement('option'); 6056 mxUtils.write(opt, mxResources.get(sizes[i])); 6057 opt.setAttribute('value', sizes[i]); 6058 exportSelect.appendChild(opt); 6059 } 6060 } 6061 6062 if (exportOption) 6063 { 6064 mxUtils.write(div, mxResources.get('size') + ':'); 6065 div.appendChild(exportSelect); 6066 mxUtils.br(div); 6067 height += 26; 6068 6069 mxEvent.addListener(exportSelect, 'change', function() 6070 { 6071 if (exportSelect.value == 'selectionOnly') 6072 { 6073 selection.checked = true; 6074 } 6075 }); 6076 } 6077 else if (cropOption) 6078 { 6079 div.appendChild(cb6); 6080 mxUtils.write(div, mxResources.get('crop')); 6081 mxUtils.br(div); 6082 6083 height += 30; 6084 6085 mxEvent.addListener(selection, 'change', function() 6086 { 6087 if (selection.checked) 6088 { 6089 cb6.removeAttribute('disabled'); 6090 } 6091 else 6092 { 6093 cb6.setAttribute('disabled', 'disabled'); 6094 } 6095 }); 6096 } 6097 6098 if (graph.isSelectionEmpty()) 6099 { 6100 if (exportOption) 6101 { 6102 selection.style.display = 'none'; 6103 selection.nextSibling.style.display = 'none'; 6104 selection.nextSibling.nextSibling.style.display = 'none'; 6105 height -= 30; 6106 } 6107 } 6108 else 6109 { 6110 exportSelect.value = 'diagram'; 6111 cb6.setAttribute('checked', 'checked'); 6112 cb6.defaultChecked = true; 6113 6114 mxEvent.addListener(selection, 'change', function() 6115 { 6116 if (selection.checked) 6117 { 6118 exportSelect.value = 'selectionOnly'; 6119 } 6120 else 6121 { 6122 exportSelect.value = 'diagram'; 6123 } 6124 }); 6125 } 6126 6127 var defaultTransparent = false; /*graph.background == mxConstants.NONE || graph.background == null*/; 6128 var transparent = this.addCheckbox(div, mxResources.get('transparentBackground'), 6129 defaultTransparent, null, null, format != 'jpeg'); 6130 var keepTheme = null; 6131 6132 if (Editor.isDarkMode()) 6133 { 6134 keepTheme = this.addCheckbox(div, mxResources.get('dark'), true); 6135 height += 26; 6136 } 6137 6138 var shadow = this.addCheckbox(div, mxResources.get('shadow'), graph.shadowVisible); 6139 6140 var cb5 = document.createElement('input'); 6141 cb5.style.marginTop = '16px'; 6142 cb5.style.marginRight = '8px'; 6143 cb5.setAttribute('type', 'checkbox'); 6144 6145 var cb7 = document.createElement('input'); 6146 cb7.style.marginTop = '16px'; 6147 cb7.style.marginRight = '8px'; 6148 cb7.setAttribute('type', 'checkbox'); 6149 6150 if (this.isOffline() || !this.canvasSupported) 6151 { 6152 cb5.setAttribute('disabled', 'disabled'); 6153 } 6154 6155 if (embedOption) 6156 { 6157 div.appendChild(cb5); 6158 mxUtils.write(div, mxResources.get('embedImages')); 6159 mxUtils.br(div); 6160 6161 div.appendChild(cb7); 6162 mxUtils.write(div, mxResources.get('embedFonts')); 6163 mxUtils.br(div); 6164 6165 height += 60; 6166 } 6167 6168 var grid = null; 6169 6170 if (format == 'png' || format == 'jpeg') 6171 { 6172 grid = this.addCheckbox(div, mxResources.get('grid'), false, this.isOffline() || !this.canvasSupported, false, true); 6173 height += 30; 6174 } 6175 6176 var include = this.addCheckbox(div, mxResources.get('includeCopyOfMyDiagram'), defaultInclude, null, null, format != 'jpeg'); 6177 include.style.marginBottom = '16px'; 6178 6179 var linkSelect = document.createElement('select'); 6180 linkSelect.style.maxWidth = '260px'; 6181 linkSelect.style.marginLeft = '8px'; 6182 linkSelect.style.marginRight = '10px'; 6183 linkSelect.className = 'geBtn'; 6184 6185 var autoOption = document.createElement('option'); 6186 autoOption.setAttribute('value', 'auto'); 6187 mxUtils.write(autoOption, mxResources.get('automatic')); 6188 linkSelect.appendChild(autoOption); 6189 6190 var blankOption = document.createElement('option'); 6191 blankOption.setAttribute('value', 'blank'); 6192 mxUtils.write(blankOption, mxResources.get('openInNewWindow')); 6193 linkSelect.appendChild(blankOption); 6194 6195 var selfOption = document.createElement('option'); 6196 selfOption.setAttribute('value', 'self'); 6197 mxUtils.write(selfOption, mxResources.get('openInThisWindow')); 6198 linkSelect.appendChild(selfOption); 6199 6200 if (format == 'svg') 6201 { 6202 mxUtils.write(div, mxResources.get('links') + ':'); 6203 div.appendChild(linkSelect); 6204 mxUtils.br(div); 6205 mxUtils.br(div); 6206 height += 50; 6207 } 6208 6209 var dlg = new CustomDialog(this, div, mxUtils.bind(this, function() 6210 { 6211 this.lastExportBorder = borderInput.value; 6212 this.lastExportZoom = zoomInput.value; 6213 6214 callback(zoomInput.value, transparent.checked, !selection.checked, shadow.checked, 6215 include.checked, cb5.checked, borderInput.value, cb6.checked, false, 6216 linkSelect.value, (grid != null) ? grid.checked : null, (keepTheme != null) ? 6217 keepTheme.checked : null, exportSelect.value, cb7.checked); 6218 }), null, btnLabel, helpLink); 6219 this.showDialog(dlg.container, 340, height, true, true, null, null, null, null, true); 6220 zoomInput.focus(); 6221 6222 if (mxClient.IS_GC || mxClient.IS_FF || document.documentMode >= 5) 6223 { 6224 zoomInput.select(); 6225 } 6226 else 6227 { 6228 document.execCommand('selectAll', false, null); 6229 } 6230 }; 6231 6232 /** 6233 * 6234 */ 6235 EditorUi.prototype.showEmbedImageDialog = function(fn, title, imageLabel, shadowEnabled, helpLink) 6236 { 6237 var div = document.createElement('div'); 6238 div.style.whiteSpace = 'nowrap'; 6239 var graph = this.editor.graph; 6240 6241 if (title != null) 6242 { 6243 var hd = document.createElement('h3'); 6244 mxUtils.write(hd, title); 6245 hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:4px'; 6246 div.appendChild(hd); 6247 } 6248 6249 var fit = this.addCheckbox(div, mxResources.get('fit'), true); 6250 var shadow = this.addCheckbox(div, mxResources.get('shadow'), 6251 graph.shadowVisible && shadowEnabled, !shadowEnabled); 6252 var image = this.addCheckbox(div, imageLabel); 6253 var lightbox = this.addCheckbox(div, mxResources.get('lightbox'), true); 6254 var editSection = this.addEditButton(div, lightbox); 6255 var edit = editSection.getEditInput(); 6256 6257 var hasLayers = graph.model.getChildCount(graph.model.getRoot()) > 1; 6258 var layers = this.addCheckbox(div, mxResources.get('layers'), hasLayers, !hasLayers); 6259 layers.style.marginLeft = edit.style.marginLeft; 6260 layers.style.marginBottom = '12px'; 6261 layers.style.marginTop = '8px'; 6262 6263 mxEvent.addListener(lightbox, 'change', function() 6264 { 6265 if (lightbox.checked) 6266 { 6267 if (hasLayers) 6268 { 6269 layers.removeAttribute('disabled'); 6270 } 6271 6272 edit.removeAttribute('disabled'); 6273 } 6274 else 6275 { 6276 layers.setAttribute('disabled', 'disabled'); 6277 edit.setAttribute('disabled', 'disabled'); 6278 } 6279 6280 if (edit.checked && lightbox.checked) 6281 { 6282 editSection.getEditSelect().removeAttribute('disabled'); 6283 } 6284 else 6285 { 6286 editSection.getEditSelect().setAttribute('disabled', 'disabled'); 6287 } 6288 }); 6289 6290 var dlg = new CustomDialog(this, div, mxUtils.bind(this, function() 6291 { 6292 fn(fit.checked, shadow.checked, image.checked, lightbox.checked, 6293 editSection.getLink(), layers.checked); 6294 }), null, mxResources.get('embed'), helpLink); 6295 this.showDialog(dlg.container, 280, 300, true, true); 6296 }; 6297 6298 /** 6299 * 6300 */ 6301 EditorUi.prototype.createEmbedImage = function(fit, shadow, retina, lightbox, edit, layers, fn, err) 6302 { 6303 var bounds = this.editor.graph.getGraphBounds(); 6304 var page = this.getSelectedPageIndex(); 6305 6306 function doUpdate(dataUri) 6307 { 6308 var onclick = ' '; 6309 var css = ''; 6310 6311 // Adds double click handling 6312 if (lightbox) 6313 { 6314 // KNOWN: Message passing does not seem to work in IE11 6315 onclick = " onclick=\"(function(img){if(img.wnd!=null&&!img.wnd.closed){img.wnd.focus();}else{var r=function(evt){if(evt.data=='ready'&&evt.source==img.wnd){img.wnd.postMessage(decodeURIComponent(" + 6316 "img.getAttribute('src')),'*');window.removeEventListener('message',r);}};window.addEventListener('message',r);img.wnd=window.open('" + EditorUi.lightboxHost + "/?client=1" + 6317 ((page != null) ? ("&page=" + page) : "") + 6318 ((edit) ? "&edit=_blank" : "") + 6319 ((layers) ? '&layers=1' : '') + "');}})(this);\""; 6320 css += 'cursor:pointer;'; 6321 } 6322 6323 if (fit) 6324 { 6325 css += 'max-width:100%;'; 6326 } 6327 6328 var atts = ''; 6329 6330 if (retina) 6331 { 6332 atts = ' width="' + Math.round(bounds.width) + '" height="' + Math.round(bounds.height) + '"'; 6333 } 6334 6335 fn('<img src="' + dataUri + '"' + atts + ((css != '') ? ' style="' + css + '"' : '') + onclick + '/>'); 6336 }; 6337 6338 if (this.isExportToCanvas()) 6339 { 6340 this.editor.exportToCanvas(mxUtils.bind(this, function(canvas) 6341 { 6342 var xml = (lightbox) ? this.getFileData(true) : null; 6343 var data = this.createImageDataUri(canvas, xml, 'png'); 6344 doUpdate(data); 6345 }), null, null, null, mxUtils.bind(this, function(e) 6346 { 6347 err({message: mxResources.get('unknownError')}); 6348 }), null, true, (retina) ? 2 : 1, null, shadow, null, null, Editor.defaultBorder); 6349 } 6350 else 6351 { 6352 var data = this.getFileData(true); 6353 6354 if (bounds.width * bounds.height <= MAX_AREA && data.length <= MAX_REQUEST_SIZE) 6355 { 6356 var size = ''; 6357 6358 if (retina) 6359 { 6360 size = '&w=' + Math.round(2 * bounds.width) + 6361 '&h=' + Math.round(2 * bounds.height); 6362 } 6363 6364 var embed = (lightbox) ? '1' : '0'; 6365 var req = new mxXmlRequest(EXPORT_URL, 'format=png' + 6366 '&base64=1&embedXml=' + embed + size + '&xml=' + 6367 encodeURIComponent(data)); 6368 6369 // LATER: Updates on each change, add a delay 6370 req.send(mxUtils.bind(this, function() 6371 { 6372 if (req.getStatus() >= 200 && req.getStatus() <= 299) 6373 { 6374 // Fixes possible "incorrect function" for select() on 6375 // DOM node which is no longer in document with IE11 6376 doUpdate('data:image/png;base64,' + req.getText()); 6377 } 6378 else 6379 { 6380 err({message: mxResources.get('unknownError')}); 6381 } 6382 })); 6383 } 6384 else 6385 { 6386 err({message: mxResources.get('drawingTooLarge')}); 6387 } 6388 } 6389 }; 6390 6391 /** 6392 * 6393 */ 6394 EditorUi.prototype.createEmbedSvg = function(fit, shadow, image, lightbox, edit, layers, fn) 6395 { 6396 var svgRoot = this.editor.graph.getSvg(null, null, null, null, null, 6397 null, null, null, null, null, !image); 6398 6399 // Keeps hashtag links on same page 6400 var links = svgRoot.getElementsByTagName('a'); 6401 6402 if (links != null) 6403 { 6404 for (var i = 0; i < links.length; i++) 6405 { 6406 var href = links[i].getAttribute('href'); 6407 6408 if (href != null && href.charAt(0) == '#' && 6409 links[i].getAttribute('target') == '_blank') 6410 { 6411 links[i].removeAttribute('target'); 6412 } 6413 } 6414 } 6415 6416 if (lightbox) 6417 { 6418 svgRoot.setAttribute('content', this.getFileData(true)); 6419 } 6420 6421 // Adds shadow filter 6422 if (shadow) 6423 { 6424 this.editor.graph.addSvgShadow(svgRoot); 6425 } 6426 6427 // SVG inside image tag 6428 if (image) 6429 { 6430 var onclick = ' '; 6431 var css = ''; 6432 6433 // Adds double click handling 6434 if (lightbox) 6435 { 6436 // KNOWN: Message passing does not seem to work in IE11 6437 onclick = "onclick=\"(function(img){if(img.wnd!=null&&!img.wnd.closed){img.wnd.focus();}else{var r=function(evt){if(evt.data=='ready'&&evt.source==img.wnd){img.wnd.postMessage(decodeURIComponent(" + 6438 "img.getAttribute('src')),'*');window.removeEventListener('message',r);}};window.addEventListener('message',r);img.wnd=window.open('" + EditorUi.lightboxHost + "/?client=1" + 6439 ((edit) ? "&edit=_blank" : "") + ((layers) ? '&layers=1' : '') + "');}})(this);\""; 6440 css += 'cursor:pointer;'; 6441 } 6442 6443 if (fit) 6444 { 6445 css += 'max-width:100%;'; 6446 } 6447 6448 // Images inside IMG don't seem to work so embed them all 6449 this.editor.convertImages(svgRoot, mxUtils.bind(this, function(svgRoot) 6450 { 6451 fn('<img src="' + Editor.createSvgDataUri(mxUtils.getXml(svgRoot)) + '"' + 6452 ((css != '') ? ' style="' + css + '"' : '') + onclick + '/>'); 6453 })); 6454 } 6455 else 6456 { 6457 var css = ''; 6458 6459 // Adds double click handling 6460 if (lightbox) 6461 { 6462 var page = this.getSelectedPageIndex(); 6463 6464 // KNOWN: Message passing does not seem to work in IE11 6465 var js = "(function(svg){var src=window.event.target||window.event.srcElement;" + 6466 // Ignores link events 6467 "while (src!=null&&src.nodeName.toLowerCase()!='a'){src=src.parentNode;}if(src==null)" + 6468 // Focus existing lightbox 6469 "{if(svg.wnd!=null&&!svg.wnd.closed){svg.wnd.focus();}else{var r=function(evt){" + 6470 // Message handling 6471 "if(evt.data=='ready'&&evt.source==svg.wnd){svg.wnd.postMessage(decodeURIComponent(" + 6472 "svg.getAttribute('content')),'*');window.removeEventListener('message',r);}};" + 6473 "window.addEventListener('message',r);" + 6474 // Opens lightbox window 6475 "svg.wnd=window.open('" + EditorUi.lightboxHost + "/?client=1" + 6476 ((page != null) ? ("&page=" + page) : "") + 6477 ((edit) ? "&edit=_blank" : "") + ((layers) ? '&layers=1' : '') + "');}}})(this);"; 6478 svgRoot.setAttribute('onclick', js); 6479 css += 'cursor:pointer;'; 6480 } 6481 6482 // Adds responsive size 6483 if (fit) 6484 { 6485 var w = parseInt(svgRoot.getAttribute('width')); 6486 var h = parseInt(svgRoot.getAttribute('height')); 6487 svgRoot.setAttribute('viewBox', '-0.5 -0.5 ' + w + ' ' + h); 6488 css += 'max-width:100%;max-height:' + h + 'px;'; 6489 svgRoot.removeAttribute('height'); 6490 } 6491 6492 if (css != '') 6493 { 6494 svgRoot.setAttribute('style', css); 6495 } 6496 6497 // Adds CSS 6498 this.editor.addFontCss(svgRoot); 6499 6500 if (this.editor.graph.mathEnabled) 6501 { 6502 this.editor.addMathCss(svgRoot); 6503 } 6504 6505 fn(mxUtils.getXml(svgRoot)); 6506 } 6507 }; 6508 6509 /** 6510 * Translates this point by the given vector. 6511 * 6512 * @param {number} dx X-coordinate of the translation. 6513 * @param {number} dy Y-coordinate of the translation. 6514 */ 6515 EditorUi.prototype.timeSince = function(date) 6516 { 6517 var seconds = Math.floor((new Date() - date) / 1000); 6518 var interval = Math.floor(seconds / 31536000); 6519 6520 if (interval > 1) 6521 { 6522 return interval + ' ' + mxResources.get('years'); 6523 } 6524 6525 interval = Math.floor(seconds / 2592000); 6526 6527 if (interval > 1) 6528 { 6529 return interval + ' ' + mxResources.get('months'); 6530 } 6531 6532 interval = Math.floor(seconds / 86400); 6533 6534 if (interval > 1) 6535 { 6536 return interval + ' ' + mxResources.get('days'); 6537 } 6538 6539 interval = Math.floor(seconds / 3600); 6540 6541 if (interval > 1) 6542 { 6543 return interval + ' ' + mxResources.get('hours'); 6544 } 6545 6546 interval = Math.floor(seconds / 60); 6547 6548 if (interval > 1) 6549 { 6550 return interval + ' ' + mxResources.get('minutes'); 6551 } 6552 6553 if (interval == 1) 6554 { 6555 return interval + ' ' + mxResources.get('minute'); 6556 } 6557 6558 return null; 6559 }; 6560 6561 /** 6562 * 6563 */ 6564 EditorUi.prototype.decodeNodeIntoGraph = function(node, graph) 6565 { 6566 if (node != null) 6567 { 6568 var diagramNode = null; 6569 6570 if (node.nodeName == 'diagram') 6571 { 6572 diagramNode = node; 6573 } 6574 else if (node.nodeName == 'mxfile') 6575 { 6576 var diagrams = node.getElementsByTagName('diagram'); 6577 6578 if (diagrams.length > 0) 6579 { 6580 diagramNode = diagrams[0]; 6581 var graphGetGlobalVariable = graph.getGlobalVariable; 6582 6583 graph.getGlobalVariable = function(name) 6584 { 6585 if (name == 'page') 6586 { 6587 return diagramNode.getAttribute('name') || mxResources.get('pageWithNumber', [1]) 6588 } 6589 else if (name == 'pagenumber') 6590 { 6591 return 1; 6592 } 6593 6594 return graphGetGlobalVariable.apply(this, arguments); 6595 }; 6596 } 6597 } 6598 6599 if (diagramNode != null) 6600 { 6601 node = Editor.parseDiagramNode(diagramNode); 6602 } 6603 } 6604 6605 // Hack to decode XML into temp graph via editor 6606 var prev = this.editor.graph; 6607 6608 try 6609 { 6610 this.editor.graph = graph; 6611 this.editor.setGraphXml(node); 6612 } 6613 catch (e) 6614 { 6615 // ignore 6616 } 6617 finally 6618 { 6619 this.editor.graph = prev; 6620 } 6621 6622 return node; 6623 }; 6624 6625 /** 6626 * 6627 */ 6628 EditorUi.prototype.getPngFileProperties = function(node) 6629 { 6630 var scale = 1; 6631 var border = 0; 6632 6633 if (node != null) 6634 { 6635 if (node.hasAttribute('scale')) 6636 { 6637 var temp = parseFloat(node.getAttribute('scale')); 6638 6639 if (!isNaN(temp) && temp > 0) 6640 { 6641 scale = temp; 6642 } 6643 } 6644 6645 if (node.hasAttribute('border')) 6646 { 6647 var temp = parseInt(node.getAttribute('border')); 6648 6649 if (!isNaN(temp) && temp > 0) 6650 { 6651 border = temp; 6652 } 6653 } 6654 } 6655 6656 return {scale: scale, border: border}; 6657 }; 6658 6659 /** 6660 * 6661 */ 6662 EditorUi.prototype.getEmbeddedPng = function(success, error, optionalData, scale, border) 6663 { 6664 try 6665 { 6666 var graph = this.editor.graph; 6667 var darkTheme = graph.themes != null && graph.defaultThemeName == 'darkTheme'; 6668 var diagramData = null; 6669 6670 // Exports PNG for given optional data 6671 if (optionalData != null && optionalData.length > 0) 6672 { 6673 graph = this.createTemporaryGraph((darkTheme) ? 6674 graph.getDefaultStylesheet() : graph.getStylesheet()); 6675 document.body.appendChild(graph.container); 6676 this.decodeNodeIntoGraph(this.editor.extractGraphModel( 6677 mxUtils.parseXml(optionalData).documentElement, true), graph); 6678 diagramData = optionalData; 6679 } 6680 // Exports PNG for first page while other page is showing 6681 else if (darkTheme || (this.pages != null && this.currentPage != this.pages[0])) 6682 { 6683 graph = this.createTemporaryGraph((darkTheme) ? 6684 graph.getDefaultStylesheet() : graph.getStylesheet()); 6685 var graphGetGlobalVariable = graph.getGlobalVariable; 6686 graph.setBackgroundImage = this.editor.graph.setBackgroundImage; 6687 var page = this.pages[0]; 6688 6689 if (this.currentPage == page) 6690 { 6691 graph.setBackgroundImage(this.editor.graph.backgroundImage); 6692 } 6693 else if (page.viewState != null && page.viewState != null) 6694 { 6695 graph.setBackgroundImage(page.viewState.backgroundImage); 6696 } 6697 6698 graph.getGlobalVariable = function(name) 6699 { 6700 if (name == 'page') 6701 { 6702 return page.getName(); 6703 } 6704 else if (name == 'pagenumber') 6705 { 6706 return 1; 6707 } 6708 6709 return graphGetGlobalVariable.apply(this, arguments); 6710 }; 6711 6712 document.body.appendChild(graph.container); 6713 graph.model.setRoot(page.root); 6714 } 6715 6716 this.editor.exportToCanvas(mxUtils.bind(this, function(canvas) 6717 { 6718 try 6719 { 6720 if (diagramData == null) 6721 { 6722 diagramData = this.getFileData(true, null, null, null, null, 6723 null, null, null, null, false); 6724 } 6725 6726 var data = canvas.toDataURL('image/png'); 6727 data = Editor.writeGraphModelToPng(data, 6728 'tEXt', 'mxfile', encodeURIComponent(diagramData)); 6729 success(data.substring(data.lastIndexOf(',') + 1)); 6730 6731 // Removes temporary graph from DOM 6732 if (graph != this.editor.graph) 6733 { 6734 graph.container.parentNode.removeChild(graph.container); 6735 } 6736 } 6737 catch (e) 6738 { 6739 if (error != null) 6740 { 6741 error(e); 6742 } 6743 } 6744 }), null, null, null, mxUtils.bind(this, function(e) 6745 { 6746 if (error != null) 6747 { 6748 error(e); 6749 } 6750 }), null, null, scale, null, graph.shadowVisible, null, 6751 graph, border, null, null, null, 'diagram', null); 6752 } 6753 catch (e) 6754 { 6755 if (error != null) 6756 { 6757 error(e); 6758 } 6759 } 6760 } 6761 6762 /** 6763 * Returns the SVG of the diagram with embedded XML. If a callback function is 6764 * used, the images are converted to data URIs. 6765 */ 6766 EditorUi.prototype.getEmbeddedSvg = function(xml, graph, url, noHeader, callback, ignoreSelection, 6767 redirect, embedImages, background, scale, border, shadow, keepTheme) 6768 { 6769 embedImages = (embedImages != null) ? embedImages : true; 6770 border = (border != null) ? border : 0; 6771 6772 var bg = (background != null) ? background : graph.background; 6773 6774 if (bg == mxConstants.NONE) 6775 { 6776 bg = null; 6777 } 6778 6779 // Sets or disables alternate text for foreignObjects. Disabling is needed 6780 // because PhantomJS seems to ignore switch statements and paint all text. 6781 var svgRoot = graph.getSvg(bg, scale, border, null, null, ignoreSelection, null, 6782 null, null, graph.shadowVisible || shadow, null, keepTheme, 'diagram'); 6783 6784 if (graph.shadowVisible || shadow) 6785 { 6786 graph.addSvgShadow(svgRoot, null, null, border == 0); 6787 } 6788 6789 if (xml != null) 6790 { 6791 svgRoot.setAttribute('content', xml); 6792 } 6793 6794 if (url != null) 6795 { 6796 svgRoot.setAttribute('resource', url); 6797 } 6798 6799 // LATER: Click on SVG content to start editing 6800// if (redirect != null) 6801// { 6802// // TODO: Ignore anchor tag source for click event 6803// svgRoot.setAttribute('style', 'cursor:pointer;'); 6804// svgRoot.setAttribute('onclick', 'window.location.href=\'' + redirect + '\';'); 6805// } 6806 6807 var done = mxUtils.bind(this, function(svgRoot) 6808 { 6809 var result = ((!noHeader) ? Graph.xmlDeclaration + '\n' + Graph.svgFileComment + 6810 '\n' + Graph.svgDoctype + '\n' : '') + mxUtils.getXml(svgRoot); 6811 6812 if (callback != null) 6813 { 6814 callback(result); 6815 } 6816 6817 return result; 6818 }); 6819 6820 // Adds CSS 6821 if (graph.mathEnabled) 6822 { 6823 this.editor.addMathCss(svgRoot); 6824 } 6825 6826 if (callback != null) 6827 { 6828 this.embedFonts(svgRoot, mxUtils.bind(this, function(svgRoot) 6829 { 6830 if (embedImages) 6831 { 6832 this.editor.convertImages(svgRoot, mxUtils.bind(this, function(svgRoot) 6833 { 6834 done(svgRoot); 6835 })); 6836 } 6837 else 6838 { 6839 done(svgRoot); 6840 } 6841 })); 6842 } 6843 else 6844 { 6845 return done(svgRoot); 6846 } 6847 }; 6848 6849 /** 6850 * Embeds font CSS as data URIs into the given svgRoot. 6851 */ 6852 EditorUi.prototype.embedFonts = function(svgRoot, callback) 6853 { 6854 this.editor.loadFonts(mxUtils.bind(this, function() 6855 { 6856 try 6857 { 6858 if (this.editor.resolvedFontCss != null) 6859 { 6860 this.editor.addFontCss(svgRoot, this.editor.resolvedFontCss); 6861 } 6862 6863 this.editor.embedExtFonts(mxUtils.bind(this, function(extFontsEmbeddedCss) 6864 { 6865 try 6866 { 6867 if (extFontsEmbeddedCss != null) 6868 { 6869 this.editor.addFontCss(svgRoot, extFontsEmbeddedCss); 6870 } 6871 6872 callback(svgRoot); 6873 } 6874 catch (e) 6875 { 6876 callback(svgRoot); 6877 } 6878 })); 6879 } 6880 catch (e) 6881 { 6882 callback(svgRoot); 6883 } 6884 })); 6885 }; 6886 6887 /** 6888 * 6889 */ 6890 EditorUi.prototype.exportImage = function(scale, transparentBackground, ignoreSelection, addShadow, 6891 editable, border, noCrop, currentPage, format, grid, dpi, keepTheme, exportType) 6892 { 6893 format = (format != null) ? format : 'png'; 6894 6895 if (this.spinner.spin(document.body, mxResources.get('exporting'))) 6896 { 6897 var selectionEmpty = this.editor.graph.isSelectionEmpty(); 6898 ignoreSelection = (ignoreSelection != null) ? ignoreSelection : selectionEmpty; 6899 6900 // Caches images 6901 if (this.thumbImageCache == null) 6902 { 6903 this.thumbImageCache = new Object(); 6904 } 6905 6906 try 6907 { 6908 this.editor.exportToCanvas(mxUtils.bind(this, function(canvas) 6909 { 6910 this.spinner.stop(); 6911 6912 try 6913 { 6914 this.saveCanvas(canvas, (editable) ? this.getFileData(true, null, 6915 null, null, ignoreSelection, currentPage) : null, 6916 format, (this.pages == null || this.pages.length == 0), dpi); 6917 } 6918 catch (e) 6919 { 6920 this.handleError(e); 6921 } 6922 }), null, this.thumbImageCache, null, mxUtils.bind(this, function(e) 6923 { 6924 this.spinner.stop(); 6925 this.handleError(e); 6926 }), null, ignoreSelection, scale || 1, transparentBackground, addShadow, 6927 null, null, border, noCrop, grid, keepTheme, exportType); 6928 } 6929 catch (e) 6930 { 6931 this.spinner.stop(); 6932 this.handleError(e); 6933 } 6934 } 6935 }; 6936 6937 /** 6938 /** 6939 * Returns true if the given URL is known to have CORS headers. 6940 */ 6941 EditorUi.prototype.isCorsEnabledForUrl = function(url) 6942 { 6943 return this.editor.isCorsEnabledForUrl(url); 6944 }; 6945 6946 /** 6947 * Handling drag and drop and import. 6948 */ 6949 6950 /** 6951 * Imports the given XML into the existing diagram. 6952 */ 6953 EditorUi.prototype.importXml = function(xml, dx, dy, crop, noErrorHandling, addNewPage, applyDefaultStyles) 6954 { 6955 dx = (dx != null) ? dx : 0; 6956 dy = (dy != null) ? dy : 0; 6957 var cells = [] 6958 6959 try 6960 { 6961 var graph = this.editor.graph; 6962 6963 if (xml != null && xml.length > 0) 6964 { 6965 // Adds pages 6966 graph.model.beginUpdate(); 6967 try 6968 { 6969 var doc = mxUtils.parseXml(xml); 6970 var mapping = {}; 6971 6972 // Checks for mxfile with multiple pages 6973 var node = this.editor.extractGraphModel(doc.documentElement, this.pages != null); 6974 6975 if (node != null && node.nodeName == 'mxfile' && this.pages != null) 6976 { 6977 var diagrams = node.getElementsByTagName('diagram'); 6978 6979 if (diagrams.length == 1 && !addNewPage) 6980 { 6981 node = Editor.parseDiagramNode(diagrams[0]); 6982 6983 if (this.currentPage != null) 6984 { 6985 mapping[diagrams[0].getAttribute('id')] = this.currentPage.getId(); 6986 6987 // Renames page if diagram has one blank page with default name 6988 if (this.pages != null && this.pages.length == 1 && 6989 this.isDiagramEmpty() && this.currentPage.getName() == 6990 mxResources.get('pageWithNumber', [1])) 6991 { 6992 var name = diagrams[0].getAttribute('name'); 6993 6994 if (name != null && name != '') 6995 { 6996 this.editor.graph.model.execute(new RenamePage( 6997 this, this.currentPage, name)); 6998 } 6999 } 7000 } 7001 } 7002 else if (diagrams.length > 0) 7003 { 7004 var pages = []; 7005 var i0 = 0; 7006 7007 // Adds first page to current page if current page is only page and empty 7008 if (this.pages != null && this.pages.length == 1 && this.isDiagramEmpty()) 7009 { 7010 mapping[diagrams[0].getAttribute('id')] = this.pages[0].getId(); 7011 node = Editor.parseDiagramNode(diagrams[0]); 7012 crop = false; 7013 i0 = 1; 7014 } 7015 7016 for (var i = i0; i < diagrams.length; i++) 7017 { 7018 // Imported pages must obtain a new ID and 7019 // all links to pages must be updated below 7020 var oldId = diagrams[i].getAttribute('id') 7021 diagrams[i].removeAttribute('id'); 7022 7023 var page = this.updatePageRoot(new DiagramPage(diagrams[i])); 7024 mapping[oldId] = diagrams[i].getAttribute('id'); 7025 var index = this.pages.length; 7026 7027 // Checks for invalid page names 7028 if (page.getName() == null) 7029 { 7030 page.setName(mxResources.get('pageWithNumber', [index + 1])); 7031 } 7032 7033 graph.model.execute(new ChangePage(this, page, page, index, true)); 7034 pages.push(page); 7035 } 7036 7037 this.updatePageLinks(mapping, pages); 7038 } 7039 } 7040 7041 if (node != null && node.nodeName === 'mxGraphModel') 7042 { 7043 cells = graph.importGraphModel(node, dx, dy, crop); 7044 7045 if (cells != null) 7046 { 7047 for (var i = 0; i < cells.length; i++) 7048 { 7049 this.updatePageLinksForCell(mapping, cells[i]); 7050 } 7051 } 7052 } 7053 7054 if (applyDefaultStyles) 7055 { 7056 this.insertHandler(cells, null, null, 7057 graph.defaultVertexStyle, 7058 graph.defaultEdgeStyle, 7059 false, true); 7060 } 7061 } 7062 finally 7063 { 7064 graph.model.endUpdate(); 7065 } 7066 } 7067 } 7068 catch (e) 7069 { 7070 if (!noErrorHandling) 7071 { 7072 this.handleError(e); 7073 } 7074 else 7075 { 7076 throw e; 7077 } 7078 } 7079 7080 return cells; 7081 }; 7082 7083 /** 7084 * Updates links to pages in shapes and labels. 7085 */ 7086 EditorUi.prototype.updatePageLinks = function(mapping, pages) 7087 { 7088 for (var i = 0; i < pages.length; i++) 7089 { 7090 this.updatePageLinksForCell(mapping, pages[i].root); 7091 } 7092 }; 7093 7094 /** 7095 * Updates links to pages in shapes and labels. 7096 */ 7097 EditorUi.prototype.updatePageLinksForCell = function(mapping, cell) 7098 { 7099 var temp = document.createElement('div'); 7100 var graph = this.editor.graph; 7101 var href = graph.getLinkForCell(cell); 7102 7103 if (href != null) 7104 { 7105 graph.setLinkForCell(cell, this.updatePageLink(mapping, href)); 7106 } 7107 7108 if (graph.isHtmlLabel(cell)) 7109 { 7110 temp.innerHTML = graph.sanitizeHtml(graph.getLabel(cell)); 7111 var links = temp.getElementsByTagName('a'); 7112 var changed = false; 7113 7114 for (var i = 0; i < links.length; i++) 7115 { 7116 href = links[i].getAttribute('href'); 7117 7118 if (href != null) 7119 { 7120 links[i].setAttribute('href', this.updatePageLink(mapping, href)); 7121 changed = true; 7122 } 7123 } 7124 7125 if (changed) 7126 { 7127 graph.labelChanged(cell, temp.innerHTML); 7128 } 7129 } 7130 7131 for (var i = 0; i < graph.model.getChildCount(cell); i++) 7132 { 7133 this.updatePageLinksForCell(mapping, graph.model.getChildAt(cell, i)); 7134 } 7135 }; 7136 7137 /** 7138 * Updates links to pages in shapes and labels. 7139 */ 7140 EditorUi.prototype.updatePageLink = function(mapping, href) 7141 { 7142 if (Graph.isPageLink(href)) 7143 { 7144 var newId = mapping[href.substring(href.indexOf(',') + 1)]; 7145 href = (newId != null) ? 'data:page/id,' + newId : null; 7146 } 7147 else if (href.substring(0, 17) == 'data:action/json,') 7148 { 7149 try 7150 { 7151 var link = JSON.parse(href.substring(17)); 7152 7153 if (link.actions != null) 7154 { 7155 for (var i = 0; i < link.actions.length; i++) 7156 { 7157 var action = link.actions[i]; 7158 7159 if (action.open != null && Graph.isPageLink(action.open)) 7160 { 7161 var oldId = action.open.substring(action.open.indexOf(',') + 1); 7162 var newId = mapping[oldId]; 7163 7164 if (newId != null) 7165 { 7166 action.open = 'data:page/id,' + newId; 7167 } 7168 else if (this.getPageById(oldId) == null) 7169 { 7170 delete action.open; 7171 } 7172 } 7173 } 7174 7175 href = 'data:action/json,' + JSON.stringify(link); 7176 } 7177 } 7178 catch (e) 7179 { 7180 // Ignore 7181 } 7182 } 7183 7184 return href; 7185 }; 7186 7187 /** 7188 * Returns true for VSD, VDX and VSS, VSX files. 7189 */ 7190 EditorUi.prototype.isRemoteVisioFormat = function(filename) 7191 { 7192 return /(\.v(sd|dx))($|\?)/i.test(filename) || /(\.vs(s|x))($|\?)/i.test(filename); 7193 }; 7194 7195 /** 7196 * Imports the given Visio file 7197 */ 7198 EditorUi.prototype.importVisio = function(file, done, onerror, filename, customParam) 7199 { 7200 //A reduced version of this code is used in conf/jira plugins, review that code whenever this function is changed 7201 filename = (filename != null) ? filename : file.name; 7202 7203 onerror = (onerror != null) ? onerror : mxUtils.bind(this, function(e) 7204 { 7205 this.handleError(e); 7206 }); 7207 7208 var delayed = mxUtils.bind(this, function() 7209 { 7210 this.loadingExtensions = false; 7211 7212 if (this.doImportVisio) 7213 { 7214 var remote = this.isRemoteVisioFormat(filename); 7215 7216 try 7217 { 7218 var ext = 'UNKNOWN-VISIO'; 7219 var dot = filename.lastIndexOf('.'); 7220 7221 if (dot >= 0 && dot < filename.length) 7222 { 7223 ext = filename.substring(dot + 1).toUpperCase(); 7224 } 7225 else 7226 { 7227 var slash = filename.lastIndexOf('/'); 7228 7229 if (slash >= 0 && slash < filename.length) 7230 { 7231 filename = filename.substring(slash + 1); 7232 } 7233 } 7234 7235 EditorUi.logEvent({category: ext + '-MS-IMPORT-FILE', 7236 action: 'filename_' + filename, 7237 label: (remote) ? 'remote' : 'local'}); 7238 } 7239 catch (e) 7240 { 7241 // ignore 7242 } 7243 7244 if (remote) 7245 { 7246 if (VSD_CONVERT_URL != null && !this.isOffline()) 7247 { 7248 var formData = new FormData(); 7249 formData.append('file1', file, filename); 7250 7251 var xhr = new XMLHttpRequest(); 7252 xhr.open('POST', VSD_CONVERT_URL + (/(\.vss|\.vsx)$/.test(filename)? '?stencil=1' : '')); 7253 xhr.responseType = 'blob'; 7254 this.addRemoteServiceSecurityCheck(xhr); 7255 7256 if (customParam != null) 7257 { 7258 xhr.setRequestHeader('x-convert-custom', customParam); 7259 } 7260 7261 xhr.onreadystatechange = mxUtils.bind(this, function() 7262 { 7263 if (xhr.readyState == 4) 7264 { 7265 if (xhr.status >= 200 && xhr.status <= 299) 7266 { 7267 try 7268 { 7269 var resp = xhr.response; 7270 7271 if (resp.type == 'text/xml') 7272 { 7273 var reader = new FileReader(); 7274 7275 reader.onload = mxUtils.bind(this, function(e) 7276 { 7277 try 7278 { 7279 done(e.target.result); 7280 } 7281 catch (e) 7282 { 7283 onerror({message: mxResources.get('errorLoadingFile')}); 7284 } 7285 }); 7286 7287 reader.readAsText(resp); 7288 } 7289 else 7290 { 7291 this.doImportVisio(resp, done, onerror, filename); 7292 } 7293 } 7294 catch (e) 7295 { 7296 onerror(e); 7297 } 7298 } 7299 else 7300 { 7301 try 7302 { 7303 if (xhr.responseType == '' || xhr.responseType == 'text') 7304 { 7305 onerror({message: xhr.responseText}); 7306 } 7307 else 7308 { 7309 var reader = new FileReader(); 7310 reader.onload = function() 7311 { 7312 onerror({message: JSON.parse(reader.result).Message}); 7313 } 7314 reader.readAsText(xhr.response); 7315 } 7316 } 7317 catch(e) 7318 { 7319 onerror({}); 7320 } 7321 } 7322 } 7323 }); 7324 7325 xhr.send(formData); 7326 } 7327 else 7328 { 7329 onerror({message: this.getServiceName() == 'conf'? mxResources.get('vsdNoConfig') : mxResources.get('serviceUnavailableOrBlocked')}); 7330 } 7331 } 7332 else 7333 { 7334 try 7335 { 7336 this.doImportVisio(file, done, onerror, filename); 7337 } 7338 catch (e) 7339 { 7340 onerror(e); 7341 } 7342 } 7343 } 7344 else 7345 { 7346 this.spinner.stop(); 7347 this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')}); 7348 } 7349 }); 7350 7351 if (!this.doImportVisio && !this.loadingExtensions && !this.isOffline(true)) 7352 { 7353 this.loadingExtensions = true; 7354 mxscript('js/extensions.min.js', delayed); 7355 } 7356 else 7357 { 7358 delayed(); 7359 } 7360 }; 7361 7362 /** 7363 * Imports the given GraphML (yEd) file 7364 */ 7365 EditorUi.prototype.importGraphML = function(xmlData, done, onerror) 7366 { 7367 onerror = (onerror != null) ? onerror : mxUtils.bind(this, function(e) 7368 { 7369 this.handleError(e); 7370 }); 7371 7372 var delayed = mxUtils.bind(this, function() 7373 { 7374 this.loadingExtensions = false; 7375 7376 if (this.doImportGraphML) 7377 { 7378 7379 try 7380 { 7381 this.doImportGraphML(xmlData, done, onerror); 7382 } 7383 catch (e) 7384 { 7385 onerror(e); 7386 } 7387 } 7388 else 7389 { 7390 this.spinner.stop(); 7391 this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')}); 7392 } 7393 }); 7394 7395 if (!this.doImportGraphML && !this.loadingExtensions && !this.isOffline(true)) 7396 { 7397 this.loadingExtensions = true; 7398 mxscript('js/extensions.min.js', delayed); 7399 } 7400 else 7401 { 7402 delayed(); 7403 } 7404 }; 7405 7406 /** 7407 * Export the diagram to VSDX 7408 */ 7409 EditorUi.prototype.exportVisio = function(currentPage) 7410 { 7411 var delayed = mxUtils.bind(this, function() 7412 { 7413 this.loadingExtensions = false; 7414 7415 if (typeof VsdxExport !== 'undefined') 7416 { 7417 try 7418 { 7419 var expSuccess = new VsdxExport(this).exportCurrentDiagrams(currentPage); 7420 7421 if (!expSuccess) 7422 { 7423 this.handleError({message: mxResources.get('unknownError')}); 7424 } 7425 } 7426 catch (e) 7427 { 7428 this.handleError(e); 7429 } 7430 } 7431 else 7432 { 7433 this.spinner.stop(); 7434 this.handleError({message: mxResources.get('serviceUnavailableOrBlocked')}); 7435 } 7436 }); 7437 7438 if (typeof VsdxExport === 'undefined' && !this.loadingExtensions && !this.isOffline(true)) 7439 { 7440 this.loadingExtensions = true; 7441 mxscript('js/extensions.min.js', delayed); 7442 } 7443 else 7444 { 7445 delayed(); 7446 } 7447 }; 7448 7449 /** 7450 * Imports the given Lucidchart data. 7451 */ 7452 EditorUi.prototype.convertLucidChart = function(data, success, error) 7453 { 7454 var delayed = mxUtils.bind(this, function() 7455 { 7456 this.loadingExtensions = false; 7457 7458 // Checks for signature method 7459 if (typeof window.LucidImporter !== 'undefined') 7460 { 7461 try 7462 { 7463 EditorUi.logEvent({category: 'LUCIDCHART-IMPORT-FILE', 7464 action: 'size_' + data.length}); 7465 EditorUi.debug('convertLucidChart', data); 7466 } 7467 catch (e) 7468 { 7469 // ignore 7470 } 7471 7472 try 7473 { 7474 success(LucidImporter.importState(JSON.parse(data))); 7475 } 7476 catch (e) 7477 { 7478 if (window.console != null) 7479 { 7480 console.error(e); 7481 } 7482 7483 error(e); 7484 } 7485 } 7486 else 7487 { 7488 error({message: mxResources.get('serviceUnavailableOrBlocked')}); 7489 } 7490 }); 7491 7492 if (typeof window.LucidImporter === 'undefined' && 7493 !this.loadingExtensions && !this.isOffline(true)) 7494 { 7495 this.loadingExtensions = true; 7496 7497 if (urlParams['dev'] == '1') 7498 { 7499 //Lucid org chart requires orgChart layout, in production, it is part of the extemsions.min.js 7500 mxscript('js/diagramly/Extensions.js', function() 7501 { 7502 mxscript('js/orgchart/bridge.min.js', function() 7503 { 7504 mxscript('js/orgchart/bridge.collections.min.js', function() 7505 { 7506 mxscript('js/orgchart/OrgChart.Layout.min.js', function() 7507 { 7508 mxscript('js/orgchart/mxOrgChartLayout.js', delayed); 7509 }); 7510 }); 7511 }); 7512 }); 7513 } 7514 else 7515 { 7516 mxscript('js/extensions.min.js', delayed); 7517 } 7518 } 7519 else 7520 { 7521 // Async needed for selection 7522 window.setTimeout(delayed, 0); 7523 } 7524 }; 7525 7526 /** 7527 * Generates a Mermaid image. 7528 */ 7529 EditorUi.prototype.generateMermaidImage = function(data, config, success, error) 7530 { 7531 var ui = this; 7532 7533 var delayed = function() 7534 { 7535 try 7536 { 7537 this.loadingMermaid = false; 7538 7539 config = (config != null) ? config : EditorUi.defaultMermaidConfig; 7540 config.securityLevel = 'strict'; 7541 config.startOnLoad = false; 7542 7543 mermaid.mermaidAPI.initialize(config); 7544 7545 mermaid.mermaidAPI.render('geMermaidOutput-' + new Date().getTime(), data, function(svg) 7546 { 7547 try 7548 { 7549 // Workaround for namespace errors in SVG output for IE 7550 if (mxClient.IS_IE || mxClient.IS_IE11) 7551 { 7552 svg = svg.replace(/ xmlns:\S*="http:\/\/www.w3.org\/XML\/1998\/namespace"/g, ''). 7553 replace(/ (NS xml|\S*):space="preserve"/g, ' xml:space="preserve"'); 7554 } 7555 7556 var doc = mxUtils.parseXml(svg); 7557 var svgs = doc.getElementsByTagName('svg'); 7558 7559 if (svgs.length > 0) 7560 { 7561 var w = parseFloat(svgs[0].getAttribute('width')); 7562 var h = parseFloat(svgs[0].getAttribute('height')); 7563 7564 if (isNaN(w) || isNaN(h)) 7565 { 7566 try 7567 { 7568 var viewBox = svgs[0].getAttribute('viewBox').split(/\s+/); 7569 w = parseFloat(viewBox[2]); 7570 h = parseFloat(viewBox[3]); 7571 } 7572 catch(e) 7573 { 7574 //Any size such that it shows up 7575 w = w || 100; 7576 h = h || 100; 7577 } 7578 } 7579 7580 success(ui.convertDataUri(Editor.createSvgDataUri(svg)), w, h); 7581 } 7582 else 7583 { 7584 error({message: mxResources.get('invalidInput')}); 7585 } 7586 } 7587 catch (e) 7588 { 7589 error(e); 7590 } 7591 }); 7592 } 7593 catch (e) 7594 { 7595 error(e); 7596 } 7597 }; 7598 7599 if (typeof mermaid === 'undefined' && !this.loadingMermaid && !this.isOffline(true)) 7600 { 7601 this.loadingMermaid = true; 7602 7603 if (urlParams['dev'] == '1') 7604 { 7605 mxscript('js/mermaid/mermaid.min.js', delayed); 7606 } 7607 else 7608 { 7609 mxscript('js/extensions.min.js', delayed); 7610 } 7611 } 7612 else 7613 { 7614 delayed(); 7615 } 7616 }; 7617 7618 /** 7619 * Generates a plant UML image. Possible types are svg, png and txt. 7620 */ 7621 EditorUi.prototype.generatePlantUmlImage = function(data, type, success, error) 7622 { 7623 function encode64(data) 7624 { 7625 r = ""; 7626 7627 for (i = 0; i < data.length; i += 3) 7628 { 7629 if (i + 2 == data.length) 7630 { 7631 r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0); 7632 } 7633 else if (i + 1 == data.length) 7634 { 7635 r += append3bytes(data.charCodeAt(i), 0, 0); 7636 } 7637 else 7638 { 7639 r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 7640 data.charCodeAt(i + 2)); 7641 } 7642 } 7643 7644 return r; 7645 } 7646 7647 function append3bytes(b1, b2, b3) 7648 { 7649 c1 = b1 >> 2; 7650 c2 = ((b1 & 0x3) << 4) | (b2 >> 4); 7651 c3 = ((b2 & 0xF) << 2) | (b3 >> 6); 7652 c4 = b3 & 0x3F; 7653 r = ""; 7654 r += encode6bit(c1 & 0x3F); 7655 r += encode6bit(c2 & 0x3F); 7656 r += encode6bit(c3 & 0x3F); 7657 r += encode6bit(c4 & 0x3F); 7658 7659 return r; 7660 } 7661 7662 function encode6bit(b) 7663 { 7664 if (b < 10) 7665 { 7666 return String.fromCharCode(48 + b); 7667 } 7668 7669 b -= 10; 7670 7671 if (b < 26) 7672 { 7673 return String.fromCharCode(65 + b); 7674 } 7675 7676 b -= 26; 7677 7678 if (b < 26) 7679 { 7680 return String.fromCharCode(97 + b); 7681 } 7682 7683 b -= 26; 7684 7685 if (b == 0) 7686 { 7687 return '-'; 7688 } 7689 7690 if (b == 1) 7691 { 7692 return '_'; 7693 } 7694 7695 return '?'; 7696 } 7697 7698 // TODO: Remove unescape, use btoa for compatibility with graph.compress 7699 function compress(s) 7700 { 7701 return encode64(Graph.arrayBufferToString(pako.deflateRaw(s))); 7702 }; 7703 7704 var plantUmlServerUrl = (type == 'txt') ? PLANT_URL + '/txt/' : 7705 ((type == 'png') ? PLANT_URL + '/png/' : PLANT_URL + '/svg/'); 7706 7707 var xhr = new XMLHttpRequest(); 7708 xhr.open('GET', plantUmlServerUrl + compress(data), true); 7709 7710 if (type != 'txt') 7711 { 7712 xhr.responseType = 'blob'; 7713 } 7714 7715 xhr.onload = function(e) 7716 { 7717 if (this.status >= 200 && this.status < 300) 7718 { 7719 if (type == 'txt') 7720 { 7721 success(this.response); 7722 } 7723 else 7724 { 7725 var reader = new FileReader(); 7726 reader.readAsDataURL(this.response); 7727 7728 reader.onloadend = function(e) 7729 { 7730 var img = new Image(); 7731 7732 img.onload = function() 7733 { 7734 try 7735 { 7736 var w = img.width; 7737 var h = img.height; 7738 7739 // Workaround for 0 image size in IE11 7740 if (w == 0 && h == 0) 7741 { 7742 var data = reader.result; 7743 var comma = data.indexOf(','); 7744 var svgText = decodeURIComponent(escape(atob(data.substring(comma + 1)))); 7745 var root = mxUtils.parseXml(svgText); 7746 var svgs = root.getElementsByTagName('svg'); 7747 7748 if (svgs.length > 0) 7749 { 7750 w = parseFloat(svgs[0].getAttribute('width')); 7751 h = parseFloat(svgs[0].getAttribute('height')); 7752 } 7753 } 7754 7755 success(reader.result, w, h); 7756 } 7757 catch (e) 7758 { 7759 error(e); 7760 } 7761 }; 7762 7763 img.src = reader.result; 7764 }; 7765 7766 reader.onerror = function(e) 7767 { 7768 error(e); 7769 }; 7770 } 7771 } 7772 else 7773 { 7774 error(e); 7775 } 7776 }; 7777 7778 xhr.onerror = function(e) 7779 { 7780 error(e); 7781 }; 7782 7783 xhr.send(); 7784 }; 7785 7786 /** 7787 * Inserts the given text as a preformatted HTML text. 7788 */ 7789 EditorUi.prototype.insertAsPreText = function(text, x, y) 7790 { 7791 var graph = this.editor.graph; 7792 var cell = null; 7793 7794 graph.getModel().beginUpdate(); 7795 try 7796 { 7797 cell = graph.insertVertex(null, null, '<pre>' + text + '</pre>', 7798 x, y, 1, 1, 'text;html=1;align=left;verticalAlign=top;'); 7799 graph.updateCellSize(cell, true); 7800 } 7801 finally 7802 { 7803 graph.getModel().endUpdate(); 7804 } 7805 7806 return cell; 7807 }; 7808 7809 /** 7810 * Imports the given XML into the existing diagram. 7811 * TODO: Make this function asynchronous 7812 */ 7813 EditorUi.prototype.insertTextAt = function(text, dx, dy, html, asImage, crop, resizeImages, addNewPage) 7814 { 7815 crop = (crop != null) ? crop : true; 7816 resizeImages = (resizeImages != null) ? resizeImages : true; 7817 7818 // Handles special case for Gliffy data which requires async server-side for parsing 7819 if (text != null) 7820 { 7821 if (Graph.fileSupport && !this.isOffline() && new XMLHttpRequest().upload && this.isRemoteFileFormat(text)) 7822 { 7823 // Fixes possible parsing problems with ASCII 160 (non-breaking space) 7824 this.parseFile(new Blob([text.replace(/\s+/g,' ')], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr) 7825 { 7826 if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status <= 299) 7827 { 7828 this.editor.graph.setSelectionCells(this.insertTextAt( 7829 xhr.responseText, dx, dy, true)); 7830 } 7831 })); 7832 7833 // Returns empty cells array as it is aysynchronous 7834 return []; 7835 } 7836 // Handles special case of data URI which requires async loading for finding size 7837 else if (text.substring(0, 5) == 'data:' || (!this.isOffline() && 7838 (asImage || (/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(text)))) 7839 { 7840 var graph = this.editor.graph; 7841 7842 // Checks for embedded XML in PDF 7843 if (text.substring(0, 28) == 'data:application/pdf;base64,') 7844 { 7845 var xml = Editor.extractGraphModelFromPdf(text); 7846 7847 if (xml != null && xml.length > 0) 7848 { 7849 return this.importXml(xml, dx, dy, crop, true, addNewPage); 7850 } 7851 } 7852 7853 // Checks for embedded XML in PNG 7854 if (text.substring(0, 22) == 'data:image/png;base64,') 7855 { 7856 var xml = this.extractGraphModelFromPng(text); 7857 7858 if (xml != null && xml.length > 0) 7859 { 7860 return this.importXml(xml, dx, dy, crop, true, addNewPage); 7861 } 7862 } 7863 7864 // Tries to extract embedded XML from SVG data URI 7865 if (text.substring(0, 19) == 'data:image/svg+xml;') 7866 { 7867 try 7868 { 7869 var xml = null; 7870 7871 if (text.substring(0, 26) == 'data:image/svg+xml;base64,') 7872 { 7873 xml = text.substring(text.indexOf(',') + 1); 7874 xml = (window.atob && !mxClient.IS_SF) ? atob(xml) : Base64.decode(xml, true); 7875 } 7876 else 7877 { 7878 xml = decodeURIComponent(text.substring(text.indexOf(',') + 1)); 7879 } 7880 7881 var result = this.importXml(xml, dx, dy, crop, true, addNewPage); 7882 7883 if (result.length > 0) 7884 { 7885 return result; 7886 } 7887 } 7888 catch (e) 7889 { 7890 // Ignore 7891 } 7892 } 7893 7894 this.loadImage(text, mxUtils.bind(this, function(img) 7895 { 7896 if (text.substring(0, 5) == 'data:') 7897 { 7898 this.resizeImage(img, text, mxUtils.bind(this, function(data2, w2, h2) 7899 { 7900 graph.setSelectionCell(graph.insertVertex(null, null, '', graph.snap(dx), graph.snap(dy), 7901 w2, h2, 'shape=image;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;' + 7902 'verticalAlign=top;aspect=fixed;imageAspect=0;image=' + this.convertDataUri(data2) + ';')); 7903 }), resizeImages, this.maxImageSize); 7904 } 7905 else 7906 { 7907 var s = Math.min(1, Math.min(this.maxImageSize / img.width, this.maxImageSize / img.height)); 7908 var w = Math.round(img.width * s); 7909 var h = Math.round(img.height * s); 7910 7911 graph.setSelectionCell(graph.insertVertex(null, null, '', graph.snap(dx), graph.snap(dy), 7912 w, h, 'shape=image;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;' + 7913 'verticalAlign=top;aspect=fixed;imageAspect=0;image=' + text + ';')); 7914 } 7915 }), mxUtils.bind(this, function() 7916 { 7917 var cell = null; 7918 7919 // Inserts invalid data URIs as text 7920 graph.getModel().beginUpdate(); 7921 try 7922 { 7923 cell = graph.insertVertex(graph.getDefaultParent(), null, text, 7924 graph.snap(dx), graph.snap(dy), 1, 1, 'text;' + ((html) ? 'html=1;' : '')); 7925 graph.updateCellSize(cell); 7926 graph.fireEvent(new mxEventObject('textInserted', 'cells', [cell])); 7927 } 7928 finally 7929 { 7930 graph.getModel().endUpdate(); 7931 } 7932 7933 graph.setSelectionCell(cell); 7934 })); 7935 7936 return []; 7937 } 7938 else 7939 { 7940 text = Graph.zapGremlins(mxUtils.trim(text)); 7941 7942 if (this.isCompatibleString(text)) 7943 { 7944 return this.importXml(text, dx, dy, crop, null, addNewPage); 7945 } 7946 else if (text.length > 0) 7947 { 7948 if (this.isLucidChartData(text)) 7949 { 7950 this.convertLucidChart(text, mxUtils.bind(this, function(xml) 7951 { 7952 this.editor.graph.setSelectionCells( 7953 this.importXml(xml, dx, dy, crop, 7954 null, addNewPage)); 7955 }), mxUtils.bind(this, function(e) 7956 { 7957 this.handleError(e); 7958 })); 7959 } 7960 else 7961 { 7962 var graph = this.editor.graph; 7963 var cell = null; 7964 7965 graph.getModel().beginUpdate(); 7966 try 7967 { 7968 // Fires cellsInserted to apply the current style to the inserted text. 7969 // This requires the value to be empty when the event is fired. 7970 cell = graph.insertVertex(graph.getDefaultParent(), null, '', 7971 graph.snap(dx), graph.snap(dy), 1, 1, 'text;whiteSpace=wrap;' + ((html) ? 'html=1;' : '')); 7972 graph.fireEvent(new mxEventObject('textInserted', 'cells', [cell])); 7973 7974 // Single tag is converted 7975 if (text.charAt(0) == '<' && text.indexOf('>') == text.length - 1) 7976 { 7977 text = mxUtils.htmlEntities(text); 7978 } 7979 7980 //TODO Refuse unsupported file types early as at this stage a lot of processing has beed done and time is wasted. 7981 // For example, 5 MB PDF files is processed and then only 0.5 MB of meaningless text is added! 7982 //Limit labels to maxTextBytes 7983 if (text.length > this.maxTextBytes) 7984 { 7985 text = text.substring(0, this.maxTextBytes) + '...'; 7986 } 7987 7988 // Apply value and updates the cell size to fit the text block 7989 cell.value = text; 7990 graph.updateCellSize(cell); 7991 7992 // Adds wrapping for large text blocks 7993 if (this.maxTextWidth > 0 && cell.geometry.width > this.maxTextWidth) 7994 { 7995 var size = graph.getPreferredSizeForCell(cell, this.maxTextWidth); 7996 cell.geometry.width = size.width; 7997 cell.geometry.height = size.height; 7998 } 7999 8000 // See https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url 8001 if (Graph.isLink(cell.value)) 8002 { 8003 graph.setLinkForCell(cell, cell.value); 8004 } 8005 8006 // Adds spacing 8007 cell.geometry.width += graph.gridSize; 8008 cell.geometry.height += graph.gridSize; 8009 } 8010 finally 8011 { 8012 graph.getModel().endUpdate(); 8013 } 8014 8015 return [cell]; 8016 } 8017 } 8018 } 8019 } 8020 8021 return []; 8022 }; 8023 8024 /** 8025 * Formats the given file size. 8026 */ 8027 EditorUi.prototype.formatFileSize = function(size) 8028 { 8029 var units = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']; 8030 var i = -1; 8031 8032 do 8033 { 8034 size = size / 1024; 8035 i++; 8036 } while (size > 1024); 8037 8038 return Math.max(size, 0.1).toFixed(1) + units[i]; 8039 }; 8040 8041 /** 8042 * Imports the given XML into the existing diagram. 8043 */ 8044 EditorUi.prototype.convertDataUri = function(uri) 8045 { 8046 // Handles special case of data URI which needs to be rewritten 8047 // to be used in a cell style to remove the semicolon 8048 if (uri.substring(0, 5) == 'data:') 8049 { 8050 var semi = uri.indexOf(';'); 8051 8052 if (semi > 0) 8053 { 8054 uri = uri.substring(0, semi) + uri.substring(uri.indexOf(',', semi + 1)); 8055 } 8056 } 8057 8058 return uri; 8059 }; 8060 8061 /** 8062 * Returns true for Gliffy data. 8063 */ 8064 EditorUi.prototype.isRemoteFileFormat = function(data, filename) 8065 { 8066 return /(\"contentType\":\s*\"application\/gliffy\+json\")/.test(data); 8067 }; 8068 8069 /** 8070 * Returns true for Gliffy 8071 */ 8072 EditorUi.prototype.isLucidChartData = function(data) 8073 { 8074 return data != null && (data.substring(0, 26) == 8075 '{"state":"{\\"Properties\\":' || 8076 data.substring(0, 14) == '{"Properties":'); 8077 }; 8078 8079 /** 8080 * Imports a local file from the device or local storage. 8081 */ 8082 EditorUi.prototype.importLocalFile = function(device, noSplash) 8083 { 8084 if (device && Graph.fileSupport) 8085 { 8086 if (this.importFileInputElt == null) 8087 { 8088 var input = document.createElement('input'); 8089 input.setAttribute('type', 'file'); 8090 8091 mxEvent.addListener(input, 'change', mxUtils.bind(this, function() 8092 { 8093 if (input.files != null) 8094 { 8095 // Using null for position will disable crop of input file 8096 this.importFiles(input.files, null, null, this.maxImageSize); 8097 8098 // Resets input to force change event for same file (type reset required for IE) 8099 input.type = ''; 8100 input.type = 'file'; 8101 input.value = ''; 8102 } 8103 })); 8104 8105 input.style.display = 'none'; 8106 document.body.appendChild(input); 8107 this.importFileInputElt = input; 8108 } 8109 8110 this.importFileInputElt.click(); 8111 } 8112 else 8113 { 8114 window.openNew = false; 8115 window.openKey = 'import'; 8116 8117 window.listBrowserFiles = mxUtils.bind(this, function(success, error) 8118 { 8119 StorageFile.listFiles(this, 'F', success, error); 8120 }); 8121 8122 window.openBrowserFile = mxUtils.bind(this, function(title, success, error) 8123 { 8124 StorageFile.getFileContent(this, title, success, error); 8125 }); 8126 8127 window.deleteBrowserFile = mxUtils.bind(this, function(title, success, error) 8128 { 8129 StorageFile.deleteFile(this, title, success, error); 8130 }); 8131 8132 if (!noSplash) 8133 { 8134 var prevValue = Editor.useLocalStorage; 8135 Editor.useLocalStorage = !device; 8136 } 8137 8138 // Closes dialog after open 8139 window.openFile = new OpenFile(mxUtils.bind(this, function(cancel) 8140 { 8141 this.hideDialog(cancel); 8142 })); 8143 8144 window.openFile.setConsumer(mxUtils.bind(this, function(xml, filename) 8145 { 8146 if (filename != null && Graph.fileSupport && /(\.v(dx|sdx?))($|\?)/i.test(filename)) 8147 { 8148 // "Not a UTF 8 file" when opening VSDX in IE so this is never called 8149 var file = new Blob([xml], {type: 'application/octet-stream'}) 8150 8151 this.importVisio(file, mxUtils.bind(this, function(xml) 8152 { 8153 this.importXml(xml, 0, 0, true); 8154 }), null, filename); 8155 } 8156 else 8157 { 8158 this.editor.graph.setSelectionCells(this.importXml(xml, 0, 0, true)); 8159 } 8160 })); 8161 8162 // Removes openFile if dialog is closed 8163 this.showDialog(new OpenDialog(this).container, (Editor.useLocalStorage) ? 640 : 360, 8164 (Editor.useLocalStorage) ? 480 : 220, true, true, function() 8165 { 8166 window.openFile = null; 8167 }); 8168 8169 // Extends dialog close to show splash screen 8170 if (!noSplash) 8171 { 8172 var dlg = this.dialog; 8173 var dlgClose = dlg.close; 8174 8175 this.dialog.close = mxUtils.bind(this, function(cancel) 8176 { 8177 Editor.useLocalStorage = prevValue; 8178 dlgClose.apply(dlg, arguments); 8179 8180 if (cancel && this.getCurrentFile() == null && urlParams['embed'] != '1') 8181 { 8182 this.showSplash(); 8183 } 8184 }); 8185 } 8186 } 8187 }; 8188 8189 /** 8190 * Imports the given zip file. 8191 */ 8192 EditorUi.prototype.importZipFile = function(file, success, onerror) 8193 { 8194 var ui = this; 8195 8196 var delayed = mxUtils.bind(this, function() 8197 { 8198 this.loadingExtensions = false; 8199 8200 if (typeof JSZip !== 'undefined') 8201 { 8202 JSZip.loadAsync(file) 8203 .then(function(zip) 8204 { 8205 if (Object.keys(zip.files).length == 0) 8206 { 8207 onerror(); 8208 } 8209 else 8210 { 8211 var gliffyLatestVer = {version: 0}; 8212 var drawioFound = false; 8213 8214 zip.forEach(function (relativePath, zipEntry) 8215 { 8216 var name = zipEntry.name.toLowerCase(); 8217 8218 if (name == 'diagram/diagram.xml') //draw.io zip format has the latest diagram version at diagram/diagram.xml 8219 { 8220 drawioFound = true; 8221 8222 zipEntry.async("string").then(function(str){ 8223 if (str.indexOf('<mxfile ') == 0) 8224 { 8225 success(str); 8226 } 8227 else 8228 { 8229 onerror(); 8230 } 8231 }); 8232 } 8233 else if (name.indexOf('versions/') == 0) //Gliffy zip format has the versions inside versions folder 8234 { 8235 var version = parseInt(name.substr(9)); //9 is the length of versions/ 8236 8237 if (version > gliffyLatestVer.version) 8238 { 8239 gliffyLatestVer = {version: version, zipEntry: zipEntry} 8240 } 8241 } 8242 }); 8243 8244 if (gliffyLatestVer.version > 0) 8245 { 8246 gliffyLatestVer.zipEntry.async("string").then(function(data) 8247 { 8248 if (!ui.isOffline() && new XMLHttpRequest().upload && ui.isRemoteFileFormat(data, file.name)) 8249 { 8250 ui.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr) 8251 { 8252 if (xhr.readyState == 4) 8253 { 8254 if (xhr.status >= 200 && xhr.status <= 299) 8255 { 8256 success(xhr.responseText); 8257 } 8258 else 8259 { 8260 onerror(); 8261 } 8262 } 8263 }), file.name); 8264 } 8265 else 8266 { 8267 onerror(); 8268 } 8269 }); 8270 } 8271 else if (!drawioFound) 8272 { 8273 onerror(); 8274 } 8275 } 8276 }, function (e) { 8277 onerror(e); 8278 }); 8279 } 8280 else 8281 { 8282 onerror(); 8283 } 8284 }); 8285 8286 if (typeof JSZip === 'undefined' && !this.loadingExtensions && !this.isOffline(true)) 8287 { 8288 this.loadingExtensions = true; 8289 mxscript('js/extensions.min.js', delayed); 8290 } 8291 else 8292 { 8293 delayed(); 8294 } 8295 }; 8296 8297 /** 8298 * Imports the given XML into the existing diagram. 8299 */ 8300 EditorUi.prototype.importFile = function(data, mimeType, dx, dy, w, h, filename, 8301 done, file, crop, ignoreEmbeddedXml, evt) 8302 { 8303 crop = (crop != null) ? crop : true; 8304 var async = false; 8305 var cells = null; 8306 8307 var handleResult = mxUtils.bind(this, function(xml) 8308 { 8309 var importedCells = null; 8310 8311 if (xml != null && xml.substring(0, 10) == '<mxlibrary') 8312 { 8313 this.loadLibrary(new LocalLibrary(this, xml, filename)); 8314 } 8315 else 8316 { 8317 importedCells = this.importXml(xml, dx, dy, crop, null, 8318 (evt != null) ? mxEvent.isControlDown(evt) : null); 8319 } 8320 8321 if (done != null) 8322 { 8323 done(importedCells); 8324 } 8325 }); 8326 8327 if (mimeType.substring(0, 5) == 'image') 8328 { 8329 var containsModel = false; 8330 8331 if (mimeType.substring(0, 9) == 'image/png') 8332 { 8333 var xml = (ignoreEmbeddedXml) ? null : this.extractGraphModelFromPng(data); 8334 8335 if (xml != null && xml.length > 0) 8336 { 8337 cells = this.importXml(xml, dx, dy, crop, null, (evt != null) ? 8338 mxEvent.isControlDown(evt) : null); 8339 containsModel = true; 8340 } 8341 } 8342 8343 if (!containsModel) 8344 { 8345 var graph = this.editor.graph; 8346 8347 // Strips encoding bit (eg. ;base64,) for cell style 8348 var semi = data.indexOf(';'); 8349 8350 if (semi > 0) 8351 { 8352 data = data.substring(0, semi) + data.substring(data.indexOf(',', semi + 1)); 8353 } 8354 8355 if (crop && graph.isGridEnabled()) 8356 { 8357 dx = graph.snap(dx); 8358 dy = graph.snap(dy); 8359 } 8360 8361 cells = [graph.insertVertex(null, null, '', dx, dy, w, h, 8362 'shape=image;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;' + 8363 'verticalAlign=top;aspect=fixed;imageAspect=0;image=' + data + ';')]; 8364 } 8365 } 8366 else if (/(\.*<graphml )/.test(data)) 8367 { 8368 async = true; 8369 8370 this.importGraphML(data, handleResult); 8371 } 8372 else if (file != null && filename != null && ((/(\.v(dx|sdx?))($|\?)/i.test(filename)) || /(\.vs(x|sx?))($|\?)/i.test(filename))) 8373 { 8374 // LATER: done and async are a hack before making this asynchronous 8375 async = true; 8376 8377 this.importVisio(file, handleResult); 8378 } 8379 else if (!this.isOffline() && new XMLHttpRequest().upload && this.isRemoteFileFormat(data, filename)) 8380 { 8381 // LATER: done and async are a hack before making this asynchronous 8382 async = true; 8383 8384 // Returns empty cells array as it is aysynchronous 8385 this.parseFile((file != null) ? file : new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr) 8386 { 8387 if (xhr.readyState == 4) 8388 { 8389 if (xhr.status >= 200 && xhr.status <= 299) 8390 { 8391 handleResult(xhr.responseText); 8392 } 8393 else if (done != null) 8394 { 8395 done(null); 8396 } 8397 } 8398 }), filename); 8399 } 8400 else if (data.indexOf('PK') == 0 && file != null) 8401 { 8402 async = true; 8403 8404 this.importZipFile(file, handleResult, mxUtils.bind(this, function() 8405 { 8406 //If importing as a zip file failed, just insert as text 8407 cells = this.insertTextAt(this.validateFileData(data), dx, dy, true, null, crop); 8408 done(cells); 8409 })); 8410 } 8411 else if (!/(\.v(sd|dx))($|\?)/i.test(filename) && !/(\.vs(s|x))($|\?)/i.test(filename)) 8412 { 8413 cells = this.insertTextAt(this.validateFileData(data), dx, dy, true, 8414 null, crop, null, (evt != null) ? mxEvent.isControlDown(evt) : null); 8415 } 8416 8417 if (!async && done != null) 8418 { 8419 done(cells); 8420 } 8421 8422 return cells; 8423 }; 8424 8425 /** 8426 * 8427 */ 8428 EditorUi.prototype.importFiles = function(files, x, y, maxSize, fn, resultFn, filterFn, barrierFn, 8429 resizeDialog, maxBytes, resampleThreshold, ignoreEmbeddedXml, evt) 8430 { 8431 maxSize = (maxSize != null) ? maxSize : this.maxImageSize; 8432 maxBytes = (maxBytes != null) ? maxBytes : this.maxImageBytes; 8433 8434 var crop = x != null && y != null; 8435 var resizeImages = true; 8436 x = (x != null) ? x : 0; 8437 y = (y != null) ? y : 0; 8438 8439 // Checks if large images are imported 8440 var largeImages = false; 8441 8442 if (!mxClient.IS_CHROMEAPP && files != null) 8443 { 8444 var thresh = resampleThreshold || this.resampleThreshold; 8445 8446 for (var i = 0; i < files.length; i++) 8447 { 8448 if (files[i].type.substring(0, 6) == 'image/' && files[i].size > thresh) 8449 { 8450 largeImages = true; 8451 8452 break; 8453 } 8454 } 8455 } 8456 8457 var doImportFiles = mxUtils.bind(this, function() 8458 { 8459 var graph = this.editor.graph; 8460 var gs = graph.gridSize; 8461 8462 fn = (fn != null) ? fn : mxUtils.bind(this, function(data, mimeType, x, y, w, h, filename, done, file) 8463 { 8464 try 8465 { 8466 if (data != null && data.substring(0, 10) == '<mxlibrary') 8467 { 8468 this.spinner.stop(); 8469 this.loadLibrary(new LocalLibrary(this, data, filename)); 8470 8471 return null; 8472 } 8473 else 8474 { 8475 return this.importFile(data, mimeType, x, y, w, h, filename, 8476 done, file, crop, ignoreEmbeddedXml, evt); 8477 } 8478 } 8479 catch (e) 8480 { 8481 this.handleError(e); 8482 8483 return null; 8484 } 8485 }); 8486 8487 resultFn = (resultFn != null) ? resultFn : mxUtils.bind(this, function(cells) 8488 { 8489 graph.setSelectionCells(cells); 8490 }); 8491 8492 if (this.spinner.spin(document.body, mxResources.get('loading'))) 8493 { 8494 var count = files.length; 8495 var remain = count; 8496 var queue = []; 8497 8498 // Barrier waits for all files to be loaded asynchronously 8499 var barrier = mxUtils.bind(this, function(index, fnc) 8500 { 8501 queue[index] = fnc; 8502 8503 if (--remain == 0) 8504 { 8505 this.spinner.stop(); 8506 8507 if (barrierFn != null) 8508 { 8509 barrierFn(queue); 8510 } 8511 else 8512 { 8513 var cells = []; 8514 8515 graph.getModel().beginUpdate(); 8516 try 8517 { 8518 for (var j = 0; j < queue.length; j++) 8519 { 8520 var tmp = queue[j](); 8521 8522 if (tmp != null) 8523 { 8524 cells = cells.concat(tmp); 8525 } 8526 } 8527 } 8528 finally 8529 { 8530 graph.getModel().endUpdate(); 8531 } 8532 } 8533 8534 resultFn(cells); 8535 } 8536 }); 8537 8538 for (var i = 0; i < count; i++) 8539 { 8540 (mxUtils.bind(this, function(index) 8541 { 8542 var file = files[index]; 8543 8544 if (file != null) 8545 { 8546 var reader = new FileReader(); 8547 8548 reader.onload = mxUtils.bind(this, function(e) 8549 { 8550 if (filterFn == null || filterFn(file)) 8551 { 8552 if (file.type.substring(0, 6) == 'image/') 8553 { 8554 if (file.type.substring(0, 9) == 'image/svg') 8555 { 8556 // Checks if SVG contains content attribute 8557 var data = Graph.clipSvgDataUri(e.target.result); 8558 var comma = data.indexOf(','); 8559 var svgText = decodeURIComponent(escape(atob(data.substring(comma + 1)))); 8560 var root = mxUtils.parseXml(svgText); 8561 var svgs = root.getElementsByTagName('svg'); 8562 8563 if (svgs.length > 0) 8564 { 8565 var svgRoot = svgs[0]; 8566 var cont = (ignoreEmbeddedXml) ? null : svgRoot.getAttribute('content'); 8567 8568 if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%') 8569 { 8570 cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true)); 8571 } 8572 8573 if (cont != null && cont.charAt(0) == '%') 8574 { 8575 cont = decodeURIComponent(cont); 8576 } 8577 8578 if (cont != null && (cont.substring(0, 8) === '<mxfile ' || 8579 cont.substring(0, 14) === '<mxGraphModel ')) 8580 { 8581 barrier(index, mxUtils.bind(this, function() 8582 { 8583 return fn(cont, 'text/xml', x + index * gs, y + index * gs, 0, 0, file.name); 8584 })); 8585 } 8586 else 8587 { 8588 // SVG needs special handling to add viewbox if missing and 8589 // find initial size from SVG attributes (only for IE11) 8590 barrier(index, mxUtils.bind(this, function() 8591 { 8592 try 8593 { 8594 var prefix = data.substring(0, comma + 1); 8595 8596 // Parses SVG and find width and height 8597 if (root != null) 8598 { 8599 var svgs = root.getElementsByTagName('svg'); 8600 8601 if (svgs.length > 0) 8602 { 8603 var svgRoot = svgs[0]; 8604 var w = svgRoot.getAttribute('width'); 8605 var h = svgRoot.getAttribute('height'); 8606 8607 if (w != null && w.charAt(w.length - 1) != '%') 8608 { 8609 w = parseFloat(w); 8610 } 8611 else 8612 { 8613 w = NaN; 8614 } 8615 8616 if (h != null && h.charAt(h.length - 1) != '%') 8617 { 8618 h = parseFloat(h); 8619 } 8620 else 8621 { 8622 h = NaN; 8623 } 8624 8625 // Check if viewBox attribute already exists 8626 var vb = svgRoot.getAttribute('viewBox'); 8627 8628 if (vb == null || vb.length == 0) 8629 { 8630 svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h); 8631 } 8632 // Uses width and height from viewbox for 8633 // missing width and height attributes 8634 else if (isNaN(w) || isNaN(h)) 8635 { 8636 var tokens = vb.split(' '); 8637 8638 if (tokens.length > 3) 8639 { 8640 w = parseFloat(tokens[2]); 8641 h = parseFloat(tokens[3]); 8642 } 8643 } 8644 8645 data = Editor.createSvgDataUri(mxUtils.getXml(svgRoot)); 8646 var s = Math.min(1, Math.min(maxSize / Math.max(1, w)), maxSize / Math.max(1, h)); 8647 var cells = fn(data, file.type, x + index * gs, y + index * gs, Math.max( 8648 1, Math.round(w * s)), Math.max(1, Math.round(h * s)), file.name); 8649 8650 // Hack to fix width and height asynchronously 8651 if (isNaN(w) || isNaN(h)) 8652 { 8653 var img = new Image(); 8654 8655 img.onload = mxUtils.bind(this, function() 8656 { 8657 w = Math.max(1, img.width); 8658 h = Math.max(1, img.height); 8659 8660 cells[0].geometry.width = w; 8661 cells[0].geometry.height = h; 8662 8663 svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h); 8664 data = Editor.createSvgDataUri(mxUtils.getXml(svgRoot)); 8665 8666 var semi = data.indexOf(';'); 8667 8668 if (semi > 0) 8669 { 8670 data = data.substring(0, semi) + data.substring(data.indexOf(',', semi + 1)); 8671 } 8672 8673 graph.setCellStyles('image', data, [cells[0]]); 8674 }); 8675 8676 img.src = Editor.createSvgDataUri(mxUtils.getXml(svgRoot)); 8677 } 8678 8679 return cells; 8680 } 8681 } 8682 } 8683 catch (e) 8684 { 8685 // ignores any SVG parsing errors 8686 } 8687 8688 return null; 8689 })); 8690 } 8691 } 8692 else 8693 { 8694 barrier(index, mxUtils.bind(this, function() 8695 { 8696 return null; 8697 })); 8698 } 8699 } 8700 else 8701 { 8702 // Checks if PNG+XML is available to bypass code below 8703 var containsModel = false; 8704 8705 if (file.type == 'image/png') 8706 { 8707 var xml = (ignoreEmbeddedXml) ? null : this.extractGraphModelFromPng(e.target.result); 8708 8709 if (xml != null && xml.length > 0) 8710 { 8711 var img = new Image(); 8712 img.src = e.target.result; 8713 8714 barrier(index, mxUtils.bind(this, function() 8715 { 8716 return fn(xml, 'text/xml', x + index * gs, y + index * gs, 8717 img.width, img.height, file.name); 8718 })); 8719 8720 containsModel = true; 8721 } 8722 } 8723 8724 // Additional asynchronous step for finding image size 8725 if (!containsModel) 8726 { 8727 // Cannot load local files in Chrome App 8728 if (mxClient.IS_CHROMEAPP) 8729 { 8730 this.spinner.stop(); 8731 this.showError(mxResources.get('error'), mxResources.get('dragAndDropNotSupported'), 8732 mxResources.get('cancel'), mxUtils.bind(this, function() 8733 { 8734 // Hides the dialog 8735 }), null, mxResources.get('ok'), mxUtils.bind(this, function() 8736 { 8737 // Redirects to import function 8738 this.actions.get('import').funct(); 8739 }) 8740 ); 8741 } 8742 else 8743 { 8744 this.loadImage(e.target.result, mxUtils.bind(this, function(img) 8745 { 8746 this.resizeImage(img, e.target.result, mxUtils.bind(this, function(data2, w2, h2) 8747 { 8748 barrier(index, mxUtils.bind(this, function() 8749 { 8750 // Refuses to insert images above a certain size as they kill the app 8751 if (data2 != null && data2.length < maxBytes) 8752 { 8753 var s = (!resizeImages || !this.isResampleImageSize(file.size, resampleThreshold)) ? 1 : Math.min(1, Math.min(maxSize / w2, maxSize / h2)); 8754 8755 return fn(data2, file.type, x + index * gs, y + index * gs, Math.round(w2 * s), Math.round(h2 * s), file.name); 8756 } 8757 else 8758 { 8759 this.handleError({message: mxResources.get('imageTooBig')}); 8760 8761 return null; 8762 } 8763 })); 8764 }), resizeImages, maxSize, resampleThreshold, file.size); 8765 }), mxUtils.bind(this, function() 8766 { 8767 this.handleError({message: mxResources.get('invalidOrMissingFile')}); 8768 })); 8769 } 8770 } 8771 } 8772 } 8773 else 8774 { 8775 var data = e.target.result; 8776 8777 fn(data, file.type, x + index * gs, y + index * gs, 240, 160, file.name, function(cells) 8778 { 8779 barrier(index, function() 8780 { 8781 return cells; 8782 }); 8783 }, file); 8784 } 8785 } 8786 }); 8787 8788 // Handles special cases 8789 if (/(\.v(dx|sdx?))($|\?)/i.test(file.name) || /(\.vs(x|sx?))($|\?)/i.test(file.name)) 8790 { 8791 fn(null, file.type, x + index * gs, y + index * gs, 240, 160, file.name, function(cells) 8792 { 8793 barrier(index, function() 8794 { 8795 return cells; 8796 }); 8797 }, file); 8798 } 8799 else if (file.type.substring(0, 5) == 'image' || file.type == 'application/pdf') 8800 { 8801 reader.readAsDataURL(file); 8802 } 8803 else 8804 { 8805 reader.readAsText(file); 8806 } 8807 } 8808 }))(i); 8809 } 8810 } 8811 }); 8812 8813 if (largeImages) 8814 { 8815 // Workaround for lost files array in async code 8816 var tmp = []; 8817 8818 for (var i = 0; i < files.length; i++) 8819 { 8820 tmp.push(files[i]); 8821 } 8822 8823 files = tmp; 8824 8825 this.confirmImageResize(function(doResize) 8826 { 8827 resizeImages = doResize; 8828 doImportFiles(); 8829 }, resizeDialog); 8830 } 8831 else 8832 { 8833 doImportFiles(); 8834 } 8835 }; 8836 8837 /** 8838 * Parses the file using XHR2 via the server. File can be a blob or file object. 8839 * Filename is an optional parameter for blobs (that do not have a filename). 8840 */ 8841 EditorUi.prototype.confirmImageResize = function(fn, force) 8842 { 8843 force = (force != null) ? force : false; 8844 var resume = (this.spinner != null && this.spinner.pause != null) ? this.spinner.pause() : function() {}; 8845 var resizeImages = (isLocalStorage || mxClient.IS_CHROMEAPP) ? mxSettings.getResizeImages() : null; 8846 8847 var wrapper = function(remember, resize) 8848 { 8849 if (remember || force) 8850 { 8851 mxSettings.setResizeImages((remember) ? resize : null); 8852 mxSettings.save(); 8853 } 8854 8855 resume(); 8856 fn(resize); 8857 }; 8858 8859 if (resizeImages != null && !force) 8860 { 8861 wrapper(false, resizeImages); 8862 } 8863 else 8864 { 8865 this.showDialog(new ConfirmDialog(this, mxResources.get('resizeLargeImages'), 8866 function(remember) 8867 { 8868 wrapper(remember, true); 8869 }, 8870 function(remember) 8871 { 8872 wrapper(remember, false); 8873 }, mxResources.get('resize'), mxResources.get('actualSize'), 8874 '<img style="margin-top:8px;" src="' + Editor.loResImage + '"/>', 8875 '<img style="margin-top:8px;" src="' + Editor.hiResImage + '"/>', 8876 isLocalStorage || mxClient.IS_CHROMEAPP).container, 340, 8877 (isLocalStorage || mxClient.IS_CHROMEAPP) ? 220 : 200, true, true); 8878 } 8879 }; 8880 8881 /** 8882 * Parses the file using XHR2 via the server. File can be a blob or file object. 8883 * Filename is an optional parameter for blobs (that do not have a filename). 8884 */ 8885 EditorUi.prototype.parseFile = function(file, fn, filename) 8886 { 8887 filename = (filename != null) ? filename : file.name; 8888 8889 var formData = new FormData(); 8890 formData.append('format', 'xml'); 8891 formData.append('upfile', file, filename); 8892 8893 var xhr = new XMLHttpRequest(); 8894 xhr.open('POST', OPEN_URL); 8895 8896 xhr.onreadystatechange = function() 8897 { 8898 fn(xhr); 8899 }; 8900 8901 xhr.send(formData); 8902 8903 try 8904 { 8905 EditorUi.logEvent({category: 'GLIFFY-IMPORT-FILE', 8906 action: 'size_' + file.size}); 8907 } 8908 catch (e) 8909 { 8910 // ignore 8911 } 8912 }; 8913 8914 /** 8915 * 8916 */ 8917 EditorUi.prototype.isResampleImageSize = function(size, thresh) 8918 { 8919 thresh = (thresh != null) ? thresh : this.resampleThreshold; 8920 8921 return size > thresh; 8922 }; 8923 8924 /** 8925 * Resizes the given image if <maxImageBytes> is not null. 8926 */ 8927 EditorUi.prototype.resizeImage = function(img, data, fn, enabled, maxSize, thresh, fileSize) 8928 { 8929 maxSize = (maxSize != null) ? maxSize : this.maxImageSize; 8930 var w = Math.max(1, img.width); 8931 var h = Math.max(1, img.height); 8932 8933 if (enabled && this.isResampleImageSize((fileSize != null) ? fileSize : data.length, thresh)) 8934 { 8935 try 8936 { 8937 var factor = Math.max(w / maxSize, h / maxSize); 8938 8939 if (factor > 1) 8940 { 8941 var w2 = Math.round(w / factor); 8942 var h2 = Math.round(h / factor); 8943 8944 var canvas = document.createElement('canvas'); 8945 canvas.width = w2; 8946 canvas.height = h2; 8947 8948 var ctx = canvas.getContext('2d'); 8949 ctx.drawImage(img, 0, 0, w2, h2); 8950 8951 var tmp = canvas.toDataURL(); 8952 8953 // Uses new image if smaller 8954 if (tmp.length < data.length) 8955 { 8956 // Checks if the image is empty by comparing 8957 // with an empty image of the same size 8958 var canvas2 = document.createElement('canvas'); 8959 canvas2.width = w2; 8960 canvas2.height = h2; 8961 var tmp2 = canvas2.toDataURL(); 8962 8963 if (tmp !== tmp2) 8964 { 8965 data = tmp; 8966 w = w2; 8967 h = h2; 8968 } 8969 } 8970 } 8971 } 8972 catch (e) 8973 { 8974 // ignores image scaling errors 8975 } 8976 } 8977 8978 fn(data, w, h); 8979 }; 8980 8981 /** 8982 * Extracts the XML from the compressed or non-compressed text chunk. 8983 */ 8984 EditorUi.prototype.extractGraphModelFromPng = function(data) 8985 { 8986 return Editor.extractGraphModelFromPng(data); 8987 }; 8988 8989 /** 8990 * Loads the image from the given URI. 8991 * 8992 * @param {number} dx X-coordinate of the translation. 8993 * @param {number} dy Y-coordinate of the translation. 8994 */ 8995 EditorUi.prototype.loadImage = function(uri, onload, onerror) 8996 { 8997 try 8998 { 8999 var img = new Image(); 9000 9001 img.onload = function() 9002 { 9003 img.width = (img.width > 0) ? img.width : 120; 9004 img.height = (img.height > 0) ? img.height : 120; 9005 9006 onload(img); 9007 }; 9008 9009 if (onerror != null) 9010 { 9011 img.onerror = onerror; 9012 }; 9013 9014 img.src = uri; 9015 } 9016 catch (e) 9017 { 9018 if (onerror != null) 9019 { 9020 onerror(e); 9021 } 9022 else 9023 { 9024 throw e; 9025 } 9026 } 9027 }; 9028 9029 // Initializes the user interface 9030 var editorUiInit = EditorUi.prototype.init; 9031 EditorUi.prototype.init = function() 9032 { 9033 mxStencilRegistry.allowEval = mxStencilRegistry.allowEval && !this.isOfflineApp(); 9034 9035 // Must be set before UI is created in superclass 9036 if (this.isSettingsEnabled()) 9037 { 9038 if (urlParams['sketch'] == '1') 9039 { 9040 this.doSetSketchMode((mxSettings.settings.sketchMode != null && 9041 urlParams['rough'] == null) ? mxSettings.settings.sketchMode : 9042 urlParams['rough'] != '0'); 9043 } 9044 9045 this.formatWidth = mxSettings.getFormatWidth(); 9046 } 9047 9048 var ui = this; 9049 var graph = this.editor.graph; 9050 9051 if (Editor.isDarkMode()) 9052 { 9053 graph.view.defaultGridColor = mxGraphView.prototype.defaultDarkGridColor; 9054 } 9055 9056 // Starts editing PlantUML data 9057 graph.cellEditor.editPlantUmlData = function(cell, trigger, data) 9058 { 9059 var obj = JSON.parse(data); 9060 9061 var dlg = new TextareaDialog(ui, mxResources.get('plantUml') + ':', 9062 obj.data, function(text) 9063 { 9064 if (text != null) 9065 { 9066 if (ui.spinner.spin(document.body, mxResources.get('inserting'))) 9067 { 9068 ui.generatePlantUmlImage(text, obj.format, function(data, w, h) 9069 { 9070 ui.spinner.stop(); 9071 9072 graph.getModel().beginUpdate(); 9073 try 9074 { 9075 if (obj.format == 'txt') 9076 { 9077 graph.labelChanged(cell, '<pre>' + data + '</pre>'); 9078 graph.updateCellSize(cell, true); 9079 } 9080 else 9081 { 9082 graph.setCellStyles('image', ui.convertDataUri(data), [cell]); 9083 var geo = graph.model.getGeometry(cell); 9084 9085 if (geo != null) 9086 { 9087 geo = geo.clone(); 9088 geo.width = w; 9089 geo.height = h; 9090 graph.cellsResized([cell], [geo], false); 9091 } 9092 } 9093 9094 graph.setAttributeForCell(cell, 'plantUmlData', 9095 JSON.stringify({data: text, format: obj.format})); 9096 } 9097 finally 9098 { 9099 graph.getModel().endUpdate(); 9100 } 9101 }, function(e) 9102 { 9103 ui.handleError(e); 9104 }); 9105 } 9106 } 9107 }, null, null, 400, 220); 9108 ui.showDialog(dlg.container, 420, 300, true, true); 9109 dlg.init(); 9110 }; 9111 9112 // Starts editing Mermaid data 9113 graph.cellEditor.editMermaidData = function(cell, trigger, data) 9114 { 9115 var obj = JSON.parse(data); 9116 9117 var dlg = new TextareaDialog(ui, mxResources.get('mermaid') + ':', 9118 obj.data, function(text) 9119 { 9120 if (text != null) 9121 { 9122 if (ui.spinner.spin(document.body, mxResources.get('inserting'))) 9123 { 9124 ui.generateMermaidImage(text, obj.config, function(data, w, h) 9125 { 9126 ui.spinner.stop(); 9127 9128 graph.getModel().beginUpdate(); 9129 try 9130 { 9131 graph.setCellStyles('image', data, [cell]); 9132 var geo = graph.model.getGeometry(cell); 9133 9134 if (geo != null) 9135 { 9136 geo = geo.clone(); 9137 geo.width = Math.max(geo.width, w); 9138 geo.height = Math.max(geo.height, h); 9139 graph.cellsResized([cell], [geo], false); 9140 } 9141 9142 graph.setAttributeForCell(cell, 'mermaidData', 9143 JSON.stringify({data: text, config: 9144 obj.config}, null, 2)); 9145 } 9146 finally 9147 { 9148 graph.getModel().endUpdate(); 9149 } 9150 }, function(e) 9151 { 9152 ui.handleError(e); 9153 }); 9154 } 9155 } 9156 }, null, null, 400, 220); 9157 ui.showDialog(dlg.container, 420, 300, true, true); 9158 dlg.init(); 9159 }; 9160 9161 // Overrides function to add editing for Plant UML. 9162 var cellEditorStartEditing = graph.cellEditor.startEditing; 9163 graph.cellEditor.startEditing = function(cell, trigger) 9164 { 9165 try 9166 { 9167 var data = this.graph.getAttributeForCell(cell, 'plantUmlData'); 9168 9169 if (data != null) 9170 { 9171 this.editPlantUmlData(cell, trigger, data); 9172 } 9173 else 9174 { 9175 data = this.graph.getAttributeForCell(cell, 'mermaidData'); 9176 9177 if (data != null) 9178 { 9179 this.editMermaidData(cell, trigger, data); 9180 } 9181 else 9182 { 9183 var style = graph.getCellStyle(cell); 9184 9185 if (mxUtils.getValue(style, 'metaEdit', '0') == '1') 9186 { 9187 ui.showDataDialog(cell); 9188 } 9189 else 9190 { 9191 cellEditorStartEditing.apply(this, arguments); 9192 } 9193 } 9194 } 9195 } 9196 catch (e) 9197 { 9198 ui.handleError(e); 9199 } 9200 }; 9201 9202 // Redirects custom link title via UI for page links 9203 graph.getLinkTitle = function(href) 9204 { 9205 return ui.getLinkTitle(href); 9206 }; 9207 9208 // Redirects custom link via UI for page link handling 9209 graph.customLinkClicked = function(link) 9210 { 9211 var done = false; 9212 9213 try 9214 { 9215 ui.handleCustomLink(link); 9216 done = true; 9217 } 9218 catch (e) 9219 { 9220 ui.handleError(e); 9221 } 9222 9223 return done; 9224 }; 9225 9226 // Parses background page references 9227 var graphParseBackgroundImage = graph.parseBackgroundImage; 9228 9229 graph.parseBackgroundImage = function(json) 9230 { 9231 var result = graphParseBackgroundImage.apply(this, arguments); 9232 9233 if (result != null && result.src != null && Graph.isPageLink(result.src)) 9234 { 9235 result = {originalSrc: result.src}; 9236 } 9237 9238 return result; 9239 }; 9240 9241 // Updates background page SVG 9242 var graphSetBackgroundImage = graph.setBackgroundImage; 9243 9244 graph.setBackgroundImage = function(img) 9245 { 9246 if (img != null && img.originalSrc != null) 9247 { 9248 img = ui.createImageForPageLink(img.originalSrc, ui.currentPage, this); 9249 } 9250 9251 graphSetBackgroundImage.apply(this, arguments); 9252 }; 9253 9254 // Updates background to update placeholders for page title 9255 this.editor.addListener('pageRenamed', mxUtils.bind(this, function() 9256 { 9257 graph.refreshBackgroundImage(); 9258 })); 9259 9260 // Updates background to update placeholders for page number 9261 this.editor.addListener('pageMoved', mxUtils.bind(this, function() 9262 { 9263 graph.refreshBackgroundImage(); 9264 })); 9265 9266 // Updates background image after remote changes to the referenced page 9267 this.editor.addListener('pagesPatched', mxUtils.bind(this, function(sender, evt) 9268 { 9269 var ref = (graph.backgroundImage != null) ? graph.backgroundImage.originalSrc : null; 9270 9271 if (ref != null) 9272 { 9273 var comma = ref.indexOf(','); 9274 9275 if (comma > 0) 9276 { 9277 var id = ref.substring(comma + 1); 9278 var patches = evt.getProperty('patches'); 9279 9280 for (var i = 0; i < patches.length; i++) 9281 { 9282 if (patches[i][EditorUi.DIFF_UPDATE] != null && 9283 patches[i][EditorUi.DIFF_UPDATE][id] != null) 9284 { 9285 graph.refreshBackgroundImage(); 9286 9287 break; 9288 } 9289 } 9290 } 9291 } 9292 })); 9293 9294 // Restores background page reference in output data or 9295 // replaces dark mode page image with normal mode image 9296 var graphGetBackgroundImageObject = graph.getBackgroundImageObject; 9297 9298 graph.getBackgroundImageObject = function(obj, resolveReferences) 9299 { 9300 var result = graphGetBackgroundImageObject.apply(this, arguments); 9301 9302 if (result != null && result.originalSrc != null) 9303 { 9304 if (!resolveReferences) 9305 { 9306 result = {src: result.originalSrc}; 9307 } 9308 else if (resolveReferences && this.themes != null && 9309 this.defaultThemeName == 'darkTheme') 9310 { 9311 var temp = this.stylesheet; 9312 this.stylesheet = this.getDefaultStylesheet(); 9313 result = ui.createImageForPageLink(result.originalSrc); 9314 this.stylesheet = temp; 9315 } 9316 } 9317 9318 return result; 9319 }; 9320 9321 // Extends clear default style to clear persisted settings 9322 var clearDefaultStyle = this.clearDefaultStyle; 9323 9324 this.clearDefaultStyle = function() 9325 { 9326 clearDefaultStyle.apply(this, arguments); 9327 }; 9328 9329 // Sets help link for placeholders 9330 if (!this.isOffline() && typeof window.EditDataDialog !== 'undefined') 9331 { 9332 EditDataDialog.placeholderHelpLink = 'https://www.diagrams.net/doc/faq/predefined-placeholders'; 9333 } 9334 9335 if (/viewer\.diagrams\.net$/.test(window.location.hostname) || 9336 /embed\.diagrams\.net$/.test(window.location.hostname)) 9337 { 9338 this.editor.editBlankUrl = 'https://app.diagrams.net/'; 9339 } 9340 9341 // Passes dev mode to new window 9342 var editorGetEditBlankUrl = ui.editor.getEditBlankUrl; 9343 9344 this.editor.getEditBlankUrl = function(params) 9345 { 9346 params = (params != null) ? params : ''; 9347 9348 if (urlParams['dev'] == '1') 9349 { 9350 params += ((params.length > 0) ? '&' : '?') + 'dev=1'; 9351 } 9352 9353 return editorGetEditBlankUrl.apply(this, arguments); 9354 }; 9355 9356 // For chromeless mode and lightbox mode in viewer 9357 // Must be overridden before supercall to be applied 9358 // in case of chromeless initialization 9359 var graphAddClickHandler = graph.addClickHandler; 9360 9361 graph.addClickHandler = function(highlight, beforeClick, onClick) 9362 { 9363 var tmp = beforeClick; 9364 9365 beforeClick = function(evt, href) 9366 { 9367 if (href == null) 9368 { 9369 var source = mxEvent.getSource(evt); 9370 9371 if (source.nodeName.toLowerCase() == 'a') 9372 { 9373 href = source.getAttribute('href'); 9374 } 9375 } 9376 9377 if (href != null && graph.isCustomLink(href) && 9378 (mxEvent.isTouchEvent(evt) || 9379 !mxEvent.isPopupTrigger(evt)) && 9380 graph.customLinkClicked(href)) 9381 { 9382 mxEvent.consume(evt); 9383 } 9384 9385 if (tmp != null) 9386 { 9387 tmp(evt, href); 9388 } 9389 }; 9390 9391 // For some reason, local argument override is not enough in this case... 9392 graphAddClickHandler.call(this, highlight, beforeClick, onClick); 9393 }; 9394 9395 editorUiInit.apply(this, arguments); 9396 9397 if (mxClient.IS_SVG) 9398 { 9399 // LATER: Add shadow for labels in graph.container (eg. math, NO_FO), scaling 9400 this.editor.graph.addSvgShadow(graph.view.canvas.ownerSVGElement, null, true); 9401 } 9402 9403 if (this.menus != null) 9404 { 9405 var menusAddPopupMenuEditItems = Menus.prototype.addPopupMenuEditItems; 9406 9407 // Inserts copyAsImage into popup menu 9408 this.menus.addPopupMenuEditItems = function(menu, cell, evt) 9409 { 9410 if (ui.editor.graph.isSelectionEmpty()) 9411 { 9412 menusAddPopupMenuEditItems.apply(this, arguments); 9413 } 9414 else 9415 { 9416 ui.menus.addMenuItems(menu, ['delete', '-', 'cut', 'copy', 9417 'copyAsImage', '-', 'duplicate'], null, evt); 9418 } 9419 }; 9420 } 9421 9422 // Overrides print dialog size 9423 ui.actions.get('print').funct = function() 9424 { 9425 ui.showDialog(new PrintDialog(ui).container, 360, 9426 (ui.pages != null && ui.pages.length > 1) ? 9427 450 : 370, true, true); 9428 }; 9429 9430 // Specifies the default filename 9431 this.defaultFilename = mxResources.get('untitledDiagram'); 9432 9433 // Adds export for %page%, %pagenumber% and %pagecount% placeholders 9434 var graphGetExportVariables = graph.getExportVariables; 9435 9436 graph.getExportVariables = function() 9437 { 9438 var vars = graphGetExportVariables.apply(this, arguments); 9439 var file = ui.getCurrentFile(); 9440 9441 if (file != null) 9442 { 9443 vars['filename'] = file.getTitle(); 9444 } 9445 9446 vars['pagecount'] = (ui.pages != null) ? ui.pages.length : 1; 9447 vars['page'] = (ui.currentPage != null) ? ui.currentPage.getName() : ''; 9448 vars['pagenumber'] = (ui.pages != null && ui.currentPage != null) ? 9449 mxUtils.indexOf(ui.pages, ui.currentPage) + 1 : 1; 9450 9451 return vars; 9452 }; 9453 9454 // Adds %page%, %pagenumber% and %pagecount% placeholders 9455 var graphGetGlobalVariable = graph.getGlobalVariable; 9456 9457 graph.getGlobalVariable = function(name) 9458 { 9459 var file = ui.getCurrentFile(); 9460 9461 if (name == 'filename' && file != null) 9462 { 9463 return file.getTitle(); 9464 } 9465 else if (name == 'page' && ui.currentPage != null) 9466 { 9467 return ui.currentPage.getName(); 9468 } 9469 else if (name == 'pagenumber') 9470 { 9471 if (ui.currentPage != null && ui.pages != null) 9472 { 9473 return mxUtils.indexOf(ui.pages, ui.currentPage) + 1; 9474 } 9475 else 9476 { 9477 return 1; 9478 } 9479 } 9480 else if (name == 'pagecount') 9481 { 9482 return (ui.pages != null) ? ui.pages.length : 1; 9483 } 9484 9485 return graphGetGlobalVariable.apply(this, arguments); 9486 }; 9487 9488 var graphLabelLinkClicked = graph.labelLinkClicked; 9489 9490 graph.labelLinkClicked = function(state, elt, evt) 9491 { 9492 var href = elt.getAttribute('href'); 9493 9494 if (href != null && graph.isCustomLink(href) && 9495 (mxEvent.isTouchEvent(evt) || 9496 !mxEvent.isPopupTrigger(evt))) 9497 { 9498 // Active links are moved to the hint 9499 if (!graph.isEnabled() || (state != null && graph.isCellLocked(state.cell))) 9500 { 9501 graph.customLinkClicked(href); 9502 9503 // Resets rubberband after click on locked cell 9504 graph.getRubberband().reset(); 9505 } 9506 9507 mxEvent.consume(evt); 9508 } 9509 else 9510 { 9511 graphLabelLinkClicked.apply(this, arguments); 9512 } 9513 }; 9514 9515 // Overrides editor filename 9516 this.editor.getOrCreateFilename = function() 9517 { 9518 var filename = ui.defaultFilename; 9519 var file = ui.getCurrentFile(); 9520 9521 if (file != null) 9522 { 9523 filename = (file.getTitle() != null) ? file.getTitle() : filename; 9524 } 9525 9526 return filename; 9527 }; 9528 9529 // Disables print action for standalone apps on iOS 9530 // because there is no way to close the new window 9531 // LATER: Use iframe for print, disable preview 9532 var printAction = this.actions.get('print'); 9533 printAction.setEnabled(!mxClient.IS_IOS || !navigator.standalone); 9534 printAction.visible = printAction.isEnabled(); 9535 9536 // Installs additional keyboard shortcuts for editor 9537 if (!this.editor.chromeless || this.editor.editable) 9538 { 9539 // Defines additional hotkeys 9540 this.keyHandler.bindAction(70, true, 'findReplace'); // Ctrl+F 9541 this.keyHandler.bindAction(67, true, 'copyStyle', true); // Ctrl+Shift+C 9542 this.keyHandler.bindAction(86, true, 'pasteStyle', true); // Ctrl+Shift+V 9543 this.keyHandler.bindAction(77, true, 'editGeometry', true); // Ctrl+Shift+M 9544 this.keyHandler.bindAction(88, true, 'insertText', true); // Ctrl+Shift+X 9545 this.keyHandler.bindAction(75, true, 'insertRectangle'); // Ctrl+K 9546 this.keyHandler.bindAction(75, true, 'insertEllipse', true); // Ctrl+Shift+K 9547 this.altShiftActions[83] = 'synchronize'; // Alt+Shift+S 9548 9549 this.installImagePasteHandler(); 9550 this.installNativeClipboardHandler(); 9551 }; 9552 9553 // Creates the spinner 9554 this.spinner = this.createSpinner(null, null, 24); 9555 9556 // Installs drag and drop handler for rich text editor 9557 if (Graph.fileSupport) 9558 { 9559 graph.addListener(mxEvent.EDITING_STARTED, mxUtils.bind(this, function(evt) 9560 { 9561 // Setup the dnd listeners 9562 var textElt = graph.cellEditor.text2; 9563 var dropElt = null; 9564 9565 if (textElt != null) 9566 { 9567 mxEvent.addListener(textElt, 'dragleave', function(evt) 9568 { 9569 if (dropElt != null) 9570 { 9571 dropElt.parentNode.removeChild(dropElt); 9572 dropElt = null; 9573 } 9574 9575 evt.stopPropagation(); 9576 evt.preventDefault(); 9577 }); 9578 9579 mxEvent.addListener(textElt, 'dragover', mxUtils.bind(this, function(evt) 9580 { 9581 // IE 10 does not implement pointer-events so it can't have a drop highlight 9582 if (dropElt == null && (!mxClient.IS_IE || document.documentMode > 10)) 9583 { 9584 dropElt = this.highlightElement(textElt); 9585 } 9586 9587 evt.stopPropagation(); 9588 evt.preventDefault(); 9589 })); 9590 9591 mxEvent.addListener(textElt, 'drop', mxUtils.bind(this, function(evt) 9592 { 9593 if (dropElt != null) 9594 { 9595 dropElt.parentNode.removeChild(dropElt); 9596 dropElt = null; 9597 } 9598 9599 if (evt.dataTransfer.files.length > 0) 9600 { 9601 this.importFiles(evt.dataTransfer.files, 0, 0, this.maxImageSize, function(data, mimeType, x, y, w, h) 9602 { 9603 // Inserts image into current text box 9604 graph.insertImage(data, w, h); 9605 }, function() 9606 { 9607 // No post processing 9608 }, function(file) 9609 { 9610 // Handles only images 9611 return file.type.substring(0, 6) == 'image/'; 9612 }, function(queue) 9613 { 9614 // Invokes elements of queue in order 9615 for (var i = 0; i < queue.length; i++) 9616 { 9617 queue[i](); 9618 } 9619 }, mxEvent.isControlDown(evt)); 9620 } 9621 else if (mxUtils.indexOf(evt.dataTransfer.types, 'text/uri-list') >= 0) 9622 { 9623 var uri = evt.dataTransfer.getData('text/uri-list'); 9624 9625 if ((/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(uri)) 9626 { 9627 this.loadImage(decodeURIComponent(uri), mxUtils.bind(this, function(img) 9628 { 9629 var w = Math.max(1, img.width); 9630 var h = Math.max(1, img.height); 9631 var maxSize = this.maxImageSize; 9632 9633 var s = Math.min(1, Math.min(maxSize / Math.max(1, w)), maxSize / Math.max(1, h)); 9634 graph.insertImage(decodeURIComponent(uri), w * s, h * s); 9635 })); 9636 } 9637 else 9638 { 9639 document.execCommand('insertHTML', false, evt.dataTransfer.getData('text/plain')); 9640 } 9641 } 9642 else 9643 { 9644 if (mxUtils.indexOf(evt.dataTransfer.types, 'text/html') >= 0) 9645 { 9646 document.execCommand('insertHTML', false, evt.dataTransfer.getData('text/html')); 9647 } 9648 else if (mxUtils.indexOf(evt.dataTransfer.types, 'text/plain') >= 0) 9649 { 9650 document.execCommand('insertHTML', false, evt.dataTransfer.getData('text/plain')); 9651 } 9652 } 9653 9654 evt.stopPropagation(); 9655 evt.preventDefault(); 9656 })); 9657 } 9658 })); 9659 } 9660 9661 // Adding mxRuler to editor 9662 if (this.isSettingsEnabled()) 9663 { 9664 var view = this.editor.graph.view; 9665 view.setUnit(mxSettings.getUnit()); 9666 9667 view.addListener('unitChanged', function(sender, evt) 9668 { 9669 mxSettings.setUnit(evt.getProperty('unit')); 9670 mxSettings.save(); 9671 }); 9672 9673 var showRuler = this.canvasSupported && document.documentMode != 9 && 9674 (urlParams['ruler'] == '1' || mxSettings.isRulerOn()) && 9675 (!this.editor.isChromelessView() || this.editor.editable); 9676 9677 this.ruler = (showRuler) ? new mxDualRuler(this, view.unit) : null; 9678 this.refresh(); 9679 } 9680 9681 // Adds an element to edit the style in the footer in test mode 9682 if (urlParams['styledev'] == '1') 9683 { 9684 var footer = document.getElementById('geFooter'); 9685 9686 if (footer != null) 9687 { 9688 this.styleInput = document.createElement('input'); 9689 this.styleInput.setAttribute('type', 'text'); 9690 this.styleInput.style.position = 'absolute'; 9691 this.styleInput.style.top = '14px'; 9692 this.styleInput.style.left = '2px'; 9693 // Workaround for ignore right CSS property in FF 9694 this.styleInput.style.width = '98%'; 9695 this.styleInput.style.visibility = 'hidden'; 9696 this.styleInput.style.opacity = '0.9'; 9697 9698 mxEvent.addListener(this.styleInput, 'change', mxUtils.bind(this, function() 9699 { 9700 this.editor.graph.getModel().setStyle(this.editor.graph.getSelectionCell(), this.styleInput.value); 9701 })); 9702 9703 footer.appendChild(this.styleInput); 9704 9705 this.editor.graph.getSelectionModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function(sender, evt) 9706 { 9707 if (this.editor.graph.getSelectionCount() > 0) 9708 { 9709 var cell = this.editor.graph.getSelectionCell(); 9710 var style = this.editor.graph.getModel().getStyle(cell); 9711 9712 this.styleInput.value = style || ''; 9713 this.styleInput.style.visibility = 'visible'; 9714 } 9715 else 9716 { 9717 this.styleInput.style.visibility = 'hidden'; 9718 } 9719 })); 9720 } 9721 9722 var isSelectionAllowed = this.isSelectionAllowed; 9723 this.isSelectionAllowed = function(evt) 9724 { 9725 if (mxEvent.getSource(evt) == this.styleInput) 9726 { 9727 return true; 9728 } 9729 9730 return isSelectionAllowed.apply(this, arguments); 9731 }; 9732 } 9733 9734 // Removes info text in page 9735 var info = document.getElementById('geInfo'); 9736 9737 if (info != null) 9738 { 9739 info.parentNode.removeChild(info); 9740 } 9741 9742 // Installs drag and drop handler for files 9743 // Enables dropping files 9744 if (Graph.fileSupport && (!this.editor.chromeless || this.editor.editable)) 9745 { 9746 // Setup the dnd listeners 9747 var dropElt = null; 9748 9749 mxEvent.addListener(graph.container, 'dragleave', function(evt) 9750 { 9751 if (graph.isEnabled()) 9752 { 9753 if (dropElt != null) 9754 { 9755 dropElt.parentNode.removeChild(dropElt); 9756 dropElt = null; 9757 } 9758 9759 evt.stopPropagation(); 9760 evt.preventDefault(); 9761 } 9762 }); 9763 9764 mxEvent.addListener(graph.container, 'dragover', mxUtils.bind(this, function(evt) 9765 { 9766 // IE 10 does not implement pointer-events so it can't have a drop highlight 9767 if (dropElt == null && (!mxClient.IS_IE || document.documentMode > 10)) 9768 { 9769 dropElt = this.highlightElement(graph.container); 9770 } 9771 9772 if (this.sidebar != null) 9773 { 9774 this.sidebar.hideTooltip(); 9775 } 9776 9777 evt.stopPropagation(); 9778 evt.preventDefault(); 9779 })); 9780 9781 mxEvent.addListener(graph.container, 'drop', mxUtils.bind(this, function(evt) 9782 { 9783 if (dropElt != null) 9784 { 9785 dropElt.parentNode.removeChild(dropElt); 9786 dropElt = null; 9787 } 9788 9789 if (graph.isEnabled()) 9790 { 9791 var pt = mxUtils.convertPoint(graph.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt)); 9792 var tr = graph.view.translate; 9793 var scale = graph.view.scale; 9794 var x = pt.x / scale - tr.x; 9795 var y = pt.y / scale - tr.y; 9796 9797 if (evt.dataTransfer.files.length > 0) 9798 { 9799 if (mxEvent.isShiftDown(evt)) 9800 { 9801 this.openFiles(evt.dataTransfer.files, true); 9802 } 9803 else 9804 { 9805 if (mxEvent.isAltDown(evt)) 9806 { 9807 x = null; 9808 y = null; 9809 } 9810 9811 this.importFiles(evt.dataTransfer.files, x, y, this.maxImageSize, null, null, null, 9812 null, mxEvent.isControlDown(evt), null, null, mxEvent.isShiftDown(evt), evt); 9813 } 9814 } 9815 else 9816 { 9817 if (mxEvent.isAltDown(evt)) 9818 { 9819 x = 0; 9820 y = 0; 9821 } 9822 9823 var uri = (mxUtils.indexOf(evt.dataTransfer.types, 'text/uri-list') >= 0) ? 9824 evt.dataTransfer.getData('text/uri-list') : null; 9825 var data = this.extractGraphModelFromEvent(evt, this.pages != null); 9826 9827 if (data != null) 9828 { 9829 graph.setSelectionCells(this.importXml(data, x, y, true)); 9830 } 9831 else if (mxUtils.indexOf(evt.dataTransfer.types, 'text/html') >= 0) 9832 { 9833 var html = evt.dataTransfer.getData('text/html'); 9834 var div = document.createElement('div'); 9835 div.innerHTML = graph.sanitizeHtml(html); 9836 9837 // The default is based on the extension 9838 var asImage = null; 9839 9840 // Extracts single image 9841 var imgs = div.getElementsByTagName('img'); 9842 9843 if (imgs != null && imgs.length == 1) 9844 { 9845 html = imgs[0].getAttribute('src'); 9846 9847 if (html == null) 9848 { 9849 html = imgs[0].getAttribute('srcset'); 9850 } 9851 9852 // Handles special case where the src attribute has no valid extension 9853 // in which case the text would be inserted as text with a link 9854 if (!(/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(html)) 9855 { 9856 asImage = true; 9857 } 9858 } 9859 else 9860 { 9861 // Extracts single link 9862 var a = div.getElementsByTagName('a'); 9863 9864 if (a != null && a.length == 1) 9865 { 9866 html = a[0].getAttribute('href'); 9867 } 9868 else 9869 { 9870 // Extracts preformatted text 9871 var pre = div.getElementsByTagName('pre'); 9872 9873 if (pre != null && pre.length == 1) 9874 { 9875 html = mxUtils.getTextContent(pre[0]); 9876 } 9877 } 9878 } 9879 9880 var resizeImages = true; 9881 9882 var doInsert = mxUtils.bind(this, function() 9883 { 9884 graph.setSelectionCells(this.insertTextAt(html, x, y, true, 9885 asImage, null, resizeImages, mxEvent.isControlDown(evt))); 9886 }); 9887 9888 if (asImage && html != null && html.length > this.resampleThreshold) 9889 { 9890 this.confirmImageResize(function(doResize) 9891 { 9892 resizeImages = doResize; 9893 doInsert(); 9894 }, mxEvent.isControlDown(evt)); 9895 } 9896 else 9897 { 9898 doInsert(); 9899 } 9900 } 9901 else if (uri != null && (/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(uri)) 9902 { 9903 this.loadImage(decodeURIComponent(uri), mxUtils.bind(this, function(img) 9904 { 9905 var w = Math.max(1, img.width); 9906 var h = Math.max(1, img.height); 9907 var maxSize = this.maxImageSize; 9908 9909 var s = Math.min(1, Math.min(maxSize / Math.max(1, w)), maxSize / Math.max(1, h)); 9910 9911 graph.setSelectionCell(graph.insertVertex(null, null, '', x, y, w * s, h * s, 9912 'shape=image;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;' + 9913 'verticalAlign=top;aspect=fixed;imageAspect=0;image=' + uri + ';')); 9914 }), mxUtils.bind(this, function(img) 9915 { 9916 graph.setSelectionCells(this.insertTextAt(uri, x, y, true)); 9917 })); 9918 } 9919 else if (mxUtils.indexOf(evt.dataTransfer.types, 'text/plain') >= 0) 9920 { 9921 graph.setSelectionCells(this.insertTextAt(evt.dataTransfer.getData('text/plain'), x, y, true)); 9922 } 9923 } 9924 } 9925 9926 evt.stopPropagation(); 9927 evt.preventDefault(); 9928 }), false); 9929 } 9930 9931 graph.enableFlowAnimation = true; 9932 this.initPages(); 9933 9934 // Embedded mode 9935 if (urlParams['embed'] == '1') 9936 { 9937 this.initializeEmbedMode(); 9938 } 9939 9940 this.installSettings(); 9941 }; 9942 9943 /** 9944 * Installs handler for pasting image from clipboard. 9945 */ 9946 EditorUi.prototype.installImagePasteHandler = function() 9947 { 9948 if (!mxClient.IS_IE) 9949 { 9950 var graph = this.editor.graph; 9951 9952 graph.container.addEventListener('paste', mxUtils.bind(this, function(evt) 9953 { 9954 if (!mxEvent.isConsumed(evt)) 9955 { 9956 try 9957 { 9958 var data = (evt.clipboardData || evt.originalEvent.clipboardData); 9959 var containsText = false; 9960 9961 // Workaround for asynchronous paste event processing in textInput 9962 // is to ignore this event if it contains text/html/rtf (see below). 9963 // NOTE: Image is not pasted into textInput so can't listen there. 9964 for (var i = 0; i < data.types.length; i++) 9965 { 9966 if (data.types[i].substring(0, 5) === 'text/') 9967 { 9968 containsText = true; 9969 break; 9970 } 9971 } 9972 9973 if (!containsText) 9974 { 9975 var items = data.items; 9976 9977 for (index in items) 9978 { 9979 var item = items[index]; 9980 9981 if (item.kind === 'file') 9982 { 9983 if (graph.isEditing()) 9984 { 9985 this.importFiles([item.getAsFile()], 0, 0, this.maxImageSize, function(data, mimeType, x, y, w, h) 9986 { 9987 // Inserts image into current text box 9988 graph.insertImage(data, w, h); 9989 }, function() 9990 { 9991 // No post processing 9992 }, function(file) 9993 { 9994 // Handles only images 9995 return file.type.substring(0, 6) == 'image/'; 9996 }, function(queue) 9997 { 9998 // Invokes elements of queue in order 9999 for (var i = 0; i < queue.length; i++) 10000 { 10001 queue[i](); 10002 } 10003 }); 10004 } 10005 else 10006 { 10007 var pt = this.editor.graph.getInsertPoint(); 10008 this.importFiles([item.getAsFile()], pt.x, pt.y, this.maxImageSize); 10009 mxEvent.consume(evt); 10010 } 10011 10012 break; 10013 } 10014 } 10015 } 10016 } 10017 catch (e) 10018 { 10019 // ignore 10020 } 10021 } 10022 }), false); 10023 } 10024 }; 10025 10026 /** 10027 * Installs the native clipboard support. 10028 */ 10029 EditorUi.prototype.installNativeClipboardHandler = function() 10030 { 10031 var graph = this.editor.graph; 10032 10033 // Focused but invisible textarea during control or meta key events 10034 // LATER: Disable text rendering to avoid delay while keeping focus 10035 var textInput = document.createElement('div'); 10036 textInput.setAttribute('autocomplete', 'off'); 10037 textInput.setAttribute('autocorrect', 'off'); 10038 textInput.setAttribute('autocapitalize', 'off'); 10039 textInput.setAttribute('spellcheck', 'false'); 10040 textInput.style.textRendering = 'optimizeSpeed'; 10041 textInput.style.fontFamily = 'monospace'; 10042 textInput.style.wordBreak = 'break-all'; 10043 textInput.style.background = 'transparent'; 10044 textInput.style.color = 'transparent'; 10045 textInput.style.position = 'absolute'; 10046 textInput.style.whiteSpace = 'nowrap'; 10047 textInput.style.overflow = 'hidden'; 10048 textInput.style.display = 'block'; 10049 textInput.style.fontSize = '1'; 10050 textInput.style.zIndex = '-1'; 10051 textInput.style.resize = 'none'; 10052 textInput.style.outline = 'none'; 10053 textInput.style.width = '1px'; 10054 textInput.style.height = '1px'; 10055 mxUtils.setOpacity(textInput, 0); 10056 textInput.contentEditable = true; 10057 textInput.innerHTML = ' '; 10058 10059 var restoreFocus = false; 10060 10061 // Disables built-in cut, copy and paste shortcuts 10062 this.keyHandler.bindControlKey(88, null); 10063 this.keyHandler.bindControlKey(67, null); 10064 this.keyHandler.bindControlKey(86, null); 10065 10066 // Shows a textare when control/cmd is pressed to handle native clipboard actions 10067 mxEvent.addListener(document, 'keydown', mxUtils.bind(this, function(evt) 10068 { 10069 // No dialog visible 10070 var source = mxEvent.getSource(evt); 10071 10072 if (graph.container != null && graph.isEnabled() && !graph.isMouseDown && !graph.isEditing() && 10073 this.dialog == null && source.nodeName != 'INPUT' && source.nodeName != 'TEXTAREA') 10074 { 10075 if (evt.keyCode == 224 /* FF */ || (!mxClient.IS_MAC && evt.keyCode == 17 /* Control */) || 10076 (mxClient.IS_MAC && (evt.keyCode == 91 || evt.keyCode == 93) /* Left/Right Meta */)) 10077 { 10078 // Cannot use parentNode for check in IE 10079 if (!restoreFocus) 10080 { 10081 // Avoid autoscroll but allow handling of all pass-through ctrl shortcuts 10082 textInput.style.left = (graph.container.scrollLeft + 10) + 'px'; 10083 textInput.style.top = (graph.container.scrollTop + 10) + 'px'; 10084 10085 graph.container.appendChild(textInput); 10086 restoreFocus = true; 10087 10088 textInput.focus(); 10089 document.execCommand('selectAll', false, null); 10090 } 10091 } 10092 } 10093 })); 10094 10095 // Clears input and restores focus and selection 10096 function clearInput() 10097 { 10098 window.setTimeout(function() 10099 { 10100 textInput.innerHTML = ' '; 10101 textInput.focus(); 10102 document.execCommand('selectAll', false, null); 10103 }, 0); 10104 }; 10105 10106 mxEvent.addListener(document, 'keyup', mxUtils.bind(this, function(evt) 10107 { 10108 // Workaround for asynchronous event read invalid in IE quirks mode 10109 var keyCode = evt.keyCode; 10110 10111 // Asynchronous workaround for scroll to origin after paste if the 10112 // Ctrl-key is not pressed for long enough in FF on Windows 10113 window.setTimeout(mxUtils.bind(this, function() 10114 { 10115 if (restoreFocus && (keyCode == 224 /* FF */ || keyCode == 17 /* Control */ || 10116 keyCode == 91 /* MetaLeft */ || keyCode == 93 /* MetaRight */)) 10117 { 10118 restoreFocus = false; 10119 10120 if (!graph.isEditing() && this.dialog == null && graph.container != null) 10121 { 10122 graph.container.focus(); 10123 } 10124 10125 textInput.parentNode.removeChild(textInput); 10126 10127 // Workaround for lost cursor in focused element 10128 if (this.dialog == null) 10129 { 10130 mxUtils.clearSelection(); 10131 } 10132 } 10133 }), 0); 10134 })); 10135 10136 mxEvent.addListener(textInput, 'copy', mxUtils.bind(this, function(evt) 10137 { 10138 if (graph.isEnabled()) 10139 { 10140 try 10141 { 10142 mxClipboard.copy(graph); 10143 this.copyCells(textInput); 10144 clearInput(); 10145 } 10146 catch (e) 10147 { 10148 this.handleError(e); 10149 } 10150 } 10151 })); 10152 10153 mxEvent.addListener(textInput, 'cut', mxUtils.bind(this, function(evt) 10154 { 10155 if (graph.isEnabled()) 10156 { 10157 try 10158 { 10159 mxClipboard.copy(graph); 10160 this.copyCells(textInput, true); 10161 clearInput(); 10162 } 10163 catch (e) 10164 { 10165 this.handleError(e); 10166 } 10167 } 10168 })); 10169 10170 mxEvent.addListener(textInput, 'paste', mxUtils.bind(this, function(evt) 10171 { 10172 if (graph.isEnabled() && !graph.isCellLocked(graph.getDefaultParent())) 10173 { 10174 textInput.innerHTML = ' '; 10175 textInput.focus(); 10176 10177 if (evt.clipboardData != null) 10178 { 10179 this.pasteCells(evt, textInput, true, true); 10180 } 10181 10182 if (!mxEvent.isConsumed(evt)) 10183 { 10184 window.setTimeout(mxUtils.bind(this, function() 10185 { 10186 this.pasteCells(evt, textInput, false, true); 10187 }), 0); 10188 } 10189 } 10190 }), true); 10191 10192 // Needed for IE11 10193 var isSelectionAllowed2 = this.isSelectionAllowed; 10194 this.isSelectionAllowed = function(evt) 10195 { 10196 if (mxEvent.getSource(evt) == textInput) 10197 { 10198 return true; 10199 } 10200 10201 return isSelectionAllowed2.apply(this, arguments); 10202 }; 10203 }; 10204 10205 /** 10206 * Overrides image dialog to add image search and Google+. 10207 */ 10208 EditorUi.prototype.setSketchMode = function(value) 10209 { 10210 if (this.spinner.spin(document.body, mxResources.get('working') + '...')) 10211 { 10212 window.setTimeout(mxUtils.bind(this, function() 10213 { 10214 this.spinner.stop(); 10215 this.doSetSketchMode(value); 10216 10217 // Persist setting 10218 if (urlParams['rough'] == null) 10219 { 10220 mxSettings.settings.sketchMode = value; 10221 mxSettings.save(); 10222 } 10223 10224 this.fireEvent(new mxEventObject('sketchModeChanged')); 10225 }), 0); 10226 } 10227 }; 10228 10229 /** 10230 * Changes Editor.pagesVisible. 10231 */ 10232 EditorUi.prototype.setPagesVisible = function(value) 10233 { 10234 if (Editor.pagesVisible != value) 10235 { 10236 Editor.pagesVisible = value; 10237 10238 // Persist setting 10239 mxSettings.settings.pagesVisible = value; 10240 mxSettings.save(); 10241 10242 this.fireEvent(new mxEventObject('pagesVisibleChanged')); 10243 } 10244 }; 10245 10246 /** 10247 * Dynamic change of dark mode. 10248 */ 10249 EditorUi.prototype.setInlineFullscreen = function(value) 10250 { 10251 if (Editor.inlineFullscreen != value) 10252 { 10253 Editor.inlineFullscreen = value; 10254 this.fireEvent(new mxEventObject('inlineFullscreenChanged')); 10255 10256 var parent = window.opener || window.parent; 10257 parent.postMessage(JSON.stringify({ 10258 event: 'resize', 10259 fullscreen: Editor.inlineFullscreen, 10260 rect: this.diagramContainer.getBoundingClientRect() 10261 }), '*'); 10262 10263 window.setTimeout(mxUtils.bind(this, function() 10264 { 10265 this.refresh(); 10266 this.actions.get('resetView').funct(); 10267 }), 10); 10268 } 10269 }; 10270 10271 /** 10272 * Dynamic change of dark mode. 10273 */ 10274 EditorUi.prototype.doSetSketchMode = function(value) 10275 { 10276 if (Editor.sketchMode != value) 10277 { 10278 var graph = this.editor.graph; 10279 Editor.sketchMode = value; 10280 10281 function setStyle(style, key, value) 10282 { 10283 if (style[key] == null) 10284 { 10285 style[key] = value; 10286 } 10287 }; 10288 10289 this.menus.defaultFonts = Menus.prototype.defaultFonts; 10290 this.menus.defaultFontSize = (value) ? 20 : 16; 10291 10292 graph.defaultVertexStyle = mxUtils.clone(Graph.prototype.defaultVertexStyle); 10293 setStyle(graph.defaultVertexStyle, 'fontSize', this.menus.defaultFontSize); 10294 10295 graph.defaultEdgeStyle = mxUtils.clone(Graph.prototype.defaultEdgeStyle); 10296 setStyle(graph.defaultEdgeStyle, 'fontSize', this.menus.defaultFontSize - 4); 10297 setStyle(graph.defaultEdgeStyle, 'edgeStyle', 'none'); 10298 setStyle(graph.defaultEdgeStyle, 'rounded', '0'); 10299 setStyle(graph.defaultEdgeStyle, 'curved', '1'); 10300 setStyle(graph.defaultEdgeStyle, 'jettySize', 'auto'); 10301 setStyle(graph.defaultEdgeStyle, 'orthogonalLoop', '1'); 10302 setStyle(graph.defaultEdgeStyle, 'endArrow', 'open'); 10303 setStyle(graph.defaultEdgeStyle, 'endSize', '14'); 10304 setStyle(graph.defaultEdgeStyle, 'startSize', '14'); 10305 10306 if (value) 10307 { 10308 setStyle(graph.defaultVertexStyle, 'fontFamily', Editor.sketchFontFamily); 10309 setStyle(graph.defaultVertexStyle, 'fontSource', Editor.sketchFontSource); 10310 setStyle(graph.defaultVertexStyle, 'hachureGap', '4'); 10311 setStyle(graph.defaultVertexStyle, 'sketch', '1'); 10312 10313 setStyle(graph.defaultEdgeStyle, 'fontFamily', Editor.sketchFontFamily); 10314 setStyle(graph.defaultEdgeStyle, 'fontSource', Editor.sketchFontSource); 10315 setStyle(graph.defaultEdgeStyle, 'sketch', '1'); 10316 setStyle(graph.defaultEdgeStyle, 'hachureGap', '4'); 10317 setStyle(graph.defaultEdgeStyle, 'sourcePerimeterSpacing', '8'); 10318 setStyle(graph.defaultEdgeStyle, 'targetPerimeterSpacing', '8'); 10319 10320 this.menus.defaultFonts = [{'fontFamily': Editor.sketchFontFamily, 10321 'fontUrl': decodeURIComponent(Editor.sketchFontSource)}, 10322 {'fontFamily': 'Rock Salt', 'fontUrl': 'https://fonts.googleapis.com/css?family=Rock+Salt'}, 10323 {'fontFamily': 'Permanent Marker', 'fontUrl': 'https://fonts.googleapis.com/css?family=Permanent+Marker'}]. 10324 concat(this.menus.defaultFonts); 10325 } 10326 10327 graph.currentVertexStyle = mxUtils.clone(graph.defaultVertexStyle); 10328 graph.currentEdgeStyle = mxUtils.clone(graph.defaultEdgeStyle); 10329 this.clearDefaultStyle(); 10330 } 10331 }; 10332 10333 /** 10334 * 10335 */ 10336 EditorUi.prototype.getLinkTitle = function(href) 10337 { 10338 var title = Graph.prototype.getLinkTitle.apply(this, arguments); 10339 10340 if (Graph.isPageLink(href)) 10341 { 10342 var comma = href.indexOf(','); 10343 10344 if (comma > 0) 10345 { 10346 var page = this.getPageById(href.substring(comma + 1)); 10347 10348 if (page != null) 10349 { 10350 title = page.getName(); 10351 } 10352 else 10353 { 10354 title = mxResources.get('pageNotFound'); 10355 } 10356 } 10357 } 10358 else if (href.substring(0, 5) == 'data:') 10359 { 10360 title = mxResources.get('action'); 10361 } 10362 10363 return title; 10364 }; 10365 10366 /** 10367 * 10368 */ 10369 EditorUi.prototype.handleCustomLink = function(href) 10370 { 10371 if (Graph.isPageLink(href)) 10372 { 10373 var comma = href.indexOf(','); 10374 var page = this.getPageById(href.substring(comma + 1)); 10375 10376 if (page) 10377 { 10378 this.selectPage(page) 10379 } 10380 else 10381 { 10382 // Needs fallback for missing resource in case of viewer lightbox 10383 throw new Error(mxResources.get('pageNotFound') || 'Page not found'); 10384 } 10385 } 10386 else 10387 { 10388 this.editor.graph.handleCustomLink(href); 10389 } 10390 }; 10391 10392 /** 10393 * 10394 */ 10395 EditorUi.prototype.isSettingsEnabled = function() 10396 { 10397 return typeof window.mxSettings !== 'undefined' && (isLocalStorage || mxClient.IS_CHROMEAPP); 10398 }; 10399 10400 /** 10401 * Creates the format panel and adds overrides. 10402 */ 10403 EditorUi.prototype.installSettings = function() 10404 { 10405 if (this.isSettingsEnabled()) 10406 { 10407 // Sets global switch for sketch mode 10408 Editor.pagesVisible = mxSettings.settings.pagesVisible; 10409 10410 // Gets recent colors from settings 10411 ColorDialog.recentColors = mxSettings.getRecentColors(); 10412 10413 // Avoids overridden values for changes in 10414 // multiple windows and updates shared values 10415 if (isLocalStorage) 10416 { 10417 try 10418 { 10419 window.addEventListener('storage', mxUtils.bind(this, function(evt) 10420 { 10421 if (evt.key == mxSettings.key) 10422 { 10423 mxSettings.load(); 10424 10425 // Updates values 10426 ColorDialog.recentColors = mxSettings.getRecentColors(); 10427 this.menus.customFonts = mxSettings.getCustomFonts(); 10428 } 10429 }), false); 10430 } 10431 catch (e) 10432 { 10433 // ignore 10434 } 10435 } 10436 10437 // Updates UI to reflect current edge style 10438 this.fireEvent(new mxEventObject('styleChanged', 'keys', [], 'values', [], 'cells', [])); 10439 10440 /** 10441 * Persists custom fonts. 10442 */ 10443 this.menus.customFonts = mxSettings.getCustomFonts(); 10444 10445 this.addListener('customFontsChanged', mxUtils.bind(this, function(sender, evt) 10446 { 10447 if (urlParams['ext-fonts'] != '1') 10448 { 10449 mxSettings.setCustomFonts(this.menus.customFonts); 10450 } 10451 else 10452 { 10453 var customFonts = evt.getProperty('customFonts'); 10454 this.menus.customFonts = customFonts; 10455 mxSettings.setCustomFonts(customFonts); 10456 } 10457 10458 mxSettings.save(); 10459 })); 10460 10461 /** 10462 * Persists copy on connect switch. 10463 */ 10464 this.editor.graph.connectionHandler.setCreateTarget(mxSettings.isCreateTarget()); 10465 this.fireEvent(new mxEventObject('copyConnectChanged')); 10466 10467 this.addListener('copyConnectChanged', mxUtils.bind(this, function(sender, evt) 10468 { 10469 mxSettings.setCreateTarget(this.editor.graph.connectionHandler.isCreateTarget()); 10470 mxSettings.save(); 10471 })); 10472 10473 /** 10474 * Persists default page format. 10475 */ 10476 this.editor.graph.pageFormat = (this.editor.graph.defaultPageFormat != null) ? 10477 this.editor.graph.defaultPageFormat : mxSettings.getPageFormat(); 10478 10479 this.addListener('pageFormatChanged', mxUtils.bind(this, function(sender, evt) 10480 { 10481 mxSettings.setPageFormat(this.editor.graph.pageFormat); 10482 mxSettings.save(); 10483 })); 10484 10485 /** 10486 * Persists default grid color. 10487 */ 10488 this.editor.graph.view.gridColor = mxSettings.getGridColor(Editor.isDarkMode()); 10489 10490 this.addListener('gridColorChanged', mxUtils.bind(this, function(sender, evt) 10491 { 10492 mxSettings.setGridColor(this.editor.graph.view.gridColor, Editor.isDarkMode()); 10493 mxSettings.save(); 10494 })); 10495 10496 /** 10497 * Persists autosave switch in Chrome app. 10498 */ 10499 if (mxClient.IS_CHROMEAPP || EditorUi.isElectronApp) 10500 { 10501 this.editor.addListener('autosaveChanged', mxUtils.bind(this, function(sender, evt) 10502 { 10503 mxSettings.setAutosave(this.editor.autosave); 10504 mxSettings.save(); 10505 })); 10506 10507 this.editor.autosave = mxSettings.getAutosave(); 10508 } 10509 10510 if (this.sidebar != null) 10511 { 10512 if (urlParams['search-shapes'] != null && this.sidebar.searchShapes != null) 10513 { 10514 this.sidebar.searchShapes(decodeURIComponent(urlParams['search-shapes'])); 10515 this.sidebar.showEntries('search'); 10516 } 10517 else 10518 { 10519 this.sidebar.showPalette('search', mxSettings.settings.search); 10520 10521 /** 10522 * Shows scratchpad if never shown. 10523 */ 10524 if ((!this.editor.chromeless || this.editor.editable) && (mxSettings.settings.isNew || 10525 parseInt(mxSettings.settings.version || 0) <= 8)) 10526 { 10527 this.toggleScratchpad(); 10528 mxSettings.save(); 10529 } 10530 } 10531 } 10532 10533 // Saves app defaults for UI 10534 this.addListener('formatWidthChanged', function() 10535 { 10536 mxSettings.setFormatWidth(this.formatWidth); 10537 mxSettings.save(); 10538 }); 10539 } 10540 }; 10541 10542 /** 10543 * Copies the given cells and XML to the clipboard as an embedded image. 10544 */ 10545 EditorUi.prototype.copyImage = function(cells, xml, scale) 10546 { 10547 try 10548 { 10549 if (navigator.clipboard != null && this.spinner.spin(document.body, mxResources.get('exporting'))) 10550 { 10551 this.editor.exportToCanvas(mxUtils.bind(this, function(canvas, svgRoot) 10552 { 10553 try 10554 { 10555 this.spinner.stop(); 10556 10557 // KNOWN: SVG and delayed content currently not supported 10558 var dataUrl = this.createImageDataUri(canvas, xml, 'png'); 10559 var w = parseInt(svgRoot.getAttribute('width')); 10560 var h = parseInt(svgRoot.getAttribute('height')); 10561 this.writeImageToClipboard(dataUrl, w, h, mxUtils.bind(this, function(e) 10562 { 10563 this.handleError(e); 10564 })); 10565 } 10566 catch (e) 10567 { 10568 this.handleError(e); 10569 } 10570 }), null, null, null, mxUtils.bind(this, function(e) 10571 { 10572 this.spinner.stop(); 10573 this.handleError(e); 10574 }), null, null, (scale != null) ? scale : 4, 10575 this.editor.graph.background == null || 10576 this.editor.graph.background == mxConstants.NONE, 10577 null, null, null, 10, null, null, false, null, 10578 (cells.length > 0) ? cells : null); 10579 } 10580 } 10581 catch (e) 10582 { 10583 this.handleError(e); 10584 } 10585 }; 10586 10587 /** 10588 * Copies the given cells and XML to the clipboard as an embedded image. 10589 */ 10590 EditorUi.prototype.writeImageToClipboard = function(dataUrl, w, h, error) 10591 { 10592 var blob = this.base64ToBlob(dataUrl.substring(dataUrl.indexOf(',') + 1), 'image/png'); 10593 var html = '<img src="' + dataUrl + '" width="' + w + '" height="' + h + '">'; 10594 var cbi = new ClipboardItem({'image/png': blob, 10595 'text/html': new Blob([html], {type: 'text/html'})}); 10596 navigator.clipboard.write([cbi])['catch'](error); 10597 }; 10598 10599 /** 10600 * Creates the format panel and adds overrides. 10601 */ 10602 EditorUi.prototype.copyCells = function(elt, removeCells) 10603 { 10604 var graph = this.editor.graph; 10605 10606 if (!graph.isSelectionEmpty()) 10607 { 10608 // Fixes cross-platform clipboard UTF8 issues by encoding as URI 10609 var cells = mxUtils.sortCells(graph.model.getTopmostCells(graph.getSelectionCells())); 10610 var xml = mxUtils.getXml(graph.encodeCells(cells)); 10611 mxUtils.setTextContent(elt, encodeURIComponent(xml)); 10612 10613 if (removeCells) 10614 { 10615 graph.removeCells(cells, false); 10616 graph.lastPasteXml = null; 10617 } 10618 else 10619 { 10620 graph.lastPasteXml = xml; 10621 graph.pasteCounter = 0; 10622 } 10623 10624 elt.focus(); 10625 document.execCommand('selectAll', false, null); 10626 } 10627 else 10628 { 10629 // Disables copy on focused element 10630 elt.innerHTML = ''; 10631 } 10632 }; 10633 10634 /** 10635 * Creates the format panel and adds overrides. 10636 */ 10637 EditorUi.prototype.copyXml = function() 10638 { 10639 var cells = null; 10640 10641 if (Editor.enableNativeCipboard) 10642 { 10643 var graph = this.editor.graph; 10644 10645 if (!graph.isSelectionEmpty()) 10646 { 10647 cells = mxUtils.sortCells(graph.getExportableCells( 10648 graph.model.getTopmostCells(graph.getSelectionCells()))); 10649 var xml = mxUtils.getXml(graph.encodeCells(cells)); 10650 navigator.clipboard.writeText(xml); 10651 } 10652 } 10653 10654 return cells; 10655 }; 10656 10657 /** 10658 * Creates the format panel and adds overrides. 10659 */ 10660 EditorUi.prototype.pasteXml = function(xml, pasteAsLabel, compat, evt) 10661 { 10662 var graph = this.editor.graph; 10663 var cells = null; 10664 10665 if (graph.lastPasteXml == xml) 10666 { 10667 graph.pasteCounter++; 10668 } 10669 else 10670 { 10671 graph.lastPasteXml = xml; 10672 graph.pasteCounter = 0; 10673 } 10674 10675 var dx = graph.pasteCounter * graph.gridSize; 10676 10677 if (compat || this.isCompatibleString(xml)) 10678 { 10679 cells = this.importXml(xml, dx, dx); 10680 graph.setSelectionCells(cells); 10681 } 10682 else if (pasteAsLabel && graph.getSelectionCount() == 1) 10683 { 10684 var cell = graph.getStartEditingCell(graph.getSelectionCell(), evt); 10685 10686 if ((/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(xml) && 10687 graph.getCurrentCellStyle(cell)[mxConstants.STYLE_SHAPE] == 'image') 10688 { 10689 graph.setCellStyles(mxConstants.STYLE_IMAGE, xml, [cell]); 10690 } 10691 else 10692 { 10693 graph.model.beginUpdate(); 10694 try 10695 { 10696 graph.labelChanged(cell, xml); 10697 10698 if (Graph.isLink(xml)) 10699 { 10700 graph.setLinkForCell(cell, xml); 10701 } 10702 } 10703 finally 10704 { 10705 graph.model.endUpdate(); 10706 } 10707 } 10708 10709 graph.setSelectionCell(cell); 10710 } 10711 else 10712 { 10713 var pt = graph.getInsertPoint(); 10714 10715 if (graph.isMouseInsertPoint()) 10716 { 10717 dx = 0; 10718 10719 // No offset for insert at mouse position 10720 if (graph.lastPasteXml == xml && graph.pasteCounter > 0) 10721 { 10722 graph.pasteCounter--; 10723 } 10724 } 10725 10726 cells = this.insertTextAt(xml, pt.x + dx, pt.y + dx, true); 10727 graph.setSelectionCells(cells); 10728 } 10729 10730 if (!graph.isSelectionEmpty()) 10731 { 10732 graph.scrollCellToVisible(graph.getSelectionCell()); 10733 10734 if (this.hoverIcons != null) 10735 { 10736 this.hoverIcons.update(graph.view.getState(graph.getSelectionCell())); 10737 } 10738 } 10739 10740 return cells; 10741 }; 10742 10743 /** 10744 * Creates the format panel and adds overrides. 10745 */ 10746 EditorUi.prototype.pasteCells = function(evt, realElt, useEvent, pasteAsLabel) 10747 { 10748 if (!mxEvent.isConsumed(evt)) 10749 { 10750 var elt = realElt; 10751 var asHtml = false; 10752 10753 if (useEvent && evt.clipboardData != null && evt.clipboardData.getData) 10754 { 10755 // Workaround for paste from IE11 where the page is copied 10756 // as HTML while the data is only available via text/plain 10757 var plain = evt.clipboardData.getData('text/plain'); 10758 var override = false; 10759 10760 if (plain != null && plain.length > 0 && plain.substring(0, 18) == '%3CmxGraphModel%3E') 10761 { 10762 var tmp = decodeURIComponent(plain); 10763 10764 if (this.isCompatibleString(tmp)) 10765 { 10766 override = true; 10767 plain = tmp; 10768 } 10769 } 10770 10771 var data = (!override) ? evt.clipboardData.getData('text/html') : null; 10772 10773 if (data != null && data.length > 0) 10774 { 10775 elt = this.parseHtmlData(data); 10776 asHtml = elt.getAttribute('data-type') != 'text/plain'; 10777 } 10778 else if (plain != null && plain.length > 0) 10779 { 10780 elt = document.createElement('div'); 10781 mxUtils.setTextContent(elt, data); 10782 } 10783 } 10784 10785 var spans = elt.getElementsByTagName('span'); 10786 10787 if (spans != null && spans.length > 0 && spans[0].getAttribute('data-lucid-type') === 10788 'application/vnd.lucid.chart.objects') 10789 { 10790 var content = spans[0].getAttribute('data-lucid-content'); 10791 10792 if (content != null && content.length > 0) 10793 { 10794 this.convertLucidChart(content, mxUtils.bind(this, function(xml) 10795 { 10796 var graph = this.editor.graph; 10797 10798 if (graph.lastPasteXml == xml) 10799 { 10800 graph.pasteCounter++; 10801 } 10802 else 10803 { 10804 graph.lastPasteXml = xml; 10805 graph.pasteCounter = 0; 10806 } 10807 10808 var dx = graph.pasteCounter * graph.gridSize; 10809 graph.setSelectionCells(this.importXml(xml, dx, dx)); 10810 graph.scrollCellToVisible(graph.getSelectionCell()); 10811 }), mxUtils.bind(this, function(e) 10812 { 10813 this.handleError(e); 10814 })); 10815 10816 mxEvent.consume(evt); 10817 } 10818 } 10819 else 10820 { 10821 // KNOWN: Paste from IE11 to other browsers on Windows 10822 // seems to paste the contents of index.html 10823 var xml = (asHtml) ? elt.innerHTML : 10824 mxUtils.trim((elt.innerText == null) ? 10825 mxUtils.getTextContent(elt) : elt.innerText); 10826 var compat = false; 10827 10828 // Workaround for junk after XML in VM 10829 try 10830 { 10831 var idx = xml.lastIndexOf('%3E'); 10832 10833 if (idx >= 0 && idx < xml.length - 3) 10834 { 10835 xml = xml.substring(0, idx + 3); 10836 } 10837 } 10838 catch (e) 10839 { 10840 // ignore 10841 } 10842 10843 // Checks for embedded XML content 10844 try 10845 { 10846 var spans = elt.getElementsByTagName('span'); 10847 var tmp = (spans != null && spans.length > 0) ? 10848 mxUtils.trim(decodeURIComponent(spans[0].textContent)) : 10849 decodeURIComponent(xml); 10850 10851 if (this.isCompatibleString(tmp)) 10852 { 10853 compat = true; 10854 xml = tmp; 10855 } 10856 } 10857 catch (e) 10858 { 10859 // ignore 10860 } 10861 10862 try 10863 { 10864 if (xml != null && xml.length > 0) 10865 { 10866 this.pasteXml(xml, pasteAsLabel, compat, evt); 10867 10868 try 10869 { 10870 mxEvent.consume(evt); 10871 } 10872 catch (e) 10873 { 10874 // ignore event no longer exists in async handler in IE8- 10875 } 10876 } 10877 else if (!useEvent) 10878 { 10879 var graph = this.editor.graph; 10880 10881 graph.lastPasteXml = null; 10882 graph.pasteCounter = 0; 10883 } 10884 } 10885 catch (e) 10886 { 10887 this.handleError(e); 10888 } 10889 } 10890 } 10891 10892 realElt.innerHTML = ' '; 10893 }; 10894 10895 /** 10896 * Adds a file drop handler for opening local files. 10897 */ 10898 EditorUi.prototype.addFileDropHandler = function(elts) 10899 { 10900 // Installs drag and drop handler for files 10901 if (Graph.fileSupport) 10902 { 10903 var dropElt = null; 10904 10905 for (var i = 0; i < elts.length; i++) 10906 { 10907 // Setup the dnd listeners 10908 mxEvent.addListener(elts[i], 'dragleave', function(evt) 10909 { 10910 if (dropElt != null) 10911 { 10912 dropElt.parentNode.removeChild(dropElt); 10913 dropElt = null; 10914 } 10915 10916 evt.stopPropagation(); 10917 evt.preventDefault(); 10918 }); 10919 10920 mxEvent.addListener(elts[i], 'dragover', mxUtils.bind(this, function(evt) 10921 { 10922 if (this.editor.graph.isEnabled() || urlParams['embed'] != '1') 10923 { 10924 // IE 10 does not implement pointer-events so it can't have a drop highlight 10925 if (dropElt == null && (!mxClient.IS_IE || (document.documentMode > 10 && document.documentMode < 12))) 10926 { 10927 dropElt = this.highlightElement(); 10928 } 10929 } 10930 10931 evt.stopPropagation(); 10932 evt.preventDefault(); 10933 })); 10934 10935 mxEvent.addListener(elts[i], 'drop', mxUtils.bind(this, function(evt) 10936 { 10937 if (dropElt != null) 10938 { 10939 dropElt.parentNode.removeChild(dropElt); 10940 dropElt = null; 10941 } 10942 10943 if (this.editor.graph.isEnabled() || urlParams['embed'] != '1') 10944 { 10945 if (evt.dataTransfer.files.length > 0) 10946 { 10947 this.hideDialog(); 10948 10949 // Never open files in embed mode 10950 if (urlParams['embed'] == '1') 10951 { 10952 this.importFiles(evt.dataTransfer.files, 0, 0, this.maxImageSize, null, null, 10953 null, null, !mxEvent.isControlDown(evt) && !mxEvent.isShiftDown(evt)); 10954 } 10955 else 10956 { 10957 this.openFiles(evt.dataTransfer.files, true); 10958 } 10959 } 10960 else 10961 { 10962 // Handles open special files via text drag and drop 10963 var data = this.extractGraphModelFromEvent(evt); 10964 10965 // Tries additional and async parsing of text content such as HTML, Gliffy data 10966 if (data == null) 10967 { 10968 var provider = (evt.dataTransfer != null) ? evt.dataTransfer : evt.clipboardData; 10969 10970 if (provider != null) 10971 { 10972 if (document.documentMode == 10 || document.documentMode == 11) 10973 { 10974 data = provider.getData('Text'); 10975 } 10976 else 10977 { 10978 var data = null; 10979 10980 if (mxUtils.indexOf(provider.types, 'text/uri-list') >= 0) 10981 { 10982 data = evt.dataTransfer.getData('text/uri-list'); 10983 } 10984 else 10985 { 10986 data = (mxUtils.indexOf(provider.types, 'text/html') >= 0) ? provider.getData('text/html') : null; 10987 } 10988 10989 if (data != null && data.length > 0) 10990 { 10991 var div = document.createElement('div'); 10992 div.innerHTML = this.editor.graph.sanitizeHtml(data); 10993 10994 // Extracts single image 10995 var imgs = div.getElementsByTagName('img'); 10996 10997 if (imgs.length > 0) 10998 { 10999 data = imgs[0].getAttribute('src'); 11000 } 11001 } 11002 else if (mxUtils.indexOf(provider.types, 'text/plain') >= 0) 11003 { 11004 data = provider.getData('text/plain'); 11005 } 11006 } 11007 11008 if (data != null) 11009 { 11010 // Checks for embedded XML in PNG 11011 if (data.substring(0, 22) == 'data:image/png;base64,') 11012 { 11013 var xml = this.extractGraphModelFromPng(data); 11014 11015 if (xml != null && xml.length > 0) 11016 { 11017 this.openLocalFile(xml, null, true); 11018 } 11019 } 11020 else if (!this.isOffline() && this.isRemoteFileFormat(data)) 11021 { 11022 new mxXmlRequest(OPEN_URL, 'format=xml&data=' + encodeURIComponent(data)).send(mxUtils.bind(this, function(req) 11023 { 11024 if (req.getStatus() >= 200 && req.getStatus() <= 299) 11025 { 11026 this.openLocalFile(req.getText(), null, true); 11027 } 11028 })); 11029 } 11030 else if (/^https?:\/\//.test(data)) 11031 { 11032 if (this.getCurrentFile() == null) 11033 { 11034 window.location.hash = '#U' + encodeURIComponent(data); 11035 } 11036 else 11037 { 11038 window.openWindow(((mxClient.IS_CHROMEAPP) ? 11039 (EditorUi.drawHost + '/') : 'https://' + location.host + '/') + 11040 window.location.search + '#U' + encodeURIComponent(data)); 11041 } 11042 } 11043 } 11044 } 11045 } 11046 else 11047 { 11048 this.openLocalFile(data, null, true); 11049 } 11050 } 11051 } 11052 11053 evt.stopPropagation(); 11054 evt.preventDefault(); 11055 })); 11056 } 11057 } 11058 }; 11059 11060 /** 11061 * Highlights the given element 11062 */ 11063 EditorUi.prototype.highlightElement = function(elt) 11064 { 11065 var x = 0; 11066 var y = 0; 11067 var w = 0; 11068 var h = 0; 11069 11070 if (elt == null) 11071 { 11072 var b = document.body; 11073 var d = document.documentElement; 11074 11075 w = (b.clientWidth || d.clientWidth) - 3; 11076 h = Math.max(b.clientHeight || 0, d.clientHeight) - 3; 11077 } 11078 else 11079 { 11080 x = elt.offsetTop; 11081 y = elt.offsetLeft; 11082 w = elt.clientWidth; 11083 h = elt.clientHeight; 11084 } 11085 11086 var hl = document.createElement('div'); 11087 hl.style.zIndex = mxPopupMenu.prototype.zIndex + 2; 11088 hl.style.border = '3px dotted rgb(254, 137, 12)'; 11089 hl.style.pointerEvents = 'none'; 11090 hl.style.position = 'absolute'; 11091 hl.style.top = x + 'px'; 11092 hl.style.left = y + 'px'; 11093 hl.style.width = Math.max(0, w - 3) + 'px'; 11094 hl.style.height = Math.max(0, h - 3) + 'px'; 11095 11096 if (elt != null && elt.parentNode == this.editor.graph.container) 11097 { 11098 this.editor.graph.container.appendChild(hl); 11099 } 11100 else 11101 { 11102 document.body.appendChild(hl); 11103 } 11104 11105 return hl; 11106 }; 11107 11108 /** 11109 * Highlights the given element 11110 */ 11111 EditorUi.prototype.stringToCells = function(xml) 11112 { 11113 var doc = mxUtils.parseXml(xml); 11114 var node = this.editor.extractGraphModel(doc.documentElement); 11115 var cells = []; 11116 11117 if (node != null) 11118 { 11119 var codec = new mxCodec(node.ownerDocument); 11120 var model = new mxGraphModel(); 11121 codec.decode(node, model); 11122 11123 var parent = model.getChildAt(model.getRoot(), 0); 11124 11125 for (var j = 0; j < model.getChildCount(parent); j++) 11126 { 11127 cells.push(model.getChildAt(parent, j)); 11128 } 11129 } 11130 11131 return cells; 11132 }; 11133 11134 /** 11135 * Opens the given files in the editor. 11136 */ 11137 EditorUi.prototype.openFileHandle = function(data, name, file, temp, fileHandle) 11138 { 11139 if (name != null && name.length > 0) 11140 { 11141 if (!this.useCanvasForExport && /(\.png)$/i.test(name)) 11142 { 11143 name = name.substring(0, name.length - 4) + '.drawio'; 11144 } 11145 else if (/(\.pdf)$/i.test(name)) 11146 { 11147 name = name.substring(0, name.length - 4) + '.drawio'; 11148 } 11149 11150 var handleResult = mxUtils.bind(this, function(xml) 11151 { 11152 var dot = name.lastIndexOf('.'); 11153 11154 if (dot >= 0) 11155 { 11156 name = name.substring(0, name.lastIndexOf('.')) + '.drawio'; 11157 } 11158 else 11159 { 11160 name = name + '.drawio'; 11161 } 11162 11163 if (xml.substring(0, 10) == '<mxlibrary') 11164 { 11165 // Creates new temporary file if library is dropped in splash screen 11166 if (this.getCurrentFile() == null && urlParams['embed'] != '1') 11167 { 11168 this.openLocalFile(this.emptyDiagramXml, this.defaultFilename, temp); 11169 } 11170 11171 try 11172 { 11173 this.loadLibrary(new LocalLibrary(this, xml, name)); 11174 } 11175 catch (e) 11176 { 11177 this.handleError(e, mxResources.get('errorLoadingFile')); 11178 } 11179 } 11180 else 11181 { 11182 this.openLocalFile(xml, name, temp); 11183 } 11184 }); 11185 11186 if (/(\.v(dx|sdx?))($|\?)/i.test(name) || /(\.vs(x|sx?))($|\?)/i.test(name)) 11187 { 11188 this.importVisio(file, mxUtils.bind(this, function(xml) 11189 { 11190 this.spinner.stop(); 11191 handleResult(xml); 11192 })); 11193 } 11194 else if (/(\.*<graphml )/.test(data)) 11195 { 11196 this.importGraphML(data, mxUtils.bind(this, function(xml) 11197 { 11198 this.spinner.stop(); 11199 handleResult(xml); 11200 })); 11201 } 11202 else if (Graph.fileSupport && !this.isOffline() && new XMLHttpRequest().upload && 11203 this.isRemoteFileFormat(data, name)) 11204 { 11205 this.parseFile(file, mxUtils.bind(this, function(xhr) 11206 { 11207 if (xhr.readyState == 4) 11208 { 11209 this.spinner.stop(); 11210 11211 if (xhr.status >= 200 && xhr.status <= 299) 11212 { 11213 handleResult(xhr.responseText); 11214 } 11215 else 11216 { 11217 this.handleError({message: mxResources.get((xhr.status == 413) ? 11218 'drawingTooLarge' : 'invalidOrMissingFile')}, 11219 mxResources.get('errorLoadingFile')); 11220 } 11221 } 11222 })); 11223 } 11224 else if (this.isLucidChartData(data)) 11225 { 11226 if (/(\.json)$/i.test(name)) 11227 { 11228 name = name.substring(0, name.length - 5) + '.drawio'; 11229 } 11230 11231 // LATER: Add import step that produces cells and use callback 11232 this.convertLucidChart(data, mxUtils.bind(this, function(xml) 11233 { 11234 this.spinner.stop(); 11235 this.openLocalFile(xml, name, temp); 11236 }), mxUtils.bind(this, function(e) 11237 { 11238 this.spinner.stop(); 11239 this.handleError(e); 11240 })); 11241 } 11242 else if (data.substring(0, 10) == '<mxlibrary') 11243 { 11244 this.spinner.stop(); 11245 11246 // Creates new temporary file if library is dropped in splash screen 11247 if (this.getCurrentFile() == null && urlParams['embed'] != '1') 11248 { 11249 this.openLocalFile(this.emptyDiagramXml, this.defaultFilename, temp); 11250 } 11251 11252 try 11253 { 11254 this.loadLibrary(new LocalLibrary(this, data, file.name)); 11255 } 11256 catch (e) 11257 { 11258 this.handleError(e, mxResources.get('errorLoadingFile')); 11259 } 11260 } 11261 else if (data.indexOf('PK') == 0) 11262 { 11263 this.importZipFile(file, mxUtils.bind(this, function(xml) 11264 { 11265 this.spinner.stop(); 11266 handleResult(xml); 11267 }), mxUtils.bind(this, function() 11268 { 11269 this.spinner.stop(); 11270 this.openLocalFile(data, name, temp); 11271 })); 11272 } 11273 else 11274 { 11275 if (file.type.substring(0, 9) == 'image/png') 11276 { 11277 data = this.extractGraphModelFromPng(data); 11278 } 11279 else if (file.type == 'application/pdf') 11280 { 11281 var xml = Editor.extractGraphModelFromPdf(data); 11282 11283 if (xml != null) 11284 { 11285 fileHandle = null; 11286 temp = true; 11287 data = xml; 11288 } 11289 } 11290 11291 this.spinner.stop(); 11292 this.openLocalFile(data, name, temp, fileHandle, (fileHandle != null) ? file : null); 11293 } 11294 } 11295 }; 11296 11297 /** 11298 * Opens the given files in the editor. 11299 */ 11300 EditorUi.prototype.openFiles = function(files, temp) 11301 { 11302 if (this.spinner.spin(document.body, mxResources.get('loading'))) 11303 { 11304 for (var i = 0; i < files.length; i++) 11305 { 11306 (mxUtils.bind(this, function(file) 11307 { 11308 var reader = new FileReader(); 11309 11310 reader.onload = mxUtils.bind(this, function(e) 11311 { 11312 try 11313 { 11314 this.openFileHandle(e.target.result, file.name, file, temp); 11315 } 11316 catch (e) 11317 { 11318 this.handleError(e); 11319 } 11320 }); 11321 11322 reader.onerror = mxUtils.bind(this, function(e) 11323 { 11324 this.spinner.stop(); 11325 this.handleError(e); 11326 window.openFile = null; 11327 }); 11328 11329 if ((file.type.substring(0, 5) === 'image' || 11330 file.type === 'application/pdf') && 11331 file.type.substring(0, 9) !== 'image/svg') 11332 { 11333 reader.readAsDataURL(file); 11334 } 11335 else 11336 { 11337 reader.readAsText(file); 11338 } 11339 }))(files[i]); 11340 } 11341 } 11342 }; 11343 11344 /** 11345 * Shows the layers dialog if the graph has more than one layer. 11346 */ 11347 EditorUi.prototype.openLocalFile = function(data, name, temp, fileHandle, desc) 11348 { 11349 var currentFile = this.getCurrentFile(); 11350 11351 var fn = mxUtils.bind(this, function() 11352 { 11353 window.openFile = null; 11354 11355 if (name == null && this.getCurrentFile() != null && this.isDiagramEmpty()) 11356 { 11357 var doc = mxUtils.parseXml(data); 11358 11359 if (doc != null) 11360 { 11361 this.editor.setGraphXml(doc.documentElement); 11362 this.editor.graph.selectAll(); 11363 } 11364 } 11365 else 11366 { 11367 this.fileLoaded(new LocalFile(this, data, name || 11368 this.defaultFilename, temp, fileHandle, desc)); 11369 } 11370 }); 11371 11372 if (data != null && data.length > 0) 11373 { 11374 if (currentFile == null || (!currentFile.isModified() && 11375 (mxClient.IS_CHROMEAPP || EditorUi.isElectronApp || fileHandle != null))) 11376 { 11377 fn(); 11378 } 11379 else if ((mxClient.IS_CHROMEAPP || EditorUi.isElectronApp || fileHandle != null) && 11380 currentFile != null && currentFile.isModified()) 11381 { 11382 this.confirm(mxResources.get('allChangesLost'), null, fn, 11383 mxResources.get('cancel'), mxResources.get('discardChanges')); 11384 } 11385 else 11386 { 11387 window.openFile = new OpenFile(function() 11388 { 11389 window.openFile = null; 11390 }); 11391 11392 window.openFile.setData(data, name); 11393 window.openWindow(this.getUrl(), null, mxUtils.bind(this, function() 11394 { 11395 if (currentFile != null && currentFile.isModified()) 11396 { 11397 this.confirm(mxResources.get('allChangesLost'), null, fn, 11398 mxResources.get('cancel'), mxResources.get('discardChanges')); 11399 } 11400 else 11401 { 11402 fn(); 11403 } 11404 })); 11405 } 11406 } 11407 else 11408 { 11409 throw new Error(mxResources.get('notADiagramFile')); 11410 } 11411 }; 11412 11413 /** 11414 * Returns a list of all shapes used in the current file. 11415 */ 11416 EditorUi.prototype.getBasenames = function() 11417 { 11418 var basenames = {}; 11419 11420 if (this.pages != null) 11421 { 11422 for (var i = 0; i < this.pages.length; i++) 11423 { 11424 this.updatePageRoot(this.pages[i]); 11425 this.addBasenamesForCell(this.pages[i].root, basenames); 11426 } 11427 } 11428 else 11429 { 11430 this.addBasenamesForCell(this.editor.graph.model.getRoot(), basenames); 11431 } 11432 11433 var result = []; 11434 11435 for (var key in basenames) 11436 { 11437 result.push(key); 11438 } 11439 11440 return result; 11441 }; 11442 11443 /** 11444 * Returns a list of all shapes used in the current file. 11445 */ 11446 EditorUi.prototype.addBasenamesForCell = function(cell, basenames) 11447 { 11448 function addName(name) 11449 { 11450 if (name != null) 11451 { 11452 // LATER: Check if this case exists 11453 var dot = name.lastIndexOf('.'); 11454 11455 if (dot > 0) 11456 { 11457 name = name.substring(dot + 1, name.length); 11458 } 11459 11460 if (basenames[name] == null) 11461 { 11462 basenames[name] = true; 11463 } 11464 } 11465 }; 11466 11467 var graph = this.editor.graph; 11468 var style = graph.getCellStyle(cell); 11469 var shape = style[mxConstants.STYLE_SHAPE]; 11470 addName(mxStencilRegistry.getBasenameForStencil(shape)); 11471 11472 // Adds package names for markers in edges 11473 if (graph.model.isEdge(cell)) 11474 { 11475 addName(mxMarker.getPackageForType(style[mxConstants.STYLE_STARTARROW])); 11476 addName(mxMarker.getPackageForType(style[mxConstants.STYLE_ENDARROW])); 11477 } 11478 11479 var childCount = graph.model.getChildCount(cell); 11480 11481 for (var i = 0; i < childCount; i++) 11482 { 11483 this.addBasenamesForCell(graph.model.getChildAt(cell, i), basenames); 11484 } 11485 }; 11486 11487 /** 11488 * Shows the layers dialog if the graph has more than one layer. 11489 */ 11490 EditorUi.prototype.setGraphEnabled = function(enabled) 11491 { 11492 this.diagramContainer.style.visibility = (enabled) ? '' : 'hidden'; 11493 this.formatContainer.style.visibility = (enabled) ? '' : 'hidden'; 11494 this.sidebarFooterContainer.style.display = (enabled) ? '' : 'none'; 11495 this.sidebarContainer.style.display = (enabled) ? '' : 'none'; 11496 this.hsplit.style.display = (enabled) ? '' : 'none'; 11497 this.editor.graph.setEnabled(enabled); 11498 11499 if (this.ruler != null) 11500 { 11501 this.ruler.hRuler.container.style.visibility = (enabled) ? '' : 'hidden'; 11502 this.ruler.vRuler.container.style.visibility = (enabled) ? '' : 'hidden'; 11503 } 11504 11505 if (this.tabContainer != null) 11506 { 11507 this.tabContainer.style.visibility = (enabled) ? '' : 'hidden'; 11508 } 11509 11510 if (!enabled) 11511 { 11512 if (this.actions.outlineWindow != null) 11513 { 11514 this.actions.outlineWindow.window.setVisible(false); 11515 } 11516 11517 if (this.actions.layersWindow != null) 11518 { 11519 this.actions.layersWindow.window.setVisible(false); 11520 } 11521 11522 if (this.menus.tagsWindow != null) 11523 { 11524 this.menus.tagsWindow.window.setVisible(false); 11525 } 11526 11527 if (this.menus.findWindow != null) 11528 { 11529 this.menus.findWindow.window.setVisible(false); 11530 } 11531 11532 if (this.menus.findReplaceWindow != null) 11533 { 11534 this.menus.findReplaceWindow.window.setVisible(false); 11535 } 11536 } 11537 }; 11538 11539 /** 11540 * Shows the layers dialog if the graph has more than one layer. 11541 */ 11542 EditorUi.prototype.initializeEmbedMode = function() 11543 { 11544 this.setGraphEnabled(false); 11545 var parent = window.opener || window.parent; 11546 11547 if (parent != window) 11548 { 11549 if (urlParams['spin'] != '1' || this.spinner.spin(document.body, mxResources.get('loading'))) 11550 { 11551 var initialized = false; 11552 11553 this.installMessageHandler(mxUtils.bind(this, function(xml, evt, modified, convertToSketch) 11554 { 11555 if (!initialized) 11556 { 11557 initialized = true; 11558 11559 this.spinner.stop(); 11560 this.addEmbedButtons(); 11561 this.setGraphEnabled(true); 11562 } 11563 11564 if (xml == null || xml.length == 0) 11565 { 11566 xml = this.emptyDiagramXml; 11567 } 11568 11569 // Creates temporary file for diff sync in embed mode 11570 this.setCurrentFile(new EmbedFile(this, xml, {})); 11571 this.mode = App.MODE_EMBED; 11572 this.setFileData(xml); 11573 11574 // TODO: Check if cellsInserted should be fired instead here 11575 if (convertToSketch) 11576 { 11577 try 11578 { 11579 //Disable grid and page view 11580 var graph = this.editor.graph; 11581 graph.setGridEnabled(false); 11582 graph.pageVisible = false; 11583 var cells = graph.model.cells; 11584 11585 //Add sketch style and font to all cells 11586 for (var id in cells) 11587 { 11588 var cell = cells[id]; 11589 11590 if (cell != null && cell.style != null) 11591 { 11592 cell.style += ';sketch=1;' + (cell.style.indexOf('fontFamily=') == -1 || cell.style.indexOf('fontFamily=Helvetica;') > -1? 11593 'fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;' : ''); 11594 } 11595 } 11596 } 11597 catch(e) 11598 { 11599 console.log(e); //Ignore 11600 } 11601 } 11602 11603 if (!this.editor.isChromelessView()) 11604 { 11605 this.showLayersDialog(); 11606 } 11607 else if (this.editor.graph.isLightboxView()) 11608 { 11609 this.lightboxFit(); 11610 } 11611 11612 if (this.chromelessResize) 11613 { 11614 this.chromelessResize(); 11615 } 11616 11617 this.editor.undoManager.clear(); 11618 this.editor.modified = (modified != null) ? modified : false; 11619 this.updateUi(); 11620 11621 // Workaround for no initial focus in FF 11622 // (does not work in Conf Cloud with FF) 11623 if (window.self !== window.top) 11624 { 11625 window.focus(); 11626 } 11627 11628 if (this.format != null) 11629 { 11630 this.format.refresh(); 11631 } 11632 })); 11633 } 11634 } 11635 }; 11636 11637 /** 11638 * Shows the layers dialog if the graph has more than one layer. 11639 */ 11640 EditorUi.prototype.showLayersDialog = function() 11641 { 11642 if (this.editor.graph.getModel().getChildCount(this.editor.graph.getModel().getRoot()) > 1) 11643 { 11644 if (this.actions.layersWindow == null) 11645 { 11646 this.actions.get('layers').funct(); 11647 } 11648 else 11649 { 11650 this.actions.layersWindow.window.setVisible(true); 11651 } 11652 } 11653 }; 11654 11655 /** 11656 * Tries to find a public URL for the given file. 11657 */ 11658 EditorUi.prototype.getPublicUrl = function(file, fn) 11659 { 11660 if (file != null) 11661 { 11662 file.getPublicUrl(fn); 11663 } 11664 else 11665 { 11666 fn(null); 11667 } 11668 }; 11669 11670 /** 11671 * Adds the buttons for embedded mode. 11672 */ 11673 EditorUi.prototype.createLoadMessage = function(eventName) 11674 { 11675 var graph = this.editor.graph; 11676 11677 return {event: eventName, pageVisible: graph.pageVisible, translate: graph.view.translate, 11678 bounds: graph.getGraphBounds(), currentPage: this.getSelectedPageIndex(), 11679 scale: graph.view.scale, page: graph.view.getBackgroundPageBounds()}; 11680 }; 11681 11682 /** 11683 * Adds the buttons for embedded mode. 11684 */ 11685 EditorUi.prototype.sendEmbeddedSvgExport = function(noExit) 11686 { 11687 var graph = this.editor.graph; 11688 11689 if (graph.isEditing()) 11690 { 11691 graph.stopEditing(!graph.isInvokesStopCellEditing()); 11692 } 11693 11694 var parent = window.opener || window.parent; 11695 11696 if (!this.editor.modified) 11697 { 11698 if (!noExit) 11699 { 11700 parent.postMessage(JSON.stringify({event: 'exit', 11701 point: this.embedExitPoint}), '*'); 11702 } 11703 } 11704 else 11705 { 11706 var bg = graph.background; 11707 11708 if (bg == null || bg == mxConstants.NONE) 11709 { 11710 bg = this.embedExportBackground; 11711 } 11712 11713 this.getEmbeddedSvg(this.getFileData(true, null, null, null, null, 11714 null, null, null, null, false), graph, null, true, 11715 mxUtils.bind(this, function(svg) 11716 { 11717 parent.postMessage(JSON.stringify({ 11718 event: 'export', point: this.embedExitPoint, 11719 exit: (noExit != null) ? !noExit : true, 11720 data: Editor.createSvgDataUri(svg) 11721 }), '*'); 11722 }), null, null, true, bg, 1, this.embedExportBorder); 11723 } 11724 11725 if (!noExit) 11726 { 11727 this.diagramContainer.removeAttribute('data-bounds'); 11728 Editor.inlineFullscreen = false; 11729 graph.model.clear(); 11730 this.editor.undoManager.clear(); 11731 this.setBackgroundImage(null); 11732 this.editor.modified = false; 11733 11734 this.fireEvent(new mxEventObject('editInlineStop')); 11735 } 11736 }; 11737 11738 /** 11739 * Adds the buttons for embedded mode. 11740 */ 11741 EditorUi.prototype.installMessageHandler = function(fn) 11742 { 11743 var changeListener = null; 11744 var ignoreChange = false; 11745 var autosave = false; 11746 var lastData = null; 11747 11748 var updateStatus = mxUtils.bind(this, function(sender, eventObject) 11749 { 11750 if (!this.editor.modified || urlParams['modified'] == '0') 11751 { 11752 this.editor.setStatus(''); 11753 } 11754 else if (urlParams['modified'] != null) 11755 { 11756 this.editor.setStatus(mxUtils.htmlEntities(mxResources.get(urlParams['modified']))); 11757 } 11758 }); 11759 11760 this.editor.graph.model.addListener(mxEvent.CHANGE, updateStatus); 11761 11762 // Receives XML message from opener and puts it into the graph 11763 mxEvent.addListener(window, 'message', mxUtils.bind(this, function(evt) 11764 { 11765 var validSource = window.opener || window.parent; 11766 11767 if (evt.source != validSource) 11768 { 11769 return; 11770 } 11771 11772 var data = evt.data; 11773 var afterLoad = null; 11774 11775 var extractDiagramXml = mxUtils.bind(this, function(data) 11776 { 11777 if (data != null && typeof data.charAt === 'function' && data.charAt(0) != '<') 11778 { 11779 try 11780 { 11781 if (data.substring(0, 22) == 'data:image/png;base64,') 11782 { 11783 data = this.extractGraphModelFromPng(data); 11784 } 11785 else if (data.substring(0, 26) == 'data:image/svg+xml;base64,') 11786 { 11787 data = atob(data.substring(26)); 11788 } 11789 else if (data.substring(0, 24) == 'data:image/svg+xml;utf8,') 11790 { 11791 data = data.substring(24); 11792 } 11793 11794 if (data != null) 11795 { 11796 if (data.charAt(0) == '%') 11797 { 11798 data = decodeURIComponent(data); 11799 } 11800 else if (data.charAt(0) != '<') 11801 { 11802 data = Graph.decompress(data); 11803 } 11804 } 11805 } 11806 catch (e) 11807 { 11808 // ignore compression errors and use empty data 11809 } 11810 } 11811 11812 return data; 11813 }); 11814 11815 if (urlParams['proto'] == 'json') 11816 { 11817 var convertToSketch = false; 11818 11819 try 11820 { 11821 data = JSON.parse(data); 11822 } 11823 catch (e) 11824 { 11825 data = null; 11826 } 11827 11828 try 11829 { 11830 if (data == null) 11831 { 11832 // Ignore 11833 return; 11834 } 11835 else if (data.action == 'dialog') 11836 { 11837 this.showError((data.titleKey != null) ? mxResources.get(data.titleKey) : data.title, 11838 (data.messageKey != null) ? mxResources.get(data.messageKey) : data.message, 11839 (data.buttonKey != null) ? mxResources.get(data.buttonKey) : data.button); 11840 11841 if (data.modified != null) 11842 { 11843 this.editor.modified = data.modified; 11844 } 11845 11846 return; 11847 } 11848 else if (data.action == 'layout') 11849 { 11850 this.executeLayoutList(data.layouts) 11851 11852 return; 11853 } 11854 else if (data.action == 'prompt') 11855 { 11856 this.spinner.stop(); 11857 11858 var dlg = new FilenameDialog(this, data.defaultValue || '', 11859 (data.okKey != null) ? mxResources.get(data.okKey) : data.ok, function(value) 11860 { 11861 if (value != null) 11862 { 11863 parent.postMessage(JSON.stringify({event: 'prompt', value: value, message: data}), '*'); 11864 } 11865 else 11866 { 11867 parent.postMessage(JSON.stringify({event: 'prompt-cancel', message: data}), '*'); 11868 } 11869 }, (data.titleKey != null) ? mxResources.get(data.titleKey) : data.title); 11870 this.showDialog(dlg.container, 300, 80, true, false); 11871 dlg.init(); 11872 11873 return; 11874 } 11875 else if (data.action == 'draft') 11876 { 11877 var tmp = extractDiagramXml(data.xml); 11878 this.spinner.stop(); 11879 11880 var dlg = new DraftDialog(this, mxResources.get('draftFound', 11881 [data.name || this.defaultFilename]), 11882 tmp, mxUtils.bind(this, function() 11883 { 11884 this.hideDialog(); 11885 parent.postMessage(JSON.stringify({event: 'draft', 11886 result: 'edit', message: data}), '*'); 11887 }), mxUtils.bind(this, function() 11888 { 11889 this.hideDialog(); 11890 parent.postMessage(JSON.stringify({event: 'draft', 11891 result: 'discard', message: data}), '*'); 11892 }), (data.editKey) ? mxResources.get(data.editKey) : null, 11893 (data.discardKey) ? mxResources.get(data.discardKey) : null, 11894 (data.ignore) ? mxUtils.bind(this, function() 11895 { 11896 this.hideDialog(); 11897 parent.postMessage(JSON.stringify({event: 'draft', 11898 result: 'ignore', message: data}), '*'); 11899 }) : null); 11900 this.showDialog(dlg.container, 640, 480, true, false, mxUtils.bind(this, function(cancel) 11901 { 11902 if (cancel) 11903 { 11904 this.actions.get('exit').funct(); 11905 } 11906 })); 11907 11908 try 11909 { 11910 dlg.init(); 11911 } 11912 catch (e) 11913 { 11914 parent.postMessage(JSON.stringify({event: 'draft', 11915 error: e.toString(), message: data}), '*'); 11916 } 11917 11918 return; 11919 } 11920 else if (data.action == 'template') 11921 { 11922 this.spinner.stop(); 11923 11924 var enableRecentDocs = data.enableRecent == 1; 11925 var enableSearchDocs = data.enableSearch == 1; 11926 var enableCustomTemp = data.enableCustomTemp == 1; 11927 11928 if (urlParams['newTempDlg'] == '1' && !data.templatesOnly && data.callback != null) 11929 { 11930 var user = this.getCurrentUser(); 11931 11932 var tempDlg = new TemplatesDialog(this, function(xml, filename, itemInfo) 11933 { 11934 xml = xml || this.emptyDiagramXml; 11935 11936 parent.postMessage(JSON.stringify({event: 'template', xml: xml, 11937 blank: xml == this.emptyDiagramXml, name: filename, 11938 tempUrl: itemInfo.url, libs: itemInfo.libs, 11939 builtIn: itemInfo.info != null && itemInfo.info.custContentId != null, 11940 message: data}), '*'); 11941 }, mxUtils.bind(this, function() 11942 { 11943 this.actions.get('exit').funct(); 11944 }), null, null, user != null? user.id : null, 11945 enableRecentDocs? mxUtils.bind(this, function(recentReadyCallback, error, username) 11946 { 11947 this.remoteInvoke('getRecentDiagrams', [username], null, recentReadyCallback, error); 11948 }) : null, enableSearchDocs? mxUtils.bind(this, function(searchStr, searchReadyCallback, error, username) 11949 { 11950 this.remoteInvoke('searchDiagrams', [searchStr, username], null, searchReadyCallback, error); 11951 }) : null, mxUtils.bind(this, function(obj, callback, error) 11952 { 11953 this.remoteInvoke('getFileContent', [obj.url], null, callback, error); 11954 }), null, enableCustomTemp? mxUtils.bind(this, function(customTempCallback) 11955 { 11956 this.remoteInvoke('getCustomTemplates', null, null, customTempCallback, function() 11957 { 11958 customTempCallback({}, 0); //ignore error by sending empty templates 11959 }); 11960 }) : null, false, false, true, true); 11961 11962 this.showDialog(tempDlg.container, window.innerWidth, window.innerHeight, true, false, null, false, true); 11963 11964 return; 11965 } 11966 11967 var dlg = new NewDialog(this, false, data.templatesOnly? false : data.callback != null, 11968 mxUtils.bind(this, function(xml, name, url, libs) 11969 { 11970 xml = xml || this.emptyDiagramXml; 11971 11972 // LATER: Add autosave option in template message 11973 if (data.callback != null) 11974 { 11975 parent.postMessage(JSON.stringify({event: 'template', xml: xml, 11976 blank: xml == this.emptyDiagramXml, name: name, 11977 tempUrl: url, libs: libs, builtIn: true, 11978 message: data}), '*'); 11979 } 11980 else 11981 { 11982 fn(xml, evt, xml != this.emptyDiagramXml, data.toSketch); 11983 11984 // Workaround for status updated before modified applied 11985 if (!this.editor.modified) 11986 { 11987 this.editor.setStatus(''); 11988 } 11989 } 11990 }), null, null, null, null, null, null, null, 11991 enableRecentDocs? mxUtils.bind(this, function(recentReadyCallback) 11992 { 11993 this.remoteInvoke('getRecentDiagrams', [null], null, recentReadyCallback, function() 11994 { 11995 recentReadyCallback(null, 'Network Error!'); 11996 }); 11997 }) : null, 11998 enableSearchDocs? mxUtils.bind(this, function(searchStr, searchReadyCallback) 11999 { 12000 this.remoteInvoke('searchDiagrams', [searchStr, null], null, searchReadyCallback, function() 12001 { 12002 searchReadyCallback(null, 'Network Error!'); 12003 }); 12004 }) : null, 12005 mxUtils.bind(this, function(url, info, name) 12006 { 12007 //If binary files are possible, we can get the file content using remote invokation, imported it, and send final mxFile back 12008 parent.postMessage(JSON.stringify({event: 'template', docUrl: url, info: info, 12009 name: name}), '*'); 12010 }), null, null, 12011 enableCustomTemp? mxUtils.bind(this, function(customTempCallback) 12012 { 12013 this.remoteInvoke('getCustomTemplates', null, null, customTempCallback, function() 12014 { 12015 customTempCallback({}, 0); //ignore error by sending empty templates 12016 }); 12017 }) : null, data.withoutType == 1); 12018 12019 this.showDialog(dlg.container, 620, 460, true, false, mxUtils.bind(this, function(cancel) 12020 { 12021 this.sidebar.hideTooltip(); 12022 12023 if (cancel) 12024 { 12025 this.actions.get('exit').funct(); 12026 } 12027 })); 12028 dlg.init(); 12029 12030 return; 12031 } 12032 else if (data.action == 'textContent') 12033 { 12034 //TODO Remove this message and use remote invokation instead 12035 var allPagesTxt = this.getDiagramTextContent(); 12036 parent.postMessage(JSON.stringify({event: 'textContent', 12037 data: allPagesTxt, message: data}), '*'); 12038 return; 12039 } 12040 else if (data.action == 'status') 12041 { 12042 if (data.messageKey != null) 12043 { 12044 this.editor.setStatus(mxUtils.htmlEntities(mxResources.get(data.messageKey))); 12045 } 12046 else if (data.message != null) 12047 { 12048 this.editor.setStatus(mxUtils.htmlEntities(data.message)); 12049 } 12050 12051 if (data.modified != null) 12052 { 12053 this.editor.modified = data.modified; 12054 } 12055 12056 return; 12057 } 12058 else if (data.action == 'spinner') 12059 { 12060 var msg = (data.messageKey != null) ? mxResources.get(data.messageKey) : data.message; 12061 12062 if (data.show != null && !data.show) 12063 { 12064 this.spinner.stop(); 12065 } 12066 else 12067 { 12068 this.spinner.spin(document.body, msg) 12069 } 12070 12071 return; 12072 } 12073 else if (data.action == 'exit') 12074 { 12075 this.actions.get('exit').funct(); 12076 12077 return; 12078 } 12079 else if (data.action == 'viewport') 12080 { 12081 if (data.viewport != null) 12082 { 12083 this.embedViewport = data.viewport; 12084 } 12085 12086 return; 12087 } 12088 else if (data.action == 'snapshot') 12089 { 12090 this.sendEmbeddedSvgExport(true); 12091 12092 return; 12093 } 12094 else if (data.action == 'export') 12095 { 12096 if (data.format == 'png' || data.format == 'xmlpng') 12097 { 12098 if ((data.spin == null && data.spinKey == null) || this.spinner.spin(document.body, 12099 (data.spinKey != null) ? mxResources.get(data.spinKey) : data.spin)) 12100 { 12101 var xml = (data.xml != null) ? data.xml : this.getFileData(true); 12102 this.editor.graph.setEnabled(false); 12103 var graph = this.editor.graph; 12104 12105 var postDataBack = mxUtils.bind(this, function(uri) 12106 { 12107 this.editor.graph.setEnabled(true); 12108 this.spinner.stop(); 12109 12110 var msg = this.createLoadMessage('export'); 12111 msg.format = data.format; 12112 msg.message = data; 12113 msg.data = uri; 12114 msg.xml = xml; 12115 parent.postMessage(JSON.stringify(msg), '*'); 12116 }); 12117 12118 var processUri = mxUtils.bind(this, function(uri) 12119 { 12120 if (uri == null) 12121 { 12122 uri = Editor.blankImage; 12123 } 12124 12125 if (data.format == 'xmlpng') 12126 { 12127 uri = Editor.writeGraphModelToPng(uri, 'tEXt', 'mxfile', 12128 encodeURIComponent(xml)); 12129 } 12130 12131 // Removes temporary graph from DOM 12132 if (graph != this.editor.graph) 12133 { 12134 graph.container.parentNode.removeChild(graph.container); 12135 } 12136 12137 postDataBack(uri); 12138 }); 12139 12140 var pageId = data.pageId || (this.pages != null? ((data.currentPage) ? 12141 this.currentPage.getId() : this.pages[0].getId()) : null); 12142 12143 if (this.isExportToCanvas()) 12144 { 12145 var graphReady = mxUtils.bind(this, function() 12146 { 12147 // Exports PNG for first/specific page while other page is visible by creating a graph 12148 // LATER: Add caching for the graph or SVG while not on first page 12149 if (this.pages != null && this.currentPage.getId() != pageId) 12150 { 12151 var graphGetGlobalVariable = graph.getGlobalVariable; 12152 graph = this.createTemporaryGraph(graph.getStylesheet()); 12153 var page; 12154 12155 for (var i = 0; i < this.pages.length; i++) 12156 { 12157 if (this.pages[i].getId() == pageId) 12158 { 12159 page = this.updatePageRoot(this.pages[i]); 12160 break; 12161 } 12162 } 12163 12164 //If pageId info is incorrect 12165 if (page == null) 12166 { 12167 page = this.currentPage; 12168 } 12169 12170 graph.getGlobalVariable = function(name) 12171 { 12172 if (name == 'page') 12173 { 12174 return page.getName(); 12175 } 12176 else if (name == 'pagenumber') 12177 { 12178 return 1; 12179 } 12180 12181 return graphGetGlobalVariable.apply(this, arguments); 12182 }; 12183 12184 document.body.appendChild(graph.container); 12185 graph.model.setRoot(page.root); 12186 } 12187 12188 // Set visible layers based on message setting 12189 if (data.layerIds != null) 12190 { 12191 var graphModel = graph.model; 12192 var layers = graphModel.getChildCells(graphModel.getRoot()); 12193 var layerIdsMap = {}; 12194 12195 for (var i = 0; i < data.layerIds.length; i++) 12196 { 12197 layerIdsMap[data.layerIds[i]] = true; 12198 } 12199 12200 for (var i = 0; i < layers.length; i++) 12201 { 12202 graphModel.setVisible(layers[i], layerIdsMap[layers[i].id] || false); 12203 } 12204 } 12205 12206 this.editor.exportToCanvas(mxUtils.bind(this, function(canvas) 12207 { 12208 processUri(canvas.toDataURL('image/png')); 12209 }), data.width, null, data.background, mxUtils.bind(this, function() 12210 { 12211 processUri(null); 12212 }), null, null, data.scale, data.transparent, data.shadow, null, 12213 graph, data.border, null, data.grid, data.keepTheme); 12214 }); 12215 12216 // Uses optional XML from incoming message 12217 if (data.xml != null && data.xml.length > 0) 12218 { 12219 ignoreChange = true; 12220 this.setFileData(xml); 12221 ignoreChange = false; 12222 12223 if (this.editor.graph.mathEnabled) 12224 { 12225 window.setTimeout(function() 12226 { 12227 window.MathJax.Hub.Queue(graphReady); 12228 }, 0); 12229 } 12230 else 12231 { 12232 graphReady(); 12233 } 12234 } 12235 else 12236 { 12237 graphReady(); 12238 } 12239 } 12240 else 12241 { 12242 // Data from server is base64 encoded to avoid binary XHR 12243 // Double encoding for XML arg is needed for UTF8 encoding 12244 var req = new mxXmlRequest(EXPORT_URL, 'format=png&embedXml=' + 12245 ((data.format == 'xmlpng') ? '1' : '0') + 12246 (pageId != null? '&pageId=' + pageId : '') + 12247 (data.layerIds != null && data.layerIds.length > 0? 12248 '&extras=' + encodeURIComponent(JSON.stringify({layerIds: data.layerIds})) : '') + 12249 (data.scale != null? '&scale=' + data.scale : '') +'&base64=1&xml=' + 12250 encodeURIComponent(xml)); 12251 12252 req.send(mxUtils.bind(this, function(req) 12253 { 12254 // Temp graph was never created at this point so we can 12255 // skip processUri since it already contains the XML 12256 if (req.getStatus() >= 200 && req.getStatus() <= 299) 12257 { 12258 postDataBack('data:image/png;base64,' + req.getText()); 12259 } 12260 else 12261 { 12262 processUri(null); 12263 } 12264 }), mxUtils.bind(this, function() 12265 { 12266 processUri(null); 12267 })); 12268 } 12269 } 12270 } 12271 else 12272 { 12273 var graphReady = mxUtils.bind(this, function() 12274 { 12275 var msg = this.createLoadMessage('export'); 12276 12277 // Attaches incoming message 12278 msg.message = data; 12279 12280 // Forces new HTML format if pages exists 12281 if (data.format == 'html2' || (data.format == 'html' && (urlParams['pages'] != '0' || 12282 (this.pages != null && this.pages.length > 1)))) 12283 { 12284 var node = this.getXmlFileData(); 12285 msg.xml = mxUtils.getXml(node); 12286 msg.data = this.getFileData(null, null, true, null, null, null, node); 12287 msg.format = data.format; 12288 } 12289 else if (data.format == 'html') 12290 { 12291 var xml = this.editor.getGraphXml(); 12292 msg.data = this.getHtml(xml, this.editor.graph); 12293 msg.xml = mxUtils.getXml(xml); 12294 msg.format = data.format; 12295 } 12296 else 12297 { 12298 // Creates a preview with no alt text for unsupported browsers 12299 mxSvgCanvas2D.prototype.foAltText = null; 12300 12301 var bg = (data.background != null) ? data.background : this.editor.graph.background; 12302 12303 if (bg == mxConstants.NONE) 12304 { 12305 bg = null; 12306 } 12307 12308 msg.xml = this.getFileData(true, null, null, null, null, 12309 null, null, null, null, false); 12310 msg.format = 'svg'; 12311 12312 var postResult = mxUtils.bind(this, function(svg) 12313 { 12314 this.editor.graph.setEnabled(true); 12315 this.spinner.stop(); 12316 12317 msg.data = Editor.createSvgDataUri(svg); 12318 parent.postMessage(JSON.stringify(msg), '*'); 12319 }); 12320 12321 if (data.format == 'xmlsvg') 12322 { 12323 if ((data.spin == null && data.spinKey == null) || this.spinner.spin(document.body, 12324 (data.spinKey != null) ? mxResources.get(data.spinKey) : data.spin)) 12325 { 12326 this.getEmbeddedSvg(msg.xml, this.editor.graph, null, true, postResult, null, null, 12327 data.embedImages, bg, data.scale, data.border, data.shadow, data.keepTheme); 12328 } 12329 } 12330 else 12331 { 12332 if ((data.spin == null && data.spinKey == null) || this.spinner.spin(document.body, 12333 (data.spinKey != null) ? mxResources.get(data.spinKey) : data.spin)) 12334 { 12335 this.editor.graph.setEnabled(false); 12336 var svgRoot = this.editor.graph.getSvg(bg, data.scale, data.border, null, null, 12337 null, null, null, null, this.editor.graph.shadowVisible || data.shadow, 12338 null, data.keepTheme); 12339 12340 if (this.editor.graph.shadowVisible || data.shadow) 12341 { 12342 this.editor.graph.addSvgShadow(svgRoot); 12343 } 12344 12345 this.embedFonts(svgRoot, mxUtils.bind(this, function(svgRoot) 12346 { 12347 if (data.embedImages || data.embedImages == null) 12348 { 12349 this.editor.convertImages(svgRoot, mxUtils.bind(this, function(svgRoot) 12350 { 12351 postResult(mxUtils.getXml(svgRoot)); 12352 })); 12353 } 12354 else 12355 { 12356 postResult(mxUtils.getXml(svgRoot)); 12357 } 12358 })); 12359 } 12360 } 12361 12362 return; 12363 } 12364 12365 parent.postMessage(JSON.stringify(msg), '*'); 12366 }); 12367 12368 // SVG is generated from graph so parse optional XML 12369 if (data.xml != null && data.xml.length > 0) 12370 { 12371 ignoreChange = true; 12372 this.setFileData(data.xml); 12373 ignoreChange = false; 12374 12375 if (this.editor.graph.mathEnabled) 12376 { 12377 window.setTimeout(function() 12378 { 12379 window.MathJax.Hub.Queue(graphReady); 12380 }, 0); 12381 } 12382 else 12383 { 12384 graphReady(); 12385 } 12386 } 12387 else 12388 { 12389 graphReady(); 12390 } 12391 } 12392 12393 return; 12394 } 12395 else if (data.action == 'load') 12396 { 12397 convertToSketch = data.toSketch; 12398 autosave = data.autosave == 1; 12399 this.hideDialog(); 12400 12401 if (data.modified != null && urlParams['modified'] == null) 12402 { 12403 urlParams['modified'] = data.modified; 12404 } 12405 12406 if (data.saveAndExit != null && urlParams['saveAndExit'] == null) 12407 { 12408 urlParams['saveAndExit'] = data.saveAndExit; 12409 } 12410 12411 if (data.noSaveBtn != null && urlParams['noSaveBtn'] == null) 12412 { 12413 urlParams['noSaveBtn'] = data.noSaveBtn; 12414 } 12415 12416 if (data.rough != null) 12417 { 12418 var initial = Editor.sketchMode; 12419 this.doSetSketchMode(data.rough); 12420 12421 if (initial != Editor.sketchMode) 12422 { 12423 this.fireEvent(new mxEventObject('sketchModeChanged')); 12424 } 12425 } 12426 12427 if (data.dark != null) 12428 { 12429 var initial = Editor.darkMode; 12430 this.doSetDarkMode(data.dark); 12431 12432 if (initial != Editor.darkMode) 12433 { 12434 this.fireEvent(new mxEventObject('darkModeChanged')); 12435 } 12436 } 12437 12438 if (data.border != null) 12439 { 12440 this.embedExportBorder = data.border; 12441 } 12442 12443 if (data.background != null) 12444 { 12445 this.embedExportBackground = data.background; 12446 } 12447 12448 if (data.viewport != null) 12449 { 12450 this.embedViewport = data.viewport; 12451 } 12452 12453 this.embedExitPoint = null; 12454 12455 if (data.rect != null) 12456 { 12457 var border = this.embedExportBorder; 12458 12459 this.diagramContainer.style.border = '2px solid #295fcc'; 12460 this.diagramContainer.style.top = data.rect.top + 'px'; 12461 this.diagramContainer.style.left = data.rect.left + 'px'; 12462 this.diagramContainer.style.height = data.rect.height + 'px'; 12463 this.diagramContainer.style.width = data.rect.width + 'px'; 12464 this.diagramContainer.style.bottom = ''; 12465 this.diagramContainer.style.right = ''; 12466 12467 afterLoad = mxUtils.bind(this, function() 12468 { 12469 var graph = this.editor.graph; 12470 var prev = graph.maxFitScale; 12471 graph.maxFitScale = data.maxFitScale; 12472 graph.fit(2 * border); 12473 graph.maxFitScale = prev; 12474 graph.container.scrollTop -= 2 * border; 12475 graph.container.scrollLeft -= 2 * border; 12476 this.fireEvent(new mxEventObject('editInlineStart', 'data', [data])); 12477 }); 12478 } 12479 12480 if (data.noExitBtn != null && urlParams['noExitBtn'] == null) 12481 { 12482 urlParams['noExitBtn'] = data.noExitBtn; 12483 } 12484 12485 if (data.title != null && this.buttonContainer != null) 12486 { 12487 var tmp = document.createElement('span'); 12488 mxUtils.write(tmp, data.title); 12489 12490 if (this.embedFilenameSpan != null) 12491 { 12492 this.embedFilenameSpan.parentNode.removeChild(this.embedFilenameSpan); 12493 } 12494 12495 this.buttonContainer.appendChild(tmp); 12496 this.embedFilenameSpan = tmp; 12497 } 12498 12499 try 12500 { 12501 if (data.libs) 12502 { 12503 this.sidebar.showEntries(data.libs); 12504 } 12505 } 12506 catch(e){} 12507 12508 if (data.xmlpng != null) 12509 { 12510 data = this.extractGraphModelFromPng(data.xmlpng); 12511 } 12512 else if (data.descriptor != null) 12513 { 12514 data = data.descriptor; 12515 } 12516 else 12517 { 12518 data = data.xml; 12519 } 12520 } 12521 else if (data.action == 'merge') 12522 { 12523 var file = this.getCurrentFile(); 12524 12525 if (file != null) 12526 { 12527 var tmp = extractDiagramXml(data.xml); 12528 12529 if (tmp != null && tmp != '') 12530 { 12531 file.mergeFile(new LocalFile(this, tmp), function() 12532 { 12533 parent.postMessage(JSON.stringify({event: 'merge', message: data}), '*'); 12534 }, function(err) 12535 { 12536 parent.postMessage(JSON.stringify({event: 'merge', message: data, error: err}), '*'); 12537 }); 12538 } 12539 } 12540 12541 return; 12542 } 12543 else if (data.action == 'remoteInvokeReady') 12544 { 12545 this.handleRemoteInvokeReady(parent); 12546 return; 12547 } 12548 else if (data.action == 'remoteInvoke') 12549 { 12550 this.handleRemoteInvoke(data, evt.origin); 12551 return; 12552 } 12553 else if (data.action == 'remoteInvokeResponse') 12554 { 12555 this.handleRemoteInvokeResponse(data); 12556 return; 12557 } 12558 else 12559 { 12560 // Unknown message must stop execution 12561 parent.postMessage(JSON.stringify({error: 'unknownMessage', data: JSON.stringify(data)}), '*'); 12562 12563 return; 12564 } 12565 } 12566 catch (e) 12567 { 12568 // TODO: Block handling of more messages when in error state 12569 this.handleError(e); 12570 } 12571 } 12572 12573 var getData = mxUtils.bind(this, function() 12574 { 12575 return (urlParams['pages'] != '0' || (this.pages != null && this.pages.length > 1)) ? 12576 this.getFileData(true): mxUtils.getXml(this.editor.getGraphXml()); 12577 }); 12578 12579 var doLoad = mxUtils.bind(this, function(data, evt) 12580 { 12581 ignoreChange = true; 12582 try 12583 { 12584 fn(data, evt, null, convertToSketch); 12585 } 12586 catch (e) 12587 { 12588 this.handleError(e); 12589 } 12590 ignoreChange = false; 12591 12592 if (urlParams['modified'] != null) 12593 { 12594 this.editor.setStatus(''); 12595 } 12596 12597 lastData = getData(); 12598 12599 if (autosave && changeListener == null) 12600 { 12601 changeListener = mxUtils.bind(this, function(sender, eventObject) 12602 { 12603 var data = getData(); 12604 12605 if (data != lastData && !ignoreChange) 12606 { 12607 var msg = this.createLoadMessage('autosave'); 12608 msg.xml = data; 12609 var parent = window.opener || window.parent; 12610 parent.postMessage(JSON.stringify(msg), '*'); 12611 } 12612 12613 lastData = data; 12614 }); 12615 12616 this.editor.graph.model.addListener(mxEvent.CHANGE, changeListener); 12617 12618 // Some options trigger autosave 12619 this.editor.graph.addListener('gridSizeChanged', changeListener); 12620 this.editor.graph.addListener('shadowVisibleChanged', changeListener); 12621 this.addListener('pageFormatChanged', changeListener); 12622 this.addListener('pageScaleChanged', changeListener); 12623 this.addListener('backgroundColorChanged', changeListener); 12624 this.addListener('backgroundImageChanged', changeListener); 12625 this.addListener('foldingEnabledChanged', changeListener); 12626 this.addListener('mathEnabledChanged', changeListener); 12627 this.addListener('gridEnabledChanged', changeListener); 12628 this.addListener('guidesEnabledChanged', changeListener); 12629 this.addListener('pageViewChanged', changeListener); 12630 } 12631 12632 // Sends the bounds of the graph to the host after parsing 12633 if (urlParams['returnbounds'] == '1' || urlParams['proto'] == 'json') 12634 { 12635 var resp = this.createLoadMessage('load'); 12636 12637 // Attaches XML to response 12638 resp.xml = data; 12639 12640 parent.postMessage(JSON.stringify(resp), '*'); 12641 } 12642 12643 if (afterLoad != null) 12644 { 12645 afterLoad(); 12646 } 12647 }); 12648 12649 if (data != null && typeof data.substring === 'function' && data.substring(0, 34) == 'data:application/vnd.visio;base64,') 12650 { 12651 // Checks VND binary magic number in base64 12652 var filename = (data.substring(34, 45) == '0M8R4KGxGuE') ? 'raw.vsd' : 'raw.vsdx'; 12653 12654 this.importVisio(this.base64ToBlob(data.substring(data.indexOf(',') + 1)), function(xml) 12655 { 12656 doLoad(xml, evt); 12657 }, mxUtils.bind(this, function(e) 12658 { 12659 this.handleError(e); 12660 }), filename); 12661 } 12662 else if (data != null && typeof data.substring === 'function' && !this.isOffline() && new XMLHttpRequest().upload && this.isRemoteFileFormat(data, '')) 12663 { 12664 // Asynchronous parsing via server 12665 this.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr) 12666 { 12667 if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status <= 299 && 12668 xhr.responseText.substring(0, 13) == '<mxGraphModel') 12669 { 12670 doLoad(xhr.responseText, evt); 12671 } 12672 }), ''); 12673 } 12674 else if (data != null && typeof data.substring === 'function' && this.isLucidChartData(data)) 12675 { 12676 this.convertLucidChart(data, mxUtils.bind(this, function(xml) 12677 { 12678 doLoad(xml); 12679 }), mxUtils.bind(this, function(e) 12680 { 12681 this.handleError(e); 12682 })); 12683 } 12684 else if (data != null && typeof data === 'object' && data.format != null && (data.data != null || data.url != null)) 12685 { 12686 this.loadDescriptor(data, mxUtils.bind(this, function(e) 12687 { 12688 doLoad(getData(), evt); 12689 }), mxUtils.bind(this, function(e) 12690 { 12691 this.handleError(e, mxResources.get('errorLoadingFile')); 12692 })); 12693 } 12694 else 12695 { 12696 data = extractDiagramXml(data); 12697 doLoad(data, evt); 12698 } 12699 })); 12700 12701 // Requests data from the sender. This is a workaround for not allowing 12702 // the opener to listen for the onload event if not in the same origin. 12703 var parent = window.opener || window.parent; 12704 var msg = (urlParams['proto'] == 'json') ? JSON.stringify({event: 'init'}) : (urlParams['ready'] || 'ready'); 12705 parent.postMessage(msg, '*'); 12706 12707 // Adds JSON event for opening links 12708 if (urlParams['proto'] == 'json') 12709 { 12710 var graphOpenLink = this.editor.graph.openLink; 12711 12712 this.editor.graph.openLink = function(href, target, allowOpener) 12713 { 12714 graphOpenLink.apply(this, arguments); 12715 12716 parent.postMessage(JSON.stringify({event: 'openLink', href: href, target: target, allowOpener: allowOpener}), '*'); 12717 }; 12718 } 12719 }; 12720 12721 /** 12722 * Adds the buttons for embedded mode. 12723 */ 12724 EditorUi.prototype.addEmbedButtons = function() 12725 { 12726 if (this.menubar != null && urlParams['embedInline'] != '1') 12727 { 12728 var div = document.createElement('div'); 12729 div.style.display = 'inline-block'; 12730 div.style.position = 'absolute'; 12731 div.style.paddingTop = (uiTheme == 'atlas' || urlParams['atlas'] == '1') ? '2px' : '0px'; 12732 div.style.paddingLeft = '8px'; 12733 div.style.paddingBottom = '2px'; 12734 12735 var button = document.createElement('button'); 12736 button.className = 'geBigButton'; 12737 var lastBtn = button; 12738 12739 if (urlParams['noSaveBtn'] == '1') 12740 { 12741 if (urlParams['saveAndExit'] != '0') 12742 { 12743 var saveAndExitTitle = urlParams['publishClose'] == '1' ? mxResources.get('publish') : mxResources.get('saveAndExit'); 12744 mxUtils.write(button, saveAndExitTitle); 12745 button.setAttribute('title', saveAndExitTitle); 12746 12747 mxEvent.addListener(button, 'click', mxUtils.bind(this, function() 12748 { 12749 this.actions.get('saveAndExit').funct(); 12750 })); 12751 12752 div.appendChild(button); 12753 } 12754 } 12755 else 12756 { 12757 mxUtils.write(button, mxResources.get('save')); 12758 button.setAttribute('title', mxResources.get('save') + ' (' + Editor.ctrlKey + '+S)'); 12759 12760 mxEvent.addListener(button, 'click', mxUtils.bind(this, function() 12761 { 12762 this.actions.get('save').funct(); 12763 })); 12764 12765 div.appendChild(button); 12766 12767 if (urlParams['saveAndExit'] == '1') 12768 { 12769 button = document.createElement('a'); 12770 mxUtils.write(button, mxResources.get('saveAndExit')); 12771 button.setAttribute('title', mxResources.get('saveAndExit')); 12772 button.className = 'geBigButton geBigStandardButton'; 12773 button.style.marginLeft = '6px'; 12774 12775 mxEvent.addListener(button, 'click', mxUtils.bind(this, function() 12776 { 12777 this.actions.get('saveAndExit').funct(); 12778 })); 12779 12780 div.appendChild(button); 12781 lastBtn = button; 12782 } 12783 } 12784 12785 if (urlParams['noExitBtn'] != '1') 12786 { 12787 button = document.createElement('a'); 12788 var exitTitle = urlParams['publishClose'] == '1' ? mxResources.get('close') : mxResources.get('exit'); 12789 mxUtils.write(button, exitTitle); 12790 button.setAttribute('title', exitTitle); 12791 button.className = 'geBigButton geBigStandardButton'; 12792 button.style.marginLeft = '6px'; 12793 12794 mxEvent.addListener(button, 'click', mxUtils.bind(this, function() 12795 { 12796 this.actions.get('exit').funct(); 12797 })); 12798 12799 div.appendChild(button); 12800 lastBtn = button; 12801 } 12802 12803 lastBtn.style.marginRight = '20px'; 12804 12805 this.toolbar.container.appendChild(div); 12806 this.toolbar.staticElements.push(div); 12807 div.style.right = (uiTheme == 'atlas' || urlParams['atlas'] == '1') ? '42px' : '52px'; 12808 } 12809 }; 12810 12811 /** 12812 * 12813 */ 12814 EditorUi.prototype.showImportCsvDialog = function() 12815 { 12816 if (this.importCsvDialog == null) 12817 { 12818 this.importCsvDialog = new TextareaDialog(this, mxResources.get('csv') + ':', 12819 Editor.defaultCsvValue, mxUtils.bind(this, function(newValue) 12820 { 12821 this.importCsv(newValue); 12822 }), null, null, 620, 430, null, true, true, mxResources.get('import'), 12823 !this.isOffline() ? 'https://drawio-app.com/import-from-csv-to-drawio/' : null); 12824 } 12825 12826 this.showDialog(this.importCsvDialog.container, 640, 520, true, true, null, null, null, null, true); 12827 this.importCsvDialog.init(); 12828 }; 12829 12830 12831 /** 12832 * Runs the layout from the given JavaScript array which is of the form [{layout: name, config: obj}, ...] 12833 * where name is the layout constructor name and config contains the properties of the layout instance. 12834 */ 12835 EditorUi.prototype.executeLayoutList = function(layoutList, done) 12836 { 12837 var graph = this.editor.graph; 12838 var cells = graph.getSelectionCells(); 12839 12840 for (var i = 0; i < layoutList.length; i++) 12841 { 12842 var layout = new window[layoutList[i].layout](graph); 12843 12844 if (layoutList[i].config != null) 12845 { 12846 for (var key in layoutList[i].config) 12847 { 12848 layout[key] = layoutList[i].config[key]; 12849 } 12850 } 12851 12852 this.executeLayout(function() 12853 { 12854 layout.execute(graph.getDefaultParent(), cells.length == 0 ? null : cells); 12855 }, i == layoutList.length - 1, done); 12856 } 12857 }; 12858 12859 /** 12860 * 12861 */ 12862 EditorUi.prototype.importCsv = function(text, done) 12863 { 12864 try 12865 { 12866 var lines = text.split('\n'); 12867 var allCells = []; 12868 var parents = []; 12869 var cells = []; 12870 var dups = {}; 12871 12872 if (lines.length > 0) 12873 { 12874 // Internal lookup table 12875 var lookups = {}; 12876 12877 // Default values 12878 var vars = null; 12879 var style = null; 12880 var styles = null; 12881 var stylename = null; 12882 var labelname = null; 12883 var labels = null; 12884 var parentstyle = 'whiteSpace=wrap;html=1;'; 12885 var identity = null; 12886 var parent = null; 12887 var namespace = ''; 12888 var width = 'auto'; 12889 var height = 'auto'; 12890 var left = null; 12891 var top = null; 12892 var edgespacing = 40; 12893 var nodespacing = 40; 12894 var levelspacing = 100; 12895 var padding = 0; 12896 12897 var graph = this.editor.graph; 12898 var view = graph.view; 12899 var bds = graph.getGraphBounds(); 12900 12901 // Delayed after optional layout 12902 var afterInsert = function() 12903 { 12904 if (done != null) 12905 { 12906 done(select); 12907 } 12908 else 12909 { 12910 graph.setSelectionCells(select); 12911 graph.scrollCellToVisible(graph.getSelectionCell()); 12912 } 12913 }; 12914 12915 // Computes unscaled, untranslated graph bounds 12916 var pt = graph.getFreeInsertPoint(); 12917 var x0 = pt.x; 12918 var y0 = pt.y; 12919 var y = y0; 12920 12921 // Default label value depends on column names 12922 var label = null; 12923 12924 // Default layout to run. 12925 var layout = 'auto'; 12926 12927 // Name of the attribute that contains the parent reference 12928 var parent = null; 12929 12930 // Name of the attribute that contains the references for creating edges 12931 var edges = []; 12932 12933 // Name of the column for hyperlinks 12934 var link = null; 12935 12936 // String array of names to remove from metadata 12937 var ignore = null; 12938 12939 // Read processing instructions first 12940 var index = 0; 12941 12942 while (index < lines.length && lines[index].charAt(0) == '#') 12943 { 12944 var text = lines[index]; 12945 index++; 12946 12947 while (index < lines.length && text.charAt(text.length - 1) == '\\' && 12948 lines[index].charAt(0) == '#') 12949 { 12950 text = text.substring(0, text.length - 1) + mxUtils.trim(lines[index].substring(1)); 12951 index++; 12952 } 12953 12954 if (text.charAt(1) != '#') 12955 { 12956 // Processing instruction 12957 var idx = text.indexOf(':'); 12958 12959 if (idx > 0) 12960 { 12961 var key = mxUtils.trim(text.substring(1, idx)); 12962 var value = mxUtils.trim(text.substring(idx + 1)); 12963 12964 if (key == 'label') 12965 { 12966 label = graph.sanitizeHtml(value); 12967 } 12968 else if (key == 'labelname' && value.length > 0 && value != '-') 12969 { 12970 labelname = value; 12971 } 12972 else if (key == 'labels' && value.length > 0 && value != '-') 12973 { 12974 labels = JSON.parse(value); 12975 } 12976 else if (key == 'style') 12977 { 12978 style = value; 12979 } 12980 else if (key == 'parentstyle') 12981 { 12982 parentstyle = value; 12983 } 12984 else if (key == 'stylename' && value.length > 0 && value != '-') 12985 { 12986 stylename = value; 12987 } 12988 else if (key == 'styles' && value.length > 0 && value != '-') 12989 { 12990 styles = JSON.parse(value); 12991 } 12992 else if (key == 'vars' && value.length > 0 && value != '-') 12993 { 12994 vars = JSON.parse(value); 12995 } 12996 else if (key == 'identity' && value.length > 0 && value != '-') 12997 { 12998 identity = value; 12999 } 13000 else if (key == 'parent' && value.length > 0 && value != '-') 13001 { 13002 parent = value; 13003 } 13004 else if (key == 'namespace' && value.length > 0 && value != '-') 13005 { 13006 namespace = value; 13007 } 13008 else if (key == 'width') 13009 { 13010 width = value; 13011 } 13012 else if (key == 'height') 13013 { 13014 height = value; 13015 } 13016 else if (key == 'left' && value.length > 0) 13017 { 13018 left = value; 13019 } 13020 else if (key == 'top' && value.length > 0) 13021 { 13022 top = value; 13023 } 13024 else if (key == 'ignore') 13025 { 13026 ignore = value.split(','); 13027 } 13028 else if (key == 'connect') 13029 { 13030 edges.push(JSON.parse(value)); 13031 } 13032 else if (key == 'link') 13033 { 13034 link = value; 13035 } 13036 else if (key == 'padding') 13037 { 13038 padding = parseFloat(value); 13039 } 13040 else if (key == 'edgespacing') 13041 { 13042 edgespacing = parseFloat(value); 13043 } 13044 else if (key == 'nodespacing') 13045 { 13046 nodespacing = parseFloat(value); 13047 } 13048 else if (key == 'levelspacing') 13049 { 13050 levelspacing = parseFloat(value); 13051 } 13052 else if (key == 'layout') 13053 { 13054 layout = value; 13055 } 13056 } 13057 } 13058 } 13059 13060 if (lines[index] == null) 13061 { 13062 throw new Error(mxResources.get('invalidOrMissingFile')); 13063 } 13064 13065 // Converts identity and parent to index and validates XML attribute names 13066 var keys = this.editor.csvToArray(lines[index]); 13067 var identityIndex = null; 13068 var parentIndex = null; 13069 var attribs = []; 13070 13071 for (var i = 0; i < keys.length; i++) 13072 { 13073 if (identity == keys[i]) 13074 { 13075 identityIndex = i; 13076 } 13077 13078 if (parent == keys[i]) 13079 { 13080 parentIndex = i; 13081 } 13082 13083 attribs.push(mxUtils.trim(keys[i]).replace(/[^a-z0-9]+/ig, '_'). 13084 replace(/^\d+/, '').replace(/_+$/, '')); 13085 } 13086 13087 if (label == null) 13088 { 13089 label = '%' + attribs[0] + '%'; 13090 } 13091 13092 if (edges != null) 13093 { 13094 for (var e = 0; e < edges.length; e++) 13095 { 13096 if (lookups[edges[e].to] == null) 13097 { 13098 lookups[edges[e].to] = {}; 13099 } 13100 } 13101 } 13102 13103 // Parse and validate input 13104 var arrays = []; 13105 13106 for (var i = index + 1; i < lines.length; i++) 13107 { 13108 var values = this.editor.csvToArray(lines[i]); 13109 13110 if (values == null) 13111 { 13112 var short = (lines[i].length > 40) ? lines[i].substring(0, 40) + '...' : lines[i]; 13113 13114 throw new Error(short + ' (' + i + '):\n' + mxResources.get('containsValidationErrors')); 13115 } 13116 else if (values.length > 0) 13117 { 13118 arrays.push(values); 13119 } 13120 } 13121 13122 graph.model.beginUpdate(); 13123 try 13124 { 13125 for (var i = 0; i < arrays.length; i++) 13126 { 13127 var values = arrays[i]; 13128 var cell = null; 13129 var id = (identityIndex != null) ? namespace + values[identityIndex] : null; 13130 13131 if (id != null) 13132 { 13133 cell = graph.model.getCell(id); 13134 } 13135 13136 var exists = cell != null; 13137 var newCell = new mxCell(label, new mxGeometry(x0, y, 13138 0, 0), style || 'whiteSpace=wrap;html=1;'); 13139 newCell.vertex = true; 13140 newCell.id = id; 13141 13142 for (var j = 0; j < values.length; j++) 13143 { 13144 graph.setAttributeForCell(newCell, attribs[j], values[j]); 13145 } 13146 13147 if (labelname != null && labels != null) 13148 { 13149 var tempLabel = labels[newCell.getAttribute(labelname)]; 13150 13151 if (tempLabel != null) 13152 { 13153 graph.labelChanged(newCell, tempLabel); 13154 } 13155 } 13156 13157 if (stylename != null && styles != null) 13158 { 13159 var tempStyle = styles[newCell.getAttribute(stylename)]; 13160 13161 if (tempStyle != null) 13162 { 13163 newCell.style = tempStyle; 13164 } 13165 } 13166 13167 graph.setAttributeForCell(newCell, 'placeholders', '1'); 13168 newCell.style = graph.replacePlaceholders(newCell, newCell.style, vars); 13169 13170 if (exists) 13171 { 13172 graph.model.setValue(cell, newCell.value); 13173 graph.model.setStyle(cell, newCell.style); 13174 13175 if (mxUtils.indexOf(cells, cell) < 0) 13176 { 13177 cells.push(cell); 13178 } 13179 13180 graph.fireEvent(new mxEventObject('cellsInserted', 'cells', [cell])); 13181 } 13182 else 13183 { 13184 graph.fireEvent(new mxEventObject('cellsInserted', 'cells', [newCell])); 13185 } 13186 13187 cell = newCell; 13188 13189 if (!exists) 13190 { 13191 for (var e = 0; e < edges.length; e++) 13192 { 13193 lookups[edges[e].to][cell.getAttribute(edges[e].to)] = cell; 13194 } 13195 } 13196 13197 if (link != null && link != 'link') 13198 { 13199 graph.setLinkForCell(cell, cell.getAttribute(link)); 13200 13201 // Removes attribute 13202 graph.setAttributeForCell(cell, link, null); 13203 } 13204 13205 // Sets the geometry 13206 var size = this.editor.graph.getPreferredSizeForCell(cell); 13207 var parent = (parentIndex != null) ? graph.model.getCell( 13208 namespace + values[parentIndex]) : null; 13209 13210 if (cell.vertex) 13211 { 13212 var originX = (parent != null) ? 0 : x0; 13213 var originY = (parent != null) ? 0 : y0; 13214 13215 if (left != null && cell.getAttribute(left) != null) 13216 { 13217 cell.geometry.x = originX + parseFloat(cell.getAttribute(left)); 13218 } 13219 13220 if (top != null && cell.getAttribute(top) != null) 13221 { 13222 cell.geometry.y = originY + parseFloat(cell.getAttribute(top)); 13223 } 13224 13225 var widthValue = (width.charAt(0) == '@') ? cell.getAttribute(width.substring(1)) : null; 13226 13227 if (widthValue != null && widthValue != 'auto') 13228 { 13229 cell.geometry.width = parseFloat(cell.getAttribute(width.substring(1))); 13230 } 13231 else 13232 { 13233 cell.geometry.width = (width == 'auto' || widthValue == 'auto') ? 13234 size.width + padding : parseFloat(width); 13235 } 13236 13237 var heightValue = (height.charAt(0) == '@') ? cell.getAttribute(height.substring(1)) : null; 13238 13239 if (heightValue != null && heightValue != 'auto') 13240 { 13241 cell.geometry.height = parseFloat(heightValue); 13242 } 13243 else 13244 { 13245 cell.geometry.height = (height == 'auto' || heightValue == 'auto') ? 13246 size.height + padding : parseFloat(height); 13247 } 13248 13249 y += cell.geometry.height + nodespacing; 13250 } 13251 13252 if (!exists) 13253 { 13254 allCells.push(cell); 13255 13256 if (parent != null) 13257 { 13258 parent.style = graph.replacePlaceholders(parent, parentstyle, vars); 13259 graph.addCell(cell, parent); 13260 parents.push(parent); 13261 } 13262 else 13263 { 13264 cells.push(graph.addCell(cell)); 13265 } 13266 } 13267 else 13268 { 13269 if (dups[id] == null) 13270 { 13271 dups[id] = []; 13272 } 13273 13274 dups[id].push(cell); 13275 } 13276 } 13277 13278 // Process parents for autosize 13279 for (var i = 0; i < parents.length; i++) 13280 { 13281 var widthValue = (width.charAt(0) == '@') ? parents[i].getAttribute(width.substring(1)) : null; 13282 var heightValue = (height.charAt(0) == '@') ? parents[i].getAttribute(height.substring(1)) : null; 13283 13284 if ((width == 'auto' || widthValue == 'auto') && 13285 (height == 'auto' || heightValue == 'auto')) 13286 { 13287 graph.updateGroupBounds([parents[i]], padding, true); 13288 } 13289 } 13290 13291 var roots = cells.slice(); 13292 var select = cells.slice(); 13293 13294 for (var e = 0; e < edges.length; e++) 13295 { 13296 var edge = edges[e]; 13297 13298 for (var i = 0; i < allCells.length; i++) 13299 { 13300 var cell = allCells[i]; 13301 13302 var insertEdge = mxUtils.bind(this, function(realCell, dataCell, edge) 13303 { 13304 var tmp = dataCell.getAttribute(edge.from); 13305 13306 if (tmp != null) 13307 { 13308 if (tmp != '') 13309 { 13310 var refs = tmp.split(','); 13311 13312 for (var j = 0; j < refs.length; j++) 13313 { 13314 var ref = lookups[edge.to][refs[j]]; 13315 13316 if (ref != null) 13317 { 13318 var label = edge.label; 13319 13320 if (edge.fromlabel != null) 13321 { 13322 label = (dataCell.getAttribute(edge.fromlabel) || '') + (label || ''); 13323 } 13324 13325 if (edge.sourcelabel != null) 13326 { 13327 label = graph.replacePlaceholders(dataCell, 13328 edge.sourcelabel, vars) + (label || ''); 13329 } 13330 13331 if (edge.tolabel != null) 13332 { 13333 label = (label || '') + (ref.getAttribute(edge.tolabel) || ''); 13334 } 13335 13336 if (edge.targetlabel != null) 13337 { 13338 label = (label || '') + graph.replacePlaceholders( 13339 ref, edge.targetlabel, vars); 13340 } 13341 13342 var placeholders = ((edge.placeholders == 'target') == 13343 !edge.invert) ? ref : realCell; 13344 var style = (edge.style != null) ? 13345 graph.replacePlaceholders(placeholders, edge.style, vars) : 13346 graph.createCurrentEdgeStyle(); 13347 13348 var edgeCell = graph.insertEdge(null, null, label || '', (edge.invert) ? 13349 ref : realCell, (edge.invert) ? realCell : ref, style); 13350 13351 // Adds additional edge labels 13352 if (edge.labels != null) 13353 { 13354 for (var k = 0; k < edge.labels.length; k++) 13355 { 13356 var def = edge.labels[k]; 13357 var elx = (def.x != null) ? def.x : 0; 13358 var ely = (def.y != null) ? def.y : 0; 13359 var st = 'resizable=0;html=1;'; 13360 var el = new mxCell(def.label || k, 13361 new mxGeometry(elx, ely, 0, 0), st); 13362 el.vertex = true; 13363 el.connectable = false; 13364 el.geometry.relative = true; 13365 13366 if (def.placeholders != null) 13367 { 13368 el.value = graph.replacePlaceholders( 13369 ((def.placeholders == 'target') == 13370 !edge.invert) ? ref : realCell, 13371 el.value, vars) 13372 } 13373 13374 if (def.dx != null || def.dy != null) 13375 { 13376 el.geometry.offset = new mxPoint( 13377 (def.dx != null) ? def.dx : 0, 13378 (def.dy != null) ? def.dy : 0); 13379 } 13380 13381 edgeCell.insert(el); 13382 } 13383 } 13384 13385 select.push(edgeCell); 13386 mxUtils.remove((edge.invert) ? realCell : ref, roots); 13387 } 13388 } 13389 } 13390 } 13391 }); 13392 13393 insertEdge(cell, cell, edge); 13394 13395 // Checks more entries 13396 if (dups[cell.id] != null) 13397 { 13398 for (var j = 0; j < dups[cell.id].length; j++) 13399 { 13400 insertEdge(cell, dups[cell.id][j], edge); 13401 } 13402 } 13403 } 13404 } 13405 13406 // Removes ignored attributes after processing above 13407 if (ignore != null) 13408 { 13409 for (var i = 0; i < allCells.length; i++) 13410 { 13411 var cell = allCells[i]; 13412 13413 for (var j = 0; j < ignore.length; j++) 13414 { 13415 graph.setAttributeForCell(cell, mxUtils.trim(ignore[j]), null); 13416 } 13417 } 13418 } 13419 13420 if (cells.length > 0) 13421 { 13422 var edgeLayout = new mxParallelEdgeLayout(graph); 13423 edgeLayout.spacing = edgespacing; 13424 edgeLayout.checkOverlap = true; 13425 13426 var postProcess = function() 13427 { 13428 if (edgeLayout.spacing > 0) 13429 { 13430 edgeLayout.execute(graph.getDefaultParent()); 13431 } 13432 13433 // Aligns cells to grid and/or rounds positions 13434 for (var i = 0; i < cells.length; i++) 13435 { 13436 var geo = graph.getCellGeometry(cells[i]); 13437 geo.x = Math.round(graph.snap(geo.x)); 13438 geo.y = Math.round(graph.snap(geo.y)); 13439 13440 if (width == 'auto') 13441 { 13442 geo.width = Math.round(graph.snap(geo.width)); 13443 } 13444 13445 if (height == 'auto') 13446 { 13447 geo.height = Math.round(graph.snap(geo.height)); 13448 } 13449 } 13450 }; 13451 13452 if (layout.charAt(0) == '[') 13453 { 13454 // Required for layouts to work with new cells 13455 var temp = afterInsert; 13456 graph.view.validate(); 13457 this.executeLayoutList(JSON.parse(layout), function() 13458 { 13459 postProcess(); 13460 temp(); 13461 }); 13462 afterInsert = null; 13463 } 13464 else if (layout == 'circle') 13465 { 13466 var circleLayout = new mxCircleLayout(graph); 13467 circleLayout.disableEdgeStyle = false; 13468 circleLayout.resetEdges = false; 13469 13470 var circleLayoutIsVertexIgnored = circleLayout.isVertexIgnored; 13471 13472 // Ignore other cells 13473 circleLayout.isVertexIgnored = function(vertex) 13474 { 13475 return circleLayoutIsVertexIgnored.apply(this, arguments) || 13476 mxUtils.indexOf(cells, vertex) < 0; 13477 }; 13478 13479 this.executeLayout(function() 13480 { 13481 circleLayout.execute(graph.getDefaultParent()); 13482 postProcess(); 13483 }, true, afterInsert); 13484 13485 afterInsert = null; 13486 } 13487 else if (layout == 'horizontaltree' || layout == 'verticaltree' || 13488 (layout == 'auto' && select.length == 2 * cells.length - 1 && roots.length == 1)) 13489 { 13490 // Required for layouts to work with new cells 13491 graph.view.validate(); 13492 13493 var treeLayout = new mxCompactTreeLayout(graph, layout == 'horizontaltree'); 13494 treeLayout.levelDistance = nodespacing; 13495 treeLayout.edgeRouting = false; 13496 treeLayout.resetEdges = false; 13497 13498 this.executeLayout(function() 13499 { 13500 treeLayout.execute(graph.getDefaultParent(), (roots.length > 0) ? roots[0] : null); 13501 }, true, afterInsert); 13502 13503 afterInsert = null; 13504 } 13505 else if (layout == 'horizontalflow' || layout == 'verticalflow' || 13506 (layout == 'auto' && roots.length == 1)) 13507 { 13508 // Required for layouts to work with new cells 13509 graph.view.validate(); 13510 13511 var flowLayout = new mxHierarchicalLayout(graph, 13512 (layout == 'horizontalflow') ? mxConstants.DIRECTION_WEST : mxConstants.DIRECTION_NORTH); 13513 flowLayout.intraCellSpacing = nodespacing; 13514 flowLayout.parallelEdgeSpacing = edgespacing; 13515 flowLayout.interRankCellSpacing = levelspacing; 13516 flowLayout.disableEdgeStyle = false; 13517 13518 this.executeLayout(function() 13519 { 13520 flowLayout.execute(graph.getDefaultParent(), select); 13521 13522 // Workaround for flow layout moving cells to origin 13523 graph.moveCells(select, x0, y0); 13524 }, true, afterInsert); 13525 13526 afterInsert = null; 13527 } 13528 else if (layout == 'organic' || (layout == 'auto' && 13529 select.length > cells.length)) 13530 { 13531 // Required for layouts to work with new cells 13532 graph.view.validate(); 13533 13534 var organicLayout = new mxFastOrganicLayout(graph); 13535 organicLayout.forceConstant = nodespacing * 3; 13536 organicLayout.disableEdgeStyle = false; 13537 organicLayout.resetEdges = false; 13538 13539 var organicLayoutIsVertexIgnored = organicLayout.isVertexIgnored; 13540 13541 // Ignore other cells 13542 organicLayout.isVertexIgnored = function(vertex) 13543 { 13544 return organicLayoutIsVertexIgnored.apply(this, arguments) || 13545 mxUtils.indexOf(cells, vertex) < 0; 13546 }; 13547 13548 this.executeLayout(function() 13549 { 13550 organicLayout.execute(graph.getDefaultParent()); 13551 postProcess(); 13552 }, true, afterInsert); 13553 13554 afterInsert = null; 13555 } 13556 } 13557 13558 this.hideDialog(); 13559 } 13560 finally 13561 { 13562 graph.model.endUpdate(); 13563 } 13564 13565 if (afterInsert != null) 13566 { 13567 afterInsert(); 13568 } 13569 } 13570 } 13571 catch (e) 13572 { 13573 this.handleError(e); 13574 } 13575 }; 13576 13577 /** 13578 * Translates this point by the given vector. 13579 * 13580 * @param {number} dx X-coordinate of the translation. 13581 * @param {number} dy Y-coordinate of the translation. 13582 */ 13583 EditorUi.prototype.getSearch = function(exclude) 13584 { 13585 var result = ''; 13586 13587 if (urlParams['offline'] != '1' && urlParams['demo'] != '1' && exclude != null && window.location.search.length > 0) 13588 { 13589 var amp = '?'; 13590 13591 for (var key in urlParams) 13592 { 13593 if (mxUtils.indexOf(exclude, key) < 0 && urlParams[key] != null) 13594 { 13595 result += amp + key + '=' + urlParams[key]; 13596 amp = '&'; 13597 } 13598 } 13599 } 13600 else 13601 { 13602 result = window.location.search; 13603 } 13604 13605 return result; 13606 }; 13607 13608 /** 13609 * Returns the URL for a copy of this editor with no state. 13610 */ 13611 EditorUi.prototype.getUrl = function(pathname) 13612 { 13613 var href = (pathname != null) ? pathname : window.location.pathname; 13614 var parms = (href.indexOf('?') > 0) ? 1 : 0; 13615 13616 if (urlParams['offline'] == '1') 13617 { 13618 href += window.location.search; 13619 } 13620 else 13621 { 13622 var ignored = ['tmp', 'libs', 'clibs', 'state', 'fileId', 'code', 'share', 'notitle', 13623 'data', 'url', 'embed', 'client', 'create', 'title', 'splash']; 13624 13625 // Removes template URL parameter for new blank diagram 13626 for (var key in urlParams) 13627 { 13628 if (mxUtils.indexOf(ignored, key) < 0) 13629 { 13630 if (parms == 0) 13631 { 13632 href += '?'; 13633 } 13634 else 13635 { 13636 href += '&'; 13637 } 13638 13639 if (urlParams[key] != null) 13640 { 13641 href += key + '=' + urlParams[key]; 13642 parms++; 13643 } 13644 } 13645 } 13646 } 13647 13648 return href; 13649 }; 13650 13651 /** 13652 * Overrides link dialog. 13653 */ 13654 EditorUi.prototype.showLinkDialog = function(value, btnLabel, fn, showNewWindowOption, linkTarget) 13655 { 13656 var dlg = new LinkDialog(this, value, btnLabel, fn, true, showNewWindowOption, linkTarget); 13657 this.showDialog(dlg.container, 560, 130, true, true); 13658 dlg.init(); 13659 }; 13660 13661 /** 13662 * Returns the number of storage options enabled 13663 */ 13664 EditorUi.prototype.getServiceCount = function(allowBrowser) 13665 { 13666 var serviceCount = 1; 13667 13668 if (this.drive != null || typeof window.DriveClient === 'function') 13669 { 13670 serviceCount++ 13671 } 13672 13673 if (this.dropbox != null || typeof window.DropboxClient === 'function') 13674 { 13675 serviceCount++ 13676 } 13677 13678 if (this.oneDrive != null || typeof window.OneDriveClient === 'function') 13679 { 13680 serviceCount++ 13681 } 13682 13683 if (this.gitHub != null) 13684 { 13685 serviceCount++ 13686 } 13687 13688 if (this.gitLab != null) 13689 { 13690 serviceCount++ 13691 } 13692 13693 if (this.notion != null) 13694 { 13695 serviceCount++ 13696 } 13697 13698 if (allowBrowser && isLocalStorage && urlParams['browser'] == '1') 13699 { 13700 serviceCount++ 13701 } 13702 13703 return serviceCount; 13704 } 13705 13706 /** 13707 * Updates action and menu states depending on the file. 13708 */ 13709 EditorUi.prototype.updateUi = function() 13710 { 13711 this.updateButtonContainer(); 13712 this.updateActionStates(); 13713 13714 // Action states that only need update for new files 13715 var file = this.getCurrentFile(); 13716 var active = file != null || (urlParams['embed'] == '1' && 13717 this.editor.graph.isEnabled()); 13718 this.menus.get('viewPanels').setEnabled(active); 13719 this.menus.get('viewZoom').setEnabled(active); 13720 13721 var restricted = (urlParams['embed'] != '1' || 13722 !this.editor.graph.isEnabled()) && 13723 (file == null || file.isRestricted()); 13724 this.actions.get('makeCopy').setEnabled(!restricted); 13725 this.actions.get('print').setEnabled(!restricted); 13726 this.menus.get('exportAs').setEnabled(!restricted); 13727 this.menus.get('embed').setEnabled(!restricted); 13728 13729 // Disables libraries and extras menu in embed mode 13730 // while waiting for file data 13731 var libsEnabled = urlParams['embed'] != '1' || 13732 this.editor.graph.isEnabled(); 13733 this.menus.get('extras').setEnabled(libsEnabled); 13734 13735 if (Editor.enableCustomLibraries) 13736 { 13737 this.menus.get('openLibraryFrom').setEnabled(libsEnabled); 13738 this.menus.get('newLibrary').setEnabled(libsEnabled); 13739 } 13740 13741 // Disables actions in the toolbar 13742 var editable = (urlParams['embed'] == '1' && 13743 this.editor.graph.isEnabled()) || 13744 (file != null && file.isEditable()); 13745 this.actions.get('image').setEnabled(active); 13746 this.actions.get('zoomIn').setEnabled(active); 13747 this.actions.get('zoomOut').setEnabled(active); 13748 this.actions.get('resetView').setEnabled(active); 13749 13750 // Updates undo history states 13751 this.actions.get('undo').setEnabled(this.canUndo() && editable); 13752 this.actions.get('redo').setEnabled(this.canRedo() && editable); 13753 13754 // Disables menus 13755 this.menus.get('edit').setEnabled(active); 13756 this.menus.get('view').setEnabled(active); 13757 this.menus.get('importFrom').setEnabled(editable); 13758 this.menus.get('arrange').setEnabled(editable); 13759 13760 // Disables connection drop downs in toolbar 13761 if (this.toolbar != null) 13762 { 13763 if (this.toolbar.edgeShapeMenu != null) 13764 { 13765 this.toolbar.edgeShapeMenu.setEnabled(editable); 13766 } 13767 13768 if (this.toolbar.edgeStyleMenu != null) 13769 { 13770 this.toolbar.edgeStyleMenu.setEnabled(editable); 13771 } 13772 } 13773 13774 this.updateUserElement(); 13775 }; 13776 13777 /** 13778 * Hook for subclassers 13779 */ 13780 EditorUi.prototype.updateButtonContainer = function() 13781 { 13782 // do nothing 13783 }; 13784 13785 /** 13786 * Hook for subclassers 13787 */ 13788 EditorUi.prototype.updateUserElement = function() 13789 { 13790 // do nothing 13791 }; 13792 13793 /** 13794 * Hook for subclassers 13795 */ 13796 EditorUi.prototype.scheduleSanityCheck = function() 13797 { 13798 // do nothing 13799 }; 13800 13801 /** 13802 * Hook for subclassers 13803 */ 13804 EditorUi.prototype.stopSanityCheck = function() 13805 { 13806 // do nothing 13807 }; 13808 13809 /** 13810 * Returns true if a diagram is cative and editable. 13811 */ 13812 EditorUi.prototype.isDiagramActive = function() 13813 { 13814 var file = this.getCurrentFile(); 13815 13816 return (file != null && file.isEditable()) || 13817 (urlParams['embed'] == '1' && this.editor.graph.isEnabled()); 13818 }; 13819 13820 /** 13821 * Updates action states depending on the selection. 13822 */ 13823 var editorUiUpdateActionStates = EditorUi.prototype.updateActionStates; 13824 EditorUi.prototype.updateActionStates = function() 13825 { 13826 editorUiUpdateActionStates.apply(this, arguments); 13827 13828 var graph = this.editor.graph; 13829 var file = this.getCurrentFile(); 13830 var active = this.isDiagramActive(); 13831 var editable = graph.getEditableCells(graph.getSelectionCells()); 13832 var enabled = file != null || urlParams['embed'] == '1'; 13833 13834 this.actions.get('pageSetup').setEnabled(active); 13835 this.actions.get('autosave').setEnabled(file != null && file.isEditable() && file.isAutosaveOptional()); 13836 this.actions.get('guides').setEnabled(active); 13837 this.actions.get('editData').setEnabled(editable.length > 0 || graph.isSelectionEmpty()); 13838 this.actions.get('shadowVisible').setEnabled(active); 13839 this.actions.get('connectionArrows').setEnabled(active); 13840 this.actions.get('connectionPoints').setEnabled(active); 13841 this.actions.get('copyStyle').setEnabled(active && !graph.isSelectionEmpty()); 13842 this.actions.get('pasteStyle').setEnabled(active && editable.length > 0); 13843 this.actions.get('editGeometry').setEnabled(editable.length > 0 && 13844 graph.getModel().isVertex(editable[0])); 13845 this.actions.get('createShape').setEnabled(active); 13846 this.actions.get('createRevision').setEnabled(active); 13847 this.actions.get('moveToFolder').setEnabled(file != null); 13848 this.actions.get('makeCopy').setEnabled(file != null && !file.isRestricted()); 13849 this.actions.get('editDiagram').setEnabled(active && (file == null || !file.isRestricted())); 13850 this.actions.get('publishLink').setEnabled(file != null && !file.isRestricted()); 13851 this.actions.get('tags').setEnabled(this.diagramContainer.style.visibility != 'hidden'); 13852 this.actions.get('layers').setEnabled(this.diagramContainer.style.visibility != 'hidden'); 13853 this.actions.get('outline').setEnabled(this.diagramContainer.style.visibility != 'hidden'); 13854 this.actions.get('rename').setEnabled((file != null && file.isRenamable()) || urlParams['embed'] == '1'); 13855 this.actions.get('close').setEnabled(file != null); 13856 this.menus.get('publish').setEnabled(file != null && !file.isRestricted()); 13857 13858 var findReplace = this.actions.get('findReplace'); 13859 findReplace.setEnabled(this.diagramContainer.style.visibility != 'hidden'); 13860 findReplace.label = mxResources.get('find') + ((graph.isEnabled()) ? 13861 '/' + mxResources.get('replace') : '') + '...'; 13862 13863 var state = graph.view.getState(graph.getSelectionCell()); 13864 this.actions.get('editShape').setEnabled(active && state != null && state.shape != null && state.shape.stencil != null); 13865 }; 13866 13867 /** 13868 * Overridden to remove export dialog in chromeless lightbox. 13869 */ 13870 var editoUiDestroy = EditorUi.prototype.destroy; 13871 13872 EditorUi.prototype.destroy = function() 13873 { 13874 if (this.exportDialog != null) 13875 { 13876 this.exportDialog.parentNode.removeChild(this.exportDialog); 13877 this.exportDialog = null; 13878 } 13879 13880 editoUiDestroy.apply(this, arguments); 13881 }; 13882 13883 /** 13884 * Overrides export dialog for using ui functions for save and setting global switches. 13885 */ 13886 if (window.ExportDialog != null) 13887 { 13888 ExportDialog.showXmlOption = false; 13889 ExportDialog.showGifOption = false; 13890 13891 ExportDialog.exportFile = function(editorUi, name, format, bg, s, b, dpi, grid) 13892 { 13893 var graph = editorUi.editor.graph; 13894 13895 if (format == 'xml') 13896 { 13897 editorUi.hideDialog(); 13898 editorUi.saveData(name, 'xml', mxUtils.getXml(editorUi.editor.getGraphXml()), 'text/xml'); 13899 } 13900 else if (format == 'svg') 13901 { 13902 editorUi.hideDialog(); 13903 editorUi.saveData(name, 'svg', mxUtils.getXml(graph.getSvg(bg, s, b)), 'image/svg+xml'); 13904 } 13905 else 13906 { 13907 var data = editorUi.getFileData(true, null, null, null, null, true); 13908 var bounds = graph.getGraphBounds(); 13909 var w = Math.floor(bounds.width * s / graph.view.scale); 13910 var h = Math.floor(bounds.height * s / graph.view.scale); 13911 13912 if (data.length <= MAX_REQUEST_SIZE && w * h < MAX_AREA) 13913 { 13914 editorUi.hideDialog(); 13915 13916 if ((format == 'png' || format == 'jpg' || format == 'jpeg') && editorUi.isExportToCanvas()) 13917 { 13918 if (format == 'png') 13919 { 13920 editorUi.exportImage(s, bg == null || bg == 'none', true, 13921 false, false, b, true, false, null, grid, dpi); 13922 } 13923 else 13924 { 13925 editorUi.exportImage(s, false, true, 13926 false, false, b, true, false, 'jpeg', grid); 13927 } 13928 } 13929 else 13930 { 13931 var extras = {globalVars: graph.getExportVariables()}; 13932 13933 if (grid) 13934 { 13935 extras.grid = { 13936 size: graph.gridSize, 13937 steps: graph.view.gridSteps, 13938 color: graph.view.gridColor 13939 }; 13940 } 13941 13942 editorUi.saveRequest(name, format, 13943 function(newTitle, base64) 13944 { 13945 return new mxXmlRequest(EXPORT_URL, 'format=' + format + '&base64=' + (base64 || '0') + 13946 ((newTitle != null) ? '&filename=' + encodeURIComponent(newTitle) : '') + 13947 '&extras=' + encodeURIComponent(JSON.stringify(extras)) + 13948 (dpi > 0? '&dpi=' + dpi : '') + 13949 '&bg=' + ((bg != null) ? bg : 'none') + '&w=' + w + '&h=' + h + 13950 '&border=' + b + '&xml=' + encodeURIComponent(data)); 13951 }); 13952 } 13953 } 13954 else 13955 { 13956 mxUtils.alert(mxResources.get('drawingTooLarge')); 13957 } 13958 } 13959 }; 13960 } 13961 13962 EditorUi.prototype.getDiagramTextContent = function() 13963 { 13964 this.editor.graph.setEnabled(false); 13965 var graph = this.editor.graph; 13966 13967 var allPagesTxt = ''; 13968 13969 if (this.pages != null) 13970 { 13971 for (var i = 0; i < this.pages.length; i++) 13972 { 13973 var pageGraph = graph; 13974 13975 if (this.currentPage != this.pages[i]) 13976 { 13977 pageGraph = this.createTemporaryGraph(graph.getStylesheet()); 13978 this.updatePageRoot(this.pages[i]); 13979 pageGraph.model.setRoot(this.pages[i].root); 13980 } 13981 allPagesTxt += this.pages[i].getName() + ' ' + pageGraph.getIndexableText() + ' '; 13982 } 13983 } 13984 else 13985 { 13986 allPagesTxt = graph.getIndexableText(); 13987 } 13988 13989 this.editor.graph.setEnabled(true); 13990 return allPagesTxt; 13991 }; 13992 13993 EditorUi.prototype.showRemotelyStoredLibrary = function(title) 13994 { 13995 var selectedLibs = {}; 13996 var div = document.createElement('div'); 13997 div.style.whiteSpace = 'nowrap'; 13998 var graph = this.editor.graph; 13999 14000 var hd = document.createElement('h3'); 14001 mxUtils.write(hd, mxUtils.htmlEntities(title)); 14002 hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px'; 14003 div.appendChild(hd); 14004 14005 var libsSection = document.createElement('div'); 14006 libsSection.style.cssText = 'border:1px solid lightGray;overflow: auto;height:300px'; 14007 14008 libsSection.innerHTML = '<div style="text-align:center;padding:8px;"><img src="' + IMAGE_PATH + '/spin.gif"></div>'; 14009 14010 var loadedLibs = {}; 14011 14012 try 14013 { 14014 var custLibs = mxSettings.getCustomLibraries(); 14015 14016 for (var j = 0; j < custLibs.length; j++) 14017 { 14018 var l = custLibs[j]; 14019 14020 if (l.substring(0, 1) == 'R') 14021 { 14022 var libDesc = JSON.parse(decodeURIComponent(l.substring(1))); 14023 loadedLibs[libDesc[0]] = { 14024 id: libDesc[0], 14025 title: libDesc[1], 14026 downloadUrl: libDesc[2] 14027 }; 14028 } 14029 } 14030 } 14031 catch(e){} 14032 14033 this.remoteInvoke('getCustomLibraries', null, null, function(libsList) 14034 { 14035 libsSection.innerHTML = ''; 14036 14037 if (libsList.length == 0) 14038 { 14039 libsSection.innerHTML = '<div style="text-align:center;padding-top:20px;color:gray;">' + 14040 mxUtils.htmlEntities(mxResources.get('noLibraries')) + '</div>'; 14041 } 14042 else 14043 { 14044 for (var i = 0; i < libsList.length; i++) 14045 { 14046 var lib = libsList[i]; 14047 14048 if (loadedLibs[lib.id]) 14049 { 14050 selectedLibs[lib.id] = lib; 14051 } 14052 14053 var libCheck = this.addCheckbox(libsSection, lib.title, loadedLibs[lib.id]); 14054 14055 (function(lib2, check) 14056 { 14057 mxEvent.addListener(check, 'change', function() 14058 { 14059 if (this.checked) 14060 { 14061 selectedLibs[lib2.id] = lib2; 14062 } 14063 else 14064 { 14065 delete selectedLibs[lib2.id]; 14066 } 14067 }); 14068 })(lib, libCheck) 14069 } 14070 } 14071 }, mxUtils.bind(this, function(e) 14072 { 14073 libsSection.innerHTML = ''; 14074 var status = document.createElement('div'); 14075 status.style.padding = '8px'; 14076 status.style.textAlign = 'center'; 14077 mxUtils.write(status, mxResources.get('error') + ': '); 14078 mxUtils.write(status, (e != null && e.message != null) ? 14079 e.message : mxResources.get('unknownError')); 14080 libsSection.appendChild(status); 14081 })); 14082 14083 div.appendChild(libsSection); 14084 14085 var dlg = new CustomDialog(this, div, mxUtils.bind(this, function() 14086 { 14087 this.spinner.spin(document.body, mxResources.get('loading')); 14088 var pendingLibs = 0; 14089 14090 for (var id in selectedLibs) 14091 { 14092 if (loadedLibs[id] != null) continue; //already loaded! 14093 14094 pendingLibs++; 14095 14096 (mxUtils.bind(this, function(lib) 14097 { 14098 this.remoteInvoke('getFileContent', [lib.downloadUrl], null, mxUtils.bind(this, function(libContent) 14099 { 14100 pendingLibs--; 14101 14102 if (pendingLibs == 0) this.spinner.stop(); 14103 14104 try 14105 { 14106 this.loadLibrary(new RemoteLibrary(this, libContent, lib)); 14107 } 14108 catch (e) 14109 { 14110 this.handleError(e, mxResources.get('errorLoadingFile')); 14111 } 14112 }), mxUtils.bind(this, function() 14113 { 14114 pendingLibs--; 14115 14116 if (pendingLibs == 0) this.spinner.stop(); 14117 14118 this.handleError(null, mxResources.get('errorLoadingFile')); 14119 })); 14120 }))(selectedLibs[id]); 14121 } 14122 14123 for (var id in loadedLibs) 14124 { 14125 if (!selectedLibs[id]) //Removed 14126 { 14127 this.closeLibrary(new RemoteLibrary(this, null, loadedLibs[id])); //create a dummy library such that we can call closeLibrary 14128 } 14129 } 14130 14131 if (pendingLibs == 0) this.spinner.stop(); 14132 }), null, null, 'https://www.diagrams.net/doc/faq/custom-libraries-confluence-cloud'); 14133 this.showDialog(dlg.container, 340, 390, true, true, null, null, null, null, true); 14134 }; 14135 14136 //Remote invokation, currently limited to functions in EditorUi (and its sub objects) for security reasons 14137 //White-listed functions and some info about it 14138 EditorUi.prototype.remoteInvokableFns = { 14139 getDiagramTextContent: {isAsync: false}, 14140 getLocalStorageFile: {isAsync: false, allowedDomains: ['app.diagrams.net']}, 14141 getLocalStorageFileNames: {isAsync: false, allowedDomains: ['app.diagrams.net']}, 14142 setMigratedFlag: {isAsync: false, allowedDomains: ['app.diagrams.net']} 14143 }; 14144 14145 EditorUi.prototype.remoteInvokeCallbacks = []; 14146 EditorUi.prototype.remoteInvokeQueue = []; 14147 14148 EditorUi.prototype.handleRemoteInvokeReady = function(remoteWin) 14149 { 14150 this.remoteWin = remoteWin; 14151 14152 for (var i = 0; i < this.remoteInvokeQueue.length; i++) 14153 { 14154 remoteWin.postMessage(this.remoteInvokeQueue[i], '*'); 14155 } 14156 14157 this.remoteInvokeQueue = []; 14158 }; 14159 14160 EditorUi.prototype.handleRemoteInvokeResponse = function(msg) 14161 { 14162 var msgMarkers = msg.msgMarkers; 14163 var callback = this.remoteInvokeCallbacks[msgMarkers.callbackId]; 14164 14165 if (callback == null) 14166 { 14167 throw new Error('No callback for ' + ((msgMarkers != null) ? msgMarkers.callbackId : 'null')); 14168 } 14169 else if (msg.error) 14170 { 14171 if (callback.error) callback.error(msg.error.errResp); 14172 } 14173 else if (callback.callback) 14174 { 14175 callback.callback.apply(this, msg.resp); 14176 } 14177 14178 this.remoteInvokeCallbacks[msgMarkers.callbackId] = null; //set it to null only to keep the index 14179 }; 14180 14181 EditorUi.prototype.remoteInvoke = function(remoteFn, remoteFnArgs, msgMarkers, callback, error) 14182 { 14183 var acceptResponse = true; 14184 14185 var timeoutThread = window.setTimeout(mxUtils.bind(this, function() 14186 { 14187 acceptResponse = false; 14188 error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout')}); 14189 }), this.timeout); 14190 14191 var wrapper = mxUtils.bind(this, function() 14192 { 14193 window.clearTimeout(timeoutThread); 14194 14195 if (acceptResponse) 14196 { 14197 callback.apply(this, arguments); 14198 } 14199 }); 14200 14201 var errWrapper = mxUtils.bind(this, function() 14202 { 14203 window.clearTimeout(timeoutThread); 14204 14205 if (acceptResponse) 14206 { 14207 error.apply(this, arguments); 14208 } 14209 }); 14210 14211 msgMarkers = msgMarkers || {}; 14212 msgMarkers.callbackId = this.remoteInvokeCallbacks.length; 14213 this.remoteInvokeCallbacks.push({callback: wrapper, error: errWrapper}); 14214 var msg = JSON.stringify({event: 'remoteInvoke', funtionName: remoteFn, functionArgs: remoteFnArgs, msgMarkers: msgMarkers}); 14215 14216 if (this.remoteWin != null) //remote invoke is ready 14217 { 14218 this.remoteWin.postMessage(msg, '*'); 14219 } 14220 else 14221 { 14222 this.remoteInvokeQueue.push(msg); 14223 } 14224 }; 14225 14226 EditorUi.prototype.handleRemoteInvoke = function(msg, origin) 14227 { 14228 var sendResponse = mxUtils.bind(this, function(resp, error) 14229 { 14230 var respMsg = {event: 'remoteInvokeResponse', msgMarkers: msg.msgMarkers}; 14231 14232 if (error != null) 14233 { 14234 respMsg.error = {errResp: error}; 14235 } 14236 else if (resp != null) 14237 { 14238 respMsg.resp = resp; 14239 } 14240 14241 this.remoteWin.postMessage(JSON.stringify(respMsg), '*'); 14242 }); 14243 14244 try 14245 { 14246 //Remote invoke are allowed to call functions in AC 14247 var funtionName = msg.funtionName; 14248 var functionInfo = this.remoteInvokableFns[funtionName]; 14249 14250 if (functionInfo != null && typeof this[funtionName] === 'function') 14251 { 14252 if (functionInfo.allowedDomains) 14253 { 14254 var allowed = false; 14255 14256 for (var i = 0; i < functionInfo.allowedDomains.length; i++) 14257 { 14258 if (origin == 'https://' + functionInfo.allowedDomains[i]) 14259 { 14260 allowed = true; 14261 break; 14262 } 14263 } 14264 14265 if (!allowed) 14266 { 14267 sendResponse(null, 'Invalid Call: ' + funtionName + ' is not allowed.'); 14268 return; 14269 } 14270 } 14271 14272 var functionArgs = msg.functionArgs; 14273 14274 //Confirm functionArgs are not null and is array, otherwise, discard it 14275 if (!Array.isArray(functionArgs)) 14276 { 14277 functionArgs = []; 14278 } 14279 14280 //for functions with callbacks (async) we assume last two arguments are success, error 14281 if (functionInfo.isAsync) 14282 { 14283 //success 14284 functionArgs.push(function() 14285 { 14286 sendResponse(Array.prototype.slice.apply(arguments)); 14287 }); 14288 14289 //error 14290 functionArgs.push(function(err) 14291 { 14292 sendResponse(null, err || 'Unkown Error'); 14293 }); 14294 14295 this[funtionName].apply(this, functionArgs); 14296 } 14297 else 14298 { 14299 var resp = this[funtionName].apply(this, functionArgs); 14300 14301 sendResponse([resp]); 14302 } 14303 } 14304 else 14305 { 14306 sendResponse(null, 'Invalid Call: ' + funtionName + ' is not found.'); 14307 } 14308 } 14309 catch(e) 14310 { 14311 sendResponse(null, 'Invalid Call: An error occurred, ' + e.message); 14312 } 14313 }; 14314 14315 /** 14316 * Opens the application keystore. 14317 */ 14318 EditorUi.prototype.openDatabase = function(success, error) 14319 { 14320 if (this.database == null) 14321 { 14322 var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB; 14323 14324 if (indexedDB != null) 14325 { 14326 try 14327 { 14328 var req = indexedDB.open('database', 2); 14329 14330 req.onupgradeneeded = function(e) 14331 { 14332 try 14333 { 14334 var db = req.result; 14335 14336 if (e.oldVersion < 1) 14337 { 14338 // Version 1 is the first version of the database. 14339 db.createObjectStore('objects', {keyPath: 'key'}); 14340 } 14341 14342 if (e.oldVersion < 2) 14343 { 14344 // Version 2 introduces browser file storage. 14345 db.createObjectStore('files', {keyPath: 'title'}); 14346 db.createObjectStore('filesInfo', {keyPath: 'title'}); 14347 EditorUi.migrateStorageFiles = isLocalStorage; 14348 } 14349 } 14350 catch (e) 14351 { 14352 if (error != null) 14353 { 14354 error(e); 14355 } 14356 } 14357 } 14358 14359 req.onsuccess = mxUtils.bind(this, function(e) 14360 { 14361 var db = req.result; 14362 this.database = db; 14363 14364 if (EditorUi.migrateStorageFiles) 14365 { 14366 StorageFile.migrate(db); 14367 EditorUi.migrateStorageFiles = false; 14368 } 14369 14370 if (location.host == 'app.diagrams.net' && !this.drawioMigrationStarted) 14371 { 14372 this.drawioMigrationStarted = true; 14373 14374 this.getDatabaseItem('.drawioMigrated3', mxUtils.bind(this, function(value) 14375 { 14376 if (value && urlParams['forceMigration'] != '1') //Already migrated 14377 { 14378 return; 14379 } 14380 14381 var drawioFrame = document.createElement('iframe'); 14382 drawioFrame.style.display = 'none'; 14383 drawioFrame.setAttribute('src', 'https://www.draw.io?embed=1&proto=json&forceMigration=' + urlParams['forceMigration']); 14384 document.body.appendChild(drawioFrame); 14385 var collectNames = true, allDone = false; 14386 var fileNames, index = 0; 14387 14388 var markAsMigrated = mxUtils.bind(this, function() 14389 { 14390 allDone = true; 14391 this.setDatabaseItem('.drawioMigrated3', true); 14392 drawioFrame.contentWindow.postMessage(JSON.stringify({action: 'remoteInvoke', funtionName: 'setMigratedFlag'}), '*'); 14393 }); 14394 14395 var next = mxUtils.bind(this, function() 14396 { 14397 index++; 14398 fetchOneFile(); 14399 }); 14400 14401 var fetchOneFile = mxUtils.bind(this, function() 14402 { 14403 try 14404 { 14405 if (index >= fileNames.length) 14406 { 14407 markAsMigrated(); 14408 return; 14409 } 14410 14411 var fileTitle = fileNames[index]; 14412 14413 StorageFile.getFileContent(this, fileTitle, mxUtils.bind(this, function(data) 14414 { 14415 if (data == null || (fileTitle == '.scratchpad' && data == this.emptyLibraryXml)) //Don't overwrite 14416 { 14417 drawioFrame.contentWindow.postMessage(JSON.stringify({action: 'remoteInvoke', funtionName: 'getLocalStorageFile', functionArgs: [fileTitle]}), '*'); 14418 } 14419 else 14420 { 14421 next(); 14422 } 14423 }), next); //Ignore errors 14424 } 14425 catch(e) 14426 { 14427 //Log error 14428 console.log(e); 14429 } 14430 }); 14431 14432 var importOneFile = mxUtils.bind(this, function(file) 14433 { 14434 try 14435 { 14436 this.setDatabaseItem(null, [{ 14437 title: file.title, 14438 size: file.data.length, 14439 lastModified: Date.now(), 14440 type: file.isLib? 'L' : 'F' 14441 }, { 14442 title: file.title, 14443 data: file.data 14444 }], next, next /* Ignore errors */, ['filesInfo', 'files']); 14445 } 14446 catch(e) 14447 { 14448 //Log error 14449 console.log(e); 14450 } 14451 }); 14452 14453 var messageListener = mxUtils.bind(this, function(evt) 14454 { 14455 try 14456 { 14457 //Only accept messages from migration iframe 14458 if (evt.source != drawioFrame.contentWindow) 14459 { 14460 return; 14461 } 14462 14463 var drawMsg = {}; 14464 14465 try 14466 { 14467 drawMsg = JSON.parse(evt.data); 14468 } 14469 catch(e){} //Ignore 14470 14471 if (drawMsg.event == 'init') 14472 { 14473 drawioFrame.contentWindow.postMessage(JSON.stringify({action: 'remoteInvokeReady'}), '*'); 14474 drawioFrame.contentWindow.postMessage(JSON.stringify({action: 'remoteInvoke', funtionName: 'getLocalStorageFileNames'}), '*'); 14475 } 14476 else if (drawMsg.event == 'remoteInvokeResponse' && !allDone) 14477 { 14478 if (collectNames) 14479 { 14480 if (drawMsg.resp != null && drawMsg.resp.length > 0 && drawMsg.resp[0] != null) 14481 { 14482 fileNames = drawMsg.resp[0]; 14483 collectNames = false; 14484 fetchOneFile(); 14485 } 14486 else 14487 { 14488 //Nothing in draw.io localStorage 14489 markAsMigrated(); 14490 } 14491 } 14492 else 14493 { 14494 //Add the file, then move to the next 14495 if (drawMsg.resp != null && drawMsg.resp.length > 0 && drawMsg.resp[0] != null) 14496 { 14497 importOneFile(drawMsg.resp[0]); 14498 } 14499 else 14500 { 14501 next(); 14502 } 14503 } 14504 } 14505 } 14506 catch(e) 14507 { 14508 console.log(e); 14509 } 14510 }); 14511 14512 window.addEventListener('message', messageListener); 14513 })); //Ignore errors 14514 } 14515 14516 success(db); 14517 14518 db.onversionchange = function() 14519 { 14520 //TODO Handle DB revision update while code is running 14521 // Save open file and request a page reload before closing the DB 14522 db.close(); 14523 }; 14524 }); 14525 14526 req.onerror = error; 14527 14528 req.onblocked = function() 14529 { 14530 //TODO Use this when a new version is introduced 14531 // there's another open connection to same database 14532 // and it wasn't closed after db.onversionchange triggered for them 14533 }; 14534 } 14535 catch (e) 14536 { 14537 if (error != null) 14538 { 14539 error(e); 14540 } 14541 } 14542 } 14543 else if (error != null) 14544 { 14545 error(); 14546 } 14547 } 14548 else 14549 { 14550 success(this.database); 14551 } 14552 }; 14553 14554 /** 14555 * Add/Update item(s) in the database. It supports multiple stores transactions by sending an array of data, storeName 14556 * (key is optional, can be an array also if multiple stores are needed) 14557 */ 14558 EditorUi.prototype.setDatabaseItem = function(key, data, success, error, storeName) 14559 { 14560 this.openDatabase(mxUtils.bind(this, function(db) 14561 { 14562 try 14563 { 14564 storeName = storeName || 'objects'; 14565 14566 if (!Array.isArray(storeName)) 14567 { 14568 storeName = [storeName]; 14569 key = [key]; 14570 data = [data]; 14571 } 14572 14573 var trx = db.transaction(storeName, 'readwrite'); 14574 trx.oncomplete = success; 14575 trx.onerror = error; 14576 14577 for (var i = 0; i < storeName.length; i++) 14578 { 14579 trx.objectStore(storeName[i]).put(key != null && key[i] != null? {key: key[i], data: data[i]} : data[i]); 14580 } 14581 } 14582 catch (e) 14583 { 14584 if (error != null) 14585 { 14586 error(e); 14587 } 14588 } 14589 }), error); 14590 }; 14591 14592 /** 14593 * Removes the item for the given key from the database. 14594 */ 14595 EditorUi.prototype.removeDatabaseItem = function(key, success, error, storeName) 14596 { 14597 this.openDatabase(mxUtils.bind(this, function(db) 14598 { 14599 storeName = storeName || 'objects'; 14600 14601 if (!Array.isArray(storeName)) 14602 { 14603 storeName = [storeName]; 14604 key = [key]; 14605 } 14606 14607 var trx = db.transaction(storeName, 'readwrite'); 14608 trx.oncomplete = success; 14609 trx.onerror = error; 14610 14611 for (var i = 0; i < storeName.length; i++) 14612 { 14613 trx.objectStore(storeName[i]).delete(key[i]); 14614 } 14615 }), error); 14616 }; 14617 14618 /** 14619 * Returns one item from the database. 14620 */ 14621 EditorUi.prototype.getDatabaseItem = function(key, success, error, storeName) 14622 { 14623 this.openDatabase(mxUtils.bind(this, function(db) 14624 { 14625 try 14626 { 14627 storeName = storeName || 'objects'; 14628 var trx = db.transaction([storeName], 'readonly'); 14629 var req = trx.objectStore(storeName).get(key); 14630 14631 req.onsuccess = function() 14632 { 14633 success(req.result); 14634 }; 14635 14636 req.onerror = error; 14637 } 14638 catch (e) 14639 { 14640 if (error != null) 14641 { 14642 error(e); 14643 } 14644 } 14645 }), error); 14646 }; 14647 14648 /** 14649 * Returns all items from the database. 14650 */ 14651 EditorUi.prototype.getDatabaseItems = function(success, error, storeName) 14652 { 14653 this.openDatabase(mxUtils.bind(this, function(db) 14654 { 14655 try 14656 { 14657 storeName = storeName || 'objects'; 14658 var trx = db.transaction([storeName], 'readonly'); 14659 var req = trx.objectStore(storeName).openCursor( 14660 IDBKeyRange.lowerBound(0)); 14661 var items = []; 14662 14663 req.onsuccess = function(e) 14664 { 14665 if (e.target.result == null) 14666 { 14667 success(items); 14668 } 14669 else 14670 { 14671 items.push(e.target.result.value); 14672 e.target.result.continue(); 14673 } 14674 }; 14675 14676 req.onerror = error; 14677 } 14678 catch (e) 14679 { 14680 if (error != null) 14681 { 14682 error(e); 14683 } 14684 } 14685 }), error); 14686 }; 14687 14688 /** 14689 * Returns all item keys from the database. 14690 */ 14691 EditorUi.prototype.getDatabaseItemKeys = function(success, error, storeName) 14692 { 14693 this.openDatabase(mxUtils.bind(this, function(db) 14694 { 14695 try 14696 { 14697 storeName = storeName || 'objects'; 14698 var trx = db.transaction([storeName], 'readonly'); 14699 var req = trx.objectStore(storeName).getAllKeys(); 14700 14701 req.onsuccess = function() 14702 { 14703 success(req.result); 14704 }; 14705 14706 req.onerror = error; 14707 } 14708 catch (e) 14709 { 14710 if (error != null) 14711 { 14712 error(e); 14713 } 14714 } 14715 }), error); 14716 }; 14717 /** 14718 * Comments: We need these functions as wrapper of File functions in order to facilitate 14719 * overriding them if comments are needed without having a file (e.g. Confluence Plugin) 14720 */ 14721 14722 /** 14723 * Are comments supported 14724 */ 14725 EditorUi.prototype.commentsSupported = function() 14726 { 14727 var file = this.getCurrentFile(); 14728 14729 return file != null? file.commentsSupported() : false; 14730 }; 14731 14732 /** 14733 * Show refresh button? 14734 */ 14735 EditorUi.prototype.commentsRefreshNeeded = function() 14736 { 14737 var file = this.getCurrentFile(); 14738 14739 return file != null? file.commentsRefreshNeeded() : true; 14740 }; 14741 14742 /** 14743 * Show save button? 14744 */ 14745 EditorUi.prototype.commentsSaveNeeded = function() 14746 { 14747 var file = this.getCurrentFile(); 14748 14749 return file != null? file.commentsSaveNeeded() : false; 14750 }; 14751 14752 /** 14753 * Get comments 14754 */ 14755 EditorUi.prototype.getComments = function(success, error) 14756 { 14757 var file = this.getCurrentFile(); 14758 14759 if (file != null) 14760 { 14761 file.getComments(success, error); 14762 } 14763 else 14764 { 14765 success([]); //placeholder 14766 } 14767 }; 14768 14769 /** 14770 * Add a comment 14771 */ 14772 EditorUi.prototype.addComment = function(comment, success, error) 14773 { 14774 var file = this.getCurrentFile(); 14775 14776 if (file != null) 14777 { 14778 file.addComment(comment, success, error); 14779 } 14780 else 14781 { 14782 success(Date.now()); //placeholder 14783 } 14784 }; 14785 14786 /** 14787 * Can add a reply to a reply 14788 */ 14789 EditorUi.prototype.canReplyToReplies = function() 14790 { 14791 var file = this.getCurrentFile(); 14792 14793 return file != null? file.canReplyToReplies() : true; 14794 }; 14795 14796 /** 14797 * Can add comments (The permission to comment) 14798 */ 14799 EditorUi.prototype.canComment = function() 14800 { 14801 var file = this.getCurrentFile(); 14802 14803 return file != null? file.canComment() : true; 14804 }; 14805 14806 /** 14807 * Get a new comment object 14808 */ 14809 EditorUi.prototype.newComment = function(content, user) 14810 { 14811 var file = this.getCurrentFile(); 14812 14813 if (file != null) 14814 { 14815 return file.newComment(content, user) 14816 } 14817 else 14818 { 14819 return new DrawioComment(this, null, content, Date.now(), Date.now(), false, user); 14820 } 14821 }; 14822 14823 //==================================================== End of comments ================================================================= 14824 14825 /** 14826 * Does revisions history available 14827 */ 14828 EditorUi.prototype.isRevisionHistorySupported = function() 14829 { 14830 var file = this.getCurrentFile(); 14831 14832 return file != null && file.isRevisionHistorySupported(); 14833 }; 14834 14835 /** 14836 * Get revisions of current file 14837 */ 14838 EditorUi.prototype.getRevisions = function(success, error) 14839 { 14840 var file = this.getCurrentFile(); 14841 14842 if (file != null && file.getRevisions) 14843 { 14844 file.getRevisions(success, error); 14845 } 14846 else 14847 { 14848 error({message: mxResources.get('unknownError')}); 14849 } 14850 }; 14851 14852 /** 14853 * Is revisions history enabled 14854 */ 14855 EditorUi.prototype.isRevisionHistoryEnabled = function() 14856 { 14857 var file = this.getCurrentFile(); 14858 14859 return file != null && 14860 ((file.constructor == DriveFile && file.isEditable()) || 14861 file.constructor == DropboxFile); 14862 }; 14863 14864 //===========Adding methods to find the service running draw.io and allowing calling draw.io remote services 14865 EditorUi.prototype.getServiceName = function() 14866 { 14867 return 'draw.io'; 14868 }; 14869 14870 EditorUi.prototype.addRemoteServiceSecurityCheck = function(xhr) 14871 { 14872 //Using a standard header with specific sequence 14873 xhr.setRequestHeader('Content-Language', 'da, mi, en, de-DE'); 14874 }; 14875 14876 //===========To Be Removed Soon========== 14877 EditorUi.prototype.loadUrl = function(url, success, error, forceBinary, retry, dataUriPrefix, noBinary, headers) 14878 { 14879 EditorUi.logEvent('SHOULD NOT BE CALLED: loadUrl'); 14880 return this.editor.loadUrl(url, success, error, forceBinary, retry, dataUriPrefix, noBinary, headers); 14881 }; 14882 14883 EditorUi.prototype.loadFonts = function(then) 14884 { 14885 EditorUi.logEvent('SHOULD NOT BE CALLED: loadFonts'); 14886 return this.editor.loadFonts(then); 14887 }; 14888 14889 EditorUi.prototype.createSvgDataUri = function(svg) 14890 { 14891 EditorUi.logEvent('SHOULD NOT BE CALLED: createSvgDataUri'); 14892 return Editor.createSvgDataUri(svg); 14893 }; 14894 14895 EditorUi.prototype.embedCssFonts = function(fontCss, then) 14896 { 14897 EditorUi.logEvent('SHOULD NOT BE CALLED: embedCssFonts'); 14898 return this.editor.embedCssFonts(fontCss, then); 14899 }; 14900 14901 EditorUi.prototype.embedExtFonts = function(callback) 14902 { 14903 EditorUi.logEvent('SHOULD NOT BE CALLED: embedExtFonts'); 14904 return this.editor.embedExtFonts(callback); 14905 }; 14906 14907 EditorUi.prototype.exportToCanvas = function(callback, width, imageCache, background, error, limitHeight, 14908 ignoreSelection, scale, transparentBackground, addShadow, converter, graph, border, noCrop, grid, keepTheme) 14909 { 14910 EditorUi.logEvent('SHOULD NOT BE CALLED: exportToCanvas'); 14911 return this.editor.exportToCanvas(callback, width, imageCache, background, error, limitHeight, 14912 ignoreSelection, scale, transparentBackground, addShadow, converter, graph, border, 14913 noCrop, grid, keepTheme); 14914 }; 14915 14916 EditorUi.prototype.createImageUrlConverter = function() 14917 { 14918 EditorUi.logEvent('SHOULD NOT BE CALLED: createImageUrlConverter'); 14919 return this.editor.createImageUrlConverter(); 14920 }; 14921 14922 EditorUi.prototype.convertImages = function(svgRoot, callback, imageCache, converter) 14923 { 14924 EditorUi.logEvent('SHOULD NOT BE CALLED: convertImages'); 14925 return this.editor.convertImages(svgRoot, callback, imageCache, converter); 14926 }; 14927 14928 EditorUi.prototype.convertImageToDataUri = function(url, callback) 14929 { 14930 EditorUi.logEvent('SHOULD NOT BE CALLED: convertImageToDataUri'); 14931 return this.editor.convertImageToDataUri(url, callback); 14932 }; 14933 14934 EditorUi.prototype.base64Encode = function(str) 14935 { 14936 EditorUi.logEvent('SHOULD NOT BE CALLED: base64Encode'); 14937 return Editor.base64Encode(str); 14938 }; 14939 14940 EditorUi.prototype.updateCRC = function(crc, data, off, len) 14941 { 14942 EditorUi.logEvent('SHOULD NOT BE CALLED: updateCRC'); 14943 return Editor.updateCRC(crc, data, off, len); 14944 }; 14945 14946 EditorUi.prototype.crc32 = function(str) 14947 { 14948 EditorUi.logEvent('SHOULD NOT BE CALLED: crc32'); 14949 return Editor.crc32(str); 14950 }; 14951 14952 EditorUi.prototype.writeGraphModelToPng = function(data, type, key, value, error) 14953 { 14954 EditorUi.logEvent('SHOULD NOT BE CALLED: writeGraphModelToPng'); 14955 return Editor.writeGraphModelToPng(data, type, key, value, error); 14956 }; 14957 14958 //=======End of To Be Removed Soon========== 14959 14960 EditorUi.prototype.getLocalStorageFileNames = function() 14961 { 14962 if (localStorage.getItem('.localStorageMigrated') == '1' && urlParams['forceMigration'] != '1') 14963 { 14964 return null; 14965 } 14966 14967 var files = []; 14968 14969 for (var i = 0; i < localStorage.length; i++) 14970 { 14971 var key = localStorage.key(i); 14972 var value = localStorage.getItem(key); 14973 14974 if (key.length > 0 && (key == '.scratchpad' || key.charAt(0) != '.') && value.length > 0) 14975 { 14976 var isFile = (value.substring(0, 8) === '<mxfile ' || 14977 value.substring(0, 5) === '<?xml' || value.substring(0, 12) === '<!--[if IE]>'); 14978 var isLib = (value.substring(0, 11) === '<mxlibrary>'); 14979 14980 if (isFile || isLib) 14981 { 14982 files.push(key); 14983 } 14984 } 14985 } 14986 14987 return files; 14988 }; 14989 14990 EditorUi.prototype.getLocalStorageFile = function(key) 14991 { 14992 if (localStorage.getItem('.localStorageMigrated') == '1' && urlParams['forceMigration'] != '1') 14993 { 14994 return null; 14995 } 14996 14997 var value = localStorage.getItem(key); 14998 return {title: key, data: value, isLib: value.substring(0, 11) === '<mxlibrary>'}; 14999 }; 15000 15001 EditorUi.prototype.setMigratedFlag = function() 15002 { 15003 localStorage.setItem('.localStorageMigrated', '1'); 15004 }; 15005})(); 15006 15007/** 15008 * Comments Window, It is used by both editor and viewer. So, it is here in a common place 15009 */ 15010var CommentsWindow = function(editorUi, x, y, w, h, saveCallback) 15011{ 15012 var readOnly = !editorUi.canComment(); 15013 var canReplyToReplies = editorUi.canReplyToReplies(); 15014 var curEdited = null; 15015 15016 var div = document.createElement('div'); 15017 div.className = 'geCommentsWin'; 15018 div.style.background = (!Editor.isDarkMode()) ? 'whiteSmoke' : Dialog.backdropColor; 15019 15020 var tbarHeight = (!EditorUi.compactUi) ? '30px' : '26px'; 15021 15022 var listDiv = document.createElement('div'); 15023 listDiv.className = 'geCommentsList'; 15024 listDiv.style.backgroundColor = (!Editor.isDarkMode()) ? 'whiteSmoke' : Dialog.backdropColor; 15025 listDiv.style.bottom = (parseInt(tbarHeight) + 7) + 'px'; 15026 div.appendChild(listDiv); 15027 15028 var noComments = document.createElement('span'); 15029 noComments.style.cssText = 'display:none;padding-top:10px;text-align:center;'; 15030 mxUtils.write(noComments, mxResources.get('noCommentsFound')); 15031 15032 var selectionComment = null; 15033 15034 var ldiv = document.createElement('div'); 15035 15036 ldiv.className = 'geToolbarContainer geCommentsToolbar'; 15037 ldiv.style.height = tbarHeight; 15038 ldiv.style.padding = (!EditorUi.compactUi) ? '1px' : '4px 0px 3px 0px'; 15039 ldiv.style.backgroundColor = (!Editor.isDarkMode()) ? 'whiteSmoke' : Dialog.backdropColor; 15040 15041 var link = document.createElement('a'); 15042 link.className = 'geButton'; 15043 15044 function updateNoComments() 15045 { 15046 var divs = listDiv.getElementsByTagName('div'); 15047 var visibleCount = 0; 15048 15049 for (var i = 0; i < divs.length; i++) 15050 { 15051 if (divs[i].style.display != 'none' && divs[i].parentNode == listDiv) 15052 { 15053 visibleCount++; 15054 } 15055 } 15056 15057 noComments.style.display = (visibleCount == 0) ? 'block' : 'none'; 15058 }; 15059 15060 function editComment(comment, cdiv, saveCallback, deleteOnCancel) 15061 { 15062 curEdited = {div: cdiv, comment: comment, saveCallback: saveCallback, deleteOnCancel: deleteOnCancel}; 15063 15064 var commentTxt = cdiv.querySelector('.geCommentTxt'); 15065 var actionsDiv = cdiv.querySelector('.geCommentActionsList'); 15066 15067 var textArea = document.createElement('textarea'); 15068 textArea.className = 'geCommentEditTxtArea'; 15069 textArea.style.minHeight = commentTxt.offsetHeight + 'px'; 15070 textArea.value = comment.content; 15071 cdiv.insertBefore(textArea, commentTxt); 15072 15073 var btnDiv = document.createElement('div'); 15074 btnDiv.className = 'geCommentEditBtns'; 15075 15076 function reset() 15077 { 15078 cdiv.removeChild(textArea); 15079 cdiv.removeChild(btnDiv); 15080 actionsDiv.style.display = 'block'; 15081 commentTxt.style.display = 'block'; 15082 }; 15083 15084 var cancelBtn = mxUtils.button(mxResources.get('cancel'), function() 15085 { 15086 if (deleteOnCancel) 15087 { 15088 cdiv.parentNode.removeChild(cdiv); 15089 updateNoComments(); 15090 } 15091 else 15092 { 15093 reset(); 15094 } 15095 15096 curEdited = null; 15097 }); 15098 15099 cancelBtn.className = 'geCommentEditBtn'; 15100 btnDiv.appendChild(cancelBtn); 15101 15102 var saveBtn = mxUtils.button(mxResources.get('save'), function() 15103 { 15104 commentTxt.innerHTML = ''; 15105 comment.content = textArea.value; 15106 mxUtils.write(commentTxt, comment.content); 15107 reset(); 15108 saveCallback(comment); 15109 curEdited = null; 15110 }); 15111 15112 // Updates modified state and handles placeholder text 15113 mxEvent.addListener(textArea, 'keydown', mxUtils.bind(this, function(evt) 15114 { 15115 if (!mxEvent.isConsumed(evt)) 15116 { 15117 if ((mxEvent.isControlDown(evt) || (mxClient.IS_MAC && 15118 mxEvent.isMetaDown(evt))) && evt.keyCode == 13 /* Ctrl+Enter */) 15119 { 15120 saveBtn.click(); 15121 mxEvent.consume(evt); 15122 } 15123 else if (evt.keyCode == 27 /* Escape */) 15124 { 15125 cancelBtn.click(); 15126 mxEvent.consume(evt); 15127 } 15128 } 15129 })); 15130 15131 // Focused to include in viewport before focusin textbox 15132 saveBtn.focus(); 15133 saveBtn.className = 'geCommentEditBtn gePrimaryBtn'; 15134 btnDiv.appendChild(saveBtn); 15135 15136 cdiv.insertBefore(btnDiv, commentTxt); 15137 actionsDiv.style.display = 'none'; 15138 commentTxt.style.display = 'none'; 15139 textArea.focus(); 15140 }; 15141 15142 function writeCommentDate(comment, dateDiv) 15143 { 15144 dateDiv.innerHTML = ''; 15145 var ts = new Date(comment.modifiedDate); 15146 var str = editorUi.timeSince(ts); 15147 15148 if (str == null) 15149 { 15150 str = mxResources.get('lessThanAMinute'); 15151 } 15152 15153 mxUtils.write(dateDiv, mxResources.get('timeAgo', [str], '{1} ago')); 15154 dateDiv.setAttribute('title', ts.toLocaleDateString() + ' ' + 15155 ts.toLocaleTimeString()); 15156 }; 15157 15158 function showBusy(commentDiv) 15159 { 15160 var busyImg = document.createElement('img'); 15161 busyImg.className = 'geCommentBusyImg'; 15162 busyImg.src= IMAGE_PATH + '/spin.gif'; 15163 commentDiv.appendChild(busyImg); 15164 commentDiv.busyImg = busyImg; 15165 }; 15166 15167 function showError(commentDiv) 15168 { 15169 commentDiv.style.border = '1px solid red'; 15170 commentDiv.removeChild(commentDiv.busyImg); 15171 }; 15172 15173 function showDone(commentDiv) 15174 { 15175 commentDiv.style.border = ''; 15176 commentDiv.removeChild(commentDiv.busyImg); 15177 }; 15178 15179 function addComment(comment, parentArr, parent, level, showResolved) 15180 { 15181 //Skip resolved comments if showResolved is not set 15182 if (!showResolved && comment.isResolved) 15183 { 15184 return; 15185 } 15186 15187 noComments.style.display = 'none'; 15188 15189 var cdiv = document.createElement('div'); 15190 cdiv.className = 'geCommentContainer'; 15191 cdiv.setAttribute('data-commentId', comment.id); 15192 cdiv.style.marginLeft = (level * 20 + 5) + 'px'; 15193 15194 if (comment.isResolved && !Editor.isDarkMode()) 15195 { 15196 cdiv.style.backgroundColor = 'ghostWhite'; 15197 } 15198 15199 var headerDiv = document.createElement('div'); 15200 headerDiv.className = 'geCommentHeader'; 15201 15202 var userImg = document.createElement('img'); 15203 userImg.className = 'geCommentUserImg'; 15204 userImg.src = comment.user.pictureUrl || Editor.userImage; 15205 headerDiv.appendChild(userImg); 15206 15207 var headerTxt = document.createElement('div'); 15208 headerTxt.className = 'geCommentHeaderTxt'; 15209 headerDiv.appendChild(headerTxt); 15210 15211 var usernameDiv = document.createElement('div'); 15212 usernameDiv.className = 'geCommentUsername'; 15213 mxUtils.write(usernameDiv, comment.user.displayName || ''); 15214 headerTxt.appendChild(usernameDiv); 15215 15216 var dateDiv = document.createElement('div'); 15217 dateDiv.className = 'geCommentDate'; 15218 dateDiv.setAttribute('data-commentId', comment.id); 15219 writeCommentDate(comment, dateDiv); 15220 headerTxt.appendChild(dateDiv); 15221 cdiv.appendChild(headerDiv); 15222 15223 var commentTxtDiv = document.createElement('div'); 15224 commentTxtDiv.className = 'geCommentTxt'; 15225 mxUtils.write(commentTxtDiv, comment.content || ''); 15226 cdiv.appendChild(commentTxtDiv); 15227 15228 if (comment.isLocked) 15229 { 15230 cdiv.style.opacity = '0.5'; 15231 } 15232 15233 var actionsDiv = document.createElement('div'); 15234 actionsDiv.className = 'geCommentActions'; 15235 var actionsList = document.createElement('ul'); 15236 actionsList.className = 'geCommentActionsList'; 15237 actionsDiv.appendChild(actionsList); 15238 15239 function addAction(name, evtHandler, hide) 15240 { 15241 var action = document.createElement('li'); 15242 action.className = 'geCommentAction'; 15243 var actionLnk = document.createElement('a'); 15244 actionLnk.className = 'geCommentActionLnk'; 15245 mxUtils.write(actionLnk, name); 15246 action.appendChild(actionLnk); 15247 15248 mxEvent.addListener(actionLnk, 'click', function(evt) 15249 { 15250 evtHandler(evt, comment); 15251 evt.preventDefault(); 15252 mxEvent.consume(evt); 15253 }); 15254 15255 actionsList.appendChild(action); 15256 15257 if (hide) action.style.display = 'none'; 15258 }; 15259 15260 function collectReplies() 15261 { 15262 var replies = []; 15263 var pdiv = cdiv; 15264 15265 function collectReplies(comment) 15266 { 15267 replies.push(pdiv); 15268 15269 if (comment.replies != null) 15270 { 15271 for (var i = 0; i < comment.replies.length; i++) 15272 { 15273 pdiv = pdiv.nextSibling; 15274 collectReplies(comment.replies[i]); 15275 } 15276 } 15277 } 15278 15279 collectReplies(comment); 15280 15281 return {pdiv: pdiv, replies: replies}; 15282 }; 15283 15284 function addReply(initContent, editIt, saveCallback, doResolve, doReopen) 15285 { 15286 var pdiv = collectReplies().pdiv; 15287 15288 var newReply = editorUi.newComment(initContent, editorUi.getCurrentUser()); 15289 newReply.pCommentId = comment.id; 15290 15291 if (comment.replies == null) comment.replies = []; 15292 15293 var replyComment = addComment(newReply, comment.replies, pdiv, level + 1); 15294 15295 function doAddReply() 15296 { 15297 showBusy(replyComment); 15298 15299 comment.addReply(newReply, function(id) 15300 { 15301 newReply.id = id; 15302 comment.replies.push(newReply); 15303 showDone(replyComment); 15304 15305 if (saveCallback) saveCallback(); 15306 15307 }, function(err) 15308 { 15309 doEdit(); 15310 showError(replyComment); 15311 editorUi.handleError(err, null, null, null, 15312 mxUtils.htmlEntities(mxResources.get('objectNotFound'))); 15313 }, doResolve, doReopen); 15314 }; 15315 15316 function doEdit() 15317 { 15318 editComment(newReply, replyComment, function(newReply) 15319 { 15320 doAddReply(); 15321 }, true); 15322 }; 15323 15324 if (editIt) 15325 { 15326 doEdit(); 15327 } 15328 else 15329 { 15330 doAddReply(); 15331 } 15332 }; 15333 15334 if (!readOnly && !comment.isLocked && (level == 0 || canReplyToReplies)) 15335 { 15336 addAction(mxResources.get('reply'), function() 15337 { 15338 addReply('', true); 15339 }, comment.isResolved); 15340 } 15341 15342 var user = editorUi.getCurrentUser(); 15343 15344 if (user != null && user.id == comment.user.id && !readOnly && !comment.isLocked) 15345 { 15346 addAction(mxResources.get('edit'), function() 15347 { 15348 function doEditComment() 15349 { 15350 editComment(comment, cdiv, function() 15351 { 15352 showBusy(cdiv); 15353 15354 comment.editComment(comment.content, function() 15355 { 15356 showDone(cdiv); 15357 }, function(err) 15358 { 15359 showError(cdiv); 15360 doEditComment(); 15361 editorUi.handleError(err, null, null, null, 15362 mxUtils.htmlEntities(mxResources.get('objectNotFound'))); 15363 }); 15364 }); 15365 }; 15366 15367 doEditComment(); 15368 }, comment.isResolved); 15369 15370 addAction(mxResources.get('delete'), function() 15371 { 15372 editorUi.confirm(mxResources.get('areYouSure'), function() 15373 { 15374 showBusy(cdiv); 15375 15376 comment.deleteComment(function(markedOnly) 15377 { 15378 if (markedOnly === true) 15379 { 15380 var commentTxt = cdiv.querySelector('.geCommentTxt'); 15381 commentTxt.innerHTML = ''; 15382 mxUtils.write(commentTxt, mxResources.get('msgDeleted')); 15383 15384 var actions = cdiv.querySelectorAll('.geCommentAction'); 15385 15386 for (var i = 0; i < actions.length; i++) 15387 { 15388 actions[i].parentNode.removeChild(actions[i]); 15389 } 15390 15391 showDone(cdiv); 15392 cdiv.style.opacity = '0.5'; 15393 } 15394 else 15395 { 15396 var replies = collectReplies(comment).replies; 15397 15398 for (var i = 0; i < replies.length; i++) 15399 { 15400 listDiv.removeChild(replies[i]); 15401 } 15402 15403 for (var i = 0; i < parentArr.length; i++) 15404 { 15405 if (parentArr[i] == comment) 15406 { 15407 parentArr.splice(i, 1); 15408 break; 15409 } 15410 } 15411 15412 noComments.style.display = (listDiv.getElementsByTagName('div').length == 0) ? 'block' : 'none'; 15413 } 15414 }, function(err) 15415 { 15416 showError(cdiv); 15417 editorUi.handleError(err, null, null, null, 15418 mxUtils.htmlEntities(mxResources.get('objectNotFound'))); 15419 }); 15420 }); 15421 }, comment.isResolved); 15422 } 15423 15424 if (!readOnly && !comment.isLocked && level == 0) //Resolve is a top-level action only 15425 { 15426 function toggleResolve(evt) 15427 { 15428 function doToggle() 15429 { 15430 var resolveActionLnk = evt.target; 15431 resolveActionLnk.innerHTML = ''; 15432 15433 comment.isResolved = !comment.isResolved; 15434 mxUtils.write(resolveActionLnk, comment.isResolved? mxResources.get('reopen') : mxResources.get('resolve')); 15435 var actionsDisplay = comment.isResolved? 'none' : ''; 15436 var replies = collectReplies(comment).replies; 15437 var color = (Editor.isDarkMode()) ? 'transparent' : (comment.isResolved? 'ghostWhite' : 'white'); 15438 15439 for (var i = 0; i < replies.length; i++) 15440 { 15441 replies[i].style.backgroundColor = color; 15442 15443 var forOpenActions = replies[i].querySelectorAll('.geCommentAction'); 15444 15445 for (var j = 0; j < forOpenActions.length; j ++) 15446 { 15447 if (forOpenActions[j] == resolveActionLnk.parentNode) continue; 15448 15449 forOpenActions[j].style.display = actionsDisplay; 15450 } 15451 15452 if (!resolvedChecked) 15453 { 15454 replies[i].style.display = 'none'; 15455 } 15456 } 15457 15458 updateNoComments(); 15459 }; 15460 15461 if (comment.isResolved) 15462 { 15463 addReply(mxResources.get('reOpened') + ': ', true, doToggle, false, true); 15464 } 15465 else 15466 { 15467 addReply(mxResources.get('markedAsResolved'), false, doToggle, true); 15468 } 15469 }; 15470 15471 addAction(comment.isResolved? mxResources.get('reopen') : mxResources.get('resolve'), toggleResolve); 15472 } 15473 15474 cdiv.appendChild(actionsDiv); 15475 15476 if (parent != null) 15477 { 15478 listDiv.insertBefore(cdiv, parent.nextSibling); 15479 } 15480 else 15481 { 15482 listDiv.appendChild(cdiv); 15483 } 15484 15485 for (var i = 0; comment.replies != null && i < comment.replies.length; i++) 15486 { 15487 var reply = comment.replies[i]; 15488 reply.isResolved = comment.isResolved; //copy isResolved to child comments (replies) 15489 addComment(reply, comment.replies, null, level + 1, showResolved); 15490 } 15491 15492 if (curEdited != null) 15493 { 15494 if (curEdited.comment.id == comment.id) 15495 { 15496 var origContent = comment.content; 15497 comment.content = curEdited.comment.content; 15498 editComment(comment, cdiv, curEdited.saveCallback, curEdited.deleteOnCancel); 15499 comment.content = origContent; 15500 } 15501 else if (curEdited.comment.id == null && curEdited.comment.pCommentId == comment.id) 15502 { 15503 listDiv.appendChild(curEdited.div); 15504 editComment(curEdited.comment, curEdited.div, curEdited.saveCallback, curEdited.deleteOnCancel); 15505 } 15506 } 15507 15508 return cdiv; 15509 }; 15510 15511 if (!readOnly) 15512 { 15513 var addLink = link.cloneNode(); 15514 addLink.innerHTML = '<div class="geSprite geSprite-plus" style="display:inline-block;"></div>'; 15515 addLink.setAttribute('title', mxResources.get('create') + '...'); 15516 15517 mxEvent.addListener(addLink, 'click', function(evt) 15518 { 15519 var newComment = editorUi.newComment('', editorUi.getCurrentUser()); 15520 var newCommentDiv = addComment(newComment, comments, null, 0); 15521 15522 function doAddComment() 15523 { 15524 editComment(newComment, newCommentDiv, function(newComment) 15525 { 15526 showBusy(newCommentDiv); 15527 15528 editorUi.addComment(newComment, function(id) 15529 { 15530 newComment.id = id; 15531 comments.push(newComment); 15532 showDone(newCommentDiv); 15533 }, function(err) 15534 { 15535 showError(newCommentDiv); 15536 doAddComment(); 15537 editorUi.handleError(err, null, null, null, 15538 mxUtils.htmlEntities(mxResources.get('objectNotFound'))); 15539 }); 15540 }, true); 15541 } 15542 15543 doAddComment(); 15544 evt.preventDefault(); 15545 mxEvent.consume(evt); 15546 }); 15547 15548 ldiv.appendChild(addLink); 15549 } 15550 15551 var resolvedLink = link.cloneNode(); 15552 resolvedLink.innerHTML = '<img src="' + IMAGE_PATH + '/check.png" style="width: 16px; padding: 2px;">'; 15553 resolvedLink.setAttribute('title', mxResources.get('showResolved')); 15554 var resolvedChecked = false; 15555 15556 if (Editor.isDarkMode()) 15557 { 15558 resolvedLink.style.filter = 'invert(100%)'; 15559 } 15560 15561 mxEvent.addListener(resolvedLink, 'click', function(evt) 15562 { 15563 resolvedChecked = !resolvedChecked; 15564 15565 this.className = resolvedChecked? 'geButton geCheckedBtn' : 'geButton'; 15566 refresh(); 15567 15568 evt.preventDefault(); 15569 mxEvent.consume(evt); 15570 }); 15571 15572 ldiv.appendChild(resolvedLink); 15573 15574 if (editorUi.commentsRefreshNeeded()) 15575 { 15576 var refreshLink = link.cloneNode(); 15577 refreshLink.innerHTML = '<img src="' + IMAGE_PATH + '/update16.png" style="width: 16px; padding: 2px;">'; 15578 refreshLink.setAttribute('title', mxResources.get('refresh')); 15579 15580 if (Editor.isDarkMode()) 15581 { 15582 refreshLink.style.filter = 'invert(100%)'; 15583 } 15584 15585 mxEvent.addListener(refreshLink, 'click', function(evt) 15586 { 15587 refresh(); 15588 15589 evt.preventDefault(); 15590 mxEvent.consume(evt); 15591 }); 15592 15593 ldiv.appendChild(refreshLink); 15594 } 15595 15596 if (editorUi.commentsSaveNeeded()) 15597 { 15598 var saveLink = link.cloneNode(); 15599 saveLink.innerHTML = '<img src="' + IMAGE_PATH + '/save.png" style="width: 20px; padding: 2px;">'; 15600 saveLink.setAttribute('title', mxResources.get('save')); 15601 15602 if (Editor.isDarkMode()) 15603 { 15604 saveLink.style.filter = 'invert(100%)'; 15605 } 15606 15607 mxEvent.addListener(saveLink, 'click', function(evt) 15608 { 15609 saveCallback(); 15610 15611 evt.preventDefault(); 15612 mxEvent.consume(evt); 15613 }); 15614 15615 ldiv.appendChild(saveLink); 15616 } 15617 15618 div.appendChild(ldiv); 15619 15620 var comments = []; 15621 15622 var refresh = mxUtils.bind(this, function() 15623 { 15624 this.hasError = false; 15625 15626 if (curEdited != null) 15627 { 15628 try 15629 { 15630 curEdited.div = curEdited.div.cloneNode(true); 15631 var commentEditTxt = curEdited.div.querySelector('.geCommentEditTxtArea'); 15632 var commentEditBtns = curEdited.div.querySelector('.geCommentEditBtns'); 15633 15634 curEdited.comment.content = commentEditTxt.value; 15635 commentEditTxt.parentNode.removeChild(commentEditTxt); 15636 commentEditBtns.parentNode.removeChild(commentEditBtns); 15637 } 15638 catch (e) 15639 { 15640 editorUi.handleError(e); 15641 } 15642 } 15643 15644 listDiv.innerHTML = '<div style="padding-top:10px;text-align:center;"><img src="' + IMAGE_PATH + '/spin.gif" valign="middle"> ' + 15645 mxUtils.htmlEntities(mxResources.get('loading')) + '...</div>'; 15646 15647 canReplyToReplies = editorUi.canReplyToReplies(); 15648 15649 if (editorUi.commentsSupported()) 15650 { 15651 editorUi.getComments(function(list) 15652 { 15653 function sortReplies(replies) 15654 { 15655 if (replies != null) 15656 { 15657 //Sort replies old to new 15658 replies.sort(function(r1, r2) 15659 { 15660 return new Date(r1.modifiedDate) - new Date(r2.modifiedDate); 15661 }); 15662 15663 for (var i = 0; i < replies.length; i++) 15664 { 15665 sortReplies(replies[i].replies); 15666 } 15667 } 15668 }; 15669 15670 //Sort comments old to new 15671 list.sort(function(c1, c2) 15672 { 15673 return new Date(c1.modifiedDate) - new Date(c2.modifiedDate); 15674 }); 15675 15676 listDiv.innerHTML = ''; 15677 listDiv.appendChild(noComments); 15678 noComments.style.display = 'block'; 15679 comments = list; 15680 15681 for (var i = 0; i < comments.length; i++) 15682 { 15683 sortReplies(comments[i].replies); 15684 addComment(comments[i], comments, null, 0, resolvedChecked); 15685 } 15686 15687 //New comment case 15688 if (curEdited != null && curEdited.comment.id == null && curEdited.comment.pCommentId == null) 15689 { 15690 listDiv.appendChild(curEdited.div); 15691 editComment(curEdited.comment, curEdited.div, curEdited.saveCallback, curEdited.deleteOnCancel); 15692 } 15693 15694 }, mxUtils.bind(this, function(err) 15695 { 15696 listDiv.innerHTML = mxUtils.htmlEntities(mxResources.get('error') + (err && err.message? ': ' + err.message : '')); 15697 this.hasError = true; 15698 })); 15699 } 15700 else 15701 { 15702 //TODO if comments are not supported, close the dialog 15703 listDiv.innerHTML = mxUtils.htmlEntities(mxResources.get('error')); 15704 } 15705 }); 15706 15707 refresh(); 15708 15709 this.refreshComments = refresh; 15710 15711 //Refresh the modified date of each comment if the window is visible 15712 var refreshCommentsTime = mxUtils.bind(this, function() 15713 { 15714 if (!this.window.isVisible()) return; //only update if it is visible 15715 15716 var modDateDivs = listDiv.querySelectorAll('.geCommentDate'); 15717 var modDateDivsMap = {}; 15718 15719 for (var i = 0; i < modDateDivs.length; i++) 15720 { 15721 var div = modDateDivs[i]; 15722 modDateDivsMap[div.getAttribute('data-commentId')] = div; 15723 } 15724 15725 function processComment(comment) 15726 { 15727 var div = modDateDivsMap[comment.id]; 15728 15729 if (div == null) return; //resolved comments 15730 15731 writeCommentDate(comment, div); 15732 15733 for (var i = 0; comment.replies != null && i < comment.replies.length; i++) 15734 { 15735 processComment(comment.replies[i]); 15736 } 15737 }; 15738 15739 for (var i = 0; i < comments.length; i++) 15740 { 15741 processComment(comments[i]); 15742 } 15743 }); 15744 15745 //Periodically refresh time every one minute 15746 setInterval(refreshCommentsTime, 60000); 15747 this.refreshCommentsTime = refreshCommentsTime; 15748 15749 this.window = new mxWindow(mxResources.get('comments'), div, x, y, w, h, true, true); 15750 this.window.minimumSize = new mxRectangle(0, 0, 300, 200); 15751 this.window.destroyOnClose = false; 15752 this.window.setMaximizable(false); 15753 this.window.setResizable(true); 15754 this.window.setClosable(true); 15755 this.window.setVisible(true); 15756 15757 this.window.addListener(mxEvent.SHOW, mxUtils.bind(this, function() 15758 { 15759 this.window.fit(); 15760 })); 15761 15762 this.window.setLocation = function(x, y) 15763 { 15764 var iw = window.innerWidth || document.body.clientWidth || document.documentElement.clientWidth; 15765 var ih = window.innerHeight || document.body.clientHeight || document.documentElement.clientHeight; 15766 15767 x = Math.max(0, Math.min(x, iw - this.table.clientWidth)); 15768 y = Math.max(0, Math.min(y, ih - this.table.clientHeight - 48)); 15769 15770 if (this.getX() != x || this.getY() != y) 15771 { 15772 mxWindow.prototype.setLocation.apply(this, arguments); 15773 } 15774 }; 15775 15776 var resizeListener = mxUtils.bind(this, function() 15777 { 15778 var x = this.window.getX(); 15779 var y = this.window.getY(); 15780 15781 this.window.setLocation(x, y); 15782 }); 15783 15784 mxEvent.addListener(window, 'resize', resizeListener); 15785 15786 this.destroy = function() 15787 { 15788 mxEvent.removeListener(window, 'resize', resizeListener); 15789 this.window.destroy(); 15790 } 15791}; 15792 15793/** 15794 * 15795 */ 15796var ConfirmDialog = function(editorUi, message, okFn, cancelFn, okLabel, cancelLabel, 15797 okImg, cancelImg, showRememberOption, imgSrc, maxHeight) 15798{ 15799 var div = document.createElement('div'); 15800 div.style.textAlign = 'center'; 15801 maxHeight = (maxHeight != null) ? maxHeight : 44; 15802 15803 var p2 = document.createElement('div'); 15804 p2.style.padding = '6px'; 15805 p2.style.overflow = 'auto'; 15806 p2.style.maxHeight = maxHeight + 'px'; 15807 p2.style.lineHeight = '1.2em'; 15808 15809 mxUtils.write(p2, message); 15810 div.appendChild(p2); 15811 15812 if (imgSrc != null) 15813 { 15814 var p3 = document.createElement('div'); 15815 p3.style.padding = '6px 0 6px 0'; 15816 var img = document.createElement('img'); 15817 img.setAttribute('src', imgSrc); 15818 p3.appendChild(img); 15819 div.appendChild(p3); 15820 } 15821 15822 var btns = document.createElement('div'); 15823 btns.style.textAlign = 'center'; 15824 btns.style.whiteSpace = 'nowrap'; 15825 15826 var cb = document.createElement('input'); 15827 cb.setAttribute('type', 'checkbox'); 15828 15829 var cancelBtn = mxUtils.button(cancelLabel || mxResources.get('cancel'), function() 15830 { 15831 editorUi.hideDialog(); 15832 15833 if (cancelFn != null) 15834 { 15835 cancelFn(cb.checked); 15836 } 15837 }); 15838 cancelBtn.className = 'geBtn'; 15839 15840 if (cancelImg != null) 15841 { 15842 cancelBtn.innerHTML = cancelImg + '<br>' + cancelBtn.innerHTML; 15843 cancelBtn.style.paddingBottom = '8px'; 15844 cancelBtn.style.paddingTop = '8px'; 15845 cancelBtn.style.height = 'auto'; 15846 cancelBtn.style.width = '40%'; 15847 } 15848 15849 if (editorUi.editor.cancelFirst) 15850 { 15851 btns.appendChild(cancelBtn); 15852 } 15853 15854 var okBtn = mxUtils.button(okLabel || mxResources.get('ok'), function() 15855 { 15856 editorUi.hideDialog(); 15857 15858 if (okFn != null) 15859 { 15860 okFn(cb.checked); 15861 } 15862 }); 15863 btns.appendChild(okBtn); 15864 15865 if (okImg != null) 15866 { 15867 okBtn.innerHTML = okImg + '<br>' + okBtn.innerHTML + '<br>'; 15868 okBtn.style.paddingBottom = '8px'; 15869 okBtn.style.paddingTop = '8px'; 15870 okBtn.style.height = 'auto'; 15871 okBtn.className = 'geBtn'; 15872 okBtn.style.width = '40%'; 15873 } 15874 else 15875 { 15876 okBtn.className = 'geBtn gePrimaryBtn'; 15877 } 15878 15879 if (!editorUi.editor.cancelFirst) 15880 { 15881 btns.appendChild(cancelBtn); 15882 } 15883 15884 div.appendChild(btns); 15885 15886 if (showRememberOption) 15887 { 15888 btns.style.marginTop = '10px'; 15889 var p2 = document.createElement('p'); 15890 p2.style.marginTop = '20px'; 15891 p2.style.marginBottom = '0px'; 15892 p2.appendChild(cb); 15893 var span = document.createElement('span'); 15894 mxUtils.write(span, ' ' + mxResources.get('rememberThisSetting')); 15895 p2.appendChild(span); 15896 div.appendChild(p2); 15897 15898 mxEvent.addListener(span, 'click', function(evt) 15899 { 15900 cb.checked = !cb.checked; 15901 mxEvent.consume(evt); 15902 }); 15903 } 15904 else 15905 { 15906 btns.style.marginTop = '12px'; 15907 } 15908 15909 this.init = function() 15910 { 15911 okBtn.focus(); 15912 }; 15913 15914 this.container = div; 15915}; 15916