diff options
Diffstat (limited to 'src/js/view')
-rw-r--r-- | src/js/view/mxCellEditor.js | 522 | ||||
-rw-r--r-- | src/js/view/mxCellOverlay.js | 233 | ||||
-rw-r--r-- | src/js/view/mxCellRenderer.js | 1480 | ||||
-rw-r--r-- | src/js/view/mxCellState.js | 375 | ||||
-rw-r--r-- | src/js/view/mxCellStatePreview.js | 223 | ||||
-rw-r--r-- | src/js/view/mxConnectionConstraint.js | 42 | ||||
-rw-r--r-- | src/js/view/mxEdgeStyle.js | 1302 | ||||
-rw-r--r-- | src/js/view/mxGraph.js | 11176 | ||||
-rw-r--r-- | src/js/view/mxGraphSelectionModel.js | 435 | ||||
-rw-r--r-- | src/js/view/mxGraphView.js | 2545 | ||||
-rw-r--r-- | src/js/view/mxLayoutManager.js | 375 | ||||
-rw-r--r-- | src/js/view/mxMultiplicity.js | 257 | ||||
-rw-r--r-- | src/js/view/mxOutline.js | 649 | ||||
-rw-r--r-- | src/js/view/mxPerimeter.js | 484 | ||||
-rw-r--r-- | src/js/view/mxPrintPreview.js | 801 | ||||
-rw-r--r-- | src/js/view/mxSpaceManager.js | 460 | ||||
-rw-r--r-- | src/js/view/mxStyleRegistry.js | 70 | ||||
-rw-r--r-- | src/js/view/mxStylesheet.js | 266 | ||||
-rw-r--r-- | src/js/view/mxSwimlaneManager.js | 449 | ||||
-rw-r--r-- | src/js/view/mxTemporaryCellStates.js | 105 |
20 files changed, 22249 insertions, 0 deletions
diff --git a/src/js/view/mxCellEditor.js b/src/js/view/mxCellEditor.js new file mode 100644 index 0000000..2086cca --- /dev/null +++ b/src/js/view/mxCellEditor.js @@ -0,0 +1,522 @@ +/** + * $Id: mxCellEditor.js,v 1.62 2012-12-11 16:59:31 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellEditor + * + * In-place editor for the graph. To control this editor, use + * <mxGraph.invokesStopCellEditing>, <mxGraph.enterStopsCellEditing> and + * <mxGraph.escapeEnabled>. If <mxGraph.enterStopsCellEditing> is true then + * ctrl-enter or shift-enter can be used to create a linefeed. The F2 and + * escape keys can always be used to stop editing. To customize the location + * of the textbox in the graph, override <getEditorBounds> as follows: + * + * (code) + * graph.cellEditor.getEditorBounds = function(state) + * { + * var result = mxCellEditor.prototype.getEditorBounds.apply(this, arguments); + * + * if (this.graph.getModel().isEdge(state.cell)) + * { + * result.x = state.getCenterX() - result.width / 2; + * result.y = state.getCenterY() - result.height / 2; + * } + * + * return result; + * }; + * (end) + * + * The textarea uses the mxCellEditor CSS class. You can modify this class in + * your custom CSS. Note: You should modify the CSS after loading the client + * in the page. + * + * Example: + * + * To only allow numeric input in the in-place editor, use the following code. + * + * (code) + * var text = graph.cellEditor.textarea; + * + * mxEvent.addListener(text, 'keydown', function (evt) + * { + * if (!(evt.keyCode >= 48 && evt.keyCode <= 57) && + * !(evt.keyCode >= 96 && evt.keyCode <= 105)) + * { + * mxEvent.consume(evt); + * } + * }); + * (end) + * + * Initial values: + * + * To implement an initial value for cells without a label, use the + * <emptyLabelText> variable. + * + * Resize in Chrome: + * + * Resize of the textarea is disabled by default. If you want to enable + * this feature extend <init> and set this.textarea.style.resize = ''. + * + * Constructor: mxCellEditor + * + * Constructs a new in-place editor for the specified graph. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxCellEditor(graph) +{ + this.graph = graph; +}; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxCellEditor.prototype.graph = null; + +/** + * Variable: textarea + * + * Holds the input textarea. Note that this may be null before the first + * edit. Instantiated in <init>. + */ +mxCellEditor.prototype.textarea = null; + +/** + * Variable: editingCell + * + * Reference to the <mxCell> that is currently being edited. + */ +mxCellEditor.prototype.editingCell = null; + +/** + * Variable: trigger + * + * Reference to the event that was used to start editing. + */ +mxCellEditor.prototype.trigger = null; + +/** + * Variable: modified + * + * Specifies if the label has been modified. + */ +mxCellEditor.prototype.modified = false; + +/** + * Variable: emptyLabelText + * + * Text to be displayed for empty labels. Default is ''. This can be set + * to eg. "[Type Here]" to easier visualize editing of empty labels. The + * value is only displayed before the first keystroke and is never used + * as the actual editin value. + */ +mxCellEditor.prototype.emptyLabelText = ''; + +/** + * Variable: textNode + * + * Reference to the label DOM node that has been hidden. + */ +mxCellEditor.prototype.textNode = ''; + +/** + * Function: init + * + * Creates the <textarea> and installs the event listeners. The key handler + * updates the <modified> state. + */ +mxCellEditor.prototype.init = function () +{ + this.textarea = document.createElement('textarea'); + + this.textarea.className = 'mxCellEditor'; + this.textarea.style.position = 'absolute'; + this.textarea.style.overflow = 'visible'; + + this.textarea.setAttribute('cols', '20'); + this.textarea.setAttribute('rows', '4'); + + if (mxClient.IS_GC) + { + this.textarea.style.resize = 'none'; + } + + mxEvent.addListener(this.textarea, 'blur', mxUtils.bind(this, function(evt) + { + this.focusLost(); + })); + + mxEvent.addListener(this.textarea, 'keydown', mxUtils.bind(this, function(evt) + { + if (!mxEvent.isConsumed(evt)) + { + if (evt.keyCode == 113 /* F2 */ || (this.graph.isEnterStopsCellEditing() && + evt.keyCode == 13 /* Enter */ && !mxEvent.isControlDown(evt) && + !mxEvent.isShiftDown(evt))) + { + this.graph.stopEditing(false); + mxEvent.consume(evt); + } + else if (evt.keyCode == 27 /* Escape */) + { + this.graph.stopEditing(true); + mxEvent.consume(evt); + } + else + { + // Clears the initial empty label on the first keystroke + if (this.clearOnChange) + { + this.clearOnChange = false; + this.textarea.value = ''; + } + + // Updates the modified flag for storing the value + this.setModified(true); + } + } + })); +}; + +/** + * Function: isModified + * + * Returns <modified>. + */ +mxCellEditor.prototype.isModified = function() +{ + return this.modified; +}; + +/** + * Function: setModified + * + * Sets <modified> to the specified boolean value. + */ +mxCellEditor.prototype.setModified = function(value) +{ + this.modified = value; +}; + +/** + * Function: focusLost + * + * Called if the textarea has lost focus. + */ +mxCellEditor.prototype.focusLost = function() +{ + this.stopEditing(!this.graph.isInvokesStopCellEditing()); +}; + +/** + * Function: startEditing + * + * Starts the editor for the given cell. + * + * Parameters: + * + * cell - <mxCell> to start editing. + * trigger - Optional mouse event that triggered the editor. + */ +mxCellEditor.prototype.startEditing = function(cell, trigger) +{ + // Lazy instantiates textarea to save memory in IE + if (this.textarea == null) + { + this.init(); + } + + this.stopEditing(true); + var state = this.graph.getView().getState(cell); + + if (state != null) + { + this.editingCell = cell; + this.trigger = trigger; + this.textNode = null; + + if (state.text != null && this.isHideLabel(state)) + { + this.textNode = state.text.node; + this.textNode.style.visibility = 'hidden'; + } + + // Configures the style of the in-place editor + var scale = this.graph.getView().scale; + var size = mxUtils.getValue(state.style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE) * scale; + var family = mxUtils.getValue(state.style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY); + var color = mxUtils.getValue(state.style, mxConstants.STYLE_FONTCOLOR, 'black'); + var align = (this.graph.model.isEdge(state.cell)) ? mxConstants.ALIGN_LEFT : + mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT); + var bold = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) & + mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD; + var italic = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) & + mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC; + var uline = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) & + mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE; + + this.textarea.style.fontSize = size + 'px'; + this.textarea.style.fontFamily = family; + this.textarea.style.textAlign = align; + this.textarea.style.color = color; + this.textarea.style.fontWeight = (bold) ? 'bold' : 'normal'; + this.textarea.style.fontStyle = (italic) ? 'italic' : ''; + this.textarea.style.textDecoration = (uline) ? 'underline' : ''; + + // Specifies the bounds of the editor box + var bounds = this.getEditorBounds(state); + + this.textarea.style.left = bounds.x + 'px'; + this.textarea.style.top = bounds.y + 'px'; + this.textarea.style.width = bounds.width + 'px'; + this.textarea.style.height = bounds.height + 'px'; + this.textarea.style.zIndex = 5; + + var value = this.getInitialValue(state, trigger); + + // Uses an optional text value for empty labels which is cleared + // when the first keystroke appears. This makes it easier to see + // that a label is being edited even if the label is empty. + if (value == null || value.length == 0) + { + value = this.getEmptyLabelText(); + this.clearOnChange = true; + } + else + { + this.clearOnChange = false; + } + + this.setModified(false); + this.textarea.value = value; + this.graph.container.appendChild(this.textarea); + + if (this.textarea.style.display != 'none') + { + // FIXME: Doesn't bring up the virtual keyboard on iPad + this.textarea.focus(); + this.textarea.select(); + } + } +}; + +/** + * Function: stopEditing + * + * Stops the editor and applies the value if cancel is false. + */ +mxCellEditor.prototype.stopEditing = function(cancel) +{ + cancel = cancel || false; + + if (this.editingCell != null) + { + if (this.textNode != null) + { + this.textNode.style.visibility = 'visible'; + this.textNode = null; + } + + if (!cancel && this.isModified()) + { + this.graph.labelChanged(this.editingCell, this.getCurrentValue(), this.trigger); + } + + this.editingCell = null; + this.trigger = null; + this.textarea.blur(); + this.textarea.parentNode.removeChild(this.textarea); + } +}; + +/** + * Function: getInitialValue + * + * Gets the initial editing value for the given cell. + */ +mxCellEditor.prototype.getInitialValue = function(state, trigger) +{ + return this.graph.getEditingValue(state.cell, trigger); +}; + +/** + * Function: getCurrentValue + * + * Returns the current editing value. + */ +mxCellEditor.prototype.getCurrentValue = function() +{ + return this.textarea.value.replace(/\r/g, ''); +}; + +/** + * Function: isHideLabel + * + * Returns true if the label should be hidden while the cell is being + * edited. + */ +mxCellEditor.prototype.isHideLabel = function(state) +{ + return true; +}; + +/** + * Function: getMinimumSize + * + * Returns the minimum width and height for editing the given state. + */ +mxCellEditor.prototype.getMinimumSize = function(state) +{ + var scale = this.graph.getView().scale; + + return new mxRectangle(0, 0, (state.text == null) ? 30 : state.text.size * scale + 20, + (this.textarea.style.textAlign == 'left') ? 120 : 40); +}; + +/** + * Function: getEditorBounds + * + * Returns the <mxRectangle> that defines the bounds of the editor. + */ +mxCellEditor.prototype.getEditorBounds = function(state) +{ + var isEdge = this.graph.getModel().isEdge(state.cell); + var scale = this.graph.getView().scale; + var minSize = this.getMinimumSize(state); + var minWidth = minSize.width; + var minHeight = minSize.height; + + var spacing = parseInt(state.style[mxConstants.STYLE_SPACING] || 2) * scale; + var spacingTop = (parseInt(state.style[mxConstants.STYLE_SPACING_TOP] || 0)) * scale + spacing; + var spacingRight = (parseInt(state.style[mxConstants.STYLE_SPACING_RIGHT] || 0)) * scale + spacing; + var spacingBottom = (parseInt(state.style[mxConstants.STYLE_SPACING_BOTTOM] || 0)) * scale + spacing; + var spacingLeft = (parseInt(state.style[mxConstants.STYLE_SPACING_LEFT] || 0)) * scale + spacing; + + var result = new mxRectangle(state.x, state.y, + Math.max(minWidth, state.width - spacingLeft - spacingRight), + Math.max(minHeight, state.height - spacingTop - spacingBottom)); + + if (isEdge) + { + result.x = state.absoluteOffset.x; + result.y = state.absoluteOffset.y; + + if (state.text != null && state.text.boundingBox != null) + { + // Workaround for label containing just spaces in which case + // the bounding box location contains negative numbers + if (state.text.boundingBox.x > 0) + { + result.x = state.text.boundingBox.x; + } + + if (state.text.boundingBox.y > 0) + { + result.y = state.text.boundingBox.y; + } + } + } + else if (state.text != null && state.text.boundingBox != null) + { + result.x = Math.min(result.x, state.text.boundingBox.x); + result.y = Math.min(result.y, state.text.boundingBox.y); + } + + result.x += spacingLeft; + result.y += spacingTop; + + if (state.text != null && state.text.boundingBox != null) + { + if (!isEdge) + { + result.width = Math.max(result.width, state.text.boundingBox.width); + result.height = Math.max(result.height, state.text.boundingBox.height); + } + else + { + result.width = Math.max(minWidth, state.text.boundingBox.width); + result.height = Math.max(minHeight, state.text.boundingBox.height); + } + } + + // Applies the horizontal and vertical label positions + if (this.graph.getModel().isVertex(state.cell)) + { + var horizontal = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER); + + if (horizontal == mxConstants.ALIGN_LEFT) + { + result.x -= state.width; + } + else if (horizontal == mxConstants.ALIGN_RIGHT) + { + result.x += state.width; + } + + var vertical = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE); + + if (vertical == mxConstants.ALIGN_TOP) + { + result.y -= state.height; + } + else if (vertical == mxConstants.ALIGN_BOTTOM) + { + result.y += state.height; + } + } + + return result; +}; + +/** + * Function: getEmptyLabelText + * + * Returns the initial label value to be used of the label of the given + * cell is empty. This label is displayed and cleared on the first keystroke. + * This implementation returns <emptyLabelText>. + * + * Parameters: + * + * cell - <mxCell> for which a text for an empty editing box should be + * returned. + */ +mxCellEditor.prototype.getEmptyLabelText = function (cell) +{ + return this.emptyLabelText; +}; + +/** + * Function: getEditingCell + * + * Returns the cell that is currently being edited or null if no cell is + * being edited. + */ +mxCellEditor.prototype.getEditingCell = function () +{ + return this.editingCell; +}; + +/** + * Function: destroy + * + * Destroys the editor and removes all associated resources. + */ +mxCellEditor.prototype.destroy = function () +{ + if (this.textarea != null) + { + mxEvent.release(this.textarea); + + if (this.textarea.parentNode != null) + { + this.textarea.parentNode.removeChild(this.textarea); + } + + this.textarea = null; + } +}; diff --git a/src/js/view/mxCellOverlay.js b/src/js/view/mxCellOverlay.js new file mode 100644 index 0000000..316e2c4 --- /dev/null +++ b/src/js/view/mxCellOverlay.js @@ -0,0 +1,233 @@ +/** + * $Id: mxCellOverlay.js,v 1.18 2012-12-06 15:58:44 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellOverlay + * + * Extends <mxEventSource> to implement a graph overlay, represented by an icon + * and a tooltip. Overlays can handle and fire <click> events and are added to + * the graph using <mxGraph.addCellOverlay>, and removed using + * <mxGraph.removeCellOverlay>, or <mxGraph.removeCellOverlays> to remove all overlays. + * The <mxGraph.getCellOverlays> function returns the array of overlays for a given + * cell in a graph. If multiple overlays exist for the same cell, then + * <getBounds> should be overridden in at least one of the overlays. + * + * Overlays appear on top of all cells in a special layer. If this is not + * desirable, then the image must be rendered as part of the shape or label of + * the cell instead. + * + * Example: + * + * The following adds a new overlays for a given vertex and selects the cell + * if the overlay is clicked. + * + * (code) + * var overlay = new mxCellOverlay(img, html); + * graph.addCellOverlay(vertex, overlay); + * overlay.addListener(mxEvent.CLICK, function(sender, evt) + * { + * var cell = evt.getProperty('cell'); + * graph.setSelectionCell(cell); + * }); + * (end) + * + * For cell overlays to be printed use <mxPrintPreview.printOverlays>. + * + * Event: mxEvent.CLICK + * + * Fires when the user clicks on the overlay. The <code>event</code> property + * contains the corresponding mouse event and the <code>cell</code> property + * contains the cell. For touch devices this is fired if the element receives + * a touchend event. + * + * Constructor: mxCellOverlay + * + * Constructs a new overlay using the given image and tooltip. + * + * Parameters: + * + * image - <mxImage> that represents the icon to be displayed. + * tooltip - Optional string that specifies the tooltip. + * align - Optional horizontal alignment for the overlay. Possible + * values are <ALIGN_LEFT>, <ALIGN_CENTER> and <ALIGN_RIGHT> + * (default). + * verticalAlign - Vertical alignment for the overlay. Possible + * values are <ALIGN_TOP>, <ALIGN_MIDDLE> and <ALIGN_BOTTOM> + * (default). + */ +function mxCellOverlay(image, tooltip, align, verticalAlign, offset, cursor) +{ + this.image = image; + this.tooltip = tooltip; + this.align = (align != null) ? align : this.align; + this.verticalAlign = (verticalAlign != null) ? verticalAlign : this.verticalAlign; + this.offset = (offset != null) ? offset : new mxPoint(); + this.cursor = (cursor != null) ? cursor : 'help'; +}; + +/** + * Extends mxEventSource. + */ +mxCellOverlay.prototype = new mxEventSource(); +mxCellOverlay.prototype.constructor = mxCellOverlay; + +/** + * Variable: image + * + * Holds the <mxImage> to be used as the icon. + */ +mxCellOverlay.prototype.image = null; + +/** + * Variable: tooltip + * + * Holds the optional string to be used as the tooltip. + */ +mxCellOverlay.prototype.tooltip = null; + +/** + * Variable: align + * + * Holds the horizontal alignment for the overlay. Default is + * <mxConstants.ALIGN_RIGHT>. For edges, the overlay always appears in the + * center of the edge. + */ +mxCellOverlay.prototype.align = mxConstants.ALIGN_RIGHT; + +/** + * Variable: verticalAlign + * + * Holds the vertical alignment for the overlay. Default is + * <mxConstants.ALIGN_BOTTOM>. For edges, the overlay always appears in the + * center of the edge. + */ +mxCellOverlay.prototype.verticalAlign = mxConstants.ALIGN_BOTTOM; + +/** + * Variable: offset + * + * Holds the offset as an <mxPoint>. The offset will be scaled according to the + * current scale. + */ +mxCellOverlay.prototype.offset = null; + +/** + * Variable: cursor + * + * Holds the cursor for the overlay. Default is 'help'. + */ +mxCellOverlay.prototype.cursor = null; + +/** + * Variable: defaultOverlap + * + * Defines the overlapping for the overlay, that is, the proportional distance + * from the origin to the point defined by the alignment. Default is 0.5. + */ +mxCellOverlay.prototype.defaultOverlap = 0.5; + +/** + * Function: getBounds + * + * Returns the bounds of the overlay for the given <mxCellState> as an + * <mxRectangle>. This should be overridden when using multiple overlays + * per cell so that the overlays do not overlap. + * + * The following example will place the overlay along an edge (where + * x=[-1..1] from the start to the end of the edge and y is the + * orthogonal offset in px). + * + * (code) + * overlay.getBounds = function(state) + * { + * var bounds = mxCellOverlay.prototype.getBounds.apply(this, arguments); + * + * if (state.view.graph.getModel().isEdge(state.cell)) + * { + * var pt = state.view.getPoint(state, {x: 0, y: 0, relative: true}); + * + * bounds.x = pt.x - bounds.width / 2; + * bounds.y = pt.y - bounds.height / 2; + * } + * + * return bounds; + * }; + * (end) + * + * Parameters: + * + * state - <mxCellState> that represents the current state of the + * associated cell. + */ +mxCellOverlay.prototype.getBounds = function(state) +{ + var isEdge = state.view.graph.getModel().isEdge(state.cell); + var s = state.view.scale; + var pt = null; + + var w = this.image.width; + var h = this.image.height; + + if (isEdge) + { + var pts = state.absolutePoints; + + if (pts.length % 2 == 1) + { + pt = pts[Math.floor(pts.length / 2)]; + } + else + { + var idx = pts.length / 2; + var p0 = pts[idx-1]; + var p1 = pts[idx]; + pt = new mxPoint(p0.x + (p1.x - p0.x) / 2, + p0.y + (p1.y - p0.y) / 2); + } + } + else + { + pt = new mxPoint(); + + if (this.align == mxConstants.ALIGN_LEFT) + { + pt.x = state.x; + } + else if (this.align == mxConstants.ALIGN_CENTER) + { + pt.x = state.x + state.width / 2; + } + else + { + pt.x = state.x + state.width; + } + + if (this.verticalAlign == mxConstants.ALIGN_TOP) + { + pt.y = state.y; + } + else if (this.verticalAlign == mxConstants.ALIGN_MIDDLE) + { + pt.y = state.y + state.height / 2; + } + else + { + pt.y = state.y + state.height; + } + } + + return new mxRectangle(pt.x - (w * this.defaultOverlap - this.offset.x) * s, + pt.y - (h * this.defaultOverlap - this.offset.y) * s, w * s, h * s); +}; + +/** + * Function: toString + * + * Returns the textual representation of the overlay to be used as the + * tooltip. This implementation returns <tooltip>. + */ +mxCellOverlay.prototype.toString = function() +{ + return this.tooltip; +}; diff --git a/src/js/view/mxCellRenderer.js b/src/js/view/mxCellRenderer.js new file mode 100644 index 0000000..6b506ad --- /dev/null +++ b/src/js/view/mxCellRenderer.js @@ -0,0 +1,1480 @@ +/** + * $Id: mxCellRenderer.js,v 1.189 2012-11-20 09:06:07 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellRenderer + * + * Renders cells into a document object model. The <defaultShapes> is a global + * map of shapename, constructor pairs that is used in all instances. You can + * get a list of all available shape names using the following code. + * + * In general the cell renderer is in charge of creating, redrawing and + * destroying the shape and label associated with a cell state, as well as + * some other graphical objects, namely controls and overlays. The shape + * hieararchy in the display (ie. the hierarchy in which the DOM nodes + * appear in the document) does not reflect the cell hierarchy. The shapes + * are a (flat) sequence of shapes and labels inside the draw pane of the + * graph view, with some exceptions, namely the HTML labels being placed + * directly inside the graph container for certain browsers. + * + * (code) + * mxLog.show(); + * for (var i in mxCellRenderer.prototype.defaultShapes) + * { + * mxLog.debug(i); + * } + * (end) + * + * Constructor: mxCellRenderer + * + * Constructs a new cell renderer with the following built-in shapes: + * arrow, rectangle, ellipse, rhombus, image, line, label, cylinder, + * swimlane, connector, actor and cloud. + */ +function mxCellRenderer() +{ + this.shapes = mxUtils.clone(this.defaultShapes); +}; + +/** + * Variable: shapes + * + * Array that maps from shape names to shape constructors. All entries + * in <defaultShapes> are added to this array. + */ +mxCellRenderer.prototype.shapes = null; + +/** + * Variable: defaultEdgeShape + * + * Defines the default shape for edges. Default is <mxConnector>. + */ +mxCellRenderer.prototype.defaultEdgeShape = mxConnector; + +/** + * Variable: defaultVertexShape + * + * Defines the default shape for vertices. Default is <mxRectangleShape>. + */ +mxCellRenderer.prototype.defaultVertexShape = mxRectangleShape; + +/** + * Variable: defaultShapes + * + * Static array that contains the globally registered shapes which are + * known to all instances of this class. For adding instance-specific + * shapes you should use <registerShape> on the instance. For adding + * a shape to this array you can use the following code: + * + * (code) + * mxCellRenderer.prototype.defaultShapes['myshape'] = myShape; + * (end) + * + * Where 'myshape' is the key under which the shape is to be registered + * and myShape is the name of the constructor function. + */ +mxCellRenderer.prototype.defaultShapes = new Object(); + +// Adds default shapes into the default shapes array +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_ARROW] = mxArrow; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_RECTANGLE] = mxRectangleShape; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_ELLIPSE] = mxEllipse; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_DOUBLE_ELLIPSE] = mxDoubleEllipse; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_RHOMBUS] = mxRhombus; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_IMAGE] = mxImageShape; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_LINE] = mxLine; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_LABEL] = mxLabel; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_CYLINDER] = mxCylinder; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_SWIMLANE] = mxSwimlane; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_CONNECTOR] = mxConnector; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_ACTOR] = mxActor; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_CLOUD] = mxCloud; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_TRIANGLE] = mxTriangle; +mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_HEXAGON] = mxHexagon; + +/** + * Function: registerShape + * + * Registers the given constructor under the specified key in this instance + * of the renderer. + * + * Example: + * + * (code) + * this.registerShape(mxConstants.SHAPE_RECTANGLE, mxRectangleShape); + * (end) + * + * Parameters: + * + * key - String representing the shape name. + * shape - Constructor of the <mxShape> subclass. + */ +mxCellRenderer.prototype.registerShape = function(key, shape) +{ + this.shapes[key] = shape; +}; + +/** + * Function: initialize + * + * Initializes the display for the given cell state. This is required once + * after the cell state has been created. This is invoked in + * mxGraphView.createState. + * + * Parameters: + * + * state - <mxCellState> for which the display should be initialized. + * rendering - Optional boolean that specifies if the cell should actually + * be initialized for any given DOM node. If this is false then init + * will not be called on the shape. + */ +mxCellRenderer.prototype.initialize = function(state, rendering) +{ + var model = state.view.graph.getModel(); + + if (state.view.graph.container != null && state.shape == null && + state.cell != state.view.currentRoot && + (model.isVertex(state.cell) || model.isEdge(state.cell))) + { + this.createShape(state); + + if (state.shape != null && (rendering == null || rendering)) + { + this.initializeShape(state); + + // Maintains the model order in the DOM + if (state.view.graph.ordered || model.isEdge(state.cell)) + { + //state.orderChanged = true; + state.invalidOrder = true; + } + else if (state.view.graph.keepEdgesInForeground && this.firstEdge != null) + { + if (this.firstEdge.parentNode == state.shape.node.parentNode) + { + this.insertState(state, this.firstEdge); + } + else + { + this.firstEdge = null; + } + } + + state.shape.scale = state.view.scale; + + this.createCellOverlays(state); + this.installListeners(state); + } + } +}; + +/** + * Function: initializeShape + * + * Initializes the shape in the given state by calling its init method with + * the correct container. + * + * Parameters: + * + * state - <mxCellState> for which the shape should be initialized. + */ +mxCellRenderer.prototype.initializeShape = function(state) +{ + state.shape.init(state.view.getDrawPane()); +}; + +/** + * Returns the previous state that has a shape inside the given parent. + */ +mxCellRenderer.prototype.getPreviousStateInContainer = function(state, container) +{ + var result = null; + var graph = state.view.graph; + var model = graph.getModel(); + var child = state.cell; + var p = model.getParent(child); + + while (p != null && result == null) + { + result = this.findPreviousStateInContainer(graph, p, child, container); + child = p; + p = model.getParent(child); + } + + return result; +}; + +/** + * Returns the previous state that has a shape inside the given parent. + */ +mxCellRenderer.prototype.findPreviousStateInContainer = function(graph, cell, stop, container) +{ + // Recurse first + var result = null; + var model = graph.getModel(); + + if (stop != null) + { + var start = cell.getIndex(stop); + + for (var i = start - 1; i >= 0 && result == null; i--) + { + result = this.findPreviousStateInContainer(graph, model.getChildAt(cell, i), null, container); + } + } + else + { + var childCount = model.getChildCount(cell); + + for (var i = childCount - 1; i >= 0 && result == null; i--) + { + result = this.findPreviousStateInContainer(graph, model.getChildAt(cell, i), null, container); + } + } + + if (result == null) + { + result = graph.view.getState(cell); + + if (result != null && (result.shape == null || result.shape.node == null || + result.shape.node.parentNode != container)) + { + result = null; + } + } + + return result; +}; + +/** + * Function: order + * + * Orders the DOM node of the shape for the given state according to the + * position of the corresponding cell in the graph model. + * + * Parameters: + * + * state - <mxCellState> whose shape's DOM node should be ordered. + */ +mxCellRenderer.prototype.order = function(state) +{ + var container = state.shape.node.parentNode; + var previous = this.getPreviousStateInContainer(state, container); + var nextNode = container.firstChild; + + if (previous != null) + { + nextNode = previous.shape.node; + + if (previous.text != null && previous.text.node != null && + previous.text.node.parentNode == container) + { + nextNode = previous.text.node; + } + + nextNode = nextNode.nextSibling; + } + + this.insertState(state, nextNode); +}; + +/** + * Function: orderEdge + * + * Orders the DOM node of the shape for the given edge's state according to + * the <mxGraph.keepEdgesInBackground> and <mxGraph.keepEdgesInBackground> + * rules. + * + * Parameters: + * + * state - <mxCellState> whose shape's DOM node should be ordered. + */ +mxCellRenderer.prototype.orderEdge = function(state) +{ + var view = state.view; + var model = view.graph.getModel(); + + // Moves edges to the foreground/background + if (view.graph.keepEdgesInForeground) + { + if (this.firstEdge == null || this.firstEdge.parentNode == null || + this.firstEdge.parentNode != state.shape.node.parentNode) + { + this.firstEdge = state.shape.node; + } + } + else if (view.graph.keepEdgesInBackground) + { + var node = state.shape.node; + var parent = node.parentNode; + + // Keeps the DOM node in front of its parent + var pcell = model.getParent(state.cell); + var pstate = view.getState(pcell); + + if (pstate != null && pstate.shape != null && pstate.shape.node != null) + { + var child = pstate.shape.node.nextSibling; + + if (child != null && child != node) + { + this.insertState(state, child); + } + } + else + { + var child = parent.firstChild; + + if (child != null && child != node) + { + this.insertState(state, child); + } + } + } +}; + +/** + * Function: insertState + * + * Inserts the given state before the given node into its parent. + * + * Parameters: + * + * state - <mxCellState> for which the shape should be created. + */ +mxCellRenderer.prototype.insertState = function(state, nextNode) +{ + state.shape.node.parentNode.insertBefore(state.shape.node, nextNode); + + if (state.text != null && state.text.node != null && + state.text.node.parentNode == state.shape.node.parentNode) + { + state.shape.node.parentNode.insertBefore(state.text.node, state.shape.node.nextSibling); + } +}; + +/** + * Function: createShape + * + * Creates the shape for the given cell state. The shape is configured + * using <configureShape>. + * + * Parameters: + * + * state - <mxCellState> for which the shape should be created. + */ +mxCellRenderer.prototype.createShape = function(state) +{ + if (state.style != null) + { + // Checks if there is a stencil for the name and creates + // a shape instance for the stencil if one exists + var key = state.style[mxConstants.STYLE_SHAPE]; + var stencil = mxStencilRegistry.getStencil(key); + + if (stencil != null) + { + state.shape = new mxStencilShape(stencil); + } + else + { + var ctor = this.getShapeConstructor(state); + state.shape = new ctor(); + } + + // Sets the initial bounds and points (will be updated in redraw) + state.shape.points = state.absolutePoints; + state.shape.bounds = new mxRectangle( + state.x, state.y, state.width, state.height); + state.shape.dialect = state.view.graph.dialect; + + this.configureShape(state); + } +}; + +/** + * Function: getShapeConstructor + * + * Returns the constructor to be used for creating the shape. + */ +mxCellRenderer.prototype.getShapeConstructor = function(state) +{ + var key = state.style[mxConstants.STYLE_SHAPE]; + var ctor = (key != null) ? this.shapes[key] : null; + + if (ctor == null) + { + ctor = (state.view.graph.getModel().isEdge(state.cell)) ? + this.defaultEdgeShape : this.defaultVertexShape; + } + + return ctor; +}; + +/** + * Function: configureShape + * + * Configures the shape for the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the shape should be configured. + */ +mxCellRenderer.prototype.configureShape = function(state) +{ + state.shape.apply(state); + var image = state.view.graph.getImage(state); + + if (image != null) + { + state.shape.image = image; + } + + var indicator = state.view.graph.getIndicatorColor(state); + var key = state.view.graph.getIndicatorShape(state); + var ctor = (key != null) ? this.shapes[key] : null; + + // Configures the indicator shape or image + if (indicator != null) + { + state.shape.indicatorShape = ctor; + state.shape.indicatorColor = indicator; + state.shape.indicatorGradientColor = + state.view.graph.getIndicatorGradientColor(state); + state.shape.indicatorDirection = + state.style[mxConstants.STYLE_INDICATOR_DIRECTION]; + } + else + { + var indicator = state.view.graph.getIndicatorImage(state); + + if (indicator != null) + { + state.shape.indicatorImage = indicator; + } + } + + this.postConfigureShape(state); +}; + +/** + * Function: postConfigureShape + * + * Replaces any reserved words used for attributes, eg. inherit, + * indicated or swimlane for colors in the shape for the given state. + * This implementation resolves these keywords on the fill, stroke + * and gradient color keys. + */ +mxCellRenderer.prototype.postConfigureShape = function(state) +{ + if (state.shape != null) + { + this.resolveColor(state, 'indicatorColor', mxConstants.STYLE_FILLCOLOR); + this.resolveColor(state, 'indicatorGradientColor', mxConstants.STYLE_GRADIENTCOLOR); + this.resolveColor(state, 'fill', mxConstants.STYLE_FILLCOLOR); + this.resolveColor(state, 'stroke', mxConstants.STYLE_STROKECOLOR); + this.resolveColor(state, 'gradient', mxConstants.STYLE_GRADIENTCOLOR); + } +}; + +/** + * Function: resolveColor + * + * Resolves special keywords 'inherit', 'indicated' and 'swimlane' and sets + * the respective color on the shape. + */ +mxCellRenderer.prototype.resolveColor = function(state, field, key) +{ + var value = state.shape[field]; + var graph = state.view.graph; + var referenced = null; + + if (value == 'inherit') + { + referenced = graph.model.getParent(state.cell); + } + else if (value == 'swimlane') + { + if (graph.model.getTerminal(state.cell, false) != null) + { + referenced = graph.model.getTerminal(state.cell, false); + } + else + { + referenced = state.cell; + } + + referenced = graph.getSwimlane(referenced); + key = graph.swimlaneIndicatorColorAttribute; + } + else if (value == 'indicated') + { + state.shape[field] = state.shape.indicatorColor; + } + + if (referenced != null) + { + var rstate = graph.getView().getState(referenced); + state.shape[field] = null; + + if (rstate != null) + { + if (rstate.shape != null && field != 'indicatorColor') + { + state.shape[field] = rstate.shape[field]; + } + else + { + state.shape[field] = rstate.style[key]; + } + } + } +}; + +/** + * Function: getLabelValue + * + * Returns the value to be used for the label. + * + * Parameters: + * + * state - <mxCellState> for which the label should be created. + */ +mxCellRenderer.prototype.getLabelValue = function(state) +{ + var graph = state.view.graph; + var value = graph.getLabel(state.cell); + + if (!graph.isHtmlLabel(state.cell) && !mxUtils.isNode(value) && + graph.dialect != mxConstants.DIALECT_SVG && value != null) + { + value = mxUtils.htmlEntities(value, false); + } + + return value; +}; + +/** + * Function: createLabel + * + * Creates the label for the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the label should be created. + */ +mxCellRenderer.prototype.createLabel = function(state, value) +{ + var graph = state.view.graph; + var isEdge = graph.getModel().isEdge(state.cell); + + if (state.style[mxConstants.STYLE_FONTSIZE] > 0 || + state.style[mxConstants.STYLE_FONTSIZE] == null) + { + // Avoids using DOM node for empty labels + var isForceHtml = (graph.isHtmlLabel(state.cell) || + (value != null && mxUtils.isNode(value))) && + graph.dialect == mxConstants.DIALECT_SVG; + + state.text = new mxText(value, new mxRectangle(), + (state.style[mxConstants.STYLE_ALIGN] || + mxConstants.ALIGN_CENTER), + graph.getVerticalAlign(state), + state.style[mxConstants.STYLE_FONTCOLOR], + state.style[mxConstants.STYLE_FONTFAMILY], + state.style[mxConstants.STYLE_FONTSIZE], + state.style[mxConstants.STYLE_FONTSTYLE], + state.style[mxConstants.STYLE_SPACING], + state.style[mxConstants.STYLE_SPACING_TOP], + state.style[mxConstants.STYLE_SPACING_RIGHT], + state.style[mxConstants.STYLE_SPACING_BOTTOM], + state.style[mxConstants.STYLE_SPACING_LEFT], + state.style[mxConstants.STYLE_HORIZONTAL], + state.style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR], + state.style[mxConstants.STYLE_LABEL_BORDERCOLOR], + graph.isWrapping(state.cell) && graph.isHtmlLabel(state.cell), + graph.isLabelClipped(state.cell), + state.style[mxConstants.STYLE_OVERFLOW], + state.style[mxConstants.STYLE_LABEL_PADDING]); + state.text.opacity = state.style[mxConstants.STYLE_TEXT_OPACITY]; + + state.text.dialect = (isForceHtml) ? + mxConstants.DIALECT_STRICTHTML : + state.view.graph.dialect; + this.initializeLabel(state); + + // Workaround for touch devices routing all events for a mouse + // gesture (down, move, up) via the initial DOM node. IE is even + // worse in that it redirects the event via the initial DOM node + // but the event source is the node under the mouse, so we need + // to check if this is the case and force getCellAt for the + // subsequent mouseMoves and the final mouseUp. + var forceGetCell = false; + + var getState = function(evt) + { + var result = state; + + if (mxClient.IS_TOUCH || forceGetCell) + { + var x = mxEvent.getClientX(evt); + var y = mxEvent.getClientY(evt); + + // Dispatches the drop event to the graph which + // consumes and executes the source function + var pt = mxUtils.convertPoint(graph.container, x, y); + result = graph.view.getState(graph.getCellAt(pt.x, pt.y)); + } + + return result; + }; + + // TODO: Add handling for gestures + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + mxEvent.addListener(state.text.node, md, + mxUtils.bind(this, function(evt) + { + if (this.isLabelEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, + new mxMouseEvent(evt, state)); + forceGetCell = graph.dialect != mxConstants.DIALECT_SVG && mxEvent.getSource(evt).nodeName == 'IMG'; + } + }) + ); + + mxEvent.addListener(state.text.node, mm, + mxUtils.bind(this, function(evt) + { + if (this.isLabelEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt, getState(evt))); + } + }) + ); + + mxEvent.addListener(state.text.node, mu, + mxUtils.bind(this, function(evt) + { + if (this.isLabelEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt, getState(evt))); + forceGetCell = false; + } + }) + ); + + mxEvent.addListener(state.text.node, 'dblclick', + mxUtils.bind(this, function(evt) + { + if (this.isLabelEvent(state, evt)) + { + graph.dblClick(evt, state.cell); + mxEvent.consume(evt); + } + }) + ); + } +}; + +/** + * Function: initializeLabel + * + * Initiailzes the label with a suitable container. + * + * Parameters: + * + * state - <mxCellState> whose label should be initialized. + */ +mxCellRenderer.prototype.initializeLabel = function(state) +{ + var graph = state.view.graph; + + if (state.text.dialect != mxConstants.DIALECT_SVG) + { + // Adds the text to the container if the dialect is not SVG and we + // have an SVG-based browser which doesn't support foreignObjects + if (mxClient.IS_SVG && mxClient.NO_FO) + { + state.text.init(graph.container); + } + else if (mxUtils.isVml(state.view.getDrawPane())) + { + if (state.shape.label != null) + { + state.text.init(state.shape.label); + } + else + { + state.text.init(state.shape.node); + } + } + } + + if (state.text.node == null) + { + state.text.init(state.view.getDrawPane()); + + if (state.shape != null && state.text != null) + { + state.shape.node.parentNode.insertBefore( + state.text.node, state.shape.node.nextSibling); + } + } +}; + +/** + * Function: createCellOverlays + * + * Creates the actual shape for showing the overlay for the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the overlay should be created. + */ +mxCellRenderer.prototype.createCellOverlays = function(state) +{ + var graph = state.view.graph; + var overlays = graph.getCellOverlays(state.cell); + var dict = null; + + if (overlays != null) + { + dict = new mxDictionary(); + + for (var i = 0; i < overlays.length; i++) + { + var shape = (state.overlays != null) ? state.overlays.remove(overlays[i]) : null; + + if (shape == null) + { + var tmp = new mxImageShape(new mxRectangle(), + overlays[i].image.src); + tmp.dialect = state.view.graph.dialect; + tmp.preserveImageAspect = false; + tmp.overlay = overlays[i]; + this.initializeOverlay(state, tmp); + this.installCellOverlayListeners(state, overlays[i], tmp); + + if (overlays[i].cursor != null) + { + tmp.node.style.cursor = overlays[i].cursor; + } + + dict.put(overlays[i], tmp); + } + else + { + dict.put(overlays[i], shape); + } + } + } + + // Removes unused + if (state.overlays != null) + { + state.overlays.visit(function(id, shape) + { + shape.destroy(); + }); + } + + state.overlays = dict; +}; + +/** + * Function: initializeOverlay + * + * Initializes the given overlay. + * + * Parameters: + * + * state - <mxCellState> for which the overlay should be created. + * overlay - <mxImageShape> that represents the overlay. + */ +mxCellRenderer.prototype.initializeOverlay = function(state, overlay) +{ + overlay.init(state.view.getOverlayPane()); +}; + +/** + * Function: installOverlayListeners + * + * Installs the listeners for the given <mxCellState>, <mxCellOverlay> and + * <mxShape> that represents the overlay. + */ +mxCellRenderer.prototype.installCellOverlayListeners = function(state, overlay, shape) +{ + var graph = state.view.graph; + + mxEvent.addListener(shape.node, 'click', function (evt) + { + if (graph.isEditing()) + { + graph.stopEditing(!graph.isInvokesStopCellEditing()); + } + + overlay.fireEvent(new mxEventObject(mxEvent.CLICK, + 'event', evt, 'cell', state.cell)); + }); + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + + mxEvent.addListener(shape.node, md, function (evt) + { + mxEvent.consume(evt); + }); + + mxEvent.addListener(shape.node, mm, function (evt) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt, state)); + }); + + if (mxClient.IS_TOUCH) + { + mxEvent.addListener(shape.node, 'touchend', function (evt) + { + overlay.fireEvent(new mxEventObject(mxEvent.CLICK, + 'event', evt, 'cell', state.cell)); + }); + } +}; + +/** + * Function: createControl + * + * Creates the control for the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the control should be created. + */ +mxCellRenderer.prototype.createControl = function(state) +{ + var graph = state.view.graph; + var image = graph.getFoldingImage(state); + + if (graph.foldingEnabled && image != null) + { + if (state.control == null) + { + var b = new mxRectangle(0, 0, image.width, image.height); + state.control = new mxImageShape(b, image.src); + state.control.dialect = graph.dialect; + state.control.preserveImageAspect = false; + + this.initControl(state, state.control, true, function (evt) + { + if (graph.isEnabled()) + { + var collapse = !graph.isCellCollapsed(state.cell); + graph.foldCells(collapse, false, [state.cell]); + mxEvent.consume(evt); + } + }); + } + } + else if (state.control != null) + { + state.control.destroy(); + state.control = null; + } +}; + +/** + * Function: initControl + * + * Initializes the given control and returns the corresponding DOM node. + * + * Parameters: + * + * state - <mxCellState> for which the control should be initialized. + * control - <mxShape> to be initialized. + * handleEvents - Boolean indicating if mousedown and mousemove should fire events via the graph. + * clickHandler - Optional function to implement clicks on the control. + */ +mxCellRenderer.prototype.initControl = function(state, control, handleEvents, clickHandler) +{ + var graph = state.view.graph; + + // In the special case where the label is in HTML and the display is SVG the image + // should go into the graph container directly in order to be clickable. Otherwise + // it is obscured by the HTML label that overlaps the cell. + var isForceHtml = graph.isHtmlLabel(state.cell) && + mxClient.NO_FO && + graph.dialect == mxConstants.DIALECT_SVG; + + if (isForceHtml) + { + control.dialect = mxConstants.DIALECT_PREFERHTML; + control.init(graph.container); + control.node.style.zIndex = 1; + } + else + { + control.init(state.view.getOverlayPane()); + } + + var node = control.innerNode || control.node; + + if (clickHandler) + { + if (graph.isEnabled()) + { + node.style.cursor = 'pointer'; + } + + mxEvent.addListener(node, 'click', clickHandler); + } + + if (handleEvents) + { + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + + mxEvent.addListener(node, md, function (evt) + { + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state)); + mxEvent.consume(evt); + }); + + mxEvent.addListener(node, mm, function (evt) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, state)); + }); + } + + return node; +}; + +/** + * Function: isShapeEvent + * + * Returns true if the event is for the shape of the given state. This + * implementation always returns true. + * + * Parameters: + * + * state - <mxCellState> whose shape fired the event. + * evt - Mouse event which was fired. + */ +mxCellRenderer.prototype.isShapeEvent = function(state, evt) +{ + return true; +}; + +/** + * Function: isLabelEvent + * + * Returns true if the event is for the label of the given state. This + * implementation always returns true. + * + * Parameters: + * + * state - <mxCellState> whose label fired the event. + * evt - Mouse event which was fired. + */ +mxCellRenderer.prototype.isLabelEvent = function(state, evt) +{ + return true; +}; + +/** + * Function: installListeners + * + * Installs the event listeners for the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the event listeners should be isntalled. + */ +mxCellRenderer.prototype.installListeners = function(state) +{ + var graph = state.view.graph; + + // Receives events from transparent backgrounds + if (graph.dialect == mxConstants.DIALECT_SVG) + { + var events = 'all'; + + // Disabled fill-events on non-filled edges + if (graph.getModel().isEdge(state.cell) && state.shape.stroke != null && + (state.shape.fill == null || state.shape.fill == mxConstants.NONE)) + { + events = 'visibleStroke'; + } + + // Specifies the event-processing on the shape + if (state.shape.innerNode != null) + { + state.shape.innerNode.setAttribute('pointer-events', events); + } + else + { + state.shape.node.setAttribute('pointer-events', events); + } + } + + // Workaround for touch devices routing all events for a mouse + // gesture (down, move, up) via the initial DOM node. Same for + // HTML images in all IE versions (VML images are working). + var getState = function(evt) + { + var result = state; + + if ((graph.dialect != mxConstants.DIALECT_SVG && mxEvent.getSource(evt).nodeName == 'IMG') || mxClient.IS_TOUCH) + { + var x = mxEvent.getClientX(evt); + var y = mxEvent.getClientY(evt); + + // Dispatches the drop event to the graph which + // consumes and executes the source function + var pt = mxUtils.convertPoint(graph.container, x, y); + result = graph.view.getState(graph.getCellAt(pt.x, pt.y)); + } + + return result; + }; + + // Experimental support for two-finger pinch to resize cells + var gestureInProgress = false; + + mxEvent.addListener(state.shape.node, 'gesturestart', + mxUtils.bind(this, function(evt) + { + // FIXME: Breaks encapsulation to reset the double + // tap event handling when gestures take place + graph.lastTouchTime = 0; + + gestureInProgress = true; + mxEvent.consume(evt); + }) + ); + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + mxEvent.addListener(state.shape.node, md, + mxUtils.bind(this, function(evt) + { + if (this.isShapeEvent(state, evt) && !gestureInProgress) + { + // Redirects events from the "event-transparent" region of + // a swimlane to the graph. This is only required in HTML, + // SVG and VML do not fire mouse events on transparent + // backgrounds. + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, + new mxMouseEvent(evt, (state.shape != null && + mxEvent.getSource(evt) == state.shape.content) ? + null : state)); + } + else if (gestureInProgress) + { + mxEvent.consume(evt); + } + }) + ); + + mxEvent.addListener(state.shape.node, mm, + mxUtils.bind(this, function(evt) + { + if (this.isShapeEvent(state, evt) && !gestureInProgress) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt, (state.shape != null && + mxEvent.getSource(evt) == state.shape.content) ? + null : getState(evt))); + } + else if (gestureInProgress) + { + mxEvent.consume(evt); + } + }) + ); + + mxEvent.addListener(state.shape.node, mu, + mxUtils.bind(this, function(evt) + { + if (this.isShapeEvent(state, evt) && !gestureInProgress) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt, (state.shape != null && + mxEvent.getSource(evt) == state.shape.content) ? + null : getState(evt))); + } + else if (gestureInProgress) + { + mxEvent.consume(evt); + } + }) + ); + + // Experimental handling for gestures. Double-tap handling is implemented + // in mxGraph.fireMouseEvent. + var dc = (mxClient.IS_TOUCH) ? 'gestureend' : 'dblclick'; + + mxEvent.addListener(state.shape.node, dc, + mxUtils.bind(this, function(evt) + { + gestureInProgress = false; + + if (dc == 'gestureend') + { + // FIXME: Breaks encapsulation to reset the double + // tap event handling when gestures take place + graph.lastTouchTime = 0; + + if (graph.gestureEnabled) + { + graph.handleGesture(state, evt); + mxEvent.consume(evt); + } + } + else if (this.isShapeEvent(state, evt)) + { + graph.dblClick(evt, (state.shape != null && + mxEvent.getSource(evt) == state.shape.content) ? + null : state.cell); + mxEvent.consume(evt); + } + }) + ); +}; + +/** + * Function: redrawLabel + * + * Redraws the label for the given cell state. + * + * Parameters: + * + * state - <mxCellState> whose label should be redrawn. + */ +mxCellRenderer.prototype.redrawLabel = function(state) +{ + var value = this.getLabelValue(state); + + // FIXME: Add label always if HTML label and NO_FO + if (state.text == null && value != null && (mxUtils.isNode(value) || value.length > 0)) + { + this.createLabel(state, value); + } + else if (state.text != null && (value == null || value.length == 0)) + { + state.text.destroy(); + state.text = null; + } + + if (state.text != null) + { + var graph = state.view.graph; + var wrapping = graph.isWrapping(state.cell); + var clipping = graph.isLabelClipped(state.cell); + var bounds = this.getLabelBounds(state); + + if (state.text.value != value || state.text.isWrapping != wrapping || + state.text.isClipping != clipping || state.text.scale != state.view.scale || + !state.text.bounds.equals(bounds)) + { + state.text.value = value; + state.text.bounds = bounds; + state.text.scale = this.getTextScale(state); + state.text.isWrapping = wrapping; + state.text.isClipping = clipping; + + state.text.redraw(); + } + } +}; + +/** + * Function: getTextScale + * + * Returns the scaling used for the label of the given state + * + * Parameters: + * + * state - <mxCellState> whose label scale should be returned. + */ +mxCellRenderer.prototype.getTextScale = function(state) +{ + return state.view.scale; +}; + +/** + * Function: getLabelBounds + * + * Returns the bounds to be used to draw the label of the given state. + * + * Parameters: + * + * state - <mxCellState> whose label bounds should be returned. + */ +mxCellRenderer.prototype.getLabelBounds = function(state) +{ + var graph = state.view.graph; + var isEdge = graph.getModel().isEdge(state.cell); + var bounds = new mxRectangle(state.absoluteOffset.x, state.absoluteOffset.y); + + if (!isEdge) + { + bounds.x += state.x; + bounds.y += state.y; + + // Minimum of 1 fixes alignment bug in HTML labels + bounds.width = Math.max(1, state.width); + bounds.height = Math.max(1, state.height); + + if (graph.isSwimlane(state.cell)) + { + var scale = graph.view.scale; + var size = graph.getStartSize(state.cell); + + if (size.width > 0) + { + bounds.width = size.width * scale; + } + else if (size.height > 0) + { + bounds.height = size.height * scale; + } + } + } + + return bounds; +}; + +/** + * Function: redrawCellOverlays + * + * Redraws the overlays for the given cell state. + * + * Parameters: + * + * state - <mxCellState> whose overlays should be redrawn. + */ +mxCellRenderer.prototype.redrawCellOverlays = function(state) +{ + this.createCellOverlays(state); + + if (state.overlays != null) + { + state.overlays.visit(function(id, shape) + { + var bounds = shape.overlay.getBounds(state); + + if (shape.bounds == null || shape.scale != state.view.scale || + !shape.bounds.equals(bounds)) + { + shape.bounds = bounds; + shape.scale = state.view.scale; + shape.redraw(); + } + }); + } +}; + +/** + * Function: redrawControl + * + * Redraws the control for the given cell state. + * + * Parameters: + * + * state - <mxCellState> whose control should be redrawn. + */ +mxCellRenderer.prototype.redrawControl = function(state) +{ + if (state.control != null) + { + var bounds = this.getControlBounds(state); + var s = state.view.scale; + + if (state.control.scale != s || !state.control.bounds.equals(bounds)) + { + state.control.bounds = bounds; + state.control.scale = s; + state.control.redraw(); + } + } +}; + +/** + * Function: getControlBounds + * + * Returns the bounds to be used to draw the control (folding icon) of the + * given state. + */ +mxCellRenderer.prototype.getControlBounds = function(state) +{ + if (state.control != null) + { + var oldScale = state.control.scale; + var w = state.control.bounds.width / oldScale; + var h = state.control.bounds.height / oldScale; + var s = state.view.scale; + + return (state.view.graph.getModel().isEdge(state.cell)) ? + new mxRectangle(state.x + state.width / 2 - w / 2 * s, + state.y + state.height / 2 - h / 2 * s, w * s, h * s) + : new mxRectangle(state.x + w / 2 * s, + state.y + h / 2 * s, w * s, h * s); + } + + return null; +}; + +/** + * Function: redraw + * + * Updates the bounds or points and scale of the shapes for the given cell + * state. This is called in mxGraphView.validatePoints as the last step of + * updating all cells. + * + * Parameters: + * + * state - <mxCellState> for which the shapes should be updated. + * force - Optional boolean that specifies if the cell should be reconfiured + * and redrawn without any additional checks. + * rendering - Optional boolean that specifies if the cell should actually + * be drawn into the DOM. If this is false then redraw and/or reconfigure + * will not be called on the shape. + */ +mxCellRenderer.prototype.redraw = function(state, force, rendering) +{ + if (state.shape != null) + { + var model = state.view.graph.getModel(); + var isEdge = model.isEdge(state.cell); + reconfigure = (force != null) ? force : false; + + // Handles changes of the collapse icon + this.createControl(state); + + // Handles changes to the order in the DOM + if (state.orderChanged || state.invalidOrder) + { + if (state.view.graph.ordered) + { + this.order(state); + } + else + { + // Assert state.cell is edge + this.orderEdge(state); + } + + // Required to update inherited styles + reconfigure = state.orderChanged; + } + + delete state.invalidOrder; + delete state.orderChanged; + + // Checks if the style in the state is different from the style + // in the shape and re-applies the style if required + if (!reconfigure && !mxUtils.equalEntries(state.shape.style, state.style)) + { + reconfigure = true; + } + + // Reconfiures the shape after an order or style change + if (reconfigure) + { + this.configureShape(state); + state.shape.reconfigure(); + } + + // Redraws the cell if required + if (force || state.shape.bounds == null || state.shape.scale != state.view.scale || + !state.shape.bounds.equals(state) || + !mxUtils.equalPoints(state.shape.points, state.absolutePoints)) + { + // FIXME: Move indicator color update into shape.redraw +// var indicator = state.view.graph.getIndicatorColor(state); +// if (indicator != null) +// { +// state.shape.indicatorColor = indicator; +// } + + if (state.absolutePoints != null) + { + state.shape.points = state.absolutePoints.slice(); + } + else + { + state.shape.points = null; + } + + state.shape.bounds = new mxRectangle( + state.x, state.y, state.width, state.height); + state.shape.scale = state.view.scale; + + if (rendering == null || rendering) + { + state.shape.redraw(); + } + else + { + state.shape.updateBoundingBox(); + } + } + + // Updates the text label, overlays and control + if (rendering == null || rendering) + { + this.redrawLabel(state); + this.redrawCellOverlays(state); + this.redrawControl(state); + } + } +}; + +/** + * Function: destroy + * + * Destroys the shapes associated with the given cell state. + * + * Parameters: + * + * state - <mxCellState> for which the shapes should be destroyed. + */ +mxCellRenderer.prototype.destroy = function(state) +{ + if (state.shape != null) + { + if (state.text != null) + { + state.text.destroy(); + state.text = null; + } + + if (state.overlays != null) + { + state.overlays.visit(function(id, shape) + { + shape.destroy(); + }); + + state.overlays = null; + } + + if (state.control != null) + { + state.control.destroy(); + state.control = null; + } + + state.shape.destroy(); + state.shape = null; + } +}; diff --git a/src/js/view/mxCellState.js b/src/js/view/mxCellState.js new file mode 100644 index 0000000..7e7a3b0 --- /dev/null +++ b/src/js/view/mxCellState.js @@ -0,0 +1,375 @@ +/** + * $Id: mxCellState.js,v 1.42 2012-03-19 10:47:08 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxCellState + * + * Represents the current state of a cell in a given <mxGraphView>. + * + * For edges, the edge label position is stored in <absoluteOffset>. + * + * The size for oversize labels can be retrieved using the boundingBox property + * of the <text> field as shown below. + * + * (code) + * var bbox = (state.text != null) ? state.text.boundingBox : null; + * (end) + * + * Constructor: mxCellState + * + * Constructs a new object that represents the current state of the given + * cell in the specified view. + * + * Parameters: + * + * view - <mxGraphView> that contains the state. + * cell - <mxCell> that this state represents. + * style - Array of key, value pairs that constitute the style. + */ +function mxCellState(view, cell, style) +{ + this.view = view; + this.cell = cell; + this.style = style; + + this.origin = new mxPoint(); + this.absoluteOffset = new mxPoint(); +}; + +/** + * Extends mxRectangle. + */ +mxCellState.prototype = new mxRectangle(); +mxCellState.prototype.constructor = mxCellState; + +/** + * Variable: view + * + * Reference to the enclosing <mxGraphView>. + */ +mxCellState.prototype.view = null; + +/** + * Variable: cell + * + * Reference to the <mxCell> that is represented by this state. + */ +mxCellState.prototype.cell = null; + +/** + * Variable: style + * + * Contains an array of key, value pairs that represent the style of the + * cell. + */ +mxCellState.prototype.style = null; + +/** + * Variable: invalid + * + * Specifies if the state is invalid. Default is true. + */ +mxCellState.prototype.invalid = true; + +/** + * Variable: invalidOrder + * + * Specifies if the cell has an invalid order. For internal use. Default is + * false. + */ +mxCellState.prototype.invalidOrder = false; + +/** + * Variable: orderChanged + * + * Specifies if the cell has changed order and the display needs to be + * updated. + */ +mxCellState.prototype.orderChanged = false; + +/** + * Variable: origin + * + * <mxPoint> that holds the origin for all child cells. Default is a new + * empty <mxPoint>. + */ +mxCellState.prototype.origin = null; + +/** + * Variable: absolutePoints + * + * Holds an array of <mxPoints> that represent the absolute points of an + * edge. + */ +mxCellState.prototype.absolutePoints = null; + +/** + * Variable: absoluteOffset + * + * <mxPoint> that holds the absolute offset. For edges, this is the + * absolute coordinates of the label position. For vertices, this is the + * offset of the label relative to the top, left corner of the vertex. + */ +mxCellState.prototype.absoluteOffset = null; + +/** + * Variable: visibleSourceState + * + * Caches the visible source terminal state. + */ +mxCellState.prototype.visibleSourceState = null; + +/** + * Variable: visibleTargetState + * + * Caches the visible target terminal state. + */ +mxCellState.prototype.visibleTargetState = null; + +/** + * Variable: terminalDistance + * + * Caches the distance between the end points for an edge. + */ +mxCellState.prototype.terminalDistance = 0; + +/** + * Variable: length + * + * Caches the length of an edge. + */ +mxCellState.prototype.length = 0; + +/** + * Variable: segments + * + * Array of numbers that represent the cached length of each segment of the + * edge. + */ +mxCellState.prototype.segments = null; + +/** + * Variable: shape + * + * Holds the <mxShape> that represents the cell graphically. + */ +mxCellState.prototype.shape = null; + +/** + * Variable: text + * + * Holds the <mxText> that represents the label of the cell. Thi smay be + * null if the cell has no label. + */ +mxCellState.prototype.text = null; + +/** + * Function: getPerimeterBounds + * + * Returns the <mxRectangle> that should be used as the perimeter of the + * cell. + * + * Parameters: + * + * border - Optional border to be added around the perimeter bounds. + * bounds - Optional <mxRectangle> to be used as the initial bounds. + */ +mxCellState.prototype.getPerimeterBounds = function (border, bounds) +{ + border = border || 0; + bounds = (bounds != null) ? bounds : new mxRectangle(this.x, this.y, this.width, this.height); + + if (this.shape != null && this.shape.stencil != null) + { + var aspect = this.shape.stencil.computeAspect(this, bounds, null); + + bounds.x = aspect.x; + bounds.y = aspect.y; + bounds.width = this.shape.stencil.w0 * aspect.width; + bounds.height = this.shape.stencil.h0 * aspect.height; + } + + if (border != 0) + { + bounds.grow(border); + } + + return bounds; +}; + +/** + * Function: setAbsoluteTerminalPoint + * + * Sets the first or last point in <absolutePoints> depending on isSource. + * + * Parameters: + * + * point - <mxPoint> that represents the terminal point. + * isSource - Boolean that specifies if the first or last point should + * be assigned. + */ +mxCellState.prototype.setAbsoluteTerminalPoint = function (point, isSource) +{ + if (isSource) + { + if (this.absolutePoints == null) + { + this.absolutePoints = []; + } + + if (this.absolutePoints.length == 0) + { + this.absolutePoints.push(point); + } + else + { + this.absolutePoints[0] = point; + } + } + else + { + if (this.absolutePoints == null) + { + this.absolutePoints = []; + this.absolutePoints.push(null); + this.absolutePoints.push(point); + } + else if (this.absolutePoints.length == 1) + { + this.absolutePoints.push(point); + } + else + { + this.absolutePoints[this.absolutePoints.length - 1] = point; + } + } +}; + +/** + * Function: setCursor + * + * Sets the given cursor on the shape and text shape. + */ +mxCellState.prototype.setCursor = function (cursor) +{ + if (this.shape != null) + { + this.shape.setCursor(cursor); + } + + if (this.text != null) + { + this.text.setCursor(cursor); + } +}; + +/** + * Function: getVisibleTerminal + * + * Returns the visible source or target terminal cell. + * + * Parameters: + * + * source - Boolean that specifies if the source or target cell should be + * returned. + */ +mxCellState.prototype.getVisibleTerminal = function (source) +{ + var tmp = this.getVisibleTerminalState(source); + + return (tmp != null) ? tmp.cell : null; +}; + +/** + * Function: getVisibleTerminalState + * + * Returns the visible source or target terminal state. + * + * Parameters: + * + * source - Boolean that specifies if the source or target state should be + * returned. + */ +mxCellState.prototype.getVisibleTerminalState = function (source) +{ + return (source) ? this.visibleSourceState : this.visibleTargetState; +}; + +/** + * Function: setVisibleTerminalState + * + * Sets the visible source or target terminal state. + * + * Parameters: + * + * terminalState - <mxCellState> that represents the terminal. + * source - Boolean that specifies if the source or target state should be set. + */ +mxCellState.prototype.setVisibleTerminalState = function (terminalState, source) +{ + if (source) + { + this.visibleSourceState = terminalState; + } + else + { + this.visibleTargetState = terminalState; + } +}; + +/** + * Destructor: destroy + * + * Destroys the state and all associated resources. + */ +mxCellState.prototype.destroy = function () +{ + this.view.graph.cellRenderer.destroy(this); +}; + +/** + * Function: clone + * + * Returns a clone of this <mxPoint>. + */ +mxCellState.prototype.clone = function() +{ + var clone = new mxCellState(this.view, this.cell, this.style); + + // Clones the absolute points + if (this.absolutePoints != null) + { + clone.absolutePoints = []; + + for (var i = 0; i < this.absolutePoints.length; i++) + { + clone.absolutePoints[i] = this.absolutePoints[i].clone(); + } + } + + if (this.origin != null) + { + clone.origin = this.origin.clone(); + } + + if (this.absoluteOffset != null) + { + clone.absoluteOffset = this.absoluteOffset.clone(); + } + + if (this.boundingBox != null) + { + clone.boundingBox = this.boundingBox.clone(); + } + + clone.terminalDistance = this.terminalDistance; + clone.segments = this.segments; + clone.length = this.length; + clone.x = this.x; + clone.y = this.y; + clone.width = this.width; + clone.height = this.height; + + return clone; +}; diff --git a/src/js/view/mxCellStatePreview.js b/src/js/view/mxCellStatePreview.js new file mode 100644 index 0000000..b853748 --- /dev/null +++ b/src/js/view/mxCellStatePreview.js @@ -0,0 +1,223 @@ +/** + * $Id: mxCellStatePreview.js,v 1.6 2012-10-26 07:19:11 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * + * Class: mxCellStatePreview + * + * Implements a live preview for moving cells. + * + * Constructor: mxCellStatePreview + * + * Constructs a move preview for the given graph. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxCellStatePreview(graph) +{ + this.graph = graph; + this.deltas = new Object(); +}; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxCellStatePreview.prototype.graph = null; + +/** + * Variable: deltas + * + * Reference to the enclosing <mxGraph>. + */ +mxCellStatePreview.prototype.deltas = null; + +/** + * Variable: count + * + * Contains the number of entries in the map. + */ +mxCellStatePreview.prototype.count = 0; + +/** + * Function: isEmpty + * + * Returns true if this contains no entries. + */ +mxCellStatePreview.prototype.isEmpty = function() +{ + return this.count == 0; +}; + +/** + * Function: moveState + */ +mxCellStatePreview.prototype.moveState = function(state, dx, dy, add, includeEdges) +{ + add = (add != null) ? add : true; + includeEdges = (includeEdges != null) ? includeEdges : true; + var id = mxCellPath.create(state.cell); + var delta = this.deltas[id]; + + if (delta == null) + { + delta = new mxPoint(dx, dy); + this.deltas[id] = delta; + this.count++; + } + else + { + if (add) + { + delta.X += dx; + delta.Y += dy; + } + else + { + delta.X = dx; + delta.Y = dy; + } + } + + if (includeEdges) + { + this.addEdges(state); + } + + return delta; +}; + +/** + * Function: show + */ +mxCellStatePreview.prototype.show = function(visitor) +{ + var model = this.graph.getModel(); + var root = model.getRoot(); + + // Translates the states in step + for (var id in this.deltas) + { + var cell = mxCellPath.resolve(root, id); + var state = this.graph.view.getState(cell); + var delta = this.deltas[id]; + var parentState = this.graph.view.getState( + model.getParent(cell)); + this.translateState(parentState, state, delta.x, delta.y); + } + + // Revalidates the states in step + for (var id in this.deltas) + { + var cell = mxCellPath.resolve(root, id); + var state = this.graph.view.getState(cell); + var delta = this.deltas[id]; + var parentState = this.graph.view.getState( + model.getParent(cell)); + this.revalidateState(parentState, state, delta.x, delta.y, visitor); + } +}; + +/** + * Function: translateState + */ +mxCellStatePreview.prototype.translateState = function(parentState, state, dx, dy) +{ + if (state != null) + { + var model = this.graph.getModel(); + + if (model.isVertex(state.cell)) + { + // LATER: Use hashtable to store initial state bounds + state.invalid = true; + this.graph.view.validateBounds(parentState, state.cell); + var geo = model.getGeometry(state.cell); + var id = mxCellPath.create(state.cell); + + // Moves selection cells and non-relative vertices in + // the first phase so that edge terminal points will + // be updated in the second phase + if ((dx != 0 || dy != 0) && geo != null && + (!geo.relative || this.deltas[id] != null)) + { + state.x += dx; + state.y += dy; + } + } + + var childCount = model.getChildCount(state.cell); + + for (var i = 0; i < childCount; i++) + { + this.translateState(state, this.graph.view.getState( + model.getChildAt(state.cell, i)), dx, dy); + } + } +}; + +/** + * Function: revalidateState + */ +mxCellStatePreview.prototype.revalidateState = function(parentState, state, dx, dy, visitor) +{ + if (state != null) + { + // Updates the edge terminal points and restores the + // (relative) positions of any (relative) children + state.invalid = true; + this.graph.view.validatePoints(parentState, state.cell); + + // Moves selection vertices which are relative + var id = mxCellPath.create(state.cell); + var model = this.graph.getModel(); + var geo = this.graph.getCellGeometry(state.cell); + + if ((dx != 0 || dy != 0) && geo != null && geo.relative && + model.isVertex(state.cell) && (parentState == null || + model.isVertex(parentState.cell) || this.deltas[id] != null)) + { + state.x += dx; + state.y += dy; + + this.graph.cellRenderer.redraw(state); + } + + // Invokes the visitor on the given state + if (visitor != null) + { + visitor(state); + } + + var childCount = model.getChildCount(state.cell); + + for (var i = 0; i < childCount; i++) + { + this.revalidateState(state, this.graph.view.getState(model.getChildAt( + state.cell, i)), dx, dy, visitor); + } + } +}; + +/** + * Function: addEdges + */ +mxCellStatePreview.prototype.addEdges = function(state) +{ + var model = this.graph.getModel(); + var edgeCount = model.getEdgeCount(state.cell); + + for (var i = 0; i < edgeCount; i++) + { + var s = this.graph.view.getState(model.getEdgeAt(state.cell, i)); + + if (s != null) + { + this.moveState(s, 0, 0); + } + } +}; diff --git a/src/js/view/mxConnectionConstraint.js b/src/js/view/mxConnectionConstraint.js new file mode 100644 index 0000000..70f457f --- /dev/null +++ b/src/js/view/mxConnectionConstraint.js @@ -0,0 +1,42 @@ +/** + * $Id: mxConnectionConstraint.js,v 1.2 2010-04-29 09:33:52 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxConnectionConstraint + * + * Defines an object that contains the constraints about how to connect one + * side of an edge to its terminal. + * + * Constructor: mxConnectionConstraint + * + * Constructs a new connection constraint for the given point and boolean + * arguments. + * + * Parameters: + * + * point - Optional <mxPoint> that specifies the fixed location of the point + * in relative coordinates. Default is null. + * perimeter - Optional boolean that specifies if the fixed point should be + * projected onto the perimeter of the terminal. Default is true. + */ +function mxConnectionConstraint(point, perimeter) +{ + this.point = point; + this.perimeter = (perimeter != null) ? perimeter : true; +}; + +/** + * Variable: point + * + * <mxPoint> that specifies the fixed location of the connection point. + */ +mxConnectionConstraint.prototype.point = null; + +/** + * Variable: perimeter + * + * Boolean that specifies if the point should be projected onto the perimeter + * of the terminal. + */ +mxConnectionConstraint.prototype.perimeter = null; diff --git a/src/js/view/mxEdgeStyle.js b/src/js/view/mxEdgeStyle.js new file mode 100644 index 0000000..41493d6 --- /dev/null +++ b/src/js/view/mxEdgeStyle.js @@ -0,0 +1,1302 @@ +/** + * $Id: mxEdgeStyle.js,v 1.68 2012-11-20 09:06:07 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxEdgeStyle = +{ + /** + * Class: mxEdgeStyle + * + * Provides various edge styles to be used as the values for + * <mxConstants.STYLE_EDGE> in a cell style. + * + * Example: + * + * (code) + * var style = stylesheet.getDefaultEdgeStyle(); + * style[mxConstants.STYLE_EDGE] = mxEdgeStyle.ElbowConnector; + * (end) + * + * Sets the default edge style to <ElbowConnector>. + * + * Custom edge style: + * + * To write a custom edge style, a function must be added to the mxEdgeStyle + * object as follows: + * + * (code) + * mxEdgeStyle.MyStyle = function(state, source, target, points, result) + * { + * if (source != null && target != null) + * { + * var pt = new mxPoint(target.getCenterX(), source.getCenterY()); + * + * if (mxUtils.contains(source, pt.x, pt.y)) + * { + * pt.y = source.y + source.height; + * } + * + * result.push(pt); + * } + * }; + * (end) + * + * In the above example, a right angle is created using a point on the + * horizontal center of the target vertex and the vertical center of the source + * vertex. The code checks if that point intersects the source vertex and makes + * the edge straight if it does. The point is then added into the result array, + * which acts as the return value of the function. + * + * The new edge style should then be registered in the <mxStyleRegistry> as follows: + * (code) + * mxStyleRegistry.putValue('myEdgeStyle', mxEdgeStyle.MyStyle); + * (end) + * + * The custom edge style above can now be used in a specific edge as follows: + * + * (code) + * model.setStyle(edge, 'edgeStyle=myEdgeStyle'); + * (end) + * + * Note that the key of the <mxStyleRegistry> entry for the function should + * be used in string values, unless <mxGraphView.allowEval> is true, in + * which case you can also use mxEdgeStyle.MyStyle for the value in the + * cell style above. + * + * Or it can be used for all edges in the graph as follows: + * + * (code) + * var style = graph.getStylesheet().getDefaultEdgeStyle(); + * style[mxConstants.STYLE_EDGE] = mxEdgeStyle.MyStyle; + * (end) + * + * Note that the object can be used directly when programmatically setting + * the value, but the key in the <mxStyleRegistry> should be used when + * setting the value via a key, value pair in a cell style. + * + * Function: EntityRelation + * + * Implements an entity relation style for edges (as used in database + * schema diagrams). At the time the function is called, the result + * array contains a placeholder (null) for the first absolute point, + * that is, the point where the edge and source terminal are connected. + * The implementation of the style then adds all intermediate waypoints + * except for the last point, that is, the connection point between the + * edge and the target terminal. The first ant the last point in the + * result array are then replaced with mxPoints that take into account + * the terminal's perimeter and next point on the edge. + * + * Parameters: + * + * state - <mxCellState> that represents the edge to be updated. + * source - <mxCellState> that represents the source terminal. + * target - <mxCellState> that represents the target terminal. + * points - List of relative control points. + * result - Array of <mxPoints> that represent the actual points of the + * edge. + */ + EntityRelation: function (state, source, target, points, result) + { + var view = state.view; + var graph = view.graph; + var segment = mxUtils.getValue(state.style, + mxConstants.STYLE_SEGMENT, + mxConstants.ENTITY_SEGMENT) * view.scale; + + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length-1]; + + var isSourceLeft = false; + + if (p0 != null) + { + source = new mxCellState(); + source.x = p0.x; + source.y = p0.y; + } + else if (source != null) + { + var constraint = mxUtils.getPortConstraints(source, state, true, mxConstants.DIRECTION_MASK_NONE); + + if (constraint != mxConstants.DIRECTION_MASK_NONE) + { + isSourceLeft = constraint == mxConstants.DIRECTION_MASK_WEST; + } + else + { + var sourceGeometry = graph.getCellGeometry(source.cell); + + if (sourceGeometry.relative) + { + isSourceLeft = sourceGeometry.x <= 0.5; + } + else if (target != null) + { + isSourceLeft = target.x + target.width < source.x; + } + } + } + else + { + return; + } + + var isTargetLeft = true; + + if (pe != null) + { + target = new mxCellState(); + target.x = pe.x; + target.y = pe.y; + } + else if (target != null) + { + var constraint = mxUtils.getPortConstraints(target, state, false, mxConstants.DIRECTION_MASK_NONE); + + if (constraint != mxConstants.DIRECTION_MASK_NONE) + { + isTargetLeft = constraint == mxConstants.DIRECTION_MASK_WEST; + } + else + { + var targetGeometry = graph.getCellGeometry(target.cell); + + if (targetGeometry.relative) + { + isTargetLeft = targetGeometry.x <= 0.5; + } + else if (source != null) + { + isTargetLeft = source.x + source.width < target.x; + } + } + } + + if (source != null && target != null) + { + var x0 = (isSourceLeft) ? source.x : source.x + source.width; + var y0 = view.getRoutingCenterY(source); + + var xe = (isTargetLeft) ? target.x : target.x + target.width; + var ye = view.getRoutingCenterY(target); + + var seg = segment; + + var dx = (isSourceLeft) ? -seg : seg; + var dep = new mxPoint(x0 + dx, y0); + + dx = (isTargetLeft) ? -seg : seg; + var arr = new mxPoint(xe + dx, ye); + + // Adds intermediate points if both go out on same side + if (isSourceLeft == isTargetLeft) + { + var x = (isSourceLeft) ? + Math.min(x0, xe)-segment : + Math.max(x0, xe)+segment; + + result.push(new mxPoint(x, y0)); + result.push(new mxPoint(x, ye)); + } + else if ((dep.x < arr.x) == isSourceLeft) + { + var midY = y0 + (ye - y0) / 2; + + result.push(dep); + result.push(new mxPoint(dep.x, midY)); + result.push(new mxPoint(arr.x, midY)); + result.push(arr); + } + else + { + result.push(dep); + result.push(arr); + } + } + }, + + /** + * Function: Loop + * + * Implements a self-reference, aka. loop. + */ + Loop: function (state, source, target, points, result) + { + if (source != null) + { + var view = state.view; + var graph = view.graph; + var pt = (points != null && points.length > 0) ? points[0] : null; + + if (pt != null) + { + pt = view.transformControlPoint(state, pt); + + if (mxUtils.contains(source, pt.x, pt.y)) + { + pt = null; + } + } + + var x = 0; + var dx = 0; + var y = 0; + var dy = 0; + + var seg = mxUtils.getValue(state.style, mxConstants.STYLE_SEGMENT, + graph.gridSize) * view.scale; + var dir = mxUtils.getValue(state.style, mxConstants.STYLE_DIRECTION, + mxConstants.DIRECTION_WEST); + + if (dir == mxConstants.DIRECTION_NORTH || + dir == mxConstants.DIRECTION_SOUTH) + { + x = view.getRoutingCenterX(source); + dx = seg; + } + else + { + y = view.getRoutingCenterY(source); + dy = seg; + } + + if (pt == null || + pt.x < source.x || + pt.x > source.x + source.width) + { + if (pt != null) + { + x = pt.x; + dy = Math.max(Math.abs(y - pt.y), dy); + } + else + { + if (dir == mxConstants.DIRECTION_NORTH) + { + y = source.y - 2 * dx; + } + else if (dir == mxConstants.DIRECTION_SOUTH) + { + y = source.y + source.height + 2 * dx; + } + else if (dir == mxConstants.DIRECTION_EAST) + { + x = source.x - 2 * dy; + } + else + { + x = source.x + source.width + 2 * dy; + } + } + } + else if (pt != null) + { + x = view.getRoutingCenterX(source); + dx = Math.max(Math.abs(x - pt.x), dy); + y = pt.y; + dy = 0; + } + + result.push(new mxPoint(x - dx, y - dy)); + result.push(new mxPoint(x + dx, y + dy)); + } + }, + + /** + * Function: ElbowConnector + * + * Uses either <SideToSide> or <TopToBottom> depending on the horizontal + * flag in the cell style. <SideToSide> is used if horizontal is true or + * unspecified. See <EntityRelation> for a description of the + * parameters. + */ + ElbowConnector: function (state, source, target, points, result) + { + var pt = (points != null && points.length > 0) ? points[0] : null; + + var vertical = false; + var horizontal = false; + + if (source != null && target != null) + { + if (pt != null) + { + var left = Math.min(source.x, target.x); + var right = Math.max(source.x + source.width, + target.x + target.width); + + var top = Math.min(source.y, target.y); + var bottom = Math.max(source.y + source.height, + target.y + target.height); + + pt = state.view.transformControlPoint(state, pt); + + vertical = pt.y < top || pt.y > bottom; + horizontal = pt.x < left || pt.x > right; + } + else + { + var left = Math.max(source.x, target.x); + var right = Math.min(source.x + source.width, + target.x + target.width); + + vertical = left == right; + + if (!vertical) + { + var top = Math.max(source.y, target.y); + var bottom = Math.min(source.y + source.height, + target.y + target.height); + + horizontal = top == bottom; + } + } + } + + if (!horizontal && (vertical || + state.style[mxConstants.STYLE_ELBOW] == mxConstants.ELBOW_VERTICAL)) + { + mxEdgeStyle.TopToBottom(state, source, target, points, result); + } + else + { + mxEdgeStyle.SideToSide(state, source, target, points, result); + } + }, + + /** + * Function: SideToSide + * + * Implements a vertical elbow edge. See <EntityRelation> for a description + * of the parameters. + */ + SideToSide: function (state, source, target, points, result) + { + var view = state.view; + var pt = (points != null && points.length > 0) ? points[0] : null; + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length-1]; + + if (pt != null) + { + pt = view.transformControlPoint(state, pt); + } + + if (p0 != null) + { + source = new mxCellState(); + source.x = p0.x; + source.y = p0.y; + } + + if (pe != null) + { + target = new mxCellState(); + target.x = pe.x; + target.y = pe.y; + } + + if (source != null && target != null) + { + var l = Math.max(source.x, target.x); + var r = Math.min(source.x + source.width, + target.x + target.width); + + var x = (pt != null) ? pt.x : r + (l - r) / 2; + + var y1 = view.getRoutingCenterY(source); + var y2 = view.getRoutingCenterY(target); + + if (pt != null) + { + if (pt.y >= source.y && pt.y <= source.y + source.height) + { + y1 = pt.y; + } + + if (pt.y >= target.y && pt.y <= target.y + target.height) + { + y2 = pt.y; + } + } + + if (!mxUtils.contains(target, x, y1) && + !mxUtils.contains(source, x, y1)) + { + result.push(new mxPoint(x, y1)); + } + + if (!mxUtils.contains(target, x, y2) && + !mxUtils.contains(source, x, y2)) + { + result.push(new mxPoint(x, y2)); + } + + if (result.length == 1) + { + if (pt != null) + { + if (!mxUtils.contains(target, x, pt.y) && + !mxUtils.contains(source, x, pt.y)) + { + result.push(new mxPoint(x, pt.y)); + } + } + else + { + var t = Math.max(source.y, target.y); + var b = Math.min(source.y + source.height, + target.y + target.height); + + result.push(new mxPoint(x, t + (b - t) / 2)); + } + } + } + }, + + /** + * Function: TopToBottom + * + * Implements a horizontal elbow edge. See <EntityRelation> for a + * description of the parameters. + */ + TopToBottom: function(state, source, target, points, result) + { + var view = state.view; + var pt = (points != null && points.length > 0) ? points[0] : null; + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length-1]; + + if (pt != null) + { + pt = view.transformControlPoint(state, pt); + } + + if (p0 != null) + { + source = new mxCellState(); + source.x = p0.x; + source.y = p0.y; + } + + if (pe != null) + { + target = new mxCellState(); + target.x = pe.x; + target.y = pe.y; + } + + if (source != null && target != null) + { + var t = Math.max(source.y, target.y); + var b = Math.min(source.y + source.height, + target.y + target.height); + + var x = view.getRoutingCenterX(source); + + if (pt != null && + pt.x >= source.x && + pt.x <= source.x + source.width) + { + x = pt.x; + } + + var y = (pt != null) ? pt.y : b + (t - b) / 2; + + if (!mxUtils.contains(target, x, y) && + !mxUtils.contains(source, x, y)) + { + result.push(new mxPoint(x, y)); + } + + if (pt != null && + pt.x >= target.x && + pt.x <= target.x + target.width) + { + x = pt.x; + } + else + { + x = view.getRoutingCenterX(target); + } + + if (!mxUtils.contains(target, x, y) && + !mxUtils.contains(source, x, y)) + { + result.push(new mxPoint(x, y)); + } + + if (result.length == 1) + { + if (pt != null && result.length == 1) + { + if (!mxUtils.contains(target, pt.x, y) && + !mxUtils.contains(source, pt.x, y)) + { + result.push(new mxPoint(pt.x, y)); + } + } + else + { + var l = Math.max(source.x, target.x); + var r = Math.min(source.x + source.width, + target.x + target.width); + + result.push(new mxPoint(l + (r - l) / 2, y)); + } + } + } + }, + + /** + * Function: SegmentConnector + * + * Implements an orthogonal edge style. Use <mxEdgeSegmentHandler> + * as an interactive handler for this style. + */ + SegmentConnector: function(state, source, target, hints, result) + { + // Creates array of all way- and terminalpoints + var pts = state.absolutePoints; + var horizontal = true; + var hint = null; + + // Adds the first point + var pt = pts[0]; + + if (pt == null && source != null) + { + pt = new mxPoint(state.view.getRoutingCenterX(source), state.view.getRoutingCenterY(source)); + } + else if (pt != null) + { + pt = pt.clone(); + } + + var lastInx = pts.length - 1; + + // Adds the waypoints + if (hints != null && hints.length > 0) + { + hint = state.view.transformControlPoint(state, hints[0]); + + var currentTerm = source; + var currentPt = pts[0]; + var hozChan = false; + var vertChan = false; + var currentHint = hint; + var hintsLen = hints.length; + + for (var i = 0; i < 2; i++) + { + var fixedVertAlign = currentPt != null && currentPt.x == currentHint.x; + var fixedHozAlign = currentPt != null && currentPt.y == currentHint.y; + var inHozChan = currentTerm != null && (currentHint.y >= currentTerm.y && + currentHint.y <= currentTerm.y + currentTerm.height); + var inVertChan = currentTerm != null && (currentHint.x >= currentTerm.x && + currentHint.x <= currentTerm.x + currentTerm.width); + + hozChan = fixedHozAlign || (currentPt == null && inHozChan); + vertChan = fixedVertAlign || (currentPt == null && inVertChan); + + if (currentPt != null && (!fixedHozAlign && !fixedVertAlign) && (inHozChan || inVertChan)) + { + horizontal = inHozChan ? false : true; + break; + } + + if (vertChan || hozChan) + { + horizontal = hozChan; + + if (i == 1) + { + // Work back from target end + horizontal = hints.length % 2 == 0 ? hozChan : vertChan; + } + + break; + } + + currentTerm = target; + currentPt = pts[lastInx]; + currentHint = state.view.transformControlPoint(state, hints[hintsLen - 1]); + } + + if (horizontal && ((pts[0] != null && pts[0].y != hint.y) || + (pts[0] == null && source != null && + (hint.y < source.y || hint.y > source.y + source.height)))) + { + result.push(new mxPoint(pt.x, hint.y)); + } + else if (!horizontal && ((pts[0] != null && pts[0].x != hint.x) || + (pts[0] == null && source != null && + (hint.x < source.x || hint.x > source.x + source.width)))) + { + result.push(new mxPoint(hint.x, pt.y)); + } + + if (horizontal) + { + pt.y = hint.y; + } + else + { + pt.x = hint.x; + } + + for (var i = 0; i < hints.length; i++) + { + horizontal = !horizontal; + hint = state.view.transformControlPoint(state, hints[i]); + +// mxLog.show(); +// mxLog.debug('hint', i, hint.x, hint.y); + + if (horizontal) + { + pt.y = hint.y; + } + else + { + pt.x = hint.x; + } + + result.push(pt.clone()); + } + } + else + { + hint = pt; + // FIXME: First click in connect preview toggles orientation + horizontal = true; + } + + // Adds the last point + pt = pts[lastInx]; + + if (pt == null && target != null) + { + pt = new mxPoint(state.view.getRoutingCenterX(target), state.view.getRoutingCenterY(target)); + } + + if (horizontal && ((pts[lastInx] != null && pts[lastInx].y != hint.y) || + (pts[lastInx] == null && target != null && + (hint.y < target.y || hint.y > target.y + target.height)))) + { + result.push(new mxPoint(pt.x, hint.y)); + } + else if (!horizontal && ((pts[lastInx] != null && pts[lastInx].x != hint.x) || + (pts[lastInx] == null && target != null && + (hint.x < target.x || hint.x > target.x + target.width)))) + { + result.push(new mxPoint(hint.x, pt.y)); + } + + // Removes bends inside the source terminal for floating ports + if (pts[0] == null && source != null) + { + while (result.length > 1 && mxUtils.contains(source, result[1].x, result[1].y)) + { + result = result.splice(1, 1); + } + } + + // Removes bends inside the target terminal + if (pts[lastInx] == null && target != null) + { + while (result.length > 1 && mxUtils.contains(target, result[result.length - 1].x, result[result.length - 1].y)) + { + result = result.splice(result.length - 1, 1); + } + } + + }, + + orthBuffer: 10, + + dirVectors: [ [ -1, 0 ], + [ 0, -1 ], [ 1, 0 ], [ 0, 1 ], [ -1, 0 ], [ 0, -1 ], [ 1, 0 ] ], + + wayPoints1: [ [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], + [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0] ], + + routePatterns: [ + [ [ 513, 2308, 2081, 2562 ], [ 513, 1090, 514, 2184, 2114, 2561 ], + [ 513, 1090, 514, 2564, 2184, 2562 ], + [ 513, 2308, 2561, 1090, 514, 2568, 2308 ] ], + [ [ 514, 1057, 513, 2308, 2081, 2562 ], [ 514, 2184, 2114, 2561 ], + [ 514, 2184, 2562, 1057, 513, 2564, 2184 ], + [ 514, 1057, 513, 2568, 2308, 2561 ] ], + [ [ 1090, 514, 1057, 513, 2308, 2081, 2562 ], [ 2114, 2561 ], + [ 1090, 2562, 1057, 513, 2564, 2184 ], + [ 1090, 514, 1057, 513, 2308, 2561, 2568 ] ], + [ [ 2081, 2562 ], [ 1057, 513, 1090, 514, 2184, 2114, 2561 ], + [ 1057, 513, 1090, 514, 2184, 2562, 2564 ], + [ 1057, 2561, 1090, 514, 2568, 2308 ] ] ], + + inlineRoutePatterns: [ + [ null, [ 2114, 2568 ], null, null ], + [ null, [ 514, 2081, 2114, 2568 ] , null, null ], + [ null, [ 2114, 2561 ], null, null ], + [ [ 2081, 2562 ], [ 1057, 2114, 2568 ], + [ 2184, 2562 ], + null ] ], + vertexSeperations: [], + + limits: [ + [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ], + + LEFT_MASK: 32, + + TOP_MASK: 64, + + RIGHT_MASK: 128, + + BOTTOM_MASK: 256, + + LEFT: 1, + + TOP: 2, + + RIGHT: 4, + + BOTTOM: 8, + + // TODO remove magic numbers + SIDE_MASK: 480, + //mxEdgeStyle.LEFT_MASK | mxEdgeStyle.TOP_MASK | mxEdgeStyle.RIGHT_MASK + //| mxEdgeStyle.BOTTOM_MASK, + + CENTER_MASK: 512, + + SOURCE_MASK: 1024, + + TARGET_MASK: 2048, + + VERTEX_MASK: 3072, + // mxEdgeStyle.SOURCE_MASK | mxEdgeStyle.TARGET_MASK, + + /** + * Function: OrthConnector + * + * Implements a local orthogonal router between the given + * cells. + */ + OrthConnector: function(state, source, target, points, result) + { + var graph = state.view.graph; + var sourceEdge = source == null ? false : graph.getModel().isEdge(source.cell); + var targetEdge = target == null ? false : graph.getModel().isEdge(target.cell); + + if ((points != null && points.length > 0) || (sourceEdge) || (targetEdge)) + { + mxEdgeStyle.SegmentConnector(state, source, target, points, result); + return; + } + + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length-1]; + + var sourceX = source != null ? source.x : p0.x; + var sourceY = source != null ? source.y : p0.y; + var sourceWidth = source != null ? source.width : 1; + var sourceHeight = source != null ? source.height : 1; + + var targetX = target != null ? target.x : pe.x; + var targetY = target != null ? target.y : pe.y; + var targetWidth = target != null ? target.width : 1; + var targetHeight = target != null ? target.height : 1; + + var scaledOrthBuffer = state.view.scale * mxEdgeStyle.orthBuffer; + // Determine the side(s) of the source and target vertices + // that the edge may connect to + // portConstraint [source, target] + var portConstraint = [mxConstants.DIRECTION_MASK_ALL, mxConstants.DIRECTION_MASK_ALL]; + + if (source != null) + { + portConstraint[0] = mxUtils.getPortConstraints(source, state, true, + mxConstants.DIRECTION_MASK_ALL); + } + + if (target != null) + { + portConstraint[1] = mxUtils.getPortConstraints(target, state, false, + mxConstants.DIRECTION_MASK_ALL); + } + + var dir = [0, 0] ; + + // Work out which faces of the vertices present against each other + // in a way that would allow a 3-segment connection if port constraints + // permitted. + // geo -> [source, target] [x, y, width, height] + var geo = [ [sourceX, sourceY, sourceWidth, sourceHeight] , + [targetX, targetY, targetWidth, targetHeight] ]; + + for (var i = 0; i < 2; i++) + { + mxEdgeStyle.limits[i][1] = geo[i][0] - scaledOrthBuffer; + mxEdgeStyle.limits[i][2] = geo[i][1] - scaledOrthBuffer; + mxEdgeStyle.limits[i][4] = geo[i][0] + geo[i][2] + scaledOrthBuffer; + mxEdgeStyle.limits[i][8] = geo[i][1] + geo[i][3] + scaledOrthBuffer; + } + + // Work out which quad the target is in + var sourceCenX = geo[0][0] + geo[0][2] / 2.0; + var sourceCenY = geo[0][1] + geo[0][3] / 2.0; + var targetCenX = geo[1][0] + geo[1][2] / 2.0; + var targetCenY = geo[1][1] + geo[1][3] / 2.0; + + var dx = sourceCenX - targetCenX; + var dy = sourceCenY - targetCenY; + + var quad = 0; + + if (dx < 0) + { + if (dy < 0) + { + quad = 2; + } + else + { + quad = 1; + } + } + else + { + if (dy <= 0) + { + quad = 3; + + // Special case on x = 0 and negative y + if (dx == 0) + { + quad = 2; + } + } + } + + // Check for connection constraints + var currentTerm = null; + + if (source != null) + { + currentTerm = p0; + } + + var constraint = [ [0.5, 0.5] , [0.5, 0.5] ]; + + for (var i = 0; i < 2; i++) + { + if (currentTerm != null) + { + constraint[i][0] = (currentTerm.x - geo[i][0]) / geo[i][2]; + + if (constraint[i][0] < 0.01) + { + dir[i] = mxConstants.DIRECTION_MASK_WEST; + } + else if (constraint[i][0] > 0.99) + { + dir[i] = mxConstants.DIRECTION_MASK_EAST; + } + + constraint[i][1] = (currentTerm.y - geo[i][1]) / geo[i][3]; + + if (constraint[i][1] < 0.01) + { + dir[i] = mxConstants.DIRECTION_MASK_NORTH; + } + else if (constraint[i][1] > 0.99) + { + dir[i] = mxConstants.DIRECTION_MASK_SOUTH; + } + } + + currentTerm = null; + + if (target != null) + { + currentTerm = pe; + } + } + + var sourceTopDist = geo[0][1] - (geo[1][1] + geo[1][3]); + var sourceLeftDist = geo[0][0] - (geo[1][0] + geo[1][2]); + var sourceBottomDist = geo[1][1] - (geo[0][1] + geo[0][3]); + var sourceRightDist = geo[1][0] - (geo[0][0] + geo[0][2]); + + mxEdgeStyle.vertexSeperations[1] = Math.max( + sourceLeftDist - 2 * scaledOrthBuffer, 0); + mxEdgeStyle.vertexSeperations[2] = Math.max(sourceTopDist - 2 * scaledOrthBuffer, + 0); + mxEdgeStyle.vertexSeperations[4] = Math.max(sourceBottomDist - 2 + * scaledOrthBuffer, 0); + mxEdgeStyle.vertexSeperations[3] = Math.max(sourceRightDist - 2 + * scaledOrthBuffer, 0); + + //============================================================== + // Start of source and target direction determination + + // Work through the preferred orientations by relative positioning + // of the vertices and list them in preferred and available order + + var dirPref = []; + var horPref = []; + var vertPref = []; + + horPref[0] = (sourceLeftDist >= sourceRightDist) ? mxConstants.DIRECTION_MASK_WEST + : mxConstants.DIRECTION_MASK_EAST; + vertPref[0] = (sourceTopDist >= sourceBottomDist) ? mxConstants.DIRECTION_MASK_NORTH + : mxConstants.DIRECTION_MASK_SOUTH; + + horPref[1] = mxUtils.reversePortConstraints(horPref[0]); + vertPref[1] = mxUtils.reversePortConstraints(vertPref[0]); + + var preferredHorizDist = sourceLeftDist >= sourceRightDist ? sourceLeftDist + : sourceRightDist; + var preferredVertDist = sourceTopDist >= sourceBottomDist ? sourceTopDist + : sourceBottomDist; + + var prefOrdering = [ [0, 0] , [0, 0] ]; + var preferredOrderSet = false; + + // If the preferred port isn't available, switch it + for (var i = 0; i < 2; i++) + { + if (dir[i] != 0x0) + { + continue; + } + + if ((horPref[i] & portConstraint[i]) == 0) + { + horPref[i] = mxUtils.reversePortConstraints(horPref[i]); + } + + if ((vertPref[i] & portConstraint[i]) == 0) + { + vertPref[i] = mxUtils + .reversePortConstraints(vertPref[i]); + } + + prefOrdering[i][0] = vertPref[i]; + prefOrdering[i][1] = horPref[i]; + } + + if (preferredVertDist > scaledOrthBuffer * 2 + && preferredHorizDist > scaledOrthBuffer * 2) + { + // Possibility of two segment edge connection + if (((horPref[0] & portConstraint[0]) > 0) + && ((vertPref[1] & portConstraint[1]) > 0)) + { + prefOrdering[0][0] = horPref[0]; + prefOrdering[0][1] = vertPref[0]; + prefOrdering[1][0] = vertPref[1]; + prefOrdering[1][1] = horPref[1]; + preferredOrderSet = true; + } + else if (((vertPref[0] & portConstraint[0]) > 0) + && ((horPref[1] & portConstraint[1]) > 0)) + { + prefOrdering[0][0] = vertPref[0]; + prefOrdering[0][1] = horPref[0]; + prefOrdering[1][0] = horPref[1]; + prefOrdering[1][1] = vertPref[1]; + preferredOrderSet = true; + } + } + if (preferredVertDist > scaledOrthBuffer * 2 && !preferredOrderSet) + { + prefOrdering[0][0] = vertPref[0]; + prefOrdering[0][1] = horPref[0]; + prefOrdering[1][0] = vertPref[1]; + prefOrdering[1][1] = horPref[1]; + preferredOrderSet = true; + + } + if (preferredHorizDist > scaledOrthBuffer * 2 && !preferredOrderSet) + { + prefOrdering[0][0] = horPref[0]; + prefOrdering[0][1] = vertPref[0]; + prefOrdering[1][0] = horPref[1]; + prefOrdering[1][1] = vertPref[1]; + preferredOrderSet = true; + } + + // The source and target prefs are now an ordered list of + // the preferred port selections + // It the list can contain gaps, compact it + + for (var i = 0; i < 2; i++) + { + if (dir[i] != 0x0) + { + continue; + } + + if ((prefOrdering[i][0] & portConstraint[i]) == 0) + { + prefOrdering[i][0] = prefOrdering[i][1]; + } + + dirPref[i] = prefOrdering[i][0] & portConstraint[i]; + dirPref[i] |= (prefOrdering[i][1] & portConstraint[i]) << 8; + dirPref[i] |= (prefOrdering[1 - i][i] & portConstraint[i]) << 16; + dirPref[i] |= (prefOrdering[1 - i][1 - i] & portConstraint[i]) << 24; + + if ((dirPref[i] & 0xF) == 0) + { + dirPref[i] = dirPref[i] << 8; + } + if ((dirPref[i] & 0xF00) == 0) + { + dirPref[i] = (dirPref[i] & 0xF) | dirPref[i] >> 8; + } + if ((dirPref[i] & 0xF0000) == 0) + { + dirPref[i] = (dirPref[i] & 0xFFFF) + | ((dirPref[i] & 0xF000000) >> 8); + } + + dir[i] = dirPref[i] & 0xF; + + if (portConstraint[i] == mxConstants.DIRECTION_MASK_WEST + || portConstraint[i] == mxConstants.DIRECTION_MASK_NORTH + || portConstraint[i] == mxConstants.DIRECTION_MASK_EAST + || portConstraint[i] == mxConstants.DIRECTION_MASK_SOUTH) + { + dir[i] = portConstraint[i]; + } + } + + //============================================================== + // End of source and target direction determination + + var sourceIndex = dir[0] == mxConstants.DIRECTION_MASK_EAST ? 3 + : dir[0]; + var targetIndex = dir[1] == mxConstants.DIRECTION_MASK_EAST ? 3 + : dir[1]; + + sourceIndex -= quad; + targetIndex -= quad; + + if (sourceIndex < 1) + { + sourceIndex += 4; + } + if (targetIndex < 1) + { + targetIndex += 4; + } + + var routePattern = mxEdgeStyle.routePatterns[sourceIndex - 1][targetIndex - 1]; + + mxEdgeStyle.wayPoints1[0][0] = geo[0][0]; + mxEdgeStyle.wayPoints1[0][1] = geo[0][1]; + + switch (dir[0]) + { + case mxConstants.DIRECTION_MASK_WEST: + mxEdgeStyle.wayPoints1[0][0] -= scaledOrthBuffer; + mxEdgeStyle.wayPoints1[0][1] += constraint[0][1] * geo[0][3]; + break; + case mxConstants.DIRECTION_MASK_SOUTH: + mxEdgeStyle.wayPoints1[0][0] += constraint[0][0] * geo[0][2]; + mxEdgeStyle.wayPoints1[0][1] += geo[0][3] + scaledOrthBuffer; + break; + case mxConstants.DIRECTION_MASK_EAST: + mxEdgeStyle.wayPoints1[0][0] += geo[0][2] + scaledOrthBuffer; + mxEdgeStyle.wayPoints1[0][1] += constraint[0][1] * geo[0][3]; + break; + case mxConstants.DIRECTION_MASK_NORTH: + mxEdgeStyle.wayPoints1[0][0] += constraint[0][0] * geo[0][2]; + mxEdgeStyle.wayPoints1[0][1] -= scaledOrthBuffer; + break; + } + + var currentIndex = 0; + + // Orientation, 0 horizontal, 1 vertical + var lastOrientation = (dir[0] & (mxConstants.DIRECTION_MASK_EAST | mxConstants.DIRECTION_MASK_WEST)) > 0 ? 0 + : 1; + var initialOrientation = lastOrientation; + var currentOrientation = 0; + + for (var i = 0; i < routePattern.length; i++) + { + var nextDirection = routePattern[i] & 0xF; + + // Rotate the index of this direction by the quad + // to get the real direction + var directionIndex = nextDirection == mxConstants.DIRECTION_MASK_EAST ? 3 + : nextDirection; + + directionIndex += quad; + + if (directionIndex > 4) + { + directionIndex -= 4; + } + + var direction = mxEdgeStyle.dirVectors[directionIndex - 1]; + + currentOrientation = (directionIndex % 2 > 0) ? 0 : 1; + // Only update the current index if the point moved + // in the direction of the current segment move, + // otherwise the same point is moved until there is + // a segment direction change + if (currentOrientation != lastOrientation) + { + currentIndex++; + // Copy the previous way point into the new one + // We can't base the new position on index - 1 + // because sometime elbows turn out not to exist, + // then we'd have to rewind. + mxEdgeStyle.wayPoints1[currentIndex][0] = mxEdgeStyle.wayPoints1[currentIndex - 1][0]; + mxEdgeStyle.wayPoints1[currentIndex][1] = mxEdgeStyle.wayPoints1[currentIndex - 1][1]; + } + + var tar = (routePattern[i] & mxEdgeStyle.TARGET_MASK) > 0; + var sou = (routePattern[i] & mxEdgeStyle.SOURCE_MASK) > 0; + var side = (routePattern[i] & mxEdgeStyle.SIDE_MASK) >> 5; + side = side << quad; + + if (side > 0xF) + { + side = side >> 4; + } + + var center = (routePattern[i] & mxEdgeStyle.CENTER_MASK) > 0; + + if ((sou || tar) && side < 9) + { + var limit = 0; + var souTar = sou ? 0 : 1; + + if (center && currentOrientation == 0) + { + limit = geo[souTar][0] + constraint[souTar][0] * geo[souTar][2]; + } + else if (center) + { + limit = geo[souTar][1] + constraint[souTar][1] * geo[souTar][3]; + } + else + { + limit = mxEdgeStyle.limits[souTar][side]; + } + + if (currentOrientation == 0) + { + var lastX = mxEdgeStyle.wayPoints1[currentIndex][0]; + var deltaX = (limit - lastX) * direction[0]; + + if (deltaX > 0) + { + mxEdgeStyle.wayPoints1[currentIndex][0] += direction[0] + * deltaX; + } + } + else + { + var lastY = mxEdgeStyle.wayPoints1[currentIndex][1]; + var deltaY = (limit - lastY) * direction[1]; + + if (deltaY > 0) + { + mxEdgeStyle.wayPoints1[currentIndex][1] += direction[1] + * deltaY; + } + } + } + + else if (center) + { + // Which center we're travelling to depend on the current direction + mxEdgeStyle.wayPoints1[currentIndex][0] += direction[0] + * Math.abs(mxEdgeStyle.vertexSeperations[directionIndex] / 2); + mxEdgeStyle.wayPoints1[currentIndex][1] += direction[1] + * Math.abs(mxEdgeStyle.vertexSeperations[directionIndex] / 2); + } + + if (currentIndex > 0 + && mxEdgeStyle.wayPoints1[currentIndex][currentOrientation] == mxEdgeStyle.wayPoints1[currentIndex - 1][currentOrientation]) + { + currentIndex--; + } + else + { + lastOrientation = currentOrientation; + } + } + + for (var i = 0; i <= currentIndex; i++) + { + if (i == currentIndex) + { + // Last point can cause last segment to be in + // same direction as jetty/approach. If so, + // check the number of points is consistent + // with the relative orientation of source and target + // jettys. Same orientation requires an even + // number of turns (points), different requires + // odd. + var targetOrientation = (dir[1] & (mxConstants.DIRECTION_MASK_EAST | mxConstants.DIRECTION_MASK_WEST)) > 0 ? 0 + : 1; + var sameOrient = targetOrientation == initialOrientation ? 0 : 1; + + // (currentIndex + 1) % 2 is 0 for even number of points, + // 1 for odd + if (sameOrient != (currentIndex + 1) % 2) + { + // The last point isn't required + break; + } + } + + result.push(new mxPoint(mxEdgeStyle.wayPoints1[i][0], mxEdgeStyle.wayPoints1[i][1])); + } + }, + + getRoutePattern: function(dir, quad, dx, dy) + { + var sourceIndex = dir[0] == mxConstants.DIRECTION_MASK_EAST ? 3 + : dir[0]; + var targetIndex = dir[1] == mxConstants.DIRECTION_MASK_EAST ? 3 + : dir[1]; + + sourceIndex -= quad; + targetIndex -= quad; + + if (sourceIndex < 1) + { + sourceIndex += 4; + } + if (targetIndex < 1) + { + targetIndex += 4; + } + + var result = routePatterns[sourceIndex - 1][targetIndex - 1]; + + if (dx == 0 || dy == 0) + { + if (inlineRoutePatterns[sourceIndex - 1][targetIndex - 1] != null) + { + result = inlineRoutePatterns[sourceIndex - 1][targetIndex - 1]; + } + } + + return result; + } +};
\ No newline at end of file diff --git a/src/js/view/mxGraph.js b/src/js/view/mxGraph.js new file mode 100644 index 0000000..7c90f9b --- /dev/null +++ b/src/js/view/mxGraph.js @@ -0,0 +1,11176 @@ +/** + * $Id: mxGraph.js,v 1.702 2012-12-13 15:07:34 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraph + * + * Extends <mxEventSource> to implement a graph component for + * the browser. This is the main class of the package. To activate + * panning and connections use <setPanning> and <setConnectable>. + * For rubberband selection you must create a new instance of + * <mxRubberband>. The following listeners are added to + * <mouseListeners> by default: + * + * - <tooltipHandler>: <mxTooltipHandler> that displays tooltips + * - <panningHandler>: <mxPanningHandler> for panning and popup menus + * - <connectionHandler>: <mxConnectionHandler> for creating connections + * - <graphHandler>: <mxGraphHandler> for moving and cloning cells + * + * These listeners will be called in the above order if they are enabled. + * + * Background Images: + * + * To display a background image, set the image, image width and + * image height using <setBackgroundImage>. If one of the + * above values has changed then the <view>'s <mxGraphView.validate> + * should be invoked. + * + * Cell Images: + * + * To use images in cells, a shape must be specified in the default + * vertex style (or any named style). Possible shapes are + * <mxConstants.SHAPE_IMAGE> and <mxConstants.SHAPE_LABEL>. + * The code to change the shape used in the default vertex style, + * the following code is used: + * + * (code) + * var style = graph.getStylesheet().getDefaultVertexStyle(); + * style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_IMAGE; + * (end) + * + * For the default vertex style, the image to be displayed can be + * specified in a cell's style using the <mxConstants.STYLE_IMAGE> + * key and the image URL as a value, for example: + * + * (code) + * image=http://www.example.com/image.gif + * (end) + * + * For a named style, the the stylename must be the first element + * of the cell style: + * + * (code) + * stylename;image=http://www.example.com/image.gif + * (end) + * + * A cell style can have any number of key=value pairs added, divided + * by a semicolon as follows: + * + * (code) + * [stylename;|key=value;] + * (end) + * + * Labels: + * + * The cell labels are defined by <getLabel> which uses <convertValueToString> + * if <labelsVisible> is true. If a label must be rendered as HTML markup, then + * <isHtmlLabel> should return true for the respective cell. If all labels + * contain HTML markup, <htmlLabels> can be set to true. NOTE: Enabling HTML + * labels carries a possible security risk (see the section on security in + * the manual). + * + * If wrapping is needed for a label, then <isHtmlLabel> and <isWrapping> must + * return true for the cell whose label should be wrapped. See <isWrapping> for + * an example. + * + * If clipping is needed to keep the rendering of a HTML label inside the + * bounds of its vertex, then <isClipping> should return true for the + * respective cell. + * + * By default, edge labels are movable and vertex labels are fixed. This can be + * changed by setting <edgeLabelsMovable> and <vertexLabelsMovable>, or by + * overriding <isLabelMovable>. + * + * In-place Editing: + * + * In-place editing is started with a doubleclick or by typing F2. + * Programmatically, <edit> is used to check if the cell is editable + * (<isCellEditable>) and call <startEditingAtCell>, which invokes + * <mxCellEditor.startEditing>. The editor uses the value returned + * by <getEditingValue> as the editing value. + * + * After in-place editing, <labelChanged> is called, which invokes + * <mxGraphModel.setValue>, which in turn calls + * <mxGraphModel.valueForCellChanged> via <mxValueChange>. + * + * The event that triggers in-place editing is passed through to the + * <cellEditor>, which may take special actions depending on the type of the + * event or mouse location, and is also passed to <getEditingValue>. The event + * is then passed back to the event processing functions which can perform + * specific actions based on the trigger event. + * + * Tooltips: + * + * Tooltips are implemented by <getTooltip>, which calls <getTooltipForCell> + * if a cell is under the mousepointer. The default implementation checks if + * the cell has a getTooltip function and calls it if it exists. Hence, in order + * to provide custom tooltips, the cell must provide a getTooltip function, or + * one of the two above functions must be overridden. + * + * Typically, for custom cell tooltips, the latter function is overridden as + * follows: + * + * (code) + * graph.getTooltipForCell = function(cell) + * { + * var label = this.convertValueToString(cell); + * return 'Tooltip for '+label; + * } + * (end) + * + * When using a config file, the function is overridden in the mxGraph section + * using the following entry: + * + * (code) + * <add as="getTooltipForCell"><![CDATA[ + * function(cell) + * { + * var label = this.convertValueToString(cell); + * return 'Tooltip for '+label; + * } + * ]]></add> + * (end) + * + * "this" refers to the graph in the implementation, so for example to check if + * a cell is an edge, you use this.getModel().isEdge(cell) + * + * For replacing the default implementation of <getTooltipForCell> (rather than + * replacing the function on a specific instance), the following code should be + * used after loading the JavaScript files, but before creating a new mxGraph + * instance using <mxGraph>: + * + * (code) + * mxGraph.prototype.getTooltipForCell = function(cell) + * { + * var label = this.convertValueToString(cell); + * return 'Tooltip for '+label; + * } + * (end) + * + * Shapes & Styles: + * + * The implementation of new shapes is demonstrated in the examples. We'll assume + * that we have implemented a custom shape with the name BoxShape which we want + * to use for drawing vertices. To use this shape, it must first be registered in + * the cell renderer as follows: + * + * (code) + * graph.cellRenderer.registerShape('box', BoxShape); + * (end) + * + * The code registers the BoxShape constructor under the name box in the cell + * renderer of the graph. The shape can now be referenced using the shape-key in + * a style definition. (The cell renderer contains a set of additional shapes, + * namely one for each constant with a SHAPE-prefix in <mxConstants>.) + * + * Styles are a collection of key, value pairs and a stylesheet is a collection + * of named styles. The names are referenced by the cellstyle, which is stored + * in <mxCell.style> with the following format: [stylename;|key=value;]. The + * string is resolved to a collection of key, value pairs, where the keys are + * overridden with the values in the string. + * + * When introducing a new shape, the name under which the shape is registered + * must be used in the stylesheet. There are three ways of doing this: + * + * - By changing the default style, so that all vertices will use the new + * shape + * - By defining a new style, so that only vertices with the respective + * cellstyle will use the new shape + * - By using shape=box in the cellstyle's optional list of key, value pairs + * to be overridden + * + * In the first case, the code to fetch and modify the default style for + * vertices is as follows: + * + * (code) + * var style = graph.getStylesheet().getDefaultVertexStyle(); + * style[mxConstants.STYLE_SHAPE] = 'box'; + * (end) + * + * The code takes the default vertex style, which is used for all vertices that + * do not have a specific cellstyle, and modifies the value for the shape-key + * in-place to use the new BoxShape for drawing vertices. This is done by + * assigning the box value in the second line, which refers to the name of the + * BoxShape in the cell renderer. + * + * In the second case, a collection of key, value pairs is created and then + * added to the stylesheet under a new name. In order to distinguish the + * shapename and the stylename we'll use boxstyle for the stylename: + * + * (code) + * var style = new Object(); + * style[mxConstants.STYLE_SHAPE] = 'box'; + * style[mxConstants.STYLE_STROKECOLOR] = '#000000'; + * style[mxConstants.STYLE_FONTCOLOR] = '#000000'; + * graph.getStylesheet().putCellStyle('boxstyle', style); + * (end) + * + * The code adds a new style with the name boxstyle to the stylesheet. To use + * this style with a cell, it must be referenced from the cellstyle as follows: + * + * (code) + * var vertex = graph.insertVertex(parent, null, 'Hello, World!', 20, 20, 80, 20, + * 'boxstyle'); + * (end) + * + * To summarize, each new shape must be registered in the <mxCellRenderer> with + * a unique name. That name is then used as the value of the shape-key in a + * default or custom style. If there are multiple custom shapes, then there + * should be a separate style for each shape. + * + * Inheriting Styles: + * + * For fill-, stroke-, gradient- and indicatorColors special keywords can be + * used. The inherit keyword for one of these colors will inherit the color + * for the same key from the parent cell. The swimlane keyword does the same, + * but inherits from the nearest swimlane in the ancestor hierarchy. Finally, + * the indicated keyword will use the color of the indicator as the color for + * the given key. + * + * Scrollbars: + * + * The <containers> overflow CSS property defines if scrollbars are used to + * display the graph. For values of 'auto' or 'scroll', the scrollbars will + * be shown. Note that the <resizeContainer> flag is normally not used + * together with scrollbars, as it will resize the container to match the + * size of the graph after each change. + * + * Multiplicities and Validation: + * + * To control the possible connections in mxGraph, <getEdgeValidationError> is + * used. The default implementation of the function uses <multiplicities>, + * which is an array of <mxMultiplicity>. Using this class allows to establish + * simple multiplicities, which are enforced by the graph. + * + * The <mxMultiplicity> uses <mxCell.is> to determine for which terminals it + * applies. The default implementation of <mxCell.is> works with DOM nodes (XML + * nodes) and checks if the given type parameter matches the nodeName of the + * node (case insensitive). Optionally, an attributename and value can be + * specified which are also checked. + * + * <getEdgeValidationError> is called whenever the connectivity of an edge + * changes. It returns an empty string or an error message if the edge is + * invalid or null if the edge is valid. If the returned string is not empty + * then it is displayed as an error message. + * + * <mxMultiplicity> allows to specify the multiplicity between a terminal and + * its possible neighbors. For example, if any rectangle may only be connected + * to, say, a maximum of two circles you can add the following rule to + * <multiplicities>: + * + * (code) + * graph.multiplicities.push(new mxMultiplicity( + * true, 'rectangle', null, null, 0, 2, ['circle'], + * 'Only 2 targets allowed', + * 'Only shape targets allowed')); + * (end) + * + * This will display the first error message whenever a rectangle is connected + * to more than two circles and the second error message if a rectangle is + * connected to anything but a circle. + * + * For certain multiplicities, such as a minimum of 1 connection, which cannot + * be enforced at cell creation time (unless the cell is created together with + * the connection), mxGraph offers <validate> which checks all multiplicities + * for all cells and displays the respective error messages in an overlay icon + * on the cells. + * + * If a cell is collapsed and contains validation errors, a respective warning + * icon is attached to the collapsed cell. + * + * Auto-Layout: + * + * For automatic layout, the <getLayout> hook is provided in <mxLayoutManager>. + * It can be overridden to return a layout algorithm for the children of a + * given cell. + * + * Unconnected edges: + * + * The default values for all switches are designed to meet the requirements of + * general diagram drawing applications. A very typical set of settings to + * avoid edges that are not connected is the following: + * + * (code) + * graph.setAllowDanglingEdges(false); + * graph.setDisconnectOnMove(false); + * (end) + * + * Setting the <cloneInvalidEdges> switch to true is optional. This switch + * controls if edges are inserted after a copy, paste or clone-drag if they are + * invalid. For example, edges are invalid if copied or control-dragged without + * having selected the corresponding terminals and allowDanglingEdges is + * false, in which case the edges will not be cloned if the switch is false. + * + * Output: + * + * To produce an XML representation for a diagram, the following code can be + * used. + * + * (code) + * var enc = new mxCodec(mxUtils.createXmlDocument()); + * var node = enc.encode(graph.getModel()); + * (end) + * + * This will produce an XML node than can be handled using the DOM API or + * turned into a string representation using the following code: + * + * (code) + * var xml = mxUtils.getXml(node); + * (end) + * + * To obtain a formatted string, mxUtils.getPrettyXml can be used instead. + * + * This string can now be stored in a local persistent storage (for example + * using Google Gears) or it can be passed to a backend using mxUtils.post as + * follows. The url variable is the URL of the Java servlet, PHP page or HTTP + * handler, depending on the server. + * + * (code) + * var xmlString = encodeURIComponent(mxUtils.getXml(node)); + * mxUtils.post(url, 'xml='+xmlString, function(req) + * { + * // Process server response using req of type mxXmlRequest + * }); + * (end) + * + * Input: + * + * To load an XML representation of a diagram into an existing graph object + * mxUtils.load can be used as follows. The url variable is the URL of the Java + * servlet, PHP page or HTTP handler that produces the XML string. + * + * (code) + * var xmlDoc = mxUtils.load(url).getXml(); + * var node = xmlDoc.documentElement; + * var dec = new mxCodec(node.ownerDocument); + * dec.decode(node, graph.getModel()); + * (end) + * + * For creating a page that loads the client and a diagram using a single + * request please refer to the deployment examples in the backends. + * + * Functional dependencies: + * + * (see images/callgraph.png) + * + * Resources: + * + * resources/graph - Language resources for mxGraph + * + * Group: Events + * + * Event: mxEvent.ROOT + * + * Fires if the root in the model has changed. This event has no properties. + * + * Event: mxEvent.ALIGN_CELLS + * + * Fires between begin- and endUpdate in <alignCells>. The <code>cells</code> + * and <code>align</code> properties contain the respective arguments that were + * passed to <alignCells>. + * + * Event: mxEvent.FLIP_EDGE + * + * Fires between begin- and endUpdate in <flipEdge>. The <code>edge</code> + * property contains the edge passed to <flipEdge>. + * + * Event: mxEvent.ORDER_CELLS + * + * Fires between begin- and endUpdate in <orderCells>. The <code>cells</code> + * and <code>back</code> properties contain the respective arguments that were + * passed to <orderCells>. + * + * Event: mxEvent.CELLS_ORDERED + * + * Fires between begin- and endUpdate in <cellsOrdered>. The <code>cells</code> + * and <code>back</code> arguments contain the respective arguments that were + * passed to <cellsOrdered>. + * + * Event: mxEvent.GROUP_CELLS + * + * Fires between begin- and endUpdate in <groupCells>. The <code>group</code>, + * <code>cells</code> and <code>border</code> arguments contain the respective + * arguments that were passed to <groupCells>. + * + * Event: mxEvent.UNGROUP_CELLS + * + * Fires between begin- and endUpdate in <ungroupCells>. The <code>cells</code> + * property contains the array of cells that was passed to <ungroupCells>. + * + * Event: mxEvent.REMOVE_CELLS_FROM_PARENT + * + * Fires between begin- and endUpdate in <removeCellsFromParent>. The + * <code>cells</code> property contains the array of cells that was passed to + * <removeCellsFromParent>. + * + * Event: mxEvent.ADD_CELLS + * + * Fires between begin- and endUpdate in <addCells>. The <code>cells</code>, + * <code>parent</code>, <code>index</code>, <code>source</code> and + * <code>target</code> properties contain the respective arguments that were + * passed to <addCells>. + * + * Event: mxEvent.CELLS_ADDED + * + * Fires between begin- and endUpdate in <cellsAdded>. The <code>cells</code>, + * <code>parent</code>, <code>index</code>, <code>source</code>, + * <code>target</code> and <code>absolute</code> properties contain the + * respective arguments that were passed to <cellsAdded>. + * + * Event: mxEvent.REMOVE_CELLS + * + * Fires between begin- and endUpdate in <removeCells>. The <code>cells</code> + * and <code>includeEdges</code> arguments contain the respective arguments + * that were passed to <removeCells>. + * + * Event: mxEvent.CELLS_REMOVED + * + * Fires between begin- and endUpdate in <cellsRemoved>. The <code>cells</code> + * argument contains the array of cells that was removed. + * + * Event: mxEvent.SPLIT_EDGE + * + * Fires between begin- and endUpdate in <splitEdge>. The <code>edge</code> + * property contains the edge to be splitted, the <code>cells</code>, + * <code>newEdge</code>, <code>dx</code> and <code>dy</code> properties contain + * the respective arguments that were passed to <splitEdge>. + * + * Event: mxEvent.TOGGLE_CELLS + * + * Fires between begin- and endUpdate in <toggleCells>. The <code>show</code>, + * <code>cells</code> and <code>includeEdges</code> properties contain the + * respective arguments that were passed to <toggleCells>. + * + * Event: mxEvent.FOLD_CELLS + * + * Fires between begin- and endUpdate in <foldCells>. The + * <code>collapse</code>, <code>cells</code> and <code>recurse</code> + * properties contain the respective arguments that were passed to <foldCells>. + * + * Event: mxEvent.CELLS_FOLDED + * + * Fires between begin- and endUpdate in cellsFolded. The + * <code>collapse</code>, <code>cells</code> and <code>recurse</code> + * properties contain the respective arguments that were passed to + * <cellsFolded>. + * + * Event: mxEvent.UPDATE_CELL_SIZE + * + * Fires between begin- and endUpdate in <updateCellSize>. The + * <code>cell</code> and <code>ignoreChildren</code> properties contain the + * respective arguments that were passed to <updateCellSize>. + * + * Event: mxEvent.RESIZE_CELLS + * + * Fires between begin- and endUpdate in <resizeCells>. The <code>cells</code> + * and <code>bounds</code> properties contain the respective arguments that + * were passed to <resizeCells>. + * + * Event: mxEvent.CELLS_RESIZED + * + * Fires between begin- and endUpdate in <cellsResized>. The <code>cells</code> + * and <code>bounds</code> properties contain the respective arguments that + * were passed to <cellsResized>. + * + * Event: mxEvent.MOVE_CELLS + * + * Fires between begin- and endUpdate in <moveCells>. The <code>cells</code>, + * <code>dx</code>, <code>dy</code>, <code>clone</code>, <code>target</code> + * and <code>event</code> properties contain the respective arguments that + * were passed to <moveCells>. + * + * Event: mxEvent.CELLS_MOVED + * + * Fires between begin- and endUpdate in <cellsMoved>. The <code>cells</code>, + * <code>dx</code>, <code>dy</code> and <code>disconnect</code> properties + * contain the respective arguments that were passed to <cellsMoved>. + * + * Event: mxEvent.CONNECT_CELL + * + * Fires between begin- and endUpdate in <connectCell>. The <code>edge</code>, + * <code>terminal</code> and <code>source</code> properties contain the + * respective arguments that were passed to <connectCell>. + * + * Event: mxEvent.CELL_CONNECTED + * + * Fires between begin- and endUpdate in <cellConnected>. The + * <code>edge</code>, <code>terminal</code> and <code>source</code> properties + * contain the respective arguments that were passed to <cellConnected>. + * + * Event: mxEvent.REFRESH + * + * Fires after <refresh> was executed. This event has no properties. + * + * Event: mxEvent.CLICK + * + * Fires in <click> after a click event. The <code>event</code> property + * contains the original mouse event and <code>cell</code> property contains + * the cell under the mouse or null if the background was clicked. + * + * To handle a click event, use the following code: + * + * (code) + * graph.addListener(mxEvent.CLICK, function(sender, evt) + * { + * var e = evt.getProperty('event'); // mouse event + * var cell = evt.getProperty('cell'); // cell may be null + * + * if (!evt.isConsumed()) + * { + * if (cell != null) + * { + * // Do something useful with cell and consume the event + * evt.consume(); + * } + * } + * }); + * (end) + * + * Event: mxEvent.DOUBLE_CLICK + * + * Fires in <dblClick> after a double click. The <code>event</code> property + * contains the original mouse event and the <code>cell</code> property + * contains the cell under the mouse or null if the background was clicked. + * + * Event: mxEvent.SIZE + * + * Fires after <sizeDidChange> was executed. The <code>bounds</code> property + * contains the new graph bounds. + * + * Event: mxEvent.START_EDITING + * + * Fires before the in-place editor starts in <startEditingAtCell>. The + * <code>cell</code> property contains the cell that is being edited and the + * <code>event</code> property contains the optional event argument that was + * passed to <startEditingAtCell>. + * + * Event: mxEvent.LABEL_CHANGED + * + * Fires between begin- and endUpdate in <cellLabelChanged>. The + * <code>cell</code> property contains the cell, the <code>value</code> + * property contains the new value for the cell and the optional + * <code>event</code> property contains the mouse event that started the edit. + * + * Event: mxEvent.ADD_OVERLAY + * + * Fires after an overlay is added in <addCellOverlay>. The <code>cell</code> + * property contains the cell and the <code>overlay</code> property contains + * the <mxCellOverlay> that was added. + * + * Event: mxEvent.REMOVE_OVERLAY + * + * Fires after an overlay is removed in <removeCellOverlay> and + * <removeCellOverlays>. The <code>cell</code> property contains the cell and + * the <code>overlay</code> property contains the <mxCellOverlay> that was + * removed. + * + * Constructor: mxGraph + * + * Constructs a new mxGraph in the specified container. Model is an optional + * mxGraphModel. If no model is provided, a new mxGraphModel instance is + * used as the model. The container must have a valid owner document prior + * to calling this function in Internet Explorer. RenderHint is a string to + * affect the display performance and rendering in IE, but not in SVG-based + * browsers. The parameter is mapped to <dialect>, which may + * be one of <mxConstants.DIALECT_SVG> for SVG-based browsers, + * <mxConstants.DIALECT_STRICTHTML> for fastest display mode, + * <mxConstants.DIALECT_PREFERHTML> for faster display mode, + * <mxConstants.DIALECT_MIXEDHTML> for fast and <mxConstants.DIALECT_VML> + * for exact display mode (slowest). The dialects are defined in mxConstants. + * The default values are DIALECT_SVG for SVG-based browsers and + * DIALECT_MIXED for IE. + * + * The possible values for the renderingHint parameter are explained below: + * + * fast - The parameter is based on the fact that the display performance is + * highly improved in IE if the VML is not contained within a VML group + * element. The lack of a group element only slightly affects the display while + * panning, but improves the performance by almost a factor of 2, while keeping + * the display sufficiently accurate. This also allows to render certain shapes as HTML + * if the display accuracy is not affected, which is implemented by + * <mxShape.isMixedModeHtml>. This is the default setting and is mapped to + * DIALECT_MIXEDHTML. + * faster - Same as fast, but more expensive shapes are avoided. This is + * controlled by <mxShape.preferModeHtml>. The default implementation will + * avoid gradients and rounded rectangles, but more significant shapes, such + * as rhombus, ellipse, actor and cylinder will be rendered accurately. This + * setting is mapped to DIALECT_PREFERHTML. + * fastest - Almost anything will be rendered in Html. This allows for + * rectangles, labels and images. This setting is mapped to + * DIALECT_STRICTHTML. + * exact - If accurate panning is required and if the diagram is small (up + * to 100 cells), then this value should be used. In this mode, a group is + * created that contains the VML. This allows for accurate panning and is + * mapped to DIALECT_VML. + * + * Example: + * + * To create a graph inside a DOM node with an id of graph: + * (code) + * var container = document.getElementById('graph'); + * var graph = new mxGraph(container); + * (end) + * + * Parameters: + * + * container - Optional DOM node that acts as a container for the graph. + * If this is null then the container can be initialized later using + * <init>. + * model - Optional <mxGraphModel> that constitutes the graph data. + * renderHint - Optional string that specifies the display accuracy and + * performance. Default is mxConstants.DIALECT_MIXEDHTML (for IE). + * stylesheet - Optional <mxStylesheet> to be used in the graph. + */ +function mxGraph(container, model, renderHint, stylesheet) +{ + // Initializes the variable in case the prototype has been + // modified to hold some listeners (which is possible because + // the createHandlers call is executed regardless of the + // arguments passed into the ctor). + this.mouseListeners = null; + + // Converts the renderHint into a dialect + this.renderHint = renderHint; + + if (mxClient.IS_SVG) + { + this.dialect = mxConstants.DIALECT_SVG; + } + else if (renderHint == mxConstants.RENDERING_HINT_EXACT && mxClient.IS_VML) + { + this.dialect = mxConstants.DIALECT_VML; + } + else if (renderHint == mxConstants.RENDERING_HINT_FASTEST) + { + this.dialect = mxConstants.DIALECT_STRICTHTML; + } + else if (renderHint == mxConstants.RENDERING_HINT_FASTER) + { + this.dialect = mxConstants.DIALECT_PREFERHTML; + } + else // default for VML + { + this.dialect = mxConstants.DIALECT_MIXEDHTML; + } + + // Initializes the main members that do not require a container + this.model = (model != null) ? model : new mxGraphModel(); + this.multiplicities = []; + this.imageBundles = []; + this.cellRenderer = this.createCellRenderer(); + this.setSelectionModel(this.createSelectionModel()); + this.setStylesheet((stylesheet != null) ? stylesheet : this.createStylesheet()); + this.view = this.createGraphView(); + + // Adds a graph model listener to update the view + this.graphModelChangeListener = mxUtils.bind(this, function(sender, evt) + { + this.graphModelChanged(evt.getProperty('edit').changes); + }); + + this.model.addListener(mxEvent.CHANGE, this.graphModelChangeListener); + + // Installs basic event handlers with disabled default settings. + this.createHandlers(); + + // Initializes the display if a container was specified + if (container != null) + { + this.init(container); + } + + this.view.revalidate(); +}; + +/** + * Installs the required language resources at class + * loading time. + */ +if (mxLoadResources) +{ + mxResources.add(mxClient.basePath+'/resources/graph'); +} + +/** + * Extends mxEventSource. + */ +mxGraph.prototype = new mxEventSource(); +mxGraph.prototype.constructor = mxGraph; + +/** + * Variable: EMPTY_ARRAY + * + * Immutable empty array instance. + */ +mxGraph.prototype.EMPTY_ARRAY = []; + +/** + * Group: Variables + */ + +/** + * Variable: mouseListeners + * + * Holds the mouse event listeners. See <fireMouseEvent>. + */ +mxGraph.prototype.mouseListeners = null; + +/** + * Variable: isMouseDown + * + * Holds the state of the mouse button. + */ +mxGraph.prototype.isMouseDown = false; + +/** + * Variable: model + * + * Holds the <mxGraphModel> that contains the cells to be displayed. + */ +mxGraph.prototype.model = null; + +/** + * Variable: view + * + * Holds the <mxGraphView> that caches the <mxCellStates> for the cells. + */ +mxGraph.prototype.view = null; + +/** + * Variable: stylesheet + * + * Holds the <mxStylesheet> that defines the appearance of the cells. + * + * + * Example: + * + * Use the following code to read a stylesheet into an existing graph. + * + * (code) + * var req = mxUtils.load('stylesheet.xml'); + * var root = req.getDocumentElement(); + * var dec = new mxCodec(root.ownerDocument); + * dec.decode(root, graph.stylesheet); + * (end) + */ +mxGraph.prototype.stylesheet = null; + +/** + * Variable: selectionModel + * + * Holds the <mxGraphSelectionModel> that models the current selection. + */ +mxGraph.prototype.selectionModel = null; + +/** + * Variable: cellEditor + * + * Holds the <mxCellEditor> that is used as the in-place editing. + */ +mxGraph.prototype.cellEditor = null; + +/** + * Variable: cellRenderer + * + * Holds the <mxCellRenderer> for rendering the cells in the graph. + */ +mxGraph.prototype.cellRenderer = null; + +/** + * Variable: multiplicities + * + * An array of <mxMultiplicities> describing the allowed + * connections in a graph. + */ +mxGraph.prototype.multiplicities = null; + +/** + * Variable: renderHint + * + * RenderHint as it was passed to the constructor. + */ +mxGraph.prototype.renderHint = null; + +/** + * Variable: dialect + * + * Dialect to be used for drawing the graph. Possible values are all + * constants in <mxConstants> with a DIALECT-prefix. + */ +mxGraph.prototype.dialect = null; + +/** + * Variable: gridSize + * + * Specifies the grid size. Default is 10. + */ +mxGraph.prototype.gridSize = 10; + +/** + * Variable: gridEnabled + * + * Specifies if the grid is enabled. This is used in <snap>. Default is + * true. + */ +mxGraph.prototype.gridEnabled = true; + +/** + * Variable: portsEnabled + * + * Specifies if ports are enabled. This is used in <cellConnected> to update + * the respective style. Default is true. + */ +mxGraph.prototype.portsEnabled = true; + +/** + * Variable: doubleTapEnabled + * + * Specifies if double taps on touch-based devices should be handled. Default + * is true. + */ +mxGraph.prototype.doubleTapEnabled = true; + +/** + * Variable: doubleTapTimeout + * + * Specifies the timeout for double taps. Default is 700 ms. + */ +mxGraph.prototype.doubleTapTimeout = 700; + +/** + * Variable: doubleTapTolerance + * + * Specifies the tolerance for double taps. Default is 25 pixels. + */ +mxGraph.prototype.doubleTapTolerance = 25; + +/** + * Variable: lastTouchX + * + * Holds the x-coordinate of the last touch event for double tap detection. + */ +mxGraph.prototype.lastTouchY = 0; + +/** + * Variable: lastTouchX + * + * Holds the y-coordinate of the last touch event for double tap detection. + */ +mxGraph.prototype.lastTouchY = 0; + +/** + * Variable: lastTouchTime + * + * Holds the time of the last touch event for double click detection. + */ +mxGraph.prototype.lastTouchTime = 0; + +/** + * Variable: gestureEnabled + * + * Specifies if the handleGesture method should be invoked. Default is true. This + * is an experimental feature for touch-based devices. + */ +mxGraph.prototype.gestureEnabled = true; + +/** + * Variable: tolerance + * + * Tolerance for a move to be handled as a single click. + * Default is 4 pixels. + */ +mxGraph.prototype.tolerance = 4; + +/** + * Variable: defaultOverlap + * + * Value returned by <getOverlap> if <isAllowOverlapParent> returns + * true for the given cell. <getOverlap> is used in <constrainChild> if + * <isConstrainChild> returns true. The value specifies the + * portion of the child which is allowed to overlap the parent. + */ +mxGraph.prototype.defaultOverlap = 0.5; + +/** + * Variable: defaultParent + * + * Specifies the default parent to be used to insert new cells. + * This is used in <getDefaultParent>. Default is null. + */ +mxGraph.prototype.defaultParent = null; + +/** + * Variable: alternateEdgeStyle + * + * Specifies the alternate edge style to be used if the main control point + * on an edge is being doubleclicked. Default is null. + */ +mxGraph.prototype.alternateEdgeStyle = null; + +/** + * Variable: backgroundImage + * + * Specifies the <mxImage> to be returned by <getBackgroundImage>. Default + * is null. + * + * Example: + * + * (code) + * var img = new mxImage('http://www.example.com/maps/examplemap.jpg', 1024, 768); + * graph.setBackgroundImage(img); + * graph.view.validate(); + * (end) + */ +mxGraph.prototype.backgroundImage = null; + +/** + * Variable: pageVisible + * + * Specifies if the background page should be visible. Default is false. + * Not yet implemented. + */ +mxGraph.prototype.pageVisible = false; + +/** + * Variable: pageBreaksVisible + * + * Specifies if a dashed line should be drawn between multiple pages. Default + * is false. If you change this value while a graph is being displayed then you + * should call <sizeDidChange> to force an update of the display. + */ +mxGraph.prototype.pageBreaksVisible = false; + +/** + * Variable: pageBreakColor + * + * Specifies the color for page breaks. Default is 'gray'. + */ +mxGraph.prototype.pageBreakColor = 'gray'; + +/** + * Variable: pageBreakDashed + * + * Specifies the page breaks should be dashed. Default is true. + */ +mxGraph.prototype.pageBreakDashed = true; + +/** + * Variable: minPageBreakDist + * + * Specifies the minimum distance for page breaks to be visible. Default is + * 20 (in pixels). + */ +mxGraph.prototype.minPageBreakDist = 20; + +/** + * Variable: preferPageSize + * + * Specifies if the graph size should be rounded to the next page number in + * <sizeDidChange>. This is only used if the graph container has scrollbars. + * Default is false. + */ +mxGraph.prototype.preferPageSize = false; + +/** + * Variable: pageFormat + * + * Specifies the page format for the background page. Default is + * <mxConstants.PAGE_FORMAT_A4_PORTRAIT>. This is used as the default in + * <mxPrintPreview> and for painting the background page if <pageVisible> is + * true and the pagebreaks if <pageBreaksVisible> is true. + */ +mxGraph.prototype.pageFormat = mxConstants.PAGE_FORMAT_A4_PORTRAIT; + +/** + * Variable: pageScale + * + * Specifies the scale of the background page. Default is 1.5. + * Not yet implemented. + */ +mxGraph.prototype.pageScale = 1.5; + +/** + * Variable: enabled + * + * Specifies the return value for <isEnabled>. Default is true. + */ +mxGraph.prototype.enabled = true; + +/** + * Variable: escapeEnabled + * + * Specifies if <mxKeyHandler> should invoke <escape> when the escape key + * is pressed. Default is true. + */ +mxGraph.prototype.escapeEnabled = true; + +/** + * Variable: invokesStopCellEditing + * + * If true, when editing is to be stopped by way of selection changing, + * data in diagram changing or other means stopCellEditing is invoked, and + * changes are saved. This is implemented in a focus handler in + * <mxCellEditor>. Default is true. + */ +mxGraph.prototype.invokesStopCellEditing = true; + +/** + * Variable: enterStopsCellEditing + * + * If true, pressing the enter key without pressing control or shift will stop + * editing and accept the new value. This is used in <mxCellEditor> to stop + * cell editing. Note: You can always use F2 and escape to stop editing. + * Default is false. + */ +mxGraph.prototype.enterStopsCellEditing = false; + +/** + * Variable: useScrollbarsForPanning + * + * Specifies if scrollbars should be used for panning in <panGraph> if + * any scrollbars are available. If scrollbars are enabled in CSS, but no + * scrollbars appear because the graph is smaller than the container size, + * then no panning occurs if this is true. Default is true. + */ +mxGraph.prototype.useScrollbarsForPanning = true; + +/** + * Variable: exportEnabled + * + * Specifies the return value for <canExportCell>. Default is true. + */ +mxGraph.prototype.exportEnabled = true; + +/** + * Variable: importEnabled + * + * Specifies the return value for <canImportCell>. Default is true. + */ +mxGraph.prototype.importEnabled = true; + +/** + * Variable: cellsLocked + * + * Specifies the return value for <isCellLocked>. Default is false. + */ +mxGraph.prototype.cellsLocked = false; + +/** + * Variable: cellsCloneable + * + * Specifies the return value for <isCellCloneable>. Default is true. + */ +mxGraph.prototype.cellsCloneable = true; + +/** + * Variable: foldingEnabled + * + * Specifies if folding (collapse and expand via an image icon in the graph + * should be enabled). Default is true. + */ +mxGraph.prototype.foldingEnabled = true; + +/** + * Variable: cellsEditable + * + * Specifies the return value for <isCellEditable>. Default is true. + */ +mxGraph.prototype.cellsEditable = true; + +/** + * Variable: cellsDeletable + * + * Specifies the return value for <isCellDeletable>. Default is true. + */ +mxGraph.prototype.cellsDeletable = true; + +/** + * Variable: cellsMovable + * + * Specifies the return value for <isCellMovable>. Default is true. + */ +mxGraph.prototype.cellsMovable = true; + +/** + * Variable: edgeLabelsMovable + * + * Specifies the return value for edges in <isLabelMovable>. Default is true. + */ +mxGraph.prototype.edgeLabelsMovable = true; + +/** + * Variable: vertexLabelsMovable + * + * Specifies the return value for vertices in <isLabelMovable>. Default is false. + */ +mxGraph.prototype.vertexLabelsMovable = false; + +/** + * Variable: dropEnabled + * + * Specifies the return value for <isDropEnabled>. Default is false. + */ +mxGraph.prototype.dropEnabled = false; + +/** + * Variable: splitEnabled + * + * Specifies if dropping onto edges should be enabled. Default is true. + */ +mxGraph.prototype.splitEnabled = true; + +/** + * Variable: cellsResizable + * + * Specifies the return value for <isCellResizable>. Default is true. + */ +mxGraph.prototype.cellsResizable = true; + +/** + * Variable: cellsBendable + * + * Specifies the return value for <isCellsBendable>. Default is true. + */ +mxGraph.prototype.cellsBendable = true; + +/** + * Variable: cellsSelectable + * + * Specifies the return value for <isCellSelectable>. Default is true. + */ +mxGraph.prototype.cellsSelectable = true; + +/** + * Variable: cellsDisconnectable + * + * Specifies the return value for <isCellDisconntable>. Default is true. + */ +mxGraph.prototype.cellsDisconnectable = true; + +/** + * Variable: autoSizeCells + * + * Specifies if the graph should automatically update the cell size after an + * edit. This is used in <isAutoSizeCell>. Default is false. + */ +mxGraph.prototype.autoSizeCells = false; + +/** + * Variable: autoScroll + * + * Specifies if the graph should automatically scroll if the mouse goes near + * the container edge while dragging. This is only taken into account if the + * container has scrollbars. Default is true. + * + * If you need this to work without scrollbars then set <ignoreScrollbars> to + * true. + */ +mxGraph.prototype.autoScroll = true; + +/** + * Variable: timerAutoScroll + * + * Specifies if timer-based autoscrolling should be used via mxPanningManager. + * Note that this disables the code in <scrollPointToVisible> and uses code in + * mxPanningManager instead. Note that <autoExtend> is disabled if this is + * true and that this should only be used with a scroll buffer or when + * scollbars are visible and scrollable in all directions. Default is false. + */ +mxGraph.prototype.timerAutoScroll = false; + +/** + * Variable: allowAutoPanning + * + * Specifies if panning via <panGraph> should be allowed to implement autoscroll + * if no scrollbars are available in <scrollPointToVisible>. Default is false. + */ +mxGraph.prototype.allowAutoPanning = false; + +/** + * Variable: ignoreScrollbars + * + * Specifies if the graph should automatically scroll regardless of the + * scrollbars. + */ +mxGraph.prototype.ignoreScrollbars = false; + +/** + * Variable: autoExtend + * + * Specifies if the size of the graph should be automatically extended if the + * mouse goes near the container edge while dragging. This is only taken into + * account if the container has scrollbars. Default is true. See <autoScroll>. + */ +mxGraph.prototype.autoExtend = true; + +/** + * Variable: maximumGraphBounds + * + * <mxRectangle> that specifies the area in which all cells in the diagram + * should be placed. Uses in <getMaximumGraphBounds>. Use a width or height of + * 0 if you only want to give a upper, left corner. + */ +mxGraph.prototype.maximumGraphBounds = null; + +/** + * Variable: minimumGraphSize + * + * <mxRectangle> that specifies the minimum size of the graph. This is ignored + * if the graph container has no scrollbars. Default is null. + */ +mxGraph.prototype.minimumGraphSize = null; + +/** + * Variable: minimumContainerSize + * + * <mxRectangle> that specifies the minimum size of the <container> if + * <resizeContainer> is true. + */ +mxGraph.prototype.minimumContainerSize = null; + +/** + * Variable: maximumContainerSize + * + * <mxRectangle> that specifies the maximum size of the container if + * <resizeContainer> is true. + */ +mxGraph.prototype.maximumContainerSize = null; + +/** + * Variable: resizeContainer + * + * Specifies if the container should be resized to the graph size when + * the graph size has changed. Default is false. + */ +mxGraph.prototype.resizeContainer = false; + +/** + * Variable: border + * + * Border to be added to the bottom and right side when the container is + * being resized after the graph has been changed. Default is 0. + */ +mxGraph.prototype.border = 0; + +/** + * Variable: ordered + * + * Specifies if the display should reflect the order of the cells in + * the model. Default is true. This has precendence over + * <keepEdgesInBackground> and <keepEdgesInForeground>. + */ +mxGraph.prototype.ordered = true; + +/** + * Variable: keepEdgesInForeground + * + * Specifies if edges should appear in the foreground regardless of their + * order in the model. This has precendence over <keepEdgeInBackground>, + * but not over <ordered>. Default is false. + */ +mxGraph.prototype.keepEdgesInForeground = false; + +/** + * Variable: keepEdgesInBackground + * + * Specifies if edges should appear in the background regardless of their + * order in the model. <ordered> and <keepEdgesInForeground> have + * precedence over this setting. Default is true. + */ +mxGraph.prototype.keepEdgesInBackground = true; + +/** + * Variable: allowNegativeCoordinates + * + * Specifies if negative coordinates for vertices are allowed. Default is true. + */ +mxGraph.prototype.allowNegativeCoordinates = true; + +/** + * Variable: constrainChildren + * + * Specifies the return value for <isConstrainChildren>. Default is + * true. + */ +mxGraph.prototype.constrainChildren = true; + +/** + * Variable: extendParents + * + * Specifies if a parent should contain the child bounds after a resize of + * the child. Default is true. + */ +mxGraph.prototype.extendParents = true; + +/** + * Variable: extendParentsOnAdd + * + * Specifies if parents should be extended according to the <extendParents> + * switch if cells are added. Default is true. + */ +mxGraph.prototype.extendParentsOnAdd = true; + +/** + * Variable: collapseToPreferredSize + * + * Specifies if the cell size should be changed to the preferred size when + * a cell is first collapsed. Default is true. + */ +mxGraph.prototype.collapseToPreferredSize = true; + +/** + * Variable: zoomFactor + * + * Specifies the factor used for <zoomIn> and <zoomOut>. Default is 1.2 + * (120%). + */ +mxGraph.prototype.zoomFactor = 1.2; + +/** + * Variable: keepSelectionVisibleOnZoom + * + * Specifies if the viewport should automatically contain the selection cells + * after a zoom operation. Default is false. + */ +mxGraph.prototype.keepSelectionVisibleOnZoom = false; + +/** + * Variable: centerZoom + * + * Specifies if the zoom operations should go into the center of the actual + * diagram rather than going from top, left. Default is true. + */ +mxGraph.prototype.centerZoom = true; + +/** + * Variable: resetViewOnRootChange + * + * Specifies if the scale and translate should be reset if the root changes in + * the model. Default is true. + */ +mxGraph.prototype.resetViewOnRootChange = true; + +/** + * Variable: resetEdgesOnResize + * + * Specifies if edge control points should be reset after the resize of a + * connected cell. Default is false. + */ +mxGraph.prototype.resetEdgesOnResize = false; + +/** + * Variable: resetEdgesOnMove + * + * Specifies if edge control points should be reset after the move of a + * connected cell. Default is false. + */ +mxGraph.prototype.resetEdgesOnMove = false; + +/** + * Variable: resetEdgesOnConnect + * + * Specifies if edge control points should be reset after the the edge has been + * reconnected. Default is true. + */ +mxGraph.prototype.resetEdgesOnConnect = true; + +/** + * Variable: allowLoops + * + * Specifies if loops (aka self-references) are allowed. Default is false. + */ +mxGraph.prototype.allowLoops = false; + +/** + * Variable: defaultLoopStyle + * + * <mxEdgeStyle> to be used for loops. This is a fallback for loops if the + * <mxConstants.STYLE_LOOP> is undefined. Default is <mxEdgeStyle.Loop>. + */ +mxGraph.prototype.defaultLoopStyle = mxEdgeStyle.Loop; + +/** + * Variable: multigraph + * + * Specifies if multiple edges in the same direction between the same pair of + * vertices are allowed. Default is true. + */ +mxGraph.prototype.multigraph = true; + +/** + * Variable: connectableEdges + * + * Specifies if edges are connectable. Default is false. This overrides the + * connectable field in edges. + */ +mxGraph.prototype.connectableEdges = false; + +/** + * Variable: allowDanglingEdges + * + * Specifies if edges with disconnected terminals are allowed in the graph. + * Default is true. + */ +mxGraph.prototype.allowDanglingEdges = true; + +/** + * Variable: cloneInvalidEdges + * + * Specifies if edges that are cloned should be validated and only inserted + * if they are valid. Default is true. + */ +mxGraph.prototype.cloneInvalidEdges = false; + +/** + * Variable: disconnectOnMove + * + * Specifies if edges should be disconnected from their terminals when they + * are moved. Default is true. + */ +mxGraph.prototype.disconnectOnMove = true; + +/** + * Variable: labelsVisible + * + * Specifies if labels should be visible. This is used in <getLabel>. Default + * is true. + */ +mxGraph.prototype.labelsVisible = true; + +/** + * Variable: htmlLabels + * + * Specifies the return value for <isHtmlLabel>. Default is false. + */ +mxGraph.prototype.htmlLabels = false; + +/** + * Variable: swimlaneSelectionEnabled + * + * Specifies if swimlanes should be selectable via the content if the + * mouse is released. Default is true. + */ +mxGraph.prototype.swimlaneSelectionEnabled = true; + +/** + * Variable: swimlaneNesting + * + * Specifies if nesting of swimlanes is allowed. Default is true. + */ +mxGraph.prototype.swimlaneNesting = true; + +/** + * Variable: swimlaneIndicatorColorAttribute + * + * The attribute used to find the color for the indicator if the indicator + * color is set to 'swimlane'. Default is <mxConstants.STYLE_FILLCOLOR>. + */ +mxGraph.prototype.swimlaneIndicatorColorAttribute = mxConstants.STYLE_FILLCOLOR; + +/** + * Variable: imageBundles + * + * Holds the list of image bundles. + */ +mxGraph.prototype.imageBundles = null; + +/** + * Variable: minFitScale + * + * Specifies the minimum scale to be applied in <fit>. Default is 0.1. Set this + * to null to allow any value. + */ +mxGraph.prototype.minFitScale = 0.1; + +/** + * Variable: maxFitScale + * + * Specifies the maximum scale to be applied in <fit>. Default is 8. Set this + * to null to allow any value. + */ +mxGraph.prototype.maxFitScale = 8; + +/** + * Variable: panDx + * + * Current horizontal panning value. Default is 0. + */ +mxGraph.prototype.panDx = 0; + +/** + * Variable: panDy + * + * Current vertical panning value. Default is 0. + */ +mxGraph.prototype.panDy = 0; + +/** + * Variable: collapsedImage + * + * Specifies the <mxImage> to indicate a collapsed state. + * Default value is mxClient.imageBasePath + '/collapsed.gif' + */ +mxGraph.prototype.collapsedImage = new mxImage(mxClient.imageBasePath + '/collapsed.gif', 9, 9); + +/** + * Variable: expandedImage + * + * Specifies the <mxImage> to indicate a expanded state. + * Default value is mxClient.imageBasePath + '/expanded.gif' + */ +mxGraph.prototype.expandedImage = new mxImage(mxClient.imageBasePath + '/expanded.gif', 9, 9); + +/** + * Variable: warningImage + * + * Specifies the <mxImage> for the image to be used to display a warning + * overlay. See <setCellWarning>. Default value is mxClient.imageBasePath + + * '/warning'. The extension for the image depends on the platform. It is + * '.png' on the Mac and '.gif' on all other platforms. + */ +mxGraph.prototype.warningImage = new mxImage(mxClient.imageBasePath + '/warning'+ + ((mxClient.IS_MAC) ? '.png' : '.gif'), 16, 16); + +/** + * Variable: alreadyConnectedResource + * + * Specifies the resource key for the error message to be displayed in + * non-multigraphs when two vertices are already connected. If the resource + * for this key does not exist then the value is used as the error message. + * Default is 'alreadyConnected'. + */ +mxGraph.prototype.alreadyConnectedResource = (mxClient.language != 'none') ? 'alreadyConnected' : ''; + +/** + * Variable: containsValidationErrorsResource + * + * Specifies the resource key for the warning message to be displayed when + * a collapsed cell contains validation errors. If the resource for this + * key does not exist then the value is used as the warning message. + * Default is 'containsValidationErrors'. + */ +mxGraph.prototype.containsValidationErrorsResource = (mxClient.language != 'none') ? 'containsValidationErrors' : ''; + +/** + * Variable: collapseExpandResource + * + * Specifies the resource key for the tooltip on the collapse/expand icon. + * If the resource for this key does not exist then the value is used as + * the tooltip. Default is 'collapse-expand'. + */ +mxGraph.prototype.collapseExpandResource = (mxClient.language != 'none') ? 'collapse-expand' : ''; + +/** + * Function: init + * + * Initializes the <container> and creates the respective datastructures. + * + * Parameters: + * + * container - DOM node that will contain the graph display. + */ + mxGraph.prototype.init = function(container) + { + this.container = container; + + // Initializes the in-place editor + this.cellEditor = this.createCellEditor(); + + // Initializes the container using the view + this.view.init(); + + // Updates the size of the container for the current graph + this.sizeDidChange(); + + // Automatic deallocation of memory + if (mxClient.IS_IE) + { + mxEvent.addListener(window, 'unload', mxUtils.bind(this, function() + { + this.destroy(); + })); + + // Disable shift-click for text + mxEvent.addListener(container, 'selectstart', + mxUtils.bind(this, function() + { + return this.isEditing(); + }) + ); + } +}; + +/** + * Function: createHandlers + * + * Creates the tooltip-, panning-, connection- and graph-handler (in this + * order). This is called in the constructor before <init> is called. + */ +mxGraph.prototype.createHandlers = function(container) +{ + this.tooltipHandler = new mxTooltipHandler(this); + this.tooltipHandler.setEnabled(false); + this.panningHandler = new mxPanningHandler(this); + this.panningHandler.panningEnabled = false; + this.selectionCellsHandler = new mxSelectionCellsHandler(this); + this.connectionHandler = new mxConnectionHandler(this); + this.connectionHandler.setEnabled(false); + this.graphHandler = new mxGraphHandler(this); +}; + +/** + * Function: createSelectionModel + * + * Creates a new <mxGraphSelectionModel> to be used in this graph. + */ +mxGraph.prototype.createSelectionModel = function() +{ + return new mxGraphSelectionModel(this); +}; + +/** + * Function: createStylesheet + * + * Creates a new <mxGraphSelectionModel> to be used in this graph. + */ +mxGraph.prototype.createStylesheet = function() +{ + return new mxStylesheet(); +}; + +/** + * Function: createGraphView + * + * Creates a new <mxGraphView> to be used in this graph. + */ +mxGraph.prototype.createGraphView = function() +{ + return new mxGraphView(this); +}; + +/** + * Function: createCellRenderer + * + * Creates a new <mxCellRenderer> to be used in this graph. + */ +mxGraph.prototype.createCellRenderer = function() +{ + return new mxCellRenderer(); +}; + +/** + * Function: createCellEditor + * + * Creates a new <mxCellEditor> to be used in this graph. + */ +mxGraph.prototype.createCellEditor = function() +{ + return new mxCellEditor(this); +}; + +/** + * Function: getModel + * + * Returns the <mxGraphModel> that contains the cells. + */ +mxGraph.prototype.getModel = function() +{ + return this.model; +}; + +/** + * Function: getView + * + * Returns the <mxGraphView> that contains the <mxCellStates>. + */ +mxGraph.prototype.getView = function() +{ + return this.view; +}; + +/** + * Function: getStylesheet + * + * Returns the <mxStylesheet> that defines the style. + */ +mxGraph.prototype.getStylesheet = function() +{ + return this.stylesheet; +}; + +/** + * Function: setStylesheet + * + * Sets the <mxStylesheet> that defines the style. + */ +mxGraph.prototype.setStylesheet = function(stylesheet) +{ + this.stylesheet = stylesheet; +}; + +/** + * Function: getSelectionModel + * + * Returns the <mxGraphSelectionModel> that contains the selection. + */ +mxGraph.prototype.getSelectionModel = function() +{ + return this.selectionModel; +}; + +/** + * Function: setSelectionModel + * + * Sets the <mxSelectionModel> that contains the selection. + */ +mxGraph.prototype.setSelectionModel = function(selectionModel) +{ + this.selectionModel = selectionModel; +}; + +/** + * Function: getSelectionCellsForChanges + * + * Returns the cells to be selected for the given array of changes. + */ +mxGraph.prototype.getSelectionCellsForChanges = function(changes) +{ + var cells = []; + + for (var i = 0; i < changes.length; i++) + { + var change = changes[i]; + + if (change.constructor != mxRootChange) + { + var cell = null; + + if (change instanceof mxChildChange && change.previous == null) + { + cell = change.child; + } + else if (change.cell != null && change.cell instanceof mxCell) + { + cell = change.cell; + } + + if (cell != null && mxUtils.indexOf(cells, cell) < 0) + { + cells.push(cell); + } + } + } + + return this.getModel().getTopmostCells(cells); +}; + +/** + * Function: graphModelChanged + * + * Called when the graph model changes. Invokes <processChange> on each + * item of the given array to update the view accordingly. + * + * Parameters: + * + * changes - Array that contains the individual changes. + */ +mxGraph.prototype.graphModelChanged = function(changes) +{ + for (var i = 0; i < changes.length; i++) + { + this.processChange(changes[i]); + } + + this.removeSelectionCells(this.getRemovedCellsForChanges(changes)); + + this.view.validate(); + this.sizeDidChange(); +}; + +/** + * Function: getRemovedCellsForChanges + * + * Returns the cells that have been removed from the model. + */ +mxGraph.prototype.getRemovedCellsForChanges = function(changes) +{ + var result = []; + + for (var i = 0; i < changes.length; i++) + { + var change = changes[i]; + + // Resets the view settings, removes all cells and clears + // the selection if the root changes. + if (change instanceof mxRootChange) + { + break; + } + else if (change instanceof mxChildChange) + { + if (change.previous != null && change.parent == null) + { + result = result.concat(this.model.getDescendants(change.child)); + } + } + else if (change instanceof mxVisibleChange) + { + result = result.concat(this.model.getDescendants(change.cell)); + } + } + + return result; +}; + +/** + * Function: processChange + * + * Processes the given change and invalidates the respective cached data + * in <view>. This fires a <root> event if the root has changed in the + * model. + * + * Parameters: + * + * change - Object that represents the change on the model. + */ +mxGraph.prototype.processChange = function(change) +{ + // Resets the view settings, removes all cells and clears + // the selection if the root changes. + if (change instanceof mxRootChange) + { + this.clearSelection(); + this.removeStateForCell(change.previous); + + if (this.resetViewOnRootChange) + { + this.view.scale = 1; + this.view.translate.x = 0; + this.view.translate.y = 0; + } + + this.fireEvent(new mxEventObject(mxEvent.ROOT)); + } + + // Adds or removes a child to the view by online invaliding + // the minimal required portions of the cache, namely, the + // old and new parent and the child. + else if (change instanceof mxChildChange) + { + var newParent = this.model.getParent(change.child); + + if (newParent != null) + { + // Flags the cell for updating the order in the renderer + this.view.invalidate(change.child, true, false, change.previous != null); + } + else + { + this.removeStateForCell(change.child); + + // Handles special case of current root of view being removed + if (this.view.currentRoot == change.child) + { + this.home(); + } + } + + if (newParent != change.previous) + { + // Refreshes the collapse/expand icons on the parents + if (newParent != null) + { + this.view.invalidate(newParent, false, false); + } + + if (change.previous != null) + { + this.view.invalidate(change.previous, false, false); + } + } + } + + // Handles two special cases where the shape does not need to be + // recreated from scratch, it only need to be invalidated. + else if (change instanceof mxTerminalChange || + change instanceof mxGeometryChange) + { + this.view.invalidate(change.cell); + } + + // Handles two special cases where only the shape, but no + // descendants need to be recreated + else if (change instanceof mxValueChange) + { + this.view.invalidate(change.cell, false, false); + } + + // Requires a new mxShape in JavaScript + else if (change instanceof mxStyleChange) + { + this.view.invalidate(change.cell, true, true, false); + this.view.removeState(change.cell); + } + + // Removes the state from the cache by default + else if (change.cell != null && + change.cell instanceof mxCell) + { + this.removeStateForCell(change.cell); + } +}; + +/** + * Function: removeStateForCell + * + * Removes all cached information for the given cell and its descendants. + * This is called when a cell was removed from the model. + * + * Paramters: + * + * cell - <mxCell> that was removed from the model. + */ +mxGraph.prototype.removeStateForCell = function(cell) +{ + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + this.removeStateForCell(this.model.getChildAt(cell, i)); + } + + this.view.removeState(cell); +}; + +/** + * Group: Overlays + */ + +/** + * Function: addCellOverlay + * + * Adds an <mxCellOverlay> for the specified cell. This method fires an + * <addoverlay> event and returns the new <mxCellOverlay>. + * + * Parameters: + * + * cell - <mxCell> to add the overlay for. + * overlay - <mxCellOverlay> to be added for the cell. + */ +mxGraph.prototype.addCellOverlay = function(cell, overlay) +{ + if (cell.overlays == null) + { + cell.overlays = []; + } + + cell.overlays.push(overlay); + + var state = this.view.getState(cell); + + // Immediately updates the cell display if the state exists + if (state != null) + { + this.cellRenderer.redraw(state); + } + + this.fireEvent(new mxEventObject(mxEvent.ADD_OVERLAY, + 'cell', cell, 'overlay', overlay)); + + return overlay; +}; + +/** + * Function: getCellOverlays + * + * Returns the array of <mxCellOverlays> for the given cell or null, if + * no overlays are defined. + * + * Parameters: + * + * cell - <mxCell> whose overlays should be returned. + */ +mxGraph.prototype.getCellOverlays = function(cell) +{ + return cell.overlays; +}; + +/** + * Function: removeCellOverlay + * + * Removes and returns the given <mxCellOverlay> from the given cell. This + * method fires a <removeoverlay> event. If no overlay is given, then all + * overlays are removed using <removeOverlays>. + * + * Parameters: + * + * cell - <mxCell> whose overlay should be removed. + * overlay - Optional <mxCellOverlay> to be removed. + */ +mxGraph.prototype.removeCellOverlay = function(cell, overlay) +{ + if (overlay == null) + { + this.removeCellOverlays(cell); + } + else + { + var index = mxUtils.indexOf(cell.overlays, overlay); + + if (index >= 0) + { + cell.overlays.splice(index, 1); + + if (cell.overlays.length == 0) + { + cell.overlays = null; + } + + // Immediately updates the cell display if the state exists + var state = this.view.getState(cell); + + if (state != null) + { + this.cellRenderer.redraw(state); + } + + this.fireEvent(new mxEventObject(mxEvent.REMOVE_OVERLAY, + 'cell', cell, 'overlay', overlay)); + } + else + { + overlay = null; + } + } + + return overlay; +}; + +/** + * Function: removeCellOverlays + * + * Removes all <mxCellOverlays> from the given cell. This method + * fires a <removeoverlay> event for each <mxCellOverlay> and returns + * the array of <mxCellOverlays> that was removed from the cell. + * + * Parameters: + * + * cell - <mxCell> whose overlays should be removed + */ +mxGraph.prototype.removeCellOverlays = function(cell) +{ + var overlays = cell.overlays; + + if (overlays != null) + { + cell.overlays = null; + + // Immediately updates the cell display if the state exists + var state = this.view.getState(cell); + + if (state != null) + { + this.cellRenderer.redraw(state); + } + + for (var i = 0; i < overlays.length; i++) + { + this.fireEvent(new mxEventObject(mxEvent.REMOVE_OVERLAY, + 'cell', cell, 'overlay', overlays[i])); + } + } + + return overlays; +}; + +/** + * Function: clearCellOverlays + * + * Removes all <mxCellOverlays> in the graph for the given cell and all its + * descendants. If no cell is specified then all overlays are removed from + * the graph. This implementation uses <removeCellOverlays> to remove the + * overlays from the individual cells. + * + * Parameters: + * + * cell - Optional <mxCell> that represents the root of the subtree to + * remove the overlays from. Default is the root in the model. + */ +mxGraph.prototype.clearCellOverlays = function(cell) +{ + cell = (cell != null) ? cell : this.model.getRoot(); + this.removeCellOverlays(cell); + + // Recursively removes all overlays from the children + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = this.model.getChildAt(cell, i); + this.clearCellOverlays(child); // recurse + } +}; + +/** + * Function: setCellWarning + * + * Creates an overlay for the given cell using the warning and image or + * <warningImage> and returns the new <mxCellOverlay>. The warning is + * displayed as a tooltip in a red font and may contain HTML markup. If + * the warning is null or a zero length string, then all overlays are + * removed from the cell. + * + * Example: + * + * (code) + * graph.setCellWarning(cell, '<b>Warning:</b>: Hello, World!'); + * (end) + * + * Parameters: + * + * cell - <mxCell> whose warning should be set. + * warning - String that represents the warning to be displayed. + * img - Optional <mxImage> to be used for the overlay. Default is + * <warningImage>. + * isSelect - Optional boolean indicating if a click on the overlay + * should select the corresponding cell. Default is false. + */ +mxGraph.prototype.setCellWarning = function(cell, warning, img, isSelect) +{ + if (warning != null && warning.length > 0) + { + img = (img != null) ? img : this.warningImage; + + // Creates the overlay with the image and warning + var overlay = new mxCellOverlay(img, + '<font color=red>'+warning+'</font>'); + + // Adds a handler for single mouseclicks to select the cell + if (isSelect) + { + overlay.addListener(mxEvent.CLICK, + mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.setSelectionCell(cell); + } + }) + ); + } + + // Sets and returns the overlay in the graph + return this.addCellOverlay(cell, overlay); + } + else + { + this.removeCellOverlays(cell); + } + + return null; +}; + +/** + * Group: In-place editing + */ + +/** + * Function: startEditing + * + * Calls <startEditingAtCell> using the given cell or the first selection + * cell. + * + * Parameters: + * + * evt - Optional mouse event that triggered the editing. + */ +mxGraph.prototype.startEditing = function(evt) +{ + this.startEditingAtCell(null, evt); +}; + +/** + * Function: startEditingAtCell + * + * Fires a <startEditing> event and invokes <mxCellEditor.startEditing> + * on <editor>. + * + * Parameters: + * + * cell - <mxCell> to start the in-place editor for. + * evt - Optional mouse event that triggered the editing. + */ +mxGraph.prototype.startEditingAtCell = function(cell, evt) +{ + if (cell == null) + { + cell = this.getSelectionCell(); + + if (cell != null && !this.isCellEditable(cell)) + { + cell = null; + } + } + + if (cell != null) + { + this.fireEvent(new mxEventObject(mxEvent.START_EDITING, + 'cell', cell, 'event', evt)); + this.cellEditor.startEditing(cell, evt); + } +}; + +/** + * Function: getEditingValue + * + * Returns the initial value for in-place editing. This implementation + * returns <convertValueToString> for the given cell. If this function is + * overridden, then <mxGraphModel.valueForCellChanged> should take care + * of correctly storing the actual new value inside the user object. + * + * Parameters: + * + * cell - <mxCell> for which the initial editing value should be returned. + * evt - Optional mouse event that triggered the editor. + */ +mxGraph.prototype.getEditingValue = function(cell, evt) +{ + return this.convertValueToString(cell); +}; + +/** + * Function: stopEditing + * + * Stops the current editing. + * + * Parameters: + * + * cancel - Boolean that specifies if the current editing value + * should be stored. + */ +mxGraph.prototype.stopEditing = function(cancel) +{ + this.cellEditor.stopEditing(cancel); +}; + +/** + * Function: labelChanged + * + * Sets the label of the specified cell to the given value using + * <cellLabelChanged> and fires <mxEvent.LABEL_CHANGED> while the + * transaction is in progress. Returns the cell whose label was changed. + * + * Parameters: + * + * cell - <mxCell> whose label should be changed. + * value - New label to be assigned. + * evt - Optional event that triggered the change. + */ +mxGraph.prototype.labelChanged = function(cell, value, evt) +{ + this.model.beginUpdate(); + try + { + this.cellLabelChanged(cell, value, this.isAutoSizeCell(cell)); + this.fireEvent(new mxEventObject(mxEvent.LABEL_CHANGED, + 'cell', cell, 'value', value, 'event', evt)); + } + finally + { + this.model.endUpdate(); + } + + return cell; +}; + +/** + * Function: cellLabelChanged + * + * Sets the new label for a cell. If autoSize is true then + * <cellSizeUpdated> will be called. + * + * In the following example, the function is extended to map changes to + * attributes in an XML node, as shown in <convertValueToString>. + * Alternatively, the handling of this can be implemented as shown in + * <mxGraphModel.valueForCellChanged> without the need to clone the + * user object. + * + * (code) + * var graphCellLabelChanged = graph.cellLabelChanged; + * graph.cellLabelChanged = function(cell, newValue, autoSize) + * { + * // Cloned for correct undo/redo + * var elt = cell.value.cloneNode(true); + * elt.setAttribute('label', newValue); + * + * newValue = elt; + * graphCellLabelChanged.apply(this, arguments); + * }; + * (end) + * + * Parameters: + * + * cell - <mxCell> whose label should be changed. + * value - New label to be assigned. + * autoSize - Boolean that specifies if <cellSizeUpdated> should be called. + */ +mxGraph.prototype.cellLabelChanged = function(cell, value, autoSize) +{ + this.model.beginUpdate(); + try + { + this.model.setValue(cell, value); + + if (autoSize) + { + this.cellSizeUpdated(cell, false); + } + } + finally + { + this.model.endUpdate(); + } +}; + +/** + * Group: Event processing + */ + +/** + * Function: escape + * + * Processes an escape keystroke. + * + * Parameters: + * + * evt - Mouseevent that represents the keystroke. + */ +mxGraph.prototype.escape = function(evt) +{ + this.stopEditing(true); + this.connectionHandler.reset(); + this.graphHandler.reset(); + + // Cancels all cell-based editing + var cells = this.getSelectionCells(); + + for (var i = 0; i < cells.length; i++) + { + var state = this.view.getState(cells[i]); + + if (state != null && state.handler != null) + { + state.handler.reset(); + } + } +}; + +/** + * Function: click + * + * Processes a singleclick on an optional cell and fires a <click> event. + * The click event is fired initially. If the graph is enabled and the + * event has not been consumed, then the cell is selected using + * <selectCellForEvent> or the selection is cleared using + * <clearSelection>. The events consumed state is set to true if the + * corresponding <mxMouseEvent> has been consumed. + * + * Parameters: + * + * me - <mxMouseEvent> that represents the single click. + */ +mxGraph.prototype.click = function(me) +{ + var evt = me.getEvent(); + var cell = me.getCell(); + var mxe = new mxEventObject(mxEvent.CLICK, 'event', evt, 'cell', cell); + + if (me.isConsumed()) + { + mxe.consume(); + } + + this.fireEvent(mxe); + + // Handles the event if it has not been consumed + if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed()) + { + if (cell != null) + { + this.selectCellForEvent(cell, evt); + } + else + { + var swimlane = null; + + if (this.isSwimlaneSelectionEnabled()) + { + // Gets the swimlane at the location (includes + // content area of swimlanes) + swimlane = this.getSwimlaneAt(me.getGraphX(), me.getGraphY()); + } + + // Selects the swimlane and consumes the event + if (swimlane != null) + { + this.selectCellForEvent(swimlane, evt); + } + + // Ignores the event if the control key is pressed + else if (!this.isToggleEvent(evt)) + { + this.clearSelection(); + } + } + } +}; + +/** + * Function: dblClick + * + * Processes a doubleclick on an optional cell and fires a <dblclick> + * event. The event is fired initially. If the graph is enabled and the + * event has not been consumed, then <edit> is called with the given + * cell. The event is ignored if no cell was specified. + * + * Example for overriding this method. + * + * (code) + * graph.dblClick = function(evt, cell) + * { + * var mxe = new mxEventObject(mxEvent.DOUBLE_CLICK, 'event', evt, 'cell', cell); + * this.fireEvent(mxe); + * + * if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed()) + * { + * mxUtils.alert('Hello, World!'); + * mxe.consume(); + * } + * } + * (end) + * + * Example listener for this event. + * + * (code) + * graph.addListener(mxEvent.DOUBLE_CLICK, function(sender, evt) + * { + * var cell = evt.getProperty('cell'); + * // do something with the cell... + * }); + * (end) + * + * Parameters: + * + * evt - Mouseevent that represents the doubleclick. + * cell - Optional <mxCell> under the mousepointer. + */ +mxGraph.prototype.dblClick = function(evt, cell) +{ + var mxe = new mxEventObject(mxEvent.DOUBLE_CLICK, 'event', evt, 'cell', cell); + this.fireEvent(mxe); + + // Handles the event if it has not been consumed + if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed() && + cell != null && this.isCellEditable(cell)) + { + this.startEditingAtCell(cell, evt); + } +}; + +/** + * Function: scrollPointToVisible + * + * Scrolls the graph to the given point, extending the graph container if + * specified. + */ +mxGraph.prototype.scrollPointToVisible = function(x, y, extend, border) +{ + if (!this.timerAutoScroll && (this.ignoreScrollbars || mxUtils.hasScrollbars(this.container))) + { + var c = this.container; + border = (border != null) ? border : 20; + + if (x >= c.scrollLeft && y >= c.scrollTop && x <= c.scrollLeft + c.clientWidth && + y <= c.scrollTop + c.clientHeight) + { + var dx = c.scrollLeft + c.clientWidth - x; + + if (dx < border) + { + var old = c.scrollLeft; + c.scrollLeft += border - dx; + + // Automatically extends the canvas size to the bottom, right + // if the event is outside of the canvas and the edge of the + // canvas has been reached. Notes: Needs fix for IE. + if (extend && old == c.scrollLeft) + { + if (this.dialect == mxConstants.DIALECT_SVG) + { + var root = this.view.getDrawPane().ownerSVGElement; + var width = this.container.scrollWidth + border - dx; + + // Updates the clipping region. This is an expensive + // operation that should not be executed too often. + root.setAttribute('width', width); + } + else + { + var width = Math.max(c.clientWidth, c.scrollWidth) + border - dx; + var canvas = this.view.getCanvas(); + canvas.style.width = width + 'px'; + } + + c.scrollLeft += border - dx; + } + } + else + { + dx = x - c.scrollLeft; + + if (dx < border) + { + c.scrollLeft -= border - dx; + } + } + + var dy = c.scrollTop + c.clientHeight - y; + + if (dy < border) + { + var old = c.scrollTop; + c.scrollTop += border - dy; + + if (old == c.scrollTop && extend) + { + if (this.dialect == mxConstants.DIALECT_SVG) + { + var root = this.view.getDrawPane().ownerSVGElement; + var height = this.container.scrollHeight + border - dy; + + // Updates the clipping region. This is an expensive + // operation that should not be executed too often. + root.setAttribute('height', height); + } + else + { + var height = Math.max(c.clientHeight, c.scrollHeight) + border - dy; + var canvas = this.view.getCanvas(); + canvas.style.height = height + 'px'; + } + + c.scrollTop += border - dy; + } + } + else + { + dy = y - c.scrollTop; + + if (dy < border) + { + c.scrollTop -= border - dy; + } + } + } + } + else if (this.allowAutoPanning && !this.panningHandler.active) + { + if (this.panningManager == null) + { + this.panningManager = this.createPanningManager(); + } + + this.panningManager.panTo(x + this.panDx, y + this.panDy); + } +}; + + +/** + * Function: createPanningManager + * + * Creates and returns an <mxPanningManager>. + */ +mxGraph.prototype.createPanningManager = function() +{ + return new mxPanningManager(this); +}; + +/** + * Function: getBorderSizes + * + * Returns the size of the border and padding on all four sides of the + * container. The left, top, right and bottom borders are stored in the x, y, + * width and height of the returned <mxRectangle>, respectively. + */ +mxGraph.prototype.getBorderSizes = function() +{ + // Helper function to handle string values for border widths (approx) + function parseBorder(value) + { + var result = 0; + + if (value == 'thin') + { + result = 2; + } + else if (value == 'medium') + { + result = 4; + } + else if (value == 'thick') + { + result = 6; + } + else + { + result = parseInt(value); + } + + if (isNaN(result)) + { + result = 0; + } + + return result; + } + + var style = mxUtils.getCurrentStyle(this.container); + var result = new mxRectangle(); + result.x = parseBorder(style.borderLeftWidth) + parseInt(style.paddingLeft || 0); + result.y = parseBorder(style.borderTopWidth) + parseInt(style.paddingTop || 0); + result.width = parseBorder(style.borderRightWidth) + parseInt(style.paddingRight || 0); + result.height = parseBorder(style.borderBottomWidth) + parseInt(style.paddingBottom || 0); + + return result; +}; + + +/** + * Function: getPreferredPageSize + * + * Returns the preferred size of the background page if <preferPageSize> is true. + */ +mxGraph.prototype.getPreferredPageSize = function(bounds, width, height) +{ + var scale = this.view.scale; + var tr = this.view.translate; + var fmt = this.pageFormat; + var ps = scale * this.pageScale; + var page = new mxRectangle(0, 0, fmt.width * ps, fmt.height * ps); + + var hCount = (this.pageBreaksVisible) ? Math.ceil(width / page.width) : 1; + var vCount = (this.pageBreaksVisible) ? Math.ceil(height / page.height) : 1; + + return new mxRectangle(0, 0, hCount * page.width + 2 + tr.x / scale, vCount * page.height + 2 + tr.y / scale); +}; + +/** + * Function: sizeDidChange + * + * Called when the size of the graph has changed. This implementation fires + * a <size> event after updating the clipping region of the SVG element in + * SVG-bases browsers. + */ +mxGraph.prototype.sizeDidChange = function() +{ + var bounds = this.getGraphBounds(); + + if (this.container != null) + { + var border = this.getBorder(); + + var width = Math.max(0, bounds.x + bounds.width + 1 + border); + var height = Math.max(0, bounds.y + bounds.height + 1 + border); + + if (this.minimumContainerSize != null) + { + width = Math.max(width, this.minimumContainerSize.width); + height = Math.max(height, this.minimumContainerSize.height); + } + + if (this.resizeContainer) + { + this.doResizeContainer(width, height); + } + + if (this.preferPageSize || (!mxClient.IS_IE && this.pageVisible)) + { + var size = this.getPreferredPageSize(bounds, width, height); + + if (size != null) + { + width = size.width; + height = size.height; + } + } + + if (this.minimumGraphSize != null) + { + width = Math.max(width, this.minimumGraphSize.width * this.view.scale); + height = Math.max(height, this.minimumGraphSize.height * this.view.scale); + } + + width = Math.ceil(width - 1); + height = Math.ceil(height - 1); + + if (this.dialect == mxConstants.DIALECT_SVG) + { + var root = this.view.getDrawPane().ownerSVGElement; + + root.style.minWidth = Math.max(1, width) + 'px'; + root.style.minHeight = Math.max(1, height) + 'px'; + } + else + { + if (mxClient.IS_QUIRKS) + { + // Quirks mode has no minWidth/minHeight support + this.view.updateHtmlCanvasSize(Math.max(1, width), Math.max(1, height)); + } + else + { + this.view.canvas.style.minWidth = Math.max(1, width) + 'px'; + this.view.canvas.style.minHeight = Math.max(1, height) + 'px'; + } + } + + this.updatePageBreaks(this.pageBreaksVisible, width - 1, height - 1); + } + + this.fireEvent(new mxEventObject(mxEvent.SIZE, 'bounds', bounds)); +}; + +/** + * Function: doResizeContainer + * + * Resizes the container for the given graph width and height. + */ +mxGraph.prototype.doResizeContainer = function(width, height) +{ + // Fixes container size for different box models + if (mxClient.IS_IE) + { + if (mxClient.IS_QUIRKS) + { + var borders = this.getBorderSizes(); + + // max(2, ...) required for native IE8 in quirks mode + width += Math.max(2, borders.x + borders.width + 1); + height += Math.max(2, borders.y + borders.height + 1); + } + else if (document.documentMode >= 9) + { + width += 3; + height += 5; + } + else + { + width += 1; + height += 1; + } + } + else + { + height += 1; + } + + if (this.maximumContainerSize != null) + { + width = Math.min(this.maximumContainerSize.width, width); + height = Math.min(this.maximumContainerSize.height, height); + } + + this.container.style.width = Math.ceil(width) + 'px'; + this.container.style.height = Math.ceil(height) + 'px'; +}; + +/** + * Function: redrawPageBreaks + * + * Invokes from <sizeDidChange> to redraw the page breaks. + * + * Parameters: + * + * visible - Boolean that specifies if page breaks should be shown. + * width - Specifies the width of the container in pixels. + * height - Specifies the height of the container in pixels. + */ +mxGraph.prototype.updatePageBreaks = function(visible, width, height) +{ + var scale = this.view.scale; + var tr = this.view.translate; + var fmt = this.pageFormat; + var ps = scale * this.pageScale; + var bounds = new mxRectangle(scale * tr.x, scale * tr.y, + fmt.width * ps, fmt.height * ps); + + // Does not show page breaks if the scale is too small + visible = visible && Math.min(bounds.width, bounds.height) > this.minPageBreakDist; + + // Draws page breaks independent of translate. To ignore + // the translate set bounds.x/y = 0. Note that modulo + // in JavaScript has a bug, so use mxUtils instead. + bounds.x = mxUtils.mod(bounds.x, bounds.width); + bounds.y = mxUtils.mod(bounds.y, bounds.height); + + var horizontalCount = (visible) ? Math.ceil((width - bounds.x) / bounds.width) : 0; + var verticalCount = (visible) ? Math.ceil((height - bounds.y) / bounds.height) : 0; + var right = width; + var bottom = height; + + if (this.horizontalPageBreaks == null && horizontalCount > 0) + { + this.horizontalPageBreaks = []; + } + + if (this.horizontalPageBreaks != null) + { + for (var i = 0; i <= horizontalCount; i++) + { + var pts = [new mxPoint(bounds.x + i * bounds.width, 1), + new mxPoint(bounds.x + i * bounds.width, bottom)]; + + if (this.horizontalPageBreaks[i] != null) + { + this.horizontalPageBreaks[i].scale = 1; + this.horizontalPageBreaks[i].points = pts; + this.horizontalPageBreaks[i].redraw(); + } + else + { + var pageBreak = new mxPolyline(pts, this.pageBreakColor, this.scale); + pageBreak.dialect = this.dialect; + pageBreak.isDashed = this.pageBreakDashed; + pageBreak.scale = scale; + pageBreak.crisp = true; + pageBreak.init(this.view.backgroundPane); + pageBreak.redraw(); + + this.horizontalPageBreaks[i] = pageBreak; + } + } + + for (var i = horizontalCount; i < this.horizontalPageBreaks.length; i++) + { + this.horizontalPageBreaks[i].destroy(); + } + + this.horizontalPageBreaks.splice(horizontalCount, this.horizontalPageBreaks.length - horizontalCount); + } + + if (this.verticalPageBreaks == null && verticalCount > 0) + { + this.verticalPageBreaks = []; + } + + if (this.verticalPageBreaks != null) + { + for (var i = 0; i <= verticalCount; i++) + { + var pts = [new mxPoint(1, bounds.y + i * bounds.height), + new mxPoint(right, bounds.y + i * bounds.height)]; + + if (this.verticalPageBreaks[i] != null) + { + this.verticalPageBreaks[i].scale = 1; + this.verticalPageBreaks[i].points = pts; + this.verticalPageBreaks[i].redraw(); + } + else + { + var pageBreak = new mxPolyline(pts, this.pageBreakColor, scale); + pageBreak.dialect = this.dialect; + pageBreak.isDashed = this.pageBreakDashed; + pageBreak.scale = scale; + pageBreak.crisp = true; + pageBreak.init(this.view.backgroundPane); + pageBreak.redraw(); + + this.verticalPageBreaks[i] = pageBreak; + } + } + + for (var i = verticalCount; i < this.verticalPageBreaks.length; i++) + { + this.verticalPageBreaks[i].destroy(); + } + + this.verticalPageBreaks.splice(verticalCount, this.verticalPageBreaks.length - verticalCount); + } +}; + +/** + * Group: Cell styles + */ + +/** + * Function: getCellStyle + * + * Returns an array of key, value pairs representing the cell style for the + * given cell. If no string is defined in the model that specifies the + * style, then the default style for the cell is returned or <EMPTY_ARRAY>, + * if not style can be found. Note: You should try and get the cell state + * for the given cell and use the cached style in the state before using + * this method. + * + * Parameters: + * + * cell - <mxCell> whose style should be returned as an array. + */ +mxGraph.prototype.getCellStyle = function(cell) +{ + var stylename = this.model.getStyle(cell); + var style = null; + + // Gets the default style for the cell + if (this.model.isEdge(cell)) + { + style = this.stylesheet.getDefaultEdgeStyle(); + } + else + { + style = this.stylesheet.getDefaultVertexStyle(); + } + + // Resolves the stylename using the above as the default + if (stylename != null) + { + style = this.postProcessCellStyle(this.stylesheet.getCellStyle(stylename, style)); + } + + // Returns a non-null value if no style can be found + if (style == null) + { + style = mxGraph.prototype.EMPTY_ARRAY; + } + + return style; +}; + +/** + * Function: postProcessCellStyle + * + * Tries to resolve the value for the image style in the image bundles and + * turns short data URIs as defined in mxImageBundle to data URIs as + * defined in RFC 2397 of the IETF. + */ +mxGraph.prototype.postProcessCellStyle = function(style) +{ + if (style != null) + { + var key = style[mxConstants.STYLE_IMAGE]; + var image = this.getImageFromBundles(key); + + if (image != null) + { + style[mxConstants.STYLE_IMAGE] = image; + } + else + { + image = key; + } + + // Converts short data uris to normal data uris + if (image != null && image.substring(0, 11) == "data:image/") + { + var comma = image.indexOf(','); + + if (comma > 0) + { + image = image.substring(0, comma) + ";base64," + + image.substring(comma + 1); + } + + style[mxConstants.STYLE_IMAGE] = image; + } + } + + return style; +}; + +/** + * Function: setCellStyle + * + * Sets the style of the specified cells. If no cells are given, then the + * selection cells are changed. + * + * Parameters: + * + * style - String representing the new style of the cells. + * cells - Optional array of <mxCells> to set the style for. Default is the + * selection cells. + */ +mxGraph.prototype.setCellStyle = function(style, cells) +{ + cells = cells || this.getSelectionCells(); + + if (cells != null) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + this.model.setStyle(cells[i], style); + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: toggleCellStyle + * + * Toggles the boolean value for the given key in the style of the + * given cell. If no cell is specified then the selection cell is + * used. + * + * Parameter: + * + * key - String representing the key for the boolean value to be toggled. + * defaultValue - Optional boolean default value if no value is defined. + * Default is false. + * cell - Optional <mxCell> whose style should be modified. Default is + * the selection cell. + */ +mxGraph.prototype.toggleCellStyle = function(key, defaultValue, cell) +{ + cell = cell || this.getSelectionCell(); + + this.toggleCellStyles(key, defaultValue, [cell]); +}; + +/** + * Function: toggleCellStyles + * + * Toggles the boolean value for the given key in the style of the given + * cells. If no cells are specified, then the selection cells are used. For + * example, this can be used to toggle <mxConstants.STYLE_ROUNDED> or any + * other style with a boolean value. + * + * Parameter: + * + * key - String representing the key for the boolean value to be toggled. + * defaultValue - Optional boolean default value if no value is defined. + * Default is false. + * cells - Optional array of <mxCells> whose styles should be modified. + * Default is the selection cells. + */ +mxGraph.prototype.toggleCellStyles = function(key, defaultValue, cells) +{ + defaultValue = (defaultValue != null) ? defaultValue : false; + cells = cells || this.getSelectionCells(); + + if (cells != null && cells.length > 0) + { + var state = this.view.getState(cells[0]); + var style = (state != null) ? state.style : this.getCellStyle(cells[0]); + + if (style != null) + { + var val = (mxUtils.getValue(style, key, defaultValue)) ? 0 : 1; + this.setCellStyles(key, val, cells); + } + } +}; + +/** + * Function: setCellStyles + * + * Sets the key to value in the styles of the given cells. This will modify + * the existing cell styles in-place and override any existing assignment + * for the given key. If no cells are specified, then the selection cells + * are changed. If no value is specified, then the respective key is + * removed from the styles. + * + * Parameters: + * + * key - String representing the key to be assigned. + * value - String representing the new value for the key. + * cells - Optional array of <mxCells> to change the style for. Default is + * the selection cells. + */ +mxGraph.prototype.setCellStyles = function(key, value, cells) +{ + cells = cells || this.getSelectionCells(); + mxUtils.setCellStyles(this.model, cells, key, value); +}; + +/** + * Function: toggleCellStyleFlags + * + * Toggles the given bit for the given key in the styles of the specified + * cells. + * + * Parameters: + * + * key - String representing the key to toggle the flag in. + * flag - Integer that represents the bit to be toggled. + * cells - Optional array of <mxCells> to change the style for. Default is + * the selection cells. + */ +mxGraph.prototype.toggleCellStyleFlags = function(key, flag, cells) +{ + this.setCellStyleFlags(key, flag, null, cells); +}; + +/** + * Function: setCellStyleFlags + * + * Sets or toggles the given bit for the given key in the styles of the + * specified cells. + * + * Parameters: + * + * key - String representing the key to toggle the flag in. + * flag - Integer that represents the bit to be toggled. + * value - Boolean value to be used or null if the value should be toggled. + * cells - Optional array of <mxCells> to change the style for. Default is + * the selection cells. + */ +mxGraph.prototype.setCellStyleFlags = function(key, flag, value, cells) +{ + cells = cells || this.getSelectionCells(); + + if (cells != null && cells.length > 0) + { + if (value == null) + { + var state = this.view.getState(cells[0]); + var style = (state != null) ? state.style : this.getCellStyle(cells[0]); + + if (style != null) + { + var current = parseInt(style[key] || 0); + value = !((current & flag) == flag); + } + } + + mxUtils.setCellStyleFlags(this.model, cells, key, flag, value); + } +}; + +/** + * Group: Cell alignment and orientation + */ + +/** + * Function: alignCells + * + * Aligns the given cells vertically or horizontally according to the given + * alignment using the optional parameter as the coordinate. + * + * Parameters: + * + * align - Specifies the alignment. Possible values are all constants in + * mxConstants with an ALIGN prefix. + * cells - Array of <mxCells> to be aligned. + * param - Optional coordinate for the alignment. + */ +mxGraph.prototype.alignCells = function(align, cells, param) +{ + if (cells == null) + { + cells = this.getSelectionCells(); + } + + if (cells != null && cells.length > 1) + { + // Finds the required coordinate for the alignment + if (param == null) + { + for (var i = 0; i < cells.length; i++) + { + var geo = this.getCellGeometry(cells[i]); + + if (geo != null && !this.model.isEdge(cells[i])) + { + if (param == null) + { + if (align == mxConstants.ALIGN_CENTER) + { + param = geo.x + geo.width / 2; + break; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + param = geo.x + geo.width; + } + else if (align == mxConstants.ALIGN_TOP) + { + param = geo.y; + } + else if (align == mxConstants.ALIGN_MIDDLE) + { + param = geo.y + geo.height / 2; + break; + } + else if (align == mxConstants.ALIGN_BOTTOM) + { + param = geo.y + geo.height; + } + else + { + param = geo.x; + } + } + else + { + if (align == mxConstants.ALIGN_RIGHT) + { + param = Math.max(param, geo.x + geo.width); + } + else if (align == mxConstants.ALIGN_TOP) + { + param = Math.min(param, geo.y); + } + else if (align == mxConstants.ALIGN_BOTTOM) + { + param = Math.max(param, geo.y + geo.height); + } + else + { + param = Math.min(param, geo.x); + } + } + } + } + } + + // Aligns the cells to the coordinate + if (param != null) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var geo = this.getCellGeometry(cells[i]); + + if (geo != null && !this.model.isEdge(cells[i])) + { + geo = geo.clone(); + + if (align == mxConstants.ALIGN_CENTER) + { + geo.x = param - geo.width / 2; + } + else if (align == mxConstants.ALIGN_RIGHT) + { + geo.x = param - geo.width; + } + else if (align == mxConstants.ALIGN_TOP) + { + geo.y = param; + } + else if (align == mxConstants.ALIGN_MIDDLE) + { + geo.y = param - geo.height / 2; + } + else if (align == mxConstants.ALIGN_BOTTOM) + { + geo.y = param - geo.height; + } + else + { + geo.x = param; + } + + this.model.setGeometry(cells[i], geo); + } + } + + this.fireEvent(new mxEventObject(mxEvent.ALIGN_CELLS, + 'align', align, 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + } + } + + return cells; +}; + +/** + * Function: flipEdge + * + * Toggles the style of the given edge between null (or empty) and + * <alternateEdgeStyle>. This method fires <mxEvent.FLIP_EDGE> while the + * transaction is in progress. Returns the edge that was flipped. + * + * Here is an example that overrides this implementation to invert the + * value of <mxConstants.STYLE_ELBOW> without removing any existing styles. + * + * (code) + * graph.flipEdge = function(edge) + * { + * if (edge != null) + * { + * var state = this.view.getState(edge); + * var style = (state != null) ? state.style : this.getCellStyle(edge); + * + * if (style != null) + * { + * var elbow = mxUtils.getValue(style, mxConstants.STYLE_ELBOW, + * mxConstants.ELBOW_HORIZONTAL); + * var value = (elbow == mxConstants.ELBOW_HORIZONTAL) ? + * mxConstants.ELBOW_VERTICAL : mxConstants.ELBOW_HORIZONTAL; + * this.setCellStyles(mxConstants.STYLE_ELBOW, value, [edge]); + * } + * } + * }; + * (end) + * + * Parameters: + * + * edge - <mxCell> whose style should be changed. + */ +mxGraph.prototype.flipEdge = function(edge) +{ + if (edge != null && + this.alternateEdgeStyle != null) + { + this.model.beginUpdate(); + try + { + var style = this.model.getStyle(edge); + + if (style == null || style.length == 0) + { + this.model.setStyle(edge, this.alternateEdgeStyle); + } + else + { + this.model.setStyle(edge, null); + } + + // Removes all existing control points + this.resetEdge(edge); + this.fireEvent(new mxEventObject(mxEvent.FLIP_EDGE, 'edge', edge)); + } + finally + { + this.model.endUpdate(); + } + } + + return edge; +}; + +/** + * Function: addImageBundle + * + * Adds the specified <mxImageBundle>. + */ +mxGraph.prototype.addImageBundle = function(bundle) +{ + this.imageBundles.push(bundle); +}; + +/** + * Function: removeImageBundle + * + * Removes the specified <mxImageBundle>. + */ +mxGraph.prototype.removeImageBundle = function(bundle) +{ + var tmp = []; + + for (var i = 0; i < this.imageBundles.length; i++) + { + if (this.imageBundles[i] != bundle) + { + tmp.push(this.imageBundles[i]); + } + } + + this.imageBundles = tmp; +}; + +/** + * Function: getImageFromBundles + * + * Searches all <imageBundles> for the specified key and returns the value + * for the first match or null if the key is not found. + */ +mxGraph.prototype.getImageFromBundles = function(key) +{ + if (key != null) + { + for (var i = 0; i < this.imageBundles.length; i++) + { + var image = this.imageBundles[i].getImage(key); + + if (image != null) + { + return image; + } + } + } + + return null; +}; + +/** + * Group: Order + */ + +/** + * Function: orderCells + * + * Moves the given cells to the front or back. The change is carried out + * using <cellsOrdered>. This method fires <mxEvent.ORDER_CELLS> while the + * transaction is in progress. + * + * Parameters: + * + * back - Boolean that specifies if the cells should be moved to back. + * cells - Array of <mxCells> to move to the background. If null is + * specified then the selection cells are used. + */ + mxGraph.prototype.orderCells = function(back, cells) + { + if (cells == null) + { + cells = mxUtils.sortCells(this.getSelectionCells(), true); + } + + this.model.beginUpdate(); + try + { + this.cellsOrdered(cells, back); + this.fireEvent(new mxEventObject(mxEvent.ORDER_CELLS, + 'back', back, 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + + return cells; + }; + +/** + * Function: cellsOrdered + * + * Moves the given cells to the front or back. This method fires + * <mxEvent.CELLS_ORDERED> while the transaction is in progress. + * + * Parameters: + * + * cells - Array of <mxCells> whose order should be changed. + * back - Boolean that specifies if the cells should be moved to back. + */ + mxGraph.prototype.cellsOrdered = function(cells, back) + { + if (cells != null) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var parent = this.model.getParent(cells[i]); + + if (back) + { + this.model.add(parent, cells[i], i); + } + else + { + this.model.add(parent, cells[i], + this.model.getChildCount(parent) - 1); + } + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_ORDERED, + 'back', back, 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Group: Grouping + */ + +/** + * Function: groupCells + * + * Adds the cells into the given group. The change is carried out using + * <cellsAdded>, <cellsMoved> and <cellsResized>. This method fires + * <mxEvent.GROUP_CELLS> while the transaction is in progress. Returns the + * new group. A group is only created if there is at least one entry in the + * given array of cells. + * + * Parameters: + * + * group - <mxCell> that represents the target group. If null is specified + * then a new group is created using <createGroupCell>. + * border - Optional integer that specifies the border between the child + * area and the group bounds. Default is 0. + * cells - Optional array of <mxCells> to be grouped. If null is specified + * then the selection cells are used. + */ +mxGraph.prototype.groupCells = function(group, border, cells) +{ + if (cells == null) + { + cells = mxUtils.sortCells(this.getSelectionCells(), true); + } + + cells = this.getCellsForGroup(cells); + + if (group == null) + { + group = this.createGroupCell(cells); + } + + var bounds = this.getBoundsForGroup(group, cells, border); + + if (cells.length > 0 && bounds != null) + { + // Uses parent of group or previous parent of first child + var parent = this.model.getParent(group); + + if (parent == null) + { + parent = this.model.getParent(cells[0]); + } + + this.model.beginUpdate(); + try + { + // Checks if the group has a geometry and + // creates one if one does not exist + if (this.getCellGeometry(group) == null) + { + this.model.setGeometry(group, new mxGeometry()); + } + + // Adds the children into the group and moves + var index = this.model.getChildCount(group); + this.cellsAdded(cells, group, index, null, null, false, false); + this.cellsMoved(cells, -bounds.x, -bounds.y, false, true); + + // Adds the group into the parent and resizes + index = this.model.getChildCount(parent); + this.cellsAdded([group], parent, index, null, null, false); + this.cellsResized([group], [bounds]); + + this.fireEvent(new mxEventObject(mxEvent.GROUP_CELLS, + 'group', group, 'border', border, 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + } + + return group; +}; + +/** + * Function: getCellsForGroup + * + * Returns the cells with the same parent as the first cell + * in the given array. + */ +mxGraph.prototype.getCellsForGroup = function(cells) +{ + var result = []; + + if (cells != null && cells.length > 0) + { + var parent = this.model.getParent(cells[0]); + result.push(cells[0]); + + // Filters selection cells with the same parent + for (var i = 1; i < cells.length; i++) + { + if (this.model.getParent(cells[i]) == parent) + { + result.push(cells[i]); + } + } + } + + return result; +}; + +/** + * Function: getBoundsForGroup + * + * Returns the bounds to be used for the given group and children. + */ +mxGraph.prototype.getBoundsForGroup = function(group, children, border) +{ + var result = this.getBoundingBoxFromGeometry(children); + + if (result != null) + { + if (this.isSwimlane(group)) + { + var size = this.getStartSize(group); + + result.x -= size.width; + result.y -= size.height; + result.width += size.width; + result.height += size.height; + } + + // Adds the border + result.x -= border; + result.y -= border; + result.width += 2 * border; + result.height += 2 * border; + } + + return result; +}; + +/** + * Function: createGroupCell + * + * Hook for creating the group cell to hold the given array of <mxCells> if + * no group cell was given to the <group> function. + * + * The following code can be used to set the style of new group cells. + * + * (code) + * var graphCreateGroupCell = graph.createGroupCell; + * graph.createGroupCell = function(cells) + * { + * var group = graphCreateGroupCell.apply(this, arguments); + * group.setStyle('group'); + * + * return group; + * }; + */ +mxGraph.prototype.createGroupCell = function(cells) +{ + var group = new mxCell(''); + group.setVertex(true); + group.setConnectable(false); + + return group; +}; + +/** + * Function: ungroupCells + * + * Ungroups the given cells by moving the children the children to their + * parents parent and removing the empty groups. Returns the children that + * have been removed from the groups. + * + * Parameters: + * + * cells - Array of cells to be ungrouped. If null is specified then the + * selection cells are used. + */ +mxGraph.prototype.ungroupCells = function(cells) +{ + var result = []; + + if (cells == null) + { + cells = this.getSelectionCells(); + + // Finds the cells with children + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + if (this.model.getChildCount(cells[i]) > 0) + { + tmp.push(cells[i]); + } + } + + cells = tmp; + } + + if (cells != null && cells.length > 0) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var children = this.model.getChildren(cells[i]); + + if (children != null && children.length > 0) + { + children = children.slice(); + var parent = this.model.getParent(cells[i]); + var index = this.model.getChildCount(parent); + + this.cellsAdded(children, parent, index, null, null, true); + result = result.concat(children); + } + } + + this.cellsRemoved(this.addAllEdges(cells)); + this.fireEvent(new mxEventObject(mxEvent.UNGROUP_CELLS, + 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + } + + return result; +}; + +/** + * Function: removeCellsFromParent + * + * Removes the specified cells from their parents and adds them to the + * default parent. Returns the cells that were removed from their parents. + * + * Parameters: + * + * cells - Array of <mxCells> to be removed from their parents. + */ +mxGraph.prototype.removeCellsFromParent = function(cells) +{ + if (cells == null) + { + cells = this.getSelectionCells(); + } + + this.model.beginUpdate(); + try + { + var parent = this.getDefaultParent(); + var index = this.model.getChildCount(parent); + + this.cellsAdded(cells, parent, index, null, null, true); + this.fireEvent(new mxEventObject(mxEvent.REMOVE_CELLS_FROM_PARENT, + 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: updateGroupBounds + * + * Updates the bounds of the given array of groups so that it includes + * all child vertices. + * + * Parameters: + * + * cells - The groups whose bounds should be updated. + * border - Optional border to be added in the group. Default is 0. + * moveGroup - Optional boolean that allows the group to be moved. Default + * is false. + */ +mxGraph.prototype.updateGroupBounds = function(cells, border, moveGroup) +{ + if (cells == null) + { + cells = this.getSelectionCells(); + } + + border = (border != null) ? border : 0; + moveGroup = (moveGroup != null) ? moveGroup : false; + + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var geo = this.getCellGeometry(cells[i]); + + if (geo != null) + { + var children = this.getChildCells(cells[i]); + + if (children != null && children.length > 0) + { + var childBounds = this.getBoundingBoxFromGeometry(children); + + if (childBounds.width > 0 && childBounds.height > 0) + { + var size = (this.isSwimlane(cells[i])) ? + this.getStartSize(cells[i]) : new mxRectangle(); + + geo = geo.clone(); + + if (moveGroup) + { + geo.x += childBounds.x - size.width - border; + geo.y += childBounds.y - size.height - border; + } + + geo.width = childBounds.width + size.width + 2 * border; + geo.height = childBounds.height + size.height + 2 * border; + + this.model.setGeometry(cells[i], geo); + this.moveCells(children, -childBounds.x + size.width + border, + -childBounds.y + size.height + border); + } + } + } + } + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Group: Cell cloning, insertion and removal + */ + +/** + * Function: cloneCells + * + * Returns the clones for the given cells. If the terminal of an edge is + * not in the given array, then the respective end is assigned a terminal + * point and the terminal is removed. + * + * Parameters: + * + * cells - Array of <mxCells> to be cloned. + * allowInvalidEdges - Optional boolean that specifies if invalid edges + * should be cloned. Default is true. + */ +mxGraph.prototype.cloneCells = function(cells, allowInvalidEdges) +{ + allowInvalidEdges = (allowInvalidEdges != null) ? allowInvalidEdges : true; + var clones = null; + + if (cells != null) + { + // Creates a hashtable for cell lookups + var hash = new Object(); + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + var id = mxCellPath.create(cells[i]); + hash[id] = cells[i]; + tmp.push(cells[i]); + } + + if (tmp.length > 0) + { + var scale = this.view.scale; + var trans = this.view.translate; + clones = this.model.cloneCells(cells, true); + + for (var i = 0; i < cells.length; i++) + { + if (!allowInvalidEdges && this.model.isEdge(clones[i]) && + this.getEdgeValidationError(clones[i], + this.model.getTerminal(clones[i], true), + this.model.getTerminal(clones[i], false)) != null) + { + clones[i] = null; + } + else + { + var g = this.model.getGeometry(clones[i]); + + if (g != null) + { + var state = this.view.getState(cells[i]); + var pstate = this.view.getState( + this.model.getParent(cells[i])); + + if (state != null && pstate != null) + { + var dx = pstate.origin.x; + var dy = pstate.origin.y; + + if (this.model.isEdge(clones[i])) + { + var pts = state.absolutePoints; + + // Checks if the source is cloned or sets the terminal point + var src = this.model.getTerminal(cells[i], true); + var srcId = mxCellPath.create(src); + + while (src != null && hash[srcId] == null) + { + src = this.model.getParent(src); + srcId = mxCellPath.create(src); + } + + if (src == null) + { + g.setTerminalPoint( + new mxPoint(pts[0].x / scale - trans.x, + pts[0].y / scale - trans.y), true); + } + + // Checks if the target is cloned or sets the terminal point + var trg = this.model.getTerminal(cells[i], false); + var trgId = mxCellPath.create(trg); + + while (trg != null && hash[trgId] == null) + { + trg = this.model.getParent(trg); + trgId = mxCellPath.create(trg); + } + + if (trg == null) + { + var n = pts.length - 1; + g.setTerminalPoint( + new mxPoint(pts[n].x / scale - trans.x, + pts[n].y / scale - trans.y), false); + } + + // Translates the control points + var points = g.points; + + if (points != null) + { + for (var j = 0; j < points.length; j++) + { + points[j].x += dx; + points[j].y += dy; + } + } + } + else + { + g.x += dx; + g.y += dy; + } + } + } + } + } + } + else + { + clones = []; + } + } + + return clones; +}; + +/** + * Function: insertVertex + * + * Adds a new vertex into the given parent <mxCell> using value as the user + * object and the given coordinates as the <mxGeometry> of the new vertex. + * The id and style are used for the respective properties of the new + * <mxCell>, which is returned. + * + * When adding new vertices from a mouse event, one should take into + * account the offset of the graph container and the scale and translation + * of the view in order to find the correct unscaled, untranslated + * coordinates using <mxGraph.getPointForEvent> as follows: + * + * (code) + * var pt = graph.getPointForEvent(evt); + * var parent = graph.getDefaultParent(); + * graph.insertVertex(parent, null, + * 'Hello, World!', x, y, 220, 30); + * (end) + * + * For adding image cells, the style parameter can be assigned as + * + * (code) + * stylename;image=imageUrl + * (end) + * + * See <mxGraph> for more information on using images. + * + * Parameters: + * + * parent - <mxCell> that specifies the parent of the new vertex. + * id - Optional string that defines the Id of the new vertex. + * value - Object to be used as the user object. + * x - Integer that defines the x coordinate of the vertex. + * y - Integer that defines the y coordinate of the vertex. + * width - Integer that defines the width of the vertex. + * height - Integer that defines the height of the vertex. + * style - Optional string that defines the cell style. + * relative - Optional boolean that specifies if the geometry is relative. + * Default is false. + */ +mxGraph.prototype.insertVertex = function(parent, id, value, + x, y, width, height, style, relative) +{ + var vertex = this.createVertex(parent, id, value, x, y, width, height, style, relative); + + return this.addCell(vertex, parent); +}; + +/** + * Function: createVertex + * + * Hook method that creates the new vertex for <insertVertex>. + */ +mxGraph.prototype.createVertex = function(parent, id, value, + x, y, width, height, style, relative) +{ + // Creates the geometry for the vertex + var geometry = new mxGeometry(x, y, width, height); + geometry.relative = (relative != null) ? relative : false; + + // Creates the vertex + var vertex = new mxCell(value, geometry, style); + vertex.setId(id); + vertex.setVertex(true); + vertex.setConnectable(true); + + return vertex; +}; + +/** + * Function: insertEdge + * + * Adds a new edge into the given parent <mxCell> using value as the user + * object and the given source and target as the terminals of the new edge. + * The id and style are used for the respective properties of the new + * <mxCell>, which is returned. + * + * Parameters: + * + * parent - <mxCell> that specifies the parent of the new edge. + * id - Optional string that defines the Id of the new edge. + * value - JavaScript object to be used as the user object. + * source - <mxCell> that defines the source of the edge. + * target - <mxCell> that defines the target of the edge. + * style - Optional string that defines the cell style. + */ +mxGraph.prototype.insertEdge = function(parent, id, value, source, target, style) +{ + var edge = this.createEdge(parent, id, value, source, target, style); + + return this.addEdge(edge, parent, source, target); +}; + +/** + * Function: createEdge + * + * Hook method that creates the new edge for <insertEdge>. This + * implementation does not set the source and target of the edge, these + * are set when the edge is added to the model. + * + */ +mxGraph.prototype.createEdge = function(parent, id, value, source, target, style) +{ + // Creates the edge + var edge = new mxCell(value, new mxGeometry(), style); + edge.setId(id); + edge.setEdge(true); + edge.geometry.relative = true; + + return edge; +}; + +/** + * Function: addEdge + * + * Adds the edge to the parent and connects it to the given source and + * target terminals. This is a shortcut method. Returns the edge that was + * added. + * + * Parameters: + * + * edge - <mxCell> to be inserted into the given parent. + * parent - <mxCell> that represents the new parent. If no parent is + * given then the default parent is used. + * source - Optional <mxCell> that represents the source terminal. + * target - Optional <mxCell> that represents the target terminal. + * index - Optional index to insert the cells at. Default is to append. + */ +mxGraph.prototype.addEdge = function(edge, parent, source, target, index) +{ + return this.addCell(edge, parent, index, source, target); +}; + +/** + * Function: addCell + * + * Adds the cell to the parent and connects it to the given source and + * target terminals. This is a shortcut method. Returns the cell that was + * added. + * + * Parameters: + * + * cell - <mxCell> to be inserted into the given parent. + * parent - <mxCell> that represents the new parent. If no parent is + * given then the default parent is used. + * index - Optional index to insert the cells at. Default is to append. + * source - Optional <mxCell> that represents the source terminal. + * target - Optional <mxCell> that represents the target terminal. + */ +mxGraph.prototype.addCell = function(cell, parent, index, source, target) +{ + return this.addCells([cell], parent, index, source, target)[0]; +}; + +/** + * Function: addCells + * + * Adds the cells to the parent at the given index, connecting each cell to + * the optional source and target terminal. The change is carried out using + * <cellsAdded>. This method fires <mxEvent.ADD_CELLS> while the + * transaction is in progress. Returns the cells that were added. + * + * Parameters: + * + * cells - Array of <mxCells> to be inserted. + * parent - <mxCell> that represents the new parent. If no parent is + * given then the default parent is used. + * index - Optional index to insert the cells at. Default is to append. + * source - Optional source <mxCell> for all inserted cells. + * target - Optional target <mxCell> for all inserted cells. + */ +mxGraph.prototype.addCells = function(cells, parent, index, source, target) +{ + if (parent == null) + { + parent = this.getDefaultParent(); + } + + if (index == null) + { + index = this.model.getChildCount(parent); + } + + this.model.beginUpdate(); + try + { + this.cellsAdded(cells, parent, index, source, target, false, true); + this.fireEvent(new mxEventObject(mxEvent.ADD_CELLS, 'cells', cells, + 'parent', parent, 'index', index, 'source', source, 'target', target)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: cellsAdded + * + * Adds the specified cells to the given parent. This method fires + * <mxEvent.CELLS_ADDED> while the transaction is in progress. + */ +mxGraph.prototype.cellsAdded = function(cells, parent, index, source, target, absolute, constrain) +{ + if (cells != null && parent != null && index != null) + { + this.model.beginUpdate(); + try + { + var parentState = (absolute) ? this.view.getState(parent) : null; + var o1 = (parentState != null) ? parentState.origin : null; + var zero = new mxPoint(0, 0); + + for (var i = 0; i < cells.length; i++) + { + if (cells[i] == null) + { + index--; + } + else + { + var previous = this.model.getParent(cells[i]); + + // Keeps the cell at its absolute location + if (o1 != null && cells[i] != parent && parent != previous) + { + var oldState = this.view.getState(previous); + var o2 = (oldState != null) ? oldState.origin : zero; + var geo = this.model.getGeometry(cells[i]); + + if (geo != null) + { + var dx = o2.x - o1.x; + var dy = o2.y - o1.y; + + // FIXME: Cells should always be inserted first before any other edit + // to avoid forward references in sessions. + geo = geo.clone(); + geo.translate(dx, dy); + + if (!geo.relative && this.model.isVertex(cells[i]) && + !this.isAllowNegativeCoordinates()) + { + geo.x = Math.max(0, geo.x); + geo.y = Math.max(0, geo.y); + } + + this.model.setGeometry(cells[i], geo); + } + } + + // Decrements all following indices + // if cell is already in parent + if (parent == previous) + { + index--; + } + + this.model.add(parent, cells[i], index + i); + + // Extends the parent + if (this.isExtendParentsOnAdd() && this.isExtendParent(cells[i])) + { + this.extendParent(cells[i]); + } + + // Constrains the child + if (constrain == null || constrain) + { + this.constrainChild(cells[i]); + } + + // Sets the source terminal + if (source != null) + { + this.cellConnected(cells[i], source, true); + } + + // Sets the target terminal + if (target != null) + { + this.cellConnected(cells[i], target, false); + } + } + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_ADDED, 'cells', cells, + 'parent', parent, 'index', index, 'source', source, 'target', target, + 'absolute', absolute)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: removeCells + * + * Removes the given cells from the graph including all connected edges if + * includeEdges is true. The change is carried out using <cellsRemoved>. + * This method fires <mxEvent.REMOVE_CELLS> while the transaction is in + * progress. The removed cells are returned as an array. + * + * Parameters: + * + * cells - Array of <mxCells> to remove. If null is specified then the + * selection cells which are deletable are used. + * includeEdges - Optional boolean which specifies if all connected edges + * should be removed as well. Default is true. + */ +mxGraph.prototype.removeCells = function(cells, includeEdges) +{ + includeEdges = (includeEdges != null) ? includeEdges : true; + + if (cells == null) + { + cells = this.getDeletableCells(this.getSelectionCells()); + } + + // Adds all edges to the cells + if (includeEdges) + { + cells = this.getDeletableCells(this.addAllEdges(cells)); + } + + this.model.beginUpdate(); + try + { + this.cellsRemoved(cells); + this.fireEvent(new mxEventObject(mxEvent.REMOVE_CELLS, + 'cells', cells, 'includeEdges', includeEdges)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: cellsRemoved + * + * Removes the given cells from the model. This method fires + * <mxEvent.CELLS_REMOVED> while the transaction is in progress. + * + * Parameters: + * + * cells - Array of <mxCells> to remove. + */ +mxGraph.prototype.cellsRemoved = function(cells) +{ + if (cells != null && cells.length > 0) + { + var scale = this.view.scale; + var tr = this.view.translate; + + this.model.beginUpdate(); + try + { + // Creates hashtable for faster lookup + var hash = new Object(); + + for (var i = 0; i < cells.length; i++) + { + var id = mxCellPath.create(cells[i]); + hash[id] = cells[i]; + } + + for (var i = 0; i < cells.length; i++) + { + // Disconnects edges which are not in cells + var edges = this.getConnections(cells[i]); + + for (var j = 0; j < edges.length; j++) + { + var id = mxCellPath.create(edges[j]); + + if (hash[id] == null) + { + var geo = this.model.getGeometry(edges[j]); + + if (geo != null) + { + var state = this.view.getState(edges[j]); + + if (state != null) + { + geo = geo.clone(); + var source = state.getVisibleTerminal(true) == cells[i]; + var pts = state.absolutePoints; + var n = (source) ? 0 : pts.length - 1; + + geo.setTerminalPoint( + new mxPoint(pts[n].x / scale - tr.x, + pts[n].y / scale - tr.y), source); + this.model.setTerminal(edges[j], null, source); + this.model.setGeometry(edges[j], geo); + } + } + } + } + + this.model.remove(cells[i]); + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_REMOVED, + 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: splitEdge + * + * Splits the given edge by adding the newEdge between the previous source + * and the given cell and reconnecting the source of the given edge to the + * given cell. This method fires <mxEvent.SPLIT_EDGE> while the transaction + * is in progress. Returns the new edge that was inserted. + * + * Parameters: + * + * edge - <mxCell> that represents the edge to be splitted. + * cells - <mxCells> that represents the cells to insert into the edge. + * newEdge - <mxCell> that represents the edge to be inserted. + * dx - Optional integer that specifies the vector to move the cells. + * dy - Optional integer that specifies the vector to move the cells. + */ +mxGraph.prototype.splitEdge = function(edge, cells, newEdge, dx, dy) +{ + dx = dx || 0; + dy = dy || 0; + + if (newEdge == null) + { + newEdge = this.cloneCells([edge])[0]; + } + + var parent = this.model.getParent(edge); + var source = this.model.getTerminal(edge, true); + + this.model.beginUpdate(); + try + { + this.cellsMoved(cells, dx, dy, false, false); + this.cellsAdded(cells, parent, this.model.getChildCount(parent), null, null, + true); + this.cellsAdded([newEdge], parent, this.model.getChildCount(parent), + source, cells[0], false); + this.cellConnected(edge, cells[0], true); + this.fireEvent(new mxEventObject(mxEvent.SPLIT_EDGE, 'edge', edge, + 'cells', cells, 'newEdge', newEdge, 'dx', dx, 'dy', dy)); + } + finally + { + this.model.endUpdate(); + } + + return newEdge; +}; + +/** + * Group: Cell visibility + */ + +/** + * Function: toggleCells + * + * Sets the visible state of the specified cells and all connected edges + * if includeEdges is true. The change is carried out using <cellsToggled>. + * This method fires <mxEvent.TOGGLE_CELLS> while the transaction is in + * progress. Returns the cells whose visible state was changed. + * + * Parameters: + * + * show - Boolean that specifies the visible state to be assigned. + * cells - Array of <mxCells> whose visible state should be changed. If + * null is specified then the selection cells are used. + * includeEdges - Optional boolean indicating if the visible state of all + * connected edges should be changed as well. Default is true. + */ +mxGraph.prototype.toggleCells = function(show, cells, includeEdges) +{ + if (cells == null) + { + cells = this.getSelectionCells(); + } + + // Adds all connected edges recursively + if (includeEdges) + { + cells = this.addAllEdges(cells); + } + + this.model.beginUpdate(); + try + { + this.cellsToggled(cells, show); + this.fireEvent(new mxEventObject(mxEvent.TOGGLE_CELLS, + 'show', show, 'cells', cells, 'includeEdges', includeEdges)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: cellsToggled + * + * Sets the visible state of the specified cells. + * + * Parameters: + * + * cells - Array of <mxCells> whose visible state should be changed. + * show - Boolean that specifies the visible state to be assigned. + */ +mxGraph.prototype.cellsToggled = function(cells, show) +{ + if (cells != null && cells.length > 0) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + this.model.setVisible(cells[i], show); + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Group: Folding + */ + +/** + * Function: foldCells + * + * Sets the collapsed state of the specified cells and all descendants + * if recurse is true. The change is carried out using <cellsFolded>. + * This method fires <mxEvent.FOLD_CELLS> while the transaction is in + * progress. Returns the cells whose collapsed state was changed. + * + * Parameters: + * + * collapsed - Boolean indicating the collapsed state to be assigned. + * recurse - Optional boolean indicating if the collapsed state of all + * descendants should be set. Default is false. + * cells - Array of <mxCells> whose collapsed state should be set. If + * null is specified then the foldable selection cells are used. + * checkFoldable - Optional boolean indicating of isCellFoldable should be + * checked. Default is false. + */ +mxGraph.prototype.foldCells = function(collapse, recurse, cells, checkFoldable) +{ + recurse = (recurse != null) ? recurse : false; + + if (cells == null) + { + cells = this.getFoldableCells(this.getSelectionCells(), collapse); + } + + this.stopEditing(false); + + this.model.beginUpdate(); + try + { + this.cellsFolded(cells, collapse, recurse, checkFoldable); + this.fireEvent(new mxEventObject(mxEvent.FOLD_CELLS, + 'collapse', collapse, 'recurse', recurse, 'cells', cells)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: cellsFolded + * + * Sets the collapsed state of the specified cells. This method fires + * <mxEvent.CELLS_FOLDED> while the transaction is in progress. Returns the + * cells whose collapsed state was changed. + * + * Parameters: + * + * cells - Array of <mxCells> whose collapsed state should be set. + * collapsed - Boolean indicating the collapsed state to be assigned. + * recurse - Boolean indicating if the collapsed state of all descendants + * should be set. + * checkFoldable - Optional boolean indicating of isCellFoldable should be + * checked. Default is false. + */ +mxGraph.prototype.cellsFolded = function(cells, collapse, recurse, checkFoldable) +{ + if (cells != null && cells.length > 0) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if ((!checkFoldable || this.isCellFoldable(cells[i], collapse)) && + collapse != this.isCellCollapsed(cells[i])) + { + this.model.setCollapsed(cells[i], collapse); + this.swapBounds(cells[i], collapse); + + if (this.isExtendParent(cells[i])) + { + this.extendParent(cells[i]); + } + + if (recurse) + { + var children = this.model.getChildren(cells[i]); + this.foldCells(children, collapse, recurse); + } + } + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_FOLDED, + 'cells', cells, 'collapse', collapse, 'recurse', recurse)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: swapBounds + * + * Swaps the alternate and the actual bounds in the geometry of the given + * cell invoking <updateAlternateBounds> before carrying out the swap. + * + * Parameters: + * + * cell - <mxCell> for which the bounds should be swapped. + * willCollapse - Boolean indicating if the cell is going to be collapsed. + */ +mxGraph.prototype.swapBounds = function(cell, willCollapse) +{ + if (cell != null) + { + var geo = this.model.getGeometry(cell); + + if (geo != null) + { + geo = geo.clone(); + + this.updateAlternateBounds(cell, geo, willCollapse); + geo.swap(); + + this.model.setGeometry(cell, geo); + } + } +}; + +/** + * Function: updateAlternateBounds + * + * Updates or sets the alternate bounds in the given geometry for the given + * cell depending on whether the cell is going to be collapsed. If no + * alternate bounds are defined in the geometry and + * <collapseToPreferredSize> is true, then the preferred size is used for + * the alternate bounds. The top, left corner is always kept at the same + * location. + * + * Parameters: + * + * cell - <mxCell> for which the geometry is being udpated. + * g - <mxGeometry> for which the alternate bounds should be updated. + * willCollapse - Boolean indicating if the cell is going to be collapsed. + */ +mxGraph.prototype.updateAlternateBounds = function(cell, geo, willCollapse) +{ + if (cell != null && geo != null) + { + if (geo.alternateBounds == null) + { + var bounds = geo; + + if (this.collapseToPreferredSize) + { + var tmp = this.getPreferredSizeForCell(cell); + + if (tmp != null) + { + bounds = tmp; + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + var startSize = mxUtils.getValue(style, mxConstants.STYLE_STARTSIZE); + + if (startSize > 0) + { + bounds.height = Math.max(bounds.height, startSize); + } + } + } + + geo.alternateBounds = new mxRectangle( + geo.x, geo.y, bounds.width, bounds.height); + } + else + { + geo.alternateBounds.x = geo.x; + geo.alternateBounds.y = geo.y; + } + } +}; + +/** + * Function: addAllEdges + * + * Returns an array with the given cells and all edges that are connected + * to a cell or one of its descendants. + */ +mxGraph.prototype.addAllEdges = function(cells) +{ + var allCells = cells.slice(); // FIXME: Required? + allCells = allCells.concat(this.getAllEdges(cells)); + + return allCells; +}; + +/** + * Function: getAllEdges + * + * Returns all edges connected to the given cells or its descendants. + */ +mxGraph.prototype.getAllEdges = function(cells) +{ + var edges = []; + + if (cells != null) + { + for (var i = 0; i < cells.length; i++) + { + var edgeCount = this.model.getEdgeCount(cells[i]); + + for (var j = 0; j < edgeCount; j++) + { + edges.push(this.model.getEdgeAt(cells[i], j)); + } + + // Recurses + var children = this.model.getChildren(cells[i]); + edges = edges.concat(this.getAllEdges(children)); + } + } + + return edges; +}; + +/** + * Group: Cell sizing + */ + +/** + * Function: updateCellSize + * + * Updates the size of the given cell in the model using <cellSizeUpdated>. + * This method fires <mxEvent.UPDATE_CELL_SIZE> while the transaction is in + * progress. Returns the cell whose size was updated. + * + * Parameters: + * + * cell - <mxCell> whose size should be updated. + */ +mxGraph.prototype.updateCellSize = function(cell, ignoreChildren) +{ + ignoreChildren = (ignoreChildren != null) ? ignoreChildren : false; + + this.model.beginUpdate(); + try + { + this.cellSizeUpdated(cell, ignoreChildren); + this.fireEvent(new mxEventObject(mxEvent.UPDATE_CELL_SIZE, + 'cell', cell, 'ignoreChildren', ignoreChildren)); + } + finally + { + this.model.endUpdate(); + } + + return cell; +}; + +/** + * Function: cellSizeUpdated + * + * Updates the size of the given cell in the model using + * <getPreferredSizeForCell> to get the new size. + * + * Parameters: + * + * cell - <mxCell> for which the size should be changed. + */ +mxGraph.prototype.cellSizeUpdated = function(cell, ignoreChildren) +{ + if (cell != null) + { + this.model.beginUpdate(); + try + { + var size = this.getPreferredSizeForCell(cell); + var geo = this.model.getGeometry(cell); + + if (size != null && geo != null) + { + var collapsed = this.isCellCollapsed(cell); + geo = geo.clone(); + + if (this.isSwimlane(cell)) + { + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + var cellStyle = this.model.getStyle(cell); + + if (cellStyle == null) + { + cellStyle = ''; + } + + if (mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true)) + { + cellStyle = mxUtils.setStyle(cellStyle, + mxConstants.STYLE_STARTSIZE, size.height + 8); + + if (collapsed) + { + geo.height = size.height + 8; + } + + geo.width = size.width; + } + else + { + cellStyle = mxUtils.setStyle(cellStyle, + mxConstants.STYLE_STARTSIZE, size.width + 8); + + if (collapsed) + { + geo.width = size.width + 8; + } + + geo.height = size.height; + } + + this.model.setStyle(cell, cellStyle); + } + else + { + geo.width = size.width; + geo.height = size.height; + } + + if (!ignoreChildren && !collapsed) + { + var bounds = this.view.getBounds(this.model.getChildren(cell)); + + if (bounds != null) + { + var tr = this.view.translate; + var scale = this.view.scale; + + var width = (bounds.x + bounds.width) / scale - geo.x - tr.x; + var height = (bounds.y + bounds.height) / scale - geo.y - tr.y; + + geo.width = Math.max(geo.width, width); + geo.height = Math.max(geo.height, height); + } + } + + this.cellsResized([cell], [geo]); + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: getPreferredSizeForCell + * + * Returns the preferred width and height of the given <mxCell> as an + * <mxRectangle>. + * + * Parameters: + * + * cell - <mxCell> for which the preferred size should be returned. + */ +mxGraph.prototype.getPreferredSizeForCell = function(cell) +{ + var result = null; + + if (cell != null) + { + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + if (style != null && !this.model.isEdge(cell)) + { + var fontSize = style[mxConstants.STYLE_FONTSIZE] || mxConstants.DEFAULT_FONTSIZE; + var dx = 0; + var dy = 0; + + // Adds dimension of image if shape is a label + if (this.getImage(state) != null || style[mxConstants.STYLE_IMAGE] != null) + { + if (style[mxConstants.STYLE_SHAPE] == mxConstants.SHAPE_LABEL) + { + if (style[mxConstants.STYLE_VERTICAL_ALIGN] == mxConstants.ALIGN_MIDDLE) + { + dx += parseFloat(style[mxConstants.STYLE_IMAGE_WIDTH]) || mxLabel.prototype.imageSize; + } + + if (style[mxConstants.STYLE_ALIGN] != mxConstants.ALIGN_CENTER) + { + dy += parseFloat(style[mxConstants.STYLE_IMAGE_HEIGHT]) || mxLabel.prototype.imageSize; + } + } + } + + // Adds spacings + dx += 2 * (style[mxConstants.STYLE_SPACING] || 0); + dx += style[mxConstants.STYLE_SPACING_LEFT] || 0; + dx += style[mxConstants.STYLE_SPACING_RIGHT] || 0; + + dy += 2 * (style[mxConstants.STYLE_SPACING] || 0); + dy += style[mxConstants.STYLE_SPACING_TOP] || 0; + dy += style[mxConstants.STYLE_SPACING_BOTTOM] || 0; + + // Add spacing for collapse/expand icon + // LATER: Check alignment and use constants + // for image spacing + var image = this.getFoldingImage(state); + + if (image != null) + { + dx += image.width + 8; + } + + // Adds space for label + var value = this.getLabel(cell); + + if (value != null && value.length > 0) + { + if (!this.isHtmlLabel(cell)) + { + value = value.replace(/\n/g, '<br>'); + } + + var size = mxUtils.getSizeForString(value, + fontSize, style[mxConstants.STYLE_FONTFAMILY]); + var width = size.width + dx; + var height = size.height + dy; + + if (!mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true)) + { + var tmp = height; + + height = width; + width = tmp; + } + + if (this.gridEnabled) + { + width = this.snap(width + this.gridSize / 2); + height = this.snap(height + this.gridSize / 2); + } + + result = new mxRectangle(0, 0, width, height); + } + else + { + var gs2 = 4 * this.gridSize; + result = new mxRectangle(0, 0, gs2, gs2); + } + } + } + + return result; +}; + +/** + * Function: handleGesture + * + * Invokes if a gesture event has been detected on a cell state. + * + * Parameters: + * + * state - <mxCellState> which was pinched. + * evt - Object that represents the gesture event. + */ +mxGraph.prototype.handleGesture = function(state, evt) +{ + if (Math.abs(1 - evt.scale) > 0.2) + { + var scale = this.view.scale; + var tr = this.view.translate; + + var w = state.width * evt.scale; + var h = state.height * evt.scale; + var x = state.x - (w - state.width) / 2; + var y = state.y - (h - state.height) / 2; + + var bounds = new mxRectangle(this.snap(x / scale) - tr.x, + this.snap(y / scale) - tr.y, + this.snap(w / scale), this.snap(h / scale)); + this.resizeCell(state.cell, bounds); + } +}; + +/** + * Function: resizeCell + * + * Sets the bounds of the given cell using <resizeCells>. Returns the + * cell which was passed to the function. + * + * Parameters: + * + * cell - <mxCell> whose bounds should be changed. + * bounds - <mxRectangle> that represents the new bounds. + */ +mxGraph.prototype.resizeCell = function(cell, bounds) +{ + return this.resizeCells([cell], [bounds])[0]; +}; + +/** + * Function: resizeCells + * + * Sets the bounds of the given cells and fires a <mxEvent.RESIZE_CELLS> + * event while the transaction is in progress. Returns the cells which + * have been passed to the function. + * + * Parameters: + * + * cells - Array of <mxCells> whose bounds should be changed. + * bounds - Array of <mxRectangles> that represent the new bounds. + */ +mxGraph.prototype.resizeCells = function(cells, bounds) +{ + this.model.beginUpdate(); + try + { + this.cellsResized(cells, bounds); + this.fireEvent(new mxEventObject(mxEvent.RESIZE_CELLS, + 'cells', cells, 'bounds', bounds)); + } + finally + { + this.model.endUpdate(); + } + + return cells; +}; + +/** + * Function: cellsResized + * + * Sets the bounds of the given cells and fires a <mxEvent.CELLS_RESIZED> + * event. If <extendParents> is true, then the parent is extended if a + * child size is changed so that it overlaps with the parent. + * + * Parameters: + * + * cells - Array of <mxCells> whose bounds should be changed. + * bounds - Array of <mxRectangles> that represent the new bounds. + */ +mxGraph.prototype.cellsResized = function(cells, bounds) +{ + if (cells != null && bounds != null && cells.length == bounds.length) + { + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var tmp = bounds[i]; + var geo = this.model.getGeometry(cells[i]); + + if (geo != null && (geo.x != tmp.x || geo.y != tmp.y || + geo.width != tmp.width || geo.height != tmp.height)) + { + geo = geo.clone(); + + if (geo.relative) + { + var offset = geo.offset; + + if (offset != null) + { + offset.x += tmp.x - geo.x; + offset.y += tmp.y - geo.y; + } + } + else + { + geo.x = tmp.x; + geo.y = tmp.y; + } + + geo.width = tmp.width; + geo.height = tmp.height; + + if (!geo.relative && this.model.isVertex(cells[i]) && + !this.isAllowNegativeCoordinates()) + { + geo.x = Math.max(0, geo.x); + geo.y = Math.max(0, geo.y); + } + + this.model.setGeometry(cells[i], geo); + + if (this.isExtendParent(cells[i])) + { + this.extendParent(cells[i]); + } + } + } + + if (this.resetEdgesOnResize) + { + this.resetEdges(cells); + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_RESIZED, + 'cells', cells, 'bounds', bounds)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: extendParent + * + * Resizes the parents recursively so that they contain the complete area + * of the resized child cell. + * + * Parameters: + * + * cell - <mxCell> that has been resized. + */ +mxGraph.prototype.extendParent = function(cell) +{ + if (cell != null) + { + var parent = this.model.getParent(cell); + var p = this.model.getGeometry(parent); + + if (parent != null && p != null && !this.isCellCollapsed(parent)) + { + var geo = this.model.getGeometry(cell); + + if (geo != null && (p.width < geo.x + geo.width || + p.height < geo.y + geo.height)) + { + p = p.clone(); + + p.width = Math.max(p.width, geo.x + geo.width); + p.height = Math.max(p.height, geo.y + geo.height); + + this.cellsResized([parent], [p]); + } + } + } +}; + +/** + * Group: Cell moving + */ + +/** + * Function: importCells + * + * Clones and inserts the given cells into the graph using the move + * method and returns the inserted cells. This shortcut is used if + * cells are inserted via datatransfer. + */ +mxGraph.prototype.importCells = function(cells, dx, dy, target, evt) +{ + return this.moveCells(cells, dx, dy, true, target, evt); +}; + +/** + * Function: moveCells + * + * Moves or clones the specified cells and moves the cells or clones by the + * given amount, adding them to the optional target cell. The evt is the + * mouse event as the mouse was released. The change is carried out using + * <cellsMoved>. This method fires <mxEvent.MOVE_CELLS> while the + * transaction is in progress. Returns the cells that were moved. + * + * Use the following code to move all cells in the graph. + * + * (code) + * graph.moveCells(graph.getChildCells(null, true, true), 10, 10); + * (end) + * + * Parameters: + * + * cells - Array of <mxCells> to be moved, cloned or added to the target. + * dx - Integer that specifies the x-coordinate of the vector. Default is 0. + * dy - Integer that specifies the y-coordinate of the vector. Default is 0. + * clone - Boolean indicating if the cells should be cloned. Default is false. + * target - <mxCell> that represents the new parent of the cells. + * evt - Mouseevent that triggered the invocation. + */ +mxGraph.prototype.moveCells = function(cells, dx, dy, clone, target, evt) +{ + dx = (dx != null) ? dx : 0; + dy = (dy != null) ? dy : 0; + clone = (clone != null) ? clone : false; + + if (cells != null && (dx != 0 || dy != 0 || clone || target != null)) + { + this.model.beginUpdate(); + try + { + if (clone) + { + cells = this.cloneCells(cells, this.isCloneInvalidEdges()); + + if (target == null) + { + target = this.getDefaultParent(); + } + } + + // FIXME: Cells should always be inserted first before any other edit + // to avoid forward references in sessions. + // Need to disable allowNegativeCoordinates if target not null to + // allow for temporary negative numbers until cellsAdded is called. + var previous = this.isAllowNegativeCoordinates(); + + if (target != null) + { + this.setAllowNegativeCoordinates(true); + } + + this.cellsMoved(cells, dx, dy, !clone && this.isDisconnectOnMove() + && this.isAllowDanglingEdges(), target == null); + + this.setAllowNegativeCoordinates(previous); + + if (target != null) + { + var index = this.model.getChildCount(target); + this.cellsAdded(cells, target, index, null, null, true); + } + + // Dispatches a move event + this.fireEvent(new mxEventObject(mxEvent.MOVE_CELLS, 'cells', cells, + 'dx', dx, 'dy', dy, 'clone', clone, 'target', target, 'event', evt)); + } + finally + { + this.model.endUpdate(); + } + } + + return cells; +}; + +/** + * Function: cellsMoved + * + * Moves the specified cells by the given vector, disconnecting the cells + * using disconnectGraph is disconnect is true. This method fires + * <mxEvent.CELLS_MOVED> while the transaction is in progress. + */ +mxGraph.prototype.cellsMoved = function(cells, dx, dy, disconnect, constrain) +{ + if (cells != null && (dx != 0 || dy != 0)) + { + this.model.beginUpdate(); + try + { + if (disconnect) + { + this.disconnectGraph(cells); + } + + for (var i = 0; i < cells.length; i++) + { + this.translateCell(cells[i], dx, dy); + + if (constrain) + { + this.constrainChild(cells[i]); + } + } + + if (this.resetEdgesOnMove) + { + this.resetEdges(cells); + } + + this.fireEvent(new mxEventObject(mxEvent.CELLS_MOVED, + 'cells', cells, 'dx', dy, 'dy', dy, 'disconnect', disconnect)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: translateCell + * + * Translates the geometry of the given cell and stores the new, + * translated geometry in the model as an atomic change. + */ +mxGraph.prototype.translateCell = function(cell, dx, dy) +{ + var geo = this.model.getGeometry(cell); + + if (geo != null) + { + geo = geo.clone(); + geo.translate(dx, dy); + + if (!geo.relative && this.model.isVertex(cell) && !this.isAllowNegativeCoordinates()) + { + geo.x = Math.max(0, geo.x); + geo.y = Math.max(0, geo.y); + } + + if (geo.relative && !this.model.isEdge(cell)) + { + if (geo.offset == null) + { + geo.offset = new mxPoint(dx, dy); + } + else + { + geo.offset.x += dx; + geo.offset.y += dy; + } + } + + this.model.setGeometry(cell, geo); + } +}; + +/** + * Function: getCellContainmentArea + * + * Returns the <mxRectangle> inside which a cell is to be kept. + * + * Parameters: + * + * cell - <mxCell> for which the area should be returned. + */ +mxGraph.prototype.getCellContainmentArea = function(cell) +{ + if (cell != null && !this.model.isEdge(cell)) + { + var parent = this.model.getParent(cell); + + if (parent == this.getDefaultParent() || parent == this.getCurrentRoot()) + { + return this.getMaximumGraphBounds(); + } + else if (parent != null && parent != this.getDefaultParent()) + { + var g = this.model.getGeometry(parent); + + if (g != null) + { + var x = 0; + var y = 0; + var w = g.width; + var h = g.height; + + if (this.isSwimlane(parent)) + { + var size = this.getStartSize(parent); + + x = size.width; + w -= size.width; + y = size.height; + h -= size.height; + } + + return new mxRectangle(x, y, w, h); + } + } + } + + return null; +}; + +/** + * Function: getMaximumGraphBounds + * + * Returns the bounds inside which the diagram should be kept as an + * <mxRectangle>. + */ +mxGraph.prototype.getMaximumGraphBounds = function() +{ + return this.maximumGraphBounds; +}; + +/** + * Function: constrainChild + * + * Keeps the given cell inside the bounds returned by + * <getCellContainmentArea> for its parent, according to the rules defined by + * <getOverlap> and <isConstrainChild>. This modifies the cell's geometry + * in-place and does not clone it. + * + * Parameters: + * + * cells - <mxCell> which should be constrained. + */ +mxGraph.prototype.constrainChild = function(cell) +{ + if (cell != null) + { + var geo = this.model.getGeometry(cell); + var area = (this.isConstrainChild(cell)) ? + this.getCellContainmentArea(cell) : + this.getMaximumGraphBounds(); + + if (geo != null && area != null) + { + // Keeps child within the content area of the parent + if (!geo.relative && (geo.x < area.x || geo.y < area.y || + area.width < geo.x + geo.width || area.height < geo.y + geo.height)) + { + var overlap = this.getOverlap(cell); + + if (area.width > 0) + { + geo.x = Math.min(geo.x, area.x + area.width - + (1 - overlap) * geo.width); + } + + if (area.height > 0) + { + geo.y = Math.min(geo.y, area.y + area.height - + (1 - overlap) * geo.height); + } + + geo.x = Math.max(geo.x, area.x - geo.width * overlap); + geo.y = Math.max(geo.y, area.y - geo.height * overlap); + } + } + } +}; + +/** + * Function: resetEdges + * + * Resets the control points of the edges that are connected to the given + * cells if not both ends of the edge are in the given cells array. + * + * Parameters: + * + * cells - Array of <mxCells> for which the connected edges should be + * reset. + */ +mxGraph.prototype.resetEdges = function(cells) +{ + if (cells != null) + { + // Prepares a hashtable for faster cell lookups + var hash = new Object(); + + for (var i = 0; i < cells.length; i++) + { + var id = mxCellPath.create(cells[i]); + hash[id] = cells[i]; + } + + this.model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + var edges = this.model.getEdges(cells[i]); + + if (edges != null) + { + for (var j = 0; j < edges.length; j++) + { + var state = this.view.getState(edges[j]); + + var source = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[j], true); + var target = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[j], false); + + var sourceId = mxCellPath.create(source); + var targetId = mxCellPath.create(target); + + // Checks if one of the terminals is not in the given array + if (hash[sourceId] == null || hash[targetId] == null) + { + this.resetEdge(edges[j]); + } + } + } + + this.resetEdges(this.model.getChildren(cells[i])); + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: resetEdge + * + * Resets the control points of the given edge. + * + * Parameters: + * + * edge - <mxCell> whose points should be reset. + */ +mxGraph.prototype.resetEdge = function(edge) +{ + var geo = this.model.getGeometry(edge); + + // Resets the control points + if (geo != null && geo.points != null && geo.points.length > 0) + { + geo = geo.clone(); + geo.points = []; + this.model.setGeometry(edge, geo); + } + + return edge; +}; + +/** + * Group: Cell connecting and connection constraints + */ + +/** + * Function: getAllConnectionConstraints + * + * Returns an array of all <mxConnectionConstraints> for the given terminal. If + * the shape of the given terminal is a <mxStencilShape> then the constraints + * of the corresponding <mxStencil> are returned. + * + * Parameters: + * + * terminal - <mxCellState> that represents the terminal. + * source - Boolean that specifies if the terminal is the source or target. + */ +mxGraph.prototype.getAllConnectionConstraints = function(terminal, source) +{ + if (terminal != null && terminal.shape != null && + terminal.shape instanceof mxStencilShape) + { + if (terminal.shape.stencil != null) + { + return terminal.shape.stencil.constraints; + } + } + + return null; +}; + +/** + * Function: getConnectionConstraint + * + * Returns an <mxConnectionConstraint> that describes the given connection + * point. This result can then be passed to <getConnectionPoint>. + * + * Parameters: + * + * edge - <mxCellState> that represents the edge. + * terminal - <mxCellState> that represents the terminal. + * source - Boolean indicating if the terminal is the source or target. + */ +mxGraph.prototype.getConnectionConstraint = function(edge, terminal, source) +{ + var point = null; + var x = edge.style[(source) ? + mxConstants.STYLE_EXIT_X : + mxConstants.STYLE_ENTRY_X]; + + if (x != null) + { + var y = edge.style[(source) ? + mxConstants.STYLE_EXIT_Y : + mxConstants.STYLE_ENTRY_Y]; + + if (y != null) + { + point = new mxPoint(parseFloat(x), parseFloat(y)); + } + } + + var perimeter = false; + + if (point != null) + { + perimeter = mxUtils.getValue(edge.style, (source) ? mxConstants.STYLE_EXIT_PERIMETER : + mxConstants.STYLE_ENTRY_PERIMETER, true); + } + + return new mxConnectionConstraint(point, perimeter); +}; + +/** + * Function: setConnectionConstraint + * + * Sets the <mxConnectionConstraint> that describes the given connection point. + * If no constraint is given then nothing is changed. To remove an existing + * constraint from the given edge, use an empty constraint instead. + * + * Parameters: + * + * edge - <mxCell> that represents the edge. + * terminal - <mxCell> that represents the terminal. + * source - Boolean indicating if the terminal is the source or target. + * constraint - Optional <mxConnectionConstraint> to be used for this + * connection. + */ +mxGraph.prototype.setConnectionConstraint = function(edge, terminal, source, constraint) +{ + if (constraint != null) + { + this.model.beginUpdate(); + try + { + if (constraint == null || constraint.point == null) + { + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_X : + mxConstants.STYLE_ENTRY_X, null, [edge]); + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_Y : + mxConstants.STYLE_ENTRY_Y, null, [edge]); + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_PERIMETER : + mxConstants.STYLE_ENTRY_PERIMETER, null, [edge]); + } + else if (constraint.point != null) + { + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_X : + mxConstants.STYLE_ENTRY_X, constraint.point.x, [edge]); + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_Y : + mxConstants.STYLE_ENTRY_Y, constraint.point.y, [edge]); + + // Only writes 0 since 1 is default + if (!constraint.perimeter) + { + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_PERIMETER : + mxConstants.STYLE_ENTRY_PERIMETER, '0', [edge]); + } + else + { + this.setCellStyles((source) ? mxConstants.STYLE_EXIT_PERIMETER : + mxConstants.STYLE_ENTRY_PERIMETER, null, [edge]); + } + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: getConnectionPoint + * + * Returns the nearest point in the list of absolute points or the center + * of the opposite terminal. + * + * Parameters: + * + * vertex - <mxCellState> that represents the vertex. + * constraint - <mxConnectionConstraint> that represents the connection point + * constraint as returned by <getConnectionConstraint>. + */ +mxGraph.prototype.getConnectionPoint = function(vertex, constraint) +{ + var point = null; + + if (vertex != null) + { + var bounds = this.view.getPerimeterBounds(vertex); + var cx = new mxPoint(bounds.getCenterX(), bounds.getCenterY()); + + var direction = vertex.style[mxConstants.STYLE_DIRECTION]; + var r1 = 0; + + // Bounds need to be rotated by 90 degrees for further computation + if (direction != null) + { + if (direction == 'north') + { + r1 += 270; + } + else if (direction == 'west') + { + r1 += 180; + } + else if (direction == 'south') + { + r1 += 90; + } + + // Bounds need to be rotated by 90 degrees for further computation + if (direction == 'north' || direction == 'south') + { + bounds.x += bounds.width / 2 - bounds.height / 2; + bounds.y += bounds.height / 2 - bounds.width / 2; + var tmp = bounds.width; + bounds.width = bounds.height; + bounds.height = tmp; + } + } + + if (constraint.point != null) + { + var sx = 1; + var sy = 1; + var dx = 0; + var dy = 0; + + // LATER: Add flipping support for image shapes + if (vertex.shape instanceof mxStencilShape) + { + var flipH = vertex.style[mxConstants.STYLE_STENCIL_FLIPH]; + var flipV = vertex.style[mxConstants.STYLE_STENCIL_FLIPV]; + + if (direction == 'north' || direction == 'south') + { + var tmp = flipH; + flipH = flipV; + flipV = tmp; + } + + if (flipH) + { + sx = -1; + dx = -bounds.width; + } + + if (flipV) + { + sy = -1; + dy = -bounds.height ; + } + } + + point = new mxPoint(bounds.x + constraint.point.x * bounds.width * sx - dx, + bounds.y + constraint.point.y * bounds.height * sy - dy); + } + + // Rotation for direction before projection on perimeter + var r2 = vertex.style[mxConstants.STYLE_ROTATION] || 0; + + if (constraint.perimeter) + { + if (r1 != 0 && point != null) + { + // Only 90 degrees steps possible here so no trig needed + var cos = 0; + var sin = 0; + + if (r1 == 90) + { + sin = 1; + } + else if (r1 == 180) + { + cos = -1; + } + else if (r2 == 270) + { + sin = -1; + } + + point = mxUtils.getRotatedPoint(point, cos, sin, cx); + } + + if (point != null && constraint.perimeter) + { + point = this.view.getPerimeterPoint(vertex, point, false); + } + } + else + { + r2 += r1; + } + + // Generic rotation after projection on perimeter + if (r2 != 0 && point != null) + { + var rad = mxUtils.toRadians(r2); + var cos = Math.cos(rad); + var sin = Math.sin(rad); + + point = mxUtils.getRotatedPoint(point, cos, sin, cx); + } + } + + return point; +}; + +/** + * Function: connectCell + * + * Connects the specified end of the given edge to the given terminal + * using <cellConnected> and fires <mxEvent.CONNECT_CELL> while the + * transaction is in progress. Returns the updated edge. + * + * Parameters: + * + * edge - <mxCell> whose terminal should be updated. + * terminal - <mxCell> that represents the new terminal to be used. + * source - Boolean indicating if the new terminal is the source or target. + * constraint - Optional <mxConnectionConstraint> to be used for this + * connection. + */ +mxGraph.prototype.connectCell = function(edge, terminal, source, constraint) +{ + this.model.beginUpdate(); + try + { + var previous = this.model.getTerminal(edge, source); + this.cellConnected(edge, terminal, source, constraint); + this.fireEvent(new mxEventObject(mxEvent.CONNECT_CELL, + 'edge', edge, 'terminal', terminal, 'source', source, + 'previous', previous)); + } + finally + { + this.model.endUpdate(); + } + + return edge; +}; + +/** + * Function: cellConnected + * + * Sets the new terminal for the given edge and resets the edge points if + * <resetEdgesOnConnect> is true. This method fires + * <mxEvent.CELL_CONNECTED> while the transaction is in progress. + * + * Parameters: + * + * edge - <mxCell> whose terminal should be updated. + * terminal - <mxCell> that represents the new terminal to be used. + * source - Boolean indicating if the new terminal is the source or target. + * constraint - <mxConnectionConstraint> to be used for this connection. + */ +mxGraph.prototype.cellConnected = function(edge, terminal, source, constraint) +{ + if (edge != null) + { + this.model.beginUpdate(); + try + { + var previous = this.model.getTerminal(edge, source); + + // Updates the constraint + this.setConnectionConstraint(edge, terminal, source, constraint); + + // Checks if the new terminal is a port, uses the ID of the port in the + // style and the parent of the port as the actual terminal of the edge. + if (this.isPortsEnabled()) + { + var id = null; + + if (this.isPort(terminal)) + { + id = terminal.getId(); + terminal = this.getTerminalForPort(terminal, source); + } + + // Sets or resets all previous information for connecting to a child port + var key = (source) ? mxConstants.STYLE_SOURCE_PORT : + mxConstants.STYLE_TARGET_PORT; + this.setCellStyles(key, id, [edge]); + } + + this.model.setTerminal(edge, terminal, source); + + if (this.resetEdgesOnConnect) + { + this.resetEdge(edge); + } + + this.fireEvent(new mxEventObject(mxEvent.CELL_CONNECTED, + 'edge', edge, 'terminal', terminal, 'source', source, + 'previous', previous)); + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Function: disconnectGraph + * + * Disconnects the given edges from the terminals which are not in the + * given array. + * + * Parameters: + * + * cells - Array of <mxCells> to be disconnected. + */ +mxGraph.prototype.disconnectGraph = function(cells) +{ + if (cells != null) + { + this.model.beginUpdate(); + try + { + var scale = this.view.scale; + var tr = this.view.translate; + + // Prepares a hashtable for faster cell lookups + var hash = new Object(); + + for (var i = 0; i < cells.length; i++) + { + var id = mxCellPath.create(cells[i]); + hash[id] = cells[i]; + } + + for (var i = 0; i < cells.length; i++) + { + if (this.model.isEdge(cells[i])) + { + var geo = this.model.getGeometry(cells[i]); + + if (geo != null) + { + var state = this.view.getState(cells[i]); + var pstate = this.view.getState( + this.model.getParent(cells[i])); + + if (state != null && + pstate != null) + { + geo = geo.clone(); + + var dx = -pstate.origin.x; + var dy = -pstate.origin.y; + var pts = state.absolutePoints; + + var src = this.model.getTerminal(cells[i], true); + + if (src != null && this.isCellDisconnectable(cells[i], src, true)) + { + var srcId = mxCellPath.create(src); + + while (src != null && hash[srcId] == null) + { + src = this.model.getParent(src); + srcId = mxCellPath.create(src); + } + + if (src == null) + { + geo.setTerminalPoint( + new mxPoint(pts[0].x / scale - tr.x + dx, + pts[0].y / scale - tr.y + dy), true); + this.model.setTerminal(cells[i], null, true); + } + } + + var trg = this.model.getTerminal(cells[i], false); + + if (trg != null && this.isCellDisconnectable(cells[i], trg, false)) + { + var trgId = mxCellPath.create(trg); + + while (trg != null && hash[trgId] == null) + { + trg = this.model.getParent(trg); + trgId = mxCellPath.create(trg); + } + + if (trg == null) + { + var n = pts.length - 1; + geo.setTerminalPoint( + new mxPoint(pts[n].x / scale - tr.x + dx, + pts[n].y / scale - tr.y + dy), false); + this.model.setTerminal(cells[i], null, false); + } + } + + this.model.setGeometry(cells[i], geo); + } + } + } + } + } + finally + { + this.model.endUpdate(); + } + } +}; + +/** + * Group: Drilldown + */ + +/** + * Function: getCurrentRoot + * + * Returns the current root of the displayed cell hierarchy. This is a + * shortcut to <mxGraphView.currentRoot> in <view>. + */ + mxGraph.prototype.getCurrentRoot = function() + { + return this.view.currentRoot; + }; + + /** + * Function: getTranslateForRoot + * + * Returns the translation to be used if the given cell is the root cell as + * an <mxPoint>. This implementation returns null. + * + * Example: + * + * To keep the children at their absolute position while stepping into groups, + * this function can be overridden as follows. + * + * (code) + * var offset = new mxPoint(0, 0); + * + * while (cell != null) + * { + * var geo = this.model.getGeometry(cell); + * + * if (geo != null) + * { + * offset.x -= geo.x; + * offset.y -= geo.y; + * } + * + * cell = this.model.getParent(cell); + * } + * + * return offset; + * (end) + * + * Parameters: + * + * cell - <mxCell> that represents the root. + */ +mxGraph.prototype.getTranslateForRoot = function(cell) +{ + return null; +}; + +/** + * Function: isPort + * + * Returns true if the given cell is a "port", that is, when connecting to + * it, the cell returned by getTerminalForPort should be used as the + * terminal and the port should be referenced by the ID in either the + * mxConstants.STYLE_SOURCE_PORT or the or the + * mxConstants.STYLE_TARGET_PORT. Note that a port should not be movable. + * This implementation always returns false. + * + * A typical implementation is the following: + * + * (code) + * graph.isPort = function(cell) + * { + * var geo = this.getCellGeometry(cell); + * + * return (geo != null) ? geo.relative : false; + * }; + * (end) + * + * Parameters: + * + * cell - <mxCell> that represents the port. + */ +mxGraph.prototype.isPort = function(cell) +{ + return false; +}; + +/** + * Function: getTerminalForPort + * + * Returns the terminal to be used for a given port. This implementation + * always returns the parent cell. + * + * Parameters: + * + * cell - <mxCell> that represents the port. + * source - If the cell is the source or target port. + */ +mxGraph.prototype.getTerminalForPort = function(cell, source) +{ + return this.model.getParent(cell); +}; + +/** + * Function: getChildOffsetForCell + * + * Returns the offset to be used for the cells inside the given cell. The + * root and layer cells may be identified using <mxGraphModel.isRoot> and + * <mxGraphModel.isLayer>. For all other current roots, the + * <mxGraphView.currentRoot> field points to the respective cell, so that + * the following holds: cell == this.view.currentRoot. This implementation + * returns null. + * + * Parameters: + * + * cell - <mxCell> whose offset should be returned. + */ +mxGraph.prototype.getChildOffsetForCell = function(cell) +{ + return null; +}; + +/** + * Function: enterGroup + * + * Uses the given cell as the root of the displayed cell hierarchy. If no + * cell is specified then the selection cell is used. The cell is only used + * if <isValidRoot> returns true. + * + * Parameters: + * + * cell - Optional <mxCell> to be used as the new root. Default is the + * selection cell. + */ +mxGraph.prototype.enterGroup = function(cell) +{ + cell = cell || this.getSelectionCell(); + + if (cell != null && this.isValidRoot(cell)) + { + this.view.setCurrentRoot(cell); + this.clearSelection(); + } +}; + +/** + * Function: exitGroup + * + * Changes the current root to the next valid root in the displayed cell + * hierarchy. + */ +mxGraph.prototype.exitGroup = function() +{ + var root = this.model.getRoot(); + var current = this.getCurrentRoot(); + + if (current != null) + { + var next = this.model.getParent(current); + + // Finds the next valid root in the hierarchy + while (next != root && !this.isValidRoot(next) && + this.model.getParent(next) != root) + { + next = this.model.getParent(next); + } + + // Clears the current root if the new root is + // the model's root or one of the layers. + if (next == root || this.model.getParent(next) == root) + { + this.view.setCurrentRoot(null); + } + else + { + this.view.setCurrentRoot(next); + } + + var state = this.view.getState(current); + + // Selects the previous root in the graph + if (state != null) + { + this.setSelectionCell(current); + } + } +}; + +/** + * Function: home + * + * Uses the root of the model as the root of the displayed cell hierarchy + * and selects the previous root. + */ +mxGraph.prototype.home = function() +{ + var current = this.getCurrentRoot(); + + if (current != null) + { + this.view.setCurrentRoot(null); + var state = this.view.getState(current); + + if (state != null) + { + this.setSelectionCell(current); + } + } +}; + +/** + * Function: isValidRoot + * + * Returns true if the given cell is a valid root for the cell display + * hierarchy. This implementation returns true for all non-null values. + * + * Parameters: + * + * cell - <mxCell> which should be checked as a possible root. + */ +mxGraph.prototype.isValidRoot = function(cell) +{ + return (cell != null); +}; + +/** + * Group: Graph display + */ + +/** + * Function: getGraphBounds + * + * Returns the bounds of the visible graph. Shortcut to + * <mxGraphView.getGraphBounds>. See also: <getBoundingBoxFromGeometry>. + */ + mxGraph.prototype.getGraphBounds = function() + { + return this.view.getGraphBounds(); + }; + +/** + * Function: getCellBounds + * + * Returns the scaled, translated bounds for the given cell. See + * <mxGraphView.getBounds> for arrays. + * + * Parameters: + * + * cell - <mxCell> whose bounds should be returned. + * includeEdge - Optional boolean that specifies if the bounds of + * the connected edges should be included. Default is false. + * includeDescendants - Optional boolean that specifies if the bounds + * of all descendants should be included. Default is false. + */ +mxGraph.prototype.getCellBounds = function(cell, includeEdges, includeDescendants) +{ + var cells = [cell]; + + // Includes all connected edges + if (includeEdges) + { + cells = cells.concat(this.model.getEdges(cell)); + } + + var result = this.view.getBounds(cells); + + // Recursively includes the bounds of the children + if (includeDescendants) + { + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var tmp = this.getCellBounds(this.model.getChildAt(cell, i), + includeEdges, true); + + if (result != null) + { + result.add(tmp); + } + else + { + result = tmp; + } + } + } + + return result; +}; + +/** + * Function: getBoundingBoxFromGeometry + * + * Returns the bounding box for the geometries of the vertices in the + * given array of cells. This can be used to find the graph bounds during + * a layout operation (ie. before the last endUpdate) as follows: + * + * (code) + * var cells = graph.getChildCells(graph.getDefaultParent(), true, true); + * var bounds = graph.getBoundingBoxFromGeometry(cells, true); + * (end) + * + * This can then be used to move cells to the origin: + * + * (code) + * if (bounds.x < 0 || bounds.y < 0) + * { + * graph.moveCells(cells, -Math.min(bounds.x, 0), -Math.min(bounds.y, 0)) + * } + * (end) + * + * Or to translate the graph view: + * + * (code) + * if (bounds.x < 0 || bounds.y < 0) + * { + * graph.view.setTranslate(-Math.min(bounds.x, 0), -Math.min(bounds.y, 0)); + * } + * (end) + * + * Parameters: + * + * cells - Array of <mxCells> whose bounds should be returned. + * includeEdges - Specifies if edge bounds should be included by computing + * the bounding box for all points its geometry. Default is false. + */ +mxGraph.prototype.getBoundingBoxFromGeometry = function(cells, includeEdges) +{ + includeEdges = (includeEdges != null) ? includeEdges : false; + var result = null; + + if (cells != null) + { + for (var i = 0; i < cells.length; i++) + { + if (includeEdges || this.model.isVertex(cells[i])) + { + // Computes the bounding box for the points in the geometry + var geo = this.getCellGeometry(cells[i]); + + if (geo != null) + { + var pts = geo.points; + + if (pts != null && pts.length > 0) + { + var tmp = new mxRectangle(pts[0].x, pts[0].y, 0, 0); + var addPoint = function(pt) + { + if (pt != null) + { + tmp.add(new mxRectangle(pt.x, pt.y, 0, 0)); + } + }; + + for (var j = 1; j < pts.length; j++) + { + addPoint(pts[j]); + } + + addPoint(geo.getTerminalPoint(true)); + addPoint(geo.getTerminalPoint(false)); + } + + if (result == null) + { + result = new mxRectangle(geo.x, geo.y, geo.width, geo.height); + } + else + { + result.add(geo); + } + } + } + } + } + + return result; +}; + +/** + * Function: refresh + * + * Clears all cell states or the states for the hierarchy starting at the + * given cell and validates the graph. This fires a refresh event as the + * last step. + * + * Parameters: + * + * cell - Optional <mxCell> for which the cell states should be cleared. + */ +mxGraph.prototype.refresh = function(cell) +{ + this.view.clear(cell, cell == null); + this.view.validate(); + this.sizeDidChange(); + this.fireEvent(new mxEventObject(mxEvent.REFRESH)); +}; + +/** + * Function: snap + * + * Snaps the given numeric value to the grid if <gridEnabled> is true. + * + * Parameters: + * + * value - Numeric value to be snapped to the grid. + */ +mxGraph.prototype.snap = function(value) +{ + if (this.gridEnabled) + { + value = Math.round(value / this.gridSize ) * this.gridSize; + } + + return value; +}; + +/** + * Function: panGraph + * + * Shifts the graph display by the given amount. This is used to preview + * panning operations, use <mxGraphView.setTranslate> to set a persistent + * translation of the view. Fires <mxEvent.PAN>. + * + * Parameters: + * + * dx - Amount to shift the graph along the x-axis. + * dy - Amount to shift the graph along the y-axis. + */ +mxGraph.prototype.panGraph = function(dx, dy) +{ + if (this.useScrollbarsForPanning && mxUtils.hasScrollbars(this.container)) + { + this.container.scrollLeft = -dx; + this.container.scrollTop = -dy; + } + else + { + var canvas = this.view.getCanvas(); + + if (this.dialect == mxConstants.DIALECT_SVG) + { + // Puts everything inside the container in a DIV so that it + // can be moved without changing the state of the container + if (dx == 0 && dy == 0) + { + // Workaround for ignored removeAttribute on SVG element in IE9 standards + if (mxClient.IS_IE) + { + canvas.setAttribute('transform', 'translate('+ dx + ',' + dy + ')'); + } + else + { + canvas.removeAttribute('transform'); + } + + if (this.shiftPreview1 != null) + { + var child = this.shiftPreview1.firstChild; + + while (child != null) + { + var next = child.nextSibling; + this.container.appendChild(child); + child = next; + } + + this.shiftPreview1.parentNode.removeChild(this.shiftPreview1); + this.shiftPreview1 = null; + + this.container.appendChild(canvas.parentNode); + + child = this.shiftPreview2.firstChild; + + while (child != null) + { + var next = child.nextSibling; + this.container.appendChild(child); + child = next; + } + + this.shiftPreview2.parentNode.removeChild(this.shiftPreview2); + this.shiftPreview2 = null; + } + } + else + { + canvas.setAttribute('transform', 'translate('+ dx + ',' + dy + ')'); + + if (this.shiftPreview1 == null) + { + // Needs two divs for stuff before and after the SVG element + this.shiftPreview1 = document.createElement('div'); + this.shiftPreview1.style.position = 'absolute'; + this.shiftPreview1.style.overflow = 'visible'; + + this.shiftPreview2 = document.createElement('div'); + this.shiftPreview2.style.position = 'absolute'; + this.shiftPreview2.style.overflow = 'visible'; + + var current = this.shiftPreview1; + var child = this.container.firstChild; + + while (child != null) + { + var next = child.nextSibling; + + // SVG element is moved via transform attribute + if (child != canvas.parentNode) + { + current.appendChild(child); + } + else + { + current = this.shiftPreview2; + } + + child = next; + } + + this.container.insertBefore(this.shiftPreview1, canvas.parentNode); + this.container.appendChild(this.shiftPreview2); + } + + this.shiftPreview1.style.left = dx + 'px'; + this.shiftPreview1.style.top = dy + 'px'; + this.shiftPreview2.style.left = dx + 'px'; + this.shiftPreview2.style.top = dy + 'px'; + } + } + else + { + canvas.style.left = dx + 'px'; + canvas.style.top = dy + 'px'; + } + + this.panDx = dx; + this.panDy = dy; + + this.fireEvent(new mxEventObject(mxEvent.PAN)); + } +}; + +/** + * Function: zoomIn + * + * Zooms into the graph by <zoomFactor>. + */ +mxGraph.prototype.zoomIn = function() +{ + this.zoom(this.zoomFactor); +}; + +/** + * Function: zoomOut + * + * Zooms out of the graph by <zoomFactor>. + */ +mxGraph.prototype.zoomOut = function() +{ + this.zoom(1 / this.zoomFactor); +}; + +/** + * Function: zoomActual + * + * Resets the zoom and panning in the view. + */ +mxGraph.prototype.zoomActual = function() +{ + if (this.view.scale == 1) + { + this.view.setTranslate(0, 0); + } + else + { + this.view.translate.x = 0; + this.view.translate.y = 0; + + this.view.setScale(1); + } +}; + +/** + * Function: zoomTo + * + * Zooms the graph to the given scale with an optional boolean center + * argument, which is passd to <zoom>. + */ +mxGraph.prototype.zoomTo = function(scale, center) +{ + this.zoom(scale / this.view.scale, center); +}; + +/** + * Function: zoom + * + * Zooms the graph using the given factor. Center is an optional boolean + * argument that keeps the graph scrolled to the center. If the center argument + * is omitted, then <centerZoom> will be used as its value. + */ +mxGraph.prototype.zoom = function(factor, center) +{ + center = (center != null) ? center : this.centerZoom; + var scale = this.view.scale * factor; + var state = this.view.getState(this.getSelectionCell()); + + if (this.keepSelectionVisibleOnZoom && state != null) + { + var rect = new mxRectangle( + state.x * factor, + state.y * factor, + state.width * factor, + state.height * factor); + + // Refreshes the display only once if a + // scroll is carried out + this.view.scale = scale; + + if (!this.scrollRectToVisible(rect)) + { + this.view.revalidate(); + + // Forces an event to be fired but does not revalidate again + this.view.setScale(scale); + } + } + else if (center && !mxUtils.hasScrollbars(this.container)) + { + var dx = this.container.offsetWidth; + var dy = this.container.offsetHeight; + + if (factor > 1) + { + var f = (factor -1) / (scale * 2); + dx *= -f; + dy *= -f; + } + else + { + var f = (1/factor -1) / (this.view.scale * 2); + dx *= f; + dy *= f; + } + + this.view.scaleAndTranslate(scale, + this.view.translate.x + dx, + this.view.translate.y + dy); + } + else + { + this.view.setScale(scale); + + if (mxUtils.hasScrollbars(this.container)) + { + var dx = 0; + var dy = 0; + + if (center) + { + dx = this.container.offsetWidth * (factor - 1) / 2; + dy = this.container.offsetHeight * (factor - 1) / 2; + } + + this.container.scrollLeft = Math.round(this.container.scrollLeft * factor + dx); + this.container.scrollTop = Math.round(this.container.scrollTop * factor + dy); + } + } +}; + +/** + * Function: zoomToRect + * + * Zooms the graph to the specified rectangle. If the rectangle does not have same aspect + * ratio as the display container, it is increased in the smaller relative dimension only + * until the aspect match. The original rectangle is centralised within this expanded one. + * + * Note that the input rectangular must be un-scaled and un-translated. + * + * Parameters: + * + * rect - The un-scaled and un-translated rectangluar region that should be just visible + * after the operation + */ +mxGraph.prototype.zoomToRect = function(rect) +{ + var scaleX = this.container.clientWidth / rect.width; + var scaleY = this.container.clientHeight / rect.height; + var aspectFactor = scaleX / scaleY; + + // Remove any overlap of the rect outside the client area + rect.x = Math.max(0, rect.x); + rect.y = Math.max(0, rect.y); + var rectRight = Math.min(this.container.scrollWidth, rect.x + rect.width); + var rectBottom = Math.min(this.container.scrollHeight, rect.y + rect.height); + rect.width = rectRight - rect.x; + rect.height = rectBottom - rect.y; + + // The selection area has to be increased to the same aspect + // ratio as the container, centred around the centre point of the + // original rect passed in. + if (aspectFactor < 1.0) + { + // Height needs increasing + var newHeight = rect.height / aspectFactor; + var deltaHeightBuffer = (newHeight - rect.height) / 2.0; + rect.height = newHeight; + + // Assign up to half the buffer to the upper part of the rect, not crossing 0 + // put the rest on the bottom + var upperBuffer = Math.min(rect.y , deltaHeightBuffer); + rect.y = rect.y - upperBuffer; + + // Check if the bottom has extended too far + rectBottom = Math.min(this.container.scrollHeight, rect.y + rect.height); + rect.height = rectBottom - rect.y; + } + else + { + // Width needs increasing + var newWidth = rect.width * aspectFactor; + var deltaWidthBuffer = (newWidth - rect.width) / 2.0; + rect.width = newWidth; + + // Assign up to half the buffer to the upper part of the rect, not crossing 0 + // put the rest on the bottom + var leftBuffer = Math.min(rect.x , deltaWidthBuffer); + rect.x = rect.x - leftBuffer; + + // Check if the right hand side has extended too far + rectRight = Math.min(this.container.scrollWidth, rect.x + rect.width); + rect.width = rectRight - rect.x; + } + + var scale = this.container.clientWidth / rect.width; + + if (!mxUtils.hasScrollbars(this.container)) + { + this.view.scaleAndTranslate(scale, -rect.x, -rect.y); + } + else + { + this.view.setScale(scale); + this.container.scrollLeft = Math.round(rect.x * scale); + this.container.scrollTop = Math.round(rect.y * scale); + } +}; + +/** + * Function: fit + * + * Scales the graph such that the complete diagram fits into <container> and + * returns the current scale in the view. To fit an initial graph prior to + * rendering, set <mxGraphView.rendering> to false prior to changing the model + * and execute the following after changing the model. + * + * (code) + * graph.fit(); + * graph.view.rendering = true; + * graph.refresh(); + * (end) + * + * Parameters: + * + * border - Optional number that specifies the border. Default is 0. + * keepOrigin - Optional boolean that specifies if the translate should be + * changed. Default is false. + */ +mxGraph.prototype.fit = function(border, keepOrigin) +{ + if (this.container != null) + { + border = (border != null) ? border : 0; + keepOrigin = (keepOrigin != null) ? keepOrigin : false; + + var w1 = this.container.clientWidth; + var h1 = this.container.clientHeight; + + var bounds = this.view.getGraphBounds(); + + if (keepOrigin && bounds.x != null && bounds.y != null) + { + bounds.width += bounds.x; + bounds.height += bounds.y; + bounds.x = 0; + bounds.y = 0; + } + + var s = this.view.scale; + var w2 = bounds.width / s; + var h2 = bounds.height / s; + + // Fits to the size of the background image if required + if (this.backgroundImage != null) + { + w2 = Math.max(w2, this.backgroundImage.width - bounds.x / s); + h2 = Math.max(h2, this.backgroundImage.height - bounds.y / s); + } + + var b = (keepOrigin) ? border : 2 * border; + var s2 = Math.floor(Math.min(w1 / (w2 + b), h1 / (h2 + b)) * 100) / 100; + + if (this.minFitScale != null) + { + s2 = Math.max(s2, this.minFitScale); + } + + if (this.maxFitScale != null) + { + s2 = Math.min(s2, this.maxFitScale); + } + + if (!keepOrigin) + { + if (!mxUtils.hasScrollbars(this.container)) + { + var x0 = (bounds.x != null) ? Math.floor(this.view.translate.x - bounds.x / s + border + 1) : border; + var y0 = (bounds.y != null) ? Math.floor(this.view.translate.y - bounds.y / s + border + 1) : border; + + this.view.scaleAndTranslate(s2, x0, y0); + } + else + { + this.view.setScale(s2); + + if (bounds.x != null) + { + this.container.scrollLeft = Math.round(bounds.x / s) * s2 - border - + Math.max(0, (this.container.clientWidth - w2 * s2) / 2); + } + + if (bounds.y != null) + { + this.container.scrollTop = Math.round(bounds.y / s) * s2 - border - + Math.max(0, (this.container.clientHeight - h2 * s2) / 2); + } + } + } + else if (this.view.scale != s2) + { + this.view.setScale(s2); + } + } + + return this.view.scale; +}; + +/** + * Function: scrollCellToVisible + * + * Pans the graph so that it shows the given cell. Optionally the cell may + * be centered in the container. + * + * To center a given graph if the <container> has no scrollbars, use the following code. + * + * [code] + * var bounds = graph.getGraphBounds(); + * graph.view.setTranslate(-bounds.x - (bounds.width - container.clientWidth) / 2, + * -bounds.y - (bounds.height - container.clientHeight) / 2); + * [/code] + * + * Parameters: + * + * cell - <mxCell> to be made visible. + * center - Optional boolean flag. Default is false. + */ +mxGraph.prototype.scrollCellToVisible = function(cell, center) +{ + var x = -this.view.translate.x; + var y = -this.view.translate.y; + + var state = this.view.getState(cell); + + if (state != null) + { + var bounds = new mxRectangle(x + state.x, y + state.y, state.width, + state.height); + + if (center && this.container != null) + { + var w = this.container.clientWidth; + var h = this.container.clientHeight; + + bounds.x = bounds.getCenterX() - w / 2; + bounds.width = w; + bounds.y = bounds.getCenterY() - h / 2; + bounds.height = h; + } + + if (this.scrollRectToVisible(bounds)) + { + // Triggers an update via the view's event source + this.view.setTranslate(this.view.translate.x, this.view.translate.y); + } + } +}; + +/** + * Function: scrollRectToVisible + * + * Pans the graph so that it shows the given rectangle. + * + * Parameters: + * + * rect - <mxRectangle> to be made visible. + */ +mxGraph.prototype.scrollRectToVisible = function(rect) +{ + var isChanged = false; + + if (rect != null) + { + var w = this.container.offsetWidth; + var h = this.container.offsetHeight; + + var widthLimit = Math.min(w, rect.width); + var heightLimit = Math.min(h, rect.height); + + if (mxUtils.hasScrollbars(this.container)) + { + var c = this.container; + rect.x += this.view.translate.x; + rect.y += this.view.translate.y; + var dx = c.scrollLeft - rect.x; + var ddx = Math.max(dx - c.scrollLeft, 0); + + if (dx > 0) + { + c.scrollLeft -= dx + 2; + } + else + { + dx = rect.x + widthLimit - c.scrollLeft - c.clientWidth; + + if (dx > 0) + { + c.scrollLeft += dx + 2; + } + } + + var dy = c.scrollTop - rect.y; + var ddy = Math.max(0, dy - c.scrollTop); + + if (dy > 0) + { + c.scrollTop -= dy + 2; + } + else + { + dy = rect.y + heightLimit - c.scrollTop - c.clientHeight; + + if (dy > 0) + { + c.scrollTop += dy + 2; + } + } + + if (!this.useScrollbarsForPanning && (ddx != 0 || ddy != 0)) + { + this.view.setTranslate(ddx, ddy); + } + } + else + { + var x = -this.view.translate.x; + var y = -this.view.translate.y; + + var s = this.view.scale; + + if (rect.x + widthLimit > x + w) + { + this.view.translate.x -= (rect.x + widthLimit - w - x) / s; + isChanged = true; + } + + if (rect.y + heightLimit > y + h) + { + this.view.translate.y -= (rect.y + heightLimit - h - y) / s; + isChanged = true; + } + + if (rect.x < x) + { + this.view.translate.x += (x - rect.x) / s; + isChanged = true; + } + + if (rect.y < y) + { + this.view.translate.y += (y - rect.y) / s; + isChanged = true; + } + + if (isChanged) + { + this.view.refresh(); + + // Repaints selection marker (ticket 18) + if (this.selectionCellsHandler != null) + { + this.selectionCellsHandler.refresh(); + } + } + } + } + + return isChanged; +}; + +/** + * Function: getCellGeometry + * + * Returns the <mxGeometry> for the given cell. This implementation uses + * <mxGraphModel.getGeometry>. Subclasses can override this to implement + * specific geometries for cells in only one graph, that is, it can return + * geometries that depend on the current state of the view. + * + * Parameters: + * + * cell - <mxCell> whose geometry should be returned. + */ +mxGraph.prototype.getCellGeometry = function(cell) +{ + return this.model.getGeometry(cell); +}; + +/** + * Function: isCellVisible + * + * Returns true if the given cell is visible in this graph. This + * implementation uses <mxGraphModel.isVisible>. Subclassers can override + * this to implement specific visibility for cells in only one graph, that + * is, without affecting the visible state of the cell. + * + * When using dynamic filter expressions for cell visibility, then the + * graph should be revalidated after the filter expression has changed. + * + * Parameters: + * + * cell - <mxCell> whose visible state should be returned. + */ +mxGraph.prototype.isCellVisible = function(cell) +{ + return this.model.isVisible(cell); +}; + +/** + * Function: isCellCollapsed + * + * Returns true if the given cell is collapsed in this graph. This + * implementation uses <mxGraphModel.isCollapsed>. Subclassers can override + * this to implement specific collapsed states for cells in only one graph, + * that is, without affecting the collapsed state of the cell. + * + * When using dynamic filter expressions for the collapsed state, then the + * graph should be revalidated after the filter expression has changed. + * + * Parameters: + * + * cell - <mxCell> whose collapsed state should be returned. + */ +mxGraph.prototype.isCellCollapsed = function(cell) +{ + return this.model.isCollapsed(cell); +}; + +/** + * Function: isCellConnectable + * + * Returns true if the given cell is connectable in this graph. This + * implementation uses <mxGraphModel.isConnectable>. Subclassers can override + * this to implement specific connectable states for cells in only one graph, + * that is, without affecting the connectable state of the cell in the model. + * + * Parameters: + * + * cell - <mxCell> whose connectable state should be returned. + */ +mxGraph.prototype.isCellConnectable = function(cell) +{ + return this.model.isConnectable(cell); +}; + +/** + * Function: isOrthogonal + * + * Returns true if perimeter points should be computed such that the + * resulting edge has only horizontal or vertical segments. + * + * Parameters: + * + * edge - <mxCellState> that represents the edge. + */ +mxGraph.prototype.isOrthogonal = function(edge) +{ + var orthogonal = edge.style[mxConstants.STYLE_ORTHOGONAL]; + + if (orthogonal != null) + { + return orthogonal; + } + + var tmp = this.view.getEdgeStyle(edge); + + return tmp == mxEdgeStyle.SegmentConnector || + tmp == mxEdgeStyle.ElbowConnector || + tmp == mxEdgeStyle.SideToSide || + tmp == mxEdgeStyle.TopToBottom || + tmp == mxEdgeStyle.EntityRelation || + tmp == mxEdgeStyle.OrthConnector; +}; + +/** + * Function: isLoop + * + * Returns true if the given cell state is a loop. + * + * Parameters: + * + * state - <mxCellState> that represents a potential loop. + */ +mxGraph.prototype.isLoop = function(state) +{ + var src = state.getVisibleTerminalState(true); + var trg = state.getVisibleTerminalState(false); + + return (src != null && src == trg); +}; + +/** + * Function: isCloneEvent + * + * Returns true if the given event is a clone event. This implementation + * returns true if control is pressed. + */ +mxGraph.prototype.isCloneEvent = function(evt) +{ + return mxEvent.isControlDown(evt); +}; + +/** + * Function: isToggleEvent + * + * Returns true if the given event is a toggle event. This implementation + * returns true if the meta key (Cmd) is pressed on Macs or if control is + * pressed on any other platform. + */ +mxGraph.prototype.isToggleEvent = function(evt) +{ + return (mxClient.IS_MAC) ? mxEvent.isMetaDown(evt) : mxEvent.isControlDown(evt); +}; + +/** + * Function: isGridEnabledEvent + * + * Returns true if the given mouse event should be aligned to the grid. + */ +mxGraph.prototype.isGridEnabledEvent = function(evt) +{ + return evt != null && !mxEvent.isAltDown(evt); +}; + +/** + * Function: isConstrainedEvent + * + * Returns true if the given mouse event should be aligned to the grid. + */ +mxGraph.prototype.isConstrainedEvent = function(evt) +{ + return mxEvent.isShiftDown(evt); +}; + +/** + * Function: isForceMarqueeEvent + * + * Returns true if the given event forces marquee selection. This implementation + * returns true if alt is pressed. + */ +mxGraph.prototype.isForceMarqueeEvent = function(evt) +{ + return mxEvent.isAltDown(evt); +}; + +/** + * Group: Validation + */ + +/** + * Function: validationAlert + * + * Displays the given validation error in a dialog. This implementation uses + * mxUtils.alert. + */ +mxGraph.prototype.validationAlert = function(message) +{ + mxUtils.alert(message); +}; + +/** + * Function: isEdgeValid + * + * Checks if the return value of <getEdgeValidationError> for the given + * arguments is null. + * + * Parameters: + * + * edge - <mxCell> that represents the edge to validate. + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + */ +mxGraph.prototype.isEdgeValid = function(edge, source, target) +{ + return this.getEdgeValidationError(edge, source, target) == null; +}; + +/** + * Function: getEdgeValidationError + * + * Returns the validation error message to be displayed when inserting or + * changing an edges' connectivity. A return value of null means the edge + * is valid, a return value of '' means it's not valid, but do not display + * an error message. Any other (non-empty) string returned from this method + * is displayed as an error message when trying to connect an edge to a + * source and target. This implementation uses the <multiplicities>, and + * checks <multigraph>, <allowDanglingEdges> and <allowLoops> to generate + * validation errors. + * + * For extending this method with specific checks for source/target cells, + * the method can be extended as follows. Returning an empty string means + * the edge is invalid with no error message, a non-null string specifies + * the error message, and null means the edge is valid. + * + * (code) + * graph.getEdgeValidationError = function(edge, source, target) + * { + * if (source != null && target != null && + * this.model.getValue(source) != null && + * this.model.getValue(target) != null) + * { + * if (target is not valid for source) + * { + * return 'Invalid Target'; + * } + * } + * + * // "Supercall" + * return mxGraph.prototype.getEdgeValidationError.apply(this, arguments); + * } + * (end) + * + * Parameters: + * + * edge - <mxCell> that represents the edge to validate. + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + */ +mxGraph.prototype.getEdgeValidationError = function(edge, source, target) +{ + if (edge != null && !this.isAllowDanglingEdges() && (source == null || target == null)) + { + return ''; + } + + if (edge != null && this.model.getTerminal(edge, true) == null && + this.model.getTerminal(edge, false) == null) + { + return null; + } + + // Checks if we're dealing with a loop + if (!this.allowLoops && source == target && source != null) + { + return ''; + } + + // Checks if the connection is generally allowed + if (!this.isValidConnection(source, target)) + { + return ''; + } + + if (source != null && target != null) + { + var error = ''; + + // Checks if the cells are already connected + // and adds an error message if required + if (!this.multigraph) + { + var tmp = this.model.getEdgesBetween(source, target, true); + + // Checks if the source and target are not connected by another edge + if (tmp.length > 1 || (tmp.length == 1 && tmp[0] != edge)) + { + error += (mxResources.get(this.alreadyConnectedResource) || + this.alreadyConnectedResource)+'\n'; + } + } + + // Gets the number of outgoing edges from the source + // and the number of incoming edges from the target + // without counting the edge being currently changed. + var sourceOut = this.model.getDirectedEdgeCount(source, true, edge); + var targetIn = this.model.getDirectedEdgeCount(target, false, edge); + + // Checks the change against each multiplicity rule + if (this.multiplicities != null) + { + for (var i = 0; i < this.multiplicities.length; i++) + { + var err = this.multiplicities[i].check(this, edge, source, + target, sourceOut, targetIn); + + if (err != null) + { + error += err; + } + } + } + + // Validates the source and target terminals independently + var err = this.validateEdge(edge, source, target); + + if (err != null) + { + error += err; + } + + return (error.length > 0) ? error : null; + } + + return (this.allowDanglingEdges) ? null : ''; +}; + +/** + * Function: validateEdge + * + * Hook method for subclassers to return an error message for the given + * edge and terminals. This implementation returns null. + * + * Parameters: + * + * edge - <mxCell> that represents the edge to validate. + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + */ +mxGraph.prototype.validateEdge = function(edge, source, target) +{ + return null; +}; + +/** + * Function: validateGraph + * + * Validates the graph by validating each descendant of the given cell or + * the root of the model. Context is an object that contains the validation + * state for the complete validation run. The validation errors are + * attached to their cells using <setCellWarning>. This function returns true + * if no validation errors exist in the graph. + * + * Paramters: + * + * cell - Optional <mxCell> to start the validation recursion. Default is + * the graph root. + * context - Object that represents the global validation state. + */ +mxGraph.prototype.validateGraph = function(cell, context) +{ + cell = (cell != null) ? cell : this.model.getRoot(); + context = (context != null) ? context : new Object(); + + var isValid = true; + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var tmp = this.model.getChildAt(cell, i); + var ctx = context; + + if (this.isValidRoot(tmp)) + { + ctx = new Object(); + } + + var warn = this.validateGraph(tmp, ctx); + + if (warn != null) + { + this.setCellWarning(tmp, warn.replace(/\n/g, '<br>')); + } + else + { + this.setCellWarning(tmp, null); + } + + isValid = isValid && warn == null; + } + + var warning = ''; + + // Adds error for invalid children if collapsed (children invisible) + if (this.isCellCollapsed(cell) && !isValid) + { + warning += (mxResources.get(this.containsValidationErrorsResource) || + this.containsValidationErrorsResource)+'\n'; + } + + // Checks edges and cells using the defined multiplicities + if (this.model.isEdge(cell)) + { + warning += this.getEdgeValidationError(cell, + this.model.getTerminal(cell, true), + this.model.getTerminal(cell, false)) || ''; + } + else + { + warning += this.getCellValidationError(cell) || ''; + } + + // Checks custom validation rules + var err = this.validateCell(cell, context); + + if (err != null) + { + warning += err; + } + + // Updates the display with the warning icons + // before any potential alerts are displayed. + // LATER: Move this into addCellOverlay. Redraw + // should check if overlay was added or removed. + if (this.model.getParent(cell) == null) + { + this.view.validate(); + } + + return (warning.length > 0 || !isValid) ? warning : null; +}; + +/** + * Function: getCellValidationError + * + * Checks all <multiplicities> that cannot be enforced while the graph is + * being modified, namely, all multiplicities that require a minimum of + * 1 edge. + * + * Parameters: + * + * cell - <mxCell> for which the multiplicities should be checked. + */ +mxGraph.prototype.getCellValidationError = function(cell) +{ + var outCount = this.model.getDirectedEdgeCount(cell, true); + var inCount = this.model.getDirectedEdgeCount(cell, false); + var value = this.model.getValue(cell); + var error = ''; + + if (this.multiplicities != null) + { + for (var i = 0; i < this.multiplicities.length; i++) + { + var rule = this.multiplicities[i]; + + if (rule.source && mxUtils.isNode(value, rule.type, + rule.attr, rule.value) && ((rule.max == 0 && outCount > 0) || + (rule.min == 1 && outCount == 0) || (rule.max == 1 && outCount > 1))) + { + error += rule.countError + '\n'; + } + else if (!rule.source && mxUtils.isNode(value, rule.type, + rule.attr, rule.value) && ((rule.max == 0 && inCount > 0) || + (rule.min == 1 && inCount == 0) || (rule.max == 1 && inCount > 1))) + { + error += rule.countError + '\n'; + } + } + } + + return (error.length > 0) ? error : null; +}; + +/** + * Function: validateCell + * + * Hook method for subclassers to return an error message for the given + * cell and validation context. This implementation returns null. Any HTML + * breaks will be converted to linefeeds in the calling method. + * + * Parameters: + * + * cell - <mxCell> that represents the cell to validate. + * context - Object that represents the global validation state. + */ +mxGraph.prototype.validateCell = function(cell, context) +{ + return null; +}; + +/** + * Group: Graph appearance + */ + +/** + * Function: getBackgroundImage + * + * Returns the <backgroundImage> as an <mxImage>. + */ +mxGraph.prototype.getBackgroundImage = function() +{ + return this.backgroundImage; +}; + +/** + * Function: setBackgroundImage + * + * Sets the new <backgroundImage>. + * + * Parameters: + * + * image - New <mxImage> to be used for the background. + */ +mxGraph.prototype.setBackgroundImage = function(image) +{ + this.backgroundImage = image; +}; + +/** + * Function: getFoldingImage + * + * Returns the <mxImage> used to display the collapsed state of + * the specified cell state. This returns null for all edges. + */ +mxGraph.prototype.getFoldingImage = function(state) +{ + if (state != null && this.foldingEnabled && !this.getModel().isEdge(state.cell)) + { + var tmp = this.isCellCollapsed(state.cell); + + if (this.isCellFoldable(state.cell, !tmp)) + { + return (tmp) ? this.collapsedImage : this.expandedImage; + } + } + + return null; +}; + +/** + * Function: convertValueToString + * + * Returns the textual representation for the given cell. This + * implementation returns the nodename or string-representation of the user + * object. + * + * Example: + * + * The following returns the label attribute from the cells user + * object if it is an XML node. + * + * (code) + * graph.convertValueToString = function(cell) + * { + * return cell.getAttribute('label'); + * } + * (end) + * + * See also: <cellLabelChanged>. + * + * Parameters: + * + * cell - <mxCell> whose textual representation should be returned. + */ +mxGraph.prototype.convertValueToString = function(cell) +{ + var value = this.model.getValue(cell); + + if (value != null) + { + if (mxUtils.isNode(value)) + { + return value.nodeName; + } + else if (typeof(value.toString) == 'function') + { + return value.toString(); + } + } + + return ''; +}; + +/** + * Function: getLabel + * + * Returns a string or DOM node that represents the label for the given + * cell. This implementation uses <convertValueToString> if <labelsVisible> + * is true. Otherwise it returns an empty string. + * + * To truncate label to match the size of the cell, the following code + * can be used. + * + * (code) + * graph.getLabel = function(cell) + * { + * var label = mxGraph.prototype.getLabel.apply(this, arguments); + * + * if (label != null && this.model.isVertex(cell)) + * { + * var geo = this.getCellGeometry(cell); + * + * if (geo != null) + * { + * var max = parseInt(geo.width / 8); + * + * if (label.length > max) + * { + * label = label.substring(0, max)+'...'; + * } + * } + * } + * return mxUtils.htmlEntities(label); + * } + * (end) + * + * A resize listener is needed in the graph to force a repaint of the label + * after a resize. + * + * (code) + * graph.addListener(mxEvent.RESIZE_CELLS, function(sender, evt) + * { + * var cells = evt.getProperty('cells'); + * + * for (var i = 0; i < cells.length; i++) + * { + * this.view.removeState(cells[i]); + * } + * }); + * (end) + * + * Parameters: + * + * cell - <mxCell> whose label should be returned. + */ +mxGraph.prototype.getLabel = function(cell) +{ + var result = ''; + + if (this.labelsVisible && cell != null) + { + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + if (!mxUtils.getValue(style, mxConstants.STYLE_NOLABEL, false)) + { + result = this.convertValueToString(cell); + } + } + + return result; +}; + +/** + * Function: isHtmlLabel + * + * Returns true if the label must be rendered as HTML markup. The default + * implementation returns <htmlLabels>. + * + * Parameters: + * + * cell - <mxCell> whose label should be displayed as HTML markup. + */ +mxGraph.prototype.isHtmlLabel = function(cell) +{ + return this.isHtmlLabels(); +}; + +/** + * Function: isHtmlLabels + * + * Returns <htmlLabels>. + */ +mxGraph.prototype.isHtmlLabels = function() +{ + return this.htmlLabels; +}; + +/** + * Function: setHtmlLabels + * + * Sets <htmlLabels>. + */ +mxGraph.prototype.setHtmlLabels = function(value) +{ + this.htmlLabels = value; +}; + +/** + * Function: isWrapping + * + * This enables wrapping for HTML labels. + * + * Returns true if no white-space CSS style directive should be used for + * displaying the given cells label. This implementation returns true if + * <mxConstants.STYLE_WHITE_SPACE> in the style of the given cell is 'wrap'. + * + * This is used as a workaround for IE ignoring the white-space directive + * of child elements if the directive appears in a parent element. It + * should be overridden to return true if a white-space directive is used + * in the HTML markup that represents the given cells label. In order for + * HTML markup to work in labels, <isHtmlLabel> must also return true + * for the given cell. + * + * Example: + * + * (code) + * graph.getLabel = function(cell) + * { + * var tmp = mxGraph.prototype.getLabel.apply(this, arguments); // "supercall" + * + * if (this.model.isEdge(cell)) + * { + * tmp = '<div style="width: 150px; white-space:normal;">'+tmp+'</div>'; + * } + * + * return tmp; + * } + * + * graph.isWrapping = function(state) + * { + * return this.model.isEdge(state.cell); + * } + * (end) + * + * Makes sure no edge label is wider than 150 pixels, otherwise the content + * is wrapped. Note: No width must be specified for wrapped vertex labels as + * the vertex defines the width in its geometry. + * + * Parameters: + * + * state - <mxCell> whose label should be wrapped. + */ +mxGraph.prototype.isWrapping = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return (style != null) ? style[mxConstants.STYLE_WHITE_SPACE] == 'wrap' : false; +}; + +/** + * Function: isLabelClipped + * + * Returns true if the overflow portion of labels should be hidden. If this + * returns true then vertex labels will be clipped to the size of the vertices. + * This implementation returns true if <mxConstants.STYLE_OVERFLOW> in the + * style of the given cell is 'hidden'. + * + * Parameters: + * + * state - <mxCell> whose label should be clipped. + */ +mxGraph.prototype.isLabelClipped = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return (style != null) ? style[mxConstants.STYLE_OVERFLOW] == 'hidden' : false; +}; + +/** + * Function: getTooltip + * + * Returns the string or DOM node that represents the tooltip for the given + * state, node and coordinate pair. This implementation checks if the given + * node is a folding icon or overlay and returns the respective tooltip. If + * this does not result in a tooltip, the handler for the cell is retrieved + * from <selectionCellsHandler> and the optional getTooltipForNode method is + * called. If no special tooltip exists here then <getTooltipForCell> is used + * with the cell in the given state as the argument to return a tooltip for the + * given state. + * + * Parameters: + * + * state - <mxCellState> whose tooltip should be returned. + * node - DOM node that is currently under the mouse. + * x - X-coordinate of the mouse. + * y - Y-coordinate of the mouse. + */ +mxGraph.prototype.getTooltip = function(state, node, x, y) +{ + var tip = null; + + if (state != null) + { + // Checks if the mouse is over the folding icon + if (state.control != null && (node == state.control.node || + node.parentNode == state.control.node)) + { + tip = this.collapseExpandResource; + tip = mxResources.get(tip) || tip; + } + + if (tip == null && state.overlays != null) + { + state.overlays.visit(function(id, shape) + { + // LATER: Exit loop if tip is not null + if (tip == null && (node == shape.node || node.parentNode == shape.node)) + { + tip = shape.overlay.toString(); + } + }); + } + + if (tip == null) + { + var handler = this.selectionCellsHandler.getHandler(state.cell); + + if (handler != null && typeof(handler.getTooltipForNode) == 'function') + { + tip = handler.getTooltipForNode(node); + } + } + + if (tip == null) + { + tip = this.getTooltipForCell(state.cell); + } + } + + return tip; +}; + +/** + * Function: getTooltipForCell + * + * Returns the string or DOM node to be used as the tooltip for the given + * cell. This implementation uses the cells getTooltip function if it + * exists, or else it returns <convertValueToString> for the cell. + * + * Example: + * + * (code) + * graph.getTooltipForCell = function(cell) + * { + * return 'Hello, World!'; + * } + * (end) + * + * Replaces all tooltips with the string Hello, World! + * + * Parameters: + * + * cell - <mxCell> whose tooltip should be returned. + */ +mxGraph.prototype.getTooltipForCell = function(cell) +{ + var tip = null; + + if (cell != null && cell.getTooltip != null) + { + tip = cell.getTooltip(); + } + else + { + tip = this.convertValueToString(cell); + } + + return tip; +}; + +/** + * Function: getCursorForCell + * + * Returns the cursor value to be used for the CSS of the shape for the + * given cell. This implementation returns null. + * + * Parameters: + * + * cell - <mxCell> whose cursor should be returned. + */ +mxGraph.prototype.getCursorForCell = function(cell) +{ + return null; +}; + +/** + * Function: getStartSize + * + * Returns the start size of the given swimlane, that is, the width or + * height of the part that contains the title, depending on the + * horizontal style. The return value is an <mxRectangle> with either + * width or height set as appropriate. + * + * Parameters: + * + * swimlane - <mxCell> whose start size should be returned. + */ +mxGraph.prototype.getStartSize = function(swimlane) +{ + var result = new mxRectangle(); + var state = this.view.getState(swimlane); + var style = (state != null) ? state.style : this.getCellStyle(swimlane); + + if (style != null) + { + var size = parseInt(mxUtils.getValue(style, + mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE)); + + if (mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, true)) + { + result.height = size; + } + else + { + result.width = size; + } + } + + return result; +}; + +/** + * Function: getImage + * + * Returns the image URL for the given cell state. This implementation + * returns the value stored under <mxConstants.STYLE_IMAGE> in the cell + * style. + * + * Parameters: + * + * state - <mxCellState> whose image URL should be returned. + */ +mxGraph.prototype.getImage = function(state) +{ + return (state != null && state.style != null) ? + state.style[mxConstants.STYLE_IMAGE] : null; +}; + +/** + * Function: getVerticalAlign + * + * Returns the vertical alignment for the given cell state. This + * implementation returns the value stored under + * <mxConstants.STYLE_VERTICAL_ALIGN> in the cell style. + * + * Parameters: + * + * state - <mxCellState> whose vertical alignment should be + * returned. + */ +mxGraph.prototype.getVerticalAlign = function(state) +{ + return (state != null && state.style != null) ? + (state.style[mxConstants.STYLE_VERTICAL_ALIGN] || + mxConstants.ALIGN_MIDDLE ): + null; +}; + +/** + * Function: getIndicatorColor + * + * Returns the indicator color for the given cell state. This + * implementation returns the value stored under + * <mxConstants.STYLE_INDICATOR_COLOR> in the cell style. + * + * Parameters: + * + * state - <mxCellState> whose indicator color should be + * returned. + */ +mxGraph.prototype.getIndicatorColor = function(state) +{ + return (state != null && state.style != null) ? + state.style[mxConstants.STYLE_INDICATOR_COLOR] : null; +}; + +/** + * Function: getIndicatorGradientColor + * + * Returns the indicator gradient color for the given cell state. This + * implementation returns the value stored under + * <mxConstants.STYLE_INDICATOR_GRADIENTCOLOR> in the cell style. + * + * Parameters: + * + * state - <mxCellState> whose indicator gradient color should be + * returned. + */ +mxGraph.prototype.getIndicatorGradientColor = function(state) +{ + return (state != null && state.style != null) ? + state.style[mxConstants.STYLE_INDICATOR_GRADIENTCOLOR] : null; +}; + +/** + * Function: getIndicatorShape + * + * Returns the indicator shape for the given cell state. This + * implementation returns the value stored under + * <mxConstants.STYLE_INDICATOR_SHAPE> in the cell style. + * + * Parameters: + * + * state - <mxCellState> whose indicator shape should be returned. + */ +mxGraph.prototype.getIndicatorShape = function(state) +{ + return (state != null && state.style != null) ? + state.style[mxConstants.STYLE_INDICATOR_SHAPE] : null; +}; + +/** + * Function: getIndicatorImage + * + * Returns the indicator image for the given cell state. This + * implementation returns the value stored under + * <mxConstants.STYLE_INDICATOR_IMAGE> in the cell style. + * + * Parameters: + * + * state - <mxCellState> whose indicator image should be returned. + */ +mxGraph.prototype.getIndicatorImage = function(state) +{ + return (state != null && state.style != null) ? + state.style[mxConstants.STYLE_INDICATOR_IMAGE] : null; +}; + +/** + * Function: getBorder + * + * Returns the value of <border>. + */ +mxGraph.prototype.getBorder = function() +{ + return this.border; +}; + +/** + * Function: setBorder + * + * Sets the value of <border>. + * + * Parameters: + * + * value - Positive integer that represents the border to be used. + */ +mxGraph.prototype.setBorder = function(value) +{ + this.border = value; +}; + +/** + * Function: isSwimlane + * + * Returns true if the given cell is a swimlane in the graph. A swimlane is + * a container cell with some specific behaviour. This implementation + * checks if the shape associated with the given cell is a <mxSwimlane>. + * + * Parameters: + * + * cell - <mxCell> to be checked. + */ +mxGraph.prototype.isSwimlane = function (cell) +{ + if (cell != null) + { + if (this.model.getParent(cell) != this.model.getRoot()) + { + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + if (style != null && !this.model.isEdge(cell)) + { + return style[mxConstants.STYLE_SHAPE] == + mxConstants.SHAPE_SWIMLANE; + } + } + } + + return false; +}; + +/** + * Group: Graph behaviour + */ + +/** + * Function: isResizeContainer + * + * Returns <resizeContainer>. + */ +mxGraph.prototype.isResizeContainer = function() +{ + return this.resizeContainer; +}; + +/** + * Function: setResizeContainer + * + * Sets <resizeContainer>. + * + * Parameters: + * + * value - Boolean indicating if the container should be resized. + */ +mxGraph.prototype.setResizeContainer = function(value) +{ + this.resizeContainer = value; +}; + +/** + * Function: isEnabled + * + * Returns true if the graph is <enabled>. + */ +mxGraph.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Specifies if the graph should allow any interactions. This + * implementation updates <enabled>. + * + * Parameters: + * + * value - Boolean indicating if the graph should be enabled. + */ +mxGraph.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: isEscapeEnabled + * + * Returns <escapeEnabled>. + */ +mxGraph.prototype.isEscapeEnabled = function() +{ + return this.escapeEnabled; +}; + +/** + * Function: setEscapeEnabled + * + * Sets <escapeEnabled>. + * + * Parameters: + * + * enabled - Boolean indicating if escape should be enabled. + */ +mxGraph.prototype.setEscapeEnabled = function(value) +{ + this.escapeEnabled = value; +}; + +/** + * Function: isInvokesStopCellEditing + * + * Returns <invokesStopCellEditing>. + */ +mxGraph.prototype.isInvokesStopCellEditing = function() +{ + return this.invokesStopCellEditing; +}; + +/** + * Function: setInvokesStopCellEditing + * + * Sets <invokesStopCellEditing>. + */ +mxGraph.prototype.setInvokesStopCellEditing = function(value) +{ + this.invokesStopCellEditing = value; +}; + +/** + * Function: isEnterStopsCellEditing + * + * Returns <enterStopsCellEditing>. + */ +mxGraph.prototype.isEnterStopsCellEditing = function() +{ + return this.enterStopsCellEditing; +}; + +/** + * Function: setEnterStopsCellEditing + * + * Sets <enterStopsCellEditing>. + */ +mxGraph.prototype.setEnterStopsCellEditing = function(value) +{ + this.enterStopsCellEditing = value; +}; + +/** + * Function: isCellLocked + * + * Returns true if the given cell may not be moved, sized, bended, + * disconnected, edited or selected. This implementation returns true for + * all vertices with a relative geometry if <locked> is false. + * + * Parameters: + * + * cell - <mxCell> whose locked state should be returned. + */ +mxGraph.prototype.isCellLocked = function(cell) +{ + var geometry = this.model.getGeometry(cell); + + return this.isCellsLocked() || (geometry != null && + this.model.isVertex(cell) && geometry.relative); +}; + +/** + * Function: isCellsLocked + * + * Returns true if the given cell may not be moved, sized, bended, + * disconnected, edited or selected. This implementation returns true for + * all vertices with a relative geometry if <locked> is false. + * + * Parameters: + * + * cell - <mxCell> whose locked state should be returned. + */ +mxGraph.prototype.isCellsLocked = function() +{ + return this.cellsLocked; +}; + +/** + * Function: setLocked + * + * Sets if any cell may be moved, sized, bended, disconnected, edited or + * selected. + * + * Parameters: + * + * value - Boolean that defines the new value for <cellsLocked>. + */ +mxGraph.prototype.setCellsLocked = function(value) +{ + this.cellsLocked = value; +}; + +/** + * Function: getCloneableCells + * + * Returns the cells which may be exported in the given array of cells. + */ +mxGraph.prototype.getCloneableCells = function(cells) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.isCellCloneable(cell); + })); +}; + +/** + * Function: isCellCloneable + * + * Returns true if the given cell is cloneable. This implementation returns + * <isCellsCloneable> for all cells unless a cell style specifies + * <mxConstants.STYLE_CLONEABLE> to be 0. + * + * Parameters: + * + * cell - Optional <mxCell> whose cloneable state should be returned. + */ +mxGraph.prototype.isCellCloneable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsCloneable() && style[mxConstants.STYLE_CLONEABLE] != 0; +}; + +/** + * Function: isCellsCloneable + * + * Returns <cellsCloneable>, that is, if the graph allows cloning of cells + * by using control-drag. + */ +mxGraph.prototype.isCellsCloneable = function() +{ + return this.cellsCloneable; +}; + +/** + * Function: setCellsCloneable + * + * Specifies if the graph should allow cloning of cells by holding down the + * control key while cells are being moved. This implementation updates + * <cellsCloneable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should be cloneable. + */ +mxGraph.prototype.setCellsCloneable = function(value) +{ + this.cellsCloneable = value; +}; + +/** + * Function: getExportableCells + * + * Returns the cells which may be exported in the given array of cells. + */ +mxGraph.prototype.getExportableCells = function(cells) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.canExportCell(cell); + })); +}; + +/** + * Function: canExportCell + * + * Returns true if the given cell may be exported to the clipboard. This + * implementation returns <exportEnabled> for all cells. + * + * Parameters: + * + * cell - <mxCell> that represents the cell to be exported. + */ +mxGraph.prototype.canExportCell = function(cell) +{ + return this.exportEnabled; +}; + +/** + * Function: getImportableCells + * + * Returns the cells which may be imported in the given array of cells. + */ +mxGraph.prototype.getImportableCells = function(cells) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.canImportCell(cell); + })); +}; + +/** + * Function: canImportCell + * + * Returns true if the given cell may be imported from the clipboard. + * This implementation returns <importEnabled> for all cells. + * + * Parameters: + * + * cell - <mxCell> that represents the cell to be imported. + */ +mxGraph.prototype.canImportCell = function(cell) +{ + return this.importEnabled; +}; + +/** + * Function: isCellSelectable + * + * Returns true if the given cell is selectable. This implementation + * returns <cellsSelectable>. + * + * To add a new style for making cells (un)selectable, use the following code. + * + * (code) + * mxGraph.prototype.isCellSelectable = function(cell) + * { + * var state = this.view.getState(cell); + * var style = (state != null) ? state.style : this.getCellStyle(cell); + * + * return this.isCellsSelectable() && !this.isCellLocked(cell) && style['selectable'] != 0; + * }; + * (end) + * + * You can then use the new style as shown in this example. + * + * (code) + * graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'selectable=0'); + * (end) + * + * Parameters: + * + * cell - <mxCell> whose selectable state should be returned. + */ +mxGraph.prototype.isCellSelectable = function(cell) +{ + return this.isCellsSelectable(); +}; + +/** + * Function: isCellsSelectable + * + * Returns <cellsSelectable>. + */ +mxGraph.prototype.isCellsSelectable = function() +{ + return this.cellsSelectable; +}; + +/** + * Function: setCellsSelectable + * + * Sets <cellsSelectable>. + */ +mxGraph.prototype.setCellsSelectable = function(value) +{ + this.cellsSelectable = value; +}; + +/** + * Function: getDeletableCells + * + * Returns the cells which may be exported in the given array of cells. + */ +mxGraph.prototype.getDeletableCells = function(cells) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.isCellDeletable(cell); + })); +}; + +/** + * Function: isCellDeletable + * + * Returns true if the given cell is moveable. This returns + * <cellsDeletable> for all given cells if a cells style does not specify + * <mxConstants.STYLE_DELETABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose deletable state should be returned. + */ +mxGraph.prototype.isCellDeletable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsDeletable() && style[mxConstants.STYLE_DELETABLE] != 0; +}; + +/** + * Function: isCellsDeletable + * + * Returns <cellsDeletable>. + */ +mxGraph.prototype.isCellsDeletable = function() +{ + return this.cellsDeletable; +}; + +/** + * Function: setCellsDeletable + * + * Sets <cellsDeletable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should allow deletion of cells. + */ +mxGraph.prototype.setCellsDeletable = function(value) +{ + this.cellsDeletable = value; +}; + +/** + * Function: isLabelMovable + * + * Returns true if the given edges's label is moveable. This returns + * <movable> for all given cells if <isLocked> does not return true + * for the given cell. + * + * Parameters: + * + * cell - <mxCell> whose label should be moved. + */ +mxGraph.prototype.isLabelMovable = function(cell) +{ + return !this.isCellLocked(cell) && + ((this.model.isEdge(cell) && this.edgeLabelsMovable) || + (this.model.isVertex(cell) && this.vertexLabelsMovable)); +}; + +/** + * Function: getMovableCells + * + * Returns the cells which are movable in the given array of cells. + */ +mxGraph.prototype.getMovableCells = function(cells) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.isCellMovable(cell); + })); +}; + +/** + * Function: isCellMovable + * + * Returns true if the given cell is moveable. This returns <cellsMovable> + * for all given cells if <isCellLocked> does not return true for the given + * cell and its style does not specify <mxConstants.STYLE_MOVABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose movable state should be returned. + */ +mxGraph.prototype.isCellMovable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsMovable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_MOVABLE] != 0; +}; + +/** + * Function: isCellsMovable + * + * Returns <cellsMovable>. + */ +mxGraph.prototype.isCellsMovable = function() +{ + return this.cellsMovable; +}; + +/** + * Function: setCellsMovable + * + * Specifies if the graph should allow moving of cells. This implementation + * updates <cellsMsovable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should allow moving of cells. + */ +mxGraph.prototype.setCellsMovable = function(value) +{ + this.cellsMovable = value; +}; + +/** + * Function: isGridEnabled + * + * Returns <gridEnabled> as a boolean. + */ +mxGraph.prototype.isGridEnabled = function() +{ + return this.gridEnabled; +}; + +/** + * Function: setGridEnabled + * + * Specifies if the grid should be enabled. + * + * Parameters: + * + * value - Boolean indicating if the grid should be enabled. + */ +mxGraph.prototype.setGridEnabled = function(value) +{ + this.gridEnabled = value; +}; + +/** + * Function: isPortsEnabled + * + * Returns <portsEnabled> as a boolean. + */ +mxGraph.prototype.isPortsEnabled = function() +{ + return this.portsEnabled; +}; + +/** + * Function: setPortsEnabled + * + * Specifies if the ports should be enabled. + * + * Parameters: + * + * value - Boolean indicating if the ports should be enabled. + */ +mxGraph.prototype.setPortsEnabled = function(value) +{ + this.portsEnabled = value; +}; + +/** + * Function: getGridSize + * + * Returns <gridSize>. + */ +mxGraph.prototype.getGridSize = function() +{ + return this.gridSize; +}; + +/** + * Function: setGridSize + * + * Sets <gridSize>. + */ +mxGraph.prototype.setGridSize = function(value) +{ + this.gridSize = value; +}; + +/** + * Function: getTolerance + * + * Returns <tolerance>. + */ +mxGraph.prototype.getTolerance = function() +{ + return this.tolerance; +}; + +/** + * Function: setTolerance + * + * Sets <tolerance>. + */ +mxGraph.prototype.setTolerance = function(value) +{ + this.tolerance = value; +}; + +/** + * Function: isVertexLabelsMovable + * + * Returns <vertexLabelsMovable>. + */ +mxGraph.prototype.isVertexLabelsMovable = function() +{ + return this.vertexLabelsMovable; +}; + +/** + * Function: setVertexLabelsMovable + * + * Sets <vertexLabelsMovable>. + */ +mxGraph.prototype.setVertexLabelsMovable = function(value) +{ + this.vertexLabelsMovable = value; +}; + +/** + * Function: isEdgeLabelsMovable + * + * Returns <edgeLabelsMovable>. + */ +mxGraph.prototype.isEdgeLabelsMovable = function() +{ + return this.edgeLabelsMovable; +}; + +/** + * Function: isEdgeLabelsMovable + * + * Sets <edgeLabelsMovable>. + */ +mxGraph.prototype.setEdgeLabelsMovable = function(value) +{ + this.edgeLabelsMovable = value; +}; + +/** + * Function: isSwimlaneNesting + * + * Returns <swimlaneNesting> as a boolean. + */ +mxGraph.prototype.isSwimlaneNesting = function() +{ + return this.swimlaneNesting; +}; + +/** + * Function: setSwimlaneNesting + * + * Specifies if swimlanes can be nested by drag and drop. This is only + * taken into account if dropEnabled is true. + * + * Parameters: + * + * value - Boolean indicating if swimlanes can be nested. + */ +mxGraph.prototype.setSwimlaneNesting = function(value) +{ + this.swimlaneNesting = value; +}; + +/** + * Function: isSwimlaneSelectionEnabled + * + * Returns <swimlaneSelectionEnabled> as a boolean. + */ +mxGraph.prototype.isSwimlaneSelectionEnabled = function() +{ + return this.swimlaneSelectionEnabled; +}; + +/** + * Function: setSwimlaneSelectionEnabled + * + * Specifies if swimlanes should be selected if the mouse is released + * over their content area. + * + * Parameters: + * + * value - Boolean indicating if swimlanes content areas + * should be selected when the mouse is released over them. + */ +mxGraph.prototype.setSwimlaneSelectionEnabled = function(value) +{ + this.swimlaneSelectionEnabled = value; +}; + +/** + * Function: isMultigraph + * + * Returns <multigraph> as a boolean. + */ +mxGraph.prototype.isMultigraph = function() +{ + return this.multigraph; +}; + +/** + * Function: setMultigraph + * + * Specifies if the graph should allow multiple connections between the + * same pair of vertices. + * + * Parameters: + * + * value - Boolean indicating if the graph allows multiple connections + * between the same pair of vertices. + */ +mxGraph.prototype.setMultigraph = function(value) +{ + this.multigraph = value; +}; + +/** + * Function: isAllowLoops + * + * Returns <allowLoops> as a boolean. + */ +mxGraph.prototype.isAllowLoops = function() +{ + return this.allowLoops; +}; + +/** + * Function: setAllowDanglingEdges + * + * Specifies if dangling edges are allowed, that is, if edges are allowed + * that do not have a source and/or target terminal defined. + * + * Parameters: + * + * value - Boolean indicating if dangling edges are allowed. + */ +mxGraph.prototype.setAllowDanglingEdges = function(value) +{ + this.allowDanglingEdges = value; +}; + +/** + * Function: isAllowDanglingEdges + * + * Returns <allowDanglingEdges> as a boolean. + */ +mxGraph.prototype.isAllowDanglingEdges = function() +{ + return this.allowDanglingEdges; +}; + +/** + * Function: setConnectableEdges + * + * Specifies if edges should be connectable. + * + * Parameters: + * + * value - Boolean indicating if edges should be connectable. + */ +mxGraph.prototype.setConnectableEdges = function(value) +{ + this.connectableEdges = value; +}; + +/** + * Function: isConnectableEdges + * + * Returns <connectableEdges> as a boolean. + */ +mxGraph.prototype.isConnectableEdges = function() +{ + return this.connectableEdges; +}; + +/** + * Function: setCloneInvalidEdges + * + * Specifies if edges should be inserted when cloned but not valid wrt. + * <getEdgeValidationError>. If false such edges will be silently ignored. + * + * Parameters: + * + * value - Boolean indicating if cloned invalid edges should be + * inserted into the graph or ignored. + */ +mxGraph.prototype.setCloneInvalidEdges = function(value) +{ + this.cloneInvalidEdges = value; +}; + +/** + * Function: isCloneInvalidEdges + * + * Returns <cloneInvalidEdges> as a boolean. + */ +mxGraph.prototype.isCloneInvalidEdges = function() +{ + return this.cloneInvalidEdges; +}; + +/** + * Function: setAllowLoops + * + * Specifies if loops are allowed. + * + * Parameters: + * + * value - Boolean indicating if loops are allowed. + */ +mxGraph.prototype.setAllowLoops = function(value) +{ + this.allowLoops = value; +}; + +/** + * Function: isDisconnectOnMove + * + * Returns <disconnectOnMove> as a boolean. + */ +mxGraph.prototype.isDisconnectOnMove = function() +{ + return this.disconnectOnMove; +}; + +/** + * Function: setDisconnectOnMove + * + * Specifies if edges should be disconnected when moved. (Note: Cloned + * edges are always disconnected.) + * + * Parameters: + * + * value - Boolean indicating if edges should be disconnected + * when moved. + */ +mxGraph.prototype.setDisconnectOnMove = function(value) +{ + this.disconnectOnMove = value; +}; + +/** + * Function: isDropEnabled + * + * Returns <dropEnabled> as a boolean. + */ +mxGraph.prototype.isDropEnabled = function() +{ + return this.dropEnabled; +}; + +/** + * Function: setDropEnabled + * + * Specifies if the graph should allow dropping of cells onto or into other + * cells. + * + * Parameters: + * + * dropEnabled - Boolean indicating if the graph should allow dropping + * of cells into other cells. + */ +mxGraph.prototype.setDropEnabled = function(value) +{ + this.dropEnabled = value; +}; + +/** + * Function: isSplitEnabled + * + * Returns <splitEnabled> as a boolean. + */ +mxGraph.prototype.isSplitEnabled = function() +{ + return this.splitEnabled; +}; + +/** + * Function: setSplitEnabled + * + * Specifies if the graph should allow dropping of cells onto or into other + * cells. + * + * Parameters: + * + * dropEnabled - Boolean indicating if the graph should allow dropping + * of cells into other cells. + */ +mxGraph.prototype.setSplitEnabled = function(value) +{ + this.splitEnabled = value; +}; + +/** + * Function: isCellResizable + * + * Returns true if the given cell is resizable. This returns + * <cellsResizable> for all given cells if <isCellLocked> does not return + * true for the given cell and its style does not specify + * <mxConstants.STYLE_RESIZABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose resizable state should be returned. + */ +mxGraph.prototype.isCellResizable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsResizable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_RESIZABLE] != 0; +}; + +/** + * Function: isCellsResizable + * + * Returns <cellsResizable>. + */ +mxGraph.prototype.isCellsResizable = function() +{ + return this.cellsResizable; +}; + +/** + * Function: setCellsResizable + * + * Specifies if the graph should allow resizing of cells. This + * implementation updates <cellsResizable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should allow resizing of + * cells. + */ +mxGraph.prototype.setCellsResizable = function(value) +{ + this.cellsResizable = value; +}; + +/** + * Function: isTerminalPointMovable + * + * Returns true if the given terminal point is movable. This is independent + * from <isCellConnectable> and <isCellDisconnectable> and controls if terminal + * points can be moved in the graph if the edge is not connected. Note that it + * is required for this to return true to connect unconnected edges. This + * implementation returns true. + * + * Parameters: + * + * cell - <mxCell> whose terminal point should be moved. + * source - Boolean indicating if the source or target terminal should be moved. + */ +mxGraph.prototype.isTerminalPointMovable = function(cell, source) +{ + return true; +}; + +/** + * Function: isCellBendable + * + * Returns true if the given cell is bendable. This returns <cellsBendable> + * for all given cells if <isLocked> does not return true for the given + * cell and its style does not specify <mxConstants.STYLE_BENDABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose bendable state should be returned. + */ +mxGraph.prototype.isCellBendable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsBendable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_BENDABLE] != 0; +}; + +/** + * Function: isCellsBendable + * + * Returns <cellsBenadable>. + */ +mxGraph.prototype.isCellsBendable = function() +{ + return this.cellsBendable; +}; + +/** + * Function: setCellsBendable + * + * Specifies if the graph should allow bending of edges. This + * implementation updates <bendable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should allow bending of + * edges. + */ +mxGraph.prototype.setCellsBendable = function(value) +{ + this.cellsBendable = value; +}; + +/** + * Function: isCellEditable + * + * Returns true if the given cell is editable. This returns <cellsEditable> for + * all given cells if <isCellLocked> does not return true for the given cell + * and its style does not specify <mxConstants.STYLE_EDITABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose editable state should be returned. + */ +mxGraph.prototype.isCellEditable = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isCellsEditable() && !this.isCellLocked(cell) && style[mxConstants.STYLE_EDITABLE] != 0; +}; + +/** + * Function: isCellsEditable + * + * Returns <cellsEditable>. + */ +mxGraph.prototype.isCellsEditable = function() +{ + return this.cellsEditable; +}; + +/** + * Function: setCellsEditable + * + * Specifies if the graph should allow in-place editing for cell labels. + * This implementation updates <cellsEditable>. + * + * Parameters: + * + * value - Boolean indicating if the graph should allow in-place + * editing. + */ +mxGraph.prototype.setCellsEditable = function(value) +{ + this.cellsEditable = value; +}; + +/** + * Function: isCellDisconnectable + * + * Returns true if the given cell is disconnectable from the source or + * target terminal. This returns <isCellsDisconnectable> for all given + * cells if <isCellLocked> does not return true for the given cell. + * + * Parameters: + * + * cell - <mxCell> whose disconnectable state should be returned. + * terminal - <mxCell> that represents the source or target terminal. + * source - Boolean indicating if the source or target terminal is to be + * disconnected. + */ +mxGraph.prototype.isCellDisconnectable = function(cell, terminal, source) +{ + return this.isCellsDisconnectable() && !this.isCellLocked(cell); +}; + +/** + * Function: isCellsDisconnectable + * + * Returns <cellsDisconnectable>. + */ +mxGraph.prototype.isCellsDisconnectable = function() +{ + return this.cellsDisconnectable; +}; + +/** + * Function: setCellsDisconnectable + * + * Sets <cellsDisconnectable>. + */ +mxGraph.prototype.setCellsDisconnectable = function(value) +{ + this.cellsDisconnectable = value; +}; + +/** + * Function: isValidSource + * + * Returns true if the given cell is a valid source for new connections. + * This implementation returns true for all non-null values and is + * called by is called by <isValidConnection>. + * + * Parameters: + * + * cell - <mxCell> that represents a possible source or null. + */ +mxGraph.prototype.isValidSource = function(cell) +{ + return (cell == null && this.allowDanglingEdges) || + (cell != null && (!this.model.isEdge(cell) || + this.connectableEdges) && this.isCellConnectable(cell)); +}; + +/** + * Function: isValidTarget + * + * Returns <isValidSource> for the given cell. This is called by + * <isValidConnection>. + * + * Parameters: + * + * cell - <mxCell> that represents a possible target or null. + */ +mxGraph.prototype.isValidTarget = function(cell) +{ + return this.isValidSource(cell); +}; + +/** + * Function: isValidConnection + * + * Returns true if the given target cell is a valid target for source. + * This is a boolean implementation for not allowing connections between + * certain pairs of vertices and is called by <getEdgeValidationError>. + * This implementation returns true if <isValidSource> returns true for + * the source and <isValidTarget> returns true for the target. + * + * Parameters: + * + * source - <mxCell> that represents the source cell. + * target - <mxCell> that represents the target cell. + */ +mxGraph.prototype.isValidConnection = function(source, target) +{ + return this.isValidSource(source) && this.isValidTarget(target); +}; + +/** + * Function: setConnectable + * + * Specifies if the graph should allow new connections. This implementation + * updates <mxConnectionHandler.enabled> in <connectionHandler>. + * + * Parameters: + * + * connectable - Boolean indicating if new connections should be allowed. + */ +mxGraph.prototype.setConnectable = function(connectable) +{ + this.connectionHandler.setEnabled(connectable); +}; + +/** + * Function: isConnectable + * + * Returns true if the <connectionHandler> is enabled. + */ +mxGraph.prototype.isConnectable = function(connectable) +{ + return this.connectionHandler.isEnabled(); +}; + +/** + * Function: setTooltips + * + * Specifies if tooltips should be enabled. This implementation updates + * <mxTooltipHandler.enabled> in <tooltipHandler>. + * + * Parameters: + * + * enabled - Boolean indicating if tooltips should be enabled. + */ +mxGraph.prototype.setTooltips = function (enabled) +{ + this.tooltipHandler.setEnabled(enabled); +}; + +/** + * Function: setPanning + * + * Specifies if panning should be enabled. This implementation updates + * <mxPanningHandler.panningEnabled> in <panningHandler>. + * + * Parameters: + * + * enabled - Boolean indicating if panning should be enabled. + */ +mxGraph.prototype.setPanning = function(enabled) +{ + this.panningHandler.panningEnabled = enabled; +}; + +/** + * Function: isEditing + * + * Returns true if the given cell is currently being edited. + * If no cell is specified then this returns true if any + * cell is currently being edited. + * + * Parameters: + * + * cell - <mxCell> that should be checked. + */ +mxGraph.prototype.isEditing = function(cell) +{ + if (this.cellEditor != null) + { + var editingCell = this.cellEditor.getEditingCell(); + + return (cell == null) ? + editingCell != null : + cell == editingCell; + } + + return false; +}; + +/** + * Function: isAutoSizeCell + * + * Returns true if the size of the given cell should automatically be + * updated after a change of the label. This implementation returns + * <autoSizeCells> or checks if the cell style does specify + * <mxConstants.STYLE_AUTOSIZE> to be 1. + * + * Parameters: + * + * cell - <mxCell> that should be resized. + */ +mxGraph.prototype.isAutoSizeCell = function(cell) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.isAutoSizeCells() || style[mxConstants.STYLE_AUTOSIZE] == 1; +}; + +/** + * Function: isAutoSizeCells + * + * Returns <autoSizeCells>. + */ +mxGraph.prototype.isAutoSizeCells = function() +{ + return this.autoSizeCells; +}; + +/** + * Function: setAutoSizeCells + * + * Specifies if cell sizes should be automatically updated after a label + * change. This implementation sets <autoSizeCells> to the given parameter. + * + * Parameters: + * + * value - Boolean indicating if cells should be resized + * automatically. + */ +mxGraph.prototype.setAutoSizeCells = function(value) +{ + this.autoSizeCells = value; +}; + +/** + * Function: isExtendParent + * + * Returns true if the parent of the given cell should be extended if the + * child has been resized so that it overlaps the parent. This + * implementation returns <isExtendParents> if the cell is not an edge. + * + * Parameters: + * + * cell - <mxCell> that has been resized. + */ +mxGraph.prototype.isExtendParent = function(cell) +{ + return !this.getModel().isEdge(cell) && this.isExtendParents(); +}; + +/** + * Function: isExtendParents + * + * Returns <extendParents>. + */ +mxGraph.prototype.isExtendParents = function() +{ + return this.extendParents; +}; + +/** + * Function: setExtendParents + * + * Sets <extendParents>. + * + * Parameters: + * + * value - New boolean value for <extendParents>. + */ +mxGraph.prototype.setExtendParents = function(value) +{ + this.extendParents = value; +}; + +/** + * Function: isExtendParentsOnAdd + * + * Returns <extendParentsOnAdd>. + */ +mxGraph.prototype.isExtendParentsOnAdd = function() +{ + return this.extendParentsOnAdd; +}; + +/** + * Function: setExtendParentsOnAdd + * + * Sets <extendParentsOnAdd>. + * + * Parameters: + * + * value - New boolean value for <extendParentsOnAdd>. + */ +mxGraph.prototype.setExtendParentsOnAdd = function(value) +{ + this.extendParentsOnAdd = value; +}; + +/** + * Function: isConstrainChild + * + * Returns true if the given cell should be kept inside the bounds of its + * parent according to the rules defined by <getOverlap> and + * <isAllowOverlapParent>. This implementation returns false for all children + * of edges and <isConstrainChildren> otherwise. + * + * Parameters: + * + * cell - <mxCell> that should be constrained. + */ +mxGraph.prototype.isConstrainChild = function(cell) +{ + return this.isConstrainChildren() && !this.getModel().isEdge(this.getModel().getParent(cell)); + +}; + +/** + * Function: isConstrainChildren + * + * Returns <constrainChildren>. + */ +mxGraph.prototype.isConstrainChildren = function() +{ + return this.constrainChildren; +}; + +/** + * Function: setConstrainChildren + * + * Sets <constrainChildren>. + */ +mxGraph.prototype.setConstrainChildren = function(value) +{ + this.constrainChildren = value; +}; + +/** + * Function: isConstrainChildren + * + * Returns <allowNegativeCoordinates>. + */ +mxGraph.prototype.isAllowNegativeCoordinates = function() +{ + return this.allowNegativeCoordinates; +}; + +/** + * Function: setConstrainChildren + * + * Sets <allowNegativeCoordinates>. + */ +mxGraph.prototype.setAllowNegativeCoordinates = function(value) +{ + this.allowNegativeCoordinates = value; +}; + +/** + * Function: getOverlap + * + * Returns a decimal number representing the amount of the width and height + * of the given cell that is allowed to overlap its parent. A value of 0 + * means all children must stay inside the parent, 1 means the child is + * allowed to be placed outside of the parent such that it touches one of + * the parents sides. If <isAllowOverlapParent> returns false for the given + * cell, then this method returns 0. + * + * Parameters: + * + * cell - <mxCell> for which the overlap ratio should be returned. + */ +mxGraph.prototype.getOverlap = function(cell) +{ + return (this.isAllowOverlapParent(cell)) ? this.defaultOverlap : 0; +}; + +/** + * Function: isAllowOverlapParent + * + * Returns true if the given cell is allowed to be placed outside of the + * parents area. + * + * Parameters: + * + * cell - <mxCell> that represents the child to be checked. + */ +mxGraph.prototype.isAllowOverlapParent = function(cell) +{ + return false; +}; + +/** + * Function: getFoldableCells + * + * Returns the cells which are movable in the given array of cells. + */ +mxGraph.prototype.getFoldableCells = function(cells, collapse) +{ + return this.model.filterCells(cells, mxUtils.bind(this, function(cell) + { + return this.isCellFoldable(cell, collapse); + })); +}; + +/** + * Function: isCellFoldable + * + * Returns true if the given cell is foldable. This implementation + * returns true if the cell has at least one child and its style + * does not specify <mxConstants.STYLE_FOLDABLE> to be 0. + * + * Parameters: + * + * cell - <mxCell> whose foldable state should be returned. + */ +mxGraph.prototype.isCellFoldable = function(cell, collapse) +{ + var state = this.view.getState(cell); + var style = (state != null) ? state.style : this.getCellStyle(cell); + + return this.model.getChildCount(cell) > 0 && style[mxConstants.STYLE_FOLDABLE] != 0; +}; + +/** + * Function: isValidDropTarget + * + * Returns true if the given cell is a valid drop target for the specified + * cells. If the given cell is an edge, then <isSplitDropTarget> is used, + * else <isParentDropTarget> is used to compute the return value. + * + * Parameters: + * + * cell - <mxCell> that represents the possible drop target. + * cells - <mxCells> that should be dropped into the target. + * evt - Mouseevent that triggered the invocation. + */ +mxGraph.prototype.isValidDropTarget = function(cell, cells, evt) +{ + return cell != null && ((this.isSplitEnabled() && + this.isSplitTarget(cell, cells, evt)) || (!this.model.isEdge(cell) && + (this.isSwimlane(cell) || (this.model.getChildCount(cell) > 0 && + !this.isCellCollapsed(cell))))); +}; + +/** + * Function: isSplitTarget + * + * Returns true if the given edge may be splitted into two edges with the + * given cell as a new terminal between the two. + * + * Parameters: + * + * target - <mxCell> that represents the edge to be splitted. + * cells - <mxCells> that should split the edge. + * evt - Mouseevent that triggered the invocation. + */ +mxGraph.prototype.isSplitTarget = function(target, cells, evt) +{ + if (this.model.isEdge(target) && cells != null && cells.length == 1 && + this.isCellConnectable(cells[0]) && this.getEdgeValidationError(target, + this.model.getTerminal(target, true), cells[0]) == null) + { + var src = this.model.getTerminal(target, true); + var trg = this.model.getTerminal(target, false); + + return (!this.model.isAncestor(cells[0], src) && + !this.model.isAncestor(cells[0], trg)); + } + + return false; +}; + +/** + * Function: getDropTarget + * + * Returns the given cell if it is a drop target for the given cells or the + * nearest ancestor that may be used as a drop target for the given cells. + * If the given array contains a swimlane and <swimlaneNesting> is false + * then this always returns null. If no cell is given, then the bottommost + * swimlane at the location of the given event is returned. + * + * This function should only be used if <isDropEnabled> returns true. + * + * Parameters: + * + * cells - Array of <mxCells> which are to be dropped onto the target. + * evt - Mouseevent for the drag and drop. + * cell - <mxCell> that is under the mousepointer. + */ +mxGraph.prototype.getDropTarget = function(cells, evt, cell) +{ + if (!this.isSwimlaneNesting()) + { + for (var i = 0; i < cells.length; i++) + { + if (this.isSwimlane(cells[i])) + { + return null; + } + } + } + + var pt = mxUtils.convertPoint(this.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + pt.x -= this.panDx; + pt.y -= this.panDy; + var swimlane = this.getSwimlaneAt(pt.x, pt.y); + + if (cell == null) + { + cell = swimlane; + } + else if (swimlane != null) + { + // Checks if the cell is an ancestor of the swimlane + // under the mouse and uses the swimlane in that case + var tmp = this.model.getParent(swimlane); + + while (tmp != null && this.isSwimlane(tmp) && tmp != cell) + { + tmp = this.model.getParent(tmp); + } + + if (tmp == cell) + { + cell = swimlane; + } + } + + while (cell != null && !this.isValidDropTarget(cell, cells, evt) && + !this.model.isLayer(cell)) + { + cell = this.model.getParent(cell); + } + + return (!this.model.isLayer(cell) && mxUtils.indexOf(cells, cell) < 0) ? cell : null; +}; + +/** + * Group: Cell retrieval + */ + +/** + * Function: getDefaultParent + * + * Returns <defaultParent> or <mxGraphView.currentRoot> or the first child + * child of <mxGraphModel.root> if both are null. The value returned by + * this function should be used as the parent for new cells (aka default + * layer). + */ +mxGraph.prototype.getDefaultParent = function() +{ + var parent = this.defaultParent; + + if (parent == null) + { + parent = this.getCurrentRoot(); + + if (parent == null) + { + var root = this.model.getRoot(); + parent = this.model.getChildAt(root, 0); + } + } + + return parent; +}; + +/** + * Function: setDefaultParent + * + * Sets the <defaultParent> to the given cell. Set this to null to return + * the first child of the root in getDefaultParent. + */ +mxGraph.prototype.setDefaultParent = function(cell) +{ + this.defaultParent = cell; +}; + +/** + * Function: getSwimlane + * + * Returns the nearest ancestor of the given cell which is a swimlane, or + * the given cell, if it is itself a swimlane. + * + * Parameters: + * + * cell - <mxCell> for which the ancestor swimlane should be returned. + */ +mxGraph.prototype.getSwimlane = function(cell) +{ + while (cell != null && !this.isSwimlane(cell)) + { + cell = this.model.getParent(cell); + } + + return cell; +}; + +/** + * Function: getSwimlaneAt + * + * Returns the bottom-most swimlane that intersects the given point (x, y) + * in the cell hierarchy that starts at the given parent. + * + * Parameters: + * + * x - X-coordinate of the location to be checked. + * y - Y-coordinate of the location to be checked. + * parent - <mxCell> that should be used as the root of the recursion. + * Default is <defaultParent>. + */ +mxGraph.prototype.getSwimlaneAt = function (x, y, parent) +{ + parent = parent || this.getDefaultParent(); + + if (parent != null) + { + var childCount = this.model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var child = this.model.getChildAt(parent, i); + var result = this.getSwimlaneAt(x, y, child); + + if (result != null) + { + return result; + } + else if (this.isSwimlane(child)) + { + var state = this.view.getState(child); + + if (this.intersects(state, x, y)) + { + return child; + } + } + } + } + + return null; +}; + +/** + * Function: getCellAt + * + * Returns the bottom-most cell that intersects the given point (x, y) in + * the cell hierarchy starting at the given parent. This will also return + * swimlanes if the given location intersects the content area of the + * swimlane. If this is not desired, then the <hitsSwimlaneContent> may be + * used if the returned cell is a swimlane to determine if the location + * is inside the content area or on the actual title of the swimlane. + * + * Parameters: + * + * x - X-coordinate of the location to be checked. + * y - Y-coordinate of the location to be checked. + * parent - <mxCell> that should be used as the root of the recursion. + * Default is <defaultParent>. + * vertices - Optional boolean indicating if vertices should be returned. + * Default is true. + * edges - Optional boolean indicating if edges should be returned. Default + * is true. + */ +mxGraph.prototype.getCellAt = function(x, y, parent, vertices, edges) +{ + vertices = (vertices != null) ? vertices : true; + edges = (edges != null) ? edges : true; + parent = (parent != null) ? parent : this.getDefaultParent(); + + if (parent != null) + { + var childCount = this.model.getChildCount(parent); + + for (var i = childCount - 1; i >= 0; i--) + { + var cell = this.model.getChildAt(parent, i); + var result = this.getCellAt(x, y, cell, vertices, edges); + + if (result != null) + { + return result; + } + else if (this.isCellVisible(cell) && (edges && this.model.isEdge(cell) || + vertices && this.model.isVertex(cell))) + { + var state = this.view.getState(cell); + + if (this.intersects(state, x, y)) + { + return cell; + } + } + } + } + + return null; +}; + +/** + * Function: intersects + * + * Returns the bottom-most cell that intersects the given point (x, y) in + * the cell hierarchy that starts at the given parent. + * + * Parameters: + * + * state - <mxCellState> that represents the cell state. + * x - X-coordinate of the location to be checked. + * y - Y-coordinate of the location to be checked. + */ +mxGraph.prototype.intersects = function(state, x, y) +{ + if (state != null) + { + var pts = state.absolutePoints; + + if (pts != null) + { + var t2 = this.tolerance * this.tolerance; + + var pt = pts[0]; + + for (var i = 1; i<pts.length; i++) + { + var next = pts[i]; + var dist = mxUtils.ptSegDistSq( + pt.x, pt.y, next.x, next.y, x, y); + + if (dist <= t2) + { + return true; + } + + pt = next; + } + } + else if (mxUtils.contains(state, x, y)) + { + return true; + } + } + + return false; +}; + +/** + * Function: hitsSwimlaneContent + * + * Returns true if the given coordinate pair is inside the content + * are of the given swimlane. + * + * Parameters: + * + * swimlane - <mxCell> that specifies the swimlane. + * x - X-coordinate of the mouse event. + * y - Y-coordinate of the mouse event. + */ +mxGraph.prototype.hitsSwimlaneContent = function(swimlane, x, y) +{ + var state = this.getView().getState(swimlane); + var size = this.getStartSize(swimlane); + + if (state != null) + { + var scale = this.getView().getScale(); + x -= state.x; + y -= state.y; + + if (size.width > 0 && x > 0 && x > size.width * scale) + { + return true; + } + else if (size.height > 0 && y > 0 && y > size.height * scale) + { + return true; + } + } + + return false; +}; + +/** + * Function: getChildVertices + * + * Returns the visible child vertices of the given parent. + * + * Parameters: + * + * parent - <mxCell> whose children should be returned. + */ +mxGraph.prototype.getChildVertices = function(parent) +{ + return this.getChildCells(parent, true, false); +}; + +/** + * Function: getChildEdges + * + * Returns the visible child edges of the given parent. + * + * Parameters: + * + * parent - <mxCell> whose child vertices should be returned. + */ +mxGraph.prototype.getChildEdges = function(parent) +{ + return this.getChildCells(parent, false, true); +}; + +/** + * Function: getChildCells + * + * Returns the visible child vertices or edges in the given parent. If + * vertices and edges is false, then all children are returned. + * + * Parameters: + * + * parent - <mxCell> whose children should be returned. + * vertices - Optional boolean that specifies if child vertices should + * be returned. Default is false. + * edges - Optional boolean that specifies if child edges should + * be returned. Default is false. + */ +mxGraph.prototype.getChildCells = function(parent, vertices, edges) +{ + parent = (parent != null) ? parent : this.getDefaultParent(); + vertices = (vertices != null) ? vertices : false; + edges = (edges != null) ? edges : false; + + var cells = this.model.getChildCells(parent, vertices, edges); + var result = []; + + // Filters out the non-visible child cells + for (var i = 0; i < cells.length; i++) + { + if (this.isCellVisible(cells[i])) + { + result.push(cells[i]); + } + } + + return result; +}; + +/** + * Function: getConnections + * + * Returns all visible edges connected to the given cell without loops. + * + * Parameters: + * + * cell - <mxCell> whose connections should be returned. + * parent - Optional parent of the opposite end for a connection to be + * returned. + */ +mxGraph.prototype.getConnections = function(cell, parent) +{ + return this.getEdges(cell, parent, true, true, false); +}; + +/** + * Function: getIncomingEdges + * + * Returns the visible incoming edges for the given cell. If the optional + * parent argument is specified, then only child edges of the given parent + * are returned. + * + * Parameters: + * + * cell - <mxCell> whose incoming edges should be returned. + * parent - Optional parent of the opposite end for an edge to be + * returned. + */ +mxGraph.prototype.getIncomingEdges = function(cell, parent) +{ + return this.getEdges(cell, parent, true, false, false); +}; + +/** + * Function: getOutgoingEdges + * + * Returns the visible outgoing edges for the given cell. If the optional + * parent argument is specified, then only child edges of the given parent + * are returned. + * + * Parameters: + * + * cell - <mxCell> whose outgoing edges should be returned. + * parent - Optional parent of the opposite end for an edge to be + * returned. + */ +mxGraph.prototype.getOutgoingEdges = function(cell, parent) +{ + return this.getEdges(cell, parent, false, true, false); +}; + +/** + * Function: getEdges + * + * Returns the incoming and/or outgoing edges for the given cell. + * If the optional parent argument is specified, then only edges are returned + * where the opposite is in the given parent cell. If at least one of incoming + * or outgoing is true, then loops are ignored, if both are false, then all + * edges connected to the given cell are returned including loops. + * + * Parameters: + * + * cell - <mxCell> whose edges should be returned. + * parent - Optional parent of the opposite end for an edge to be + * returned. + * incoming - Optional boolean that specifies if incoming edges should + * be included in the result. Default is true. + * outgoing - Optional boolean that specifies if outgoing edges should + * be included in the result. Default is true. + * includeLoops - Optional boolean that specifies if loops should be + * included in the result. Default is true. + * recurse - Optional boolean the specifies if the parent specified only + * need be an ancestral parent, true, or the direct parent, false. + * Default is false + */ +mxGraph.prototype.getEdges = function(cell, parent, incoming, outgoing, includeLoops, recurse) +{ + incoming = (incoming != null) ? incoming : true; + outgoing = (outgoing != null) ? outgoing : true; + includeLoops = (includeLoops != null) ? includeLoops : true; + recurse = (recurse != null) ? recurse : false; + + var edges = []; + var isCollapsed = this.isCellCollapsed(cell); + var childCount = this.model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = this.model.getChildAt(cell, i); + + if (isCollapsed || !this.isCellVisible(child)) + { + edges = edges.concat(this.model.getEdges(child, incoming, outgoing)); + } + } + + edges = edges.concat(this.model.getEdges(cell, incoming, outgoing)); + var result = []; + + for (var i = 0; i < edges.length; i++) + { + var state = this.view.getState(edges[i]); + + var source = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[i], true); + var target = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[i], false); + + if ((includeLoops && source == target) || ((source != target) && ((incoming && + target == cell && (parent == null || this.isValidAncestor(source, parent, recurse))) || + (outgoing && source == cell && (parent == null || + this.isValidAncestor(target, parent, recurse)))))) + { + result.push(edges[i]); + } + } + + return result; +}; + +/** + * Function: isValidAncestor + * + * Returns whether or not the specified parent is a valid + * ancestor of the specified cell, either direct or indirectly + * based on whether ancestor recursion is enabled. + * + * Parameters: + * + * cell - <mxCell> the possible child cell + * parent - <mxCell> the possible parent cell + * recurse - boolean whether or not to recurse the child ancestors + */ +mxGraph.prototype.isValidAncestor = function(cell, parent, recurse) +{ + return (recurse ? this.model.isAncestor(parent, cell) : this.model + .getParent(cell) == parent); +}; + +/** + * Function: getOpposites + * + * Returns all distinct visible opposite cells for the specified terminal + * on the given edges. + * + * Parameters: + * + * edges - Array of <mxCells> that contains the edges whose opposite + * terminals should be returned. + * terminal - Terminal that specifies the end whose opposite should be + * returned. + * source - Optional boolean that specifies if source terminals should be + * included in the result. Default is true. + * targets - Optional boolean that specifies if targer terminals should be + * included in the result. Default is true. + */ +mxGraph.prototype.getOpposites = function(edges, terminal, sources, targets) +{ + sources = (sources != null) ? sources : true; + targets = (targets != null) ? targets : true; + + var terminals = []; + + // Implements set semantic on the terminals array using a string + // representation of each cell in an associative array lookup + var hash = new Object(); + + if (edges != null) + { + for (var i = 0; i < edges.length; i++) + { + var state = this.view.getState(edges[i]); + + var source = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[i], true); + var target = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[i], false); + + // Checks if the terminal is the source of the edge and if the + // target should be stored in the result + if (source == terminal && target != null && + target != terminal && targets) + { + var id = mxCellPath.create(target); + + if (hash[id] == null) + { + hash[id] = target; + terminals.push(target); + } + } + + // Checks if the terminal is the taget of the edge and if the + // source should be stored in the result + else if (target == terminal && source != null && + source != terminal && sources) + { + var id = mxCellPath.create(source); + + if (hash[id] == null) + { + hash[id] = source; + terminals.push(source); + } + } + } + } + + return terminals; +}; + +/** + * Function: getEdgesBetween + * + * Returns the edges between the given source and target. This takes into + * account collapsed and invisible cells and returns the connected edges + * as displayed on the screen. + * + * Parameters: + * + * source - + * target - + * directed - + */ +mxGraph.prototype.getEdgesBetween = function(source, target, directed) +{ + directed = (directed != null) ? directed : false; + var edges = this.getEdges(source); + var result = []; + + // Checks if the edge is connected to the correct + // cell and returns the first match + for (var i = 0; i < edges.length; i++) + { + var state = this.view.getState(edges[i]); + + var src = (state != null) ? state.getVisibleTerminal(true) : this.view.getVisibleTerminal(edges[i], true); + var trg = (state != null) ? state.getVisibleTerminal(false) : this.view.getVisibleTerminal(edges[i], false); + + if ((src == source && trg == target) || (!directed && src == target && trg == source)) + { + result.push(edges[i]); + } + } + + return result; +}; + +/** + * Function: getPointForEvent + * + * Returns an <mxPoint> representing the given event in the unscaled, + * non-translated coordinate space of <container> and applies the grid. + * + * Parameters: + * + * evt - Mousevent that contains the mouse pointer location. + * addOffset - Optional boolean that specifies if the position should be + * offset by half of the <gridSize>. Default is true. + */ + mxGraph.prototype.getPointForEvent = function(evt, addOffset) + { + var p = mxUtils.convertPoint(this.container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + + var s = this.view.scale; + var tr = this.view.translate; + var off = (addOffset != false) ? this.gridSize / 2 : 0; + + p.x = this.snap(p.x / s - tr.x - off); + p.y = this.snap(p.y / s - tr.y - off); + + return p; + }; + +/** + * Function: getCells + * + * Returns the children of the given parent that are contained in the given + * rectangle (x, y, width, height). The result is added to the optional + * result array, which is returned from the function. If no result array + * is specified then a new array is created and returned. + * + * Parameters: + * + * x - X-coordinate of the rectangle. + * y - Y-coordinate of the rectangle. + * width - Width of the rectangle. + * height - Height of the rectangle. + * parent - <mxCell> whose children should be checked. Default is + * <defaultParent>. + * result - Optional array to store the result in. + */ +mxGraph.prototype.getCells = function(x, y, width, height, parent, result) +{ + result = (result != null) ? result : []; + + if (width > 0 || height > 0) + { + var right = x + width; + var bottom = y + height; + + parent = parent || this.getDefaultParent(); + + if (parent != null) + { + var childCount = this.model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var cell = this.model.getChildAt(parent, i); + var state = this.view.getState(cell); + + if (this.isCellVisible(cell) && state != null) + { + if (state.x >= x && state.y >= y && + state.x + state.width <= right && + state.y + state.height <= bottom) + { + result.push(cell); + } + else + { + this.getCells(x, y, width, height, cell, result); + } + } + } + } + } + + return result; +}; + +/** + * Function: getCellsBeyond + * + * Returns the children of the given parent that are contained in the + * halfpane from the given point (x0, y0) rightwards and/or downwards + * depending on rightHalfpane and bottomHalfpane. + * + * Parameters: + * + * x0 - X-coordinate of the origin. + * y0 - Y-coordinate of the origin. + * parent - Optional <mxCell> whose children should be checked. Default is + * <defaultParent>. + * rightHalfpane - Boolean indicating if the cells in the right halfpane + * from the origin should be returned. + * bottomHalfpane - Boolean indicating if the cells in the bottom halfpane + * from the origin should be returned. + */ +mxGraph.prototype.getCellsBeyond = function(x0, y0, parent, rightHalfpane, bottomHalfpane) +{ + var result = []; + + if (rightHalfpane || bottomHalfpane) + { + if (parent == null) + { + parent = this.getDefaultParent(); + } + + if (parent != null) + { + var childCount = this.model.getChildCount(parent); + + for (var i = 0; i < childCount; i++) + { + var child = this.model.getChildAt(parent, i); + var state = this.view.getState(child); + + if (this.isCellVisible(child) && state != null) + { + if ((!rightHalfpane || + state.x >= x0) && + (!bottomHalfpane || + state.y >= y0)) + { + result.push(child); + } + } + } + } + } + + return result; +}; + +/** + * Function: findTreeRoots + * + * Returns all children in the given parent which do not have incoming + * edges. If the result is empty then the with the greatest difference + * between incoming and outgoing edges is returned. + * + * Parameters: + * + * parent - <mxCell> whose children should be checked. + * isolate - Optional boolean that specifies if edges should be ignored if + * the opposite end is not a child of the given parent cell. Default is + * false. + * invert - Optional boolean that specifies if outgoing or incoming edges + * should be counted for a tree root. If false then outgoing edges will be + * counted. Default is false. + */ +mxGraph.prototype.findTreeRoots = function(parent, isolate, invert) +{ + isolate = (isolate != null) ? isolate : false; + invert = (invert != null) ? invert : false; + var roots = []; + + if (parent != null) + { + var model = this.getModel(); + var childCount = model.getChildCount(parent); + var best = null; + var maxDiff = 0; + + for (var i=0; i<childCount; i++) + { + var cell = model.getChildAt(parent, i); + + if (this.model.isVertex(cell) && this.isCellVisible(cell)) + { + var conns = this.getConnections(cell, (isolate) ? parent : null); + var fanOut = 0; + var fanIn = 0; + + for (var j = 0; j < conns.length; j++) + { + var src = this.view.getVisibleTerminal(conns[j], true); + + if (src == cell) + { + fanOut++; + } + else + { + fanIn++; + } + } + + if ((invert && fanOut == 0 && fanIn > 0) || + (!invert && fanIn == 0 && fanOut > 0)) + { + roots.push(cell); + } + + var diff = (invert) ? fanIn - fanOut : fanOut - fanIn; + + if (diff > maxDiff) + { + maxDiff = diff; + best = cell; + } + } + } + + if (roots.length == 0 && best != null) + { + roots.push(best); + } + } + + return roots; +}; + +/** + * Function: traverse + * + * Traverses the (directed) graph invoking the given function for each + * visited vertex and edge. The function is invoked with the current vertex + * and the incoming edge as a parameter. This implementation makes sure + * each vertex is only visited once. The function may return false if the + * traversal should stop at the given vertex. + * + * Example: + * + * (code) + * mxLog.show(); + * var cell = graph.getSelectionCell(); + * graph.traverse(cell, false, function(vertex, edge) + * { + * mxLog.debug(graph.getLabel(vertex)); + * }); + * (end) + * + * Parameters: + * + * vertex - <mxCell> that represents the vertex where the traversal starts. + * directed - Optional boolean indicating if edges should only be traversed + * from source to target. Default is true. + * func - Visitor function that takes the current vertex and the incoming + * edge as arguments. The traversal stops if the function returns false. + * edge - Optional <mxCell> that represents the incoming edge. This is + * null for the first step of the traversal. + * visited - Optional array of cell paths for the visited cells. + */ +mxGraph.prototype.traverse = function(vertex, directed, func, edge, visited) +{ + if (func != null && vertex != null) + { + directed = (directed != null) ? directed : true; + visited = visited || []; + var id = mxCellPath.create(vertex); + + if (visited[id] == null) + { + visited[id] = vertex; + var result = func(vertex, edge); + + if (result == null || result) + { + var edgeCount = this.model.getEdgeCount(vertex); + + if (edgeCount > 0) + { + for (var i = 0; i < edgeCount; i++) + { + var e = this.model.getEdgeAt(vertex, i); + var isSource = this.model.getTerminal(e, true) == vertex; + + if (!directed || isSource) + { + var next = this.model.getTerminal(e, !isSource); + this.traverse(next, directed, func, e, visited); + } + } + } + } + } + } +}; + +/** + * Group: Selection + */ + +/** + * Function: isCellSelected + * + * Returns true if the given cell is selected. + * + * Parameters: + * + * cell - <mxCell> for which the selection state should be returned. + */ +mxGraph.prototype.isCellSelected = function(cell) +{ + return this.getSelectionModel().isSelected(cell); +}; + +/** + * Function: isSelectionEmpty + * + * Returns true if the selection is empty. + */ +mxGraph.prototype.isSelectionEmpty = function() +{ + return this.getSelectionModel().isEmpty(); +}; + +/** + * Function: clearSelection + * + * Clears the selection using <mxGraphSelectionModel.clear>. + */ +mxGraph.prototype.clearSelection = function() +{ + return this.getSelectionModel().clear(); +}; + +/** + * Function: getSelectionCount + * + * Returns the number of selected cells. + */ +mxGraph.prototype.getSelectionCount = function() +{ + return this.getSelectionModel().cells.length; +}; + +/** + * Function: getSelectionCell + * + * Returns the first cell from the array of selected <mxCells>. + */ +mxGraph.prototype.getSelectionCell = function() +{ + return this.getSelectionModel().cells[0]; +}; + +/** + * Function: getSelectionCells + * + * Returns the array of selected <mxCells>. + */ +mxGraph.prototype.getSelectionCells = function() +{ + return this.getSelectionModel().cells.slice(); +}; + +/** + * Function: setSelectionCell + * + * Sets the selection cell. + * + * Parameters: + * + * cell - <mxCell> to be selected. + */ +mxGraph.prototype.setSelectionCell = function(cell) +{ + this.getSelectionModel().setCell(cell); +}; + +/** + * Function: setSelectionCells + * + * Sets the selection cell. + * + * Parameters: + * + * cells - Array of <mxCells> to be selected. + */ +mxGraph.prototype.setSelectionCells = function(cells) +{ + this.getSelectionModel().setCells(cells); +}; + +/** + * Function: addSelectionCell + * + * Adds the given cell to the selection. + * + * Parameters: + * + * cell - <mxCell> to be add to the selection. + */ +mxGraph.prototype.addSelectionCell = function(cell) +{ + this.getSelectionModel().addCell(cell); +}; + +/** + * Function: addSelectionCells + * + * Adds the given cells to the selection. + * + * Parameters: + * + * cells - Array of <mxCells> to be added to the selection. + */ +mxGraph.prototype.addSelectionCells = function(cells) +{ + this.getSelectionModel().addCells(cells); +}; + +/** + * Function: removeSelectionCell + * + * Removes the given cell from the selection. + * + * Parameters: + * + * cell - <mxCell> to be removed from the selection. + */ +mxGraph.prototype.removeSelectionCell = function(cell) +{ + this.getSelectionModel().removeCell(cell); +}; + +/** + * Function: removeSelectionCells + * + * Removes the given cells from the selection. + * + * Parameters: + * + * cells - Array of <mxCells> to be removed from the selection. + */ +mxGraph.prototype.removeSelectionCells = function(cells) +{ + this.getSelectionModel().removeCells(cells); +}; + +/** + * Function: selectRegion + * + * Selects and returns the cells inside the given rectangle for the + * specified event. + * + * Parameters: + * + * rect - <mxRectangle> that represents the region to be selected. + * evt - Mouseevent that triggered the selection. + */ +mxGraph.prototype.selectRegion = function(rect, evt) +{ + var cells = this.getCells(rect.x, rect.y, rect.width, rect.height); + this.selectCellsForEvent(cells, evt); + + return cells; +}; + +/** + * Function: selectNextCell + * + * Selects the next cell. + */ +mxGraph.prototype.selectNextCell = function() +{ + this.selectCell(true); +}; + +/** + * Function: selectPreviousCell + * + * Selects the previous cell. + */ +mxGraph.prototype.selectPreviousCell = function() +{ + this.selectCell(); +}; + +/** + * Function: selectParentCell + * + * Selects the parent cell. + */ +mxGraph.prototype.selectParentCell = function() +{ + this.selectCell(false, true); +}; + +/** + * Function: selectChildCell + * + * Selects the first child cell. + */ +mxGraph.prototype.selectChildCell = function() +{ + this.selectCell(false, false, true); +}; + +/** + * Function: selectCell + * + * Selects the next, parent, first child or previous cell, if all arguments + * are false. + * + * Parameters: + * + * isNext - Boolean indicating if the next cell should be selected. + * isParent - Boolean indicating if the parent cell should be selected. + * isChild - Boolean indicating if the first child cell should be selected. + */ +mxGraph.prototype.selectCell = function(isNext, isParent, isChild) +{ + var sel = this.selectionModel; + var cell = (sel.cells.length > 0) ? sel.cells[0] : null; + + if (sel.cells.length > 1) + { + sel.clear(); + } + + var parent = (cell != null) ? + this.model.getParent(cell) : + this.getDefaultParent(); + + var childCount = this.model.getChildCount(parent); + + if (cell == null && childCount > 0) + { + var child = this.model.getChildAt(parent, 0); + this.setSelectionCell(child); + } + else if ((cell == null || isParent) && + this.view.getState(parent) != null && + this.model.getGeometry(parent) != null) + { + if (this.getCurrentRoot() != parent) + { + this.setSelectionCell(parent); + } + } + else if (cell != null && isChild) + { + var tmp = this.model.getChildCount(cell); + + if (tmp > 0) + { + var child = this.model.getChildAt(cell, 0); + this.setSelectionCell(child); + } + } + else if (childCount > 0) + { + var i = parent.getIndex(cell); + + if (isNext) + { + i++; + var child = this.model.getChildAt(parent, i % childCount); + this.setSelectionCell(child); + } + else + { + i--; + var index = (i < 0) ? childCount - 1 : i; + var child = this.model.getChildAt(parent, index); + this.setSelectionCell(child); + } + } +}; + +/** + * Function: selectAll + * + * Selects all children of the given parent cell or the children of the + * default parent if no parent is specified. To select leaf vertices and/or + * edges use <selectCells>. + * + * Parameters: + * + * parent - Optional <mxCell> whose children should be selected. + * Default is <defaultParent>. + */ +mxGraph.prototype.selectAll = function(parent) +{ + parent = parent || this.getDefaultParent(); + + var children = this.model.getChildren(parent); + + if (children != null) + { + this.setSelectionCells(children); + } +}; + +/** + * Function: selectVertices + * + * Select all vertices inside the given parent or the default parent. + */ +mxGraph.prototype.selectVertices = function(parent) +{ + this.selectCells(true, false, parent); +}; + +/** + * Function: selectVertices + * + * Select all vertices inside the given parent or the default parent. + */ +mxGraph.prototype.selectEdges = function(parent) +{ + this.selectCells(false, true, parent); +}; + +/** + * Function: selectCells + * + * Selects all vertices and/or edges depending on the given boolean + * arguments recursively, starting at the given parent or the default + * parent if no parent is specified. Use <selectAll> to select all cells. + * + * Parameters: + * + * vertices - Boolean indicating if vertices should be selected. + * edges - Boolean indicating if edges should be selected. + * parent - Optional <mxCell> that acts as the root of the recursion. + * Default is <defaultParent>. + */ +mxGraph.prototype.selectCells = function(vertices, edges, parent) +{ + parent = parent || this.getDefaultParent(); + + var filter = mxUtils.bind(this, function(cell) + { + return this.view.getState(cell) != null && + this.model.getChildCount(cell) == 0 && + ((this.model.isVertex(cell) && vertices) || + (this.model.isEdge(cell) && edges)); + }); + + var cells = this.model.filterDescendants(filter, parent); + this.setSelectionCells(cells); +}; + +/** + * Function: selectCellForEvent + * + * Selects the given cell by either adding it to the selection or + * replacing the selection depending on whether the given mouse event is a + * toggle event. + * + * Parameters: + * + * cell - <mxCell> to be selected. + * evt - Optional mouseevent that triggered the selection. + */ +mxGraph.prototype.selectCellForEvent = function(cell, evt) +{ + var isSelected = this.isCellSelected(cell); + + if (this.isToggleEvent(evt)) + { + if (isSelected) + { + this.removeSelectionCell(cell); + } + else + { + this.addSelectionCell(cell); + } + } + else if (!isSelected || this.getSelectionCount() != 1) + { + this.setSelectionCell(cell); + } +}; + +/** + * Function: selectCellsForEvent + * + * Selects the given cells by either adding them to the selection or + * replacing the selection depending on whether the given mouse event is a + * toggle event. + * + * Parameters: + * + * cells - Array of <mxCells> to be selected. + * evt - Optional mouseevent that triggered the selection. + */ +mxGraph.prototype.selectCellsForEvent = function(cells, evt) +{ + if (this.isToggleEvent(evt)) + { + this.addSelectionCells(cells); + } + else + { + this.setSelectionCells(cells); + } +}; + +/** + * Group: Selection state + */ + +/** + * Function: createHandler + * + * Creates a new handler for the given cell state. This implementation + * returns a new <mxEdgeHandler> of the corresponding cell is an edge, + * otherwise it returns an <mxVertexHandler>. + * + * Parameters: + * + * state - <mxCellState> whose handler should be created. + */ +mxGraph.prototype.createHandler = function(state) +{ + var result = null; + + if (state != null) + { + if (this.model.isEdge(state.cell)) + { + var style = this.view.getEdgeStyle(state); + + if (this.isLoop(state) || + style == mxEdgeStyle.ElbowConnector || + style == mxEdgeStyle.SideToSide || + style == mxEdgeStyle.TopToBottom) + { + result = new mxElbowEdgeHandler(state); + } + else if (style == mxEdgeStyle.SegmentConnector || + style == mxEdgeStyle.OrthConnector) + { + result = new mxEdgeSegmentHandler(state); + } + else + { + result = new mxEdgeHandler(state); + } + } + else + { + result = new mxVertexHandler(state); + } + } + + return result; +}; + +/** + * Group: Graph events + */ + +/** + * Function: addMouseListener + * + * Adds a listener to the graph event dispatch loop. The listener + * must implement the mouseDown, mouseMove and mouseUp methods + * as shown in the <mxMouseEvent> class. + * + * Parameters: + * + * listener - Listener to be added to the graph event listeners. + */ +mxGraph.prototype.addMouseListener = function(listener) +{ + if (this.mouseListeners == null) + { + this.mouseListeners = []; + } + + this.mouseListeners.push(listener); +}; + +/** + * Function: removeMouseListener + * + * Removes the specified graph listener. + * + * Parameters: + * + * listener - Listener to be removed from the graph event listeners. + */ +mxGraph.prototype.removeMouseListener = function(listener) +{ + if (this.mouseListeners != null) + { + for (var i = 0; i < this.mouseListeners.length; i++) + { + if (this.mouseListeners[i] == listener) + { + this.mouseListeners.splice(i, 1); + break; + } + } + } +}; + +/** + * Function: updateMouseEvent + * + * Sets the graphX and graphY properties if the given <mxMouseEvent> if + * required. + */ +mxGraph.prototype.updateMouseEvent = function(me) +{ + if (me.graphX == null || me.graphY == null) + { + var pt = mxUtils.convertPoint(this.container, me.getX(), me.getY()); + + me.graphX = pt.x - this.panDx; + me.graphY = pt.y - this.panDy; + } +}; + +/** + * Function: fireMouseEvent + * + * Dispatches the given event in the graph event dispatch loop. Possible + * event names are <mxEvent.MOUSE_DOWN>, <mxEvent.MOUSE_MOVE> and + * <mxEvent.MOUSE_UP>. All listeners are invoked for all events regardless + * of the consumed state of the event. + * + * Parameters: + * + * evtName - String that specifies the type of event to be dispatched. + * me - <mxMouseEvent> to be fired. + * sender - Optional sender argument. Default is this. + */ +mxGraph.prototype.fireMouseEvent = function(evtName, me, sender) +{ + if (sender == null) + { + sender = this; + } + + // Updates the graph coordinates in the event + this.updateMouseEvent(me); + + // Makes sure we have a uniform event-sequence across all + // browsers for a double click. Since evt.detail == 2 is only + // available on Firefox we use the fact that each mousedown + // must be followed by a mouseup, all out-of-sync downs + // will be dropped silently. + if (evtName == mxEvent.MOUSE_DOWN) + { + this.isMouseDown = true; + } + + // Detects and processes double taps for touch-based devices + // which do not have native double click events + if (mxClient.IS_TOUCH && this.doubleTapEnabled && evtName == mxEvent.MOUSE_DOWN) + { + var currentTime = new Date().getTime(); + + if (currentTime - this.lastTouchTime < this.doubleTapTimeout && + Math.abs(this.lastTouchX - me.getX()) < this.doubleTapTolerance && + Math.abs(this.lastTouchY - me.getY()) < this.doubleTapTolerance) + { + // FIXME: The actual editing should start on MOUSE_UP event but + // the detection of the double click should use the mouse_down event + // to make it consistent with behaviour in browser with mouse. + this.lastTouchTime = 0; + this.dblClick(me.getEvent(), me.getCell()); + + // Stop bubbling but do not consume to make sure the device + // can bring up the virtual keyboard for editing + me.getEvent().cancelBubble = true; + } + else + { + this.lastTouchX = me.getX(); + this.lastTouchY = me.getY(); + this.lastTouchTime = currentTime; + } + } + + // Workaround for IE9 standards mode ignoring tolerance for double clicks + var noDoubleClick = me.getEvent().detail/*clickCount*/ != 2; + + if (mxClient.IS_IE && document.compatMode == 'CSS1Compat') + { + if ((this.lastMouseX != null && Math.abs(this.lastMouseX - me.getX()) > this.doubleTapTolerance) || + (this.lastMouseY != null && Math.abs(this.lastMouseY - me.getY()) > this.doubleTapTolerance)) + { + noDoubleClick = true; + } + + if (evtName == mxEvent.MOUSE_UP) + { + this.lastMouseX = me.getX(); + this.lastMouseY = me.getY(); + } + } + + // Filters too many mouse ups when the mouse is down + if ((evtName != mxEvent.MOUSE_UP || this.isMouseDown) && noDoubleClick) + { + if (evtName == mxEvent.MOUSE_UP) + { + this.isMouseDown = false; + } + + if (!this.isEditing() && (mxClient.IS_OP || mxClient.IS_SF || mxClient.IS_GC || + (mxClient.IS_IE && mxClient.IS_SVG) || me.getEvent().target != this.container)) + { + if (evtName == mxEvent.MOUSE_MOVE && this.isMouseDown && this.autoScroll) + { + this.scrollPointToVisible(me.getGraphX(), me.getGraphY(), this.autoExtend); + } + + if (this.mouseListeners != null) + { + var args = [sender, me]; + + // Does not change returnValue in Opera + me.getEvent().returnValue = true; + + for (var i = 0; i < this.mouseListeners.length; i++) + { + var l = this.mouseListeners[i]; + + if (evtName == mxEvent.MOUSE_DOWN) + { + l.mouseDown.apply(l, args); + } + else if (evtName == mxEvent.MOUSE_MOVE) + { + l.mouseMove.apply(l, args); + } + else if (evtName == mxEvent.MOUSE_UP) + { + l.mouseUp.apply(l, args); + } + } + } + + // Invokes the click handler + if (evtName == mxEvent.MOUSE_UP) + { + this.click(me); + } + } + } + else if (evtName == mxEvent.MOUSE_UP) + { + this.isMouseDown = false; + } +}; + +/** + * Function: destroy + * + * Destroys the graph and all its resources. + */ +mxGraph.prototype.destroy = function() +{ + if (!this.destroyed) + { + this.destroyed = true; + + if (this.tooltipHandler != null) + { + this.tooltipHandler.destroy(); + } + + if (this.selectionCellsHandler != null) + { + this.selectionCellsHandler.destroy(); + } + + if (this.panningHandler != null) + { + this.panningHandler.destroy(); + } + + if (this.connectionHandler != null) + { + this.connectionHandler.destroy(); + } + + if (this.graphHandler != null) + { + this.graphHandler.destroy(); + } + + if (this.cellEditor != null) + { + this.cellEditor.destroy(); + } + + if (this.view != null) + { + this.view.destroy(); + } + + if (this.model != null && this.graphModelChangeListener != null) + { + this.model.removeListener(this.graphModelChangeListener); + this.graphModelChangeListener = null; + } + + this.container = null; + } +}; diff --git a/src/js/view/mxGraphSelectionModel.js b/src/js/view/mxGraphSelectionModel.js new file mode 100644 index 0000000..5cd16a8 --- /dev/null +++ b/src/js/view/mxGraphSelectionModel.js @@ -0,0 +1,435 @@ +/** + * $Id: mxGraphSelectionModel.js,v 1.14 2011-11-25 10:16:08 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraphSelectionModel + * + * Implements the selection model for a graph. Here is a listener that handles + * all removed selection cells. + * + * (code) + * graph.getSelectionModel().addListener(mxEvent.CHANGE, function(sender, evt) + * { + * var cells = evt.getProperty('added'); + * + * for (var i = 0; i < cells.length; i++) + * { + * // Handle cells[i]... + * } + * }); + * (end) + * + * Event: mxEvent.UNDO + * + * Fires after the selection was changed in <changeSelection>. The + * <code>edit</code> property contains the <mxUndoableEdit> which contains the + * <mxSelectionChange>. + * + * Event: mxEvent.CHANGE + * + * Fires after the selection changes by executing an <mxSelectionChange>. The + * <code>added</code> and <code>removed</code> properties contain arrays of + * cells that have been added to or removed from the selection, respectively. + * + * Constructor: mxGraphSelectionModel + * + * Constructs a new graph selection model for the given <mxGraph>. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxGraphSelectionModel(graph) +{ + this.graph = graph; + this.cells = []; +}; + +/** + * Extends mxEventSource. + */ +mxGraphSelectionModel.prototype = new mxEventSource(); +mxGraphSelectionModel.prototype.constructor = mxGraphSelectionModel; + +/** + * Variable: doneResource + * + * Specifies the resource key for the status message after a long operation. + * If the resource for this key does not exist then the value is used as + * the status message. Default is 'done'. + */ +mxGraphSelectionModel.prototype.doneResource = (mxClient.language != 'none') ? 'done' : ''; + +/** + * Variable: updatingSelectionResource + * + * Specifies the resource key for the status message while the selection is + * being updated. If the resource for this key does not exist then the + * value is used as the status message. Default is 'updatingSelection'. + */ +mxGraphSelectionModel.prototype.updatingSelectionResource = (mxClient.language != 'none') ? 'updatingSelection' : ''; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxGraphSelectionModel.prototype.graph = null; + +/** + * Variable: singleSelection + * + * Specifies if only one selected item at a time is allowed. + * Default is false. + */ +mxGraphSelectionModel.prototype.singleSelection = false; + +/** + * Function: isSingleSelection + * + * Returns <singleSelection> as a boolean. + */ +mxGraphSelectionModel.prototype.isSingleSelection = function() +{ + return this.singleSelection; +}; + +/** + * Function: setSingleSelection + * + * Sets the <singleSelection> flag. + * + * Parameters: + * + * singleSelection - Boolean that specifies the new value for + * <singleSelection>. + */ +mxGraphSelectionModel.prototype.setSingleSelection = function(singleSelection) +{ + this.singleSelection = singleSelection; +}; + +/** + * Function: isSelected + * + * Returns true if the given <mxCell> is selected. + */ +mxGraphSelectionModel.prototype.isSelected = function(cell) +{ + if (cell != null) + { + return mxUtils.indexOf(this.cells, cell) >= 0; + } + + return false; +}; + +/** + * Function: isEmpty + * + * Returns true if no cells are currently selected. + */ +mxGraphSelectionModel.prototype.isEmpty = function() +{ + return this.cells.length == 0; +}; + +/** + * Function: clear + * + * Clears the selection and fires a <change> event if the selection was not + * empty. + */ +mxGraphSelectionModel.prototype.clear = function() +{ + this.changeSelection(null, this.cells); +}; + +/** + * Function: setCell + * + * Selects the specified <mxCell> using <setCells>. + * + * Parameters: + * + * cell - <mxCell> to be selected. + */ +mxGraphSelectionModel.prototype.setCell = function(cell) +{ + if (cell != null) + { + this.setCells([cell]); + } +}; + +/** + * Function: setCells + * + * Selects the given array of <mxCells> and fires a <change> event. + * + * Parameters: + * + * cells - Array of <mxCells> to be selected. + */ +mxGraphSelectionModel.prototype.setCells = function(cells) +{ + if (cells != null) + { + if (this.singleSelection) + { + cells = [this.getFirstSelectableCell(cells)]; + } + + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + if (this.graph.isCellSelectable(cells[i])) + { + tmp.push(cells[i]); + } + } + + this.changeSelection(tmp, this.cells); + } +}; + +/** + * Function: getFirstSelectableCell + * + * Returns the first selectable cell in the given array of cells. + */ +mxGraphSelectionModel.prototype.getFirstSelectableCell = function(cells) +{ + if (cells != null) + { + for (var i = 0; i < cells.length; i++) + { + if (this.graph.isCellSelectable(cells[i])) + { + return cells[i]; + } + } + } + + return null; +}; + +/** + * Function: addCell + * + * Adds the given <mxCell> to the selection and fires a <select> event. + * + * Parameters: + * + * cell - <mxCell> to add to the selection. + */ +mxGraphSelectionModel.prototype.addCell = function(cell) +{ + if (cell != null) + { + this.addCells([cell]); + } +}; + +/** + * Function: addCells + * + * Adds the given array of <mxCells> to the selection and fires a <select> + * event. + * + * Parameters: + * + * cells - Array of <mxCells> to add to the selection. + */ +mxGraphSelectionModel.prototype.addCells = function(cells) +{ + if (cells != null) + { + var remove = null; + + if (this.singleSelection) + { + remove = this.cells; + cells = [this.getFirstSelectableCell(cells)]; + } + + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + if (!this.isSelected(cells[i]) && + this.graph.isCellSelectable(cells[i])) + { + tmp.push(cells[i]); + } + } + + this.changeSelection(tmp, remove); + } +}; + +/** + * Function: removeCell + * + * Removes the specified <mxCell> from the selection and fires a <select> + * event for the remaining cells. + * + * Parameters: + * + * cell - <mxCell> to remove from the selection. + */ +mxGraphSelectionModel.prototype.removeCell = function(cell) +{ + if (cell != null) + { + this.removeCells([cell]); + } +}; + +/** + * Function: removeCells + */ +mxGraphSelectionModel.prototype.removeCells = function(cells) +{ + if (cells != null) + { + var tmp = []; + + for (var i = 0; i < cells.length; i++) + { + if (this.isSelected(cells[i])) + { + tmp.push(cells[i]); + } + } + + this.changeSelection(null, tmp); + } +}; + +/** + * Function: changeSelection + * + * Inner callback to add the specified <mxCell> to the selection. No event + * is fired in this implementation. + * + * Paramters: + * + * cell - <mxCell> to add to the selection. + */ +mxGraphSelectionModel.prototype.changeSelection = function(added, removed) +{ + if ((added != null && + added.length > 0 && + added[0] != null) || + (removed != null && + removed.length > 0 && + removed[0] != null)) + { + var change = new mxSelectionChange(this, added, removed); + change.execute(); + var edit = new mxUndoableEdit(this, false); + edit.add(change); + this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit)); + } +}; + +/** + * Function: cellAdded + * + * Inner callback to add the specified <mxCell> to the selection. No event + * is fired in this implementation. + * + * Paramters: + * + * cell - <mxCell> to add to the selection. + */ +mxGraphSelectionModel.prototype.cellAdded = function(cell) +{ + if (cell != null && + !this.isSelected(cell)) + { + this.cells.push(cell); + } +}; + +/** + * Function: cellRemoved + * + * Inner callback to remove the specified <mxCell> from the selection. No + * event is fired in this implementation. + * + * Parameters: + * + * cell - <mxCell> to remove from the selection. + */ +mxGraphSelectionModel.prototype.cellRemoved = function(cell) +{ + if (cell != null) + { + var index = mxUtils.indexOf(this.cells, cell); + + if (index >= 0) + { + this.cells.splice(index, 1); + } + } +}; + +/** + * Class: mxSelectionChange + * + * Action to change the current root in a view. + * + * Constructor: mxCurrentRootChange + * + * Constructs a change of the current root in the given view. + */ +function mxSelectionChange(selectionModel, added, removed) +{ + this.selectionModel = selectionModel; + this.added = (added != null) ? added.slice() : null; + this.removed = (removed != null) ? removed.slice() : null; +}; + +/** + * Function: execute + * + * Changes the current root of the view. + */ +mxSelectionChange.prototype.execute = function() +{ + var t0 = mxLog.enter('mxSelectionChange.execute'); + window.status = mxResources.get( + this.selectionModel.updatingSelectionResource) || + this.selectionModel.updatingSelectionResource; + + if (this.removed != null) + { + for (var i = 0; i < this.removed.length; i++) + { + this.selectionModel.cellRemoved(this.removed[i]); + } + } + + if (this.added != null) + { + for (var i = 0; i < this.added.length; i++) + { + this.selectionModel.cellAdded(this.added[i]); + } + } + + var tmp = this.added; + this.added = this.removed; + this.removed = tmp; + + window.status = mxResources.get(this.selectionModel.doneResource) || + this.selectionModel.doneResource; + mxLog.leave('mxSelectionChange.execute', t0); + + this.selectionModel.fireEvent(new mxEventObject(mxEvent.CHANGE, + 'added', this.added, 'removed', this.removed)); +}; diff --git a/src/js/view/mxGraphView.js b/src/js/view/mxGraphView.js new file mode 100644 index 0000000..0ef2dc8 --- /dev/null +++ b/src/js/view/mxGraphView.js @@ -0,0 +1,2545 @@ +/** + * $Id: mxGraphView.js,v 1.195 2012-11-20 09:06:07 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxGraphView + * + * Extends <mxEventSource> to implement a view for a graph. This class is in + * charge of computing the absolute coordinates for the relative child + * geometries, the points for perimeters and edge styles and keeping them + * cached in <mxCellStates> for faster retrieval. The states are updated + * whenever the model or the view state (translate, scale) changes. The scale + * and translate are honoured in the bounds. + * + * Event: mxEvent.UNDO + * + * Fires after the root was changed in <setCurrentRoot>. The <code>edit</code> + * property contains the <mxUndoableEdit> which contains the + * <mxCurrentRootChange>. + * + * Event: mxEvent.SCALE_AND_TRANSLATE + * + * Fires after the scale and translate have been changed in <scaleAndTranslate>. + * The <code>scale</code>, <code>previousScale</code>, <code>translate</code> + * and <code>previousTranslate</code> properties contain the new and previous + * scale and translate, respectively. + * + * Event: mxEvent.SCALE + * + * Fires after the scale was changed in <setScale>. The <code>scale</code> and + * <code>previousScale</code> properties contain the new and previous scale. + * + * Event: mxEvent.TRANSLATE + * + * Fires after the translate was changed in <setTranslate>. The + * <code>translate</code> and <code>previousTranslate</code> properties contain + * the new and previous value for translate. + * + * Event: mxEvent.DOWN and mxEvent.UP + * + * Fire if the current root is changed by executing an <mxCurrentRootChange>. + * The event name depends on the location of the root in the cell hierarchy + * with respect to the current root. The <code>root</code> and + * <code>previous</code> properties contain the new and previous root, + * respectively. + * + * Constructor: mxGraphView + * + * Constructs a new view for the given <mxGraph>. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph>. + */ +function mxGraphView(graph) +{ + this.graph = graph; + this.translate = new mxPoint(); + this.graphBounds = new mxRectangle(); + this.states = new mxDictionary(); +}; + +/** + * Extends mxEventSource. + */ +mxGraphView.prototype = new mxEventSource(); +mxGraphView.prototype.constructor = mxGraphView; + +/** + * + */ +mxGraphView.prototype.EMPTY_POINT = new mxPoint(); + +/** + * Variable: doneResource + * + * Specifies the resource key for the status message after a long operation. + * If the resource for this key does not exist then the value is used as + * the status message. Default is 'done'. + */ +mxGraphView.prototype.doneResource = (mxClient.language != 'none') ? 'done' : ''; + +/** + * Function: updatingDocumentResource + * + * Specifies the resource key for the status message while the document is + * being updated. If the resource for this key does not exist then the + * value is used as the status message. Default is 'updatingDocument'. + */ +mxGraphView.prototype.updatingDocumentResource = (mxClient.language != 'none') ? 'updatingDocument' : ''; + +/** + * Variable: allowEval + * + * Specifies if string values in cell styles should be evaluated using + * <mxUtils.eval>. This will only be used if the string values can't be mapped + * to objects using <mxStyleRegistry>. Default is false. NOTE: Enabling this + * switch carries a possible security risk (see the section on security in + * the manual). + */ +mxGraphView.prototype.allowEval = false; + +/** + * Variable: captureDocumentGesture + * + * Specifies if a gesture should be captured when it goes outside of the + * graph container. Default is true. + */ +mxGraphView.prototype.captureDocumentGesture = true; + +/** + * Variable: rendering + * + * Specifies if shapes should be created, updated and destroyed using the + * methods of <mxCellRenderer> in <graph>. Default is true. + */ +mxGraphView.prototype.rendering = true; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxGraphView.prototype.graph = null; + +/** + * Variable: currentRoot + * + * <mxCell> that acts as the root of the displayed cell hierarchy. + */ +mxGraphView.prototype.currentRoot = null; + +/** + * Variable: graphBounds + * + * <mxRectangle> that caches the scales, translated bounds of the current view. + */ +mxGraphView.prototype.graphBounds = null; + +/** + * Variable: scale + * + * Specifies the scale. Default is 1 (100%). + */ +mxGraphView.prototype.scale = 1; + +/** + * Variable: translate + * + * <mxPoint> that specifies the current translation. Default is a new + * empty <mxPoint>. + */ +mxGraphView.prototype.translate = null; + +/** + * Variable: updateStyle + * + * Specifies if the style should be updated in each validation step. If this + * is false then the style is only updated if the state is created or if the + * style of the cell was changed. Default is false. + */ +mxGraphView.prototype.updateStyle = false; + +/** + * Function: getGraphBounds + * + * Returns <graphBounds>. + */ +mxGraphView.prototype.getGraphBounds = function() +{ + return this.graphBounds; +}; + +/** + * Function: setGraphBounds + * + * Sets <graphBounds>. + */ +mxGraphView.prototype.setGraphBounds = function(value) +{ + this.graphBounds = value; +}; + +/** + * Function: getBounds + * + * Returns the bounds (on the screen) for the given array of <mxCells>. + * + * Parameters: + * + * cells - Array of <mxCells> to return the bounds for. + */ +mxGraphView.prototype.getBounds = function(cells) +{ + var result = null; + + if (cells != null && cells.length > 0) + { + var model = this.graph.getModel(); + + for (var i = 0; i < cells.length; i++) + { + if (model.isVertex(cells[i]) || model.isEdge(cells[i])) + { + var state = this.getState(cells[i]); + + if (state != null) + { + if (result == null) + { + result = new mxRectangle(state.x, state.y, + state.width, state.height); + } + else + { + result.add(state); + } + } + } + } + } + + return result; +}; + +/** + * Function: setCurrentRoot + * + * Sets and returns the current root and fires an <undo> event before + * calling <mxGraph.sizeDidChange>. + * + * Parameters: + * + * root - <mxCell> that specifies the root of the displayed cell hierarchy. + */ +mxGraphView.prototype.setCurrentRoot = function(root) +{ + if (this.currentRoot != root) + { + var change = new mxCurrentRootChange(this, root); + change.execute(); + var edit = new mxUndoableEdit(this, false); + edit.add(change); + this.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit)); + this.graph.sizeDidChange(); + } + + return root; +}; + +/** + * Function: scaleAndTranslate + * + * Sets the scale and translation and fires a <scale> and <translate> event + * before calling <revalidate> followed by <mxGraph.sizeDidChange>. + * + * Parameters: + * + * scale - Decimal value that specifies the new scale (1 is 100%). + * dx - X-coordinate of the translation. + * dy - Y-coordinate of the translation. + */ +mxGraphView.prototype.scaleAndTranslate = function(scale, dx, dy) +{ + var previousScale = this.scale; + var previousTranslate = new mxPoint(this.translate.x, this.translate.y); + + if (this.scale != scale || this.translate.x != dx || this.translate.y != dy) + { + this.scale = scale; + + this.translate.x = dx; + this.translate.y = dy; + + if (this.isEventsEnabled()) + { + this.revalidate(); + this.graph.sizeDidChange(); + } + } + + this.fireEvent(new mxEventObject(mxEvent.SCALE_AND_TRANSLATE, + 'scale', scale, 'previousScale', previousScale, + 'translate', this.translate, 'previousTranslate', previousTranslate)); +}; + +/** + * Function: getScale + * + * Returns the <scale>. + */ +mxGraphView.prototype.getScale = function() +{ + return this.scale; +}; + +/** + * Function: setScale + * + * Sets the scale and fires a <scale> event before calling <revalidate> followed + * by <mxGraph.sizeDidChange>. + * + * Parameters: + * + * value - Decimal value that specifies the new scale (1 is 100%). + */ +mxGraphView.prototype.setScale = function(value) +{ + var previousScale = this.scale; + + if (this.scale != value) + { + this.scale = value; + + if (this.isEventsEnabled()) + { + this.revalidate(); + this.graph.sizeDidChange(); + } + } + + this.fireEvent(new mxEventObject(mxEvent.SCALE, + 'scale', value, 'previousScale', previousScale)); +}; + +/** + * Function: getTranslate + * + * Returns the <translate>. + */ +mxGraphView.prototype.getTranslate = function() +{ + return this.translate; +}; + +/** + * Function: setTranslate + * + * Sets the translation and fires a <translate> event before calling + * <revalidate> followed by <mxGraph.sizeDidChange>. The translation is the + * negative of the origin. + * + * Parameters: + * + * dx - X-coordinate of the translation. + * dy - Y-coordinate of the translation. + */ +mxGraphView.prototype.setTranslate = function(dx, dy) +{ + var previousTranslate = new mxPoint(this.translate.x, this.translate.y); + + if (this.translate.x != dx || this.translate.y != dy) + { + this.translate.x = dx; + this.translate.y = dy; + + if (this.isEventsEnabled()) + { + this.revalidate(); + this.graph.sizeDidChange(); + } + } + + this.fireEvent(new mxEventObject(mxEvent.TRANSLATE, + 'translate', this.translate, 'previousTranslate', previousTranslate)); +}; + +/** + * Function: refresh + * + * Clears the view if <currentRoot> is not null and revalidates. + */ +mxGraphView.prototype.refresh = function() +{ + if (this.currentRoot != null) + { + this.clear(); + } + + this.revalidate(); +}; + +/** + * Function: revalidate + * + * Revalidates the complete view with all cell states. + */ +mxGraphView.prototype.revalidate = function() +{ + this.invalidate(); + this.validate(); +}; + +/** + * Function: clear + * + * Removes the state of the given cell and all descendants if the given + * cell is not the current root. + * + * Parameters: + * + * cell - Optional <mxCell> for which the state should be removed. Default + * is the root of the model. + * force - Boolean indicating if the current root should be ignored for + * recursion. + */ +mxGraphView.prototype.clear = function(cell, force, recurse) +{ + var model = this.graph.getModel(); + cell = cell || model.getRoot(); + force = (force != null) ? force : false; + recurse = (recurse != null) ? recurse : true; + + this.removeState(cell); + + if (recurse && (force || cell != this.currentRoot)) + { + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + this.clear(model.getChildAt(cell, i), force); + } + } + else + { + this.invalidate(cell); + } +}; + +/** + * Function: invalidate + * + * Invalidates the state of the given cell, all its descendants and + * connected edges. + * + * Parameters: + * + * cell - Optional <mxCell> to be invalidated. Default is the root of the + * model. + */ +mxGraphView.prototype.invalidate = function(cell, recurse, includeEdges, orderChanged) +{ + var model = this.graph.getModel(); + cell = cell || model.getRoot(); + recurse = (recurse != null) ? recurse : true; + includeEdges = (includeEdges != null) ? includeEdges : true; + orderChanged = (orderChanged != null) ? orderChanged : false; + + var state = this.getState(cell); + + if (state != null) + { + state.invalid = true; + + if (orderChanged) + { + state.orderChanged = true; + } + } + + // Recursively invalidates all descendants + if (recurse) + { + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(cell, i); + this.invalidate(child, recurse, includeEdges, orderChanged); + } + } + + // Propagates invalidation to all connected edges + if (includeEdges) + { + var edgeCount = model.getEdgeCount(cell); + + for (var i = 0; i < edgeCount; i++) + { + this.invalidate(model.getEdgeAt(cell, i), recurse, includeEdges); + } + } +}; + +/** + * Function: validate + * + * First validates all bounds and then validates all points recursively on + * all visible cells starting at the given cell. Finally the background + * is validated using <validateBackground>. + * + * Parameters: + * + * cell - Optional <mxCell> to be used as the root of the validation. + * Default is <currentRoot> or the root of the model. + */ +mxGraphView.prototype.validate = function(cell) +{ + var t0 = mxLog.enter('mxGraphView.validate'); + window.status = mxResources.get(this.updatingDocumentResource) || + this.updatingDocumentResource; + + cell = cell || ((this.currentRoot != null) ? + this.currentRoot : + this.graph.getModel().getRoot()); + this.validateBounds(null, cell); + var graphBounds = this.validatePoints(null, cell); + + if (graphBounds == null) + { + graphBounds = new mxRectangle(); + } + + this.setGraphBounds(graphBounds); + this.validateBackground(); + + window.status = mxResources.get(this.doneResource) || + this.doneResource; + mxLog.leave('mxGraphView.validate', t0); +}; + +/** + * Function: createBackgroundPageShape + * + * Creates and returns the shape used as the background page. + * + * Parameters: + * + * bounds - <mxRectangle> that represents the bounds of the shape. + */ +mxGraphView.prototype.createBackgroundPageShape = function(bounds) +{ + return new mxRectangleShape(bounds, 'white', 'black'); +}; + +/** + * Function: validateBackground + * + * Validates the background image. + */ +mxGraphView.prototype.validateBackground = function() +{ + var bg = this.graph.getBackgroundImage(); + + if (bg != null) + { + if (this.backgroundImage == null || this.backgroundImage.image != bg.src) + { + if (this.backgroundImage != null) + { + this.backgroundImage.destroy(); + } + + var bounds = new mxRectangle(0, 0, 1, 1); + + this.backgroundImage = new mxImageShape(bounds, bg.src); + this.backgroundImage.dialect = this.graph.dialect; + this.backgroundImage.init(this.backgroundPane); + this.backgroundImage.redraw(); + } + + this.redrawBackgroundImage(this.backgroundImage, bg); + } + else if (this.backgroundImage != null) + { + this.backgroundImage.destroy(); + this.backgroundImage = null; + } + + if (this.graph.pageVisible) + { + var bounds = this.getBackgroundPageBounds(); + + if (this.backgroundPageShape == null) + { + this.backgroundPageShape = this.createBackgroundPageShape(bounds); + this.backgroundPageShape.scale = this.scale; + this.backgroundPageShape.isShadow = true; + this.backgroundPageShape.dialect = this.graph.dialect; + this.backgroundPageShape.init(this.backgroundPane); + this.backgroundPageShape.redraw(); + + // Adds listener for double click handling on background + mxEvent.addListener(this.backgroundPageShape.node, 'dblclick', + mxUtils.bind(this, function(evt) + { + this.graph.dblClick(evt); + }) + ); + + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + // Adds basic listeners for graph event dispatching outside of the + // container and finishing the handling of a single gesture + mxEvent.addListener(this.backgroundPageShape.node, md, + mxUtils.bind(this, function(evt) + { + this.graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt)); + }) + ); + mxEvent.addListener(this.backgroundPageShape.node, mm, + mxUtils.bind(this, function(evt) + { + // Hides the tooltip if mouse is outside container + if (this.graph.tooltipHandler != null && + this.graph.tooltipHandler.isHideOnHover()) + { + this.graph.tooltipHandler.hide(); + } + + if (this.graph.isMouseDown && + !mxEvent.isConsumed(evt)) + { + this.graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt)); + } + }) + ); + mxEvent.addListener(this.backgroundPageShape.node, mu, + mxUtils.bind(this, function(evt) + { + this.graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt)); + }) + ); + } + else + { + this.backgroundPageShape.scale = this.scale; + this.backgroundPageShape.bounds = bounds; + this.backgroundPageShape.redraw(); + } + } + else if (this.backgroundPageShape != null) + { + this.backgroundPageShape.destroy(); + this.backgroundPageShape = null; + } +}; + +/** + * Function: getBackgroundPageBounds + * + * Returns the bounds for the background page. + */ +mxGraphView.prototype.getBackgroundPageBounds = function() +{ + var fmt = this.graph.pageFormat; + var ps = this.scale * this.graph.pageScale; + var bounds = new mxRectangle(this.scale * this.translate.x, this.scale * this.translate.y, + fmt.width * ps, fmt.height * ps); + + return bounds; +}; + +/** + * Function: redrawBackgroundImage + * + * Updates the bounds and redraws the background image. + * + * Example: + * + * If the background image should not be scaled, this can be replaced with + * the following. + * + * (code) + * mxGraphView.prototype.redrawBackground = function(backgroundImage, bg) + * { + * backgroundImage.bounds.x = this.translate.x; + * backgroundImage.bounds.y = this.translate.y; + * backgroundImage.bounds.width = bg.width; + * backgroundImage.bounds.height = bg.height; + * + * backgroundImage.redraw(); + * }; + * (end) + * + * Parameters: + * + * backgroundImage - <mxImageShape> that represents the background image. + * bg - <mxImage> that specifies the image and its dimensions. + */ +mxGraphView.prototype.redrawBackgroundImage = function(backgroundImage, bg) +{ + backgroundImage.scale = this.scale; + backgroundImage.bounds.x = this.scale * this.translate.x; + backgroundImage.bounds.y = this.scale * this.translate.y; + backgroundImage.bounds.width = this.scale * bg.width; + backgroundImage.bounds.height = this.scale * bg.height; + + backgroundImage.redraw(); +}; + +/** + * Function: validateBounds + * + * Validates the bounds of the given parent's child using the given parent + * state as the origin for the child. The validation is carried out + * recursively for all non-collapsed descendants. + * + * Parameters: + * + * parentState - <mxCellState> for the given parent. + * cell - <mxCell> for which the bounds in the state should be updated. + */ +mxGraphView.prototype.validateBounds = function(parentState, cell) +{ + var model = this.graph.getModel(); + var state = this.getState(cell, true); + + if (state != null && state.invalid) + { + if (!this.graph.isCellVisible(cell)) + { + this.removeState(cell); + } + + // Updates the cell state's origin + else if (cell != this.currentRoot && parentState != null) + { + state.absoluteOffset.x = 0; + state.absoluteOffset.y = 0; + state.origin.x = parentState.origin.x; + state.origin.y = parentState.origin.y; + var geo = this.graph.getCellGeometry(cell); + + if (geo != null) + { + if (!model.isEdge(cell)) + { + var offset = geo.offset || this.EMPTY_POINT; + + if (geo.relative) + { + state.origin.x += geo.x * parentState.width / + this.scale + offset.x; + state.origin.y += geo.y * parentState.height / + this.scale + offset.y; + } + else + { + state.absoluteOffset.x = this.scale * offset.x; + state.absoluteOffset.y = this.scale * offset.y; + state.origin.x += geo.x; + state.origin.y += geo.y; + } + } + + // Updates cell state's bounds + state.x = this.scale * (this.translate.x + state.origin.x); + state.y = this.scale * (this.translate.y + state.origin.y); + state.width = this.scale * geo.width; + state.height = this.scale * geo.height; + + if (model.isVertex(cell)) + { + this.updateVertexLabelOffset(state); + } + } + } + + // Applies child offset to origin + var offset = this.graph.getChildOffsetForCell(cell); + + if (offset != null) + { + state.origin.x += offset.x; + state.origin.y += offset.y; + } + } + + // Recursively validates the child bounds + if (state != null && (!this.graph.isCellCollapsed(cell) || + cell == this.currentRoot)) + { + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(cell, i); + this.validateBounds(state, child); + } + } +}; + +/** + * Function: updateVertexLabelOffset + * + * Updates the absoluteOffset of the given vertex cell state. This takes + * into account the label position styles. + * + * Parameters: + * + * state - <mxCellState> whose absolute offset should be updated. + */ +mxGraphView.prototype.updateVertexLabelOffset = function(state) +{ + var horizontal = mxUtils.getValue(state.style, + mxConstants.STYLE_LABEL_POSITION, + mxConstants.ALIGN_CENTER); + + if (horizontal == mxConstants.ALIGN_LEFT) + { + state.absoluteOffset.x -= state.width; + } + else if (horizontal == mxConstants.ALIGN_RIGHT) + { + state.absoluteOffset.x += state.width; + } + + var vertical = mxUtils.getValue(state.style, + mxConstants.STYLE_VERTICAL_LABEL_POSITION, + mxConstants.ALIGN_MIDDLE); + + if (vertical == mxConstants.ALIGN_TOP) + { + state.absoluteOffset.y -= state.height; + } + else if (vertical == mxConstants.ALIGN_BOTTOM) + { + state.absoluteOffset.y += state.height; + } +}; + +/** + * Function: validatePoints + * + * Validates the points for the state of the given cell recursively if the + * cell is not collapsed and returns the bounding box of all visited states + * as an <mxRectangle>. + * + * Parameters: + * + * parentState - <mxCellState> for the parent cell. + * cell - <mxCell> whose points in the state should be updated. + */ +mxGraphView.prototype.validatePoints = function(parentState, cell) +{ + var model = this.graph.getModel(); + var state = this.getState(cell); + var bbox = null; + + if (state != null) + { + if (state.invalid) + { + var geo = this.graph.getCellGeometry(cell); + + if (geo != null && model.isEdge(cell)) + { + // Updates the points on the source terminal if its an edge + var source = this.getState(this.getVisibleTerminal(cell, true)); + state.setVisibleTerminalState(source, true); + + if (source != null && model.isEdge(source.cell) && + !model.isAncestor(source.cell, cell)) + { + var tmp = this.getState(model.getParent(source.cell)); + this.validatePoints(tmp, source.cell); + } + + // Updates the points on the target terminal if its an edge + var target = this.getState(this.getVisibleTerminal(cell, false)); + state.setVisibleTerminalState(target, false); + + if (target != null && model.isEdge(target.cell) && + !model.isAncestor(target.cell, cell)) + { + var tmp = this.getState(model.getParent(target.cell)); + this.validatePoints(tmp, target.cell); + } + + this.updateFixedTerminalPoints(state, source, target); + this.updatePoints(state, geo.points, source, target); + this.updateFloatingTerminalPoints(state, source, target); + this.updateEdgeBounds(state); + this.updateEdgeLabelOffset(state); + } + else if (geo != null && geo.relative && parentState != null && + model.isEdge(parentState.cell)) + { + var origin = this.getPoint(parentState, geo); + + if (origin != null) + { + state.x = origin.x; + state.y = origin.y; + + origin.x = (origin.x / this.scale) - this.translate.x; + origin.y = (origin.y / this.scale) - this.translate.y; + state.origin = origin; + + this.childMoved(parentState, state); + } + } + + state.invalid = false; + + if (cell != this.currentRoot) + { + // NOTE: Label bounds currently ignored if rendering is false + this.graph.cellRenderer.redraw(state, false, this.isRendering()); + } + } + + if (model.isEdge(cell) || model.isVertex(cell)) + { + if (state.shape != null && state.shape.boundingBox != null) + { + bbox = state.shape.boundingBox.clone(); + } + + if (state.text != null && !this.graph.isLabelClipped(state.cell)) + { + // Adds label bounding box to graph bounds + if (state.text.boundingBox != null) + { + if (bbox != null) + { + bbox.add(state.text.boundingBox); + } + else + { + bbox = state.text.boundingBox.clone(); + } + } + } + } + } + + if (state != null && (!this.graph.isCellCollapsed(cell) || + cell == this.currentRoot)) + { + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(cell, i); + var bounds = this.validatePoints(state, child); + + if (bounds != null) + { + if (bbox == null) + { + bbox = bounds; + } + else + { + bbox.add(bounds); + } + } + } + } + + return bbox; +}; + +/** + * Function: childMoved + * + * Invoked when a child state was moved as a result of late evaluation + * of its position. This is invoked for relative edge children whose + * position can only be determined after the points of the parent edge + * are updated in validatePoints, and validates the bounds of all + * descendants of the child using validateBounds. + * + * Parameters: + * + * parent - <mxCellState> that represents the parent state. + * child - <mxCellState> that represents the child state. + */ +mxGraphView.prototype.childMoved = function(parent, child) +{ + var cell = child.cell; + + // Children of relative edge children need to validate + // their bounds after their parent state was updated + if (!this.graph.isCellCollapsed(cell) || cell == this.currentRoot) + { + var model = this.graph.getModel(); + var childCount = model.getChildCount(cell); + + for (var i = 0; i < childCount; i++) + { + this.validateBounds(child, model.getChildAt(cell, i)); + } + } +}; + +/** + * Function: updateFixedTerminalPoints + * + * Sets the initial absolute terminal points in the given state before the edge + * style is computed. + * + * Parameters: + * + * edge - <mxCellState> whose initial terminal points should be updated. + * source - <mxCellState> which represents the source terminal. + * target - <mxCellState> which represents the target terminal. + */ +mxGraphView.prototype.updateFixedTerminalPoints = function(edge, source, target) +{ + this.updateFixedTerminalPoint(edge, source, true, + this.graph.getConnectionConstraint(edge, source, true)); + this.updateFixedTerminalPoint(edge, target, false, + this.graph.getConnectionConstraint(edge, target, false)); +}; + +/** + * Function: updateFixedTerminalPoint + * + * Sets the fixed source or target terminal point on the given edge. + * + * Parameters: + * + * edge - <mxCellState> whose terminal point should be updated. + * terminal - <mxCellState> which represents the actual terminal. + * source - Boolean that specifies if the terminal is the source. + * constraint - <mxConnectionConstraint> that specifies the connection. + */ +mxGraphView.prototype.updateFixedTerminalPoint = function(edge, terminal, source, constraint) +{ + var pt = null; + + if (constraint != null) + { + pt = this.graph.getConnectionPoint(terminal, constraint); + } + + if (pt == null && terminal == null) + { + var s = this.scale; + var tr = this.translate; + var orig = edge.origin; + var geo = this.graph.getCellGeometry(edge.cell); + pt = geo.getTerminalPoint(source); + + if (pt != null) + { + pt = new mxPoint(s * (tr.x + pt.x + orig.x), + s * (tr.y + pt.y + orig.y)); + } + } + + edge.setAbsoluteTerminalPoint(pt, source); +}; + +/** + * Function: updatePoints + * + * Updates the absolute points in the given state using the specified array + * of <mxPoints> as the relative points. + * + * Parameters: + * + * edge - <mxCellState> whose absolute points should be updated. + * points - Array of <mxPoints> that constitute the relative points. + * source - <mxCellState> that represents the source terminal. + * target - <mxCellState> that represents the target terminal. + */ +mxGraphView.prototype.updatePoints = function(edge, points, source, target) +{ + if (edge != null) + { + var pts = []; + pts.push(edge.absolutePoints[0]); + var edgeStyle = this.getEdgeStyle(edge, points, source, target); + + if (edgeStyle != null) + { + var src = this.getTerminalPort(edge, source, true); + var trg = this.getTerminalPort(edge, target, false); + + edgeStyle(edge, src, trg, points, pts); + } + else if (points != null) + { + for (var i = 0; i < points.length; i++) + { + if (points[i] != null) + { + var pt = mxUtils.clone(points[i]); + pts.push(this.transformControlPoint(edge, pt)); + } + } + } + + var tmp = edge.absolutePoints; + pts.push(tmp[tmp.length-1]); + + edge.absolutePoints = pts; + } +}; + +/** + * Function: transformControlPoint + * + * Transforms the given control point to an absolute point. + */ +mxGraphView.prototype.transformControlPoint = function(state, pt) +{ + var orig = state.origin; + + return new mxPoint(this.scale * (pt.x + this.translate.x + orig.x), + this.scale * (pt.y + this.translate.y + orig.y)); +}; + +/** + * Function: getEdgeStyle + * + * Returns the edge style function to be used to render the given edge + * state. + */ +mxGraphView.prototype.getEdgeStyle = function(edge, points, source, target) +{ + var edgeStyle = (source != null && source == target) ? + mxUtils.getValue(edge.style, mxConstants.STYLE_LOOP, + this.graph.defaultLoopStyle) : + (!mxUtils.getValue(edge.style, + mxConstants.STYLE_NOEDGESTYLE, false) ? + edge.style[mxConstants.STYLE_EDGE] : + null); + + // Converts string values to objects + if (typeof(edgeStyle) == "string") + { + var tmp = mxStyleRegistry.getValue(edgeStyle); + + if (tmp == null && this.isAllowEval()) + { + tmp = mxUtils.eval(edgeStyle); + } + + edgeStyle = tmp; + } + + if (typeof(edgeStyle) == "function") + { + return edgeStyle; + } + + return null; +}; + +/** + * Function: updateFloatingTerminalPoints + * + * Updates the terminal points in the given state after the edge style was + * computed for the edge. + * + * Parameters: + * + * state - <mxCellState> whose terminal points should be updated. + * source - <mxCellState> that represents the source terminal. + * target - <mxCellState> that represents the target terminal. + */ +mxGraphView.prototype.updateFloatingTerminalPoints = function(state, source, target) +{ + var pts = state.absolutePoints; + var p0 = pts[0]; + var pe = pts[pts.length - 1]; + + if (pe == null && target != null) + { + this.updateFloatingTerminalPoint(state, target, source, false); + } + + if (p0 == null && source != null) + { + this.updateFloatingTerminalPoint(state, source, target, true); + } +}; + +/** + * Function: updateFloatingTerminalPoint + * + * Updates the absolute terminal point in the given state for the given + * start and end state, where start is the source if source is true. + * + * Parameters: + * + * edge - <mxCellState> whose terminal point should be updated. + * start - <mxCellState> for the terminal on "this" side of the edge. + * end - <mxCellState> for the terminal on the other side of the edge. + * source - Boolean indicating if start is the source terminal state. + */ +mxGraphView.prototype.updateFloatingTerminalPoint = function(edge, start, end, source) +{ + start = this.getTerminalPort(edge, start, source); + var next = this.getNextPoint(edge, end, source); + + var alpha = mxUtils.toRadians(Number(start.style[mxConstants.STYLE_ROTATION] || '0')); + var center = new mxPoint(start.getCenterX(), start.getCenterY()); + + if (alpha != 0) + { + var cos = Math.cos(-alpha); + var sin = Math.sin(-alpha); + next = mxUtils.getRotatedPoint(next, cos, sin, center); + } + + var border = parseFloat(edge.style[mxConstants.STYLE_PERIMETER_SPACING] || 0); + border += parseFloat(edge.style[(source) ? + mxConstants.STYLE_SOURCE_PERIMETER_SPACING : + mxConstants.STYLE_TARGET_PERIMETER_SPACING] || 0); + var pt = this.getPerimeterPoint(start, next, this.graph.isOrthogonal(edge), border); + + if (alpha != 0) + { + var cos = Math.cos(alpha); + var sin = Math.sin(alpha); + pt = mxUtils.getRotatedPoint(pt, cos, sin, center); + } + + edge.setAbsoluteTerminalPoint(pt, source); +}; + +/** + * Function: getTerminalPort + * + * Returns an <mxCellState> that represents the source or target terminal or + * port for the given edge. + * + * Parameters: + * + * state - <mxCellState> that represents the state of the edge. + * terminal - <mxCellState> that represents the terminal. + * source - Boolean indicating if the given terminal is the source terminal. + */ +mxGraphView.prototype.getTerminalPort = function(state, terminal, source) +{ + var key = (source) ? mxConstants.STYLE_SOURCE_PORT : + mxConstants.STYLE_TARGET_PORT; + var id = mxUtils.getValue(state.style, key); + + if (id != null) + { + var tmp = this.getState(this.graph.getModel().getCell(id)); + + // Only uses ports where a cell state exists + if (tmp != null) + { + terminal = tmp; + } + } + + return terminal; +}; + +/** + * Function: getPerimeterPoint + * + * Returns an <mxPoint> that defines the location of the intersection point between + * the perimeter and the line between the center of the shape and the given point. + * + * Parameters: + * + * terminal - <mxCellState> for the source or target terminal. + * next - <mxPoint> that lies outside of the given terminal. + * orthogonal - Boolean that specifies if the orthogonal projection onto + * the perimeter should be returned. If this is false then the intersection + * of the perimeter and the line between the next and the center point is + * returned. + * border - Optional border between the perimeter and the shape. + */ +mxGraphView.prototype.getPerimeterPoint = function(terminal, next, orthogonal, border) +{ + var point = null; + + if (terminal != null) + { + var perimeter = this.getPerimeterFunction(terminal); + + if (perimeter != null && next != null) + { + var bounds = this.getPerimeterBounds(terminal, border); + + if (bounds.width > 0 || bounds.height > 0) + { + point = perimeter(bounds, terminal, next, orthogonal); + } + } + + if (point == null) + { + point = this.getPoint(terminal); + } + } + + return point; +}; + +/** + * Function: getRoutingCenterX + * + * Returns the x-coordinate of the center point for automatic routing. + */ +mxGraphView.prototype.getRoutingCenterX = function (state) +{ + var f = (state.style != null) ? parseFloat(state.style + [mxConstants.STYLE_ROUTING_CENTER_X]) || 0 : 0; + + return state.getCenterX() + f * state.width; +}; + +/** + * Function: getRoutingCenterY + * + * Returns the y-coordinate of the center point for automatic routing. + */ +mxGraphView.prototype.getRoutingCenterY = function (state) +{ + var f = (state.style != null) ? parseFloat(state.style + [mxConstants.STYLE_ROUTING_CENTER_Y]) || 0 : 0; + + return state.getCenterY() + f * state.height; +}; + +/** + * Function: getPerimeterBounds + * + * Returns the perimeter bounds for the given terminal, edge pair as an + * <mxRectangle>. + * + * If you have a model where each terminal has a relative child that should + * act as the graphical endpoint for a connection from/to the terminal, then + * this method can be replaced as follows: + * + * (code) + * var oldGetPerimeterBounds = mxGraphView.prototype.getPerimeterBounds; + * mxGraphView.prototype.getPerimeterBounds = function(terminal, edge, isSource) + * { + * var model = this.graph.getModel(); + * var childCount = model.getChildCount(terminal.cell); + * + * if (childCount > 0) + * { + * var child = model.getChildAt(terminal.cell, 0); + * var geo = model.getGeometry(child); + * + * if (geo != null && + * geo.relative) + * { + * var state = this.getState(child); + * + * if (state != null) + * { + * terminal = state; + * } + * } + * } + * + * return oldGetPerimeterBounds.apply(this, arguments); + * }; + * (end) + * + * Parameters: + * + * terminal - <mxCellState> that represents the terminal. + * border - Number that adds a border between the shape and the perimeter. + */ +mxGraphView.prototype.getPerimeterBounds = function(terminal, border) +{ + border = (border != null) ? border : 0; + + if (terminal != null) + { + border += parseFloat(terminal.style[mxConstants.STYLE_PERIMETER_SPACING] || 0); + } + + return terminal.getPerimeterBounds(border * this.scale); +}; + +/** + * Function: getPerimeterFunction + * + * Returns the perimeter function for the given state. + */ +mxGraphView.prototype.getPerimeterFunction = function(state) +{ + var perimeter = state.style[mxConstants.STYLE_PERIMETER]; + + // Converts string values to objects + if (typeof(perimeter) == "string") + { + var tmp = mxStyleRegistry.getValue(perimeter); + + if (tmp == null && this.isAllowEval()) + { + tmp = mxUtils.eval(perimeter); + } + + perimeter = tmp; + } + + if (typeof(perimeter) == "function") + { + return perimeter; + } + + return null; +}; + +/** + * Function: getNextPoint + * + * Returns the nearest point in the list of absolute points or the center + * of the opposite terminal. + * + * Parameters: + * + * edge - <mxCellState> that represents the edge. + * opposite - <mxCellState> that represents the opposite terminal. + * source - Boolean indicating if the next point for the source or target + * should be returned. + */ +mxGraphView.prototype.getNextPoint = function(edge, opposite, source) +{ + var pts = edge.absolutePoints; + var point = null; + + if (pts != null && (source || pts.length > 2 || opposite == null)) + { + var count = pts.length; + point = pts[(source) ? Math.min(1, count - 1) : Math.max(0, count - 2)]; + } + + if (point == null && opposite != null) + { + point = new mxPoint(opposite.getCenterX(), opposite.getCenterY()); + } + + return point; +}; + +/** + * Function: getVisibleTerminal + * + * Returns the nearest ancestor terminal that is visible. The edge appears + * to be connected to this terminal on the display. The result of this method + * is cached in <mxCellState.getVisibleTerminalState>. + * + * Parameters: + * + * edge - <mxCell> whose visible terminal should be returned. + * source - Boolean that specifies if the source or target terminal + * should be returned. + */ +mxGraphView.prototype.getVisibleTerminal = function(edge, source) +{ + var model = this.graph.getModel(); + var result = model.getTerminal(edge, source); + var best = result; + + while (result != null && result != this.currentRoot) + { + if (!this.graph.isCellVisible(best) || this.graph.isCellCollapsed(result)) + { + best = result; + } + + result = model.getParent(result); + } + + // Checks if the result is not a layer + if (model.getParent(best) == model.getRoot()) + { + best = null; + } + + return best; +}; + +/** + * Function: updateEdgeBounds + * + * Updates the given state using the bounding box of the absolute points. + * Also updates <mxCellState.terminalDistance>, <mxCellState.length> and + * <mxCellState.segments>. + * + * Parameters: + * + * state - <mxCellState> whose bounds should be updated. + */ +mxGraphView.prototype.updateEdgeBounds = function(state) +{ + var points = state.absolutePoints; + state.length = 0; + + if (points != null && points.length > 0) + { + var p0 = points[0]; + var pe = points[points.length - 1]; + + if (p0 == null || pe == null) + { + // Drops the edge state if the edge is not the root + if (state.cell != this.currentRoot) + { + // Note: This condition normally occurs if a connected edge has a + // null-terminal, ie. edge.source == null or edge.target == null, + // and no corresponding terminal point defined, which happens for + // example if the terminal-id was not resolved at cell decoding time. + this.clear(state.cell, true); + } + } + else + { + if (p0.x != pe.x || p0.y != pe.y) + { + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + state.terminalDistance = Math.sqrt(dx * dx + dy * dy); + } + else + { + state.terminalDistance = 0; + } + + var length = 0; + var segments = []; + var pt = p0; + + if (pt != null) + { + var minX = pt.x; + var minY = pt.y; + var maxX = minX; + var maxY = minY; + + for (var i = 1; i < points.length; i++) + { + var tmp = points[i]; + + if (tmp != null) + { + var dx = pt.x - tmp.x; + var dy = pt.y - tmp.y; + + var segment = Math.sqrt(dx * dx + dy * dy); + segments.push(segment); + length += segment; + + pt = tmp; + + minX = Math.min(pt.x, minX); + minY = Math.min(pt.y, minY); + maxX = Math.max(pt.x, maxX); + maxY = Math.max(pt.y, maxY); + } + } + + state.length = length; + state.segments = segments; + + var markerSize = 1; // TODO: include marker size + + state.x = minX; + state.y = minY; + state.width = Math.max(markerSize, maxX - minX); + state.height = Math.max(markerSize, maxY - minY); + } + } + } +}; + +/** + * Function: getPoint + * + * Returns the absolute point on the edge for the given relative + * <mxGeometry> as an <mxPoint>. The edge is represented by the given + * <mxCellState>. + * + * Parameters: + * + * state - <mxCellState> that represents the state of the parent edge. + * geometry - <mxGeometry> that represents the relative location. + */ +mxGraphView.prototype.getPoint = function(state, geometry) +{ + var x = state.getCenterX(); + var y = state.getCenterY(); + + if (state.segments != null && (geometry == null || geometry.relative)) + { + var gx = (geometry != null) ? geometry.x / 2 : 0; + var pointCount = state.absolutePoints.length; + var dist = (gx + 0.5) * state.length; + var segment = state.segments[0]; + var length = 0; + var index = 1; + + while (dist > length + segment && index < pointCount-1) + { + length += segment; + segment = state.segments[index++]; + } + + var factor = (segment == 0) ? 0 : (dist - length) / segment; + var p0 = state.absolutePoints[index-1]; + var pe = state.absolutePoints[index]; + + if (p0 != null && pe != null) + { + var gy = 0; + var offsetX = 0; + var offsetY = 0; + + if (geometry != null) + { + gy = geometry.y; + var offset = geometry.offset; + + if (offset != null) + { + offsetX = offset.x; + offsetY = offset.y; + } + } + + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + var nx = (segment == 0) ? 0 : dy / segment; + var ny = (segment == 0) ? 0 : dx / segment; + + x = p0.x + dx * factor + (nx * gy + offsetX) * this.scale; + y = p0.y + dy * factor - (ny * gy - offsetY) * this.scale; + } + } + else if (geometry != null) + { + var offset = geometry.offset; + + if (offset != null) + { + x += offset.x; + y += offset.y; + } + } + + return new mxPoint(x, y); +}; + +/** + * Function: getRelativePoint + * + * Gets the relative point that describes the given, absolute label + * position for the given edge state. + * + * Parameters: + * + * state - <mxCellState> that represents the state of the parent edge. + * x - Specifies the x-coordinate of the absolute label location. + * y - Specifies the y-coordinate of the absolute label location. + */ +mxGraphView.prototype.getRelativePoint = function(edgeState, x, y) +{ + var model = this.graph.getModel(); + var geometry = model.getGeometry(edgeState.cell); + + if (geometry != null) + { + var pointCount = edgeState.absolutePoints.length; + + if (geometry.relative && pointCount > 1) + { + var totalLength = edgeState.length; + var segments = edgeState.segments; + + // Works which line segment the point of the label is closest to + var p0 = edgeState.absolutePoints[0]; + var pe = edgeState.absolutePoints[1]; + var minDist = mxUtils.ptSegDistSq(p0.x, p0.y, pe.x, pe.y, x, y); + + var index = 0; + var tmp = 0; + var length = 0; + + for (var i = 2; i < pointCount; i++) + { + tmp += segments[i - 2]; + pe = edgeState.absolutePoints[i]; + var dist = mxUtils.ptSegDistSq(p0.x, p0.y, pe.x, pe.y, x, y); + + if (dist <= minDist) + { + minDist = dist; + index = i - 1; + length = tmp; + } + + p0 = pe; + } + + var seg = segments[index]; + p0 = edgeState.absolutePoints[index]; + pe = edgeState.absolutePoints[index + 1]; + + var x2 = p0.x; + var y2 = p0.y; + + var x1 = pe.x; + var y1 = pe.y; + + var px = x; + var py = y; + + var xSegment = x2 - x1; + var ySegment = y2 - y1; + + px -= x1; + py -= y1; + var projlenSq = 0; + + px = xSegment - px; + py = ySegment - py; + var dotprod = px * xSegment + py * ySegment; + + if (dotprod <= 0.0) + { + projlenSq = 0; + } + else + { + projlenSq = dotprod * dotprod + / (xSegment * xSegment + ySegment * ySegment); + } + + var projlen = Math.sqrt(projlenSq); + + if (projlen > seg) + { + projlen = seg; + } + + var yDistance = Math.sqrt(mxUtils.ptSegDistSq(p0.x, p0.y, pe + .x, pe.y, x, y)); + var direction = mxUtils.relativeCcw(p0.x, p0.y, pe.x, pe.y, x, y); + + if (direction == -1) + { + yDistance = -yDistance; + } + + // Constructs the relative point for the label + return new mxPoint(((totalLength / 2 - length - projlen) / totalLength) * -2, + yDistance / this.scale); + } + } + + return new mxPoint(); +}; + +/** + * Function: updateEdgeLabelOffset + * + * Updates <mxCellState.absoluteOffset> for the given state. The absolute + * offset is normally used for the position of the edge label. Is is + * calculated from the geometry as an absolute offset from the center + * between the two endpoints if the geometry is absolute, or as the + * relative distance between the center along the line and the absolute + * orthogonal distance if the geometry is relative. + * + * Parameters: + * + * state - <mxCellState> whose absolute offset should be updated. + */ +mxGraphView.prototype.updateEdgeLabelOffset = function(state) +{ + var points = state.absolutePoints; + + state.absoluteOffset.x = state.getCenterX(); + state.absoluteOffset.y = state.getCenterY(); + + if (points != null && points.length > 0 && state.segments != null) + { + var geometry = this.graph.getCellGeometry(state.cell); + + if (geometry.relative) + { + var offset = this.getPoint(state, geometry); + + if (offset != null) + { + state.absoluteOffset = offset; + } + } + else + { + var p0 = points[0]; + var pe = points[points.length - 1]; + + if (p0 != null && pe != null) + { + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + var x0 = 0; + var y0 = 0; + + var off = geometry.offset; + + if (off != null) + { + x0 = off.x; + y0 = off.y; + } + + var x = p0.x + dx / 2 + x0 * this.scale; + var y = p0.y + dy / 2 + y0 * this.scale; + + state.absoluteOffset.x = x; + state.absoluteOffset.y = y; + } + } + } +}; + +/** + * Function: getState + * + * Returns the <mxCellState> for the given cell. If create is true, then + * the state is created if it does not yet exist. + * + * Parameters: + * + * cell - <mxCell> for which the <mxCellState> should be returned. + * create - Optional boolean indicating if a new state should be created + * if it does not yet exist. Default is false. + */ +mxGraphView.prototype.getState = function(cell, create) +{ + create = create || false; + var state = null; + + if (cell != null) + { + state = this.states.get(cell); + + if (this.graph.isCellVisible(cell)) + { + if (state == null && create && this.graph.isCellVisible(cell)) + { + state = this.createState(cell); + this.states.put(cell, state); + } + else if (create && state != null && this.updateStyle) + { + state.style = this.graph.getCellStyle(cell); + } + } + } + + return state; +}; + +/** + * Function: isRendering + * + * Returns <rendering>. + */ +mxGraphView.prototype.isRendering = function() +{ + return this.rendering; +}; + +/** + * Function: setRendering + * + * Sets <rendering>. + */ +mxGraphView.prototype.setRendering = function(value) +{ + this.rendering = value; +}; + +/** + * Function: isAllowEval + * + * Returns <allowEval>. + */ +mxGraphView.prototype.isAllowEval = function() +{ + return this.allowEval; +}; + +/** + * Function: setAllowEval + * + * Sets <allowEval>. + */ +mxGraphView.prototype.setAllowEval = function(value) +{ + this.allowEval = value; +}; + +/** + * Function: getStates + * + * Returns <states>. + */ +mxGraphView.prototype.getStates = function() +{ + return this.states; +}; + +/** + * Function: setStates + * + * Sets <states>. + */ +mxGraphView.prototype.setStates = function(value) +{ + this.states = value; +}; + +/** + * Function: getCellStates + * + * Returns the <mxCellStates> for the given array of <mxCells>. The array + * contains all states that are not null, that is, the returned array may + * have less elements than the given array. If no argument is given, then + * this returns <states>. + */ +mxGraphView.prototype.getCellStates = function(cells) +{ + if (cells == null) + { + return this.states; + } + else + { + var result = []; + + for (var i = 0; i < cells.length; i++) + { + var state = this.getState(cells[i]); + + if (state != null) + { + result.push(state); + } + } + + return result; + } +}; + +/** + * Function: removeState + * + * Removes and returns the <mxCellState> for the given cell. + * + * Parameters: + * + * cell - <mxCell> for which the <mxCellState> should be removed. + */ +mxGraphView.prototype.removeState = function(cell) +{ + var state = null; + + if (cell != null) + { + state = this.states.remove(cell); + + if (state != null) + { + this.graph.cellRenderer.destroy(state); + state.destroy(); + } + } + + return state; +}; + +/** + * Function: createState + * + * Creates and returns an <mxCellState> for the given cell and initializes + * it using <mxCellRenderer.initialize>. + * + * Parameters: + * + * cell - <mxCell> for which a new <mxCellState> should be created. + */ +mxGraphView.prototype.createState = function(cell) +{ + var style = this.graph.getCellStyle(cell); + var state = new mxCellState(this, cell, style); + this.graph.cellRenderer.initialize(state, this.isRendering()); + + return state; +}; + +/** + * Function: getCanvas + * + * Returns the DOM node that contains the background-, draw- and + * overlaypane. + */ +mxGraphView.prototype.getCanvas = function() +{ + return this.canvas; +}; + +/** + * Function: getBackgroundPane + * + * Returns the DOM node that represents the background layer. + */ +mxGraphView.prototype.getBackgroundPane = function() +{ + return this.backgroundPane; +}; + +/** + * Function: getDrawPane + * + * Returns the DOM node that represents the main drawing layer. + */ +mxGraphView.prototype.getDrawPane = function() +{ + return this.drawPane; +}; + +/** + * Function: getOverlayPane + * + * Returns the DOM node that represents the topmost drawing layer. + */ +mxGraphView.prototype.getOverlayPane = function() +{ + return this.overlayPane; +}; + +/** + * Function: isContainerEvent + * + * Returns true if the event origin is one of the drawing panes or + * containers of the view. + */ +mxGraphView.prototype.isContainerEvent = function(evt) +{ + var source = mxEvent.getSource(evt); + + return (source == this.graph.container || + source.parentNode == this.backgroundPane || + (source.parentNode != null && + source.parentNode.parentNode == this.backgroundPane) || + source == this.canvas.parentNode || + source == this.canvas || + source == this.backgroundPane || + source == this.drawPane || + source == this.overlayPane); +}; + +/** + * Function: isScrollEvent + * + * Returns true if the event origin is one of the scrollbars of the + * container in IE. Such events are ignored. + */ + mxGraphView.prototype.isScrollEvent = function(evt) +{ + var offset = mxUtils.getOffset(this.graph.container); + var pt = new mxPoint(evt.clientX - offset.x, evt.clientY - offset.y); + + var outWidth = this.graph.container.offsetWidth; + var inWidth = this.graph.container.clientWidth; + + if (outWidth > inWidth && pt.x > inWidth + 2 && pt.x <= outWidth) + { + return true; + } + + var outHeight = this.graph.container.offsetHeight; + var inHeight = this.graph.container.clientHeight; + + if (outHeight > inHeight && pt.y > inHeight + 2 && pt.y <= outHeight) + { + return true; + } + + return false; +}; + +/** + * Function: init + * + * Initializes the graph event dispatch loop for the specified container + * and invokes <create> to create the required DOM nodes for the display. + */ +mxGraphView.prototype.init = function() +{ + this.installListeners(); + + // Creates the DOM nodes for the respective display dialect + var graph = this.graph; + + if (graph.dialect == mxConstants.DIALECT_SVG) + { + this.createSvg(); + } + else if (graph.dialect == mxConstants.DIALECT_VML) + { + this.createVml(); + } + else + { + this.createHtml(); + } +}; + +/** + * Function: installListeners + * + * Installs the required listeners in the container. + */ +mxGraphView.prototype.installListeners = function() +{ + var graph = this.graph; + var container = graph.container; + + if (container != null) + { + var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; + var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; + var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; + + // Adds basic listeners for graph event dispatching + mxEvent.addListener(container, md, + mxUtils.bind(this, function(evt) + { + // Workaround for touch-based device not transferring + // the focus while editing with virtual keyboard + if (mxClient.IS_TOUCH && graph.isEditing()) + { + graph.stopEditing(!graph.isInvokesStopCellEditing()); + } + + // Condition to avoid scrollbar events starting a rubberband + // selection + if (this.isContainerEvent(evt) && ((!mxClient.IS_IE && + !mxClient.IS_GC && !mxClient.IS_OP && !mxClient.IS_SF) || + !this.isScrollEvent(evt))) + { + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, + new mxMouseEvent(evt)); + } + }) + ); + mxEvent.addListener(container, mm, + mxUtils.bind(this, function(evt) + { + if (this.isContainerEvent(evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt)); + } + }) + ); + mxEvent.addListener(container, mu, + mxUtils.bind(this, function(evt) + { + if (this.isContainerEvent(evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt)); + } + }) + ); + + // Adds listener for double click handling on background + mxEvent.addListener(container, 'dblclick', + mxUtils.bind(this, function(evt) + { + graph.dblClick(evt); + }) + ); + + // Workaround for touch events which started on some DOM node + // on top of the container, in which case the cells under the + // mouse for the move and up events are not detected. + var getState = function(evt) + { + var state = null; + + // Workaround for touch events which started on some DOM node + // on top of the container, in which case the cells under the + // mouse for the move and up events are not detected. + if (mxClient.IS_TOUCH) + { + var x = mxEvent.getClientX(evt); + var y = mxEvent.getClientY(evt); + + // Dispatches the drop event to the graph which + // consumes and executes the source function + var pt = mxUtils.convertPoint(container, x, y); + state = graph.view.getState(graph.getCellAt(pt.x, pt.y)); + } + + return state; + }; + + // Adds basic listeners for graph event dispatching outside of the + // container and finishing the handling of a single gesture + // Implemented via graph event dispatch loop to avoid duplicate events + // in Firefox and Chrome + graph.addMouseListener( + { + mouseDown: function(sender, me) + { + graph.panningHandler.hideMenu(); + }, + mouseMove: function() { }, + mouseUp: function() { } + }); + mxEvent.addListener(document, mm, + mxUtils.bind(this, function(evt) + { + // Hides the tooltip if mouse is outside container + if (graph.tooltipHandler != null && + graph.tooltipHandler.isHideOnHover()) + { + graph.tooltipHandler.hide(); + } + + if (this.captureDocumentGesture && graph.isMouseDown && + !mxEvent.isConsumed(evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, + new mxMouseEvent(evt, getState(evt))); + } + }) + ); + mxEvent.addListener(document, mu, + mxUtils.bind(this, function(evt) + { + if (this.captureDocumentGesture) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, + new mxMouseEvent(evt)); + } + }) + ); + } +}; + +/** + * Function: create + * + * Creates the DOM nodes for the HTML display. + */ +mxGraphView.prototype.createHtml = function() +{ + var container = this.graph.container; + + if (container != null) + { + this.canvas = this.createHtmlPane('100%', '100%'); + + // Uses minimal size for inner DIVs on Canvas. This is required + // for correct event processing in IE. If we have an overlapping + // DIV then the events on the cells are only fired for labels. + this.backgroundPane = this.createHtmlPane('1px', '1px'); + this.drawPane = this.createHtmlPane('1px', '1px'); + this.overlayPane = this.createHtmlPane('1px', '1px'); + + this.canvas.appendChild(this.backgroundPane); + this.canvas.appendChild(this.drawPane); + this.canvas.appendChild(this.overlayPane); + + container.appendChild(this.canvas); + + // Implements minWidth/minHeight in quirks mode + if (mxClient.IS_QUIRKS) + { + var onResize = mxUtils.bind(this, function(evt) + { + var bounds = this.getGraphBounds(); + var width = bounds.x + bounds.width + this.graph.border; + var height = bounds.y + bounds.height + this.graph.border; + + this.updateHtmlCanvasSize(width, height); + }); + + mxEvent.addListener(window, 'resize', onResize); + } + } +}; + +/** + * Function: updateHtmlCanvasSize + * + * Updates the size of the HTML canvas. + */ +mxGraphView.prototype.updateHtmlCanvasSize = function(width, height) +{ + if (this.graph.container != null) + { + var ow = this.graph.container.offsetWidth; + var oh = this.graph.container.offsetHeight; + + if (ow < width) + { + this.canvas.style.width = width + 'px'; + } + else + { + this.canvas.style.width = '100%'; + } + + if (oh < height) + { + this.canvas.style.height = height + 'px'; + } + else + { + this.canvas.style.height = '100%'; + } + } +}; + +/** + * Function: createHtmlPane + * + * Creates and returns a drawing pane in HTML (DIV). + */ +mxGraphView.prototype.createHtmlPane = function(width, height) +{ + var pane = document.createElement('DIV'); + + if (width != null && height != null) + { + pane.style.position = 'absolute'; + pane.style.left = '0px'; + pane.style.top = '0px'; + + pane.style.width = width; + pane.style.height = height; + } + else + { + pane.style.position = 'relative'; + } + + return pane; +}; + +/** + * Function: create + * + * Creates the DOM nodes for the VML display. + */ +mxGraphView.prototype.createVml = function() +{ + var container = this.graph.container; + + if (container != null) + { + var width = container.offsetWidth; + var height = container.offsetHeight; + this.canvas = this.createVmlPane(width, height); + + this.backgroundPane = this.createVmlPane(width, height); + this.drawPane = this.createVmlPane(width, height); + this.overlayPane = this.createVmlPane(width, height); + + this.canvas.appendChild(this.backgroundPane); + this.canvas.appendChild(this.drawPane); + this.canvas.appendChild(this.overlayPane); + + container.appendChild(this.canvas); + } +}; + +/** + * Function: createVmlPane + * + * Creates a drawing pane in VML (group). + */ +mxGraphView.prototype.createVmlPane = function(width, height) +{ + var pane = document.createElement('v:group'); + + // At this point the width and height are potentially + // uninitialized. That's OK. + pane.style.position = 'absolute'; + pane.style.left = '0px'; + pane.style.top = '0px'; + + pane.style.width = width+'px'; + pane.style.height = height+'px'; + + pane.setAttribute('coordsize', width+','+height); + pane.setAttribute('coordorigin', '0,0'); + + return pane; +}; + +/** + * Function: create + * + * Creates and returns the DOM nodes for the SVG display. + */ +mxGraphView.prototype.createSvg = function() +{ + var container = this.graph.container; + this.canvas = document.createElementNS(mxConstants.NS_SVG, 'g'); + + // For background image + this.backgroundPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + this.canvas.appendChild(this.backgroundPane); + + // Adds two layers (background is early feature) + this.drawPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + this.canvas.appendChild(this.drawPane); + + this.overlayPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + this.canvas.appendChild(this.overlayPane); + + var root = document.createElementNS(mxConstants.NS_SVG, 'svg'); + root.style.width = '100%'; + root.style.height = '100%'; + + if (mxClient.IS_IE) + { + root.style.marginBottom = '-4px'; + } + + root.appendChild(this.canvas); + + if (container != null) + { + container.appendChild(root); + + // Workaround for offset of container + var style = mxUtils.getCurrentStyle(container); + + if (style.position == 'static') + { + container.style.position = 'relative'; + } + } +}; + +/** + * Function: destroy + * + * Destroys the view and all its resources. + */ +mxGraphView.prototype.destroy = function() +{ + var root = (this.canvas != null) ? this.canvas.ownerSVGElement : null; + + if (root == null) + { + root = this.canvas; + } + + if (root != null && root.parentNode != null) + { + this.clear(this.currentRoot, true); + mxEvent.removeAllListeners(document); + mxEvent.release(this.graph.container); + root.parentNode.removeChild(root); + + this.canvas = null; + this.backgroundPane = null; + this.drawPane = null; + this.overlayPane = null; + } +}; + +/** + * Class: mxCurrentRootChange + * + * Action to change the current root in a view. + * + * Constructor: mxCurrentRootChange + * + * Constructs a change of the current root in the given view. + */ +function mxCurrentRootChange(view, root) +{ + this.view = view; + this.root = root; + this.previous = root; + this.isUp = root == null; + + if (!this.isUp) + { + var tmp = this.view.currentRoot; + var model = this.view.graph.getModel(); + + while (tmp != null) + { + if (tmp == root) + { + this.isUp = true; + break; + } + + tmp = model.getParent(tmp); + } + } +}; + +/** + * Function: execute + * + * Changes the current root of the view. + */ +mxCurrentRootChange.prototype.execute = function() +{ + var tmp = this.view.currentRoot; + this.view.currentRoot = this.previous; + this.previous = tmp; + + var translate = this.view.graph.getTranslateForRoot(this.view.currentRoot); + + if (translate != null) + { + this.view.translate = new mxPoint(-translate.x, -translate.y); + } + + var name = (this.isUp) ? mxEvent.UP : mxEvent.DOWN; + this.view.fireEvent(new mxEventObject(name, + 'root', this.view.currentRoot, 'previous', this.previous)); + + if (this.isUp) + { + this.view.clear(this.view.currentRoot, true); + this.view.validate(); + } + else + { + this.view.refresh(); + } + + this.isUp = !this.isUp; +}; diff --git a/src/js/view/mxLayoutManager.js b/src/js/view/mxLayoutManager.js new file mode 100644 index 0000000..ee8ec65 --- /dev/null +++ b/src/js/view/mxLayoutManager.js @@ -0,0 +1,375 @@ +/** + * $Id: mxLayoutManager.js,v 1.21 2012-01-04 10:01:16 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxLayoutManager + * + * Implements a layout manager that updates the layout for a given transaction. + * + * Example: + * + * (code) + * var layoutMgr = new mxLayoutManager(graph); + * layoutMgr.getLayout = function(cell) + * { + * return layout; + * }; + * (end) + * + * Event: mxEvent.LAYOUT_CELLS + * + * Fires between begin- and endUpdate after all cells have been layouted in + * <layoutCells>. The <code>cells</code> property contains all cells that have + * been passed to <layoutCells>. + * + * Constructor: mxLayoutManager + * + * Constructs a new automatic layout for the given graph. + * + * Arguments: + * + * graph - Reference to the enclosing graph. + */ +function mxLayoutManager(graph) +{ + // Executes the layout before the changes are dispatched + this.undoHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.beforeUndo(evt.getProperty('edit')); + } + }); + + // Notifies the layout of a move operation inside a parent + this.moveHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.cellsMoved(evt.getProperty('cells'), evt.getProperty('event')); + } + }); + + this.setGraph(graph); +}; + +/** + * Extends mxEventSource. + */ +mxLayoutManager.prototype = new mxEventSource(); +mxLayoutManager.prototype.constructor = mxLayoutManager; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxLayoutManager.prototype.graph = null; + +/** + * Variable: bubbling + * + * Specifies if the layout should bubble along + * the cell hierarchy. Default is true. + */ +mxLayoutManager.prototype.bubbling = true; + +/** + * Variable: enabled + * + * Specifies if event handling is enabled. Default is true. + */ +mxLayoutManager.prototype.enabled = true; + +/** + * Variable: updateHandler + * + * Holds the function that handles the endUpdate event. + */ +mxLayoutManager.prototype.updateHandler = null; + +/** + * Variable: moveHandler + * + * Holds the function that handles the move event. + */ +mxLayoutManager.prototype.moveHandler = null; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxLayoutManager.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxLayoutManager.prototype.setEnabled = function(enabled) +{ + this.enabled = enabled; +}; + +/** + * Function: isBubbling + * + * Returns true if a layout should bubble, that is, if the parent layout + * should be executed whenever a cell layout (layout of the children of + * a cell) has been executed. This implementation returns <bubbling>. + */ +mxLayoutManager.prototype.isBubbling = function() +{ + return this.bubbling; +}; + +/** + * Function: setBubbling + * + * Sets <bubbling>. + */ +mxLayoutManager.prototype.setBubbling = function(value) +{ + this.bubbling = value; +}; + +/** + * Function: getGraph + * + * Returns the graph that this layout operates on. + */ +mxLayoutManager.prototype.getGraph = function() +{ + return this.graph; +}; + +/** + * Function: setGraph + * + * Sets the graph that the layouts operate on. + */ +mxLayoutManager.prototype.setGraph = function(graph) +{ + if (this.graph != null) + { + var model = this.graph.getModel(); + model.removeListener(this.undoHandler); + this.graph.removeListener(this.moveHandler); + } + + this.graph = graph; + + if (this.graph != null) + { + var model = this.graph.getModel(); + model.addListener(mxEvent.BEFORE_UNDO, this.undoHandler); + this.graph.addListener(mxEvent.MOVE_CELLS, this.moveHandler); + } +}; + +/** + * Function: getLayout + * + * Returns the layout to be executed for the given graph and parent. + */ +mxLayoutManager.prototype.getLayout = function(parent) +{ + return null; +}; + +/** + * Function: beforeUndo + * + * Called from the undoHandler. + * + * Parameters: + * + * cell - Array of <mxCells> that have been moved. + * evt - Mouse event that represents the mousedown. + */ +mxLayoutManager.prototype.beforeUndo = function(undoableEdit) +{ + var cells = this.getCellsForChanges(undoableEdit.changes); + var model = this.getGraph().getModel(); + + // Adds all parent ancestors + if (this.isBubbling()) + { + var tmp = model.getParents(cells); + + while (tmp.length > 0) + { + cells = cells.concat(tmp); + tmp = model.getParents(tmp); + } + } + + this.layoutCells(mxUtils.sortCells(cells, false)); +}; + +/** + * Function: cellsMoved + * + * Called from the moveHandler. + * + * Parameters: + * + * cell - Array of <mxCells> that have been moved. + * evt - Mouse event that represents the mousedown. + */ +mxLayoutManager.prototype.cellsMoved = function(cells, evt) +{ + if (cells != null && + evt != null) + { + var point = mxUtils.convertPoint(this.getGraph().container, + mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + var model = this.getGraph().getModel(); + + // Checks if a layout exists to take care of the moving + for (var i = 0; i < cells.length; i++) + { + var layout = this.getLayout(model.getParent(cells[i])); + + if (layout != null) + { + layout.moveCell(cells[i], point.x, point.y); + } + } + } +}; + +/** + * Function: getCellsForEdit + * + * Returns the cells to be layouted for the given sequence of changes. + */ +mxLayoutManager.prototype.getCellsForChanges = function(changes) +{ + var result = []; + var hash = new Object(); + + for (var i = 0; i < changes.length; i++) + { + var change = changes[i]; + + if (change instanceof mxRootChange) + { + return []; + } + else + { + var cells = this.getCellsForChange(change); + + for (var j = 0; j < cells.length; j++) + { + if (cells[j] != null) + { + var id = mxCellPath.create(cells[j]); + + if (hash[id] == null) + { + hash[id] = cells[j]; + result.push(cells[j]); + } + } + } + } + } + + return result; +}; + +/** + * Function: getCellsForChange + * + * Executes all layouts which have been scheduled during the + * changes. + */ +mxLayoutManager.prototype.getCellsForChange = function(change) +{ + var model = this.getGraph().getModel(); + + if (change instanceof mxChildChange) + { + return [change.child, change.previous, model.getParent(change.child)]; + } + else if (change instanceof mxTerminalChange || + change instanceof mxGeometryChange) + { + return [change.cell, model.getParent(change.cell)]; + } + + return []; +}; + +/** + * Function: layoutCells + * + * Executes all layouts which have been scheduled during the + * changes. + */ +mxLayoutManager.prototype.layoutCells = function(cells) +{ + if (cells.length > 0) + { + // Invokes the layouts while removing duplicates + var model = this.getGraph().getModel(); + + model.beginUpdate(); + try + { + var last = null; + + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != model.getRoot() && + cells[i] != last) + { + last = cells[i]; + this.executeLayout(this.getLayout(last), last); + } + } + + this.fireEvent(new mxEventObject(mxEvent.LAYOUT_CELLS, 'cells', cells)); + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: executeLayout + * + * Executes the given layout on the given parent. + */ +mxLayoutManager.prototype.executeLayout = function(layout, parent) +{ + if (layout != null && parent != null) + { + layout.execute(parent); + } +}; + +/** + * Function: destroy + * + * Removes all handlers from the <graph> and deletes the reference to it. + */ +mxLayoutManager.prototype.destroy = function() +{ + this.setGraph(null); +}; diff --git a/src/js/view/mxMultiplicity.js b/src/js/view/mxMultiplicity.js new file mode 100644 index 0000000..c927d3f --- /dev/null +++ b/src/js/view/mxMultiplicity.js @@ -0,0 +1,257 @@ +/** + * $Id: mxMultiplicity.js,v 1.24 2010-11-03 14:52:40 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxMultiplicity + * + * Defines invalid connections along with the error messages that they produce. + * To add or remove rules on a graph, you must add/remove instances of this + * class to <mxGraph.multiplicities>. + * + * Example: + * + * (code) + * graph.multiplicities.push(new mxMultiplicity( + * true, 'rectangle', null, null, 0, 2, ['circle'], + * 'Only 2 targets allowed', + * 'Only circle targets allowed')); + * (end) + * + * Defines a rule where each rectangle must be connected to no more than 2 + * circles and no other types of targets are allowed. + * + * Constructor: mxMultiplicity + * + * Instantiate class mxMultiplicity in order to describe allowed + * connections in a graph. Not all constraints can be enforced while + * editing, some must be checked at validation time. The <countError> and + * <typeError> are treated as resource keys in <mxResources>. + * + * Parameters: + * + * source - Boolean indicating if this rule applies to the source or target + * terminal. + * type - Type of the source or target terminal that this rule applies to. + * See <type> for more information. + * attr - Optional attribute name to match the source or target terminal. + * value - Optional attribute value to match the source or target terminal. + * min - Minimum number of edges for this rule. Default is 1. + * max - Maximum number of edges for this rule. n means infinite. Default + * is n. + * validNeighbors - Array of types of the opposite terminal for which this + * rule applies. + * countError - Error to be displayed for invalid number of edges. + * typeError - Error to be displayed for invalid opposite terminals. + * validNeighborsAllowed - Optional boolean indicating if the array of + * opposite types should be valid or invalid. + */ +function mxMultiplicity(source, type, attr, value, min, max, + validNeighbors, countError, typeError, validNeighborsAllowed) +{ + this.source = source; + this.type = type; + this.attr = attr; + this.value = value; + this.min = (min != null) ? min : 0; + this.max = (max != null) ? max : 'n'; + this.validNeighbors = validNeighbors; + this.countError = mxResources.get(countError) || countError; + this.typeError = mxResources.get(typeError) || typeError; + this.validNeighborsAllowed = (validNeighborsAllowed != null) ? + validNeighborsAllowed : true; +}; + +/** + * Variable: type + * + * Defines the type of the source or target terminal. The type is a string + * passed to <mxUtils.isNode> together with the source or target vertex + * value as the first argument. + */ +mxMultiplicity.prototype.type = null; + +/** + * Variable: attr + * + * Optional string that specifies the attributename to be passed to + * <mxUtils.isNode> to check if the rule applies to a cell. + */ +mxMultiplicity.prototype.attr = null; + +/** + * Variable: value + * + * Optional string that specifies the value of the attribute to be passed + * to <mxUtils.isNode> to check if the rule applies to a cell. + */ +mxMultiplicity.prototype.value = null; + +/** + * Variable: source + * + * Boolean that specifies if the rule is applied to the source or target + * terminal of an edge. + */ +mxMultiplicity.prototype.source = null; + +/** + * Variable: min + * + * Defines the minimum number of connections for which this rule applies. + * Default is 0. + */ +mxMultiplicity.prototype.min = null; + +/** + * Variable: max + * + * Defines the maximum number of connections for which this rule applies. + * A value of 'n' means unlimited times. Default is 'n'. + */ +mxMultiplicity.prototype.max = null; + +/** + * Variable: validNeighbors + * + * Holds an array of strings that specify the type of neighbor for which + * this rule applies. The strings are used in <mxCell.is> on the opposite + * terminal to check if the rule applies to the connection. + */ +mxMultiplicity.prototype.validNeighbors = null; + +/** + * Variable: validNeighborsAllowed + * + * Boolean indicating if the list of validNeighbors are those that are allowed + * for this rule or those that are not allowed for this rule. + */ +mxMultiplicity.prototype.validNeighborsAllowed = true; + +/** + * Variable: countError + * + * Holds the localized error message to be displayed if the number of + * connections for which the rule applies is smaller than <min> or greater + * than <max>. + */ +mxMultiplicity.prototype.countError = null; + +/** + * Variable: typeError + * + * Holds the localized error message to be displayed if the type of the + * neighbor for a connection does not match the rule. + */ +mxMultiplicity.prototype.typeError = null; + +/** + * Function: check + * + * Checks the multiplicity for the given arguments and returns the error + * for the given connection or null if the multiplicity does not apply. + * + * Parameters: + * + * graph - Reference to the enclosing <mxGraph> instance. + * edge - <mxCell> that represents the edge to validate. + * source - <mxCell> that represents the source terminal. + * target - <mxCell> that represents the target terminal. + * sourceOut - Number of outgoing edges from the source terminal. + * targetIn - Number of incoming edges for the target terminal. + */ +mxMultiplicity.prototype.check = function(graph, edge, source, target, sourceOut, targetIn) +{ + var error = ''; + + if ((this.source && this.checkTerminal(graph, source, edge)) || + (!this.source && this.checkTerminal(graph, target, edge))) + { + if (this.countError != null && + ((this.source && (this.max == 0 || (sourceOut >= this.max))) || + (!this.source && (this.max == 0 || (targetIn >= this.max))))) + { + error += this.countError + '\n'; + } + + if (this.validNeighbors != null && this.typeError != null && this.validNeighbors.length > 0) + { + var isValid = this.checkNeighbors(graph, edge, source, target); + + if (!isValid) + { + error += this.typeError + '\n'; + } + } + } + + return (error.length > 0) ? error : null; +}; + +/** + * Function: checkNeighbors + * + * Checks if there are any valid neighbours in <validNeighbors>. This is only + * called if <validNeighbors> is a non-empty array. + */ +mxMultiplicity.prototype.checkNeighbors = function(graph, edge, source, target) +{ + var sourceValue = graph.model.getValue(source); + var targetValue = graph.model.getValue(target); + var isValid = !this.validNeighborsAllowed; + var valid = this.validNeighbors; + + for (var j = 0; j < valid.length; j++) + { + if (this.source && + this.checkType(graph, targetValue, valid[j])) + { + isValid = this.validNeighborsAllowed; + break; + } + else if (!this.source && + this.checkType(graph, sourceValue, valid[j])) + { + isValid = this.validNeighborsAllowed; + break; + } + } + + return isValid; +}; + +/** + * Function: checkTerminal + * + * Checks the given terminal cell and returns true if this rule applies. The + * given cell is the source or target of the given edge, depending on + * <source>. This implementation uses <checkType> on the terminal's value. + */ +mxMultiplicity.prototype.checkTerminal = function(graph, terminal, edge) +{ + var value = graph.model.getValue(terminal); + + return this.checkType(graph, value, this.type, this.attr, this.value); +}; + +/** + * Function: checkType + * + * Checks the type of the given value. + */ +mxMultiplicity.prototype.checkType = function(graph, value, type, attr, attrValue) +{ + if (value != null) + { + if (!isNaN(value.nodeType)) // Checks if value is a DOM node + { + return mxUtils.isNode(value, type, attr, attrValue); + } + else + { + return value == type; + } + } + + return false; +}; diff --git a/src/js/view/mxOutline.js b/src/js/view/mxOutline.js new file mode 100644 index 0000000..a0d6fd3 --- /dev/null +++ b/src/js/view/mxOutline.js @@ -0,0 +1,649 @@ +/** + * $Id: mxOutline.js,v 1.81 2012-06-20 14:13:37 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxOutline + * + * Implements an outline (aka overview) for a graph. Set <updateOnPan> to true + * to enable updates while the source graph is panning. + * + * Example: + * + * (code) + * var outline = new mxOutline(graph, div); + * (end) + * + * If the selection border in the outline appears behind the contents of the + * graph, then you can use the following code. (This may happen when using a + * transparent container for the outline in IE.) + * + * (code) + * mxOutline.prototype.graphRenderHint = mxConstants.RENDERING_HINT_EXACT; + * (end) + * + * To move the graph to the top, left corner the following code can be used. + * + * (code) + * var scale = graph.view.scale; + * var bounds = graph.getGraphBounds(); + * graph.view.setTranslate(-bounds.x / scale, -bounds.y / scale); + * (end) + * + * To toggle the suspended mode, the following can be used. + * + * (code) + * outline.suspended = !outln.suspended; + * if (!outline.suspended) + * { + * outline.update(true); + * } + * (end) + * + * Constructor: mxOutline + * + * Constructs a new outline for the specified graph inside the given + * container. + * + * Parameters: + * + * source - <mxGraph> to create the outline for. + * container - DOM node that will contain the outline. + */ +function mxOutline(source, container) +{ + this.source = source; + + if (container != null) + { + this.init(container); + } +}; + +/** + * Function: source + * + * Reference to the source <mxGraph>. + */ +mxOutline.prototype.source = null; + +/** + * Function: outline + * + * Reference to the outline <mxGraph>. + */ +mxOutline.prototype.outline = null; + +/** + * Function: graphRenderHint + * + * Renderhint to be used for the outline graph. Default is faster. + */ +mxOutline.prototype.graphRenderHint = mxConstants.RENDERING_HINT_FASTER; + +/** + * Variable: enabled + * + * Specifies if events are handled. Default is true. + */ +mxOutline.prototype.enabled = true; + +/** + * Variable: showViewport + * + * Specifies a viewport rectangle should be shown. Default is true. + */ +mxOutline.prototype.showViewport = true; + +/** + * Variable: border + * + * Border to be added at the bottom and right. Default is 10. + */ +mxOutline.prototype.border = 10; + +/** + * Variable: enabled + * + * Specifies the size of the sizer handler. Default is 8. + */ +mxOutline.prototype.sizerSize = 8; + +/** + * Variable: updateOnPan + * + * Specifies if <update> should be called for <mxEvent.PAN> in the source + * graph. Default is false. + */ +mxOutline.prototype.updateOnPan = false; + +/** + * Variable: sizerImage + * + * Optional <mxImage> to be used for the sizer. Default is null. + */ +mxOutline.prototype.sizerImage = null; + +/** + * Variable: suspended + * + * Optional boolean flag to suspend updates. Default is false. This flag will + * also suspend repaints of the outline. To toggle this switch, use the + * following code. + * + * (code) + * nav.suspended = !nav.suspended; + * + * if (!nav.suspended) + * { + * nav.update(true); + * } + * (end) + */ +mxOutline.prototype.suspended = false; + +/** + * Function: init + * + * Initializes the outline inside the given container. + */ +mxOutline.prototype.init = function(container) +{ + this.outline = new mxGraph(container, this.source.getModel(), this.graphRenderHint, this.source.getStylesheet()); + this.outline.foldingEnabled = false; + this.outline.autoScroll = false; + + // Do not repaint when suspended + var outlineGraphModelChanged = this.outline.graphModelChanged; + this.outline.graphModelChanged = mxUtils.bind(this, function(changes) + { + if (!this.suspended && this.outline != null) + { + outlineGraphModelChanged.apply(this.outline, arguments); + } + }); + + // Enables faster painting in SVG + if (mxClient.IS_SVG) + { + var node = this.outline.getView().getCanvas().parentNode; + node.setAttribute('shape-rendering', 'optimizeSpeed'); + node.setAttribute('image-rendering', 'optimizeSpeed'); + } + + // Hides cursors and labels + this.outline.labelsVisible = false; + this.outline.setEnabled(false); + + this.updateHandler = mxUtils.bind(this, function(sender, evt) + { + if (!this.suspended && !this.active) + { + this.update(); + } + }); + + // Updates the scale of the outline after a change of the main graph + this.source.getModel().addListener(mxEvent.CHANGE, this.updateHandler); + this.outline.addMouseListener(this); + + // Adds listeners to keep the outline in sync with the source graph + var view = this.source.getView(); + view.addListener(mxEvent.SCALE, this.updateHandler); + view.addListener(mxEvent.TRANSLATE, this.updateHandler); + view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.updateHandler); + view.addListener(mxEvent.DOWN, this.updateHandler); + view.addListener(mxEvent.UP, this.updateHandler); + + // Updates blue rectangle on scroll + mxEvent.addListener(this.source.container, 'scroll', this.updateHandler); + + this.panHandler = mxUtils.bind(this, function(sender) + { + if (this.updateOnPan) + { + this.updateHandler.apply(this, arguments); + } + }); + this.source.addListener(mxEvent.PAN, this.panHandler); + + // Refreshes the graph in the outline after a refresh of the main graph + this.refreshHandler = mxUtils.bind(this, function(sender) + { + this.outline.setStylesheet(this.source.getStylesheet()); + this.outline.refresh(); + }); + this.source.addListener(mxEvent.REFRESH, this.refreshHandler); + + // Creates the blue rectangle for the viewport + this.bounds = new mxRectangle(0, 0, 0, 0); + this.selectionBorder = new mxRectangleShape(this.bounds, null, + mxConstants.OUTLINE_COLOR, mxConstants.OUTLINE_STROKEWIDTH); + this.selectionBorder.dialect = + (this.outline.dialect != mxConstants.DIALECT_SVG) ? + mxConstants.DIALECT_VML : mxConstants.DIALECT_SVG; + this.selectionBorder.crisp = true; + this.selectionBorder.init(this.outline.getView().getOverlayPane()); + mxEvent.redirectMouseEvents(this.selectionBorder.node, this.outline); + this.selectionBorder.node.style.background = ''; + + // Creates a small blue rectangle for sizing (sizer handle) + this.sizer = this.createSizer(); + this.sizer.init(this.outline.getView().getOverlayPane()); + + if (this.enabled) + { + this.sizer.node.style.cursor = 'pointer'; + } + + // Redirects all events from the sizerhandle to the outline + mxEvent.addListener(this.sizer.node, (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown', + mxUtils.bind(this, function(evt) + { + this.outline.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt)); + }) + ); + + this.selectionBorder.node.style.display = (this.showViewport) ? '' : 'none'; + this.sizer.node.style.display = this.selectionBorder.node.style.display; + this.selectionBorder.node.style.cursor = 'move'; + + this.update(false); +}; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxOutline.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * value - Boolean that specifies the new enabled state. + */ +mxOutline.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: setZoomEnabled + * + * Enables or disables the zoom handling by showing or hiding the respective + * handle. + * + * Parameters: + * + * value - Boolean that specifies the new enabled state. + */ +mxOutline.prototype.setZoomEnabled = function(value) +{ + this.sizer.node.style.visibility = (value) ? 'visible' : 'hidden'; +}; + +/** + * Function: refresh + * + * Invokes <update> and revalidate the outline. This method is deprecated. + */ +mxOutline.prototype.refresh = function() +{ + this.update(true); +}; + +/** + * Function: createSizer + * + * Creates the shape used as the sizer. + */ +mxOutline.prototype.createSizer = function() +{ + if (this.sizerImage != null) + { + var sizer = new mxImageShape(new mxRectangle(0, 0, this.sizerImage.width, this.sizerImage.height), this.sizerImage.src); + sizer.dialect = this.outline.dialect; + + return sizer; + } + else + { + var sizer = new mxRectangleShape(new mxRectangle(0, 0, this.sizerSize, this.sizerSize), + mxConstants.OUTLINE_HANDLE_FILLCOLOR, mxConstants.OUTLINE_HANDLE_STROKECOLOR); + sizer.dialect = this.outline.dialect; + sizer.crisp = true; + + return sizer; + } +}; + +/** + * Function: getSourceContainerSize + * + * Returns the size of the source container. + */ +mxOutline.prototype.getSourceContainerSize = function() +{ + return new mxRectangle(0, 0, this.source.container.scrollWidth, this.source.container.scrollHeight); +}; + +/** + * Function: getOutlineOffset + * + * Returns the offset for drawing the outline graph. + */ +mxOutline.prototype.getOutlineOffset = function(scale) +{ + return null; +}; + +/** + * Function: update + * + * Updates the outline. + */ +mxOutline.prototype.update = function(revalidate) +{ + if (this.source != null) + { + var sourceScale = this.source.view.scale; + var scaledGraphBounds = this.source.getGraphBounds(); + var unscaledGraphBounds = new mxRectangle(scaledGraphBounds.x / sourceScale + this.source.panDx, + scaledGraphBounds.y / sourceScale + this.source.panDy, scaledGraphBounds.width / sourceScale, + scaledGraphBounds.height / sourceScale); + + var unscaledFinderBounds = new mxRectangle(0, 0, + this.source.container.clientWidth / sourceScale, + this.source.container.clientHeight / sourceScale); + + var union = unscaledGraphBounds.clone(); + union.add(unscaledFinderBounds); + + // Zooms to the scrollable area if that is bigger than the graph + var size = this.getSourceContainerSize(); + var completeWidth = Math.max(size.width / sourceScale, union.width); + var completeHeight = Math.max(size.height / sourceScale, union.height); + + var availableWidth = Math.max(0, this.outline.container.clientWidth - this.border); + var availableHeight = Math.max(0, this.outline.container.clientHeight - this.border); + + var outlineScale = Math.min(availableWidth / completeWidth, availableHeight / completeHeight); + var scale = outlineScale; + + if (scale > 0) + { + if (this.outline.getView().scale != scale) + { + this.outline.getView().scale = scale; + revalidate = true; + } + + var navView = this.outline.getView(); + + if (navView.currentRoot != this.source.getView().currentRoot) + { + navView.setCurrentRoot(this.source.getView().currentRoot); + } + + var t = this.source.view.translate; + var tx = t.x + this.source.panDx; + var ty = t.y + this.source.panDy; + + var off = this.getOutlineOffset(scale); + + if (off != null) + { + tx += off.x; + ty += off.y; + } + + if (unscaledGraphBounds.x < 0) + { + tx = tx - unscaledGraphBounds.x; + } + if (unscaledGraphBounds.y < 0) + { + ty = ty - unscaledGraphBounds.y; + } + + if (navView.translate.x != tx || navView.translate.y != ty) + { + navView.translate.x = tx; + navView.translate.y = ty; + revalidate = true; + } + + // Prepares local variables for computations + var t2 = navView.translate; + scale = this.source.getView().scale; + var scale2 = scale / navView.scale; + var scale3 = 1.0 / navView.scale; + var container = this.source.container; + + // Updates the bounds of the viewrect in the navigation + this.bounds = new mxRectangle( + (t2.x - t.x - this.source.panDx) / scale3, + (t2.y - t.y - this.source.panDy) / scale3, + (container.clientWidth / scale2), + (container.clientHeight / scale2)); + + // Adds the scrollbar offset to the finder + this.bounds.x += this.source.container.scrollLeft * navView.scale / scale; + this.bounds.y += this.source.container.scrollTop * navView.scale / scale; + + var b = this.selectionBorder.bounds; + + if (b.x != this.bounds.x || b.y != this.bounds.y || b.width != this.bounds.width || b.height != this.bounds.height) + { + this.selectionBorder.bounds = this.bounds; + this.selectionBorder.redraw(); + } + + // Updates the bounds of the zoom handle at the bottom right + var b = this.sizer.bounds; + var b2 = new mxRectangle(this.bounds.x + this.bounds.width - b.width / 2, + this.bounds.y + this.bounds.height - b.height / 2, b.width, b.height); + + if (b.x != b2.x || b.y != b2.y || b.width != b2.width || b.height != b2.height) + { + this.sizer.bounds = b2; + + // Avoids update of visibility in redraw for VML + if (this.sizer.node.style.visibility != 'hidden') + { + this.sizer.redraw(); + } + } + + if (revalidate) + { + this.outline.view.revalidate(); + } + } + } +}; + +/** + * Function: mouseDown + * + * Handles the event by starting a translation or zoom. + */ +mxOutline.prototype.mouseDown = function(sender, me) +{ + if (this.enabled && this.showViewport) + { + this.zoom = me.isSource(this.sizer); + this.startX = me.getX(); + this.startY = me.getY(); + this.active = true; + + if (this.source.useScrollbarsForPanning && + mxUtils.hasScrollbars(this.source.container)) + { + this.dx0 = this.source.container.scrollLeft; + this.dy0 = this.source.container.scrollTop; + } + else + { + this.dx0 = 0; + this.dy0 = 0; + } + } + + me.consume(); +}; + +/** + * Function: mouseMove + * + * Handles the event by previewing the viewrect in <graph> and updating the + * rectangle that represents the viewrect in the outline. + */ +mxOutline.prototype.mouseMove = function(sender, me) +{ + if (this.active) + { + this.selectionBorder.node.style.display = (this.showViewport) ? '' : 'none'; + this.sizer.node.style.display = this.selectionBorder.node.style.display; + + var dx = me.getX() - this.startX; + var dy = me.getY() - this.startY; + var bounds = null; + + if (!this.zoom) + { + // Previews the panning on the source graph + var scale = this.outline.getView().scale; + bounds = new mxRectangle(this.bounds.x + dx, + this.bounds.y + dy, this.bounds.width, this.bounds.height); + this.selectionBorder.bounds = bounds; + this.selectionBorder.redraw(); + dx /= scale; + dx *= this.source.getView().scale; + dy /= scale; + dy *= this.source.getView().scale; + this.source.panGraph(-dx - this.dx0, -dy - this.dy0); + } + else + { + // Does *not* preview zooming on the source graph + var container = this.source.container; + var viewRatio = container.clientWidth / container.clientHeight; + dy = dx / viewRatio; + bounds = new mxRectangle(this.bounds.x, + this.bounds.y, + Math.max(1, this.bounds.width + dx), + Math.max(1, this.bounds.height + dy)); + this.selectionBorder.bounds = bounds; + this.selectionBorder.redraw(); + } + + // Updates the zoom handle + var b = this.sizer.bounds; + this.sizer.bounds = new mxRectangle( + bounds.x + bounds.width - b.width / 2, + bounds.y + bounds.height - b.height / 2, + b.width, b.height); + + // Avoids update of visibility in redraw for VML + if (this.sizer.node.style.visibility != 'hidden') + { + this.sizer.redraw(); + } + + me.consume(); + } +}; + +/** + * Function: mouseUp + * + * Handles the event by applying the translation or zoom to <graph>. + */ +mxOutline.prototype.mouseUp = function(sender, me) +{ + if (this.active) + { + var dx = me.getX() - this.startX; + var dy = me.getY() - this.startY; + + if (Math.abs(dx) > 0 || Math.abs(dy) > 0) + { + if (!this.zoom) + { + // Applies the new translation if the source + // has no scrollbars + if (!this.source.useScrollbarsForPanning || + !mxUtils.hasScrollbars(this.source.container)) + { + this.source.panGraph(0, 0); + dx /= this.outline.getView().scale; + dy /= this.outline.getView().scale; + var t = this.source.getView().translate; + this.source.getView().setTranslate(t.x - dx, t.y - dy); + } + } + else + { + // Applies the new zoom + var w = this.selectionBorder.bounds.width; + var scale = this.source.getView().scale; + this.source.zoomTo(scale - (dx * scale) / w, false); + } + + this.update(); + me.consume(); + } + + // Resets the state of the handler + this.index = null; + this.active = false; + } +}; + +/** + * Function: destroy + * + * Destroy this outline and removes all listeners from <source>. + */ +mxOutline.prototype.destroy = function() +{ + if (this.source != null) + { + this.source.removeListener(this.panHandler); + this.source.removeListener(this.refreshHandler); + this.source.getModel().removeListener(this.updateHandler); + this.source.getView().removeListener(this.updateHandler); + mxEvent.addListener(this.source.container, 'scroll', this.updateHandler); + this.source = null; + } + + if (this.outline != null) + { + this.outline.removeMouseListener(this); + this.outline.destroy(); + this.outline = null; + } + + if (this.selectionBorder != null) + { + this.selectionBorder.destroy(); + this.selectionBorder = null; + } + + if (this.sizer != null) + { + this.sizer.destroy(); + this.sizer = null; + } +}; diff --git a/src/js/view/mxPerimeter.js b/src/js/view/mxPerimeter.js new file mode 100644 index 0000000..7aaa187 --- /dev/null +++ b/src/js/view/mxPerimeter.js @@ -0,0 +1,484 @@ +/** + * $Id: mxPerimeter.js,v 1.28 2012-01-11 09:06:56 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxPerimeter = +{ + /** + * Class: mxPerimeter + * + * Provides various perimeter functions to be used in a style + * as the value of <mxConstants.STYLE_PERIMETER>. Perimeters for + * rectangle, circle, rhombus and triangle are available. + * + * Example: + * + * (code) + * <add as="perimeter">mxPerimeter.RightAngleRectanglePerimeter</add> + * (end) + * + * Or programmatically: + * + * (code) + * style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter; + * (end) + * + * When adding new perimeter functions, it is recommended to use the + * mxPerimeter-namespace as follows: + * + * (code) + * mxPerimeter.CustomPerimeter = function (bounds, vertex, next, orthogonal) + * { + * var x = 0; // Calculate x-coordinate + * var y = 0; // Calculate y-coordainte + * + * return new mxPoint(x, y); + * } + * (end) + * + * The new perimeter should then be registered in the <mxStyleRegistry> as follows: + * (code) + * mxStyleRegistry.putValue('customPerimeter', mxPerimeter.CustomPerimeter); + * (end) + * + * The custom perimeter above can now be used in a specific vertex as follows: + * + * (code) + * model.setStyle(vertex, 'perimeter=customPerimeter'); + * (end) + * + * Note that the key of the <mxStyleRegistry> entry for the function should + * be used in string values, unless <mxGraphView.allowEval> is true, in + * which case you can also use mxPerimeter.CustomPerimeter for the value in + * the cell style above. + * + * Or it can be used for all vertices in the graph as follows: + * + * (code) + * var style = graph.getStylesheet().getDefaultVertexStyle(); + * style[mxConstants.STYLE_PERIMETER] = mxPerimeter.CustomPerimeter; + * (end) + * + * Note that the object can be used directly when programmatically setting + * the value, but the key in the <mxStyleRegistry> should be used when + * setting the value via a key, value pair in a cell style. + * + * The parameters are explained in <RectanglePerimeter>. + * + * Function: RectanglePerimeter + * + * Describes a rectangular perimeter for the given bounds. + * + * Parameters: + * + * bounds - <mxRectangle> that represents the absolute bounds of the + * vertex. + * vertex - <mxCellState> that represents the vertex. + * next - <mxPoint> that represents the nearest neighbour point on the + * given edge. + * orthogonal - Boolean that specifies if the orthogonal projection onto + * the perimeter should be returned. If this is false then the intersection + * of the perimeter and the line between the next and the center point is + * returned. + */ + RectanglePerimeter: function (bounds, vertex, next, orthogonal) + { + var cx = bounds.getCenterX(); + var cy = bounds.getCenterY(); + var dx = next.x - cx; + var dy = next.y - cy; + var alpha = Math.atan2(dy, dx); + var p = new mxPoint(0, 0); + var pi = Math.PI; + var pi2 = Math.PI/2; + var beta = pi2 - alpha; + var t = Math.atan2(bounds.height, bounds.width); + + if (alpha < -pi + t || alpha > pi - t) + { + // Left edge + p.x = bounds.x; + p.y = cy - bounds.width * Math.tan(alpha) / 2; + } + else if (alpha < -t) + { + // Top Edge + p.y = bounds.y; + p.x = cx - bounds.height * Math.tan(beta) / 2; + } + else if (alpha < t) + { + // Right Edge + p.x = bounds.x + bounds.width; + p.y = cy + bounds.width * Math.tan(alpha) / 2; + } + else + { + // Bottom Edge + p.y = bounds.y + bounds.height; + p.x = cx + bounds.height * Math.tan(beta) / 2; + } + + if (orthogonal) + { + if (next.x >= bounds.x && + next.x <= bounds.x + bounds.width) + { + p.x = next.x; + } + else if (next.y >= bounds.y && + next.y <= bounds.y + bounds.height) + { + p.y = next.y; + } + if (next.x < bounds.x) + { + p.x = bounds.x; + } + else if (next.x > bounds.x + bounds.width) + { + p.x = bounds.x + bounds.width; + } + if (next.y < bounds.y) + { + p.y = bounds.y; + } + else if (next.y > bounds.y + bounds.height) + { + p.y = bounds.y + bounds.height; + } + } + + return p; + }, + + /** + * Function: EllipsePerimeter + * + * Describes an elliptic perimeter. See <RectanglePerimeter> + * for a description of the parameters. + */ + EllipsePerimeter: function (bounds, vertex, next, orthogonal) + { + var x = bounds.x; + var y = bounds.y; + var a = bounds.width / 2; + var b = bounds.height / 2; + var cx = x + a; + var cy = y + b; + var px = next.x; + var py = next.y; + + // Calculates straight line equation through + // point and ellipse center y = d * x + h + var dx = parseInt(px - cx); + var dy = parseInt(py - cy); + + if (dx == 0 && dy != 0) + { + return new mxPoint(cx, cy + b * dy / Math.abs(dy)); + } + else if (dx == 0 && dy == 0) + { + return new mxPoint(px, py); + } + + if (orthogonal) + { + if (py >= y && py <= y + bounds.height) + { + var ty = py - cy; + var tx = Math.sqrt(a*a*(1-(ty*ty)/(b*b))) || 0; + + if (px <= x) + { + tx = -tx; + } + + return new mxPoint(cx+tx, py); + } + + if (px >= x && px <= x + bounds.width) + { + var tx = px - cx; + var ty = Math.sqrt(b*b*(1-(tx*tx)/(a*a))) || 0; + + if (py <= y) + { + ty = -ty; + } + + return new mxPoint(px, cy+ty); + } + } + + // Calculates intersection + var d = dy / dx; + var h = cy - d * cx; + var e = a * a * d * d + b * b; + var f = -2 * cx * e; + var g = a * a * d * d * cx * cx + + b * b * cx * cx - + a * a * b * b; + var det = Math.sqrt(f * f - 4 * e * g); + + // Two solutions (perimeter points) + var xout1 = (-f + det) / (2 * e); + var xout2 = (-f - det) / (2 * e); + var yout1 = d * xout1 + h; + var yout2 = d * xout2 + h; + var dist1 = Math.sqrt(Math.pow((xout1 - px), 2) + + Math.pow((yout1 - py), 2)); + var dist2 = Math.sqrt(Math.pow((xout2 - px), 2) + + Math.pow((yout2 - py), 2)); + + // Correct solution + var xout = 0; + var yout = 0; + + if (dist1 < dist2) + { + xout = xout1; + yout = yout1; + } + else + { + xout = xout2; + yout = yout2; + } + + return new mxPoint(xout, yout); + }, + + /** + * Function: RhombusPerimeter + * + * Describes a rhombus (aka diamond) perimeter. See <RectanglePerimeter> + * for a description of the parameters. + */ + RhombusPerimeter: function (bounds, vertex, next, orthogonal) + { + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + var cx = x + w / 2; + var cy = y + h / 2; + + var px = next.x; + var py = next.y; + + // Special case for intersecting the diamond's corners + if (cx == px) + { + if (cy > py) + { + return new mxPoint(cx, y); // top + } + else + { + return new mxPoint(cx, y + h); // bottom + } + } + else if (cy == py) + { + if (cx > px) + { + return new mxPoint(x, cy); // left + } + else + { + return new mxPoint(x + w, cy); // right + } + } + + var tx = cx; + var ty = cy; + + if (orthogonal) + { + if (px >= x && px <= x + w) + { + tx = px; + } + else if (py >= y && py <= y + h) + { + ty = py; + } + } + + // In which quadrant will the intersection be? + // set the slope and offset of the border line accordingly + if (px < cx) + { + if (py < cy) + { + return mxUtils.intersection(px, py, tx, ty, cx, y, x, cy); + } + else + { + return mxUtils.intersection(px, py, tx, ty, cx, y + h, x, cy); + } + } + else if (py < cy) + { + return mxUtils.intersection(px, py, tx, ty, cx, y, x + w, cy); + } + else + { + return mxUtils.intersection(px, py, tx, ty, cx, y + h, x + w, cy); + } + }, + + /** + * Function: TrianglePerimeter + * + * Describes a triangle perimeter. See <RectanglePerimeter> + * for a description of the parameters. + */ + TrianglePerimeter: function (bounds, vertex, next, orthogonal) + { + var direction = (vertex != null) ? + vertex.style[mxConstants.STYLE_DIRECTION] : null; + var vertical = direction == mxConstants.DIRECTION_NORTH || + direction == mxConstants.DIRECTION_SOUTH; + + var x = bounds.x; + var y = bounds.y; + var w = bounds.width; + var h = bounds.height; + + var cx = x + w / 2; + var cy = y + h / 2; + + var start = new mxPoint(x, y); + var corner = new mxPoint(x + w, cy); + var end = new mxPoint(x, y + h); + + if (direction == mxConstants.DIRECTION_NORTH) + { + start = end; + corner = new mxPoint(cx, y); + end = new mxPoint(x + w, y + h); + } + else if (direction == mxConstants.DIRECTION_SOUTH) + { + corner = new mxPoint(cx, y + h); + end = new mxPoint(x + w, y); + } + else if (direction == mxConstants.DIRECTION_WEST) + { + start = new mxPoint(x + w, y); + corner = new mxPoint(x, cy); + end = new mxPoint(x + w, y + h); + } + + var dx = next.x - cx; + var dy = next.y - cy; + + var alpha = (vertical) ? Math.atan2(dx, dy) : Math.atan2(dy, dx); + var t = (vertical) ? Math.atan2(w, h) : Math.atan2(h, w); + + var base = false; + + if (direction == mxConstants.DIRECTION_NORTH || + direction == mxConstants.DIRECTION_WEST) + { + base = alpha > -t && alpha < t; + } + else + { + base = alpha < -Math.PI + t || alpha > Math.PI - t; + } + + var result = null; + + if (base) + { + if (orthogonal && ((vertical && next.x >= start.x && next.x <= end.x) || + (!vertical && next.y >= start.y && next.y <= end.y))) + { + if (vertical) + { + result = new mxPoint(next.x, start.y); + } + else + { + result = new mxPoint(start.x, next.y); + } + } + else + { + if (direction == mxConstants.DIRECTION_NORTH) + { + result = new mxPoint(x + w / 2 + h * Math.tan(alpha) / 2, + y + h); + } + else if (direction == mxConstants.DIRECTION_SOUTH) + { + result = new mxPoint(x + w / 2 - h * Math.tan(alpha) / 2, + y); + } + else if (direction == mxConstants.DIRECTION_WEST) + { + result = new mxPoint(x + w, y + h / 2 + + w * Math.tan(alpha) / 2); + } + else + { + result = new mxPoint(x, y + h / 2 - + w * Math.tan(alpha) / 2); + } + } + } + else + { + if (orthogonal) + { + var pt = new mxPoint(cx, cy); + + if (next.y >= y && next.y <= y + h) + { + pt.x = (vertical) ? cx : ( + (direction == mxConstants.DIRECTION_WEST) ? + x + w : x); + pt.y = next.y; + } + else if (next.x >= x && next.x <= x + w) + { + pt.x = next.x; + pt.y = (!vertical) ? cy : ( + (direction == mxConstants.DIRECTION_NORTH) ? + y + h : y); + } + + // Compute angle + dx = next.x - pt.x; + dy = next.y - pt.y; + + cx = pt.x; + cy = pt.y; + } + + if ((vertical && next.x <= x + w / 2) || + (!vertical && next.y <= y + h / 2)) + { + result = mxUtils.intersection(next.x, next.y, cx, cy, + start.x, start.y, corner.x, corner.y); + } + else + { + result = mxUtils.intersection(next.x, next.y, cx, cy, + corner.x, corner.y, end.x, end.y); + } + } + + if (result == null) + { + result = new mxPoint(cx, cy); + } + + return result; + } +}; diff --git a/src/js/view/mxPrintPreview.js b/src/js/view/mxPrintPreview.js new file mode 100644 index 0000000..24a65e6 --- /dev/null +++ b/src/js/view/mxPrintPreview.js @@ -0,0 +1,801 @@ +/** + * $Id: mxPrintPreview.js,v 1.61 2012-05-15 14:12:40 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxPrintPreview + * + * Implements printing of a diagram across multiple pages. The following opens + * a print preview for an existing graph: + * + * (code) + * var preview = new mxPrintPreview(graph); + * preview.open(); + * (end) + * + * Use <mxUtils.getScaleForPageCount> as follows in order to print the graph + * across a given number of pages: + * + * (code) + * var pageCount = mxUtils.prompt('Enter page count', '1'); + * + * if (pageCount != null) + * { + * var scale = mxUtils.getScaleForPageCount(pageCount, graph); + * var preview = new mxPrintPreview(graph, scale); + * preview.open(); + * } + * (end) + * + * Headers: + * + * Apart from setting the title argument in the mxPrintPreview constructor you + * can override <renderPage> as follows to add a header to any page: + * + * (code) + * var oldRenderPage = mxPrintPreview.prototype.renderPage; + * mxPrintPreview.prototype.renderPage = function(w, h, dx, dy, scale, pageNumber) + * { + * var div = oldRenderPage.apply(this, arguments); + * + * var header = document.createElement('div'); + * header.style.position = 'absolute'; + * header.style.top = '0px'; + * header.style.width = '100%'; + * header.style.textAlign = 'right'; + * mxUtils.write(header, 'Your header here - Page ' + pageNumber + ' / ' + this.pageCount); + * div.firstChild.appendChild(header); + * + * return div; + * }; + * (end) + * + * Page Format: + * + * For landscape printing, use <mxConstants.PAGE_FORMAT_A4_LANDSCAPE> as + * the pageFormat in <mxUtils.getScaleForPageCount> and <mxPrintPreview>. + * Keep in mind that one can not set the defaults for the print dialog + * of the operating system from JavaScript so the user must manually choose + * a page format that matches this setting. + * + * You can try passing the following CSS directive to <open> to set the + * page format in the print dialog to landscape. However, this CSS + * directive seems to be ignored in most major browsers, including IE. + * + * (code) + * @page { + * size: landscape; + * } + * (end) + * + * Note that the print preview behaves differently in IE when used from the + * filesystem or via HTTP so printing should always be tested via HTTP. + * + * If you are using a DOCTYPE in the source page you can override <getDoctype> + * and provide the same DOCTYPE for the print preview if required. Here is + * an example for IE8 standards mode. + * + * (code) + * var preview = new mxPrintPreview(graph); + * preview.getDoctype = function() + * { + * return '<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5,IE=8" ><![endif]-->'; + * }; + * preview.open(); + * (end) + * + * Constructor: mxPrintPreview + * + * Constructs a new print preview for the given parameters. + * + * Parameters: + * + * graph - <mxGraph> to be previewed. + * scale - Optional scale of the output. Default is 1 / <mxGraph.pageScale>. + * border - Border in pixels along each side of every page. Note that the + * actual print function in the browser will add another border for + * printing. + * pageFormat - <mxRectangle> that specifies the page format (in pixels). + * This should match the page format of the printer. Default uses the + * <mxGraph.pageFormat> of the given graph. + * x0 - Optional left offset of the output. Default is 0. + * y0 - Optional top offset of the output. Default is 0. + * borderColor - Optional color of the page border. Default is no border. + * Note that a border is sometimes useful to highlight the printed page + * border in the print preview of the browser. + * title - Optional string that is used for the window title. Default + * is 'Printer-friendly version'. + * pageSelector - Optional boolean that specifies if the page selector + * should appear in the window with the print preview. Default is true. + */ +function mxPrintPreview(graph, scale, pageFormat, border, x0, y0, borderColor, title, pageSelector) +{ + this.graph = graph; + this.scale = (scale != null) ? scale : 1 / graph.pageScale; + this.border = (border != null) ? border : 0; + this.pageFormat = (pageFormat != null) ? pageFormat : graph.pageFormat; + this.title = (title != null) ? title : 'Printer-friendly version'; + this.x0 = (x0 != null) ? x0 : 0; + this.y0 = (y0 != null) ? y0 : 0; + this.borderColor = borderColor; + this.pageSelector = (pageSelector != null) ? pageSelector : true; +}; + +/** + * Variable: graph + * + * Reference to the <mxGraph> that should be previewed. + */ +mxPrintPreview.prototype.graph = null; + +/** + * Variable: pageFormat + * + * Holds the <mxRectangle> that defines the page format. + */ +mxPrintPreview.prototype.pageFormat = null; + +/** + * Variable: scale + * + * Holds the scale of the print preview. + */ +mxPrintPreview.prototype.scale = null; + +/** + * Variable: border + * + * The border inset around each side of every page in the preview. This is set + * to 0 if autoOrigin is false. + */ +mxPrintPreview.prototype.border = 0; + +/** +/** + * Variable: x0 + * + * Holds the horizontal offset of the output. + */ +mxPrintPreview.prototype.x0 = 0; + +/** + * Variable: y0 + * + * Holds the vertical offset of the output. + */ +mxPrintPreview.prototype.y0 = 0; + +/** + * Variable: autoOrigin + * + * Specifies if the origin should be automatically computed based on the top, + * left corner of the actual diagram contents. If this is set to false then the + * values for <x0> and <y0> will be overridden in <open>. Default is true. + */ +mxPrintPreview.prototype.autoOrigin = true; + +/** + * Variable: printOverlays + * + * Specifies if overlays should be printed. Default is false. + */ +mxPrintPreview.prototype.printOverlays = false; + +/** + * Variable: borderColor + * + * Holds the color value for the page border. + */ +mxPrintPreview.prototype.borderColor = null; + +/** + * Variable: title + * + * Holds the title of the preview window. + */ +mxPrintPreview.prototype.title = null; + +/** + * Variable: pageSelector + * + * Boolean that specifies if the page selector should be + * displayed. Default is true. + */ +mxPrintPreview.prototype.pageSelector = null; + +/** + * Variable: wnd + * + * Reference to the preview window. + */ +mxPrintPreview.prototype.wnd = null; + +/** + * Variable: pageCount + * + * Holds the actual number of pages in the preview. + */ +mxPrintPreview.prototype.pageCount = 0; + +/** + * Function: getWindow + * + * Returns <wnd>. + */ +mxPrintPreview.prototype.getWindow = function() +{ + return this.wnd; +}; + +/** + * Function: getDocType + * + * Returns the string that should go before the HTML tag in the print preview + * page. This implementation returns an empty string. + */ +mxPrintPreview.prototype.getDoctype = function() +{ + return ''; +}; + +/** + * Function: open + * + * Shows the print preview window. The window is created here if it does + * not exist. + * + * Parameters: + * + * css - Optional CSS string to be used in the new page's head section. + */ +mxPrintPreview.prototype.open = function(css) +{ + // Closing the window while the page is being rendered may cause an + // exception in IE. This and any other exceptions are simply ignored. + var previousInitializeOverlay = this.graph.cellRenderer.initializeOverlay; + var div = null; + + try + { + // Temporarily overrides the method to redirect rendering of overlays + // to the draw pane so that they are visible in the printout + if (this.printOverlays) + { + this.graph.cellRenderer.initializeOverlay = function(state, overlay) + { + overlay.init(state.view.getDrawPane()); + }; + } + + if (this.wnd == null) + { + this.wnd = window.open(); + var doc = this.wnd.document; + var dt = this.getDoctype(); + + if (dt != null && dt.length > 0) + { + doc.writeln(dt); + } + + doc.writeln('<html>'); + doc.writeln('<head>'); + this.writeHead(doc, css); + doc.writeln('</head>'); + doc.writeln('<body class="mxPage">'); + + // Adds all required stylesheets and namespaces + mxClient.link('stylesheet', mxClient.basePath + '/css/common.css', doc); + + if (mxClient.IS_IE && document.documentMode != 9) + { + doc.namespaces.add('v', 'urn:schemas-microsoft-com:vml'); + doc.namespaces.add('o', 'urn:schemas-microsoft-com:office:office'); + var ss = doc.createStyleSheet(); + ss.cssText = 'v\\:*{behavior:url(#default#VML)}o\\:*{behavior:url(#default#VML)}'; + mxClient.link('stylesheet', mxClient.basePath + '/css/explorer.css', doc); + } + + // Computes the horizontal and vertical page count + var bounds = this.graph.getGraphBounds().clone(); + var currentScale = this.graph.getView().getScale(); + var sc = currentScale / this.scale; + var tr = this.graph.getView().getTranslate(); + + // Uses the absolute origin with no offset for all printing + if (!this.autoOrigin) + { + this.x0 = -tr.x * this.scale; + this.y0 = -tr.y * this.scale; + bounds.width += bounds.x; + bounds.height += bounds.y; + bounds.x = 0; + bounds.y = 0; + this.border = 0; + } + + // Compute the unscaled, untranslated bounds to find + // the number of vertical and horizontal pages + bounds.width /= sc; + bounds.height /= sc; + + // Store the available page area + var availableWidth = this.pageFormat.width - (this.border * 2); + var availableHeight = this.pageFormat.height - (this.border * 2); + + var hpages = Math.max(1, Math.ceil((bounds.width + this.x0) / availableWidth)); + var vpages = Math.max(1, Math.ceil((bounds.height + this.y0) / availableHeight)); + this.pageCount = hpages * vpages; + + var writePageSelector = mxUtils.bind(this, function() + { + if (this.pageSelector && (vpages > 1 || hpages > 1)) + { + var table = this.createPageSelector(vpages, hpages); + doc.body.appendChild(table); + + // Workaround for position: fixed which isn't working in IE + if (mxClient.IS_IE) + { + table.style.position = 'absolute'; + + var update = function() + { + table.style.top = (doc.body.scrollTop + 10) + 'px'; + }; + + mxEvent.addListener(this.wnd, 'scroll', function(evt) + { + update(); + }); + + mxEvent.addListener(this.wnd, 'resize', function(evt) + { + update(); + }); + } + } + }); + + // Stores pages for later retrieval + var pages = null; + + // Workaround for aspect of image shapes updated asynchronously + // in VML so we need to fetch the markup of the DIV containing + // the image after the udpate of the style of the DOM node. + // LATER: Allow document for display markup to be customized. + if (mxClient.IS_IE && document.documentMode != 9) + { + pages = []; + + // Overrides asynchronous loading of images for fetching HTML markup + var waitCounter = 0; + var isDone = false; + + var mxImageShapeScheduleUpdateAspect = mxImageShape.prototype.scheduleUpdateAspect; + var mxImageShapeUpdateAspect = mxImageShape.prototype.updateAspect; + + var writePages = function() + { + if (isDone && waitCounter == 0) + { + // Restores previous implementations + mxImageShape.prototype.scheduleUpdateAspect = mxImageShapeScheduleUpdateAspect; + mxImageShape.prototype.updateAspect = mxImageShapeUpdateAspect; + + var markup = ''; + + for (var i = 0; i < pages.length; i++) + { + markup += pages[i].outerHTML; + pages[i].parentNode.removeChild(pages[i]); + + if (i < pages.length - 1) + { + markup += '<hr/>'; + } + } + + doc.body.innerHTML = markup; + writePageSelector(); + } + }; + + // Overrides functions to implement wait counter + mxImageShape.prototype.scheduleUpdateAspect = function() + { + waitCounter++; + mxImageShapeScheduleUpdateAspect.apply(this, arguments); + }; + + // Overrides functions to implement wait counter + mxImageShape.prototype.updateAspect = function() + { + mxImageShapeUpdateAspect.apply(this, arguments); + waitCounter--; + writePages(); + }; + } + + // Appends each page to the page output for printing, making + // sure there will be a page break after each page (ie. div) + for (var i = 0; i < vpages; i++) + { + var dy = i * availableHeight / this.scale - this.y0 / this.scale + + (bounds.y - tr.y * currentScale) / currentScale; + + for (var j = 0; j < hpages; j++) + { + if (this.wnd == null) + { + return null; + } + + var dx = j * availableWidth / this.scale - this.x0 / this.scale + + (bounds.x - tr.x * currentScale) / currentScale; + var pageNum = i * hpages + j + 1; + + div = this.renderPage(this.pageFormat.width, this.pageFormat.height, + -dx, -dy, this.scale, pageNum); + + // Gives the page a unique ID for later accessing the page + div.setAttribute('id', 'mxPage-'+pageNum); + + // Border of the DIV (aka page) inside the document + if (this.borderColor != null) + { + div.style.borderColor = this.borderColor; + div.style.borderStyle = 'solid'; + div.style.borderWidth = '1px'; + } + + // Needs to be assigned directly because IE doesn't support + // child selectors, eg. body > div { background: white; } + div.style.background = 'white'; + + if (i < vpages - 1 || j < hpages - 1) + { + div.style.pageBreakAfter = 'always'; + } + + // NOTE: We are dealing with cross-window DOM here, which + // is a problem in IE, so we copy the HTML markup instead. + // The underlying problem is that the graph display markup + // creation (in mxShape, mxGraphView) is hardwired to using + // document.createElement and hence we must use document + // to create the complete page and then copy it over to the + // new window.document. This can be fixed later by using the + // ownerDocument of the container in mxShape and mxGraphView. + if (mxClient.IS_IE) + { + // For some obscure reason, removing the DIV from the + // parent before fetching its outerHTML has missing + // fillcolor properties and fill children, so the div + // must be removed afterwards to keep the fillcolors. + // For delayed output we remote the DIV from the + // original document when we write out all pages. + doc.writeln(div.outerHTML); + + if (pages != null) + { + pages.push(div); + } + else + { + div.parentNode.removeChild(div); + } + } + else + { + div.parentNode.removeChild(div); + doc.body.appendChild(div); + } + + if (i < vpages - 1 || j < hpages - 1) + { + var hr = doc.createElement('hr'); + hr.className = 'mxPageBreak'; + doc.body.appendChild(hr); + } + } + } + + doc.writeln('</body>'); + doc.writeln('</html>'); + doc.close(); + + // Marks the printing complete for async handling + if (pages != null) + { + isDone = true; + writePages(); + } + else + { + writePageSelector(); + } + + // Removes all event handlers in the print output + mxEvent.release(doc.body); + } + + this.wnd.focus(); + } + catch (e) + { + // Removes the DIV from the document in case of an error + if (div != null && div.parentNode != null) + { + div.parentNode.removeChild(div); + } + } + finally + { + this.graph.cellRenderer.initializeOverlay = previousInitializeOverlay; + } + + return this.wnd; +}; + +/** + * Function: writeHead + * + * Writes the HEAD section into the given document, without the opening + * and closing HEAD tags. + */ +mxPrintPreview.prototype.writeHead = function(doc, css) +{ + if (this.title != null) + { + doc.writeln('<title>' + this.title + '</title>'); + } + + // Makes sure no horizontal rulers are printed + doc.writeln('<style type="text/css">'); + doc.writeln('@media print {'); + doc.writeln(' table.mxPageSelector { display: none; }'); + doc.writeln(' hr.mxPageBreak { display: none; }'); + doc.writeln('}'); + doc.writeln('@media screen {'); + + // NOTE: position: fixed is not supported in IE, so the page selector + // position (absolute) needs to be updated in IE (see below) + doc.writeln(' table.mxPageSelector { position: fixed; right: 10px; top: 10px;' + + 'font-family: Arial; font-size:10pt; border: solid 1px darkgray;' + + 'background: white; border-collapse:collapse; }'); + doc.writeln(' table.mxPageSelector td { border: solid 1px gray; padding:4px; }'); + doc.writeln(' body.mxPage { background: gray; }'); + doc.writeln('}'); + + if (css != null) + { + doc.writeln(css); + } + + doc.writeln('</style>'); +}; + +/** + * Function: createPageSelector + * + * Creates the page selector table. + */ +mxPrintPreview.prototype.createPageSelector = function(vpages, hpages) +{ + var doc = this.wnd.document; + var table = doc.createElement('table'); + table.className = 'mxPageSelector'; + table.setAttribute('border', '0'); + + var tbody = doc.createElement('tbody'); + + for (var i = 0; i < vpages; i++) + { + var row = doc.createElement('tr'); + + for (var j = 0; j < hpages; j++) + { + var pageNum = i * hpages + j + 1; + var cell = doc.createElement('td'); + + // Needs anchor for all browers to work without JavaScript + // LATER: Does not work in Firefox because the generated document + // has the URL of the opening document, the anchor is appended + // to that URL and the full URL is loaded on click. + if (!mxClient.IS_NS || mxClient.IS_SF || mxClient.IS_GC) + { + var a = doc.createElement('a'); + a.setAttribute('href', '#mxPage-' + pageNum); + mxUtils.write(a, pageNum, doc); + cell.appendChild(a); + } + else + { + mxUtils.write(cell, pageNum, doc); + } + + row.appendChild(cell); + } + + tbody.appendChild(row); + } + + table.appendChild(tbody); + + return table; +}; + +/** + * Function: renderPage + * + * Creates a DIV that prints a single page of the given + * graph using the given scale and returns the DIV that + * represents the page. + * + * Parameters: + * + * w - Width of the page in pixels. + * h - Height of the page in pixels. + * dx - Horizontal translation for the diagram. + * dy - Vertical translation for the diagram. + * scale - Scale for the diagram. + * pageNumber - Number of the page to be rendered. + */ +mxPrintPreview.prototype.renderPage = function(w, h, dx, dy, scale, pageNumber) +{ + var div = document.createElement('div'); + + try + { + div.style.width = w + 'px'; + div.style.height = h + 'px'; + div.style.overflow = 'hidden'; + div.style.pageBreakInside = 'avoid'; + + var innerDiv = document.createElement('div'); + innerDiv.style.top = this.border + 'px'; + innerDiv.style.left = this.border + 'px'; + innerDiv.style.width = (w - 2 * this.border) + 'px'; + innerDiv.style.height = (h - 2 * this.border) + 'px'; + innerDiv.style.overflow = 'hidden'; + + if (this.graph.dialect == mxConstants.DIALECT_VML) + { + innerDiv.style.position = 'absolute'; + } + + div.appendChild(innerDiv); + document.body.appendChild(div); + var view = this.graph.getView(); + + var previousContainer = this.graph.container; + this.graph.container = innerDiv; + + var canvas = view.getCanvas(); + var backgroundPane = view.getBackgroundPane(); + var drawPane = view.getDrawPane(); + var overlayPane = view.getOverlayPane(); + + if (this.graph.dialect == mxConstants.DIALECT_SVG) + { + view.createSvg(); + } + else if (this.graph.dialect == mxConstants.DIALECT_VML) + { + view.createVml(); + } + else + { + view.createHtml(); + } + + // Disables events on the view + var eventsEnabled = view.isEventsEnabled(); + view.setEventsEnabled(false); + + // Disables the graph to avoid cursors + var graphEnabled = this.graph.isEnabled(); + this.graph.setEnabled(false); + + // Resets the translation + var translate = view.getTranslate(); + view.translate = new mxPoint(dx, dy); + + var temp = null; + + try + { + // Creates the temporary cell states in the view and + // draws them onto the temporary DOM nodes in the view + var model = this.graph.getModel(); + var cells = [model.getRoot()]; + temp = new mxTemporaryCellStates(view, scale, cells); + } + finally + { + // Removes overlay pane with selection handles + // controls and icons from the print output + if (mxClient.IS_IE) + { + view.overlayPane.innerHTML = ''; + } + else + { + // Removes everything but the SVG node + var tmp = innerDiv.firstChild; + + while (tmp != null) + { + var next = tmp.nextSibling; + var name = tmp.nodeName.toLowerCase(); + + // Note: Width and heigh are required in FF 11 + if (name == 'svg') + { + tmp.setAttribute('width', parseInt(innerDiv.style.width)); + tmp.setAttribute('height', parseInt(innerDiv.style.height)); + } + // Tries to fetch all text labels and only text labels + else if (tmp.style.cursor != 'default' && name != 'table') + { + tmp.parentNode.removeChild(tmp); + } + + tmp = next; + } + } + + // Completely removes the overlay pane to remove more handles + view.overlayPane.parentNode.removeChild(view.overlayPane); + + // Restores the state of the view + this.graph.setEnabled(graphEnabled); + this.graph.container = previousContainer; + view.canvas = canvas; + view.backgroundPane = backgroundPane; + view.drawPane = drawPane; + view.overlayPane = overlayPane; + view.translate = translate; + temp.destroy(); + view.setEventsEnabled(eventsEnabled); + } + } + catch (e) + { + div.parentNode.removeChild(div); + div = null; + + throw e; + } + + return div; +}; + +/** + * Function: print + * + * Opens the print preview and shows the print dialog. + */ +mxPrintPreview.prototype.print = function() +{ + var wnd = this.open(); + + if (wnd != null) + { + wnd.print(); + } +}; + +/** + * Function: close + * + * Closes the print preview window. + */ +mxPrintPreview.prototype.close = function() +{ + if (this.wnd != null) + { + this.wnd.close(); + this.wnd = null; + } +}; diff --git a/src/js/view/mxSpaceManager.js b/src/js/view/mxSpaceManager.js new file mode 100644 index 0000000..2a2dd11 --- /dev/null +++ b/src/js/view/mxSpaceManager.js @@ -0,0 +1,460 @@ +/** + * $Id: mxSpaceManager.js,v 1.9 2010-01-02 09:45:15 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxSpaceManager + * + * In charge of moving cells after a resize. + * + * Constructor: mxSpaceManager + * + * Constructs a new automatic layout for the given graph. + * + * Arguments: + * + * graph - Reference to the enclosing graph. + */ +function mxSpaceManager(graph, shiftRightwards, shiftDownwards, extendParents) +{ + this.resizeHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.cellsResized(evt.getProperty('cells')); + } + }); + + this.foldHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled()) + { + this.cellsResized(evt.getProperty('cells')); + } + }); + + this.shiftRightwards = (shiftRightwards != null) ? shiftRightwards : true; + this.shiftDownwards = (shiftDownwards != null) ? shiftDownwards : true; + this.extendParents = (extendParents != null) ? extendParents : true; + this.setGraph(graph); +}; + +/** + * Extends mxEventSource. + */ +mxSpaceManager.prototype = new mxEventSource(); +mxSpaceManager.prototype.constructor = mxSpaceManager; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxSpaceManager.prototype.graph = null; + +/** + * Variable: enabled + * + * Specifies if event handling is enabled. Default is true. + */ +mxSpaceManager.prototype.enabled = true; + +/** + * Variable: shiftRightwards + * + * Specifies if event handling is enabled. Default is true. + */ +mxSpaceManager.prototype.shiftRightwards = true; + +/** + * Variable: shiftDownwards + * + * Specifies if event handling is enabled. Default is true. + */ +mxSpaceManager.prototype.shiftDownwards = true; + +/** + * Variable: extendParents + * + * Specifies if event handling is enabled. Default is true. + */ +mxSpaceManager.prototype.extendParents = true; + +/** + * Variable: resizeHandler + * + * Holds the function that handles the move event. + */ +mxSpaceManager.prototype.resizeHandler = null; + +/** + * Variable: foldHandler + * + * Holds the function that handles the fold event. + */ +mxSpaceManager.prototype.foldHandler = null; + +/** + * Function: isCellIgnored + * + * Sets the graph that the layouts operate on. + */ +mxSpaceManager.prototype.isCellIgnored = function(cell) +{ + return !this.getGraph().getModel().isVertex(cell); +}; + +/** + * Function: isCellShiftable + * + * Sets the graph that the layouts operate on. + */ +mxSpaceManager.prototype.isCellShiftable = function(cell) +{ + return this.getGraph().getModel().isVertex(cell) && + this.getGraph().isCellMovable(cell); +}; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxSpaceManager.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxSpaceManager.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: isShiftRightwards + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxSpaceManager.prototype.isShiftRightwards = function() +{ + return this.shiftRightwards; +}; + +/** + * Function: setShiftRightwards + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxSpaceManager.prototype.setShiftRightwards = function(value) +{ + this.shiftRightwards = value; +}; + +/** + * Function: isShiftDownwards + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxSpaceManager.prototype.isShiftDownwards = function() +{ + return this.shiftDownwards; +}; + +/** + * Function: setShiftDownwards + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxSpaceManager.prototype.setShiftDownwards = function(value) +{ + this.shiftDownwards = value; +}; + +/** + * Function: isExtendParents + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxSpaceManager.prototype.isExtendParents = function() +{ + return this.extendParents; +}; + +/** + * Function: setShiftDownwards + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxSpaceManager.prototype.setExtendParents = function(value) +{ + this.extendParents = value; +}; + +/** + * Function: getGraph + * + * Returns the graph that this layout operates on. + */ +mxSpaceManager.prototype.getGraph = function() +{ + return this.graph; +}; + +/** + * Function: setGraph + * + * Sets the graph that the layouts operate on. + */ +mxSpaceManager.prototype.setGraph = function(graph) +{ + if (this.graph != null) + { + this.graph.removeListener(this.resizeHandler); + this.graph.removeListener(this.foldHandler); + } + + this.graph = graph; + + if (this.graph != null) + { + this.graph.addListener(mxEvent.RESIZE_CELLS, this.resizeHandler); + this.graph.addListener(mxEvent.FOLD_CELLS, this.foldHandler); + } +}; + +/** + * Function: cellsResized + * + * Called from <moveCellsIntoParent> to invoke the <move> hook in the + * automatic layout of each modified cell's parent. The event is used to + * define the x- and y-coordinates passed to the move function. + * + * Parameters: + * + * cell - Array of <mxCells> that have been resized. + */ +mxSpaceManager.prototype.cellsResized = function(cells) +{ + if (cells != null) + { + var model = this.graph.getModel(); + + // Raising the update level should not be required + // since only one call is made below + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (!this.isCellIgnored(cells[i])) + { + this.cellResized(cells[i]); + break; + } + } + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: cellResized + * + * Called from <moveCellsIntoParent> to invoke the <move> hook in the + * automatic layout of each modified cell's parent. The event is used to + * define the x- and y-coordinates passed to the move function. + * + * Parameters: + * + * cell - <mxCell> that has been resized. + */ +mxSpaceManager.prototype.cellResized = function(cell) +{ + var graph = this.getGraph(); + var view = graph.getView(); + var model = graph.getModel(); + + var state = view.getState(cell); + var pstate = view.getState(model.getParent(cell)); + + if (state != null && + pstate != null) + { + var cells = this.getCellsToShift(state); + var geo = model.getGeometry(cell); + + if (cells != null && + geo != null) + { + var tr = view.translate; + var scale = view.scale; + + var x0 = state.x - pstate.origin.x - tr.x * scale; + var y0 = state.y - pstate.origin.y - tr.y * scale; + var right = state.x + state.width; + var bottom = state.y + state.height; + + var dx = state.width - geo.width * scale + x0 - geo.x * scale; + var dy = state.height - geo.height * scale + y0 - geo.y * scale; + + var fx = 1 - geo.width * scale / state.width; + var fy = 1 - geo.height * scale / state.height; + + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != cell && + this.isCellShiftable(cells[i])) + { + this.shiftCell(cells[i], dx, dy, x0, y0, right, bottom, fx, fy, + this.isExtendParents() && + graph.isExtendParent(cells[i])); + } + } + } + finally + { + model.endUpdate(); + } + } + } +}; + +/** + * Function: shiftCell + * + * Called from <moveCellsIntoParent> to invoke the <move> hook in the + * automatic layout of each modified cell's parent. The event is used to + * define the x- and y-coordinates passed to the move function. + * + * Parameters: + * + * cell - Array of <mxCells> that have been moved. + * evt - Mouse event that represents the mousedown. + */ +mxSpaceManager.prototype.shiftCell = function(cell, dx, dy, Ox0, y0, right, + bottom, fx, fy, extendParent) +{ + var graph = this.getGraph(); + var state = graph.getView().getState(cell); + + if (state != null) + { + var model = graph.getModel(); + var geo = model.getGeometry(cell); + + if (geo != null) + { + model.beginUpdate(); + try + { + if (this.isShiftRightwards()) + { + if (state.x >= right) + { + geo = geo.clone(); + geo.translate(-dx, 0); + } + else + { + var tmpDx = Math.max(0, state.x - x0); + geo = geo.clone(); + geo.translate(-fx * tmpDx, 0); + } + } + + if (this.isShiftDownwards()) + { + if (state.y >= bottom) + { + geo = geo.clone(); + geo.translate(0, -dy); + } + else + { + var tmpDy = Math.max(0, state.y - y0); + geo = geo.clone(); + geo.translate(0, -fy * tmpDy); + } + } + + if (geo != model.getGeometry(cell)) + { + model.setGeometry(cell, geo); + + // Parent size might need to be updated if this + // is seen as part of the resize + if (extendParent) + { + graph.extendParent(cell); + } + } + } + finally + { + model.endUpdate(); + } + } + } +}; + +/** + * Function: getCellsToShift + * + * Returns the cells to shift after a resize of the + * specified <mxCellState>. + */ +mxSpaceManager.prototype.getCellsToShift = function(state) +{ + var graph = this.getGraph(); + var parent = graph.getModel().getParent(state.cell); + var down = this.isShiftDownwards(); + var right = this.isShiftRightwards(); + + return graph.getCellsBeyond(state.x + ((down) ? 0 : state.width), + state.y + ((down && right) ? 0 : state.height), parent, right, down); +}; + +/** + * Function: destroy + * + * Removes all handlers from the <graph> and deletes the reference to it. + */ +mxSpaceManager.prototype.destroy = function() +{ + this.setGraph(null); +}; diff --git a/src/js/view/mxStyleRegistry.js b/src/js/view/mxStyleRegistry.js new file mode 100644 index 0000000..6ad878d --- /dev/null +++ b/src/js/view/mxStyleRegistry.js @@ -0,0 +1,70 @@ +/** + * $Id: mxStyleRegistry.js,v 1.10 2011-04-27 10:15:39 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +var mxStyleRegistry = +{ + /** + * Class: mxStyleRegistry + * + * Singleton class that acts as a global converter from string to object values + * in a style. This is currently only used to perimeters and edge styles. + * + * Variable: values + * + * Maps from strings to objects. + */ + values: [], + + /** + * Function: putValue + * + * Puts the given object into the registry under the given name. + */ + putValue: function(name, obj) + { + mxStyleRegistry.values[name] = obj; + }, + + /** + * Function: getValue + * + * Returns the value associated with the given name. + */ + getValue: function(name) + { + return mxStyleRegistry.values[name]; + }, + + /** + * Function: getName + * + * Returns the name for the given value. + */ + getName: function(value) + { + for (var key in mxStyleRegistry.values) + { + if (mxStyleRegistry.values[key] == value) + { + return key; + } + } + + return null; + } + +}; + +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ELBOW, mxEdgeStyle.ElbowConnector); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ENTITY_RELATION, mxEdgeStyle.EntityRelation); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_LOOP, mxEdgeStyle.Loop); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_SIDETOSIDE, mxEdgeStyle.SideToSide); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_TOPTOBOTTOM, mxEdgeStyle.TopToBottom); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ORTHOGONAL, mxEdgeStyle.OrthConnector); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_SEGMENT, mxEdgeStyle.SegmentConnector); + +mxStyleRegistry.putValue(mxConstants.PERIMETER_ELLIPSE, mxPerimeter.EllipsePerimeter); +mxStyleRegistry.putValue(mxConstants.PERIMETER_RECTANGLE, mxPerimeter.RectanglePerimeter); +mxStyleRegistry.putValue(mxConstants.PERIMETER_RHOMBUS, mxPerimeter.RhombusPerimeter); +mxStyleRegistry.putValue(mxConstants.PERIMETER_TRIANGLE, mxPerimeter.TrianglePerimeter); diff --git a/src/js/view/mxStylesheet.js b/src/js/view/mxStylesheet.js new file mode 100644 index 0000000..82a520e --- /dev/null +++ b/src/js/view/mxStylesheet.js @@ -0,0 +1,266 @@ +/** + * $Id: mxStylesheet.js,v 1.35 2010-03-26 10:24:58 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxStylesheet + * + * Defines the appearance of the cells in a graph. See <putCellStyle> for an + * example of creating a new cell style. It is recommended to use objects, not + * arrays for holding cell styles. Existing styles can be cloned using + * <mxUtils.clone> and turned into a string for debugging using + * <mxUtils.toString>. + * + * Default Styles: + * + * The stylesheet contains two built-in styles, which are used if no style is + * defined for a cell: + * + * defaultVertex - Default style for vertices + * defaultEdge - Default style for edges + * + * Example: + * + * (code) + * var vertexStyle = stylesheet.getDefaultVertexStyle(); + * vertexStyle[mxConstants.ROUNDED] = true; + * var edgeStyle = stylesheet.getDefaultEdgeStyle(); + * edgeStyle[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation; + * (end) + * + * Modifies the built-in default styles. + * + * To avoid the default style for a cell, add a leading semicolon + * to the style definition, eg. + * + * (code) + * ;shadow=1 + * (end) + * + * Removing keys: + * + * For removing a key in a cell style of the form [stylename;|key=value;] the + * special value none can be used, eg. highlight;fillColor=none + * + * See also the helper methods in mxUtils to modify strings of this format, + * namely <mxUtils.setStyle>, <mxUtils.indexOfStylename>, + * <mxUtils.addStylename>, <mxUtils.removeStylename>, + * <mxUtils.removeAllStylenames> and <mxUtils.setStyleFlag>. + * + * Constructor: mxStylesheet + * + * Constructs a new stylesheet and assigns default styles. + */ +function mxStylesheet() +{ + this.styles = new Object(); + + this.putDefaultVertexStyle(this.createDefaultVertexStyle()); + this.putDefaultEdgeStyle(this.createDefaultEdgeStyle()); +}; + +/** + * Function: styles + * + * Maps from names to cell styles. Each cell style is a map of key, + * value pairs. + */ +mxStylesheet.prototype.styles; + +/** + * Function: createDefaultVertexStyle + * + * Creates and returns the default vertex style. + */ +mxStylesheet.prototype.createDefaultVertexStyle = function() +{ + var style = new Object(); + + style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE; + style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter; + style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE; + style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER; + style[mxConstants.STYLE_FILLCOLOR] = '#C3D9FF'; + style[mxConstants.STYLE_STROKECOLOR] = '#6482B9'; + style[mxConstants.STYLE_FONTCOLOR] = '#774400'; + + return style; +}; + +/** + * Function: createDefaultEdgeStyle + * + * Creates and returns the default edge style. + */ +mxStylesheet.prototype.createDefaultEdgeStyle = function() +{ + var style = new Object(); + + style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_CONNECTOR; + style[mxConstants.STYLE_ENDARROW] = mxConstants.ARROW_CLASSIC; + style[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_MIDDLE; + style[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER; + style[mxConstants.STYLE_STROKECOLOR] = '#6482B9'; + style[mxConstants.STYLE_FONTCOLOR] = '#446299'; + + return style; +}; + +/** + * Function: putDefaultVertexStyle + * + * Sets the default style for vertices using defaultVertex as the + * stylename. + * + * Parameters: + * style - Key, value pairs that define the style. + */ +mxStylesheet.prototype.putDefaultVertexStyle = function(style) +{ + this.putCellStyle('defaultVertex', style); +}; + +/** + * Function: putDefaultEdgeStyle + * + * Sets the default style for edges using defaultEdge as the stylename. + */ +mxStylesheet.prototype.putDefaultEdgeStyle = function(style) +{ + this.putCellStyle('defaultEdge', style); +}; + +/** + * Function: getDefaultVertexStyle + * + * Returns the default style for vertices. + */ +mxStylesheet.prototype.getDefaultVertexStyle = function() +{ + return this.styles['defaultVertex']; +}; + +/** + * Function: getDefaultEdgeStyle + * + * Sets the default style for edges. + */ +mxStylesheet.prototype.getDefaultEdgeStyle = function() +{ + return this.styles['defaultEdge']; +}; + +/** + * Function: putCellStyle + * + * Stores the given map of key, value pairs under the given name in + * <styles>. + * + * Example: + * + * The following example adds a new style called 'rounded' into an + * existing stylesheet: + * + * (code) + * var style = new Object(); + * style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE; + * style[mxConstants.STYLE_PERIMETER] = mxPerimeter.RectanglePerimeter; + * style[mxConstants.STYLE_ROUNDED] = true; + * graph.getStylesheet().putCellStyle('rounded', style); + * (end) + * + * In the above example, the new style is an object. The possible keys of + * the object are all the constants in <mxConstants> that start with STYLE + * and the values are either JavaScript objects, such as + * <mxPerimeter.RightAngleRectanglePerimeter> (which is in fact a function) + * or expressions, such as true. Note that not all keys will be + * interpreted by all shapes (eg. the line shape ignores the fill color). + * The final call to this method associates the style with a name in the + * stylesheet. The style is used in a cell with the following code: + * + * (code) + * model.setStyle(cell, 'rounded'); + * (end) + * + * Parameters: + * + * name - Name for the style to be stored. + * style - Key, value pairs that define the style. + */ +mxStylesheet.prototype.putCellStyle = function(name, style) +{ + this.styles[name] = style; +}; + +/** + * Function: getCellStyle + * + * Returns the cell style for the specified stylename or the given + * defaultStyle if no style can be found for the given stylename. + * + * Parameters: + * + * name - String of the form [(stylename|key=value);] that represents the + * style. + * defaultStyle - Default style to be returned if no style can be found. + */ +mxStylesheet.prototype.getCellStyle = function(name, defaultStyle) +{ + var style = defaultStyle; + + if (name != null && name.length > 0) + { + var pairs = name.split(';'); + + if (style != null && + name.charAt(0) != ';') + { + style = mxUtils.clone(style); + } + else + { + style = new Object(); + } + + // Parses each key, value pair into the existing style + for (var i = 0; i < pairs.length; i++) + { + var tmp = pairs[i]; + var pos = tmp.indexOf('='); + + if (pos >= 0) + { + var key = tmp.substring(0, pos); + var value = tmp.substring(pos + 1); + + if (value == mxConstants.NONE) + { + delete style[key]; + } + else if (mxUtils.isNumeric(value)) + { + style[key] = parseFloat(value); + } + else + { + style[key] = value; + } + } + else + { + // Merges the entries from a named style + var tmpStyle = this.styles[tmp]; + + if (tmpStyle != null) + { + for (var key in tmpStyle) + { + style[key] = tmpStyle[key]; + } + } + } + } + } + + return style; +}; diff --git a/src/js/view/mxSwimlaneManager.js b/src/js/view/mxSwimlaneManager.js new file mode 100644 index 0000000..fe40613 --- /dev/null +++ b/src/js/view/mxSwimlaneManager.js @@ -0,0 +1,449 @@ +/** + * $Id: mxSwimlaneManager.js,v 1.17 2011-01-14 15:21:10 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxSwimlaneManager + * + * Manager for swimlanes and nested swimlanes that sets the size of newly added + * swimlanes to that of their siblings, and propagates changes to the size of a + * swimlane to its siblings, if <siblings> is true, and its ancestors, if + * <bubbling> is true. + * + * Constructor: mxSwimlaneManager + * + * Constructs a new swimlane manager for the given graph. + * + * Arguments: + * + * graph - Reference to the enclosing graph. + */ +function mxSwimlaneManager(graph, horizontal, addEnabled, resizeEnabled) +{ + this.horizontal = (horizontal != null) ? horizontal : true; + this.addEnabled = (addEnabled != null) ? addEnabled : true; + this.resizeEnabled = (resizeEnabled != null) ? resizeEnabled : true; + + this.addHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled() && this.isAddEnabled()) + { + this.cellsAdded(evt.getProperty('cells')); + } + }); + + this.resizeHandler = mxUtils.bind(this, function(sender, evt) + { + if (this.isEnabled() && this.isResizeEnabled()) + { + this.cellsResized(evt.getProperty('cells')); + } + }); + + this.setGraph(graph); +}; + +/** + * Extends mxEventSource. + */ +mxSwimlaneManager.prototype = new mxEventSource(); +mxSwimlaneManager.prototype.constructor = mxSwimlaneManager; + +/** + * Variable: graph + * + * Reference to the enclosing <mxGraph>. + */ +mxSwimlaneManager.prototype.graph = null; + +/** + * Variable: enabled + * + * Specifies if event handling is enabled. Default is true. + */ +mxSwimlaneManager.prototype.enabled = true; + +/** + * Variable: horizontal + * + * Specifies the orientation of the swimlanes. Default is true. + */ +mxSwimlaneManager.prototype.horizontal = true; + +/** + * Variable: addEnabled + * + * Specifies if newly added cells should be resized to match the size of their + * existing siblings. Default is true. + */ +mxSwimlaneManager.prototype.addEnabled = true; + +/** + * Variable: resizeEnabled + * + * Specifies if resizing of swimlanes should be handled. Default is true. + */ +mxSwimlaneManager.prototype.resizeEnabled = true; + +/** + * Variable: moveHandler + * + * Holds the function that handles the move event. + */ +mxSwimlaneManager.prototype.addHandler = null; + +/** + * Variable: moveHandler + * + * Holds the function that handles the move event. + */ +mxSwimlaneManager.prototype.resizeHandler = null; + +/** + * Function: isEnabled + * + * Returns true if events are handled. This implementation + * returns <enabled>. + */ +mxSwimlaneManager.prototype.isEnabled = function() +{ + return this.enabled; +}; + +/** + * Function: setEnabled + * + * Enables or disables event handling. This implementation + * updates <enabled>. + * + * Parameters: + * + * enabled - Boolean that specifies the new enabled state. + */ +mxSwimlaneManager.prototype.setEnabled = function(value) +{ + this.enabled = value; +}; + +/** + * Function: isHorizontal + * + * Returns <horizontal>. + */ +mxSwimlaneManager.prototype.isHorizontal = function() +{ + return this.horizontal; +}; + +/** + * Function: setHorizontal + * + * Sets <horizontal>. + */ +mxSwimlaneManager.prototype.setHorizontal = function(value) +{ + this.horizontal = value; +}; + +/** + * Function: isAddEnabled + * + * Returns <addEnabled>. + */ +mxSwimlaneManager.prototype.isAddEnabled = function() +{ + return this.addEnabled; +}; + +/** + * Function: setAddEnabled + * + * Sets <addEnabled>. + */ +mxSwimlaneManager.prototype.setAddEnabled = function(value) +{ + this.addEnabled = value; +}; + +/** + * Function: isResizeEnabled + * + * Returns <resizeEnabled>. + */ +mxSwimlaneManager.prototype.isResizeEnabled = function() +{ + return this.resizeEnabled; +}; + +/** + * Function: setResizeEnabled + * + * Sets <resizeEnabled>. + */ +mxSwimlaneManager.prototype.setResizeEnabled = function(value) +{ + this.resizeEnabled = value; +}; + +/** + * Function: getGraph + * + * Returns the graph that this manager operates on. + */ +mxSwimlaneManager.prototype.getGraph = function() +{ + return this.graph; +}; + +/** + * Function: setGraph + * + * Sets the graph that the manager operates on. + */ +mxSwimlaneManager.prototype.setGraph = function(graph) +{ + if (this.graph != null) + { + this.graph.removeListener(this.addHandler); + this.graph.removeListener(this.resizeHandler); + } + + this.graph = graph; + + if (this.graph != null) + { + this.graph.addListener(mxEvent.ADD_CELLS, this.addHandler); + this.graph.addListener(mxEvent.CELLS_RESIZED, this.resizeHandler); + } +}; + +/** + * Function: isSwimlaneIgnored + * + * Returns true if the given swimlane should be ignored. + */ +mxSwimlaneManager.prototype.isSwimlaneIgnored = function(swimlane) +{ + return !this.getGraph().isSwimlane(swimlane); +}; + +/** + * Function: isCellHorizontal + * + * Returns true if the given cell is horizontal. If the given cell is not a + * swimlane, then the global orientation is returned. + */ +mxSwimlaneManager.prototype.isCellHorizontal = function(cell) +{ + if (this.graph.isSwimlane(cell)) + { + var state = this.graph.view.getState(cell); + var style = (state != null) ? state.style : this.graph.getCellStyle(cell); + + return mxUtils.getValue(style, mxConstants.STYLE_HORIZONTAL, 1) == 1; + } + + return !this.isHorizontal(); +}; + +/** + * Function: cellsAdded + * + * Called if any cells have been added. + * + * Parameters: + * + * cell - Array of <mxCells> that have been added. + */ +mxSwimlaneManager.prototype.cellsAdded = function(cells) +{ + if (cells != null) + { + var model = this.getGraph().getModel(); + + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (!this.isSwimlaneIgnored(cells[i])) + { + this.swimlaneAdded(cells[i]); + } + } + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: swimlaneAdded + * + * Updates the size of the given swimlane to match that of any existing + * siblings swimlanes. + * + * Parameters: + * + * swimlane - <mxCell> that represents the new swimlane. + */ +mxSwimlaneManager.prototype.swimlaneAdded = function(swimlane) +{ + var model = this.getGraph().getModel(); + var parent = model.getParent(swimlane); + var childCount = model.getChildCount(parent); + var geo = null; + + // Finds the first valid sibling swimlane as reference + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(parent, i); + + if (child != swimlane && !this.isSwimlaneIgnored(child)) + { + geo = model.getGeometry(child); + + if (geo != null) + { + break; + } + } + } + + // Applies the size of the refernece to the newly added swimlane + if (geo != null) + { + this.resizeSwimlane(swimlane, geo.width, geo.height); + } +}; + +/** + * Function: cellsResized + * + * Called if any cells have been resizes. Calls <swimlaneResized> for all + * swimlanes where <isSwimlaneIgnored> returns false. + * + * Parameters: + * + * cells - Array of <mxCells> whose size was changed. + */ +mxSwimlaneManager.prototype.cellsResized = function(cells) +{ + if (cells != null) + { + var model = this.getGraph().getModel(); + + model.beginUpdate(); + try + { + // Finds the top-level swimlanes and adds offsets + for (var i = 0; i < cells.length; i++) + { + if (!this.isSwimlaneIgnored(cells[i])) + { + var geo = model.getGeometry(cells[i]); + + if (geo != null) + { + var size = new mxRectangle(0, 0, geo.width, geo.height); + var top = cells[i]; + var current = top; + + while (current != null) + { + top = current; + current = model.getParent(current); + var tmp = (this.graph.isSwimlane(current)) ? + this.graph.getStartSize(current) : + new mxRectangle(); + size.width += tmp.width; + size.height += tmp.height; + } + + this.resizeSwimlane(top, size.width, size.height); + } + } + } + } + finally + { + model.endUpdate(); + } + } +}; + +/** + * Function: resizeSwimlane + * + * Called from <cellsResized> for all swimlanes that are not ignored to update + * the size of the siblings and the size of the parent swimlanes, recursively, + * if <bubbling> is true. + * + * Parameters: + * + * swimlane - <mxCell> whose size has changed. + */ +mxSwimlaneManager.prototype.resizeSwimlane = function(swimlane, w, h) +{ + var model = this.getGraph().getModel(); + + model.beginUpdate(); + try + { + if (!this.isSwimlaneIgnored(swimlane)) + { + var geo = model.getGeometry(swimlane); + + if (geo != null) + { + var horizontal = this.isCellHorizontal(swimlane); + + if ((horizontal && geo.height != h) || (!horizontal && geo.width != w)) + { + geo = geo.clone(); + + if (horizontal) + { + geo.height = h; + } + else + { + geo.width = w; + } + + model.setGeometry(swimlane, geo); + } + } + } + + var tmp = (this.graph.isSwimlane(swimlane)) ? + this.graph.getStartSize(swimlane) : + new mxRectangle(); + w -= tmp.width; + h -= tmp.height; + + var childCount = model.getChildCount(swimlane); + + for (var i = 0; i < childCount; i++) + { + var child = model.getChildAt(swimlane, i); + this.resizeSwimlane(child, w, h); + } + } + finally + { + model.endUpdate(); + } +}; + +/** + * Function: destroy + * + * Removes all handlers from the <graph> and deletes the reference to it. + */ +mxSwimlaneManager.prototype.destroy = function() +{ + this.setGraph(null); +}; diff --git a/src/js/view/mxTemporaryCellStates.js b/src/js/view/mxTemporaryCellStates.js new file mode 100644 index 0000000..ce8232c --- /dev/null +++ b/src/js/view/mxTemporaryCellStates.js @@ -0,0 +1,105 @@ +/** + * $Id: mxTemporaryCellStates.js,v 1.10 2010-04-20 14:43:12 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxTemporaryCellStates + * + * Extends <mxPoint> to implement a 2-dimensional rectangle with double + * precision coordinates. + * + * Constructor: mxRectangle + * + * Constructs a new rectangle for the optional parameters. If no parameters + * are given then the respective default values are used. + */ +function mxTemporaryCellStates(view, scale, cells) +{ + this.view = view; + scale = (scale != null) ? scale : 1; + + // Stores the previous state + this.oldBounds = view.getGraphBounds(); + this.oldStates = view.getStates(); + this.oldScale = view.getScale(); + + // Creates space for new states + view.setStates(new mxDictionary()); + view.setScale(scale); + + if (cells != null) + { + // Creates virtual parent state for validation + var state = view.createState(new mxCell()); + + // Validates the vertices and edges without adding them to + // the model so that the original cells are not modified + for (var i = 0; i < cells.length; i++) + { + view.validateBounds(state, cells[i]); + } + + var bbox = null; + + for (var i = 0; i < cells.length; i++) + { + var bounds = view.validatePoints(state, cells[i]); + + if (bbox == null) + { + bbox = bounds; + } + else + { + bbox.add(bounds); + } + } + + if (bbox == null) + { + bbox = new mxRectangle(); + } + + view.setGraphBounds(bbox); + } +}; + +/** + * Variable: view + * + * Holds the width of the rectangle. Default is 0. + */ +mxTemporaryCellStates.prototype.view = null; + +/** + * Variable: oldStates + * + * Holds the height of the rectangle. Default is 0. + */ +mxTemporaryCellStates.prototype.oldStates = null; + +/** + * Variable: oldBounds + * + * Holds the height of the rectangle. Default is 0. + */ +mxTemporaryCellStates.prototype.oldBounds = null; + +/** + * Variable: oldScale + * + * Holds the height of the rectangle. Default is 0. + */ +mxTemporaryCellStates.prototype.oldScale = null; + +/** + * Function: destroy + * + * Returns the top, left corner as a new <mxPoint>. + */ +mxTemporaryCellStates.prototype.destroy = function() +{ + this.view.setScale(this.oldScale); + this.view.setStates(this.oldStates); + this.view.setGraphBounds(this.oldBounds); +}; |