| /** |
| * Copyright (c) 2006-2015, JGraph Ltd |
| * Copyright (c) 2006-2015, Gaudenz Alder |
| */ |
| /** |
| * Class: mxSvgCanvas2D |
| * |
| * Extends <mxAbstractCanvas2D> to implement a canvas for SVG. This canvas writes all |
| * calls as SVG output to the given SVG root node. |
| * |
| * (code) |
| * var svgDoc = mxUtils.createXmlDocument(); |
| * var root = (svgDoc.createElementNS != null) ? |
| * svgDoc.createElementNS(mxConstants.NS_SVG, 'svg') : svgDoc.createElement('svg'); |
| * |
| * if (svgDoc.createElementNS == null) |
| * { |
| * root.setAttribute('xmlns', mxConstants.NS_SVG); |
| * root.setAttribute('xmlns:xlink', mxConstants.NS_XLINK); |
| * } |
| * else |
| * { |
| * root.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', mxConstants.NS_XLINK); |
| * } |
| * |
| * var bounds = graph.getGraphBounds(); |
| * root.setAttribute('width', (bounds.x + bounds.width + 4) + 'px'); |
| * root.setAttribute('height', (bounds.y + bounds.height + 4) + 'px'); |
| * root.setAttribute('version', '1.1'); |
| * |
| * svgDoc.appendChild(root); |
| * |
| * var svgCanvas = new mxSvgCanvas2D(root); |
| * (end) |
| * |
| * A description of the public API is available in <mxXmlCanvas2D>. |
| * |
| * To disable anti-aliasing in the output, use the following code. |
| * |
| * (code) |
| * graph.view.canvas.ownerSVGElement.setAttribute('shape-rendering', 'crispEdges'); |
| * (end) |
| * |
| * Or set the respective attribute in the SVG element directly. |
| * |
| * Constructor: mxSvgCanvas2D |
| * |
| * Constructs a new SVG canvas. |
| * |
| * Parameters: |
| * |
| * root - SVG container for the output. |
| * styleEnabled - Optional boolean that specifies if a style section should be |
| * added. The style section sets the default font-size, font-family and |
| * stroke-miterlimit globally. Default is false. |
| */ |
| function mxSvgCanvas2D(root, styleEnabled) |
| { |
| mxAbstractCanvas2D.call(this); |
| |
| /** |
| * Variable: root |
| * |
| * Reference to the container for the SVG content. |
| */ |
| this.root = root; |
| |
| /** |
| * Variable: gradients |
| * |
| * Local cache of gradients for quick lookups. |
| */ |
| this.gradients = []; |
| |
| /** |
| * Variable: defs |
| * |
| * Reference to the defs section of the SVG document. Only for export. |
| */ |
| this.defs = null; |
| |
| /** |
| * Variable: styleEnabled |
| * |
| * Stores the value of styleEnabled passed to the constructor. |
| */ |
| this.styleEnabled = (styleEnabled != null) ? styleEnabled : false; |
| |
| var svg = null; |
| |
| // Adds optional defs section for export |
| if (root.ownerDocument != document) |
| { |
| var node = root; |
| |
| // Finds owner SVG element in XML DOM |
| while (node != null && node.nodeName != 'svg') |
| { |
| node = node.parentNode; |
| } |
| |
| svg = node; |
| } |
| |
| if (svg != null) |
| { |
| // Tries to get existing defs section |
| var tmp = svg.getElementsByTagName('defs'); |
| |
| if (tmp.length > 0) |
| { |
| this.defs = svg.getElementsByTagName('defs')[0]; |
| } |
| |
| // Adds defs section if none exists |
| if (this.defs == null) |
| { |
| this.defs = this.createElement('defs'); |
| |
| if (svg.firstChild != null) |
| { |
| svg.insertBefore(this.defs, svg.firstChild); |
| } |
| else |
| { |
| svg.appendChild(this.defs); |
| } |
| } |
| |
| // Adds stylesheet |
| if (this.styleEnabled) |
| { |
| this.defs.appendChild(this.createStyle()); |
| } |
| } |
| }; |
| |
| /** |
| * Extends mxAbstractCanvas2D |
| */ |
| mxUtils.extend(mxSvgCanvas2D, mxAbstractCanvas2D); |
| |
| /** |
| * Capability check for DOM parser. |
| */ |
| (function() |
| { |
| mxSvgCanvas2D.prototype.useDomParser = !mxClient.IS_IE && typeof DOMParser === 'function' && typeof XMLSerializer === 'function'; |
| |
| if (mxSvgCanvas2D.prototype.useDomParser) |
| { |
| // Checks using a generic test text if the parsing actually works. This is a workaround |
| // for older browsers where the capability check returns true but the parsing fails. |
| try |
| { |
| var doc = new DOMParser().parseFromString('test text', 'text/html'); |
| mxSvgCanvas2D.prototype.useDomParser = doc != null; |
| } |
| catch (e) |
| { |
| mxSvgCanvas2D.prototype.useDomParser = false; |
| } |
| } |
| })(); |
| |
| /** |
| * Variable: path |
| * |
| * Holds the current DOM node. |
| */ |
| mxSvgCanvas2D.prototype.node = null; |
| |
| /** |
| * Variable: matchHtmlAlignment |
| * |
| * Specifies if plain text output should match the vertical HTML alignment. |
| * Defaul is true. |
| */ |
| mxSvgCanvas2D.prototype.matchHtmlAlignment = true; |
| |
| /** |
| * Variable: textEnabled |
| * |
| * Specifies if text output should be enabled. Default is true. |
| */ |
| mxSvgCanvas2D.prototype.textEnabled = true; |
| |
| /** |
| * Variable: foEnabled |
| * |
| * Specifies if use of foreignObject for HTML markup is allowed. Default is true. |
| */ |
| mxSvgCanvas2D.prototype.foEnabled = true; |
| |
| /** |
| * Variable: foAltText |
| * |
| * Specifies the fallback text for unsupported foreignObjects in exported |
| * documents. Default is '[Object]'. If this is set to null then no fallback |
| * text is added to the exported document. |
| */ |
| mxSvgCanvas2D.prototype.foAltText = '[Object]'; |
| |
| /** |
| * Variable: foOffset |
| * |
| * Offset to be used for foreignObjects. |
| */ |
| mxSvgCanvas2D.prototype.foOffset = 0; |
| |
| /** |
| * Variable: textOffset |
| * |
| * Offset to be used for text elements. |
| */ |
| mxSvgCanvas2D.prototype.textOffset = 0; |
| |
| /** |
| * Variable: imageOffset |
| * |
| * Offset to be used for image elements. |
| */ |
| mxSvgCanvas2D.prototype.imageOffset = 0; |
| |
| /** |
| * Variable: strokeTolerance |
| * |
| * Adds transparent paths for strokes. |
| */ |
| mxSvgCanvas2D.prototype.strokeTolerance = 0; |
| |
| /** |
| * Variable: refCount |
| * |
| * Local counter for references in SVG export. |
| */ |
| mxSvgCanvas2D.prototype.refCount = 0; |
| |
| /** |
| * Variable: blockImagePointerEvents |
| * |
| * Specifies if a transparent rectangle should be added on top of images to absorb |
| * all pointer events. Default is false. This is only needed in Firefox to disable |
| * control-clicks on images. |
| */ |
| mxSvgCanvas2D.prototype.blockImagePointerEvents = false; |
| |
| /** |
| * Variable: lineHeightCorrection |
| * |
| * Correction factor for <mxConstants.LINE_HEIGHT> in HTML output. Default is 1. |
| */ |
| mxSvgCanvas2D.prototype.lineHeightCorrection = 1; |
| |
| /** |
| * Variable: pointerEventsValue |
| * |
| * Default value for active pointer events. Default is all. |
| */ |
| mxSvgCanvas2D.prototype.pointerEventsValue = 'all'; |
| |
| /** |
| * Variable: fontMetricsPadding |
| * |
| * Padding to be added for text that is not wrapped to account for differences |
| * in font metrics on different platforms in pixels. Default is 10. |
| */ |
| mxSvgCanvas2D.prototype.fontMetricsPadding = 10; |
| |
| /** |
| * Variable: cacheOffsetSize |
| * |
| * Specifies if offsetWidth and offsetHeight should be cached. Default is true. |
| * This is used to speed up repaint of text in <updateText>. |
| */ |
| mxSvgCanvas2D.prototype.cacheOffsetSize = true; |
| |
| /** |
| * Function: format |
| * |
| * Rounds all numbers to 2 decimal points. |
| */ |
| mxSvgCanvas2D.prototype.format = function(value) |
| { |
| return parseFloat(parseFloat(value).toFixed(2)); |
| }; |
| |
| /** |
| * Function: getBaseUrl |
| * |
| * Returns the URL of the page without the hash part. This needs to use href to |
| * include any search part with no params (ie question mark alone). This is a |
| * workaround for the fact that window.location.search is empty if there is |
| * no search string behind the question mark. |
| */ |
| mxSvgCanvas2D.prototype.getBaseUrl = function() |
| { |
| var href = window.location.href; |
| var hash = href.lastIndexOf('#'); |
| |
| if (hash > 0) |
| { |
| href = href.substring(0, hash); |
| } |
| |
| return href; |
| }; |
| |
| /** |
| * Function: reset |
| * |
| * Returns any offsets for rendering pixels. |
| */ |
| mxSvgCanvas2D.prototype.reset = function() |
| { |
| mxAbstractCanvas2D.prototype.reset.apply(this, arguments); |
| this.gradients = []; |
| }; |
| |
| /** |
| * Function: createStyle |
| * |
| * Creates the optional style section. |
| */ |
| mxSvgCanvas2D.prototype.createStyle = function(x) |
| { |
| var style = this.createElement('style'); |
| style.setAttribute('type', 'text/css'); |
| mxUtils.write(style, 'svg{font-family:' + mxConstants.DEFAULT_FONTFAMILY + |
| ';font-size:' + mxConstants.DEFAULT_FONTSIZE + |
| ';fill:none;stroke-miterlimit:10}'); |
| |
| return style; |
| }; |
| |
| /** |
| * Function: createElement |
| * |
| * Private helper function to create SVG elements |
| */ |
| mxSvgCanvas2D.prototype.createElement = function(tagName, namespace) |
| { |
| if (this.root.ownerDocument.createElementNS != null) |
| { |
| return this.root.ownerDocument.createElementNS(namespace || mxConstants.NS_SVG, tagName); |
| } |
| else |
| { |
| var elt = this.root.ownerDocument.createElement(tagName); |
| |
| if (namespace != null) |
| { |
| elt.setAttribute('xmlns', namespace); |
| } |
| |
| return elt; |
| } |
| }; |
| |
| /** |
| * Function: getAlternateContent |
| * |
| * Returns the alternate content for the given foreignObject. |
| */ |
| mxSvgCanvas2D.prototype.createAlternateContent = function(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation) |
| { |
| if (this.foAltText != null) |
| { |
| var s = this.state; |
| var alt = this.createElement('text'); |
| alt.setAttribute('x', Math.round(w / 2)); |
| alt.setAttribute('y', Math.round((h + s.fontSize) / 2)); |
| alt.setAttribute('fill', s.fontColor || 'black'); |
| alt.setAttribute('text-anchor', 'middle'); |
| alt.setAttribute('font-size', s.fontSize + 'px'); |
| alt.setAttribute('font-family', s.fontFamily); |
| |
| if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) |
| { |
| alt.setAttribute('font-weight', 'bold'); |
| } |
| |
| if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) |
| { |
| alt.setAttribute('font-style', 'italic'); |
| } |
| |
| if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) |
| { |
| alt.setAttribute('text-decoration', 'underline'); |
| } |
| |
| mxUtils.write(alt, this.foAltText); |
| |
| return alt; |
| } |
| else |
| { |
| return null; |
| } |
| }; |
| |
| /** |
| * Function: createGradientId |
| * |
| * Private helper function to create SVG elements |
| */ |
| mxSvgCanvas2D.prototype.createGradientId = function(start, end, alpha1, alpha2, direction) |
| { |
| // Removes illegal characters from gradient ID |
| if (start.charAt(0) == '#') |
| { |
| start = start.substring(1); |
| } |
| |
| if (end.charAt(0) == '#') |
| { |
| end = end.substring(1); |
| } |
| |
| // Workaround for gradient IDs not working in Safari 5 / Chrome 6 |
| // if they contain uppercase characters |
| start = start.toLowerCase() + '-' + alpha1; |
| end = end.toLowerCase() + '-' + alpha2; |
| |
| // Wrong gradient directions possible? |
| var dir = null; |
| |
| if (direction == null || direction == mxConstants.DIRECTION_SOUTH) |
| { |
| dir = 's'; |
| } |
| else if (direction == mxConstants.DIRECTION_EAST) |
| { |
| dir = 'e'; |
| } |
| else |
| { |
| var tmp = start; |
| start = end; |
| end = tmp; |
| |
| if (direction == mxConstants.DIRECTION_NORTH) |
| { |
| dir = 's'; |
| } |
| else if (direction == mxConstants.DIRECTION_WEST) |
| { |
| dir = 'e'; |
| } |
| } |
| |
| return 'mx-gradient-' + start + '-' + end + '-' + dir; |
| }; |
| |
| /** |
| * Function: getSvgGradient |
| * |
| * Private helper function to create SVG elements |
| */ |
| mxSvgCanvas2D.prototype.getSvgGradient = function(start, end, alpha1, alpha2, direction) |
| { |
| var id = this.createGradientId(start, end, alpha1, alpha2, direction); |
| var gradient = this.gradients[id]; |
| |
| if (gradient == null) |
| { |
| var svg = this.root.ownerSVGElement; |
| |
| var counter = 0; |
| var tmpId = id + '-' + counter; |
| |
| if (svg != null) |
| { |
| gradient = svg.ownerDocument.getElementById(tmpId); |
| |
| while (gradient != null && gradient.ownerSVGElement != svg) |
| { |
| tmpId = id + '-' + counter++; |
| gradient = svg.ownerDocument.getElementById(tmpId); |
| } |
| } |
| else |
| { |
| // Uses shorter IDs for export |
| tmpId = 'id' + (++this.refCount); |
| } |
| |
| if (gradient == null) |
| { |
| gradient = this.createSvgGradient(start, end, alpha1, alpha2, direction); |
| gradient.setAttribute('id', tmpId); |
| |
| if (this.defs != null) |
| { |
| this.defs.appendChild(gradient); |
| } |
| else |
| { |
| svg.appendChild(gradient); |
| } |
| } |
| |
| this.gradients[id] = gradient; |
| } |
| |
| return gradient.getAttribute('id'); |
| }; |
| |
| /** |
| * Function: createSvgGradient |
| * |
| * Creates the given SVG gradient. |
| */ |
| mxSvgCanvas2D.prototype.createSvgGradient = function(start, end, alpha1, alpha2, direction) |
| { |
| var gradient = this.createElement('linearGradient'); |
| gradient.setAttribute('x1', '0%'); |
| gradient.setAttribute('y1', '0%'); |
| gradient.setAttribute('x2', '0%'); |
| gradient.setAttribute('y2', '0%'); |
| |
| if (direction == null || direction == mxConstants.DIRECTION_SOUTH) |
| { |
| gradient.setAttribute('y2', '100%'); |
| } |
| else if (direction == mxConstants.DIRECTION_EAST) |
| { |
| gradient.setAttribute('x2', '100%'); |
| } |
| else if (direction == mxConstants.DIRECTION_NORTH) |
| { |
| gradient.setAttribute('y1', '100%'); |
| } |
| else if (direction == mxConstants.DIRECTION_WEST) |
| { |
| gradient.setAttribute('x1', '100%'); |
| } |
| |
| var op = (alpha1 < 1) ? ';stop-opacity:' + alpha1 : ''; |
| |
| var stop = this.createElement('stop'); |
| stop.setAttribute('offset', '0%'); |
| stop.setAttribute('style', 'stop-color:' + start + op); |
| gradient.appendChild(stop); |
| |
| op = (alpha2 < 1) ? ';stop-opacity:' + alpha2 : ''; |
| |
| stop = this.createElement('stop'); |
| stop.setAttribute('offset', '100%'); |
| stop.setAttribute('style', 'stop-color:' + end + op); |
| gradient.appendChild(stop); |
| |
| return gradient; |
| }; |
| |
| /** |
| * Function: addNode |
| * |
| * Private helper function to create SVG elements |
| */ |
| mxSvgCanvas2D.prototype.addNode = function(filled, stroked) |
| { |
| var node = this.node; |
| var s = this.state; |
| |
| if (node != null) |
| { |
| if (node.nodeName == 'path') |
| { |
| // Checks if the path is not empty |
| if (this.path != null && this.path.length > 0) |
| { |
| node.setAttribute('d', this.path.join(' ')); |
| } |
| else |
| { |
| return; |
| } |
| } |
| |
| if (filled && s.fillColor != null) |
| { |
| this.updateFill(); |
| } |
| else if (!this.styleEnabled) |
| { |
| // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=814952 |
| if (node.nodeName == 'ellipse' && mxClient.IS_FF) |
| { |
| node.setAttribute('fill', 'transparent'); |
| } |
| else |
| { |
| node.setAttribute('fill', 'none'); |
| } |
| |
| // Sets the actual filled state for stroke tolerance |
| filled = false; |
| } |
| |
| if (stroked && s.strokeColor != null) |
| { |
| this.updateStroke(); |
| } |
| else if (!this.styleEnabled) |
| { |
| node.setAttribute('stroke', 'none'); |
| } |
| |
| if (s.transform != null && s.transform.length > 0) |
| { |
| node.setAttribute('transform', s.transform); |
| } |
| |
| if (s.shadow) |
| { |
| this.root.appendChild(this.createShadow(node)); |
| } |
| |
| // Adds stroke tolerance |
| if (this.strokeTolerance > 0 && !filled) |
| { |
| this.root.appendChild(this.createTolerance(node)); |
| } |
| |
| // Adds pointer events |
| if (this.pointerEvents && (node.nodeName != 'path' || |
| this.path[this.path.length - 1] == this.closeOp)) |
| { |
| node.setAttribute('pointer-events', this.pointerEventsValue); |
| } |
| // Enables clicks for nodes inside a link element |
| else if (!this.pointerEvents && this.originalRoot == null) |
| { |
| node.setAttribute('pointer-events', 'none'); |
| } |
| |
| // Removes invisible nodes from output if they don't handle events |
| if ((node.nodeName != 'rect' && node.nodeName != 'path' && node.nodeName != 'ellipse') || |
| (node.getAttribute('fill') != 'none' && node.getAttribute('fill') != 'transparent') || |
| node.getAttribute('stroke') != 'none' || node.getAttribute('pointer-events') != 'none') |
| { |
| // LATER: Update existing DOM for performance |
| this.root.appendChild(node); |
| } |
| |
| this.node = null; |
| } |
| }; |
| |
| /** |
| * Function: updateFill |
| * |
| * Transfers the stroke attributes from <state> to <node>. |
| */ |
| mxSvgCanvas2D.prototype.updateFill = function() |
| { |
| var s = this.state; |
| |
| if (s.alpha < 1 || s.fillAlpha < 1) |
| { |
| this.node.setAttribute('fill-opacity', s.alpha * s.fillAlpha); |
| } |
| |
| if (s.fillColor != null) |
| { |
| if (s.gradientColor != null) |
| { |
| var id = this.getSvgGradient(s.fillColor, s.gradientColor, s.gradientFillAlpha, s.gradientAlpha, s.gradientDirection); |
| |
| if (!mxClient.IS_CHROME_APP && !mxClient.IS_IE && !mxClient.IS_IE11 && |
| !mxClient.IS_EDGE && this.root.ownerDocument == document) |
| { |
| // Workaround for potential base tag and brackets must be escaped |
| var base = this.getBaseUrl().replace(/([\(\)])/g, '\\$1'); |
| this.node.setAttribute('fill', 'url(' + base + '#' + id + ')'); |
| } |
| else |
| { |
| this.node.setAttribute('fill', 'url(#' + id + ')'); |
| } |
| } |
| else |
| { |
| this.node.setAttribute('fill', s.fillColor.toLowerCase()); |
| } |
| } |
| }; |
| |
| /** |
| * Function: getCurrentStrokeWidth |
| * |
| * Returns the current stroke width (>= 1), ie. max(1, this.format(this.state.strokeWidth * this.state.scale)). |
| */ |
| mxSvgCanvas2D.prototype.getCurrentStrokeWidth = function() |
| { |
| return Math.max(1, this.format(this.state.strokeWidth * this.state.scale)); |
| }; |
| |
| /** |
| * Function: updateStroke |
| * |
| * Transfers the stroke attributes from <state> to <node>. |
| */ |
| mxSvgCanvas2D.prototype.updateStroke = function() |
| { |
| var s = this.state; |
| |
| this.node.setAttribute('stroke', s.strokeColor.toLowerCase()); |
| |
| if (s.alpha < 1 || s.strokeAlpha < 1) |
| { |
| this.node.setAttribute('stroke-opacity', s.alpha * s.strokeAlpha); |
| } |
| |
| var sw = this.getCurrentStrokeWidth(); |
| |
| if (sw != 1) |
| { |
| this.node.setAttribute('stroke-width', sw); |
| } |
| |
| if (this.node.nodeName == 'path') |
| { |
| this.updateStrokeAttributes(); |
| } |
| |
| if (s.dashed) |
| { |
| this.node.setAttribute('stroke-dasharray', this.createDashPattern( |
| ((s.fixDash) ? 1 : s.strokeWidth) * s.scale)); |
| } |
| }; |
| |
| /** |
| * Function: updateStrokeAttributes |
| * |
| * Transfers the stroke attributes from <state> to <node>. |
| */ |
| mxSvgCanvas2D.prototype.updateStrokeAttributes = function() |
| { |
| var s = this.state; |
| |
| // Linejoin miter is default in SVG |
| if (s.lineJoin != null && s.lineJoin != 'miter') |
| { |
| this.node.setAttribute('stroke-linejoin', s.lineJoin); |
| } |
| |
| if (s.lineCap != null) |
| { |
| // flat is called butt in SVG |
| var value = s.lineCap; |
| |
| if (value == 'flat') |
| { |
| value = 'butt'; |
| } |
| |
| // Linecap butt is default in SVG |
| if (value != 'butt') |
| { |
| this.node.setAttribute('stroke-linecap', value); |
| } |
| } |
| |
| // Miterlimit 10 is default in our document |
| if (s.miterLimit != null && (!this.styleEnabled || s.miterLimit != 10)) |
| { |
| this.node.setAttribute('stroke-miterlimit', s.miterLimit); |
| } |
| }; |
| |
| /** |
| * Function: createDashPattern |
| * |
| * Creates the SVG dash pattern for the given state. |
| */ |
| mxSvgCanvas2D.prototype.createDashPattern = function(scale) |
| { |
| var pat = []; |
| |
| if (typeof(this.state.dashPattern) === 'string') |
| { |
| var dash = this.state.dashPattern.split(' '); |
| |
| if (dash.length > 0) |
| { |
| for (var i = 0; i < dash.length; i++) |
| { |
| pat[i] = Number(dash[i]) * scale; |
| } |
| } |
| } |
| |
| return pat.join(' '); |
| }; |
| |
| /** |
| * Function: createTolerance |
| * |
| * Creates a hit detection tolerance shape for the given node. |
| */ |
| mxSvgCanvas2D.prototype.createTolerance = function(node) |
| { |
| var tol = node.cloneNode(true); |
| var sw = parseFloat(tol.getAttribute('stroke-width') || 1) + this.strokeTolerance; |
| tol.setAttribute('pointer-events', 'stroke'); |
| tol.setAttribute('visibility', 'hidden'); |
| tol.removeAttribute('stroke-dasharray'); |
| tol.setAttribute('stroke-width', sw); |
| tol.setAttribute('fill', 'none'); |
| |
| // Workaround for Opera ignoring the visiblity attribute above while |
| // other browsers need a stroke color to perform the hit-detection but |
| // do not ignore the visibility attribute. Side-effect is that Opera's |
| // hit detection for horizontal/vertical edges seems to ignore the tol. |
| tol.setAttribute('stroke', (mxClient.IS_OT) ? 'none' : 'white'); |
| |
| return tol; |
| }; |
| |
| /** |
| * Function: createShadow |
| * |
| * Creates a shadow for the given node. |
| */ |
| mxSvgCanvas2D.prototype.createShadow = function(node) |
| { |
| var shadow = node.cloneNode(true); |
| var s = this.state; |
| |
| // Firefox uses transparent for no fill in ellipses |
| if (shadow.getAttribute('fill') != 'none' && (!mxClient.IS_FF || shadow.getAttribute('fill') != 'transparent')) |
| { |
| shadow.setAttribute('fill', s.shadowColor); |
| } |
| |
| if (shadow.getAttribute('stroke') != 'none') |
| { |
| shadow.setAttribute('stroke', s.shadowColor); |
| } |
| |
| shadow.setAttribute('transform', 'translate(' + this.format(s.shadowDx * s.scale) + |
| ',' + this.format(s.shadowDy * s.scale) + ')' + (s.transform || '')); |
| shadow.setAttribute('opacity', s.shadowAlpha); |
| |
| return shadow; |
| }; |
| |
| /** |
| * Function: setLink |
| * |
| * Experimental implementation for hyperlinks. |
| */ |
| mxSvgCanvas2D.prototype.setLink = function(link) |
| { |
| if (link == null) |
| { |
| this.root = this.originalRoot; |
| } |
| else |
| { |
| this.originalRoot = this.root; |
| |
| var node = this.createElement('a'); |
| |
| // Workaround for implicit namespace handling in HTML5 export, IE adds NS1 namespace so use code below |
| // in all IE versions except quirks mode. KNOWN: Adds xlink namespace to each image tag in output. |
| if (node.setAttributeNS == null || (this.root.ownerDocument != document && document.documentMode == null)) |
| { |
| node.setAttribute('xlink:href', link); |
| } |
| else |
| { |
| node.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', link); |
| } |
| |
| this.root.appendChild(node); |
| this.root = node; |
| } |
| }; |
| |
| /** |
| * Function: rotate |
| * |
| * Sets the rotation of the canvas. Note that rotation cannot be concatenated. |
| */ |
| mxSvgCanvas2D.prototype.rotate = function(theta, flipH, flipV, cx, cy) |
| { |
| if (theta != 0 || flipH || flipV) |
| { |
| var s = this.state; |
| cx += s.dx; |
| cy += s.dy; |
| |
| cx *= s.scale; |
| cy *= s.scale; |
| |
| s.transform = s.transform || ''; |
| |
| // This implementation uses custom scale/translate and built-in rotation |
| // Rotation state is part of the AffineTransform in state.transform |
| if (flipH && flipV) |
| { |
| theta += 180; |
| } |
| else if (flipH != flipV) |
| { |
| var tx = (flipH) ? cx : 0; |
| var sx = (flipH) ? -1 : 1; |
| |
| var ty = (flipV) ? cy : 0; |
| var sy = (flipV) ? -1 : 1; |
| |
| s.transform += 'translate(' + this.format(tx) + ',' + this.format(ty) + ')' + |
| 'scale(' + this.format(sx) + ',' + this.format(sy) + ')' + |
| 'translate(' + this.format(-tx) + ',' + this.format(-ty) + ')'; |
| } |
| |
| if (flipH ? !flipV : flipV) |
| { |
| theta *= -1; |
| } |
| |
| if (theta != 0) |
| { |
| s.transform += 'rotate(' + this.format(theta) + ',' + this.format(cx) + ',' + this.format(cy) + ')'; |
| } |
| |
| s.rotation = s.rotation + theta; |
| s.rotationCx = cx; |
| s.rotationCy = cy; |
| } |
| }; |
| |
| /** |
| * Function: begin |
| * |
| * Extends superclass to create path. |
| */ |
| mxSvgCanvas2D.prototype.begin = function() |
| { |
| mxAbstractCanvas2D.prototype.begin.apply(this, arguments); |
| this.node = this.createElement('path'); |
| }; |
| |
| /** |
| * Function: rect |
| * |
| * Private helper function to create SVG elements |
| */ |
| mxSvgCanvas2D.prototype.rect = function(x, y, w, h) |
| { |
| var s = this.state; |
| var n = this.createElement('rect'); |
| n.setAttribute('x', this.format((x + s.dx) * s.scale)); |
| n.setAttribute('y', this.format((y + s.dy) * s.scale)); |
| n.setAttribute('width', this.format(w * s.scale)); |
| n.setAttribute('height', this.format(h * s.scale)); |
| |
| this.node = n; |
| }; |
| |
| /** |
| * Function: roundrect |
| * |
| * Private helper function to create SVG elements |
| */ |
| mxSvgCanvas2D.prototype.roundrect = function(x, y, w, h, dx, dy) |
| { |
| this.rect(x, y, w, h); |
| |
| if (dx > 0) |
| { |
| this.node.setAttribute('rx', this.format(dx * this.state.scale)); |
| } |
| |
| if (dy > 0) |
| { |
| this.node.setAttribute('ry', this.format(dy * this.state.scale)); |
| } |
| }; |
| |
| /** |
| * Function: ellipse |
| * |
| * Private helper function to create SVG elements |
| */ |
| mxSvgCanvas2D.prototype.ellipse = function(x, y, w, h) |
| { |
| var s = this.state; |
| var n = this.createElement('ellipse'); |
| // No rounding for consistent output with 1.x |
| n.setAttribute('cx', Math.round((x + w / 2 + s.dx) * s.scale)); |
| n.setAttribute('cy', Math.round((y + h / 2 + s.dy) * s.scale)); |
| n.setAttribute('rx', w / 2 * s.scale); |
| n.setAttribute('ry', h / 2 * s.scale); |
| this.node = n; |
| }; |
| |
| /** |
| * Function: image |
| * |
| * Private helper function to create SVG elements |
| */ |
| mxSvgCanvas2D.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV) |
| { |
| src = this.converter.convert(src); |
| |
| // LATER: Add option for embedding images as base64. |
| aspect = (aspect != null) ? aspect : true; |
| flipH = (flipH != null) ? flipH : false; |
| flipV = (flipV != null) ? flipV : false; |
| |
| var s = this.state; |
| x += s.dx; |
| y += s.dy; |
| |
| var node = this.createElement('image'); |
| node.setAttribute('x', this.format(x * s.scale) + this.imageOffset); |
| node.setAttribute('y', this.format(y * s.scale) + this.imageOffset); |
| node.setAttribute('width', this.format(w * s.scale)); |
| node.setAttribute('height', this.format(h * s.scale)); |
| |
| // Workaround for missing namespace support |
| if (node.setAttributeNS == null) |
| { |
| node.setAttribute('xlink:href', src); |
| } |
| else |
| { |
| node.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', src); |
| } |
| |
| if (!aspect) |
| { |
| node.setAttribute('preserveAspectRatio', 'none'); |
| } |
| |
| if (s.alpha < 1 || s.fillAlpha < 1) |
| { |
| node.setAttribute('opacity', s.alpha * s.fillAlpha); |
| } |
| |
| var tr = this.state.transform || ''; |
| |
| if (flipH || flipV) |
| { |
| var sx = 1; |
| var sy = 1; |
| var dx = 0; |
| var dy = 0; |
| |
| if (flipH) |
| { |
| sx = -1; |
| dx = -w - 2 * x; |
| } |
| |
| if (flipV) |
| { |
| sy = -1; |
| dy = -h - 2 * y; |
| } |
| |
| // Adds image tansformation to existing transform |
| tr += 'scale(' + sx + ',' + sy + ')translate(' + (dx * s.scale) + ',' + (dy * s.scale) + ')'; |
| } |
| |
| if (tr.length > 0) |
| { |
| node.setAttribute('transform', tr); |
| } |
| |
| if (!this.pointerEvents) |
| { |
| node.setAttribute('pointer-events', 'none'); |
| } |
| |
| this.root.appendChild(node); |
| |
| // Disables control-clicks on images in Firefox to open in new tab |
| // by putting a rect in the foreground that absorbs all events and |
| // disabling all pointer-events on the original image tag. |
| if (this.blockImagePointerEvents) |
| { |
| node.setAttribute('style', 'pointer-events:none'); |
| |
| node = this.createElement('rect'); |
| node.setAttribute('visibility', 'hidden'); |
| node.setAttribute('pointer-events', 'fill'); |
| node.setAttribute('x', this.format(x * s.scale)); |
| node.setAttribute('y', this.format(y * s.scale)); |
| node.setAttribute('width', this.format(w * s.scale)); |
| node.setAttribute('height', this.format(h * s.scale)); |
| this.root.appendChild(node); |
| } |
| }; |
| |
| /** |
| * Function: convertHtml |
| * |
| * Converts the given HTML string to XHTML. |
| */ |
| mxSvgCanvas2D.prototype.convertHtml = function(val) |
| { |
| if (this.useDomParser) |
| { |
| var doc = new DOMParser().parseFromString(val, 'text/html'); |
| |
| if (doc != null) |
| { |
| val = new XMLSerializer().serializeToString(doc.body); |
| |
| // Extracts body content from DOM |
| if (val.substring(0, 5) == '<body') |
| { |
| val = val.substring(val.indexOf('>', 5) + 1); |
| } |
| |
| if (val.substring(val.length - 7, val.length) == '</body>') |
| { |
| val = val.substring(0, val.length - 7); |
| } |
| } |
| } |
| else if (document.implementation != null && document.implementation.createDocument != null) |
| { |
| var xd = document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null); |
| var xb = xd.createElement('body'); |
| xd.documentElement.appendChild(xb); |
| |
| var div = document.createElement('div'); |
| div.innerHTML = val; |
| var child = div.firstChild; |
| |
| while (child != null) |
| { |
| var next = child.nextSibling; |
| xb.appendChild(xd.adoptNode(child)); |
| child = next; |
| } |
| |
| return xb.innerHTML; |
| } |
| else |
| { |
| var ta = document.createElement('textarea'); |
| |
| // Handles special HTML entities < and > and double escaping |
| // and converts unclosed br, hr and img tags to XHTML |
| // LATER: Convert all unclosed tags |
| ta.innerHTML = val.replace(/&/g, '&amp;'). |
| replace(/</g, '&lt;').replace(/>/g, '&gt;'). |
| replace(/</g, '&lt;').replace(/>/g, '&gt;'). |
| replace(/</g, '<').replace(/>/g, '>'); |
| val = ta.value.replace(/&/g, '&').replace(/&lt;/g, '<'). |
| replace(/&gt;/g, '>').replace(/&amp;/g, '&'). |
| replace(/<br>/g, '<br />').replace(/<hr>/g, '<hr />'). |
| replace(/(<img[^>]+)>/gm, "$1 />"); |
| } |
| |
| return val; |
| }; |
| |
| /** |
| * Function: createDiv |
| * |
| * Private helper function to create SVG elements |
| */ |
| mxSvgCanvas2D.prototype.createDiv = function(str, align, valign, style, overflow) |
| { |
| var s = this.state; |
| |
| // Inline block for rendering HTML background over SVG in Safari |
| var lh = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (s.fontSize * mxConstants.LINE_HEIGHT) + 'px' : |
| (mxConstants.LINE_HEIGHT * this.lineHeightCorrection); |
| |
| style = 'display:inline-block;font-size:' + s.fontSize + 'px;font-family:' + s.fontFamily + |
| ';color:' + s.fontColor + ';line-height:' + lh + ';' + style; |
| |
| if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) |
| { |
| style += 'font-weight:bold;'; |
| } |
| |
| if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) |
| { |
| style += 'font-style:italic;'; |
| } |
| |
| if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) |
| { |
| style += 'text-decoration:underline;'; |
| } |
| |
| if (align == mxConstants.ALIGN_CENTER) |
| { |
| style += 'text-align:center;'; |
| } |
| else if (align == mxConstants.ALIGN_RIGHT) |
| { |
| style += 'text-align:right;'; |
| } |
| |
| var css = ''; |
| |
| if (s.fontBackgroundColor != null) |
| { |
| css += 'background-color:' + s.fontBackgroundColor + ';'; |
| } |
| |
| if (s.fontBorderColor != null) |
| { |
| css += 'border:1px solid ' + s.fontBorderColor + ';'; |
| } |
| |
| var val = str; |
| |
| if (!mxUtils.isNode(val)) |
| { |
| val = this.convertHtml(val); |
| |
| if (overflow != 'fill' && overflow != 'width') |
| { |
| // Inner div always needed to measure wrapped text |
| val = '<div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;' + css + '">' + val + '</div>'; |
| } |
| else |
| { |
| style += css; |
| } |
| } |
| |
| // Uses DOM API where available. This cannot be used in IE to avoid |
| // an opening and two (!) closing TBODY tags being added to tables. |
| if (!mxClient.IS_IE && document.createElementNS) |
| { |
| var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); |
| div.setAttribute('style', style); |
| |
| if (mxUtils.isNode(val)) |
| { |
| // Creates a copy for export |
| if (this.root.ownerDocument != document) |
| { |
| div.appendChild(val.cloneNode(true)); |
| } |
| else |
| { |
| div.appendChild(val); |
| } |
| } |
| else |
| { |
| div.innerHTML = val; |
| } |
| |
| return div; |
| } |
| else |
| { |
| // Serializes for export |
| if (mxUtils.isNode(val) && this.root.ownerDocument != document) |
| { |
| val = val.outerHTML; |
| } |
| |
| // NOTE: FF 3.6 crashes if content CSS contains "height:100%" |
| return mxUtils.parseXml('<div xmlns="http://www.w3.org/1999/xhtml" style="' + style + |
| '">' + val + '</div>').documentElement; |
| } |
| }; |
| |
| /** |
| * Invalidates the cached offset size for the given node. |
| */ |
| mxSvgCanvas2D.prototype.invalidateCachedOffsetSize = function(node) |
| { |
| delete node.firstChild.mxCachedOffsetWidth; |
| delete node.firstChild.mxCachedFinalOffsetWidth; |
| delete node.firstChild.mxCachedFinalOffsetHeight; |
| }; |
| |
| /** |
| * Updates existing DOM nodes for text rendering. LATER: Merge common parts with text function below. |
| */ |
| mxSvgCanvas2D.prototype.updateText = function(x, y, w, h, align, valign, wrap, overflow, clip, rotation, node) |
| { |
| if (node != null && node.firstChild != null && node.firstChild.firstChild != null && |
| node.firstChild.firstChild.firstChild != null) |
| { |
| // Uses outer group for opacity and transforms to |
| // fix rendering order in Chrome |
| var group = node.firstChild; |
| var fo = group.firstChild; |
| var div = fo.firstChild; |
| |
| rotation = (rotation != null) ? rotation : 0; |
| |
| var s = this.state; |
| x += s.dx; |
| y += s.dy; |
| |
| if (clip) |
| { |
| div.style.maxHeight = Math.round(h) + 'px'; |
| div.style.maxWidth = Math.round(w) + 'px'; |
| } |
| else if (overflow == 'fill') |
| { |
| div.style.width = Math.round(w + 1) + 'px'; |
| div.style.height = Math.round(h + 1) + 'px'; |
| } |
| else if (overflow == 'width') |
| { |
| div.style.width = Math.round(w + 1) + 'px'; |
| |
| if (h > 0) |
| { |
| div.style.maxHeight = Math.round(h) + 'px'; |
| } |
| } |
| |
| if (wrap && w > 0) |
| { |
| div.style.width = Math.round(w + 1) + 'px'; |
| } |
| |
| // Code that depends on the size which is computed after |
| // the element was added to the DOM. |
| var ow = 0; |
| var oh = 0; |
| |
| // Padding avoids clipping on border and wrapping for differing font metrics on platforms |
| var padX = 2; |
| var padY = 2; |
| |
| var sizeDiv = div; |
| |
| if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV') |
| { |
| sizeDiv = sizeDiv.firstChild; |
| } |
| |
| var tmp = (group.mxCachedOffsetWidth != null) ? group.mxCachedOffsetWidth : sizeDiv.offsetWidth; |
| ow = tmp + padX; |
| |
| // Recomputes the height of the element for wrapped width |
| if (wrap && overflow != 'fill') |
| { |
| if (clip) |
| { |
| ow = Math.min(ow, w); |
| } |
| |
| div.style.width = ow + 'px'; |
| } |
| |
| ow = ((group.mxCachedFinalOffsetWidth != null) ? group.mxCachedFinalOffsetWidth : |
| sizeDiv.offsetWidth) + padX; |
| oh = ((group.mxCachedFinalOffsetHeight != null) ? group.mxCachedFinalOffsetHeight : |
| sizeDiv.offsetHeight) - 2; |
| |
| if (clip) |
| { |
| oh = Math.min(oh, h); |
| ow = Math.min(ow, w); |
| } |
| |
| if (overflow == 'width') |
| { |
| h = oh; |
| } |
| else if (overflow != 'fill') |
| { |
| w = ow; |
| h = oh; |
| } |
| |
| var dx = 0; |
| var dy = 0; |
| |
| if (align == mxConstants.ALIGN_CENTER) |
| { |
| dx -= w / 2; |
| } |
| else if (align == mxConstants.ALIGN_RIGHT) |
| { |
| dx -= w; |
| } |
| |
| x += dx; |
| |
| // FIXME: LINE_HEIGHT not ideal for all text sizes, fix for export |
| if (valign == mxConstants.ALIGN_MIDDLE) |
| { |
| dy -= h / 2; |
| } |
| else if (valign == mxConstants.ALIGN_BOTTOM) |
| { |
| dy -= h; |
| } |
| |
| // Workaround for rendering offsets |
| // TODO: Check if export needs these fixes, too |
| if (overflow != 'fill' && mxClient.IS_FF && mxClient.IS_WIN) |
| { |
| dy -= 2; |
| } |
| |
| y += dy; |
| |
| var tr = (s.scale != 1) ? 'scale(' + s.scale + ')' : ''; |
| |
| if (s.rotation != 0 && this.rotateHtml) |
| { |
| tr += 'rotate(' + (s.rotation) + ',' + (w / 2) + ',' + (h / 2) + ')'; |
| var pt = this.rotatePoint((x + w / 2) * s.scale, (y + h / 2) * s.scale, |
| s.rotation, s.rotationCx, s.rotationCy); |
| x = pt.x - w * s.scale / 2; |
| y = pt.y - h * s.scale / 2; |
| } |
| else |
| { |
| x *= s.scale; |
| y *= s.scale; |
| } |
| |
| if (rotation != 0) |
| { |
| tr += 'rotate(' + (rotation) + ',' + (-dx) + ',' + (-dy) + ')'; |
| } |
| |
| group.setAttribute('transform', 'translate(' + Math.round(x) + ',' + Math.round(y) + ')' + tr); |
| fo.setAttribute('width', Math.round(Math.max(1, w))); |
| fo.setAttribute('height', Math.round(Math.max(1, h))); |
| } |
| }; |
| |
| /** |
| * Function: text |
| * |
| * Paints the given text. Possible values for format are empty string for plain |
| * text and html for HTML markup. Note that HTML markup is only supported if |
| * foreignObject is supported and <foEnabled> is true. (This means IE9 and later |
| * does currently not support HTML text as part of shapes.) |
| */ |
| mxSvgCanvas2D.prototype.text = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir) |
| { |
| if (this.textEnabled && str != null) |
| { |
| rotation = (rotation != null) ? rotation : 0; |
| |
| var s = this.state; |
| x += s.dx; |
| y += s.dy; |
| |
| if (this.foEnabled && format == 'html') |
| { |
| var style = 'vertical-align:top;'; |
| |
| if (clip) |
| { |
| style += 'overflow:hidden;max-height:' + Math.round(h) + 'px;max-width:' + Math.round(w) + 'px;'; |
| } |
| else if (overflow == 'fill') |
| { |
| style += 'width:' + Math.round(w + 1) + 'px;height:' + Math.round(h + 1) + 'px;overflow:hidden;'; |
| } |
| else if (overflow == 'width') |
| { |
| style += 'width:' + Math.round(w + 1) + 'px;'; |
| |
| if (h > 0) |
| { |
| style += 'max-height:' + Math.round(h) + 'px;overflow:hidden;'; |
| } |
| } |
| |
| if (wrap && w > 0) |
| { |
| style += 'width:' + Math.round(w + 1) + 'px;white-space:normal;word-wrap:' + |
| mxConstants.WORD_WRAP + ';'; |
| } |
| else |
| { |
| style += 'white-space:nowrap;'; |
| } |
| |
| // Uses outer group for opacity and transforms to |
| // fix rendering order in Chrome |
| var group = this.createElement('g'); |
| |
| if (s.alpha < 1) |
| { |
| group.setAttribute('opacity', s.alpha); |
| } |
| |
| var fo = this.createElement('foreignObject'); |
| fo.setAttribute('style', 'overflow:visible;'); |
| fo.setAttribute('pointer-events', 'all'); |
| |
| var div = this.createDiv(str, align, valign, style, overflow); |
| |
| // Ignores invalid XHTML labels |
| if (div == null) |
| { |
| return; |
| } |
| else if (dir != null) |
| { |
| div.setAttribute('dir', dir); |
| } |
| |
| group.appendChild(fo); |
| this.root.appendChild(group); |
| |
| // Code that depends on the size which is computed after |
| // the element was added to the DOM. |
| var ow = 0; |
| var oh = 0; |
| |
| // Padding avoids clipping on border and wrapping for differing font metrics on platforms |
| var padX = 2; |
| var padY = 2; |
| |
| // NOTE: IE is always export as it does not support foreign objects |
| if (mxClient.IS_IE && (document.documentMode == 9 || !mxClient.IS_SVG)) |
| { |
| // Handles non-standard namespace for getting size in IE |
| var clone = document.createElement('div'); |
| |
| clone.style.cssText = div.getAttribute('style'); |
| clone.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; |
| clone.style.position = 'absolute'; |
| clone.style.visibility = 'hidden'; |
| |
| // Inner DIV is needed for text measuring |
| var div2 = document.createElement('div'); |
| div2.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; |
| div2.style.wordWrap = mxConstants.WORD_WRAP; |
| div2.innerHTML = (mxUtils.isNode(str)) ? str.outerHTML : str; |
| clone.appendChild(div2); |
| |
| document.body.appendChild(clone); |
| |
| // Workaround for different box models |
| if (document.documentMode != 8 && document.documentMode != 9 && s.fontBorderColor != null) |
| { |
| padX += 2; |
| padY += 2; |
| } |
| |
| if (wrap && w > 0) |
| { |
| var tmp = div2.offsetWidth; |
| |
| // Workaround for adding padding twice in IE8/IE9 standards mode if label is wrapped |
| var padDx = 0; |
| |
| // For export, if no wrapping occurs, we add a large padding to make |
| // sure there is no wrapping even if the text metrics are different. |
| // This adds support for text metrics on different operating systems. |
| // Disables wrapping if text is not wrapped for given width |
| if (!clip && wrap && w > 0 && this.root.ownerDocument != document && overflow != 'fill') |
| { |
| var ws = clone.style.whiteSpace; |
| div2.style.whiteSpace = 'nowrap'; |
| |
| if (tmp < div2.offsetWidth) |
| { |
| clone.style.whiteSpace = ws; |
| } |
| } |
| |
| if (clip) |
| { |
| tmp = Math.min(tmp, w); |
| } |
| |
| clone.style.width = tmp + 'px'; |
| |
| // Padding avoids clipping on border |
| ow = div2.offsetWidth + padX + padDx; |
| oh = div2.offsetHeight + padY; |
| |
| // Overrides the width of the DIV via XML DOM by using the |
| // clone DOM style, getting the CSS text for that and |
| // then setting that on the DIV via setAttribute |
| clone.style.display = 'inline-block'; |
| clone.style.position = ''; |
| clone.style.visibility = ''; |
| clone.style.width = ow + 'px'; |
| |
| div.setAttribute('style', clone.style.cssText); |
| } |
| else |
| { |
| // Padding avoids clipping on border |
| ow = div2.offsetWidth + padX; |
| oh = div2.offsetHeight + padY; |
| } |
| |
| clone.parentNode.removeChild(clone); |
| fo.appendChild(div); |
| } |
| else |
| { |
| // Uses document for text measuring during export |
| if (this.root.ownerDocument != document) |
| { |
| div.style.visibility = 'hidden'; |
| document.body.appendChild(div); |
| } |
| else |
| { |
| fo.appendChild(div); |
| } |
| |
| var sizeDiv = div; |
| |
| if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName == 'DIV') |
| { |
| sizeDiv = sizeDiv.firstChild; |
| |
| if (wrap && div.style.wordWrap == 'break-word') |
| { |
| sizeDiv.style.width = '100%'; |
| } |
| } |
| |
| var tmp = sizeDiv.offsetWidth; |
| |
| // Workaround for text measuring in hidden containers |
| if (tmp == 0 && div.parentNode == fo) |
| { |
| div.style.visibility = 'hidden'; |
| document.body.appendChild(div); |
| |
| tmp = sizeDiv.offsetWidth; |
| } |
| |
| if (this.cacheOffsetSize) |
| { |
| group.mxCachedOffsetWidth = tmp; |
| } |
| |
| // Disables wrapping if text is not wrapped for given width |
| if (!clip && wrap && w > 0 && this.root.ownerDocument != document && |
| overflow != 'fill' && overflow != 'width') |
| { |
| var ws = div.style.whiteSpace; |
| div.style.whiteSpace = 'nowrap'; |
| |
| if (tmp < sizeDiv.offsetWidth) |
| { |
| div.style.whiteSpace = ws; |
| } |
| } |
| |
| ow = tmp + padX - 1; |
| |
| // Recomputes the height of the element for wrapped width |
| if (wrap && overflow != 'fill' && overflow != 'width') |
| { |
| if (clip) |
| { |
| ow = Math.min(ow, w); |
| } |
| |
| div.style.width = ow + 'px'; |
| } |
| |
| ow = sizeDiv.offsetWidth; |
| oh = sizeDiv.offsetHeight; |
| |
| if (this.cacheOffsetSize) |
| { |
| group.mxCachedFinalOffsetWidth = ow; |
| group.mxCachedFinalOffsetHeight = oh; |
| } |
| |
| oh -= padY; |
| |
| if (div.parentNode != fo) |
| { |
| fo.appendChild(div); |
| div.style.visibility = ''; |
| } |
| } |
| |
| if (clip) |
| { |
| oh = Math.min(oh, h); |
| ow = Math.min(ow, w); |
| } |
| |
| if (overflow == 'width') |
| { |
| h = oh; |
| } |
| else if (overflow != 'fill') |
| { |
| w = ow; |
| h = oh; |
| } |
| |
| if (s.alpha < 1) |
| { |
| group.setAttribute('opacity', s.alpha); |
| } |
| |
| var dx = 0; |
| var dy = 0; |
| |
| if (align == mxConstants.ALIGN_CENTER) |
| { |
| dx -= w / 2; |
| } |
| else if (align == mxConstants.ALIGN_RIGHT) |
| { |
| dx -= w; |
| } |
| |
| x += dx; |
| |
| // FIXME: LINE_HEIGHT not ideal for all text sizes, fix for export |
| if (valign == mxConstants.ALIGN_MIDDLE) |
| { |
| dy -= h / 2; |
| } |
| else if (valign == mxConstants.ALIGN_BOTTOM) |
| { |
| dy -= h; |
| } |
| |
| // Workaround for rendering offsets |
| // TODO: Check if export needs these fixes, too |
| //if (this.root.ownerDocument == document) |
| if (overflow != 'fill' && mxClient.IS_FF && mxClient.IS_WIN) |
| { |
| dy -= 2; |
| } |
| |
| y += dy; |
| |
| var tr = (s.scale != 1) ? 'scale(' + s.scale + ')' : ''; |
| |
| if (s.rotation != 0 && this.rotateHtml) |
| { |
| tr += 'rotate(' + (s.rotation) + ',' + (w / 2) + ',' + (h / 2) + ')'; |
| var pt = this.rotatePoint((x + w / 2) * s.scale, (y + h / 2) * s.scale, |
| s.rotation, s.rotationCx, s.rotationCy); |
| x = pt.x - w * s.scale / 2; |
| y = pt.y - h * s.scale / 2; |
| } |
| else |
| { |
| x *= s.scale; |
| y *= s.scale; |
| } |
| |
| if (rotation != 0) |
| { |
| tr += 'rotate(' + (rotation) + ',' + (-dx) + ',' + (-dy) + ')'; |
| } |
| |
| group.setAttribute('transform', 'translate(' + (Math.round(x) + this.foOffset) + ',' + |
| (Math.round(y) + this.foOffset) + ')' + tr); |
| fo.setAttribute('width', Math.round(Math.max(1, w))); |
| fo.setAttribute('height', Math.round(Math.max(1, h))); |
| |
| // Adds alternate content if foreignObject not supported in viewer |
| if (this.root.ownerDocument != document) |
| { |
| var alt = this.createAlternateContent(fo, x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation); |
| |
| if (alt != null) |
| { |
| fo.setAttribute('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility'); |
| var sw = this.createElement('switch'); |
| sw.appendChild(fo); |
| sw.appendChild(alt); |
| group.appendChild(sw); |
| } |
| } |
| } |
| else |
| { |
| this.plainText(x, y, w, h, str, align, valign, wrap, overflow, clip, rotation, dir); |
| } |
| } |
| }; |
| |
| /** |
| * Function: createClip |
| * |
| * Creates a clip for the given coordinates. |
| */ |
| mxSvgCanvas2D.prototype.createClip = function(x, y, w, h) |
| { |
| x = Math.round(x); |
| y = Math.round(y); |
| w = Math.round(w); |
| h = Math.round(h); |
| |
| var id = 'mx-clip-' + x + '-' + y + '-' + w + '-' + h; |
| |
| var counter = 0; |
| var tmp = id + '-' + counter; |
| |
| // Resolves ID conflicts |
| while (document.getElementById(tmp) != null) |
| { |
| tmp = id + '-' + (++counter); |
| } |
| |
| clip = this.createElement('clipPath'); |
| clip.setAttribute('id', tmp); |
| |
| var rect = this.createElement('rect'); |
| rect.setAttribute('x', x); |
| rect.setAttribute('y', y); |
| rect.setAttribute('width', w); |
| rect.setAttribute('height', h); |
| |
| clip.appendChild(rect); |
| |
| return clip; |
| }; |
| |
| /** |
| * Function: text |
| * |
| * Paints the given text. Possible values for format are empty string for |
| * plain text and html for HTML markup. |
| */ |
| mxSvgCanvas2D.prototype.plainText = function(x, y, w, h, str, align, valign, wrap, overflow, clip, rotation, dir) |
| { |
| rotation = (rotation != null) ? rotation : 0; |
| var s = this.state; |
| var size = s.fontSize; |
| var node = this.createElement('g'); |
| var tr = s.transform || ''; |
| this.updateFont(node); |
| |
| // Non-rotated text |
| if (rotation != 0) |
| { |
| tr += 'rotate(' + rotation + ',' + this.format(x * s.scale) + ',' + this.format(y * s.scale) + ')'; |
| } |
| |
| if (dir != null) |
| { |
| node.setAttribute('direction', dir); |
| } |
| |
| if (clip && w > 0 && h > 0) |
| { |
| var cx = x; |
| var cy = y; |
| |
| if (align == mxConstants.ALIGN_CENTER) |
| { |
| cx -= w / 2; |
| } |
| else if (align == mxConstants.ALIGN_RIGHT) |
| { |
| cx -= w; |
| } |
| |
| if (overflow != 'fill') |
| { |
| if (valign == mxConstants.ALIGN_MIDDLE) |
| { |
| cy -= h / 2; |
| } |
| else if (valign == mxConstants.ALIGN_BOTTOM) |
| { |
| cy -= h; |
| } |
| } |
| |
| // LATER: Remove spacing from clip rectangle |
| var c = this.createClip(cx * s.scale - 2, cy * s.scale - 2, w * s.scale + 4, h * s.scale + 4); |
| |
| if (this.defs != null) |
| { |
| this.defs.appendChild(c); |
| } |
| else |
| { |
| // Makes sure clip is removed with referencing node |
| this.root.appendChild(c); |
| } |
| |
| if (!mxClient.IS_CHROME_APP && !mxClient.IS_IE && !mxClient.IS_IE11 && |
| !mxClient.IS_EDGE && this.root.ownerDocument == document) |
| { |
| // Workaround for potential base tag |
| var base = this.getBaseUrl().replace(/([\(\)])/g, '\\$1'); |
| node.setAttribute('clip-path', 'url(' + base + '#' + c.getAttribute('id') + ')'); |
| } |
| else |
| { |
| node.setAttribute('clip-path', 'url(#' + c.getAttribute('id') + ')'); |
| } |
| } |
| |
| // Default is left |
| var anchor = (align == mxConstants.ALIGN_RIGHT) ? 'end' : |
| (align == mxConstants.ALIGN_CENTER) ? 'middle' : |
| 'start'; |
| |
| // Text-anchor start is default in SVG |
| if (anchor != 'start') |
| { |
| node.setAttribute('text-anchor', anchor); |
| } |
| |
| if (!this.styleEnabled || size != mxConstants.DEFAULT_FONTSIZE) |
| { |
| node.setAttribute('font-size', (size * s.scale) + 'px'); |
| } |
| |
| if (tr.length > 0) |
| { |
| node.setAttribute('transform', tr); |
| } |
| |
| if (s.alpha < 1) |
| { |
| node.setAttribute('opacity', s.alpha); |
| } |
| |
| var lines = str.split('\n'); |
| var lh = Math.round(size * mxConstants.LINE_HEIGHT); |
| var textHeight = size + (lines.length - 1) * lh; |
| |
| var cy = y + size - 1; |
| |
| if (valign == mxConstants.ALIGN_MIDDLE) |
| { |
| if (overflow == 'fill') |
| { |
| cy -= h / 2; |
| } |
| else |
| { |
| var dy = ((this.matchHtmlAlignment && clip && h > 0) ? Math.min(textHeight, h) : textHeight) / 2; |
| cy -= dy + 1; |
| } |
| } |
| else if (valign == mxConstants.ALIGN_BOTTOM) |
| { |
| if (overflow == 'fill') |
| { |
| cy -= h; |
| } |
| else |
| { |
| var dy = (this.matchHtmlAlignment && clip && h > 0) ? Math.min(textHeight, h) : textHeight; |
| cy -= dy + 2; |
| } |
| } |
| |
| for (var i = 0; i < lines.length; i++) |
| { |
| // Workaround for bounding box of empty lines and spaces |
| if (lines[i].length > 0 && mxUtils.trim(lines[i]).length > 0) |
| { |
| var text = this.createElement('text'); |
| // LATER: Match horizontal HTML alignment |
| text.setAttribute('x', this.format(x * s.scale) + this.textOffset); |
| text.setAttribute('y', this.format(cy * s.scale) + this.textOffset); |
| |
| mxUtils.write(text, lines[i]); |
| node.appendChild(text); |
| } |
| |
| cy += lh; |
| } |
| |
| this.root.appendChild(node); |
| this.addTextBackground(node, str, x, y, w, (overflow == 'fill') ? h : textHeight, align, valign, overflow); |
| }; |
| |
| /** |
| * Function: updateFont |
| * |
| * Updates the text properties for the given node. (NOTE: For this to work in |
| * IE, the given node must be a text or tspan element.) |
| */ |
| mxSvgCanvas2D.prototype.updateFont = function(node) |
| { |
| var s = this.state; |
| |
| node.setAttribute('fill', s.fontColor); |
| |
| if (!this.styleEnabled || s.fontFamily != mxConstants.DEFAULT_FONTFAMILY) |
| { |
| node.setAttribute('font-family', s.fontFamily); |
| } |
| |
| if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) |
| { |
| node.setAttribute('font-weight', 'bold'); |
| } |
| |
| if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) |
| { |
| node.setAttribute('font-style', 'italic'); |
| } |
| |
| if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) |
| { |
| node.setAttribute('text-decoration', 'underline'); |
| } |
| }; |
| |
| /** |
| * Function: addTextBackground |
| * |
| * Background color and border |
| */ |
| mxSvgCanvas2D.prototype.addTextBackground = function(node, str, x, y, w, h, align, valign, overflow) |
| { |
| var s = this.state; |
| |
| if (s.fontBackgroundColor != null || s.fontBorderColor != null) |
| { |
| var bbox = null; |
| |
| if (overflow == 'fill' || overflow == 'width') |
| { |
| if (align == mxConstants.ALIGN_CENTER) |
| { |
| x -= w / 2; |
| } |
| else if (align == mxConstants.ALIGN_RIGHT) |
| { |
| x -= w; |
| } |
| |
| if (valign == mxConstants.ALIGN_MIDDLE) |
| { |
| y -= h / 2; |
| } |
| else if (valign == mxConstants.ALIGN_BOTTOM) |
| { |
| y -= h; |
| } |
| |
| bbox = new mxRectangle((x + 1) * s.scale, y * s.scale, (w - 2) * s.scale, (h + 2) * s.scale); |
| } |
| else if (node.getBBox != null && this.root.ownerDocument == document) |
| { |
| // Uses getBBox only if inside document for correct size |
| try |
| { |
| bbox = node.getBBox(); |
| var ie = mxClient.IS_IE && mxClient.IS_SVG; |
| bbox = new mxRectangle(bbox.x, bbox.y + ((ie) ? 0 : 1), bbox.width, bbox.height + ((ie) ? 1 : 0)); |
| } |
| catch (e) |
| { |
| // Ignores NS_ERROR_FAILURE in FF if container display is none. |
| } |
| } |
| else |
| { |
| // Computes size if not in document or no getBBox available |
| var div = document.createElement('div'); |
| |
| // Wrapping and clipping can be ignored here |
| div.style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? (s.fontSize * mxConstants.LINE_HEIGHT) + 'px' : mxConstants.LINE_HEIGHT; |
| div.style.fontSize = s.fontSize + 'px'; |
| div.style.fontFamily = s.fontFamily; |
| div.style.whiteSpace = 'nowrap'; |
| div.style.position = 'absolute'; |
| div.style.visibility = 'hidden'; |
| div.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block'; |
| div.style.zoom = '1'; |
| |
| if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) |
| { |
| div.style.fontWeight = 'bold'; |
| } |
| |
| if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) |
| { |
| div.style.fontStyle = 'italic'; |
| } |
| |
| str = mxUtils.htmlEntities(str, false); |
| div.innerHTML = str.replace(/\n/g, '<br/>'); |
| |
| document.body.appendChild(div); |
| var w = div.offsetWidth; |
| var h = div.offsetHeight; |
| div.parentNode.removeChild(div); |
| |
| if (align == mxConstants.ALIGN_CENTER) |
| { |
| x -= w / 2; |
| } |
| else if (align == mxConstants.ALIGN_RIGHT) |
| { |
| x -= w; |
| } |
| |
| if (valign == mxConstants.ALIGN_MIDDLE) |
| { |
| y -= h / 2; |
| } |
| else if (valign == mxConstants.ALIGN_BOTTOM) |
| { |
| y -= h; |
| } |
| |
| bbox = new mxRectangle((x + 1) * s.scale, (y + 2) * s.scale, w * s.scale, (h + 1) * s.scale); |
| } |
| |
| if (bbox != null) |
| { |
| var n = this.createElement('rect'); |
| n.setAttribute('fill', s.fontBackgroundColor || 'none'); |
| n.setAttribute('stroke', s.fontBorderColor || 'none'); |
| n.setAttribute('x', Math.floor(bbox.x - 1)); |
| n.setAttribute('y', Math.floor(bbox.y - 1)); |
| n.setAttribute('width', Math.ceil(bbox.width + 2)); |
| n.setAttribute('height', Math.ceil(bbox.height)); |
| |
| var sw = (s.fontBorderColor != null) ? Math.max(1, this.format(s.scale)) : 0; |
| n.setAttribute('stroke-width', sw); |
| |
| // Workaround for crisp rendering - only required if not exporting |
| if (this.root.ownerDocument == document && mxUtils.mod(sw, 2) == 1) |
| { |
| n.setAttribute('transform', 'translate(0.5, 0.5)'); |
| } |
| |
| node.insertBefore(n, node.firstChild); |
| } |
| } |
| }; |
| |
| /** |
| * Function: stroke |
| * |
| * Paints the outline of the current path. |
| */ |
| mxSvgCanvas2D.prototype.stroke = function() |
| { |
| this.addNode(false, true); |
| }; |
| |
| /** |
| * Function: fill |
| * |
| * Fills the current path. |
| */ |
| mxSvgCanvas2D.prototype.fill = function() |
| { |
| this.addNode(true, false); |
| }; |
| |
| /** |
| * Function: fillAndStroke |
| * |
| * Fills and paints the outline of the current path. |
| */ |
| mxSvgCanvas2D.prototype.fillAndStroke = function() |
| { |
| this.addNode(true, true); |
| }; |