9858989b2ec98d37f466b9bac2c1f2b487304b38
[motion.git] / public / assets / plugins / square / js / Editor.js
1 /**
2  * $Id: Editor.js,v 1.14 2013/03/06 17:57:10 boris Exp $
3  * Copyright (c) 2006-2012, JGraph Ltd
4  */
5 // Specifies if local storage should be used (eg. on the iPad which has no filesystem)
6 var useLocalStorage = (mxClient.IS_TOUCH || urlParams['storage'] == 'local') && typeof(localStorage) != 'undefined';
7 var fileSupport = window.File != null && window.FileReader != null && window.FileList != null;
8
9 // Specifies if connector should be shown on selected cells
10 var touchStyle = mxClient.IS_TOUCH || urlParams['touch'] == '1';
11
12 // Counts open editor tabs (must be global for cross-window access)
13 var counter = 0;
14
15 // Cross-domain window access is not allowed in FF, so if we
16 // were opened from another domain then this will fail.
17 try {
18         var op = window;
19
20         while (op.opener != null && !isNaN(op.opener.counter)) {
21                 op = op.opener;
22         }
23
24         // Increments the counter in the first opener in the chain
25         if (op != null) {
26                 op.counter++;
27                 counter = op.counter;
28         }
29 } catch (e) {
30         // ignore
31 }
32
33 /**
34  * Editor constructor executed on page load.
35  */
36 Editor = function() {
37         mxEventSource.call(this);
38         this.init();
39         this.initStencilRegistry();
40         this.graph = new Graph();
41         this.outline = new mxOutline(this.graph);
42         this.outline.updateOnPan = true;
43         this.undoManager = this.createUndoManager();
44         this.status = '';
45
46         // Contains the name which was used for the last save. Default value is null.
47         this.filename = null;
48
49         this.getOrCreateFilename = function() {
50                 return this.filename || mxResources.get('drawing', [counter]) + '.xml';
51         };
52
53         this.getFilename = function() {
54                 return this.filename;
55         };
56
57         // Sets the status and fires a statusChanged event
58         this.setStatus = function(value) {
59                 this.status = value;
60                 this.fireEvent(new mxEventObject('statusChanged'));
61         };
62
63         // Returns the current status
64         this.getStatus = function() {
65                 return this.status;
66         };
67
68         // Contains the current modified state of the diagram. This is false for
69         // new diagrams and after the diagram was saved.
70         this.modified = false;
71
72         // Updates modified state if graph changes
73         this.graphChangeListener = function() {
74                 this.modified = true;
75         };
76         this.graph.getModel().addListener(mxEvent.CHANGE, mxUtils.bind(this, function() {
77                 this.graphChangeListener.apply(this, arguments);
78         }));
79
80         // Installs dialog if browser window is closed without saving
81         // This must be disabled during save and image export
82         window.onbeforeunload = mxUtils.bind(this, function() {
83                 if (this.modified) {
84                         return mxResources.get('allChangesLost');
85                 }
86         });
87
88         // Sets persistent graph state defaults
89         this.graph.resetViewOnRootChange = false;
90         this.graph.scrollbars = true;
91         this.graph.background = null;
92 };
93
94 // Editor inherits from mxEventSource
95 mxUtils.extend(Editor, mxEventSource);
96
97 /**
98  * Specifies the image URL to be used for the grid.
99  */
100 Editor.prototype.gridImage = IMAGE_PATH + '/grid.gif';
101
102 /**
103  * Specifies the image URL to be used for the transparent background.
104  */
105 Editor.prototype.transparentImage = IMAGE_PATH + '/transparent.gif';
106
107 /**
108  * Sets the XML node for the current diagram.
109  */
110 Editor.prototype.setGraphXml = function(node) {
111         var dec = new mxCodec(node.ownerDocument);
112
113         if (node.nodeName == 'mxGraphModel') {
114                 this.graph.view.scale = 1;
115                 this.graph.gridEnabled = node.getAttribute('grid') != '0';
116                 this.graph.graphHandler.guidesEnabled = node.getAttribute('guides') != '0';
117                 this.graph.setTooltips(node.getAttribute('tooltips') != '0');
118                 this.graph.setConnectable(node.getAttribute('connect') != '0');
119                 this.graph.foldingEnabled = node.getAttribute('fold') != '0';
120                 this.graph.scrollbars = node.getAttribute('scrollbars') != '0';
121
122                 if (!this.graph.scrollbars) {
123                         this.graph.container.scrollLeft = 0;
124                         this.graph.container.scrollTop = 0;
125                         this.graph.view.translate.x = Number(node.getAttribute('dx') || 0);
126                         this.graph.view.translate.y = Number(node.getAttribute('dy') || 0);
127                 }
128
129                 this.graph.pageVisible = node.getAttribute('page') == '1';
130                 this.graph.pageBreaksVisible = this.graph.pageVisible;
131                 this.graph.preferPageSize = this.graph.pageBreaksVisible;
132
133                 // Loads the persistent state settings
134                 var ps = node.getAttribute('pageScale');
135
136                 if (ps != null) {
137                         this.graph.pageScale = ps;
138                 } else {
139                         this.graph.pageScale = 1.5;
140                 }
141
142                 var pw = node.getAttribute('pageWidth');
143                 var ph = node.getAttribute('pageHeight');
144
145                 if (pw != null && ph != null) {
146                         this.graph.pageFormat = new mxRectangle(0, 0, parseFloat(pw), parseFloat(ph));
147                         this.outline.outline.pageFormat = this.graph.pageFormat;
148                 }
149
150                 // Loads the persistent state settings
151                 var bg = node.getAttribute('background');
152
153                 if (bg != null && bg.length > 0) {
154                         this.graph.background = bg;
155                 }
156
157                 dec.decode(node, this.graph.getModel());
158                 this.updateGraphComponents();
159         }
160 };
161
162 /**
163  * Returns the XML node that represents the current diagram.
164  */
165 Editor.prototype.getGraphXml = function() {
166         var enc = new mxCodec(mxUtils.createXmlDocument());
167         var node = enc.encode(this.graph.getModel());
168
169         if (this.graph.view.translate.x != 0 || this.graph.view.translate.y != 0) {
170                 node.setAttribute('dx', Math.round(this.graph.view.translate.x * 100) / 100);
171                 node.setAttribute('dy', Math.round(this.graph.view.translate.y * 100) / 100);
172         }
173
174         node.setAttribute('grid', (this.graph.isGridEnabled()) ? '1' : '0');
175         node.setAttribute('guides', (this.graph.graphHandler.guidesEnabled) ? '1' : '0');
176         node.setAttribute('guides', (this.graph.graphHandler.guidesEnabled) ? '1' : '0');
177         node.setAttribute('tooltips', (this.graph.tooltipHandler.isEnabled()) ? '1' : '0');
178         node.setAttribute('connect', (this.graph.connectionHandler.isEnabled()) ? '1' : '0');
179         node.setAttribute('fold', (this.graph.foldingEnabled) ? '1' : '0');
180         node.setAttribute('page', (this.graph.pageVisible) ? '1' : '0');
181         node.setAttribute('pageScale', this.graph.pageScale);
182         node.setAttribute('pageWidth', this.graph.pageFormat.width);
183         node.setAttribute('pageHeight', this.graph.pageFormat.height);
184
185         if (!this.graph.scrollbars) {
186                 node.setAttribute('scrollbars', '0');
187         }
188
189         if (this.graph.background != null) {
190                 node.setAttribute('background', this.graph.background);
191         }
192
193         return node;
194 };
195
196 /**
197  * Keeps the graph container in sync with the persistent graph state
198  */
199 Editor.prototype.updateGraphComponents = function() {
200         var graph = this.graph;
201         var outline = this.outline;
202
203         if (graph.container != null && outline.outline.container != null) {
204                 if (graph.background != null) {
205                         if (graph.background == 'none') {
206                                 graph.container.style.backgroundColor = 'transparent';
207                         } else {
208                                 if (graph.view.backgroundPageShape != null) {
209                                         graph.view.backgroundPageShape.fill = graph.background;
210                                         graph.view.backgroundPageShape.reconfigure();
211                                 }
212
213                                 graph.container.style.backgroundColor = graph.background;
214                         }
215                 } else {
216                         graph.container.style.backgroundColor = '';
217                 }
218
219                 if (graph.pageVisible) {
220                         graph.container.style.backgroundColor = '#ebebeb';
221                         graph.container.style.borderStyle = 'solid';
222                         graph.container.style.borderColor = '#e5e5e5';
223                         graph.container.style.borderTopWidth = '1px';
224                         graph.container.style.borderLeftWidth = '1px';
225                         graph.container.style.borderRightWidth = '0px';
226                         graph.container.style.borderBottomWidth = '0px';
227                 } else {
228                         graph.container.style.border = '';
229                 }
230
231                 outline.outline.container.style.backgroundColor = graph.container.style.backgroundColor;
232
233                 if (outline.outline.pageVisible != graph.pageVisible ||
234                         outline.outline.pageScale != graph.pageScale) {
235                         outline.outline.pageScale = graph.pageScale;
236                         outline.outline.pageVisible = graph.pageVisible;
237                         outline.outline.view.validate();
238                 }
239
240                 if (graph.scrollbars && graph.container.style.overflow == 'hidden' && !touchStyle) {
241                         graph.container.style.overflow = 'auto';
242                 } else if (!graph.scrollbars || touchStyle) {
243                         graph.container.style.overflow = 'hidden';
244                 }
245
246                 // Transparent.gif is a workaround for focus repaint problems in IE
247                 var noBackground = (mxClient.IS_IE && document.documentMode >= 9) ? 'url(' + this.transparentImage + ')' : 'none';
248                 graph.container.style.backgroundImage = (!graph.pageVisible && graph.isGridEnabled()) ? 'url(' + this.gridImage + ')' : noBackground;
249
250                 if (graph.view.backgroundPageShape != null) {
251                         graph.view.backgroundPageShape.node.style.backgroundImage = (this.graph.isGridEnabled()) ? 'url(' + this.gridImage + ')' : 'none';
252                 }
253         }
254 };
255
256 /**
257  * Initializes the environment.
258  */
259 Editor.prototype.init = function() {
260         // Adds stylesheet for IE6
261         if (mxClient.IS_IE6) {
262                 mxClient.link('stylesheet', CSS_PATH + '/grapheditor-ie6.css');
263         }
264
265         // Adds required resources (disables loading of fallback properties, this can only
266         // be used if we know that all keys are defined in the language specific file)
267         mxResources.loadDefaultBundle = false;
268         mxResources.add(RESOURCE_BASE);
269
270         // Makes the connection hotspot smaller
271         mxConstants.DEFAULT_HOTSPOT = 0.3;
272
273         var mxConnectionHandlerCreateMarker = mxConnectionHandler.prototype.createMarker;
274         mxConnectionHandler.prototype.createMarker = function() {
275                 var marker = mxConnectionHandlerCreateMarker.apply(this, arguments);
276
277                 // Overrides to ignore hotspot only for target terminal
278                 marker.intersects = mxUtils.bind(this, function(state, evt) {
279                         if (this.isConnecting()) {
280                                 return true;
281                         }
282
283                         return mxCellMarker.prototype.intersects.apply(marker, arguments);
284                 });
285
286                 return marker;
287         };
288
289         // Makes the shadow brighter
290         mxConstants.SHADOWCOLOR = '#d0d0d0';
291
292         // Changes some default colors
293         mxConstants.HANDLE_FILLCOLOR = '#99ccff';
294         mxConstants.HANDLE_STROKECOLOR = '#0088cf';
295         mxConstants.VERTEX_SELECTION_COLOR = '#00a8ff';
296         mxConstants.OUTLINE_COLOR = '#00a8ff';
297         mxConstants.OUTLINE_HANDLE_FILLCOLOR = '#99ccff';
298         mxConstants.OUTLINE_HANDLE_STROKECOLOR = '#00a8ff';
299         mxConstants.CONNECT_HANDLE_FILLCOLOR = '#cee7ff';
300         mxConstants.EDGE_SELECTION_COLOR = '#00a8ff';
301         mxConstants.DEFAULT_VALID_COLOR = '#00a8ff';
302         mxConstants.LABEL_HANDLE_FILLCOLOR = '#cee7ff';
303         mxConstants.GUIDE_COLOR = '#0088cf';
304
305         mxGraph.prototype.pageBreakColor = '#c0c0c0';
306         mxGraph.prototype.pageScale = 1;
307
308         // Increases default rubberband opacity (default is 20)
309         mxRubberband.prototype.defaultOpacity = 30;
310
311         // Changes border color of background page shape
312         mxGraphView.prototype.createBackgroundPageShape = function(bounds) {
313                 return new mxRectangleShape(bounds, this.graph.background || 'white', '#cacaca');
314         };
315
316         // Fits the number of background pages to the graph
317         mxGraphView.prototype.getBackgroundPageBounds = function() {
318                 var gb = this.getGraphBounds();
319
320                 // Computes unscaled, untranslated graph bounds
321                 var x = (gb.width > 0) ? gb.x / this.scale - this.translate.x : 0;
322                 var y = (gb.height > 0) ? gb.y / this.scale - this.translate.y : 0;
323                 var w = gb.width / this.scale;
324                 var h = gb.height / this.scale;
325
326                 var fmt = this.graph.pageFormat;
327                 var ps = this.graph.pageScale;
328
329                 var pw = fmt.width * ps;
330                 var ph = fmt.height * ps;
331
332                 var x0 = Math.floor(Math.min(0, x) / pw);
333                 var y0 = Math.floor(Math.min(0, y) / ph);
334                 var xe = Math.ceil(Math.max(1, x + w) / pw);
335                 var ye = Math.ceil(Math.max(1, y + h) / ph);
336
337                 var rows = xe - x0;
338                 var cols = ye - y0;
339
340                 var bounds = new mxRectangle(this.scale * (this.translate.x + x0 * pw), this.scale *
341                         (this.translate.y + y0 * ph), this.scale * rows * pw, this.scale * cols * ph);
342
343                 return bounds;
344         };
345
346         // Add panning for background page in VML
347         var graphPanGraph = mxGraph.prototype.panGraph;
348         mxGraph.prototype.panGraph = function(dx, dy) {
349                 graphPanGraph.apply(this, arguments);
350
351                 if ((this.dialect != mxConstants.DIALECT_SVG && this.view.backgroundPageShape != null) &&
352                         (!this.useScrollbarsForPanning || !mxUtils.hasScrollbars(this.container))) {
353                         this.view.backgroundPageShape.node.style.marginLeft = dx + 'px';
354                         this.view.backgroundPageShape.node.style.marginTop = dy + 'px';
355                 }
356         };
357
358         var editor = this;
359
360         // Uses HTML for background pages (to support grid background image)
361         mxGraphView.prototype.validateBackground = function() {
362                 var bg = this.graph.getBackgroundImage();
363
364                 if (bg != null) {
365                         if (this.backgroundImage == null || this.backgroundImage.image != bg.src) {
366                                 if (this.backgroundImage != null) {
367                                         this.backgroundImage.destroy();
368                                 }
369
370                                 var bounds = new mxRectangle(0, 0, 1, 1);
371
372                                 this.backgroundImage = new mxImageShape(bounds, bg.src);
373                                 this.backgroundImage.dialect = this.graph.dialect;
374                                 this.backgroundImage.init(this.backgroundPane);
375                                 this.backgroundImage.redraw();
376                         }
377
378                         this.redrawBackgroundImage(this.backgroundImage, bg);
379                 } else if (this.backgroundImage != null) {
380                         this.backgroundImage.destroy();
381                         this.backgroundImage = null;
382                 }
383
384                 if (this.graph.pageVisible) {
385                         var bounds = this.getBackgroundPageBounds();
386
387                         if (this.backgroundPageShape == null) {
388                                 this.backgroundPageShape = this.createBackgroundPageShape(bounds);
389                                 this.backgroundPageShape.scale = 1;
390                                 this.backgroundPageShape.isShadow = true;
391                                 this.backgroundPageShape.dialect = mxConstants.DIALECT_STRICTHTML;
392                                 this.backgroundPageShape.init(this.graph.container);
393                                 // Required for the browser to render the background page in correct order
394                                 this.graph.container.firstChild.style.position = 'absolute';
395                                 this.graph.container.insertBefore(this.backgroundPageShape.node, this.graph.container.firstChild);
396                                 this.backgroundPageShape.redraw();
397
398                                 this.backgroundPageShape.node.className = 'geBackgroundPage';
399                                 this.backgroundPageShape.node.style.backgroundPosition = '-1px -1px';
400
401                                 // Adds listener for double click handling on background
402                                 mxEvent.addListener(this.backgroundPageShape.node, 'dblclick',
403                                         mxUtils.bind(this, function(evt) {
404                                                 this.graph.dblClick(evt);
405                                         })
406                                 );
407
408                                 // Adds basic listeners for graph event dispatching outside of the
409                                 // container and finishing the handling of a single gesture
410                                 mxEvent.addGestureListeners(this.backgroundPageShape.node,
411                                         mxUtils.bind(this, function(evt) {
412                                                 this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt));
413                                         }),
414                                         mxUtils.bind(this, function(evt) {
415                                                 // Hides the tooltip if mouse is outside container
416                                                 if (this.graph.tooltipHandler != null &&
417                                                         this.graph.tooltipHandler.isHideOnHover()) {
418                                                         this.graph.tooltipHandler.hide();
419                                                 }
420
421                                                 if (this.graph.isMouseDown &&
422                                                         !mxEvent.isConsumed(evt)) {
423                                                         this.graph.fireMouseEvent(mxEvent.MOUSE_MOVE,
424                                                                 new mxMouseEvent(evt));
425                                                 }
426                                         }),
427                                         mxUtils.bind(this, function(evt) {
428                                                 this.graph.fireMouseEvent(mxEvent.MOUSE_UP,
429                                                         new mxMouseEvent(evt));
430                                         }));
431                         } else {
432                                 this.backgroundPageShape.scale = 1;
433                                 this.backgroundPageShape.bounds = bounds;
434                                 this.backgroundPageShape.redraw();
435                         }
436
437                         this.backgroundPageShape.node.style.backgroundImage = (this.graph.isGridEnabled()) ?
438                                 'url(' + editor.gridImage + ')' : 'none';
439                 } else if (this.backgroundPageShape != null) {
440                         this.backgroundPageShape.destroy();
441                         this.backgroundPageShape = null;
442                 }
443         };
444
445         // Draws page breaks only within the page
446         mxGraph.prototype.updatePageBreaks = function(visible, width, height) {
447                 var scale = this.view.scale;
448                 var tr = this.view.translate;
449                 var fmt = this.pageFormat;
450                 var ps = scale * this.pageScale;
451
452                 var bounds2 = this.view.getBackgroundPageBounds();
453
454                 width = bounds2.width;
455                 height = bounds2.height;
456                 var bounds = new mxRectangle(scale * tr.x, scale * tr.y,
457                         fmt.width * ps, fmt.height * ps);
458
459                 // Does not show page breaks if the scale is too small
460                 visible = visible && Math.min(bounds.width, bounds.height) > this.minPageBreakDist;
461
462                 var horizontalCount = (visible) ? Math.ceil(width / bounds.width) - 1 : 0;
463                 var verticalCount = (visible) ? Math.ceil(height / bounds.height) - 1 : 0;
464                 var right = bounds2.x + width;
465                 var bottom = bounds2.y + height;
466
467                 if (this.horizontalPageBreaks == null && horizontalCount > 0) {
468                         this.horizontalPageBreaks = [];
469                 }
470
471                 if (this.horizontalPageBreaks != null) {
472                         for (var i = 0; i <= horizontalCount; i++) {
473                                 var pts = [new mxPoint(bounds2.x + (i + 1) * bounds.width, bounds2.y),
474                                         new mxPoint(bounds2.x + (i + 1) * bounds.width, bottom)
475                                 ];
476
477                                 if (this.horizontalPageBreaks[i] != null) {
478                                         this.horizontalPageBreaks[i].scale = 1;
479                                         this.horizontalPageBreaks[i].points = pts;
480                                         this.horizontalPageBreaks[i].redraw();
481                                 } else {
482                                         var pageBreak = new mxPolyline(pts, this.pageBreakColor, this.scale);
483                                         pageBreak.dialect = this.dialect;
484                                         pageBreak.isDashed = this.pageBreakDashed;
485                                         pageBreak.addPipe = false;
486                                         pageBreak.scale = scale;
487                                         pageBreak.init(this.view.backgroundPane);
488                                         pageBreak.redraw();
489
490                                         this.horizontalPageBreaks[i] = pageBreak;
491                                 }
492                         }
493
494                         for (var i = horizontalCount; i < this.horizontalPageBreaks.length; i++) {
495                                 this.horizontalPageBreaks[i].destroy();
496                         }
497
498                         this.horizontalPageBreaks.splice(horizontalCount, this.horizontalPageBreaks.length - horizontalCount);
499                 }
500
501                 if (this.verticalPageBreaks == null && verticalCount > 0) {
502                         this.verticalPageBreaks = [];
503                 }
504
505                 if (this.verticalPageBreaks != null) {
506                         for (var i = 0; i <= verticalCount; i++) {
507                                 var pts = [new mxPoint(bounds2.x, bounds2.y + (i + 1) * bounds.height),
508                                         new mxPoint(right, bounds2.y + (i + 1) * bounds.height)
509                                 ];
510
511                                 if (this.verticalPageBreaks[i] != null) {
512                                         this.verticalPageBreaks[i].scale = 1; //scale;
513                                         this.verticalPageBreaks[i].points = pts;
514                                         this.verticalPageBreaks[i].redraw();
515                                 } else {
516                                         var pageBreak = new mxPolyline(pts, this.pageBreakColor, scale);
517                                         pageBreak.dialect = this.dialect;
518                                         pageBreak.isDashed = this.pageBreakDashed;
519                                         pageBreak.addPipe = false;
520                                         pageBreak.scale = scale;
521                                         pageBreak.init(this.view.backgroundPane);
522                                         pageBreak.redraw();
523
524                                         this.verticalPageBreaks[i] = pageBreak;
525                                 }
526                         }
527
528                         for (var i = verticalCount; i < this.verticalPageBreaks.length; i++) {
529                                 this.verticalPageBreaks[i].destroy();
530                         }
531
532                         this.verticalPageBreaks.splice(verticalCount, this.verticalPageBreaks.length - verticalCount);
533                 }
534         };
535
536         // Enables snapping to off-grid terminals for edge waypoints
537         mxEdgeHandler.prototype.snapToTerminals = true;
538
539         // Enables guides
540         mxGraphHandler.prototype.guidesEnabled = true;
541
542         // Disables removing relative children from parents
543         var mxGraphHandlerShouldRemoveCellsFromParent = mxGraphHandler.prototype.shouldRemoveCellsFromParent;
544         mxGraphHandler.prototype.shouldRemoveCellsFromParent = function(parent, cells, evt) {
545                 for (var i = 0; i < cells.length; i++) {
546                         if (this.graph.getModel().isVertex(cells[i])) {
547                                 var geo = this.graph.getCellGeometry(cells[i]);
548
549                                 if (geo != null && geo.relative) {
550                                         return false;
551                                 }
552                         }
553                 }
554
555                 return mxGraphHandlerShouldRemoveCellsFromParent.apply(this, arguments);
556         };
557
558         // Alt-move disables guides
559         mxGuide.prototype.isEnabledForEvent = function(evt) {
560                 return !mxEvent.isAltDown(evt);
561         };
562
563         // Consumes click events for disabled menu items
564         mxPopupMenuAddItem = mxPopupMenu.prototype.addItem;
565         mxPopupMenu.prototype.addItem = function(title, image, funct, parent, iconCls, enabled) {
566                 var result = mxPopupMenuAddItem.apply(this, arguments);
567
568                 if (enabled != null && !enabled) {
569                         mxEvent.addListener(result, 'mousedown', function(evt) {
570                                 mxEvent.consume(evt);
571                         });
572                 }
573
574                 return result;
575         };
576
577         // Selects descendants before children selection mode
578         var graphHandlerGetInitialCellForEvent = mxGraphHandler.prototype.getInitialCellForEvent;
579         mxGraphHandler.prototype.getInitialCellForEvent = function(me) {
580                 var model = this.graph.getModel();
581                 var psel = model.getParent(this.graph.getSelectionCell());
582                 var cell = graphHandlerGetInitialCellForEvent.apply(this, arguments);
583                 var parent = model.getParent(cell);
584
585                 if (psel == null || (psel != cell && psel != parent)) {
586                         while (!this.graph.isCellSelected(cell) && !this.graph.isCellSelected(parent) &&
587                                 model.isVertex(parent) && !this.graph.isValidRoot(parent)) {
588                                 cell = parent;
589                                 parent = this.graph.getModel().getParent(cell);
590                         }
591                 }
592
593                 return cell;
594         };
595
596         // Selection is delayed to mouseup if child selected
597         var graphHandlerIsDelayedSelection = mxGraphHandler.prototype.isDelayedSelection;
598         mxGraphHandler.prototype.isDelayedSelection = function(cell) {
599                 var result = graphHandlerIsDelayedSelection.apply(this, arguments);
600                 var model = this.graph.getModel();
601                 var psel = model.getParent(this.graph.getSelectionCell());
602                 var parent = model.getParent(cell);
603
604                 if (psel == null || (psel != cell && psel != parent)) {
605                         if (!this.graph.isCellSelected(cell) && model.isVertex(parent) && !this.graph.isValidRoot(parent)) {
606                                 result = true;
607                         }
608                 }
609
610                 return result;
611         };
612
613         // Delayed selection of parent group
614         mxGraphHandler.prototype.selectDelayed = function(me) {
615                 var cell = me.getCell();
616
617                 if (cell == null) {
618                         cell = this.cell;
619                 }
620
621                 var model = this.graph.getModel();
622                 var parent = model.getParent(cell);
623
624                 while (this.graph.isCellSelected(cell) && model.isVertex(parent) && !this.graph.isValidRoot(parent)) {
625                         cell = parent;
626                         parent = model.getParent(cell);
627                 }
628
629                 this.graph.selectCellForEvent(cell, me.getEvent());
630         };
631
632         // Returns last selected ancestor
633         mxPanningHandler.prototype.getCellForPopupEvent = function(me) {
634                 var cell = me.getCell();
635                 var model = this.graph.getModel();
636                 var parent = model.getParent(cell);
637
638                 while (model.isVertex(parent) && !this.graph.isValidRoot(parent)) {
639                         if (this.graph.isCellSelected(parent)) {
640                                 cell = parent;
641                         }
642
643                         parent = model.getParent(parent);
644                 }
645
646                 return cell;
647         };
648 };
649
650 /**
651  * Creates and returns a new undo manager.
652  */
653 Editor.prototype.createUndoManager = function() {
654         var graph = this.graph;
655         var undoMgr = new mxUndoManager();
656
657         // Installs the command history
658         var listener = function(sender, evt) {
659                 undoMgr.undoableEditHappened(evt.getProperty('edit'));
660         };
661
662         graph.getModel().addListener(mxEvent.UNDO, listener);
663         graph.getView().addListener(mxEvent.UNDO, listener);
664
665         // Keeps the selection in sync with the history
666         var undoHandler = function(sender, evt) {
667                 var cand = graph.getSelectionCellsForChanges(evt.getProperty('edit').changes);
668                 var cells = [];
669
670                 for (var i = 1; i < cand.length; i++) {
671                         if (graph.view.getState(cand[i]) != null) {
672                                 cells.push(cand[i]);
673                         }
674                 }
675
676                 graph.setSelectionCells(cells);
677         };
678
679         undoMgr.addListener(mxEvent.UNDO, undoHandler);
680         undoMgr.addListener(mxEvent.REDO, undoHandler);
681
682         return undoMgr;
683 };
684
685 /**
686  * Adds basic stencil set (no namespace).
687  */
688 Editor.prototype.initStencilRegistry = function() {
689         // Loads default stencils
690         mxStencilRegistry.loadStencilSet(STENCIL_PATH + '/general.xml');
691 };
692
693 /**
694  * Overrides stencil registry for dynamic loading of stencils.
695  */
696 (function() {
697         /**
698          * Maps from library names to an array of Javascript filenames,
699          * which are synchronously loaded. Currently only stencil files
700          * (.xml) and JS files (.js) are supported.
701          * IMPORTANT: For embedded diagrams to work entries must also
702          * be added in EmbedServlet.java.
703          */
704         mxStencilRegistry.libraries = {};
705
706         /**
707          * Stores all package names that have been dynamically loaded.
708          * Each package is only loaded once.
709          */
710         mxStencilRegistry.packages = [];
711
712         // Extends the default stencil registry to add dynamic loading
713         mxStencilRegistry.getStencil = function(name) {
714                 var result = mxStencilRegistry.stencils[name];
715
716                 if (result == null) {
717                         var basename = mxStencilRegistry.getBasenameForStencil(name);
718
719                         // Loads stencil files and tries again
720                         if (basename != null) {
721                                 var libs = mxStencilRegistry.libraries[basename];
722
723                                 if (libs != null) {
724                                         if (mxStencilRegistry.packages[basename] == null) {
725                                                 mxStencilRegistry.packages[basename] = 1;
726
727                                                 for (var i = 0; i < libs.length; i++) {
728                                                         var fname = libs[i];
729
730                                                         if (fname.toLowerCase().substring(fname.length - 4, fname.length) == '.xml') {
731                                                                 mxStencilRegistry.loadStencilSet(fname, null);
732                                                         } else if (fname.toLowerCase().substring(fname.length - 3, fname.length) == '.js') {
733                                                                 var req = mxUtils.load(fname);
734
735                                                                 if (req != null) {
736                                                                         eval.call(window, req.getText());
737                                                                 }
738                                                         } else {
739                                                                 // FIXME: This does not yet work as the loading is triggered after
740                                                                 // the shape was used in the graph, at which point the keys have
741                                                                 // typically been translated in the calling method.
742                                                                 //mxResources.add(fname);
743                                                         }
744                                                 }
745                                         }
746                                 } else {
747                                         mxStencilRegistry.loadStencilSet(STENCIL_PATH + '/' + basename + '.xml', null);
748                                 }
749
750                                 result = mxStencilRegistry.stencils[name];
751                         }
752                 }
753
754                 return result;
755         };
756
757         // Returns the basename for the given stencil or null if no file must be
758         // loaded to render the given stencil.
759         mxStencilRegistry.getBasenameForStencil = function(name) {
760                 var parts = name.split('.');
761                 var tmp = null;
762
763                 if (parts.length > 0 && parts[0] == 'mxgraph') {
764                         tmp = parts[1];
765
766                         for (var i = 2; i < parts.length - 1; i++) {
767                                 tmp += '/' + parts[i];
768                         }
769                 }
770
771                 return tmp;
772         };
773
774         // Loads the given stencil set
775         mxStencilRegistry.loadStencilSet = function(stencilFile, postStencilLoad, force) {
776                 force = (force != null) ? force : false;
777
778                 // Uses additional cache for detecting previous load attempts
779                 var xmlDoc = mxStencilRegistry.packages[stencilFile];
780
781                 if (force || xmlDoc == null) {
782                         var install = false;
783
784                         if (xmlDoc == null) {
785                                 var req = mxUtils.load(stencilFile);
786                                 xmlDoc = req.getXml();
787                                 mxStencilRegistry.packages[stencilFile] = xmlDoc;
788                                 install = true;
789                         }
790
791                         mxStencilRegistry.parseStencilSet(xmlDoc, postStencilLoad, install);
792                 }
793         };
794
795         // Parses the given stencil set
796         mxStencilRegistry.parseStencilSet = function(xmlDocument, postStencilLoad, install) {
797                 install = (install != null) ? install : true;
798                 var root = xmlDocument.documentElement;
799                 var shape = root.firstChild;
800                 var packageName = '';
801                 var name = root.getAttribute('name');
802
803                 if (name != null) {
804                         packageName = name + '.';
805                 }
806
807                 while (shape != null) {
808                         if (shape.nodeType == mxConstants.NODETYPE_ELEMENT) {
809                                 name = shape.getAttribute('name');
810
811                                 if (name != null) {
812                                         packageName = packageName.toLowerCase();
813                                         var stencilName = name.replace(/ /g, "_");
814
815                                         if (install) {
816                                                 mxStencilRegistry.addStencil(packageName + stencilName.toLowerCase(), new mxStencil(shape));
817                                         }
818
819                                         if (postStencilLoad != null) {
820                                                 var w = shape.getAttribute('w');
821                                                 var h = shape.getAttribute('h');
822
823                                                 w = (w == null) ? 80 : parseInt(w, 10);
824                                                 h = (h == null) ? 80 : parseInt(h, 10);
825
826                                                 postStencilLoad(packageName, stencilName, name, w, h);
827                                         }
828                                 }
829                         }
830
831                         shape = shape.nextSibling;
832                 }
833         };
834 })();
835
836 /**
837  * Class for asynchronously opening a new window and loading a file at the same
838  * time. This acts as a bridge between the open dialog and the new editor.
839  */
840 OpenFile = function(done) {
841         this.producer = null;
842         this.consumer = null;
843         this.done = done;
844 };
845
846 /**
847  * Registers the editor from the new window.
848  */
849 OpenFile.prototype.setConsumer = function(value) {
850         this.consumer = value;
851         this.execute();
852 };
853
854 /**
855  * Sets the data from the loaded file.
856  */
857 OpenFile.prototype.setData = function(value, filename) {
858         this.data = value;
859         this.filename = filename;
860         this.execute();
861 };
862
863 /**
864  * Displays an error message.
865  */
866 OpenFile.prototype.error = function(msg) {
867         this.cancel();
868         mxUtils.alert(msg);
869 };
870
871 /**
872  * Consumes the data.
873  */
874 OpenFile.prototype.execute = function() {
875         if (this.consumer != null && this.data != null) {
876                 this.consumer(this.data, this.filename);
877                 this.cancel();
878         }
879 };
880
881 /**
882  * Cancels the operation.
883  */
884 OpenFile.prototype.cancel = function() {
885         if (this.done != null) {
886                 this.done();
887         }
888 };