mxgraph
Version:
mxGraph is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.
2,217 lines (1,990 loc) • 107 kB
JavaScript
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
var mxUtils =
{
/**
* Class: mxUtils
*
* A singleton class that provides cross-browser helper methods.
* This is a global functionality. To access the functions in this
* class, use the global classname appended by the functionname.
* You may have to load chrome://global/content/contentAreaUtils.js
* to disable certain security restrictions in Mozilla for the <open>,
* <save>, <saveAs> and <copy> function.
*
* For example, the following code displays an error message:
*
* (code)
* mxUtils.error('Browser is not supported!', 200, false);
* (end)
*
* Variable: errorResource
*
* Specifies the resource key for the title of the error window. If the
* resource for this key does not exist then the value is used as
* the title. Default is 'error'.
*/
errorResource: (mxClient.language != 'none') ? 'error' : '',
/**
* Variable: closeResource
*
* Specifies the resource key for the label of the close button. If the
* resource for this key does not exist then the value is used as
* the label. Default is 'close'.
*/
closeResource: (mxClient.language != 'none') ? 'close' : '',
/**
* Variable: errorImage
*
* Defines the image used for error dialogs.
*/
errorImage: mxClient.imageBasePath + '/error.gif',
/**
* Function: removeCursors
*
* Removes the cursors from the style of the given DOM node and its
* descendants.
*
* Parameters:
*
* element - DOM node to remove the cursor style from.
*/
removeCursors: function(element)
{
if (element.style != null)
{
element.style.cursor = '';
}
var children = element.childNodes;
if (children != null)
{
var childCount = children.length;
for (var i = 0; i < childCount; i += 1)
{
mxUtils.removeCursors(children[i]);
}
}
},
/**
* Function: getCurrentStyle
*
* Returns the current style of the specified element.
*
* Parameters:
*
* element - DOM node whose current style should be returned.
*/
getCurrentStyle: function()
{
if (mxClient.IS_IE && (document.documentMode == null || document.documentMode < 9))
{
return function(element)
{
return (element != null) ? element.currentStyle : null;
};
}
else
{
return function(element)
{
return (element != null) ?
window.getComputedStyle(element, '') :
null;
};
}
}(),
/**
* Function: parseCssNumber
*
* Parses the given CSS numeric value adding handling for the values thin,
* medium and thick (2, 4 and 6).
*/
parseCssNumber: function(value)
{
if (value == 'thin')
{
value = '2';
}
else if (value == 'medium')
{
value = '4';
}
else if (value == 'thick')
{
value = '6';
}
value = parseFloat(value);
if (isNaN(value))
{
value = 0;
}
return value;
},
/**
* Function: setPrefixedStyle
*
* Adds the given style with the standard name and an optional vendor prefix for the current
* browser.
*
* (code)
* mxUtils.setPrefixedStyle(node.style, 'transformOrigin', '0% 0%');
* (end)
*/
setPrefixedStyle: function()
{
var prefix = null;
if (mxClient.IS_OT)
{
prefix = 'O';
}
else if (mxClient.IS_SF || mxClient.IS_GC)
{
prefix = 'Webkit';
}
else if (mxClient.IS_MT)
{
prefix = 'Moz';
}
else if (mxClient.IS_IE && document.documentMode >= 9 && document.documentMode < 10)
{
prefix = 'ms';
}
return function(style, name, value)
{
style[name] = value;
if (prefix != null && name.length > 0)
{
name = prefix + name.substring(0, 1).toUpperCase() + name.substring(1);
style[name] = value;
}
};
}(),
/**
* Function: hasScrollbars
*
* Returns true if the overflow CSS property of the given node is either
* scroll or auto.
*
* Parameters:
*
* node - DOM node whose style should be checked for scrollbars.
*/
hasScrollbars: function(node)
{
var style = mxUtils.getCurrentStyle(node);
return style != null && (style.overflow == 'scroll' || style.overflow == 'auto');
},
/**
* Function: bind
*
* Returns a wrapper function that locks the execution scope of the given
* function to the specified scope. Inside funct, the "this" keyword
* becomes a reference to that scope.
*/
bind: function(scope, funct)
{
return function()
{
return funct.apply(scope, arguments);
};
},
/**
* Function: eval
*
* Evaluates the given expression using eval and returns the JavaScript
* object that represents the expression result. Supports evaluation of
* expressions that define functions and returns the function object for
* these expressions.
*
* Parameters:
*
* expr - A string that represents a JavaScript expression.
*/
eval: function(expr)
{
var result = null;
if (expr.indexOf('function') >= 0)
{
try
{
eval('var _mxJavaScriptExpression='+expr);
result = _mxJavaScriptExpression;
// TODO: Use delete here?
_mxJavaScriptExpression = null;
}
catch (e)
{
mxLog.warn(e.message + ' while evaluating ' + expr);
}
}
else
{
try
{
result = eval(expr);
}
catch (e)
{
mxLog.warn(e.message + ' while evaluating ' + expr);
}
}
return result;
},
/**
* Function: findNode
*
* Returns the first node where attr equals value.
* This implementation does not use XPath.
*/
findNode: function(node, attr, value)
{
if (node.nodeType == mxConstants.NODETYPE_ELEMENT)
{
var tmp = node.getAttribute(attr);
if (tmp != null && tmp == value)
{
return node;
}
}
node = node.firstChild;
while (node != null)
{
var result = mxUtils.findNode(node, attr, value);
if (result != null)
{
return result;
}
node = node.nextSibling;
}
return null;
},
/**
* Function: getFunctionName
*
* Returns the name for the given function.
*
* Parameters:
*
* f - JavaScript object that represents a function.
*/
getFunctionName: function(f)
{
var str = null;
if (f != null)
{
if (f.name != null)
{
str = f.name;
}
else
{
str = mxUtils.trim(f.toString());
if (/^function\s/.test(str))
{
str = mxUtils.ltrim(str.substring(9));
var idx2 = str.indexOf('(');
if (idx2 > 0)
{
str = str.substring(0, idx2);
}
}
}
}
return str;
},
/**
* Function: indexOf
*
* Returns the index of obj in array or -1 if the array does not contain
* the given object.
*
* Parameters:
*
* array - Array to check for the given obj.
* obj - Object to find in the given array.
*/
indexOf: function(array, obj)
{
if (array != null && obj != null)
{
for (var i = 0; i < array.length; i++)
{
if (array[i] == obj)
{
return i;
}
}
}
return -1;
},
/**
* Function: forEach
*
* Calls the given function for each element of the given array and returns
* the array.
*
* Parameters:
*
* array - Array that contains the elements.
* fn - Function to be called for each object.
*/
forEach: function(array, fn)
{
if (array != null && fn != null)
{
for (var i = 0; i < array.length; i++)
{
fn(array[i]);
}
}
return array;
},
/**
* Function: remove
*
* Removes all occurrences of the given object in the given array or
* object. If there are multiple occurrences of the object, be they
* associative or as an array entry, all occurrences are removed from
* the array or deleted from the object. By removing the object from
* the array, all elements following the removed element are shifted
* by one step towards the beginning of the array.
*
* The length of arrays is not modified inside this function.
*
* Parameters:
*
* obj - Object to find in the given array.
* array - Array to check for the given obj.
*/
remove: function(obj, array)
{
var result = null;
if (typeof(array) == 'object')
{
var index = mxUtils.indexOf(array, obj);
while (index >= 0)
{
array.splice(index, 1);
result = obj;
index = mxUtils.indexOf(array, obj);
}
}
for (var key in array)
{
if (array[key] == obj)
{
delete array[key];
result = obj;
}
}
return result;
},
/**
* Function: isNode
*
* Returns true if the given value is an XML node with the node name
* and if the optional attribute has the specified value.
*
* This implementation assumes that the given value is a DOM node if the
* nodeType property is numeric, that is, if isNaN returns false for
* value.nodeType.
*
* Parameters:
*
* value - Object that should be examined as a node.
* nodeName - String that specifies the node name.
* attributeName - Optional attribute name to check.
* attributeValue - Optional attribute value to check.
*/
isNode: function(value, nodeName, attributeName, attributeValue)
{
if (value != null && !isNaN(value.nodeType) && (nodeName == null ||
value.nodeName.toLowerCase() == nodeName.toLowerCase()))
{
return attributeName == null ||
value.getAttribute(attributeName) == attributeValue;
}
return false;
},
/**
* Function: isAncestorNode
*
* Returns true if the given ancestor is an ancestor of the
* given DOM node in the DOM. This also returns true if the
* child is the ancestor.
*
* Parameters:
*
* ancestor - DOM node that represents the ancestor.
* child - DOM node that represents the child.
*/
isAncestorNode: function(ancestor, child)
{
var parent = child;
while (parent != null)
{
if (parent == ancestor)
{
return true;
}
parent = parent.parentNode;
}
return false;
},
/**
* Function: getChildNodes
*
* Returns an array of child nodes that are of the given node type.
*
* Parameters:
*
* node - Parent DOM node to return the children from.
* nodeType - Optional node type to return. Default is
* <mxConstants.NODETYPE_ELEMENT>.
*/
getChildNodes: function(node, nodeType)
{
nodeType = nodeType || mxConstants.NODETYPE_ELEMENT;
var children = [];
var tmp = node.firstChild;
while (tmp != null)
{
if (tmp.nodeType == nodeType)
{
children.push(tmp);
}
tmp = tmp.nextSibling;
}
return children;
},
/**
* Function: importNode
*
* Cross browser implementation for document.importNode. Uses document.importNode
* in all browsers but IE, where the node is cloned by creating a new node and
* copying all attributes and children into it using importNode, recursively.
*
* Parameters:
*
* doc - Document to import the node into.
* node - Node to be imported.
* allChildren - If all children should be imported.
*/
importNode: function(doc, node, allChildren)
{
if (mxClient.IS_IE && (document.documentMode == null || document.documentMode < 10))
{
return mxUtils.importNodeImplementation(doc, node, allChildren);
}
else
{
return doc.importNode(node, allChildren);
}
},
/**
* Function: importNodeImplementation
*
* Full DOM API implementation for importNode without using importNode API call.
*
* Parameters:
*
* doc - Document to import the node into.
* node - Node to be imported.
* allChildren - If all children should be imported.
*/
importNodeImplementation: function(doc, node, allChildren)
{
switch (node.nodeType)
{
case 1: /* element */
{
var newNode = doc.createElement(node.nodeName);
if (node.attributes && node.attributes.length > 0)
{
for (var i = 0; i < node.attributes.length; i++)
{
newNode.setAttribute(node.attributes[i].nodeName,
node.getAttribute(node.attributes[i].nodeName));
}
}
if (allChildren && node.childNodes && node.childNodes.length > 0)
{
for (var i = 0; i < node.childNodes.length; i++)
{
newNode.appendChild(mxUtils.importNodeImplementation(doc, node.childNodes[i], allChildren));
}
}
return newNode;
break;
}
case 3: /* text */
case 4: /* cdata-section */
case 8: /* comment */
{
return doc.createTextNode((node.nodeValue != null) ? node.nodeValue : node.value);
break;
}
};
},
/**
* Function: createXmlDocument
*
* Returns a new, empty XML document.
*/
createXmlDocument: function()
{
var doc = null;
if (document.implementation && document.implementation.createDocument)
{
doc = document.implementation.createDocument('', '', null);
}
else if ("ActiveXObject" in window)
{
doc = mxUtils.createMsXmlDocument();
}
return doc;
},
/**
* Function: createMsXmlDocument
*
* Returns a new, empty Microsoft.XMLDOM document using ActiveXObject.
*/
createMsXmlDocument: function()
{
var doc = new ActiveXObject('Microsoft.XMLDOM');
doc.async = false;
// Workaround for parsing errors with SVG DTD
doc.validateOnParse = false;
doc.resolveExternals = false;
return doc;
},
/**
* Function: parseXml
*
* Parses the specified XML string into a new XML document and returns the
* new document.
*
* Example:
*
* (code)
* var doc = mxUtils.parseXml(
* '<mxGraphModel><root><MyDiagram id="0"><mxCell/></MyDiagram>'+
* '<MyLayer id="1"><mxCell parent="0" /></MyLayer><MyObject id="2">'+
* '<mxCell style="strokeColor=blue;fillColor=red" parent="1" vertex="1">'+
* '<mxGeometry x="10" y="10" width="80" height="30" as="geometry"/>'+
* '</mxCell></MyObject></root></mxGraphModel>');
* (end)
*
* Parameters:
*
* xml - String that contains the XML data.
*/
parseXml: function()
{
if (window.DOMParser)
{
return function(xml)
{
var parser = new DOMParser();
return parser.parseFromString(xml, 'text/xml');
};
}
else // IE<=9
{
return function(xml)
{
var doc = mxUtils.createMsXmlDocument();
doc.loadXML(xml);
return doc;
};
}
}(),
/**
* Function: clearSelection
*
* Clears the current selection in the page.
*/
clearSelection: function()
{
if (document.selection)
{
return function()
{
document.selection.empty();
};
}
else if (window.getSelection)
{
return function()
{
if (window.getSelection().empty)
{
window.getSelection().empty();
}
else if (window.getSelection().removeAllRanges)
{
window.getSelection().removeAllRanges();
}
};
}
else
{
return function() { };
}
}(),
/**
* Function: removeWhitespace
*
* Removes the sibling text nodes for the given node that only consists
* of tabs, newlines and spaces.
*
* Parameters:
*
* node - DOM node whose siblings should be removed.
* before - Optional boolean that specifies the direction of the traversal.
*/
removeWhitespace: function(node, before)
{
var tmp = (before) ? node.previousSibling : node.nextSibling;
while (tmp != null && tmp.nodeType == mxConstants.NODETYPE_TEXT)
{
var next = (before) ? tmp.previousSibling : tmp.nextSibling;
var text = mxUtils.getTextContent(tmp);
if (mxUtils.trim(text).length == 0)
{
tmp.parentNode.removeChild(tmp);
}
tmp = next;
}
},
/**
* Function: htmlEntities
*
* Replaces characters (less than, greater than, newlines and quotes) with
* their HTML entities in the given string and returns the result.
*
* Parameters:
*
* s - String that contains the characters to be converted.
* newline - If newlines should be replaced. Default is true.
*/
htmlEntities: function(s, newline)
{
s = String(s || '');
s = s.replace(/&/g,'&'); // 38 26
s = s.replace(/"/g,'"'); // 34 22
s = s.replace(/\'/g,'''); // 39 27
s = s.replace(/</g,'<'); // 60 3C
s = s.replace(/>/g,'>'); // 62 3E
if (newline == null || newline)
{
s = s.replace(/\n/g, '
');
}
return s;
},
/**
* Function: isVml
*
* Returns true if the given node is in the VML namespace.
*
* Parameters:
*
* node - DOM node whose tag urn should be checked.
*/
isVml: function(node)
{
return node != null && node.tagUrn == 'urn:schemas-microsoft-com:vml';
},
/**
* Function: getXml
*
* Returns the XML content of the specified node. For Internet Explorer,
* all \r\n\t[\t]* are removed from the XML string and the remaining \r\n
* are replaced by \n. All \n are then replaced with linefeed, or 
 if
* no linefeed is defined.
*
* Parameters:
*
* node - DOM node to return the XML for.
* linefeed - Optional string that linefeeds are converted into. Default is
* 

*/
getXml: function(node, linefeed)
{
var xml = '';
if (mxClient.IS_IE || mxClient.IS_IE11)
{
xml = mxUtils.getPrettyXml(node, '', '', '');
}
else if (window.XMLSerializer != null)
{
var xmlSerializer = new XMLSerializer();
xml = xmlSerializer.serializeToString(node);
}
else if (node.xml != null)
{
xml = node.xml.replace(/\r\n\t[\t]*/g, '').
replace(/>\r\n/g, '>').
replace(/\r\n/g, '\n');
}
// Replaces linefeeds with HTML Entities.
linefeed = linefeed || '
';
xml = xml.replace(/\n/g, linefeed);
return xml;
},
/**
* Function: getPrettyXML
*
* Returns a pretty printed string that represents the XML tree for the
* given node. This method should only be used to print XML for reading,
* use <getXml> instead to obtain a string for processing.
*
* Parameters:
*
* node - DOM node to return the XML for.
* tab - Optional string that specifies the indentation for one level.
* Default is two spaces.
* indent - Optional string that represents the current indentation.
* Default is an empty string.
* newline - Option string that represents a linefeed. Default is '\n'.
*/
getPrettyXml: function(node, tab, indent, newline, ns)
{
var result = [];
if (node != null)
{
tab = (tab != null) ? tab : ' ';
indent = (indent != null) ? indent : '';
newline = (newline != null) ? newline : '\n';
if (node.namespaceURI != null && node.namespaceURI != ns)
{
ns = node.namespaceURI;
if (node.getAttribute('xmlns') == null)
{
node.setAttribute('xmlns', node.namespaceURI);
}
}
if (node.nodeType == mxConstants.NODETYPE_DOCUMENT)
{
result.push(mxUtils.getPrettyXml(node.documentElement, tab, indent, newline, ns));
}
else if (node.nodeType == mxConstants.NODETYPE_DOCUMENT_FRAGMENT)
{
var tmp = node.firstChild;
if (tmp != null)
{
while (tmp != null)
{
result.push(mxUtils.getPrettyXml(tmp, tab, indent, newline, ns));
tmp = tmp.nextSibling;
}
}
}
else if (node.nodeType == mxConstants.NODETYPE_COMMENT)
{
var value = mxUtils.getTextContent(node);
if (value.length > 0)
{
result.push(indent + '<!--' + value + '-->' + newline);
}
}
else if (node.nodeType == mxConstants.NODETYPE_TEXT)
{
var value = mxUtils.trim(mxUtils.getTextContent(node));
if (value.length > 0)
{
result.push(indent + mxUtils.htmlEntities(value, false) + newline);
}
}
else if (node.nodeType == mxConstants.NODETYPE_CDATA)
{
var value = mxUtils.getTextContent(node);
if (value.length > 0)
{
result.push(indent + '<![CDATA[' + value + ']]' + newline);
}
}
else
{
result.push(indent + '<' + node.nodeName);
// Creates the string with the node attributes
// and converts all HTML entities in the values
var attrs = node.attributes;
if (attrs != null)
{
for (var i = 0; i < attrs.length; i++)
{
var val = mxUtils.htmlEntities(attrs[i].value);
result.push(' ' + attrs[i].nodeName + '="' + val + '"');
}
}
// Recursively creates the XML string for each child
// node and appends it here with an indentation
var tmp = node.firstChild;
if (tmp != null)
{
result.push('>' + newline);
while (tmp != null)
{
result.push(mxUtils.getPrettyXml(tmp, tab, indent + tab, newline, ns));
tmp = tmp.nextSibling;
}
result.push(indent + '</'+ node.nodeName + '>' + newline);
}
else
{
result.push(' />' + newline);
}
}
}
return result.join('');
},
/**
* Function: extractTextWithWhitespace
*
* Returns the text content of the specified node.
*
* Parameters:
*
* elems - DOM nodes to return the text for.
*/
extractTextWithWhitespace: function(elems)
{
// Known block elements for handling linefeeds (list is not complete)
var blocks = ['BLOCKQUOTE', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'OL', 'P', 'PRE', 'TABLE', 'UL'];
var ret = [];
function doExtract(elts)
{
// Single break should be ignored
if (elts.length == 1 && (elts[0].nodeName == 'BR' ||
elts[0].innerHTML == '\n'))
{
return;
}
for (var i = 0; i < elts.length; i++)
{
var elem = elts[i];
// DIV with a br or linefeed forces a linefeed
if (elem.nodeName == 'BR' || elem.innerHTML == '\n' ||
((elts.length == 1 || i == 0) && (elem.nodeName == 'DIV' &&
elem.innerHTML.toLowerCase() == '<br>')))
{
ret.push('\n');
}
else
{
if (elem.nodeType === 3 || elem.nodeType === 4)
{
if (elem.nodeValue.length > 0)
{
ret.push(elem.nodeValue);
}
}
else if (elem.nodeType !== 8 && elem.childNodes.length > 0)
{
doExtract(elem.childNodes);
}
if (i < elts.length - 1 && mxUtils.indexOf(blocks, elts[i + 1].nodeName) >= 0)
{
ret.push('\n');
}
}
}
};
doExtract(elems);
return ret.join('');
},
/**
* Function: replaceTrailingNewlines
*
* Replaces each trailing newline with the given pattern.
*/
replaceTrailingNewlines: function(str, pattern)
{
// LATER: Check is this can be done with a regular expression
var postfix = '';
while (str.length > 0 && str.charAt(str.length - 1) == '\n')
{
str = str.substring(0, str.length - 1);
postfix += pattern;
}
return str + postfix;
},
/**
* Function: getTextContent
*
* Returns the text content of the specified node.
*
* Parameters:
*
* node - DOM node to return the text content for.
*/
getTextContent: function(node)
{
// Only IE10-
if (mxClient.IS_IE && node.innerText !== undefined)
{
return node.innerText;
}
else
{
return (node != null) ? node[(node.textContent === undefined) ? 'text' : 'textContent'] : '';
}
},
/**
* Function: setTextContent
*
* Sets the text content of the specified node.
*
* Parameters:
*
* node - DOM node to set the text content for.
* text - String that represents the text content.
*/
setTextContent: function(node, text)
{
if (node.innerText !== undefined)
{
node.innerText = text;
}
else
{
node[(node.textContent === undefined) ? 'text' : 'textContent'] = text;
}
},
/**
* Function: getInnerHtml
*
* Returns the inner HTML for the given node as a string or an empty string
* if no node was specified. The inner HTML is the text representing all
* children of the node, but not the node itself.
*
* Parameters:
*
* node - DOM node to return the inner HTML for.
*/
getInnerHtml: function()
{
if (mxClient.IS_IE)
{
return function(node)
{
if (node != null)
{
return node.innerHTML;
}
return '';
};
}
else
{
return function(node)
{
if (node != null)
{
var serializer = new XMLSerializer();
return serializer.serializeToString(node);
}
return '';
};
}
}(),
/**
* Function: getOuterHtml
*
* Returns the outer HTML for the given node as a string or an empty
* string if no node was specified. The outer HTML is the text representing
* all children of the node including the node itself.
*
* Parameters:
*
* node - DOM node to return the outer HTML for.
*/
getOuterHtml: function()
{
if (mxClient.IS_IE)
{
return function(node)
{
if (node != null)
{
if (node.outerHTML != null)
{
return node.outerHTML;
}
else
{
var tmp = [];
tmp.push('<'+node.nodeName);
var attrs = node.attributes;
if (attrs != null)
{
for (var i = 0; i < attrs.length; i++)
{
var value = attrs[i].value;
if (value != null && value.length > 0)
{
tmp.push(' ');
tmp.push(attrs[i].nodeName);
tmp.push('="');
tmp.push(value);
tmp.push('"');
}
}
}
if (node.innerHTML.length == 0)
{
tmp.push('/>');
}
else
{
tmp.push('>');
tmp.push(node.innerHTML);
tmp.push('</'+node.nodeName+'>');
}
return tmp.join('');
}
}
return '';
};
}
else
{
return function(node)
{
if (node != null)
{
var serializer = new XMLSerializer();
return serializer.serializeToString(node);
}
return '';
};
}
}(),
/**
* Function: write
*
* Creates a text node for the given string and appends it to the given
* parent. Returns the text node.
*
* Parameters:
*
* parent - DOM node to append the text node to.
* text - String representing the text to be added.
*/
write: function(parent, text)
{
var doc = parent.ownerDocument;
var node = doc.createTextNode(text);
if (parent != null)
{
parent.appendChild(node);
}
return node;
},
/**
* Function: writeln
*
* Creates a text node for the given string and appends it to the given
* parent with an additional linefeed. Returns the text node.
*
* Parameters:
*
* parent - DOM node to append the text node to.
* text - String representing the text to be added.
*/
writeln: function(parent, text)
{
var doc = parent.ownerDocument;
var node = doc.createTextNode(text);
if (parent != null)
{
parent.appendChild(node);
parent.appendChild(document.createElement('br'));
}
return node;
},
/**
* Function: br
*
* Appends a linebreak to the given parent and returns the linebreak.
*
* Parameters:
*
* parent - DOM node to append the linebreak to.
*/
br: function(parent, count)
{
count = count || 1;
var br = null;
for (var i = 0; i < count; i++)
{
if (parent != null)
{
br = parent.ownerDocument.createElement('br');
parent.appendChild(br);
}
}
return br;
},
/**
* Function: button
*
* Returns a new button with the given level and function as an onclick
* event handler.
*
* (code)
* document.body.appendChild(mxUtils.button('Test', function(evt)
* {
* alert('Hello, World!');
* }));
* (end)
*
* Parameters:
*
* label - String that represents the label of the button.
* funct - Function to be called if the button is pressed.
* doc - Optional document to be used for creating the button. Default is the
* current document.
*/
button: function(label, funct, doc)
{
doc = (doc != null) ? doc : document;
var button = doc.createElement('button');
mxUtils.write(button, label);
mxEvent.addListener(button, 'click', function(evt)
{
funct(evt);
});
return button;
},
/**
* Function: para
*
* Appends a new paragraph with the given text to the specified parent and
* returns the paragraph.
*
* Parameters:
*
* parent - DOM node to append the text node to.
* text - String representing the text for the new paragraph.
*/
para: function(parent, text)
{
var p = document.createElement('p');
mxUtils.write(p, text);
if (parent != null)
{
parent.appendChild(p);
}
return p;
},
/**
* Function: addTransparentBackgroundFilter
*
* Adds a transparent background to the filter of the given node. This
* background can be used in IE8 standards mode (native IE8 only) to pass
* events through the node.
*/
addTransparentBackgroundFilter: function(node)
{
node.style.filter += 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\'' +
mxClient.imageBasePath + '/transparent.gif\', sizingMethod=\'scale\')';
},
/**
* Function: linkAction
*
* Adds a hyperlink to the specified parent that invokes action on the
* specified editor.
*
* Parameters:
*
* parent - DOM node to contain the new link.
* text - String that is used as the link label.
* editor - <mxEditor> that will execute the action.
* action - String that defines the name of the action to be executed.
* pad - Optional left-padding for the link. Default is 0.
*/
linkAction: function(parent, text, editor, action, pad)
{
return mxUtils.link(parent, text, function()
{
editor.execute(action);
}, pad);
},
/**
* Function: linkInvoke
*
* Adds a hyperlink to the specified parent that invokes the specified
* function on the editor passing along the specified argument. The
* function name is the name of a function of the editor instance,
* not an action name.
*
* Parameters:
*
* parent - DOM node to contain the new link.
* text - String that is used as the link label.
* editor - <mxEditor> instance to execute the function on.
* functName - String that represents the name of the function.
* arg - Object that represents the argument to the function.
* pad - Optional left-padding for the link. Default is 0.
*/
linkInvoke: function(parent, text, editor, functName, arg, pad)
{
return mxUtils.link(parent, text, function()
{
editor[functName](arg);
}, pad);
},
/**
* Function: link
*
* Adds a hyperlink to the specified parent and invokes the given function
* when the link is clicked.
*
* Parameters:
*
* parent - DOM node to contain the new link.
* text - String that is used as the link label.
* funct - Function to execute when the link is clicked.
* pad - Optional left-padding for the link. Default is 0.
*/
link: function(parent, text, funct, pad)
{
var a = document.createElement('span');
a.style.color = 'blue';
a.style.textDecoration = 'underline';
a.style.cursor = 'pointer';
if (pad != null)
{
a.style.paddingLeft = pad+'px';
}
mxEvent.addListener(a, 'click', funct);
mxUtils.write(a, text);
if (parent != null)
{
parent.appendChild(a);
}
return a;
},
/**
* Function: getDocumentSize
*
* Returns the client size for the current document as an <mxRectangle>.
*/
getDocumentSize: function()
{
var b = document.body;
var d = document.documentElement;
try
{
return new mxRectangle(0, 0, b.clientWidth || d.clientWidth, Math.max(b.clientHeight || 0, d.clientHeight));
}
catch (e)
{
return new mxRectangle();
}
},
/**
* Function: fit
*
* Makes sure the given node is inside the visible area of the window. This
* is done by setting the left and top in the style.
*/
fit: function(node)
{
var ds = mxUtils.getDocumentSize();
var left = parseInt(node.offsetLeft);
var width = parseInt(node.offsetWidth);
var offset = mxUtils.getDocumentScrollOrigin(node.ownerDocument);
var sl = offset.x;
var st = offset.y;
var b = document.body;
var d = document.documentElement;
var right = (sl) + ds.width;
if (left + width > right)
{
node.style.left = Math.max(sl, right - width) + 'px';
}
var top = parseInt(node.offsetTop);
var height = parseInt(node.offsetHeight);
var bottom = st + ds.height;
if (top + height > bottom)
{
node.style.top = Math.max(st, bottom - height) + 'px';
}
},
/**
* Function: load
*
* Loads the specified URL *synchronously* and returns the <mxXmlRequest>.
* Throws an exception if the file cannot be loaded. See <mxUtils.get> for
* an asynchronous implementation.
*
* Example:
*
* (code)
* try
* {
* var req = mxUtils.load(filename);
* var root = req.getDocumentElement();
* // Process XML DOM...
* }
* catch (ex)
* {
* mxUtils.alert('Cannot load '+filename+': '+ex);
* }
* (end)
*
* Parameters:
*
* url - URL to get the data from.
*/
load: function(url)
{
var req = new mxXmlRequest(url, null, 'GET', false);
req.send();
return req;
},
/**
* Function: get
*
* Loads the specified URL *asynchronously* and invokes the given functions
* depending on the request status. Returns the <mxXmlRequest> in use. Both
* functions take the <mxXmlRequest> as the only parameter. See
* <mxUtils.load> for a synchronous implementation.
*
* Example:
*
* (code)
* mxUtils.get(url, function(req)
* {
* var node = req.getDocumentElement();
* // Process XML DOM...
* });
* (end)
*
* So for example, to load a diagram into an existing graph model, the
* following code is used.
*
* (code)
* mxUtils.get(url, function(req)
* {
* var node = req.getDocumentElement();
* var dec = new mxCodec(node.ownerDocument);
* dec.decode(node, graph.getModel());
* });
* (end)
*
* Parameters:
*
* url - URL to get the data from.
* onload - Optional function to execute for a successful response.
* onerror - Optional function to execute on error.
* binary - Optional boolean parameter that specifies if the request is
* binary.
* timeout - Optional timeout in ms before calling ontimeout.
* ontimeout - Optional function to execute on timeout.
* headers - Optional with headers, eg. {'Authorization': 'token xyz'}
*/
get: function(url, onload, onerror, binary, timeout, ontimeout, headers)
{
var req = new mxXmlRequest(url, null, 'GET');
var setRequestHeaders = req.setRequestHeaders;
if (headers)
{
req.setRequestHeaders = function(request, params)
{
setRequestHeaders.apply(this, arguments);
for (var key in headers)
{
request.setRequestHeader(key, headers[key]);
}
};
}
if (binary != null)
{
req.setBinary(binary);
}
req.send(onload, onerror, timeout, ontimeout);
return req;
},
/**
* Function: getAll
*
* Loads the URLs in the given array *asynchronously* and invokes the given function
* if all requests returned with a valid 2xx status. The error handler is invoked
* once on the first error or invalid response.
*
* Parameters:
*
* urls - Array of URLs to be loaded.
* onload - Callback with array of <mxXmlRequests>.
* onerror - Optional function to execute on error.
*/
getAll: function(urls, onload, onerror)
{
var remain = urls.length;
var result = [];
var errors = 0;
var err = function()
{
if (errors == 0 && onerror != null)
{
onerror();
}
errors++;
};
for (var i = 0; i < urls.length; i++)
{
(function(url, index)
{
mxUtils.get(url, function(req)
{
var status = req.getStatus();
if (status < 200 || status > 299)
{
err();
}
else
{
result[index] = req;
remain--;
if (remain == 0)
{
onload(result);
}
}
}, err);
})(urls[i], i);
}
if (remain == 0)
{
onload(result);
}
},
/**
* Function: post
*
* Posts the specified params to the given URL *asynchronously* and invokes
* the given functions depending on the request status. Returns the
* <mxXmlRequest> in use. Both functions take the <mxXmlRequest> as the
* only parameter. Make sure to use encodeURIComponent for the parameter
* values.
*
* Example:
*
* (code)
* mxUtils.post(url, 'key=value', function(req)
* {
* mxUtils.alert('Ready: '+req.isReady()+' Status: '+req.getStatus());
* // Process req.getDocumentElement() using DOM API if OK...
* });
* (end)
*
* Parameters:
*
* url - URL to get the data from.
* params - Parameters for the post request.
* onload - Optional function to execute for a successful response.
* onerror - Optional function to execute on error.
*/
post: function(url, params, onload, onerror)
{
return new mxXmlRequest(url, params).send(onload, onerror);
},
/**
* Function: submit
*
* Submits the given parameters to the specified URL using
* <mxXmlRequest.simulate> and returns the <mxXmlRequest>.
* Make sure to use encodeURIComponent for the parameter
* values.
*
* Parameters:
*
* url - URL to get the data from.
* params - Parameters for the form.
* doc - Document to create the form in.
* target - Target to send the form result to.
*/
submit: function(url, params, doc, target)
{
return new mxXmlRequest(url, params).simulate(doc, target);
},
/**
* Function: loadInto
*
* Loads the specified URL *asynchronously* into the specified document,
* invoking onload after the document has been loaded. This implementation
* does not use <mxXmlRequest>, but the document.load method.
*
* Parameters:
*
* url - URL to get the data from.
* doc - The document to load the URL into.
* onload - Function to execute when the URL has been loaded.
*/
loadInto: function(url, doc, onload)
{
if (mxClient.IS_IE)
{
doc.onreadystatechange = function ()
{
if (doc.readyState == 4)
{
onload();
}
};
}
else
{
doc.addEventListener('load', onload, false);
}
doc.load(url);
},
/**
* Function: getValue
*
* Returns the value for the given key in the given associative array or
* the given default value if the value is null.
*
* Parameters:
*
* array - Associative array that contains the value for the key.
* key - Key whose value should be returned.
* defaultValue - Value to be returned if the value for the given
* key is null.
*/
getValue: function(array, key, defaultValue)
{
var value = (array != null) ? array[key] : null;
if (value == null)
{
value = defaultValue;
}
return value;
},
/**
* Function: getNumber
*
* Returns the numeric value for the given key in the given associative
* array or the given default value (or 0) if the value is null. The value
* is converted to a numeric value using the Number function.
*
* Parameters:
*
* array - Associative array that contains the value for the key.
* key - Key whose value should be returned.
* defaultValue - Value to be returned if the value for the given
* key is null. Default is 0.
*/
getNumber: function(array, key, defaultValue)
{
var value = (array != null) ? array[key] : null;
if (value == null)
{
value = defaultValue || 0;
}
return Number(value);
},
/**
* Function: getColor
*
* Returns the color value for the given key in the given associative
* array or the given default value if the value is null. If the value
* is <mxConstants.NONE> then null is returned.
*
* Parameters:
*
* array - Associative array that contains the value for the key.
* key - Key whose value should be returned.
* defaultValue - Value to be returned if the value for the given
* key is null. Default is null.
*/
getColor: function(array, key, defaultValue)
{
var value = (array != null) ? array[key] : null;
if (value == null)
{
value = defaultValue;
}
else if (value == mxConstants.NONE)
{
value = null;
}
return value;
},
/**
* Function: clone
*
* Recursively clones the specified object ignoring all fieldnames in the
* given array of transient fields. <mxObjectIdentity.FIELD_NAME> is always
* ignored by this function.
*
* Parameters:
*
* obj - Object to be cloned.
* transients - Optional array of strings representing the fieldname to be
* ignored.
* shallow - Optional boolean argument to specify if a shallow clone should
* be created, that is, one where all object references are not cloned or,
* in other words, one where only atomic (strings, numbers) values are
* cloned. Default is false.
*/
clone: function(obj, transients, shallow)
{
shallow = (shallow != null) ? shallow : false;
var clone = null;
if (obj != null && typeof(obj.constructor) == 'function')
{
clone = new obj.constructor();
for (var i in obj)
{
if (i != mxObjectIdentity.FIELD_NAME && (transients == null ||
mxUtils.indexOf(transients, i) < 0))
{
if (!shallow && typeof(obj[i]) == 'object')
{
clone[i] = mxUtils.clone(obj[i]);
}
else
{
clone[i] = obj[i];
}
}
}
}
return clone;
},
/**
* Function: equalPoints
*
* Compares all mxPoints in the given lists.
*
* Parameters:
*
* a - Array of <mxPoints> to be compared.
* b - Array of <mxPoints> to be compared.
*/
equalPoints: function(a, b)
{
if ((a == null && b != null) || (a != null && b == null) ||
(a != null && b != null && a.length != b.length))
{
return false;
}
else if (a != null && b != null)
{
for (var i = 0; i < a.length; i++)
{
if ((a[i] != null && b[i] == null) ||
(a[i] == null && b[i] != null) ||
(a[i] != null && b[i] != null &&
(a[i].x != b[i].x || a[i].y != b[i].y)))
{
return false;
}
}
}
return true;
},
/**
* Function: equalEntries
*
* Returns true if all properties of the given objects are equal. Values
* with NaN are equal to NaN and unequal to any other value.
*
* Parameters:
*
* a - First object to be compared.
* b - Second object to be compared.
*/
equalEntries: function(a, b)
{
// Counts keys in b to check if all values have been compared
var count = 0;
if ((a == null && b != null) || (a != null && b == null) ||
(a != null && b != null && a.length != b.length))
{
return false;
}
else if (a != null && b != null)
{
for (var key in b)
{
count++;
}
for (var key in a)
{
count--
if ((!mxUtils.isNaN(a[key]) || !mxUtils.isNaN(b[key])) && a[key] != b[key])
{
return false;
}
}
}
return count == 0;
},
/**
* Function: removeDuplicates
*
* Removes all duplicates from the given array.
*/
removeDuplicates: function(arr)
{
var dict = new mxDictionary();
var result = [];
for (var i = 0; i < arr.length; i++)
{
if (!dict.get(arr[i]))
{
result.push(arr[i]);
dict.put(arr[i], true);
}
}
return result;
},
/**
* Function: isNaN
*
* Returns true if the given value is of type number and isNaN returns true.
*/
isNaN: function(value)
{
return typeof(value) == 'number' && isNaN(value);
},
/**
* Function: extend
*
* Assigns a copy of the superclass prototype to the subclass prototype.
* Note that this does not call the constructor of the superclass at this
* point, the superclass constructor should be called explicitely in the
* subclass constructor. Below is an example.
*
* (code)
* MyGraph = function(container, model, renderHint, stylesheet)
* {
* mxGraph.call(this, container, model, renderHint, stylesheet);
* }
*
* mxUtils.extend(MyGraph, mxGraph);
* (end)
*
* Parameters:
*
* ctor - Constructor of the subclass.
* superCtor - Constructor of the superclass.
*/
extend: function(ctor, superCtor)
{
var f = function() {};
f.prototype = superCtor.prototype;
ctor.prototype = new f();
ctor.prototype.constructor = ctor;
},
/**
* Function: toString
*
* Returns a textual representation of the specified object.
*
* Parameters:
*
* obj - Object to return the string representation for.
*/
toString: function(obj)
{
var output = '';
for (var i in obj)
{
try
{
if (obj[i] == null)
{
output += i + ' = [null]\n';
}
else if (typeof(obj[i]) == 'function')
{
output += i + ' => [Function]\n';
}
else if (typeof(obj[i]) == 'object')
{
var ctor = mxUtils.getFunctionName(obj[i].constructor);
output += i + ' => [' + ctor + ']\n';
}
else
{
output += i + ' = ' + obj[i] + '\n';
}
}
catch (e)
{
output += i + '=' + e.message;
}
}
return output;
},
/**
* Function: toRadians
*
* Converts the given degree to radians.
*/
toRadians: function(deg)
{
return Math.PI * deg / 180;
},
/**
* Function: toDegree
*
* Converts the given radians to degree.
*/
toDegree: function(rad)
{
return rad * 180 / Math.PI;
},
/**
* Function: arcToCurves
*
* Converts the given arc to a series of curves.
*/
arcToCurves: function(x0, y0, r1, r2, angle, largeArcFlag, sweepFlag, x, y)
{
x -= x0;
y -= y0;
if (r1 === 0 || r2 === 0)
{
return result;
}
var fS = sweepFlag;
var psai = angle;
r1 = Math.abs(r1);
r2 = Math.abs(r2);
var ctx = -x / 2;
var cty = -y / 2;
var cpsi = Math.cos(psai * Math.PI / 180);
var spsi = Math.sin(psai * Math.PI / 180);
var rxd = cpsi * ctx + spsi * cty;
var ryd = -1 * spsi * ctx + cpsi * cty;
var rxdd = rxd * rxd;
var rydd = ryd * ryd;
var r1x = r1 * r1;
var r2y = r2 * r2;
var lamda = rxdd / r1x + rydd / r2y;
var sds;
if (lamda > 1)
{
r1 = Math.sqrt(lamda) * r1;
r2 = Math.sqrt(lamda) * r2;
sds = 0;
}
else
{
var seif = 1;
if (largeArcFlag === fS)
{
seif = -1;
}
sds = seif * Math.sqrt((r1x * r2y - r1x * rydd - r2y * rxdd) / (r1x * rydd + r2y * rxdd));
}
var txd = sds * r1 * ryd / r2;
var tyd = -1 * sds * r2 * rxd / r1;
var tx = cpsi * txd - spsi * tyd + x / 2;
var ty = spsi * txd + cpsi * tyd + y / 2;
var rad = Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1) - Math.atan2(0, 1);
var s1 = (rad >= 0) ? rad : 2 * Math.PI + rad;
rad = Math.atan2((-ryd - tyd) / r2, (-rxd - txd) / r1) - Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1);
var dr = (rad >= 0) ? rad : 2 * Math.PI + rad;
if (fS == 0 && dr > 0)
{
dr -= 2 * Math.PI;
}
else if (fS != 0 && dr < 0)
{
dr += 2 * Math.PI;
}
var sse = dr * 2 / Math.PI;
var seg = Math.ceil(sse < 0 ? -1 * sse : sse);
var segr = dr / seg;
var t = 8/3 * Math.sin(segr / 4) * Math.sin(segr / 4) / Math.sin(segr / 2);
var cpsir1 = cpsi * r1;
var cpsir2 = cpsi * r2;
var spsir1 = spsi * r1;
var spsir2 = spsi * r2;
var mc = Math.cos(s1);
var ms = Math.sin(s1);
var x2 = -t * (cpsir1 * ms + spsir2 * mc);
var y2 = -t * (spsir1 * ms - cpsir2 * mc);
var x3 = 0;
var y3 = 0;
var result = [];
for (var n = 0; n < seg; ++n)
{
s1 += segr;
mc = Math.cos(s1);
ms = Math.sin(s1);
x3 = cpsir1 * mc - spsir2 * ms + tx;
y3 = spsir1 * mc + cpsir2 * ms + ty;
var dx = -t * (cpsir1 * ms + spsir2 * mc);
var dy = -t * (spsir1 * ms - cpsir2 * mc);
// CurveTo updates x0, y0 so need to restore it
var index = n * 6;
result[index] = Number(x2 + x0);
result[index + 1] = Number(y2 + y0);
result[index + 2] = Number(x3 - dx + x0);
result[index + 3] = Number(y3 - dy + y0);
result[index + 4] = Number(x3 + x0);
result[index + 5] = Number(y3 + y0);
x2 = x3 + dx;
y2 = y3 + dy;
}
return result;
},
/**
* Function: getBoundingBox
*
* Returns the bounding box for the rotated rectangle.
*
* Parameters:
*
* rect - <mxRectangle> to be rotated.
* angle - Number that represents the angle (in degrees).
* cx - Optional <mxPoint> that represents the rotation center. If no
* rotation center is given then the center of rect is used.
*/
getBoundingBox: function(rect, rotation, cx)
{
var result = null;
if (rect != null && rotation != null && rotation != 0)
{
var rad = mxUtils.toRadians(rotation);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
cx = (cx != null) ? cx : new mxPoint(rect.x + rect.width / 2, rect.y + rect.height / 2);
var p1 = new mxPoint(rect.x, rect.y);
var p2 = new mxPoint(rect.x + rect.width, rect.y);
var p3 = new mxPoint(p2.x, rect.y + rect.height);
var p4 = new mxPoint(rect.x, p3.y);
p1 = mxUtils.getRotatedPoint(p1, cos, sin, cx);
p2 = mxUtils.getRotatedPoint(p2, cos, sin, cx);
p3 = mxUtils.getRotatedPoint(p3, cos, sin, cx);
p4 = mxUtils.getRotatedPoint(p4, cos, sin, cx);
result = new mxRectangle(p1.x, p1.y, 0, 0);
result.add(new mxRectangle(p2.x, p2.y, 0, 0));
result.add(new mxRectangle(p3.x, p3.y, 0, 0));
result.add(new mxRectangle(p4.x, p4.y, 0, 0));
}
return result;
},
/**
* Function: getRotatedPoint
*
* Rotates the given point by the given cos and sin.
*/
getRotatedPoint: function(pt, cos, sin, c)
{
c = (c != null) ? c : new mxPoint();
var x = pt.x - c.x;
var y = pt.y - c.y;
var x1 = x