diff --git a/js/ui/mxgraph/src/js/view/mxCellRenderer.js b/js/ui/mxgraph/src/js/view/mxCellRenderer.js new file mode 100644 index 0000000..876315d --- /dev/null +++ b/js/ui/mxgraph/src/js/view/mxCellRenderer.js @@ -0,0 +1,1530 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * 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() { }; + +/** + * 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: defaultTextShape + * + * Defines the default shape for labels. Default is . + */ +mxCellRenderer.prototype.defaultTextShape = mxText; + +/** + * Variable: legacyControlPosition + * + * Specifies if the folding icon should ignore the horizontal + * orientation of a swimlane. Default is true. + */ +mxCellRenderer.prototype.legacyControlPosition = true; + +/** + * Variable: legacySpacing + * + * Specifies if spacing and label position should be ignored if overflow is + * fill or width. Default is true for backwards compatiblity. + */ +mxCellRenderer.prototype.legacySpacing = true; + +/** + * Variable: defaultShapes + * + * Static array that contains the globally registered shapes which are + * known to all instances of this class. For adding new shapes you should + * use the static function. + */ +mxCellRenderer.prototype.defaultShapes = new Object(); + +/** + * Variable: antiAlias + * + * Anti-aliasing option for new shapes. Default is true. + */ +mxCellRenderer.prototype.antiAlias = true; + +/** + * Variable: forceControlClickHandler + * + * Specifies if the enabled state of the graph should be ignored in the control + * click handler (to allow folding in disabled graphs). Default is false. + */ +mxCellRenderer.prototype.forceControlClickHandler = false; + +/** + * Function: registerShape + * + * Registers the given constructor under the specified key in this instance + * of the renderer. + * + * Example: + * + * (code) + * mxCellRenderer.registerShape(mxConstants.SHAPE_RECTANGLE, mxRectangleShape); + * (end) + * + * Parameters: + * + * key - String representing the shape name. + * shape - Constructor of the subclass. + */ +mxCellRenderer.registerShape = function(key, shape) +{ + mxCellRenderer.prototype.defaultShapes[key] = shape; +}; + +// Adds default shapes into the default shapes array +mxCellRenderer.registerShape(mxConstants.SHAPE_RECTANGLE, mxRectangleShape); +mxCellRenderer.registerShape(mxConstants.SHAPE_ELLIPSE, mxEllipse); +mxCellRenderer.registerShape(mxConstants.SHAPE_RHOMBUS, mxRhombus); +mxCellRenderer.registerShape(mxConstants.SHAPE_CYLINDER, mxCylinder); +mxCellRenderer.registerShape(mxConstants.SHAPE_CONNECTOR, mxConnector); +mxCellRenderer.registerShape(mxConstants.SHAPE_ACTOR, mxActor); +mxCellRenderer.registerShape(mxConstants.SHAPE_TRIANGLE, mxTriangle); +mxCellRenderer.registerShape(mxConstants.SHAPE_HEXAGON, mxHexagon); +mxCellRenderer.registerShape(mxConstants.SHAPE_CLOUD, mxCloud); +mxCellRenderer.registerShape(mxConstants.SHAPE_LINE, mxLine); +mxCellRenderer.registerShape(mxConstants.SHAPE_ARROW, mxArrow); +mxCellRenderer.registerShape(mxConstants.SHAPE_ARROW_CONNECTOR, mxArrowConnector); +mxCellRenderer.registerShape(mxConstants.SHAPE_DOUBLE_ELLIPSE, mxDoubleEllipse); +mxCellRenderer.registerShape(mxConstants.SHAPE_SWIMLANE, mxSwimlane); +mxCellRenderer.registerShape(mxConstants.SHAPE_IMAGE, mxImageShape); +mxCellRenderer.registerShape(mxConstants.SHAPE_LABEL, mxLabel); + +/** + * Function: initializeShape + * + * Initializes the shape in the given state by calling its init method with + * the correct container after configuring it using . + * + * Parameters: + * + * state - for which the shape should be initialized. + */ +mxCellRenderer.prototype.initializeShape = function(state) +{ + state.shape.dialect = state.view.graph.dialect; + this.configureShape(state); + state.shape.init(state.view.getDrawPane()); +}; + +/** + * Function: createShape + * + * Creates and returns the shape for the given cell state. + * + * Parameters: + * + * state - for which the shape should be created. + */ +mxCellRenderer.prototype.createShape = function(state) +{ + var shape = null; + + 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 stencil = mxStencilRegistry.getStencil(state.style[mxConstants.STYLE_SHAPE]); + + if (stencil != null) + { + shape = new mxShape(stencil); + } + else + { + var ctor = this.getShapeConstructor(state); + shape = new ctor(); + } + } + + return shape; +}; + +/** + * Function: createIndicatorShape + * + * Creates the indicator shape for the given cell state. + * + * Parameters: + * + * state - for which the indicator shape should be created. + */ +mxCellRenderer.prototype.createIndicatorShape = function(state) +{ + state.shape.indicatorShape = this.getShape(state.view.graph.getIndicatorShape(state)); +}; + +/** + * Function: getShape + * + * Returns the shape for the given name from . + */ +mxCellRenderer.prototype.getShape = function(name) +{ + return (name != null) ? mxCellRenderer.prototype.defaultShapes[name] : null; +}; + +/** + * Function: getShapeConstructor + * + * Returns the constructor to be used for creating the shape. + */ +mxCellRenderer.prototype.getShapeConstructor = function(state) +{ + var ctor = this.getShape(state.style[mxConstants.STYLE_SHAPE]); + + 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); + state.shape.image = state.view.graph.getImage(state); + state.shape.indicatorColor = state.view.graph.getIndicatorColor(state); + state.shape.indicatorStrokeColor = state.style[mxConstants.STYLE_INDICATOR_STROKECOLOR]; + state.shape.indicatorGradientColor = state.view.graph.getIndicatorGradientColor(state); + state.shape.indicatorDirection = state.style[mxConstants.STYLE_INDICATOR_DIRECTION]; + state.shape.indicatorImage = state.view.graph.getIndicatorImage(state); + + 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) +{ + return state.view.graph.getLabel(state.cell); +}; + +/** + * 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))); + + state.text = new this.defaultTextShape(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], + mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION)); + state.text.opacity = mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_OPACITY, 100); + state.text.dialect = (isForceHtml) ? mxConstants.DIALECT_STRICTHTML : state.view.graph.dialect; + state.text.style = state.style; + state.text.state = state; + this.initializeLabel(state, state.text); + + // Workaround for touch devices routing all events for a mouse gesture + // (down, move, up) via the initial DOM node. IE additionally 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 special touch device gestures + mxEvent.addGestureListeners(state.text.node, + 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'; + } + }), + mxUtils.bind(this, function(evt) + { + if (this.isLabelEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt))); + } + }), + mxUtils.bind(this, function(evt) + { + if (this.isLabelEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, getState(evt))); + forceGetCell = false; + } + }) + ); + + // Uses double click timeout in mxGraph for quirks mode + if (graph.nativeDblClickEnabled) + { + 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, shape) +{ + if (mxClient.IS_SVG && mxClient.NO_FO && shape.dialect != mxConstants.DIALECT_SVG) + { + shape.init(state.view.graph.container); + } + else + { + shape.init(state.view.getDrawPane()); + } +}; + +/** + * 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)); + }); + + mxEvent.addGestureListeners(shape.node, + function (evt) + { + mxEvent.consume(evt); + }, + 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.preserveImageAspect = false; + state.control.dialect = graph.dialect; + + this.initControl(state, state.control, true, this.createControlClickHandler(state)); + } + } + else if (state.control != null) + { + state.control.destroy(); + state.control = null; + } +}; + +/** + * Function: createControlClickHandler + * + * Hook for creating the click handler for the folding icon. + * + * Parameters: + * + * state - whose control click handler should be returned. + */ +mxCellRenderer.prototype.createControlClickHandler = function(state) +{ + var graph = state.view.graph; + + return mxUtils.bind(this, function (evt) + { + if (this.forceControlClickHandler || graph.isEnabled()) + { + var collapse = !graph.isCellCollapsed(state.cell); + graph.foldCells(collapse, false, [state.cell], null, evt); + mxEvent.consume(evt); + } + }); +}; + +/** + * 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; + + // Workaround for missing click event on iOS is to check tolerance below + if (clickHandler != null && !mxClient.IS_IOS) + { + if (graph.isEnabled()) + { + node.style.cursor = 'pointer'; + } + + mxEvent.addListener(node, 'click', clickHandler); + } + + if (handleEvents) + { + var first = null; + + mxEvent.addGestureListeners(node, + function (evt) + { + first = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt)); + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state)); + mxEvent.consume(evt); + }, + function (evt) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, state)); + }, + function (evt) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, state)); + mxEvent.consume(evt); + }); + + // Uses capture phase for event interception to stop bubble phase + if (clickHandler != null && mxClient.IS_IOS) + { + node.addEventListener('touchend', function(evt) + { + if (first != null) + { + var tol = graph.tolerance; + + if (Math.abs(first.x - mxEvent.getClientX(evt)) < tol && + Math.abs(first.y - mxEvent.getClientY(evt)) < tol) + { + clickHandler.call(clickHandler, evt); + mxEvent.consume(evt); + } + } + }, true); + } + } + + 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; + + // 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; + }; + + mxEvent.addGestureListeners(state.shape.node, + mxUtils.bind(this, function(evt) + { + if (this.isShapeEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state)); + } + }), + mxUtils.bind(this, function(evt) + { + if (this.isShapeEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt))); + } + }), + mxUtils.bind(this, function(evt) + { + if (this.isShapeEvent(state, evt)) + { + graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, getState(evt))); + } + }) + ); + + // Uses double click timeout in mxGraph for quirks mode + if (graph.nativeDblClickEnabled) + { + mxEvent.addListener(state.shape.node, 'dblclick', + mxUtils.bind(this, function(evt) + { + if (this.isShapeEvent(state, evt)) + { + graph.dblClick(evt, 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, forced) +{ + var value = this.getLabelValue(state); + + 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; + + // Forced is true if the style has changed, so to get the updated + // result in getLabelBounds we apply the new style to the shape + if (forced) + { + // Checks if a full repaint is needed + if (state.text.lastValue != null && this.isTextShapeInvalid(state, state.text)) + { + // Forces a full repaint + state.text.lastValue = null; + } + + state.text.resetStyles(); + state.text.apply(state); + + // Special case opacity which is taken from textOpacity style for text + state.text.opacity = mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_OPACITY, 100); + } + + var bounds = this.getLabelBounds(state); + var wrapping = graph.isWrapping(state.cell); + var clipping = graph.isLabelClipped(state.cell); + var isForceHtml = (state.view.graph.isHtmlLabel(state.cell) || (value != null && mxUtils.isNode(value))); + var dialect = (isForceHtml) ? mxConstants.DIALECT_STRICTHTML : state.view.graph.dialect; + + // Text is a special case where change of dialect is possible at runtime + var overflow = state.style[mxConstants.STYLE_OVERFLOW] || 'visible'; + + if (forced || state.text.value != value || state.text.isWrapping != wrapping || + state.text.overflow != overflow || state.text.isClipping != clipping || + state.text.scale != this.getTextScale(state) || state.text.dialect != dialect || + !state.text.bounds.equals(bounds)) + { + state.text.dialect = dialect; + state.text.value = value; + state.text.bounds = bounds; + state.text.scale = this.getTextScale(state); + state.text.wrap = wrapping; + state.text.clipped = clipping; + state.text.overflow = overflow; + + // Preserves visible state + var vis = state.text.node.style.visibility; + this.redrawLabelShape(state.text); + state.text.node.style.visibility = vis; + } + } +}; + +/** + * Function: isTextShapeInvalid + * + * Returns true if the style for the text shape has changed. + * + * Parameters: + * + * state - whose label should be checked. + * shape - shape to be checked. + */ +mxCellRenderer.prototype.isTextShapeInvalid = function(state, shape) +{ + function check(property, stylename, defaultValue) + { + return shape[property] != (state.style[stylename] || defaultValue); + }; + + return check('fontStyle', mxConstants.STYLE_FONTSTYLE, mxConstants.DEFAULT_FONTSTYLE) || + check('family', mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY) || + check('size', mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE) || + check('color', mxConstants.STYLE_FONTCOLOR, 'black') || + check('align', mxConstants.STYLE_ALIGN, '') || + check('valign', mxConstants.STYLE_VERTICAL_ALIGN, '') || + check('spacing', mxConstants.STYLE_SPACING, 2) || + check('spacingTop', mxConstants.STYLE_SPACING_TOP, 2) || + check('spacingRight', mxConstants.STYLE_SPACING_RIGHT, 2) || + check('spacingBottom', mxConstants.STYLE_SPACING_BOTTOM, 2) || + check('spacingLeft', mxConstants.STYLE_SPACING_LEFT, 2) || + check('horizontal', mxConstants.STYLE_HORIZONTAL, true) || + check('background', mxConstants.STYLE_LABEL_BACKGROUNDCOLOR) || + check('border', mxConstants.STYLE_LABEL_BORDERCOLOR) || + check('opacity', mxConstants.STYLE_TEXT_OPACITY, 100) || + check('textDirection', mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION); +}; + +/** + * Function: redrawLabelShape + * + * Called to invoked redraw on the given text shape. + * + * Parameters: + * + * shape - shape to be redrawn. + */ +mxCellRenderer.prototype.redrawLabelShape = function(shape) +{ + shape.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 scale = state.view.scale; + var isEdge = graph.getModel().isEdge(state.cell); + var bounds = new mxRectangle(state.absoluteOffset.x, state.absoluteOffset.y); + + if (isEdge) + { + var spacing = state.text.getSpacing(); + bounds.x += spacing.x * scale; + bounds.y += spacing.y * scale; + + var geo = graph.getCellGeometry(state.cell); + + if (geo != null) + { + bounds.width = Math.max(0, geo.width * scale); + bounds.height = Math.max(0, geo.height * scale); + } + } + else + { + // Inverts label position + if (state.text.isPaintBoundsInverted()) + { + var tmp = bounds.x; + bounds.x = bounds.y; + bounds.y = tmp; + } + + 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); + + var sc = mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE); + + if (sc != mxConstants.NONE && sc != '') + { + var s = parseFloat(mxUtils.getValue(state.style, mxConstants.STYLE_STROKEWIDTH, 1)) * scale; + var dx = 1 + Math.floor((s - 1) / 2); + var dh = Math.floor(s + 1); + + bounds.x += dx; + bounds.y += dx; + bounds.width -= dh; + bounds.height -= dh; + } + } + + if (state.text.isPaintBoundsInverted()) + { + // Rotates around center of state + var t = (state.width - state.height) / 2; + bounds.x += t; + bounds.y -= t; + var tmp = bounds.width; + bounds.width = bounds.height; + bounds.height = tmp; + } + + // Shape can modify its label bounds + if (state.shape != null) + { + var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER); + var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE); + + if (hpos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_MIDDLE) + { + bounds = state.shape.getLabelBounds(bounds); + } + } + + // Label width style overrides actual label width + var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null); + + if (lw != null) + { + bounds.width = parseFloat(lw) * scale; + } + + if (!isEdge) + { + this.rotateLabelBounds(state, bounds); + } + + return bounds; +}; + +/** + * Function: rotateLabelBounds + * + * Adds the shape rotation to the given label bounds and + * applies the alignment and offsets. + * + * Parameters: + * + * state - whose label bounds should be rotated. + * bounds - the rectangle to be rotated. + */ +mxCellRenderer.prototype.rotateLabelBounds = function(state, bounds) +{ + bounds.y -= state.text.margin.y * bounds.height; + bounds.x -= state.text.margin.x * bounds.width; + + if (!this.legacySpacing || (state.style[mxConstants.STYLE_OVERFLOW] != 'fill' && state.style[mxConstants.STYLE_OVERFLOW] != 'width')) + { + var s = state.view.scale; + var spacing = state.text.getSpacing(); + bounds.x += spacing.x * s; + bounds.y += spacing.y * s; + + var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER); + var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE); + var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null); + + bounds.width = Math.max(0, bounds.width - ((hpos == mxConstants.ALIGN_CENTER && lw == null) ? (state.text.spacingLeft * s + state.text.spacingRight * s) : 0)); + bounds.height = Math.max(0, bounds.height - ((vpos == mxConstants.ALIGN_MIDDLE) ? (state.text.spacingTop * s + state.text.spacingBottom * s) : 0)); + } + + var theta = state.text.getTextRotation(); + + // Only needed if rotated around another center + if (theta != 0 && state != null && state.view.graph.model.isVertex(state.cell)) + { + var cx = state.getCenterX(); + var cy = state.getCenterY(); + + if (bounds.x != cx || bounds.y != cy) + { + var rad = theta * (Math.PI / 180); + pt = mxUtils.getRotatedPoint(new mxPoint(bounds.x, bounds.y), + Math.cos(rad), Math.sin(rad), new mxPoint(cx, cy)); + + bounds.x = pt.x; + bounds.y = pt.y; + } + } +}; + +/** + * Function: redrawCellOverlays + * + * Redraws the overlays for the given cell state. + * + * Parameters: + * + * state - whose overlays should be redrawn. + */ +mxCellRenderer.prototype.redrawCellOverlays = function(state, forced) +{ + this.createCellOverlays(state); + + if (state.overlays != null) + { + var rot = mxUtils.mod(mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0), 90); + var rad = mxUtils.toRadians(rot); + var cos = Math.cos(rad); + var sin = Math.sin(rad); + + state.overlays.visit(function(id, shape) + { + var bounds = shape.overlay.getBounds(state); + + if (!state.view.graph.getModel().isEdge(state.cell)) + { + if (state.shape != null && rot != 0) + { + var cx = bounds.getCenterX(); + var cy = bounds.getCenterY(); + + var point = mxUtils.getRotatedPoint(new mxPoint(cx, cy), cos, sin, + new mxPoint(state.getCenterX(), state.getCenterY())); + + cx = point.x; + cy = point.y; + bounds.x = Math.round(cx - bounds.width / 2); + bounds.y = Math.round(cy - bounds.height / 2); + } + } + + if (forced || 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, forced) +{ + var image = state.view.graph.getFoldingImage(state); + + if (state.control != null && image != null) + { + var bounds = this.getControlBounds(state, image.width, image.height); + var r = (this.legacyControlPosition) ? + mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0) : + state.shape.getTextRotation(); + var s = state.view.scale; + + if (forced || state.control.scale != s || !state.control.bounds.equals(bounds) || + state.control.rotation != r) + { + state.control.rotation = r; + 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, w, h) +{ + if (state.control != null) + { + var s = state.view.scale; + var cx = state.getCenterX(); + var cy = state.getCenterY(); + + if (!state.view.graph.getModel().isEdge(state.cell)) + { + cx = state.x + w * s; + cy = state.y + h * s; + + if (state.shape != null) + { + // TODO: Factor out common code + var rot = state.shape.getShapeRotation(); + + if (this.legacyControlPosition) + { + rot = mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0); + } + else + { + if (state.shape.isPaintBoundsInverted()) + { + var t = (state.width - state.height) / 2; + cx += t; + cy -= t; + } + } + + if (rot != 0) + { + var rad = mxUtils.toRadians(rot); + var cos = Math.cos(rad); + var sin = Math.sin(rad); + + var point = mxUtils.getRotatedPoint(new mxPoint(cx, cy), cos, sin, + new mxPoint(state.getCenterX(), state.getCenterY())); + cx = point.x; + cy = point.y; + } + } + } + + return (state.view.graph.getModel().isEdge(state.cell)) ? + new mxRectangle(Math.round(cx - w / 2 * s), Math.round(cy - h / 2 * s), Math.round(w * s), Math.round(h * s)) + : new mxRectangle(Math.round(cx - w / 2 * s), Math.round(cy - h / 2 * s), Math.round(w * s), Math.round(h * s)); + } + + return null; +}; + +/** + * Function: insertStateAfter + * + * Inserts the given array of after the given nodes in the DOM. + * + * Parameters: + * + * shapes - Array of to be inserted. + * node - Node in after which the shapes should be inserted. + * htmlNode - Node in the graph container after which the shapes should be inserted that + * will not go into the (eg. HTML labels without foreignObjects). + */ +mxCellRenderer.prototype.insertStateAfter = function(state, node, htmlNode) +{ + var shapes = this.getShapesForState(state); + + for (var i = 0; i < shapes.length; i++) + { + if (shapes[i] != null && shapes[i].node != null) + { + var html = shapes[i].node.parentNode != state.view.getDrawPane() && + shapes[i].node.parentNode != state.view.getOverlayPane(); + var temp = (html) ? htmlNode : node; + + if (temp != null && temp.nextSibling != shapes[i].node) + { + if (temp.nextSibling == null) + { + temp.parentNode.appendChild(shapes[i].node); + } + else + { + temp.parentNode.insertBefore(shapes[i].node, temp.nextSibling); + } + } + else if (temp == null) + { + // Special case: First HTML node should be first sibling after canvas + if (shapes[i].node.parentNode == state.view.graph.container) + { + var canvas = state.view.canvas; + + while (canvas != null && canvas.parentNode != state.view.graph.container) + { + canvas = canvas.parentNode; + } + + if (canvas != null && canvas.nextSibling != null) + { + if (canvas.nextSibling != shapes[i].node) + { + shapes[i].node.parentNode.insertBefore(shapes[i].node, canvas.nextSibling); + } + } + else + { + shapes[i].node.parentNode.appendChild(shapes[i].node); + } + } + else if (shapes[i].node.parentNode.firstChild != null && shapes[i].node.parentNode.firstChild != shapes[i].node) + { + // Inserts the node as the first child of the parent to implement the order + shapes[i].node.parentNode.insertBefore(shapes[i].node, shapes[i].node.parentNode.firstChild); + } + } + + if (html) + { + htmlNode = shapes[i].node; + } + else + { + node = shapes[i].node; + } + } + } + + return [node, htmlNode]; +}; + +/** + * Function: getShapesForState + * + * Returns the for the given cell state in the order in which they should + * appear in the DOM. + * + * Parameters: + * + * state - whose shapes should be returned. + */ +mxCellRenderer.prototype.getShapesForState = function(state) +{ + return [state.shape, state.text, state.control]; +}; + +/** + * 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) +{ + var shapeChanged = this.redrawShape(state, force, rendering); + + if (state.shape != null && (rendering == null || rendering)) + { + this.redrawLabel(state, shapeChanged); + this.redrawCellOverlays(state, shapeChanged); + this.redrawControl(state, shapeChanged); + } +}; + +/** + * Function: redrawShape + * + * Redraws the shape for the given cell state. + * + * Parameters: + * + * state - whose label should be redrawn. + */ +mxCellRenderer.prototype.redrawShape = function(state, force, rendering) +{ + var model = state.view.graph.model; + var shapeChanged = false; + + // Forces creation of new shape if shape style has changed + if (state.shape != null && state.shape.style != null && state.style != null && + state.shape.style[mxConstants.STYLE_SHAPE] != state.style[mxConstants.STYLE_SHAPE]) + { + state.shape.destroy(); + state.shape = null; + } + + if (state.shape == null && state.view.graph.container != null && + state.cell != state.view.currentRoot && + (model.isVertex(state.cell) || model.isEdge(state.cell))) + { + state.shape = this.createShape(state); + + if (state.shape != null) + { + state.shape.antiAlias = this.antiAlias; + + this.createIndicatorShape(state); + this.initializeShape(state); + this.createCellOverlays(state); + this.installListeners(state); + + // Forces a refresh of the handler of one exists + state.view.graph.selectionCellsHandler.updateHandler(state); + } + } + else if (state.shape != null && !mxUtils.equalEntries(state.shape.style, state.style)) + { + state.shape.resetStyles(); + this.configureShape(state); + // LATER: Ignore update for realtime to fix reset of current gesture + state.view.graph.selectionCellsHandler.updateHandler(state); + force = true; + } + + if (state.shape != null) + { + // Handles changes of the collapse icon + this.createControl(state); + + // Redraws the cell if required, ignores changes to bounds if points are + // defined as the bounds are updated for the given points inside the shape + if (force || state.shape.bounds == null || state.shape.scale != state.view.scale || + (state.absolutePoints == null && !state.shape.bounds.equals(state)) || + (state.absolutePoints != null && !mxUtils.equalPoints(state.shape.points, state.absolutePoints))) + { + if (state.absolutePoints != null) + { + state.shape.points = state.absolutePoints.slice(); + state.shape.bounds = null; + } + 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(); + } + + shapeChanged = true; + } + } + + return shapeChanged; +}; + +/** + * 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; + } +};