diff options
Diffstat (limited to 'src/js/util/mxUtils.js')
-rw-r--r-- | src/js/util/mxUtils.js | 3920 |
1 files changed, 3920 insertions, 0 deletions
diff --git a/src/js/util/mxUtils.js b/src/js/util/mxUtils.js new file mode 100644 index 0000000..34c0318 --- /dev/null +++ b/src/js/util/mxUtils.js @@ -0,0 +1,3920 @@ +/** + * $Id: mxUtils.js,v 1.297 2012-12-07 19:47:29 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +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: repaintGraph + * + * Normally not required, this contains the code to workaround a repaint + * issue and force a repaint of the graph container in AppleWebKit. + * + * Parameters: + * + * graph - <mxGraph> to be repainted. + * pt - <mxPoint> where the dummy element should be placed. + */ + repaintGraph: function(graph, pt) + { + if (mxClient.IS_GC || mxClient.IS_SF || mxClient.IS_OP) + { + var c = graph.container; + + if (c != null && pt != null && (c.scrollLeft > 0 || c.scrollTop > 0)) + { + var dummy = document.createElement('div'); + dummy.style.position = 'absolute'; + dummy.style.left = pt.x + 'px'; + dummy.style.top = pt.y + 'px'; + dummy.style.width = '1px'; + dummy.style.height = '1px'; + + c.appendChild(dummy); + c.removeChild(dummy); + } + } + }, + + /** + * 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) + { + return function(element) + { + return (element != null) ? element.currentStyle : null; + }; + } + else + { + return function(element) + { + return (element != null) ? + window.getComputedStyle(element, '') : + null; + }; + } + }(), + + /** + * 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) + { + 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: findNodeByAttribute + * + * Returns the first node where the given attribute matches the given value. + * + * Parameters: + * + * node - Root node where the search should start. + * attr - Name of the attribute to be checked. + * value - Value of the attribute to match. + */ + findNodeByAttribute: function() + { + // Workaround for missing XPath support in IE9 + if (document.documentMode >= 9) + { + return function(node, attr, value) + { + var result = null; + + if (node != null) + { + if (node.nodeType == mxConstants.NODETYPE_ELEMENT && node.getAttribute(attr) == value) + { + result = node; + } + else + { + var child = node.firstChild; + + while (child != null && result == null) + { + result = mxUtils.findNodeByAttribute(child, attr, value); + child = child.nextSibling; + } + } + } + + return result; + }; + } + else if (mxClient.IS_IE) + { + return function(node, attr, value) + { + if (node == null) + { + return null; + } + else + { + var expr = '//*[@' + attr + '=\'' + value + '\']'; + + return node.ownerDocument.selectSingleNode(expr); + } + }; + } + else + { + return function(node, attr, value) + { + if (node == null) + { + return null; + } + else + { + var result = node.ownerDocument.evaluate( + '//*[@' + attr + '=\'' + value + '\']', + node.ownerDocument, null, + XPathResult.ANY_TYPE, null); + + return result.iterateNext(); + } + }; + } + }(), + + /** + * 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 + { + var tmp = f.toString(); + var idx1 = 9; + + while (tmp.charAt(idx1) == ' ') + { + idx1++; + } + + var idx2 = tmp.indexOf('(', idx1); + str = tmp.substring(idx1, idx2); + } + } + + return str; + }, + + /** + * Function: indexOf + * + * Returns the index of obj in array or -1 if the array does not contains + * 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: 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: 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: 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 (window.ActiveXObject) + { + doc = new ActiveXObject('Microsoft.XMLDOM'); + } + + 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 (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9)) + { + return function(xml) + { + var result = mxUtils.createXmlDocument(); + + result.async = 'false'; + result.loadXML(xml); + + return result; + }; + } + else + { + return function(xml) + { + var parser = new DOMParser(); + + return parser.parseFromString(xml, 'text/xml'); + }; + } + }(), + + /** + * 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() + { + window.getSelection().removeAllRanges(); + }; + } + }(), + + /** + * 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. + */ + getPrettyXml: function(node, tab, indent) + { + var result = []; + + if (node != null) + { + tab = tab || ' '; + indent = indent || ''; + + if (node.nodeType == mxConstants.NODETYPE_TEXT) + { + result.push(node.nodeValue); + } + 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].nodeValue); + result.push(' ' + attrs[i].nodeName + + '="' + val + '"'); + } + } + + // Recursively creates the XML string for each + // child nodes and appends it here with an + // indentation + var tmp = node.firstChild; + + if (tmp != null) + { + result.push('>\n'); + + while (tmp != null) + { + result.push(mxUtils.getPrettyXml( + tmp, tab, indent + tab)); + tmp = tmp.nextSibling; + } + + result.push(indent + '</'+node.nodeName+'>\n'); + } + else + { + result.push('/>\n'); + } + } + } + + return result.join(''); + }, + + /** + * 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 = 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 (node != null) + { + xml = node.xml; + + if (xml == null) + { + if (node.innerHTML) + { + xml = node.innerHTML; + } + else + { + var xmlSerializer = new XMLSerializer(); + xml = xmlSerializer.serializeToString(node); + } + } + else + { + xml = 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: getTextContent + * + * Returns the text content of the specified node. + * + * Parameters: + * + * node - DOM node to return the text content for. + */ + getTextContent: function(node) + { + var result = ''; + + if (node != null) + { + if (node.firstChild != null) + { + node = node.firstChild; + } + + result = node.nodeValue || ''; + } + + return result; + }, + + /** + * 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].nodeValue; + + 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: 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: 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 left = parseInt(node.offsetLeft); + var width = parseInt(node.offsetWidth); + + var b = document.body; + var d = document.documentElement; + + var right = (b.scrollLeft || d.scrollLeft) + + (b.clientWidth || d.clientWidth); + + if (left + width > right) + { + node.style.left = Math.max((b.scrollLeft || d.scrollLeft), + right - width)+'px'; + } + + var top = parseInt(node.offsetTop); + var height = parseInt(node.offsetHeight); + + var bottom = (b.scrollTop || d.scrollTop) + + Math.max(b.clientHeight || 0, d.clientHeight); + + if (top + height > bottom) + { + node.style.top = Math.max((b.scrollTop || d.scrollTop), + bottom - height)+'px'; + } + }, + + /** + * Function: open + * + * Opens the specified file from the local filesystem and returns the + * contents of the file as a string. This implementation requires an + * ActiveX object in IE and special privileges in Firefox. Relative + * filenames are only supported in IE and will go onto the users' + * Desktop. You may have to load + * chrome://global/content/contentAreaUtils.js to disable certain + * security restrictions in Mozilla for this to work. + * + * See known-issues before using this function. + * + * Example: + * (code) + * var data = mxUtils.open('C:\\temp\\test.txt'); + * mxUtils.alert('Data: '+data); + * (end) + * + * Parameters: + * + * filename - String representing the local file name. + */ + open: function(filename) + { + // Requests required privileges in Firefox + if (mxClient.IS_NS) + { + try + { + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + } + catch (e) + { + mxUtils.alert('Permission to read file denied.'); + + return ''; + } + + var file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile); + file.initWithPath(filename); + + if (!file.exists()) + { + mxUtils.alert('File not found.'); + return ''; + } + + var is = Components.classes['@mozilla.org/network/file-input-stream;1'].createInstance(Components.interfaces.nsIFileInputStream); + is.init(file,0x01, 00004, null); + + var sis = Components.classes['@mozilla.org/scriptableinputstream;1'].createInstance(Components.interfaces.nsIScriptableInputStream); + sis.init(is); + + var output = sis.read(sis.available()); + + return output; + } + else + { + var activeXObject = new ActiveXObject('Scripting.FileSystemObject'); + + var newStream = activeXObject.OpenTextFile(filename, 1); + var text = newStream.readAll(); + newStream.close(); + + return text; + } + }, + + /** + * Function: save + * + * Saves the specified content in the given file on the local file system. + * This implementation requires an ActiveX object in IE and special + * privileges in Firefox. Relative filenames are only supported in IE and + * will be loaded from the users' Desktop. You may have to load + * chrome://global/content/contentAreaUtils.js to disable certain + * security restrictions in Mozilla for this to work. + * + * See known-issues before using this function. + * + * Example: + * + * (code) + * var data = 'Hello, World!'; + * mxUtils.save('C:\\test.txt', data); + * (end) + * + * Parameters: + * + * filename - String representing the local file name. + */ + save: function(filename, content) + { + if (mxClient.IS_NS) + { + try + { + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + } + catch (e) + { + mxUtils.alert('Permission to write file denied.'); + return; + } + + var file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile); + file.initWithPath(filename); + + if (!file.exists()) + { + file.create(0x00, 0644); + } + + var outputStream = Components.classes['@mozilla.org/network/file-output-stream;1'].createInstance(Components.interfaces.nsIFileOutputStream); + + outputStream.init(file, 0x20 | 0x02,00004, null); + outputStream.write(content, content.length); + outputStream.flush(); + outputStream.close(); + } + else + { + var fso = new ActiveXObject('Scripting.FileSystemObject'); + + var file = fso.CreateTextFile(filename, true); + file.Write(content); + file.Close(); + } + }, + + /** + * Function: saveAs + * + * Saves the specified content by displaying a dialog to save the content + * as a file on the local filesystem. This implementation does not use an + * ActiveX object in IE, however, it does require special privileges in + * Firefox. You may have to load + * chrome://global/content/contentAreaUtils.js to disable certain + * security restrictions in Mozilla for this to work. + * + * See known-issues before using this function. It is not recommended using + * this function in production environment as access to the filesystem + * cannot be guaranteed in Firefox. The following code is used in + * Firefox to try and enable saving to the filesystem. + * + * (code) + * netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + * (end) + * + * Example: + * + * (code) + * mxUtils.saveAs('Hello, World!'); + * (end) + * + * Parameters: + * + * content - String representing the file's content. + */ + saveAs: function(content) + { + var iframe = document.createElement('iframe'); + iframe.setAttribute('src', ''); + iframe.style.visibility = 'hidden'; + document.body.appendChild(iframe); + + try + { + if (mxClient.IS_NS) + { + var doc = iframe.contentDocument; + + doc.open(); + doc.write(content); + doc.close(); + + try + { + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + // LATER: Remove existing HTML markup in file + iframe.focus(); + saveDocument(doc); + } + catch (e) + { + mxUtils.alert('Permission to save document denied.'); + } + } + else + { + var doc = iframe.contentWindow.document; + doc.write(content); + doc.execCommand('SaveAs', false, document.location); + } + } + finally + { + document.body.removeChild(iframe); + } + }, + + /** + * Function: copy + * + * Copies the specified content to the local clipboard. This implementation + * requires special privileges in Firefox. You may have to load + * chrome://global/content/contentAreaUtils.js to disable certain + * security restrictions in Mozilla for this to work. + * + * Parameters: + * + * content - String to be copied to the clipboard. + */ + copy: function(content) + { + if (window.clipboardData) + { + window.clipboardData.setData('Text', content); + } + else + { + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + + var clip = Components.classes['@mozilla.org/widget/clipboard;1'] + .createInstance(Components.interfaces.nsIClipboard); + + if (!clip) + { + return; + } + + var trans = Components.classes['@mozilla.org/widget/transferable;1'] + .createInstance(Components.interfaces.nsITransferable); + + if (!trans) + { + return; + } + + trans.addDataFlavor('text/unicode'); + var str = Components.classes['@mozilla.org/supports-string;1'] + .createInstance(Components.interfaces.nsISupportsString); + + var copytext=content; + str.data=copytext; + trans.setTransferData('text/unicode', str, copytext.length*2); + var clipid=Components.interfaces.nsIClipboard; + + clip.setData(trans,null,clipid.kGlobalClipboard); + } + }, + + /** + * 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. + */ + get: function(url, onload, onerror) + { + return new mxXmlRequest(url, null, 'GET').send(onload, onerror); + }, + + /** + * 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] == b[i] || (a[i] != null && !a[i].equals(b[i]))) + { + return false; + } + } + } + + return true; + }, + + /** + * Function: equalEntries + * + * Compares all entries in the given dictionaries. + * + * Parameters: + * + * a - <mxRectangle> to be compared. + * b - <mxRectangle> to be compared. + */ + equalEntries: 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 key in a) + { + if (a[key] != b[key]) + { + return false; + } + } + } + + return true; + }, + + /** + * 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: 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. + */ + getBoundingBox: function(rect, rotation) + { + 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); + + var 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 * cos - y * sin; + var y1 = y * cos + x * sin; + + return new mxPoint(x1 + c.x, y1 + c.y); + }, + + /** + * Returns an integer mask of the port constraints of the given map + * @param dict the style map to determine the port constraints for + * @param defaultValue Default value to return if the key is undefined. + * @return the mask of port constraint directions + * + * Parameters: + * + * terminal - <mxCelState> that represents the terminal. + * edge - <mxCellState> that represents the edge. + * source - Boolean that specifies if the terminal is the source terminal. + * defaultValue - Default value to be returned. + */ + getPortConstraints: function(terminal, edge, source, defaultValue) + { + var value = mxUtils.getValue(terminal.style, mxConstants.STYLE_PORT_CONSTRAINT, null); + + if (value == null) + { + return defaultValue; + } + else + { + var directions = value.toString(); + var returnValue = mxConstants.DIRECTION_MASK_NONE; + + if (directions.indexOf(mxConstants.DIRECTION_NORTH) >= 0) + { + returnValue |= mxConstants.DIRECTION_MASK_NORTH; + } + if (directions.indexOf(mxConstants.DIRECTION_WEST) >= 0) + { + returnValue |= mxConstants.DIRECTION_MASK_WEST; + } + if (directions.indexOf(mxConstants.DIRECTION_SOUTH) >= 0) + { + returnValue |= mxConstants.DIRECTION_MASK_SOUTH; + } + if (directions.indexOf(mxConstants.DIRECTION_EAST) >= 0) + { + returnValue |= mxConstants.DIRECTION_MASK_EAST; + } + + return returnValue; + } + }, + + /** + * Function: reversePortConstraints + * + * Reverse the port constraint bitmask. For example, north | east + * becomes south | west + */ + reversePortConstraints: function(constraint) + { + var result = 0; + + result = (constraint & mxConstants.DIRECTION_MASK_WEST) << 3; + result |= (constraint & mxConstants.DIRECTION_MASK_NORTH) << 1; + result |= (constraint & mxConstants.DIRECTION_MASK_SOUTH) >> 1; + result |= (constraint & mxConstants.DIRECTION_MASK_EAST) >> 3; + + return result; + }, + + /** + * Function: findNearestSegment + * + * Finds the index of the nearest segment on the given cell state for + * the specified coordinate pair. + */ + findNearestSegment: function(state, x, y) + { + var index = -1; + + if (state.absolutePoints.length > 0) + { + var last = state.absolutePoints[0]; + var min = null; + + for (var i = 1; i < state.absolutePoints.length; i++) + { + var current = state.absolutePoints[i]; + var dist = mxUtils.ptSegDistSq(last.x, last.y, + current.x, current.y, x, y); + + if (min == null || dist < min) + { + min = dist; + index = i - 1; + } + + last = current; + } + } + + return index; + }, + + /** + * Function: rectangleIntersectsSegment + * + * Returns true if the given rectangle intersects the given segment. + * + * Parameters: + * + * bounds - <mxRectangle> that represents the rectangle. + * p1 - <mxPoint> that represents the first point of the segment. + * p2 - <mxPoint> that represents the second point of the segment. + */ + rectangleIntersectsSegment: function(bounds, p1, p2) + { + var top = bounds.y; + var left = bounds.x; + var bottom = top + bounds.height; + var right = left + bounds.width; + + // Find min and max X for the segment + var minX = p1.x; + var maxX = p2.x; + + if (p1.x > p2.x) + { + minX = p2.x; + maxX = p1.x; + } + + // Find the intersection of the segment's and rectangle's x-projections + if (maxX > right) + { + maxX = right; + } + + if (minX < left) + { + minX = left; + } + + if (minX > maxX) // If their projections do not intersect return false + { + return false; + } + + // Find corresponding min and max Y for min and max X we found before + var minY = p1.y; + var maxY = p2.y; + var dx = p2.x - p1.x; + + if (Math.abs(dx) > 0.0000001) + { + var a = (p2.y - p1.y) / dx; + var b = p1.y - a * p1.x; + minY = a * minX + b; + maxY = a * maxX + b; + } + + if (minY > maxY) + { + var tmp = maxY; + maxY = minY; + minY = tmp; + } + + // Find the intersection of the segment's and rectangle's y-projections + if (maxY > bottom) + { + maxY = bottom; + } + + if (minY < top) + { + minY = top; + } + + if (minY > maxY) // If Y-projections do not intersect return false + { + return false; + } + + return true; + }, + + /** + * Function: contains + * + * Returns true if the specified point (x, y) is contained in the given rectangle. + * + * Parameters: + * + * bounds - <mxRectangle> that represents the area. + * x - X-coordinate of the point. + * y - Y-coordinate of the point. + */ + contains: function(bounds, x, y) + { + return (bounds.x <= x && bounds.x + bounds.width >= x && + bounds.y <= y && bounds.y + bounds.height >= y); + }, + + /** + * Function: intersects + * + * Returns true if the two rectangles intersect. + * + * Parameters: + * + * a - <mxRectangle> to be checked for intersection. + * b - <mxRectangle> to be checked for intersection. + */ + intersects: function(a, b) + { + var tw = a.width; + var th = a.height; + var rw = b.width; + var rh = b.height; + + if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) + { + return false; + } + + var tx = a.x; + var ty = a.y; + var rx = b.x; + var ry = b.y; + + rw += rx; + rh += ry; + tw += tx; + th += ty; + + return ((rw < rx || rw > tx) && + (rh < ry || rh > ty) && + (tw < tx || tw > rx) && + (th < ty || th > ry)); + }, + + /** + * Function: intersects + * + * Returns true if the two rectangles intersect. + * + * Parameters: + * + * a - <mxRectangle> to be checked for intersection. + * b - <mxRectangle> to be checked for intersection. + */ + intersectsHotspot: function(state, x, y, hotspot, min, max) + { + hotspot = (hotspot != null) ? hotspot : 1; + min = (min != null) ? min : 0; + max = (max != null) ? max : 0; + + if (hotspot > 0) + { + var cx = state.getCenterX(); + var cy = state.getCenterY(); + var w = state.width; + var h = state.height; + + var start = mxUtils.getValue(state.style, mxConstants.STYLE_STARTSIZE) * state.view.scale; + + if (start > 0) + { + if (mxUtils.getValue(state.style, + mxConstants.STYLE_HORIZONTAL, true)) + { + cy = state.y + start / 2; + h = start; + } + else + { + cx = state.x + start / 2; + w = start; + } + } + + w = Math.max(min, w * hotspot); + h = Math.max(min, h * hotspot); + + if (max > 0) + { + w = Math.min(w, max); + h = Math.min(h, max); + } + + var rect = new mxRectangle(cx - w / 2, cy - h / 2, w, h); + + return mxUtils.contains(rect, x, y); + } + + return true; + }, + + /** + * Function: getOffset + * + * Returns the offset for the specified container as an <mxPoint>. The + * offset is the distance from the top left corner of the container to the + * top left corner of the document. + * + * Parameters: + * + * container - DOM node to return the offset for. + * scollOffset - Optional boolean to add the scroll offset of the document. + * Default is false. + */ + getOffset: function(container, scrollOffset) + { + var offsetLeft = 0; + var offsetTop = 0; + + if (scrollOffset != null && scrollOffset) + { + var b = document.body; + var d = document.documentElement; + offsetLeft += (b.scrollLeft || d.scrollLeft); + offsetTop += (b.scrollTop || d.scrollTop); + } + + while (container.offsetParent) + { + offsetLeft += container.offsetLeft; + offsetTop += container.offsetTop; + + container = container.offsetParent; + } + + return new mxPoint(offsetLeft, offsetTop); + }, + + /** + * Function: getScrollOrigin + * + * Returns the top, left corner of the viewrect as an <mxPoint>. + */ + getScrollOrigin: function(node) + { + var b = document.body; + var d = document.documentElement; + var sl = (b.scrollLeft || d.scrollLeft); + var st = (b.scrollTop || d.scrollTop); + + var result = new mxPoint(sl, st); + + while (node != null && node != b && node != d) + { + if (!isNaN(node.scrollLeft) && !isNaN(node.scrollTop)) + { + result.x += node.scrollLeft; + result.y += node.scrollTop; + } + + node = node.parentNode; + } + + return result; + }, + + /** + * Function: convertPoint + * + * Converts the specified point (x, y) using the offset of the specified + * container and returns a new <mxPoint> with the result. + * + * Parameters: + * + * container - DOM node to use for the offset. + * x - X-coordinate of the point to be converted. + * y - Y-coordinate of the point to be converted. + */ + convertPoint: function(container, x, y) + { + var origin = mxUtils.getScrollOrigin(container); + var offset = mxUtils.getOffset(container); + + offset.x -= origin.x; + offset.y -= origin.y; + + return new mxPoint(x - offset.x, y - offset.y); + }, + + /** + * Function: ltrim + * + * Strips all whitespaces from the beginning of the string. + * Without the second parameter, Javascript function will trim these + * characters: + * + * - " " (ASCII 32 (0x20)), an ordinary space + * - "\t" (ASCII 9 (0x09)), a tab + * - "\n" (ASCII 10 (0x0A)), a new line (line feed) + * - "\r" (ASCII 13 (0x0D)), a carriage return + * - "\0" (ASCII 0 (0x00)), the NUL-byte + * - "\x0B" (ASCII 11 (0x0B)), a vertical tab + */ + ltrim: function(str, chars) + { + chars = chars || "\\s"; + + return str.replace(new RegExp("^[" + chars + "]+", "g"), ""); + }, + + /** + * Function: rtrim + * + * Strips all whitespaces from the end of the string. + * Without the second parameter, Javascript function will trim these + * characters: + * + * - " " (ASCII 32 (0x20)), an ordinary space + * - "\t" (ASCII 9 (0x09)), a tab + * - "\n" (ASCII 10 (0x0A)), a new line (line feed) + * - "\r" (ASCII 13 (0x0D)), a carriage return + * - "\0" (ASCII 0 (0x00)), the NUL-byte + * - "\x0B" (ASCII 11 (0x0B)), a vertical tab + */ + rtrim: function(str, chars) + { + chars = chars || "\\s"; + + return str.replace(new RegExp("[" + chars + "]+$", "g"), ""); + }, + + /** + * Function: trim + * + * Strips all whitespaces from both end of the string. + * Without the second parameter, Javascript function will trim these + * characters: + * + * - " " (ASCII 32 (0x20)), an ordinary space + * - "\t" (ASCII 9 (0x09)), a tab + * - "\n" (ASCII 10 (0x0A)), a new line (line feed) + * - "\r" (ASCII 13 (0x0D)), a carriage return + * - "\0" (ASCII 0 (0x00)), the NUL-byte + * - "\x0B" (ASCII 11 (0x0B)), a vertical tab + */ + trim: function(str, chars) + { + return mxUtils.ltrim(mxUtils.rtrim(str, chars), chars); + }, + + /** + * Function: isNumeric + * + * Returns true if the specified value is numeric, that is, if it is not + * null, not an empty string, not a HEX number and isNaN returns false. + * + * Parameters: + * + * str - String representing the possibly numeric value. + */ + isNumeric: function(str) + { + return str != null && (str.length == null || (str.length > 0 && + str.indexOf('0x') < 0) && str.indexOf('0X') < 0) && !isNaN(str); + }, + + /** + * Function: mod + * + * Returns the remainder of division of n by m. You should use this instead + * of the built-in operation as the built-in operation does not properly + * handle negative numbers. + */ + mod: function(n, m) + { + return ((n % m) + m) % m; + }, + + /** + * Function: intersection + * + * Returns the intersection of two lines as an <mxPoint>. + * + * Parameters: + * + * x0 - X-coordinate of the first line's startpoint. + * y0 - X-coordinate of the first line's startpoint. + * x1 - X-coordinate of the first line's endpoint. + * y1 - Y-coordinate of the first line's endpoint. + * x2 - X-coordinate of the second line's startpoint. + * y2 - Y-coordinate of the second line's startpoint. + * x3 - X-coordinate of the second line's endpoint. + * y3 - Y-coordinate of the second line's endpoint. + */ + intersection: function (x0, y0, x1, y1, x2, y2, x3, y3) + { + var denom = ((y3 - y2)*(x1 - x0)) - ((x3 - x2)*(y1 - y0)); + var nume_a = ((x3 - x2)*(y0 - y2)) - ((y3 - y2)*(x0 - x2)); + var nume_b = ((x1 - x0)*(y0 - y2)) - ((y1 - y0)*(x0 - x2)); + + var ua = nume_a / denom; + var ub = nume_b / denom; + + if(ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0) + { + // Get the intersection point + var intersectionX = x0 + ua*(x1 - x0); + var intersectionY = y0 + ua*(y1 - y0); + + return new mxPoint(intersectionX, intersectionY); + } + + // No intersection + return null; + }, + + /** + * Function: ptSeqDistSq + * + * Returns the square distance between a segment and a point. + * + * Parameters: + * + * x1 - X-coordinate of the startpoint of the segment. + * y1 - Y-coordinate of the startpoint of the segment. + * x2 - X-coordinate of the endpoint of the segment. + * y2 - Y-coordinate of the endpoint of the segment. + * px - X-coordinate of the point. + * py - Y-coordinate of the point. + */ + ptSegDistSq: function(x1, y1, x2, y2, px, py) + { + x2 -= x1; + y2 -= y1; + + px -= x1; + py -= y1; + + var dotprod = px * x2 + py * y2; + var projlenSq; + + if (dotprod <= 0.0) + { + projlenSq = 0.0; + } + else + { + px = x2 - px; + py = y2 - py; + dotprod = px * x2 + py * y2; + + if (dotprod <= 0.0) + { + projlenSq = 0.0; + } + else + { + projlenSq = dotprod * dotprod / (x2 * x2 + y2 * y2); + } + } + + var lenSq = px * px + py * py - projlenSq; + + if (lenSq < 0) + { + lenSq = 0; + } + + return lenSq; + }, + + /** + * Function: relativeCcw + * + * Returns 1 if the given point on the right side of the segment, 0 if its + * on the segment, and -1 if the point is on the left side of the segment. + * + * Parameters: + * + * x1 - X-coordinate of the startpoint of the segment. + * y1 - Y-coordinate of the startpoint of the segment. + * x2 - X-coordinate of the endpoint of the segment. + * y2 - Y-coordinate of the endpoint of the segment. + * px - X-coordinate of the point. + * py - Y-coordinate of the point. + */ + relativeCcw: function(x1, y1, x2, y2, px, py) + { + x2 -= x1; + y2 -= y1; + px -= x1; + py -= y1; + var ccw = px * y2 - py * x2; + + if (ccw == 0.0) + { + ccw = px * x2 + py * y2; + + if (ccw > 0.0) + { + px -= x2; + py -= y2; + ccw = px * x2 + py * y2; + + if (ccw < 0.0) + { + ccw = 0.0; + } + } + } + + return (ccw < 0.0) ? -1 : ((ccw > 0.0) ? 1 : 0); + }, + + /** + * Function: animateChanges + * + * See <mxEffects.animateChanges>. This is for backwards compatibility and + * will be removed later. + */ + animateChanges: function(graph, changes) + { + // LATER: Deprecated, remove this function + mxEffects.animateChanges.apply(this, arguments); + }, + + /** + * Function: cascadeOpacity + * + * See <mxEffects.cascadeOpacity>. This is for backwards compatibility and + * will be removed later. + */ + cascadeOpacity: function(graph, cell, opacity) + { + mxEffects.cascadeOpacity.apply(this, arguments); + }, + + /** + * Function: fadeOut + * + * See <mxEffects.fadeOut>. This is for backwards compatibility and + * will be removed later. + */ + fadeOut: function(node, from, remove, step, delay, isEnabled) + { + mxEffects.fadeOut.apply(this, arguments); + }, + + /** + * Function: setOpacity + * + * Sets the opacity of the specified DOM node to the given value in %. + * + * Parameters: + * + * node - DOM node to set the opacity for. + * value - Opacity in %. Possible values are between 0 and 100. + */ + setOpacity: function(node, value) + { + if (mxUtils.isVml(node)) + { + if (value >= 100) + { + node.style.filter = null; + } + else + { + // TODO: Why is the division by 5 needed in VML? + node.style.filter = 'alpha(opacity=' + (value/5) + ')'; + } + } + else if (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9)) + { + if (value >= 100) + { + node.style.filter = null; + } + else + { + node.style.filter = 'alpha(opacity=' + value + ')'; + } + } + else + { + node.style.opacity = (value / 100); + } + }, + + /** + * Function: createImage + * + * Creates and returns an image (IMG node) or VML image (v:image) in IE6 in + * quirs mode. + * + * Parameters: + * + * src - URL that points to the image to be displayed. + */ + createImage: function(src) + { + var imageNode = null; + + if (mxClient.IS_IE6 && document.compatMode != 'CSS1Compat') + { + imageNode = document.createElement('v:image'); + imageNode.setAttribute('src', src); + imageNode.style.borderStyle = 'none'; + } + else + { + imageNode = document.createElement('img'); + imageNode.setAttribute('src', src); + imageNode.setAttribute('border', '0'); + } + + return imageNode; + }, + + /** + * Function: sortCells + * + * Sorts the given cells according to the order in the cell hierarchy. + * Ascending is optional and defaults to true. + */ + sortCells: function(cells, ascending) + { + ascending = (ascending != null) ? ascending : true; + var lookup = new mxDictionary(); + cells.sort(function(o1, o2) + { + var p1 = lookup.get(o1); + + if (p1 == null) + { + p1 = mxCellPath.create(o1).split(mxCellPath.PATH_SEPARATOR); + lookup.put(o1, p1); + } + + var p2 = lookup.get(o2); + + if (p2 == null) + { + p2 = mxCellPath.create(o2).split(mxCellPath.PATH_SEPARATOR); + lookup.put(o2, p2); + } + + var comp = mxCellPath.compare(p1, p2); + + return (comp == 0) ? 0 : (((comp > 0) == ascending) ? 1 : -1); + }); + + return cells; + }, + + /** + * Function: getStylename + * + * Returns the stylename in a style of the form [(stylename|key=value);] or + * an empty string if the given style does not contain a stylename. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + */ + getStylename: function(style) + { + if (style != null) + { + var pairs = style.split(';'); + var stylename = pairs[0]; + + if (stylename.indexOf('=') < 0) + { + return stylename; + } + } + + return ''; + }, + + /** + * Function: getStylenames + * + * Returns the stylenames in a style of the form [(stylename|key=value);] + * or an empty array if the given style does not contain any stylenames. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + */ + getStylenames: function(style) + { + var result = []; + + if (style != null) + { + var pairs = style.split(';'); + + for (var i = 0; i < pairs.length; i++) + { + if (pairs[i].indexOf('=') < 0) + { + result.push(pairs[i]); + } + } + } + + return result; + }, + + /** + * Function: indexOfStylename + * + * Returns the index of the given stylename in the given style. This + * returns -1 if the given stylename does not occur (as a stylename) in the + * given style, otherwise it returns the index of the first character. + */ + indexOfStylename: function(style, stylename) + { + if (style != null && stylename != null) + { + var tokens = style.split(';'); + var pos = 0; + + for (var i = 0; i < tokens.length; i++) + { + if (tokens[i] == stylename) + { + return pos; + } + + pos += tokens[i].length + 1; + } + } + + return -1; + }, + + /** + * Function: addStylename + * + * Adds the specified stylename to the given style if it does not already + * contain the stylename. + */ + addStylename: function(style, stylename) + { + if (mxUtils.indexOfStylename(style, stylename) < 0) + { + if (style == null) + { + style = ''; + } + else if (style.length > 0 && style.charAt(style.length - 1) != ';') + { + style += ';'; + } + + style += stylename; + } + + return style; + }, + + /** + * Function: removeStylename + * + * Removes all occurrences of the specified stylename in the given style + * and returns the updated style. Trailing semicolons are not preserved. + */ + removeStylename: function(style, stylename) + { + var result = []; + + if (style != null) + { + var tokens = style.split(';'); + + for (var i = 0; i < tokens.length; i++) + { + if (tokens[i] != stylename) + { + result.push(tokens[i]); + } + } + } + + return result.join(';'); + }, + + /** + * Function: removeAllStylenames + * + * Removes all stylenames from the given style and returns the updated + * style. + */ + removeAllStylenames: function(style) + { + var result = []; + + if (style != null) + { + var tokens = style.split(';'); + + for (var i = 0; i < tokens.length; i++) + { + // Keeps the key, value assignments + if (tokens[i].indexOf('=') >= 0) + { + result.push(tokens[i]); + } + } + } + + return result.join(';'); + }, + + /** + * Function: setCellStyles + * + * Assigns the value for the given key in the styles of the given cells, or + * removes the key from the styles if the value is null. + * + * Parameters: + * + * model - <mxGraphModel> to execute the transaction in. + * cells - Array of <mxCells> to be updated. + * key - Key of the style to be changed. + * value - New value for the given key. + */ + setCellStyles: function(model, cells, key, value) + { + if (cells != null && cells.length > 0) + { + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != null) + { + var style = mxUtils.setStyle( + model.getStyle(cells[i]), + key, value); + model.setStyle(cells[i], style); + } + } + } + finally + { + model.endUpdate(); + } + } + }, + + /** + * Function: setStyle + * + * Adds or removes the given key, value pair to the style and returns the + * new style. If value is null or zero length then the key is removed from + * the style. This is for cell styles, not for CSS styles. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + * key - Key of the style to be changed. + * value - New value for the given key. + */ + setStyle: function(style, key, value) + { + var isValue = value != null && (typeof(value.length) == 'undefined' || value.length > 0); + + if (style == null || style.length == 0) + { + if (isValue) + { + style = key+'='+value; + } + } + else + { + var index = style.indexOf(key+'='); + + if (index < 0) + { + if (isValue) + { + var sep = (style.charAt(style.length-1) == ';') ? '' : ';'; + style = style + sep + key+'='+value; + } + } + else + { + var tmp = (isValue) ? (key + '=' + value) : ''; + var cont = style.indexOf(';', index); + + if (!isValue) + { + cont++; + } + + style = style.substring(0, index) + tmp + + ((cont > index) ? style.substring(cont) : ''); + } + } + + return style; + }, + + /** + * Function: setCellStyleFlags + * + * Sets or toggles the flag bit for the given key in the cell's styles. + * If value is null then the flag is toggled. + * + * Example: + * + * (code) + * var cells = graph.getSelectionCells(); + * mxUtils.setCellStyleFlags(graph.model, + * cells, + * mxConstants.STYLE_FONTSTYLE, + * mxConstants.FONT_BOLD); + * (end) + * + * Toggles the bold font style. + * + * Parameters: + * + * model - <mxGraphModel> that contains the cells. + * cells - Array of <mxCells> to change the style for. + * key - Key of the style to be changed. + * flag - Integer for the bit to be changed. + * value - Optional boolean value for the flag. + */ + setCellStyleFlags: function(model, cells, key, flag, value) + { + if (cells != null && cells.length > 0) + { + model.beginUpdate(); + try + { + for (var i = 0; i < cells.length; i++) + { + if (cells[i] != null) + { + var style = mxUtils.setStyleFlag( + model.getStyle(cells[i]), + key, flag, value); + model.setStyle(cells[i], style); + } + } + } + finally + { + model.endUpdate(); + } + } + }, + + /** + * Function: setStyleFlag + * + * Sets or removes the given key from the specified style and returns the + * new style. If value is null then the flag is toggled. + * + * Parameters: + * + * style - String of the form [(stylename|key=value);]. + * key - Key of the style to be changed. + * flag - Integer for the bit to be changed. + * value - Optional boolean value for the given flag. + */ + setStyleFlag: function(style, key, flag, value) + { + if (style == null || style.length == 0) + { + if (value || value == null) + { + style = key+'='+flag; + } + else + { + style = key+'=0'; + } + } + else + { + var index = style.indexOf(key+'='); + + if (index < 0) + { + var sep = (style.charAt(style.length-1) == ';') ? '' : ';'; + + if (value || value == null) + { + style = style + sep + key + '=' + flag; + } + else + { + style = style + sep + key + '=0'; + } + } + else + { + var cont = style.indexOf(';', index); + var tmp = ''; + + if (cont < 0) + { + tmp = style.substring(index+key.length+1); + } + else + { + tmp = style.substring(index+key.length+1, cont); + } + + if (value == null) + { + tmp = parseInt(tmp) ^ flag; + } + else if (value) + { + tmp = parseInt(tmp) | flag; + } + else + { + tmp = parseInt(tmp) & ~flag; + } + + style = style.substring(0, index) + key + '=' + tmp + + ((cont >= 0) ? style.substring(cont) : ''); + } + } + + return style; + }, + + /** + * Function: getSizeForString + * + * Returns an <mxRectangle> with the size (width and height in pixels) of + * the given string. The string may contain HTML markup. Newlines should be + * converted to <br> before calling this method. + * + * Example: + * + * (code) + * var label = graph.getLabel(cell).replace(/\n/g, "<br>"); + * var size = graph.getSizeForString(label); + * (end) + * + * Parameters: + * + * text - String whose size should be returned. + * fontSize - Integer that specifies the font size in pixels. Default is + * <mxConstants.DEFAULT_FONTSIZE>. + * fontFamily - String that specifies the name of the font family. Default + * is <mxConstants.DEFAULT_FONTFAMILY>. + */ + getSizeForString: function(text, fontSize, fontFamily) + { + var div = document.createElement('div'); + + // Sets the font size and family if non-default + div.style.fontSize = (fontSize || mxConstants.DEFAULT_FONTSIZE) + 'px'; + div.style.fontFamily = fontFamily || mxConstants.DEFAULT_FONTFAMILY; + + // Disables block layout and outside wrapping and hides the div + div.style.position = 'absolute'; + div.style.display = 'inline'; + div.style.visibility = 'hidden'; + + // Adds the text and inserts into DOM for updating of size + div.innerHTML = text; + document.body.appendChild(div); + + // Gets the size and removes from DOM + var size = new mxRectangle(0, 0, div.offsetWidth, div.offsetHeight); + document.body.removeChild(div); + + return size; + }, + + /** + * Function: getViewXml + */ + getViewXml: function(graph, scale, cells, x0, y0) + { + x0 = (x0 != null) ? x0 : 0; + y0 = (y0 != null) ? y0 : 0; + scale = (scale != null) ? scale : 1; + + if (cells == null) + { + var model = graph.getModel(); + cells = [model.getRoot()]; + } + + var view = graph.getView(); + var result = null; + + // Disables events on the view + var eventsEnabled = view.isEventsEnabled(); + view.setEventsEnabled(false); + + // Workaround for label bounds not taken into account for image export. + // Creates a temporary draw pane which is used for rendering the text. + // Text rendering is required for finding the bounds of the labels. + var drawPane = view.drawPane; + var overlayPane = view.overlayPane; + + if (graph.dialect == mxConstants.DIALECT_SVG) + { + view.drawPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + view.canvas.appendChild(view.drawPane); + + // Redirects cell overlays into temporary container + view.overlayPane = document.createElementNS(mxConstants.NS_SVG, 'g'); + view.canvas.appendChild(view.overlayPane); + } + else + { + view.drawPane = view.drawPane.cloneNode(false); + view.canvas.appendChild(view.drawPane); + + // Redirects cell overlays into temporary container + view.overlayPane = view.overlayPane.cloneNode(false); + view.canvas.appendChild(view.overlayPane); + } + + // Resets the translation + var translate = view.getTranslate(); + view.translate = new mxPoint(x0, y0); + + // Creates the temporary cell states in the view + var temp = new mxTemporaryCellStates(graph.getView(), scale, cells); + + try + { + var enc = new mxCodec(); + result = enc.encode(graph.getView()); + } + finally + { + temp.destroy(); + view.translate = translate; + view.canvas.removeChild(view.drawPane); + view.canvas.removeChild(view.overlayPane); + view.drawPane = drawPane; + view.overlayPane = overlayPane; + view.setEventsEnabled(eventsEnabled); + } + + return result; + }, + + /** + * Function: getScaleForPageCount + * + * Returns the scale to be used for printing the graph with the given + * bounds across the specifies number of pages with the given format. The + * scale is always computed such that it given the given amount or fewer + * pages in the print output. See <mxPrintPreview> for an example. + * + * Parameters: + * + * pageCount - Specifies the number of pages in the print output. + * graph - <mxGraph> that should be printed. + * pageFormat - Optional <mxRectangle> that specifies the page format. + * Default is <mxConstants.PAGE_FORMAT_A4_PORTRAIT>. + * border - The border along each side of every page. + */ + getScaleForPageCount: function(pageCount, graph, pageFormat, border) + { + if (pageCount < 1) + { + // We can't work with less than 1 page, return no scale + // change + return 1; + } + + pageFormat = (pageFormat != null) ? pageFormat : mxConstants.PAGE_FORMAT_A4_PORTRAIT; + border = (border != null) ? border : 0; + + var availablePageWidth = pageFormat.width - (border * 2); + var availablePageHeight = pageFormat.height - (border * 2); + + // Work out the number of pages required if the + // graph is not scaled. + var graphBounds = graph.getGraphBounds().clone(); + var sc = graph.getView().getScale(); + graphBounds.width /= sc; + graphBounds.height /= sc; + var graphWidth = graphBounds.width; + var graphHeight = graphBounds.height; + + var scale = 1; + + // The ratio of the width/height for each printer page + var pageFormatAspectRatio = availablePageWidth / availablePageHeight; + // The ratio of the width/height for the graph to be printer + var graphAspectRatio = graphWidth / graphHeight; + + // The ratio of horizontal pages / vertical pages for this + // graph to maintain its aspect ratio on this page format + var pagesAspectRatio = graphAspectRatio / pageFormatAspectRatio; + + // Factor the square root of the page count up and down + // by the pages aspect ratio to obtain a horizontal and + // vertical page count that adds up to the page count + // and has the correct aspect ratio + var pageRoot = Math.sqrt(pageCount); + var pagesAspectRatioSqrt = Math.sqrt(pagesAspectRatio); + var numRowPages = pageRoot * pagesAspectRatioSqrt; + var numColumnPages = pageRoot / pagesAspectRatioSqrt; + + // These value are rarely more than 2 rounding downs away from + // a total that meets the page count. In cases of one being less + // than 1 page, the other value can be too high and take more iterations + // In this case, just change that value to be the page count, since + // we know the other value is 1 + if (numRowPages < 1 && numColumnPages > pageCount) + { + var scaleChange = numColumnPages / pageCount; + numColumnPages = pageCount; + numRowPages /= scaleChange; + } + + if (numColumnPages < 1 && numRowPages > pageCount) + { + var scaleChange = numRowPages / pageCount; + numRowPages = pageCount; + numColumnPages /= scaleChange; + } + + var currentTotalPages = Math.ceil(numRowPages) * Math.ceil(numColumnPages); + + var numLoops = 0; + + // Iterate through while the rounded up number of pages comes to + // a total greater than the required number + while (currentTotalPages > pageCount) + { + // Round down the page count (rows or columns) that is + // closest to its next integer down in percentage terms. + // i.e. Reduce the page total by reducing the total + // page area by the least possible amount + + var roundRowDownProportion = Math.floor(numRowPages) / numRowPages; + var roundColumnDownProportion = Math.floor(numColumnPages) / numColumnPages; + + // If the round down proportion is, work out the proportion to + // round down to 1 page less + if (roundRowDownProportion == 1) + { + roundRowDownProportion = Math.floor(numRowPages-1) / numRowPages; + } + if (roundColumnDownProportion == 1) + { + roundColumnDownProportion = Math.floor(numColumnPages-1) / numColumnPages; + } + + // Check which rounding down is smaller, but in the case of very small roundings + // try the other dimension instead + var scaleChange = 1; + + // Use the higher of the two values + if (roundRowDownProportion > roundColumnDownProportion) + { + scaleChange = roundRowDownProportion; + } + else + { + scaleChange = roundColumnDownProportion; + } + + numRowPages = numRowPages * scaleChange; + numColumnPages = numColumnPages * scaleChange; + currentTotalPages = Math.ceil(numRowPages) * Math.ceil(numColumnPages); + + numLoops++; + + if (numLoops > 10) + { + break; + } + } + + // Work out the scale from the number of row pages required + // The column pages will give the same value + var posterWidth = availablePageWidth * numRowPages; + scale = posterWidth / graphWidth; + + // Allow for rounding errors + return scale * 0.99999; + }, + + /** + * Function: show + * + * Copies the styles and the markup from the graph's container into the + * given document and removes all cursor styles. The document is returned. + * + * This function should be called from within the document with the graph. + * If you experience problems with missing stylesheets in IE then try adding + * the domain to the trusted sites. + * + * Parameters: + * + * graph - <mxGraph> to be copied. + * doc - Document where the new graph is created. + * x0 - X-coordinate of the graph view origin. Default is 0. + * y0 - Y-coordinate of the graph view origin. Default is 0. + */ + show: function(graph, doc, x0, y0) + { + x0 = (x0 != null) ? x0 : 0; + y0 = (y0 != null) ? y0 : 0; + + if (doc == null) + { + var wnd = window.open(); + doc = wnd.document; + } + else + { + doc.open(); + } + + var bounds = graph.getGraphBounds(); + var dx = -bounds.x + x0; + var dy = -bounds.y + y0; + + // Needs a special way of creating the page so that no click is required + // to refresh the contents after the external CSS styles have been loaded. + // To avoid a click or programmatic refresh, the styleSheets[].cssText + // property is copied over from the original document. + if (mxClient.IS_IE) + { + var html = '<html>'; + html += '<head>'; + + var base = document.getElementsByTagName('base'); + + for (var i = 0; i < base.length; i++) + { + html += base[i].outerHTML; + } + + html += '<style>'; + + // Copies the stylesheets without having to load them again + for (var i = 0; i < document.styleSheets.length; i++) + { + try + { + html += document.styleSheets(i).cssText; + } + catch (e) + { + // ignore security exception + } + } + + html += '</style>'; + + html += '</head>'; + html += '<body>'; + + // Copies the contents of the graph container + html += graph.container.innerHTML; + + html += '</body>'; + html += '<html>'; + + doc.writeln(html); + doc.close(); + + // Makes sure the inner container is on the top, left + var node = doc.body.getElementsByTagName('DIV')[0]; + + if (node != null) + { + node.style.position = 'absolute'; + node.style.left = dx + 'px'; + node.style.top = dy + 'px'; + } + } + else + { + doc.writeln('<html'); + doc.writeln('<head>'); + + var base = document.getElementsByTagName('base'); + + for (var i=0; i<base.length; i++) + { + doc.writeln(mxUtils.getOuterHtml(base[i])); + } + + var links = document.getElementsByTagName('link'); + + for (var i=0; i<links.length; i++) + { + doc.writeln(mxUtils.getOuterHtml(links[i])); + } + + var styles = document.getElementsByTagName('style'); + + for (var i=0; i<styles.length; i++) + { + doc.writeln(mxUtils.getOuterHtml(styles[i])); + } + + doc.writeln('</head>'); + doc.writeln('</html>'); + doc.close(); + + // Workaround for FF2 which has no body element in a document where + // the body has been added using document.write. + if (doc.body == null) + { + doc.documentElement.appendChild(doc.createElement('body')); + } + + // Workaround for missing scrollbars in FF + doc.body.style.overflow = 'auto'; + + var node = graph.container.firstChild; + + while (node != null) + { + var clone = node.cloneNode(true); + doc.body.appendChild(clone); + node = node.nextSibling; + } + + // Shifts negative coordinates into visible space + var node = doc.getElementsByTagName('g')[0]; + + if (node != null) + { + node.setAttribute('transform', 'translate(' + dx + ',' + dy + ')'); + + // Updates the size of the SVG container + var root = node.ownerSVGElement; + root.setAttribute('width', bounds.width + Math.max(bounds.x, 0) + 3); + root.setAttribute('height', bounds.height + Math.max(bounds.y, 0) + 3); + } + } + + mxUtils.removeCursors(doc.body); + + return doc; + }, + + /** + * Function: printScreen + * + * Prints the specified graph using a new window and the built-in print + * dialog. + * + * This function should be called from within the document with the graph. + * + * Parameters: + * + * graph - <mxGraph> to be printed. + */ + printScreen: function(graph) + { + var wnd = window.open(); + mxUtils.show(graph, wnd.document); + + var print = function() + { + wnd.focus(); + wnd.print(); + wnd.close(); + }; + + // Workaround for Google Chrome which needs a bit of a + // delay in order to render the SVG contents + if (mxClient.IS_GC) + { + wnd.setTimeout(print, 500); + } + else + { + print(); + } + }, + + /** + * Function: popup + * + * Shows the specified text content in a new <mxWindow> or a new browser + * window if isInternalWindow is false. + * + * Parameters: + * + * content - String that specifies the text to be displayed. + * isInternalWindow - Optional boolean indicating if an mxWindow should be + * used instead of a new browser window. Default is false. + */ + popup: function(content, isInternalWindow) + { + if (isInternalWindow) + { + var div = document.createElement('div'); + + div.style.overflow = 'scroll'; + div.style.width = '636px'; + div.style.height = '460px'; + + var pre = document.createElement('pre'); + pre.innerHTML = mxUtils.htmlEntities(content, false). + replace(/\n/g,'<br>').replace(/ /g, ' '); + + div.appendChild(pre); + + var w = document.body.clientWidth; + var h = (document.body.clientHeight || document.documentElement.clientHeight); + var wnd = new mxWindow('Popup Window', div, + w/2-320, h/2-240, 640, 480, false, true); + + wnd.setClosable(true); + wnd.setVisible(true); + } + else + { + // Wraps up the XML content in a textarea + if (mxClient.IS_NS) + { + var wnd = window.open(); + wnd.document.writeln('<pre>'+mxUtils.htmlEntities(content)+'</pre'); + wnd.document.close(); + } + else + { + var wnd = window.open(); + var pre = wnd.document.createElement('pre'); + pre.innerHTML = mxUtils.htmlEntities(content, false). + replace(/\n/g,'<br>').replace(/ /g, ' '); + wnd.document.body.appendChild(pre); + } + } + }, + + /** + * Function: alert + * + * Displayss the given alert in a new dialog. This implementation uses the + * built-in alert function. This is used to display validation errors when + * connections cannot be changed or created. + * + * Parameters: + * + * message - String specifying the message to be displayed. + */ + alert: function(message) + { + alert(message); + }, + + /** + * Function: prompt + * + * Displays the given message in a prompt dialog. This implementation uses + * the built-in prompt function. + * + * Parameters: + * + * message - String specifying the message to be displayed. + * defaultValue - Optional string specifying the default value. + */ + prompt: function(message, defaultValue) + { + return prompt(message, defaultValue); + }, + + /** + * Function: confirm + * + * Displays the given message in a confirm dialog. This implementation uses + * the built-in confirm function. + * + * Parameters: + * + * message - String specifying the message to be displayed. + */ + confirm: function(message) + { + return confirm(message); + }, + + /** + * Function: error + * + * Displays the given error message in a new <mxWindow> of the given width. + * If close is true then an additional close button is added to the window. + * The optional icon specifies the icon to be used for the window. Default + * is <mxUtils.errorImage>. + * + * Parameters: + * + * message - String specifying the message to be displayed. + * width - Integer specifying the width of the window. + * close - Optional boolean indicating whether to add a close button. + * icon - Optional icon for the window decoration. + */ + error: function(message, width, close, icon) + { + var div = document.createElement('div'); + div.style.padding = '20px'; + + var img = document.createElement('img'); + img.setAttribute('src', icon || mxUtils.errorImage); + img.setAttribute('valign', 'bottom'); + img.style.verticalAlign = 'middle'; + div.appendChild(img); + + div.appendChild(document.createTextNode('\u00a0')); // + div.appendChild(document.createTextNode('\u00a0')); // + div.appendChild(document.createTextNode('\u00a0')); // + mxUtils.write(div, message); + + var w = document.body.clientWidth; + var h = (document.body.clientHeight || document.documentElement.clientHeight); + var warn = new mxWindow(mxResources.get(mxUtils.errorResource) || + mxUtils.errorResource, div, (w-width)/2, h/4, width, null, + false, true); + + if (close) + { + mxUtils.br(div); + + var tmp = document.createElement('p'); + var button = document.createElement('button'); + + if (mxClient.IS_IE) + { + button.style.cssText = 'float:right'; + } + else + { + button.setAttribute('style', 'float:right'); + } + + mxEvent.addListener(button, 'click', function(evt) + { + warn.destroy(); + }); + + mxUtils.write(button, mxResources.get(mxUtils.closeResource) || + mxUtils.closeResource); + + tmp.appendChild(button); + div.appendChild(tmp); + + mxUtils.br(div); + + warn.setClosable(true); + } + + warn.setVisible(true); + + return warn; + }, + + /** + * Function: makeDraggable + * + * Configures the given DOM element to act as a drag source for the + * specified graph. Returns a a new <mxDragSource>. If + * <mxDragSource.guideEnabled> is enabled then the x and y arguments must + * be used in funct to match the preview location. + * + * Example: + * + * (code) + * var funct = function(graph, evt, cell, x, y) + * { + * if (graph.canImportCell(cell)) + * { + * var parent = graph.getDefaultParent(); + * var vertex = null; + * + * graph.getModel().beginUpdate(); + * try + * { + * vertex = graph.insertVertex(parent, null, 'Hello', x, y, 80, 30); + * } + * finally + * { + * graph.getModel().endUpdate(); + * } + * + * graph.setSelectionCell(vertex); + * } + * } + * + * var img = document.createElement('img'); + * img.setAttribute('src', 'editors/images/rectangle.gif'); + * img.style.position = 'absolute'; + * img.style.left = '0px'; + * img.style.top = '0px'; + * img.style.width = '16px'; + * img.style.height = '16px'; + * + * var dragImage = img.cloneNode(true); + * dragImage.style.width = '32px'; + * dragImage.style.height = '32px'; + * mxUtils.makeDraggable(img, graph, funct, dragImage); + * document.body.appendChild(img); + * (end) + * + * Parameters: + * + * element - DOM element to make draggable. + * graphF - <mxGraph> that acts as the drop target or a function that takes a + * mouse event and returns the current <mxGraph>. + * funct - Function to execute on a successful drop. + * dragElement - Optional DOM node to be used for the drag preview. + * dx - Optional horizontal offset between the cursor and the drag + * preview. + * dy - Optional vertical offset between the cursor and the drag + * preview. + * autoscroll - Optional boolean that specifies if autoscroll should be + * used. Default is mxGraph.autoscroll. + * scalePreview - Optional boolean that specifies if the preview element + * should be scaled according to the graph scale. If this is true, then + * the offsets will also be scaled. Default is false. + * highlightDropTargets - Optional boolean that specifies if dropTargets + * should be highlighted. Default is true. + * getDropTarget - Optional function to return the drop target for a given + * location (x, y). Default is mxGraph.getCellAt. + */ + makeDraggable: function(element, graphF, funct, dragElement, dx, dy, autoscroll, + scalePreview, highlightDropTargets, getDropTarget) + { + var dragSource = new mxDragSource(element, funct); + dragSource.dragOffset = new mxPoint((dx != null) ? dx : 0, + (dy != null) ? dy : mxConstants.TOOLTIP_VERTICAL_OFFSET); + dragSource.autoscroll = autoscroll; + + // Cannot enable this by default. This needs to be enabled in the caller + // if the funct argument uses the new x- and y-arguments. + dragSource.setGuidesEnabled(false); + + if (highlightDropTargets != null) + { + dragSource.highlightDropTargets = highlightDropTargets; + } + + // Overrides function to find drop target cell + if (getDropTarget != null) + { + dragSource.getDropTarget = getDropTarget; + } + + // Overrides function to get current graph + dragSource.getGraphForEvent = function(evt) + { + return (typeof(graphF) == 'function') ? graphF(evt) : graphF; + }; + + // Translates switches into dragSource customizations + if (dragElement != null) + { + dragSource.createDragElement = function() + { + return dragElement.cloneNode(true); + }; + + if (scalePreview) + { + dragSource.createPreviewElement = function(graph) + { + var elt = dragElement.cloneNode(true); + + var w = parseInt(elt.style.width); + var h = parseInt(elt.style.height); + elt.style.width = Math.round(w * graph.view.scale) + 'px'; + elt.style.height = Math.round(h * graph.view.scale) + 'px'; + + return elt; + }; + } + } + + return dragSource; + } + +}; |