1/** 2 * Copyright (c) 2006-2016, JGraph Ltd 3 * Copyright (c) 2006-2016, Gaudenz Alder 4 */ 5/** 6 * Constructs a new point for the optional x and y coordinates. If no 7 * coordinates are given, then the default values for <x> and <y> are used. 8 * @constructor 9 * @class Implements a basic 2D point. Known subclassers = {@link mxRectangle}. 10 * @param {number} x X-coordinate of the point. 11 * @param {number} y Y-coordinate of the point. 12 */ 13/** 14 * Global types 15 */ 16function DiagramPage(node, id) 17{ 18 this.node = node; 19 20 if (id != null) 21 { 22 this.node.setAttribute('id', id); 23 } 24 else if (this.getId() == null) 25 { 26 this.node.setAttribute('id', Editor.guid()); 27 } 28}; 29 30/** 31 * Holds the diagram node for the page. 32 */ 33DiagramPage.prototype.node = null; 34 35/** 36 * Holds the root cell for the page. 37 */ 38DiagramPage.prototype.root = null; 39 40/** 41 * Holds the view state for the page. 42 */ 43DiagramPage.prototype.viewState = null; 44 45/** 46 * 47 */ 48DiagramPage.prototype.getId = function() 49{ 50 return this.node.getAttribute('id'); 51}; 52 53/** 54 * 55 */ 56DiagramPage.prototype.getName = function() 57{ 58 return this.node.getAttribute('name'); 59}; 60 61/** 62 * 63 */ 64DiagramPage.prototype.setName = function(value) 65{ 66 if (value == null) 67 { 68 this.node.removeAttribute('name'); 69 } 70 else 71 { 72 this.node.setAttribute('name', value); 73 } 74}; 75 76/** 77 * Change types 78 */ 79function RenamePage(ui, page, name) 80{ 81 this.ui = ui; 82 this.page = page; 83 this.name = name; 84 this.previous = name; 85} 86 87/** 88 * Implementation of the undoable page rename. 89 */ 90RenamePage.prototype.execute = function() 91{ 92 var tmp = this.page.getName(); 93 this.page.setName(this.previous); 94 this.name = this.previous; 95 this.previous = tmp; 96 97 // Required to update page name in placeholders 98 this.ui.editor.graph.updatePlaceholders(); 99 this.ui.editor.fireEvent(new mxEventObject('pageRenamed')); 100}; 101 102/** 103 * Undoable change of page title. 104 */ 105function MovePage(ui, oldIndex, newIndex) 106{ 107 this.ui = ui; 108 this.oldIndex = oldIndex; 109 this.newIndex = newIndex; 110} 111 112/** 113 * Implementation of the undoable page rename. 114 */ 115MovePage.prototype.execute = function() 116{ 117 this.ui.pages.splice(this.newIndex, 0, this.ui.pages.splice(this.oldIndex, 1)[0]); 118 var tmp = this.oldIndex; 119 this.oldIndex = this.newIndex; 120 this.newIndex = tmp; 121 122 // Required to update page numbers in placeholders 123 this.ui.editor.graph.updatePlaceholders(); 124 this.ui.editor.fireEvent(new mxEventObject('pageMoved')); 125}; 126 127/** 128 * Class: mxCurrentRootChange 129 * 130 * Action to change the current root in a view. 131 * 132 * Constructor: mxCurrentRootChange 133 * 134 * Constructs a change of the current root in the given view. 135 */ 136function SelectPage(ui, page, viewState) 137{ 138 this.ui = ui; 139 this.page = page; 140 this.previousPage = page; 141 this.neverShown = true; 142 143 if (page != null) 144 { 145 this.neverShown = page.viewState == null; 146 this.ui.updatePageRoot(page); 147 148 if (viewState != null) 149 { 150 page.viewState = viewState; 151 this.neverShown = false; 152 } 153 } 154}; 155 156/** 157 * Executes selection of a new page. 158 */ 159SelectPage.prototype.execute = function() 160{ 161 var prevIndex = mxUtils.indexOf(this.ui.pages, this.previousPage); 162 163 if (this.page != null && prevIndex >= 0) 164 { 165 var page = this.ui.currentPage; 166 var editor = this.ui.editor; 167 var graph = editor.graph; 168 169 // Stores current diagram state in the page 170 var data = Graph.compressNode(editor.getGraphXml(true)); 171 mxUtils.setTextContent(page.node, data); 172 page.viewState = graph.getViewState(); 173 page.root = graph.model.root; 174 175 if (page.model != null) 176 { 177 // Updates internal structures of offpage model 178 page.model.rootChanged(page.root); 179 } 180 181 // Transitions for switching pages 182// var curIndex = mxUtils.indexOf(this.ui.pages, page); 183// mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transition', null); 184// mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transform', 185// (curIndex > prevIndex) ? 'translate(-50%,0)' : 'translate(50%,0)'); 186 187 // Removes the previous cells and clears selection 188 graph.view.clear(page.root, true); 189 graph.clearSelection(); 190 191 // Switches the current page 192 this.ui.currentPage = this.previousPage; 193 this.previousPage = page; 194 page = this.ui.currentPage; 195 196 // Switches the root cell and sets the view state 197 graph.model.prefix = Editor.guid() + '-'; 198 graph.model.rootChanged(page.root); 199 graph.setViewState(page.viewState); 200 201 // Handles grid state in chromeless mode which is stored in Editor instance 202 graph.gridEnabled = graph.gridEnabled && (!this.ui.editor.isChromelessView() || 203 urlParams['grid'] == '1'); 204 205 // Updates the display 206 editor.updateGraphComponents(); 207 graph.view.validate(); 208 graph.blockMathRender = true; 209 graph.sizeDidChange(); 210 graph.blockMathRender = false; 211 212// mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transition', 'transform 0.2s'); 213// mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transform', 'translate(0,0)'); 214 215 if (this.neverShown) 216 { 217 this.neverShown = false; 218 graph.selectUnlockedLayer(); 219 } 220 221 // Fires events 222 editor.graph.fireEvent(new mxEventObject(mxEvent.ROOT)); 223 editor.fireEvent(new mxEventObject('pageSelected', 'change', this)); 224 } 225}; 226 227/** 228 * 229 */ 230function ChangePage(ui, page, select, index, noSelect) 231{ 232 SelectPage.call(this, ui, select); 233 this.relatedPage = page; 234 this.index = index; 235 this.previousIndex = null; 236 this.noSelect = noSelect; 237}; 238 239mxUtils.extend(ChangePage, SelectPage); 240 241/** 242 * Function: execute 243 * 244 * Changes the current root of the view. 245 */ 246ChangePage.prototype.execute = function() 247{ 248 // Fires event to setting view state from realtime 249 this.ui.editor.fireEvent(new mxEventObject('beforePageChange', 'change', this)); 250 this.previousIndex = this.index; 251 252 if (this.index == null) 253 { 254 var tmp = mxUtils.indexOf(this.ui.pages, this.relatedPage); 255 this.ui.pages.splice(tmp, 1); 256 this.index = tmp; 257 } 258 else 259 { 260 this.ui.pages.splice(this.index, 0, this.relatedPage); 261 this.index = null; 262 } 263 264 if (!this.noSelect) 265 { 266 SelectPage.prototype.execute.apply(this, arguments); 267 } 268}; 269 270/** 271 * Specifies the height of the tab container. Default is 38. 272 */ 273EditorUi.prototype.tabContainerHeight = 38; 274 275/** 276 * Returns the index of the selected page. 277 */ 278EditorUi.prototype.getSelectedPageIndex = function() 279{ 280 return this.getPageIndex(this.currentPage); 281}; 282 283/** 284 * Returns the index of the given page. 285 */ 286 EditorUi.prototype.getPageIndex = function(page) 287 { 288 var result = null; 289 290 if (this.pages != null && page != null) 291 { 292 for (var i = 0; i < this.pages.length; i++) 293 { 294 if (this.pages[i] == page) 295 { 296 result = i; 297 298 break; 299 } 300 } 301 } 302 303 return result; 304 }; 305 306/** 307 * Returns true if the given string contains an mxfile. 308 */ 309EditorUi.prototype.getPageById = function(id) 310{ 311 if (this.pages != null) 312 { 313 for (var i = 0; i < this.pages.length; i++) 314 { 315 if (this.pages[i].getId() == id) 316 { 317 return this.pages[i]; 318 } 319 } 320 } 321 322 return null; 323}; 324 325/** 326 * Returns the background image for the given page link. 327 */ 328EditorUi.prototype.createImageForPageLink = function(src, sourcePage, sourceGraph) 329{ 330 var comma = src.indexOf(','); 331 var result = null; 332 333 if (comma > 0) 334 { 335 var page = this.getPageById(src.substring(comma + 1)); 336 337 if (page != null && page != sourcePage) 338 { 339 result = this.getImageForPage(page, sourcePage, sourceGraph); 340 result.originalSrc = src; 341 } 342 } 343 344 if (result == null) 345 { 346 result = {originalSrc: src}; 347 } 348 349 return result; 350}; 351 352/** 353 * Returns true if the given string contains an mxfile. 354 */ 355EditorUi.prototype.getImageForPage = function(page, sourcePage, sourceGraph) 356{ 357 sourceGraph = (sourceGraph != null) ? sourceGraph : this.editor.graph; 358 var graphGetGlobalVariable = sourceGraph.getGlobalVariable; 359 var graph = this.createTemporaryGraph(sourceGraph.getStylesheet()); 360 graph.defaultPageBackgroundColor = sourceGraph.defaultPageBackgroundColor; 361 graph.shapeBackgroundColor = sourceGraph.shapeBackgroundColor; 362 graph.shapeForegroundColor = sourceGraph.shapeForegroundColor; 363 var index = this.getPageIndex((sourcePage != null) ? 364 sourcePage : this.currentPage); 365 366 graph.getGlobalVariable = function(name) 367 { 368 if (name == 'pagenumber') 369 { 370 return index + 1; 371 } 372 else if (name == 'page' && sourcePage != null) 373 { 374 return sourcePage.getName(); 375 } 376 else 377 { 378 return graphGetGlobalVariable.apply(this, arguments); 379 } 380 }; 381 382 document.body.appendChild(graph.container); 383 384 this.updatePageRoot(page); 385 graph.model.setRoot(page.root); 386 var svgRoot = graph.getSvg(null, null, null, null, null, 387 null, null, null, null, null, null, true); 388 var bounds = graph.getGraphBounds(); 389 document.body.removeChild(graph.container); 390 391 return new mxImage(Editor.createSvgDataUri(mxUtils.getXml(svgRoot)), 392 bounds.width, bounds.height, bounds.x, bounds.y); 393}; 394 395/** 396 * Returns true if the given string contains an mxfile. 397 */ 398EditorUi.prototype.initPages = function() 399{ 400 if (!this.editor.graph.standalone) 401 { 402 this.actions.addAction('previousPage', mxUtils.bind(this, function() 403 { 404 this.selectNextPage(false); 405 })); 406 407 this.actions.addAction('nextPage', mxUtils.bind(this, function() 408 { 409 this.selectNextPage(true); 410 })); 411 412 if (this.isPagesEnabled()) 413 { 414 this.keyHandler.bindAction(33, true, 'previousPage', true); // Ctrl+Shift+PageUp 415 this.keyHandler.bindAction(34, true, 'nextPage', true); // Ctrl+Shift+PageDown 416 } 417 418 // Updates the tabs after loading the diagram 419 var graph = this.editor.graph; 420 var graphViewValidateBackground = graph.view.validateBackground; 421 422 graph.view.validateBackground = mxUtils.bind(this, function() 423 { 424 if (this.tabContainer != null) 425 { 426 var prevHeight = this.tabContainer.style.height; 427 428 if (this.fileNode == null || this.pages == null || 429 (this.pages.length == 1 && urlParams['pages'] == '0')) 430 { 431 this.tabContainer.style.height = '0px'; 432 } 433 else 434 { 435 this.tabContainer.style.height = this.tabContainerHeight + 'px'; 436 } 437 438 if (prevHeight != this.tabContainer.style.height) 439 { 440 this.refresh(false); 441 } 442 } 443 444 graphViewValidateBackground.apply(graph.view, arguments); 445 }); 446 447 var lastPage = null; 448 449 var updateTabs = mxUtils.bind(this, function() 450 { 451 this.updateTabContainer(); 452 453 // Updates scrollbar positions and backgrounds after validation 454 var p = this.currentPage; 455 456 if (p != null && p != lastPage) 457 { 458 if (p.viewState == null || p.viewState.scrollLeft == null) 459 { 460 this.resetScrollbars(); 461 462 if (graph.isLightboxView()) 463 { 464 this.lightboxFit(); 465 } 466 467 if (this.chromelessResize != null) 468 { 469 graph.container.scrollLeft = 0; 470 graph.container.scrollTop = 0; 471 this.chromelessResize(); 472 } 473 } 474 else 475 { 476 graph.container.scrollLeft = graph.view.translate.x * graph.view.scale + p.viewState.scrollLeft; 477 graph.container.scrollTop = graph.view.translate.y * graph.view.scale + p.viewState.scrollTop; 478 } 479 480 lastPage = p; 481 } 482 483 // Updates layers window 484 if (this.actions.layersWindow != null) 485 { 486 this.actions.layersWindow.refreshLayers(); 487 } 488 489 // Workaround for math if tab is switched before typesetting has stopped 490 if (typeof(MathJax) !== 'undefined' && typeof(MathJax.Hub) !== 'undefined') 491 { 492 // Pending math should not be rendered if the graph has no math enabled 493 if (MathJax.Hub.queue.pending == 1 && this.editor != null && !this.editor.graph.mathEnabled) 494 { 495 // Since there is no way to stop/undo mathjax or 496 // clear the queue we have to refresh after typeset 497 MathJax.Hub.Queue(mxUtils.bind(this, function() 498 { 499 if (this.editor != null) 500 { 501 this.editor.graph.refresh(); 502 } 503 })); 504 } 505 } 506 else if (typeof(Editor.MathJaxClear) !== 'undefined' && (this.editor == null || !this.editor.graph.mathEnabled)) 507 { 508 // Clears our own queue for async loading 509 Editor.MathJaxClear(); 510 } 511 }); 512 513 // Adds a graph model listener to update the view 514 this.editor.graph.model.addListener(mxEvent.CHANGE, mxUtils.bind(this, function(sender, evt) 515 { 516 var edit = evt.getProperty('edit'); 517 var changes = edit.changes; 518 519 for (var i = 0; i < changes.length; i++) 520 { 521 if (changes[i] instanceof SelectPage || 522 changes[i] instanceof RenamePage || 523 changes[i] instanceof MovePage || 524 changes[i] instanceof mxRootChange) 525 { 526 updateTabs(); 527 break; 528 } 529 } 530 })); 531 532 // Updates zoom in toolbar 533 if (this.toolbar != null) 534 { 535 this.editor.addListener('pageSelected', this.toolbar.updateZoom); 536 } 537 } 538}; 539 540/** 541 * Adds the listener for automatically saving the diagram for local changes. 542 */ 543EditorUi.prototype.restoreViewState = function(page, viewState, selection) 544{ 545 var newPage = (page != null) ? this.getPageById(page.getId()) : null; 546 var graph = this.editor.graph; 547 548 if (newPage != null && this.currentPage != null && this.pages != null) 549 { 550 if (newPage != this.currentPage) 551 { 552 this.selectPage(newPage, true, viewState); 553 } 554 else 555 { 556 // TODO: Pass viewState to setGraphXml 557 graph.setViewState(viewState); 558 this.editor.updateGraphComponents(); 559 graph.view.revalidate(); 560 graph.sizeDidChange(); 561 } 562 563 graph.container.scrollLeft = graph.view.translate.x * graph.view.scale + viewState.scrollLeft; 564 graph.container.scrollTop = graph.view.translate.y * graph.view.scale + viewState.scrollTop; 565 graph.restoreSelection(selection); 566 } 567}; 568 569/** 570 * Overrides setDefaultParent 571 */ 572Graph.prototype.createViewState = function(node) 573{ 574 var pv = node.getAttribute('page'); 575 var ps = parseFloat(node.getAttribute('pageScale')); 576 var pw = parseFloat(node.getAttribute('pageWidth')); 577 var ph = parseFloat(node.getAttribute('pageHeight')); 578 var bg = node.getAttribute('background'); 579 var bgImg = this.parseBackgroundImage(node.getAttribute('backgroundImage')); 580 var extFonts = node.getAttribute('extFonts'); 581 582 if (extFonts) 583 { 584 try 585 { 586 extFonts = extFonts.split('|').map(function(ef) 587 { 588 var parts = ef.split('^'); 589 return {name: parts[0], url: parts[1]}; 590 }); 591 } 592 catch(e) 593 { 594 console.log('ExtFonts format error: ' + e.message); 595 } 596 } 597 598 return { 599 gridEnabled: node.getAttribute('grid') != '0', 600 //gridColor: node.getAttribute('gridColor') || mxSettings.getGridColor(uiTheme == 'dark'), 601 gridSize: parseFloat(node.getAttribute('gridSize')) || mxGraph.prototype.gridSize, 602 guidesEnabled: node.getAttribute('guides') != '0', 603 foldingEnabled: node.getAttribute('fold') != '0', 604 shadowVisible: node.getAttribute('shadow') == '1', 605 pageVisible: (this.isLightboxView()) ? false : ((pv != null) ? (pv != '0') : this.defaultPageVisible), 606 background: (bg != null && bg.length > 0) ? bg : null, 607 backgroundImage: bgImg, 608 pageScale: (!isNaN(ps)) ? ps : mxGraph.prototype.pageScale, 609 pageFormat: (!isNaN(pw) && !isNaN(ph)) ? new mxRectangle(0, 0, pw, ph) : 610 ((typeof mxSettings === 'undefined' || this.defaultPageFormat != null) ? 611 mxGraph.prototype.pageFormat : mxSettings.getPageFormat()), 612 tooltips: node.getAttribute('tooltips') != '0', 613 connect: node.getAttribute('connect') != '0', 614 arrows: node.getAttribute('arrows') != '0', 615 mathEnabled: node.getAttribute('math') == '1', 616 selectionCells: null, 617 defaultParent: null, 618 scrollbars: this.defaultScrollbars, 619 scale: 1, 620 hiddenTags: [], 621 extFonts: extFonts || [] 622 }; 623}; 624 625/** 626 * Writes the graph properties from the realtime model to the given mxGraphModel node. 627 */ 628Graph.prototype.saveViewState = function(vs, node, ignoreTransient, resolveReferences) 629{ 630 if (!ignoreTransient) 631 { 632 node.setAttribute('grid', (vs == null || vs.gridEnabled) ? '1' : '0'); 633 node.setAttribute('gridSize', (vs != null) ? vs.gridSize : mxGraph.prototype.gridSize); 634 node.setAttribute('guides', (vs == null || vs.guidesEnabled) ? '1' : '0'); 635 node.setAttribute('tooltips', (vs == null || vs.tooltips) ? '1' : '0'); 636 node.setAttribute('connect', (vs == null || vs.connect) ? '1' : '0'); 637 node.setAttribute('arrows', (vs == null || vs.arrows) ? '1' : '0'); 638 node.setAttribute('page', ((vs == null && this.defaultPageVisible ) || 639 (vs != null && vs.pageVisible)) ? '1' : '0'); 640 641 // Ignores fold to avoid checksum errors for lightbox mode 642 node.setAttribute('fold', (vs == null || vs.foldingEnabled) ? '1' : '0'); 643 } 644 645 node.setAttribute('pageScale', (vs != null && vs.pageScale != null) ? 646 vs.pageScale : mxGraph.prototype.pageScale); 647 648 var pf = (vs != null) ? vs.pageFormat : (typeof mxSettings === 'undefined' || 649 this.defaultPageFormat != null) ? mxGraph.prototype.pageFormat : 650 mxSettings.getPageFormat(); 651 652 if (pf != null) 653 { 654 node.setAttribute('pageWidth', pf.width); 655 node.setAttribute('pageHeight', pf.height); 656 } 657 658 if (vs != null) 659 { 660 if (vs.background != null) 661 { 662 node.setAttribute('background', vs.background); 663 } 664 665 var bgImg = this.getBackgroundImageObject(vs.backgroundImage, resolveReferences); 666 667 if (bgImg != null) 668 { 669 node.setAttribute('backgroundImage', JSON.stringify(bgImg)); 670 } 671 } 672 673 node.setAttribute('math', (vs != null && vs.mathEnabled) ? '1' : '0'); 674 node.setAttribute('shadow', (vs != null && vs.shadowVisible) ? '1' : '0'); 675 676 if (vs != null && vs.extFonts != null && vs.extFonts.length > 0) 677 { 678 node.setAttribute('extFonts', vs.extFonts.map(function(ef) 679 { 680 return ef.name + '^' + ef.url; 681 }).join('|')); 682 } 683}; 684 685/** 686 * Overrides setDefaultParent 687 */ 688Graph.prototype.getViewState = function() 689{ 690 return { 691 defaultParent: this.defaultParent, 692 currentRoot: this.view.currentRoot, 693 gridEnabled: this.gridEnabled, 694 //gridColor: this.view.gridColor, 695 gridSize: this.gridSize, 696 guidesEnabled: this.graphHandler.guidesEnabled, 697 foldingEnabled: this.foldingEnabled, 698 shadowVisible: this.shadowVisible, 699 scrollbars: this.scrollbars, 700 pageVisible: this.pageVisible, 701 background: this.background, 702 backgroundImage: this.backgroundImage, 703 pageScale: this.pageScale, 704 pageFormat: this.pageFormat, 705 tooltips: this.tooltipHandler.isEnabled(), 706 connect: this.connectionHandler.isEnabled(), 707 arrows: this.connectionArrowsEnabled, 708 scale: this.view.scale, 709 scrollLeft: this.container.scrollLeft - this.view.translate.x * this.view.scale, 710 scrollTop: this.container.scrollTop - this.view.translate.y * this.view.scale, 711 translate: this.view.translate.clone(), 712 lastPasteXml: this.lastPasteXml, 713 pasteCounter: this.pasteCounter, 714 mathEnabled: this.mathEnabled, 715 hiddenTags: this.hiddenTags, 716 extFonts: this.extFonts 717 }; 718}; 719 720/** 721 * Overrides setDefaultParent 722 */ 723Graph.prototype.setViewState = function(state, removeOldExtFonts) 724{ 725 if (state != null) 726 { 727 this.lastPasteXml = state.lastPasteXml; 728 this.pasteCounter = state.pasteCounter || 0; 729 this.mathEnabled = state.mathEnabled; 730 this.gridEnabled = state.gridEnabled; 731 //this.view.gridColor = state.gridColor; 732 this.gridSize = state.gridSize; 733 this.graphHandler.guidesEnabled = state.guidesEnabled; 734 this.foldingEnabled = state.foldingEnabled; 735 this.setShadowVisible(state.shadowVisible, false); 736 this.scrollbars = state.scrollbars; 737 this.pageVisible = !this.isViewer() && state.pageVisible; 738 this.background = state.background; 739 this.pageScale = state.pageScale; 740 this.pageFormat = state.pageFormat; 741 this.view.currentRoot = state.currentRoot; 742 this.defaultParent = state.defaultParent; 743 this.connectionArrowsEnabled = state.arrows; 744 this.setTooltips(state.tooltips); 745 this.setConnectable(state.connect); 746 this.setBackgroundImage(state.backgroundImage); 747 this.hiddenTags = state.hiddenTags; 748 749 var oldExtFonts = this.extFonts; 750 this.extFonts = state.extFonts || []; 751 752 // Removing old fonts is important for real-time synchronization 753 // But, for page change, it results in undesirable font flicker 754 if (removeOldExtFonts && oldExtFonts != null) 755 { 756 for (var i = 0; i < oldExtFonts.length; i++) 757 { 758 var fontElem = document.getElementById('extFont_' + oldExtFonts[i].name); 759 760 if (fontElem != null) 761 { 762 fontElem.parentNode.removeChild(fontElem); 763 } 764 } 765 } 766 767 for (var i = 0; i < this.extFonts.length; i++) 768 { 769 this.addExtFont(this.extFonts[i].name, this.extFonts[i].url, true); 770 } 771 772 if (state.scale != null) 773 { 774 this.view.scale = state.scale; 775 } 776 else 777 { 778 this.view.scale = 1; 779 } 780 781 // Checks if current root or default parent have been removed 782 if (this.view.currentRoot != null && 783 !this.model.contains(this.view.currentRoot)) 784 { 785 this.view.currentRoot = null; 786 } 787 788 if (this.defaultParent != null && 789 !this.model.contains(this.defaultParent)) 790 { 791 this.setDefaultParent(null); 792 this.selectUnlockedLayer(); 793 } 794 795 if (state.translate != null) 796 { 797 this.view.translate = state.translate; 798 } 799 } 800 else 801 { 802 this.view.currentRoot = null; 803 this.view.scale = 1; 804 this.gridEnabled = this.defaultGridEnabled; 805 this.gridSize = mxGraph.prototype.gridSize; 806 this.pageScale = mxGraph.prototype.pageScale; 807 this.pageFormat = (typeof mxSettings === 'undefined' || this.defaultPageFormat != null) ? 808 mxGraph.prototype.pageFormat : mxSettings.getPageFormat(); 809 this.pageVisible = this.defaultPageVisible; 810 this.background = null; 811 this.backgroundImage = null; 812 this.scrollbars = this.defaultScrollbars; 813 this.graphHandler.guidesEnabled = true; 814 this.foldingEnabled = true; 815 this.setShadowVisible(false, false); 816 this.defaultParent = null; 817 this.setTooltips(true); 818 this.setConnectable(true); 819 this.lastPasteXml = null; 820 this.pasteCounter = 0; 821 this.mathEnabled = this.defaultMathEnabled; 822 this.connectionArrowsEnabled = true; 823 this.hiddenTags = []; 824 this.extFonts = []; 825 } 826 827 // Implicit settings 828 this.pageBreaksVisible = this.pageVisible; 829 this.preferPageSize = this.pageVisible; 830 this.fireEvent(new mxEventObject('viewStateChanged', 'state', state)); 831}; 832 833Graph.prototype.addExtFont = function(fontName, fontUrl, dontRemember) 834{ 835 // KNOWN: Font not added when pasting cells with custom fonts 836 if (fontName && fontUrl) 837 { 838 if (urlParams['ext-fonts'] != '1') 839 { 840 // Adds inserted fonts to font family menu 841 Graph.recentCustomFonts[fontName.toLowerCase()] = {name: fontName, url: fontUrl}; 842 } 843 844 var fontId = 'extFont_' + fontName; 845 846 if (document.getElementById(fontId) == null) 847 { 848 if (fontUrl.indexOf(Editor.GOOGLE_FONTS) == 0) 849 { 850 mxClient.link('stylesheet', fontUrl, null, fontId); 851 } 852 else 853 { 854 var head = document.getElementsByTagName('head')[0]; 855 856 // KNOWN: Should load fonts synchronously 857 var style = document.createElement('style'); 858 859 style.appendChild(document.createTextNode('@font-face {\n' + 860 '\tfont-family: "'+ fontName +'";\n' + 861 '\tsrc: url("'+ fontUrl +'");\n}')); 862 863 style.setAttribute('id', fontId); 864 var head = document.getElementsByTagName('head')[0]; 865 head.appendChild(style); 866 } 867 } 868 869 if (!dontRemember) 870 { 871 if (this.extFonts == null) 872 { 873 this.extFonts = []; 874 } 875 876 var extFonts = this.extFonts, notFound = true; 877 878 for (var i = 0; i < extFonts.length; i++) 879 { 880 if (extFonts[i].name == fontName) 881 { 882 notFound = false; 883 break; 884 } 885 } 886 887 if (notFound) 888 { 889 this.extFonts.push({name: fontName, url: fontUrl}); 890 } 891 } 892 } 893}; 894 895/** 896 * Executes selection of a new page. 897 */ 898EditorUi.prototype.updatePageRoot = function(page, checked) 899{ 900 if (page.root == null) 901 { 902 var node = this.editor.extractGraphModel(page.node, null, checked); 903 var cause = Editor.extractParserError(node); 904 905 if (cause) 906 { 907 throw new Error(cause); 908 } 909 else if (node != null) 910 { 911 page.graphModelNode = node; 912 913 // Converts model XML into page object with root cell 914 page.viewState = this.editor.graph.createViewState(node); 915 var codec = new mxCodec(node.ownerDocument); 916 page.root = codec.decode(node).root; 917 } 918 else 919 { 920 // Initializes page object with new empty root 921 page.root = this.editor.graph.model.createRoot(); 922 } 923 } 924 else if (page.viewState == null) 925 { 926 if (page.graphModelNode == null) 927 { 928 var node = this.editor.extractGraphModel(page.node); 929 930 var cause = Editor.extractParserError(node); 931 932 if (cause) 933 { 934 throw new Error(cause); 935 } 936 else if (node != null) 937 { 938 page.graphModelNode = node; 939 } 940 } 941 942 if (page.graphModelNode != null) 943 { 944 page.viewState = this.editor.graph.createViewState(page.graphModelNode); 945 } 946 } 947 948 return page; 949}; 950 951/** 952 * Returns true if the given string contains an mxfile. 953 */ 954EditorUi.prototype.selectPage = function(page, quiet, viewState) 955{ 956 try 957 { 958 if (page != this.currentPage) 959 { 960 if (this.editor.graph.isEditing()) 961 { 962 this.editor.graph.stopEditing(false); 963 } 964 965 quiet = (quiet != null) ? quiet : false; 966 this.editor.graph.isMouseDown = false; 967 this.editor.graph.reset(); 968 969 var edit = this.editor.graph.model.createUndoableEdit(); 970 971 // Special flag to bypass autosave for this edit 972 edit.ignoreEdit = true; 973 974 var change = new SelectPage(this, page, viewState); 975 change.execute(); 976 edit.add(change); 977 edit.notify(); 978 979 this.editor.graph.tooltipHandler.hide(); 980 981 if (!quiet) 982 { 983 this.editor.graph.model.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit)); 984 } 985 } 986 } 987 catch (e) 988 { 989 this.handleError(e); 990 } 991}; 992 993/** 994 * 995 */ 996EditorUi.prototype.selectNextPage = function(forward) 997{ 998 var next = this.currentPage; 999 1000 if (next != null && this.pages != null) 1001 { 1002 var tmp = mxUtils.indexOf(this.pages, next); 1003 1004 if (forward) 1005 { 1006 this.selectPage(this.pages[mxUtils.mod(tmp + 1, this.pages.length)]); 1007 } 1008 else if (!forward) 1009 { 1010 this.selectPage(this.pages[mxUtils.mod(tmp - 1, this.pages.length)]); 1011 } 1012 } 1013}; 1014 1015/** 1016 * Returns true if the given string contains an mxfile. 1017 */ 1018EditorUi.prototype.insertPage = function(page, index) 1019{ 1020 if (this.editor.graph.isEnabled()) 1021 { 1022 if (this.editor.graph.isEditing()) 1023 { 1024 this.editor.graph.stopEditing(false); 1025 } 1026 1027 page = (page != null) ? page : this.createPage(null, this.createPageId()); 1028 index = (index != null) ? index : this.pages.length; 1029 1030 // Uses model to fire event and trigger autosave 1031 var change = new ChangePage(this, page, page, index); 1032 this.editor.graph.model.execute(change); 1033 } 1034 1035 return page; 1036}; 1037 1038/** 1039 * Returns a unique page ID. 1040 */ 1041EditorUi.prototype.createPageId = function() 1042{ 1043 var id = null; 1044 1045 do 1046 { 1047 id = Editor.guid(); 1048 } while (this.getPageById(id) != null) 1049 1050 return id; 1051}; 1052 1053/** 1054 * Returns a new DiagramPage instance. 1055 */ 1056EditorUi.prototype.createPage = function(name, id) 1057{ 1058 var page = new DiagramPage(this.fileNode.ownerDocument.createElement('diagram'), id); 1059 page.setName((name != null) ? name : this.createPageName()); 1060 1061 return page; 1062}; 1063 1064/** 1065 * Returns a page name. 1066 */ 1067EditorUi.prototype.createPageName = function() 1068{ 1069 // Creates a lookup with names 1070 var existing = {}; 1071 1072 for (var i = 0; i < this.pages.length; i++) 1073 { 1074 var tmp = this.pages[i].getName(); 1075 1076 if (tmp != null && tmp.length > 0) 1077 { 1078 existing[tmp] = tmp; 1079 } 1080 } 1081 1082 // Avoids existing names 1083 var nr = this.pages.length; 1084 var name = null; 1085 1086 do 1087 { 1088 name = mxResources.get('pageWithNumber', [++nr]); 1089 } 1090 while (existing[name] != null); 1091 1092 return name; 1093}; 1094 1095/** 1096 * Removes the given page. 1097 */ 1098EditorUi.prototype.removePage = function(page) 1099{ 1100 try 1101 { 1102 var graph = this.editor.graph; 1103 var tmp = mxUtils.indexOf(this.pages, page); 1104 1105 if (graph.isEnabled() && tmp >= 0) 1106 { 1107 if (this.editor.graph.isEditing()) 1108 { 1109 this.editor.graph.stopEditing(false); 1110 } 1111 1112 graph.model.beginUpdate(); 1113 try 1114 { 1115 var next = this.currentPage; 1116 1117 if (next == page && this.pages.length > 1) 1118 { 1119 if (tmp == this.pages.length - 1) 1120 { 1121 tmp--; 1122 } 1123 else 1124 { 1125 tmp++; 1126 } 1127 1128 next = this.pages[tmp]; 1129 } 1130 else if (this.pages.length <= 1) 1131 { 1132 // Removes label with incorrect page number to force 1133 // default page name which is OK for a single page 1134 next = this.insertPage(); 1135 graph.model.execute(new RenamePage(this, next, 1136 mxResources.get('pageWithNumber', [1]))); 1137 } 1138 1139 // Uses model to fire event to trigger autosave 1140 graph.model.execute(new ChangePage(this, page, next)); 1141 } 1142 finally 1143 { 1144 graph.model.endUpdate(); 1145 } 1146 } 1147 } 1148 catch (e) 1149 { 1150 this.handleError(e); 1151 } 1152 1153 return page; 1154}; 1155 1156/** 1157 * Duplicates the given page. 1158 */ 1159EditorUi.prototype.duplicatePage = function(page, name) 1160{ 1161 var newPage = null; 1162 1163 try 1164 { 1165 var graph = this.editor.graph; 1166 1167 if (graph.isEnabled()) 1168 { 1169 if (graph.isEditing()) 1170 { 1171 graph.stopEditing(); 1172 } 1173 1174 // Clones the current page and takes a snapshot of the graph model and view state 1175 var node = page.node.cloneNode(false); 1176 node.removeAttribute('id'); 1177 1178 var cloneMap = new Object(); 1179 var lookup = graph.createCellLookup([graph.model.root]); 1180 1181 var newPage = new DiagramPage(node); 1182 newPage.root = graph.cloneCell(graph.model.root, null, cloneMap); 1183 newPage.viewState = graph.getViewState(); 1184 1185 // Resets zoom and scrollbar positions 1186 newPage.viewState.scale = 1; 1187 newPage.viewState.scrollLeft = null; 1188 newPage.viewState.scrollTop = null; 1189 newPage.viewState.currentRoot = null; 1190 newPage.viewState.defaultParent = null; 1191 newPage.setName(name); 1192 1193 newPage = this.insertPage(newPage, mxUtils.indexOf(this.pages, page) + 1); 1194 1195 // Updates custom links after inserting into the model for cells to have new IDs 1196 graph.updateCustomLinks(graph.createCellMapping(cloneMap, lookup), [newPage.root]); 1197 } 1198 } 1199 catch (e) 1200 { 1201 this.handleError(e); 1202 } 1203 1204 return newPage; 1205}; 1206 1207/** 1208 * Renames the given page using a dialog. 1209 */ 1210EditorUi.prototype.renamePage = function(page) 1211{ 1212 var graph = this.editor.graph; 1213 1214 if (graph.isEnabled()) 1215 { 1216 var dlg = new FilenameDialog(this, page.getName(), mxResources.get('rename'), mxUtils.bind(this, function(name) 1217 { 1218 if (name != null && name.length > 0) 1219 { 1220 this.editor.graph.model.execute(new RenamePage(this, page, name)); 1221 } 1222 }), mxResources.get('rename')); 1223 this.showDialog(dlg.container, 300, 80, true, true); 1224 dlg.init(); 1225 } 1226 1227 return page; 1228} 1229 1230/** 1231 * Returns true if the given string contains an mxfile. 1232 */ 1233EditorUi.prototype.movePage = function(oldIndex, newIndex) 1234{ 1235 this.editor.graph.model.execute(new MovePage(this, oldIndex, newIndex)); 1236} 1237 1238/** 1239 * Returns true if the given string contains an mxfile. 1240 */ 1241EditorUi.prototype.createTabContainer = function() 1242{ 1243 var div = document.createElement('div'); 1244 div.className = 'geTabContainer'; 1245 div.style.position = 'absolute'; 1246 div.style.whiteSpace = 'nowrap'; 1247 div.style.overflow = 'hidden'; 1248 div.style.height = '0px'; 1249 1250 return div; 1251}; 1252 1253/** 1254 * Returns true if the given string contains an mxfile. 1255 */ 1256EditorUi.prototype.updateTabContainer = function() 1257{ 1258 if (this.tabContainer != null && this.pages != null) 1259 { 1260 var graph = this.editor.graph; 1261 var wrapper = document.createElement('div'); 1262 wrapper.style.position = 'relative'; 1263 wrapper.style.display = 'inline-block'; 1264 wrapper.style.verticalAlign = 'top'; 1265 wrapper.style.height = this.tabContainer.style.height; 1266 wrapper.style.whiteSpace = 'nowrap'; 1267 wrapper.style.overflow = 'hidden'; 1268 wrapper.style.fontSize = '13px'; 1269 1270 // Allows for negative left margin of first tab 1271 wrapper.style.marginLeft = '30px'; 1272 1273 // Automatic tab width to match available width 1274 // TODO: Fix tabWidth in chromeless mode 1275 var btnWidth = (this.editor.isChromelessView()) ? 29 : 59; 1276 var tabWidth = Math.min(140, Math.max(20, (this.tabContainer.clientWidth - btnWidth) / this.pages.length) + 1); 1277 var startIndex = null; 1278 1279 for (var i = 0; i < this.pages.length; i++) 1280 { 1281 // Install drag and drop for page reorder 1282 (mxUtils.bind(this, function(index, tab) 1283 { 1284 if (this.pages[index] == this.currentPage) 1285 { 1286 tab.className = 'geActivePage'; 1287 tab.style.backgroundColor = Editor.isDarkMode() ? Editor.darkColor : '#fff'; 1288 } 1289 else 1290 { 1291 tab.className = 'geInactivePage'; 1292 } 1293 1294 tab.setAttribute('draggable', 'true'); 1295 1296 mxEvent.addListener(tab, 'dragstart', mxUtils.bind(this, function(evt) 1297 { 1298 if (graph.isEnabled()) 1299 { 1300 // Workaround for no DnD on DIV in FF 1301 if (mxClient.IS_FF) 1302 { 1303 // LATER: Check what triggers a parse as XML on this in FF after drop 1304 evt.dataTransfer.setData('Text', '<diagram/>'); 1305 } 1306 1307 startIndex = index; 1308 } 1309 else 1310 { 1311 // Blocks event 1312 mxEvent.consume(evt); 1313 } 1314 })); 1315 1316 mxEvent.addListener(tab, 'dragend', mxUtils.bind(this, function(evt) 1317 { 1318 startIndex = null; 1319 evt.stopPropagation(); 1320 evt.preventDefault(); 1321 })); 1322 1323 mxEvent.addListener(tab, 'dragover', mxUtils.bind(this, function(evt) 1324 { 1325 if (startIndex != null) 1326 { 1327 evt.dataTransfer.dropEffect = 'move'; 1328 } 1329 1330 evt.stopPropagation(); 1331 evt.preventDefault(); 1332 })); 1333 1334 mxEvent.addListener(tab, 'drop', mxUtils.bind(this, function(evt) 1335 { 1336 if (startIndex != null && index != startIndex) 1337 { 1338 // LATER: Shift+drag for merge, ctrl+drag for clone 1339 this.movePage(startIndex, index); 1340 } 1341 1342 evt.stopPropagation(); 1343 evt.preventDefault(); 1344 })); 1345 1346 wrapper.appendChild(tab); 1347 }))(i, this.createTabForPage(this.pages[i], tabWidth, this.pages[i] != this.currentPage, i + 1)); 1348 } 1349 1350 this.tabContainer.innerHTML = ''; 1351 this.tabContainer.appendChild(wrapper); 1352 1353 // Adds floating menu with all pages and insert option 1354 var menutab = this.createPageMenuTab(); 1355 this.tabContainer.appendChild(menutab); 1356 var insertTab = null; 1357 1358 // Not chromeless and not read-only file 1359 if (this.isPageInsertTabVisible()) 1360 { 1361 insertTab = this.createPageInsertTab(); 1362 this.tabContainer.appendChild(insertTab); 1363 } 1364 1365 if (wrapper.clientWidth > this.tabContainer.clientWidth - btnWidth) 1366 { 1367 if (insertTab != null) 1368 { 1369 insertTab.style.position = 'absolute'; 1370 insertTab.style.right = '0px'; 1371 wrapper.style.marginRight = '30px'; 1372 } 1373 1374 var temp = this.createControlTab(4, ' ❮ '); 1375 temp.style.position = 'absolute'; 1376 temp.style.right = (this.editor.chromeless) ? '29px' : '55px'; 1377 temp.style.fontSize = '13pt'; 1378 1379 this.tabContainer.appendChild(temp); 1380 1381 var temp2 = this.createControlTab(4, ' ❯'); 1382 temp2.style.position = 'absolute'; 1383 temp2.style.right = (this.editor.chromeless) ? '0px' : '29px'; 1384 temp2.style.fontSize = '13pt'; 1385 1386 this.tabContainer.appendChild(temp2); 1387 1388 // TODO: Scroll to current page 1389 var dx = Math.max(0, this.tabContainer.clientWidth - ((this.editor.chromeless) ? 86 : 116)); 1390 wrapper.style.width = dx + 'px'; 1391 1392 var fade = 50; 1393 1394 mxEvent.addListener(temp, 'click', mxUtils.bind(this, function(evt) 1395 { 1396 wrapper.scrollLeft -= Math.max(20, dx - 20); 1397 mxUtils.setOpacity(temp, (wrapper.scrollLeft > 0) ? 100 : fade); 1398 mxUtils.setOpacity(temp2, (wrapper.scrollLeft < wrapper.scrollWidth - wrapper.clientWidth) ? 100 : fade); 1399 mxEvent.consume(evt); 1400 })); 1401 1402 mxUtils.setOpacity(temp, (wrapper.scrollLeft > 0) ? 100 : fade); 1403 mxUtils.setOpacity(temp2, (wrapper.scrollLeft < wrapper.scrollWidth - wrapper.clientWidth) ? 100 : fade); 1404 1405 mxEvent.addListener(temp2, 'click', mxUtils.bind(this, function(evt) 1406 { 1407 wrapper.scrollLeft += Math.max(20, dx - 20); 1408 mxUtils.setOpacity(temp, (wrapper.scrollLeft > 0) ? 100 : fade); 1409 mxUtils.setOpacity(temp2, (wrapper.scrollLeft < wrapper.scrollWidth - wrapper.clientWidth) ? 100 : fade); 1410 mxEvent.consume(evt); 1411 })); 1412 } 1413 } 1414}; 1415 1416/** 1417 * Returns true if the given string contains an mxfile. 1418 */ 1419EditorUi.prototype.isPageInsertTabVisible = function() 1420{ 1421 return urlParams['embed'] == 1 || (this.getCurrentFile() != null && 1422 this.getCurrentFile().isEditable()); 1423}; 1424 1425/** 1426 * Returns true if the given string contains an mxfile. 1427 */ 1428EditorUi.prototype.createTab = function(hoverEnabled) 1429{ 1430 var tab = document.createElement('div'); 1431 tab.style.display = 'inline-block'; 1432 tab.style.whiteSpace = 'nowrap'; 1433 tab.style.boxSizing = 'border-box'; 1434 tab.style.position = 'relative'; 1435 tab.style.overflow = 'hidden'; 1436 tab.style.textAlign = 'center'; 1437 tab.style.marginLeft = '-1px'; 1438 tab.style.height = this.tabContainer.clientHeight + 'px'; 1439 tab.style.padding = '12px 4px 8px 4px'; 1440 tab.style.border = Editor.isDarkMode() ? '1px solid #505759' : '1px solid #e8eaed'; 1441 tab.style.borderTopStyle = 'none'; 1442 tab.style.borderBottomStyle = 'none'; 1443 tab.style.backgroundColor = this.tabContainer.style.backgroundColor; 1444 tab.style.cursor = 'move'; 1445 tab.style.color = 'gray'; 1446 1447 if (hoverEnabled) 1448 { 1449 mxEvent.addListener(tab, 'mouseenter', mxUtils.bind(this, function(evt) 1450 { 1451 if (!this.editor.graph.isMouseDown) 1452 { 1453 tab.style.backgroundColor = Editor.isDarkMode() ? 'black' : '#e8eaed'; 1454 mxEvent.consume(evt); 1455 } 1456 })); 1457 1458 mxEvent.addListener(tab, 'mouseleave', mxUtils.bind(this, function(evt) 1459 { 1460 tab.style.backgroundColor = this.tabContainer.style.backgroundColor; 1461 mxEvent.consume(evt); 1462 })); 1463 } 1464 1465 return tab; 1466}; 1467 1468/** 1469 * Returns true if the given string contains an mxfile. 1470 */ 1471EditorUi.prototype.createControlTab = function(paddingTop, html, hoverEnabled) 1472{ 1473 var tab = this.createTab((hoverEnabled != null) ? hoverEnabled : true); 1474 tab.style.lineHeight = this.tabContainerHeight + 'px'; 1475 tab.style.paddingTop = paddingTop + 'px'; 1476 tab.style.cursor = 'pointer'; 1477 tab.style.width = '30px'; 1478 tab.innerHTML = html; 1479 1480 if (tab.firstChild != null && tab.firstChild.style != null) 1481 { 1482 mxUtils.setOpacity(tab.firstChild, 40); 1483 } 1484 1485 return tab; 1486}; 1487 1488/** 1489 * Returns true if the given string contains an mxfile. 1490 */ 1491EditorUi.prototype.createPageMenuTab = function(hoverEnabled) 1492{ 1493 var tab = this.createControlTab(3, '<div class="geSprite geSprite-dots"></div>', hoverEnabled); 1494 tab.setAttribute('title', mxResources.get('pages')); 1495 tab.style.position = 'absolute'; 1496 tab.style.marginLeft = '0px'; 1497 tab.style.top = '0px'; 1498 tab.style.left = '1px'; 1499 1500 var div = tab.getElementsByTagName('div')[0]; 1501 div.style.display = 'inline-block'; 1502 div.style.marginTop = '5px'; 1503 div.style.width = '21px'; 1504 div.style.height = '21px'; 1505 1506 mxEvent.addListener(tab, 'click', mxUtils.bind(this, function(evt) 1507 { 1508 this.editor.graph.popupMenuHandler.hideMenu(); 1509 var menu = new mxPopupMenu(mxUtils.bind(this, function(menu, parent) 1510 { 1511 for (var i = 0; i < this.pages.length; i++) 1512 { 1513 (mxUtils.bind(this, function(index) 1514 { 1515 var item = menu.addItem(this.pages[index].getName(), null, mxUtils.bind(this, function() 1516 { 1517 this.selectPage(this.pages[index]); 1518 }), parent); 1519 1520 var id = this.pages[index].getId(); 1521 item.setAttribute('title', this.pages[index].getName() + 1522 ((id != null) ? ' (' + id + ')' : '') + 1523 ' [' + (index + 1)+ ']'); 1524 1525 // Adds checkmark to current page 1526 if (this.pages[index] == this.currentPage) 1527 { 1528 menu.addCheckmark(item, Editor.checkmarkImage); 1529 } 1530 }))(i); 1531 } 1532 1533 if (this.editor.graph.isEnabled()) 1534 { 1535 menu.addSeparator(parent); 1536 1537 var item = menu.addItem(mxResources.get('insertPage'), null, mxUtils.bind(this, function() 1538 { 1539 this.insertPage(); 1540 }), parent); 1541 1542 var page = this.currentPage; 1543 1544 if (page != null) 1545 { 1546 menu.addSeparator(parent); 1547 var pageName = page.getName(); 1548 1549 menu.addItem(mxResources.get('removeIt', [pageName]), null, mxUtils.bind(this, function() 1550 { 1551 this.removePage(page); 1552 }), parent); 1553 1554 menu.addItem(mxResources.get('renameIt', [pageName]), null, mxUtils.bind(this, function() 1555 { 1556 this.renamePage(page, page.getName()); 1557 }), parent); 1558 1559 menu.addSeparator(parent); 1560 1561 menu.addItem(mxResources.get('duplicateIt', [pageName]), null, mxUtils.bind(this, function() 1562 { 1563 this.duplicatePage(page, mxResources.get('copyOf', [page.getName()])); 1564 }), parent); 1565 } 1566 } 1567 })); 1568 1569 menu.div.className += ' geMenubarMenu'; 1570 menu.smartSeparators = true; 1571 menu.showDisabled = true; 1572 menu.autoExpand = true; 1573 1574 // Disables autoexpand and destroys menu when hidden 1575 menu.hideMenu = mxUtils.bind(this, function() 1576 { 1577 mxPopupMenu.prototype.hideMenu.apply(menu, arguments); 1578 menu.destroy(); 1579 }); 1580 1581 var x = mxEvent.getClientX(evt); 1582 var y = mxEvent.getClientY(evt); 1583 menu.popup(x, y, null, evt); 1584 1585 // Allows hiding by clicking on document 1586 this.setCurrentMenu(menu); 1587 1588 mxEvent.consume(evt); 1589 })); 1590 1591 return tab; 1592}; 1593 1594/** 1595 * Returns true if the given string contains an mxfile. 1596 */ 1597EditorUi.prototype.createPageInsertTab = function() 1598{ 1599 var tab = this.createControlTab(4, '<div class="geSprite geSprite-plus"></div>'); 1600 tab.setAttribute('title', mxResources.get('insertPage')); 1601 var graph = this.editor.graph; 1602 1603 mxEvent.addListener(tab, 'click', mxUtils.bind(this, function(evt) 1604 { 1605 this.insertPage(); 1606 mxEvent.consume(evt); 1607 })); 1608 1609 var div = tab.getElementsByTagName('div')[0]; 1610 div.style.display = 'inline-block'; 1611 div.style.width = '21px'; 1612 div.style.height = '21px'; 1613 1614 return tab; 1615}; 1616 1617/** 1618 * Returns true if the given string contains an mxfile. 1619 */ 1620EditorUi.prototype.createTabForPage = function(page, tabWidth, hoverEnabled, pageNumber) 1621{ 1622 var tab = this.createTab(hoverEnabled); 1623 var name = page.getName() || mxResources.get('untitled'); 1624 var id = page.getId(); 1625 tab.setAttribute('title', name + ((id != null) ? ' (' + id + ')' : '') + ' [' + pageNumber + ']'); 1626 mxUtils.write(tab, name); 1627 tab.style.maxWidth = tabWidth + 'px'; 1628 tab.style.width = tabWidth + 'px'; 1629 this.addTabListeners(page, tab); 1630 1631 if (tabWidth > 42) 1632 { 1633 tab.style.textOverflow = 'ellipsis'; 1634 } 1635 1636 return tab; 1637}; 1638 1639/** 1640 * Translates this point by the given vector. 1641 * 1642 * @param {number} dx X-coordinate of the translation. 1643 * @param {number} dy Y-coordinate of the translation. 1644 */ 1645EditorUi.prototype.addTabListeners = function(page, tab) 1646{ 1647 mxEvent.disableContextMenu(tab); 1648 var graph = this.editor.graph; 1649 var model = graph.model; 1650 1651 mxEvent.addListener(tab, 'dblclick', mxUtils.bind(this, function(evt) 1652 { 1653 this.renamePage(page) 1654 mxEvent.consume(evt); 1655 })); 1656 1657 var menuWasVisible = false; 1658 var pageWasActive = false; 1659 1660 mxEvent.addGestureListeners(tab, mxUtils.bind(this, function(evt) 1661 { 1662 // Do not consume event here to allow for drag and drop of tabs 1663 menuWasVisible = this.currentMenu != null; 1664 pageWasActive = page == this.currentPage; 1665 1666 if (!graph.isMouseDown && !pageWasActive) 1667 { 1668 this.selectPage(page); 1669 } 1670 }), null, mxUtils.bind(this, function(evt) 1671 { 1672 if (graph.isEnabled() && !graph.isMouseDown && 1673 ((mxEvent.isTouchEvent(evt) && pageWasActive) || 1674 mxEvent.isPopupTrigger(evt))) 1675 { 1676 graph.popupMenuHandler.hideMenu(); 1677 this.hideCurrentMenu(); 1678 1679 if (!mxEvent.isTouchEvent(evt) || !menuWasVisible) 1680 { 1681 var menu = new mxPopupMenu(this.createPageMenu(page)); 1682 1683 menu.div.className += ' geMenubarMenu'; 1684 menu.smartSeparators = true; 1685 menu.showDisabled = true; 1686 menu.autoExpand = true; 1687 1688 // Disables autoexpand and destroys menu when hidden 1689 menu.hideMenu = mxUtils.bind(this, function() 1690 { 1691 mxPopupMenu.prototype.hideMenu.apply(menu, arguments); 1692 this.resetCurrentMenu(); 1693 menu.destroy(); 1694 }); 1695 1696 var x = mxEvent.getClientX(evt); 1697 var y = mxEvent.getClientY(evt); 1698 menu.popup(x, y, null, evt); 1699 this.setCurrentMenu(menu, tab); 1700 } 1701 1702 mxEvent.consume(evt); 1703 } 1704 })); 1705}; 1706 1707/** 1708 * Returns an absolute URL to the given page or null of absolute links 1709 * to pages are not supported in this file. 1710 */ 1711EditorUi.prototype.getLinkForPage = function(page, params, lightbox) 1712{ 1713 if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp) 1714 { 1715 var file = this.getCurrentFile(); 1716 1717 if (file != null && file.constructor != LocalFile && this.getServiceName() == 'draw.io') 1718 { 1719 var search = this.getSearch(['create', 'title', 'mode', 'url', 'drive', 'splash', 1720 'state', 'clibs', 'ui', 'viewbox', 'hide-pages']); 1721 search += ((search.length == 0) ? '?' : '&') + 'page-id=' + page.getId(); 1722 1723 if (params != null) 1724 { 1725 search += '&' + params.join('&'); 1726 } 1727 1728 return ((lightbox && urlParams['dev'] != '1') ? EditorUi.lightboxHost : 1729 (((mxClient.IS_CHROMEAPP || EditorUi.isElectronApp || 1730 !(/.*\.draw\.io$/.test(window.location.hostname))) ? 1731 EditorUi.drawHost : 'https://' + window.location.host))) + 1732 '/' + search + '#' + file.getHash(); 1733 } 1734 } 1735 1736 return null; 1737}; 1738 1739/** 1740 * Returns true if the given string contains an mxfile. 1741 */ 1742EditorUi.prototype.createPageMenu = function(page, label) 1743{ 1744 return mxUtils.bind(this, function(menu, parent) 1745 { 1746 var graph = this.editor.graph; 1747 var model = graph.model; 1748 1749 menu.addItem(mxResources.get('insert'), null, mxUtils.bind(this, function() 1750 { 1751 this.insertPage(null, mxUtils.indexOf(this.pages, page) + 1); 1752 }), parent); 1753 1754 menu.addItem(mxResources.get('delete'), null, mxUtils.bind(this, function() 1755 { 1756 this.removePage(page); 1757 }), parent); 1758 1759 menu.addItem(mxResources.get('rename'), null, mxUtils.bind(this, function() 1760 { 1761 this.renamePage(page, label); 1762 }), parent); 1763 1764 var url = this.getLinkForPage(page); 1765 1766 if (url != null) 1767 { 1768 menu.addSeparator(parent); 1769 1770 menu.addItem(mxResources.get('link'), null, mxUtils.bind(this, function() 1771 { 1772 this.showPublishLinkDialog(mxResources.get('url'), true, null, null, 1773 mxUtils.bind(this, function(linkTarget, linkColor, allPages, lightbox, editLink, layers) 1774 { 1775 var params = this.createUrlParameters(linkTarget, linkColor, allPages, lightbox, editLink, layers); 1776 1777 if (!allPages) 1778 { 1779 params.push('hide-pages=1'); 1780 } 1781 1782 if (!graph.isSelectionEmpty()) 1783 { 1784 var bounds = graph.getBoundingBox(graph.getSelectionCells()); 1785 1786 var t = graph.view.translate; 1787 var s = graph.view.scale; 1788 bounds.width /= s; 1789 bounds.height /= s; 1790 bounds.x = bounds.x / s - t.x; 1791 bounds.y = bounds.y / s - t.y; 1792 1793 params.push('viewbox=' + encodeURIComponent(JSON.stringify({x: Math.round(bounds.x), y: Math.round(bounds.y), 1794 width: Math.round(bounds.width), height: Math.round(bounds.height), border: 100}))); 1795 } 1796 1797 var dlg = new EmbedDialog(this, this.getLinkForPage(page, params, lightbox)); 1798 this.showDialog(dlg.container, 450, 240, true, true); 1799 dlg.init(); 1800 })); 1801 })); 1802 } 1803 1804 menu.addSeparator(parent); 1805 1806 menu.addItem(mxResources.get('duplicate'), null, mxUtils.bind(this, function() 1807 { 1808 this.duplicatePage(page, mxResources.get('copyOf', [page.getName()])); 1809 }), parent); 1810 1811 if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp && this.getServiceName() == 'draw.io') 1812 { 1813 menu.addSeparator(parent); 1814 1815 menu.addItem(mxResources.get('openInNewWindow'), null, mxUtils.bind(this, function() 1816 { 1817 this.editor.editAsNew(this.getFileData(true, null, null, null, true, true)); 1818 }), parent); 1819 } 1820 }); 1821}; 1822 1823// Overrides refresh to repaint tab container 1824(function() 1825{ 1826 var editorUiRefresh = EditorUi.prototype.refresh; 1827 1828 EditorUi.prototype.refresh = function(sizeDidChange) 1829 { 1830 editorUiRefresh.apply(this, arguments); 1831 this.updateTabContainer(); 1832 } 1833})(); 1834 1835//Overrides ChangePageSetup codec to exclude page 1836(function() 1837{ 1838 var codec = mxCodecRegistry.getCodec(ChangePageSetup); 1839 codec.exclude.push('page'); 1840})(); 1841 1842//Registers codec for MovePage 1843(function() 1844{ 1845 var codec = new mxObjectCodec(new MovePage(), ['ui']); 1846 1847 codec.beforeDecode = function(dec, node, obj) 1848 { 1849 obj.ui = dec.ui; 1850 1851 return node; 1852 }; 1853 1854 codec.afterDecode = function(dec, node, obj) 1855 { 1856 var tmp = obj.oldIndex; 1857 obj.oldIndex = obj.newIndex; 1858 obj.newIndex = tmp; 1859 1860 return obj; 1861 }; 1862 1863 mxCodecRegistry.register(codec); 1864})(); 1865 1866//Registers codec for RenamePage 1867(function() 1868{ 1869 var codec = new mxObjectCodec(new RenamePage(), ['ui', 'page']); 1870 1871 codec.beforeDecode = function(dec, node, obj) 1872 { 1873 obj.ui = dec.ui; 1874 1875 return node; 1876 }; 1877 1878 codec.afterDecode = function(dec, node, obj) 1879 { 1880 var tmp = obj.previous; 1881 obj.previous = obj.name; 1882 obj.name = tmp; 1883 1884 return obj; 1885 }; 1886 1887 mxCodecRegistry.register(codec); 1888})(); 1889 1890//Registers codec for ChangePage 1891(function() 1892{ 1893 var codec = new mxObjectCodec(new ChangePage(), ['ui', 'relatedPage', 1894 'index', 'neverShown', 'page', 'previousPage']); 1895 1896 var viewStateIgnored = ['defaultParent', 'currentRoot', 'scrollLeft', 1897 'scrollTop', 'scale', 'translate', 'lastPasteXml', 'pasteCounter']; 1898 1899 codec.afterEncode = function(enc, obj, node) 1900 { 1901 node.setAttribute('relatedPage', obj.relatedPage.getId()) 1902 1903 if (obj.index == null) 1904 { 1905 node.setAttribute('name', obj.relatedPage.getName()); 1906 1907 if (obj.relatedPage.viewState != null) 1908 { 1909 node.setAttribute('viewState', JSON.stringify( 1910 obj.relatedPage.viewState, function(key, value) 1911 { 1912 return (mxUtils.indexOf(viewStateIgnored, key) < 0) ? value : undefined; 1913 })); 1914 } 1915 1916 if (obj.relatedPage.root != null) 1917 { 1918 enc.encodeCell(obj.relatedPage.root, node); 1919 } 1920 } 1921 1922 return node; 1923 }; 1924 1925 codec.beforeDecode = function(dec, node, obj) 1926 { 1927 obj.ui = dec.ui; 1928 obj.relatedPage = obj.ui.getPageById(node.getAttribute('relatedPage')); 1929 1930 if (obj.relatedPage == null) 1931 { 1932 var temp = node.ownerDocument.createElement('diagram'); 1933 temp.setAttribute('id', node.getAttribute('relatedPage')); 1934 temp.setAttribute('name', node.getAttribute('name')); 1935 obj.relatedPage = new DiagramPage(temp); 1936 1937 var vs = node.getAttribute('viewState'); 1938 1939 if (vs != null) 1940 { 1941 obj.relatedPage.viewState = JSON.parse(vs); 1942 node.removeAttribute('viewState'); 1943 } 1944 1945 // Makes sure the original node isn't modified 1946 node = node.cloneNode(true); 1947 var tmp = node.firstChild; 1948 1949 if (tmp != null) 1950 { 1951 obj.relatedPage.root = dec.decodeCell(tmp, false); 1952 1953 var tmp2 = tmp.nextSibling; 1954 tmp.parentNode.removeChild(tmp); 1955 tmp = tmp2; 1956 1957 while (tmp != null) 1958 { 1959 tmp2 = tmp.nextSibling; 1960 1961 if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT) 1962 { 1963 // Ignores all existing cells because those do not need to 1964 // be re-inserted into the model. Since the encoded version 1965 // of these cells contains the new parent, this would leave 1966 // to an inconsistent state on the model (ie. a parent 1967 // change without a call to parentForCellChanged). 1968 var id = tmp.getAttribute('id'); 1969 1970 if (dec.lookup(id) == null) 1971 { 1972 dec.decodeCell(tmp); 1973 } 1974 } 1975 1976 tmp.parentNode.removeChild(tmp); 1977 tmp = tmp2; 1978 } 1979 } 1980 } 1981 1982 return node; 1983 }; 1984 1985 codec.afterDecode = function(dec, node, obj) 1986 { 1987 obj.index = obj.previousIndex; 1988 1989 return obj; 1990 }; 1991 1992 mxCodecRegistry.register(codec); 1993})();