/** * $Id: mxCellRenderer.js,v 1.189 2012-11-20 09:06:07 gaudenz Exp $ * Copyright (c) 2006-2010, JGraph Ltd */ /** * Class: mxCellRenderer * * Renders cells into a document object model. The is a global * map of shapename, constructor pairs that is used in all instances. You can * get a list of all available shape names using the following code. * * In general the cell renderer is in charge of creating, redrawing and * destroying the shape and label associated with a cell state, as well as * some other graphical objects, namely controls and overlays. The shape * hieararchy in the display (ie. the hierarchy in which the DOM nodes * appear in the document) does not reflect the cell hierarchy. The shapes * are a (flat) sequence of shapes and labels inside the draw pane of the * graph view, with some exceptions, namely the HTML labels being placed * directly inside the graph container for certain browsers. * * (code) * mxLog.show(); * for (var i in mxCellRenderer.prototype.defaultShapes) * { * mxLog.debug(i); * } * (end) * * Constructor: mxCellRenderer * * Constructs a new cell renderer with the following built-in shapes: * arrow, rectangle, ellipse, rhombus, image, line, label, cylinder, * swimlane, connector, actor and cloud. */ function mxCellRenderer() { this.shapes = mxUtils.clone(this.defaultShapes); }; /** * Variable: shapes * * Array that maps from shape names to shape constructors. All entries * in are added to this array. */ mxCellRenderer.prototype.shapes = null; /** * Variable: defaultEdgeShape * * Defines the default shape for edges. Default is . */ mxCellRenderer.prototype.defaultEdgeShape = mxConnector; /** * Variable: defaultVertexShape * * Defines the default shape for vertices. Default is . */ mxCellRenderer.prototype.defaultVertexShape = mxRectangleShape; /** * Variable: defaultShapes * * Static array that contains the globally registered shapes which are * known to all instances of this class. For adding instance-specific * shapes you should use on the instance. For adding * a shape to this array you can use the following code: * * (code) * mxCellRenderer.prototype.defaultShapes['myshape'] = myShape; * (end) * * Where 'myshape' is the key under which the shape is to be registered * and myShape is the name of the constructor function. */ mxCellRenderer.prototype.defaultShapes = new Object(); // Adds default shapes into the default shapes array mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_ARROW] = mxArrow; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_RECTANGLE] = mxRectangleShape; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_ELLIPSE] = mxEllipse; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_DOUBLE_ELLIPSE] = mxDoubleEllipse; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_RHOMBUS] = mxRhombus; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_IMAGE] = mxImageShape; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_LINE] = mxLine; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_LABEL] = mxLabel; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_CYLINDER] = mxCylinder; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_SWIMLANE] = mxSwimlane; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_CONNECTOR] = mxConnector; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_ACTOR] = mxActor; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_CLOUD] = mxCloud; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_TRIANGLE] = mxTriangle; mxCellRenderer.prototype.defaultShapes[mxConstants.SHAPE_HEXAGON] = mxHexagon; /** * Function: registerShape * * Registers the given constructor under the specified key in this instance * of the renderer. * * Example: * * (code) * this.registerShape(mxConstants.SHAPE_RECTANGLE, mxRectangleShape); * (end) * * Parameters: * * key - String representing the shape name. * shape - Constructor of the subclass. */ mxCellRenderer.prototype.registerShape = function(key, shape) { this.shapes[key] = shape; }; /** * Function: initialize * * Initializes the display for the given cell state. This is required once * after the cell state has been created. This is invoked in * mxGraphView.createState. * * Parameters: * * state - for which the display should be initialized. * rendering - Optional boolean that specifies if the cell should actually * be initialized for any given DOM node. If this is false then init * will not be called on the shape. */ mxCellRenderer.prototype.initialize = function(state, rendering) { var model = state.view.graph.getModel(); if (state.view.graph.container != null && state.shape == null && state.cell != state.view.currentRoot && (model.isVertex(state.cell) || model.isEdge(state.cell))) { this.createShape(state); if (state.shape != null && (rendering == null || rendering)) { this.initializeShape(state); // Maintains the model order in the DOM if (state.view.graph.ordered || model.isEdge(state.cell)) { //state.orderChanged = true; state.invalidOrder = true; } else if (state.view.graph.keepEdgesInForeground && this.firstEdge != null) { if (this.firstEdge.parentNode == state.shape.node.parentNode) { this.insertState(state, this.firstEdge); } else { this.firstEdge = null; } } state.shape.scale = state.view.scale; this.createCellOverlays(state); this.installListeners(state); } } }; /** * Function: initializeShape * * Initializes the shape in the given state by calling its init method with * the correct container. * * Parameters: * * state - for which the shape should be initialized. */ mxCellRenderer.prototype.initializeShape = function(state) { state.shape.init(state.view.getDrawPane()); }; /** * Returns the previous state that has a shape inside the given parent. */ mxCellRenderer.prototype.getPreviousStateInContainer = function(state, container) { var result = null; var graph = state.view.graph; var model = graph.getModel(); var child = state.cell; var p = model.getParent(child); while (p != null && result == null) { result = this.findPreviousStateInContainer(graph, p, child, container); child = p; p = model.getParent(child); } return result; }; /** * Returns the previous state that has a shape inside the given parent. */ mxCellRenderer.prototype.findPreviousStateInContainer = function(graph, cell, stop, container) { // Recurse first var result = null; var model = graph.getModel(); if (stop != null) { var start = cell.getIndex(stop); for (var i = start - 1; i >= 0 && result == null; i--) { result = this.findPreviousStateInContainer(graph, model.getChildAt(cell, i), null, container); } } else { var childCount = model.getChildCount(cell); for (var i = childCount - 1; i >= 0 && result == null; i--) { result = this.findPreviousStateInContainer(graph, model.getChildAt(cell, i), null, container); } } if (result == null) { result = graph.view.getState(cell); if (result != null && (result.shape == null || result.shape.node == null || result.shape.node.parentNode != container)) { result = null; } } return result; }; /** * Function: order * * Orders the DOM node of the shape for the given state according to the * position of the corresponding cell in the graph model. * * Parameters: * * state - whose shape's DOM node should be ordered. */ mxCellRenderer.prototype.order = function(state) { var container = state.shape.node.parentNode; var previous = this.getPreviousStateInContainer(state, container); var nextNode = container.firstChild; if (previous != null) { nextNode = previous.shape.node; if (previous.text != null && previous.text.node != null && previous.text.node.parentNode == container) { nextNode = previous.text.node; } nextNode = nextNode.nextSibling; } this.insertState(state, nextNode); }; /** * Function: orderEdge * * Orders the DOM node of the shape for the given edge's state according to * the and * rules. * * Parameters: * * state - whose shape's DOM node should be ordered. */ mxCellRenderer.prototype.orderEdge = function(state) { var view = state.view; var model = view.graph.getModel(); // Moves edges to the foreground/background if (view.graph.keepEdgesInForeground) { if (this.firstEdge == null || this.firstEdge.parentNode == null || this.firstEdge.parentNode != state.shape.node.parentNode) { this.firstEdge = state.shape.node; } } else if (view.graph.keepEdgesInBackground) { var node = state.shape.node; var parent = node.parentNode; // Keeps the DOM node in front of its parent var pcell = model.getParent(state.cell); var pstate = view.getState(pcell); if (pstate != null && pstate.shape != null && pstate.shape.node != null) { var child = pstate.shape.node.nextSibling; if (child != null && child != node) { this.insertState(state, child); } } else { var child = parent.firstChild; if (child != null && child != node) { this.insertState(state, child); } } } }; /** * Function: insertState * * Inserts the given state before the given node into its parent. * * Parameters: * * state - for which the shape should be created. */ mxCellRenderer.prototype.insertState = function(state, nextNode) { state.shape.node.parentNode.insertBefore(state.shape.node, nextNode); if (state.text != null && state.text.node != null && state.text.node.parentNode == state.shape.node.parentNode) { state.shape.node.parentNode.insertBefore(state.text.node, state.shape.node.nextSibling); } }; /** * Function: createShape * * Creates the shape for the given cell state. The shape is configured * using . * * Parameters: * * state - for which the shape should be created. */ mxCellRenderer.prototype.createShape = function(state) { if (state.style != null) { // Checks if there is a stencil for the name and creates // a shape instance for the stencil if one exists var key = state.style[mxConstants.STYLE_SHAPE]; var stencil = mxStencilRegistry.getStencil(key); if (stencil != null) { state.shape = new mxStencilShape(stencil); } else { var ctor = this.getShapeConstructor(state); state.shape = new ctor(); } // Sets the initial bounds and points (will be updated in redraw) state.shape.points = state.absolutePoints; state.shape.bounds = new mxRectangle( state.x, state.y, state.width, state.height); state.shape.dialect = state.view.graph.dialect; this.configureShape(state); } }; /** * Function: getShapeConstructor * * Returns the constructor to be used for creating the shape. */ mxCellRenderer.prototype.getShapeConstructor = function(state) { var key = state.style[mxConstants.STYLE_SHAPE]; var ctor = (key != null) ? this.shapes[key] : null; if (ctor == null) { ctor = (state.view.graph.getModel().isEdge(state.cell)) ? this.defaultEdgeShape : this.defaultVertexShape; } return ctor; }; /** * Function: configureShape * * Configures the shape for the given cell state. * * Parameters: * * state - for which the shape should be configured. */ mxCellRenderer.prototype.configureShape = function(state) { state.shape.apply(state); var image = state.view.graph.getImage(state); if (image != null) { state.shape.image = image; } var indicator = state.view.graph.getIndicatorColor(state); var key = state.view.graph.getIndicatorShape(state); var ctor = (key != null) ? this.shapes[key] : null; // Configures the indicator shape or image if (indicator != null) { state.shape.indicatorShape = ctor; state.shape.indicatorColor = indicator; state.shape.indicatorGradientColor = state.view.graph.getIndicatorGradientColor(state); state.shape.indicatorDirection = state.style[mxConstants.STYLE_INDICATOR_DIRECTION]; } else { var indicator = state.view.graph.getIndicatorImage(state); if (indicator != null) { state.shape.indicatorImage = indicator; } } this.postConfigureShape(state); }; /** * Function: postConfigureShape * * Replaces any reserved words used for attributes, eg. inherit, * indicated or swimlane for colors in the shape for the given state. * This implementation resolves these keywords on the fill, stroke * and gradient color keys. */ mxCellRenderer.prototype.postConfigureShape = function(state) { if (state.shape != null) { this.resolveColor(state, 'indicatorColor', mxConstants.STYLE_FILLCOLOR); this.resolveColor(state, 'indicatorGradientColor', mxConstants.STYLE_GRADIENTCOLOR); this.resolveColor(state, 'fill', mxConstants.STYLE_FILLCOLOR); this.resolveColor(state, 'stroke', mxConstants.STYLE_STROKECOLOR); this.resolveColor(state, 'gradient', mxConstants.STYLE_GRADIENTCOLOR); } }; /** * Function: resolveColor * * Resolves special keywords 'inherit', 'indicated' and 'swimlane' and sets * the respective color on the shape. */ mxCellRenderer.prototype.resolveColor = function(state, field, key) { var value = state.shape[field]; var graph = state.view.graph; var referenced = null; if (value == 'inherit') { referenced = graph.model.getParent(state.cell); } else if (value == 'swimlane') { if (graph.model.getTerminal(state.cell, false) != null) { referenced = graph.model.getTerminal(state.cell, false); } else { referenced = state.cell; } referenced = graph.getSwimlane(referenced); key = graph.swimlaneIndicatorColorAttribute; } else if (value == 'indicated') { state.shape[field] = state.shape.indicatorColor; } if (referenced != null) { var rstate = graph.getView().getState(referenced); state.shape[field] = null; if (rstate != null) { if (rstate.shape != null && field != 'indicatorColor') { state.shape[field] = rstate.shape[field]; } else { state.shape[field] = rstate.style[key]; } } } }; /** * Function: getLabelValue * * Returns the value to be used for the label. * * Parameters: * * state - for which the label should be created. */ mxCellRenderer.prototype.getLabelValue = function(state) { var graph = state.view.graph; var value = graph.getLabel(state.cell); if (!graph.isHtmlLabel(state.cell) && !mxUtils.isNode(value) && graph.dialect != mxConstants.DIALECT_SVG && value != null) { value = mxUtils.htmlEntities(value, false); } return value; }; /** * Function: createLabel * * Creates the label for the given cell state. * * Parameters: * * state - for which the label should be created. */ mxCellRenderer.prototype.createLabel = function(state, value) { var graph = state.view.graph; var isEdge = graph.getModel().isEdge(state.cell); if (state.style[mxConstants.STYLE_FONTSIZE] > 0 || state.style[mxConstants.STYLE_FONTSIZE] == null) { // Avoids using DOM node for empty labels var isForceHtml = (graph.isHtmlLabel(state.cell) || (value != null && mxUtils.isNode(value))) && graph.dialect == mxConstants.DIALECT_SVG; state.text = new mxText(value, new mxRectangle(), (state.style[mxConstants.STYLE_ALIGN] || mxConstants.ALIGN_CENTER), graph.getVerticalAlign(state), state.style[mxConstants.STYLE_FONTCOLOR], state.style[mxConstants.STYLE_FONTFAMILY], state.style[mxConstants.STYLE_FONTSIZE], state.style[mxConstants.STYLE_FONTSTYLE], state.style[mxConstants.STYLE_SPACING], state.style[mxConstants.STYLE_SPACING_TOP], state.style[mxConstants.STYLE_SPACING_RIGHT], state.style[mxConstants.STYLE_SPACING_BOTTOM], state.style[mxConstants.STYLE_SPACING_LEFT], state.style[mxConstants.STYLE_HORIZONTAL], state.style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR], state.style[mxConstants.STYLE_LABEL_BORDERCOLOR], graph.isWrapping(state.cell) && graph.isHtmlLabel(state.cell), graph.isLabelClipped(state.cell), state.style[mxConstants.STYLE_OVERFLOW], state.style[mxConstants.STYLE_LABEL_PADDING]); state.text.opacity = state.style[mxConstants.STYLE_TEXT_OPACITY]; state.text.dialect = (isForceHtml) ? mxConstants.DIALECT_STRICTHTML : state.view.graph.dialect; this.initializeLabel(state); // Workaround for touch devices routing all events for a mouse // gesture (down, move, up) via the initial DOM node. IE is even // worse in that it redirects the event via the initial DOM node // but the event source is the node under the mouse, so we need // to check if this is the case and force getCellAt for the // subsequent mouseMoves and the final mouseUp. var forceGetCell = false; var getState = function(evt) { var result = state; if (mxClient.IS_TOUCH || forceGetCell) { var x = mxEvent.getClientX(evt); var y = mxEvent.getClientY(evt); // Dispatches the drop event to the graph which // consumes and executes the source function var pt = mxUtils.convertPoint(graph.container, x, y); result = graph.view.getState(graph.getCellAt(pt.x, pt.y)); } return result; }; // TODO: Add handling for gestures var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; mxEvent.addListener(state.text.node, md, mxUtils.bind(this, function(evt) { if (this.isLabelEvent(state, evt)) { graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state)); forceGetCell = graph.dialect != mxConstants.DIALECT_SVG && mxEvent.getSource(evt).nodeName == 'IMG'; } }) ); mxEvent.addListener(state.text.node, mm, mxUtils.bind(this, function(evt) { if (this.isLabelEvent(state, evt)) { graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt))); } }) ); mxEvent.addListener(state.text.node, mu, mxUtils.bind(this, function(evt) { if (this.isLabelEvent(state, evt)) { graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, getState(evt))); forceGetCell = false; } }) ); mxEvent.addListener(state.text.node, 'dblclick', mxUtils.bind(this, function(evt) { if (this.isLabelEvent(state, evt)) { graph.dblClick(evt, state.cell); mxEvent.consume(evt); } }) ); } }; /** * Function: initializeLabel * * Initiailzes the label with a suitable container. * * Parameters: * * state - whose label should be initialized. */ mxCellRenderer.prototype.initializeLabel = function(state) { var graph = state.view.graph; if (state.text.dialect != mxConstants.DIALECT_SVG) { // Adds the text to the container if the dialect is not SVG and we // have an SVG-based browser which doesn't support foreignObjects if (mxClient.IS_SVG && mxClient.NO_FO) { state.text.init(graph.container); } else if (mxUtils.isVml(state.view.getDrawPane())) { if (state.shape.label != null) { state.text.init(state.shape.label); } else { state.text.init(state.shape.node); } } } if (state.text.node == null) { state.text.init(state.view.getDrawPane()); if (state.shape != null && state.text != null) { state.shape.node.parentNode.insertBefore( state.text.node, state.shape.node.nextSibling); } } }; /** * Function: createCellOverlays * * Creates the actual shape for showing the overlay for the given cell state. * * Parameters: * * state - for which the overlay should be created. */ mxCellRenderer.prototype.createCellOverlays = function(state) { var graph = state.view.graph; var overlays = graph.getCellOverlays(state.cell); var dict = null; if (overlays != null) { dict = new mxDictionary(); for (var i = 0; i < overlays.length; i++) { var shape = (state.overlays != null) ? state.overlays.remove(overlays[i]) : null; if (shape == null) { var tmp = new mxImageShape(new mxRectangle(), overlays[i].image.src); tmp.dialect = state.view.graph.dialect; tmp.preserveImageAspect = false; tmp.overlay = overlays[i]; this.initializeOverlay(state, tmp); this.installCellOverlayListeners(state, overlays[i], tmp); if (overlays[i].cursor != null) { tmp.node.style.cursor = overlays[i].cursor; } dict.put(overlays[i], tmp); } else { dict.put(overlays[i], shape); } } } // Removes unused if (state.overlays != null) { state.overlays.visit(function(id, shape) { shape.destroy(); }); } state.overlays = dict; }; /** * Function: initializeOverlay * * Initializes the given overlay. * * Parameters: * * state - for which the overlay should be created. * overlay - that represents the overlay. */ mxCellRenderer.prototype.initializeOverlay = function(state, overlay) { overlay.init(state.view.getOverlayPane()); }; /** * Function: installOverlayListeners * * Installs the listeners for the given , and * that represents the overlay. */ mxCellRenderer.prototype.installCellOverlayListeners = function(state, overlay, shape) { var graph = state.view.graph; mxEvent.addListener(shape.node, 'click', function (evt) { if (graph.isEditing()) { graph.stopEditing(!graph.isInvokesStopCellEditing()); } overlay.fireEvent(new mxEventObject(mxEvent.CLICK, 'event', evt, 'cell', state.cell)); }); var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; mxEvent.addListener(shape.node, md, function (evt) { mxEvent.consume(evt); }); mxEvent.addListener(shape.node, mm, function (evt) { graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, state)); }); if (mxClient.IS_TOUCH) { mxEvent.addListener(shape.node, 'touchend', function (evt) { overlay.fireEvent(new mxEventObject(mxEvent.CLICK, 'event', evt, 'cell', state.cell)); }); } }; /** * Function: createControl * * Creates the control for the given cell state. * * Parameters: * * state - for which the control should be created. */ mxCellRenderer.prototype.createControl = function(state) { var graph = state.view.graph; var image = graph.getFoldingImage(state); if (graph.foldingEnabled && image != null) { if (state.control == null) { var b = new mxRectangle(0, 0, image.width, image.height); state.control = new mxImageShape(b, image.src); state.control.dialect = graph.dialect; state.control.preserveImageAspect = false; this.initControl(state, state.control, true, function (evt) { if (graph.isEnabled()) { var collapse = !graph.isCellCollapsed(state.cell); graph.foldCells(collapse, false, [state.cell]); mxEvent.consume(evt); } }); } } else if (state.control != null) { state.control.destroy(); state.control = null; } }; /** * Function: initControl * * Initializes the given control and returns the corresponding DOM node. * * Parameters: * * state - for which the control should be initialized. * control - to be initialized. * handleEvents - Boolean indicating if mousedown and mousemove should fire events via the graph. * clickHandler - Optional function to implement clicks on the control. */ mxCellRenderer.prototype.initControl = function(state, control, handleEvents, clickHandler) { var graph = state.view.graph; // In the special case where the label is in HTML and the display is SVG the image // should go into the graph container directly in order to be clickable. Otherwise // it is obscured by the HTML label that overlaps the cell. var isForceHtml = graph.isHtmlLabel(state.cell) && mxClient.NO_FO && graph.dialect == mxConstants.DIALECT_SVG; if (isForceHtml) { control.dialect = mxConstants.DIALECT_PREFERHTML; control.init(graph.container); control.node.style.zIndex = 1; } else { control.init(state.view.getOverlayPane()); } var node = control.innerNode || control.node; if (clickHandler) { if (graph.isEnabled()) { node.style.cursor = 'pointer'; } mxEvent.addListener(node, 'click', clickHandler); } if (handleEvents) { var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; mxEvent.addListener(node, md, function (evt) { graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state)); mxEvent.consume(evt); }); mxEvent.addListener(node, mm, function (evt) { graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, state)); }); } return node; }; /** * Function: isShapeEvent * * Returns true if the event is for the shape of the given state. This * implementation always returns true. * * Parameters: * * state - whose shape fired the event. * evt - Mouse event which was fired. */ mxCellRenderer.prototype.isShapeEvent = function(state, evt) { return true; }; /** * Function: isLabelEvent * * Returns true if the event is for the label of the given state. This * implementation always returns true. * * Parameters: * * state - whose label fired the event. * evt - Mouse event which was fired. */ mxCellRenderer.prototype.isLabelEvent = function(state, evt) { return true; }; /** * Function: installListeners * * Installs the event listeners for the given cell state. * * Parameters: * * state - for which the event listeners should be isntalled. */ mxCellRenderer.prototype.installListeners = function(state) { var graph = state.view.graph; // Receives events from transparent backgrounds if (graph.dialect == mxConstants.DIALECT_SVG) { var events = 'all'; // Disabled fill-events on non-filled edges if (graph.getModel().isEdge(state.cell) && state.shape.stroke != null && (state.shape.fill == null || state.shape.fill == mxConstants.NONE)) { events = 'visibleStroke'; } // Specifies the event-processing on the shape if (state.shape.innerNode != null) { state.shape.innerNode.setAttribute('pointer-events', events); } else { state.shape.node.setAttribute('pointer-events', events); } } // Workaround for touch devices routing all events for a mouse // gesture (down, move, up) via the initial DOM node. Same for // HTML images in all IE versions (VML images are working). var getState = function(evt) { var result = state; if ((graph.dialect != mxConstants.DIALECT_SVG && mxEvent.getSource(evt).nodeName == 'IMG') || mxClient.IS_TOUCH) { var x = mxEvent.getClientX(evt); var y = mxEvent.getClientY(evt); // Dispatches the drop event to the graph which // consumes and executes the source function var pt = mxUtils.convertPoint(graph.container, x, y); result = graph.view.getState(graph.getCellAt(pt.x, pt.y)); } return result; }; // Experimental support for two-finger pinch to resize cells var gestureInProgress = false; mxEvent.addListener(state.shape.node, 'gesturestart', mxUtils.bind(this, function(evt) { // FIXME: Breaks encapsulation to reset the double // tap event handling when gestures take place graph.lastTouchTime = 0; gestureInProgress = true; mxEvent.consume(evt); }) ); var md = (mxClient.IS_TOUCH) ? 'touchstart' : 'mousedown'; var mm = (mxClient.IS_TOUCH) ? 'touchmove' : 'mousemove'; var mu = (mxClient.IS_TOUCH) ? 'touchend' : 'mouseup'; mxEvent.addListener(state.shape.node, md, mxUtils.bind(this, function(evt) { if (this.isShapeEvent(state, evt) && !gestureInProgress) { // Redirects events from the "event-transparent" region of // a swimlane to the graph. This is only required in HTML, // SVG and VML do not fire mouse events on transparent // backgrounds. graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, (state.shape != null && mxEvent.getSource(evt) == state.shape.content) ? null : state)); } else if (gestureInProgress) { mxEvent.consume(evt); } }) ); mxEvent.addListener(state.shape.node, mm, mxUtils.bind(this, function(evt) { if (this.isShapeEvent(state, evt) && !gestureInProgress) { graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, (state.shape != null && mxEvent.getSource(evt) == state.shape.content) ? null : getState(evt))); } else if (gestureInProgress) { mxEvent.consume(evt); } }) ); mxEvent.addListener(state.shape.node, mu, mxUtils.bind(this, function(evt) { if (this.isShapeEvent(state, evt) && !gestureInProgress) { graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, (state.shape != null && mxEvent.getSource(evt) == state.shape.content) ? null : getState(evt))); } else if (gestureInProgress) { mxEvent.consume(evt); } }) ); // Experimental handling for gestures. Double-tap handling is implemented // in mxGraph.fireMouseEvent. var dc = (mxClient.IS_TOUCH) ? 'gestureend' : 'dblclick'; mxEvent.addListener(state.shape.node, dc, mxUtils.bind(this, function(evt) { gestureInProgress = false; if (dc == 'gestureend') { // FIXME: Breaks encapsulation to reset the double // tap event handling when gestures take place graph.lastTouchTime = 0; if (graph.gestureEnabled) { graph.handleGesture(state, evt); mxEvent.consume(evt); } } else if (this.isShapeEvent(state, evt)) { graph.dblClick(evt, (state.shape != null && mxEvent.getSource(evt) == state.shape.content) ? null : state.cell); mxEvent.consume(evt); } }) ); }; /** * Function: redrawLabel * * Redraws the label for the given cell state. * * Parameters: * * state - whose label should be redrawn. */ mxCellRenderer.prototype.redrawLabel = function(state) { var value = this.getLabelValue(state); // FIXME: Add label always if HTML label and NO_FO if (state.text == null && value != null && (mxUtils.isNode(value) || value.length > 0)) { this.createLabel(state, value); } else if (state.text != null && (value == null || value.length == 0)) { state.text.destroy(); state.text = null; } if (state.text != null) { var graph = state.view.graph; var wrapping = graph.isWrapping(state.cell); var clipping = graph.isLabelClipped(state.cell); var bounds = this.getLabelBounds(state); if (state.text.value != value || state.text.isWrapping != wrapping || state.text.isClipping != clipping || state.text.scale != state.view.scale || !state.text.bounds.equals(bounds)) { state.text.value = value; state.text.bounds = bounds; state.text.scale = this.getTextScale(state); state.text.isWrapping = wrapping; state.text.isClipping = clipping; state.text.redraw(); } } }; /** * Function: getTextScale * * Returns the scaling used for the label of the given state * * Parameters: * * state - whose label scale should be returned. */ mxCellRenderer.prototype.getTextScale = function(state) { return state.view.scale; }; /** * Function: getLabelBounds * * Returns the bounds to be used to draw the label of the given state. * * Parameters: * * state - whose label bounds should be returned. */ mxCellRenderer.prototype.getLabelBounds = function(state) { var graph = state.view.graph; var isEdge = graph.getModel().isEdge(state.cell); var bounds = new mxRectangle(state.absoluteOffset.x, state.absoluteOffset.y); if (!isEdge) { bounds.x += state.x; bounds.y += state.y; // Minimum of 1 fixes alignment bug in HTML labels bounds.width = Math.max(1, state.width); bounds.height = Math.max(1, state.height); if (graph.isSwimlane(state.cell)) { var scale = graph.view.scale; var size = graph.getStartSize(state.cell); if (size.width > 0) { bounds.width = size.width * scale; } else if (size.height > 0) { bounds.height = size.height * scale; } } } return bounds; }; /** * Function: redrawCellOverlays * * Redraws the overlays for the given cell state. * * Parameters: * * state - whose overlays should be redrawn. */ mxCellRenderer.prototype.redrawCellOverlays = function(state) { this.createCellOverlays(state); if (state.overlays != null) { state.overlays.visit(function(id, shape) { var bounds = shape.overlay.getBounds(state); if (shape.bounds == null || shape.scale != state.view.scale || !shape.bounds.equals(bounds)) { shape.bounds = bounds; shape.scale = state.view.scale; shape.redraw(); } }); } }; /** * Function: redrawControl * * Redraws the control for the given cell state. * * Parameters: * * state - whose control should be redrawn. */ mxCellRenderer.prototype.redrawControl = function(state) { if (state.control != null) { var bounds = this.getControlBounds(state); var s = state.view.scale; if (state.control.scale != s || !state.control.bounds.equals(bounds)) { state.control.bounds = bounds; state.control.scale = s; state.control.redraw(); } } }; /** * Function: getControlBounds * * Returns the bounds to be used to draw the control (folding icon) of the * given state. */ mxCellRenderer.prototype.getControlBounds = function(state) { if (state.control != null) { var oldScale = state.control.scale; var w = state.control.bounds.width / oldScale; var h = state.control.bounds.height / oldScale; var s = state.view.scale; return (state.view.graph.getModel().isEdge(state.cell)) ? new mxRectangle(state.x + state.width / 2 - w / 2 * s, state.y + state.height / 2 - h / 2 * s, w * s, h * s) : new mxRectangle(state.x + w / 2 * s, state.y + h / 2 * s, w * s, h * s); } return null; }; /** * Function: redraw * * Updates the bounds or points and scale of the shapes for the given cell * state. This is called in mxGraphView.validatePoints as the last step of * updating all cells. * * Parameters: * * state - for which the shapes should be updated. * force - Optional boolean that specifies if the cell should be reconfiured * and redrawn without any additional checks. * rendering - Optional boolean that specifies if the cell should actually * be drawn into the DOM. If this is false then redraw and/or reconfigure * will not be called on the shape. */ mxCellRenderer.prototype.redraw = function(state, force, rendering) { if (state.shape != null) { var model = state.view.graph.getModel(); var isEdge = model.isEdge(state.cell); reconfigure = (force != null) ? force : false; // Handles changes of the collapse icon this.createControl(state); // Handles changes to the order in the DOM if (state.orderChanged || state.invalidOrder) { if (state.view.graph.ordered) { this.order(state); } else { // Assert state.cell is edge this.orderEdge(state); } // Required to update inherited styles reconfigure = state.orderChanged; } delete state.invalidOrder; delete state.orderChanged; // Checks if the style in the state is different from the style // in the shape and re-applies the style if required if (!reconfigure && !mxUtils.equalEntries(state.shape.style, state.style)) { reconfigure = true; } // Reconfiures the shape after an order or style change if (reconfigure) { this.configureShape(state); state.shape.reconfigure(); } // Redraws the cell if required if (force || state.shape.bounds == null || state.shape.scale != state.view.scale || !state.shape.bounds.equals(state) || !mxUtils.equalPoints(state.shape.points, state.absolutePoints)) { // FIXME: Move indicator color update into shape.redraw // var indicator = state.view.graph.getIndicatorColor(state); // if (indicator != null) // { // state.shape.indicatorColor = indicator; // } if (state.absolutePoints != null) { state.shape.points = state.absolutePoints.slice(); } else { state.shape.points = null; } state.shape.bounds = new mxRectangle( state.x, state.y, state.width, state.height); state.shape.scale = state.view.scale; if (rendering == null || rendering) { state.shape.redraw(); } else { state.shape.updateBoundingBox(); } } // Updates the text label, overlays and control if (rendering == null || rendering) { this.redrawLabel(state); this.redrawCellOverlays(state); this.redrawControl(state); } } }; /** * Function: destroy * * Destroys the shapes associated with the given cell state. * * Parameters: * * state - for which the shapes should be destroyed. */ mxCellRenderer.prototype.destroy = function(state) { if (state.shape != null) { if (state.text != null) { state.text.destroy(); state.text = null; } if (state.overlays != null) { state.overlays.visit(function(id, shape) { shape.destroy(); }); state.overlays = null; } if (state.control != null) { state.control.destroy(); state.control = null; } state.shape.destroy(); state.shape = null; } };