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 = _.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 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;
}
var attributesNS = {
xlinkHref: {
set: 'xlink:href'
},
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'
},
filter: {
qualify: isPlainObject,
set: function(filter) {
return 'url(#' + this.paper.defineFilter(filter) + ')';
}
},
fill: {
qualify: isPlainObject,
set: function(fill) {
return 'url(#' + this.paper.defineGradient(fill) + ')';
}
},
stroke: {
qualify: isPlainObject,
set: function(stroke) {
return 'url(#' + this.paper.defineGradient(stroke) + ')';
}
},
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');
var fontSize = textAttrs.fontSize = attrs['font-size'] || attrs['fontSize'];
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) {
// option `width`
var width = value.width || 0;
if (isPercentage(width)) {
refBBox.width *= parseFloat(width) / 100;
} else if (width <= 0) {
refBBox.width += width;
} else {
refBBox.width = width;
}
// option `height`
var height = value.height || 0;
if (isPercentage(height)) {
refBBox.height *= parseFloat(height) / 100;
} else if (height <= 0) {
refBBox.height += height;
} else {
refBBox.height = height;
}
// option `text`
var wrappedText;
var text = value.text;
if (text === undefined) { text = attrs.text; }
if (text !== undefined) {
wrappedText = breakText('' + text, refBBox, {
'font-weight': attrs['font-weight'] || attrs.fontWeight,
'font-size': attrs['font-size'] || attrs.fontSize,
'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
});
} 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 sourceParts = path.divideAtLength(offset);
var targetParts = path.divideAtLength(-offset);
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 })
}
};
// Aliases
attributesNS.refR = attributesNS.refRInscribed;
attributesNS.refD = attributesNS.refDResetOffset;
attributesNS.refPoints = attributesNS.refPointsResetOffset;
attributesNS.atConnectionLength = attributesNS.atConnectionLengthKeepGradient;
attributesNS.atConnectionRatio = attributesNS.atConnectionRatioKeepGradient;
// 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 || {};
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 defaultAttrs = this.constructor.prototype.defaults.attrs || {};
var attrs = this.attributes.attrs;
var finalAttrs = {};
// Loop through all the attributes and
// omit the default attributes as they are implicitly reconstructable 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) {
if (!options || !options.id) {
this.set('id', this.generateId(), { silent: true });
}
this._transitionIds = {};
// Collect ports defined in `attrs` and keep collecting whenever `attrs` object changes.
this.processPorts();
this.on('change:attrs', this.processPorts, this);
},
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: true });
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: true });
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) {
if (this === cell || this.isEmbeddedIn(cell)) {
throw new Error('Recursive embedding not allowed.');
} else {
this.startBatch('embed');
var embeds = assign([], this.get('embeds'));
// We keep all element ids after link ids.
embeds[cell.isLink() ? 'unshift' : 'push'](cell.id);
cell.parent(this.id, opt);
this.set('embeds', uniq(embeds), opt);
this.stopBatch('embed');
}
return this;
},
unembed: function(cell, opt) {
this.startBatch('unembed');
cell.unset('parent', opt);
this.set('embeds', without(this.get('embeds'), cell.id), opt);
this.stopBatch('unembed');
return this;
},
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 = defaults({ 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('id', 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 = cloneDeep(this.get(property));
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) {
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 initiator = function(callback) {
this.stopTransitions(path);
interpolatingFunction = opt.valueFunction(getByPath(this.attributes, path, delim), value);
this._transitionIds[path] = nextFrame(callback);
this.trigger('transition:start', this, path);
}.bind(this);
return setTimeout(initiator, opt.delay, setter);
},
getTransitions: function() {
return Object.keys(this._transitionIds);
},
stopTransitions: function(path, delim) {
delim = delim || '/';
var pathArray = path && path.split(delim);
Object.keys(this._transitionIds).filter(pathArray && function(key) {
return isEqual(pathArray, key.split(delim).slice(0, pathArray.length));
}).forEach(function(key) {
cancelFrame(this._transitionIds[key]);
delete this._transitionIds[key];
this.trigger('transition:end', this, key);
}, this);
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);
}
}, {
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
/* global joint: true */
if (typeof joint !== 'undefined' && has$2(joint, 'shapes')) {
setByPath(joint.shapes, type, Cell, '.');
}
/* global joint: false */
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);
};
}
};
var index = ({
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,
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,
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
});
function portTransformAttrs(point, angle, opt) {
var trans = point.toJSON();
trans.angle = angle || 0;
return defaults({}, opt, trans);
}
function lineLayout(ports, p1, p2) {
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, 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, port);
});
}
// Creates a point stored in arguments
function argPoint(bbox, args) {
var x = args.x;
if (isString(x)) {
x = parseFloat(x) / 100 * bbox.width;
}
var y = args.y;
if (isString(y)) {
y = parseFloat(y) / 100 * bbox.height;
}
return point(x || 0, y || 0);
}
/**
* @param {Array