tag (e.g. , )
+
+ // If documentContext (second parameter) is not specified or given as `null` or `undefined`, a new document is used.
+ // Inline events will not execute when the HTML is parsed; this includes, for example, sending GET requests for images.
+
+ // If keepScripts (last parameter) is `false`, scripts are not executed.
+ var output = $($.parseHTML('
' + html + '
', null, false));
+
+ output.find('*').each(function() { // for all nodes
+ var currentNode = this;
+
+ $.each(currentNode.attributes, function() { // for all attributes in each node
+ var currentAttribute = this;
+
+ var attrName = currentAttribute.name;
+ var attrValue = currentAttribute.value;
+
+ // Remove attribute names that start with "on" (e.g. onload, onerror...).
+ // Remove attribute values that start with "javascript:" pseudo protocol (e.g. `href="javascript:alert(1)"`).
+ if (attrName.startsWith('on') || attrValue.startsWith('javascript:') || attrValue.startsWith('data:') || attrValue.startsWith('vbscript:')) {
+ $(currentNode).removeAttr(attrName);
+ }
+ });
+ });
+
+ return output.html();
+ };
+
+ // Download `blob` as file with `fileName`.
+ // Does not work in IE9.
+ var downloadBlob = function(blob, fileName) {
+
+ if (window.navigator.msSaveBlob) { // requires IE 10+
+ // pulls up a save dialog
+ window.navigator.msSaveBlob(blob, fileName);
+
+ } else { // other browsers
+ // downloads directly in Chrome and Safari
+
+ // presents a save/open dialog in Firefox
+ // Firefox bug: `from` field in save dialog always shows `from:blob:`
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1053327
+
+ var url = window.URL.createObjectURL(blob);
+ var link = document.createElement('a');
+
+ link.href = url;
+ link.download = fileName;
+ document.body.appendChild(link);
+
+ link.click();
+
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url); // mark the url for garbage collection
+ }
+ };
+
+ // Download `dataUri` as file with `fileName`.
+ // Does not work in IE9.
+ var downloadDataUri = function(dataUri, fileName) {
+
+ var blob = dataUriToBlob(dataUri);
+ downloadBlob(blob, fileName);
+ };
+
+ // Convert an uri-encoded data component (possibly also base64-encoded) to a blob.
+ var dataUriToBlob = function(dataUri) {
+
+ // first, make sure there are no newlines in the data uri
+ dataUri = dataUri.replace(/\s/g, '');
+ dataUri = decodeURIComponent(dataUri);
+
+ var firstCommaIndex = dataUri.indexOf(','); // split dataUri as `dataTypeString`,`data`
+
+ var dataTypeString = dataUri.slice(0, firstCommaIndex); // e.g. 'data:image/jpeg;base64'
+ var mimeString = dataTypeString.split(':')[1].split(';')[0]; // e.g. 'image/jpeg'
+
+ var data = dataUri.slice(firstCommaIndex + 1);
+ var decodedString;
+ if (dataTypeString.indexOf('base64') >= 0) { // data may be encoded in base64
+ decodedString = atob(data); // decode data
+ } else {
+ // convert the decoded string to UTF-8
+ decodedString = unescape(encodeURIComponent(data));
+ }
+ // write the bytes of the string to a typed array
+ var ia = new Uint8Array(decodedString.length);
+ for (var i = 0; i < decodedString.length; i++) {
+ ia[i] = decodedString.charCodeAt(i);
+ }
+
+ return new Blob([ia], { type: mimeString }); // return the typed array as Blob
+ };
+
+ // Read an image at `url` and return it as base64-encoded data uri.
+ // The mime type of the image is inferred from the `url` file extension.
+ // If data uri is provided as `url`, it is returned back unchanged.
+ // `callback` is a method with `err` as first argument and `dataUri` as second argument.
+ // Works with IE9.
+ var imageToDataUri = function(url, callback) {
+
+ if (!url || url.substr(0, 'data:'.length) === 'data:') {
+ // No need to convert to data uri if it is already in data uri.
+
+ // This not only convenient but desired. For example,
+ // IE throws a security error if data:image/svg+xml is used to render
+ // an image to the canvas and an attempt is made to read out data uri.
+ // Now if our image is already in data uri, there is no need to render it to the canvas
+ // and so we can bypass this error.
+
+ // Keep the async nature of the function.
+ return setTimeout(function() {
+ callback(null, url);
+ }, 0);
+ }
+
+ // chrome, IE10+
+ var modernHandler = function(xhr, callback) {
+
+ if (xhr.status === 200) {
+
+ var reader = new FileReader();
+
+ reader.onload = function(evt) {
+ var dataUri = evt.target.result;
+ callback(null, dataUri);
+ };
+
+ reader.onerror = function() {
+ callback(new Error('Failed to load image ' + url));
+ };
+
+ reader.readAsDataURL(xhr.response);
+ } else {
+ callback(new Error('Failed to load image ' + url));
+ }
+ };
+
+ var legacyHandler = function(xhr, callback) {
+
+ var Uint8ToString = function(u8a) {
+ var CHUNK_SZ = 0x8000;
+ var c = [];
+ for (var i = 0; i < u8a.length; i += CHUNK_SZ) {
+ c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ)));
+ }
+ return c.join('');
+ };
+
+ if (xhr.status === 200) {
+
+ var bytes = new Uint8Array(xhr.response);
+
+ var suffix = (url.split('.').pop()) || 'png';
+ var map = {
+ 'svg': 'svg+xml'
+ };
+ var meta = 'data:image/' + (map[suffix] || suffix) + ';base64,';
+ var b64encoded = meta + btoa(Uint8ToString(bytes));
+ callback(null, b64encoded);
+ } else {
+ callback(new Error('Failed to load image ' + url));
+ }
+ };
+
+ var xhr = new XMLHttpRequest();
+
+ xhr.open('GET', url, true);
+ xhr.addEventListener('error', function() {
+ callback(new Error('Failed to load image ' + url));
+ });
+
+ xhr.responseType = window.FileReader ? 'blob' : 'arraybuffer';
+
+ xhr.addEventListener('load', function() {
+ if (window.FileReader) {
+ modernHandler(xhr, callback);
+ } else {
+ legacyHandler(xhr, callback);
+ }
+ });
+
+ xhr.send();
+ };
+
+ var getElementBBox = function(el) {
+
+ var $el = $(el);
+ if ($el.length === 0) {
+ throw new Error('Element not found');
+ }
+
+ var element = $el[0];
+ var doc = element.ownerDocument;
+ var clientBBox = element.getBoundingClientRect();
+
+ var strokeWidthX = 0;
+ var strokeWidthY = 0;
+
+ // Firefox correction
+ if (element.ownerSVGElement) {
+
+ var vel = V(element);
+ var bbox = vel.getBBox({ target: vel.svg() });
+
+ // if FF getBoundingClientRect includes stroke-width, getBBox doesn't.
+ // To unify this across all browsers we need to adjust the final bBox with `stroke-width` value.
+ strokeWidthX = (clientBBox.width - bbox.width);
+ strokeWidthY = (clientBBox.height - bbox.height);
+ }
+
+ return {
+ x: clientBBox.left + window.pageXOffset - doc.documentElement.offsetLeft + strokeWidthX / 2,
+ y: clientBBox.top + window.pageYOffset - doc.documentElement.offsetTop + strokeWidthY / 2,
+ width: clientBBox.width - strokeWidthX,
+ height: clientBBox.height - strokeWidthY
+ };
+ };
+
+
+ // Highly inspired by the jquery.sortElements plugin by Padolsey.
+ // See http://james.padolsey.com/javascript/sorting-elements-with-jquery/.
+ var sortElements = function(elements, comparator) {
+
+ var $elements = $(elements);
+ var placements = $elements.map(function() {
+
+ var sortElement = this;
+ var parentNode = sortElement.parentNode;
+ // Since the element itself will change position, we have
+ // to have some way of storing it's original position in
+ // the DOM. The easiest way is to have a 'flag' node:
+ var nextSibling = parentNode.insertBefore(document.createTextNode(''), sortElement.nextSibling);
+
+ return function() {
+
+ if (parentNode === this) {
+ throw new Error('You can\'t sort elements if any one is a descendant of another.');
+ }
+
+ // Insert before flag:
+ parentNode.insertBefore(this, nextSibling);
+ // Remove flag:
+ parentNode.removeChild(nextSibling);
+ };
+ });
+
+ return Array.prototype.sort.call($elements, comparator).each(function(i) {
+ placements[i].call(this);
+ });
+ };
+
+ // Sets attributes on the given element and its descendants based on the selector.
+ // `attrs` object: { [SELECTOR1]: { attrs1 }, [SELECTOR2]: { attrs2}, ... } e.g. { 'input': { color : 'red' }}
+ var setAttributesBySelector = function(element, attrs) {
+
+ var $element = $(element);
+
+ forIn(attrs, function(attrs, selector) {
+ var $elements = $element.find(selector).addBack().filter(selector);
+ // Make a special case for setting classes.
+ // We do not want to overwrite any existing class.
+ if (has$2(attrs, 'class')) {
+ $elements.addClass(attrs['class']);
+ attrs = omit(attrs, 'class');
+ }
+ $elements.attr(attrs);
+ });
+ };
+
+ // Return a new object with all four sides (top, right, bottom, left) in it.
+ // Value of each side is taken from the given argument (either number or object).
+ // Default value for a side is 0.
+ // Examples:
+ // normalizeSides(5) --> { top: 5, right: 5, bottom: 5, left: 5 }
+ // normalizeSides({ horizontal: 5 }) --> { top: 0, right: 5, bottom: 0, left: 5 }
+ // normalizeSides({ left: 5 }) --> { top: 0, right: 0, bottom: 0, left: 5 }
+ // normalizeSides({ horizontal: 10, left: 5 }) --> { top: 0, right: 10, bottom: 0, left: 5 }
+ // normalizeSides({ horizontal: 0, left: 5 }) --> { top: 0, right: 0, bottom: 0, left: 5 }
+ var normalizeSides = function(box) {
+
+ if (Object(box) !== box) { // `box` is not an object
+ var val = 0; // `val` left as 0 if `box` cannot be understood as finite number
+ if (isFinite(box)) { val = +box; } // actually also accepts string numbers (e.g. '100')
+
+ return { top: val, right: val, bottom: val, left: val };
+ }
+
+ // `box` is an object
+ var top, right, bottom, left;
+ top = right = bottom = left = 0;
+
+ if (isFinite(box.vertical)) { top = bottom = +box.vertical; }
+ if (isFinite(box.horizontal)) { right = left = +box.horizontal; }
+
+ if (isFinite(box.top)) { top = +box.top; } // overwrite vertical
+ if (isFinite(box.right)) { right = +box.right; } // overwrite horizontal
+ if (isFinite(box.bottom)) { bottom = +box.bottom; } // overwrite vertical
+ if (isFinite(box.left)) { left = +box.left; } // overwrite horizontal
+
+ return { top: top, right: right, bottom: bottom, left: left };
+ };
+
+ var timing = {
+
+ linear: function(t) {
+ return t;
+ },
+
+ quad: function(t) {
+ return t * t;
+ },
+
+ cubic: function(t) {
+ return t * t * t;
+ },
+
+ inout: function(t) {
+ if (t <= 0) { return 0; }
+ if (t >= 1) { return 1; }
+ var t2 = t * t;
+ var t3 = t2 * t;
+ return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75);
+ },
+
+ exponential: function(t) {
+ return Math.pow(2, 10 * (t - 1));
+ },
+
+ bounce: function(t) {
+ for (var a = 0, b = 1; 1; a += b, b /= 2) {
+ if (t >= (7 - 4 * a) / 11) {
+ var q = (11 - 6 * a - 11 * t) / 4;
+ return -q * q + b * b;
+ }
+ }
+ },
+
+ reverse: function(f) {
+ return function(t) {
+ return 1 - f(1 - t);
+ };
+ },
+
+ reflect: function(f) {
+ return function(t) {
+ return .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t)));
+ };
+ },
+
+ clamp: function(f, n, x) {
+ n = n || 0;
+ x = x || 1;
+ return function(t) {
+ var r = f(t);
+ return r < n ? n : r > x ? x : r;
+ };
+ },
+
+ back: function(s) {
+ if (!s) { s = 1.70158; }
+ return function(t) {
+ return t * t * ((s + 1) * t - s);
+ };
+ },
+
+ elastic: function(x) {
+ if (!x) { x = 1.5; }
+ return function(t) {
+ return Math.pow(2, 10 * (t - 1)) * Math.cos(20 * Math.PI * x / 3 * t);
+ };
+ }
+ };
+
+ var interpolate = {
+
+ number: function(a, b) {
+ var d = b - a;
+ return function(t) {
+ return a + d * t;
+ };
+ },
+
+ object: function(a, b) {
+ var s = Object.keys(a);
+ return function(t) {
+ var i, p;
+ var r = {};
+ for (i = s.length - 1; i != -1; i--) {
+ p = s[i];
+ r[p] = a[p] + (b[p] - a[p]) * t;
+ }
+ return r;
+ };
+ },
+
+ hexColor: function(a, b) {
+
+ var ca = parseInt(a.slice(1), 16);
+ var cb = parseInt(b.slice(1), 16);
+ var ra = ca & 0x0000ff;
+ var rd = (cb & 0x0000ff) - ra;
+ var ga = ca & 0x00ff00;
+ var gd = (cb & 0x00ff00) - ga;
+ var ba = ca & 0xff0000;
+ var bd = (cb & 0xff0000) - ba;
+
+ return function(t) {
+
+ var r = (ra + rd * t) & 0x000000ff;
+ var g = (ga + gd * t) & 0x0000ff00;
+ var b = (ba + bd * t) & 0x00ff0000;
+
+ return '#' + (1 << 24 | r | g | b).toString(16).slice(1);
+ };
+ },
+
+ unit: function(a, b) {
+
+ var r = /(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/;
+ var ma = r.exec(a);
+ var mb = r.exec(b);
+ var p = mb[1].indexOf('.');
+ var f = p > 0 ? mb[1].length - p - 1 : 0;
+ a = +ma[1];
+ var d = +mb[1] - a;
+ var u = ma[2];
+
+ return function(t) {
+ return (a + d * t).toFixed(f) + u;
+ };
+ }
+ };
+
+ // SVG filters.
+ // (values in parentheses are default values)
+ var filter = {
+
+ // `color` ... outline color ('blue')
+ // `width`... outline width (1)
+ // `opacity` ... outline opacity (1)
+ // `margin` ... gap between outline and the element (2)
+ outline: function(args) {
+
+ var tpl = '
';
+
+ var margin = Number.isFinite(args.margin) ? args.margin : 2;
+ var width = Number.isFinite(args.width) ? args.width : 1;
+
+ return template(tpl)({
+ color: args.color || 'blue',
+ opacity: Number.isFinite(args.opacity) ? args.opacity : 1,
+ outerRadius: margin + width,
+ innerRadius: margin
+ });
+ },
+
+ // `color` ... color ('red')
+ // `width`... width (1)
+ // `blur` ... blur (0)
+ // `opacity` ... opacity (1)
+ highlight: function(args) {
+
+ var tpl = '
';
+
+ return template(tpl)({
+ color: args.color || 'red',
+ width: Number.isFinite(args.width) ? args.width : 1,
+ blur: Number.isFinite(args.blur) ? args.blur : 0,
+ opacity: Number.isFinite(args.opacity) ? args.opacity : 1
+ });
+ },
+
+ // `x` ... horizontal blur (2)
+ // `y` ... vertical blur (optional)
+ blur: function(args) {
+
+ var x = Number.isFinite(args.x) ? args.x : 2;
+
+ return template('
')({
+ stdDeviation: Number.isFinite(args.y) ? [x, args.y] : x
+ });
+ },
+
+ // `dx` ... horizontal shift (0)
+ // `dy` ... vertical shift (0)
+ // `blur` ... blur (4)
+ // `color` ... color ('black')
+ // `opacity` ... opacity (1)
+ dropShadow: function(args) {
+
+ var tpl = 'SVGFEDropShadowElement' in window
+ ? '
'
+ : '
';
+
+ return template(tpl)({
+ dx: args.dx || 0,
+ dy: args.dy || 0,
+ opacity: Number.isFinite(args.opacity) ? args.opacity : 1,
+ color: args.color || 'black',
+ blur: Number.isFinite(args.blur) ? args.blur : 4
+ });
+ },
+
+ // `amount` ... the proportion of the conversion (1). A value of 1 (default) is completely grayscale. A value of 0 leaves the input unchanged.
+ grayscale: function(args) {
+
+ var amount = Number.isFinite(args.amount) ? args.amount : 1;
+
+ return template('
')({
+ a: 0.2126 + 0.7874 * (1 - amount),
+ b: 0.7152 - 0.7152 * (1 - amount),
+ c: 0.0722 - 0.0722 * (1 - amount),
+ d: 0.2126 - 0.2126 * (1 - amount),
+ e: 0.7152 + 0.2848 * (1 - amount),
+ f: 0.0722 - 0.0722 * (1 - amount),
+ g: 0.2126 - 0.2126 * (1 - amount),
+ h: 0.0722 + 0.9278 * (1 - amount)
+ });
+ },
+
+ // `amount` ... the proportion of the conversion (1). A value of 1 (default) is completely sepia. A value of 0 leaves the input unchanged.
+ sepia: function(args) {
+
+ var amount = Number.isFinite(args.amount) ? args.amount : 1;
+
+ return template('
')({
+ a: 0.393 + 0.607 * (1 - amount),
+ b: 0.769 - 0.769 * (1 - amount),
+ c: 0.189 - 0.189 * (1 - amount),
+ d: 0.349 - 0.349 * (1 - amount),
+ e: 0.686 + 0.314 * (1 - amount),
+ f: 0.168 - 0.168 * (1 - amount),
+ g: 0.272 - 0.272 * (1 - amount),
+ h: 0.534 - 0.534 * (1 - amount),
+ i: 0.131 + 0.869 * (1 - amount)
+ });
+ },
+
+ // `amount` ... the proportion of the conversion (1). A value of 0 is completely un-saturated. A value of 1 (default) leaves the input unchanged.
+ saturate: function(args) {
+
+ var amount = Number.isFinite(args.amount) ? args.amount : 1;
+
+ return template('
')({
+ amount: 1 - amount
+ });
+ },
+
+ // `angle` ... the number of degrees around the color circle the input samples will be adjusted (0).
+ hueRotate: function(args) {
+
+ return template('
')({
+ angle: args.angle || 0
+ });
+ },
+
+ // `amount` ... the proportion of the conversion (1). A value of 1 (default) is completely inverted. A value of 0 leaves the input unchanged.
+ invert: function(args) {
+
+ var amount = Number.isFinite(args.amount) ? args.amount : 1;
+
+ return template('
')({
+ amount: amount,
+ amount2: 1 - amount
+ });
+ },
+
+ // `amount` ... proportion of the conversion (1). A value of 0 will create an image that is completely black. A value of 1 (default) leaves the input unchanged.
+ brightness: function(args) {
+
+ return template('
')({
+ amount: Number.isFinite(args.amount) ? args.amount : 1
+ });
+ },
+
+ // `amount` ... proportion of the conversion (1). A value of 0 will create an image that is completely black. A value of 1 (default) leaves the input unchanged.
+ contrast: function(args) {
+
+ var amount = Number.isFinite(args.amount) ? args.amount : 1;
+
+ return template('
')({
+ amount: amount,
+ amount2: .5 - amount / 2
+ });
+ }
+ };
+
+ var format = {
+
+ // Formatting numbers via the Python Format Specification Mini-language.
+ // See http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language.
+ // Heavilly inspired by the D3.js library implementation.
+ number: function(specifier, value, locale) {
+
+ locale = locale || {
+
+ currency: ['$', ''],
+ decimal: '.',
+ thousands: ',',
+ grouping: [3]
+ };
+
+ // See Python format specification mini-language: http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language.
+ // [[fill]align][sign][symbol][0][width][,][.precision][type]
+ var re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i;
+
+ var match = re.exec(specifier);
+ var fill = match[1] || ' ';
+ var align = match[2] || '>';
+ var sign = match[3] || '';
+ var symbol = match[4] || '';
+ var zfill = match[5];
+ var width = +match[6];
+ var comma = match[7];
+ var precision = match[8];
+ var type = match[9];
+ var scale = 1;
+ var prefix = '';
+ var suffix = '';
+ var integer = false;
+
+ if (precision) { precision = +precision.substring(1); }
+
+ if (zfill || fill === '0' && align === '=') {
+ zfill = fill = '0';
+ align = '=';
+ if (comma) { width -= Math.floor((width - 1) / 4); }
+ }
+
+ switch (type) {
+ case 'n':
+ comma = true;
+ type = 'g';
+ break;
+ case '%':
+ scale = 100;
+ suffix = '%';
+ type = 'f';
+ break;
+ case 'p':
+ scale = 100;
+ suffix = '%';
+ type = 'r';
+ break;
+ case 'b':
+ case 'o':
+ case 'x':
+ case 'X':
+ if (symbol === '#') { prefix = '0' + type.toLowerCase(); }
+ break;
+ case 'c':
+ case 'd':
+ integer = true;
+ precision = 0;
+ break;
+ case 's':
+ scale = -1;
+ type = 'r';
+ break;
+ }
+
+ if (symbol === '$') {
+ prefix = locale.currency[0];
+ suffix = locale.currency[1];
+ }
+
+ // If no precision is specified for `'r'`, fallback to general notation.
+ if (type == 'r' && !precision) { type = 'g'; }
+
+ // Ensure that the requested precision is in the supported range.
+ if (precision != null) {
+ if (type == 'g') { precision = Math.max(1, Math.min(21, precision)); }
+ else if (type == 'e' || type == 'f') { precision = Math.max(0, Math.min(20, precision)); }
+ }
+
+ var zcomma = zfill && comma;
+
+ // Return the empty string for floats formatted as ints.
+ if (integer && (value % 1)) { return ''; }
+
+ // Convert negative to positive, and record the sign prefix.
+ var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, '-') : sign;
+
+ var fullSuffix = suffix;
+
+ // Apply the scale, computing it from the value's exponent for si format.
+ // Preserve the existing suffix, if any, such as the currency symbol.
+ if (scale < 0) {
+ var unit = this.prefix(value, precision);
+ value = unit.scale(value);
+ fullSuffix = unit.symbol + suffix;
+ } else {
+ value *= scale;
+ }
+
+ // Convert to the desired precision.
+ value = this.convert(type, value, precision);
+
+ // Break the value into the integer part (before) and decimal part (after).
+ var i = value.lastIndexOf('.');
+ var before = i < 0 ? value : value.substring(0, i);
+ var after = i < 0 ? '' : locale.decimal + value.substring(i + 1);
+
+ function formatGroup(value) {
+
+ var i = value.length;
+ var t = [];
+ var j = 0;
+ var g = locale.grouping[0];
+ while (i > 0 && g > 0) {
+ t.push(value.substring(i -= g, i + g));
+ g = locale.grouping[j = (j + 1) % locale.grouping.length];
+ }
+ return t.reverse().join(locale.thousands);
+ }
+
+ // If the fill character is not `'0'`, grouping is applied before padding.
+ if (!zfill && comma && locale.grouping) {
+
+ before = formatGroup(before);
+ }
+
+ var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length);
+ var padding = length < width ? new Array(length = width - length + 1).join(fill) : '';
+
+ // If the fill character is `'0'`, grouping is applied after padding.
+ if (zcomma) { before = formatGroup(padding + before); }
+
+ // Apply prefix.
+ negative += prefix;
+
+ // Rejoin integer and decimal parts.
+ value = before + after;
+
+ return (align === '<' ? negative + value + padding
+ : align === '>' ? padding + negative + value
+ : align === '^' ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length)
+ : negative + (zcomma ? value : padding + value)) + fullSuffix;
+ },
+
+ // Formatting string via the Python Format string.
+ // See https://docs.python.org/2/library/string.html#format-string-syntax)
+ string: function(formatString, value) {
+
+ var fieldDelimiterIndex;
+ var fieldDelimiter = '{';
+ var endPlaceholder = false;
+ var formattedStringArray = [];
+
+ while ((fieldDelimiterIndex = formatString.indexOf(fieldDelimiter)) !== -1) {
+
+ var pieceFormattedString, formatSpec, fieldName;
+
+ pieceFormattedString = formatString.slice(0, fieldDelimiterIndex);
+
+ if (endPlaceholder) {
+ formatSpec = pieceFormattedString.split(':');
+ fieldName = formatSpec.shift().split('.');
+ pieceFormattedString = value;
+
+ for (var i = 0; i < fieldName.length; i++)
+ { pieceFormattedString = pieceFormattedString[fieldName[i]]; }
+
+ if (formatSpec.length)
+ { pieceFormattedString = this.number(formatSpec, pieceFormattedString); }
+ }
+
+ formattedStringArray.push(pieceFormattedString);
+
+ formatString = formatString.slice(fieldDelimiterIndex + 1);
+ endPlaceholder = !endPlaceholder;
+ fieldDelimiter = (endPlaceholder) ? '}' : '{';
+ }
+ formattedStringArray.push(formatString);
+
+ return formattedStringArray.join('');
+ },
+
+ convert: function(type, value, precision) {
+
+ switch (type) {
+ case 'b':
+ return value.toString(2);
+ case 'c':
+ return String.fromCharCode(value);
+ case 'o':
+ return value.toString(8);
+ case 'x':
+ return value.toString(16);
+ case 'X':
+ return value.toString(16).toUpperCase();
+ case 'g':
+ return value.toPrecision(precision);
+ case 'e':
+ return value.toExponential(precision);
+ case 'f':
+ return value.toFixed(precision);
+ case 'r':
+ return (value = this.round(value, this.precision(value, precision))).toFixed(Math.max(0, Math.min(20, this.precision(value * (1 + 1e-15), precision))));
+ default:
+ return value + '';
+ }
+ },
+
+ round: function(value, precision) {
+
+ return precision
+ ? Math.round(value * (precision = Math.pow(10, precision))) / precision
+ : Math.round(value);
+ },
+
+ precision: function(value, precision) {
+
+ return precision - (value ? Math.ceil(Math.log(value) / Math.LN10) : 1);
+ },
+
+ prefix: function(value, precision) {
+
+ var prefixes = ['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'].map(function(d, i) {
+ var k = Math.pow(10, Math.abs(8 - i) * 3);
+ return {
+ scale: i > 8 ? function(d) {
+ return d / k;
+ } : function(d) {
+ return d * k;
+ },
+ symbol: d
+ };
+ });
+
+ var i = 0;
+ if (value) {
+ if (value < 0) { value *= -1; }
+ if (precision) { value = this.round(value, this.precision(value, precision)); }
+ i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10);
+ i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3));
+ }
+ return prefixes[8 + i / 3];
+ }
+ };
+
+ /*
+ Pre-compile the HTML to be used as a template.
+ */
+ var template = function(html) {
+
+ /*
+ Must support the variation in templating syntax found here:
+ https://lodash.com/docs#template
+ */
+ var regex = /<%= ([^ ]+) %>|\$\{ ?([^{} ]+) ?\}|\{\{([^{} ]+)\}\}/g;
+
+ return function(data) {
+
+ data = data || {};
+
+ return html.replace(regex, function(match) {
+
+ var args = Array.from(arguments);
+ var attr = args.slice(1, 4).find(function(_attr) {
+ return !!_attr;
+ });
+
+ var attrArray = attr.split('.');
+ var value = data[attrArray.shift()];
+
+ while (value !== undefined && attrArray.length) {
+ value = value[attrArray.shift()];
+ }
+
+ return value !== undefined ? value : '';
+ });
+ };
+ };
+
+ /**
+ * @param {Element} el Element, which content is intent to display in full-screen mode, 'window.top.document.body' is default.
+ */
+ var toggleFullScreen = function(el) {
+
+ var topDocument = window.top.document;
+ el = el || topDocument.body;
+
+ function prefixedResult(el, prop) {
+
+ var prefixes = ['webkit', 'moz', 'ms', 'o', ''];
+ for (var i = 0; i < prefixes.length; i++) {
+ var prefix = prefixes[i];
+ var propName = prefix ? (prefix + prop) : (prop.substr(0, 1).toLowerCase() + prop.substr(1));
+ if (el[propName] !== undefined) {
+ return isFunction(el[propName]) ? el[propName]() : el[propName];
+ }
+ }
+ }
+
+ if (prefixedResult(topDocument, 'FullscreenElement') || prefixedResult(topDocument, 'FullScreenElement')) {
+ prefixedResult(topDocument, 'ExitFullscreen') || // Spec.
+ prefixedResult(topDocument, 'CancelFullScreen'); // Firefox
+ } else {
+ prefixedResult(el, 'RequestFullscreen') || // Spec.
+ prefixedResult(el, 'RequestFullScreen'); // Firefox
+ }
+ };
+
+ // Deprecated
+ // Copy all the properties to the first argument from the following arguments.
+ // All the properties will be overwritten by the properties from the following
+ // arguments. Inherited properties are ignored.
+ var mixin = _.assign;
+
+ // Deprecated
+ // Copy all properties to the first argument from the following
+ // arguments only in case if they don't exists in the first argument.
+ // All the function propererties in the first argument will get
+ // additional property base pointing to the extenders same named
+ // property function's call method.
+ var supplement = _.defaults;
+
+ // Same as `mixin()` but deep version.
+ var deepMixin = mixin;
+
+ // Deprecated
+ // Same as `supplement()` but deep version.
+ var deepSupplement = _.defaultsDeep;
+
+ // Replacements for deprecated functions
+ var assign = _.assign;
+ var defaults = _.defaults;
+ // no better-named replacement for `deepMixin`
+ var defaultsDeep = _.defaultsDeep;
+
+ // Lodash 3 vs 4 incompatible
+ var invoke = _.invokeMap || _.invoke;
+ var sortedIndex = _.sortedIndexBy || _.sortedIndex;
+ var uniq = _.uniqBy || _.uniq;
+
+ var clone = _.clone;
+ var cloneDeep = _.cloneDeep;
+ var isEmpty = _.isEmpty;
+ var isEqual = _.isEqual;
+ var isFunction = _.isFunction;
+ var isPlainObject = _.isPlainObject;
+ var toArray = _.toArray;
+ var debounce = _.debounce;
+ var groupBy = _.groupBy;
+ var sortBy = _.sortBy;
+ var flattenDeep = _.flattenDeep;
+ var without = _.without;
+ var difference = _.difference;
+ var intersection$1 = _.intersection;
+ var union = _.union;
+ var has$2 = _.has;
+ var result = _.result;
+ var omit = _.omit;
+ var pick = _.pick;
+ var bindAll = _.bindAll;
+ var forIn = _.forIn;
+ var camelCase = _.camelCase;
+ var uniqueId = _.uniqueId;
+
+ var merge = function() {
+ if (_.mergeWith) {
+ var args = Array.from(arguments);
+ var last = args[args.length - 1];
+
+ var customizer = isFunction(last) ? last : noop;
+ args.push(function(a, b) {
+ var customResult = customizer(a, b);
+ if (customResult !== undefined) {
+ return customResult;
+ }
+
+ if (Array.isArray(a) && !Array.isArray(b)) {
+ return b;
+ }
+ });
+
+ return _.mergeWith.apply(this, args);
+ }
+ return _.merge.apply(this, arguments);
+ };
+
+ var isBoolean = function(value) {
+ var toString = Object.prototype.toString;
+ return value === true || value === false || (!!value && typeof value === 'object' && toString.call(value) === '[object Boolean]');
+ };
+
+ var isObject$1 = function(value) {
+ return !!value && (typeof value === 'object' || typeof value === 'function');
+ };
+
+ var isNumber = function(value) {
+ var toString = Object.prototype.toString;
+ return typeof value === 'number' || (!!value && typeof value === 'object' && toString.call(value) === '[object Number]');
+ };
+
+ var isString = function(value) {
+ var toString = Object.prototype.toString;
+ return typeof value === 'string' || (!!value && typeof value === 'object' && toString.call(value) === '[object String]');
+ };
+
+ var noop = function() {
+ };
+
+ // Clone `cells` returning an object that maps the original cell ID to the clone. The number
+ // of clones is exactly the same as the `cells.length`.
+ // This function simply clones all the `cells`. However, it also reconstructs
+ // all the `source/target` and `parent/embed` references within the `cells`.
+ // This is the main difference from the `cell.clone()` method. The
+ // `cell.clone()` method works on one single cell only.
+ // For example, for a graph: `A --- L ---> B`, `cloneCells([A, L, B])`
+ // returns `[A2, L2, B2]` resulting to a graph: `A2 --- L2 ---> B2`, i.e.
+ // the source and target of the link `L2` is changed to point to `A2` and `B2`.
+ function cloneCells(cells) {
+
+ cells = uniq(cells);
+
+ // A map of the form [original cell ID] -> [clone] helping
+ // us to reconstruct references for source/target and parent/embeds.
+ // This is also the returned value.
+ var cloneMap = toArray(cells).reduce(function(map, cell) {
+ map[cell.id] = cell.clone();
+ return map;
+ }, {});
+
+ toArray(cells).forEach(function(cell) {
+
+ var clone = cloneMap[cell.id];
+ // assert(clone exists)
+
+ if (clone.isLink()) {
+ var source = clone.source();
+ var target = clone.target();
+ if (source.id && cloneMap[source.id]) {
+ // Source points to an element and the element is among the clones.
+ // => Update the source of the cloned link.
+ clone.prop('source/id', cloneMap[source.id].id);
+ }
+ if (target.id && cloneMap[target.id]) {
+ // Target points to an element and the element is among the clones.
+ // => Update the target of the cloned link.
+ clone.prop('target/id', cloneMap[target.id].id);
+ }
+ }
+
+ // Find the parent of the original cell
+ var parent = cell.get('parent');
+ if (parent && cloneMap[parent]) {
+ clone.set('parent', cloneMap[parent].id);
+ }
+
+ // Find the embeds of the original cell
+ var embeds = toArray(cell.get('embeds')).reduce(function(newEmbeds, embed) {
+ // Embedded cells that are not being cloned can not be carried
+ // over with other embedded cells.
+ if (cloneMap[embed]) {
+ newEmbeds.push(cloneMap[embed].id);
+ }
+ return newEmbeds;
+ }, []);
+
+ if (!isEmpty(embeds)) {
+ clone.set('embeds', embeds);
+ }
+ });
+
+ return cloneMap;
+ }
+
+ function setWrapper(attrName, dimension) {
+ return function(value, refBBox) {
+ var isValuePercentage = isPercentage(value);
+ value = parseFloat(value);
+ if (isValuePercentage) {
+ value /= 100;
+ }
+
+ var attrs = {};
+ if (isFinite(value)) {
+ var attrValue = (isValuePercentage || value >= 0 && value <= 1)
+ ? value * refBBox[dimension]
+ : Math.max(value + refBBox[dimension], 0);
+ attrs[attrName] = attrValue;
+ }
+
+ return attrs;
+ };
+ }
+
+ function positionWrapper(axis, dimension, origin) {
+ return function(value, refBBox) {
+ var valuePercentage = isPercentage(value);
+ value = parseFloat(value);
+ if (valuePercentage) {
+ value /= 100;
+ }
+
+ var delta;
+ if (isFinite(value)) {
+ var refOrigin = refBBox[origin]();
+ if (valuePercentage || value > 0 && value < 1) {
+ delta = refOrigin[axis] + refBBox[dimension] * value;
+ } else {
+ delta = refOrigin[axis] + value;
+ }
+ }
+
+ var point = Point();
+ point[axis] = delta || 0;
+ return point;
+ };
+ }
+
+ function offsetWrapper(axis, dimension, corner) {
+ return function(value, nodeBBox) {
+ var delta;
+ if (value === 'middle') {
+ delta = nodeBBox[dimension] / 2;
+ } else if (value === corner) {
+ delta = nodeBBox[dimension];
+ } else if (isFinite(value)) {
+ // TODO: or not to do a breaking change?
+ delta = (value > -1 && value < 1) ? (-nodeBBox[dimension] * value) : -value;
+ } else if (isPercentage(value)) {
+ delta = nodeBBox[dimension] * parseFloat(value) / 100;
+ } else {
+ delta = 0;
+ }
+
+ var point = Point();
+ point[axis] = -(nodeBBox[axis] + delta);
+ return point;
+ };
+ }
+
+ function shapeWrapper(shapeConstructor, opt) {
+ var cacheName = 'joint-shape';
+ var resetOffset = opt && opt.resetOffset;
+ return function(value, refBBox, node) {
+ var $node = $(node);
+ var cache = $node.data(cacheName);
+ if (!cache || cache.value !== value) {
+ // only recalculate if value has changed
+ var cachedShape = shapeConstructor(value);
+ cache = {
+ value: value,
+ shape: cachedShape,
+ shapeBBox: cachedShape.bbox()
+ };
+ $node.data(cacheName, cache);
+ }
+
+ var shape = cache.shape.clone();
+ var shapeBBox = cache.shapeBBox.clone();
+ var shapeOrigin = shapeBBox.origin();
+ var refOrigin = refBBox.origin();
+
+ shapeBBox.x = refOrigin.x;
+ shapeBBox.y = refOrigin.y;
+
+ var fitScale = refBBox.maxRectScaleToFit(shapeBBox, refOrigin);
+ // `maxRectScaleToFit` can give Infinity if width or height is 0
+ var sx = (shapeBBox.width === 0 || refBBox.width === 0) ? 1 : fitScale.sx;
+ var sy = (shapeBBox.height === 0 || refBBox.height === 0) ? 1 : fitScale.sy;
+
+ shape.scale(sx, sy, shapeOrigin);
+ if (resetOffset) {
+ shape.translate(-shapeOrigin.x, -shapeOrigin.y);
+ }
+
+ return shape;
+ };
+ }
+
+ // `d` attribute for SVGPaths
+ function dWrapper(opt) {
+ function pathConstructor(value) {
+ return new Path(V.normalizePathData(value));
+ }
+
+ var shape = shapeWrapper(pathConstructor, opt);
+ return function(value, refBBox, node) {
+ var path = shape(value, refBBox, node);
+ return {
+ d: path.serialize()
+ };
+ };
+ }
+
+ // `points` attribute for SVGPolylines and SVGPolygons
+ function pointsWrapper(opt) {
+ var shape = shapeWrapper(Polyline, opt);
+ return function(value, refBBox, node) {
+ var polyline = shape(value, refBBox, node);
+ return {
+ points: polyline.serialize()
+ };
+ };
+ }
+
+ function atConnectionWrapper(method, opt) {
+ var zeroVector = new Point(1, 0);
+ return function(value) {
+ var p, angle;
+ var tangent = this[method](value);
+ if (tangent) {
+ angle = (opt.rotate) ? tangent.vector().vectorAngle(zeroVector) : 0;
+ p = tangent.start;
+ } else {
+ p = this.path.start;
+ angle = 0;
+ }
+ if (angle === 0) { return { transform: 'translate(' + p.x + ',' + p.y + ')' }; }
+ return { transform: 'translate(' + p.x + ',' + p.y + ') rotate(' + angle + ')' };
+ };
+ }
+
+ function setIfChangedWrapper(attribute) {
+ return function setIfChanged(value, _, node) {
+ var vel = V(node);
+ if (vel.attr(attribute) === value) { return; }
+ vel.attr(attribute, value);
+ };
+ }
+
+ function isTextInUse(_value, _node, attrs) {
+ return (attrs.text !== undefined);
+ }
+
+ function isLinkView() {
+ return this.model.isLink();
+ }
+
+ function contextMarker(context) {
+ var marker = {};
+ // Stroke
+ // The context 'fill' is disregared here. The usual case is to use the marker with a connection
+ // (for which 'fill' attribute is set to 'none').
+ var stroke = context.stroke;
+ if (typeof stroke === 'string') {
+ marker['stroke'] = stroke;
+ marker['fill'] = stroke;
+ }
+ // Opacity
+ // Again the context 'fill-opacity' is ignored.
+ var strokeOpacity = context.strokeOpacity;
+ if (strokeOpacity === undefined) { strokeOpacity = context['stroke-opacity']; }
+ if (strokeOpacity === undefined) { strokeOpacity = context.opacity; }
+ if (strokeOpacity !== undefined) {
+ marker['stroke-opacity'] = strokeOpacity;
+ marker['fill-opacity'] = strokeOpacity;
+ }
+ return marker;
+ }
+
+ function setPaintURL(def) {
+ var ref = this;
+ var paper = ref.paper;
+ var url = (def.type === 'pattern')
+ ? paper.definePattern(def)
+ : paper.defineGradient(def);
+ return ("url(#" + url + ")");
+ }
+
+ var attributesNS = {
+
+ xlinkShow: {
+ set: 'xlink:show'
+ },
+
+ xlinkRole: {
+ set: 'xlink:role'
+ },
+
+ xlinkType: {
+ set: 'xlink:type'
+ },
+
+ xlinkArcrole: {
+ set: 'xlink:arcrole'
+ },
+
+ xlinkTitle: {
+ set: 'xlink:title'
+ },
+
+ xlinkActuate: {
+ set: 'xlink:actuate'
+ },
+
+ xmlSpace: {
+ set: 'xml:space'
+ },
+
+ xmlBase: {
+ set: 'xml:base'
+ },
+
+ xmlLang: {
+ set: 'xml:lang'
+ },
+
+ preserveAspectRatio: {
+ set: 'preserveAspectRatio'
+ },
+
+ requiredExtension: {
+ set: 'requiredExtension'
+ },
+
+ requiredFeatures: {
+ set: 'requiredFeatures'
+ },
+
+ systemLanguage: {
+ set: 'systemLanguage'
+ },
+
+ externalResourcesRequired: {
+ set: 'externalResourceRequired'
+ },
+
+ href: {
+ set: setIfChangedWrapper('href')
+ },
+
+ xlinkHref: {
+ set: setIfChangedWrapper('xlink:href')
+ },
+
+ filter: {
+ qualify: isPlainObject,
+ set: function(filter) {
+ return 'url(#' + this.paper.defineFilter(filter) + ')';
+ }
+ },
+
+ fill: {
+ qualify: isPlainObject,
+ set: setPaintURL
+ },
+
+ stroke: {
+ qualify: isPlainObject,
+ set: setPaintURL
+ },
+
+ sourceMarker: {
+ qualify: isPlainObject,
+ set: function(marker, refBBox, node, attrs) {
+ marker = assign(contextMarker(attrs), marker);
+ return { 'marker-start': 'url(#' + this.paper.defineMarker(marker) + ')' };
+ }
+ },
+
+ targetMarker: {
+ qualify: isPlainObject,
+ set: function(marker, refBBox, node, attrs) {
+ marker = assign(contextMarker(attrs), { 'transform': 'rotate(180)' }, marker);
+ return { 'marker-end': 'url(#' + this.paper.defineMarker(marker) + ')' };
+ }
+ },
+
+ vertexMarker: {
+ qualify: isPlainObject,
+ set: function(marker, refBBox, node, attrs) {
+ marker = assign(contextMarker(attrs), marker);
+ return { 'marker-mid': 'url(#' + this.paper.defineMarker(marker) + ')' };
+ }
+ },
+
+ text: {
+ qualify: function(_text, _node, attrs) {
+ return !attrs.textWrap || !isPlainObject(attrs.textWrap);
+ },
+ set: function(text, refBBox, node, attrs) {
+ var $node = $(node);
+ var cacheName = 'joint-text';
+ var cache = $node.data(cacheName);
+ var textAttrs = pick(attrs, 'lineHeight', 'annotations', 'textPath', 'x', 'textVerticalAnchor', 'eol', 'displayEmpty');
+ // eval `x` if using calc()
+ var x = textAttrs.x;
+ if (isCalcAttribute(x)) {
+ textAttrs.x = evalCalcAttribute(x, refBBox);
+ }
+
+ var fontSizeAttr = attrs['font-size'] || attrs['fontSize'];
+ if (isCalcAttribute(fontSizeAttr)) {
+ fontSizeAttr = evalCalcAttribute(fontSizeAttr, refBBox);
+ }
+ var fontSize = textAttrs.fontSize = fontSizeAttr;
+ var textHash = JSON.stringify([text, textAttrs]);
+ // Update the text only if there was a change in the string
+ // or any of its attributes.
+ if (cache === undefined || cache !== textHash) {
+ // Chrome bug:
+ // Tspans positions defined as `em` are not updated
+ // when container `font-size` change.
+ if (fontSize) { node.setAttribute('font-size', fontSize); }
+ // Text Along Path Selector
+ var textPath = textAttrs.textPath;
+ if (isObject$1(textPath)) {
+ var pathSelector = textPath.selector;
+ if (typeof pathSelector === 'string') {
+ var pathNode = this.findBySelector(pathSelector)[0];
+ if (pathNode instanceof SVGPathElement) {
+ textAttrs.textPath = assign({ 'xlink:href': '#' + pathNode.id }, textPath);
+ }
+ }
+ }
+ V(node).text('' + text, textAttrs);
+ $node.data(cacheName, textHash);
+ }
+ }
+ },
+
+ textWrap: {
+ qualify: isPlainObject,
+ set: function(value, refBBox, node, attrs) {
+ var size = {};
+ // option `width`
+ var width = value.width || 0;
+ if (isPercentage(width)) {
+ size.width = refBBox.width * parseFloat(width) / 100;
+ } else if (isCalcAttribute(width)) {
+ size.width = Number(evalCalcAttribute(width, refBBox));
+ } else {
+ if (value.width === null) {
+ // breakText() requires width to be specified.
+ size.width = Infinity;
+ } else if (width <= 0) {
+ size.width = refBBox.width + width;
+ } else {
+ size.width = width;
+ }
+ }
+ // option `height`
+ var height = value.height || 0;
+ if (isPercentage(height)) {
+ size.height = refBBox.height * parseFloat(height) / 100;
+ } else if (isCalcAttribute(height)) {
+ size.height = Number(evalCalcAttribute(height, refBBox));
+ } else {
+ if (value.height === null) {
+ // if height is not specified breakText() does not
+ // restrict the height of the text.
+ } else if (height <= 0) {
+ size.height = refBBox.height + height;
+ } else {
+ size.height = height;
+ }
+ }
+ // option `text`
+ var wrappedText;
+ var text = value.text;
+ if (text === undefined) { text = attrs.text; }
+ if (text !== undefined) {
+ var breakTextFn = value.breakText || breakText;
+ var fontSizeAttr = attrs['font-size'] || attrs.fontSize;
+ wrappedText = breakTextFn('' + text, size, {
+ 'font-weight': attrs['font-weight'] || attrs.fontWeight,
+ 'font-size': isCalcAttribute(fontSizeAttr) ? evalCalcAttribute(fontSizeAttr, refBBox) : fontSizeAttr,
+ 'font-family': attrs['font-family'] || attrs.fontFamily,
+ 'lineHeight': attrs.lineHeight,
+ 'letter-spacing': 'letter-spacing' in attrs ? attrs['letter-spacing'] : attrs.letterSpacing
+ }, {
+ // Provide an existing SVG Document here
+ // instead of creating a temporary one over again.
+ svgDocument: this.paper.svg,
+ ellipsis: value.ellipsis,
+ hyphen: value.hyphen,
+ maxLineCount: value.maxLineCount,
+ preserveSpaces: value.preserveSpaces
+ });
+ } else {
+ wrappedText = '';
+ }
+ attributesNS.text.set.call(this, wrappedText, refBBox, node, attrs);
+ }
+ },
+
+ title: {
+ qualify: function(title, node) {
+ // HTMLElement title is specified via an attribute (i.e. not an element)
+ return node instanceof SVGElement;
+ },
+ set: function(title, refBBox, node) {
+ var $node = $(node);
+ var cacheName = 'joint-title';
+ var cache = $node.data(cacheName);
+ if (cache === undefined || cache !== title) {
+ $node.data(cacheName, title);
+ // Generally
element should be the first child element of its parent.
+ var firstChild = node.firstChild;
+ if (firstChild && firstChild.tagName.toUpperCase() === 'TITLE') {
+ // Update an existing title
+ firstChild.textContent = title;
+ } else {
+ // Create a new title
+ var titleNode = document.createElementNS(node.namespaceURI, 'title');
+ titleNode.textContent = title;
+ node.insertBefore(titleNode, firstChild);
+ }
+ }
+ }
+ },
+
+ lineHeight: {
+ qualify: isTextInUse
+ },
+
+ textVerticalAnchor: {
+ qualify: isTextInUse
+ },
+
+ textPath: {
+ qualify: isTextInUse
+ },
+
+ annotations: {
+ qualify: isTextInUse
+ },
+
+ eol: {
+ qualify: isTextInUse
+ },
+
+ displayEmpty: {
+ qualify: isTextInUse
+ },
+
+ // `port` attribute contains the `id` of the port that the underlying magnet represents.
+ port: {
+ set: function(port) {
+ return (port === null || port.id === undefined) ? port : port.id;
+ }
+ },
+
+ // `style` attribute is special in the sense that it sets the CSS style of the subelement.
+ style: {
+ qualify: isPlainObject,
+ set: function(styles, refBBox, node) {
+ $(node).css(styles);
+ }
+ },
+
+ html: {
+ set: function(html, refBBox, node) {
+ $(node).html(html + '');
+ }
+ },
+
+ ref: {
+ // We do not set `ref` attribute directly on an element.
+ // The attribute itself does not qualify for relative positioning.
+ },
+
+ // if `refX` is in [0, 1] then `refX` is a fraction of bounding box width
+ // if `refX` is < 0 then `refX`'s absolute values is the right coordinate of the bounding box
+ // otherwise, `refX` is the left coordinate of the bounding box
+
+ refX: {
+ position: positionWrapper('x', 'width', 'origin')
+ },
+
+ refY: {
+ position: positionWrapper('y', 'height', 'origin')
+ },
+
+ // `ref-dx` and `ref-dy` define the offset of the subelement relative to the right and/or bottom
+ // coordinate of the reference element.
+
+ refDx: {
+ position: positionWrapper('x', 'width', 'corner')
+ },
+
+ refDy: {
+ position: positionWrapper('y', 'height', 'corner')
+ },
+
+ // 'ref-width'/'ref-height' defines the width/height of the subelement relatively to
+ // the reference element size
+ // val in 0..1 ref-width = 0.75 sets the width to 75% of the ref. el. width
+ // val < 0 || val > 1 ref-height = -20 sets the height to the ref. el. height shorter by 20
+
+ refWidth: {
+ set: setWrapper('width', 'width')
+ },
+
+ refHeight: {
+ set: setWrapper('height', 'height')
+ },
+
+ refRx: {
+ set: setWrapper('rx', 'width')
+ },
+
+ refRy: {
+ set: setWrapper('ry', 'height')
+ },
+
+ refRInscribed: {
+ set: (function(attrName) {
+ var widthFn = setWrapper(attrName, 'width');
+ var heightFn = setWrapper(attrName, 'height');
+ return function(value, refBBox) {
+ var fn = (refBBox.height > refBBox.width) ? widthFn : heightFn;
+ return fn(value, refBBox);
+ };
+ })('r')
+ },
+
+ refRCircumscribed: {
+ set: function(value, refBBox) {
+ var isValuePercentage = isPercentage(value);
+ value = parseFloat(value);
+ if (isValuePercentage) {
+ value /= 100;
+ }
+
+ var diagonalLength = Math.sqrt((refBBox.height * refBBox.height) + (refBBox.width * refBBox.width));
+
+ var rValue;
+ if (isFinite(value)) {
+ if (isValuePercentage || value >= 0 && value <= 1) { rValue = value * diagonalLength; }
+ else { rValue = Math.max(value + diagonalLength, 0); }
+ }
+
+ return { r: rValue };
+ }
+ },
+
+ refCx: {
+ set: setWrapper('cx', 'width')
+ },
+
+ refCy: {
+ set: setWrapper('cy', 'height')
+ },
+
+ // `x-alignment` when set to `middle` causes centering of the subelement around its new x coordinate.
+ // `x-alignment` when set to `right` uses the x coordinate as referenced to the right of the bbox.
+
+ xAlignment: {
+ offset: offsetWrapper('x', 'width', 'right')
+ },
+
+ // `y-alignment` when set to `middle` causes centering of the subelement around its new y coordinate.
+ // `y-alignment` when set to `bottom` uses the y coordinate as referenced to the bottom of the bbox.
+
+ yAlignment: {
+ offset: offsetWrapper('y', 'height', 'bottom')
+ },
+
+ resetOffset: {
+ offset: function(val, nodeBBox) {
+ return (val)
+ ? { x: -nodeBBox.x, y: -nodeBBox.y }
+ : { x: 0, y: 0 };
+ }
+
+ },
+
+ refDResetOffset: {
+ set: dWrapper({ resetOffset: true })
+ },
+
+ refDKeepOffset: {
+ set: dWrapper({ resetOffset: false })
+ },
+
+ refPointsResetOffset: {
+ set: pointsWrapper({ resetOffset: true })
+ },
+
+ refPointsKeepOffset: {
+ set: pointsWrapper({ resetOffset: false })
+ },
+
+ // LinkView Attributes
+
+ connection: {
+ qualify: isLinkView,
+ set: function(ref) {
+ var stubs = ref.stubs; if ( stubs === void 0 ) stubs = 0;
+
+ var d;
+ if (isFinite(stubs) && stubs !== 0) {
+ var offset;
+ if (stubs < 0) {
+ offset = (this.getConnectionLength() + stubs) / 2;
+ } else {
+ offset = stubs;
+ }
+ var path = this.getConnection();
+ var segmentSubdivisions = this.getConnectionSubdivisions();
+ var sourceParts = path.divideAtLength(offset, { segmentSubdivisions: segmentSubdivisions });
+ var targetParts = path.divideAtLength(-offset, { segmentSubdivisions: segmentSubdivisions });
+ if (sourceParts && targetParts) {
+ d = (sourceParts[0].serialize()) + " " + (targetParts[1].serialize());
+ }
+ }
+
+ return { d: d || this.getSerializedConnection() };
+ }
+ },
+
+ atConnectionLengthKeepGradient: {
+ qualify: isLinkView,
+ set: atConnectionWrapper('getTangentAtLength', { rotate: true })
+ },
+
+ atConnectionLengthIgnoreGradient: {
+ qualify: isLinkView,
+ set: atConnectionWrapper('getTangentAtLength', { rotate: false })
+ },
+
+ atConnectionRatioKeepGradient: {
+ qualify: isLinkView,
+ set: atConnectionWrapper('getTangentAtRatio', { rotate: true })
+ },
+
+ atConnectionRatioIgnoreGradient: {
+ qualify: isLinkView,
+ set: atConnectionWrapper('getTangentAtRatio', { rotate: false })
+ }
+ };
+
+ attributesNS['xlink:href'] = attributesNS.xlinkHref;
+
+ // Support `calc()` with the following SVG attributes
+ [
+ 'transform', // g
+ 'd', // path
+ 'points', // polyline / polygon
+ 'cx', 'cy', // circle / ellipse
+ 'x1', 'x2', 'y1', 'y2', // line
+ 'x', 'y', // rect / text / image
+ 'dx', 'dy' // text
+ ].forEach(function (attribute) {
+ attributesNS[attribute] = {
+ qualify: isCalcAttribute,
+ set: function setCalcAttribute(value, refBBox) {
+ var obj;
+
+ return ( obj = {}, obj[attribute] = evalCalcAttribute(value, refBBox), obj );
+ }
+ };
+ });
+
+ // Prevent "A negative value is not valid" error.
+ [
+ 'width', 'height', // rect / image
+ 'r', // circle
+ 'rx', 'ry', // rect / ellipse
+ 'font-size', // text
+ 'stroke-width' // elements
+ ].forEach(function (attribute) {
+ attributesNS[attribute] = {
+ qualify: isCalcAttribute,
+ set: function setCalcAttribute(value, refBBox) {
+ var obj;
+
+ return ( obj = {}, obj[attribute] = Math.max(0, evalCalcAttribute(value, refBBox)), obj );
+ }
+ };
+ });
+
+
+ // Aliases
+ attributesNS.refR = attributesNS.refRInscribed;
+ attributesNS.refD = attributesNS.refDResetOffset;
+ attributesNS.refPoints = attributesNS.refPointsResetOffset;
+ attributesNS.atConnectionLength = attributesNS.atConnectionLengthKeepGradient;
+ attributesNS.atConnectionRatio = attributesNS.atConnectionRatioKeepGradient;
+ attributesNS.fontSize = attributesNS['font-size'];
+ attributesNS.strokeWidth = attributesNS['stroke-width'];
+
+ // This allows to combine both absolute and relative positioning
+ // refX: 50%, refX2: 20
+ attributesNS.refX2 = attributesNS.refX;
+ attributesNS.refY2 = attributesNS.refY;
+ attributesNS.refWidth2 = attributesNS.refWidth;
+ attributesNS.refHeight2 = attributesNS.refHeight;
+
+ // Aliases for backwards compatibility
+ attributesNS['ref-x'] = attributesNS.refX;
+ attributesNS['ref-y'] = attributesNS.refY;
+ attributesNS['ref-dy'] = attributesNS.refDy;
+ attributesNS['ref-dx'] = attributesNS.refDx;
+ attributesNS['ref-width'] = attributesNS.refWidth;
+ attributesNS['ref-height'] = attributesNS.refHeight;
+ attributesNS['x-alignment'] = attributesNS.xAlignment;
+ attributesNS['y-alignment'] = attributesNS.yAlignment;
+
+ var attributes = attributesNS;
+
+ // Cell base model.
+ // --------------------------
+
+ var Cell = Backbone.Model.extend({
+
+ // This is the same as Backbone.Model with the only difference that is uses util.merge
+ // instead of just _.extend. The reason is that we want to mixin attributes set in upper classes.
+ constructor: function(attributes, options) {
+
+ var defaults;
+ var attrs = attributes || {};
+ if (typeof this.preinitialize === 'function') {
+ // Check to support an older version of Backbone (prior v1.4)
+ this.preinitialize.apply(this, arguments);
+ }
+ this.cid = uniqueId('c');
+ this.attributes = {};
+ if (options && options.collection) { this.collection = options.collection; }
+ if (options && options.parse) { attrs = this.parse(attrs, options) || {}; }
+ if ((defaults = result(this, 'defaults'))) {
+ //
+ // Replaced the call to _.defaults with util.merge.
+ attrs = merge({}, defaults, attrs);
+ //
+ }
+ this.set(attrs, options);
+ this.changed = {};
+ this.initialize.apply(this, arguments);
+ },
+
+ translate: function(dx, dy, opt) {
+
+ throw new Error('Must define a translate() method.');
+ },
+
+ toJSON: function() {
+
+ var defaults = result(this.constructor.prototype, 'defaults');
+ var defaultAttrs = defaults.attrs || {};
+ var attrs = this.attributes.attrs;
+ var finalAttrs = {};
+
+ // Loop through all the attributes and
+ // omit the default attributes as they are implicitly reconstructible by the cell 'type'.
+ forIn(attrs, function(attr, selector) {
+
+ var defaultAttr = defaultAttrs[selector];
+
+ forIn(attr, function(value, name) {
+
+ // attr is mainly flat though it might have one more level (consider the `style` attribute).
+ // Check if the `value` is object and if yes, go one level deep.
+ if (isObject$1(value) && !Array.isArray(value)) {
+
+ forIn(value, function(value2, name2) {
+
+ if (!defaultAttr || !defaultAttr[name] || !isEqual(defaultAttr[name][name2], value2)) {
+
+ finalAttrs[selector] = finalAttrs[selector] || {};
+ (finalAttrs[selector][name] || (finalAttrs[selector][name] = {}))[name2] = value2;
+ }
+ });
+
+ } else if (!defaultAttr || !isEqual(defaultAttr[name], value)) {
+ // `value` is not an object, default attribute for such a selector does not exist
+ // or it is different than the attribute value set on the model.
+
+ finalAttrs[selector] = finalAttrs[selector] || {};
+ finalAttrs[selector][name] = value;
+ }
+ });
+ });
+
+ var attributes = cloneDeep(omit(this.attributes, 'attrs'));
+ attributes.attrs = finalAttrs;
+
+ return attributes;
+ },
+
+ initialize: function(options) {
+
+ var idAttribute = this.getIdAttribute();
+ if (!options || !(idAttribute in options)) {
+ this.set(idAttribute, this.generateId(), { silent: true });
+ }
+
+ this._transitionIds = {};
+ this._scheduledTransitionIds = {};
+
+ // Collect ports defined in `attrs` and keep collecting whenever `attrs` object changes.
+ this.processPorts();
+ this.on('change:attrs', this.processPorts, this);
+ },
+
+ getIdAttribute: function() {
+ return this.idAttribute || 'id';
+ },
+
+ generateId: function() {
+ return uuid();
+ },
+
+ /**
+ * @deprecated
+ */
+ processPorts: function() {
+
+ // Whenever `attrs` changes, we extract ports from the `attrs` object and store it
+ // in a more accessible way. Also, if any port got removed and there were links that had `target`/`source`
+ // set to that port, we remove those links as well (to follow the same behaviour as
+ // with a removed element).
+
+ var previousPorts = this.ports;
+
+ // Collect ports from the `attrs` object.
+ var ports = {};
+ forIn(this.get('attrs'), function(attrs, selector) {
+
+ if (attrs && attrs.port) {
+
+ // `port` can either be directly an `id` or an object containing an `id` (and potentially other data).
+ if (attrs.port.id !== undefined) {
+ ports[attrs.port.id] = attrs.port;
+ } else {
+ ports[attrs.port] = { id: attrs.port };
+ }
+ }
+ });
+
+ // Collect ports that have been removed (compared to the previous ports) - if any.
+ // Use hash table for quick lookup.
+ var removedPorts = {};
+ forIn(previousPorts, function(port, id) {
+
+ if (!ports[id]) { removedPorts[id] = true; }
+ });
+
+ // Remove all the incoming/outgoing links that have source/target port set to any of the removed ports.
+ if (this.graph && !isEmpty(removedPorts)) {
+
+ var inboundLinks = this.graph.getConnectedLinks(this, { inbound: true });
+ inboundLinks.forEach(function(link) {
+
+ if (removedPorts[link.get('target').port]) { link.remove(); }
+ });
+
+ var outboundLinks = this.graph.getConnectedLinks(this, { outbound: true });
+ outboundLinks.forEach(function(link) {
+
+ if (removedPorts[link.get('source').port]) { link.remove(); }
+ });
+ }
+
+ // Update the `ports` object.
+ this.ports = ports;
+ },
+
+ remove: function(opt) {
+ if ( opt === void 0 ) opt = {};
+
+
+ // Store the graph in a variable because `this.graph` won't be accessible
+ // after `this.trigger('remove', ...)` down below.
+ var ref = this;
+ var graph = ref.graph;
+ var collection = ref.collection;
+ if (!graph) {
+ // The collection is a common Backbone collection (not the graph collection).
+ if (collection) { collection.remove(this, opt); }
+ return this;
+ }
+
+ graph.startBatch('remove');
+
+ // First, unembed this cell from its parent cell if there is one.
+ var parentCell = this.getParentCell();
+ if (parentCell) {
+ parentCell.unembed(this, opt);
+ }
+
+ // Remove also all the cells, which were embedded into this cell
+ var embeddedCells = this.getEmbeddedCells();
+ for (var i = 0, n = embeddedCells.length; i < n; i++) {
+ var embed = embeddedCells[i];
+ if (embed) {
+ embed.remove(opt);
+ }
+ }
+
+ this.trigger('remove', this, graph.attributes.cells, opt);
+
+ graph.stopBatch('remove');
+
+ return this;
+ },
+
+ toFront: function(opt) {
+
+ var graph = this.graph;
+ if (graph) {
+
+ opt = opt || {};
+
+ var z = graph.maxZIndex();
+
+ var cells;
+
+ if (opt.deep) {
+ cells = this.getEmbeddedCells({ deep: true, breadthFirst: opt.breadthFirst !== false });
+ cells.unshift(this);
+ } else {
+ cells = [this];
+ }
+
+ z = z - cells.length + 1;
+
+ var collection = graph.get('cells');
+ var shouldUpdate = (collection.indexOf(this) !== (collection.length - cells.length));
+ if (!shouldUpdate) {
+ shouldUpdate = cells.some(function(cell, index) {
+ return cell.get('z') !== z + index;
+ });
+ }
+
+ if (shouldUpdate) {
+ this.startBatch('to-front');
+
+ z = z + cells.length;
+
+ cells.forEach(function(cell, index) {
+ cell.set('z', z + index, opt);
+ });
+
+ this.stopBatch('to-front');
+ }
+ }
+
+ return this;
+ },
+
+ toBack: function(opt) {
+
+ var graph = this.graph;
+ if (graph) {
+
+ opt = opt || {};
+
+ var z = graph.minZIndex();
+
+ var cells;
+
+ if (opt.deep) {
+ cells = this.getEmbeddedCells({ deep: true, breadthFirst: opt.breadthFirst !== false });
+ cells.unshift(this);
+ } else {
+ cells = [this];
+ }
+
+ var collection = graph.get('cells');
+ var shouldUpdate = (collection.indexOf(this) !== 0);
+ if (!shouldUpdate) {
+ shouldUpdate = cells.some(function(cell, index) {
+ return cell.get('z') !== z + index;
+ });
+ }
+
+ if (shouldUpdate) {
+ this.startBatch('to-back');
+
+ z -= cells.length;
+
+ cells.forEach(function(cell, index) {
+ cell.set('z', z + index, opt);
+ });
+
+ this.stopBatch('to-back');
+ }
+ }
+
+ return this;
+ },
+
+ parent: function(parent, opt) {
+
+ // getter
+ if (parent === undefined) { return this.get('parent'); }
+ // setter
+ return this.set('parent', parent, opt);
+ },
+
+ embed: function(cell, opt) {
+ var this$1 = this;
+
+ var cells = Array.isArray(cell) ? cell : [cell];
+ if (!this.canEmbed(cells)) {
+ throw new Error('Recursive embedding not allowed.');
+ }
+ if (cells.some(function (c) { return c.isEmbedded() && this$1.id !== c.parent(); })) {
+ throw new Error('Embedding of already embedded cells is not allowed.');
+ }
+ this._embedCells(cells, opt);
+ return this;
+ },
+
+ unembed: function(cell, opt) {
+ var cells = Array.isArray(cell) ? cell : [cell];
+ this._unembedCells(cells, opt);
+ return this;
+ },
+
+ canEmbed: function(cell) {
+ var this$1 = this;
+
+ var cells = Array.isArray(cell) ? cell : [cell];
+ return cells.every(function (c) { return this$1 !== c && !this$1.isEmbeddedIn(c); });
+ },
+
+ _embedCells: function(cells, opt) {
+ var this$1 = this;
+
+ var batchName = 'embed';
+ this.startBatch(batchName);
+ var embeds = assign([], this.get('embeds'));
+ cells.forEach(function (cell) {
+ // We keep all element ids after link ids.
+ embeds[cell.isLink() ? 'unshift' : 'push'](cell.id);
+ cell.parent(this$1.id, opt);
+ });
+ this.set('embeds', uniq(embeds), opt);
+ this.stopBatch(batchName);
+ },
+
+ _unembedCells: function(cells, opt) {
+ var batchName = 'unembed';
+ this.startBatch(batchName);
+ cells.forEach(function (cell) { return cell.unset('parent', opt); });
+ this.set('embeds', without.apply(void 0, [ this.get('embeds') ].concat( cells.map(function (cell) { return cell.id; }) )), opt);
+ this.stopBatch(batchName);
+ },
+
+ getParentCell: function() {
+
+ // unlike link.source/target, cell.parent stores id directly as a string
+ var parentId = this.parent();
+ var graph = this.graph;
+
+ return (parentId && graph && graph.getCell(parentId)) || null;
+ },
+
+ // Return an array of ancestor cells.
+ // The array is ordered from the parent of the cell
+ // to the most distant ancestor.
+ getAncestors: function() {
+
+ var ancestors = [];
+
+ if (!this.graph) {
+ return ancestors;
+ }
+
+ var parentCell = this.getParentCell();
+ while (parentCell) {
+ ancestors.push(parentCell);
+ parentCell = parentCell.getParentCell();
+ }
+
+ return ancestors;
+ },
+
+ getEmbeddedCells: function(opt) {
+
+ opt = opt || {};
+
+ // Cell models can only be retrieved when this element is part of a collection.
+ // There is no way this element knows about other cells otherwise.
+ // This also means that calling e.g. `translate()` on an element with embeds before
+ // adding it to a graph does not translate its embeds.
+ if (this.graph) {
+
+ var cells;
+
+ if (opt.deep) {
+
+ if (opt.breadthFirst) {
+
+ // breadthFirst algorithm
+ cells = [];
+ var queue = this.getEmbeddedCells();
+
+ while (queue.length > 0) {
+
+ var parent = queue.shift();
+ cells.push(parent);
+ queue.push.apply(queue, parent.getEmbeddedCells());
+ }
+
+ } else {
+
+ // depthFirst algorithm
+ cells = this.getEmbeddedCells();
+ cells.forEach(function(cell) {
+ cells.push.apply(cells, cell.getEmbeddedCells(opt));
+ });
+ }
+
+ } else {
+
+ cells = toArray(this.get('embeds')).map(this.graph.getCell, this.graph);
+ }
+
+ return cells;
+ }
+ return [];
+ },
+
+ isEmbeddedIn: function(cell, opt) {
+
+ var cellId = isString(cell) ? cell : cell.id;
+ var parentId = this.parent();
+
+ opt = assign({ deep: true }, opt);
+
+ // See getEmbeddedCells().
+ if (this.graph && opt.deep) {
+
+ while (parentId) {
+ if (parentId === cellId) {
+ return true;
+ }
+ parentId = this.graph.getCell(parentId).parent();
+ }
+
+ return false;
+
+ } else {
+
+ // When this cell is not part of a collection check
+ // at least whether it's a direct child of given cell.
+ return parentId === cellId;
+ }
+ },
+
+ // Whether or not the cell is embedded in any other cell.
+ isEmbedded: function() {
+
+ return !!this.parent();
+ },
+
+ // Isolated cloning. Isolated cloning has two versions: shallow and deep (pass `{ deep: true }` in `opt`).
+ // Shallow cloning simply clones the cell and returns a new cell with different ID.
+ // Deep cloning clones the cell and all its embedded cells recursively.
+ clone: function(opt) {
+
+ opt = opt || {};
+
+ if (!opt.deep) {
+ // Shallow cloning.
+
+ var clone = Backbone.Model.prototype.clone.apply(this, arguments);
+ // We don't want the clone to have the same ID as the original.
+ clone.set(this.getIdAttribute(), this.generateId());
+ // A shallow cloned element does not carry over the original embeds.
+ clone.unset('embeds');
+ // And can not be embedded in any cell
+ // as the clone is not part of the graph.
+ clone.unset('parent');
+
+ return clone;
+
+ } else {
+ // Deep cloning.
+
+ // For a deep clone, simply call `graph.cloneCells()` with the cell and all its embedded cells.
+ return toArray(cloneCells([this].concat(this.getEmbeddedCells({ deep: true }))));
+ }
+ },
+
+ // A convenient way to set nested properties.
+ // This method merges the properties you'd like to set with the ones
+ // stored in the cell and makes sure change events are properly triggered.
+ // You can either set a nested property with one object
+ // or use a property path.
+ // The most simple use case is:
+ // `cell.prop('name/first', 'John')` or
+ // `cell.prop({ name: { first: 'John' } })`.
+ // Nested arrays are supported too:
+ // `cell.prop('series/0/data/0/degree', 50)` or
+ // `cell.prop({ series: [ { data: [ { degree: 50 } ] } ] })`.
+ prop: function(props, value, opt) {
+
+ var delim = '/';
+ var _isString = isString(props);
+
+ if (_isString || Array.isArray(props)) {
+ // Get/set an attribute by a special path syntax that delimits
+ // nested objects by the colon character.
+
+ if (arguments.length > 1) {
+
+ var path;
+ var pathArray;
+
+ if (_isString) {
+ path = props;
+ pathArray = path.split('/');
+ } else {
+ path = props.join(delim);
+ pathArray = props.slice();
+ }
+
+ var property = pathArray[0];
+ var pathArrayLength = pathArray.length;
+
+ opt = opt || {};
+ opt.propertyPath = path;
+ opt.propertyValue = value;
+ opt.propertyPathArray = pathArray;
+
+ if (pathArrayLength === 1) {
+ // Property is not nested. We can simply use `set()`.
+ return this.set(property, value, opt);
+ }
+
+ var update = {};
+ // Initialize the nested object. Subobjects are either arrays or objects.
+ // An empty array is created if the sub-key is an integer. Otherwise, an empty object is created.
+ // Note that this imposes a limitation on object keys one can use with Inspector.
+ // Pure integer keys will cause issues and are therefore not allowed.
+ var initializer = update;
+ var prevProperty = property;
+
+ for (var i = 1; i < pathArrayLength; i++) {
+ var pathItem = pathArray[i];
+ var isArrayIndex = Number.isFinite(_isString ? Number(pathItem) : pathItem);
+ initializer = initializer[prevProperty] = isArrayIndex ? [] : {};
+ prevProperty = pathItem;
+ }
+
+ // Fill update with the `value` on `path`.
+ update = setByPath(update, pathArray, value, '/');
+
+ var baseAttributes = merge({}, this.attributes);
+ // if rewrite mode enabled, we replace value referenced by path with
+ // the new one (we don't merge).
+ opt.rewrite && unsetByPath(baseAttributes, path, '/');
+
+ // Merge update with the model attributes.
+ var attributes = merge(baseAttributes, update);
+ // Finally, set the property to the updated attributes.
+ return this.set(property, attributes[property], opt);
+
+ } else {
+
+ return getByPath(this.attributes, props, delim);
+ }
+ }
+
+ return this.set(merge({}, this.attributes, props), value);
+ },
+
+ // A convenient way to unset nested properties
+ removeProp: function(path, opt) {
+
+ opt = opt || {};
+
+ var pathArray = Array.isArray(path) ? path : path.split('/');
+
+ // Once a property is removed from the `attrs` attribute
+ // the cellView will recognize a `dirty` flag and re-render itself
+ // in order to remove the attribute from SVG element.
+ var property = pathArray[0];
+ if (property === 'attrs') { opt.dirty = true; }
+
+ if (pathArray.length === 1) {
+ // A top level property
+ return this.unset(path, opt);
+ }
+
+ // A nested property
+ var nestedPath = pathArray.slice(1);
+ var propertyValue = this.get(property);
+ if (propertyValue === undefined || propertyValue === null) { return this; }
+ propertyValue = cloneDeep(propertyValue);
+
+ unsetByPath(propertyValue, nestedPath, '/');
+
+ return this.set(property, propertyValue, opt);
+ },
+
+ // A convenient way to set nested attributes.
+ attr: function(attrs, value, opt) {
+
+ var args = Array.from(arguments);
+ if (args.length === 0) {
+ return this.get('attrs');
+ }
+
+ if (Array.isArray(attrs)) {
+ args[0] = ['attrs'].concat(attrs);
+ } else if (isString(attrs)) {
+ // Get/set an attribute by a special path syntax that delimits
+ // nested objects by the colon character.
+ args[0] = 'attrs/' + attrs;
+
+ } else {
+
+ args[0] = { 'attrs' : attrs };
+ }
+
+ return this.prop.apply(this, args);
+ },
+
+ // A convenient way to unset nested attributes
+ removeAttr: function(path, opt) {
+
+ if (Array.isArray(path)) {
+
+ return this.removeProp(['attrs'].concat(path));
+ }
+
+ return this.removeProp('attrs/' + path, opt);
+ },
+
+ transition: function(path, value, opt, delim) {
+ var this$1 = this;
+
+
+ delim = delim || '/';
+
+ var defaults = {
+ duration: 100,
+ delay: 10,
+ timingFunction: timing.linear,
+ valueFunction: interpolate.number
+ };
+
+ opt = assign(defaults, opt);
+
+ var firstFrameTime = 0;
+ var interpolatingFunction;
+
+ var setter = function(runtime) {
+
+ var id, progress, propertyValue;
+
+ firstFrameTime = firstFrameTime || runtime;
+ runtime -= firstFrameTime;
+ progress = runtime / opt.duration;
+
+ if (progress < 1) {
+ this._transitionIds[path] = id = nextFrame(setter);
+ } else {
+ progress = 1;
+ delete this._transitionIds[path];
+ }
+
+ propertyValue = interpolatingFunction(opt.timingFunction(progress));
+
+ opt.transitionId = id;
+
+ this.prop(path, propertyValue, opt);
+
+ if (!id) { this.trigger('transition:end', this, path); }
+
+ }.bind(this);
+
+ var ref = this;
+ var _scheduledTransitionIds = ref._scheduledTransitionIds;
+ var initialId;
+
+ var initiator = function (callback) {
+
+ if (_scheduledTransitionIds[path]) {
+ _scheduledTransitionIds[path] = without(_scheduledTransitionIds[path], initialId);
+ if (_scheduledTransitionIds[path].length === 0) {
+ delete _scheduledTransitionIds[path];
+ }
+ }
+
+ this$1.stopPendingTransitions(path, delim);
+
+ interpolatingFunction = opt.valueFunction(getByPath(this$1.attributes, path, delim), value);
+
+ this$1._transitionIds[path] = nextFrame(callback);
+
+ this$1.trigger('transition:start', this$1, path);
+
+ };
+
+ initialId = setTimeout(initiator, opt.delay, setter);
+
+ _scheduledTransitionIds[path] || (_scheduledTransitionIds[path] = []);
+ _scheduledTransitionIds[path].push(initialId);
+
+ return initialId;
+ },
+
+ getTransitions: function() {
+ return union(
+ Object.keys(this._transitionIds),
+ Object.keys(this._scheduledTransitionIds)
+ );
+ },
+
+ stopScheduledTransitions: function(path, delim) {
+ if ( delim === void 0 ) delim = '/';
+
+ var ref = this;
+ var _scheduledTransitionIds = ref._scheduledTransitionIds; if ( _scheduledTransitionIds === void 0 ) _scheduledTransitionIds = {};
+ var transitions = Object.keys(_scheduledTransitionIds);
+ if (path) {
+ var pathArray = path.split(delim);
+ transitions = transitions.filter(function (key) {
+ return isEqual(pathArray, key.split(delim).slice(0, pathArray.length));
+ });
+ }
+ transitions.forEach(function (key) {
+ var transitionIds = _scheduledTransitionIds[key];
+ // stop the initiator
+ transitionIds.forEach(function (transitionId) { return clearTimeout(transitionId); });
+ delete _scheduledTransitionIds[key];
+ // Note: we could trigger transition:cancel` event here
+ });
+ return this;
+ },
+
+ stopPendingTransitions: function stopPendingTransitions(path, delim) {
+ var this$1 = this;
+ if ( delim === void 0 ) delim = '/';
+
+ var ref = this;
+ var _transitionIds = ref._transitionIds; if ( _transitionIds === void 0 ) _transitionIds = {};
+ var transitions = Object.keys(_transitionIds);
+ if (path) {
+ var pathArray = path.split(delim);
+ transitions = transitions.filter(function (key) {
+ return isEqual(pathArray, key.split(delim).slice(0, pathArray.length));
+ });
+ }
+ transitions.forEach(function (key) {
+ var transitionId = _transitionIds[key];
+ // stop the setter
+ cancelFrame(transitionId);
+ delete _transitionIds[key];
+ this$1.trigger('transition:end', this$1, key);
+ });
+ },
+
+ stopTransitions: function(path, delim) {
+ if ( delim === void 0 ) delim = '/';
+
+ this.stopScheduledTransitions(path, delim);
+ this.stopPendingTransitions(path, delim);
+ return this;
+ },
+
+ // A shorcut making it easy to create constructs like the following:
+ // `var el = (new joint.shapes.basic.Rect).addTo(graph)`.
+ addTo: function(graph, opt) {
+
+ graph.addCell(this, opt);
+ return this;
+ },
+
+ // A shortcut for an equivalent call: `paper.findViewByModel(cell)`
+ // making it easy to create constructs like the following:
+ // `cell.findView(paper).highlight()`
+ findView: function(paper) {
+
+ return paper.findViewByModel(this);
+ },
+
+ isElement: function() {
+
+ return false;
+ },
+
+ isLink: function() {
+
+ return false;
+ },
+
+ startBatch: function(name, opt) {
+
+ if (this.graph) { this.graph.startBatch(name, assign({}, opt, { cell: this })); }
+ return this;
+ },
+
+ stopBatch: function(name, opt) {
+
+ if (this.graph) { this.graph.stopBatch(name, assign({}, opt, { cell: this })); }
+ return this;
+ },
+
+ getChangeFlag: function(attributes) {
+
+ var flag = 0;
+ if (!attributes) { return flag; }
+ for (var key in attributes) {
+ if (!attributes.hasOwnProperty(key) || !this.hasChanged(key)) { continue; }
+ flag |= attributes[key];
+ }
+ return flag;
+ },
+
+ angle: function() {
+
+ // To be overridden.
+ return 0;
+ },
+
+ position: function() {
+
+ // To be overridden.
+ return new Point(0, 0);
+ },
+
+ getPointFromConnectedLink: function() {
+
+ // To be overridden
+ return new Point();
+ },
+
+ getBBox: function() {
+
+ // To be overridden
+ return new Rect(0, 0, 0, 0);
+ },
+
+ getPointRotatedAroundCenter: function getPointRotatedAroundCenter(angle, x, y) {
+ var point = new Point(x, y);
+ if (angle) { point.rotate(this.getBBox().center(), angle); }
+ return point;
+ },
+
+ getAbsolutePointFromRelative: function getAbsolutePointFromRelative(x, y) {
+ // Rotate the position to take the model angle into account
+ return this.getPointRotatedAroundCenter(
+ -this.angle(),
+ // Transform the relative position to absolute
+ this.position().offset(x, y)
+ );
+ },
+
+ getRelativePointFromAbsolute: function getRelativePointFromAbsolute(x, y) {
+ return this
+ // Rotate the coordinates to mitigate the element's rotation.
+ .getPointRotatedAroundCenter(this.angle(), x, y)
+ // Transform the absolute position into relative
+ .difference(this.position());
+ }
+
+ }, {
+
+ getAttributeDefinition: function(attrName) {
+
+ var defNS = this.attributes;
+ var globalDefNS = attributes;
+ return (defNS && defNS[attrName]) || globalDefNS[attrName];
+ },
+
+ define: function(type, defaults, protoProps, staticProps) {
+
+ protoProps = assign({
+ defaults: defaultsDeep({ type: type }, defaults, this.prototype.defaults)
+ }, protoProps);
+
+ var Cell = this.extend(protoProps, staticProps);
+ // es5 backward compatibility
+ /* eslint-disable no-undef */
+ if (typeof joint !== 'undefined' && has$2(joint, 'shapes')) {
+ setByPath(joint.shapes, type, Cell, '.');
+ }
+ /* eslint-enable no-undef */
+ return Cell;
+ }
+ });
+
+ var wrapWith = function(object, methods, wrapper) {
+
+ if (isString(wrapper)) {
+
+ if (!wrappers[wrapper]) {
+ throw new Error('Unknown wrapper: "' + wrapper + '"');
+ }
+
+ wrapper = wrappers[wrapper];
+ }
+
+ if (!isFunction(wrapper)) {
+ throw new Error('Wrapper must be a function.');
+ }
+
+ toArray(methods).forEach(function(method) {
+ object[method] = wrapper(object[method]);
+ });
+ };
+
+ var wrappers = {
+
+ cells: function(fn) {
+
+ return function() {
+
+ var args = Array.from(arguments);
+ var n = args.length;
+ var cells = n > 0 && args[0] || [];
+ var opt = n > 1 && args[n - 1] || {};
+
+ if (!Array.isArray(cells)) {
+
+ if (opt instanceof Cell) {
+ cells = args;
+ } else if (cells instanceof Cell) {
+ if (args.length > 1) {
+ args.pop();
+ }
+ cells = args;
+ }
+ }
+
+ if (opt instanceof Cell) {
+ opt = {};
+ }
+
+ return fn.call(this, cells, opt);
+ };
+ }
+
+ };
+
+ function svg(strings) {
+ var expressions = [], len = arguments.length - 1;
+ while ( len-- > 0 ) expressions[ len ] = arguments[ len + 1 ];
+
+ var svgParts = [];
+ strings.forEach(function (part, index) {
+ svgParts.push(part);
+ if (index in expressions) {
+ svgParts.push(expressions[index]);
+ }
+ });
+ var markup = parseFromSVGString(svgParts.join(''));
+ return markup;
+ }
+
+ function parseFromSVGString(str) {
+ var parser = new DOMParser();
+ var markupString = "";
+ var xmldocument = parser.parseFromString(markupString.replace(/@/g, ''), 'application/xml');
+ if (xmldocument.getElementsByTagName('parsererror')[0]) {
+ throw new Error('Invalid SVG markup');
+ }
+ var document = parser.parseFromString(markupString, 'text/html');
+ var svg = document.querySelector('svg');
+ return build(svg);
+ }
+
+ function build(root) {
+ var markup = [];
+
+ Array.from(root.children).forEach(function (node) {
+ var markupNode = {};
+ var tagName = node.tagName;
+ var attributes = node.attributes;
+ var textContent = node.textContent;
+ var namespaceURI = node.namespaceURI;
+ var style = node.style;
+
+ markupNode.tagName = tagName;
+ markupNode.namespaceURI = namespaceURI;
+
+ var stylesObject = {};
+ for (var i = style.length; i--;) {
+ var nameString = style[i];
+ stylesObject[nameString] = style.getPropertyValue(nameString);
+ }
+ markupNode.style = stylesObject;
+
+ // selector fallbacks to tagName
+ var selectorAttribute = attributes.getNamedItem('@selector');
+ if (selectorAttribute) {
+ markupNode.selector = selectorAttribute.value;
+ attributes.removeNamedItem('@selector');
+ }
+
+ var groupSelectorAttribute = attributes.getNamedItem('@group-selector');
+ if (groupSelectorAttribute) {
+ var groupSelectors = groupSelectorAttribute.value.split(',');
+ markupNode.groupSelector = groupSelectors.map(function (s) { return s.trim(); });
+
+ attributes.removeNamedItem('@group-selector');
+ }
+
+ var className = attributes.getNamedItem('class');
+ if (className) {
+ markupNode.className = className.value;
+ }
+
+ if (textContent) {
+ markupNode.textContent = textContent;
+ }
+
+ var nodeAttrs = {};
+
+ Array.from(attributes).forEach(function (nodeAttribute) {
+ var name = nodeAttribute.name;
+ var value = nodeAttribute.value;
+ nodeAttrs[name] = value;
+ });
+
+ if (Object.keys(nodeAttrs).length > 0) {
+ markupNode.attributes = nodeAttrs;
+ }
+
+ if (node.childElementCount > 0) {
+ markupNode.children = build(node);
+ }
+
+ markup.push(markupNode);
+ });
+
+ return markup;
+ }
+
+ var Positions = {
+ TOP: 'top',
+ RIGHT: 'right',
+ BOTTOM: 'bottom',
+ LEFT: 'left',
+ TOP_LEFT: 'top-left',
+ TOP_RIGHT: 'top-right',
+ BOTTOM_LEFT: 'bottom-left',
+ BOTTOM_RIGHT: 'bottom-right',
+ CENTER: 'center',
+ };
+
+ function getRectPoint(rect, position) {
+ var r = new Rect(rect);
+ switch (position) {
+ case undefined:
+ throw new Error('Position required');
+
+ // Middle Points
+ case Positions.LEFT:
+ case 'leftMiddle':
+ return r.leftMiddle();
+
+ case Positions.RIGHT:
+ case 'rightMiddle':
+ return r.rightMiddle();
+
+ case Positions.TOP:
+ case 'topMiddle':
+ return r.topMiddle();
+
+ case Positions.BOTTOM:
+ case 'bottomMiddle':
+ return r.bottomMiddle();
+
+ // Corners
+ case Positions.TOP_LEFT:
+ case 'topLeft':
+ case 'origin':
+ return r.topLeft();
+
+ case Positions.TOP_RIGHT:
+ case 'topRight':
+ return r.topRight();
+
+ case Positions.BOTTOM_LEFT:
+ case 'bottomLeft':
+ return r.bottomLeft();
+
+ case Positions.BOTTOM_RIGHT:
+ case 'bottomRight':
+ case 'corner':
+ return r.bottomRight();
+
+ // Center
+ case Positions.CENTER:
+ return r.center();
+
+ // TODO: calc(), percentage etc.
+ default:
+ throw new Error(("Unknown position: " + position));
+ }
+ }
+
+
+
+ var index = ({
+ getRectPoint: getRectPoint,
+ wrapWith: wrapWith,
+ wrappers: wrappers,
+ addClassNamePrefix: addClassNamePrefix,
+ removeClassNamePrefix: removeClassNamePrefix,
+ parseDOMJSON: parseDOMJSON,
+ hashCode: hashCode,
+ getByPath: getByPath,
+ setByPath: setByPath,
+ unsetByPath: unsetByPath,
+ flattenObject: flattenObject,
+ uuid: uuid,
+ guid: guid,
+ toKebabCase: toKebabCase,
+ normalizeEvent: normalizeEvent,
+ normalizeWheel: normalizeWheel,
+ cap: cap,
+ nextFrame: nextFrame,
+ cancelFrame: cancelFrame,
+ shapePerimeterConnectionPoint: shapePerimeterConnectionPoint,
+ isPercentage: isPercentage,
+ parseCssNumeric: parseCssNumeric,
+ breakText: breakText,
+ sanitizeHTML: sanitizeHTML,
+ downloadBlob: downloadBlob,
+ downloadDataUri: downloadDataUri,
+ dataUriToBlob: dataUriToBlob,
+ imageToDataUri: imageToDataUri,
+ getElementBBox: getElementBBox,
+ sortElements: sortElements,
+ setAttributesBySelector: setAttributesBySelector,
+ normalizeSides: normalizeSides,
+ timing: timing,
+ interpolate: interpolate,
+ filter: filter,
+ format: format,
+ template: template,
+ toggleFullScreen: toggleFullScreen,
+ mixin: mixin,
+ supplement: supplement,
+ deepMixin: deepMixin,
+ deepSupplement: deepSupplement,
+ assign: assign,
+ defaults: defaults,
+ defaultsDeep: defaultsDeep,
+ invoke: invoke,
+ sortedIndex: sortedIndex,
+ uniq: uniq,
+ clone: clone,
+ cloneDeep: cloneDeep,
+ isEmpty: isEmpty,
+ isEqual: isEqual,
+ isFunction: isFunction,
+ isPlainObject: isPlainObject,
+ toArray: toArray,
+ debounce: debounce,
+ groupBy: groupBy,
+ sortBy: sortBy,
+ flattenDeep: flattenDeep,
+ without: without,
+ difference: difference,
+ intersection: intersection$1,
+ union: union,
+ has: has$2,
+ result: result,
+ omit: omit,
+ pick: pick,
+ bindAll: bindAll,
+ forIn: forIn,
+ camelCase: camelCase,
+ uniqueId: uniqueId,
+ merge: merge,
+ isBoolean: isBoolean,
+ isObject: isObject$1,
+ isNumber: isNumber,
+ isString: isString,
+ noop: noop,
+ cloneCells: cloneCells,
+ svg: svg
+ });
+
+ function portTransformAttrs(point, angle, opt) {
+
+ var trans = point.toJSON();
+
+ trans.angle = angle || 0;
+
+ return defaults({}, opt, trans);
+ }
+
+ function lineLayout(ports, p1, p2, elBBox) {
+ return ports.map(function(port, index, ports) {
+ var p = this.pointAt(((index + 0.5) / ports.length));
+ // `dx`,`dy` per port offset option
+ if (port.dx || port.dy) {
+ p.offset(port.dx || 0, port.dy || 0);
+ }
+ return portTransformAttrs(p.round(), 0, argTransform(elBBox, port));
+ }, line(p1, p2));
+ }
+
+ function ellipseLayout(ports, elBBox, startAngle, stepFn) {
+
+ var center = elBBox.center();
+ var ratio = elBBox.width / elBBox.height;
+ var p1 = elBBox.topMiddle();
+
+ var ellipse = Ellipse.fromRect(elBBox);
+
+ return ports.map(function(port, index, ports) {
+
+ var angle = startAngle + stepFn(index, ports.length);
+ var p2 = p1.clone()
+ .rotate(center, -angle)
+ .scale(ratio, 1, center);
+
+ var theta = port.compensateRotation ? -ellipse.tangentTheta(p2) : 0;
+
+ // `dx`,`dy` per port offset option
+ if (port.dx || port.dy) {
+ p2.offset(port.dx || 0, port.dy || 0);
+ }
+
+ // `dr` delta radius option
+ if (port.dr) {
+ p2.move(center, port.dr);
+ }
+
+ return portTransformAttrs(p2.round(), theta, argTransform(elBBox, port));
+ });
+ }
+
+
+ function argTransform(bbox, args) {
+ var x = args.x;
+ var y = args.y;
+ var angle = args.angle;
+ if (isPercentage(x)) {
+ x = parseFloat(x) / 100 * bbox.width;
+ } else if (isCalcAttribute(x)) {
+ x = Number(evalCalcAttribute(x, bbox));
+ }
+ if (isPercentage(y)) {
+ y = parseFloat(y) / 100 * bbox.height;
+ } else if (isCalcAttribute(y)) {
+ y = Number(evalCalcAttribute(y, bbox));
+ }
+ return { x: x, y: y, angle: angle };
+ }
+
+ // Creates a point stored in arguments
+ function argPoint(bbox, args) {
+ var ref = argTransform(bbox, args);
+ var x = ref.x;
+ var y = ref.y;
+ return new Point(x || 0, y || 0);
+ }
+
+
+ /**
+ * @param {Array