blob: 6d2ab863cfc50d5046c5ba1d5959d1446c3c85af [file] [log] [blame]
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* 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)
* mxCellRenderer.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.
*
* 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.GESTURE
*
* Fires in <fireGestureEvent> after a touch gesture. The <code>event</code>
* property contains the original gesture end event and the <code>cell</code>
* property contains the optional cell associated with the gesture.
*
* Event: mxEvent.TAP_AND_HOLD
*
* Fires in <tapAndHold> if a tap and hold event was detected. The <code>event</code>
* property contains the initial touch event and the <code>cell</code> property
* contains the cell under the mouse or null if the background was clicked.
*
* Event: mxEvent.FIRE_MOUSE_EVENT
*
* Fires in <fireMouseEvent> before the mouse listeners are invoked. The
* <code>eventName</code> property contains the event name and the
* <code>event</code> property contains the <mxMouseEvent>.
*
* 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.EDITING_STARTED
*
* Fires after 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.EDITING_STOPPED
*
* Fires after the in-place editor stops in <stopEditing>.
*
* 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, the <code>old</code> property
* contains the old value 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: nativeDoubleClickEnabled
*
* Specifies if native double click events should be detected. Default is true.
*/
mxGraph.prototype.nativeDblClickEnabled = true;
/**
* Variable: doubleTapEnabled
*
* Specifies if double taps on touch-based devices should be handled as a
* double click. Default is true.
*/
mxGraph.prototype.doubleTapEnabled = true;
/**
* Variable: doubleTapTimeout
*
* Specifies the timeout for double taps and non-native double clicks. Default
* is 500 ms.
*/
mxGraph.prototype.doubleTapTimeout = 500;
/**
* Variable: doubleTapTolerance
*
* Specifies the tolerance for double taps and double clicks in quirks mode.
* 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: tapAndHoldEnabled
*
* Specifies if tap and hold should be used for starting connections on touch-based
* devices. Default is true.
*/
mxGraph.prototype.tapAndHoldEnabled = true;
/**
* Variable: tapAndHoldDelay
*
* Specifies the time for a tap and hold. Default is 500 ms.
*/
mxGraph.prototype.tapAndHoldDelay = 500;
/**
* Variable: tapAndHoldInProgress
*
* True if the timer for tap and hold events is running.
*/
mxGraph.prototype.tapAndHoldInProgress = false;
/**
* Variable: tapAndHoldValid
*
* True as long as the timer is running and the touch events
* stay within the given <tapAndHoldTolerance>.
*/
mxGraph.prototype.tapAndHoldValid = false;
/**
* Variable: initialTouchX
*
* Holds the x-coordinate of the intial touch event for tap and hold.
*/
mxGraph.prototype.initialTouchX = 0;
/**
* Variable: initialTouchY
*
* Holds the y-coordinate of the intial touch event for tap and hold.
*/
mxGraph.prototype.initialTouchY = 0;
/**
* 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. This is ignored if
* <dropEnabled> is false. If enabled, it will call <splitEdge> to carry
* out the drop operation. 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: autoSizeCellsOnAdd
*
* Specifies if autoSize style should be applied when cells are added. Default is false.
*/
mxGraph.prototype.autoSizeCellsOnAdd = 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. Please consult the <ignoreScrollbars> for details. In general, with
* no scrollbars, the use of <allowAutoPanning> is recommended.
*/
mxGraph.prototype.autoScroll = true;
/**
* Variable: ignoreScrollbars
*
* Specifies if the graph should automatically scroll regardless of the
* scrollbars. This will scroll the container using positive values for
* scroll positions (ie usually only rightwards and downwards). To avoid
* possible conflicts with panning, set <translateToScrollPosition> to true.
*/
mxGraph.prototype.ignoreScrollbars = false;
/**
* Variable: translateToScrollPosition
*
* Specifies if the graph should automatically convert the current scroll
* position to a translate in the graph view when a mouseUp event is received.
* This can be used to avoid conflicts when using <autoScroll> and
* <ignoreScrollbars> with no scrollbars in the container.
*/
mxGraph.prototype.translateToScrollPosition = false;
/**
* Variable: timerAutoScroll
*
* Specifies if autoscrolling should be carried out via mxPanningManager even
* if the container has scrollbars. This disables <scrollPointToVisible> and
* uses <mxPanningManager> instead. If this is true then <autoExtend> is
* disabled. It 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>. To enable panning
* inside the container, near the edge, set <mxPanningManager.border> to a
* positive value. Default is false.
*/
mxGraph.prototype.allowAutoPanning = 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: keepEdgesInForeground
*
* Specifies if edges should appear in the foreground regardless of their order
* in the model. If <keepEdgesInForeground> and <keepEdgesInBackground> are
* both true then the normal order is applied. Default is false.
*/
mxGraph.prototype.keepEdgesInForeground = false;
/**
* Variable: keepEdgesInBackground
*
* Specifies if edges should appear in the background regardless of their order
* in the model. If <keepEdgesInForeground> and <keepEdgesInBackground> are
* both true then the normal order is applied. Default is false.
*/
mxGraph.prototype.keepEdgesInBackground = false;
/**
* Variable: allowNegativeCoordinates
*
* Specifies if negative coordinates for vertices are allowed. Default is true.
*/
mxGraph.prototype.allowNegativeCoordinates = true;
/**
* Variable: constrainChildren
*
* Specifies if a child should be constrained inside the parent bounds after a
* move or resize of the child. Default is true.
*/
mxGraph.prototype.constrainChildren = true;
/**
* Variable: constrainRelativeChildren
*
* Specifies if child cells with relative geometries should be constrained
* inside the parent bounds, if <constrainChildren> is true, and/or the
* <maximumGraphBounds>. Default is false.
*/
mxGraph.prototype.constrainRelativeChildren = false;
/**
* Variable: extendParents
*
* Specifies if a parent should contain the child bounds after a resize of
* the child. Default is true. This has precedence over <constrainChildren>.
*/
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: extendParentsOnAdd
*
* Specifies if parents should be extended according to the <extendParents>
* switch if cells are added. Default is false for backwards compatiblity.
*/
mxGraph.prototype.extendParentsOnMove = false;
/**
* Variable: recursiveResize
*
* Specifies the return value for <isRecursiveResize>. Default is
* false for backwards compatiblity.
*/
mxGraph.prototype.recursiveResize = false;
/**
* 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();
// Hides tooltips and resets tooltip timer if mouse leaves container
mxEvent.addListener(container, 'mouseleave', mxUtils.bind(this, function()
{
if (this.tooltipHandler != null)
{
this.tooltipHandler.hide();
}
}));
// 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(evt)
{
return this.isEditing() || (!this.isMouseDown && !mxEvent.isShiftDown(evt));
})
);
}
// Workaround for missing last shape and connect preview in IE8 standards
// mode if no initial graph displayed or no label for shape defined
if (document.documentMode == 8)
{
container.insertAdjacentHTML('beforeend', '<' + mxClient.VML_PREFIX + ':group' +
' style="DISPLAY: none;"></' + mxClient.VML_PREFIX + ':group>');
}
};
/**
* 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()
{
this.tooltipHandler = this.createTooltipHandler();
this.tooltipHandler.setEnabled(false);
this.selectionCellsHandler = this.createSelectionCellsHandler();
this.connectionHandler = this.createConnectionHandler();
this.connectionHandler.setEnabled(false);
this.graphHandler = this.createGraphHandler();
this.panningHandler = this.createPanningHandler();
this.panningHandler.panningEnabled = false;
this.popupMenuHandler = this.createPopupMenuHandler();
};
/**
* Function: createTooltipHandler
*
* Creates and returns a new <mxTooltipHandler> to be used in this graph.
*/
mxGraph.prototype.createTooltipHandler = function()
{
return new mxTooltipHandler(this);
};
/**
* Function: createSelectionCellsHandler
*
* Creates and returns a new <mxTooltipHandler> to be used in this graph.
*/
mxGraph.prototype.createSelectionCellsHandler = function()
{
return new mxSelectionCellsHandler(this);
};
/**
* Function: createConnectionHandler
*
* Creates and returns a new <mxConnectionHandler> to be used in this graph.
*/
mxGraph.prototype.createConnectionHandler = function()
{
return new mxConnectionHandler(this);
};
/**
* Function: createGraphHandler
*
* Creates and returns a new <mxGraphHandler> to be used in this graph.
*/
mxGraph.prototype.createGraphHandler = function()
{
return new mxGraphHandler(this);
};
/**
* Function: createPanningHandler
*
* Creates and returns a new <mxPanningHandler> to be used in this graph.
*/
mxGraph.prototype.createPanningHandler = function()
{
return new mxPanningHandler(this);
};
/**
* Function: createPopupMenuHandler
*
* Creates and returns a new <mxPopupMenuHandler> to be used in this graph.
*/
mxGraph.prototype.createPopupMenuHandler = function()
{
return new mxPopupMenuHandler(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.setDefaultParent(null);
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);
this.view.invalidate(change.child, true, true);
if (newParent == null || this.isCellCollapsed(newParent))
{
this.view.invalidate(change.child, true, true);
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 needs to be invalidated.
else if (change instanceof mxTerminalChange || change instanceof mxGeometryChange)
{
// Checks if the geometry has changed to avoid unnessecary revalidation
if (change instanceof mxTerminalChange || ((change.previous == null && change.geometry != null) ||
(change.previous != null && !change.previous.equals(change.geometry))))
{
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);
var state = this.view.getState(change.cell);
if (state != null)
{
state.style = null;
}
}
// 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.invalidate(cell, false, true);
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>. After editing was started, a <editingStarted> event is
* fired.
*
* 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 (evt == null || !mxEvent.isMultiTouchEvent(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);
this.fireEvent(new mxEventObject(mxEvent.EDITING_STARTED,
'cell', cell, 'event', 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 and fires a <editingStopped> event.
*
* Parameters:
*
* cancel - Boolean that specifies if the current editing value
* should be stored.
*/
mxGraph.prototype.stopEditing = function(cancel)
{
this.cellEditor.stopEditing(cancel);
this.fireEvent(new mxEventObject(mxEvent.EDITING_STOPPED, 'cancel', 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
{
var old = cell.value;
this.cellLabelChanged(cell, value, this.isAutoSizeCell(cell));
this.fireEvent(new mxEventObject(mxEvent.LABEL_CHANGED,
'cell', cell, 'value', value, 'old', old, '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.fireEvent(new mxEventObject(mxEvent.ESCAPE, 'event', evt));
};
/**
* 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.
*
* 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 (cell != null)
* {
* // Do something useful with cell and consume the event
* evt.consume();
* }
* });
* (end)
*
* 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)
{
if (this.isTransparentClickEvent(evt))
{
var active = false;
var tmp = this.getCellAt(me.graphX, me.graphY, null, null, null, mxUtils.bind(this, function(state)
{
var selected = this.isCellSelected(state.cell);
active = active || selected;
return !active || selected;
}));
if (tmp != null)
{
cell = tmp;
}
}
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 and consume the
* // event to prevent in-place editing from start
* });
* (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.isEditing(cell))
{
this.startEditingAtCell(cell, evt);
mxEvent.consume(evt);
}
};
/**
* Function: tapAndHold
*
* Handles the <mxMouseEvent> by highlighting the <mxCellState>.
*
* Parameters:
*
* me - <mxMouseEvent> that represents the touch event.
* state - Optional <mxCellState> that is associated with the event.
*/
mxGraph.prototype.tapAndHold = function(me)
{
var evt = me.getEvent();
var mxe = new mxEventObject(mxEvent.TAP_AND_HOLD, 'event', evt, 'cell', me.getCell());
// LATER: Check if event should be consumed if me is consumed
this.fireEvent(mxe);
if (mxe.isConsumed())
{
// Resets the state of the panning handler
this.panningHandler.panningTrigger = false;
}
// Handles the event if it has not been consumed
if (this.isEnabled() && !mxEvent.isConsumed(evt) && !mxe.isConsumed() && this.connectionHandler.isEnabled())
{
var state = this.view.getState(this.connectionHandler.marker.getCell(me));
if (state != null)
{
this.connectionHandler.marker.currentColor = this.connectionHandler.marker.validColor;
this.connectionHandler.marker.markedState = state;
this.connectionHandler.marker.mark();
this.connectionHandler.first = new mxPoint(me.getGraphX(), me.getGraphY());
this.connectionHandler.edgeState = this.connectionHandler.createEdgeState(me);
this.connectionHandler.previous = state;
this.connectionHandler.fireEvent(new mxEventObject(mxEvent.START, 'state', this.connectionHandler.previous));
}
}
};
/**
* 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.style.width = width + 'px';
}
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.style.height = height + 'px';
}
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.isActive())
{
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()
{
var css = mxUtils.getCurrentStyle(this.container);
return new mxRectangle(mxUtils.parseCssNumber(css.paddingLeft) +
((css.borderLeftStyle != 'none') ? mxUtils.parseCssNumber(css.borderLeftWidth) : 0),
mxUtils.parseCssNumber(css.paddingTop) +
((css.borderTopStyle != 'none') ? mxUtils.parseCssNumber(css.borderTopWidth) : 0),
mxUtils.parseCssNumber(css.paddingRight) +
((css.borderRightStyle != 'none') ? mxUtils.parseCssNumber(css.borderRightWidth) : 0),
mxUtils.parseCssNumber(css.paddingBottom) +
((css.borderBottomStyle != 'none') ? mxUtils.parseCssNumber(css.borderBottomWidth) : 0));
};
/**
* 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 = this.pageScale;
var page = new mxRectangle(0, 0, Math.ceil(fmt.width * ps), Math.ceil(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, vCount * page.height + 2 + tr.y);
};
/**
* 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)
*
* To fit and center the graph, the following code can be used.
*
* (code)
* var margin = 2;
* var max = 3;
*
* var bounds = graph.getGraphBounds();
* var cw = graph.container.clientWidth - margin;
* var ch = graph.container.clientHeight - margin;
* var w = bounds.width / graph.view.scale;
* var h = bounds.height / graph.view.scale;
* var s = Math.min(max, Math.min(cw / w, ch / h));
*
* graph.view.scaleAndTranslate(s,
* (margin + cw - w * s) / (2 * s) - bounds.x / graph.view.scale,
* (margin + ch - h * s) / (2 * s) - bounds.y / graph.view.scale);
* (end)
*
* Parameters:
*
* border - Optional number that specifies the border. Default is <border>.
* keepOrigin - Optional boolean that specifies if the translate should be
* changed. Default is false.
* margin - Optional margin in pixels. Default is 0.
* enabled - Optional boolean that specifies if the scale should be set or
* just returned. Default is true.
* ignoreWidth - Optional boolean that specifies if the width should be
* ignored. Default is false.
* ignoreHeight - Optional boolean that specifies if the height should be
* ignored. Default is false.
*/
mxGraph.prototype.fit = function(border, keepOrigin, margin, enabled, ignoreWidth, ignoreHeight)
{
if (this.container != null)
{
border = (border != null) ? border : this.getBorder();
keepOrigin = (keepOrigin != null) ? keepOrigin : false;
margin = (margin != null) ? margin : 0;
enabled = (enabled != null) ? enabled : true;
ignoreWidth = (ignoreWidth != null) ? ignoreWidth : false;
ignoreHeight = (ignoreHeight != null) ? ignoreHeight : false;
// Adds spacing and border from css
var cssBorder = this.getBorderSizes();
var w1 = this.container.offsetWidth - cssBorder.x - cssBorder.width - 1;
var h1 = this.container.offsetHeight - cssBorder.y - cssBorder.height - 1;
var bounds = this.view.getGraphBounds();
if (bounds.width > 0 && bounds.height > 0)
{
if (keepOrigin && bounds.x != null && bounds.y != null)
{
bounds = bounds.clone();
bounds.width += bounds.x;
bounds.height += bounds.y;
bounds.x = 0;
bounds.y = 0;
}
// LATER: Use unscaled bounding boxes to fix rounding errors
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) + margin;
w1 -= b;
h1 -= b;
var s2 = (((ignoreWidth) ? h1 / h2 : (ignoreHeight) ? w1 / w2 :
Math.min(w1 / w2, h1 / h2)));
if (this.minFitScale != null)
{
s2 = Math.max(s2, this.minFitScale);
}
if (this.maxFitScale != null)
{
s2 = Math.min(s2, this.maxFitScale);
}
if (enabled)
{
if (!keepOrigin)
{
if (!mxUtils.hasScrollbars(this.container))
{
var x0 = (bounds.x != null) ? Math.floor(this.view.translate.x - bounds.x / s + border / s2 + margin / 2) : border;
var y0 = (bounds.y != null) ? Math.floor(this.view.translate.y - bounds.y / s + border / s2 + margin / 2) : border;
this.view.scaleAndTranslate(s2, x0, y0);
}
else
{
this.view.setScale(s2);
var b2 = this.getGraphBounds();
if (b2.x != null)
{
this.container.scrollLeft = b2.x;
}
if (b2.y != null)
{
this.container.scrollTop = b2.y;
}
}
}
else if (this.view.scale != s2)
{
this.view.setScale(s2);
}
}
else
{
return s2;
}
}
}
return this.view.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 + border);
var height = Math.max(0, bounds.y + bounds.height + 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, Math.max(1, width), Math.max(1, height));
if (size != null)
{
width = size.width * this.view.scale;
height = size.height * this.view.scale;
}
}
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);
height = Math.ceil(height);
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';
root.style.width = '100%';
root.style.height = '100%';
}
else
{
if (mxClient.IS_QUIRKS)
{
// Quirks mode does not support minWidth/-Height
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, height);
}
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)
{
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: updatePageBreaks
*
* 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(0, 0, fmt.width * ps, fmt.height * ps);
var gb = mxRectangle.fromRectangle(this.getGraphBounds());
gb.width = Math.max(1, gb.width);
gb.height = Math.max(1, gb.height);
bounds.x = Math.floor((gb.x - tr.x * scale) / bounds.width) * bounds.width + tr.x * scale;
bounds.y = Math.floor((gb.y - tr.y * scale) / bounds.height) * bounds.height + tr.y * scale;
gb.width = Math.ceil((gb.width + (gb.x - bounds.x)) / bounds.width) * bounds.width;
gb.height = Math.ceil((gb.height + (gb.y - bounds.y)) / bounds.height) * bounds.height;
// Does not show page breaks if the scale is too small
visible = visible && Math.min(bounds.width, bounds.height) > this.minPageBreakDist;
var horizontalCount = (visible) ? Math.ceil(gb.height / bounds.height) + 1 : 0;
var verticalCount = (visible) ? Math.ceil(gb.width / bounds.width) + 1 : 0;
var right = (verticalCount - 1) * bounds.width;
var bottom = (horizontalCount - 1) * bounds.height;
if (this.horizontalPageBreaks == null && horizontalCount > 0)
{
this.horizontalPageBreaks = [];
}
if (this.verticalPageBreaks == null && verticalCount > 0)
{
this.verticalPageBreaks = [];
}
var drawPageBreaks = mxUtils.bind(this, function(breaks)
{
if (breaks != null)
{
var count = (breaks == this.horizontalPageBreaks) ? horizontalCount : verticalCount;
for (var i = 0; i <= count; i++)
{
var pts = (breaks == this.horizontalPageBreaks) ?
[new mxPoint(Math.round(bounds.x), Math.round(bounds.y + i * bounds.height)),
new mxPoint(Math.round(bounds.x + right), Math.round(bounds.y + i * bounds.height))] :
[new mxPoint(Math.round(bounds.x + i * bounds.width), Math.round(bounds.y)),
new mxPoint(Math.round(bounds.x + i * bounds.width), Math.round(bounds.y + bottom))];
if (breaks[i] != null)
{
breaks[i].points = pts;
breaks[i].redraw();
}
else
{
var pageBreak = new mxPolyline(pts, this.pageBreakColor);
pageBreak.dialect = this.dialect;
pageBreak.pointerEvents = false;
pageBreak.isDashed = this.pageBreakDashed;
pageBreak.init(this.view.backgroundPane);
pageBreak.redraw();
breaks[i] = pageBreak;
}
}
for (var i = count; i < breaks.length; i++)
{
breaks[i].destroy();
}
breaks.splice(count, breaks.length - count);
}
});
drawPageBreaks(this.horizontalPageBreaks);
drawPageBreaks(this.verticalPageBreaks);
};
/**
* 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/')
{
if (image.substring(0, 20) == 'data:image/svg+xml,<')
{
// Required for FF and IE11
image = image.substring(0, 19) + encodeURIComponent(image.substring(19));
}
else if (image.substring(0, 22) != 'data:image/svg+xml,%3C')
{
var comma = image.indexOf(',');
// Adds base64 encoding prefix if needed
if (comma > 0 && image.substring(comma - 7, comma + 1) != ';base64,')
{
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
* and returns the new value as 0 or 1. 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();
return this.toggleCellStyles(key, defaultValue, [cell]);
};
/**
* Function: toggleCellStyles
*
* Toggles the boolean value for the given key in the style of the given cells
* and returns the new value as 0 or 1. 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();
var value = null;
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)
{
value = (mxUtils.getValue(style, key, defaultValue)) ? 0 : 1;
this.setCellStyles(key, value, cells);
}
}
return value;
};
/**
* 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 state = this.view.getState(cells[i]);
if (state != null && !this.model.isEdge(cells[i]))
{
if (param == null)
{
if (align == mxConstants.ALIGN_CENTER)
{
param = state.x + state.width / 2;
break;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
param = state.x + state.width;
}
else if (align == mxConstants.ALIGN_TOP)
{
param = state.y;
}
else if (align == mxConstants.ALIGN_MIDDLE)
{
param = state.y + state.height / 2;
break;
}
else if (align == mxConstants.ALIGN_BOTTOM)
{
param = state.y + state.height;
}
else
{
param = state.x;
}
}
else
{
if (align == mxConstants.ALIGN_RIGHT)
{
param = Math.max(param, state.x + state.width);
}
else if (align == mxConstants.ALIGN_TOP)
{
param = Math.min(param, state.y);
}
else if (align == mxConstants.ALIGN_BOTTOM)
{
param = Math.max(param, state.y + state.height);
}
else
{
param = Math.min(param, state.x);
}
}
}
}
}
// Aligns the cells to the coordinate
if (param != null)
{
var s = this.view.scale;
this.model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
var state = this.view.getState(cells[i]);
if (state != null)
{
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 - state.x - state.width / 2) / s;
}
else if (align == mxConstants.ALIGN_RIGHT)
{
geo.x += (param - state.x - state.width) / s;
}
else if (align == mxConstants.ALIGN_TOP)
{
geo.y += (param - state.y) / s;
}
else if (align == mxConstants.ALIGN_MIDDLE)
{
geo.y += (param - state.y - state.height / 2) / s;
}
else if (align == mxConstants.ALIGN_BOTTOM)
{
geo.y += (param - state.y - state.height) / s;
}
else
{
geo.x += (param - state.x) / s;
}
this.resizeCell(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 group into the parent
var index = this.model.getChildCount(parent);
this.cellsAdded([group], parent, index, null, null, false, false, false);
// Adds the children into the group and moves
index = this.model.getChildCount(group);
this.cellsAdded(cells, group, index, null, null, false, false, false);
this.cellsMoved(cells, -bounds.x, -bounds.y, false, false, false);
// Resizes the group
this.cellsResized([group], [bounds], false);
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, true);
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
if (border != null)
{
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.removeCellsAfterUngroup(cells);
this.fireEvent(new mxEventObject(mxEvent.UNGROUP_CELLS, 'cells', cells));
}
finally
{
this.model.endUpdate();
}
}
return result;
};
/**
* Function: removeCellsAfterUngroup
*
* Hook to remove the groups after <ungroupCells>.
*
* Parameters:
*
* cells - Array of <mxCells> that were ungrouped.
*/
mxGraph.prototype.removeCellsAfterUngroup = function(cells)
{
this.cellsRemoved(this.addAllEdges(cells));
};
/**
* 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 groups to include all children and returns
* the passed-in cells. Call this with the groups in parent to child order,
* top-most group first, the cells are processed in reverse order and cells
* with no children are ignored.
*
* Parameters:
*
* cells - The groups whose bounds should be updated. If this is null, then
* the selection cells are used.
* 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.
* topBorder - Optional top border to be added in the group. Default is 0.
* rightBorder - Optional top border to be added in the group. Default is 0.
* bottomBorder - Optional top border to be added in the group. Default is 0.
* leftBorder - Optional top border to be added in the group. Default is 0.
*/
mxGraph.prototype.updateGroupBounds = function(cells, border, moveGroup, topBorder, rightBorder, bottomBorder, leftBorder)
{
if (cells == null)
{
cells = this.getSelectionCells();
}
border = (border != null) ? border : 0;
moveGroup = (moveGroup != null) ? moveGroup : false;
topBorder = (topBorder != null) ? topBorder : 0;
rightBorder = (rightBorder != null) ? rightBorder : 0;
bottomBorder = (bottomBorder != null) ? bottomBorder : 0;
leftBorder = (leftBorder != null) ? leftBorder : 0;
this.model.beginUpdate();
try
{
for (var i = cells.length - 1; i >= 0; i--)
{
var geo = this.getCellGeometry(cells[i]);
if (geo != null)
{
var children = this.getChildCells(cells[i]);
if (children != null && children.length > 0)
{
var bounds = this.getBoundingBoxFromGeometry(children, true);
if (bounds != null && bounds.width > 0 && bounds.height > 0)
{
var left = 0;
var top = 0;
// Adds the size of the title area for swimlanes
if (this.isSwimlane(cells[i]))
{
var size = this.getStartSize(cells[i]);
left = size.width;
top = size.height;
}
geo = geo.clone();
if (moveGroup)
{
geo.x = Math.round(geo.x + bounds.x - border - left - leftBorder);
geo.y = Math.round(geo.y + bounds.y - border - top - topBorder);
}
geo.width = Math.round(bounds.width + 2 * border + left + leftBorder + rightBorder);
geo.height = Math.round(bounds.height + 2 * border + top + topBorder + bottomBorder);
this.model.setGeometry(cells[i], geo);
this.moveCells(children, border + left - bounds.x + leftBorder,
border + top - bounds.y + topBorder);
}
}
}
}
}
finally
{
this.model.endUpdate();
}
return cells;
};
/**
* Function: getBoundingBox
*
* Returns the bounding box for the given array of <mxCells>. The bounding box for
* each cell and its descendants is computed using <mxGraphView.getBoundingBox>.
*
* Parameters:
*
* cells - Array of <mxCells> whose bounding box should be returned.
*/
mxGraph.prototype.getBoundingBox = function(cells)
{
var result = null;
if (cells != null && cells.length > 0)
{
for (var i = 0; i < cells.length; i++)
{
if (this.model.isVertex(cells[i]) || this.model.isEdge(cells[i]))
{
var bbox = this.view.getBoundingBox(this.view.getState(cells[i]), true);
if (bbox != null)
{
if (result == null)
{
result = mxRectangle.fromRectangle(bbox);
}
else
{
result.add(bbox);
}
}
}
}
}
return result;
};
/**
* Group: Cell cloning, insertion and removal
*/
/**
* Function: cloneCells
*
* Returns the clones for the given cells. The clones are created recursively
* using <mxGraphModel.cloneCells>. 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.
* mapping - Optional mapping for existing clones.
*/
mxGraph.prototype.cloneCells = function(cells, allowInvalidEdges, mapping)
{
allowInvalidEdges = (allowInvalidEdges != null) ? allowInvalidEdges : true;
var clones = null;
if (cells != null)
{
// Creates a dictionary for fast lookups
var dict = new mxDictionary();
var tmp = [];
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
tmp.push(cells[i]);
}
if (tmp.length > 0)
{
var scale = this.view.scale;
var trans = this.view.translate;
clones = this.model.cloneCells(cells, true, mapping);
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);
while (src != null && !dict.get(src))
{
src = this.model.getParent(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);
while (trg != null && !dict.get(trg))
{
trg = this.model.getParent(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.translate(dx, 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, extend)
{
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 + i > this.model.getChildCount(parent))
{
index--;
}
this.model.add(parent, cells[i], index + i);
if (this.autoSizeCellsOnAdd)
{
this.autoSizeCell(cells[i], true);
}
// Extends the parent or constrains the child
if ((extend == null || extend) &&
this.isExtendParentsOnAdd(cells[i]) && this.isExtendParent(cells[i]))
{
this.extendParent(cells[i]);
}
// Additionally constrains the child after extending the parent
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: autoSizeCell
*
* Resizes the specified cell to just fit around the its label and/or children
*
* Parameters:
*
* cell - <mxCells> to be resized.
* recurse - Optional boolean which specifies if all descendants should be
* autosized. Default is true.
*/
mxGraph.prototype.autoSizeCell = function(cell, recurse)
{
recurse = (recurse != null) ? recurse : true;
if (recurse)
{
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.autoSizeCell(this.model.getChildAt(cell, i));
}
}
if (this.getModel().isVertex(cell) && this.isAutoSizeCell(cell))
{
this.updateCellSize(cell);
}
};
/**
* 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)
{
// FIXME: Remove duplicate cells in result or do not add if
// in cells or descendant of cells
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 dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
for (var i = 0; i < cells.length; i++)
{
// Disconnects edges which are not in cells
var edges = this.getAllEdges([cells[i]]);
var disconnectTerminal = mxUtils.bind(this, function(edge, source)
{
var geo = this.model.getGeometry(edge);
if (geo != null)
{
var state = this.view.getState(edge);
if (state != null)
{
// Checks which side of the edge is being disconnected
var tmp = state.getVisibleTerminal(source);
var connected = false;
while (tmp != null)
{
if (cells[i] == tmp)
{
connected = true;
break;
}
tmp = this.model.getParent(tmp);
}
if (connected)
{
var dx = tr.x;
var dy = tr.y;
var parentState = this.view.getState(this.model.getParent(edge));
if (parentState != null && this.model.isVertex(parentState.cell))
{
dx = parentState.x / scale;
dy = parentState.y / scale;
}
geo = geo.clone();
var pts = state.absolutePoints;
var n = (source) ? 0 : pts.length - 1;
geo.setTerminalPoint(new mxPoint(pts[n].x / scale - dx, pts[n].y / scale - dy), source);
this.model.setTerminal(edges[j], null, source);
this.model.setGeometry(edges[j], geo);
}
}
}
});
for (var j = 0; j < edges.length; j++)
{
if (!dict.get(edges[j]))
{
disconnectTerminal(edges[j], true);
disconnectTerminal(edges[j], false);
}
}
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;
var parent = this.model.getParent(edge);
var source = this.model.getTerminal(edge, true);
this.model.beginUpdate();
try
{
if (newEdge == null)
{
newEdge = this.cloneCells([edge])[0];
// Removes waypoints before/after new cell
var state = this.view.getState(edge);
var geo = this.getCellGeometry(newEdge);
if (geo != null && geo.points != null && state != null)
{
var t = this.view.translate;
var s = this.view.scale;
var idx = mxUtils.findNearestSegment(state, (dx + t.x) * s, (dy + t.y) * s);
geo.points = geo.points.slice(0, idx);
geo = this.getCellGeometry(edge);
if (geo != null && geo.points != null)
{
geo = geo.clone();
geo.points = geo.points.slice(idx);
this.model.setGeometry(edge, geo);
}
}
}
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.
* evt - Optional native event that triggered the invocation.
*/
mxGraph.prototype.foldCells = function(collapse, recurse, cells, checkFoldable, evt)
{
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.constrainChild(cells[i]);
}
}
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)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
if (geo.alternateBounds == null)
{
var bounds = geo;
if (this.collapseToPreferredSize)
{
var tmp = this.getPreferredSizeForCell(cell);
if (tmp != null)
{
bounds = tmp;
var startSize = mxUtils.getValue(style, mxConstants.STYLE_STARTSIZE);
if (startSize > 0)
{
bounds.height = Math.max(bounds.height, startSize);
}
}
}
geo.alternateBounds = new mxRectangle(0, 0, bounds.width, bounds.height);
}
if (geo.alternateBounds != null)
{
geo.alternateBounds.x = geo.x;
geo.alternateBounds.y = geo.y;
var alpha = mxUtils.toRadians(style[mxConstants.STYLE_ROTATION] || 0);
if (alpha != 0)
{
var dx = geo.alternateBounds.getCenterX() - geo.getCenterX();
var dy = geo.alternateBounds.getCenterY() - geo.getCenterY();
var cos = Math.cos(alpha);
var sin = Math.sin(alpha);
var dx2 = cos * dx - sin * dy;
var dy2 = sin * dx + cos * dy;
geo.alternateBounds.x += dx2 - dx;
geo.alternateBounds.y += dy2 - dy;
}
}
}
};
/**
* 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();
return mxUtils.removeDuplicates(allCells.concat(this.getAllEdges(cells)));
};
/**
* 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], false);
}
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: getPreferredSizeForCell
*
* Returns the preferred width and height of the given <mxCell> as an
* <mxRectangle>. To implement a minimum width, add a new style eg.
* minWidth in the vertex and override this method as follows.
*
* (code)
* var graphGetPreferredSizeForCell = graph.getPreferredSizeForCell;
* graph.getPreferredSizeForCell = function(cell)
* {
* var result = graphGetPreferredSizeForCell.apply(this, arguments);
* var style = this.getCellStyle(cell);
*
* if (style['minWidth'] > 0)
* {
* result.width = Math.max(style['minWidth'], result.width);
* }
*
* return result;
* };
* (end)
*
* 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) || this.view.createState(cell);
var style = state.style;
if (!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.cellRenderer.getLabelValue(state);
if (value != null && value.length > 0)
{
if (!this.isHtmlLabel(state.cell))
{
value = mxUtils.htmlEntities(value);
}
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: 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, recurse)
{
return this.resizeCells([cell], [bounds], recurse)[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, recurse)
{
recurse = (recurse != null) ? recurse : this.isRecursiveResize();
this.model.beginUpdate();
try
{
this.cellsResized(cells, bounds, recurse);
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.
*
* The following example shows how to control group resizes to make sure
* that all child cells stay within the group.
*
* (code)
* graph.addListener(mxEvent.CELLS_RESIZED, function(sender, evt)
* {
* var cells = evt.getProperty('cells');
*
* if (cells != null)
* {
* for (var i = 0; i < cells.length; i++)
* {
* if (graph.getModel().getChildCount(cells[i]) > 0)
* {
* var geo = graph.getCellGeometry(cells[i]);
*
* if (geo != null)
* {
* var children = graph.getChildCells(cells[i], true, true);
* var bounds = graph.getBoundingBoxFromGeometry(children, true);
*
* geo = geo.clone();
* geo.width = Math.max(geo.width, bounds.width);
* geo.height = Math.max(geo.height, bounds.height);
*
* graph.getModel().setGeometry(cells[i], geo);
* }
* }
* }
* }
* });
* (end)
*
* Parameters:
*
* cells - Array of <mxCells> whose bounds should be changed.
* bounds - Array of <mxRectangles> that represent the new bounds.
* recurse - Optional boolean that specifies if the children should be resized.
*/
mxGraph.prototype.cellsResized = function(cells, bounds, recurse)
{
recurse = (recurse != null) ? recurse : false;
if (cells != null && bounds != null && cells.length == bounds.length)
{
this.model.beginUpdate();
try
{
for (var i = 0; i < cells.length; i++)
{
this.cellResized(cells[i], bounds[i], false, recurse);
if (this.isExtendParent(cells[i]))
{
this.extendParent(cells[i]);
}
this.constrainChild(cells[i]);
}
if (this.resetEdgesOnResize)
{
this.resetEdges(cells);
}
this.fireEvent(new mxEventObject(mxEvent.CELLS_RESIZED,
'cells', cells, 'bounds', bounds));
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: cellResized
*
* Resizes the parents recursively so that they contain the complete area
* of the resized child cell.
*
* Parameters:
*
* cell - <mxCell> whose bounds should be changed.
* bounds - <mxRectangles> that represent the new bounds.
* ignoreRelative - Boolean that indicates if relative cells should be ignored.
* recurse - Optional boolean that specifies if the children should be resized.
*/
mxGraph.prototype.cellResized = function(cell, bounds, ignoreRelative, recurse)
{
var geo = this.model.getGeometry(cell);
if (geo != null && (geo.x != bounds.x || geo.y != bounds.y ||
geo.width != bounds.width || geo.height != bounds.height))
{
geo = geo.clone();
if (!ignoreRelative && geo.relative)
{
var offset = geo.offset;
if (offset != null)
{
offset.x += bounds.x - geo.x;
offset.y += bounds.y - geo.y;
}
}
else
{
geo.x = bounds.x;
geo.y = bounds.y;
}
geo.width = bounds.width;
geo.height = bounds.height;
if (!geo.relative && this.model.isVertex(cell) && !this.isAllowNegativeCoordinates())
{
geo.x = Math.max(0, geo.x);
geo.y = Math.max(0, geo.y);
}
this.model.beginUpdate();
try
{
if (recurse)
{
this.resizeChildCells(cell, geo);
}
this.model.setGeometry(cell, geo);
this.constrainChildCells(cell);
}
finally
{
this.model.endUpdate();
}
}
};
/**
* Function: resizeChildCells
*
* Resizes the child cells of the given cell for the given new geometry with
* respect to the current geometry of the cell.
*
* Parameters:
*
* cell - <mxCell> that has been resized.
* newGeo - <mxGeometry> that represents the new bounds.
*/
mxGraph.prototype.resizeChildCells = function(cell, newGeo)
{
var geo = this.model.getGeometry(cell);
var dx = newGeo.width / geo.width;
var dy = newGeo.height / geo.height;
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.scaleCell(this.model.getChildAt(cell, i), dx, dy, true);
}
};
/**
* Function: constrainChildCells
*
* Constrains the children of the given cell using <constrainChild>.
*
* Parameters:
*
* cell - <mxCell> that has been resized.
*/
mxGraph.prototype.constrainChildCells = function(cell)
{
var childCount = this.model.getChildCount(cell);
for (var i = 0; i < childCount; i++)
{
this.constrainChild(this.model.getChildAt(cell, i));
}
};
/**
* Function: scaleCell
*
* Scales the points, position and size of the given cell according to the
* given vertical and horizontal scaling factors.
*
* Parameters:
*
* cell - <mxCell> whose geometry should be scaled.
* dx - Horizontal scaling factor.
* dy - Vertical scaling factor.
* recurse - Boolean indicating if the child cells should be scaled.
*/
mxGraph.prototype.scaleCell = function(cell, dx, dy, recurse)
{
var geo = this.model.getGeometry(cell);
if (geo != null)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
geo = geo.clone();
// Stores values for restoring based on style
var x = geo.x;
var y = geo.y
var w = geo.width;
var h = geo.height;
geo.scale(dx, dy, style[mxConstants.STYLE_ASPECT] == 'fixed');
if (style[mxConstants.STYLE_RESIZE_WIDTH] == '1')
{
geo.width = w * dx;
}
else if (style[mxConstants.STYLE_RESIZE_WIDTH] == '0')
{
geo.width = w;
}
if (style[mxConstants.STYLE_RESIZE_HEIGHT] == '1')
{
geo.height = h * dy;
}
else if (style[mxConstants.STYLE_RESIZE_HEIGHT] == '0')
{
geo.height = h;
}
if (!this.isCellMovable(cell))
{
geo.x = x;
geo.y = y;
}
if (!this.isCellResizable(cell))
{
geo.width = w;
geo.height = h;
}
if (this.model.isVertex(cell))
{
this.cellResized(cell, geo, true, recurse);
}
else
{
this.model.setGeometry(cell, geo);
}
}
};
/**
* 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.getCellGeometry(parent);
if (parent != null && p != null && !this.isCellCollapsed(parent))
{
var geo = this.getCellGeometry(cell);
if (geo != null && !geo.relative &&
(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], false);
}
}
}
};
/**
* 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.
*
* Parameters:
*
* cells - Array of <mxCells> to be imported.
* 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.
* target - <mxCell> that represents the new parent of the cells.
* evt - Mouseevent that triggered the invocation.
* mapping - Optional mapping for existing clones.
*/
mxGraph.prototype.importCells = function(cells, dx, dy, target, evt, mapping)
{
return this.moveCells(cells, dx, dy, true, target, evt, mapping);
};
/**
* 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.
* mapping - Optional mapping for existing clones.
*/
mxGraph.prototype.moveCells = function(cells, dx, dy, clone, target, evt, mapping)
{
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))
{
// Removes descandants with ancestors in cells to avoid multiple moving
cells = this.model.getTopmostCells(cells);
this.model.beginUpdate();
try
{
// Faster cell lookups to remove relative edge labels with selected
// terminals to avoid explicit and implicit move at same time
var dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
var isSelected = mxUtils.bind(this, function(cell)
{
while (cell != null)
{
if (dict.get(cell))
{
return true;
}
cell = this.model.getParent(cell);
}
return false;
});
// Removes relative edge labels with selected terminals
var checked = [];
for (var i = 0; i < cells.length; i++)
{
var geo = this.getCellGeometry(cells[i]);
var parent = this.model.getParent(cells[i]);
if ((geo == null || !geo.relative) || !this.model.isEdge(parent) ||
(!isSelected(this.model.getTerminal(parent, true)) &&
!isSelected(this.model.getTerminal(parent, false))))
{
checked.push(cells[i]);
}
}
cells = checked;
if (clone)
{
cells = this.cloneCells(cells, this.isCloneInvalidEdges(), mapping);
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.isExtendParentsOnMove() && 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, extend)
{
if (cells != null && (dx != 0 || dy != 0))
{
extend = (extend != null) ? extend : false;
this.model.beginUpdate();
try
{
if (disconnect)
{
this.disconnectGraph(cells);
}
for (var i = 0; i < cells.length; i++)
{
this.translateCell(cells[i], dx, dy);
if (extend && this.isExtendParent(cells[i]))
{
this.extendParent(cells[i]);
}
else if (constrain)
{
this.constrainChild(cells[i]);
}
}
if (this.resetEdgesOnMove)
{
this.resetEdges(cells);
}
this.fireEvent(new mxEventObject(mxEvent.CELLS_MOVED,
'cells', cells, 'dx', dx, '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)
{
dx = parseFloat(dx);
dy = parseFloat(dy);
geo = geo.clone();
geo.translate(dx, dy);
if (!geo.relative && this.model.isVertex(cell) && !this.isAllowNegativeCoordinates())
{
geo.x = Math.max(0, parseFloat(geo.x));
geo.y = Math.max(0, parseFloat(geo.y));
}
if (geo.relative && !this.model.isEdge(cell))
{
var parent = this.model.getParent(cell);
var angle = 0;
if (this.model.isVertex(parent))
{
var state = this.view.getState(parent);
var style = (state != null) ? state.style : this.getCellStyle(parent);
angle = mxUtils.getValue(style, mxConstants.STYLE_ROTATION, 0);
}
if (angle != 0)
{
var rad = mxUtils.toRadians(-angle);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var pt = mxUtils.getRotatedPoint(new mxPoint(dx, dy), cos, sin, new mxPoint(0, 0));
dx = pt.x;
dy = pt.y;
}
if (geo.offset == null)
{
geo.offset = new mxPoint(dx, dy);
}
else
{
geo.offset.x = parseFloat(geo.offset.x) + dx;
geo.offset.y = parseFloat(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 != 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);
var state = this.view.getState(parent);
var style = (state != null) ? state.style : this.getCellStyle(parent);
var dir = mxUtils.getValue(style, mxConstants.STYLE_DIRECTION, mxConstants.DIRECTION_EAST);
var flipH = mxUtils.getValue(style, mxConstants.STYLE_FLIPH, 0) == 1;
var flipV = mxUtils.getValue(style, mxConstants.STYLE_FLIPV, 0) == 1;
if (dir == mxConstants.DIRECTION_SOUTH || dir == mxConstants.DIRECTION_NORTH)
{
var tmp = size.width;
size.width = size.height;
size.height = tmp;
}
if ((dir == mxConstants.DIRECTION_EAST && !flipV) || (dir == mxConstants.DIRECTION_NORTH && !flipH) ||
(dir == mxConstants.DIRECTION_WEST && flipV) || (dir == mxConstants.DIRECTION_SOUTH && flipH))
{
x = size.width;
y = size.height;
}
w -= size.width;
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.
* sizeFirst - Specifies if the size should be changed first. Default is true.
*/
mxGraph.prototype.constrainChild = function(cell, sizeFirst)
{
sizeFirst = (sizeFirst != null) ? sizeFirst : true;
if (cell != null)
{
var geo = this.getCellGeometry(cell);
if (geo != null && (this.isConstrainRelativeChildren() || !geo.relative))
{
var parent = this.model.getParent(cell);
var pgeo = this.getCellGeometry(parent);
var max = this.getMaximumGraphBounds();
// Finds parent offset
if (max != null)
{
var off = this.getBoundingBoxFromGeometry([parent], false);
if (off != null)
{
max = mxRectangle.fromRectangle(max);
max.x -= off.x;
max.y -= off.y;
}
}
if (this.isConstrainChild(cell))
{
var tmp = this.getCellContainmentArea(cell);
if (tmp != null)
{
var overlap = this.getOverlap(cell);
if (overlap > 0)
{
tmp = mxRectangle.fromRectangle(tmp);
tmp.x -= tmp.width * overlap;
tmp.y -= tmp.height * overlap;
tmp.width += 2 * tmp.width * overlap;
tmp.height += 2 * tmp.height * overlap;
}
// Find the intersection between max and tmp
if (max == null)
{
max = tmp;
}
else
{
max = mxRectangle.fromRectangle(max);
max.intersect(tmp);
}
}
}
if (max != null)
{
var cells = [cell];
if (!this.isCellCollapsed(cell))
{
var desc = this.model.getDescendants(cell);
for (var i = 0; i < desc.length; i++)
{
if (this.isCellVisible(desc[i]))
{
cells.push(desc[i]);
}
}
}
var bbox = this.getBoundingBoxFromGeometry(cells, false);
if (bbox != null)
{
geo = geo.clone();
// Cumulative horizontal movement
var dx = 0;
if (geo.width > max.width)
{
dx = geo.width - max.width;
geo.width -= dx;
}
if (bbox.x + bbox.width > max.x + max.width)
{
dx -= bbox.x + bbox.width - max.x - max.width - dx;
}
// Cumulative vertical movement
var dy = 0;
if (geo.height > max.height)
{
dy = geo.height - max.height;
geo.height -= dy;
}
if (bbox.y + bbox.height > max.y + max.height)
{
dy -= bbox.y + bbox.height - max.y - max.height - dy;
}
if (bbox.x < max.x)
{
dx -= bbox.x - max.x;
}
if (bbox.y < max.y)
{
dy -= bbox.y - max.y;
}
if (dx != 0 || dy != 0)
{
if (geo.relative)
{
// Relative geometries are moved via absolute offset
if (geo.offset == null)
{
geo.offset = new mxPoint();
}
geo.offset.x += dx;
geo.offset.y += dy;
}
else
{
geo.x += dx;
geo.y += dy;
}
}
this.model.setGeometry(cell, geo);
}
}
}
}
};
/**
* 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 faster cells lookup
var dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
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);
// Checks if one of the terminals is not in the given array
if (!dict.get(source) || !dict.get(target))
{
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: getOutlineConstraint
*
* Returns the constraint used to connect to the outline of the given state.
*/
mxGraph.prototype.getOutlineConstraint = function(point, terminalState, me)
{
if (terminalState.shape != null)
{
var bounds = this.view.getPerimeterBounds(terminalState);
var direction = terminalState.style[mxConstants.STYLE_DIRECTION];
if (direction == mxConstants.DIRECTION_NORTH || direction == mxConstants.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;
}
var alpha = mxUtils.toRadians(terminalState.shape.getShapeRotation());
if (alpha != 0)
{
var cos = Math.cos(-alpha);
var sin = Math.sin(-alpha);
var ct = new mxPoint(bounds.getCenterX(), bounds.getCenterY());
point = mxUtils.getRotatedPoint(point, cos, sin, ct);
}
var sx = 1;
var sy = 1;
var dx = 0;
var dy = 0;
// LATER: Add flipping support for image shapes
if (this.getModel().isVertex(terminalState.cell))
{
var flipH = terminalState.style[mxConstants.STYLE_FLIPH];
var flipV = terminalState.style[mxConstants.STYLE_FLIPV];
// Legacy support for stencilFlipH/V
if (terminalState.shape != null && terminalState.shape.stencil != null)
{
flipH = mxUtils.getValue(terminalState.style, 'stencilFlipH', 0) == 1 || flipH;
flipV = mxUtils.getValue(terminalState.style, 'stencilFlipV', 0) == 1 || flipV;
}
if (direction == mxConstants.DIRECTION_NORTH || direction == mxConstants.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((point.x - bounds.x) * sx - dx + bounds.x, (point.y - bounds.y) * sy - dy + bounds.y);
var x = Math.round((point.x - bounds.x) * 1000 / bounds.width) / 1000;
var y = Math.round((point.y - bounds.y) * 1000 / bounds.height) / 1000;
return new mxConnectionConstraint(new mxPoint(x, y), false);
}
return null;
};
/**
* 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.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 && constraint.point != 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 == mxConstants.DIRECTION_NORTH)
{
r1 += 270;
}
else if (direction == mxConstants.DIRECTION_WEST)
{
r1 += 180;
}
else if (direction == mxConstants.DIRECTION_SOUTH)
{
r1 += 90;
}
// Bounds need to be rotated by 90 degrees for further computation
if (direction == mxConstants.DIRECTION_NORTH || direction == mxConstants.DIRECTION_SOUTH)
{
bounds.rotate90();
}
}
point = new mxPoint(bounds.x + constraint.point.x * bounds.width,
bounds.y + constraint.point.y * bounds.height);
// Rotation for direction before projection on perimeter
var r2 = vertex.style[mxConstants.STYLE_ROTATION] || 0;
if (constraint.perimeter)
{
if (r1 != 0)
{
// 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 (r1 == 270)
{
sin = -1;
}
point = mxUtils.getRotatedPoint(point, cos, sin, cx);
}
point = this.view.getPerimeterPoint(vertex, point, false);
}
else
{
r2 += r1;
if (this.getModel().isVertex(vertex.cell))
{
var flipH = vertex.style[mxConstants.STYLE_FLIPH] == 1;
var flipV = vertex.style[mxConstants.STYLE_FLIPV] == 1;
// Legacy support for stencilFlipH/V
if (vertex.shape != null && vertex.shape.stencil != null)
{
flipH = (mxUtils.getValue(vertex.style, 'stencilFlipH', 0) == 1) || flipH;
flipV = (mxUtils.getValue(vertex.style, 'stencilFlipV', 0) == 1) || flipV;
}
if (flipH)
{
point.x = 2 * bounds.getCenterX() - point.x;
}
if (flipV)
{
point.y = 2 * bounds.getCenterY() - point.y;
}
}
}
// 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);
}
}
if (point != null)
{
point.x = Math.round(point.x);
point.y = Math.round(point.y);
}
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;
// Fast lookup for finding cells in array
var dict = new mxDictionary();
for (var i = 0; i < cells.length; i++)
{
dict.put(cells[i], true);
}
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))
{
while (src != null && !dict.get(src))
{
src = this.model.getParent(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))
{
while (trg != null && !dict.get(trg))
{
trg = this.model.getParent(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 in 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 bbox = null;
if (this.model.isEdge(cells[i]))
{
var addPoint = function(pt)
{
if (pt != null)
{
if (tmp == null)
{
tmp = new mxRectangle(pt.x, pt.y, 0, 0);
}
else
{
tmp.add(new mxRectangle(pt.x, pt.y, 0, 0));
}
}
};
if (this.model.getTerminal(cells[i], true) == null)
{
addPoint(geo.getTerminalPoint(true));
}
if (this.model.getTerminal(cells[i], false) == null)
{
addPoint(geo.getTerminalPoint(false));
}
var pts = geo.points;
if (pts != null && pts.length > 0)
{
var tmp = new mxRectangle(pts[0].x, pts[0].y, 0, 0);
for (var j = 1; j < pts.length; j++)
{
addPoint(pts[j]);
}
}
bbox = tmp;
}
else
{
var parent = this.model.getParent(cells[i]);
if (geo.relative)
{
if (this.model.isVertex(parent) && parent != this.view.currentRoot)
{
var tmp = this.getBoundingBoxFromGeometry([parent], false);
if (tmp != null)
{
bbox = new mxRectangle(geo.x * tmp.width, geo.y * tmp.height, geo.width, geo.height);
if (mxUtils.indexOf(cells, parent) >= 0)
{
bbox.x += tmp.x;
bbox.y += tmp.y;
}
}
}
}
else
{
bbox = mxRectangle.fromRectangle(geo);
if (this.model.isVertex(parent) && mxUtils.indexOf(cells, parent) >= 0)
{
var tmp = this.getBoundingBoxFromGeometry([parent], false);
if (tmp != null)
{
bbox.x += tmp.x;
bbox.y += tmp.y;
}
}
}
if (bbox != null && geo.offset != null)
{
bbox.x += geo.offset.x;
bbox.y += geo.offset.y;
}
}
if (bbox != null)
{
if (result == null)
{
result = mxRectangle.fromRectangle(bbox);
}
else
{
result.add(bbox);
}
}
}
}
}
}
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;
}
if (this.shiftPreview1.parentNode != null)
{
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;
}
if (this.shiftPreview2.parentNode != null)
{
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;
}
// Inserts elements only if not empty
if (this.shiftPreview1.firstChild != null)
{
this.container.insertBefore(this.shiftPreview1, canvas.parentNode);
}
if (this.shiftPreview2.firstChild != null)
{
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: center
*
* Centers the graph in the container.
*
* Parameters:
*
* horizontal - Optional boolean that specifies if the graph should be centered
* horizontally. Default is true.
* vertical - Optional boolean that specifies if the graph should be centered
* vertically. Default is true.
* cx - Optional float that specifies the horizontal center. Default is 0.5.
* cy - Optional float that specifies the vertical center. Default is 0.5.
*/
mxGraph.prototype.center = function(horizontal, vertical, cx, cy)
{
horizontal = (horizontal != null) ? horizontal : true;
vertical = (vertical != null) ? vertical : true;
cx = (cx != null) ? cx : 0.5;
cy = (cy != null) ? cy : 0.5;
var hasScrollbars = mxUtils.hasScrollbars(this.container);
var cw = this.container.clientWidth;
var ch = this.container.clientHeight;
var bounds = this.getGraphBounds();
var t = this.view.translate;
var s = this.view.scale;
var dx = (horizontal) ? cw - bounds.width : 0;
var dy = (vertical) ? ch - bounds.height : 0;
if (!hasScrollbars)
{
this.view.setTranslate((horizontal) ? Math.floor(t.x - bounds.x * s + dx * cx / s) : t.x,
(vertical) ? Math.floor(t.y - bounds.y * s + dy * cy / s) : t.y);
}
else
{
bounds.x -= t.x;
bounds.y -= t.y;
var sw = this.container.scrollWidth;
var sh = this.container.scrollHeight;
if (sw > cw)
{
dx = 0;
}
if (sh > ch)
{
dy = 0;
}
this.view.setTranslate(Math.floor(dx / 2 - bounds.x), Math.floor(dy / 2 - bounds.y));
this.container.scrollLeft = (sw - cw) / 2;
this.container.scrollTop = (sh - ch) / 2;
}
};
/**
* 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 = Math.round(this.view.scale * factor * 100) / 100;
var state = this.view.getState(this.getSelectionCell());
factor = scale / this.view.scale;
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
{
var hasScrollbars = mxUtils.hasScrollbars(this.container);
if (center && !hasScrollbars)
{
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
{
// Allows for changes of translate and scrollbars during setscale
var tx = this.view.translate.x;
var ty = this.view.translate.y;
var sl = this.container.scrollLeft;
var st = this.container.scrollTop;
this.view.setScale(scale);
if (hasScrollbars)
{
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 = (this.view.translate.x - tx) * this.view.scale + Math.round(sl * factor + dx);
this.container.scrollTop = (this.view.translate.y - ty) * this.view.scale + Math.round(st * 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;
var newScale = this.view.scale * scale;
if (!mxUtils.hasScrollbars(this.container))
{
this.view.scaleAndTranslate(newScale, (this.view.translate.x - rect.x / this.view.scale), (this.view.translate.y - rect.y / this.view.scale));
}
else
{
this.view.setScale(newScale);
this.container.scrollLeft = Math.round(rect.x * scale);
this.container.scrollTop = Math.round(rect.y * 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;
}
var tr = new mxPoint(this.view.translate.x, this.view.translate.y);
if (this.scrollRectToVisible(bounds))
{
// Triggers an update via the view's event source
var tr2 = new mxPoint(this.view.translate.x, this.view.translate.y);
this.view.translate.x = tr.x;
this.view.translate.y = tr.y;
this.view.setTranslate(tr2.x, tr2.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: isTransparentClickEvent
*
* Hook for implementing click-through behaviour on selected cells. If this
* returns true the cell behind the selected cell will be selected. This
* implementation returns false;
*/
mxGraph.prototype.isTransparentClickEvent = function(evt)
{
return false;
};
/**
* 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: isIgnoreTerminalEvent
*
* Returns true if the given mouse event should not allow any connections to be
* made. This implementation returns false.
*/
mxGraph.prototype.isIgnoreTerminalEvent = function(evt)
{
return false;
};
/**
* 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>. Returns null in the case of
* successful validation or an array of strings (warnings) in the case of
* failed validations.
*
* 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) && (outCount > rule.max ||
outCount < rule.min))
{
error += rule.countError + '\n';
}
else if (!rule.source && mxUtils.isNode(value, rule.type,
rule.attr, rule.value) && (inCount > rule.max ||
inCount < rule.min))
{
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 a 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 = mxUtils.htmlEntities(mxResources.get(tip) || tip).replace(/\\n/g, '<br>');
}
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: getCursorForMouseEvent
*
* Returns the cursor value to be used for the CSS of the shape for the
* given event. This implementation calls <getCursorForCell>.
*
* Parameters:
*
* me - <mxMouseEvent> whose cursor should be returned.
*/
mxGraph.prototype.getCursorForMouseEvent = function(me)
{
return this.getCursorForCell(me.getCell());
};
/**
* 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: isCellRotatable
*
* Returns true if the given cell is rotatable. This returns true for the given
* cell if its style does not specify <mxConstants.STYLE_ROTATABLE> to be 0.
*
* Parameters:
*
* cell - <mxCell> whose rotatable state should be returned.
*/
mxGraph.prototype.isCellRotatable = function(cell)
{
var state = this.view.getState(cell);
var style = (state != null) ? state.style : this.getCellStyle(cell);
return style[mxConstants.STYLE_ROTATABLE] != 0;
};
/**
* 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) &&
mxUtils.getValue(style, mxConstants.STYLE_RESIZABLE, '1') != '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.
* To update the size of cells when the cells are added, set
* <autoSizeCellsOnAdd> to true.
*
* 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(cell)
{
return this.extendParentsOnAdd;
};
/**
* Function: setExtendParentsOnAdd
*
* Sets <extendParentsOnAdd>.
*
* Parameters:
*
* value - New boolean value for <extendParentsOnAdd>.
*/
mxGraph.prototype.setExtendParentsOnAdd = function(value)
{
this.extendParentsOnAdd = value;
};
/**
* Function: isExtendParentsOnMove
*
* Returns <extendParentsOnMove>.
*/
mxGraph.prototype.isExtendParentsOnMove = function()
{
return this.extendParentsOnMove;
};
/**
* Function: setExtendParentsOnMove
*
* Sets <extendParentsOnMove>.
*
* Parameters:
*
* value - New boolean value for <extendParentsOnAdd>.
*/
mxGraph.prototype.setExtendParentsOnMove = function(value)
{
this.extendParentsOnMove = value;
};
/**
* Function: isRecursiveResize
*
* Returns <recursiveResize>.
*
* Parameters:
*
* state - <mxCellState> that is being resized.
*/
mxGraph.prototype.isRecursiveResize = function(state)
{
return this.recursiveResize;
};
/**
* Function: setRecursiveResize
*
* Sets <recursiveResize>.
*
* Parameters:
*
* value - New boolean value for <recursiveResize>.
*/
mxGraph.prototype.setRecursiveResize = function(value)
{
this.recursiveResize = 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: isConstrainRelativeChildren
*
* Returns <constrainRelativeChildren>.
*/
mxGraph.prototype.isConstrainRelativeChildren = function()
{
return this.constrainRelativeChildren;
};
/**
* Function: setConstrainRelativeChildren
*
* Sets <constrainRelativeChildren>.
*/
mxGraph.prototype.setConstrainRelativeChildren = function(value)
{
this.constrainRelativeChildren = 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 <splitEnabled> is true then this returns <isSplitTarget> for
* the given arguments else it returns true if the cell is not collapsed
* and its child count is greater than 0.
*
* 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.
* clone - Optional boolean to indicate of cells will be cloned.
*/
mxGraph.prototype.getDropTarget = function(cells, evt, cell, clone)
{
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);
}
// Checks if parent is dropped into child if not cloning
if (clone == null || !clone)
{
var parent = cell;
while (parent != null && mxUtils.indexOf(cells, parent) < 0)
{
parent = this.model.getParent(parent);
}
}
return (!this.model.isLayer(cell) && parent == null) ? 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.getCurrentRoot();
if (parent == null)
{
parent = this.defaultParent;
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 current root of the view or the root of the model.
* vertices - Optional boolean indicating if vertices should be returned.
* Default is true.
* edges - Optional boolean indicating if edges should be returned. Default
* is true.
* ignoreFn - Optional function that returns true if cell should be ignored.
* The function is passed the cell state and the x and y parameter.
*/
mxGraph.prototype.getCellAt = function(x, y, parent, vertices, edges, ignoreFn)
{
vertices = (vertices != null) ? vertices : true;
edges = (edges != null) ? edges : true;
if (parent == null)
{
parent = this.getCurrentRoot();
if (parent == null)
{
parent = this.getModel().getRoot();
}
}
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, ignoreFn);
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 (state != null && (ignoreFn == null || !ignoreFn(state, x, y)) &&
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
{
var alpha = mxUtils.toRadians(mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION) || 0);
if (alpha != 0)
{
var cos = Math.cos(-alpha);
var sin = Math.sin(-alpha);
var cx = new mxPoint(state.getCenterX(), state.getCenterY());
var pt = mxUtils.getRotatedPoint(new mxPoint(x, y), cos, sin, cx);
x = pt.x;
y = pt.y;
}
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 = [];
// Fast lookup to avoid duplicates in terminals array
var dict = new mxDictionary();
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)
{
if (!dict.get(target))
{
dict.put(target, true);
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)
{
if (!dict.get(source))
{
dict.put(source, true);
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 child vertices and edges of the given parent that are contained
* in the given rectangle. The result is added to the optional result array,
* which is returned. 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> that should be used as the root of the recursion.
* Default is current root of the view or the root of the model.
* 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 model = this.getModel();
var right = x + width;
var bottom = y + height;
if (parent == null)
{
parent = this.getCurrentRoot();
if (parent == null)
{
parent = model.getRoot();
}
}
if (parent != null)
{
var childCount = model.getChildCount(parent);
for (var i = 0; i < childCount; i++)
{
var cell = model.getChildAt(parent, i);
var state = this.view.getState(cell);
if (state != null && this.isCellVisible(cell))
{
var deg = mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION) || 0;
var box = state;
if (deg != 0)
{
box = mxUtils.getBoundingBox(box, deg);
}
if ((model.isEdge(cell) || model.isVertex(cell)) &&
box.x >= x && box.y + box.height <= bottom &&
box.y >= y && box.x + box.width <= right)
{
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 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 <mxDictionary> from cells to true for the visited cells.
* inverse - Optional boolean to traverse in inverse direction. Default is false.
* This is ignored if directed is false.
*/
mxGraph.prototype.traverse = function(vertex, directed, func, edge, visited, inverse)
{
if (func != null && vertex != null)
{
directed = (directed != null) ? directed : true;
inverse = (inverse != null) ? inverse : false;
visited = visited || new mxDictionary();
if (!visited.get(vertex))
{
visited.put(vertex, true);
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 || (!inverse == isSource))
{
var next = this.model.getTerminal(e, !isSource);
this.traverse(next, directed, func, e, visited, inverse);
}
}
}
}
}
}
};
/**
* 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>.
* descendants - Optional boolean specifying whether all descendants should be
* selected. Default is false.
*/
mxGraph.prototype.selectAll = function(parent, descendants)
{
parent = parent || this.getDefaultParent();
var cells = (descendants) ? this.model.filterDescendants(function(cell)
{
return cell != parent;
}, parent) : this.model.getChildren(parent);
if (cells != null)
{
this.setSelectionCells(cells);
}
};
/**
* 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.
* For vertices, only cells with no children are selected.
*
* 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(this.model.getParent(cell))) ||
(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 source = state.getVisibleTerminalState(true);
var target = state.getVisibleTerminalState(false);
var geo = this.getCellGeometry(state.cell);
var edgeStyle = this.view.getEdgeStyle(state, (geo != null) ? geo.points : null, source, target);
result = this.createEdgeHandler(state, edgeStyle);
}
else
{
result = this.createVertexHandler(state);
}
}
return result;
};
/**
* Function: createVertexHandler
*
* Hooks to create a new <mxVertexHandler> for the given <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> to create the handler for.
*/
mxGraph.prototype.createVertexHandler = function(state)
{
return new mxVertexHandler(state);
};
/**
* Function: createEdgeHandler
*
* Hooks to create a new <mxEdgeHandler> for the given <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> to create the handler for.
*/
mxGraph.prototype.createEdgeHandler = function(state, edgeStyle)
{
var result = null;
if (edgeStyle == mxEdgeStyle.Loop ||
edgeStyle == mxEdgeStyle.ElbowConnector ||
edgeStyle == mxEdgeStyle.SideToSide ||
edgeStyle == mxEdgeStyle.TopToBottom)
{
result = this.createElbowEdgeHandler(state);
}
else if (edgeStyle == mxEdgeStyle.SegmentConnector ||
edgeStyle == mxEdgeStyle.OrthConnector)
{
result = this.createEdgeSegmentHandler(state);
}
else
{
result = new mxEdgeHandler(state);
}
return result;
};
/**
* Function: createEdgeSegmentHandler
*
* Hooks to create a new <mxEdgeSegmentHandler> for the given <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> to create the handler for.
*/
mxGraph.prototype.createEdgeSegmentHandler = function(state)
{
return new mxEdgeSegmentHandler(state);
};
/**
* Function: createElbowEdgeHandler
*
* Hooks to create a new <mxElbowEdgeHandler> for the given <mxCellState>.
*
* Parameters:
*
* state - <mxCellState> to create the handler for.
*/
mxGraph.prototype.createElbowEdgeHandler = function(state)
{
return new mxElbowEdgeHandler(state);
};
/**
* 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 and returned the event.
*
* Parameters:
*
* me - <mxMouseEvent> to be updated.
* evtName - Name of the mouse event.
*/
mxGraph.prototype.updateMouseEvent = function(me, evtName)
{
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;
// Searches for rectangles using method if native hit detection is disabled on shape
if (me.getCell() == null && this.isMouseDown && evtName == mxEvent.MOUSE_MOVE)
{
me.state = this.view.getState(this.getCellAt(pt.x, pt.y, null, null, null, function(state)
{
return state.shape == null || state.shape.paintBackground != mxRectangleShape.prototype.paintBackground ||
mxUtils.getValue(state.style, mxConstants.STYLE_POINTER_EVENTS, '1') == '1' ||
(state.shape.fill != null && state.shape.fill != mxConstants.NONE);
}));
}
}
return me;
};
/**
* Function: getStateForEvent
*
* Returns the state for the given touch event.
*/
mxGraph.prototype.getStateForTouchEvent = function(evt)
{
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(this.container, x, y);
return this.view.getState(this.getCellAt(pt.x, pt.y));
};
/**
* Function: isEventIgnored
*
* Returns true if the event should be ignored in <fireMouseEvent>.
*/
mxGraph.prototype.isEventIgnored = function(evtName, me, sender)
{
var mouseEvent = mxEvent.isMouseEvent(me.getEvent());
var result = false;
// Drops events that are fired more than once
if (me.getEvent() == this.lastEvent)
{
result = true;
}
else
{
this.lastEvent = me.getEvent();
}
// Installs event listeners to capture the complete gesture from the event source
// for non-MS touch events as a workaround for all events for the same geture being
// fired from the event source even if that was removed from the DOM.
if (this.eventSource != null && evtName != mxEvent.MOUSE_MOVE)
{
mxEvent.removeGestureListeners(this.eventSource, null, this.mouseMoveRedirect, this.mouseUpRedirect);
this.mouseMoveRedirect = null;
this.mouseUpRedirect = null;
this.eventSource = null;
}
else if (this.eventSource != null && me.getSource() != this.eventSource)
{
result = true;
}
else if (mxClient.IS_TOUCH && evtName == mxEvent.MOUSE_DOWN && !mouseEvent && !mxEvent.isPenEvent(me.getEvent()))
{
this.eventSource = me.getSource();
this.mouseMoveRedirect = mxUtils.bind(this, function(evt)
{
this.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, this.getStateForTouchEvent(evt)));
});
this.mouseUpRedirect = mxUtils.bind(this, function(evt)
{
this.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, this.getStateForTouchEvent(evt)));
});
mxEvent.addGestureListeners(this.eventSource, null, this.mouseMoveRedirect, this.mouseUpRedirect);
}
// Factored out the workarounds for FF to make it easier to override/remove
// Note this method has side-effects!
if (this.isSyntheticEventIgnored(evtName, me, sender))
{
result = true;
}
// Never fires mouseUp/-Down for double clicks
if (!mxEvent.isPopupTrigger(this.lastEvent) && evtName != mxEvent.MOUSE_MOVE && this.lastEvent.detail == 2)
{
return true;
}
// Filters out of sequence events or mixed event types during a gesture
if (evtName == mxEvent.MOUSE_UP && this.isMouseDown)
{
this.isMouseDown = false;
}
else if (evtName == mxEvent.MOUSE_DOWN && !this.isMouseDown)
{
this.isMouseDown = true;
this.isMouseTrigger = mouseEvent;
}
// Drops mouse events that are fired during touch gestures as a workaround for Webkit
// and mouse events that are not in sync with the current internal button state
else if (!result && (((!mxClient.IS_FF || evtName != mxEvent.MOUSE_MOVE) &&
this.isMouseDown && this.isMouseTrigger != mouseEvent) ||
(evtName == mxEvent.MOUSE_DOWN && this.isMouseDown) ||
(evtName == mxEvent.MOUSE_UP && !this.isMouseDown)))
{
result = true;
}
if (!result && evtName == mxEvent.MOUSE_DOWN)
{
this.lastMouseX = me.getX();
this.lastMouseY = me.getY();
}
return result;
};
/**
* Function: isSyntheticEventIgnored
*
* Hook for ignoring synthetic mouse events after touchend in Firefox.
*/
mxGraph.prototype.isSyntheticEventIgnored = function(evtName, me, sender)
{
var result = false;
var mouseEvent = mxEvent.isMouseEvent(me.getEvent());
// LATER: This does not cover all possible cases that can go wrong in FF
if (this.ignoreMouseEvents && mouseEvent && evtName != mxEvent.MOUSE_MOVE)
{
this.ignoreMouseEvents = evtName != mxEvent.MOUSE_UP;
result = true;
}
else if (mxClient.IS_FF && !mouseEvent && evtName == mxEvent.MOUSE_UP)
{
this.ignoreMouseEvents = true;
}
return result;
};
/**
* Function: isEventSourceIgnored
*
* Returns true if the event should be ignored in <fireMouseEvent>. This
* implementation returns true for select, option and input (if not of type
* checkbox, radio, button, submit or file) event sources if the event is not
* a mouse event or a left mouse button press event.
*
* Parameters:
*
* evtName - The name of the event.
* me - <mxMouseEvent> that should be ignored.
*/
mxGraph.prototype.isEventSourceIgnored = function(evtName, me)
{
var source = me.getSource();
var name = (source.nodeName != null) ? source.nodeName.toLowerCase() : '';
var candidate = !mxEvent.isMouseEvent(me.getEvent()) || mxEvent.isLeftMouseButton(me.getEvent());
return evtName == mxEvent.MOUSE_DOWN && candidate && (name == 'select' || name == 'option' ||
(name == 'input' && source.type != 'checkbox' && source.type != 'radio' &&
source.type != 'button' && source.type != 'submit' && source.type != 'file'));
};
/**
* Function: getEventState
*
* Returns the <mxCellState> to be used when firing the mouse event for the
* given state. This implementation returns the given state.
*
* Parameters:
*
* <mxCellState> - State whose event source should be returned.
*/
mxGraph.prototype.getEventState = function(state)
{
return state;
};
/**
* 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 (this.isEventSourceIgnored(evtName, me))
{
if (this.tooltipHandler != null)
{
this.tooltipHandler.hide();
}
return;
}
if (sender == null)
{
sender = this;
}
// Updates the graph coordinates in the event
me = this.updateMouseEvent(me, evtName);
// Detects and processes double taps for touch-based devices which do not have native double click events
// or where detection of double click is not always possible (quirks, IE10+). Note that this can only handle
// double clicks on cells because the sequence of events in IE prevents detection on the background, it fires
// two mouse ups, one of which without a cell but no mousedown for the second click which means we cannot
// detect which mouseup(s) are part of the first click, ie we do not know when the first click ends.
if ((!this.nativeDblClickEnabled && !mxEvent.isPopupTrigger(me.getEvent())) || (this.doubleTapEnabled &&
mxClient.IS_TOUCH && (mxEvent.isTouchEvent(me.getEvent()) || mxEvent.isPenEvent(me.getEvent()))))
{
var currentTime = new Date().getTime();
// NOTE: Second mouseDown for double click missing in quirks mode
if ((!mxClient.IS_QUIRKS && evtName == mxEvent.MOUSE_DOWN) || (mxClient.IS_QUIRKS && evtName == mxEvent.MOUSE_UP && !this.fireDoubleClick))
{
if (this.lastTouchEvent != null && this.lastTouchEvent != me.getEvent() &&
currentTime - this.lastTouchTime < this.doubleTapTimeout &&
Math.abs(this.lastTouchX - me.getX()) < this.doubleTapTolerance &&
Math.abs(this.lastTouchY - me.getY()) < this.doubleTapTolerance &&
this.doubleClickCounter < 2)
{
this.doubleClickCounter++;
var doubleClickFired = false;
if (evtName == mxEvent.MOUSE_UP)
{
if (me.getCell() == this.lastTouchCell && this.lastTouchCell != null)
{
this.lastTouchTime = 0;
var cell = this.lastTouchCell;
this.lastTouchCell = null;
// Fires native dblclick event via event source
// NOTE: This fires two double click events on edges in quirks mode. While
// trying to fix this, we realized that nativeDoubleClick can be disabled for
// quirks and IE10+ (or we didn't find the case mentioned above where it
// would not work), ie. all double clicks seem to be working without this.
if (mxClient.IS_QUIRKS)
{
me.getSource().fireEvent('ondblclick');
}
this.dblClick(me.getEvent(), cell);
doubleClickFired = true;
}
}
else
{
this.fireDoubleClick = true;
this.lastTouchTime = 0;
}
// Do not ignore mouse up in quirks in this case
if (!mxClient.IS_QUIRKS || doubleClickFired)
{
mxEvent.consume(me.getEvent());
return;
}
}
else if (this.lastTouchEvent == null || this.lastTouchEvent != me.getEvent())
{
this.lastTouchCell = me.getCell();
this.lastTouchX = me.getX();
this.lastTouchY = me.getY();
this.lastTouchTime = currentTime;
this.lastTouchEvent = me.getEvent();
this.doubleClickCounter = 0;
}
}
else if ((this.isMouseDown || evtName == mxEvent.MOUSE_UP) && this.fireDoubleClick)
{
this.fireDoubleClick = false;
var cell = this.lastTouchCell;
this.lastTouchCell = null;
this.isMouseDown = false;
// Workaround for Chrome/Safari not firing native double click events for double touch on background
var valid = (cell != null) || ((mxEvent.isTouchEvent(me.getEvent()) || mxEvent.isPenEvent(me.getEvent())) &&
(mxClient.IS_GC || mxClient.IS_SF));
if (valid && Math.abs(this.lastTouchX - me.getX()) < this.doubleTapTolerance &&
Math.abs(this.lastTouchY - me.getY()) < this.doubleTapTolerance)
{
this.dblClick(me.getEvent(), cell);
}
else
{
mxEvent.consume(me.getEvent());
}
return;
}
}
if (!this.isEventIgnored(evtName, me, sender))
{
// Updates the event state via getEventState
me.state = this.getEventState(me.getState());
this.fireEvent(new mxEventObject(mxEvent.FIRE_MOUSE_EVENT, 'eventName', evtName, 'event', me));
if ((mxClient.IS_OP || mxClient.IS_SF || mxClient.IS_GC || mxClient.IS_IE11 ||
(mxClient.IS_IE && mxClient.IS_SVG) || me.getEvent().target != this.container))
{
if (evtName == mxEvent.MOUSE_MOVE && this.isMouseDown && this.autoScroll && !mxEvent.isMultiTouchEvent(me.getEvent))
{
this.scrollPointToVisible(me.getGraphX(), me.getGraphY(), this.autoExtend);
}
else if (evtName == mxEvent.MOUSE_UP && this.ignoreScrollbars && this.translateToScrollPosition &&
(this.container.scrollLeft != 0 || this.container.scrollTop != 0))
{
var s = this.view.scale;
var tr = this.view.translate;
this.view.setTranslate(tr.x - this.container.scrollLeft / s, tr.y - this.container.scrollTop / s);
this.container.scrollLeft = 0;
this.container.scrollTop = 0;
}
if (this.mouseListeners != null)
{
var args = [sender, me];
// Does not change returnValue in Opera
if (!me.getEvent().preventDefault)
{
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);
}
}
// Detects tapAndHold events using a timer
if ((mxEvent.isTouchEvent(me.getEvent()) || mxEvent.isPenEvent(me.getEvent())) &&
evtName == mxEvent.MOUSE_DOWN && this.tapAndHoldEnabled && !this.tapAndHoldInProgress)
{
this.tapAndHoldInProgress = true;
this.initialTouchX = me.getGraphX();
this.initialTouchY = me.getGraphY();
var handler = function()
{
if (this.tapAndHoldValid)
{
this.tapAndHold(me);
}
this.tapAndHoldInProgress = false;
this.tapAndHoldValid = false;
};
if (this.tapAndHoldThread)
{
window.clearTimeout(this.tapAndHoldThread);
}
this.tapAndHoldThread = window.setTimeout(mxUtils.bind(this, handler), this.tapAndHoldDelay);
this.tapAndHoldValid = true;
}
else if (evtName == mxEvent.MOUSE_UP)
{
this.tapAndHoldInProgress = false;
this.tapAndHoldValid = false;
}
else if (this.tapAndHoldValid)
{
this.tapAndHoldValid =
Math.abs(this.initialTouchX - me.getGraphX()) < this.tolerance &&
Math.abs(this.initialTouchY - me.getGraphY()) < this.tolerance;
}
// Stops editing for all events other than from cellEditor
if (evtName == mxEvent.MOUSE_DOWN && this.isEditing() && !this.cellEditor.isEventSource(me.getEvent()))
{
this.stopEditing(!this.isInvokesStopCellEditing());
}
this.consumeMouseEvent(evtName, me, sender);
}
};
/**
* Function: consumeMouseEvent
*
* Consumes the given <mxMouseEvent> if it's a touchStart event.
*/
mxGraph.prototype.consumeMouseEvent = function(evtName, me, sender)
{
// Workaround for duplicate click in Windows 8 with Chrome/FF/Opera with touch
if (evtName == mxEvent.MOUSE_DOWN && mxEvent.isTouchEvent(me.getEvent()))
{
me.consume(false);
}
};
/**
* Function: fireGestureEvent
*
* Dispatches a <mxEvent.GESTURE> event. The following example will resize the
* cell under the mouse based on the scale property of the native touch event.
*
* (code)
* graph.addListener(mxEvent.GESTURE, function(sender, eo)
* {
* var evt = eo.getProperty('event');
* var state = graph.view.getState(eo.getProperty('cell'));
*
* if (graph.isEnabled() && graph.isCellResizable(state.cell) && Math.abs(1 - evt.scale) > 0.2)
* {
* var scale = graph.view.scale;
* var tr = graph.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(graph.snap(x / scale) - tr.x,
* graph.snap(y / scale) - tr.y, graph.snap(w / scale), graph.snap(h / scale));
* graph.resizeCell(state.cell, bounds);
* eo.consume();
* }
* });
* (end)
*
* Parameters:
*
* evt - Gestureend event that represents the gesture.
* cell - Optional <mxCell> associated with the gesture.
*/
mxGraph.prototype.fireGestureEvent = function(evt, cell)
{
// Resets double tap event handling when gestures take place
this.lastTouchTime = 0;
this.fireEvent(new mxEventObject(mxEvent.GESTURE, 'event', evt, 'cell', cell));
};
/**
* 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.popupMenuHandler != null)
{
this.popupMenuHandler.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;
}
};