airavata-kubernetes/web-console/src/assets/js/util/mxSvgCanvas2D.js (1,413 lines of code) (raw):

/** * 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(/&amp;/g, '&amp;amp;'). replace(/&#60;/g, '&amp;lt;').replace(/&#62;/g, '&amp;gt;'). replace(/&lt;/g, '&amp;lt;').replace(/&gt;/g, '&amp;gt;'). replace(/</g, '&lt;').replace(/>/g, '&gt;'); val = ta.value.replace(/&/g, '&amp;').replace(/&amp;lt;/g, '&lt;'). replace(/&amp;gt;/g, '&gt;').replace(/&amp;amp;/g, '&amp;'). 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); };