diff --git a/js/ui/mxgraph/src/js/util/mxSvgCanvas2D.js b/js/ui/mxgraph/src/js/util/mxSvgCanvas2D.js new file mode 100644 index 0000000..0b51bd3 --- /dev/null +++ b/js/ui/mxgraph/src/js/util/mxSvgCanvas2D.js @@ -0,0 +1,2171 @@ +/** + * Copyright (c) 2006-2015, JGraph Ltd + * Copyright (c) 2006-2015, Gaudenz Alder + */ +/** + * Class: mxSvgCanvas2D + * + * Extends 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 . + * + * 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: 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 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 . + */ +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 to . + */ +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 to . + */ +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.strokeWidth * s.scale)); + } +}; + +/** + * Function: updateStrokeAttributes + * + * Transfers the stroke attributes from to . + */ +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)); + node.setAttribute('y', this.format(y * s.scale)); + 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) == '', 5) + 1); + } + + if (val.substring(val.length - 7, val.length) == '') + { + 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, '>'); + val = ta.value.replace(/&/g, '&').replace(/&lt;/g, '<'). + replace(/&gt;/g, '>').replace(/&amp;/g, '&'). + replace(/
/g, '
').replace(/
/g, '
'). + replace(/(]+)>/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 = '
' + val + '
'; + } + 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('
' + val + '
').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 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, '
'); + + 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); +};