2 * $Id: Graph.js,v 1.14 2013-02-16 10:19:54 gaudenz Exp $
3 * Copyright (c) 2006-2012, JGraph Ltd
6 * Constructs a new graph instance. Note that the constructor does not take a
7 * container because the graph instance is needed for creating the UI, which
8 * in turn will create the container for the graph. Hence, the container is
9 * assigned later in EditorUi.
11 Graph = function(container, model, renderHint, stylesheet)
13 mxGraph.call(this, container, model, renderHint, stylesheet);
15 this.setConnectable(true);
16 this.setDropEnabled(false);
17 this.setPanning(true);
18 this.setTooltips(!mxClient.IS_TOUCH);
19 this.setAllowLoops(false);
20 this.setMultigraph(false);
21 this.allowAutoPanning = true;
22 this.resetEdgesOnConnect = false;
23 this.constrainChildren = false;
25 // Centers the port icon on the target port
26 this.connectionHandler.targetConnectImage = true;
28 // Does not allow dangling edges
29 this.setAllowDanglingEdges(false);
31 // Enables cloning of connection sources
32 this.connectionHandler.setCreateTarget(false);
34 // Disables built-in connection starts
35 this.connectionHandler.isValidSource = function()
37 return mxConnectionHandler.prototype.isValidSource.apply(this, arguments) && urlParams['connect'] != '2';
40 // Sets the style to be used when an elbow edge is double clicked
41 this.alternateEdgeStyle = 'vertical';
43 if (stylesheet == null)
45 this.loadStylesheet();
48 // Creates rubberband selection
49 var rubberband = new mxRubberband(this);
51 this.getRubberband = function()
56 // Shows hand cursor while panning
57 this.panningHandler.addListener(mxEvent.PAN_START, mxUtils.bind(this, function()
59 this.container.style.cursor = 'pointer';
62 this.panningHandler.addListener(mxEvent.PAN_END, mxUtils.bind(this, function()
64 this.container.style.cursor = 'default';
67 // Adds support for HTML labels via style. Note: Currently, only the Java
68 // backend supports HTML labels but CSS support is limited to the following:
69 // http://docs.oracle.com/javase/6/docs/api/index.html?javax/swing/text/html/CSS.html
70 this.isHtmlLabel = function(cell)
72 var state = this.view.getState(cell);
73 var style = (state != null) ? state.style : this.getCellStyle(cell);
75 return style['html'] == '1' || style['whiteSpace'] == 'wrap';
78 // HTML entities are displayed as plain text in wrapped plain text labels
79 this.cellRenderer.getLabelValue = function(state)
81 var result = mxCellRenderer.prototype.getLabelValue.apply(this, arguments);
83 if (state.style['whiteSpace'] == 'wrap' && state.style['html'] != 1)
85 result = mxUtils.htmlEntities(result, false);
92 this.isCellLocked = function(cell)
97 // Tap and hold brings up context menu.
98 // Tolerance slightly below graph tolerance is better.
99 this.connectionHandler.tapAndHoldTolerance = 16;
101 // Tap and hold on background starts rubberband on cell starts connecting
102 var connectionHandlerTapAndHold = this.connectionHandler.tapAndHold;
103 this.connectionHandler.tapAndHold = function(me, state)
107 if (!this.graph.panningHandler.active)
109 rubberband.start(me.getGraphX(), me.getGraphY());
110 this.graph.panningHandler.panningTrigger = false;
113 else if (tapAndHoldStartsConnection)
115 connectionHandlerTapAndHold.apply(this, arguments);
117 else if (this.graph.isCellSelected(state.cell) && this.graph.getSelectionCount() > 1)
119 this.graph.removeSelectionCell(state.cell);
129 // Graph inherits from mxGraph
130 mxUtils.extend(Graph, mxGraph);
133 * Allows to all values in fit.
135 Graph.prototype.minFitScale = null;
138 * Allows to all values in fit.
140 Graph.prototype.maxFitScale = null;
143 * Loads the stylesheet for this graph.
145 Graph.prototype.loadStylesheet = function()
147 var node = mxUtils.load(STYLE_PATH + '/default.xml').getDocumentElement();
148 var dec = new mxCodec(node.ownerDocument);
149 dec.decode(node, this.getStylesheet());
153 * Inverts the elbow edge style without removing existing styles.
155 Graph.prototype.flipEdge = function(edge)
159 var state = this.view.getState(edge);
160 var style = (state != null) ? state.style : this.getCellStyle(edge);
164 var elbow = mxUtils.getValue(style, mxConstants.STYLE_ELBOW,
165 mxConstants.ELBOW_HORIZONTAL);
166 var value = (elbow == mxConstants.ELBOW_HORIZONTAL) ?
167 mxConstants.ELBOW_VERTICAL : mxConstants.ELBOW_HORIZONTAL;
168 this.setCellStyles(mxConstants.STYLE_ELBOW, value, [edge]);
174 * Sets the default edge for future connections.
176 Graph.prototype.setDefaultEdge = function(cell)
178 if (cell != null && this.getModel().isEdge(cell))
180 // Take a snapshot of the cell at the moment of calling
181 var proto = this.getModel().cloneCells([cell])[0];
183 // Delete existing points
184 if (proto.geometry != null)
186 proto.geometry.points = null;
189 // Delete entry-/exitXY styles
190 var style = proto.getStyle();
191 style = mxUtils.setStyle(style, mxConstants.STYLE_ENTRY_X, null);
192 style = mxUtils.setStyle(style, mxConstants.STYLE_ENTRY_Y, null);
193 style = mxUtils.setStyle(style, mxConstants.STYLE_EXIT_X, null);
194 style = mxUtils.setStyle(style, mxConstants.STYLE_EXIT_Y, null);
195 proto.setStyle(style);
197 // Uses edge template for connect preview
198 this.connectionHandler.createEdgeState = function(me)
200 return this.graph.view.createState(proto);
203 // Creates new connections from edge template
204 this.connectionHandler.factoryMethod = function()
206 return this.graph.cloneCells([proto])[0];
212 * Disables folding for non-swimlanes.
214 Graph.prototype.isCellFoldable = function(cell)
216 return this.foldingEnabled && this.isSwimlane(cell);
220 * Disables drill-down for non-swimlanes.
222 Graph.prototype.isValidRoot = function(cell)
224 return this.isSwimlane(cell);
228 * Overrides createGroupCell to set the group style for new groups to 'group'.
230 Graph.prototype.createGroupCell = function()
232 var group = mxGraph.prototype.createGroupCell.apply(this, arguments);
233 group.setStyle('group');
240 * Overrides tooltips to show position and size
242 Graph.prototype.getTooltipForCell = function(cell)
246 if (this.getModel().isVertex(cell))
248 // var geo = this.getCellGeometry(cell);
250 // var f2 = function(x)
252 // return Math.round(parseFloat(x) * 100) / 100;
261 // else if (tip.length > 0)
266 // tip += 'X: ' + f2(geo.x) + '\nY: ' + f2(geo.y) + '\nW: ' + f2(geo.width) + '\nH: ' + f2(geo.height);
269 tip += mxResources.get('description_' + cell.value.nodeName);
271 else if (this.getModel().isEdge(cell))
273 tip = mxGraph.prototype.getTooltipForCell.apply(this, arguments);
281 * Returns the label for the given cell.
283 Graph.prototype.convertValueToString = function(cell)
285 if (cell.value != null && typeof(cell.value) == 'object')
287 return cell.value.getAttribute('label');
290 return mxGraph.prototype.convertValueToString.apply(this, arguments);
295 * Handles label changes for XML user objects.
297 Graph.prototype.cellLabelChanged = function(cell, value, autoSize)
299 if (cell.value != null && typeof(cell.value) == 'object')
301 var tmp = cell.value.cloneNode(true);
302 tmp.setAttribute('label', value);
306 mxGraph.prototype.cellLabelChanged.apply(this, arguments);
310 * Sets the link for the given cell.
312 Graph.prototype.setLinkForCell = function(cell, link)
316 if (cell.value != null && typeof(cell.value) == 'object')
318 value = cell.value.cloneNode(true);
322 var doc = mxUtils.createXmlDocument();
324 value = doc.createElement('UserObject');
325 value.setAttribute('label', cell.value);
328 if (link != null && link.length > 0)
330 value.setAttribute('link', link);
334 value.removeAttribute('link');
337 this.model.setValue(cell, value);
341 * Returns the link for the given cell.
343 Graph.prototype.getLinkForCell = function(cell)
345 if (cell.value != null && typeof(cell.value) == 'object')
347 return cell.value.getAttribute('link');
354 * Customized graph for touch devices.
356 Graph.prototype.initTouch = function()
358 // Disables new connections via "hotspot"
359 this.connectionHandler.marker.isEnabled = function()
361 return this.graph.connectionHandler.first != null;
364 // Hides menu when editing starts
365 this.addListener(mxEvent.START_EDITING, function(sender, evt)
367 this.panningHandler.hideMenu();
370 // Context menu for touchstyle
371 var showMenu = false;
374 // Checks if native hit detection did not return anything and does custom
375 // hit detection for edges to take into account the tolerance
376 this.updateMouseEvent = function(me)
378 mxGraph.prototype.updateMouseEvent.apply(this, arguments);
380 if (me.getState() == null)
382 var cell = this.getCellAt(me.graphX, me.graphY);
384 if (this.getModel().isEdge(cell))
386 me.state = this.view.getState(cell);
388 if (me.state != null && me.state.shape != null)
390 this.container.style.cursor = me.state.shape.node.style.cursor;
395 if (me.getState() == null)
397 this.container.style.cursor = 'default';
401 // Handles popup menu on touch devices (tap selected cell)
402 this.fireMouseEvent = function(evtName, me, sender)
404 if (evtName == mxEvent.MOUSE_DOWN)
406 if (!this.panningHandler.isMenuShowing())
408 menuCell = me.getCell();
409 showMenu = (menuCell != null) ? this.isCellSelected(menuCell) : this.isSelectionEmpty();
417 else if (evtName == mxEvent.MOUSE_UP)
419 if (showMenu && !this.isEditing())
421 if (!this.panningHandler.isMenuShowing())
423 var x = mxEvent.getClientX(me.getEvent());
424 var y = mxEvent.getClientY(me.getEvent());
426 this.panningHandler.popup(x + 16, y, menuCell, me.getEvent());
440 mxGraph.prototype.fireMouseEvent.apply(this, arguments);
442 if (evtName == mxEvent.MOUSE_MOVE && me.isConsumed())
451 * Implements touch devices.
455 // Enables rotation handle
456 mxVertexHandler.prototype.rotationEnabled = true;
458 // Matches label positions of mxGraph 1.x
459 mxText.prototype.baseSpacingTop = 5;
460 mxText.prototype.baseSpacingBottom = 1;
462 // Touch-specific static overrides
465 // Sets constants for touch style
466 mxConstants.HANDLE_SIZE = 16;
467 mxConstants.LABEL_HANDLE_SIZE = 7;
469 // Larger tolerance and grid for real touch devices
470 if (mxClient.IS_TOUCH)
472 mxVertexHandler.prototype.tolerance = 4;
473 mxEdgeHandler.prototype.tolerance = 6;
474 Graph.prototype.tolerance = 14;
475 Graph.prototype.gridSize = 20;
477 // One finger pans (no rubberband selection) must start regardless of mouse button
478 mxPanningHandler.prototype.selectOnPopup = false;
479 mxPanningHandler.prototype.useLeftButtonForPanning = true;
480 mxPanningHandler.prototype.isPanningTrigger = function(me)
482 var evt = me.getEvent();
484 return (this.useLeftButtonForPanning && (this.ignoreCell || me.getState() == null)/* &&
485 mxEvent.isLeftMouseButton(evt)*/) || (mxEvent.isControlDown(evt) &&
486 mxEvent.isShiftDown(evt)) || (this.usePopupTrigger &&
487 mxEvent.isPopupTrigger(evt));
491 // Don't clear selection if multiple cells selected
492 var graphHandlerMouseDown = mxGraphHandler.prototype.mouseDown;
493 mxGraphHandler.prototype.mouseDown = function(sender, me)
495 graphHandlerMouseDown.apply(this, arguments);
497 if (this.graph.isCellSelected(me.getCell()) && this.graph.getSelectionCount() > 1)
499 this.delayedSelection = false;
503 // Changes order of panninghandler
504 Graph.prototype.createHandlers = function(container)
506 this.tooltipHandler = new mxTooltipHandler(this);
507 this.tooltipHandler.setEnabled(false);
508 // Selection cells first
509 this.selectionCellsHandler = new mxSelectionCellsHandler(this);
510 this.panningHandler = new mxPanningHandler(this);
511 this.panningHandler.panningEnabled = false;
512 this.connectionHandler = new mxConnectionHandler(this);
513 this.connectionHandler.setEnabled(false);
514 this.graphHandler = new mxGraphHandler(this);
517 // On connect the target is selected and we clone the cell of the preview edge for insert
518 mxConnectionHandler.prototype.selectCells = function(edge, target)
520 if (touchStyle && target != null)
522 this.graph.setSelectionCell(target);
526 this.graph.setSelectionCell(edge);
530 // Overrides double click handling to use the tolerance
531 // FIXME: Double click on edges in iPad needs focus on textarea
532 var graphDblClick = mxGraph.prototype.dblClick;
533 Graph.prototype.dblClick = function(evt, cell)
537 var pt = mxUtils.convertPoint(this.container,
538 mxEvent.getClientX(evt), mxEvent.getClientY(evt));
539 cell = this.getCellAt(pt.x, pt.y);
542 graphDblClick.call(this, evt, cell);
545 // Rounded edge and vertex handles
546 var touchHandle = new mxImage(IMAGE_PATH + '/touch-handle.png', 16, 16);
547 mxVertexHandler.prototype.handleImage = touchHandle;
548 mxEdgeHandler.prototype.handleImage = touchHandle;
549 mxOutline.prototype.sizerImage = touchHandle;
551 // Pre-fetches touch handle
552 new Image().src = touchHandle.src;
554 // Adds connect icon to selected vertices
555 var connectorSrc = IMAGE_PATH + '/touch-connector.png';
557 var vertexHandlerInit = mxVertexHandler.prototype.init;
558 mxVertexHandler.prototype.init = function()
560 vertexHandlerInit.apply(this, arguments);
562 // Only show connector image on one cell and do not show on containers
563 if (showConnectorImg && this.graph.connectionHandler.isEnabled() &&
564 this.graph.isCellConnectable(this.state.cell) &&
565 !this.graph.isValidRoot(this.state.cell) &&
566 this.graph.getSelectionCount() == 1)
568 this.connectorImg = mxUtils.createImage(connectorSrc);
569 this.connectorImg.style.cursor = 'pointer';
570 this.connectorImg.style.width = '29px';
571 this.connectorImg.style.height = '29px';
572 this.connectorImg.style.position = 'absolute';
574 if (!mxClient.IS_TOUCH)
576 this.connectorImg.setAttribute('title', mxResources.get('connect'));
577 mxEvent.redirectMouseEvents(this.connectorImg, this.graph, this.state);
580 // Adds 2px tolerance
581 this.connectorImg.style.padding = '2px';
583 // Starts connecting on touch/mouse down
584 mxEvent.addGestureListeners(this.connectorImg,
585 mxUtils.bind(this, function(evt)
587 this.graph.panningHandler.hideMenu();
588 var pt = mxUtils.convertPoint(this.graph.container,
589 mxEvent.getClientX(evt), mxEvent.getClientY(evt));
590 this.graph.connectionHandler.start(this.state, pt.x, pt.y);
591 this.graph.isMouseDown = true;
592 mxEvent.consume(evt);
596 this.graph.container.appendChild(this.connectorImg);
602 var vertexHandlerRedraw = mxVertexHandler.prototype.redraw;
603 mxVertexHandler.prototype.redraw = function()
605 vertexHandlerRedraw.apply(this);
609 mxVertexHandler.prototype.redrawTools = function()
611 if (this.state != null && this.connectorImg != null)
613 // Top right for single-sizer
614 if (mxVertexHandler.prototype.singleSizer)
616 this.connectorImg.style.left = (this.state.x + this.state.width - this.connectorImg.offsetWidth / 2) + 'px';
617 this.connectorImg.style.top = (this.state.y - this.connectorImg.offsetHeight / 2) + 'px';
621 this.connectorImg.style.left = (this.state.x + this.state.width + mxConstants.HANDLE_SIZE / 2 + 4/* - 2 padding*/) + 'px';
622 this.connectorImg.style.top = (this.state.y + (this.state.height - this.connectorImg.offsetHeight) / 2) + 'px';
627 var vertexHandlerDestroy = mxVertexHandler.prototype.destroy;
628 mxVertexHandler.prototype.destroy = function(sender, me)
630 vertexHandlerDestroy.apply(this, arguments);
632 if (this.connectorImg != null)
634 this.connectorImg.parentNode.removeChild(this.connectorImg);
635 this.connectorImg = null;
639 // Pre-fetches touch connector
640 new Image().src = connectorSrc;
644 var img = new mxImage(IMAGE_PATH + '/connector.png', 15, 15);
645 mxConnectionHandler.prototype.connectImage = img;
648 new Image().src = img.src;
650 if (urlParams['connect'] == '2') // not touchStyle
652 var img = new mxImage(IMAGE_PATH + '/connector.png', 15, 15);
654 var vertexHandlerInit = mxVertexHandler.prototype.init;
655 mxVertexHandler.prototype.init = function()
657 vertexHandlerInit.apply(this, arguments);
659 // Only show connector image on one cell and do not show on containers
660 if (showConnectorImg && this.graph.connectionHandler.isEnabled() &&
661 this.graph.isCellConnectable(this.state.cell) &&
662 !this.graph.isValidRoot(this.state.cell) &&
663 this.graph.getSelectionCount() == 1)
665 // Workaround for event redirection via image tag in quirks and IE8
666 if (mxClient.IS_IE && !mxClient.IS_SVG)
668 this.connectorImg = document.createElement('div');
669 this.connectorImg.style.backgroundImage = 'url(' + img.src + ')';
670 this.connectorImg.style.backgroundPosition = 'center';
671 this.connectorImg.style.backgroundRepeat = 'no-repeat';
672 this.connectorImg.style.width = (img.width + 4) + 'px';
673 this.connectorImg.style.height = (img.height + 4) + 'px';
674 this.connectorImg.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block';
678 this.connectorImg = mxUtils.createImage(img.src);
679 this.connectorImg.style.width = img.width + 'px';
680 this.connectorImg.style.height = img.height + 'px';
683 this.connectorImg.style.cursor = 'pointer';
684 this.connectorImg.style.position = 'absolute';
685 this.connectorImg.setAttribute('title', mxResources.get('connect'));
686 mxEvent.redirectMouseEvents(this.connectorImg, this.graph, this.state);
688 // Adds 2px tolerance
689 this.connectorImg.style.padding = '2px';
691 // Starts connecting on touch/mouse down
692 mxEvent.addListener(this.connectorImg, 'mousedown',
693 mxUtils.bind(this, function(evt)
695 this.graph.panningHandler.hideMenu();
696 var pt = mxUtils.convertPoint(this.graph.container,
697 mxEvent.getClientX(evt), mxEvent.getClientY(evt));
698 this.graph.connectionHandler.start(this.state, pt.x, pt.y);
699 this.graph.isMouseDown = true;
700 mxEvent.consume(evt);
704 this.graph.container.appendChild(this.connectorImg);
710 var vertexHandlerRedraw = mxVertexHandler.prototype.redraw;
711 mxVertexHandler.prototype.redraw = function()
713 vertexHandlerRedraw.apply(this);
717 mxVertexHandler.prototype.redrawTools = function()
719 if (this.state != null && this.connectorImg != null)
721 // Top right for single-sizer
722 if (mxVertexHandler.prototype.singleSizer)
724 this.connectorImg.style.left = (this.state.x + this.state.width - this.connectorImg.offsetWidth / 2) + 'px';
725 this.connectorImg.style.top = (this.state.y - this.connectorImg.offsetHeight / 2) + 'px';
729 this.connectorImg.style.left = (this.state.x + this.state.width + mxConstants.HANDLE_SIZE / 2 + 2/* - 2 padding*/) + 'px';
730 this.connectorImg.style.top = (this.state.y + (this.state.height - this.connectorImg.offsetHeight) / 2) + 'px';
735 var vertexHandlerDestroy = mxVertexHandler.prototype.destroy;
736 mxVertexHandler.prototype.destroy = function(sender, me)
738 vertexHandlerDestroy.apply(this, arguments);
740 if (this.connectorImg != null)
742 this.connectorImg.parentNode.removeChild(this.connectorImg);
743 this.connectorImg = null;